1 {-| Implementation of command-line functions.
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.
11 Copyright (C) 2009, 2010, 2011, 2012 Google Inc.
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.
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.
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
30 module Ganeti.HTools.CLI
83 import Data.Maybe (fromMaybe)
84 import qualified Data.Version
85 import System.Console.GetOpt
89 import Text.Printf (printf, hPrintf)
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
101 -- | The default value for the luxi socket.
103 -- This is re-exported from the "Ganeti.Constants" module.
104 defaultLuxiSocket :: FilePath
105 defaultLuxiSocket = C.masterSocket
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
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
158 , optExTags = Nothing
159 , optExecJobs = False
163 , optMachineReadable = False
167 , optMdsk = defReservedDiskRatio
169 , optMinGainLim = 1e-1
171 , optNoHeaders = False
175 , optSaveCluster = Nothing
176 , optShowCmds = Nothing
177 , optShowHelp = False
178 , optShowInsts = False
179 , optShowNodes = Nothing
181 , optStdSpec = Nothing
182 , optTestCount = Nothing
183 , optTieredSpec = Nothing
184 , optReplay = Nothing
188 -- | Abrreviation for the option type.
189 type OptType = OptDescr (Options -> Result Options)
191 -- * Helper functions
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")
205 [dsk, ram, cpu] -> return $ RSpec cpu ram dsk
208 -- * Command line options
211 oDataFile = Option "t" ["text-data"]
212 (ReqArg (\ f o -> Ok o { optDataFile = Just f }) "FILE")
213 "the cluster data FILE"
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"
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"
229 oSelInst = Option "" ["select-instances"]
230 (ReqArg (\ f opts -> Ok opts { optSelInst = sepSplit ',' f }) "INSTS")
231 "only select given instances for any moves"
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"
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"
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"
252 oExInst = Option "" ["exclude-instances"]
253 (ReqArg (\ f opts -> Ok opts { optExInst = sepSplit ',' f }) "INSTS")
254 "exclude given instances from any moves"
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"
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)"
268 oGroup = Option "G" ["group"]
269 (ReqArg (\ f o -> Ok o { optGroup = Just f }) "ID")
270 "the ID of the group to balance"
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"
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\
288 oMaxCpu = Option "" ["max-cpu"]
289 (ReqArg (\ n opts -> do
290 mcpu <- tryRead "parsing max-cpu" n
292 fail "Invalid value of the max-cpu ratio,\
294 return $ opts { optMcpu = Just mcpu }) "RATIO")
295 "maximum virtual-to-physical cpu ratio for nodes (from 0\
296 \ upwards) [default read from cluster]"
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 \
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]"
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"
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"
321 oMinScore = Option "e" ["min-score"]
322 (ReqArg (\ e opts -> Ok opts { optMinScore = read e }) "EPSILON")
323 "mininum score to aim for"
325 oNoHeaders :: OptType
326 oNoHeaders = Option "" ["no-headers"]
327 (NoArg (\ opts -> Ok opts { optNoHeaders = True }))
328 "do not show a header line"
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'"
335 oOfflineNode :: OptType
336 oOfflineNode = Option "O" ["offline"]
337 (ReqArg (\ n o -> Ok o { optOffline = n:optOffline o }) "NODE")
338 "set node as offline"
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"
345 oPrintCommands :: OptType
346 oPrintCommands = Option "C" ["print-commands"]
347 (OptArg ((\ f opts -> Ok opts { optShowCmds = Just f }) .
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"
354 oPrintInsts :: OptType
355 oPrintInsts = Option "" ["print-instances"]
356 (NoArg (\ opts -> Ok opts { optShowInsts = True }))
357 "print the final instance map"
359 oPrintNodes :: OptType
360 oPrintNodes = Option "p" ["print-nodes"]
361 (OptArg ((\ f opts ->
362 let (prefix, realf) = case f of
363 '+':rest -> (["+"], rest)
365 splitted = prefix ++ sepSplit ',' realf
366 in Ok opts { optShowNodes = Just splitted }) .
367 fromMaybe []) "FIELDS")
368 "print the final node list"
371 oQuiet = Option "q" ["quiet"]
372 (NoArg (\ opts -> Ok opts { optVerbose = optVerbose opts - 1 }))
373 "decrease the verbosity level"
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"
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"
386 oShowHelp = Option "h" ["help"]
387 (NoArg (\ opts -> Ok opts { optShowHelp = True}))
391 oShowVer = Option "V" ["version"]
392 (NoArg (\ opts -> Ok opts { optShowVer = True}))
393 "show the version of the program"
396 oStdSpec = Option "" ["standard-alloc"]
397 (ReqArg (\ inp opts -> do
398 tspec <- parseISpecString "standard" inp
399 return $ opts { optStdSpec = Just tspec } )
401 "enable standard specs allocation, given as 'disk,ram,cpu'"
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 } )
409 "override the target test count"
411 oTieredSpec :: OptType
412 oTieredSpec = Option "" ["tiered-alloc"]
413 (ReqArg (\ inp opts -> do
414 tspec <- parseISpecString "tiered" inp
415 return $ opts { optTieredSpec = Just tspec } )
417 "enable tiered specs allocation, given as 'disk,ram,cpu'"
420 oReplay = Option "" ["replay"]
421 (ReqArg (\ stat opts -> Ok opts { optReplay = Just stat } ) "STATE")
422 "Pre-seed the random number generator with STATE"
425 oVerbose = Option "v" ["verbose"]
426 (NoArg (\ opts -> Ok opts { optVerbose = optVerbose opts + 1 }))
427 "increase the verbosity level"
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'")
442 usageHelp :: String -> [OptType] -> String
444 usageInfo (printf "%s %s\nUsage: %s [OPTION...]"
445 progname Version.version progname)
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)
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
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)
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
477 let (pr, args) = (foldM (flip id) defaultOptions o, n)
479 Bad msg -> Left (1, "Error while parsing command\
480 \line arguments:\n" ++ msg ++ "\n")
482 select (Right (po, args))
483 [ (optShowHelp po, Left (0, usageHelp progname options))
484 , (optShowVer po, Left (0, versionInfo progname))
487 Left (2, "Command line error: " ++ concat errs ++ "\n" ++
488 usageHelp progname options)
490 -- | A shell script template for autogenerated scripts.
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\
498 \ if [ -f /tmp/stop-htools ]; then\n\
499 \ echo 'Stop requested, exiting'\n\
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
509 maybePrintNodes Nothing _ _ = return ()
510 maybePrintNodes (Just fields) msg fn = do
512 hPutStrLn stderr (msg ++ " status:")
513 hPutStrLn stderr $ fn fields
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
521 maybePrintInsts do_print msg instdata =
524 hPutStrLn stderr $ msg ++ " instance map:"
525 hPutStr stderr instdata
527 -- | Function to display warning messages from parsing the cluster
529 maybeShowWarnings :: [String] -- ^ The warning messages
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
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
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)
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
563 Just new_mcpu -> flip Node.setMcpu new_mcpu
564 let nm = Container.map (setNodeOffline offline_indices .
565 flip Node.setMdsk m_dsk .