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
45 import Control.Exception
47 import Data.Maybe (fromMaybe)
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
56 import System.IO.Error (isDoesNotExistError)
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
64 import Ganeti.Common as Common
67 import Ganeti.BasicTypes
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 , optShowComp :: Bool -- ^ Just show the completion info
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
102 , optNoUserChecks = False
103 , optBindAddress = Nothing
104 , optSyslogUsage = Nothing
107 instance StandardOptions DaemonOptions where
108 helpRequested = optShowHelp
109 verRequested = optShowVer
110 compRequested = optShowComp
111 requestHelp o = o { optShowHelp = True }
112 requestVer o = o { optShowVer = True }
113 requestComp o = o { optShowComp = True }
115 -- | Abrreviation for the option type.
116 type OptType = GenericOptType DaemonOptions
118 -- * Command line options
120 oNoDaemonize :: OptType
122 (Option "f" ["foreground"]
123 (NoArg (\ opts -> Ok opts { optDaemonize = False}))
124 "Don't detach from the current terminal",
129 (Option "d" ["debug"]
130 (NoArg (\ opts -> Ok opts { optDebug = True }))
131 "Enable debug messages",
134 oNoUserChecks :: OptType
136 (Option "" ["no-user-checks"]
137 (NoArg (\ opts -> Ok opts { optNoUserChecks = True }))
138 "Ignore user checks",
141 oPort :: Int -> OptType
144 (reqWithConversion (tryRead "reading port")
145 (\port opts -> Ok opts { optPort = Just port }) "PORT")
146 ("Network port (default: " ++ show def ++ ")"),
149 oBindAddress :: OptType
152 (ReqArg (\addr opts -> Ok opts { optBindAddress = Just addr })
154 "Bind address (default depends on cluster configuration)",
157 oSyslogUsage :: OptType
159 (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 ++
166 OptComplChoices ["yes", "no", "only"])
168 -- | Generic options.
169 genericOpts :: [OptType]
170 genericOpts = [ oShowHelp
175 -- | Small wrapper over getArgs and 'parseOpts'.
176 parseArgs :: String -> [OptType] -> IO (DaemonOptions, [String])
177 parseArgs cmd options = do
179 parseOpts defaultOptions cmd_args cmd (options ++ genericOpts) []
181 -- * Daemon-related functions
183 pidFileMode :: FileMode
184 pidFileMode = unionFileModes ownerReadMode ownerWriteMode
186 -- | Writes a PID file and locks it.
187 _writePidFile :: FilePath -> IO Fd
188 _writePidFile path = do
189 fd <- createFile path pidFileMode
190 setLock fd (WriteLock, AbsoluteSeek, 0, 0)
191 my_pid <- getProcessID
192 _ <- fdWrite fd (show my_pid ++ "\n")
195 -- | Helper to format an IOError.
196 formatIOError :: String -> IOError -> String
197 formatIOError msg err = msg ++ ": " ++ show err
199 -- | Wrapper over '_writePidFile' that transforms IO exceptions into a
201 writePidFile :: FilePath -> IO (Result Fd)
203 catch (fmap Ok $ _writePidFile path)
204 (return . Bad . formatIOError "Failure during writing of the pid file")
206 -- | Helper function to ensure a socket doesn't exist. Should only be
207 -- called once we have locked the pid file successfully.
208 cleanupSocket :: FilePath -> IO ()
209 cleanupSocket socketPath =
210 catchJust (guard . isDoesNotExistError) (removeLink socketPath)
213 -- | Sets up a daemon's environment.
214 setupDaemonEnv :: FilePath -> FileMode -> IO ()
215 setupDaemonEnv cwd umask = do
216 changeWorkingDirectory cwd
217 _ <- setFileCreationMask umask
221 -- | Signal handler for reopening log files.
222 handleSigHup :: FilePath -> IO ()
223 handleSigHup path = do
224 setupDaemonFDs (Just path)
225 logInfo "Reopening log files after receiving SIGHUP"
227 -- | Sets up a daemon's standard file descriptors.
228 setupDaemonFDs :: Maybe FilePath -> IO ()
229 setupDaemonFDs logfile = do
230 null_in_handle <- openFile devNull ReadMode
231 null_out_handle <- openFile (fromMaybe devNull logfile) AppendMode
232 hDuplicateTo null_in_handle stdin
233 hDuplicateTo null_out_handle stdout
234 hDuplicateTo null_out_handle stderr
235 hClose null_in_handle
236 hClose null_out_handle
238 -- | Computes the default bind address for a given family.
239 defaultBindAddr :: Int -- ^ The port we want
240 -> Socket.Family -- ^ The cluster IP family
241 -> Result (Socket.Family, Socket.SockAddr)
242 defaultBindAddr port Socket.AF_INET =
244 Socket.SockAddrInet (fromIntegral port) Socket.iNADDR_ANY)
245 defaultBindAddr port Socket.AF_INET6 =
247 Socket.SockAddrInet6 (fromIntegral port) 0 Socket.iN6ADDR_ANY 0)
248 defaultBindAddr _ fam = Bad $ "Unsupported address family: " ++ show fam
250 -- | Default hints for the resolver
251 resolveAddrHints :: Maybe Socket.AddrInfo
253 Just Socket.defaultHints { Socket.addrFlags = [Socket.AI_NUMERICHOST,
254 Socket.AI_NUMERICSERV] }
256 -- | Resolves a numeric address.
257 resolveAddr :: Int -> String -> IO (Result (Socket.Family, Socket.SockAddr))
258 resolveAddr port str = do
259 resolved <- Socket.getAddrInfo resolveAddrHints (Just str) (Just (show port))
260 return $ case resolved of
261 [] -> Bad "Invalid results from lookup?"
262 best:_ -> Ok (Socket.addrFamily best, Socket.addrAddress best)
264 -- | Based on the options, compute the socket address to use for the
266 parseAddress :: DaemonOptions -- ^ Command line options
267 -> Int -- ^ Default port for this daemon
268 -> IO (Result (Socket.Family, Socket.SockAddr))
269 parseAddress opts defport = do
270 let port = maybe defport fromIntegral $ optPort opts
271 def_family <- Ssconf.getPrimaryIPFamily Nothing
272 case optBindAddress opts of
273 Nothing -> return (def_family >>= defaultBindAddr port)
274 Just saddr -> catch (resolveAddr port saddr)
275 (annotateIOError $ "Invalid address " ++ saddr)
277 -- | Run an I/O action as a daemon.
279 -- WARNING: this only works in single-threaded mode (either using the
280 -- single-threaded runtime, or using the multi-threaded one but with
281 -- only one OS thread, i.e. -N1).
283 -- FIXME: this doesn't support error reporting and the prepfn
285 daemonize :: FilePath -> IO () -> IO ()
286 daemonize logfile action = do
288 _ <- forkProcess $ do
290 setupDaemonEnv "/" (unionFileModes groupModes otherModes)
291 setupDaemonFDs $ Just logfile
292 _ <- installHandler lostConnection (Catch (handleSigHup logfile)) Nothing
293 _ <- forkProcess action
294 exitImmediately ExitSuccess
295 exitImmediately ExitSuccess
297 -- | Generic daemon startup.
298 genericMain :: GanetiDaemon -> [OptType] -> (DaemonOptions -> IO ()) -> IO ()
299 genericMain daemon options main = do
300 let progname = daemonName daemon
301 (opts, args) <- parseArgs progname options
303 exitUnless (null args) "This program doesn't take any arguments"
305 unless (optNoUserChecks opts) $ do
306 runtimeEnts <- getEnts
307 ents <- exitIfBad "Can't find required user/groups" runtimeEnts
308 verifyDaemonUser daemon ents
310 syslog <- case optSyslogUsage opts of
311 Nothing -> exitIfBad "Invalid cluster syslog setting" $
312 syslogUsageFromRaw C.syslogUsage
314 let processFn = if optDaemonize opts
315 then daemonize (daemonLogFile daemon)
317 processFn $ innerMain daemon opts syslog (main opts)
319 -- | Inner daemon function.
321 -- This is executed after daemonization.
322 innerMain :: GanetiDaemon -> DaemonOptions -> SyslogUsage -> IO () -> IO ()
323 innerMain daemon opts syslog main = do
324 let logfile = if optDaemonize opts
326 else Just $ daemonLogFile daemon
327 setupLogging logfile (daemonName daemon) (optDebug opts) True False syslog
328 pid_fd <- writePidFile (daemonPidFile daemon)
329 _ <- exitIfBad "Cannot write PID file; already locked? Error" pid_fd