Add a command-line option to filter exclusion tags
[ganeti-local] / Ganeti / HTools / CLI.hs
1 {-| Implementation of command-line functions.
2
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.
6
7 -}
8
9 {-
10
11 Copyright (C) 2009 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     , shTemplate
35     -- * The options
36     , oPrintNodes
37     , oPrintInsts
38     , oPrintCommands
39     , oOneline
40     , oNoHeaders
41     , oOutputDir
42     , oNodeFile
43     , oInstFile
44     , oNodeSim
45     , oRapiMaster
46     , oLuxiSocket
47     , oExecJobs
48     , oMaxSolLength
49     , oVerbose
50     , oQuiet
51     , oOfflineNode
52     , oMinScore
53     , oIMem
54     , oIDisk
55     , oIVcpus
56     , oINodes
57     , oMaxCpu
58     , oMinDisk
59     , oDiskMoves
60     , oDynuFile
61     , oTieredSpec
62     , oExTags
63     , oShowVer
64     , oShowHelp
65     ) where
66
67 import Data.Maybe (fromMaybe)
68 import qualified Data.Version
69 import Monad
70 import System.Console.GetOpt
71 import System.IO
72 import System.Info
73 import System
74 import Text.Printf (printf)
75
76 import qualified Ganeti.HTools.Version as Version(version)
77 import Ganeti.HTools.Types
78 import Ganeti.HTools.Utils
79
80 -- | The default value for the luxi socket
81 defaultLuxiSocket :: FilePath
82 defaultLuxiSocket = "/var/run/ganeti/socket/ganeti-master"
83
84 -- | Command line options structure.
85 data Options = Options
86     { optShowNodes   :: Maybe [String] -- ^ Whether to show node status
87     , optShowInsts   :: Bool           -- ^ Whether to show the instance map
88     , optShowCmds    :: Maybe FilePath -- ^ Whether to show the command list
89     , optOneline     :: Bool           -- ^ Switch output to a single line
90     , optOutPath     :: FilePath       -- ^ Path to the output directory
91     , optNoHeaders   :: Bool           -- ^ Do not show a header line
92     , optNodeFile    :: FilePath       -- ^ Path to the nodes file
93     , optNodeSet     :: Bool           -- ^ The nodes have been set by options
94     , optInstFile    :: FilePath       -- ^ Path to the instances file
95     , optInstSet     :: Bool           -- ^ The insts have been set by options
96     , optNodeSim     :: Maybe String   -- ^ Cluster simulation mode
97     , optMaxLength   :: Int            -- ^ Stop after this many steps
98     , optMaster      :: String         -- ^ Collect data from RAPI
99     , optLuxi        :: Maybe FilePath -- ^ Collect data from Luxi
100     , optExecJobs    :: Bool           -- ^ Execute the commands via Luxi
101     , optOffline     :: [String]       -- ^ Names of offline nodes
102     , optINodes      :: Int            -- ^ Nodes required for an instance
103     , optISpec       :: RSpec          -- ^ Requested instance specs
104     , optTieredSpec  :: Maybe RSpec    -- ^ Requested specs for tiered mode
105     , optMinScore    :: Score          -- ^ The minimum score we aim for
106     , optMcpu        :: Double         -- ^ Max cpu ratio for nodes
107     , optMdsk        :: Double         -- ^ Max disk usage ratio for nodes
108     , optDiskMoves   :: Bool           -- ^ Allow disk moves
109     , optDynuFile    :: Maybe FilePath -- ^ Optional file with dynamic use data
110     , optExTags      :: Maybe [String] -- ^ Tags to use for exclusion
111     , optVerbose     :: Int            -- ^ Verbosity level
112     , optShowVer     :: Bool           -- ^ Just show the program version
113     , optShowHelp    :: Bool           -- ^ Just show the help
114     } deriving Show
115
116 -- | Default values for the command line options.
117 defaultOptions :: Options
118 defaultOptions  = Options
119  { optShowNodes   = Nothing
120  , optShowInsts   = False
121  , optShowCmds    = Nothing
122  , optOneline     = False
123  , optNoHeaders   = False
124  , optOutPath     = "."
125  , optNodeFile    = "nodes"
126  , optNodeSet     = False
127  , optInstFile    = "instances"
128  , optInstSet     = False
129  , optNodeSim     = Nothing
130  , optMaxLength   = -1
131  , optMaster      = ""
132  , optLuxi        = Nothing
133  , optExecJobs    = False
134  , optOffline     = []
135  , optINodes      = 2
136  , optISpec       = RSpec 1 4096 102400
137  , optTieredSpec  = Nothing
138  , optMinScore    = 1e-9
139  , optMcpu        = -1
140  , optMdsk        = -1
141  , optDiskMoves   = True
142  , optDynuFile    = Nothing
143  , optExTags      = Nothing
144  , optVerbose     = 1
145  , optShowVer     = False
146  , optShowHelp    = False
147  }
148
149 -- | Abrreviation for the option type
150 type OptType = OptDescr (Options -> Result Options)
151
152 oPrintNodes :: OptType
153 oPrintNodes = Option "p" ["print-nodes"]
154               (OptArg ((\ f opts ->
155                             let splitted = sepSplit ',' f
156                             in Ok opts { optShowNodes = Just splitted }) .
157                        fromMaybe []) "FIELDS")
158               "print the final node list"
159
160 oPrintInsts :: OptType
161 oPrintInsts = Option "" ["print-instances"]
162               (NoArg (\ opts -> Ok opts { optShowInsts = True }))
163               "print the final instance map"
164
165 oPrintCommands :: OptType
166 oPrintCommands = Option "C" ["print-commands"]
167                  (OptArg ((\ f opts -> Ok opts { optShowCmds = Just f }) .
168                           fromMaybe "-")
169                   "FILE")
170                  "print the ganeti command list for reaching the solution,\
171                  \ if an argument is passed then write the commands to a\
172                  \ file named as such"
173
174 oOneline :: OptType
175 oOneline = Option "o" ["oneline"]
176            (NoArg (\ opts -> Ok opts { optOneline = True }))
177            "print the ganeti command list for reaching the solution"
178
179 oNoHeaders :: OptType
180 oNoHeaders = Option "" ["no-headers"]
181              (NoArg (\ opts -> Ok opts { optNoHeaders = True }))
182              "do not show a header line"
183
184 oOutputDir :: OptType
185 oOutputDir = Option "d" ["output-dir"]
186              (ReqArg (\ d opts -> Ok opts { optOutPath = d }) "PATH")
187              "directory in which to write output files"
188
189 oNodeFile :: OptType
190 oNodeFile = Option "n" ["nodes"]
191             (ReqArg (\ f o -> Ok o { optNodeFile = f,
192                                      optNodeSet = True }) "FILE")
193             "the node list FILE"
194
195 oInstFile :: OptType
196 oInstFile = Option "i" ["instances"]
197             (ReqArg (\ f o -> Ok o { optInstFile = f,
198                                      optInstSet = True }) "FILE")
199             "the instance list FILE"
200
201 oNodeSim :: OptType
202 oNodeSim = Option "" ["simulate"]
203             (ReqArg (\ f o -> Ok o { optNodeSim = Just f }) "SPEC")
204             "simulate an empty cluster, given as 'num_nodes,disk,ram,cpu'"
205
206 oRapiMaster :: OptType
207 oRapiMaster = Option "m" ["master"]
208               (ReqArg (\ m opts -> Ok opts { optMaster = m }) "ADDRESS")
209               "collect data via RAPI at the given ADDRESS"
210
211 oLuxiSocket :: OptType
212 oLuxiSocket = Option "L" ["luxi"]
213               (OptArg ((\ f opts -> Ok opts { optLuxi = Just f }) .
214                        fromMaybe defaultLuxiSocket) "SOCKET")
215               "collect data via Luxi, optionally using the given SOCKET path"
216
217 oExecJobs :: OptType
218 oExecJobs = Option "X" ["exec"]
219              (NoArg (\ opts -> Ok opts { optExecJobs = True}))
220              "execute the suggested moves via Luxi (only available when using\
221              \ it for data gathering"
222
223 oVerbose :: OptType
224 oVerbose = Option "v" ["verbose"]
225            (NoArg (\ opts -> Ok opts { optVerbose = optVerbose opts + 1 }))
226            "increase the verbosity level"
227
228 oQuiet :: OptType
229 oQuiet = Option "q" ["quiet"]
230          (NoArg (\ opts -> Ok opts { optVerbose = optVerbose opts - 1 }))
231          "decrease the verbosity level"
232
233 oOfflineNode :: OptType
234 oOfflineNode = Option "O" ["offline"]
235                (ReqArg (\ n o -> Ok o { optOffline = n:optOffline o }) "NODE")
236                "set node as offline"
237
238 oMaxSolLength :: OptType
239 oMaxSolLength = Option "l" ["max-length"]
240                 (ReqArg (\ i opts -> Ok opts { optMaxLength = read i }) "N")
241                 "cap the solution at this many moves (useful for very\
242                 \ unbalanced clusters)"
243
244 oMinScore :: OptType
245 oMinScore = Option "e" ["min-score"]
246             (ReqArg (\ e opts -> Ok opts { optMinScore = read e }) "EPSILON")
247             " mininum score to aim for"
248
249 oIMem :: OptType
250 oIMem = Option "" ["memory"]
251         (ReqArg (\ m opts ->
252                      let ospec = optISpec opts
253                          nspec = ospec { rspecMem = read m }
254                      in Ok opts { optISpec = nspec }) "MEMORY")
255         "memory size for instances"
256
257 oIDisk :: OptType
258 oIDisk = Option "" ["disk"]
259          (ReqArg (\ d opts ->
260                      let ospec = optISpec opts
261                          nspec = ospec { rspecDsk = read d }
262                      in Ok opts { optISpec = nspec }) "DISK")
263          "disk size for instances"
264
265 oIVcpus :: OptType
266 oIVcpus = Option "" ["vcpus"]
267           (ReqArg (\ p opts ->
268                        let ospec = optISpec opts
269                            nspec = ospec { rspecCpu = read p }
270                        in Ok opts { optISpec = nspec }) "NUM")
271           "number of virtual cpus for instances"
272
273 oINodes :: OptType
274 oINodes = Option "" ["req-nodes"]
275           (ReqArg (\ n opts -> Ok opts { optINodes = read n }) "NODES")
276           "number of nodes for the new instances (1=plain, 2=mirrored)"
277
278 oMaxCpu :: OptType
279 oMaxCpu = Option "" ["max-cpu"]
280           (ReqArg (\ n opts -> Ok opts { optMcpu = read n }) "RATIO")
281           "maximum virtual-to-physical cpu ratio for nodes"
282
283 oMinDisk :: OptType
284 oMinDisk = Option "" ["min-disk"]
285            (ReqArg (\ n opts -> Ok opts { optMdsk = read n }) "RATIO")
286            "minimum free disk space for nodes (between 0 and 1)"
287
288 oDiskMoves :: OptType
289 oDiskMoves = Option "" ["no-disk-moves"]
290              (NoArg (\ opts -> Ok opts { optDiskMoves = False}))
291              "disallow disk moves from the list of allowed instance changes,\
292              \ thus allowing only the 'cheap' failover/migrate operations"
293
294 oDynuFile :: OptType
295 oDynuFile = Option "U" ["dynu-file"]
296             (ReqArg (\ f opts -> Ok opts { optDynuFile = Just f }) "FILE")
297             "Import dynamic utilisation data from the given FILE"
298
299 oExTags :: OptType
300 oExTags = Option "" ["exclusion-tags"]
301             (ReqArg (\ f opts -> Ok opts { optExTags = Just $ sepSplit ',' f })
302              "TAG,...") "Enable instance exclusion based on given tag prefix"
303
304 oTieredSpec :: OptType
305 oTieredSpec = Option "" ["tiered-alloc"]
306              (ReqArg (\ inp opts -> do
307                           let sp = sepSplit ',' inp
308                           prs <- mapM (tryRead "tiered specs") sp
309                           tspec <-
310                               case prs of
311                                 [dsk, ram, cpu] -> return $ RSpec cpu ram dsk
312                                 _ -> Bad $ "Invalid specification: " ++ inp
313                           return $ opts { optTieredSpec = Just tspec } )
314               "TSPEC")
315              "enable tiered specs allocation, given as 'disk,ram,cpu'"
316
317 oShowVer :: OptType
318 oShowVer = Option "V" ["version"]
319            (NoArg (\ opts -> Ok opts { optShowVer = True}))
320            "show the version of the program"
321
322 oShowHelp :: OptType
323 oShowHelp = Option "h" ["help"]
324             (NoArg (\ opts -> Ok opts { optShowHelp = True}))
325             "show help"
326
327 -- | Usage info
328 usageHelp :: String -> [OptType] -> String
329 usageHelp progname =
330     usageInfo (printf "%s %s\nUsage: %s [OPTION...]"
331                progname Version.version progname)
332
333 -- | Command line parser, using the 'options' structure.
334 parseOpts :: [String]               -- ^ The command line arguments
335           -> String                 -- ^ The program name
336           -> [OptType]              -- ^ The supported command line options
337           -> IO (Options, [String]) -- ^ The resulting options and leftover
338                                     -- arguments
339 parseOpts argv progname options =
340     case getOpt Permute options argv of
341       (o, n, []) ->
342           do
343             let (pr, args) = (foldM (flip id) defaultOptions o, n)
344             po <- (case pr of
345                      Bad msg -> do
346                        hPutStrLn stderr "Error while parsing command\
347                                         \line arguments:"
348                        hPutStrLn stderr msg
349                        exitWith $ ExitFailure 1
350                      Ok val -> return val)
351             when (optShowHelp po) $ do
352               putStr $ usageHelp progname options
353               exitWith ExitSuccess
354             when (optShowVer po) $ do
355               printf "%s %s\ncompiled with %s %s\nrunning on %s %s\n"
356                      progname Version.version
357                      compilerName (Data.Version.showVersion compilerVersion)
358                      os arch
359               exitWith ExitSuccess
360             return (po, args)
361       (_, _, errs) -> do
362         hPutStrLn stderr $ "Command line error: "  ++ concat errs
363         hPutStrLn stderr $ usageHelp progname options
364         exitWith $ ExitFailure 2
365
366 -- | A shell script template for autogenerated scripts.
367 shTemplate :: String
368 shTemplate =
369     printf "#!/bin/sh\n\n\
370            \# Auto-generated script for executing cluster rebalancing\n\n\
371            \# To stop, touch the file /tmp/stop-htools\n\n\
372            \set -e\n\n\
373            \check() {\n\
374            \  if [ -f /tmp/stop-htools ]; then\n\
375            \    echo 'Stop requested, exiting'\n\
376            \    exit 0\n\
377            \  fi\n\
378            \}\n\n"