Allow overriding the target test count
[ganeti-local] / htools / Ganeti / HTools / CLI.hs
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   , parseOpts
34   , parseOptsInner
35   , parseYesNo
36   , parseISpecString
37   , shTemplate
38   , defaultLuxiSocket
39   , maybePrintNodes
40   , maybePrintInsts
41   , maybeShowWarnings
42   , setNodeStatus
43   -- * The options
44   , oDataFile
45   , oDiskMoves
46   , oDiskTemplate
47   , oDynuFile
48   , oEvacMode
49   , oExInst
50   , oExTags
51   , oExecJobs
52   , oGroup
53   , oInstMoves
54   , oLuxiSocket
55   , oMachineReadable
56   , oMaxCpu
57   , oMaxSolLength
58   , oMinDisk
59   , oMinGain
60   , oMinGainLim
61   , oMinScore
62   , oNoHeaders
63   , oNodeSim
64   , oOfflineNode
65   , oOutputDir
66   , oPrintCommands
67   , oPrintInsts
68   , oPrintNodes
69   , oQuiet
70   , oRapiMaster
71   , oReplay
72   , oSaveCluster
73   , oSelInst
74   , oShowHelp
75   , oShowVer
76   , oStdSpec
77   , oTestCount
78   , oTieredSpec
79   , oVerbose
80   ) where
81
82 import Control.Monad
83 import Data.Maybe (fromMaybe)
84 import qualified Data.Version
85 import System.Console.GetOpt
86 import System.IO
87 import System.Info
88 import System.Exit
89 import Text.Printf (printf, hPrintf)
90
91 import qualified Ganeti.HTools.Version as Version(version)
92 import qualified Ganeti.HTools.Container as Container
93 import qualified Ganeti.HTools.Node as Node
94 import qualified Ganeti.Constants as C
95 import Ganeti.HTools.Types
96 import Ganeti.HTools.Utils
97 import Ganeti.HTools.Loader
98
99 -- * Constants
100
101 -- | The default value for the luxi socket.
102 --
103 -- This is re-exported from the "Ganeti.Constants" module.
104 defaultLuxiSocket :: FilePath
105 defaultLuxiSocket = C.masterSocket
106
107 -- * Data types
108
109 -- | Command line options structure.
110 data Options = Options
111   { optDataFile    :: Maybe FilePath -- ^ Path to the cluster data file
112   , optDiskMoves   :: Bool           -- ^ Allow disk moves
113   , optInstMoves   :: Bool           -- ^ Allow instance moves
114   , optDiskTemplate :: Maybe DiskTemplate  -- ^ Override for the disk template
115   , optDynuFile    :: Maybe FilePath -- ^ Optional file with dynamic use data
116   , optEvacMode    :: Bool           -- ^ Enable evacuation mode
117   , optExInst      :: [String]       -- ^ Instances to be excluded
118   , optExTags      :: Maybe [String] -- ^ Tags to use for exclusion
119   , optExecJobs    :: Bool           -- ^ Execute the commands via Luxi
120   , optGroup       :: Maybe GroupID  -- ^ The UUID of the group to process
121   , optSelInst     :: [String]       -- ^ Instances to be excluded
122   , optLuxi        :: Maybe FilePath -- ^ Collect data from Luxi
123   , optMachineReadable :: Bool       -- ^ Output machine-readable format
124   , optMaster      :: String         -- ^ Collect data from RAPI
125   , optMaxLength   :: Int            -- ^ Stop after this many steps
126   , optMcpu        :: Maybe Double   -- ^ Override max cpu ratio for nodes
127   , optMdsk        :: Double         -- ^ Max disk usage ratio for nodes
128   , optMinGain     :: Score          -- ^ Min gain we aim for in a step
129   , optMinGainLim  :: Score          -- ^ Limit below which we apply mingain
130   , optMinScore    :: Score          -- ^ The minimum score we aim for
131   , optNoHeaders   :: Bool           -- ^ Do not show a header line
132   , optNodeSim     :: [String]       -- ^ Cluster simulation mode
133   , optOffline     :: [String]       -- ^ Names of offline nodes
134   , optOutPath     :: FilePath       -- ^ Path to the output directory
135   , optSaveCluster :: Maybe FilePath -- ^ Save cluster state to this file
136   , optShowCmds    :: Maybe FilePath -- ^ Whether to show the command list
137   , optShowHelp    :: Bool           -- ^ Just show the help
138   , optShowInsts   :: Bool           -- ^ Whether to show the instance map
139   , optShowNodes   :: Maybe [String] -- ^ Whether to show node status
140   , optShowVer     :: Bool           -- ^ Just show the program version
141   , optStdSpec     :: Maybe RSpec    -- ^ Requested standard specs
142   , optTestCount   :: Maybe Int      -- ^ Optional test count override
143   , optTieredSpec  :: Maybe RSpec    -- ^ Requested specs for tiered mode
144   , optReplay      :: Maybe String   -- ^ Unittests: RNG state
145   , optVerbose     :: Int            -- ^ Verbosity level
146   } deriving Show
147
148 -- | Default values for the command line options.
149 defaultOptions :: Options
150 defaultOptions  = Options
151   { optDataFile    = Nothing
152   , optDiskMoves   = True
153   , optInstMoves   = True
154   , optDiskTemplate = Nothing
155   , optDynuFile    = Nothing
156   , optEvacMode    = False
157   , optExInst      = []
158   , optExTags      = Nothing
159   , optExecJobs    = False
160   , optGroup       = Nothing
161   , optSelInst     = []
162   , optLuxi        = Nothing
163   , optMachineReadable = False
164   , optMaster      = ""
165   , optMaxLength   = -1
166   , optMcpu        = Nothing
167   , optMdsk        = defReservedDiskRatio
168   , optMinGain     = 1e-2
169   , optMinGainLim  = 1e-1
170   , optMinScore    = 1e-9
171   , optNoHeaders   = False
172   , optNodeSim     = []
173   , optOffline     = []
174   , optOutPath     = "."
175   , optSaveCluster = Nothing
176   , optShowCmds    = Nothing
177   , optShowHelp    = False
178   , optShowInsts   = False
179   , optShowNodes   = Nothing
180   , optShowVer     = False
181   , optStdSpec     = Nothing
182   , optTestCount   = Nothing
183   , optTieredSpec  = Nothing
184   , optReplay      = Nothing
185   , optVerbose     = 1
186   }
187
188 -- | Abrreviation for the option type.
189 type OptType = OptDescr (Options -> Result Options)
190
191 -- * Helper functions
192
193 parseISpecString :: String -> String -> Result RSpec
194 parseISpecString descr inp = do
195   let sp = sepSplit ',' inp
196       err = Bad ("Invalid " ++ descr ++ " specification: '" ++ inp ++
197                  "', expected disk,ram,cpu")
198   when (length sp /= 3) err
199   prs <- mapM (\(fn, val) -> fn val) $
200          zip [ annotateResult (descr ++ " specs disk") . parseUnit
201              , annotateResult (descr ++ " specs memory") . parseUnit
202              , tryRead (descr ++ " specs cpus")
203              ] sp
204   case prs of
205     [dsk, ram, cpu] -> return $ RSpec cpu ram dsk
206     _ -> err
207
208 -- * Command line options
209
210 oDataFile :: OptType
211 oDataFile = Option "t" ["text-data"]
212             (ReqArg (\ f o -> Ok o { optDataFile = Just f }) "FILE")
213             "the cluster data FILE"
214
215 oDiskMoves :: OptType
216 oDiskMoves = Option "" ["no-disk-moves"]
217              (NoArg (\ opts -> Ok opts { optDiskMoves = False}))
218              "disallow disk moves from the list of allowed instance changes,\
219              \ thus allowing only the 'cheap' failover/migrate operations"
220
221 oDiskTemplate :: OptType
222 oDiskTemplate = Option "" ["disk-template"]
223                 (ReqArg (\ t opts -> do
224                            dt <- diskTemplateFromRaw t
225                            return $ opts { optDiskTemplate = Just dt })
226                  "TEMPLATE") "select the desired disk template"
227
228 oSelInst :: OptType
229 oSelInst = Option "" ["select-instances"]
230           (ReqArg (\ f opts -> Ok opts { optSelInst = sepSplit ',' f }) "INSTS")
231           "only select given instances for any moves"
232
233 oInstMoves :: OptType
234 oInstMoves = Option "" ["no-instance-moves"]
235              (NoArg (\ opts -> Ok opts { optInstMoves = False}))
236              "disallow instance (primary node) moves from the list of allowed,\
237              \ instance changes, thus allowing only slower, but sometimes\
238              \ safer, drbd secondary changes"
239
240 oDynuFile :: OptType
241 oDynuFile = Option "U" ["dynu-file"]
242             (ReqArg (\ f opts -> Ok opts { optDynuFile = Just f }) "FILE")
243             "Import dynamic utilisation data from the given FILE"
244
245 oEvacMode :: OptType
246 oEvacMode = Option "E" ["evac-mode"]
247             (NoArg (\opts -> Ok opts { optEvacMode = True }))
248             "enable evacuation mode, where the algorithm only moves \
249             \ instances away from offline and drained nodes"
250
251 oExInst :: OptType
252 oExInst = Option "" ["exclude-instances"]
253           (ReqArg (\ f opts -> Ok opts { optExInst = sepSplit ',' f }) "INSTS")
254           "exclude given instances from any moves"
255
256 oExTags :: OptType
257 oExTags = Option "" ["exclusion-tags"]
258             (ReqArg (\ f opts -> Ok opts { optExTags = Just $ sepSplit ',' f })
259              "TAG,...") "Enable instance exclusion based on given tag prefix"
260
261 oExecJobs :: OptType
262 oExecJobs = Option "X" ["exec"]
263              (NoArg (\ opts -> Ok opts { optExecJobs = True}))
264              "execute the suggested moves via Luxi (only available when using\
265              \ it for data gathering)"
266
267 oGroup :: OptType
268 oGroup = Option "G" ["group"]
269             (ReqArg (\ f o -> Ok o { optGroup = Just f }) "ID")
270             "the ID of the group to balance"
271
272 oLuxiSocket :: OptType
273 oLuxiSocket = Option "L" ["luxi"]
274               (OptArg ((\ f opts -> Ok opts { optLuxi = Just f }) .
275                        fromMaybe defaultLuxiSocket) "SOCKET")
276               "collect data via Luxi, optionally using the given SOCKET path"
277
278 oMachineReadable :: OptType
279 oMachineReadable = Option "" ["machine-readable"]
280                    (OptArg (\ f opts -> do
281                      flag <- parseYesNo True f
282                      return $ opts { optMachineReadable = flag }) "CHOICE")
283           "enable machine readable output (pass either 'yes' or 'no' to\
284           \ explicitely control the flag, or without an argument defaults to\
285           \ yes"
286
287 oMaxCpu :: OptType
288 oMaxCpu = Option "" ["max-cpu"]
289           (ReqArg (\ n opts -> do
290                      mcpu <- tryRead "parsing max-cpu" n
291                      when (mcpu <= 0) $
292                           fail "Invalid value of the max-cpu ratio,\
293                                \ expected >0"
294                      return $ opts { optMcpu = Just mcpu }) "RATIO")
295           "maximum virtual-to-physical cpu ratio for nodes (from 0\
296           \ upwards) [default read from cluster]"
297
298 oMaxSolLength :: OptType
299 oMaxSolLength = Option "l" ["max-length"]
300                 (ReqArg (\ i opts -> Ok opts { optMaxLength = read i }) "N")
301                 "cap the solution at this many balancing or allocation \
302                 \ rounds (useful for very unbalanced clusters or empty \
303                 \ clusters)"
304
305 oMinDisk :: OptType
306 oMinDisk = Option "" ["min-disk"]
307            (ReqArg (\ n opts -> Ok opts { optMdsk = read n }) "RATIO")
308            "minimum free disk space for nodes (between 0 and 1) [0]"
309
310 oMinGain :: OptType
311 oMinGain = Option "g" ["min-gain"]
312             (ReqArg (\ g opts -> Ok opts { optMinGain = read g }) "DELTA")
313             "minimum gain to aim for in a balancing step before giving up"
314
315 oMinGainLim :: OptType
316 oMinGainLim = Option "" ["min-gain-limit"]
317             (ReqArg (\ g opts -> Ok opts { optMinGainLim = read g }) "SCORE")
318             "minimum cluster score for which we start checking the min-gain"
319
320 oMinScore :: OptType
321 oMinScore = Option "e" ["min-score"]
322             (ReqArg (\ e opts -> Ok opts { optMinScore = read e }) "EPSILON")
323             "mininum score to aim for"
324
325 oNoHeaders :: OptType
326 oNoHeaders = Option "" ["no-headers"]
327              (NoArg (\ opts -> Ok opts { optNoHeaders = True }))
328              "do not show a header line"
329
330 oNodeSim :: OptType
331 oNodeSim = Option "" ["simulate"]
332             (ReqArg (\ f o -> Ok o { optNodeSim = f:optNodeSim o }) "SPEC")
333             "simulate an empty cluster, given as 'num_nodes,disk,ram,cpu'"
334
335 oOfflineNode :: OptType
336 oOfflineNode = Option "O" ["offline"]
337                (ReqArg (\ n o -> Ok o { optOffline = n:optOffline o }) "NODE")
338                "set node as offline"
339
340 oOutputDir :: OptType
341 oOutputDir = Option "d" ["output-dir"]
342              (ReqArg (\ d opts -> Ok opts { optOutPath = d }) "PATH")
343              "directory in which to write output files"
344
345 oPrintCommands :: OptType
346 oPrintCommands = Option "C" ["print-commands"]
347                  (OptArg ((\ f opts -> Ok opts { optShowCmds = Just f }) .
348                           fromMaybe "-")
349                   "FILE")
350                  "print the ganeti command list for reaching the solution,\
351                  \ if an argument is passed then write the commands to a\
352                  \ file named as such"
353
354 oPrintInsts :: OptType
355 oPrintInsts = Option "" ["print-instances"]
356               (NoArg (\ opts -> Ok opts { optShowInsts = True }))
357               "print the final instance map"
358
359 oPrintNodes :: OptType
360 oPrintNodes = Option "p" ["print-nodes"]
361               (OptArg ((\ f opts ->
362                           let (prefix, realf) = case f of
363                                                   '+':rest -> (["+"], rest)
364                                                   _ -> ([], f)
365                               splitted = prefix ++ sepSplit ',' realf
366                           in Ok opts { optShowNodes = Just splitted }) .
367                        fromMaybe []) "FIELDS")
368               "print the final node list"
369
370 oQuiet :: OptType
371 oQuiet = Option "q" ["quiet"]
372          (NoArg (\ opts -> Ok opts { optVerbose = optVerbose opts - 1 }))
373          "decrease the verbosity level"
374
375 oRapiMaster :: OptType
376 oRapiMaster = Option "m" ["master"]
377               (ReqArg (\ m opts -> Ok opts { optMaster = m }) "ADDRESS")
378               "collect data via RAPI at the given ADDRESS"
379
380 oSaveCluster :: OptType
381 oSaveCluster = Option "S" ["save"]
382             (ReqArg (\ f opts -> Ok opts { optSaveCluster = Just f }) "FILE")
383             "Save cluster state at the end of the processing to FILE"
384
385 oShowHelp :: OptType
386 oShowHelp = Option "h" ["help"]
387             (NoArg (\ opts -> Ok opts { optShowHelp = True}))
388             "show help"
389
390 oShowVer :: OptType
391 oShowVer = Option "V" ["version"]
392            (NoArg (\ opts -> Ok opts { optShowVer = True}))
393            "show the version of the program"
394
395 oStdSpec :: OptType
396 oStdSpec = Option "" ["standard-alloc"]
397              (ReqArg (\ inp opts -> do
398                         tspec <- parseISpecString "standard" inp
399                         return $ opts { optStdSpec = Just tspec } )
400               "STDSPEC")
401              "enable standard specs allocation, given as 'disk,ram,cpu'"
402
403 oTestCount :: OptType
404 oTestCount = Option "" ["test-count"]
405              (ReqArg (\ inp opts -> do
406                         tcount <- tryRead "parsing test count" inp
407                         return $ opts { optTestCount = Just tcount } )
408               "COUNT")
409              "override the target test count"
410
411 oTieredSpec :: OptType
412 oTieredSpec = Option "" ["tiered-alloc"]
413              (ReqArg (\ inp opts -> do
414                         tspec <- parseISpecString "tiered" inp
415                         return $ opts { optTieredSpec = Just tspec } )
416               "TSPEC")
417              "enable tiered specs allocation, given as 'disk,ram,cpu'"
418
419 oReplay :: OptType
420 oReplay = Option "" ["replay"]
421           (ReqArg (\ stat opts -> Ok opts { optReplay = Just stat } ) "STATE")
422           "Pre-seed the random number generator with STATE"
423
424 oVerbose :: OptType
425 oVerbose = Option "v" ["verbose"]
426            (NoArg (\ opts -> Ok opts { optVerbose = optVerbose opts + 1 }))
427            "increase the verbosity level"
428
429 -- * Functions
430
431 -- | Helper for parsing a yes\/no command line flag.
432 parseYesNo :: Bool         -- ^ Default value (when we get a @Nothing@)
433            -> Maybe String -- ^ Parameter value
434            -> Result Bool  -- ^ Resulting boolean value
435 parseYesNo v Nothing      = return v
436 parseYesNo _ (Just "yes") = return True
437 parseYesNo _ (Just "no")  = return False
438 parseYesNo _ (Just s)     = fail ("Invalid choice '" ++ s ++
439                                   "', pass one of 'yes' or 'no'")
440
441 -- | Usage info.
442 usageHelp :: String -> [OptType] -> String
443 usageHelp progname =
444   usageInfo (printf "%s %s\nUsage: %s [OPTION...]"
445              progname Version.version progname)
446
447 -- | Show the program version info.
448 versionInfo :: String -> String
449 versionInfo progname =
450   printf "%s %s\ncompiled with %s %s\nrunning on %s %s\n"
451          progname Version.version compilerName
452          (Data.Version.showVersion compilerVersion)
453          os arch
454
455 -- | Command line parser, using the 'Options' structure.
456 parseOpts :: [String]               -- ^ The command line arguments
457           -> String                 -- ^ The program name
458           -> [OptType]              -- ^ The supported command line options
459           -> IO (Options, [String]) -- ^ The resulting options and leftover
460                                     -- arguments
461 parseOpts argv progname options =
462   case parseOptsInner argv progname options of
463     Left (code, msg) -> do
464       hPutStr (if code == 0 then stdout else stderr) msg
465       exitWith (if code == 0 then ExitSuccess else ExitFailure code)
466     Right result ->
467       return result
468
469 -- | Inner parse options. The arguments are similar to 'parseOpts',
470 -- but it returns either a 'Left' composed of exit code and message,
471 -- or a 'Right' for the success case.
472 parseOptsInner :: [String] -> String -> [OptType]
473                -> Either (Int, String) (Options, [String])
474 parseOptsInner argv progname options =
475   case getOpt Permute options argv of
476     (o, n, []) ->
477       let (pr, args) = (foldM (flip id) defaultOptions o, n)
478       in case pr of
479            Bad msg -> Left (1, "Error while parsing command\
480                                \line arguments:\n" ++ msg ++ "\n")
481            Ok po ->
482              select (Right (po, args))
483                  [ (optShowHelp po, Left (0, usageHelp progname options))
484                  , (optShowVer po,  Left (0, versionInfo progname))
485                  ]
486     (_, _, errs) ->
487       Left (2, "Command line error: "  ++ concat errs ++ "\n" ++
488             usageHelp progname options)
489
490 -- | A shell script template for autogenerated scripts.
491 shTemplate :: String
492 shTemplate =
493   printf "#!/bin/sh\n\n\
494          \# Auto-generated script for executing cluster rebalancing\n\n\
495          \# To stop, touch the file /tmp/stop-htools\n\n\
496          \set -e\n\n\
497          \check() {\n\
498          \  if [ -f /tmp/stop-htools ]; then\n\
499          \    echo 'Stop requested, exiting'\n\
500          \    exit 0\n\
501          \  fi\n\
502          \}\n\n"
503
504 -- | Optionally print the node list.
505 maybePrintNodes :: Maybe [String]       -- ^ The field list
506                 -> String               -- ^ Informational message
507                 -> ([String] -> String) -- ^ Function to generate the listing
508                 -> IO ()
509 maybePrintNodes Nothing _ _ = return ()
510 maybePrintNodes (Just fields) msg fn = do
511   hPutStrLn stderr ""
512   hPutStrLn stderr (msg ++ " status:")
513   hPutStrLn stderr $ fn fields
514
515
516 -- | Optionally print the instance list.
517 maybePrintInsts :: Bool   -- ^ Whether to print the instance list
518                 -> String -- ^ Type of the instance map (e.g. initial)
519                 -> String -- ^ The instance data
520                 -> IO ()
521 maybePrintInsts do_print msg instdata =
522   when do_print $ do
523     hPutStrLn stderr ""
524     hPutStrLn stderr $ msg ++ " instance map:"
525     hPutStr stderr instdata
526
527 -- | Function to display warning messages from parsing the cluster
528 -- state.
529 maybeShowWarnings :: [String] -- ^ The warning messages
530                   -> IO ()
531 maybeShowWarnings fix_msgs =
532   unless (null fix_msgs) $ do
533     hPutStrLn stderr "Warning: cluster has inconsistent data:"
534     hPutStrLn stderr . unlines . map (printf "  - %s") $ fix_msgs
535
536 -- | Potentially set the node as offline based on passed offline list.
537 setNodeOffline :: [Ndx] -> Node.Node -> Node.Node
538 setNodeOffline offline_indices n =
539   if Node.idx n `elem` offline_indices
540     then Node.setOffline n True
541     else n
542
543 -- | Set node properties based on command line options.
544 setNodeStatus :: Options -> Node.List -> IO Node.List
545 setNodeStatus opts fixed_nl = do
546   let offline_passed = optOffline opts
547       all_nodes = Container.elems fixed_nl
548       offline_lkp = map (lookupName (map Node.name all_nodes)) offline_passed
549       offline_wrong = filter (not . goodLookupResult) offline_lkp
550       offline_names = map lrContent offline_lkp
551       offline_indices = map Node.idx $
552                         filter (\n -> Node.name n `elem` offline_names)
553                                all_nodes
554       m_cpu = optMcpu opts
555       m_dsk = optMdsk opts
556
557   unless (null offline_wrong) $ do
558          hPrintf stderr "Error: Wrong node name(s) set as offline: %s\n"
559                      (commaJoin (map lrContent offline_wrong)) :: IO ()
560          exitWith $ ExitFailure 1
561   let setMCpuFn = case m_cpu of
562                     Nothing -> id
563                     Just new_mcpu -> flip Node.setMcpu new_mcpu
564   let nm = Container.map (setNodeOffline offline_indices .
565                           flip Node.setMdsk m_dsk .
566                           setMCpuFn) fixed_nl
567   return nm