propellor/src/Propellor/CmdLine.hs

420 lines
14 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
2014-04-01 15:59:48 +00:00
import System.Environment (getArgs)
2014-03-30 23:10:32 +00:00
import Data.List
import System.Exit
2014-04-01 15:59:48 +00:00
import System.Log.Logger
import System.Log.Formatter
import System.Log.Handler (setFormatter, LogHandler)
import System.Log.Handler.Simple
2014-04-03 17:49:26 +00:00
import System.PosixCompat
2014-04-04 20:20:20 +00:00
import Control.Exception (bracket)
import System.Posix.IO
import Data.Time.Clock.POSIX
2014-11-18 19:05:15 +00:00
import Control.Concurrent.Async
2014-11-18 20:10:13 +00:00
import qualified Data.ByteString as B
2014-11-18 19:32:53 +00:00
import System.Process (std_in, std_out)
2014-03-30 23:10:32 +00:00
2014-03-31 03:55:59 +00:00
import Propellor
2014-11-18 17:29:50 +00:00
import Propellor.Protocol
2014-11-11 16:58:53 +00:00
import Propellor.PrivData.Paths
import Propellor.Gpg
import qualified Propellor.Property.Docker as Docker
import qualified Propellor.Property.Docker.Shim as DockerShim
2014-03-30 23:10:32 +00:00
import Utility.FileMode
2014-03-31 03:55:59 +00:00
import Utility.SafeCommand
import Utility.UserInfo
2014-03-30 23:10:32 +00:00
2014-03-31 20:37:19 +00:00
usage :: IO a
usage = do
putStrLn $ unlines
[ "Usage:"
, " propellor"
, " propellor hostname"
, " propellor --spin hostname"
, " propellor --add-key keyid"
2014-07-06 19:56:56 +00:00
, " propellor --set field context"
, " propellor --dump field context"
, " propellor --edit field context"
, " propellor --list-fields"
2014-03-31 20:37:19 +00:00
]
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-11-18 05:25:54 +00:00
go ("--run":h:[]) = return $ Run h
2014-03-31 16:06:04 +00:00
go ("--add-key":k:[]) = return $ AddKey k
2014-07-06 19:56:56 +00:00
go ("--set":f:c:[]) = withprivfield f c Set
go ("--dump":f:c:[]) = withprivfield f c Dump
go ("--edit":f:c:[]) = withprivfield f c Edit
go ("--list-fields":[]) = return ListFields
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 ("--chain":h:[]) = return $ Chain h
go ("--docker":h:[]) = return $ Docker h
2014-11-18 19:05:15 +00:00
go ("--gitpush":fin:fout:_) = return $ GitPush (Prelude.read fin) (Prelude.read fout)
2014-04-01 15:59:48 +00:00
go (h:[])
| "--" `isPrefixOf` h = usage
| otherwise = return $ Run h
2014-03-30 23:10:32 +00:00
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
2014-07-06 19:56:56 +00:00
withprivfield s c f = case readish s of
Just pf -> return $ f pf (Context c)
Nothing -> errorMessage $ "Unknown privdata field " ++ s
2014-04-11 01:09:20 +00:00
defaultMain :: [Host] -> IO ()
defaultMain hostlist = do
DockerShim.cleanEnv
2014-04-01 15:59:48 +00:00
checkDebugMode
cmdline <- processCmdLine
debug ["command line: ", show cmdline]
go True cmdline
2014-03-30 23:10:32 +00:00
where
2014-03-31 20:37:19 +00:00
go _ (Continue cmdline) = go False cmdline
2014-07-06 19:56:56 +00:00
go _ (Set field context) = setPrivData field context
go _ (Dump field context) = dumpPrivData field context
go _ (Edit field context) = editPrivData field context
go _ ListFields = listPrivDataFields hostlist
2014-03-31 20:37:19 +00:00
go _ (AddKey keyid) = addKey keyid
go _ (Chain hn) = withhost hn $ \h -> do
r <- runPropellor h $ ensureProperties $ hostProperties h
putStrLn $ "\n" ++ show r
2014-04-11 01:09:20 +00:00
go _ (Docker hn) = Docker.chain hn
2014-11-18 19:05:15 +00:00
go _ (GitPush fin fout) = gitPush fin fout
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-07-06 21:37:10 +00:00
go False (Spin hn) = withhost hn $ spin hn
2014-04-11 01:09:20 +00:00
go False (Run hn) = ifM ((==) 0 <$> getRealUserID)
( onlyProcess $ withhost hn mainProperties
2014-04-11 01:09:20 +00:00
, go True (Spin hn)
2014-04-03 17:58:21 +00:00
)
2014-11-18 05:10:53 +00:00
go False (Boot _) = onlyProcess boot
2014-03-31 20:37:19 +00:00
withhost :: HostName -> (Host -> IO ()) -> IO ()
2014-07-07 05:57:59 +00:00
withhost hn a = maybe (unknownhost hn hostlist) a (findHost hostlist hn)
2014-03-30 23:10:32 +00:00
2014-04-04 20:20:20 +00:00
onlyProcess :: IO a -> IO a
onlyProcess a = bracket lock unlock (const a)
where
lock = do
2014-04-04 22:34:03 +00:00
l <- createFile lockfile stdFileMode
2014-04-04 20:20:20 +00:00
setLock l (WriteLock, AbsoluteSeek, 0, 0)
2014-04-04 22:34:03 +00:00
`catchIO` const alreadyrunning
2014-04-04 20:20:20 +00:00
return l
unlock = closeFd
alreadyrunning = error "Propellor is already running on this host!"
lockfile = localdir </> ".lock"
2014-07-07 05:57:59 +00:00
unknownhost :: HostName -> [Host] -> IO a
unknownhost h hosts = errorMessage $ unlines
2014-04-08 22:42:54 +00:00
[ "Propellor does not know about host: " ++ h
, "(Perhaps you should specify the real hostname on the command line?)"
, "(Or, edit propellor's config.hs to configure this host)"
2014-07-07 05:57:59 +00:00
, "Known hosts: " ++ unwords (map hostName hosts)
2014-03-31 03:02:10 +00:00
]
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
2014-07-07 06:01:57 +00:00
else void $ boolSystem "./propellor" [Param "--continue", Param (show cmdline)]
2014-03-31 22:53:27 +00:00
, errorMessage "Propellor build failed!"
)
where
getmtime = catchMaybeIO $ getModificationTime "propellor"
getCurrentBranch :: IO String
getCurrentBranch = takeWhile (/= '\n')
<$> readProcess "git" ["symbolic-ref", "--short", "HEAD"]
2014-03-31 21:57:12 +00:00
updateFirst :: CmdLine -> IO () -> IO ()
2014-11-18 19:43:00 +00:00
updateFirst cmdline next = ifM hasOrigin (updateFirst' cmdline next, next)
updateFirst' :: CmdLine -> IO () -> IO ()
updateFirst' cmdline next = do
branchref <- getCurrentBranch
let originbranch = "origin" </> 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
oldsha <- getCurrentGitSha1 branchref
2014-04-10 20:12:06 +00:00
whenM (doesFileExist keyring) $ do
2014-04-03 17:49:26 +00:00
{- To verify origin branch commit's signature, have to
2014-03-31 20:20:38 +00:00
- 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)])
2014-04-03 16:20:42 +00:00
nukeFile $ privDataDir </> "trustdb.gpg"
nukeFile $ privDataDir </> "pubring.gpg"
2014-03-31 20:20:38 +00:00
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
void $ boolSystem "git" [Param "merge", Param originbranch]
else warningMessage $ "git branch " ++ originbranch ++ " is not signed with a trusted gpg key; refusing to deploy it! (Running with previous configuration instead.)"
2014-03-31 20:20:38 +00:00
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-11-18 17:29:50 +00:00
-- spin handles deploying propellor to a remote host, if it's not already
-- installed there, or updating it if it is. Once the remote propellor is
-- updated, it's run.
2014-07-06 21:37:10 +00:00
spin :: HostName -> Host -> IO ()
spin hn hst = do
2014-03-31 16:06:04 +00:00
void $ gitCommit [Param "--allow-empty", Param "-a", Param "-m", Param "propellor spin"]
2014-11-18 21:10:10 +00:00
-- Push to central origin repo first, if possible.
-- The remote propellor will pull from there, which avoids
-- us needing to send stuff directly to the remote host.
whenM hasOrigin $
void $ boolSystem "git" [Param "push"]
2014-04-11 01:09:20 +00:00
cacheparams <- toCommand <$> sshCachingParams hn
2014-11-18 21:05:25 +00:00
comm cacheparams =<< hostprivdata
2014-11-18 05:25:54 +00:00
unlessM (boolSystem "ssh" (map Param (cacheparams ++ ["-t", user, runcmd]))) $
error $ "remote propellor failed (running: " ++ runcmd ++")"
2014-03-31 19:40:16 +00:00
where
2014-07-06 21:37:10 +00:00
hostprivdata = show . filterPrivData hst <$> decryptPrivData
2014-11-18 21:05:25 +00:00
comm cacheparams privdata =
withBothHandles createProcessSuccess
(proc "ssh" $ cacheparams ++ [user, bootstrapcmd])
(comm' cacheparams privdata)
comm' cacheparams privdata (toh, fromh) = loop
where
loop = dispatch =<< (maybe Nothing readish <$> getMarked fromh statusMarker)
dispatch (Just NeedRepoUrl) = do
sendMarked toh repoUrlMarker
=<< (fromMaybe "" <$> getRepoUrl)
loop
dispatch (Just NeedPrivData) = do
sendprivdata toh privdata
loop
dispatch (Just NeedGitPush) = do
void $ actionMessage ("Sending git update to " ++ hn) $ do
sendMarked toh gitPushMarker ""
let p = (proc "git" ["upload-pack", "."])
{ std_in = UseHandle fromh
, std_out = UseHandle toh
}
(Nothing, Nothing, Nothing, h) <- createProcess p
r <- waitForProcess h
-- no more protocol possible after git push
hClose fromh
hClose toh
return (r == ExitSuccess)
dispatch (Just NeedGitClone) = do
hClose toh
hClose fromh
sendGitClone hn
comm cacheparams privdata
-- Ready is only sent by old versions of
-- propellor. They expect to get privdata,
-- and then no more protocol communication.
dispatch (Just Ready) = do
sendprivdata toh privdata
hClose toh
-- Display remaining output.
void $ tryIO $ forever $
showremote =<< hGetLine fromh
hClose fromh
dispatch Nothing = return ()
2014-03-31 19:40:16 +00:00
2014-04-11 01:09:20 +00:00
user = "root@"++hn
2014-03-31 19:40:16 +00:00
mkcmd = shellWrap . intercalate " ; "
bootstrapcmd = mkcmd
2014-03-31 20:45:32 +00:00
[ "if [ ! -d " ++ localdir ++ " ]"
, "then " ++ intercalate " && "
[ "apt-get update"
, "apt-get --no-install-recommends --no-upgrade -y install git make"
2014-03-31 20:45:32 +00:00
, "echo " ++ toMarked statusMarker (show NeedGitClone)
]
, "else " ++ intercalate " && "
[ "cd " ++ localdir
2014-04-10 04:40:38 +00:00
, "if ! test -x ./propellor; then make deps build; fi"
, "./propellor --boot " ++ hn
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-11-18 05:25:54 +00:00
runcmd = mkcmd
[ "cd " ++ localdir ++ " && ./propellor --run " ++ hn ]
2014-03-31 18:44:38 +00:00
showremote s = putStrLn s
2014-11-18 17:29:50 +00:00
2014-11-18 18:09:18 +00:00
sendprivdata toh privdata = void $
actionMessage ("Sending privdata (" ++ show (length privdata) ++ " bytes) to " ++ hn) $ do
sendMarked toh privDataMarker privdata
2014-03-31 22:31:08 +00:00
return True
2014-03-30 23:10:32 +00:00
2014-09-25 19:11:19 +00:00
-- Initial git clone, used for bootstrapping.
2014-11-18 17:59:50 +00:00
sendGitClone :: HostName -> IO ()
sendGitClone hn = void $ actionMessage ("Pushing git repository to " ++ hn) $ do
2014-04-03 17:49:26 +00:00
branch <- getCurrentBranch
2014-04-11 01:09:20 +00:00
cacheparams <- sshCachingParams hn
2014-04-03 16:26:27 +00:00
withTmpFile "propellor.git" $ \tmp _ -> allM id
2014-03-31 23:01:56 +00:00
[ boolSystem "git" [Param "bundle", Param "create", File tmp, Param "HEAD"]
2014-04-11 01:09:20 +00:00
, boolSystem "scp" $ cacheparams ++ [File tmp, Param ("root@"++hn++":"++remotebundle)]
, boolSystem "ssh" $ cacheparams ++ [Param ("root@"++hn), Param $ unpackcmd branch]
2014-03-31 22:31:08 +00:00
]
2014-03-31 19:40:16 +00:00
where
remotebundle = "/usr/local/propellor.git"
2014-04-03 17:49:26 +00:00
unpackcmd branch = shellWrap $ intercalate " && "
2014-03-31 19:40:16 +00:00
[ "git clone " ++ remotebundle ++ " " ++ localdir
, "cd " ++ localdir
2014-04-03 17:49:26 +00:00
, "git checkout -b " ++ branch
2014-03-31 19:40:16 +00:00
, "git remote rm origin"
, "rm -f " ++ remotebundle
]
2014-11-18 17:59:50 +00:00
-- Called "boot" for historical reasons, but what this really does is
-- update the privdata, repo url, and git repo over the ssh connection from the
-- client that ran propellor --spin.
2014-11-18 05:10:53 +00:00
boot :: IO ()
boot = do
2014-11-18 18:09:18 +00:00
req NeedRepoUrl repoUrlMarker setRepoUrl
2014-03-30 23:19:29 +00:00
makePrivDataDir
2014-11-18 18:09:18 +00:00
req NeedPrivData privDataMarker $
writeFileProtected privDataLocal
2014-11-18 19:05:15 +00:00
req NeedGitPush gitPushMarker $ \_ -> do
hin <- dup stdInput
hout <- dup stdOutput
2014-11-18 19:18:26 +00:00
hClose stdin
2014-11-18 19:05:15 +00:00
hClose stdout
2014-11-18 21:06:39 +00:00
unlessM (boolSystem "git" [Param "pull", Param "--progress", Param "--upload-pack", Param $ "./propellor --gitpush " ++ show hin ++ " " ++ show hout, Param "."]) $
2014-11-18 20:28:27 +00:00
errorMessage "git pull from client failed"
2014-11-18 19:05:15 +00:00
-- Shim for git push over the propellor ssh channel.
2014-11-18 19:53:15 +00:00
-- Reads from stdin and sends it to hout;
-- reads from hin and sends it to stdout.
2014-11-18 19:05:15 +00:00
gitPush :: Fd -> Fd -> IO ()
2014-11-18 19:37:22 +00:00
gitPush hin hout = void $ fromstdin `concurrently` tostdout
2014-11-18 19:05:15 +00:00
where
fromstdin = do
2014-11-18 19:53:15 +00:00
h <- fdToHandle hout
2014-11-18 20:10:13 +00:00
connect stdin h
2014-11-18 19:05:15 +00:00
tostdout = do
2014-11-18 19:53:15 +00:00
h <- fdToHandle hin
2014-11-18 20:10:13 +00:00
connect h stdout
connect fromh toh = do
2014-11-18 20:45:31 +00:00
hSetBinaryMode fromh True
hSetBinaryMode toh True
2014-11-18 20:10:13 +00:00
b <- B.hGetSome fromh 40960
2014-11-18 20:36:34 +00:00
if B.null b
then do
hClose fromh
hClose toh
else do
B.hPut toh b
hFlush toh
connect fromh toh
2014-03-30 23:10:32 +00:00
2014-11-18 19:43:00 +00:00
hasOrigin :: IO Bool
hasOrigin = do
rs <- lines <$> readProcess "git" ["remote"]
return $ "origin" `elem` rs
2014-11-18 17:59:50 +00:00
setRepoUrl :: String -> IO ()
setRepoUrl "" = return ()
setRepoUrl url = do
2014-11-18 19:43:00 +00:00
subcmd <- ifM hasOrigin (pure "set-url", pure "add")
2014-11-18 17:59:50 +00:00
void $ boolSystem "git" [Param "remote", Param subcmd, Param "origin", Param url]
-- same as --set-upstream-to, except origin branch
-- may not have been pulled yet
branch <- getCurrentBranch
let branchval s = "branch." ++ branch ++ "." ++ s
void $ boolSystem "git" [Param "config", Param (branchval "remote"), Param "origin"]
void $ boolSystem "git" [Param "config", Param (branchval "merge"), Param $ "refs/heads/"++branch]
getRepoUrl :: IO (Maybe String)
getRepoUrl = getM get urls
2014-03-30 23:10:32 +00:00
where
urls = ["remote.deploy.url", "remote.origin.url"]
get u = do
v <- catchMaybeIO $
takeWhile (/= '\n')
<$> readProcess "git" ["config", u]
return $ case v of
Just url | not (null url) -> Just url
_ -> Nothing
2014-04-01 15:59:48 +00:00
checkDebugMode :: IO ()
checkDebugMode = go =<< getEnv "PROPELLOR_DEBUG"
where
2014-11-18 19:38:18 +00:00
go (Just "1") = do
2014-05-11 13:19:21 +00:00
f <- setFormatter
<$> streamHandler stderr DEBUG
<*> pure (simpleLogFormatter "[$time] $msg")
updateGlobalLogger rootLoggerName $
setLevel DEBUG . setHandlers [f]
2014-11-18 19:38:18 +00:00
go _ = noop
-- Parameters can be passed to both ssh and scp, to enable a ssh connection
-- caching socket.
--
-- If the socket already exists, check if its mtime is older than 10
-- minutes, and if so stop that ssh process, in order to not try to
-- use an old stale connection. (atime would be nicer, but there's
-- a good chance a laptop uses noatime)
sshCachingParams :: HostName -> IO [CommandParam]
2014-04-11 01:09:20 +00:00
sshCachingParams hn = do
home <- myHomeDir
let cachedir = home </> ".ssh" </> "propellor"
createDirectoryIfMissing False cachedir
2014-04-11 01:09:20 +00:00
let socketfile = cachedir </> hn ++ ".sock"
let ps =
[ Param "-o", Param ("ControlPath=" ++ socketfile)
, Params "-o ControlMaster=auto -o ControlPersist=yes"
]
maybe noop (expireold ps socketfile)
=<< catchMaybeIO (getFileStatus socketfile)
return ps
where
expireold ps f s = do
now <- truncate <$> getPOSIXTime :: IO Integer
if modificationTime s > fromIntegral now - tenminutes
then touchFile f
else do
void $ boolSystem "ssh" $
[ Params "-O stop" ] ++ ps ++
[ Param "localhost" ]
nukeFile f
tenminutes = 600