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