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
34 , Ganeti.HTools.CLI.parseOpts
87 import Data.Char (toUpper)
88 import Data.Maybe (fromMaybe)
89 import System.Console.GetOpt
91 import Text.Printf (printf)
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
103 -- | The default value for the luxi socket.
105 -- This is re-exported from the "Ganeti.Constants" module.
106 defaultLuxiSocket :: FilePath
107 defaultLuxiSocket = C.masterSocket
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
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
164 , optExTags = Nothing
165 , optExecJobs = False
167 , optIAllocSrc = Nothing
170 , optMachineReadable = False
174 , optMdsk = defReservedDiskRatio
176 , optMinGainLim = 1e-1
178 , optNoHeaders = False
179 , optNoSimulation = False
183 , optSaveCluster = Nothing
184 , optShowCmds = Nothing
185 , optShowHelp = False
186 , optShowInsts = False
187 , optShowNodes = Nothing
189 , optStdSpec = Nothing
190 , optTestCount = Nothing
191 , optTieredSpec = Nothing
192 , optReplay = Nothing
196 -- | Abrreviation for the option type.
197 type OptType = GenericOptType Options
199 instance StandardOptions Options where
200 helpRequested = optShowHelp
201 verRequested = optShowVer
202 requestHelp = \opts -> opts { optShowHelp = True }
203 requestVer = \opts -> opts { optShowVer = True }
205 -- * Helper functions
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")
219 [dsk, ram, cpu] -> return $ RSpec cpu ram dsk
222 -- * Command line options
225 oDataFile = Option "t" ["text-data"]
226 (ReqArg (\ f o -> Ok o { optDataFile = Just f }) "FILE")
227 "the cluster data FILE"
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"
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"
241 oSpindleUse :: OptType
242 oSpindleUse = Option "" ["spindle-use"]
243 (reqWithConversion (tryRead "parsing spindle-use")
246 fail "Invalid value of the spindle-use\
248 return $ opts { optSpindleUse = Just su })
249 "SPINDLES") "select how many virtual spindle instances use\
250 \ [default read from cluster]"
253 oSelInst = Option "" ["select-instances"]
254 (ReqArg (\ f opts -> Ok opts { optSelInst = sepSplit ',' f }) "INSTS")
255 "only select given instances for any moves"
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"
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"
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"
276 oExInst = Option "" ["exclude-instances"]
277 (ReqArg (\ f opts -> Ok opts { optExInst = sepSplit ',' f }) "INSTS")
278 "exclude given instances from any moves"
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"
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)"
292 oGroup = Option "G" ["group"]
293 (ReqArg (\ f o -> Ok o { optGroup = Just f }) "ID")
294 "the ID of the group to balance"
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"
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"
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\
317 oMaxCpu = Option "" ["max-cpu"]
318 (reqWithConversion (tryRead "parsing max-cpu")
321 fail "Invalid value of the max-cpu ratio,\
323 return $ opts { optMcpu = Just mcpu }) "RATIO")
324 "maximum virtual-to-physical cpu ratio for nodes (from 0\
325 \ upwards) [default read from cluster]"
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 \
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]"
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"
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"
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"
359 oNoHeaders :: OptType
360 oNoHeaders = Option "" ["no-headers"]
361 (NoArg (\ opts -> Ok opts { optNoHeaders = True }))
362 "do not show a header line"
364 oNoSimulation :: OptType
365 oNoSimulation = Option "" ["no-simulation"]
366 (NoArg (\opts -> Ok opts {optNoSimulation = True}))
367 "do not perform rebalancing simulation"
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'"
375 oOfflineNode :: OptType
376 oOfflineNode = Option "O" ["offline"]
377 (ReqArg (\ n o -> Ok o { optOffline = n:optOffline o }) "NODE")
378 "set node as offline"
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"
385 oPrintCommands :: OptType
386 oPrintCommands = Option "C" ["print-commands"]
387 (OptArg ((\ f opts -> Ok opts { optShowCmds = Just f }) .
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"
394 oPrintInsts :: OptType
395 oPrintInsts = Option "" ["print-instances"]
396 (NoArg (\ opts -> Ok opts { optShowInsts = True }))
397 "print the final instance map"
399 oPrintNodes :: OptType
400 oPrintNodes = Option "p" ["print-nodes"]
401 (OptArg ((\ f opts ->
402 let (prefix, realf) = case f of
403 '+':rest -> (["+"], rest)
405 splitted = prefix ++ sepSplit ',' realf
406 in Ok opts { optShowNodes = Just splitted }) .
407 fromMaybe []) "FIELDS")
408 "print the final node list"
411 oQuiet = Option "q" ["quiet"]
412 (NoArg (\ opts -> Ok opts { optVerbose = optVerbose opts - 1 }))
413 "decrease the verbosity level"
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"
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"
426 oStdSpec = Option "" ["standard-alloc"]
427 (ReqArg (\ inp opts -> do
428 tspec <- parseISpecString "standard" inp
429 return $ opts { optStdSpec = Just tspec } )
431 "enable standard specs allocation, given as 'disk,ram,cpu'"
433 oTieredSpec :: OptType
434 oTieredSpec = Option "" ["tiered-alloc"]
435 (ReqArg (\ inp opts -> do
436 tspec <- parseISpecString "tiered" inp
437 return $ opts { optTieredSpec = Just tspec } )
439 "enable tiered specs allocation, given as 'disk,ram,cpu'"
442 oVerbose = Option "v" ["verbose"]
443 (NoArg (\ opts -> Ok opts { optVerbose = optVerbose opts + 1 }))
444 "increase the verbosity level"
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
454 parseOpts = Common.parseOpts defaultOptions
457 -- | A shell script template for autogenerated scripts.
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\
465 \ if [ -f /tmp/stop-htools ]; then\n\
466 \ echo 'Stop requested, exiting'\n\
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
476 maybePrintNodes Nothing _ _ = return ()
477 maybePrintNodes (Just fields) msg fn = do
479 hPutStrLn stderr (msg ++ " status:")
480 hPutStrLn stderr $ fn fields
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
487 maybePrintInsts do_print msg instdata =
490 hPutStrLn stderr $ msg ++ " instance map:"
491 hPutStr stderr instdata
493 -- | Function to display warning messages from parsing the cluster
495 maybeShowWarnings :: [String] -- ^ The warning messages
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
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
508 printf "%s_%s=%s\n" prefix (map toUpper k) (ensureQuoted v))
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
515 printFinal prefix True =
516 -- this should be the final entry
517 printKeys prefix [("OK", "1")]
519 printFinal _ False = return ()
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
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)
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
547 Just new_mcpu -> flip Node.setMcpu new_mcpu
548 let nm = Container.map (setNodeOffline offline_indices .
549 flip Node.setMdsk m_dsk .