1 {-| Generic data loader.
3 This module holds the common code for parsing the input data after it
4 has been loaded from external sources.
10 Copyright (C) 2009, 2010, 2011 Google Inc.
12 This program is free software; you can redistribute it and/or modify
13 it under the terms of the GNU General Public License as published by
14 the Free Software Foundation; either version 2 of the License, or
15 (at your option) any later version.
17 This program is distributed in the hope that it will be useful, but
18 WITHOUT ANY WARRANTY; without even the implied warranty of
19 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
20 General Public License for more details.
22 You should have received a copy of the GNU General Public License
23 along with this program; if not, write to the Free Software
24 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
29 module Ganeti.HTools.Loader
43 , compareNameComponent
51 import qualified Data.Map as M
52 import Text.Printf (printf)
54 import qualified Ganeti.HTools.Container as Container
55 import qualified Ganeti.HTools.Instance as Instance
56 import qualified Ganeti.HTools.Node as Node
57 import qualified Ganeti.HTools.Group as Group
59 import Ganeti.HTools.Types
60 import Ganeti.HTools.Utils
64 -- | The exclusion tag prefix.
65 exTagsPrefix :: String
66 exTagsPrefix = "htools:iextags:"
70 -- | The iallocator multi-evac group mode type.
71 data RelocMode = KeepGroup
76 {-| The iallocator request type.
78 This type denotes what request we got from Ganeti and also holds
79 request-specific fields.
83 = Allocate Instance.Instance Int -- ^ A new instance allocation
84 | Relocate Idx Int [Ndx] -- ^ Move an instance to a new
86 | Evacuate [Ndx] -- ^ Evacuate nodes
87 | MultiReloc [Idx] RelocMode -- ^ Multi-relocate mode
88 | NodeEvacuate [Idx] EvacMode -- ^ node-evacuate mode
91 -- | A complete request, as received from Ganeti.
92 data Request = Request RqType ClusterData
95 -- | The cluster state.
96 data ClusterData = ClusterData
97 { cdGroups :: Group.List -- ^ The node group list
98 , cdNodes :: Node.List -- ^ The node list
99 , cdInstances :: Instance.List -- ^ The instance list
100 , cdTags :: [String] -- ^ The cluster tags
101 } deriving (Show, Read)
103 -- | The priority of a match in a lookup result.
104 data MatchPriority = ExactMatch
108 deriving (Show, Read, Enum, Eq, Ord)
110 -- | The result of a name lookup in a list.
111 data LookupResult = LookupResult
112 { lrMatchPriority :: MatchPriority -- ^ The result type
113 -- | Matching value (for ExactMatch, PartialMatch), Lookup string otherwise
114 , lrContent :: String
115 } deriving (Show, Read)
117 -- | Lookup results have an absolute preference ordering.
118 instance Eq LookupResult where
119 (==) = (==) `on` lrMatchPriority
121 instance Ord LookupResult where
122 compare = compare `on` lrMatchPriority
124 -- | An empty cluster.
125 emptyCluster :: ClusterData
126 emptyCluster = ClusterData Container.empty Container.empty Container.empty []
130 -- | Lookups a node into an assoc list.
131 lookupNode :: (Monad m) => NameAssoc -> String -> String -> m Ndx
132 lookupNode ktn inst node =
133 case M.lookup node ktn of
134 Nothing -> fail $ "Unknown node '" ++ node ++ "' for instance " ++ inst
135 Just idx -> return idx
137 -- | Lookups an instance into an assoc list.
138 lookupInstance :: (Monad m) => NameAssoc -> String -> m Idx
139 lookupInstance kti inst =
140 case M.lookup inst kti of
141 Nothing -> fail $ "Unknown instance '" ++ inst ++ "'"
142 Just idx -> return idx
144 -- | Lookups a group into an assoc list.
145 lookupGroup :: (Monad m) => NameAssoc -> String -> String -> m Gdx
146 lookupGroup ktg nname gname =
147 case M.lookup gname ktg of
148 Nothing -> fail $ "Unknown group '" ++ gname ++ "' for node " ++ nname
149 Just idx -> return idx
151 -- | Check for prefix matches in names.
152 -- Implemented in Ganeti core utils.text.MatchNameComponent
153 -- as the regexp r"^%s(\..*)?$" % re.escape(key)
154 prefixMatch :: String -- ^ Lookup
155 -> String -- ^ Full name
156 -> Bool -- ^ Whether there is a prefix match
157 prefixMatch lkp = isPrefixOf (lkp ++ ".")
159 -- | Is the lookup priority a "good" one?
160 goodMatchPriority :: MatchPriority -> Bool
161 goodMatchPriority ExactMatch = True
162 goodMatchPriority PartialMatch = True
163 goodMatchPriority _ = False
165 -- | Is the lookup result an actual match?
166 goodLookupResult :: LookupResult -> Bool
167 goodLookupResult = goodMatchPriority . lrMatchPriority
169 -- | Compares a canonical name and a lookup string.
170 compareNameComponent :: String -- ^ Canonical (target) name
171 -> String -- ^ Partial (lookup) name
172 -> LookupResult -- ^ Result of the lookup
173 compareNameComponent cnl lkp =
174 select (LookupResult FailMatch lkp)
175 [ (cnl == lkp , LookupResult ExactMatch cnl)
176 , (prefixMatch lkp cnl , LookupResult PartialMatch cnl)
179 -- | Lookup a string and choose the best result.
180 chooseLookupResult :: String -- ^ Lookup key
181 -> String -- ^ String to compare to the lookup key
182 -> LookupResult -- ^ Previous result
183 -> LookupResult -- ^ New result
184 chooseLookupResult lkp cstr old =
185 -- default: use class order to pick the minimum result
188 -- short circuit if the new result is an exact match
189 [ ((lrMatchPriority new) == ExactMatch, new)
190 -- if both are partial matches generate a multiple match
191 , (partial2, LookupResult MultipleMatch lkp)
192 ] where new = compareNameComponent cstr lkp
193 partial2 = all ((PartialMatch==) . lrMatchPriority) [old, new]
195 -- | Find the canonical name for a lookup string in a list of names.
196 lookupName :: [String] -- ^ List of keys
197 -> String -- ^ Lookup string
198 -> LookupResult -- ^ Result of the lookup
199 lookupName l s = foldr (chooseLookupResult s)
200 (LookupResult FailMatch s) l
202 -- | Given a list of elements (and their names), assign indices to them.
203 assignIndices :: (Element a) =>
205 -> (NameAssoc, Container.Container a)
206 assignIndices nodes =
208 unzip . map (\ (idx, (k, v)) -> ((k, idx), (idx, setIdx v idx)))
210 in (M.fromList na, Container.fromList idx_node)
212 -- | For each instance, add its index to its primary and secondary nodes.
213 fixNodes :: Node.List
218 pdx = Instance.pNode inst
219 sdx = Instance.sNode inst
220 pold = Container.find pdx accu
221 pnew = Node.setPri pold inst
222 ac2 = Container.add pdx pnew accu
224 if sdx /= Node.noSecondary
225 then let sold = Container.find sdx accu
226 snew = Node.setSec sold inst
227 in Container.add sdx snew ac2
230 -- | Remove non-selected tags from the exclusion list.
231 filterExTags :: [String] -> Instance.Instance -> Instance.Instance
232 filterExTags tl inst =
233 let old_tags = Instance.tags inst
234 new_tags = filter (\tag -> any (`isPrefixOf` tag) tl)
236 in inst { Instance.tags = new_tags }
238 -- | Update the movable attribute.
239 updateMovable :: [String] -- ^ Selected instances (if not empty)
240 -> [String] -- ^ Excluded instances
241 -> Instance.Instance -- ^ Target Instance
242 -> Instance.Instance -- ^ Target Instance with updated attribute
243 updateMovable selinsts exinsts inst =
244 if Instance.sNode inst == Node.noSecondary ||
245 Instance.name inst `elem` exinsts ||
246 not (null selinsts || Instance.name inst `elem` selinsts)
247 then Instance.setMovable inst False
250 -- | Compute the longest common suffix of a list of strings that
251 -- starts with a dot.
252 longestDomain :: [String] -> String
253 longestDomain [] = ""
254 longestDomain (x:xs) =
255 foldr (\ suffix accu -> if all (isSuffixOf suffix) xs
258 "" $ filter (isPrefixOf ".") (tails x)
260 -- | Extracts the exclusion tags from the cluster configuration.
261 extractExTags :: [String] -> [String]
263 map (drop (length exTagsPrefix)) .
264 filter (isPrefixOf exTagsPrefix)
266 -- | Extracts the common suffix from node\/instance names.
267 commonSuffix :: Node.List -> Instance.List -> String
269 let node_names = map Node.name $ Container.elems nl
270 inst_names = map Instance.name $ Container.elems il
271 in longestDomain (node_names ++ inst_names)
273 -- | Initializer function that loads the data from a node and instance
274 -- list and massages it into the correct format.
275 mergeData :: [(String, DynUtil)] -- ^ Instance utilisation data
276 -> [String] -- ^ Exclusion tags
277 -> [String] -- ^ Selected instances (if not empty)
278 -> [String] -- ^ Excluded instances
279 -> ClusterData -- ^ Data from backends
280 -> Result ClusterData -- ^ Fixed cluster data
281 mergeData um extags selinsts exinsts cdata@(ClusterData _ nl il2 tags) =
282 let il = Container.elems il2
283 il3 = foldl' (\im (name, n_util) ->
284 case Container.findByName im name of
285 Nothing -> im -- skipping unknown instance
287 let new_i = inst { Instance.util = n_util }
288 in Container.add (Instance.idx inst) new_i im
290 allextags = extags ++ extractExTags tags
291 inst_names = map Instance.name il
292 selinst_lkp = map (lookupName inst_names) selinsts
293 exinst_lkp = map (lookupName inst_names) exinsts
294 lkp_unknown = filter (not . goodLookupResult) (selinst_lkp ++ exinst_lkp)
295 selinst_names = map lrContent selinst_lkp
296 exinst_names = map lrContent exinst_lkp
297 il4 = Container.map (filterExTags allextags .
298 updateMovable selinst_names exinst_names) il3
299 nl2 = foldl' fixNodes nl (Container.elems il4)
300 nl3 = Container.map (flip Node.buildPeers il4) nl2
301 node_names = map Node.name (Container.elems nl)
302 common_suffix = longestDomain (node_names ++ inst_names)
303 snl = Container.map (computeAlias common_suffix) nl3
304 sil = Container.map (computeAlias common_suffix) il4
305 in if' (null lkp_unknown)
306 (Ok cdata { cdNodes = snl, cdInstances = sil })
307 (Bad $ "Unknown instance(s): " ++ show(map lrContent lkp_unknown))
309 -- | Checks the cluster data for consistency.
310 checkData :: Node.List -> Instance.List
311 -> ([String], Node.List)
315 let nname = Node.name node
316 nilst = map (`Container.find` il) (Node.pList node)
317 dilst = filter (not . Instance.running) nilst
318 adj_mem = sum . map Instance.mem $ dilst
319 delta_mem = truncate (Node.tMem node)
324 delta_dsk = truncate (Node.tDsk node)
327 newn = Node.setFmem (Node.setXmem node delta_mem)
328 (Node.fMem node - adj_mem)
329 umsg1 = [printf "node %s is missing %d MB ram \
331 nname delta_mem (delta_dsk `div` 1024) |
332 delta_mem > 512 || delta_dsk > 1024]::[String]
333 in (msgs ++ umsg1, newn)
336 -- | Compute the amount of memory used by primary instances on a node.
337 nodeImem :: Node.Node -> Instance.List -> Int
339 let rfind = flip Container.find il
340 in sum . map (Instance.mem . rfind)
343 -- | Compute the amount of disk used by instances on a node (either primary
345 nodeIdsk :: Node.Node -> Instance.List -> Int
347 let rfind = flip Container.find il
348 in sum . map (Instance.dsk . rfind)
349 $ Node.pList node ++ Node.sList node