Statistics
| Branch: | Tag: | Revision:

root / htools / Ganeti / HTools / CLI.hs @ 51000365

History | View | Annotate | Download (20.1 kB)

1
{-| Implementation of command-line functions.
2

    
3
This module holds the common command-line related functions for the
4
binaries, separated into this module since "Ganeti.HTools.Utils" is
5
used in many other places and this is more IO oriented.
6

    
7
-}
8

    
9
{-
10

    
11
Copyright (C) 2009, 2010, 2011, 2012 Google Inc.
12

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

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

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

    
28
-}
29

    
30
module Ganeti.HTools.CLI
31
  ( Options(..)
32
  , OptType
33
  , defaultOptions
34
  , Ganeti.HTools.CLI.parseOpts
35
  , parseOptsInner
36
  , parseYesNo
37
  , parseISpecString
38
  , shTemplate
39
  , defaultLuxiSocket
40
  , maybePrintNodes
41
  , maybePrintInsts
42
  , maybeShowWarnings
43
  , printKeys
44
  , printFinal
45
  , setNodeStatus
46
  -- * The options
47
  , oDataFile
48
  , oDiskMoves
49
  , oDiskTemplate
50
  , oSpindleUse
51
  , oDynuFile
52
  , oEvacMode
53
  , oExInst
54
  , oExTags
55
  , oExecJobs
56
  , oGroup
57
  , oIAllocSrc
58
  , oInstMoves
59
  , oLuxiSocket
60
  , oMachineReadable
61
  , oMaxCpu
62
  , oMaxSolLength
63
  , oMinDisk
64
  , oMinGain
65
  , oMinGainLim
66
  , oMinScore
67
  , oNoHeaders
68
  , oNoSimulation
69
  , oNodeSim
70
  , oOfflineNode
71
  , oOutputDir
72
  , oPrintCommands
73
  , oPrintInsts
74
  , oPrintNodes
75
  , oQuiet
76
  , oRapiMaster
77
  , oSaveCluster
78
  , oSelInst
79
  , oShowHelp
80
  , oShowVer
81
  , oStdSpec
82
  , oTieredSpec
83
  , oVerbose
84
  ) where
85

    
86
import Control.Monad
87
import Data.Char (toUpper)
88
import Data.Maybe (fromMaybe)
89
import System.Console.GetOpt
90
import System.IO
91
import Text.Printf (printf)
92

    
93
import qualified Ganeti.HTools.Container as Container
94
import qualified Ganeti.HTools.Node as Node
95
import qualified Ganeti.Constants as C
96
import Ganeti.HTools.Types
97
import Ganeti.HTools.Utils
98
import Ganeti.BasicTypes
99
import Ganeti.Common as Common
100

    
101
-- * Constants
102

    
103
-- | The default value for the luxi socket.
104
--
105
-- This is re-exported from the "Ganeti.Constants" module.
106
defaultLuxiSocket :: FilePath
107
defaultLuxiSocket = C.masterSocket
108

    
109
-- * Data types
110

    
111
-- | Command line options structure.
112
data Options = Options
113
  { optDataFile    :: Maybe FilePath -- ^ Path to the cluster data file
114
  , optDiskMoves   :: Bool           -- ^ Allow disk moves
115
  , optInstMoves   :: Bool           -- ^ Allow instance moves
116
  , optDiskTemplate :: Maybe DiskTemplate  -- ^ Override for the disk template
117
  , optSpindleUse  :: Maybe Int      -- ^ Override for the spindle usage
118
  , optDynuFile    :: Maybe FilePath -- ^ Optional file with dynamic use data
119
  , optEvacMode    :: Bool           -- ^ Enable evacuation mode
120
  , optExInst      :: [String]       -- ^ Instances to be excluded
121
  , optExTags      :: Maybe [String] -- ^ Tags to use for exclusion
122
  , optExecJobs    :: Bool           -- ^ Execute the commands via Luxi
123
  , optGroup       :: Maybe GroupID  -- ^ The UUID of the group to process
124
  , optIAllocSrc   :: Maybe FilePath -- ^ The iallocation spec
125
  , optSelInst     :: [String]       -- ^ Instances to be excluded
126
  , optLuxi        :: Maybe FilePath -- ^ Collect data from Luxi
127
  , optMachineReadable :: Bool       -- ^ Output machine-readable format
128
  , optMaster      :: String         -- ^ Collect data from RAPI
129
  , optMaxLength   :: Int            -- ^ Stop after this many steps
130
  , optMcpu        :: Maybe Double   -- ^ Override max cpu ratio for nodes
131
  , optMdsk        :: Double         -- ^ Max disk usage ratio for nodes
132
  , optMinGain     :: Score          -- ^ Min gain we aim for in a step
133
  , optMinGainLim  :: Score          -- ^ Limit below which we apply mingain
134
  , optMinScore    :: Score          -- ^ The minimum score we aim for
135
  , optNoHeaders   :: Bool           -- ^ Do not show a header line
136
  , optNoSimulation :: Bool          -- ^ Skip the rebalancing dry-run
137
  , optNodeSim     :: [String]       -- ^ Cluster simulation mode
138
  , optOffline     :: [String]       -- ^ Names of offline nodes
139
  , optOutPath     :: FilePath       -- ^ Path to the output directory
140
  , optSaveCluster :: Maybe FilePath -- ^ Save cluster state to this file
141
  , optShowCmds    :: Maybe FilePath -- ^ Whether to show the command list
142
  , optShowHelp    :: Bool           -- ^ Just show the help
143
  , optShowInsts   :: Bool           -- ^ Whether to show the instance map
144
  , optShowNodes   :: Maybe [String] -- ^ Whether to show node status
145
  , optShowVer     :: Bool           -- ^ Just show the program version
146
  , optStdSpec     :: Maybe RSpec    -- ^ Requested standard specs
147
  , optTestCount   :: Maybe Int      -- ^ Optional test count override
148
  , optTieredSpec  :: Maybe RSpec    -- ^ Requested specs for tiered mode
149
  , optReplay      :: Maybe String   -- ^ Unittests: RNG state
150
  , optVerbose     :: Int            -- ^ Verbosity level
151
  } deriving Show
152

    
153
-- | Default values for the command line options.
154
defaultOptions :: Options
155
defaultOptions  = Options
156
  { optDataFile    = Nothing
157
  , optDiskMoves   = True
158
  , optInstMoves   = True
159
  , optDiskTemplate = Nothing
160
  , optSpindleUse  = Nothing
161
  , optDynuFile    = Nothing
162
  , optEvacMode    = False
163
  , optExInst      = []
164
  , optExTags      = Nothing
165
  , optExecJobs    = False
166
  , optGroup       = Nothing
167
  , optIAllocSrc   = Nothing
168
  , optSelInst     = []
169
  , optLuxi        = Nothing
170
  , optMachineReadable = False
171
  , optMaster      = ""
172
  , optMaxLength   = -1
173
  , optMcpu        = Nothing
174
  , optMdsk        = defReservedDiskRatio
175
  , optMinGain     = 1e-2
176
  , optMinGainLim  = 1e-1
177
  , optMinScore    = 1e-9
178
  , optNoHeaders   = False
179
  , optNoSimulation = False
180
  , optNodeSim     = []
181
  , optOffline     = []
182
  , optOutPath     = "."
183
  , optSaveCluster = Nothing
184
  , optShowCmds    = Nothing
185
  , optShowHelp    = False
186
  , optShowInsts   = False
187
  , optShowNodes   = Nothing
188
  , optShowVer     = False
189
  , optStdSpec     = Nothing
190
  , optTestCount   = Nothing
191
  , optTieredSpec  = Nothing
192
  , optReplay      = Nothing
193
  , optVerbose     = 1
194
  }
195

    
196
-- | Abrreviation for the option type.
197
type OptType = GenericOptType Options
198

    
199
instance StandardOptions Options where
200
  helpRequested = optShowHelp
201
  verRequested  = optShowVer
202
  requestHelp   = \opts -> opts { optShowHelp = True }
203
  requestVer    = \opts -> opts { optShowVer  = True }
204

    
205
-- * Helper functions
206

    
207
parseISpecString :: String -> String -> Result RSpec
208
parseISpecString descr inp = do
209
  let sp = sepSplit ',' inp
210
      err = Bad ("Invalid " ++ descr ++ " specification: '" ++ inp ++
211
                 "', expected disk,ram,cpu")
212
  when (length sp /= 3) err
213
  prs <- mapM (\(fn, val) -> fn val) $
214
         zip [ annotateResult (descr ++ " specs disk") . parseUnit
215
             , annotateResult (descr ++ " specs memory") . parseUnit
216
             , tryRead (descr ++ " specs cpus")
217
             ] sp
218
  case prs of
219
    [dsk, ram, cpu] -> return $ RSpec cpu ram dsk
220
    _ -> err
221

    
222
-- * Command line options
223

    
224
oDataFile :: OptType
225
oDataFile = Option "t" ["text-data"]
226
            (ReqArg (\ f o -> Ok o { optDataFile = Just f }) "FILE")
227
            "the cluster data FILE"
228

    
229
oDiskMoves :: OptType
230
oDiskMoves = Option "" ["no-disk-moves"]
231
             (NoArg (\ opts -> Ok opts { optDiskMoves = False}))
232
             "disallow disk moves from the list of allowed instance changes,\
233
             \ thus allowing only the 'cheap' failover/migrate operations"
234

    
235
oDiskTemplate :: OptType
236
oDiskTemplate = Option "" ["disk-template"]
237
                (reqWithConversion diskTemplateFromRaw
238
                 (\dt opts -> Ok opts { optDiskTemplate = Just dt })
239
                 "TEMPLATE") "select the desired disk template"
240

    
241
oSpindleUse :: OptType
242
oSpindleUse = Option "" ["spindle-use"]
243
              (reqWithConversion (tryRead "parsing spindle-use")
244
               (\su opts -> do
245
                  when (su < 0) $
246
                       fail "Invalid value of the spindle-use\
247
                            \ (expected >= 0)"
248
                  return $ opts { optSpindleUse = Just su })
249
               "SPINDLES") "select how many virtual spindle instances use\
250
                           \ [default read from cluster]"
251

    
252
oSelInst :: OptType
253
oSelInst = Option "" ["select-instances"]
254
          (ReqArg (\ f opts -> Ok opts { optSelInst = sepSplit ',' f }) "INSTS")
255
          "only select given instances for any moves"
256

    
257
oInstMoves :: OptType
258
oInstMoves = Option "" ["no-instance-moves"]
259
             (NoArg (\ opts -> Ok opts { optInstMoves = False}))
260
             "disallow instance (primary node) moves from the list of allowed,\
261
             \ instance changes, thus allowing only slower, but sometimes\
262
             \ safer, drbd secondary changes"
263

    
264
oDynuFile :: OptType
265
oDynuFile = Option "U" ["dynu-file"]
266
            (ReqArg (\ f opts -> Ok opts { optDynuFile = Just f }) "FILE")
267
            "Import dynamic utilisation data from the given FILE"
268

    
269
oEvacMode :: OptType
270
oEvacMode = Option "E" ["evac-mode"]
271
            (NoArg (\opts -> Ok opts { optEvacMode = True }))
272
            "enable evacuation mode, where the algorithm only moves \
273
            \ instances away from offline and drained nodes"
274

    
275
oExInst :: OptType
276
oExInst = Option "" ["exclude-instances"]
277
          (ReqArg (\ f opts -> Ok opts { optExInst = sepSplit ',' f }) "INSTS")
278
          "exclude given instances from any moves"
279

    
280
oExTags :: OptType
281
oExTags = Option "" ["exclusion-tags"]
282
            (ReqArg (\ f opts -> Ok opts { optExTags = Just $ sepSplit ',' f })
283
             "TAG,...") "Enable instance exclusion based on given tag prefix"
284

    
285
oExecJobs :: OptType
286
oExecJobs = Option "X" ["exec"]
287
             (NoArg (\ opts -> Ok opts { optExecJobs = True}))
288
             "execute the suggested moves via Luxi (only available when using\
289
             \ it for data gathering)"
290

    
291
oGroup :: OptType
292
oGroup = Option "G" ["group"]
293
            (ReqArg (\ f o -> Ok o { optGroup = Just f }) "ID")
294
            "the ID of the group to balance"
295

    
296
oIAllocSrc :: OptType
297
oIAllocSrc = Option "I" ["ialloc-src"]
298
             (ReqArg (\ f opts -> Ok opts { optIAllocSrc = Just f }) "FILE")
299
             "Specify an iallocator spec as the cluster data source"
300

    
301
oLuxiSocket :: OptType
302
oLuxiSocket = Option "L" ["luxi"]
303
              (OptArg ((\ f opts -> Ok opts { optLuxi = Just f }) .
304
                       fromMaybe defaultLuxiSocket) "SOCKET")
305
              "collect data via Luxi, optionally using the given SOCKET path"
306

    
307
oMachineReadable :: OptType
308
oMachineReadable = Option "" ["machine-readable"]
309
                   (OptArg (\ f opts -> do
310
                     flag <- parseYesNo True f
311
                     return $ opts { optMachineReadable = flag }) "CHOICE")
312
          "enable machine readable output (pass either 'yes' or 'no' to\
313
          \ explicitly control the flag, or without an argument defaults to\
314
          \ yes"
315

    
316
oMaxCpu :: OptType
317
oMaxCpu = Option "" ["max-cpu"]
318
          (reqWithConversion (tryRead "parsing max-cpu")
319
           (\mcpu opts -> do
320
              when (mcpu <= 0) $
321
                   fail "Invalid value of the max-cpu ratio,\
322
                        \ expected >0"
323
              return $ opts { optMcpu = Just mcpu }) "RATIO")
324
          "maximum virtual-to-physical cpu ratio for nodes (from 0\
325
          \ upwards) [default read from cluster]"
326

    
327
oMaxSolLength :: OptType
328
oMaxSolLength = Option "l" ["max-length"]
329
                (reqWithConversion (tryRead "max solution length")
330
                 (\i opts -> Ok opts { optMaxLength = i }) "N")
331
                "cap the solution at this many balancing or allocation \
332
                \ rounds (useful for very unbalanced clusters or empty \
333
                \ clusters)"
334

    
335
oMinDisk :: OptType
336
oMinDisk = Option "" ["min-disk"]
337
           (reqWithConversion (tryRead "min free disk space")
338
            (\n opts -> Ok opts { optMdsk = n }) "RATIO")
339
           "minimum free disk space for nodes (between 0 and 1) [0]"
340

    
341
oMinGain :: OptType
342
oMinGain = Option "g" ["min-gain"]
343
           (reqWithConversion (tryRead "min gain")
344
            (\g opts -> Ok opts { optMinGain = g }) "DELTA")
345
            "minimum gain to aim for in a balancing step before giving up"
346

    
347
oMinGainLim :: OptType
348
oMinGainLim = Option "" ["min-gain-limit"]
349
            (reqWithConversion (tryRead "min gain limit")
350
             (\g opts -> Ok opts { optMinGainLim = g }) "SCORE")
351
            "minimum cluster score for which we start checking the min-gain"
352

    
353
oMinScore :: OptType
354
oMinScore = Option "e" ["min-score"]
355
            (reqWithConversion (tryRead "min score")
356
             (\e opts -> Ok opts { optMinScore = e }) "EPSILON")
357
            "mininum score to aim for"
358

    
359
oNoHeaders :: OptType
360
oNoHeaders = Option "" ["no-headers"]
361
             (NoArg (\ opts -> Ok opts { optNoHeaders = True }))
362
             "do not show a header line"
363

    
364
oNoSimulation :: OptType
365
oNoSimulation = Option "" ["no-simulation"]
366
                (NoArg (\opts -> Ok opts {optNoSimulation = True}))
367
                "do not perform rebalancing simulation"
368

    
369
oNodeSim :: OptType
370
oNodeSim = Option "" ["simulate"]
371
            (ReqArg (\ f o -> Ok o { optNodeSim = f:optNodeSim o }) "SPEC")
372
            "simulate an empty cluster, given as\
373
            \ 'alloc_policy,num_nodes,disk,ram,cpu'"
374

    
375
oOfflineNode :: OptType
376
oOfflineNode = Option "O" ["offline"]
377
               (ReqArg (\ n o -> Ok o { optOffline = n:optOffline o }) "NODE")
378
               "set node as offline"
379

    
380
oOutputDir :: OptType
381
oOutputDir = Option "d" ["output-dir"]
382
             (ReqArg (\ d opts -> Ok opts { optOutPath = d }) "PATH")
383
             "directory in which to write output files"
384

    
385
oPrintCommands :: OptType
386
oPrintCommands = Option "C" ["print-commands"]
387
                 (OptArg ((\ f opts -> Ok opts { optShowCmds = Just f }) .
388
                          fromMaybe "-")
389
                  "FILE")
390
                 "print the ganeti command list for reaching the solution,\
391
                 \ if an argument is passed then write the commands to a\
392
                 \ file named as such"
393

    
394
oPrintInsts :: OptType
395
oPrintInsts = Option "" ["print-instances"]
396
              (NoArg (\ opts -> Ok opts { optShowInsts = True }))
397
              "print the final instance map"
398

    
399
oPrintNodes :: OptType
400
oPrintNodes = Option "p" ["print-nodes"]
401
              (OptArg ((\ f opts ->
402
                          let (prefix, realf) = case f of
403
                                                  '+':rest -> (["+"], rest)
404
                                                  _ -> ([], f)
405
                              splitted = prefix ++ sepSplit ',' realf
406
                          in Ok opts { optShowNodes = Just splitted }) .
407
                       fromMaybe []) "FIELDS")
408
              "print the final node list"
409

    
410
oQuiet :: OptType
411
oQuiet = Option "q" ["quiet"]
412
         (NoArg (\ opts -> Ok opts { optVerbose = optVerbose opts - 1 }))
413
         "decrease the verbosity level"
414

    
415
oRapiMaster :: OptType
416
oRapiMaster = Option "m" ["master"]
417
              (ReqArg (\ m opts -> Ok opts { optMaster = m }) "ADDRESS")
418
              "collect data via RAPI at the given ADDRESS"
419

    
420
oSaveCluster :: OptType
421
oSaveCluster = Option "S" ["save"]
422
            (ReqArg (\ f opts -> Ok opts { optSaveCluster = Just f }) "FILE")
423
            "Save cluster state at the end of the processing to FILE"
424

    
425
oStdSpec :: OptType
426
oStdSpec = Option "" ["standard-alloc"]
427
             (ReqArg (\ inp opts -> do
428
                        tspec <- parseISpecString "standard" inp
429
                        return $ opts { optStdSpec = Just tspec } )
430
              "STDSPEC")
431
             "enable standard specs allocation, given as 'disk,ram,cpu'"
432

    
433
oTieredSpec :: OptType
434
oTieredSpec = Option "" ["tiered-alloc"]
435
             (ReqArg (\ inp opts -> do
436
                        tspec <- parseISpecString "tiered" inp
437
                        return $ opts { optTieredSpec = Just tspec } )
438
              "TSPEC")
439
             "enable tiered specs allocation, given as 'disk,ram,cpu'"
440

    
441
oVerbose :: OptType
442
oVerbose = Option "v" ["verbose"]
443
           (NoArg (\ opts -> Ok opts { optVerbose = optVerbose opts + 1 }))
444
           "increase the verbosity level"
445

    
446
-- * Functions
447

    
448
-- | Wrapper over 'Common.parseOpts' with our custom options.
449
parseOpts :: [String]               -- ^ The command line arguments
450
          -> String                 -- ^ The program name
451
          -> [OptType]              -- ^ The supported command line options
452
          -> IO (Options, [String]) -- ^ The resulting options and leftover
453
                                    -- arguments
454
parseOpts = Common.parseOpts defaultOptions
455

    
456

    
457
-- | A shell script template for autogenerated scripts.
458
shTemplate :: String
459
shTemplate =
460
  printf "#!/bin/sh\n\n\
461
         \# Auto-generated script for executing cluster rebalancing\n\n\
462
         \# To stop, touch the file /tmp/stop-htools\n\n\
463
         \set -e\n\n\
464
         \check() {\n\
465
         \  if [ -f /tmp/stop-htools ]; then\n\
466
         \    echo 'Stop requested, exiting'\n\
467
         \    exit 0\n\
468
         \  fi\n\
469
         \}\n\n"
470

    
471
-- | Optionally print the node list.
472
maybePrintNodes :: Maybe [String]       -- ^ The field list
473
                -> String               -- ^ Informational message
474
                -> ([String] -> String) -- ^ Function to generate the listing
475
                -> IO ()
476
maybePrintNodes Nothing _ _ = return ()
477
maybePrintNodes (Just fields) msg fn = do
478
  hPutStrLn stderr ""
479
  hPutStrLn stderr (msg ++ " status:")
480
  hPutStrLn stderr $ fn fields
481

    
482
-- | Optionally print the instance list.
483
maybePrintInsts :: Bool   -- ^ Whether to print the instance list
484
                -> String -- ^ Type of the instance map (e.g. initial)
485
                -> String -- ^ The instance data
486
                -> IO ()
487
maybePrintInsts do_print msg instdata =
488
  when do_print $ do
489
    hPutStrLn stderr ""
490
    hPutStrLn stderr $ msg ++ " instance map:"
491
    hPutStr stderr instdata
492

    
493
-- | Function to display warning messages from parsing the cluster
494
-- state.
495
maybeShowWarnings :: [String] -- ^ The warning messages
496
                  -> IO ()
497
maybeShowWarnings fix_msgs =
498
  unless (null fix_msgs) $ do
499
    hPutStrLn stderr "Warning: cluster has inconsistent data:"
500
    hPutStrLn stderr . unlines . map (printf "  - %s") $ fix_msgs
501

    
502
-- | Format a list of key, value as a shell fragment.
503
printKeys :: String              -- ^ Prefix to printed variables
504
          -> [(String, String)]  -- ^ List of (key, value) pairs to be printed
505
          -> IO ()
506
printKeys prefix =
507
  mapM_ (\(k, v) ->
508
           printf "%s_%s=%s\n" prefix (map toUpper k) (ensureQuoted v))
509

    
510
-- | Prints the final @OK@ marker in machine readable output.
511
printFinal :: String    -- ^ Prefix to printed variable
512
           -> Bool      -- ^ Whether output should be machine readable;
513
                        -- note: if not, there is nothing to print
514
           -> IO ()
515
printFinal prefix True =
516
  -- this should be the final entry
517
  printKeys prefix [("OK", "1")]
518

    
519
printFinal _ False = return ()
520

    
521
-- | Potentially set the node as offline based on passed offline list.
522
setNodeOffline :: [Ndx] -> Node.Node -> Node.Node
523
setNodeOffline offline_indices n =
524
  if Node.idx n `elem` offline_indices
525
    then Node.setOffline n True
526
    else n
527

    
528
-- | Set node properties based on command line options.
529
setNodeStatus :: Options -> Node.List -> IO Node.List
530
setNodeStatus opts fixed_nl = do
531
  let offline_passed = optOffline opts
532
      all_nodes = Container.elems fixed_nl
533
      offline_lkp = map (lookupName (map Node.name all_nodes)) offline_passed
534
      offline_wrong = filter (not . goodLookupResult) offline_lkp
535
      offline_names = map lrContent offline_lkp
536
      offline_indices = map Node.idx $
537
                        filter (\n -> Node.name n `elem` offline_names)
538
                               all_nodes
539
      m_cpu = optMcpu opts
540
      m_dsk = optMdsk opts
541

    
542
  unless (null offline_wrong) $ do
543
         exitErr $ printf "wrong node name(s) set as offline: %s\n"
544
                   (commaJoin (map lrContent offline_wrong))
545
  let setMCpuFn = case m_cpu of
546
                    Nothing -> id
547
                    Just new_mcpu -> flip Node.setMcpu new_mcpu
548
  let nm = Container.map (setNodeOffline offline_indices .
549
                          flip Node.setMdsk m_dsk .
550
                          setMCpuFn) fixed_nl
551
  return nm