root / Ganeti / HTools / Luxi.hs @ 175cc337
History | View | Annotate | Download (8.9 kB)
1 |
{-| Implementation of the LUXI client interface. |
---|---|
2 |
|
3 |
-} |
4 |
|
5 |
{- |
6 |
|
7 |
Copyright (C) 2009 Google Inc. |
8 |
|
9 |
This program is free software; you can redistribute it and/or modify |
10 |
it under the terms of the GNU General Public License as published by |
11 |
the Free Software Foundation; either version 2 of the License, or |
12 |
(at your option) any later version. |
13 |
|
14 |
This program is distributed in the hope that it will be useful, but |
15 |
WITHOUT ANY WARRANTY; without even the implied warranty of |
16 |
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
17 |
General Public License for more details. |
18 |
|
19 |
You should have received a copy of the GNU General Public License |
20 |
along with this program; if not, write to the Free Software |
21 |
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA |
22 |
02110-1301, USA. |
23 |
|
24 |
-} |
25 |
|
26 |
module Ganeti.HTools.Luxi |
27 |
( |
28 |
loadData |
29 |
) where |
30 |
|
31 |
import Data.List |
32 |
import Data.IORef |
33 |
import qualified Control.Exception as E |
34 |
import Control.Monad |
35 |
import Text.JSON (JSObject, JSValue, toJSObject, encodeStrict |
36 |
, decodeStrict, readJSON, JSON) |
37 |
import qualified Text.JSON as J |
38 |
import Text.JSON.Types |
39 |
import System.Timeout |
40 |
import qualified Network.Socket as S |
41 |
|
42 |
import Ganeti.HTools.Utils |
43 |
import Ganeti.HTools.Loader |
44 |
import Ganeti.HTools.Types |
45 |
import qualified Ganeti.HTools.Node as Node |
46 |
import qualified Ganeti.HTools.Instance as Instance |
47 |
|
48 |
-- * Utility functions |
49 |
|
50 |
-- | Small wrapper over readJSON. |
51 |
fromJVal :: (Monad m, JSON a) => JSValue -> m a |
52 |
fromJVal v = |
53 |
case readJSON v of |
54 |
J.Error s -> fail ("Cannot convert value " ++ show v ++ ", error: " ++ s) |
55 |
J.Ok x -> return x |
56 |
|
57 |
-- | Ensure a given JSValue is actually a JSArray. |
58 |
toArray :: (Monad m) => JSValue -> m [JSValue] |
59 |
toArray v = |
60 |
case v of |
61 |
JSArray arr -> return arr |
62 |
o -> fail ("Invalid input, expected array but got " ++ show o) |
63 |
|
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 |
68 |
(case result of |
69 |
Nothing -> fail $ "Timeout in " ++ descr |
70 |
Just v -> return v) |
71 |
|
72 |
-- * Generic protocol functionality |
73 |
|
74 |
-- | Currently supported Luxi operations. |
75 |
data LuxiOp = QueryInstances |
76 |
| QueryNodes |
77 |
|
78 |
-- | The serialisation of LuxiOps into strings in messages. |
79 |
strOfOp :: LuxiOp -> String |
80 |
strOfOp QueryNodes = "QueryNodes" |
81 |
strOfOp QueryInstances = "QueryInstances" |
82 |
|
83 |
-- | The end-of-message separator. |
84 |
eOM :: Char |
85 |
eOM = '\3' |
86 |
|
87 |
-- | Valid keys in the requests and responses. |
88 |
data MsgKeys = Method |
89 |
| Args |
90 |
| Success |
91 |
| Result |
92 |
|
93 |
-- | The serialisation of MsgKeys into strings in messages. |
94 |
strOfKey :: MsgKeys -> String |
95 |
strOfKey Method = "method" |
96 |
strOfKey Args = "args" |
97 |
strOfKey Success = "success" |
98 |
strOfKey Result = "result" |
99 |
|
100 |
-- | Luxi client encapsulation. |
101 |
data Client = Client { socket :: S.Socket -- ^ The socket of the client |
102 |
, rbuf :: IORef String -- ^ Already received buffer |
103 |
} |
104 |
|
105 |
-- | Connects to the master daemon and returns a luxi Client. |
106 |
getClient :: String -> IO Client |
107 |
getClient path = do |
108 |
s <- S.socket S.AF_UNIX S.Stream S.defaultProtocol |
109 |
withTimeout connTimeout "creating luxi connection" $ |
110 |
S.connect s (S.SockAddrUnix path) |
111 |
rf <- newIORef "" |
112 |
return Client { socket=s, rbuf=rf} |
113 |
|
114 |
-- | Closes the client socket. |
115 |
closeClient :: Client -> IO () |
116 |
closeClient = S.sClose . socket |
117 |
|
118 |
-- | Sends a message over a luxi transport. |
119 |
sendMsg :: Client -> String -> IO () |
120 |
sendMsg s buf = |
121 |
let _send obuf = do |
122 |
sbytes <- withTimeout queryTimeout |
123 |
"sending luxi message" $ |
124 |
S.send (socket s) obuf |
125 |
(if sbytes == length obuf |
126 |
then return () |
127 |
else _send (drop sbytes obuf)) |
128 |
in _send (buf ++ [eOM]) |
129 |
|
130 |
-- | Waits for a message over a luxi transport. |
131 |
recvMsg :: Client -> IO String |
132 |
recvMsg s = do |
133 |
let _recv obuf = do |
134 |
nbuf <- withTimeout queryTimeout "reading luxi response" $ |
135 |
S.recv (socket s) 4096 |
136 |
let (msg, rbuf) = break ((==) eOM) (obuf ++ nbuf) |
137 |
(if null msg |
138 |
then _recv rbuf |
139 |
else return (msg, drop 1 rbuf)) |
140 |
cbuf <- readIORef $ rbuf s |
141 |
(msg, nbuf) <- _recv cbuf |
142 |
writeIORef (rbuf s) nbuf |
143 |
return msg |
144 |
|
145 |
-- | Serialize a request to String. |
146 |
buildCall :: LuxiOp -- ^ The method |
147 |
-> JSValue -- ^ The arguments |
148 |
-> String -- ^ The serialized form |
149 |
buildCall msg args = |
150 |
let ja = [(strOfKey Method, |
151 |
JSString $ toJSString $ strOfOp msg::JSValue), |
152 |
(strOfKey Args, |
153 |
args::JSValue) |
154 |
] |
155 |
jo = toJSObject ja |
156 |
in encodeStrict jo |
157 |
|
158 |
-- | Check that luxi responses contain the required keys and that the |
159 |
-- call was successful. |
160 |
validateResult :: String -> Result JSValue |
161 |
validateResult s = do |
162 |
arr <- fromJResult $ decodeStrict s::Result (JSObject JSValue) |
163 |
status <- fromObj (strOfKey Success) arr::Result Bool |
164 |
let rkey = strOfKey Result |
165 |
(if status |
166 |
then fromObj rkey arr |
167 |
else fromObj rkey arr >>= fail) |
168 |
|
169 |
-- | Generic luxi method call. |
170 |
callMethod :: LuxiOp -> JSValue -> Client -> IO (Result JSValue) |
171 |
callMethod method args s = do |
172 |
sendMsg s $ buildCall method args |
173 |
result <- recvMsg s |
174 |
let rval = validateResult result |
175 |
return rval |
176 |
|
177 |
-- * Data querying functionality |
178 |
|
179 |
-- | The input data for node query. |
180 |
queryNodesMsg :: JSValue |
181 |
queryNodesMsg = |
182 |
let nnames = JSArray [] |
183 |
fnames = ["name", |
184 |
"mtotal", "mnode", "mfree", |
185 |
"dtotal", "dfree", |
186 |
"ctotal", |
187 |
"offline", "drained"] |
188 |
fields = JSArray $ map (JSString . toJSString) fnames |
189 |
use_locking = JSBool False |
190 |
in JSArray [nnames, fields, use_locking] |
191 |
|
192 |
-- | The input data for instance query. |
193 |
queryInstancesMsg :: JSValue |
194 |
queryInstancesMsg = |
195 |
let nnames = JSArray [] |
196 |
fnames = ["name", |
197 |
"disk_usage", "be/memory", "be/vcpus", |
198 |
"status", "pnode", "snodes"] |
199 |
fields = JSArray $ map (JSString . toJSString) fnames |
200 |
use_locking = JSBool False |
201 |
in JSArray [nnames, fields, use_locking] |
202 |
|
203 |
|
204 |
-- | Wraper over callMethod doing node query. |
205 |
queryNodes :: Client -> IO (Result JSValue) |
206 |
queryNodes = callMethod QueryNodes queryNodesMsg |
207 |
|
208 |
-- | Wraper over callMethod doing instance query. |
209 |
queryInstances :: Client -> IO (Result JSValue) |
210 |
queryInstances = callMethod QueryInstances queryInstancesMsg |
211 |
|
212 |
-- | Parse a instance list in JSON format. |
213 |
getInstances :: NameAssoc |
214 |
-> JSValue |
215 |
-> Result [(String, Instance.Instance)] |
216 |
getInstances ktn arr = toArray arr >>= mapM (parseInstance ktn) |
217 |
|
218 |
-- | Construct an instance from a JSON object. |
219 |
parseInstance :: [(String, Ndx)] |
220 |
-> JSValue |
221 |
-> Result (String, Instance.Instance) |
222 |
parseInstance ktn (JSArray (name:disk:mem:vcpus:status:pnode:snodes:[])) = do |
223 |
xname <- fromJVal name |
224 |
xdisk <- fromJVal disk |
225 |
xmem <- fromJVal mem |
226 |
xvcpus <- fromJVal vcpus |
227 |
xpnode <- fromJVal pnode >>= lookupNode ktn xname |
228 |
xsnodes <- fromJVal snodes::Result [JSString] |
229 |
snode <- (if null xsnodes then return Node.noSecondary |
230 |
else lookupNode ktn xname (fromJSString $ head xsnodes)) |
231 |
xrunning <- fromJVal status |
232 |
let inst = Instance.create xname xmem xdisk xvcpus xrunning xpnode snode |
233 |
return (xname, inst) |
234 |
|
235 |
parseInstance _ v = fail ("Invalid instance query result: " ++ show v) |
236 |
|
237 |
-- | Parse a node list in JSON format. |
238 |
getNodes :: JSValue -> Result [(String, Node.Node)] |
239 |
getNodes arr = toArray arr >>= mapM parseNode |
240 |
|
241 |
-- | Construct a node from a JSON object. |
242 |
parseNode :: JSValue -> Result (String, Node.Node) |
243 |
parseNode (JSArray |
244 |
(name:mtotal:mnode:mfree:dtotal:dfree:ctotal:offline:drained:[])) |
245 |
= do |
246 |
xname <- fromJVal name |
247 |
xoffline <- fromJVal offline |
248 |
node <- (if xoffline |
249 |
then return $ Node.create xname 0 0 0 0 0 0 True |
250 |
else do |
251 |
xdrained <- fromJVal drained |
252 |
xmtotal <- fromJVal mtotal |
253 |
xmnode <- fromJVal mnode |
254 |
xmfree <- fromJVal mfree |
255 |
xdtotal <- fromJVal dtotal |
256 |
xdfree <- fromJVal dfree |
257 |
xctotal <- fromJVal ctotal |
258 |
return $ Node.create xname xmtotal xmnode xmfree |
259 |
xdtotal xdfree xctotal (xoffline || xdrained)) |
260 |
return (xname, node) |
261 |
|
262 |
parseNode v = fail ("Invalid node query result: " ++ show v) |
263 |
|
264 |
-- * Main loader functionality |
265 |
|
266 |
-- | Builds the cluster data from an URL. |
267 |
loadData :: String -- ^ Unix socket to use as source |
268 |
-> IO (Result (Node.AssocList, Instance.AssocList)) |
269 |
loadData master = |
270 |
E.bracket |
271 |
(getClient master) |
272 |
closeClient |
273 |
(\s -> do |
274 |
nodes <- queryNodes s |
275 |
instances <- queryInstances s |
276 |
return $ do -- Result monad |
277 |
node_data <- nodes >>= getNodes |
278 |
let (node_names, node_idx) = assignIndices node_data |
279 |
inst_data <- instances >>= getInstances node_names |
280 |
let (_, inst_idx) = assignIndices inst_data |
281 |
return (node_idx, inst_idx) |
282 |
) |