Statistics
| Branch: | Tag: | Revision:

root / src / Ganeti / Common.hs @ b9202225

History | View | Annotate | Download (14.4 kB)

1
{-| Base common functionality.
2

    
3
This module holds common functionality shared across Ganeti daemons,
4
HTools and any other programs.
5

    
6
-}
7

    
8
{-
9

    
10
Copyright (C) 2009, 2010, 2011, 2012, 2013 Google Inc.
11

    
12
This program is free software; you can redistribute it and/or modify
13
it under the terms of the GNU General Public License as published by
14
the Free Software Foundation; either version 2 of the License, or
15
(at your option) any later version.
16

    
17
This program is distributed in the hope that it will be useful, but
18
WITHOUT ANY WARRANTY; without even the implied warranty of
19
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
20
General Public License for more details.
21

    
22
You should have received a copy of the GNU General Public License
23
along with this program; if not, write to the Free Software
24
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
25
02110-1301, USA.
26

    
27
-}
28

    
29
module Ganeti.Common
30
  ( GenericOptType
31
  , StandardOptions(..)
32
  , OptCompletion(..)
33
  , ArgCompletion(..)
34
  , PersonalityList
35
  , optComplYesNo
36
  , oShowHelp
37
  , oShowVer
38
  , oShowComp
39
  , usageHelp
40
  , versionInfo
41
  , formatCommands
42
  , reqWithConversion
43
  , parseYesNo
44
  , parseOpts
45
  , parseOptsInner
46
  , parseOptsCmds
47
  , genericMainCmds
48
  , fillUpList
49
  , fillPairFromMaybe
50
  , pickPairUnique
51
  ) where
52

    
53
import Control.Monad (foldM)
54
import Data.Char (toLower)
55
import Data.List (intercalate, stripPrefix, sortBy)
56
import Data.Maybe (fromMaybe)
57
import Data.Ord (comparing)
58
import qualified Data.Version
59
import System.Console.GetOpt
60
import System.Environment
61
import System.Exit
62
import System.Info
63
import System.IO
64
import Text.Printf (printf)
65

    
66
import Ganeti.BasicTypes
67
import qualified Ganeti.Constants as C
68
import Ganeti.Utils (wrap)
69
import qualified Ganeti.Version as Version (version)
70

    
71
-- | Parameter type.
72
data OptCompletion = OptComplNone             -- ^ No parameter to this option
73
                   | OptComplFile             -- ^ An existing file
74
                   | OptComplDir              -- ^ An existing directory
75
                   | OptComplHost             -- ^ Host name
76
                   | OptComplInetAddr         -- ^ One ipv4\/ipv6 address
77
                   | OptComplOneNode          -- ^ One node
78
                   | OptComplManyNodes        -- ^ Many nodes, comma-sep
79
                   | OptComplOneInstance      -- ^ One instance
80
                   | OptComplManyInstances    -- ^ Many instances, comma-sep
81
                   | OptComplOneOs            -- ^ One OS name
82
                   | OptComplOneIallocator    -- ^ One iallocator
83
                   | OptComplInstAddNodes     -- ^ Either one or two nodes
84
                   | OptComplOneGroup         -- ^ One group
85
                   | OptComplInteger          -- ^ Integer values
86
                   | OptComplFloat            -- ^ Float values
87
                   | OptComplJobId            -- ^ Job Id
88
                   | OptComplCommand          -- ^ Command (executable)
89
                   | OptComplString           -- ^ Arbitrary string
90
                   | OptComplChoices [String] -- ^ List of string choices
91
                   | OptComplSuggest [String] -- ^ Suggested choices
92
                   deriving (Show, Eq)
93

    
94
-- | Argument type. This differs from (and wraps) an Option by the
95
-- fact that it can (and usually does) support multiple repetitions of
96
-- the same argument, via a min and max limit.
97
data ArgCompletion = ArgCompletion OptCompletion Int (Maybe Int)
98
                     deriving (Show, Eq)
99

    
100
-- | A personality definition.
101
type Personality a = ( a -> [String] -> IO () -- The main function
102
                     , IO [GenericOptType a]  -- The options
103
                     , [ArgCompletion]        -- The description of args
104
                     , String                 -- Description
105
                     )
106

    
107
-- | Personality lists type, common across all binaries that expose
108
-- multiple personalities.
109
type PersonalityList  a = [(String, Personality a)]
110

    
111
-- | Yes\/no choices completion.
112
optComplYesNo :: OptCompletion
113
optComplYesNo = OptComplChoices ["yes", "no"]
114

    
115
-- | Text serialisation for 'OptCompletion', used on the Python side.
116
complToText :: OptCompletion -> String
117
complToText (OptComplChoices choices) = "choices=" ++ intercalate "," choices
118
complToText (OptComplSuggest choices) = "suggest=" ++ intercalate "," choices
119
complToText compl =
120
  let show_compl = show compl
121
      stripped = stripPrefix "OptCompl" show_compl
122
  in map toLower $ fromMaybe show_compl stripped
123

    
124
-- | Text serialisation for 'ArgCompletion'.
125
argComplToText :: ArgCompletion -> String
126
argComplToText (ArgCompletion optc min_cnt max_cnt) =
127
  complToText optc ++ " " ++ show min_cnt ++ " " ++ maybe "none" show max_cnt
128

    
129
-- | Abbreviation for the option type.
130
type GenericOptType a = (OptDescr (a -> Result a), OptCompletion)
131

    
132
-- | Type class for options which support help and version.
133
class StandardOptions a where
134
  helpRequested :: a -> Bool
135
  verRequested  :: a -> Bool
136
  compRequested :: a -> Bool
137
  requestHelp   :: a -> a
138
  requestVer    :: a -> a
139
  requestComp   :: a -> a
140

    
141
-- | Option to request help output.
142
oShowHelp :: (StandardOptions a) => GenericOptType a
143
oShowHelp = (Option "h" ["help"] (NoArg (Ok . requestHelp)) "show help",
144
             OptComplNone)
145

    
146
-- | Option to request version information.
147
oShowVer :: (StandardOptions a) => GenericOptType a
148
oShowVer = (Option "V" ["version"] (NoArg (Ok . requestVer))
149
            "show the version of the program",
150
            OptComplNone)
151

    
152
-- | Option to request completion information
153
oShowComp :: (StandardOptions a) => GenericOptType a
154
oShowComp =
155
  (Option "" ["help-completion"] (NoArg (Ok . requestComp) )
156
   "show completion info", OptComplNone)
157

    
158
-- | Usage info.
159
usageHelp :: String -> [GenericOptType a] -> String
160
usageHelp progname =
161
  usageInfo (printf "%s %s\nUsage: %s [OPTION...]"
162
             progname Version.version progname) . map fst
163

    
164
-- | Show the program version info.
165
versionInfo :: String -> String
166
versionInfo progname =
167
  printf "%s %s\ncompiled with %s %s\nrunning on %s %s\n"
168
         progname Version.version compilerName
169
         (Data.Version.showVersion compilerVersion)
170
         os arch
171

    
172
-- | Show completion info.
173
completionInfo :: String -> [GenericOptType a] -> [ArgCompletion] -> String
174
completionInfo _ opts args =
175
  unlines $
176
  map (\(Option shorts longs _ _, compinfo) ->
177
         let all_opts = map (\c -> ['-', c]) shorts ++ map ("--" ++) longs
178
         in intercalate "," all_opts ++ " " ++ complToText compinfo
179
      ) opts ++
180
  map argComplToText args
181

    
182
-- | Helper for parsing a yes\/no command line flag.
183
parseYesNo :: Bool         -- ^ Default value (when we get a @Nothing@)
184
           -> Maybe String -- ^ Parameter value
185
           -> Result Bool  -- ^ Resulting boolean value
186
parseYesNo v Nothing      = return v
187
parseYesNo _ (Just "yes") = return True
188
parseYesNo _ (Just "no")  = return False
189
parseYesNo _ (Just s)     = fail ("Invalid choice '" ++ s ++
190
                                  "', pass one of 'yes' or 'no'")
191

    
192
-- | Helper function for required arguments which need to be converted
193
-- as opposed to stored just as string.
194
reqWithConversion :: (String -> Result a)
195
                  -> (a -> b -> Result b)
196
                  -> String
197
                  -> ArgDescr (b -> Result b)
198
reqWithConversion conversion_fn updater_fn =
199
  ReqArg (\string_opt opts -> do
200
            parsed_value <- conversion_fn string_opt
201
            updater_fn parsed_value opts)
202

    
203
-- | Max command length when formatting command list output.
204
maxCmdLen :: Int
205
maxCmdLen = 60
206

    
207
-- | Formats the description of various commands.
208
formatCommands :: (StandardOptions a) => PersonalityList a -> [String]
209
formatCommands personalities =
210
  concatMap (\(cmd, (_, _, _, desc)) ->
211
              fmtDesc cmd (wrap maxWidth desc) "-" []) $
212
  sortBy (comparing fst) personalities
213
    where mlen = min maxCmdLen . maximum $ map (length . fst) personalities
214
          maxWidth = 79 - 3 - mlen
215
          fmtDesc _ [] _ acc = reverse acc
216
          fmtDesc cmd (d : ds) sep acc =
217
            fmtDesc "" ds " " (printf " %-*s %s %s" mlen cmd sep d : acc)
218

    
219
-- | Formats usage for a multi-personality program.
220
formatCmdUsage :: (StandardOptions a) => String -> PersonalityList a -> String
221
formatCmdUsage prog personalities =
222
  let header = [ printf "Usage: %s {command} [options...] [argument...]" prog
223
               , printf "%s <command> --help to see details, or man %s"
224
                   prog prog
225
               , ""
226
               , "Commands:"
227
               ]
228
      rows = formatCommands personalities
229
  in unlines $ header ++ rows
230

    
231
-- | Displays usage for a program and exits.
232
showCmdUsage :: (StandardOptions a) =>
233
                String            -- ^ Program name
234
             -> PersonalityList a -- ^ Personality list
235
             -> Bool              -- ^ Whether the exit code is success or not
236
             -> IO b
237
showCmdUsage prog personalities success = do
238
  let usage = formatCmdUsage prog personalities
239
  putStr usage
240
  if success
241
    then exitSuccess
242
    else exitWith $ ExitFailure C.exitFailure
243

    
244
-- | Generates completion information for a multi-command binary.
245
multiCmdCompletion :: (StandardOptions a) => PersonalityList a -> String
246
multiCmdCompletion personalities =
247
  argComplToText $
248
    ArgCompletion (OptComplChoices (map fst personalities))
249
      1 (Just 1)
250

    
251
-- | Displays completion information for a multi-command binary and exits.
252
showCmdCompletion :: (StandardOptions a) => PersonalityList a -> IO b
253
showCmdCompletion personalities =
254
  putStrLn (multiCmdCompletion personalities) >> exitSuccess
255

    
256
-- | Command line parser, using a generic 'Options' structure.
257
parseOpts :: (StandardOptions a) =>
258
             a                      -- ^ The default options
259
          -> [String]               -- ^ The command line arguments
260
          -> String                 -- ^ The program name
261
          -> [GenericOptType a]     -- ^ The supported command line options
262
          -> [ArgCompletion]        -- ^ The supported command line arguments
263
          -> IO (a, [String])       -- ^ The resulting options and
264
                                    -- leftover arguments
265
parseOpts defaults argv progname options arguments =
266
  case parseOptsInner defaults argv progname options arguments of
267
    Left (code, msg) -> do
268
      hPutStr (if code == ExitSuccess then stdout else stderr) msg
269
      exitWith code
270
    Right result ->
271
      return result
272

    
273
-- | Command line parser, for programs with sub-commands.
274
parseOptsCmds :: (StandardOptions a) =>
275
                 a                      -- ^ The default options
276
              -> [String]               -- ^ The command line arguments
277
              -> String                 -- ^ The program name
278
              -> PersonalityList a      -- ^ The supported commands
279
              -> [GenericOptType a]     -- ^ Generic options
280
              -> IO (a, [String], a -> [String] -> IO ())
281
                     -- ^ The resulting options and leftover arguments
282
parseOptsCmds defaults argv progname personalities genopts = do
283
  let usage = showCmdUsage progname personalities
284
      check c = case c of
285
                  -- hardcoded option strings here!
286
                  "--version" -> putStrLn (versionInfo progname) >> exitSuccess
287
                  "--help"    -> usage True
288
                  "--help-completion" -> showCmdCompletion personalities
289
                  _           -> return c
290
  (cmd, cmd_args) <- case argv of
291
                       cmd:cmd_args -> do
292
                         cmd' <- check cmd
293
                         return (cmd', cmd_args)
294
                       [] -> usage False
295
  case cmd `lookup` personalities of
296
    Nothing -> usage False
297
    Just (mainfn, optdefs, argdefs, _) -> do
298
      optdefs' <- optdefs
299
      (opts, args) <- parseOpts defaults cmd_args progname
300
                      (optdefs' ++ genopts) argdefs
301
      return (opts, args, mainfn)
302

    
303
-- | Inner parse options. The arguments are similar to 'parseOpts',
304
-- but it returns either a 'Left' composed of exit code and message,
305
-- or a 'Right' for the success case.
306
parseOptsInner :: (StandardOptions a) =>
307
                  a
308
               -> [String]
309
               -> String
310
               -> [GenericOptType a]
311
               -> [ArgCompletion]
312
               -> Either (ExitCode, String) (a, [String])
313
parseOptsInner defaults argv progname options arguments  =
314
  case getOpt Permute (map fst options) argv of
315
    (opts, args, []) ->
316
      case foldM (flip id) defaults opts of
317
           Bad msg -> Left (ExitFailure 1,
318
                            "Error while parsing command line arguments:\n"
319
                            ++ msg ++ "\n")
320
           Ok parsed ->
321
             select (Right (parsed, args))
322
                 [ (helpRequested parsed,
323
                    Left (ExitSuccess, usageHelp progname options))
324
                 , (verRequested parsed,
325
                    Left (ExitSuccess, versionInfo progname))
326
                 , (compRequested parsed,
327
                    Left (ExitSuccess, completionInfo progname options
328
                                         arguments))
329
                 ]
330
    (_, _, errs) ->
331
      Left (ExitFailure 2, "Command line error: "  ++ concat errs ++ "\n" ++
332
            usageHelp progname options)
333

    
334
-- | Parse command line options and execute the main function of a
335
-- multi-personality binary.
336
genericMainCmds :: (StandardOptions a) =>
337
                   a
338
                -> PersonalityList a
339
                -> [GenericOptType a]
340
                -> IO ()
341
genericMainCmds defaults personalities genopts = do
342
  cmd_args <- getArgs
343
  prog <- getProgName
344
  (opts, args, fn) <-
345
    parseOptsCmds defaults cmd_args prog personalities genopts
346
  fn opts args
347

    
348
-- | Order a list of pairs in the order of the given list and fill up
349
-- the list for elements that don't have a matching pair
350
fillUpList :: ([(a, b)] -> a -> (a, b)) -> [a] -> [(a, b)] -> [(a, b)]
351
fillUpList fill_fn inputs pairs =
352
  map (fill_fn pairs) inputs
353

    
354
-- | Fill up a pair with fillup element if no matching pair is present
355
fillPairFromMaybe :: (a -> (a, b)) -> (a -> [(a, b)] -> Maybe (a, b))
356
                  -> [(a, b)] -> a -> (a, b)
357
fillPairFromMaybe fill_fn pick_fn pairs element = fromMaybe (fill_fn element)
358
    (pick_fn element pairs)
359

    
360
-- | Check if the given element matches the given pair
361
isMatchingPair :: (Eq a) => a -> (a, b) -> Bool
362
isMatchingPair element (pair_element, _) = element == pair_element
363

    
364
-- | Pick a specific element's pair from the list
365
pickPairUnique :: (Eq a) => a -> [(a, b)] -> Maybe (a, b)
366
pickPairUnique element pairs =
367
  let res = filter (isMatchingPair element) pairs
368
  in case res of
369
    [x] -> Just x
370
    -- if we have more than one result, we should get suspcious
371
    _ -> Nothing