1 {-| Cluster space sizing
7 Copyright (C) 2009, 2010, 2011, 2012 Google Inc.
9 This program is free software; you can redistribute it and/or modify
10 it under the terms of the GNU General Public License as published by
11 the Free Software Foundation; either version 2 of the License, or
12 (at your option) any later version.
14 This program is distributed in the hope that it will be useful, but
15 WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17 General Public License for more details.
19 You should have received a copy of the GNU General Public License
20 along with this program; if not, write to the Free Software
21 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 module Ganeti.HTools.Program.Hspace (main, options) where
29 import Data.Char (toUpper, isAlphaNum, toLower)
30 import Data.Function (on)
32 import Data.Maybe (fromMaybe)
33 import Data.Ord (comparing)
37 import Text.Printf (printf, hPrintf)
39 import qualified Ganeti.HTools.Container as Container
40 import qualified Ganeti.HTools.Cluster as Cluster
41 import qualified Ganeti.HTools.Node as Node
42 import qualified Ganeti.HTools.Instance as Instance
44 import Ganeti.HTools.Utils
45 import Ganeti.HTools.Types
46 import Ganeti.HTools.CLI
47 import Ganeti.HTools.ExtLoader
48 import Ganeti.HTools.Loader
50 -- | Options list and functions.
75 -- | The allocation phase we're in (initial, after tiered allocs, or
76 -- after regular allocation).
81 -- | The kind of instance spec we print.
82 data SpecType = SpecNormal
85 -- | What we prefix a spec with.
86 specPrefix :: SpecType -> String
87 specPrefix SpecNormal = "SPEC"
88 specPrefix SpecTiered = "TSPEC_INI"
90 -- | The description of a spec.
91 specDescription :: SpecType -> String
92 specDescription SpecNormal = "Standard (fixed-size)"
93 specDescription SpecTiered = "Tiered (initial size)"
95 -- | Efficiency generic function.
96 effFn :: (Cluster.CStats -> Integer)
97 -> (Cluster.CStats -> Double)
98 -> Cluster.CStats -> Double
99 effFn fi ft cs = fromIntegral (fi cs) / ft cs
101 -- | Memory efficiency.
102 memEff :: Cluster.CStats -> Double
103 memEff = effFn Cluster.csImem Cluster.csTmem
105 -- | Disk efficiency.
106 dskEff :: Cluster.CStats -> Double
107 dskEff = effFn Cluster.csIdsk Cluster.csTdsk
110 cpuEff :: Cluster.CStats -> Double
111 cpuEff = effFn Cluster.csIcpu (fromIntegral . Cluster.csVcpu)
113 -- | Holds data for converting a 'Cluster.CStats' structure into
114 -- detailed statictics.
115 statsData :: [(String, Cluster.CStats -> String)]
116 statsData = [ ("SCORE", printf "%.8f" . Cluster.csScore)
117 , ("INST_CNT", printf "%d" . Cluster.csNinst)
118 , ("MEM_FREE", printf "%d" . Cluster.csFmem)
119 , ("MEM_AVAIL", printf "%d" . Cluster.csAmem)
121 \cs -> printf "%d" (Cluster.csFmem cs - Cluster.csAmem cs))
122 , ("MEM_INST", printf "%d" . Cluster.csImem)
124 \cs -> printf "%d" (Cluster.csXmem cs + Cluster.csNmem cs))
125 , ("MEM_EFF", printf "%.8f" . memEff)
126 , ("DSK_FREE", printf "%d" . Cluster.csFdsk)
127 , ("DSK_AVAIL", printf "%d". Cluster.csAdsk)
129 \cs -> printf "%d" (Cluster.csFdsk cs - Cluster.csAdsk cs))
130 , ("DSK_INST", printf "%d" . Cluster.csIdsk)
131 , ("DSK_EFF", printf "%.8f" . dskEff)
132 , ("CPU_INST", printf "%d" . Cluster.csIcpu)
133 , ("CPU_EFF", printf "%.8f" . cpuEff)
134 , ("MNODE_MEM_AVAIL", printf "%d" . Cluster.csMmem)
135 , ("MNODE_DSK_AVAIL", printf "%d" . Cluster.csMdsk)
138 -- | List holding 'RSpec' formatting information.
139 specData :: [(String, RSpec -> String)]
140 specData = [ ("MEM", printf "%d" . rspecMem)
141 , ("DSK", printf "%d" . rspecDsk)
142 , ("CPU", printf "%d" . rspecCpu)
145 -- | List holding 'Cluster.CStats' formatting information.
146 clusterData :: [(String, Cluster.CStats -> String)]
147 clusterData = [ ("MEM", printf "%.0f" . Cluster.csTmem)
148 , ("DSK", printf "%.0f" . Cluster.csTdsk)
149 , ("CPU", printf "%.0f" . Cluster.csTcpu)
150 , ("VCPU", printf "%d" . Cluster.csVcpu)
153 -- | Function to print stats for a given phase.
154 printStats :: Phase -> Cluster.CStats -> [(String, String)]
156 map (\(s, fn) -> (printf "%s_%s" kind s, fn cs)) statsData
157 where kind = case ph of
162 -- | Print failure reason and scores
163 printFRScores :: Node.List -> Node.List -> [(FailMode, Int)] -> IO ()
164 printFRScores ini_nl fin_nl sreason = do
165 printf " - most likely failure reason: %s\n" $ failureReason sreason::IO ()
166 printClusterScores ini_nl fin_nl
167 printClusterEff (Cluster.totalResources fin_nl)
169 -- | Print final stats and related metrics.
170 printResults :: Bool -> Node.List -> Node.List -> Int -> Int
171 -> [(FailMode, Int)] -> IO ()
172 printResults True _ fin_nl num_instances allocs sreason = do
173 let fin_stats = Cluster.totalResources fin_nl
174 fin_instances = num_instances + allocs
176 when (num_instances + allocs /= Cluster.csNinst fin_stats) $
178 hPrintf stderr "ERROR: internal inconsistency, allocated (%d)\
179 \ != counted (%d)\n" (num_instances + allocs)
180 (Cluster.csNinst fin_stats) :: IO ()
181 exitWith $ ExitFailure 1
183 printKeys $ printStats PFinal fin_stats
184 printKeys [ ("ALLOC_USAGE", printf "%.8f"
185 ((fromIntegral num_instances::Double) /
186 fromIntegral fin_instances))
187 , ("ALLOC_INSTANCES", printf "%d" allocs)
188 , ("ALLOC_FAIL_REASON", map toUpper . show . fst $ head sreason)
190 printKeys $ map (\(x, y) -> (printf "ALLOC_%s_CNT" (show x),
191 printf "%d" y)) sreason
193 printResults False ini_nl fin_nl _ allocs sreason = do
194 putStrLn "Normal (fixed-size) allocation results:"
195 printf " - %3d instances allocated\n" allocs :: IO ()
196 printFRScores ini_nl fin_nl sreason
198 -- | Prints the final @OK@ marker in machine readable output.
199 printFinal :: Bool -> IO ()
201 -- this should be the final entry
202 printKeys [("OK", "1")]
204 printFinal False = return ()
206 -- | Compute the tiered spec counts from a list of allocated
208 tieredSpecMap :: [Instance.Instance]
210 tieredSpecMap trl_ixes =
211 let fin_trl_ixes = reverse trl_ixes
212 ix_byspec = groupBy ((==) `on` Instance.specOf) fin_trl_ixes
213 spec_map = map (\ixs -> (Instance.specOf $ head ixs, length ixs))
217 -- | Formats a spec map to strings.
218 formatSpecMap :: [(RSpec, Int)] -> [String]
220 map (\(spec, cnt) -> printf "%d,%d,%d=%d" (rspecMem spec)
221 (rspecDsk spec) (rspecCpu spec) cnt)
223 -- | Formats \"key-metrics\" values.
224 formatRSpec :: String -> AllocInfo -> [(String, String)]
226 [ ("KM_" ++ s ++ "_CPU", show $ allocInfoVCpus r)
227 , ("KM_" ++ s ++ "_NPU", show $ allocInfoNCpus r)
228 , ("KM_" ++ s ++ "_MEM", show $ allocInfoMem r)
229 , ("KM_" ++ s ++ "_DSK", show $ allocInfoDisk r)
232 -- | Shows allocations stats.
233 printAllocationStats :: Node.List -> Node.List -> IO ()
234 printAllocationStats ini_nl fin_nl = do
235 let ini_stats = Cluster.totalResources ini_nl
236 fin_stats = Cluster.totalResources fin_nl
237 (rini, ralo, runa) = Cluster.computeAllocationDelta ini_stats fin_stats
238 printKeys $ formatRSpec "USED" rini
239 printKeys $ formatRSpec "POOL" ralo
240 printKeys $ formatRSpec "UNAV" runa
242 -- | Ensure a value is quoted if needed.
243 ensureQuoted :: String -> String
244 ensureQuoted v = if not (all (\c -> isAlphaNum c || c == '.') v)
248 -- | Format a list of key\/values as a shell fragment.
249 printKeys :: [(String, String)] -> IO ()
250 printKeys = mapM_ (\(k, v) ->
251 printf "HTS_%s=%s\n" (map toUpper k) (ensureQuoted v))
253 -- | Converts instance data to a list of strings.
254 printInstance :: Node.List -> Instance.Instance -> [String]
255 printInstance nl i = [ Instance.name i
256 , Container.nameOf nl $ Instance.pNode i
257 , let sdx = Instance.sNode i
258 in if sdx == Node.noSecondary then ""
259 else Container.nameOf nl sdx
260 , show (Instance.mem i)
261 , show (Instance.dsk i)
262 , show (Instance.vcpus i)
265 -- | Optionally print the allocation map.
266 printAllocationMap :: Int -> String
267 -> Node.List -> [Instance.Instance] -> IO ()
268 printAllocationMap verbose msg nl ixes =
269 when (verbose > 1) $ do
270 hPutStrLn stderr (msg ++ " map")
271 hPutStr stderr . unlines . map ((:) ' ' . unwords) $
272 formatTable (map (printInstance nl) (reverse ixes))
273 -- This is the numberic-or-not field
274 -- specification; the first three fields are
275 -- strings, whereas the rest are numeric
276 [False, False, False, True, True, True]
278 -- | Formats nicely a list of resources.
279 formatResources :: a -> [(String, a->String)] -> String
280 formatResources res =
281 intercalate ", " . map (\(a, fn) -> a ++ " " ++ fn res)
283 -- | Print the cluster resources.
284 printCluster :: Bool -> Cluster.CStats -> Int -> IO ()
285 printCluster True ini_stats node_count = do
286 printKeys $ map (\(a, fn) -> ("CLUSTER_" ++ a, fn ini_stats)) clusterData
287 printKeys [("CLUSTER_NODES", printf "%d" node_count)]
288 printKeys $ printStats PInitial ini_stats
290 printCluster False ini_stats node_count = do
291 printf "The cluster has %d nodes and the following resources:\n %s.\n"
292 node_count (formatResources ini_stats clusterData)::IO ()
293 printf "There are %s initial instances on the cluster.\n"
294 (if inst_count > 0 then show inst_count else "no" )
295 where inst_count = Cluster.csNinst ini_stats
297 -- | Prints the normal instance spec.
298 printISpec :: Bool -> RSpec -> SpecType -> DiskTemplate -> IO ()
299 printISpec True ispec spec disk_template = do
300 printKeys $ map (\(a, fn) -> (prefix ++ "_" ++ a, fn ispec)) specData
301 printKeys [ (prefix ++ "_RQN", printf "%d" req_nodes) ]
302 printKeys [ (prefix ++ "_DISK_TEMPLATE",
303 diskTemplateToRaw disk_template) ]
304 where req_nodes = Instance.requiredNodes disk_template
305 prefix = specPrefix spec
307 printISpec False ispec spec disk_template =
308 printf "%s instance spec is:\n %s, using disk\
310 (specDescription spec)
311 (formatResources ispec specData) (diskTemplateToRaw disk_template)
313 -- | Prints the tiered results.
314 printTiered :: Bool -> [(RSpec, Int)]
315 -> Node.List -> Node.List -> [(FailMode, Int)] -> IO ()
316 printTiered True spec_map nl trl_nl _ = do
317 printKeys $ printStats PTiered (Cluster.totalResources trl_nl)
318 printKeys [("TSPEC", unwords (formatSpecMap spec_map))]
319 printAllocationStats nl trl_nl
321 printTiered False spec_map ini_nl fin_nl sreason = do
322 _ <- printf "Tiered allocation results:\n"
324 then putStrLn " - no instances allocated"
325 else mapM_ (\(ispec, cnt) ->
326 printf " - %3d instances of spec %s\n" cnt
327 (formatResources ispec specData)) spec_map
328 printFRScores ini_nl fin_nl sreason
330 -- | Displays the initial/final cluster scores.
331 printClusterScores :: Node.List -> Node.List -> IO ()
332 printClusterScores ini_nl fin_nl = do
333 printf " - initial cluster score: %.8f\n" $ Cluster.compCV ini_nl::IO ()
334 printf " - final cluster score: %.8f\n" $ Cluster.compCV fin_nl
336 -- | Displays the cluster efficiency.
337 printClusterEff :: Cluster.CStats -> IO ()
340 printf " - %s usage efficiency: %5.2f%%\n" s (fn cs * 100))
345 -- | Computes the most likely failure reason.
346 failureReason :: [(FailMode, Int)] -> String
347 failureReason = show . fst . head
349 -- | Sorts the failure reasons.
350 sortReasons :: [(FailMode, Int)] -> [(FailMode, Int)]
351 sortReasons = reverse . sortBy (comparing snd)
353 -- | Aborts the program if we get a bad value.
354 exitIfBad :: Result a -> IO a
356 hPrintf stderr "Failure: %s\n" s >> exitWith (ExitFailure 1)
357 exitIfBad (Ok v) = return v
359 -- | Runs an allocation algorithm and saves cluster state.
360 runAllocation :: ClusterData -- ^ Cluster data
361 -> Maybe Cluster.AllocResult -- ^ Optional stop-allocation
362 -> Result Cluster.AllocResult -- ^ Allocation result
363 -> RSpec -- ^ Requested instance spec
364 -> DiskTemplate -- ^ Requested disk template
365 -> SpecType -- ^ Allocation type
366 -> Options -- ^ CLI options
367 -> IO (FailStats, Node.List, Int, [(RSpec, Int)])
368 runAllocation cdata stop_allocation actual_result spec dt mode opts = do
369 (reasons, new_nl, new_il, new_ixes, _) <-
370 case stop_allocation of
371 Just result_noalloc -> return result_noalloc
372 Nothing -> exitIfBad actual_result
374 let name = head . words . specDescription $ mode
375 descr = name ++ " allocation"
376 ldescr = "after " ++ map toLower descr
378 printISpec (optMachineReadable opts) spec mode dt
380 printAllocationMap (optVerbose opts) descr new_nl new_ixes
382 maybePrintNodes (optShowNodes opts) descr (Cluster.printNodes new_nl)
384 maybeSaveData (optSaveCluster opts) (map toLower name) ldescr
385 (cdata { cdNodes = new_nl, cdInstances = new_il})
387 return (sortReasons reasons, new_nl, length new_ixes, tieredSpecMap new_ixes)
389 -- | Create an instance from a given spec.
390 instFromSpec :: RSpec -> DiskTemplate -> Int -> Instance.Instance
391 instFromSpec spx disk_template su =
392 Instance.create "new" (rspecMem spx) (rspecDsk spx)
393 (rspecCpu spx) Running [] True (-1) (-1) disk_template su
396 main :: Options -> [String] -> IO ()
398 unless (null args) $ do
399 hPutStrLn stderr "Error: this program doesn't take any arguments."
400 exitWith $ ExitFailure 1
402 let verbose = optVerbose opts
403 machine_r = optMachineReadable opts
405 orig_cdata@(ClusterData gl fixed_nl il _ ipol) <- loadExternalData opts
406 nl <- setNodeStatus opts fixed_nl
408 cluster_disk_template <-
409 case iPolicyDiskTemplates ipol of
410 first_templ:_ -> return first_templ
412 _ <- hPutStrLn stderr $ "Error: null list of disk templates\
413 \ received from cluster!"
414 exitWith $ ExitFailure 1
416 let num_instances = Container.size il
417 all_nodes = Container.elems fixed_nl
418 cdata = orig_cdata { cdNodes = fixed_nl }
419 disk_template = fromMaybe cluster_disk_template (optDiskTemplate opts)
420 req_nodes = Instance.requiredNodes disk_template
421 csf = commonSuffix fixed_nl il
422 su = fromMaybe (iSpecSpindleUse $ iPolicyStdSpec ipol)
425 when (not (null csf) && verbose > 1) $
426 hPrintf stderr "Note: Stripping common suffix of '%s' from names\n" csf
428 maybePrintNodes (optShowNodes opts) "Initial cluster" (Cluster.printNodes nl)
431 hPrintf stderr "Initial coefficients: overall %.8f\n%s"
432 (Cluster.compCV nl) (Cluster.printStats " " nl)
434 printCluster machine_r (Cluster.totalResources nl) (length all_nodes)
436 let stop_allocation = case Cluster.computeBadItems nl il of
438 _ -> Just ([(FailN1, 1)]::FailStats, nl, il, [], [])
439 alloclimit = if optMaxLength opts == -1
441 else Just (optMaxLength opts)
443 allocnodes <- exitIfBad $ Cluster.genAllocNodes gl nl req_nodes True
445 -- Run the tiered allocation
447 let tspec = fromMaybe (rspecFromISpec (iPolicyMaxSpec ipol))
450 (treason, trl_nl, _, spec_map) <-
451 runAllocation cdata stop_allocation
452 (Cluster.tieredAlloc nl il alloclimit
453 (instFromSpec tspec disk_template su) allocnodes [] [])
454 tspec disk_template SpecTiered opts
456 printTiered machine_r spec_map nl trl_nl treason
458 -- Run the standard (avg-mode) allocation
460 let ispec = fromMaybe (rspecFromISpec (iPolicyStdSpec ipol))
463 (sreason, fin_nl, allocs, _) <-
464 runAllocation cdata stop_allocation
465 (Cluster.iterateAlloc nl il alloclimit
466 (instFromSpec ispec disk_template su) allocnodes [] [])
467 ispec disk_template SpecNormal opts
469 printResults machine_r nl fin_nl num_instances allocs sreason
471 -- Print final result