Statistics
| Branch: | Tag: | Revision:

root / htools / Ganeti / Confd / Server.hs @ 19cff311

History | View | Annotate | Download (18.8 kB)

1
{-# LANGUAGE BangPatterns #-}
2

    
3
{-| Implementation of the Ganeti confd server functionality.
4

    
5
-}
6

    
7
{-
8

    
9
Copyright (C) 2011, 2012 Google Inc.
10

    
11
This program is free software; you can redistribute it and/or modify
12
it under the terms of the GNU General Public License as published by
13
the Free Software Foundation; either version 2 of the License, or
14
(at your option) any later version.
15

    
16
This program is distributed in the hope that it will be useful, but
17
WITHOUT ANY WARRANTY; without even the implied warranty of
18
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
19
General Public License for more details.
20

    
21
You should have received a copy of the GNU General Public License
22
along with this program; if not, write to the Free Software
23
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
24
02110-1301, USA.
25

    
26
-}
27

    
28
module Ganeti.Confd.Server
29
  ( main
30
  ) where
31

    
32
import Control.Concurrent
33
import Control.Monad (forever)
34
import qualified Data.ByteString as B
35
import Data.IORef
36
import Data.List
37
import qualified Data.Map as M
38
import qualified Network.Socket as S
39
import System.Posix.Files
40
import System.Posix.Types
41
import System.Time
42
import qualified Text.JSON as J
43
import System.INotify
44

    
45
import Ganeti.Daemon
46
import Ganeti.HTools.JSON
47
import Ganeti.HTools.Types
48
import Ganeti.HTools.Utils
49
import Ganeti.Objects
50
import Ganeti.Confd
51
import Ganeti.Config
52
import Ganeti.Hash
53
import Ganeti.Logging
54
import Ganeti.BasicTypes
55
import qualified Ganeti.Constants as C
56

    
57
-- * Types and constants definitions
58

    
59
-- | What we store as configuration.
60
type CRef = IORef (Result (ConfigData, LinkIpMap))
61

    
62
-- | File stat identifier.
63
type FStat = (EpochTime, FileID, FileOffset)
64

    
65
-- | Null 'FStat' value.
66
nullFStat :: FStat
67
nullFStat = (-1, -1, -1)
68

    
69
-- | A small type alias for readability.
70
type StatusAnswer = (ConfdReplyStatus, J.JSValue)
71

    
72
-- | Reload model data type.
73
data ReloadModel = ReloadNotify      -- ^ We are using notifications
74
                 | ReloadPoll Int    -- ^ We are using polling
75
                   deriving (Eq, Show)
76

    
77
-- | Server state data type.
78
data ServerState = ServerState
79
  { reloadModel  :: ReloadModel
80
  , reloadTime   :: Integer
81
  , reloadFStat  :: FStat
82
  }
83

    
84
-- | Maximum no-reload poll rounds before reverting to inotify.
85
maxIdlePollRounds :: Int
86
maxIdlePollRounds = 2
87

    
88
-- | Reload timeout in microseconds.
89
configReloadTimeout :: Int
90
configReloadTimeout = C.confdConfigReloadTimeout * 1000000
91

    
92
-- | Ratelimit timeout in microseconds.
93
configReloadRatelimit :: Int
94
configReloadRatelimit = C.confdConfigReloadRatelimit * 1000000
95

    
96
-- | Initial poll round.
97
initialPoll :: ReloadModel
98
initialPoll = ReloadPoll 0
99

    
100
-- | Initial server state.
101
initialState :: ServerState
102
initialState = ServerState initialPoll 0 nullFStat
103

    
104
-- | Reload status data type.
105
data ConfigReload = ConfigToDate    -- ^ No need to reload
106
                  | ConfigReloaded  -- ^ Configuration reloaded
107
                  | ConfigIOError   -- ^ Error during configuration reload
108

    
109
-- | Unknown entry standard response.
110
queryUnknownEntry :: StatusAnswer
111
queryUnknownEntry = (ReplyStatusError, J.showJSON ConfdErrorUnknownEntry)
112

    
113
{- not used yet
114
-- | Internal error standard response.
115
queryInternalError :: StatusAnswer
116
queryInternalError = (ReplyStatusError, J.showJSON ConfdErrorInternal)
117
-}
118

    
119
-- | Argument error standard response.
120
queryArgumentError :: StatusAnswer
121
queryArgumentError = (ReplyStatusError, J.showJSON ConfdErrorArgument)
122

    
123
-- | Returns the current time.
124
getCurrentTime :: IO Integer
125
getCurrentTime = do
126
  TOD ctime _ <- getClockTime
127
  return ctime
128

    
129
-- * Confd base functionality
130

    
131
-- | Returns the HMAC key.
132
getClusterHmac :: IO HashKey
133
getClusterHmac = fmap B.unpack $ B.readFile C.confdHmacKey
134

    
135
-- | Computes the node role.
136
nodeRole :: ConfigData -> String -> Result ConfdNodeRole
137
nodeRole cfg name =
138
  let cmaster = clusterMasterNode . configCluster $ cfg
139
      mnode = M.lookup name . configNodes $ cfg
140
  in case mnode of
141
       Nothing -> Bad "Node not found"
142
       Just node | cmaster == name -> Ok NodeRoleMaster
143
                 | nodeDrained node -> Ok NodeRoleDrained
144
                 | nodeOffline node -> Ok NodeRoleOffline
145
                 | nodeMasterCandidate node -> Ok NodeRoleCandidate
146
       _ -> Ok NodeRoleRegular
147

    
148
-- | Does an instance ip -> instance -> primary node -> primary ip
149
-- transformation.
150
getNodePipByInstanceIp :: ConfigData
151
                       -> LinkIpMap
152
                       -> String
153
                       -> String
154
                       -> StatusAnswer
155
getNodePipByInstanceIp cfg linkipmap link instip =
156
  case M.lookup instip (M.findWithDefault M.empty link linkipmap) of
157
    Nothing -> queryUnknownEntry
158
    Just instname ->
159
      case getInstPrimaryNode cfg instname of
160
        Bad _ -> queryUnknownEntry -- either instance or node not found
161
        Ok node -> (ReplyStatusOk, J.showJSON (nodePrimaryIp node))
162

    
163
-- | Builds the response to a given query.
164
buildResponse :: (ConfigData, LinkIpMap) -> ConfdRequest -> Result StatusAnswer
165
buildResponse (cfg, _) (ConfdRequest { confdRqType = ReqPing }) =
166
  return (ReplyStatusOk, J.showJSON (configVersion cfg))
167

    
168
buildResponse cdata req@(ConfdRequest { confdRqType = ReqClusterMaster }) =
169
  case confdRqQuery req of
170
    EmptyQuery -> return (ReplyStatusOk, J.showJSON master_name)
171
    PlainQuery _ -> return queryArgumentError
172
    DictQuery reqq -> do
173
      mnode <- getNode cfg master_name
174
      let fvals =map (\field -> case field of
175
                                  ReqFieldName -> master_name
176
                                  ReqFieldIp -> clusterMasterIp cluster
177
                                  ReqFieldMNodePip -> nodePrimaryIp mnode
178
                     ) (confdReqQFields reqq)
179
      return (ReplyStatusOk, J.showJSON fvals)
180
    where master_name = clusterMasterNode cluster
181
          cluster = configCluster cfg
182
          cfg = fst cdata
183

    
184
buildResponse cdata req@(ConfdRequest { confdRqType = ReqNodeRoleByName }) = do
185
  node_name <- case confdRqQuery req of
186
                 PlainQuery str -> return str
187
                 _ -> fail $ "Invalid query type " ++ show (confdRqQuery req)
188
  role <- nodeRole (fst cdata) node_name
189
  return (ReplyStatusOk, J.showJSON role)
190

    
191
buildResponse cdata (ConfdRequest { confdRqType = ReqNodePipList }) =
192
  -- note: we use foldlWithKey because that's present accross more
193
  -- versions of the library
194
  return (ReplyStatusOk, J.showJSON $
195
          M.foldlWithKey (\accu _ n -> nodePrimaryIp n:accu) []
196
          (configNodes (fst cdata)))
197

    
198
buildResponse cdata (ConfdRequest { confdRqType = ReqMcPipList }) =
199
  -- note: we use foldlWithKey because that's present accross more
200
  -- versions of the library
201
  return (ReplyStatusOk, J.showJSON $
202
          M.foldlWithKey (\accu _ n -> if nodeMasterCandidate n
203
                                         then nodePrimaryIp n:accu
204
                                         else accu) []
205
          (configNodes (fst cdata)))
206

    
207
buildResponse (cfg, linkipmap)
208
              req@(ConfdRequest { confdRqType = ReqInstIpsList }) = do
209
  link <- case confdRqQuery req of
210
            PlainQuery str -> return str
211
            EmptyQuery -> return (getDefaultNicLink cfg)
212
            _ -> fail "Invalid query type"
213
  return (ReplyStatusOk, J.showJSON $ getInstancesIpByLink linkipmap link)
214

    
215
buildResponse cdata (ConfdRequest { confdRqType = ReqNodePipByInstPip
216
                                  , confdRqQuery = DictQuery query}) =
217
  let (cfg, linkipmap) = cdata
218
      link = maybe (getDefaultNicLink cfg) id (confdReqQLink query)
219
  in case confdReqQIp query of
220
       Just ip -> return $ getNodePipByInstanceIp cfg linkipmap link ip
221
       Nothing -> return (ReplyStatusOk,
222
                          J.showJSON $
223
                           map (getNodePipByInstanceIp cfg linkipmap link)
224
                           (confdReqQIpList query))
225

    
226
buildResponse _ (ConfdRequest { confdRqType = ReqNodePipByInstPip }) =
227
  return queryArgumentError
228

    
229
-- | Parses a signed request.
230
parseRequest :: HashKey -> String -> Result (String, String, ConfdRequest)
231
parseRequest key str = do
232
  (SignedMessage hmac msg salt) <- fromJResult "parsing request" $ J.decode str
233
  req <- if verifyMac key (Just salt) msg hmac
234
           then fromJResult "parsing message" $ J.decode msg
235
           else Bad "HMAC verification failed"
236
  return (salt, msg, req)
237

    
238
-- | Creates a ConfdReply from a given answer.
239
serializeResponse :: Result StatusAnswer -> ConfdReply
240
serializeResponse r =
241
    let (status, result) = case r of
242
                    Bad err -> (ReplyStatusError, J.showJSON err)
243
                    Ok (code, val) -> (code, val)
244
    in ConfdReply { confdReplyProtocol = 1
245
                  , confdReplyStatus   = status
246
                  , confdReplyAnswer   = result
247
                  , confdReplySerial   = 0 }
248

    
249
-- | Signs a message with a given key and salt.
250
signMessage :: HashKey -> String -> String -> SignedMessage
251
signMessage key salt msg =
252
  SignedMessage { signedMsgMsg  = msg
253
                , signedMsgSalt = salt
254
                , signedMsgHmac = hmac
255
                }
256
    where hmac = computeMac key (Just salt) msg
257

    
258
-- * Configuration handling
259

    
260
-- ** Helper functions
261

    
262
-- | Helper function for logging transition into polling mode.
263
moveToPolling :: String -> INotify -> FilePath -> CRef -> MVar ServerState
264
              -> IO ReloadModel
265
moveToPolling msg inotify path cref mstate = do
266
  logInfo $ "Moving to polling mode: " ++ msg
267
  let inotiaction = addNotifier inotify path cref mstate
268
  _ <- forkIO $ onReloadTimer inotiaction path cref mstate
269
  return initialPoll
270

    
271
-- | Helper function for logging transition into inotify mode.
272
moveToNotify :: IO ReloadModel
273
moveToNotify = do
274
  logInfo "Moving to inotify mode"
275
  return ReloadNotify
276

    
277
-- ** Configuration loading
278

    
279
-- | (Re)loads the configuration.
280
updateConfig :: FilePath -> CRef -> IO ()
281
updateConfig path r = do
282
  newcfg <- loadConfig path
283
  let !newdata = case newcfg of
284
                   Ok !cfg -> Ok (cfg, buildLinkIpInstnameMap cfg)
285
                   Bad _ -> Bad "Cannot load configuration"
286
  writeIORef r newdata
287
  case newcfg of
288
    Ok cfg -> logInfo ("Loaded new config, serial " ++
289
                       show (configSerial cfg))
290
    Bad msg -> logError $ "Failed to load config: " ++ msg
291
  return ()
292

    
293
-- | Wrapper over 'updateConfig' that handles IO errors.
294
safeUpdateConfig :: FilePath -> FStat -> CRef -> IO (FStat, ConfigReload)
295
safeUpdateConfig path oldfstat cref = do
296
  catch (do
297
          nt <- needsReload oldfstat path
298
          case nt of
299
            Nothing -> return (oldfstat, ConfigToDate)
300
            Just nt' -> do
301
                    updateConfig path cref
302
                    return (nt', ConfigReloaded)
303
        ) (\e -> do
304
             let msg = "Failure during configuration update: " ++ show e
305
             writeIORef cref (Bad msg)
306
             return (nullFStat, ConfigIOError)
307
          )
308

    
309
-- | Computes the file cache data from a FileStatus structure.
310
buildFileStatus :: FileStatus -> FStat
311
buildFileStatus ofs =
312
    let modt = modificationTime ofs
313
        inum = fileID ofs
314
        fsize = fileSize ofs
315
    in (modt, inum, fsize)
316

    
317
-- | Wrapper over 'buildFileStatus'. This reads the data from the
318
-- filesystem and then builds our cache structure.
319
getFStat :: FilePath -> IO FStat
320
getFStat p = getFileStatus p >>= (return . buildFileStatus)
321

    
322
-- | Check if the file needs reloading
323
needsReload :: FStat -> FilePath -> IO (Maybe FStat)
324
needsReload oldstat path = do
325
  newstat <- getFStat path
326
  return $ if newstat /= oldstat
327
             then Just newstat
328
             else Nothing
329

    
330
-- ** Watcher threads
331

    
332
-- $watcher
333
-- We have three threads/functions that can mutate the server state:
334
--
335
-- 1. the long-interval watcher ('onTimeoutTimer')
336
--
337
-- 2. the polling watcher ('onReloadTimer')
338
--
339
-- 3. the inotify event handler ('onInotify')
340
--
341
-- All of these will mutate the server state under 'modifyMVar' or
342
-- 'modifyMVar_', so that server transitions are more or less
343
-- atomic. The inotify handler remains active during polling mode, but
344
-- checks for polling mode and doesn't do anything in this case (this
345
-- check is needed even if we would unregister the event handler due
346
-- to how events are serialised).
347

    
348
-- | Long-interval reload watcher.
349
--
350
-- This is on top of the inotify-based triggered reload.
351
onTimeoutTimer :: IO Bool -> FilePath -> CRef -> MVar ServerState -> IO ()
352
onTimeoutTimer inotiaction path cref state = do
353
  threadDelay configReloadTimeout
354
  modifyMVar_ state (onTimeoutInner path cref)
355
  _ <- inotiaction
356
  onTimeoutTimer inotiaction path cref state
357

    
358
-- | Inner onTimeout handler.
359
--
360
-- This mutates the server state under a modifyMVar_ call. It never
361
-- changes the reload model, just does a safety reload and tried to
362
-- re-establish the inotify watcher.
363
onTimeoutInner :: FilePath -> CRef -> ServerState -> IO ServerState
364
onTimeoutInner path cref state  = do
365
  (newfstat, _) <- safeUpdateConfig path (reloadFStat state) cref
366
  return state { reloadFStat = newfstat }
367

    
368
-- | Short-interval (polling) reload watcher.
369
--
370
-- This is only active when we're in polling mode; it will
371
-- automatically exit when it detects that the state has changed to
372
-- notification.
373
onReloadTimer :: IO Bool -> FilePath -> CRef -> MVar ServerState -> IO ()
374
onReloadTimer inotiaction path cref state = do
375
  continue <- modifyMVar state (onReloadInner inotiaction path cref)
376
  if continue
377
    then do
378
      threadDelay configReloadRatelimit
379
      onReloadTimer inotiaction path cref state
380
    else -- the inotify watch has been re-established, we can exit
381
      return ()
382

    
383
-- | Inner onReload handler.
384
--
385
-- This again mutates the state under a modifyMVar call, and also
386
-- returns whether the thread should continue or not.
387
onReloadInner :: IO Bool -> FilePath -> CRef -> ServerState
388
              -> IO (ServerState, Bool)
389
onReloadInner _ _ _ state@(ServerState { reloadModel = ReloadNotify } ) =
390
  return (state, False)
391
onReloadInner inotiaction path cref
392
              state@(ServerState { reloadModel = ReloadPoll pround } ) = do
393
  (newfstat, reload) <- safeUpdateConfig path (reloadFStat state) cref
394
  let state' = state { reloadFStat = newfstat }
395
  -- compute new poll model based on reload data; however, failure to
396
  -- re-establish the inotifier means we stay on polling
397
  newmode <- case reload of
398
               ConfigToDate ->
399
                 if pround >= maxIdlePollRounds
400
                   then do -- try to switch to notify
401
                     result <- inotiaction
402
                     if result
403
                       then moveToNotify
404
                       else return initialPoll
405
                   else return (ReloadPoll (pround + 1))
406
               _ -> return initialPoll
407
  let continue = case newmode of
408
                   ReloadNotify -> False
409
                   _            -> True
410
  return (state' { reloadModel = newmode }, continue)
411

    
412
-- | Setup inotify watcher.
413
--
414
-- This tries to setup the watch descriptor; in case of any IO errors,
415
-- it will return False.
416
addNotifier :: INotify -> FilePath -> CRef -> MVar ServerState -> IO Bool
417
addNotifier inotify path cref mstate = do
418
  catch (addWatch inotify [CloseWrite] path
419
                    (onInotify inotify path cref mstate) >> return True)
420
        (const $ return False)
421

    
422
-- | Inotify event handler.
423
onInotify :: INotify -> String -> CRef -> MVar ServerState -> Event -> IO ()
424
onInotify inotify path cref mstate Ignored = do
425
  logInfo "File lost, trying to re-establish notifier"
426
  modifyMVar_ mstate $ \state -> do
427
    result <- addNotifier inotify path cref mstate
428
    (newfstat, _) <- safeUpdateConfig path (reloadFStat state) cref
429
    let state' = state { reloadFStat = newfstat }
430
    if result
431
      then return state' -- keep notify
432
      else do
433
        mode <- moveToPolling "cannot re-establish inotify watch" inotify
434
                  path cref mstate
435
        return state' { reloadModel = mode }
436

    
437
onInotify inotify path cref mstate _ = do
438
  modifyMVar_ mstate $ \state ->
439
    if (reloadModel state == ReloadNotify)
440
       then do
441
         ctime <- getCurrentTime
442
         (newfstat, _) <- safeUpdateConfig path (reloadFStat state) cref
443
         let state' = state { reloadFStat = newfstat, reloadTime = ctime }
444
         if abs (reloadTime state - ctime) <
445
            fromIntegral C.confdConfigReloadRatelimit
446
           then do
447
             mode <- moveToPolling "too many reloads" inotify path cref mstate
448
             return state' { reloadModel = mode }
449
           else return state'
450
      else return state
451

    
452
-- ** Client input/output handlers
453

    
454
-- | Main loop for a given client.
455
responder :: CRef -> S.Socket -> HashKey -> String -> S.SockAddr -> IO ()
456
responder cfgref socket hmac msg peer = do
457
  ctime <- getCurrentTime
458
  case parseMessage hmac msg ctime of
459
    Ok (origmsg, rq) -> do
460
              logDebug $ "Processing request: " ++ origmsg
461
              mcfg <- readIORef cfgref
462
              let response = respondInner mcfg hmac rq
463
              _ <- S.sendTo socket response peer
464
              return ()
465
    Bad err -> logInfo $ "Failed to parse incoming message: " ++ err
466
  return ()
467

    
468
-- | Mesage parsing. This can either result in a good, valid message,
469
-- or fail in the Result monad.
470
parseMessage :: HashKey -> String -> Integer
471
             -> Result (String, ConfdRequest)
472
parseMessage hmac msg curtime = do
473
  (salt, origmsg, request) <- parseRequest hmac msg
474
  ts <- tryRead "Parsing timestamp" salt::Result Integer
475
  if (abs (ts - curtime) > (fromIntegral C.confdMaxClockSkew))
476
    then fail "Too old/too new timestamp or clock skew"
477
    else return (origmsg, request)
478

    
479
-- | Inner helper function for a given client. This generates the
480
-- final encoded message (as a string), ready to be sent out to the
481
-- client.
482
respondInner :: Result (ConfigData, LinkIpMap) -> HashKey
483
             -> ConfdRequest -> String
484
respondInner cfg hmac rq =
485
  let rsalt = confdRqRsalt rq
486
      innermsg = serializeResponse (cfg >>= flip buildResponse rq)
487
      innerserialised = J.encodeStrict innermsg
488
      outermsg = signMessage hmac rsalt innerserialised
489
      outerserialised = confdMagicFourcc ++ J.encodeStrict outermsg
490
  in outerserialised
491

    
492
-- | Main listener loop.
493
listener :: S.Socket -> HashKey
494
         -> (S.Socket -> HashKey -> String -> S.SockAddr -> IO ())
495
         -> IO ()
496
listener s hmac resp = do
497
  (msg, _, peer) <- S.recvFrom s 4096
498
  if confdMagicFourcc `isPrefixOf` msg
499
    then (forkIO $ resp s hmac (drop 4 msg) peer) >> return ()
500
    else logDebug "Invalid magic code!" >> return ()
501
  return ()
502

    
503
-- | Main function.
504
main :: DaemonOptions -> IO ()
505
main opts = do
506
  parseresult <- parseAddress opts C.defaultConfdPort
507
  (af_family, bindaddr) <- exitIfBad parseresult
508
  s <- S.socket af_family S.Datagram S.defaultProtocol
509
  S.bindSocket s bindaddr
510
  cref <- newIORef (Bad "Configuration not yet loaded")
511
  statemvar <- newMVar initialState
512
  hmac <- getClusterHmac
513
  -- Inotify setup
514
  inotify <- initINotify
515
  let inotiaction = addNotifier inotify C.clusterConfFile cref statemvar
516
  -- fork the timeout timer
517
  _ <- forkIO $ onTimeoutTimer inotiaction C.clusterConfFile cref statemvar
518
  -- fork the polling timer
519
  _ <- forkIO $ onReloadTimer inotiaction C.clusterConfFile cref statemvar
520
  -- and finally enter the responder loop
521
  forever $ listener s hmac (responder cref)