1 {-| Implementation of the generic daemon functionality.
7 Copyright (C) 2011, 2012 Google Inc.
9 This program is free software; you can redistribute it and/or modify
10 it under the terms of the GNU General Public License as published by
11 the Free Software Foundation; either version 2 of the License, or
12 (at your option) any later version.
14 This program is distributed in the hope that it will be useful, but
15 WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17 General Public License for more details.
19 You should have received a copy of the GNU General Public License
20 along with this program; if not, write to the Free Software
21 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
44 import Control.Exception
46 import Data.Maybe (fromMaybe)
47 import qualified Data.Version
49 import GHC.IO.Handle (hDuplicateTo)
50 import qualified Network.Socket as Socket
51 import Prelude hiding (catch)
52 import System.Console.GetOpt
54 import System.Environment
57 import System.Posix.Directory
58 import System.Posix.Files
59 import System.Posix.IO
60 import System.Posix.Process
61 import System.Posix.Types
62 import System.Posix.Signals
67 import Ganeti.BasicTypes
68 import Ganeti.HTools.Utils
69 import qualified Ganeti.HTools.Version as Version(version)
70 import qualified Ganeti.Constants as C
71 import qualified Ganeti.Ssconf as Ssconf
75 -- | \/dev\/null path.
81 -- | Command line options structure.
82 data DaemonOptions = DaemonOptions
83 { optShowHelp :: Bool -- ^ Just show the help
84 , optShowVer :: Bool -- ^ Just show the program version
85 , optDaemonize :: Bool -- ^ Whether to daemonize or not
86 , optPort :: Maybe Word16 -- ^ Override for the network port
87 , optDebug :: Bool -- ^ Enable debug messages
88 , optNoUserChecks :: Bool -- ^ Ignore user checks
89 , optBindAddress :: Maybe String -- ^ Override for the bind address
90 , optSyslogUsage :: Maybe SyslogUsage -- ^ Override for Syslog usage
93 -- | Default values for the command line options.
94 defaultOptions :: DaemonOptions
95 defaultOptions = DaemonOptions
101 , optNoUserChecks = False
102 , optBindAddress = Nothing
103 , optSyslogUsage = Nothing
106 -- | Abrreviation for the option type.
107 type OptType = OptDescr (DaemonOptions -> Result DaemonOptions)
109 -- | Helper function for required arguments which need to be converted
110 -- as opposed to stored just as string.
111 reqWithConversion :: (String -> Result a)
112 -> (a -> DaemonOptions -> Result DaemonOptions)
114 -> ArgDescr (DaemonOptions -> Result DaemonOptions)
115 reqWithConversion conversion_fn updater_fn metavar =
116 ReqArg (\string_opt opts -> do
117 parsed_value <- conversion_fn string_opt
118 updater_fn parsed_value opts) metavar
120 -- * Command line options
123 oShowHelp = Option "h" ["help"]
124 (NoArg (\ opts -> Ok opts { optShowHelp = True}))
125 "Show the help message and exit"
128 oShowVer = Option "V" ["version"]
129 (NoArg (\ opts -> Ok opts { optShowVer = True}))
130 "Show the version of the program and exit"
132 oNoDaemonize :: OptType
133 oNoDaemonize = Option "f" ["foreground"]
134 (NoArg (\ opts -> Ok opts { optDaemonize = False}))
135 "Don't detach from the current terminal"
138 oDebug = Option "d" ["debug"]
139 (NoArg (\ opts -> Ok opts { optDebug = True }))
140 "Enable debug messages"
142 oNoUserChecks :: OptType
143 oNoUserChecks = Option "" ["no-user-checks"]
144 (NoArg (\ opts -> Ok opts { optNoUserChecks = True }))
147 oPort :: Int -> OptType
148 oPort def = Option "p" ["port"]
149 (reqWithConversion (tryRead "reading port")
150 (\port opts -> Ok opts { optPort = Just port }) "PORT")
151 ("Network port (default: " ++ show def ++ ")")
153 oBindAddress :: OptType
154 oBindAddress = Option "b" ["bind"]
155 (ReqArg (\addr opts -> Ok opts { optBindAddress = Just addr })
157 "Bind address (default depends on cluster configuration)"
159 oSyslogUsage :: OptType
160 oSyslogUsage = Option "" ["syslog"]
161 (reqWithConversion syslogUsageFromRaw
162 (\su opts -> Ok opts { optSyslogUsage = Just su })
164 ("Enable logging to syslog (except debug \
165 \messages); one of 'no', 'yes' or 'only' [" ++ C.syslogUsage ++
169 usageHelp :: String -> [OptType] -> String
171 usageInfo (printf "%s %s\nUsage: %s [OPTION...]"
172 progname Version.version progname)
174 -- | Command line parser, using the 'Options' structure.
175 parseOpts :: [String] -- ^ The command line arguments
176 -> String -- ^ The program name
177 -> [OptType] -- ^ The supported command line options
178 -> IO (DaemonOptions, [String]) -- ^ The resulting options
179 -- and leftover arguments
180 parseOpts argv progname options =
181 case getOpt Permute options argv of
182 (opt_list, args, []) ->
185 exitIfBad "Error while parsing command line arguments" $
186 foldM (flip id) defaultOptions opt_list
187 return (parsed_opts, args)
189 hPutStrLn stderr $ "Command line error: " ++ concat errs
190 hPutStrLn stderr $ usageHelp progname options
191 exitWith $ ExitFailure 2
193 -- | Small wrapper over getArgs and 'parseOpts'.
194 parseArgs :: String -> [OptType] -> IO (DaemonOptions, [String])
195 parseArgs cmd options = do
197 parseOpts cmd_args cmd options
199 -- * Daemon-related functions
201 pidFileMode :: FileMode
202 pidFileMode = unionFileModes ownerReadMode ownerWriteMode
204 -- | Writes a PID file and locks it.
205 _writePidFile :: FilePath -> IO Fd
206 _writePidFile path = do
207 fd <- createFile path pidFileMode
208 setLock fd (WriteLock, AbsoluteSeek, 0, 0)
209 my_pid <- getProcessID
210 _ <- fdWrite fd (show my_pid ++ "\n")
213 -- | Helper to format an IOError.
214 formatIOError :: String -> IOError -> String
215 formatIOError msg err = msg ++ ": " ++ show err
217 -- | Wrapper over '_writePidFile' that transforms IO exceptions into a
219 writePidFile :: FilePath -> IO (Result Fd)
220 writePidFile path = do
221 catch (fmap Ok $ _writePidFile path)
222 (return . Bad . formatIOError "Failure during writing of the pid file")
224 -- | Sets up a daemon's environment.
225 setupDaemonEnv :: FilePath -> FileMode -> IO ()
226 setupDaemonEnv cwd umask = do
227 changeWorkingDirectory cwd
228 _ <- setFileCreationMask umask
232 -- | Signal handler for reopening log files.
233 handleSigHup :: FilePath -> IO ()
234 handleSigHup path = do
235 setupDaemonFDs (Just path)
236 logInfo "Reopening log files after receiving SIGHUP"
238 -- | Sets up a daemon's standard file descriptors.
239 setupDaemonFDs :: Maybe FilePath -> IO ()
240 setupDaemonFDs logfile = do
241 null_in_handle <- openFile devNull ReadMode
242 null_out_handle <- openFile (fromMaybe devNull logfile) AppendMode
243 hDuplicateTo null_in_handle stdin
244 hDuplicateTo null_out_handle stdout
245 hDuplicateTo null_out_handle stderr
246 hClose null_in_handle
247 hClose null_out_handle
249 -- | Computes the default bind address for a given family.
250 defaultBindAddr :: Int -- ^ The port we want
251 -> Socket.Family -- ^ The cluster IP family
252 -> Result (Socket.Family, Socket.SockAddr)
253 defaultBindAddr port Socket.AF_INET =
254 Ok $ (Socket.AF_INET,
255 Socket.SockAddrInet (fromIntegral port) Socket.iNADDR_ANY)
256 defaultBindAddr port Socket.AF_INET6 =
257 Ok $ (Socket.AF_INET6,
258 Socket.SockAddrInet6 (fromIntegral port) 0 Socket.iN6ADDR_ANY 0)
259 defaultBindAddr _ fam = Bad $ "Unsupported address family: " ++ show fam
261 -- | Default hints for the resolver
262 resolveAddrHints :: Maybe Socket.AddrInfo
264 Just Socket.defaultHints { Socket.addrFlags = [Socket.AI_NUMERICHOST,
265 Socket.AI_NUMERICSERV] }
267 -- | Resolves a numeric address.
268 resolveAddr :: Int -> String -> IO (Result (Socket.Family, Socket.SockAddr))
269 resolveAddr port str = do
270 resolved <- Socket.getAddrInfo resolveAddrHints (Just str) (Just (show port))
271 return $ case resolved of
272 [] -> Bad "Invalid results from lookup?"
273 best:_ -> Ok $ (Socket.addrFamily best, Socket.addrAddress best)
275 -- | Based on the options, compute the socket address to use for the
277 parseAddress :: DaemonOptions -- ^ Command line options
278 -> Int -- ^ Default port for this daemon
279 -> IO (Result (Socket.Family, Socket.SockAddr))
280 parseAddress opts defport = do
281 let port = maybe defport fromIntegral $ optPort opts
282 def_family <- Ssconf.getPrimaryIPFamily Nothing
283 ainfo <- case optBindAddress opts of
284 Nothing -> return (def_family >>= defaultBindAddr port)
285 Just saddr -> catch (resolveAddr port saddr)
286 (annotateIOError $ "Invalid address " ++ saddr)
289 -- | Run an I/O action as a daemon.
291 -- WARNING: this only works in single-threaded mode (either using the
292 -- single-threaded runtime, or using the multi-threaded one but with
293 -- only one OS thread, i.e. -N1).
295 -- FIXME: this doesn't support error reporting and the prepfn
297 daemonize :: FilePath -> IO () -> IO ()
298 daemonize logfile action = do
300 _ <- forkProcess $ do
302 setupDaemonEnv "/" (unionFileModes groupModes otherModes)
303 setupDaemonFDs $ Just logfile
304 _ <- installHandler lostConnection (Catch (handleSigHup logfile)) Nothing
305 _ <- forkProcess action
306 exitImmediately ExitSuccess
307 exitImmediately ExitSuccess
309 -- | Generic daemon startup.
310 genericMain :: GanetiDaemon -> [OptType] -> (DaemonOptions -> IO ()) -> IO ()
311 genericMain daemon options main = do
312 let progname = daemonName daemon
313 (opts, args) <- parseArgs progname options
315 when (optShowHelp opts) $ do
316 putStr $ usageHelp progname options
318 when (optShowVer opts) $ do
319 printf "%s %s\ncompiled with %s %s\nrunning on %s %s\n"
320 progname Version.version
321 compilerName (Data.Version.showVersion compilerVersion)
325 exitUnless (null args) "This program doesn't take any arguments"
327 unless (optNoUserChecks opts) $ do
328 runtimeEnts <- getEnts
329 ents <- exitIfBad "Can't find required user/groups" runtimeEnts
330 verifyDaemonUser daemon ents
332 syslog <- case optSyslogUsage opts of
333 Nothing -> exitIfBad "Invalid cluster syslog setting" $
334 syslogUsageFromRaw C.syslogUsage
336 let processFn = if optDaemonize opts
337 then daemonize (daemonLogFile daemon)
339 processFn $ innerMain daemon opts syslog (main opts)
341 -- | Inner daemon function.
343 -- This is executed after daemonization.
344 innerMain :: GanetiDaemon -> DaemonOptions -> SyslogUsage -> IO () -> IO ()
345 innerMain daemon opts syslog main = do
346 let logfile = if optDaemonize opts
348 else Just $ daemonLogFile daemon
349 setupLogging logfile (daemonName daemon) (optDebug opts) True False syslog
350 pid_fd <- writePidFile (daemonPidFile daemon)
351 _ <- exitIfBad "Cannot write PID file; already locked? Error" pid_fd