1 {-# LANGUAGE TemplateHaskell #-}
3 {-| Implementation of the Ganeti LUXI interface.
9 Copyright (C) 2009, 2010, 2011, 2012 Google Inc.
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.
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.
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
47 import Text.JSON (encodeStrict, decodeStrict)
48 import qualified Text.JSON as J
49 import Text.JSON.Types
51 import qualified Network.Socket as S
53 import Ganeti.HTools.JSON
54 import Ganeti.HTools.Types
55 import Ganeti.HTools.Utils
57 import Ganeti.Constants
58 import Ganeti.Jobs (JobStatus)
59 import Ganeti.OpCodes (OpCode)
62 -- * Utility functions
64 -- | Wrapper over System.Timeout.timeout that fails in the IO monad.
65 withTimeout :: Int -> String -> IO a -> IO a
66 withTimeout secs descr action = do
67 result <- timeout (secs * 1000000) action
69 Nothing -> fail $ "Timeout in " ++ descr
72 -- * Generic protocol functionality
74 $(declareSADT "QrViaLuxi"
76 , ("QRInstance", 'qrInstance)
78 , ("QRGroup", 'qrGroup)
81 $(makeJSONInstance ''QrViaLuxi)
83 -- | Currently supported Luxi operations and JSON serialization.
86 [ ("what", [t| QrViaLuxi |], [| id |])
87 , ("fields", [t| [String] |], [| id |])
88 , ("qfilter", [t| () |], [| const JSNull |])
91 [ ("names", [t| [String] |], [| id |])
92 , ("fields", [t| [String] |], [| id |])
93 , ("lock", [t| Bool |], [| id |])
95 , (luxiReqQueryGroups,
96 [ ("names", [t| [String] |], [| id |])
97 , ("fields", [t| [String] |], [| id |])
98 , ("lock", [t| Bool |], [| id |])
100 , (luxiReqQueryInstances,
101 [ ("names", [t| [String] |], [| id |])
102 , ("fields", [t| [String] |], [| id |])
103 , ("lock", [t| Bool |], [| id |])
106 [ ("ids", [t| [Int] |], [| map show |])
107 , ("fields", [t| [String] |], [| id |])
109 , (luxiReqQueryExports,
110 [ ("nodes", [t| [String] |], [| id |])
111 , ("lock", [t| Bool |], [| id |])
113 , (luxiReqQueryConfigValues,
114 [ ("fields", [t| [String] |], [| id |]) ]
116 , (luxiReqQueryClusterInfo, [])
118 [ ("kind", [t| String |], [| id |])
119 , ("name", [t| String |], [| id |])
122 [ ("job", [t| [OpCode] |], [| id |]) ]
124 , (luxiReqSubmitManyJobs,
125 [ ("ops", [t| [[OpCode]] |], [| id |]) ]
127 , (luxiReqWaitForJobChange,
128 [ ("job", [t| Int |], [| show |])
129 , ("fields", [t| [String]|], [| id |])
130 , ("prev_job", [t| JSValue |], [| id |])
131 , ("prev_log", [t| JSValue |], [| id |])
132 , ("tmout", [t| Int |], [| id |])
134 , (luxiReqArchiveJob,
135 [ ("job", [t| Int |], [| show |]) ]
137 , (luxiReqAutoArchiveJobs,
138 [ ("age", [t| Int |], [| id |])
139 , ("tmout", [t| Int |], [| id |])
142 [ ("job", [t| Int |], [| show |]) ]
144 , (luxiReqSetDrainFlag,
145 [ ("flag", [t| Bool |], [| id |]) ]
147 , (luxiReqSetWatcherPause,
148 [ ("duration", [t| Double |], [| id |]) ]
152 $(makeJSONInstance ''LuxiReq)
154 -- | The serialisation of LuxiOps into strings in messages.
155 $(genStrOfOp ''LuxiOp "strOfOp")
157 $(declareIADT "ResultStatus"
158 [ ("RSNormal", 'rsNormal)
159 , ("RSUnknown", 'rsUnknown)
160 , ("RSNoData", 'rsNodata)
161 , ("RSUnavailable", 'rsUnavail)
162 , ("RSOffline", 'rsOffline)
165 $(makeJSONInstance ''ResultStatus)
167 -- | Type holding the initial (unparsed) Luxi call.
168 data LuxiCall = LuxiCall LuxiReq JSValue
170 -- | Check that ResultStatus is success or fail with descriptive message.
171 checkRS :: (Monad m) => ResultStatus -> a -> m a
172 checkRS RSNormal val = return val
173 checkRS RSUnknown _ = fail "Unknown field"
174 checkRS RSNoData _ = fail "No data for a field"
175 checkRS RSUnavailable _ = fail "Ganeti reports unavailable data"
176 checkRS RSOffline _ = fail "Ganeti reports resource as offline"
178 -- | The end-of-message separator.
182 -- | Valid keys in the requests and responses.
183 data MsgKeys = Method
188 -- | The serialisation of MsgKeys into strings in messages.
189 $(genStrOfKey ''MsgKeys "strOfKey")
191 -- | Luxi client encapsulation.
192 data Client = Client { socket :: S.Socket -- ^ The socket of the client
193 , rbuf :: IORef String -- ^ Already received buffer
196 -- | Connects to the master daemon and returns a luxi Client.
197 getClient :: String -> IO Client
199 s <- S.socket S.AF_UNIX S.Stream S.defaultProtocol
200 withTimeout connTimeout "creating luxi connection" $
201 S.connect s (S.SockAddrUnix path)
203 return Client { socket=s, rbuf=rf}
205 -- | Closes the client socket.
206 closeClient :: Client -> IO ()
207 closeClient = S.sClose . socket
209 -- | Sends a message over a luxi transport.
210 sendMsg :: Client -> String -> IO ()
213 sbytes <- withTimeout queryTimeout
214 "sending luxi message" $
215 S.send (socket s) obuf
216 unless (sbytes == length obuf) $ _send (drop sbytes obuf)
217 in _send (buf ++ [eOM])
219 -- | Waits for a message over a luxi transport.
220 recvMsg :: Client -> IO String
223 nbuf <- withTimeout queryTimeout "reading luxi response" $
224 S.recv (socket s) 4096
225 let (msg, remaining) = break (eOM ==) nbuf
227 then _recv (obuf ++ msg)
228 else return (obuf ++ msg, tail remaining)
229 cbuf <- readIORef $ rbuf s
230 let (imsg, ibuf) = break (eOM ==) cbuf
232 if null ibuf -- if old buffer didn't contain a full message
233 then _recv cbuf -- then we read from network
234 else return (imsg, tail ibuf) -- else we return data from our buffer
235 writeIORef (rbuf s) nbuf
238 -- | Serialize a request to String.
239 buildCall :: LuxiOp -- ^ The method
240 -> String -- ^ The serialized form
242 let ja = [ (strOfKey Method, JSString $ toJSString $ strOfOp lo::JSValue)
243 , (strOfKey Args, opToArgs lo::JSValue)
248 -- | Check that luxi request contains the required keys and parse it.
249 validateCall :: String -> Result LuxiCall
251 arr <- fromJResult "luxi call" $ decodeStrict s::Result (JSObject JSValue)
252 let aobj = fromJSObject arr
253 call <- fromObj aobj (strOfKey Method)::Result LuxiReq
254 args <- fromObj aobj (strOfKey Args)
255 return (LuxiCall call args)
257 -- | Converts Luxi call arguments into a 'LuxiOp' data structure.
259 -- This is currently hand-coded until we make it more uniform so that
260 -- it can be generated using TH.
261 decodeCall :: LuxiCall -> Result LuxiOp
262 decodeCall (LuxiCall call args) =
265 (jid, jargs) <- fromJVal args
266 rid <- mapM (tryRead "parsing job ID" . fromJSString) jid
267 let rargs = map fromJSString jargs
268 return $ QueryJobs rid rargs
269 ReqQueryInstances -> do
270 (names, fields, locking) <- fromJVal args
271 return $ QueryInstances names fields locking
273 (names, fields, locking) <- fromJVal args
274 return $ QueryNodes names fields locking
276 (names, fields, locking) <- fromJVal args
277 return $ QueryGroups names fields locking
278 ReqQueryClusterInfo -> do
279 return QueryClusterInfo
282 fromJVal args::Result (QrViaLuxi, [String], JSValue)
283 return $ Query what fields ()
285 [ops1] <- fromJVal args
286 ops2 <- mapM (fromJResult (luxiReqToRaw call) . J.readJSON) ops1
287 return $ SubmitJob ops2
288 ReqSubmitManyJobs -> do
289 [ops1] <- fromJVal args
290 ops2 <- mapM (fromJResult (luxiReqToRaw call) . J.readJSON) ops1
291 return $ SubmitManyJobs ops2
292 ReqWaitForJobChange -> do
293 (jid, fields, pinfo, pidx, wtmout) <-
294 -- No instance for 5-tuple, code copied from the
295 -- json sources and adapted
296 fromJResult "Parsing WaitForJobChange message" $
298 JSArray [a, b, c, d, e] ->
305 _ -> J.Error "Not enough values"
306 rid <- tryRead "parsing job ID" jid
307 return $ WaitForJobChange rid fields pinfo pidx wtmout
309 [jid] <- fromJVal args
310 rid <- tryRead "parsing job ID" jid
311 return $ ArchiveJob rid
312 ReqAutoArchiveJobs -> do
313 (age, tmout) <- fromJVal args
314 return $ AutoArchiveJobs age tmout
315 ReqQueryExports -> do
316 (nodes, lock) <- fromJVal args
317 return $ QueryExports nodes lock
318 ReqQueryConfigValues -> do
319 [fields] <- fromJVal args
320 return $ QueryConfigValues fields
322 (kind, name) <- fromJVal args
323 return $ QueryTags kind name
325 [job] <- fromJVal args
326 rid <- tryRead "parsing job ID" job
327 return $ CancelJob rid
328 ReqSetDrainFlag -> do
329 [flag] <- fromJVal args
330 return $ SetDrainFlag flag
331 ReqSetWatcherPause -> do
332 [duration] <- fromJVal args
333 return $ SetWatcherPause duration
335 -- | Check that luxi responses contain the required keys and that the
336 -- call was successful.
337 validateResult :: String -> Result JSValue
338 validateResult s = do
339 oarr <- fromJResult "Parsing LUXI response"
340 (decodeStrict s)::Result (JSObject JSValue)
341 let arr = J.fromJSObject oarr
342 status <- fromObj arr (strOfKey Success)::Result Bool
343 let rkey = strOfKey Result
345 then fromObj arr rkey
346 else fromObj arr rkey >>= fail
348 -- | Generic luxi method call.
349 callMethod :: LuxiOp -> Client -> IO (Result JSValue)
350 callMethod method s = do
351 sendMsg s $ buildCall method
353 let rval = validateResult result
356 -- | Specialized submitManyJobs call.
357 submitManyJobs :: Client -> [[OpCode]] -> IO (Result [String])
358 submitManyJobs s jobs = do
359 rval <- callMethod (SubmitManyJobs jobs) s
360 -- map each result (status, payload) pair into a nice Result ADT
361 return $ case rval of
364 mapM (\v -> case v of
365 JSArray [JSBool True, JSString x] ->
367 JSArray [JSBool False, JSString x] ->
369 _ -> Bad "Unknown result from the master daemon"
371 x -> Bad ("Cannot parse response from Ganeti: " ++ show x)
373 -- | Custom queryJobs call.
374 queryJobsStatus :: Client -> [String] -> IO (Result [JobStatus])
375 queryJobsStatus s jids = do
376 rval <- callMethod (QueryJobs (map read jids) ["status"]) s
377 return $ case rval of
379 Ok y -> case J.readJSON y::(J.Result [[JobStatus]]) of
380 J.Ok vals -> if any null vals
381 then Bad "Missing job status field"
382 else Ok (map head vals)