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
44 import Text.JSON (encodeStrict, decodeStrict)
45 import qualified Text.JSON as J
46 import Text.JSON.Types
48 import qualified Network.Socket as S
50 import Ganeti.HTools.JSON
51 import Ganeti.HTools.Types
53 import Ganeti.Constants
54 import Ganeti.Jobs (JobStatus)
55 import Ganeti.OpCodes (OpCode)
58 -- * Utility functions
60 -- | Wrapper over System.Timeout.timeout that fails in the IO monad.
61 withTimeout :: Int -> String -> IO a -> IO a
62 withTimeout secs descr action = do
63 result <- timeout (secs * 1000000) action
65 Nothing -> fail $ "Timeout in " ++ descr
68 -- * Generic protocol functionality
70 $(declareSADT "QrViaLuxi"
72 , ("QRInstance", 'qrInstance)
74 , ("QRGroup", 'qrGroup)
77 $(makeJSONInstance ''QrViaLuxi)
79 -- | Currently supported Luxi operations and JSON serialization.
82 [ ("what", [t| QrViaLuxi |], [| id |])
83 , ("fields", [t| [String] |], [| id |])
84 , ("qfilter", [t| () |], [| const JSNull |])
87 [ ("names", [t| [String] |], [| id |])
88 , ("fields", [t| [String] |], [| id |])
89 , ("lock", [t| Bool |], [| id |])
91 , (luxiReqQueryGroups,
92 [ ("names", [t| [String] |], [| id |])
93 , ("fields", [t| [String] |], [| id |])
94 , ("lock", [t| Bool |], [| id |])
96 , (luxiReqQueryInstances,
97 [ ("names", [t| [String] |], [| id |])
98 , ("fields", [t| [String] |], [| id |])
99 , ("lock", [t| Bool |], [| id |])
102 [ ("ids", [t| [Int] |], [| map show |])
103 , ("fields", [t| [String] |], [| id |])
105 , (luxiReqQueryExports,
106 [ ("nodes", [t| [String] |], [| id |])
107 , ("lock", [t| Bool |], [| id |])
109 , (luxiReqQueryConfigValues,
110 [ ("fields", [t| [String] |], [| id |]) ]
112 , (luxiReqQueryClusterInfo, [])
114 [ ("kind", [t| String |], [| id |])
115 , ("name", [t| String |], [| id |])
118 [ ("job", [t| [OpCode] |], [| id |]) ]
120 , (luxiReqSubmitManyJobs,
121 [ ("ops", [t| [[OpCode]] |], [| id |]) ]
123 , (luxiReqWaitForJobChange,
124 [ ("job", [t| Int |], [| id |])
125 , ("fields", [t| [String]|], [| id |])
126 , ("prev_job", [t| JSValue |], [| id |])
127 , ("prev_log", [t| JSValue |], [| id |])
128 , ("tmout", [t| Int |], [| id |])
130 , (luxiReqArchiveJob,
131 [ ("job", [t| Int |], [| show |]) ]
133 , (luxiReqAutoArchiveJobs,
134 [ ("age", [t| Int |], [| id |])
135 , ("tmout", [t| Int |], [| id |])
138 [ ("job", [t| Int |], [| show |]) ]
140 , (luxiReqSetDrainFlag,
141 [ ("flag", [t| Bool |], [| id |]) ]
143 , (luxiReqSetWatcherPause,
144 [ ("duration", [t| Double |], [| id |]) ]
148 $(makeJSONInstance ''LuxiReq)
150 -- | The serialisation of LuxiOps into strings in messages.
151 $(genStrOfOp ''LuxiOp "strOfOp")
153 $(declareIADT "ResultStatus"
154 [ ("RSNormal", 'rsNormal)
155 , ("RSUnknown", 'rsUnknown)
156 , ("RSNoData", 'rsNodata)
157 , ("RSUnavailable", 'rsUnavail)
158 , ("RSOffline", 'rsOffline)
161 $(makeJSONInstance ''ResultStatus)
163 -- | Check that ResultStatus is success or fail with descriptive message.
164 checkRS :: (Monad m) => ResultStatus -> a -> m a
165 checkRS RSNormal val = return val
166 checkRS RSUnknown _ = fail "Unknown field"
167 checkRS RSNoData _ = fail "No data for a field"
168 checkRS RSUnavailable _ = fail "Ganeti reports unavailable data"
169 checkRS RSOffline _ = fail "Ganeti reports resource as offline"
171 -- | The end-of-message separator.
175 -- | Valid keys in the requests and responses.
176 data MsgKeys = Method
181 -- | The serialisation of MsgKeys into strings in messages.
182 $(genStrOfKey ''MsgKeys "strOfKey")
184 -- | Luxi client encapsulation.
185 data Client = Client { socket :: S.Socket -- ^ The socket of the client
186 , rbuf :: IORef String -- ^ Already received buffer
189 -- | Connects to the master daemon and returns a luxi Client.
190 getClient :: String -> IO Client
192 s <- S.socket S.AF_UNIX S.Stream S.defaultProtocol
193 withTimeout connTimeout "creating luxi connection" $
194 S.connect s (S.SockAddrUnix path)
196 return Client { socket=s, rbuf=rf}
198 -- | Closes the client socket.
199 closeClient :: Client -> IO ()
200 closeClient = S.sClose . socket
202 -- | Sends a message over a luxi transport.
203 sendMsg :: Client -> String -> IO ()
206 sbytes <- withTimeout queryTimeout
207 "sending luxi message" $
208 S.send (socket s) obuf
209 unless (sbytes == length obuf) $ _send (drop sbytes obuf)
210 in _send (buf ++ [eOM])
212 -- | Waits for a message over a luxi transport.
213 recvMsg :: Client -> IO String
216 nbuf <- withTimeout queryTimeout "reading luxi response" $
217 S.recv (socket s) 4096
218 let (msg, remaining) = break (eOM ==) nbuf
220 then _recv (obuf ++ msg)
221 else return (obuf ++ msg, tail remaining)
222 cbuf <- readIORef $ rbuf s
223 let (imsg, ibuf) = break (eOM ==) cbuf
225 if null ibuf -- if old buffer didn't contain a full message
226 then _recv cbuf -- then we read from network
227 else return (imsg, tail ibuf) -- else we return data from our buffer
228 writeIORef (rbuf s) nbuf
231 -- | Serialize a request to String.
232 buildCall :: LuxiOp -- ^ The method
233 -> String -- ^ The serialized form
235 let ja = [ (strOfKey Method, JSString $ toJSString $ strOfOp lo::JSValue)
236 , (strOfKey Args, opToArgs lo::JSValue)
241 -- | Check that luxi responses contain the required keys and that the
242 -- call was successful.
243 validateResult :: String -> Result JSValue
244 validateResult s = do
245 oarr <- fromJResult "Parsing LUXI response"
246 (decodeStrict s)::Result (JSObject JSValue)
247 let arr = J.fromJSObject oarr
248 status <- fromObj arr (strOfKey Success)::Result Bool
249 let rkey = strOfKey Result
251 then fromObj arr rkey
252 else fromObj arr rkey >>= fail
254 -- | Generic luxi method call.
255 callMethod :: LuxiOp -> Client -> IO (Result JSValue)
256 callMethod method s = do
257 sendMsg s $ buildCall method
259 let rval = validateResult result
262 -- | Specialized submitManyJobs call.
263 submitManyJobs :: Client -> [[OpCode]] -> IO (Result [String])
264 submitManyJobs s jobs = do
265 rval <- callMethod (SubmitManyJobs jobs) s
266 -- map each result (status, payload) pair into a nice Result ADT
267 return $ case rval of
270 mapM (\v -> case v of
271 JSArray [JSBool True, JSString x] ->
273 JSArray [JSBool False, JSString x] ->
275 _ -> Bad "Unknown result from the master daemon"
277 x -> Bad ("Cannot parse response from Ganeti: " ++ show x)
279 -- | Custom queryJobs call.
280 queryJobsStatus :: Client -> [String] -> IO (Result [JobStatus])
281 queryJobsStatus s jids = do
282 rval <- callMethod (QueryJobs (map read jids) ["status"]) s
283 return $ case rval of
285 Ok y -> case J.readJSON y::(J.Result [[JobStatus]]) of
286 J.Ok vals -> if any null vals
287 then Bad "Missing job status field"
288 else Ok (map head vals)