1 {-| Implementation of the iallocator interface.
7 Copyright (C) 2009, 2010, 2011 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
32 import Data.Maybe (fromMaybe, isJust)
35 import Text.JSON (JSObject, JSValue(JSArray),
36 makeObj, encodeStrict, decodeStrict, fromJSObject, showJSON)
37 import System (exitWith, ExitCode(..))
40 import qualified Ganeti.HTools.Cluster as Cluster
41 import qualified Ganeti.HTools.Container as Container
42 import qualified Ganeti.HTools.Group as Group
43 import qualified Ganeti.HTools.Node as Node
44 import qualified Ganeti.HTools.Instance as Instance
45 import qualified Ganeti.Constants as C
46 import Ganeti.HTools.CLI
47 import Ganeti.HTools.Loader
48 import Ganeti.HTools.ExtLoader (loadExternalData)
49 import Ganeti.HTools.Utils
50 import Ganeti.HTools.Types
52 -- | Type alias for the result of an IAllocator call.
53 type IAllocResult = (String, JSValue, Node.List, Instance.List)
55 -- | Parse the basic specifications of an instance.
57 -- Instances in the cluster instance list and the instance in an
58 -- 'Allocate' request share some common properties, which are read by
60 parseBaseInstance :: String
62 -> Result (String, Instance.Instance)
63 parseBaseInstance n a = do
64 let extract x = tryFromObj ("invalid data for instance '" ++ n ++ "'") a x
65 disk <- extract "disk_space_total"
66 mem <- extract "memory"
67 vcpus <- extract "vcpus"
68 tags <- extract "tags"
69 dt <- extract "disk_template"
70 let running = "running"
71 return (n, Instance.create n mem disk vcpus running tags True 0 0 dt)
73 -- | Parses an instance as found in the cluster instance list.
74 parseInstance :: NameAssoc -- ^ The node name-to-index association list
75 -> String -- ^ The name of the instance
76 -> JSRecord -- ^ The JSON object
77 -> Result (String, Instance.Instance)
78 parseInstance ktn n a = do
79 base <- parseBaseInstance n a
80 nodes <- fromObj a "nodes"
81 pnode <- if null nodes
82 then Bad $ "empty node list for instance " ++ n
83 else readEitherString $ head nodes
84 pidx <- lookupNode ktn n pnode
85 let snodes = tail nodes
86 sidx <- (if null snodes then return Node.noSecondary
87 else readEitherString (head snodes) >>= lookupNode ktn n)
88 return (n, Instance.setBoth (snd base) pidx sidx)
90 -- | Parses a node as found in the cluster node list.
91 parseNode :: NameAssoc -- ^ The group association
92 -> String -- ^ The node's name
93 -> JSRecord -- ^ The JSON object
94 -> Result (String, Node.Node)
95 parseNode ktg n a = do
96 let desc = "invalid data for node '" ++ n ++ "'"
97 extract x = tryFromObj desc a x
98 offline <- extract "offline"
99 drained <- extract "drained"
100 guuid <- extract "group"
101 vm_capable <- annotateResult desc $ maybeFromObj a "vm_capable"
102 let vm_capable' = fromMaybe True vm_capable
103 gidx <- lookupGroup ktg n guuid
104 node <- (if offline || drained || not vm_capable'
105 then return $ Node.create n 0 0 0 0 0 0 True gidx
107 mtotal <- extract "total_memory"
108 mnode <- extract "reserved_memory"
109 mfree <- extract "free_memory"
110 dtotal <- extract "total_disk"
111 dfree <- extract "free_disk"
112 ctotal <- extract "total_cpus"
113 return $ Node.create n mtotal mnode mfree
114 dtotal dfree ctotal False gidx)
117 -- | Parses a group as found in the cluster group list.
118 parseGroup :: String -- ^ The group UUID
119 -> JSRecord -- ^ The JSON object
120 -> Result (String, Group.Group)
122 let extract x = tryFromObj ("invalid data for group '" ++ u ++ "'") a x
123 name <- extract "name"
124 apol <- extract "alloc_policy"
125 return (u, Group.create name u apol)
127 -- | Top-level parser.
129 -- The result is a tuple of eventual warning messages and the parsed
130 -- request; if parsing the input data fails, we'll return a 'Bad'
132 parseData :: String -- ^ The JSON message as received from Ganeti
133 -> Result ([String], Request) -- ^ Result tuple
135 decoded <- fromJResult "Parsing input IAllocator message" (decodeStrict body)
136 let obj = fromJSObject decoded
137 extrObj x = tryFromObj "invalid iallocator message" obj x
139 request <- liftM fromJSObject (extrObj "request")
140 let extrReq x = tryFromObj "invalid request dict" request x
141 -- existing group parsing
142 glist <- liftM fromJSObject (extrObj "nodegroups")
143 gobj <- mapM (\(x, y) -> asJSObject y >>= parseGroup x . fromJSObject) glist
144 let (ktg, gl) = assignIndices gobj
145 -- existing node parsing
146 nlist <- liftM fromJSObject (extrObj "nodes")
147 nobj <- mapM (\(x,y) ->
148 asJSObject y >>= parseNode ktg x . fromJSObject) nlist
149 let (ktn, nl) = assignIndices nobj
150 -- existing instance parsing
151 ilist <- extrObj "instances"
152 let idata = fromJSObject ilist
153 iobj <- mapM (\(x,y) ->
154 asJSObject y >>= parseInstance ktn x . fromJSObject) idata
155 let (_, il) = assignIndices iobj
157 ctags <- extrObj "cluster_tags"
158 cdata1 <- mergeData [] [] [] [] (ClusterData gl nl il ctags)
159 let (msgs, fix_nl) = checkData (cdNodes cdata1) (cdInstances cdata1)
160 cdata = cdata1 { cdNodes = fix_nl }
161 map_i = cdInstances cdata
162 map_g = cdGroups cdata
163 optype <- extrReq "type"
166 _ | optype == C.iallocatorModeAlloc ->
168 rname <- extrReq "name"
169 req_nodes <- extrReq "required_nodes"
170 inew <- parseBaseInstance rname request
172 return $ Allocate io req_nodes
173 | optype == C.iallocatorModeChgGroup ->
175 rl_names <- extrReq "instances"
176 rl_insts <- mapM (liftM Instance.idx .
177 Container.findByName map_i) rl_names
178 gr_uuids <- extrReq "target_groups"
179 gr_idxes <- mapM (liftM Group.idx .
180 Container.findByName map_g) gr_uuids
181 return $ ChangeGroup rl_insts gr_idxes
182 | optype == C.iallocatorModeNodeEvac ->
184 rl_names <- extrReq "instances"
185 rl_insts <- mapM (Container.findByName map_i) rl_names
186 let rl_idx = map Instance.idx rl_insts
188 case extrReq "evac_mode" of
189 Ok s | s == C.iallocatorNevacAll -> return ChangeAll
190 | s == C.iallocatorNevacPri -> return ChangePrimary
191 | s == C.iallocatorNevacSec -> return ChangeSecondary
192 | otherwise -> Bad $ "Invalid evacuate mode " ++ s
194 return $ NodeEvacuate rl_idx rl_mode
196 | otherwise -> fail ("Invalid request type '" ++ optype ++ "'")
197 return $ (msgs, Request rqtype cdata)
199 -- | Formats the result into a valid IAllocator response message.
200 formatResponse :: Bool -- ^ Whether the request was successful
201 -> String -- ^ Information text
202 -> JSValue -- ^ The JSON encoded result
203 -> String -- ^ The full JSON-formatted message
204 formatResponse success info result =
206 e_success = ("success", showJSON success)
207 e_info = ("info", showJSON info)
208 e_result = ("result", result)
209 in encodeStrict $ makeObj [e_success, e_info, e_result]
211 -- | Flatten the log of a solution into a string.
212 describeSolution :: Cluster.AllocSolution -> String
213 describeSolution = intercalate ", " . Cluster.asLog
215 -- | Convert allocation/relocation results into the result format.
216 formatAllocate :: Instance.List -> Cluster.AllocSolution -> Result IAllocResult
217 formatAllocate il as = do
218 let info = describeSolution as
219 case Cluster.asSolutions as of
221 (nl, inst, nodes, _):[] ->
223 let il' = Container.add (Instance.idx inst) inst il
224 return (info, showJSON $ map (Node.name) nodes, nl, il')
225 _ -> fail "Internal error: multiple allocation solutions"
227 -- | Convert a node-evacuation/change group result.
228 formatNodeEvac :: Group.List
231 -> (Node.List, Instance.List, Cluster.EvacSolution)
232 -> Result IAllocResult
233 formatNodeEvac gl nl il (fin_nl, fin_il, es) =
234 let iname = Instance.name . flip Container.find il
235 nname = Node.name . flip Container.find nl
236 gname = Group.name . flip Container.find gl
237 fes = map (\(idx, msg) -> (iname idx, msg)) $ Cluster.esFailed es
238 mes = map (\(idx, gdx, ndxs) -> (iname idx, gname gdx, map nname ndxs))
242 info = show failed ++ " instances failed to move and " ++ show moved ++
243 " were moved successfully"
244 in Ok (info, showJSON (mes, fes, Cluster.esOpCodes es), fin_nl, fin_il)
246 -- | Process a request and return new node lists
247 processRequest :: Request -> Result IAllocResult
248 processRequest request =
249 let Request rqtype (ClusterData gl nl il _) = request
252 Cluster.tryMGAlloc gl nl il xi reqn >>= formatAllocate il
253 ChangeGroup gdxs idxs ->
254 Cluster.tryChangeGroup gl nl il idxs gdxs >>=
255 formatNodeEvac gl nl il
256 NodeEvacuate xi mode ->
257 Cluster.tryNodeEvac gl nl il mode xi >>=
258 formatNodeEvac gl nl il
260 -- | Reads the request from the data file(s)
261 readRequest :: Options -> [String] -> IO Request
262 readRequest opts args = do
263 when (null args) $ do
264 hPutStrLn stderr "Error: this program needs an input file."
265 exitWith $ ExitFailure 1
267 input_data <- readFile (head args)
268 r1 <- case parseData input_data of
270 hPutStrLn stderr $ "Error: " ++ err
271 exitWith $ ExitFailure 1
272 Ok (fix_msgs, rq) -> maybeShowWarnings fix_msgs >> return rq
273 (if isJust (optDataFile opts) || (not . null . optNodeSim) opts
275 cdata <- loadExternalData opts
276 let Request rqt _ = r1
277 return $ Request rqt cdata
280 -- | Main iallocator pipeline.
281 runIAllocator :: Request -> (Maybe (Node.List, Instance.List), String)
282 runIAllocator request =
283 let (ok, info, result, cdata) =
284 case processRequest request of
285 Ok (msg, r, nl, il) -> (True, "Request successful: " ++ msg, r,
287 Bad msg -> (False, "Request failed: " ++ msg, JSArray [], Nothing)
288 rstring = formatResponse ok info result