Statistics
| Branch: | Tag: | Revision:

root / src / Ganeti / Kvmd.hs @ db519e20

History | View | Annotate | Download (11.5 kB)

1 db519e20 Jose A. Lopes
{-| KVM daemon
2 db519e20 Jose A. Lopes
3 db519e20 Jose A. Lopes
The KVM daemon is responsible for determining whether a given KVM
4 db519e20 Jose A. Lopes
instance was shutdown by an administrator or a user.  For more
5 db519e20 Jose A. Lopes
information read the design document on the KVM daemon.
6 db519e20 Jose A. Lopes
7 db519e20 Jose A. Lopes
The KVM daemon design is split in 2 parts, namely, monitors for Qmp
8 db519e20 Jose A. Lopes
sockets and directory/file watching.
9 db519e20 Jose A. Lopes
10 db519e20 Jose A. Lopes
The monitors are spawned in lightweight Haskell threads and are
11 db519e20 Jose A. Lopes
reponsible for handling the communication between the KVM daemon and
12 db519e20 Jose A. Lopes
the KVM instance using the Qmp protocol.  During the communcation, the
13 db519e20 Jose A. Lopes
monitor parses the Qmp messages and if powerdown or shutdown is
14 db519e20 Jose A. Lopes
received, then the shutdown file is written in the KVM control
15 db519e20 Jose A. Lopes
directory.  Otherwise, when the communication terminates, that same
16 db519e20 Jose A. Lopes
file is removed.  The communication terminates when the KVM instance
17 db519e20 Jose A. Lopes
stops or crashes.
18 db519e20 Jose A. Lopes
19 db519e20 Jose A. Lopes
The directory and file watching uses inotify to track down events on
20 db519e20 Jose A. Lopes
the KVM control directory and its parents.  There is a directory
21 db519e20 Jose A. Lopes
crawler that will try to add a watch to the KVM control directory if
22 db519e20 Jose A. Lopes
available or its parents, thus replacing watches until the KVM control
23 db519e20 Jose A. Lopes
directory becomes available.  When this happens, a monitor for the Qmp
24 db519e20 Jose A. Lopes
socket is spawned.  Given that the KVM daemon might stop or crash, the
25 db519e20 Jose A. Lopes
directory watching also simulates events for the Qmp sockets that
26 db519e20 Jose A. Lopes
already exist in the KVM control directory when the KVM daemon starts.
27 db519e20 Jose A. Lopes
28 db519e20 Jose A. Lopes
-}
29 db519e20 Jose A. Lopes
30 db519e20 Jose A. Lopes
{-
31 db519e20 Jose A. Lopes
32 db519e20 Jose A. Lopes
Copyright (C) 2013 Google Inc.
33 db519e20 Jose A. Lopes
34 db519e20 Jose A. Lopes
This program is free software; you can redistribute it and/or modify
35 db519e20 Jose A. Lopes
it under the terms of the GNU General Public License as published by
36 db519e20 Jose A. Lopes
the Free Software Foundation; either version 2 of the License, or
37 db519e20 Jose A. Lopes
(at your option) any later version.
38 db519e20 Jose A. Lopes
39 db519e20 Jose A. Lopes
This program is distributed in the hope that it will be useful, but
40 db519e20 Jose A. Lopes
WITHOUT ANY WARRANTY; without even the implied warranty of
41 db519e20 Jose A. Lopes
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
42 db519e20 Jose A. Lopes
General Public License for more details.
43 db519e20 Jose A. Lopes
44 db519e20 Jose A. Lopes
You should have received a copy of the GNU General Public License
45 db519e20 Jose A. Lopes
along with this program; if not, write to the Free Software
46 db519e20 Jose A. Lopes
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
47 db519e20 Jose A. Lopes
02110-1301, USA.
48 db519e20 Jose A. Lopes
49 db519e20 Jose A. Lopes
-}
50 db519e20 Jose A. Lopes
51 db519e20 Jose A. Lopes
module Ganeti.Kvmd where
52 db519e20 Jose A. Lopes
53 db519e20 Jose A. Lopes
import Prelude hiding (catch, rem)
54 db519e20 Jose A. Lopes
55 db519e20 Jose A. Lopes
import Control.Applicative ((<$>))
56 db519e20 Jose A. Lopes
import Control.Exception (try)
57 db519e20 Jose A. Lopes
import Control.Concurrent
58 db519e20 Jose A. Lopes
import Control.Monad (unless, when)
59 db519e20 Jose A. Lopes
import Data.List
60 db519e20 Jose A. Lopes
import Data.Set (Set)
61 db519e20 Jose A. Lopes
import qualified Data.Set as Set (delete, empty, insert, member)
62 db519e20 Jose A. Lopes
import System.Directory
63 db519e20 Jose A. Lopes
import System.FilePath
64 db519e20 Jose A. Lopes
import System.IO
65 db519e20 Jose A. Lopes
import System.IO.Error (isEOFError)
66 db519e20 Jose A. Lopes
import System.INotify
67 db519e20 Jose A. Lopes
68 db519e20 Jose A. Lopes
import qualified AutoConf
69 db519e20 Jose A. Lopes
import qualified Ganeti.Constants as Constants
70 db519e20 Jose A. Lopes
import qualified Ganeti.Logging as Logging
71 db519e20 Jose A. Lopes
import qualified Ganeti.UDSServer as UDSServer
72 db519e20 Jose A. Lopes
73 db519e20 Jose A. Lopes
type Lock = MVar ()
74 db519e20 Jose A. Lopes
type Monitors = MVar (Set FilePath)
75 db519e20 Jose A. Lopes
76 db519e20 Jose A. Lopes
-- * Utils
77 db519e20 Jose A. Lopes
78 db519e20 Jose A. Lopes
-- | @isPrefixPath x y@ determines whether @x@ is a 'FilePath' prefix
79 db519e20 Jose A. Lopes
-- of 'FilePath' @y@.
80 db519e20 Jose A. Lopes
isPrefixPath :: FilePath -> FilePath -> Bool
81 db519e20 Jose A. Lopes
isPrefixPath x y =
82 db519e20 Jose A. Lopes
  (splitPath x `isPrefixOf` splitPath y) ||
83 db519e20 Jose A. Lopes
  (splitPath (x ++ "/") `isPrefixOf` splitPath y)
84 db519e20 Jose A. Lopes
85 db519e20 Jose A. Lopes
monitorGreeting :: String
86 db519e20 Jose A. Lopes
monitorGreeting = "{\"execute\": \"qmp_capabilities\"}"
87 db519e20 Jose A. Lopes
88 db519e20 Jose A. Lopes
-- | KVM control directory containing the Qmp sockets.
89 db519e20 Jose A. Lopes
monitorDir :: String
90 db519e20 Jose A. Lopes
monitorDir = AutoConf.localstatedir </> "run/ganeti/kvm-hypervisor/ctrl/"
91 db519e20 Jose A. Lopes
92 db519e20 Jose A. Lopes
monitorExtension :: String
93 db519e20 Jose A. Lopes
monitorExtension = ".qmp"
94 db519e20 Jose A. Lopes
95 db519e20 Jose A. Lopes
isMonitorPath :: FilePath -> Bool
96 db519e20 Jose A. Lopes
isMonitorPath = (== monitorExtension) . takeExtension
97 db519e20 Jose A. Lopes
98 db519e20 Jose A. Lopes
shutdownExtension :: String
99 db519e20 Jose A. Lopes
shutdownExtension = ".shutdown"
100 db519e20 Jose A. Lopes
101 db519e20 Jose A. Lopes
shutdownPath :: String -> String
102 db519e20 Jose A. Lopes
shutdownPath = (`replaceExtension` shutdownExtension)
103 db519e20 Jose A. Lopes
104 db519e20 Jose A. Lopes
touchFile :: FilePath -> IO ()
105 db519e20 Jose A. Lopes
touchFile file = withFile file WriteMode (const . return $ ())
106 db519e20 Jose A. Lopes
107 db519e20 Jose A. Lopes
-- * Monitors for Qmp sockets
108 db519e20 Jose A. Lopes
109 db519e20 Jose A. Lopes
-- | @parseQmp isPowerdown isShutdown isStop str@ parses the packet
110 db519e20 Jose A. Lopes
-- @str@ and returns whether a powerdown, shutdown, or stop event is
111 db519e20 Jose A. Lopes
-- contained in that packet, defaulting to the values @isPowerdown@,
112 db519e20 Jose A. Lopes
-- @isShutdown@, and @isStop@, otherwise.
113 db519e20 Jose A. Lopes
parseQmp :: Bool -> Bool -> Bool -> String -> (Bool, Bool, Bool)
114 db519e20 Jose A. Lopes
parseQmp isPowerdown isShutdown isStop str =
115 db519e20 Jose A. Lopes
  let
116 db519e20 Jose A. Lopes
    isPowerdown'
117 db519e20 Jose A. Lopes
      | "\"POWERDOWN\"" `isInfixOf` str = True
118 db519e20 Jose A. Lopes
      | otherwise = isPowerdown
119 db519e20 Jose A. Lopes
    isShutdown'
120 db519e20 Jose A. Lopes
      | "\"SHUTDOWN\"" `isInfixOf` str = True
121 db519e20 Jose A. Lopes
      | otherwise = isShutdown
122 db519e20 Jose A. Lopes
    isStop'
123 db519e20 Jose A. Lopes
      | "\"STOP\"" `isInfixOf` str = True
124 db519e20 Jose A. Lopes
      | otherwise = isStop
125 db519e20 Jose A. Lopes
  in
126 db519e20 Jose A. Lopes
   (isPowerdown', isShutdown', isStop')
127 db519e20 Jose A. Lopes
128 db519e20 Jose A. Lopes
-- | @receiveQmp handle@ listens for Qmp events on @handle@ and, when
129 db519e20 Jose A. Lopes
-- @handle@ is closed, it returns 'True' if a user shutdown event was
130 db519e20 Jose A. Lopes
-- received, and 'False' otherwise.
131 db519e20 Jose A. Lopes
receiveQmp :: Handle -> IO Bool
132 db519e20 Jose A. Lopes
receiveQmp handle = isUserShutdown <$> receive False False False
133 db519e20 Jose A. Lopes
  where -- | A user shutdown consists of a shutdown event with no
134 db519e20 Jose A. Lopes
        -- prior powerdown event and no stop event.
135 db519e20 Jose A. Lopes
        isUserShutdown (isShutdown, isPowerdown, isStop)
136 db519e20 Jose A. Lopes
          = isPowerdown && not isShutdown && not isStop
137 db519e20 Jose A. Lopes
138 db519e20 Jose A. Lopes
        receive isPowerdown isShutdown isStop =
139 db519e20 Jose A. Lopes
          do res <- try $ hGetLine handle
140 db519e20 Jose A. Lopes
             case res of
141 db519e20 Jose A. Lopes
               Left err -> do
142 db519e20 Jose A. Lopes
                 unless (isEOFError err) $
143 db519e20 Jose A. Lopes
                   hPrint stderr err
144 db519e20 Jose A. Lopes
                 return (isPowerdown, isShutdown, isStop)
145 db519e20 Jose A. Lopes
               Right str -> do
146 db519e20 Jose A. Lopes
                 let (isPowerdown', isShutdown', isStop') =
147 db519e20 Jose A. Lopes
                       parseQmp isPowerdown isShutdown isStop str
148 db519e20 Jose A. Lopes
                 Logging.logDebug $ "Receive QMP message: " ++ str
149 db519e20 Jose A. Lopes
                 receive isPowerdown' isShutdown' isStop'
150 db519e20 Jose A. Lopes
151 db519e20 Jose A. Lopes
-- | @detectMonitor monitorFile handle@ listens for Qmp events on
152 db519e20 Jose A. Lopes
-- @handle@ for Qmp socket @monitorFile@ and, when communcation
153 db519e20 Jose A. Lopes
-- terminates, it either creates the shutdown file, if a user shutdown
154 db519e20 Jose A. Lopes
-- was detected, or it deletes that same file, if an administrator
155 db519e20 Jose A. Lopes
-- shutdown was detected.
156 db519e20 Jose A. Lopes
detectMonitor :: FilePath -> Handle -> IO ()
157 db519e20 Jose A. Lopes
detectMonitor monitorFile handle =
158 db519e20 Jose A. Lopes
  do let shutdownFile = shutdownPath monitorFile
159 db519e20 Jose A. Lopes
     res <- receiveQmp handle
160 db519e20 Jose A. Lopes
     if res
161 db519e20 Jose A. Lopes
       then do
162 db519e20 Jose A. Lopes
         Logging.logInfo $ "Detect user shutdown, creating file " ++
163 db519e20 Jose A. Lopes
           show shutdownFile
164 db519e20 Jose A. Lopes
         touchFile shutdownFile
165 db519e20 Jose A. Lopes
       else do
166 db519e20 Jose A. Lopes
         Logging.logInfo $ "Detect admin shutdown, removing file " ++
167 db519e20 Jose A. Lopes
           show shutdownFile
168 db519e20 Jose A. Lopes
         (try (removeFile shutdownFile) :: IO (Either IOError ())) >> return ()
169 db519e20 Jose A. Lopes
170 db519e20 Jose A. Lopes
-- | @runMonitor monitorFile@ creates a monitor for the Qmp socket
171 db519e20 Jose A. Lopes
-- @monitorFile@ and calls 'detectMonitor'.
172 db519e20 Jose A. Lopes
runMonitor :: FilePath -> IO ()
173 db519e20 Jose A. Lopes
runMonitor monitorFile =
174 db519e20 Jose A. Lopes
  do handle <- UDSServer.openClientSocket Constants.luxiDefRwto monitorFile
175 db519e20 Jose A. Lopes
     hPutStrLn handle monitorGreeting
176 db519e20 Jose A. Lopes
     hFlush handle
177 db519e20 Jose A. Lopes
     detectMonitor monitorFile handle
178 db519e20 Jose A. Lopes
     UDSServer.closeClientSocket handle
179 db519e20 Jose A. Lopes
180 db519e20 Jose A. Lopes
-- | @ensureMonitor monitors monitorFile@ ensures that there is
181 db519e20 Jose A. Lopes
-- exactly one monitor running for the Qmp socket @monitorFile@, given
182 db519e20 Jose A. Lopes
-- the existing set of monitors @monitors@.
183 db519e20 Jose A. Lopes
ensureMonitor :: Monitors -> FilePath -> IO ()
184 db519e20 Jose A. Lopes
ensureMonitor monitors monitorFile =
185 db519e20 Jose A. Lopes
  modifyMVar_ monitors $
186 db519e20 Jose A. Lopes
    \files ->
187 db519e20 Jose A. Lopes
      if monitorFile `Set.member` files
188 db519e20 Jose A. Lopes
      then return files
189 db519e20 Jose A. Lopes
      else do
190 db519e20 Jose A. Lopes
        forkIO tryMonitor >> return ()
191 db519e20 Jose A. Lopes
        return $ monitorFile `Set.insert` files
192 db519e20 Jose A. Lopes
  where tryMonitor =
193 db519e20 Jose A. Lopes
          do Logging.logInfo $ "Start monitor " ++ show monitorFile
194 db519e20 Jose A. Lopes
             res <- try (runMonitor monitorFile) :: IO (Either IOError ())
195 db519e20 Jose A. Lopes
             case res of
196 db519e20 Jose A. Lopes
               Left err ->
197 db519e20 Jose A. Lopes
                 Logging.logError $ "Catch monitor exception: " ++ show err
198 db519e20 Jose A. Lopes
               _ ->
199 db519e20 Jose A. Lopes
                 return ()
200 db519e20 Jose A. Lopes
             Logging.logInfo $ "Stop monitor " ++ show monitorFile
201 db519e20 Jose A. Lopes
             modifyMVar_ monitors (return . Set.delete monitorFile)
202 db519e20 Jose A. Lopes
203 db519e20 Jose A. Lopes
-- * Directory and file watching
204 db519e20 Jose A. Lopes
205 db519e20 Jose A. Lopes
-- | Handles an inotify event outside the target directory.
206 db519e20 Jose A. Lopes
--
207 db519e20 Jose A. Lopes
-- Tracks events on the parent directory of the KVM control directory
208 db519e20 Jose A. Lopes
-- until one of its parents becomes available.
209 db519e20 Jose A. Lopes
handleGenericEvent :: Lock -> String -> String -> Event -> IO ()
210 db519e20 Jose A. Lopes
handleGenericEvent lock curDir tarDir ev@Created {}
211 db519e20 Jose A. Lopes
  | isDirectory ev && curDir /= tarDir &&
212 db519e20 Jose A. Lopes
    (curDir </> filePath ev) `isPrefixPath` tarDir = putMVar lock ()
213 db519e20 Jose A. Lopes
handleGenericEvent lock _ _ event
214 db519e20 Jose A. Lopes
  | event == DeletedSelf || event == Unmounted = putMVar lock ()
215 db519e20 Jose A. Lopes
handleGenericEvent _ _ _ _ = return ()
216 db519e20 Jose A. Lopes
217 db519e20 Jose A. Lopes
-- | Handles an inotify event in the target directory.
218 db519e20 Jose A. Lopes
--
219 db519e20 Jose A. Lopes
-- Upon a create or open event inside the KVM control directory, it
220 db519e20 Jose A. Lopes
-- ensures that there is a monitor running for the new Qmp socket.
221 db519e20 Jose A. Lopes
handleTargetEvent :: Lock -> Monitors -> String -> Event -> IO ()
222 db519e20 Jose A. Lopes
handleTargetEvent _ monitors tarDir ev@Created {}
223 db519e20 Jose A. Lopes
  | not (isDirectory ev) && isMonitorPath (filePath ev) =
224 db519e20 Jose A. Lopes
    ensureMonitor monitors $ tarDir </> filePath ev
225 db519e20 Jose A. Lopes
handleTargetEvent lock monitors tarDir ev@Opened {}
226 db519e20 Jose A. Lopes
  | not (isDirectory ev) =
227 db519e20 Jose A. Lopes
    case maybeFilePath ev of
228 db519e20 Jose A. Lopes
      Just p | isMonitorPath p ->
229 db519e20 Jose A. Lopes
        ensureMonitor monitors $ tarDir </> filePath ev
230 db519e20 Jose A. Lopes
      _ ->
231 db519e20 Jose A. Lopes
        handleGenericEvent lock tarDir tarDir ev
232 db519e20 Jose A. Lopes
handleTargetEvent _ _ tarDir ev@Created {}
233 db519e20 Jose A. Lopes
  | not (isDirectory ev) && takeExtension (filePath ev) == shutdownExtension =
234 db519e20 Jose A. Lopes
    Logging.logInfo $ "User shutdown file opened " ++
235 db519e20 Jose A. Lopes
      show (tarDir </> filePath ev)
236 db519e20 Jose A. Lopes
handleTargetEvent _ _ tarDir ev@Deleted {}
237 db519e20 Jose A. Lopes
  | not (isDirectory ev) && takeExtension (filePath ev) == shutdownExtension =
238 db519e20 Jose A. Lopes
    Logging.logInfo $ "User shutdown file deleted " ++
239 db519e20 Jose A. Lopes
      show (tarDir </> filePath ev)
240 db519e20 Jose A. Lopes
handleTargetEvent lock _ tarDir ev =
241 db519e20 Jose A. Lopes
  handleGenericEvent lock tarDir tarDir ev
242 db519e20 Jose A. Lopes
243 db519e20 Jose A. Lopes
-- | Dispatches inotify events depending on the directory they occur in.
244 db519e20 Jose A. Lopes
handleDir :: Lock -> Monitors -> String -> String -> Event -> IO ()
245 db519e20 Jose A. Lopes
handleDir lock monitors curDir tarDir event =
246 db519e20 Jose A. Lopes
  do Logging.logDebug $ "Handle event " ++ show event
247 db519e20 Jose A. Lopes
     if curDir == tarDir
248 db519e20 Jose A. Lopes
       then handleTargetEvent lock monitors tarDir event
249 db519e20 Jose A. Lopes
       else handleGenericEvent lock curDir tarDir event
250 db519e20 Jose A. Lopes
251 db519e20 Jose A. Lopes
-- | Simulates file creation events for the Qmp sockets that already
252 db519e20 Jose A. Lopes
-- exist in @dir@.
253 db519e20 Jose A. Lopes
recapDir :: Lock -> Monitors -> FilePath -> IO ()
254 db519e20 Jose A. Lopes
recapDir lock monitors dir =
255 db519e20 Jose A. Lopes
  do files <- getDirectoryContents dir
256 db519e20 Jose A. Lopes
     let files' = filter isMonitorPath files
257 db519e20 Jose A. Lopes
     mapM_ sendEvent files'
258 db519e20 Jose A. Lopes
  where sendEvent file =
259 db519e20 Jose A. Lopes
          handleTargetEvent lock monitors dir Created { isDirectory = False
260 db519e20 Jose A. Lopes
                                                      , filePath = file }
261 db519e20 Jose A. Lopes
262 db519e20 Jose A. Lopes
-- | Crawls @tarDir@, or its parents until @tarDir@ becomes available,
263 db519e20 Jose A. Lopes
-- always listening for inotify events.
264 db519e20 Jose A. Lopes
--
265 db519e20 Jose A. Lopes
-- Used for crawling the KVM control directory and its parents, as
266 db519e20 Jose A. Lopes
-- well as simulating file creation events.
267 db519e20 Jose A. Lopes
watchDir :: Lock -> FilePath -> INotify -> IO ()
268 db519e20 Jose A. Lopes
watchDir lock tarDir inotify = watchDir' tarDir
269 db519e20 Jose A. Lopes
  where watchDirEvents dir
270 db519e20 Jose A. Lopes
          | dir == tarDir = [AllEvents]
271 db519e20 Jose A. Lopes
          | otherwise = [Create, DeleteSelf]
272 db519e20 Jose A. Lopes
273 db519e20 Jose A. Lopes
        watchDir' dir =
274 db519e20 Jose A. Lopes
          do add <- doesDirectoryExist dir
275 db519e20 Jose A. Lopes
             if add
276 db519e20 Jose A. Lopes
               then do
277 db519e20 Jose A. Lopes
                 let events = watchDirEvents dir
278 db519e20 Jose A. Lopes
                 Logging.logInfo $ "Watch directory " ++ show dir
279 db519e20 Jose A. Lopes
                 monitors <- newMVar Set.empty
280 db519e20 Jose A. Lopes
                 wd <- addWatch inotify events dir
281 db519e20 Jose A. Lopes
                       (handleDir lock monitors dir tarDir)
282 db519e20 Jose A. Lopes
                 when (dir == tarDir) $ recapDir lock monitors dir
283 db519e20 Jose A. Lopes
                 () <- takeMVar lock
284 db519e20 Jose A. Lopes
                 rem <- doesDirectoryExist dir
285 db519e20 Jose A. Lopes
                 if rem
286 db519e20 Jose A. Lopes
                   then do
287 db519e20 Jose A. Lopes
                     Logging.logInfo $ "Unwatch directory " ++ show dir
288 db519e20 Jose A. Lopes
                     removeWatch wd
289 db519e20 Jose A. Lopes
                   else
290 db519e20 Jose A. Lopes
                     Logging.logInfo $ "Throw away watch from directory " ++
291 db519e20 Jose A. Lopes
                       show dir
292 db519e20 Jose A. Lopes
               else
293 db519e20 Jose A. Lopes
                 watchDir' (takeDirectory dir)
294 db519e20 Jose A. Lopes
295 db519e20 Jose A. Lopes
rewatchDir :: Lock -> FilePath -> INotify -> IO ()
296 db519e20 Jose A. Lopes
rewatchDir lock tarDir inotify =
297 db519e20 Jose A. Lopes
  do watchDir lock tarDir inotify
298 db519e20 Jose A. Lopes
     rewatchDir lock tarDir inotify
299 db519e20 Jose A. Lopes
300 db519e20 Jose A. Lopes
-- * Starting point
301 db519e20 Jose A. Lopes
302 db519e20 Jose A. Lopes
startWith :: FilePath -> IO ()
303 db519e20 Jose A. Lopes
startWith dir =
304 db519e20 Jose A. Lopes
  do lock <- newEmptyMVar
305 db519e20 Jose A. Lopes
     withINotify (rewatchDir lock dir)
306 db519e20 Jose A. Lopes
307 db519e20 Jose A. Lopes
start :: IO ()
308 db519e20 Jose A. Lopes
start = startWith monitorDir