propellor/Propellor/CmdLine.hs

304 lines
8.9 KiB
Haskell
Raw Normal View History

2014-03-31 03:37:54 +00:00
module Propellor.CmdLine where
2014-03-30 23:10:32 +00:00
import System.Environment
import Data.List
import System.Exit
2014-03-31 03:55:59 +00:00
import Propellor
import Propellor.SimpleSh
2014-03-30 23:10:32 +00:00
import Utility.FileMode
2014-03-31 03:55:59 +00:00
import Utility.SafeCommand
2014-03-30 23:10:32 +00:00
data CmdLine
= Run HostName
| Spin HostName
| Boot HostName
2014-03-31 01:01:18 +00:00
| Set HostName PrivDataField
2014-03-31 16:06:04 +00:00
| AddKey String
2014-03-31 20:37:19 +00:00
| Continue CmdLine
| SimpleSh FilePath
| Chain HostName
2014-03-31 20:37:19 +00:00
deriving (Read, Show, Eq)
usage :: IO a
usage = do
putStrLn $ unlines
[ "Usage:"
, " propellor"
, " propellor hostname"
, " propellor --spin hostname"
, " propellor --set hostname field"
, " propellor --add-key keyid"
]
exitFailure
2014-03-30 23:10:32 +00:00
processCmdLine :: IO CmdLine
processCmdLine = go =<< getArgs
where
go ("--help":_) = usage
go ("--spin":h:[]) = return $ Spin h
go ("--boot":h:[]) = return $ Boot h
2014-03-31 16:06:04 +00:00
go ("--add-key":k:[]) = return $ AddKey k
2014-03-31 01:01:18 +00:00
go ("--set":h:f:[]) = case readish f of
Just pf -> return $ Set h pf
2014-03-31 22:31:08 +00:00
Nothing -> errorMessage $ "Unknown privdata field " ++ f
go ("--continue":s:[]) = case readish s of
2014-03-31 20:37:19 +00:00
Just cmdline -> return $ Continue cmdline
2014-03-31 22:31:08 +00:00
Nothing -> errorMessage "--continue serialization failure"
go ("--simplesh":f:[]) = return $ SimpleSh f
go ("--chain":h:[]) = return $ Chain h
2014-03-30 23:10:32 +00:00
go (h:[]) = return $ Run h
go [] = do
s <- takeWhile (/= '\n') <$> readProcess "hostname" ["-f"]
if null s
2014-03-31 22:31:08 +00:00
then errorMessage "Cannot determine hostname! Pass it on the command line."
2014-03-30 23:10:32 +00:00
else return $ Run s
go _ = usage
defaultMain :: [HostName -> Maybe [Property]] -> IO ()
2014-03-31 20:37:19 +00:00
defaultMain getprops = go True =<< processCmdLine
2014-03-30 23:10:32 +00:00
where
2014-03-31 20:37:19 +00:00
go _ (Continue cmdline) = go False cmdline
go _ (Set host field) = setPrivData host field
go _ (AddKey keyid) = addKey keyid
go _ (SimpleSh f) = simpleSh f
go _ (Chain host) = withprops host $ print <=< ensureProperties'
2014-03-31 22:53:27 +00:00
go True cmdline@(Spin _) = buildFirst cmdline $ go False cmdline
2014-03-31 21:57:12 +00:00
go True cmdline = updateFirst cmdline $ go False cmdline
2014-03-31 22:53:27 +00:00
go False (Spin host) = withprops host $ const $ spin host
go False (Run host) = withprops host $ ensureProperties
go False (Boot host) = withprops host $ boot
2014-03-31 20:37:19 +00:00
withprops host a = maybe (unknownhost host) a $
headMaybe $ catMaybes $ map (\get -> get host) getprops
2014-03-30 23:10:32 +00:00
2014-03-31 03:02:10 +00:00
unknownhost :: HostName -> IO a
2014-03-31 22:31:08 +00:00
unknownhost h = errorMessage $ unwords
2014-03-31 03:02:10 +00:00
[ "Unknown host:", h
, "(perhaps you should specify the real hostname on the command line?)"
]
2014-03-31 22:53:27 +00:00
buildFirst :: CmdLine -> IO () -> IO ()
buildFirst cmdline next = do
oldtime <- getmtime
2014-03-31 23:31:35 +00:00
ifM (actionMessage "Propellor build" $ boolSystem "make" [Param "build"])
2014-03-31 22:53:27 +00:00
( do
newtime <- getmtime
if newtime == oldtime
then next
else void $ boolSystem "./propellor" [Param "--continue", Param (show cmdline)]
, errorMessage "Propellor build failed!"
)
where
getmtime = catchMaybeIO $ getModificationTime "propellor"
2014-03-31 21:57:12 +00:00
updateFirst :: CmdLine -> IO () -> IO ()
updateFirst cmdline next = do
2014-03-31 20:20:38 +00:00
branchref <- takeWhile (/= '\n')
<$> readProcess "git" ["symbolic-ref", "HEAD"]
let originbranch = "origin" </> takeFileName branchref
2014-03-31 22:39:39 +00:00
2014-03-31 23:31:35 +00:00
void $ actionMessage "Git fetch" $ boolSystem "git" [Param "fetch"]
2014-03-31 20:20:38 +00:00
whenM (doesFileExist keyring) $ do
{- To verify origin/master commit's signature, have to
- convince gpg to use our keyring. While running git log.
- Which has no way to pass options to gpg.
- Argh! -}
let gpgconf = privDataDir </> "gpg.conf"
writeFile gpgconf $ unlines
[ " keyring " ++ keyring
, "no-auto-check-trustdb"
]
-- gpg is picky about perms
modifyFileMode privDataDir (removeModes otherGroupModes)
s <- readProcessEnv "git" ["log", "-n", "1", "--format=%G?", originbranch]
(Just [("GNUPGHOME", privDataDir)])
nukeFile $ privDataDir </> "trustring.gpg"
nukeFile $ privDataDir </> "gpg.conf"
2014-03-31 20:37:19 +00:00
if s == "U\n" || s == "G\n"
2014-03-31 20:42:25 +00:00
then do
putStrLn $ "git branch " ++ originbranch ++ " gpg signature verified; merging"
hFlush stdout
2014-03-31 22:31:08 +00:00
else errorMessage $ "git branch " ++ originbranch ++ " is not signed with a trusted gpg key; refusing to deploy it!"
2014-03-31 20:20:38 +00:00
2014-03-31 20:40:03 +00:00
oldsha <- getCurrentGitSha1 branchref
2014-03-31 20:20:38 +00:00
void $ boolSystem "git" [Param "merge", Param originbranch]
2014-03-31 20:40:03 +00:00
newsha <- getCurrentGitSha1 branchref
2014-03-31 20:37:19 +00:00
if oldsha == newsha
2014-03-31 20:50:30 +00:00
then next
2014-03-31 23:31:35 +00:00
else ifM (actionMessage "Propellor build" $ boolSystem "make" [Param "build"])
2014-03-31 22:31:08 +00:00
( void $ boolSystem "./propellor" [Param "--continue", Param (show cmdline)]
, errorMessage "Propellor build failed!"
)
2014-03-31 20:20:38 +00:00
2014-03-31 20:40:03 +00:00
getCurrentGitSha1 :: String -> IO String
getCurrentGitSha1 branchref = readProcess "git" ["show-ref", "--hash", branchref]
2014-03-31 20:20:38 +00:00
2014-03-30 23:10:32 +00:00
spin :: HostName -> IO ()
spin host = do
url <- getUrl
2014-03-31 16:06:04 +00:00
void $ gitCommit [Param "--allow-empty", Param "-a", Param "-m", Param "propellor spin"]
2014-03-30 23:19:29 +00:00
void $ boolSystem "git" [Param "push"]
2014-03-31 19:40:16 +00:00
go url =<< gpgDecrypt (privDataFile host)
where
go url privdata = withBothHandles createProcessSuccess (proc "ssh" [user, bootstrapcmd]) $ \(toh, fromh) -> do
let finish = do
senddata toh (privDataFile host) privDataMarker privdata
hClose toh
-- Display remaining output.
void $ tryIO $ forever $
showremote =<< hGetLine fromh
hClose fromh
2014-03-31 22:39:39 +00:00
status <- getstatus fromh `catchIO` (const $ errorMessage "protocol error (perhaps the remote propellor failed to run?)")
2014-03-31 16:06:04 +00:00
case status of
Ready -> finish
2014-03-31 19:40:16 +00:00
NeedGitClone -> do
hClose toh
hClose fromh
sendGitClone host url
go url privdata
2014-03-31 16:06:04 +00:00
user = "root@"++host
2014-03-31 19:40:16 +00:00
2014-03-31 20:52:20 +00:00
bootstrapcmd = shellWrap $ intercalate " ; "
2014-03-31 20:45:32 +00:00
[ "if [ ! -d " ++ localdir ++ " ]"
, "then " ++ intercalate " && "
[ "apt-get -y install git"
, "echo " ++ toMarked statusMarker (show NeedGitClone)
]
, "else " ++ intercalate " && "
[ "cd " ++ localdir
2014-03-31 22:36:53 +00:00
, "if ! test -x ./propellor; then make build; fi"
2014-03-31 20:45:32 +00:00
, "./propellor --boot " ++ host
2014-03-30 23:10:32 +00:00
]
2014-03-31 20:52:58 +00:00
, "fi"
2014-03-30 23:10:32 +00:00
]
2014-03-31 19:40:16 +00:00
2014-03-31 16:28:40 +00:00
getstatus :: Handle -> IO BootStrapStatus
2014-03-31 18:22:48 +00:00
getstatus h = do
l <- hGetLine h
case readish =<< fromMarked statusMarker l of
Nothing -> do
2014-03-31 18:24:15 +00:00
showremote l
2014-03-31 18:22:48 +00:00
getstatus h
Just status -> return status
2014-03-31 19:40:16 +00:00
2014-03-31 18:44:38 +00:00
showremote s = putStrLn s
2014-03-31 22:31:08 +00:00
senddata toh f marker s = void $
actionMessage ("Sending " ++ f ++ " (" ++ show (length s) ++ " bytes) to " ++ host) $ do
2014-03-31 22:44:10 +00:00
sendMarked toh marker s
2014-03-31 22:31:08 +00:00
return True
2014-03-30 23:10:32 +00:00
2014-03-31 19:40:16 +00:00
sendGitClone :: HostName -> String -> IO ()
2014-03-31 22:31:08 +00:00
sendGitClone host url = void $ actionMessage ("Pushing git repository to " ++ host) $
2014-03-31 23:06:50 +00:00
withTmpFile "propellor.git." $ \tmp _ -> allM id
2014-03-31 19:40:16 +00:00
-- TODO: ssh connection caching, or better push method
-- with less connections.
2014-03-31 23:01:56 +00:00
[ boolSystem "git" [Param "bundle", Param "create", File tmp, Param "HEAD"]
2014-03-31 22:31:08 +00:00
, boolSystem "scp" [File tmp, Param ("root@"++host++":"++remotebundle)]
, boolSystem "ssh" [Param ("root@"++host), Param unpackcmd]
]
2014-03-31 19:40:16 +00:00
where
remotebundle = "/usr/local/propellor.git"
unpackcmd = shellWrap $ intercalate " && "
[ "git clone " ++ remotebundle ++ " " ++ localdir
, "cd " ++ localdir
, "git checkout -b master"
, "git remote rm origin"
, "git remote add origin " ++ url
, "rm -f " ++ remotebundle
]
data BootStrapStatus = Ready | NeedGitClone
2014-03-31 16:06:04 +00:00
deriving (Read, Show, Eq)
type Marker = String
type Marked = String
statusMarker :: Marker
statusMarker = "STATUS"
privDataMarker :: String
privDataMarker = "PRIVDATA "
toMarked :: Marker -> String -> String
2014-03-31 19:43:24 +00:00
toMarked marker = intercalate "\n" . map (marker ++) . lines
2014-03-31 16:06:04 +00:00
2014-03-31 22:44:10 +00:00
sendMarked :: Handle -> Marker -> String -> IO ()
sendMarked h marker s = do
-- Prefix string with newline because sometimes a
-- incomplete line is output.
hPutStrLn h ("\n" ++ toMarked marker s)
hFlush h
2014-03-31 18:21:14 +00:00
fromMarked :: Marker -> Marked -> Maybe String
fromMarked marker s
| null matches = Nothing
2014-03-31 19:43:24 +00:00
| otherwise = Just $ intercalate "\n" $
map (drop len) matches
2014-03-31 16:06:04 +00:00
where
len = length marker
2014-03-31 18:21:14 +00:00
matches = filter (marker `isPrefixOf`) $ lines s
2014-03-31 16:06:04 +00:00
2014-03-30 23:10:32 +00:00
boot :: [Property] -> IO ()
boot props = do
2014-03-31 22:44:10 +00:00
sendMarked stdout statusMarker $ show Ready
2014-03-31 18:26:56 +00:00
reply <- hGetContentsStrict stdin
2014-03-31 18:15:12 +00:00
2014-03-30 23:19:29 +00:00
makePrivDataDir
2014-03-31 18:21:14 +00:00
maybe noop (writeFileProtected privDataLocal) $
fromMarked privDataMarker reply
2014-03-30 23:10:32 +00:00
ensureProperties props
2014-03-31 16:06:04 +00:00
addKey :: String -> IO ()
addKey keyid = exitBool =<< allM id [ gpg, gitadd, gitcommit ]
where
gpg = boolSystem "sh"
[ Param "-c"
, Param $ "gpg --export " ++ keyid ++ " | gpg " ++
unwords (gpgopts ++ ["--import"])
]
gitadd = boolSystem "git"
[ Param "add"
, File keyring
]
gitcommit = gitCommit
[ File keyring
, Param "-m"
, Param "propellor addkey"
]
{- Automatically sign the commit if there'a a keyring. -}
gitCommit :: [CommandParam] -> IO Bool
gitCommit ps = do
k <- doesFileExist keyring
boolSystem "git" $ catMaybes $
[ Just (Param "commit")
, if k then Just (Param "--gpg-sign") else Nothing
] ++ map Just ps
keyring :: FilePath
keyring = privDataDir </> "keyring.gpg"
gpgopts :: [String]
gpgopts = ["--options", "/dev/null", "--no-default-keyring", "--keyring", keyring]
2014-03-30 23:10:32 +00:00
localdir :: FilePath
localdir = "/usr/local/propellor"
getUrl :: IO String
2014-03-31 22:31:08 +00:00
getUrl = maybe nourl return =<< getM get urls
2014-03-30 23:10:32 +00:00
where
urls = ["remote.deploy.url", "remote.origin.url"]
2014-03-31 22:31:08 +00:00
nourl = errorMessage $ "Cannot find deploy url in " ++ show urls
2014-03-30 23:10:32 +00:00
get u = do
v <- catchMaybeIO $
takeWhile (/= '\n')
<$> readProcess "git" ["config", u]
return $ case v of
Just url | not (null url) -> Just url
_ -> Nothing