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 request type.
72 This type denotes what request we got from Ganeti and also holds
73 request-specific fields.
77 = Allocate Instance.Instance Int -- ^ A new instance allocation
78 | NodeEvacuate [Idx] EvacMode -- ^ node-evacuate mode
79 | ChangeGroup [Gdx] [Idx] -- ^ Multi-relocate mode
82 -- | A complete request, as received from Ganeti.
83 data Request = Request RqType ClusterData
86 -- | The cluster state.
87 data ClusterData = ClusterData
88 { cdGroups :: Group.List -- ^ The node group list
89 , cdNodes :: Node.List -- ^ The node list
90 , cdInstances :: Instance.List -- ^ The instance list
91 , cdTags :: [String] -- ^ The cluster tags
92 } deriving (Show, Read)
94 -- | The priority of a match in a lookup result.
95 data MatchPriority = ExactMatch
99 deriving (Show, Read, Enum, Eq, Ord)
101 -- | The result of a name lookup in a list.
102 data LookupResult = LookupResult
103 { lrMatchPriority :: MatchPriority -- ^ The result type
104 -- | Matching value (for ExactMatch, PartialMatch), Lookup string otherwise
105 , lrContent :: String
106 } deriving (Show, Read)
108 -- | Lookup results have an absolute preference ordering.
109 instance Eq LookupResult where
110 (==) = (==) `on` lrMatchPriority
112 instance Ord LookupResult where
113 compare = compare `on` lrMatchPriority
115 -- | An empty cluster.
116 emptyCluster :: ClusterData
117 emptyCluster = ClusterData Container.empty Container.empty Container.empty []
121 -- | Lookups a node into an assoc list.
122 lookupNode :: (Monad m) => NameAssoc -> String -> String -> m Ndx
123 lookupNode ktn inst node =
124 case M.lookup node ktn of
125 Nothing -> fail $ "Unknown node '" ++ node ++ "' for instance " ++ inst
126 Just idx -> return idx
128 -- | Lookups an instance into an assoc list.
129 lookupInstance :: (Monad m) => NameAssoc -> String -> m Idx
130 lookupInstance kti inst =
131 case M.lookup inst kti of
132 Nothing -> fail $ "Unknown instance '" ++ inst ++ "'"
133 Just idx -> return idx
135 -- | Lookups a group into an assoc list.
136 lookupGroup :: (Monad m) => NameAssoc -> String -> String -> m Gdx
137 lookupGroup ktg nname gname =
138 case M.lookup gname ktg of
139 Nothing -> fail $ "Unknown group '" ++ gname ++ "' for node " ++ nname
140 Just idx -> return idx
142 -- | Check for prefix matches in names.
143 -- Implemented in Ganeti core utils.text.MatchNameComponent
144 -- as the regexp r"^%s(\..*)?$" % re.escape(key)
145 prefixMatch :: String -- ^ Lookup
146 -> String -- ^ Full name
147 -> Bool -- ^ Whether there is a prefix match
148 prefixMatch lkp = isPrefixOf (lkp ++ ".")
150 -- | Is the lookup priority a "good" one?
151 goodMatchPriority :: MatchPriority -> Bool
152 goodMatchPriority ExactMatch = True
153 goodMatchPriority PartialMatch = True
154 goodMatchPriority _ = False
156 -- | Is the lookup result an actual match?
157 goodLookupResult :: LookupResult -> Bool
158 goodLookupResult = goodMatchPriority . lrMatchPriority
160 -- | Compares a canonical name and a lookup string.
161 compareNameComponent :: String -- ^ Canonical (target) name
162 -> String -- ^ Partial (lookup) name
163 -> LookupResult -- ^ Result of the lookup
164 compareNameComponent cnl lkp =
165 select (LookupResult FailMatch lkp)
166 [ (cnl == lkp , LookupResult ExactMatch cnl)
167 , (prefixMatch lkp cnl , LookupResult PartialMatch cnl)
170 -- | Lookup a string and choose the best result.
171 chooseLookupResult :: String -- ^ Lookup key
172 -> String -- ^ String to compare to the lookup key
173 -> LookupResult -- ^ Previous result
174 -> LookupResult -- ^ New result
175 chooseLookupResult lkp cstr old =
176 -- default: use class order to pick the minimum result
179 -- short circuit if the new result is an exact match
180 [ ((lrMatchPriority new) == ExactMatch, new)
181 -- if both are partial matches generate a multiple match
182 , (partial2, LookupResult MultipleMatch lkp)
183 ] where new = compareNameComponent cstr lkp
184 partial2 = all ((PartialMatch==) . lrMatchPriority) [old, new]
186 -- | Find the canonical name for a lookup string in a list of names.
187 lookupName :: [String] -- ^ List of keys
188 -> String -- ^ Lookup string
189 -> LookupResult -- ^ Result of the lookup
190 lookupName l s = foldr (chooseLookupResult s)
191 (LookupResult FailMatch s) l
193 -- | Given a list of elements (and their names), assign indices to them.
194 assignIndices :: (Element a) =>
196 -> (NameAssoc, Container.Container a)
197 assignIndices nodes =
199 unzip . map (\ (idx, (k, v)) -> ((k, idx), (idx, setIdx v idx)))
201 in (M.fromList na, Container.fromList idx_node)
203 -- | For each instance, add its index to its primary and secondary nodes.
204 fixNodes :: Node.List
209 pdx = Instance.pNode inst
210 sdx = Instance.sNode inst
211 pold = Container.find pdx accu
212 pnew = Node.setPri pold inst
213 ac2 = Container.add pdx pnew accu
215 if sdx /= Node.noSecondary
216 then let sold = Container.find sdx accu
217 snew = Node.setSec sold inst
218 in Container.add sdx snew ac2
221 -- | Remove non-selected tags from the exclusion list.
222 filterExTags :: [String] -> Instance.Instance -> Instance.Instance
223 filterExTags tl inst =
224 let old_tags = Instance.tags inst
225 new_tags = filter (\tag -> any (`isPrefixOf` tag) tl)
227 in inst { Instance.tags = new_tags }
229 -- | Update the movable attribute.
230 updateMovable :: [String] -- ^ Selected instances (if not empty)
231 -> [String] -- ^ Excluded instances
232 -> Instance.Instance -- ^ Target Instance
233 -> Instance.Instance -- ^ Target Instance with updated attribute
234 updateMovable selinsts exinsts inst =
235 if Instance.sNode inst == Node.noSecondary ||
236 Instance.name inst `elem` exinsts ||
237 not (null selinsts || Instance.name inst `elem` selinsts)
238 then Instance.setMovable inst False
241 -- | Compute the longest common suffix of a list of strings that
242 -- starts with a dot.
243 longestDomain :: [String] -> String
244 longestDomain [] = ""
245 longestDomain (x:xs) =
246 foldr (\ suffix accu -> if all (isSuffixOf suffix) xs
249 "" $ filter (isPrefixOf ".") (tails x)
251 -- | Extracts the exclusion tags from the cluster configuration.
252 extractExTags :: [String] -> [String]
254 map (drop (length exTagsPrefix)) .
255 filter (isPrefixOf exTagsPrefix)
257 -- | Extracts the common suffix from node\/instance names.
258 commonSuffix :: Node.List -> Instance.List -> String
260 let node_names = map Node.name $ Container.elems nl
261 inst_names = map Instance.name $ Container.elems il
262 in longestDomain (node_names ++ inst_names)
264 -- | Initializer function that loads the data from a node and instance
265 -- list and massages it into the correct format.
266 mergeData :: [(String, DynUtil)] -- ^ Instance utilisation data
267 -> [String] -- ^ Exclusion tags
268 -> [String] -- ^ Selected instances (if not empty)
269 -> [String] -- ^ Excluded instances
270 -> ClusterData -- ^ Data from backends
271 -> Result ClusterData -- ^ Fixed cluster data
272 mergeData um extags selinsts exinsts cdata@(ClusterData _ nl il2 tags) =
273 let il = Container.elems il2
274 il3 = foldl' (\im (name, n_util) ->
275 case Container.findByName im name of
276 Nothing -> im -- skipping unknown instance
278 let new_i = inst { Instance.util = n_util }
279 in Container.add (Instance.idx inst) new_i im
281 allextags = extags ++ extractExTags tags
282 inst_names = map Instance.name il
283 selinst_lkp = map (lookupName inst_names) selinsts
284 exinst_lkp = map (lookupName inst_names) exinsts
285 lkp_unknown = filter (not . goodLookupResult) (selinst_lkp ++ exinst_lkp)
286 selinst_names = map lrContent selinst_lkp
287 exinst_names = map lrContent exinst_lkp
288 il4 = Container.map (filterExTags allextags .
289 updateMovable selinst_names exinst_names) il3
290 nl2 = foldl' fixNodes nl (Container.elems il4)
291 nl3 = Container.map (flip Node.buildPeers il4) nl2
292 node_names = map Node.name (Container.elems nl)
293 common_suffix = longestDomain (node_names ++ inst_names)
294 snl = Container.map (computeAlias common_suffix) nl3
295 sil = Container.map (computeAlias common_suffix) il4
296 in if' (null lkp_unknown)
297 (Ok cdata { cdNodes = snl, cdInstances = sil })
298 (Bad $ "Unknown instance(s): " ++ show(map lrContent lkp_unknown))
300 -- | Checks the cluster data for consistency.
301 checkData :: Node.List -> Instance.List
302 -> ([String], Node.List)
306 let nname = Node.name node
307 nilst = map (`Container.find` il) (Node.pList node)
308 dilst = filter (not . Instance.running) nilst
309 adj_mem = sum . map Instance.mem $ dilst
310 delta_mem = truncate (Node.tMem node)
315 delta_dsk = truncate (Node.tDsk node)
318 newn = Node.setFmem (Node.setXmem node delta_mem)
319 (Node.fMem node - adj_mem)
320 umsg1 = [printf "node %s is missing %d MB ram \
322 nname delta_mem (delta_dsk `div` 1024) |
323 delta_mem > 512 || delta_dsk > 1024]::[String]
324 in (msgs ++ umsg1, newn)
327 -- | Compute the amount of memory used by primary instances on a node.
328 nodeImem :: Node.Node -> Instance.List -> Int
330 let rfind = flip Container.find il
331 in sum . map (Instance.mem . rfind)
334 -- | Compute the amount of disk used by instances on a node (either primary
336 nodeIdsk :: Node.Node -> Instance.List -> Int
338 let rfind = flip Container.find il
339 in sum . map (Instance.dsk . rfind)
340 $ Node.pList node ++ Node.sList node