1 {-| Implementation of the iallocator interface.
7 Copyright (C) 2009, 2010, 2011, 2012 Google Inc.
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.
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.
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
26 module Ganeti.HTools.IAlloc
34 import Data.Maybe (fromMaybe, isJust, fromJust)
37 import Text.JSON (JSObject, JSValue(JSArray),
38 makeObj, encodeStrict, decodeStrict, fromJSObject, showJSON)
42 import Ganeti.BasicTypes
43 import qualified Ganeti.HTools.Cluster as Cluster
44 import qualified Ganeti.HTools.Container as Container
45 import qualified Ganeti.HTools.Group as Group
46 import qualified Ganeti.HTools.Node as Node
47 import qualified Ganeti.HTools.Instance as Instance
48 import qualified Ganeti.Constants as C
49 import Ganeti.HTools.CLI
50 import Ganeti.HTools.Loader
51 import Ganeti.HTools.Types
54 {-# ANN module "HLint: ignore Eta reduce" #-}
56 -- | Type alias for the result of an IAllocator call.
57 type IAllocResult = (String, JSValue, Node.List, Instance.List)
59 -- | Parse the basic specifications of an instance.
61 -- Instances in the cluster instance list and the instance in an
62 -- 'Allocate' request share some common properties, which are read by
64 parseBaseInstance :: String
66 -> Result (String, Instance.Instance)
67 parseBaseInstance n a = do
68 let extract x = tryFromObj ("invalid data for instance '" ++ n ++ "'") a x
69 disk <- extract "disk_space_total"
70 mem <- extract "memory"
71 vcpus <- extract "vcpus"
72 tags <- extract "tags"
73 dt <- extract "disk_template"
74 su <- extract "spindle_use"
75 return (n, Instance.create n mem disk vcpus Running tags True 0 0 dt su)
77 -- | Parses an instance as found in the cluster instance list.
78 parseInstance :: NameAssoc -- ^ The node name-to-index association list
79 -> String -- ^ The name of the instance
80 -> JSRecord -- ^ The JSON object
81 -> Result (String, Instance.Instance)
82 parseInstance ktn n a = do
83 base <- parseBaseInstance n a
84 nodes <- fromObj a "nodes"
85 pnode <- if null nodes
86 then Bad $ "empty node list for instance " ++ n
87 else readEitherString $ head nodes
88 pidx <- lookupNode ktn n pnode
89 let snodes = tail nodes
90 sidx <- if null snodes
91 then return Node.noSecondary
92 else readEitherString (head snodes) >>= lookupNode ktn n
93 return (n, Instance.setBoth (snd base) pidx sidx)
95 -- | Parses a node as found in the cluster node list.
96 parseNode :: NameAssoc -- ^ The group association
97 -> String -- ^ The node's name
98 -> JSRecord -- ^ The JSON object
99 -> Result (String, Node.Node)
100 parseNode ktg n a = do
101 let desc = "invalid data for node '" ++ n ++ "'"
102 extract x = tryFromObj desc a x
103 offline <- extract "offline"
104 drained <- extract "drained"
105 guuid <- extract "group"
106 vm_capable <- annotateResult desc $ maybeFromObj a "vm_capable"
107 let vm_capable' = fromMaybe True vm_capable
108 gidx <- lookupGroup ktg n guuid
109 node <- if offline || drained || not vm_capable'
110 then return $ Node.create n 0 0 0 0 0 0 True 0 gidx
112 mtotal <- extract "total_memory"
113 mnode <- extract "reserved_memory"
114 mfree <- extract "free_memory"
115 dtotal <- extract "total_disk"
116 dfree <- extract "free_disk"
117 ctotal <- extract "total_cpus"
118 ndparams <- extract "ndparams" >>= asJSObject
119 spindles <- tryFromObj desc (fromJSObject ndparams)
121 return $ Node.create n mtotal mnode mfree
122 dtotal dfree ctotal False spindles gidx
125 -- | Parses a group as found in the cluster group list.
126 parseGroup :: String -- ^ The group UUID
127 -> JSRecord -- ^ The JSON object
128 -> Result (String, Group.Group)
130 let extract x = tryFromObj ("invalid data for group '" ++ u ++ "'") a x
131 name <- extract "name"
132 apol <- extract "alloc_policy"
133 ipol <- extract "ipolicy"
134 return (u, Group.create name u apol ipol)
136 -- | Top-level parser.
138 -- The result is a tuple of eventual warning messages and the parsed
139 -- request; if parsing the input data fails, we'll return a 'Bad'
141 parseData :: String -- ^ The JSON message as received from Ganeti
142 -> Result ([String], Request) -- ^ Result tuple
144 decoded <- fromJResult "Parsing input IAllocator message" (decodeStrict body)
145 let obj = fromJSObject decoded
146 extrObj x = tryFromObj "invalid iallocator message" obj x
148 request <- liftM fromJSObject (extrObj "request")
149 let extrFromReq r x = tryFromObj "invalid request dict" r x
150 let extrReq x = extrFromReq request x
151 -- existing group parsing
152 glist <- liftM fromJSObject (extrObj "nodegroups")
153 gobj <- mapM (\(x, y) -> asJSObject y >>= parseGroup x . fromJSObject) glist
154 let (ktg, gl) = assignIndices gobj
155 -- existing node parsing
156 nlist <- liftM fromJSObject (extrObj "nodes")
157 nobj <- mapM (\(x,y) ->
158 asJSObject y >>= parseNode ktg x . fromJSObject) nlist
159 let (ktn, nl) = assignIndices nobj
160 -- existing instance parsing
161 ilist <- extrObj "instances"
162 let idata = fromJSObject ilist
163 iobj <- mapM (\(x,y) ->
164 asJSObject y >>= parseInstance ktn x . fromJSObject) idata
165 let (kti, il) = assignIndices iobj
167 ctags <- extrObj "cluster_tags"
168 cdata1 <- mergeData [] [] [] [] (ClusterData gl nl il ctags defIPolicy)
169 let (msgs, fix_nl) = checkData (cdNodes cdata1) (cdInstances cdata1)
170 cdata = cdata1 { cdNodes = fix_nl }
171 map_n = cdNodes cdata
172 map_i = cdInstances cdata
173 map_g = cdGroups cdata
174 optype <- extrReq "type"
177 _ | optype == C.iallocatorModeAlloc ->
179 rname <- extrReq "name"
180 req_nodes <- extrReq "required_nodes"
181 inew <- parseBaseInstance rname request
183 return $ Allocate io req_nodes
184 | optype == C.iallocatorModeReloc ->
186 rname <- extrReq "name"
187 ridx <- lookupInstance kti rname
188 req_nodes <- extrReq "required_nodes"
189 ex_nodes <- extrReq "relocate_from"
190 ex_idex <- mapM (Container.findByName map_n) ex_nodes
191 return $ Relocate ridx req_nodes (map Node.idx ex_idex)
192 | optype == C.iallocatorModeChgGroup ->
194 rl_names <- extrReq "instances"
195 rl_insts <- mapM (liftM Instance.idx .
196 Container.findByName map_i) rl_names
197 gr_uuids <- extrReq "target_groups"
198 gr_idxes <- mapM (liftM Group.idx .
199 Container.findByName map_g) gr_uuids
200 return $ ChangeGroup rl_insts gr_idxes
201 | optype == C.iallocatorModeNodeEvac ->
203 rl_names <- extrReq "instances"
204 rl_insts <- mapM (Container.findByName map_i) rl_names
205 let rl_idx = map Instance.idx rl_insts
206 rl_mode <- extrReq "evac_mode"
207 return $ NodeEvacuate rl_idx rl_mode
208 | optype == C.iallocatorModeMultiAlloc ->
210 arry <- extrReq "instances" :: Result [JSObject JSValue]
211 let inst_reqs = map fromJSObject arry
214 rname <- extrFromReq r "name"
215 req_nodes <- extrFromReq r "required_nodes"
216 inew <- parseBaseInstance rname r
218 return (io, req_nodes)) inst_reqs
219 return $ MultiAllocate prqs
220 | otherwise -> fail ("Invalid request type '" ++ optype ++ "'")
221 return (msgs, Request rqtype cdata)
223 -- | Formats the result into a valid IAllocator response message.
224 formatResponse :: Bool -- ^ Whether the request was successful
225 -> String -- ^ Information text
226 -> JSValue -- ^ The JSON encoded result
227 -> String -- ^ The full JSON-formatted message
228 formatResponse success info result =
229 let e_success = ("success", showJSON success)
230 e_info = ("info", showJSON info)
231 e_result = ("result", result)
232 in encodeStrict $ makeObj [e_success, e_info, e_result]
234 -- | Flatten the log of a solution into a string.
235 describeSolution :: Cluster.AllocSolution -> String
236 describeSolution = intercalate ", " . Cluster.asLog
238 -- | Convert allocation/relocation results into the result format.
239 formatAllocate :: Instance.List -> Cluster.AllocSolution -> Result IAllocResult
240 formatAllocate il as = do
241 let info = describeSolution as
242 case Cluster.asSolution as of
244 Just (nl, inst, nodes, _) ->
246 let il' = Container.add (Instance.idx inst) inst il
247 return (info, showJSON $ map Node.name nodes, nl, il')
249 -- | Convert multi allocation results into the result format.
250 formatMultiAlloc :: (Node.List, Instance.List, Cluster.AllocSolutionList)
251 -> Result IAllocResult
252 formatMultiAlloc (fin_nl, fin_il, ars) =
253 let rars = reverse ars
254 (allocated, failed) = partition (isJust . Cluster.asSolution . snd) rars
255 aars = map (\(_, ar) ->
256 let (_, inst, nodes, _) = fromJust $ Cluster.asSolution ar
257 iname = Instance.name inst
258 nnames = map Node.name nodes
259 in (iname, nnames)) allocated
260 fars = map (\(inst, ar) ->
261 let iname = Instance.name inst
262 in (iname, describeSolution ar)) failed
263 info = show (length failed) ++ " instances failed to allocate and " ++
264 show (length allocated) ++ " were allocated successfully"
265 in return (info, showJSON (aars, fars), fin_nl, fin_il)
267 -- | Convert a node-evacuation/change group result.
268 formatNodeEvac :: Group.List
271 -> (Node.List, Instance.List, Cluster.EvacSolution)
272 -> Result IAllocResult
273 formatNodeEvac gl nl il (fin_nl, fin_il, es) =
274 let iname = Instance.name . flip Container.find il
275 nname = Node.name . flip Container.find nl
276 gname = Group.name . flip Container.find gl
277 fes = map (\(idx, msg) -> (iname idx, msg)) $ Cluster.esFailed es
278 mes = map (\(idx, gdx, ndxs) -> (iname idx, gname gdx, map nname ndxs))
282 info = show failed ++ " instances failed to move and " ++ show moved ++
283 " were moved successfully"
284 in Ok (info, showJSON (mes, fes, Cluster.esOpCodes es), fin_nl, fin_il)
286 -- | Runs relocate for a single instance.
288 -- This is wrapper over the 'Cluster.tryNodeEvac' function that is run
289 -- with a single instance (ours), and further it checks that the
290 -- result it got (in the nodes field) is actually consistent, as
291 -- tryNodeEvac is designed to output primarily an opcode list, not a
293 processRelocate :: Group.List -- ^ The group list
294 -> Node.List -- ^ The node list
295 -> Instance.List -- ^ The instance list
296 -> Idx -- ^ The index of the instance to move
297 -> Int -- ^ The number of nodes required
298 -> [Ndx] -- ^ Nodes which should not be used
299 -> Result (Node.List, Instance.List, [Ndx]) -- ^ Solution list
300 processRelocate gl nl il idx 1 exndx = do
301 let orig = Container.find idx il
302 sorig = Instance.sNode orig
303 porig = Instance.pNode orig
304 mir_type = Instance.mirrorType orig
305 (exp_node, node_type, reloc_type) <-
307 MirrorNone -> fail "Can't relocate non-mirrored instances"
308 MirrorInternal -> return (sorig, "secondary", ChangeSecondary)
309 MirrorExternal -> return (porig, "primary", ChangePrimary)
310 when (exndx /= [exp_node]) .
311 -- FIXME: we can't use the excluded nodes here; the logic is
312 -- already _but only partially_ implemented in tryNodeEvac...
313 fail $ "Unsupported request: excluded nodes not equal to\
314 \ instance's " ++ node_type ++ "(" ++ show exp_node
315 ++ " versus " ++ show exndx ++ ")"
316 (nl', il', esol) <- Cluster.tryNodeEvac gl nl il reloc_type [idx]
317 nodes <- case lookup idx (Cluster.esFailed esol) of
320 case lookup idx (map (\(a, _, b) -> (a, b))
321 (Cluster.esMoved esol)) of
323 fail "Internal error: lost instance idx during move"
325 let inst = Container.find idx il'
326 pnode = Instance.pNode inst
327 snode = Instance.sNode inst
330 MirrorNone -> fail "Internal error: mirror type none after relocation?!"
333 when (snode == sorig) $
334 fail "Internal error: instance didn't change secondary node?!"
335 when (snode == pnode) $
336 fail "Internal error: selected primary as new secondary?!"
337 if nodes == [pnode, snode]
338 then return [snode] -- only the new secondary is needed
339 else fail $ "Internal error: inconsistent node list (" ++
340 show nodes ++ ") versus instance nodes (" ++ show pnode ++
341 "," ++ show snode ++ ")"
344 when (pnode == porig) $
345 fail "Internal error: instance didn't change primary node?!"
348 else fail $ "Internal error: inconsistent node list (" ++
349 show nodes ++ ") versus instance node (" ++ show pnode ++ ")"
350 return (nl', il', nodes')
352 processRelocate _ _ _ _ reqn _ =
353 fail $ "Exchange " ++ show reqn ++ " nodes mode is not implemented"
355 formatRelocate :: (Node.List, Instance.List, [Ndx])
356 -> Result IAllocResult
357 formatRelocate (nl, il, ndxs) =
358 let nodes = map (`Container.find` nl) ndxs
359 names = map Node.name nodes
360 in Ok ("success", showJSON names, nl, il)
362 -- | Process a request and return new node lists.
363 processRequest :: Request -> Result IAllocResult
364 processRequest request =
365 let Request rqtype (ClusterData gl nl il _ _) = request
368 Cluster.tryMGAlloc gl nl il xi reqn >>= formatAllocate il
369 Relocate idx reqn exnodes ->
370 processRelocate gl nl il idx reqn exnodes >>= formatRelocate
371 ChangeGroup gdxs idxs ->
372 Cluster.tryChangeGroup gl nl il idxs gdxs >>=
373 formatNodeEvac gl nl il
374 NodeEvacuate xi mode ->
375 Cluster.tryNodeEvac gl nl il mode xi >>=
376 formatNodeEvac gl nl il
377 MultiAllocate xies ->
378 Cluster.allocList gl nl il xies [] >>= formatMultiAlloc
380 -- | Reads the request from the data file(s).
381 readRequest :: FilePath -> IO Request
383 input_data <- case fp of
386 case parseData input_data of
388 hPutStrLn stderr $ "Error: " ++ err
389 exitWith $ ExitFailure 1
390 Ok (fix_msgs, rq) -> maybeShowWarnings fix_msgs >> return rq
392 -- | Main iallocator pipeline.
393 runIAllocator :: Request -> (Maybe (Node.List, Instance.List), String)
394 runIAllocator request =
395 let (ok, info, result, cdata) =
396 case processRequest request of
397 Ok (msg, r, nl, il) -> (True, "Request successful: " ++ msg, r,
399 Bad msg -> (False, "Request failed: " ++ msg, JSArray [], Nothing)
400 rstring = formatResponse ok info result
403 -- | Load the data from an iallocation request file
404 loadData :: FilePath -- ^ The path to the file
405 -> IO (Result ClusterData)
407 Request _ cdata <- readRequest fp