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