1 {-| Implementation of command-line functions.
3 This module holds the common cli-related functions for the binaries,
4 separated into this module since Utils.hs is used in many other places
5 and this is more IO oriented.
11 Copyright (C) 2009 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
65 import Control.Exception
66 import Data.Maybe (isJust, fromJust, fromMaybe)
67 import qualified Data.Version
69 import System.Console.GetOpt
70 import System.Posix.Env
74 import Text.Printf (printf, hPrintf)
76 import qualified Ganeti.HTools.Version as Version(version)
77 import qualified Ganeti.HTools.Luxi as Luxi
78 import qualified Ganeti.HTools.Rapi as Rapi
79 import qualified Ganeti.HTools.Simu as Simu
80 import qualified Ganeti.HTools.Text as Text
81 import qualified Ganeti.HTools.Loader as Loader
82 import qualified Ganeti.HTools.Instance as Instance
83 import qualified Ganeti.HTools.Node as Node
84 import qualified Ganeti.HTools.Cluster as Cluster
86 import Ganeti.HTools.Types
88 -- | The default value for the luxi socket
89 defaultLuxiSocket :: FilePath
90 defaultLuxiSocket = "/var/run/ganeti/socket/ganeti-master"
92 -- | Command line options structure.
93 data Options = Options
94 { optShowNodes :: Bool -- ^ Whether to show node status
95 , optShowCmds :: Maybe FilePath -- ^ Whether to show the command list
96 , optOneline :: Bool -- ^ Switch output to a single line
97 , optOutPath :: FilePath -- ^ Path to the output directory
98 , optNoHeaders :: Bool -- ^ Do not show a header line
99 , optNodeFile :: FilePath -- ^ Path to the nodes file
100 , optNodeSet :: Bool -- ^ The nodes have been set by options
101 , optInstFile :: FilePath -- ^ Path to the instances file
102 , optInstSet :: Bool -- ^ The insts have been set by options
103 , optNodeSim :: Maybe String -- ^ Cluster simulation mode
104 , optMaxLength :: Int -- ^ Stop after this many steps
105 , optMaster :: String -- ^ Collect data from RAPI
106 , optLuxi :: Maybe FilePath -- ^ Collect data from Luxi
107 , optOffline :: [String] -- ^ Names of offline nodes
108 , optIMem :: Int -- ^ Instance memory
109 , optIDsk :: Int -- ^ Instance disk
110 , optIVCPUs :: Int -- ^ Instance VCPUs
111 , optINodes :: Int -- ^ Nodes required for an instance
112 , optMinScore :: Cluster.Score -- ^ The minimum score we aim for
113 , optMcpu :: Double -- ^ Max cpu ratio for nodes
114 , optMdsk :: Double -- ^ Max disk usage ratio for nodes
115 , optDiskMoves :: Bool -- ^ Allow disk moves
116 , optVerbose :: Int -- ^ Verbosity level
117 , optShowVer :: Bool -- ^ Just show the program version
118 , optShowHelp :: Bool -- ^ Just show the help
121 -- | Default values for the command line options.
122 defaultOptions :: Options
123 defaultOptions = Options
124 { optShowNodes = False
125 , optShowCmds = Nothing
127 , optNoHeaders = False
129 , optNodeFile = "nodes"
131 , optInstFile = "instances"
133 , optNodeSim = Nothing
145 , optDiskMoves = True
148 , optShowHelp = False
151 -- | Abrreviation for the option type
152 type OptType = OptDescr (Options -> Options)
154 oPrintNodes :: OptType
155 oPrintNodes = Option "p" ["print-nodes"]
156 (NoArg (\ opts -> opts { optShowNodes = True }))
157 "print the final node list"
159 oPrintCommands :: OptType
160 oPrintCommands = Option "C" ["print-commands"]
161 (OptArg ((\ f opts -> opts { optShowCmds = Just f }) .
164 "print the ganeti command list for reaching the solution,\
165 \ if an argument is passed then write the commands to a\
166 \ file named as such"
169 oOneline = Option "o" ["oneline"]
170 (NoArg (\ opts -> opts { optOneline = True }))
171 "print the ganeti command list for reaching the solution"
173 oNoHeaders :: OptType
174 oNoHeaders = Option "" ["no-headers"]
175 (NoArg (\ opts -> opts { optNoHeaders = True }))
176 "do not show a header line"
178 oOutputDir :: OptType
179 oOutputDir = Option "d" ["output-dir"]
180 (ReqArg (\ d opts -> opts { optOutPath = d }) "PATH")
181 "directory in which to write output files"
184 oNodeFile = Option "n" ["nodes"]
185 (ReqArg (\ f o -> o { optNodeFile = f, optNodeSet = True }) "FILE")
189 oInstFile = Option "i" ["instances"]
190 (ReqArg (\ f o -> o { optInstFile = f, optInstSet = True }) "FILE")
191 "the instance list FILE"
194 oNodeSim = Option "" ["simulate"]
195 (ReqArg (\ f o -> o { optNodeSim = Just f }) "SPEC")
196 "simulate an empty cluster, given as 'num_nodes,disk,memory,cpus'"
198 oRapiMaster :: OptType
199 oRapiMaster = Option "m" ["master"]
200 (ReqArg (\ m opts -> opts { optMaster = m }) "ADDRESS")
201 "collect data via RAPI at the given ADDRESS"
203 oLuxiSocket :: OptType
204 oLuxiSocket = Option "L" ["luxi"]
205 (OptArg ((\ f opts -> opts { optLuxi = Just f }) .
206 fromMaybe defaultLuxiSocket) "SOCKET")
207 "collect data via Luxi, optionally using the given SOCKET path"
210 oVerbose = Option "v" ["verbose"]
211 (NoArg (\ opts -> opts { optVerbose = optVerbose opts + 1 }))
212 "increase the verbosity level"
215 oQuiet = Option "q" ["quiet"]
216 (NoArg (\ opts -> opts { optVerbose = optVerbose opts - 1 }))
217 "decrease the verbosity level"
219 oOfflineNode :: OptType
220 oOfflineNode = Option "O" ["offline"]
221 (ReqArg (\ n o -> o { optOffline = n:optOffline o }) "NODE")
222 "set node as offline"
224 oMaxSolLength :: OptType
225 oMaxSolLength = Option "l" ["max-length"]
226 (ReqArg (\ i opts -> opts { optMaxLength = read i::Int }) "N")
227 "cap the solution at this many moves (useful for very\
228 \ unbalanced clusters)"
231 oMinScore = Option "e" ["min-score"]
232 (ReqArg (\ e opts -> opts { optMinScore = read e }) "EPSILON")
233 " mininum score to aim for"
236 oIMem = Option "" ["memory"]
237 (ReqArg (\ m opts -> opts { optIMem = read m }) "MEMORY")
238 "memory size for instances"
241 oIDisk = Option "" ["disk"]
242 (ReqArg (\ d opts -> opts { optIDsk = read d }) "DISK")
243 "disk size for instances"
246 oIVcpus = Option "" ["vcpus"]
247 (ReqArg (\ p opts -> opts { optIVCPUs = read p }) "NUM")
248 "number of virtual cpus for instances"
251 oINodes = Option "" ["req-nodes"]
252 (ReqArg (\ n opts -> opts { optINodes = read n }) "NODES")
253 "number of nodes for the new instances (1=plain, 2=mirrored)"
256 oMaxCpu = Option "" ["max-cpu"]
257 (ReqArg (\ n opts -> opts { optMcpu = read n }) "RATIO")
258 "maximum virtual-to-physical cpu ratio for nodes"
261 oMinDisk = Option "" ["min-disk"]
262 (ReqArg (\ n opts -> opts { optMdsk = read n }) "RATIO")
263 "minimum free disk space for nodes (between 0 and 1)"
265 oDiskMoves :: OptType
266 oDiskMoves = Option "" ["no-disk-moves"]
267 (NoArg (\ opts -> opts { optDiskMoves = False}))
268 "disallow disk moves from the list of allowed instance changes,\
269 \ thus allowing only the 'cheap' failover/migrate operations"
272 oShowVer = Option "V" ["version"]
273 (NoArg (\ opts -> opts { optShowVer = True}))
274 "show the version of the program"
277 oShowHelp = Option "h" ["help"]
278 (NoArg (\ opts -> opts { optShowHelp = True}))
282 usageHelp :: String -> [OptType] -> String
284 usageInfo (printf "%s %s\nUsage: %s [OPTION...]"
285 progname Version.version progname)
287 -- | Command line parser, using the 'options' structure.
288 parseOpts :: [String] -- ^ The command line arguments
289 -> String -- ^ The program name
290 -> [OptType] -- ^ The supported command line options
291 -> IO (Options, [String]) -- ^ The resulting options and leftover
293 parseOpts argv progname options =
294 case getOpt Permute options argv of
297 let resu@(po, _) = (foldl (flip id) defaultOptions o, n)
298 when (optShowHelp po) $ do
299 putStr $ usageHelp progname options
301 when (optShowVer po) $ do
302 printf "%s %s\ncompiled with %s %s\nrunning on %s %s\n"
303 progname Version.version
304 compilerName (Data.Version.showVersion compilerVersion)
309 hPutStrLn stderr $ "Command line error: " ++ concat errs
310 hPutStrLn stderr $ usageHelp progname options
311 exitWith $ ExitFailure 2
313 -- | Parse the environment and return the node\/instance names.
315 -- This also hardcodes here the default node\/instance file names.
316 parseEnv :: () -> IO (String, String)
318 a <- getEnvDefault "HTOOLS_NODES" "nodes"
319 b <- getEnvDefault "HTOOLS_INSTANCES" "instances"
322 -- | A shell script template for autogenerated scripts.
325 printf "#!/bin/sh\n\n\
326 \# Auto-generated script for executing cluster rebalancing\n\n\
327 \# To stop, touch the file /tmp/stop-htools\n\n\
330 \ if [ -f /tmp/stop-htools ]; then\n\
331 \ echo 'Stop requested, exiting'\n\
336 -- | Error beautifier
337 wrapIO :: IO (Result a) -> IO (Result a)
338 wrapIO = handle (return . Bad . show)
340 -- | External tool data loader from a variety of sources.
341 loadExternalData :: Options
342 -> IO (Node.List, Instance.List, String)
343 loadExternalData opts = do
344 (env_node, env_inst) <- parseEnv ()
345 let nodef = if optNodeSet opts then optNodeFile opts
347 instf = if optInstSet opts then optInstFile opts
349 mhost = optMaster opts
351 simdata = optNodeSim opts
352 setRapi = mhost /= ""
353 setLuxi = isJust lsock
354 setSim = isJust simdata
355 setFiles = optNodeSet opts || optInstSet opts
356 allSet = filter id [setRapi, setLuxi, setFiles]
357 when (length allSet > 1) $
359 hPutStrLn stderr "Error: Only one of the rapi, luxi, and data\
360 \ files options should be given."
361 exitWith $ ExitFailure 1
365 _ | setRapi -> wrapIO $ Rapi.loadData mhost
366 | setLuxi -> wrapIO $ Luxi.loadData $ fromJust lsock
367 | setSim -> Simu.loadData $ fromJust simdata
368 | otherwise -> wrapIO $ Text.loadData nodef instf
370 let ldresult = input_data >>= Loader.mergeData
371 (loaded_nl, il, csf) <-
375 hPrintf stderr "Error: failed to load data. Details:\n%s\n" s
376 exitWith $ ExitFailure 1
378 let (fix_msgs, fixed_nl) = Loader.checkData loaded_nl il
380 unless (null fix_msgs || optVerbose opts == 0) $ do
381 hPutStrLn stderr "Warning: cluster has inconsistent data:"
382 hPutStrLn stderr . unlines . map (printf " - %s") $ fix_msgs
384 return (fixed_nl, il, csf)