1 {-| Implementation of the iallocator interface.
7 Copyright (C) 2009, 2010, 2011, 2012, 2013 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.Backend.IAlloc
34 import Data.Maybe (fromMaybe, isJust, fromJust)
38 import Text.JSON (JSObject, JSValue(JSArray),
39 makeObj, encodeStrict, decodeStrict, fromJSObject, showJSON)
41 import Ganeti.BasicTypes
42 import qualified Ganeti.HTools.Cluster as Cluster
43 import qualified Ganeti.HTools.Container as Container
44 import qualified Ganeti.HTools.Group as Group
45 import qualified Ganeti.HTools.Node as Node
46 import qualified Ganeti.HTools.Instance as Instance
47 import qualified Ganeti.HTools.Nic as Nic
48 import qualified Ganeti.Constants as C
49 import Ganeti.HTools.CLI
50 import Ganeti.HTools.Loader
51 import Ganeti.HTools.Types
55 {-# ANN module "HLint: ignore Eta reduce" #-}
57 -- | Type alias for the result of an IAllocator call.
58 type IAllocResult = (String, JSValue, Node.List, Instance.List)
60 -- | Parse a NIC within an instance (in a creation request)
61 parseNic :: String -> JSRecord -> Result Nic.Nic
63 mac <- maybeFromObj a "mac"
64 ip <- maybeFromObj a "ip"
65 mode <- maybeFromObj a "mode" >>= \m -> case m of
66 Just "bridged" -> Ok $ Just Nic.Bridged
67 Just "routed" -> Ok $ Just Nic.Routed
68 Just "openvswitch" -> Ok $ Just Nic.OpenVSwitch
70 _ -> Bad $ "invalid NIC mode in instance " ++ n
71 link <- maybeFromObj a "link"
72 bridge <- maybeFromObj a "bridge"
73 network <- maybeFromObj a "network"
74 return (Nic.create mac ip mode link bridge network)
76 -- | Parse the basic specifications of an instance.
78 -- Instances in the cluster instance list and the instance in an
79 -- 'Allocate' request share some common properties, which are read by
81 parseBaseInstance :: String
83 -> Result (String, Instance.Instance)
84 parseBaseInstance n a = do
85 let errorMessage = "invalid data for instance '" ++ n ++ "'"
86 let extract x = tryFromObj errorMessage a x
87 disk <- extract "disk_space_total"
88 jsdisks <- extract "disks" >>= toArray >>= asObjectList
89 dsizes <- mapM (flip (tryFromObj errorMessage) "size" . fromJSObject) jsdisks
90 dspindles <- mapM (annotateResult errorMessage .
91 flip maybeFromObj "spindles" . fromJSObject) jsdisks
92 let disks = zipWith Instance.Disk dsizes dspindles
93 mem <- extract "memory"
94 vcpus <- extract "vcpus"
95 tags <- extract "tags"
96 dt <- extract "disk_template"
97 su <- extract "spindle_use"
98 nics <- extract "nics" >>= toArray >>= asObjectList >>=
99 mapM (parseNic n . fromJSObject)
102 Instance.create n mem disk disks vcpus Running tags True 0 0 dt su nics)
104 -- | Parses an instance as found in the cluster instance list.
105 parseInstance :: NameAssoc -- ^ The node name-to-index association list
106 -> String -- ^ The name of the instance
107 -> JSRecord -- ^ The JSON object
108 -> Result (String, Instance.Instance)
109 parseInstance ktn n a = do
110 base <- parseBaseInstance n a
111 nodes <- fromObj a "nodes"
114 [] -> Bad $ "empty node list for instance " ++ n
115 x:xs -> readEitherString x >>= \x' -> return (x', xs)
116 pidx <- lookupNode ktn n pnode
117 sidx <- case snodes of
118 [] -> return Node.noSecondary
119 x:_ -> readEitherString x >>= lookupNode ktn n
120 return (n, Instance.setBoth (snd base) pidx sidx)
122 -- | Parses a node as found in the cluster node list.
123 parseNode :: NameAssoc -- ^ The group association
124 -> String -- ^ The node's name
125 -> JSRecord -- ^ The JSON object
126 -> Result (String, Node.Node)
127 parseNode ktg n a = do
128 let desc = "invalid data for node '" ++ n ++ "'"
129 extract x = tryFromObj desc a x
130 offline <- extract "offline"
131 drained <- extract "drained"
132 guuid <- extract "group"
133 vm_capable <- annotateResult desc $ maybeFromObj a "vm_capable"
134 let vm_capable' = fromMaybe True vm_capable
135 gidx <- lookupGroup ktg n guuid
136 ndparams <- extract "ndparams" >>= asJSObject
137 excl_stor <- tryFromObj desc (fromJSObject ndparams) "exclusive_storage"
138 let live = not offline && not drained && vm_capable'
139 lvextract def = eitherLive live def . extract
140 sptotal <- if excl_stor
141 then lvextract 0 "total_spindles"
142 else tryFromObj desc (fromJSObject ndparams) "spindle_count"
143 spfree <- lvextract 0 "free_spindles"
144 mtotal <- lvextract 0.0 "total_memory"
145 mnode <- lvextract 0 "reserved_memory"
146 mfree <- lvextract 0 "free_memory"
147 dtotal <- lvextract 0.0 "total_disk"
148 dfree <- lvextract 0 "free_disk"
149 ctotal <- lvextract 0.0 "total_cpus"
150 let node = Node.create n mtotal mnode mfree dtotal dfree ctotal (not live)
151 sptotal spfree gidx excl_stor
154 -- | Parses a group as found in the cluster group list.
155 parseGroup :: String -- ^ The group UUID
156 -> JSRecord -- ^ The JSON object
157 -> Result (String, Group.Group)
159 let extract x = tryFromObj ("invalid data for group '" ++ u ++ "'") a x
160 name <- extract "name"
161 apol <- extract "alloc_policy"
162 nets <- extract "networks"
163 ipol <- extract "ipolicy"
164 tags <- extract "tags"
165 return (u, Group.create name u apol nets ipol tags)
167 -- | Top-level parser.
169 -- The result is a tuple of eventual warning messages and the parsed
170 -- request; if parsing the input data fails, we'll return a 'Bad'
172 parseData :: ClockTime -- ^ The current time
173 -> String -- ^ The JSON message as received from Ganeti
174 -> Result ([String], Request) -- ^ Result tuple
175 parseData now body = do
176 decoded <- fromJResult "Parsing input IAllocator message" (decodeStrict body)
177 let obj = fromJSObject decoded
178 extrObj x = tryFromObj "invalid iallocator message" obj x
180 request <- liftM fromJSObject (extrObj "request")
181 let extrFromReq r x = tryFromObj "invalid request dict" r x
182 let extrReq x = extrFromReq request x
183 -- existing group parsing
184 glist <- liftM fromJSObject (extrObj "nodegroups")
185 gobj <- mapM (\(x, y) -> asJSObject y >>= parseGroup x . fromJSObject) glist
186 let (ktg, gl) = assignIndices gobj
187 -- existing node parsing
188 nlist <- liftM fromJSObject (extrObj "nodes")
189 nobj <- mapM (\(x,y) ->
190 asJSObject y >>= parseNode ktg x . fromJSObject) nlist
191 let (ktn, nl) = assignIndices nobj
192 -- existing instance parsing
193 ilist <- extrObj "instances"
194 let idata = fromJSObject ilist
195 iobj <- mapM (\(x,y) ->
196 asJSObject y >>= parseInstance ktn x . fromJSObject) idata
197 let (kti, il) = assignIndices iobj
199 ctags <- extrObj "cluster_tags"
200 cdata1 <- mergeData [] [] [] [] now (ClusterData gl nl il ctags defIPolicy)
201 let (msgs, fix_nl) = checkData (cdNodes cdata1) (cdInstances cdata1)
202 cdata = cdata1 { cdNodes = fix_nl }
203 map_n = cdNodes cdata
204 map_i = cdInstances cdata
205 map_g = cdGroups cdata
206 optype <- extrReq "type"
209 _ | optype == C.iallocatorModeAlloc ->
211 rname <- extrReq "name"
212 req_nodes <- extrReq "required_nodes"
213 inew <- parseBaseInstance rname request
215 return $ Allocate io req_nodes
216 | optype == C.iallocatorModeReloc ->
218 rname <- extrReq "name"
219 ridx <- lookupInstance kti rname
220 req_nodes <- extrReq "required_nodes"
221 ex_nodes <- extrReq "relocate_from"
222 ex_idex <- mapM (Container.findByName map_n) ex_nodes
223 return $ Relocate ridx req_nodes (map Node.idx ex_idex)
224 | optype == C.iallocatorModeChgGroup ->
226 rl_names <- extrReq "instances"
227 rl_insts <- mapM (liftM Instance.idx .
228 Container.findByName map_i) rl_names
229 gr_uuids <- extrReq "target_groups"
230 gr_idxes <- mapM (liftM Group.idx .
231 Container.findByName map_g) gr_uuids
232 return $ ChangeGroup rl_insts gr_idxes
233 | optype == C.iallocatorModeNodeEvac ->
235 rl_names <- extrReq "instances"
236 rl_insts <- mapM (Container.findByName map_i) rl_names
237 let rl_idx = map Instance.idx rl_insts
238 rl_mode <- extrReq "evac_mode"
239 return $ NodeEvacuate rl_idx rl_mode
240 | optype == C.iallocatorModeMultiAlloc ->
242 arry <- extrReq "instances" :: Result [JSObject JSValue]
243 let inst_reqs = map fromJSObject arry
246 rname <- extrFromReq r "name"
247 req_nodes <- extrFromReq r "required_nodes"
248 inew <- parseBaseInstance rname r
250 return (io, req_nodes)) inst_reqs
251 return $ MultiAllocate prqs
252 | otherwise -> fail ("Invalid request type '" ++ optype ++ "'")
253 return (msgs, Request rqtype cdata)
255 -- | Formats the result into a valid IAllocator response message.
256 formatResponse :: Bool -- ^ Whether the request was successful
257 -> String -- ^ Information text
258 -> JSValue -- ^ The JSON encoded result
259 -> String -- ^ The full JSON-formatted message
260 formatResponse success info result =
261 let e_success = ("success", showJSON success)
262 e_info = ("info", showJSON info)
263 e_result = ("result", result)
264 in encodeStrict $ makeObj [e_success, e_info, e_result]
266 -- | Flatten the log of a solution into a string.
267 describeSolution :: Cluster.AllocSolution -> String
268 describeSolution = intercalate ", " . Cluster.asLog
270 -- | Convert allocation/relocation results into the result format.
271 formatAllocate :: Instance.List -> Cluster.AllocSolution -> Result IAllocResult
272 formatAllocate il as = do
273 let info = describeSolution as
274 case Cluster.asSolution as of
276 Just (nl, inst, nodes, _) ->
278 let il' = Container.add (Instance.idx inst) inst il
279 return (info, showJSON $ map Node.name nodes, nl, il')
281 -- | Convert multi allocation results into the result format.
282 formatMultiAlloc :: (Node.List, Instance.List, Cluster.AllocSolutionList)
283 -> Result IAllocResult
284 formatMultiAlloc (fin_nl, fin_il, ars) =
285 let rars = reverse ars
286 (allocated, failed) = partition (isJust . Cluster.asSolution . snd) rars
287 aars = map (\(_, ar) ->
288 let (_, inst, nodes, _) = fromJust $ Cluster.asSolution ar
289 iname = Instance.name inst
290 nnames = map Node.name nodes
291 in (iname, nnames)) allocated
292 fars = map (\(inst, ar) ->
293 let iname = Instance.name inst
294 in (iname, describeSolution ar)) failed
295 info = show (length failed) ++ " instances failed to allocate and " ++
296 show (length allocated) ++ " were allocated successfully"
297 in return (info, showJSON (aars, fars), fin_nl, fin_il)
299 -- | Convert a node-evacuation/change group result.
300 formatNodeEvac :: Group.List
303 -> (Node.List, Instance.List, Cluster.EvacSolution)
304 -> Result IAllocResult
305 formatNodeEvac gl nl il (fin_nl, fin_il, es) =
306 let iname = Instance.name . flip Container.find il
307 nname = Node.name . flip Container.find nl
308 gname = Group.name . flip Container.find gl
309 fes = map (\(idx, msg) -> (iname idx, msg)) $ Cluster.esFailed es
310 mes = map (\(idx, gdx, ndxs) -> (iname idx, gname gdx, map nname ndxs))
314 info = show failed ++ " instances failed to move and " ++ show moved ++
315 " were moved successfully"
316 in Ok (info, showJSON (mes, fes, Cluster.esOpCodes es), fin_nl, fin_il)
318 -- | Runs relocate for a single instance.
320 -- This is wrapper over the 'Cluster.tryNodeEvac' function that is run
321 -- with a single instance (ours), and further it checks that the
322 -- result it got (in the nodes field) is actually consistent, as
323 -- tryNodeEvac is designed to output primarily an opcode list, not a
325 processRelocate :: Group.List -- ^ The group list
326 -> Node.List -- ^ The node list
327 -> Instance.List -- ^ The instance list
328 -> Idx -- ^ The index of the instance to move
329 -> Int -- ^ The number of nodes required
330 -> [Ndx] -- ^ Nodes which should not be used
331 -> Result (Node.List, Instance.List, [Ndx]) -- ^ Solution list
332 processRelocate gl nl il idx 1 exndx = do
333 let orig = Container.find idx il
334 sorig = Instance.sNode orig
335 porig = Instance.pNode orig
336 mir_type = Instance.mirrorType orig
337 (exp_node, node_type, reloc_type) <-
339 MirrorNone -> fail "Can't relocate non-mirrored instances"
340 MirrorInternal -> return (sorig, "secondary", ChangeSecondary)
341 MirrorExternal -> return (porig, "primary", ChangePrimary)
342 when (exndx /= [exp_node]) .
343 -- FIXME: we can't use the excluded nodes here; the logic is
344 -- already _but only partially_ implemented in tryNodeEvac...
345 fail $ "Unsupported request: excluded nodes not equal to\
346 \ instance's " ++ node_type ++ "(" ++ show exp_node
347 ++ " versus " ++ show exndx ++ ")"
348 (nl', il', esol) <- Cluster.tryNodeEvac gl nl il reloc_type [idx]
349 nodes <- case lookup idx (Cluster.esFailed esol) of
352 case lookup idx (map (\(a, _, b) -> (a, b))
353 (Cluster.esMoved esol)) of
355 fail "Internal error: lost instance idx during move"
357 let inst = Container.find idx il'
358 pnode = Instance.pNode inst
359 snode = Instance.sNode inst
362 MirrorNone -> fail "Internal error: mirror type none after relocation?!"
365 when (snode == sorig) $
366 fail "Internal error: instance didn't change secondary node?!"
367 when (snode == pnode) $
368 fail "Internal error: selected primary as new secondary?!"
369 if nodes == [pnode, snode]
370 then return [snode] -- only the new secondary is needed
371 else fail $ "Internal error: inconsistent node list (" ++
372 show nodes ++ ") versus instance nodes (" ++ show pnode ++
373 "," ++ show snode ++ ")"
376 when (pnode == porig) $
377 fail "Internal error: instance didn't change primary node?!"
380 else fail $ "Internal error: inconsistent node list (" ++
381 show nodes ++ ") versus instance node (" ++ show pnode ++ ")"
382 return (nl', il', nodes')
384 processRelocate _ _ _ _ reqn _ =
385 fail $ "Exchange " ++ show reqn ++ " nodes mode is not implemented"
387 formatRelocate :: (Node.List, Instance.List, [Ndx])
388 -> Result IAllocResult
389 formatRelocate (nl, il, ndxs) =
390 let nodes = map (`Container.find` nl) ndxs
391 names = map Node.name nodes
392 in Ok ("success", showJSON names, nl, il)
394 -- | Process a request and return new node lists.
395 processRequest :: Request -> Result IAllocResult
396 processRequest request =
397 let Request rqtype (ClusterData gl nl il _ _) = request
400 Cluster.tryMGAlloc gl nl il xi reqn >>= formatAllocate il
401 Relocate idx reqn exnodes ->
402 processRelocate gl nl il idx reqn exnodes >>= formatRelocate
403 ChangeGroup gdxs idxs ->
404 Cluster.tryChangeGroup gl nl il idxs gdxs >>=
405 formatNodeEvac gl nl il
406 NodeEvacuate xi mode ->
407 Cluster.tryNodeEvac gl nl il mode xi >>=
408 formatNodeEvac gl nl il
409 MultiAllocate xies ->
410 Cluster.allocList gl nl il xies [] >>= formatMultiAlloc
412 -- | Reads the request from the data file(s).
413 readRequest :: FilePath -> IO Request
416 input_data <- case fp of
419 case parseData now input_data of
420 Bad err -> exitErr err
421 Ok (fix_msgs, rq) -> maybeShowWarnings fix_msgs >> return rq
423 -- | Main iallocator pipeline.
424 runIAllocator :: Request -> (Maybe (Node.List, Instance.List), String)
425 runIAllocator request =
426 let (ok, info, result, cdata) =
427 case processRequest request of
428 Ok (msg, r, nl, il) -> (True, "Request successful: " ++ msg, r,
430 Bad msg -> (False, "Request failed: " ++ msg, JSArray [], Nothing)
431 rstring = formatResponse ok info result
434 -- | Load the data from an iallocation request file
435 loadData :: FilePath -- ^ The path to the file
436 -> IO (Result ClusterData)
438 Request _ cdata <- readRequest fp