Hspace.hs 17 KB
Newer Older
Iustin Pop's avatar
Iustin Pop committed
1
2
3
4
5
6
{-| Cluster space sizing

-}

{-

Iustin Pop's avatar
Iustin Pop committed
7
Copyright (C) 2009, 2010, 2011 Google Inc.
Iustin Pop's avatar
Iustin Pop committed
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
02110-1301, USA.

-}

26
module Ganeti.HTools.Program.Hspace (main) where
Iustin Pop's avatar
Iustin Pop committed
27

Iustin Pop's avatar
Iustin Pop committed
28
import Control.Monad
Iustin Pop's avatar
Iustin Pop committed
29
import Data.Char (toUpper, isAlphaNum, toLower)
30
import Data.Function (on)
Iustin Pop's avatar
Iustin Pop committed
31
import Data.List
32
import Data.Maybe (fromMaybe)
33
import Data.Ord (comparing)
34
import System.Exit
Iustin Pop's avatar
Iustin Pop committed
35
import System.IO
36
import System.Environment (getArgs)
Iustin Pop's avatar
Iustin Pop committed
37

38
import Text.Printf (printf, hPrintf)
Iustin Pop's avatar
Iustin Pop committed
39
40
41
42
43
44
45

import qualified Ganeti.HTools.Container as Container
import qualified Ganeti.HTools.Cluster as Cluster
import qualified Ganeti.HTools.Node as Node
import qualified Ganeti.HTools.Instance as Instance

import Ganeti.HTools.Utils
46
import Ganeti.HTools.Types
47
import Ganeti.HTools.CLI
48
import Ganeti.HTools.ExtLoader
49
import Ganeti.HTools.Loader
Iustin Pop's avatar
Iustin Pop committed
50

Iustin Pop's avatar
Iustin Pop committed
51
-- | Options list and functions.
52
options :: [OptType]
Iustin Pop's avatar
Iustin Pop committed
53
options =
Iustin Pop's avatar
Iustin Pop committed
54
55
56
57
58
59
60
61
62
63
64
65
66
  [ oPrintNodes
  , oDataFile
  , oDiskTemplate
  , oNodeSim
  , oRapiMaster
  , oLuxiSocket
  , oVerbose
  , oQuiet
  , oOfflineNode
  , oMachineReadable
  , oMaxCpu
  , oMaxSolLength
  , oMinDisk
67
  , oStdSpec
Iustin Pop's avatar
Iustin Pop committed
68
69
70
71
72
  , oTieredSpec
  , oSaveCluster
  , oShowVer
  , oShowHelp
  ]
Iustin Pop's avatar
Iustin Pop committed
73

74
75
76
77
78
-- | The allocation phase we're in (initial, after tiered allocs, or
-- after regular allocation).
data Phase = PInitial
           | PFinal
           | PTiered
79

80
81
82
83
84
85
86
87
88
89
90
-- | The kind of instance spec we print.
data SpecType = SpecNormal
              | SpecTiered

-- | What we prefix a spec with.
specPrefix :: SpecType -> String
specPrefix SpecNormal = "SPEC"
specPrefix SpecTiered = "TSPEC_INI"

-- | The description of a spec.
specDescription :: SpecType -> String
Iustin Pop's avatar
Iustin Pop committed
91
specDescription SpecNormal = "Standard (fixed-size)"
92
93
94
95
96
specDescription SpecTiered = "Tiered (initial size)"

-- | Efficiency generic function.
effFn :: (Cluster.CStats -> Integer)
      -> (Cluster.CStats -> Double)
Iustin Pop's avatar
Iustin Pop committed
97
      -> Cluster.CStats -> Double
98
99
100
101
102
103
104
105
106
107
108
109
110
111
effFn fi ft cs = fromIntegral (fi cs) / ft cs

-- | Memory efficiency.
memEff :: Cluster.CStats -> Double
memEff = effFn Cluster.csImem Cluster.csTmem

-- | Disk efficiency.
dskEff :: Cluster.CStats -> Double
dskEff = effFn Cluster.csIdsk Cluster.csTdsk

-- | Cpu efficiency.
cpuEff :: Cluster.CStats -> Double
cpuEff = effFn Cluster.csIcpu (fromIntegral . Cluster.csVcpu)

Iustin Pop's avatar
Iustin Pop committed
112
113
-- | Holds data for converting a 'Cluster.CStats' structure into
-- detailed statictics.
114
statsData :: [(String, Cluster.CStats -> String)]
115
116
117
118
statsData = [ ("SCORE", printf "%.8f" . Cluster.csScore)
            , ("INST_CNT", printf "%d" . Cluster.csNinst)
            , ("MEM_FREE", printf "%d" . Cluster.csFmem)
            , ("MEM_AVAIL", printf "%d" . Cluster.csAmem)
119
            , ("MEM_RESVD",
120
121
               \cs -> printf "%d" (Cluster.csFmem cs - Cluster.csAmem cs))
            , ("MEM_INST", printf "%d" . Cluster.csImem)
122
            , ("MEM_OVERHEAD",
123
               \cs -> printf "%d" (Cluster.csXmem cs + Cluster.csNmem cs))
124
            , ("MEM_EFF", printf "%.8f" . memEff)
125
            , ("DSK_FREE", printf "%d" . Cluster.csFdsk)
126
            , ("DSK_AVAIL", printf "%d". Cluster.csAdsk)
127
            , ("DSK_RESVD",
128
129
               \cs -> printf "%d" (Cluster.csFdsk cs - Cluster.csAdsk cs))
            , ("DSK_INST", printf "%d" . Cluster.csIdsk)
130
            , ("DSK_EFF", printf "%.8f" . dskEff)
131
            , ("CPU_INST", printf "%d" . Cluster.csIcpu)
132
            , ("CPU_EFF", printf "%.8f" . cpuEff)
133
134
            , ("MNODE_MEM_AVAIL", printf "%d" . Cluster.csMmem)
            , ("MNODE_DSK_AVAIL", printf "%d" . Cluster.csMdsk)
135
136
            ]

Iustin Pop's avatar
Iustin Pop committed
137
-- | List holding 'RSpec' formatting information.
138
139
140
141
specData :: [(String, RSpec -> String)]
specData = [ ("MEM", printf "%d" . rspecMem)
           , ("DSK", printf "%d" . rspecDsk)
           , ("CPU", printf "%d" . rspecCpu)
142
143
           ]

Iustin Pop's avatar
Iustin Pop committed
144
-- | List holding 'Cluster.CStats' formatting information.
145
clusterData :: [(String, Cluster.CStats -> String)]
146
147
148
clusterData = [ ("MEM", printf "%.0f" . Cluster.csTmem)
              , ("DSK", printf "%.0f" . Cluster.csTdsk)
              , ("CPU", printf "%.0f" . Cluster.csTcpu)
Iustin Pop's avatar
Iustin Pop committed
149
              , ("VCPU", printf "%d" . Cluster.csVcpu)
150
151
              ]

Iustin Pop's avatar
Iustin Pop committed
152
-- | Function to print stats for a given phase.
153
154
155
156
157
158
printStats :: Phase -> Cluster.CStats -> [(String, String)]
printStats ph cs =
  map (\(s, fn) -> (printf "%s_%s" kind s, fn cs)) statsData
  where kind = case ph of
                 PInitial -> "INI"
                 PFinal -> "FIN"
159
                 PTiered -> "TRL"
Iustin Pop's avatar
Iustin Pop committed
160

161
162
163
164
165
166
167
-- | Print failure reason and scores
printFRScores :: Node.List -> Node.List -> [(FailMode, Int)] -> IO ()
printFRScores ini_nl fin_nl sreason = do
  printf "  - most likely failure reason: %s\n" $ failureReason sreason::IO ()
  printClusterScores ini_nl fin_nl
  printClusterEff (Cluster.totalResources fin_nl)

168
169
170
171
-- | Print final stats and related metrics.
printResults :: Bool -> Node.List -> Node.List -> Int -> Int
             -> [(FailMode, Int)] -> IO ()
printResults True _ fin_nl num_instances allocs sreason = do
172
173
174
  let fin_stats = Cluster.totalResources fin_nl
      fin_instances = num_instances + allocs

175
  when (num_instances + allocs /= Cluster.csNinst fin_stats) $
176
177
178
       do
         hPrintf stderr "ERROR: internal inconsistency, allocated (%d)\
                        \ != counted (%d)\n" (num_instances + allocs)
Iustin Pop's avatar
Iustin Pop committed
179
                                 (Cluster.csNinst fin_stats) :: IO ()
180
181
         exitWith $ ExitFailure 1

182
183
184
185
  printKeys $ printStats PFinal fin_stats
  printKeys [ ("ALLOC_USAGE", printf "%.8f"
                                ((fromIntegral num_instances::Double) /
                                 fromIntegral fin_instances))
186
            , ("ALLOC_INSTANCES", printf "%d" allocs)
187
188
189
190
            , ("ALLOC_FAIL_REASON", map toUpper . show . fst $ head sreason)
            ]
  printKeys $ map (\(x, y) -> (printf "ALLOC_%s_CNT" (show x),
                               printf "%d" y)) sreason
191
192
193
194

printResults False ini_nl fin_nl _ allocs sreason = do
  putStrLn "Normal (fixed-size) allocation results:"
  printf "  - %3d instances allocated\n" allocs :: IO ()
195
  printFRScores ini_nl fin_nl sreason
196
197
198
199

-- | Prints the final @OK@ marker in machine readable output.
printFinal :: Bool -> IO ()
printFinal True =
200
201
202
  -- this should be the final entry
  printKeys [("OK", "1")]

203
204
printFinal False = return ()

205
206
207
208
209
-- | Compute the tiered spec counts from a list of allocated
-- instances.
tieredSpecMap :: [Instance.Instance]
              -> [(RSpec, Int)]
tieredSpecMap trl_ixes =
Iustin Pop's avatar
Iustin Pop committed
210
211
212
213
214
  let fin_trl_ixes = reverse trl_ixes
      ix_byspec = groupBy ((==) `on` Instance.specOf) fin_trl_ixes
      spec_map = map (\ixs -> (Instance.specOf $ head ixs, length ixs))
                 ix_byspec
  in spec_map
215
216
217
218

-- | Formats a spec map to strings.
formatSpecMap :: [(RSpec, Int)] -> [String]
formatSpecMap =
Iustin Pop's avatar
Iustin Pop committed
219
220
  map (\(spec, cnt) -> printf "%d,%d,%d=%d" (rspecMem spec)
                       (rspecDsk spec) (rspecCpu spec) cnt)
221

Iustin Pop's avatar
Iustin Pop committed
222
-- | Formats \"key-metrics\" values.
223
224
formatRSpec :: Double -> String -> RSpec -> [(String, String)]
formatRSpec m_cpu s r =
Iustin Pop's avatar
Iustin Pop committed
225
226
227
228
229
  [ ("KM_" ++ s ++ "_CPU", show $ rspecCpu r)
  , ("KM_" ++ s ++ "_NPU", show $ fromIntegral (rspecCpu r) / m_cpu)
  , ("KM_" ++ s ++ "_MEM", show $ rspecMem r)
  , ("KM_" ++ s ++ "_DSK", show $ rspecDsk r)
  ]
Iustin Pop's avatar
Iustin Pop committed
230

Iustin Pop's avatar
Iustin Pop committed
231
-- | Shows allocations stats.
232
233
printAllocationStats :: Double -> Node.List -> Node.List -> IO ()
printAllocationStats m_cpu ini_nl fin_nl = do
Iustin Pop's avatar
Iustin Pop committed
234
235
236
  let ini_stats = Cluster.totalResources ini_nl
      fin_stats = Cluster.totalResources fin_nl
      (rini, ralo, runa) = Cluster.computeAllocationDelta ini_stats fin_stats
237
238
239
  printKeys $ formatRSpec m_cpu  "USED" rini
  printKeys $ formatRSpec m_cpu "POOL"ralo
  printKeys $ formatRSpec m_cpu "UNAV" runa
Iustin Pop's avatar
Iustin Pop committed
240

Iustin Pop's avatar
Iustin Pop committed
241
-- | Ensure a value is quoted if needed.
242
ensureQuoted :: String -> String
Iustin Pop's avatar
Iustin Pop committed
243
ensureQuoted v = if not (all (\c -> isAlphaNum c || c == '.') v)
244
245
246
                 then '\'':v ++ "'"
                 else v

Iustin Pop's avatar
Iustin Pop committed
247
-- | Format a list of key\/values as a shell fragment.
248
printKeys :: [(String, String)] -> IO ()
249
250
printKeys = mapM_ (\(k, v) ->
                   printf "HTS_%s=%s\n" (map toUpper k) (ensureQuoted v))
251

Iustin Pop's avatar
Iustin Pop committed
252
-- | Converts instance data to a list of strings.
253
254
printInstance :: Node.List -> Instance.Instance -> [String]
printInstance nl i = [ Instance.name i
255
256
257
258
                     , Container.nameOf nl $ Instance.pNode i
                     , let sdx = Instance.sNode i
                       in if sdx == Node.noSecondary then ""
                          else Container.nameOf nl sdx
259
260
261
262
263
                     , show (Instance.mem i)
                     , show (Instance.dsk i)
                     , show (Instance.vcpus i)
                     ]

Iustin Pop's avatar
Iustin Pop committed
264
-- | Optionally print the allocation map.
265
266
267
268
printAllocationMap :: Int -> String
                   -> Node.List -> [Instance.Instance] -> IO ()
printAllocationMap verbose msg nl ixes =
  when (verbose > 1) $ do
Iustin Pop's avatar
Iustin Pop committed
269
    hPutStrLn stderr (msg ++ " map")
Iustin Pop's avatar
Iustin Pop committed
270
    hPutStr stderr . unlines . map ((:) ' ' .  unwords) $
271
272
273
274
275
276
            formatTable (map (printInstance nl) (reverse ixes))
                        -- This is the numberic-or-not field
                        -- specification; the first three fields are
                        -- strings, whereas the rest are numeric
                       [False, False, False, True, True, True]

277
-- | Formats nicely a list of resources.
Iustin Pop's avatar
Iustin Pop committed
278
formatResources :: a -> [(String, a->String)] -> String
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
formatResources res =
    intercalate ", " . map (\(a, fn) -> a ++ " " ++ fn res)

-- | Print the cluster resources.
printCluster :: Bool -> Cluster.CStats -> Int -> IO ()
printCluster True ini_stats node_count = do
  printKeys $ map (\(a, fn) -> ("CLUSTER_" ++ a, fn ini_stats)) clusterData
  printKeys [("CLUSTER_NODES", printf "%d" node_count)]
  printKeys $ printStats PInitial ini_stats

printCluster False ini_stats node_count = do
  printf "The cluster has %d nodes and the following resources:\n  %s.\n"
         node_count (formatResources ini_stats clusterData)::IO ()
  printf "There are %s initial instances on the cluster.\n"
             (if inst_count > 0 then show inst_count else "no" )
      where inst_count = Cluster.csNinst ini_stats

-- | Prints the normal instance spec.
printISpec :: Bool -> RSpec -> SpecType -> DiskTemplate -> IO ()
printISpec True ispec spec disk_template = do
  printKeys $ map (\(a, fn) -> (prefix ++ "_" ++ a, fn ispec)) specData
  printKeys [ (prefix ++ "_RQN", printf "%d" req_nodes) ]
301
  printKeys [ (prefix ++ "_DISK_TEMPLATE",
302
               diskTemplateToRaw disk_template) ]
303
304
305
      where req_nodes = Instance.requiredNodes disk_template
            prefix = specPrefix spec

Iustin Pop's avatar
Iustin Pop committed
306
printISpec False ispec spec disk_template =
307
308
309
  printf "%s instance spec is:\n  %s, using disk\
         \ template '%s'.\n"
         (specDescription spec)
310
         (formatResources ispec specData) (diskTemplateToRaw disk_template)
311
312
313
314
315
316

-- | Prints the tiered results.
printTiered :: Bool -> [(RSpec, Int)] -> Double
            -> Node.List -> Node.List -> [(FailMode, Int)] -> IO ()
printTiered True spec_map m_cpu nl trl_nl _ = do
  printKeys $ printStats PTiered (Cluster.totalResources trl_nl)
Iustin Pop's avatar
Iustin Pop committed
317
  printKeys [("TSPEC", unwords (formatSpecMap spec_map))]
318
319
320
321
322
323
324
  printAllocationStats m_cpu nl trl_nl

printTiered False spec_map _ ini_nl fin_nl sreason = do
  _ <- printf "Tiered allocation results:\n"
  mapM_ (\(ispec, cnt) ->
             printf "  - %3d instances of spec %s\n" cnt
                        (formatResources ispec specData)) spec_map
325
  printFRScores ini_nl fin_nl sreason
326

Iustin Pop's avatar
Iustin Pop committed
327
-- | Displays the initial/final cluster scores.
328
329
330
331
332
printClusterScores :: Node.List -> Node.List -> IO ()
printClusterScores ini_nl fin_nl = do
  printf "  - initial cluster score: %.8f\n" $ Cluster.compCV ini_nl::IO ()
  printf "  -   final cluster score: %.8f\n" $ Cluster.compCV fin_nl

Iustin Pop's avatar
Iustin Pop committed
333
-- | Displays the cluster efficiency.
334
335
printClusterEff :: Cluster.CStats -> IO ()
printClusterEff cs =
Iustin Pop's avatar
Iustin Pop committed
336
337
  mapM_ (\(s, fn) ->
           printf "  - %s usage efficiency: %5.2f%%\n" s (fn cs * 100))
338
339
340
341
342
343
344
345
346
347
348
349
          [("memory", memEff),
           ("  disk", dskEff),
           ("  vcpu", cpuEff)]

-- | Computes the most likely failure reason.
failureReason :: [(FailMode, Int)] -> String
failureReason = show . fst . head

-- | Sorts the failure reasons.
sortReasons :: [(FailMode, Int)] -> [(FailMode, Int)]
sortReasons = reverse . sortBy (comparing snd)

Iustin Pop's avatar
Iustin Pop committed
350
351
352
353
354
355
-- | Aborts the program if we get a bad value.
exitIfBad :: Result a -> IO a
exitIfBad (Bad s) =
  hPrintf stderr "Failure: %s\n" s >> exitWith (ExitFailure 1)
exitIfBad (Ok v) = return v

Iustin Pop's avatar
Iustin Pop committed
356
357
358
359
360
-- | Runs an allocation algorithm and saves cluster state.
runAllocation :: ClusterData                -- ^ Cluster data
              -> Maybe Cluster.AllocResult  -- ^ Optional stop-allocation
              -> Result Cluster.AllocResult -- ^ Allocation result
              -> RSpec                      -- ^ Requested instance spec
361
              -> DiskTemplate               -- ^ Requested disk template
Iustin Pop's avatar
Iustin Pop committed
362
363
364
              -> SpecType                   -- ^ Allocation type
              -> Options                    -- ^ CLI options
              -> IO (FailStats, Node.List, Int, [(RSpec, Int)])
365
runAllocation cdata stop_allocation actual_result spec dt mode opts = do
Iustin Pop's avatar
Iustin Pop committed
366
367
368
369
370
371
372
373
374
  (reasons, new_nl, new_il, new_ixes, _) <-
      case stop_allocation of
        Just result_noalloc -> return result_noalloc
        Nothing -> exitIfBad actual_result

  let name = head . words . specDescription $ mode
      descr = name ++ " allocation"
      ldescr = "after " ++ map toLower descr

375
  printISpec (optMachineReadable opts) spec mode dt
Iustin Pop's avatar
Iustin Pop committed
376
377
378
379
380
381
382
383
384
385

  printAllocationMap (optVerbose opts) descr new_nl new_ixes

  maybePrintNodes (optShowNodes opts) descr (Cluster.printNodes new_nl)

  maybeSaveData (optSaveCluster opts) (map toLower name) ldescr
                    (cdata { cdNodes = new_nl, cdInstances = new_il})

  return (sortReasons reasons, new_nl, length new_ixes, tieredSpecMap new_ixes)

386
387
388
389
390
391
-- | Create an instance from a given spec.
instFromSpec :: RSpec -> DiskTemplate -> Instance.Instance
instFromSpec spx disk_template =
  Instance.create "new" (rspecMem spx) (rspecDsk spx)
    (rspecCpu spx) Running [] True (-1) (-1) disk_template

Iustin Pop's avatar
Iustin Pop committed
392
393
394
-- | Main function.
main :: IO ()
main = do
395
  cmd_args <- getArgs
396
  (opts, args) <- parseOpts cmd_args "hspace" options
Iustin Pop's avatar
Iustin Pop committed
397
398
399
400
401

  unless (null args) $ do
         hPutStrLn stderr "Error: this program doesn't take any arguments."
         exitWith $ ExitFailure 1

402
  let verbose = optVerbose opts
403
      machine_r = optMachineReadable opts
404

405
  orig_cdata@(ClusterData gl fixed_nl il _ ipol) <- loadExternalData opts
Iustin Pop's avatar
Iustin Pop committed
406
  nl <- setNodeStatus opts fixed_nl
407

408
409
410
411
412
413
414
415
  cluster_disk_template <-
    case iPolicyDiskTemplates ipol of
      first_templ:_ -> return first_templ
      _ -> do
         _ <- hPutStrLn stderr $ "Error: null list of disk templates\
                               \ received from cluster!"
         exitWith $ ExitFailure 1

Iustin Pop's avatar
Iustin Pop committed
416
  let num_instances = Container.size il
Iustin Pop's avatar
Iustin Pop committed
417
      all_nodes = Container.elems fixed_nl
418
      cdata = orig_cdata { cdNodes = fixed_nl }
419
420
      disk_template = fromMaybe cluster_disk_template (optDiskTemplate opts)
      req_nodes = Instance.requiredNodes disk_template
421
      csf = commonSuffix fixed_nl il
Iustin Pop's avatar
Iustin Pop committed
422

Iustin Pop's avatar
Iustin Pop committed
423
  when (not (null csf) && verbose > 1) $
424
       hPrintf stderr "Note: Stripping common suffix of '%s' from names\n" csf
Iustin Pop's avatar
Iustin Pop committed
425

Iustin Pop's avatar
Iustin Pop committed
426
  maybePrintNodes (optShowNodes opts) "Initial cluster" (Cluster.printNodes nl)
Iustin Pop's avatar
Iustin Pop committed
427

Iustin Pop's avatar
Iustin Pop committed
428
  when (verbose > 2) $
429
         hPrintf stderr "Initial coefficients: overall %.8f, %s\n"
Iustin Pop's avatar
Iustin Pop committed
430
                 (Cluster.compCV nl) (Cluster.printStats nl)
431

Iustin Pop's avatar
Iustin Pop committed
432
  printCluster machine_r (Cluster.totalResources nl) (length all_nodes)
433

Iustin Pop's avatar
Iustin Pop committed
434
435
436
437
438
439
  let stop_allocation = case Cluster.computeBadItems nl il of
                          ([], _) -> Nothing
                          _ -> Just ([(FailN1, 1)]::FailStats, nl, il, [], [])
      alloclimit = if optMaxLength opts == -1
                   then Nothing
                   else Just (optMaxLength opts)
440

Iustin Pop's avatar
Iustin Pop committed
441
  allocnodes <- exitIfBad $ Cluster.genAllocNodes gl nl req_nodes True
Iustin Pop's avatar
Iustin Pop committed
442

443
  -- Run the tiered allocation
444

445
446
  let tspec = fromMaybe (rspecFromISpec (iPolicyMaxSpec ipol))
              (optTieredSpec opts)
447

448
449
450
451
  (treason, trl_nl, _, spec_map) <-
    runAllocation cdata stop_allocation
       (Cluster.tieredAlloc nl il alloclimit
        (instFromSpec tspec disk_template) allocnodes [] [])
452
       tspec disk_template SpecTiered opts
453
454

  printTiered machine_r spec_map (optMcpu opts) nl trl_nl treason
455
456

  -- Run the standard (avg-mode) allocation
Iustin Pop's avatar
Iustin Pop committed
457

458
459
460
  let ispec = fromMaybe (rspecFromISpec (iPolicyStdSpec ipol))
              (optStdSpec opts)

Iustin Pop's avatar
Iustin Pop committed
461
462
  (sreason, fin_nl, allocs, _) <-
      runAllocation cdata stop_allocation
463
464
            (Cluster.iterateAlloc nl il alloclimit
             (instFromSpec ispec disk_template) allocnodes [] [])
465
            ispec disk_template SpecNormal opts
466

467
468
  printResults machine_r nl fin_nl num_instances allocs sreason

469
470
  -- Print final result

471
  printFinal machine_r