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, 2012 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
48 import qualified Data.Map as M
50 import Text.Printf (printf)
51 import System.Time (ClockTime(..))
53 import qualified Ganeti.HTools.Container as Container
54 import qualified Ganeti.HTools.Instance as Instance
55 import qualified Ganeti.HTools.Node as Node
56 import qualified Ganeti.HTools.Group as Group
57 import qualified Ganeti.HTools.Cluster as Cluster
59 import Ganeti.BasicTypes
60 import qualified Ganeti.Constants as C
61 import Ganeti.HTools.Types
66 -- | The exclusion tag prefix.
67 exTagsPrefix :: String
68 exTagsPrefix = "htools:iextags:"
72 {-| The iallocator request type.
74 This type denotes what request we got from Ganeti and also holds
75 request-specific fields.
79 = Allocate Instance.Instance Int -- ^ A new instance allocation
80 | Relocate Idx Int [Ndx] -- ^ Choose a new secondary node
81 | NodeEvacuate [Idx] EvacMode -- ^ node-evacuate mode
82 | ChangeGroup [Gdx] [Idx] -- ^ Multi-relocate mode
83 | MultiAllocate [(Instance.Instance, Int)] -- ^ Multi-allocate mode
86 -- | A complete request, as received from Ganeti.
87 data Request = Request RqType ClusterData
90 -- | The cluster state.
91 data ClusterData = ClusterData
92 { cdGroups :: Group.List -- ^ The node group list
93 , cdNodes :: Node.List -- ^ The node list
94 , cdInstances :: Instance.List -- ^ The instance list
95 , cdTags :: [String] -- ^ The cluster tags
96 , cdIPolicy :: IPolicy -- ^ The cluster instance policy
99 -- | An empty cluster.
100 emptyCluster :: ClusterData
101 emptyCluster = ClusterData Container.empty Container.empty Container.empty []
106 -- | Lookups a node into an assoc list.
107 lookupNode :: (Monad m) => NameAssoc -> String -> String -> m Ndx
108 lookupNode ktn inst node =
109 maybe (fail $ "Unknown node '" ++ node ++ "' for instance " ++ inst) return $
112 -- | Lookups an instance into an assoc list.
113 lookupInstance :: (Monad m) => NameAssoc -> String -> m Idx
114 lookupInstance kti inst =
115 maybe (fail $ "Unknown instance '" ++ inst ++ "'") return $ M.lookup inst kti
117 -- | Lookups a group into an assoc list.
118 lookupGroup :: (Monad m) => NameAssoc -> String -> String -> m Gdx
119 lookupGroup ktg nname gname =
120 maybe (fail $ "Unknown group '" ++ gname ++ "' for node " ++ nname) return $
123 -- | Given a list of elements (and their names), assign indices to them.
124 assignIndices :: (Element a) =>
126 -> (NameAssoc, Container.Container a)
127 assignIndices name_element =
128 let (name_idx, idx_element) =
129 unzip . map (\ (idx, (k, v)) -> ((k, idx), (idx, setIdx v idx)))
130 . zip [0..] $ name_element
131 in (M.fromList name_idx, Container.fromList idx_element)
133 -- | Given am indexed node list, and the name of the master, mark it as such.
134 setMaster :: (Monad m) => NameAssoc -> Node.List -> String -> m Node.List
135 setMaster node_names node_idx master = do
136 kmaster <- maybe (fail $ "Master node " ++ master ++ " unknown") return $
137 M.lookup master node_names
138 let mnode = Container.find kmaster node_idx
139 return $ Container.add kmaster (Node.setMaster mnode True) node_idx
141 -- | For each instance, add its index to its primary and secondary nodes.
142 fixNodes :: Node.List
146 let pdx = Instance.pNode inst
147 sdx = Instance.sNode inst
148 pold = Container.find pdx accu
149 pnew = Node.setPri pold inst
150 ac2 = Container.add pdx pnew accu
151 in if sdx /= Node.noSecondary
152 then let sold = Container.find sdx accu
153 snew = Node.setSec sold inst
154 in Container.add sdx snew ac2
157 -- | Set the node's policy to its group one. Note that this requires
158 -- the group to exist (should have been checked before), otherwise it
159 -- will abort with a runtime error.
160 setNodePolicy :: Group.List -> Node.Node -> Node.Node
161 setNodePolicy gl node =
162 let grp = Container.find (Node.group node) gl
163 gpol = Group.iPolicy grp
164 in Node.setPolicy gpol node
166 -- | Update instance with exclusion tags list.
167 updateExclTags :: [String] -> Instance.Instance -> Instance.Instance
168 updateExclTags tl inst =
169 let allTags = Instance.allTags inst
170 exclTags = filter (\tag -> any (`isPrefixOf` tag) tl) allTags
171 in inst { Instance.exclTags = exclTags }
173 -- | Update the movable attribute.
174 updateMovable :: [String] -- ^ Selected instances (if not empty)
175 -> [String] -- ^ Excluded instances
176 -> Instance.Instance -- ^ Target Instance
177 -> Instance.Instance -- ^ Target Instance with updated attribute
178 updateMovable selinsts exinsts inst =
179 if Instance.name inst `elem` exinsts ||
180 not (null selinsts || Instance.name inst `elem` selinsts)
181 then Instance.setMovable inst False
184 -- | Disables moves for instances with a split group.
185 disableSplitMoves :: Node.List -> Instance.Instance -> Instance.Instance
186 disableSplitMoves nl inst =
187 if not . isOk . Cluster.instanceGroup nl $ inst
188 then Instance.setMovable inst False
191 -- | Set the auto-repair policy for an instance.
192 setArPolicy :: [String] -- ^ Cluster tags
193 -> Group.List -- ^ List of node groups
194 -> Node.List -- ^ List of nodes
195 -> Instance.List -- ^ List of instances
196 -> ClockTime -- ^ Current timestamp, to evaluate ArSuspended
197 -> Instance.List -- ^ Updated list of instances
198 setArPolicy ctags gl nl il time =
199 let getArPolicy' = flip getArPolicy time
200 cpol = fromMaybe ArNotEnabled $ getArPolicy' ctags
201 gpols = Container.map (fromMaybe cpol . getArPolicy' . Group.allTags) gl
202 ipolfn = getArPolicy' . Instance.allTags
203 nlookup = flip Container.find nl . Instance.pNode
204 glookup = flip Container.find gpols . Node.group . nlookup
205 updateInstance inst = inst {
206 Instance.arPolicy = fromMaybe (glookup inst) $ ipolfn inst }
208 Container.map updateInstance il
210 -- | Get the auto-repair policy from a list of tags.
212 -- This examines the ganeti:watcher:autorepair and
213 -- ganeti:watcher:autorepair:suspend tags to determine the policy. If none of
214 -- these tags are present, Nothing (and not ArNotEnabled) is returned.
215 getArPolicy :: [String] -> ClockTime -> Maybe AutoRepairPolicy
216 getArPolicy tags time =
217 let enabled = mapMaybe (autoRepairTypeFromRaw <=<
218 chompPrefix C.autoRepairTagEnabled) tags
219 suspended = mapMaybe (chompPrefix C.autoRepairTagSuspended) tags
220 futureTs = filter (> time) . map (flip TOD 0) $
221 mapMaybe (tryRead "auto-repair suspend time") suspended
224 -- Note how we must return ArSuspended even if "enabled" is empty, so that
225 -- node groups or instances can suspend repairs that were enabled at an
226 -- upper scope (cluster or node group).
227 _ | "" `elem` suspended -> Just $ ArSuspended Forever
228 | not $ null futureTs -> Just . ArSuspended . Until . maximum $ futureTs
229 | not $ null enabled -> Just $ ArEnabled (minimum enabled)
230 | otherwise -> Nothing
232 -- | Compute the longest common suffix of a list of strings that
233 -- starts with a dot.
234 longestDomain :: [String] -> String
235 longestDomain [] = ""
236 longestDomain (x:xs) =
237 foldr (\ suffix accu -> if all (isSuffixOf suffix) xs
240 "" $ filter (isPrefixOf ".") (tails x)
242 -- | Extracts the exclusion tags from the cluster configuration.
243 extractExTags :: [String] -> [String]
244 extractExTags = filter (not . null) . mapMaybe (chompPrefix exTagsPrefix)
246 -- | Extracts the common suffix from node\/instance names.
247 commonSuffix :: Node.List -> Instance.List -> String
249 let node_names = map Node.name $ Container.elems nl
250 inst_names = map Instance.name $ Container.elems il
251 in longestDomain (node_names ++ inst_names)
253 -- | Initializer function that loads the data from a node and instance
254 -- list and massages it into the correct format.
255 mergeData :: [(String, DynUtil)] -- ^ Instance utilisation data
256 -> [String] -- ^ Exclusion tags
257 -> [String] -- ^ Selected instances (if not empty)
258 -> [String] -- ^ Excluded instances
259 -> ClockTime -- ^ The current timestamp
260 -> ClusterData -- ^ Data from backends
261 -> Result ClusterData -- ^ Fixed cluster data
262 mergeData um extags selinsts exinsts time cdata@(ClusterData gl nl il ctags _) =
263 let il2 = setArPolicy ctags gl nl il time
264 il3 = foldl' (\im (name, n_util) ->
265 case Container.findByName im name of
266 Nothing -> im -- skipping unknown instance
268 let new_i = inst { Instance.util = n_util }
269 in Container.add (Instance.idx inst) new_i im
271 allextags = extags ++ extractExTags ctags
272 inst_names = map Instance.name $ Container.elems il3
273 selinst_lkp = map (lookupName inst_names) selinsts
274 exinst_lkp = map (lookupName inst_names) exinsts
275 lkp_unknown = filter (not . goodLookupResult) (selinst_lkp ++ exinst_lkp)
276 selinst_names = map lrContent selinst_lkp
277 exinst_names = map lrContent exinst_lkp
278 node_names = map Node.name (Container.elems nl)
279 common_suffix = longestDomain (node_names ++ inst_names)
280 il4 = Container.map (computeAlias common_suffix .
281 updateExclTags allextags .
282 updateMovable selinst_names exinst_names) il3
283 nl2 = foldl' fixNodes nl (Container.elems il4)
284 nl3 = Container.map (setNodePolicy gl .
285 computeAlias common_suffix .
286 (`Node.buildPeers` il4)) nl2
287 il5 = Container.map (disableSplitMoves nl3) il4
288 in if' (null lkp_unknown)
289 (Ok cdata { cdNodes = nl3, cdInstances = il5 })
290 (Bad $ "Unknown instance(s): " ++ show(map lrContent lkp_unknown))
292 -- | In a cluster description, clear dynamic utilisation information.
293 clearDynU :: ClusterData -> Result ClusterData
294 clearDynU cdata@(ClusterData _ _ il _ _) =
295 let il2 = Container.map (\ inst -> inst {Instance.util = zeroUtil }) il
296 in Ok cdata { cdInstances = il2 }
298 -- | Checks the cluster data for consistency.
299 checkData :: Node.List -> Instance.List
300 -> ([String], Node.List)
304 let nname = Node.name node
305 nilst = map (`Container.find` il) (Node.pList node)
306 dilst = filter Instance.instanceDown nilst
307 adj_mem = sum . map Instance.mem $ dilst
308 delta_mem = truncate (Node.tMem node)
313 delta_dsk = truncate (Node.tDsk node)
316 newn = Node.setFmem (Node.setXmem node delta_mem)
317 (Node.fMem node - adj_mem)
319 if delta_mem > 512 || delta_dsk > 1024
320 then printf "node %s is missing %d MB ram \
322 nname delta_mem (delta_dsk `div` 1024):msgs
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 il' = map rfind $ Node.pList node
332 oil' = filter Instance.notOffline il'
333 in sum . map Instance.mem $ oil'
336 -- | Compute the amount of disk used by instances on a node (either primary
338 nodeIdsk :: Node.Node -> Instance.List -> Int
340 let rfind = flip Container.find il
341 in sum . map (Instance.dsk . rfind)
342 $ Node.pList node ++ Node.sList node
344 -- | Get live information or a default value
345 eitherLive :: (Monad m) => Bool -> a -> m a -> m a
346 eitherLive True _ live_data = live_data
347 eitherLive False def_data _ = return def_data