root / htools / Ganeti / Luxi.hs @ fae980e5
History | View | Annotate | Download (8.8 kB)
1 |
{-# LANGUAGE TemplateHaskell #-} |
---|---|
2 |
|
3 |
{-| Implementation of the Ganeti LUXI interface. |
4 |
|
5 |
-} |
6 |
|
7 |
{- |
8 |
|
9 |
Copyright (C) 2009, 2010, 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.Luxi |
29 |
( LuxiOp(..) |
30 |
, QrViaLuxi(..) |
31 |
, ResultStatus(..) |
32 |
, Client |
33 |
, checkRS |
34 |
, getClient |
35 |
, closeClient |
36 |
, callMethod |
37 |
, submitManyJobs |
38 |
, queryJobsStatus |
39 |
) where |
40 |
|
41 |
import Data.IORef |
42 |
import Control.Monad |
43 |
import Text.JSON (encodeStrict, decodeStrict) |
44 |
import qualified Text.JSON as J |
45 |
import Text.JSON.Types |
46 |
import System.Timeout |
47 |
import qualified Network.Socket as S |
48 |
|
49 |
import Ganeti.HTools.JSON |
50 |
import Ganeti.HTools.Types |
51 |
|
52 |
import Ganeti.Constants |
53 |
import Ganeti.Jobs (JobStatus) |
54 |
import Ganeti.OpCodes (OpCode) |
55 |
import Ganeti.THH |
56 |
|
57 |
-- * Utility functions |
58 |
|
59 |
-- | Wrapper over System.Timeout.timeout that fails in the IO monad. |
60 |
withTimeout :: Int -> String -> IO a -> IO a |
61 |
withTimeout secs descr action = do |
62 |
result <- timeout (secs * 1000000) action |
63 |
case result of |
64 |
Nothing -> fail $ "Timeout in " ++ descr |
65 |
Just v -> return v |
66 |
|
67 |
-- * Generic protocol functionality |
68 |
|
69 |
$(declareSADT "QrViaLuxi" |
70 |
[ ("QRLock", 'qrLock) |
71 |
, ("QRInstance", 'qrInstance) |
72 |
, ("QRNode", 'qrNode) |
73 |
, ("QRGroup", 'qrGroup) |
74 |
, ("QROs", 'qrOs) |
75 |
]) |
76 |
$(makeJSONInstance ''QrViaLuxi) |
77 |
|
78 |
-- | Currently supported Luxi operations and JSON serialization. |
79 |
$(genLuxiOp "LuxiOp" |
80 |
[(luxiReqQuery, |
81 |
[ ("what", [t| QrViaLuxi |], [| id |]) |
82 |
, ("fields", [t| [String] |], [| id |]) |
83 |
, ("qfilter", [t| () |], [| const JSNull |]) |
84 |
]) |
85 |
, (luxiReqQueryNodes, |
86 |
[ ("names", [t| [String] |], [| id |]) |
87 |
, ("fields", [t| [String] |], [| id |]) |
88 |
, ("lock", [t| Bool |], [| id |]) |
89 |
]) |
90 |
, (luxiReqQueryGroups, |
91 |
[ ("names", [t| [String] |], [| id |]) |
92 |
, ("fields", [t| [String] |], [| id |]) |
93 |
, ("lock", [t| Bool |], [| id |]) |
94 |
]) |
95 |
, (luxiReqQueryInstances, |
96 |
[ ("names", [t| [String] |], [| id |]) |
97 |
, ("fields", [t| [String] |], [| id |]) |
98 |
, ("lock", [t| Bool |], [| id |]) |
99 |
]) |
100 |
, (luxiReqQueryJobs, |
101 |
[ ("ids", [t| [Int] |], [| map show |]) |
102 |
, ("fields", [t| [String] |], [| id |]) |
103 |
]) |
104 |
, (luxiReqQueryExports, |
105 |
[ ("nodes", [t| [String] |], [| id |]) |
106 |
, ("lock", [t| Bool |], [| id |]) |
107 |
]) |
108 |
, (luxiReqQueryConfigValues, |
109 |
[ ("fields", [t| [String] |], [| id |]) ] |
110 |
) |
111 |
, (luxiReqQueryClusterInfo, []) |
112 |
, (luxiReqQueryTags, |
113 |
[ ("kind", [t| String |], [| id |]) |
114 |
, ("name", [t| String |], [| id |]) |
115 |
]) |
116 |
, (luxiReqSubmitJob, |
117 |
[ ("job", [t| [OpCode] |], [| id |]) ] |
118 |
) |
119 |
, (luxiReqSubmitManyJobs, |
120 |
[ ("ops", [t| [[OpCode]] |], [| id |]) ] |
121 |
) |
122 |
, (luxiReqWaitForJobChange, |
123 |
[ ("job", [t| Int |], [| id |]) |
124 |
, ("fields", [t| [String]|], [| id |]) |
125 |
, ("prev_job", [t| JSValue |], [| id |]) |
126 |
, ("prev_log", [t| JSValue |], [| id |]) |
127 |
, ("tmout", [t| Int |], [| id |]) |
128 |
]) |
129 |
, (luxiReqArchiveJob, |
130 |
[ ("job", [t| Int |], [| show |]) ] |
131 |
) |
132 |
, (luxiReqAutoArchiveJobs, |
133 |
[ ("age", [t| Int |], [| id |]) |
134 |
, ("tmout", [t| Int |], [| id |]) |
135 |
]) |
136 |
, (luxiReqCancelJob, |
137 |
[ ("job", [t| Int |], [| show |]) ] |
138 |
) |
139 |
, (luxiReqSetDrainFlag, |
140 |
[ ("flag", [t| Bool |], [| id |]) ] |
141 |
) |
142 |
, (luxiReqSetWatcherPause, |
143 |
[ ("duration", [t| Double |], [| id |]) ] |
144 |
) |
145 |
]) |
146 |
|
147 |
-- | The serialisation of LuxiOps into strings in messages. |
148 |
$(genStrOfOp ''LuxiOp "strOfOp") |
149 |
|
150 |
$(declareIADT "ResultStatus" |
151 |
[ ("RSNormal", 'rsNormal) |
152 |
, ("RSUnknown", 'rsUnknown) |
153 |
, ("RSNoData", 'rsNodata) |
154 |
, ("RSUnavailable", 'rsUnavail) |
155 |
, ("RSOffline", 'rsOffline) |
156 |
]) |
157 |
|
158 |
$(makeJSONInstance ''ResultStatus) |
159 |
|
160 |
-- | Check that ResultStatus is success or fail with descriptive message. |
161 |
checkRS :: (Monad m) => ResultStatus -> a -> m a |
162 |
checkRS RSNormal val = return val |
163 |
checkRS RSUnknown _ = fail "Unknown field" |
164 |
checkRS RSNoData _ = fail "No data for a field" |
165 |
checkRS RSUnavailable _ = fail "Ganeti reports unavailable data" |
166 |
checkRS RSOffline _ = fail "Ganeti reports resource as offline" |
167 |
|
168 |
-- | The end-of-message separator. |
169 |
eOM :: Char |
170 |
eOM = '\3' |
171 |
|
172 |
-- | Valid keys in the requests and responses. |
173 |
data MsgKeys = Method |
174 |
| Args |
175 |
| Success |
176 |
| Result |
177 |
|
178 |
-- | The serialisation of MsgKeys into strings in messages. |
179 |
$(genStrOfKey ''MsgKeys "strOfKey") |
180 |
|
181 |
-- | Luxi client encapsulation. |
182 |
data Client = Client { socket :: S.Socket -- ^ The socket of the client |
183 |
, rbuf :: IORef String -- ^ Already received buffer |
184 |
} |
185 |
|
186 |
-- | Connects to the master daemon and returns a luxi Client. |
187 |
getClient :: String -> IO Client |
188 |
getClient path = do |
189 |
s <- S.socket S.AF_UNIX S.Stream S.defaultProtocol |
190 |
withTimeout connTimeout "creating luxi connection" $ |
191 |
S.connect s (S.SockAddrUnix path) |
192 |
rf <- newIORef "" |
193 |
return Client { socket=s, rbuf=rf} |
194 |
|
195 |
-- | Closes the client socket. |
196 |
closeClient :: Client -> IO () |
197 |
closeClient = S.sClose . socket |
198 |
|
199 |
-- | Sends a message over a luxi transport. |
200 |
sendMsg :: Client -> String -> IO () |
201 |
sendMsg s buf = |
202 |
let _send obuf = do |
203 |
sbytes <- withTimeout queryTimeout |
204 |
"sending luxi message" $ |
205 |
S.send (socket s) obuf |
206 |
unless (sbytes == length obuf) $ _send (drop sbytes obuf) |
207 |
in _send (buf ++ [eOM]) |
208 |
|
209 |
-- | Waits for a message over a luxi transport. |
210 |
recvMsg :: Client -> IO String |
211 |
recvMsg s = do |
212 |
let _recv obuf = do |
213 |
nbuf <- withTimeout queryTimeout "reading luxi response" $ |
214 |
S.recv (socket s) 4096 |
215 |
let (msg, remaining) = break (eOM ==) nbuf |
216 |
if null remaining |
217 |
then _recv (obuf ++ msg) |
218 |
else return (obuf ++ msg, tail remaining) |
219 |
cbuf <- readIORef $ rbuf s |
220 |
let (imsg, ibuf) = break (eOM ==) cbuf |
221 |
(msg, nbuf) <- |
222 |
if null ibuf -- if old buffer didn't contain a full message |
223 |
then _recv cbuf -- then we read from network |
224 |
else return (imsg, tail ibuf) -- else we return data from our buffer |
225 |
writeIORef (rbuf s) nbuf |
226 |
return msg |
227 |
|
228 |
-- | Serialize a request to String. |
229 |
buildCall :: LuxiOp -- ^ The method |
230 |
-> String -- ^ The serialized form |
231 |
buildCall lo = |
232 |
let ja = [ (strOfKey Method, JSString $ toJSString $ strOfOp lo::JSValue) |
233 |
, (strOfKey Args, opToArgs lo::JSValue) |
234 |
] |
235 |
jo = toJSObject ja |
236 |
in encodeStrict jo |
237 |
|
238 |
-- | Check that luxi responses contain the required keys and that the |
239 |
-- call was successful. |
240 |
validateResult :: String -> Result JSValue |
241 |
validateResult s = do |
242 |
oarr <- fromJResult "Parsing LUXI response" |
243 |
(decodeStrict s)::Result (JSObject JSValue) |
244 |
let arr = J.fromJSObject oarr |
245 |
status <- fromObj arr (strOfKey Success)::Result Bool |
246 |
let rkey = strOfKey Result |
247 |
if status |
248 |
then fromObj arr rkey |
249 |
else fromObj arr rkey >>= fail |
250 |
|
251 |
-- | Generic luxi method call. |
252 |
callMethod :: LuxiOp -> Client -> IO (Result JSValue) |
253 |
callMethod method s = do |
254 |
sendMsg s $ buildCall method |
255 |
result <- recvMsg s |
256 |
let rval = validateResult result |
257 |
return rval |
258 |
|
259 |
-- | Specialized submitManyJobs call. |
260 |
submitManyJobs :: Client -> [[OpCode]] -> IO (Result [String]) |
261 |
submitManyJobs s jobs = do |
262 |
rval <- callMethod (SubmitManyJobs jobs) s |
263 |
-- map each result (status, payload) pair into a nice Result ADT |
264 |
return $ case rval of |
265 |
Bad x -> Bad x |
266 |
Ok (JSArray r) -> |
267 |
mapM (\v -> case v of |
268 |
JSArray [JSBool True, JSString x] -> |
269 |
Ok (fromJSString x) |
270 |
JSArray [JSBool False, JSString x] -> |
271 |
Bad (fromJSString x) |
272 |
_ -> Bad "Unknown result from the master daemon" |
273 |
) r |
274 |
x -> Bad ("Cannot parse response from Ganeti: " ++ show x) |
275 |
|
276 |
-- | Custom queryJobs call. |
277 |
queryJobsStatus :: Client -> [String] -> IO (Result [JobStatus]) |
278 |
queryJobsStatus s jids = do |
279 |
rval <- callMethod (QueryJobs (map read jids) ["status"]) s |
280 |
return $ case rval of |
281 |
Bad x -> Bad x |
282 |
Ok y -> case J.readJSON y::(J.Result [[JobStatus]]) of |
283 |
J.Ok vals -> if any null vals |
284 |
then Bad "Missing job status field" |
285 |
else Ok (map head vals) |
286 |
J.Error x -> Bad x |