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
66 import Ganeti.BasicTypes
67 import Ganeti.HTools.Utils
68 import qualified Ganeti.HTools.Version as Version(version)
69 import qualified Ganeti.Constants as C
70 import qualified Ganeti.Ssconf as Ssconf
74 -- | \/dev\/null path.
80 -- | Command line options structure.
81 data DaemonOptions = DaemonOptions
82 { optShowHelp :: Bool -- ^ Just show the help
83 , optShowVer :: Bool -- ^ Just show the program version
84 , optDaemonize :: Bool -- ^ Whether to daemonize or not
85 , optPort :: Maybe Word16 -- ^ Override for the network port
86 , optDebug :: Bool -- ^ Enable debug messages
87 , optNoUserChecks :: Bool -- ^ Ignore user checks
88 , optBindAddress :: Maybe String -- ^ Override for the bind address
89 , optSyslogUsage :: Maybe SyslogUsage -- ^ Override for Syslog usage
92 -- | Default values for the command line options.
93 defaultOptions :: DaemonOptions
94 defaultOptions = DaemonOptions
100 , optNoUserChecks = False
101 , optBindAddress = Nothing
102 , optSyslogUsage = Nothing
105 -- | Abrreviation for the option type.
106 type OptType = OptDescr (DaemonOptions -> Result DaemonOptions)
108 -- | Helper function for required arguments which need to be converted
109 -- as opposed to stored just as string.
110 reqWithConversion :: (String -> Result a)
111 -> (a -> DaemonOptions -> Result DaemonOptions)
113 -> ArgDescr (DaemonOptions -> Result DaemonOptions)
114 reqWithConversion conversion_fn updater_fn metavar =
115 ReqArg (\string_opt opts -> do
116 parsed_value <- conversion_fn string_opt
117 updater_fn parsed_value opts) metavar
119 -- * Command line options
122 oShowHelp = Option "h" ["help"]
123 (NoArg (\ opts -> Ok opts { optShowHelp = True}))
124 "Show the help message and exit"
127 oShowVer = Option "V" ["version"]
128 (NoArg (\ opts -> Ok opts { optShowVer = True}))
129 "Show the version of the program and exit"
131 oNoDaemonize :: OptType
132 oNoDaemonize = Option "f" ["foreground"]
133 (NoArg (\ opts -> Ok opts { optDaemonize = False}))
134 "Don't detach from the current terminal"
137 oDebug = Option "d" ["debug"]
138 (NoArg (\ opts -> Ok opts { optDebug = True }))
139 "Enable debug messages"
141 oNoUserChecks :: OptType
142 oNoUserChecks = Option "" ["no-user-checks"]
143 (NoArg (\ opts -> Ok opts { optNoUserChecks = True }))
146 oPort :: Int -> OptType
147 oPort def = Option "p" ["port"]
148 (reqWithConversion (tryRead "reading port")
149 (\port opts -> Ok opts { optPort = Just port }) "PORT")
150 ("Network port (default: " ++ show def ++ ")")
152 oBindAddress :: OptType
153 oBindAddress = Option "b" ["bind"]
154 (ReqArg (\addr opts -> Ok opts { optBindAddress = Just addr })
156 "Bind address (default depends on cluster configuration)"
158 oSyslogUsage :: OptType
159 oSyslogUsage = Option "" ["syslog"]
160 (reqWithConversion syslogUsageFromRaw
161 (\su opts -> Ok opts { optSyslogUsage = Just su })
163 ("Enable logging to syslog (except debug \
164 \messages); one of 'no', 'yes' or 'only' [" ++ C.syslogUsage ++
168 usageHelp :: String -> [OptType] -> String
170 usageInfo (printf "%s %s\nUsage: %s [OPTION...]"
171 progname Version.version progname)
173 -- | Command line parser, using the 'Options' structure.
174 parseOpts :: [String] -- ^ The command line arguments
175 -> String -- ^ The program name
176 -> [OptType] -- ^ The supported command line options
177 -> IO (DaemonOptions, [String]) -- ^ The resulting options
178 -- and leftover arguments
179 parseOpts argv progname options =
180 case getOpt Permute options argv of
181 (opt_list, args, []) ->
184 exitIfBad "Error while parsing command line arguments" $
185 foldM (flip id) defaultOptions opt_list
186 return (parsed_opts, args)
188 hPutStrLn stderr $ "Command line error: " ++ concat errs
189 hPutStrLn stderr $ usageHelp progname options
190 exitWith $ ExitFailure 2
192 -- | Small wrapper over getArgs and 'parseOpts'.
193 parseArgs :: String -> [OptType] -> IO (DaemonOptions, [String])
194 parseArgs cmd options = do
196 parseOpts cmd_args cmd options
198 -- * Daemon-related functions
200 pidFileMode :: FileMode
201 pidFileMode = unionFileModes ownerReadMode ownerWriteMode
203 -- | Writes a PID file and locks it.
204 _writePidFile :: FilePath -> IO Fd
205 _writePidFile path = do
206 fd <- createFile path pidFileMode
207 setLock fd (WriteLock, AbsoluteSeek, 0, 0)
208 my_pid <- getProcessID
209 _ <- fdWrite fd (show my_pid ++ "\n")
212 -- | Helper to format an IOError.
213 formatIOError :: String -> IOError -> String
214 formatIOError msg err = msg ++ ": " ++ show err
216 -- | Wrapper over '_writePidFile' that transforms IO exceptions into a
218 writePidFile :: FilePath -> IO (Result Fd)
219 writePidFile path = do
220 catch (fmap Ok $ _writePidFile path)
221 (return . Bad . formatIOError "Failure during writing of the pid file")
223 -- | Sets up a daemon's environment.
224 setupDaemonEnv :: FilePath -> FileMode -> IO ()
225 setupDaemonEnv cwd umask = do
226 changeWorkingDirectory cwd
227 _ <- setFileCreationMask umask
231 -- | Sets up a daemon's standard file descriptors.
232 setupDaemonFDs :: Maybe FilePath -> IO ()
233 setupDaemonFDs logfile = do
234 null_in_handle <- openFile devNull ReadMode
235 null_out_handle <- openFile (fromMaybe devNull logfile) AppendMode
236 hDuplicateTo null_in_handle stdin
237 hDuplicateTo null_out_handle stdout
238 hDuplicateTo null_out_handle stderr
239 hClose null_in_handle
240 hClose null_out_handle
242 -- | Computes the default bind address for a given family.
243 defaultBindAddr :: Int -- ^ The port we want
244 -> Socket.Family -- ^ The cluster IP family
245 -> Result (Socket.Family, Socket.SockAddr)
246 defaultBindAddr port Socket.AF_INET =
247 Ok $ (Socket.AF_INET,
248 Socket.SockAddrInet (fromIntegral port) Socket.iNADDR_ANY)
249 defaultBindAddr port Socket.AF_INET6 =
250 Ok $ (Socket.AF_INET6,
251 Socket.SockAddrInet6 (fromIntegral port) 0 Socket.iN6ADDR_ANY 0)
252 defaultBindAddr _ fam = Bad $ "Unsupported address family: " ++ show fam
254 -- | Default hints for the resolver
255 resolveAddrHints :: Maybe Socket.AddrInfo
257 Just Socket.defaultHints { Socket.addrFlags = [Socket.AI_NUMERICHOST,
258 Socket.AI_NUMERICSERV] }
260 -- | Resolves a numeric address.
261 resolveAddr :: Int -> String -> IO (Result (Socket.Family, Socket.SockAddr))
262 resolveAddr port str = do
263 resolved <- Socket.getAddrInfo resolveAddrHints (Just str) (Just (show port))
264 return $ case resolved of
265 [] -> Bad "Invalid results from lookup?"
266 best:_ -> Ok $ (Socket.addrFamily best, Socket.addrAddress best)
268 -- | Based on the options, compute the socket address to use for the
270 parseAddress :: DaemonOptions -- ^ Command line options
271 -> Int -- ^ Default port for this daemon
272 -> IO (Result (Socket.Family, Socket.SockAddr))
273 parseAddress opts defport = do
274 let port = maybe defport fromIntegral $ optPort opts
275 def_family <- Ssconf.getPrimaryIPFamily Nothing
276 ainfo <- case optBindAddress opts of
277 Nothing -> return (def_family >>= defaultBindAddr port)
278 Just saddr -> catch (resolveAddr port saddr)
279 (annotateIOError $ "Invalid address " ++ saddr)
282 -- | Run an I/O action as a daemon.
284 -- WARNING: this only works in single-threaded mode (either using the
285 -- single-threaded runtime, or using the multi-threaded one but with
286 -- only one OS thread, i.e. -N1).
288 -- FIXME: this doesn't support error reporting and the prepfn
290 daemonize :: FilePath -> IO () -> IO ()
291 daemonize logfile action = do
293 _ <- forkProcess $ do
295 setupDaemonEnv "/" (unionFileModes groupModes otherModes)
296 setupDaemonFDs $ Just logfile
297 _ <- forkProcess action
298 exitImmediately ExitSuccess
299 exitImmediately ExitSuccess
301 -- | Generic daemon startup.
302 genericMain :: GanetiDaemon -> [OptType] -> (DaemonOptions -> IO ()) -> IO ()
303 genericMain daemon options main = do
304 let progname = daemonName daemon
305 (opts, args) <- parseArgs progname options
307 when (optShowHelp opts) $ do
308 putStr $ usageHelp progname options
310 when (optShowVer opts) $ do
311 printf "%s %s\ncompiled with %s %s\nrunning on %s %s\n"
312 progname Version.version
313 compilerName (Data.Version.showVersion compilerVersion)
317 exitUnless (null args) "This program doesn't take any arguments"
319 unless (optNoUserChecks opts) $ do
320 runtimeEnts <- getEnts
321 ents <- exitIfBad "Can't find required user/groups" runtimeEnts
322 verifyDaemonUser daemon ents
324 syslog <- case optSyslogUsage opts of
325 Nothing -> exitIfBad "Invalid cluster syslog setting" $
326 syslogUsageFromRaw C.syslogUsage
328 let processFn = if optDaemonize opts
329 then daemonize (daemonLogFile daemon)
331 processFn $ innerMain daemon opts syslog (main opts)
333 -- | Inner daemon function.
335 -- This is executed after daemonization.
336 innerMain :: GanetiDaemon -> DaemonOptions -> SyslogUsage -> IO () -> IO ()
337 innerMain daemon opts syslog main = do
338 let logfile = if optDaemonize opts
340 else Just $ daemonLogFile daemon
341 setupLogging logfile (daemonName daemon) (optDebug opts) True False syslog
342 pid_fd <- writePidFile (daemonPidFile daemon)
343 _ <- exitIfBad "Cannot write PID file; already locked? Error" pid_fd