Move more cluster init opts to cli.py
[ganeti-local] / lib / cli.py
1 #
2 #
3
4 # Copyright (C) 2006, 2007 Google Inc.
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 # General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19 # 02110-1301, USA.
20
21
22 """Module dealing with command line parsing"""
23
24
25 import sys
26 import textwrap
27 import os.path
28 import copy
29 import time
30 import logging
31 from cStringIO import StringIO
32
33 from ganeti import utils
34 from ganeti import errors
35 from ganeti import constants
36 from ganeti import opcodes
37 from ganeti import luxi
38 from ganeti import ssconf
39 from ganeti import rpc
40
41 from optparse import (OptionParser, TitledHelpFormatter,
42                       Option, OptionValueError)
43
44
45 __all__ = [
46   # Command line options
47   "ALLOCATABLE_OPT",
48   "ALL_OPT",
49   "AUTO_REPLACE_OPT",
50   "BACKEND_OPT",
51   "CLEANUP_OPT",
52   "CONFIRM_OPT",
53   "CP_SIZE_OPT",
54   "DEBUG_OPT",
55   "DEBUG_SIMERR_OPT",
56   "DISKIDX_OPT",
57   "DISK_OPT",
58   "DISK_TEMPLATE_OPT",
59   "DRAINED_OPT",
60   "ENABLED_HV_OPT",
61   "FIELDS_OPT",
62   "FILESTORE_DIR_OPT",
63   "FILESTORE_DRIVER_OPT",
64   "GLOBAL_FILEDIR_OPT",
65   "HVLIST_OPT",
66   "HVOPTS_OPT",
67   "HYPERVISOR_OPT",
68   "IALLOCATOR_OPT",
69   "IGNORE_CONSIST_OPT",
70   "IGNORE_FAILURES_OPT",
71   "IGNORE_SIZE_OPT",
72   "FORCE_OPT",
73   "MAC_PREFIX_OPT",
74   "MASTER_NETDEV_OPT",
75   "MC_OPT",
76   "NET_OPT",
77   "NEW_SECONDARY_OPT",
78   "NIC_PARAMS_OPT",
79   "NODE_LIST_OPT",
80   "NODE_PLACEMENT_OPT",
81   "NOHDR_OPT",
82   "NOIPCHECK_OPT",
83   "NOLVM_STORAGE_OPT",
84   "NOMODIFY_ETCHOSTS_OPT",
85   "NONICS_OPT",
86   "NONLIVE_OPT",
87   "NOSTART_OPT",
88   "NOSSH_KEYCHECK_OPT",
89   "NOVOTING_OPT",
90   "NWSYNC_OPT",
91   "ON_PRIMARY_OPT",
92   "ON_SECONDARY_OPT",
93   "OFFLINE_OPT",
94   "OS_OPT",
95   "OS_SIZE_OPT",
96   "READD_OPT",
97   "SECONDARY_IP_OPT",
98   "SELECT_OS_OPT",
99   "SEP_OPT",
100   "SHOWCMD_OPT",
101   "SINGLE_NODE_OPT",
102   "SRC_DIR_OPT",
103   "SRC_NODE_OPT",
104   "SUBMIT_OPT",
105   "STATIC_OPT",
106   "SYNC_OPT",
107   "TAG_SRC_OPT",
108   "USEUNITS_OPT",
109   "VERBOSE_OPT",
110   "VG_NAME_OPT",
111   "YES_DOIT_OPT",
112   # Generic functions for CLI programs
113   "GenericMain",
114   "GetClient",
115   "GetOnlineNodes",
116   "JobExecutor",
117   "JobSubmittedException",
118   "ParseTimespec",
119   "SubmitOpCode",
120   "SubmitOrSend",
121   "UsesRPC",
122   # Formatting functions
123   "ToStderr", "ToStdout",
124   "FormatError",
125   "GenerateTable",
126   "AskUser",
127   "FormatTimestamp",
128   # Tags functions
129   "ListTags",
130   "AddTags",
131   "RemoveTags",
132   # command line options support infrastructure
133   "ARGS_MANY_INSTANCES",
134   "ARGS_MANY_NODES",
135   "ARGS_NONE",
136   "ARGS_ONE_INSTANCE",
137   "ARGS_ONE_NODE",
138   "ArgChoice",
139   "ArgCommand",
140   "ArgFile",
141   "ArgHost",
142   "ArgInstance",
143   "ArgJobId",
144   "ArgNode",
145   "ArgSuggest",
146   "ArgUnknown",
147   "OPT_COMPL_INST_ADD_NODES",
148   "OPT_COMPL_MANY_NODES",
149   "OPT_COMPL_ONE_IALLOCATOR",
150   "OPT_COMPL_ONE_INSTANCE",
151   "OPT_COMPL_ONE_NODE",
152   "OPT_COMPL_ONE_OS",
153   "cli_option",
154   "SplitNodeOption",
155   ]
156
157 NO_PREFIX = "no_"
158 UN_PREFIX = "-"
159
160
161 class _Argument:
162   def __init__(self, min=0, max=None):
163     self.min = min
164     self.max = max
165
166   def __repr__(self):
167     return ("<%s min=%s max=%s>" %
168             (self.__class__.__name__, self.min, self.max))
169
170
171 class ArgSuggest(_Argument):
172   """Suggesting argument.
173
174   Value can be any of the ones passed to the constructor.
175
176   """
177   def __init__(self, min=0, max=None, choices=None):
178     _Argument.__init__(self, min=min, max=max)
179     self.choices = choices
180
181   def __repr__(self):
182     return ("<%s min=%s max=%s choices=%r>" %
183             (self.__class__.__name__, self.min, self.max, self.choices))
184
185
186 class ArgChoice(ArgSuggest):
187   """Choice argument.
188
189   Value can be any of the ones passed to the constructor. Like L{ArgSuggest},
190   but value must be one of the choices.
191
192   """
193
194
195 class ArgUnknown(_Argument):
196   """Unknown argument to program (e.g. determined at runtime).
197
198   """
199
200
201 class ArgInstance(_Argument):
202   """Instances argument.
203
204   """
205
206
207 class ArgNode(_Argument):
208   """Node argument.
209
210   """
211
212 class ArgJobId(_Argument):
213   """Job ID argument.
214
215   """
216
217
218 class ArgFile(_Argument):
219   """File path argument.
220
221   """
222
223
224 class ArgCommand(_Argument):
225   """Command argument.
226
227   """
228
229
230 class ArgHost(_Argument):
231   """Host argument.
232
233   """
234
235
236 ARGS_NONE = []
237 ARGS_MANY_INSTANCES = [ArgInstance()]
238 ARGS_MANY_NODES = [ArgNode()]
239 ARGS_ONE_INSTANCE = [ArgInstance(min=1, max=1)]
240 ARGS_ONE_NODE = [ArgNode(min=1, max=1)]
241
242
243
244 def _ExtractTagsObject(opts, args):
245   """Extract the tag type object.
246
247   Note that this function will modify its args parameter.
248
249   """
250   if not hasattr(opts, "tag_type"):
251     raise errors.ProgrammerError("tag_type not passed to _ExtractTagsObject")
252   kind = opts.tag_type
253   if kind == constants.TAG_CLUSTER:
254     retval = kind, kind
255   elif kind == constants.TAG_NODE or kind == constants.TAG_INSTANCE:
256     if not args:
257       raise errors.OpPrereqError("no arguments passed to the command")
258     name = args.pop(0)
259     retval = kind, name
260   else:
261     raise errors.ProgrammerError("Unhandled tag type '%s'" % kind)
262   return retval
263
264
265 def _ExtendTags(opts, args):
266   """Extend the args if a source file has been given.
267
268   This function will extend the tags with the contents of the file
269   passed in the 'tags_source' attribute of the opts parameter. A file
270   named '-' will be replaced by stdin.
271
272   """
273   fname = opts.tags_source
274   if fname is None:
275     return
276   if fname == "-":
277     new_fh = sys.stdin
278   else:
279     new_fh = open(fname, "r")
280   new_data = []
281   try:
282     # we don't use the nice 'new_data = [line.strip() for line in fh]'
283     # because of python bug 1633941
284     while True:
285       line = new_fh.readline()
286       if not line:
287         break
288       new_data.append(line.strip())
289   finally:
290     new_fh.close()
291   args.extend(new_data)
292
293
294 def ListTags(opts, args):
295   """List the tags on a given object.
296
297   This is a generic implementation that knows how to deal with all
298   three cases of tag objects (cluster, node, instance). The opts
299   argument is expected to contain a tag_type field denoting what
300   object type we work on.
301
302   """
303   kind, name = _ExtractTagsObject(opts, args)
304   op = opcodes.OpGetTags(kind=kind, name=name)
305   result = SubmitOpCode(op)
306   result = list(result)
307   result.sort()
308   for tag in result:
309     ToStdout(tag)
310
311
312 def AddTags(opts, args):
313   """Add tags on a given object.
314
315   This is a generic implementation that knows how to deal with all
316   three cases of tag objects (cluster, node, instance). The opts
317   argument is expected to contain a tag_type field denoting what
318   object type we work on.
319
320   """
321   kind, name = _ExtractTagsObject(opts, args)
322   _ExtendTags(opts, args)
323   if not args:
324     raise errors.OpPrereqError("No tags to be added")
325   op = opcodes.OpAddTags(kind=kind, name=name, tags=args)
326   SubmitOpCode(op)
327
328
329 def RemoveTags(opts, args):
330   """Remove tags from a given object.
331
332   This is a generic implementation that knows how to deal with all
333   three cases of tag objects (cluster, node, instance). The opts
334   argument is expected to contain a tag_type field denoting what
335   object type we work on.
336
337   """
338   kind, name = _ExtractTagsObject(opts, args)
339   _ExtendTags(opts, args)
340   if not args:
341     raise errors.OpPrereqError("No tags to be removed")
342   op = opcodes.OpDelTags(kind=kind, name=name, tags=args)
343   SubmitOpCode(op)
344
345
346 def check_unit(option, opt, value):
347   """OptParsers custom converter for units.
348
349   """
350   try:
351     return utils.ParseUnit(value)
352   except errors.UnitParseError, err:
353     raise OptionValueError("option %s: %s" % (opt, err))
354
355
356 def _SplitKeyVal(opt, data):
357   """Convert a KeyVal string into a dict.
358
359   This function will convert a key=val[,...] string into a dict. Empty
360   values will be converted specially: keys which have the prefix 'no_'
361   will have the value=False and the prefix stripped, the others will
362   have value=True.
363
364   @type opt: string
365   @param opt: a string holding the option name for which we process the
366       data, used in building error messages
367   @type data: string
368   @param data: a string of the format key=val,key=val,...
369   @rtype: dict
370   @return: {key=val, key=val}
371   @raises errors.ParameterError: if there are duplicate keys
372
373   """
374   kv_dict = {}
375   if data:
376     for elem in data.split(","):
377       if "=" in elem:
378         key, val = elem.split("=", 1)
379       else:
380         if elem.startswith(NO_PREFIX):
381           key, val = elem[len(NO_PREFIX):], False
382         elif elem.startswith(UN_PREFIX):
383           key, val = elem[len(UN_PREFIX):], None
384         else:
385           key, val = elem, True
386       if key in kv_dict:
387         raise errors.ParameterError("Duplicate key '%s' in option %s" %
388                                     (key, opt))
389       kv_dict[key] = val
390   return kv_dict
391
392
393 def check_ident_key_val(option, opt, value):
394   """Custom parser for ident:key=val,key=val options.
395
396   This will store the parsed values as a tuple (ident, {key: val}). As such,
397   multiple uses of this option via action=append is possible.
398
399   """
400   if ":" not in value:
401     ident, rest = value, ''
402   else:
403     ident, rest = value.split(":", 1)
404
405   if ident.startswith(NO_PREFIX):
406     if rest:
407       msg = "Cannot pass options when removing parameter groups: %s" % value
408       raise errors.ParameterError(msg)
409     retval = (ident[len(NO_PREFIX):], False)
410   elif ident.startswith(UN_PREFIX):
411     if rest:
412       msg = "Cannot pass options when removing parameter groups: %s" % value
413       raise errors.ParameterError(msg)
414     retval = (ident[len(UN_PREFIX):], None)
415   else:
416     kv_dict = _SplitKeyVal(opt, rest)
417     retval = (ident, kv_dict)
418   return retval
419
420
421 def check_key_val(option, opt, value):
422   """Custom parser class for key=val,key=val options.
423
424   This will store the parsed values as a dict {key: val}.
425
426   """
427   return _SplitKeyVal(opt, value)
428
429
430 # completion_suggestion is normally a list. Using numeric values not evaluating
431 # to False for dynamic completion.
432 (OPT_COMPL_MANY_NODES,
433  OPT_COMPL_ONE_NODE,
434  OPT_COMPL_ONE_INSTANCE,
435  OPT_COMPL_ONE_OS,
436  OPT_COMPL_ONE_IALLOCATOR,
437  OPT_COMPL_INST_ADD_NODES) = range(100, 106)
438
439 OPT_COMPL_ALL = frozenset([
440   OPT_COMPL_MANY_NODES,
441   OPT_COMPL_ONE_NODE,
442   OPT_COMPL_ONE_INSTANCE,
443   OPT_COMPL_ONE_OS,
444   OPT_COMPL_ONE_IALLOCATOR,
445   OPT_COMPL_INST_ADD_NODES,
446   ])
447
448
449 class CliOption(Option):
450   """Custom option class for optparse.
451
452   """
453   ATTRS = Option.ATTRS + [
454     "completion_suggest",
455     ]
456   TYPES = Option.TYPES + (
457     "identkeyval",
458     "keyval",
459     "unit",
460     )
461   TYPE_CHECKER = Option.TYPE_CHECKER.copy()
462   TYPE_CHECKER["identkeyval"] = check_ident_key_val
463   TYPE_CHECKER["keyval"] = check_key_val
464   TYPE_CHECKER["unit"] = check_unit
465
466
467 # optparse.py sets make_option, so we do it for our own option class, too
468 cli_option = CliOption
469
470
471 _YESNO = ("yes", "no")
472 _YORNO = "yes|no"
473
474 DEBUG_OPT = cli_option("-d", "--debug", default=False,
475                        action="store_true",
476                        help="Turn debugging on")
477
478 NOHDR_OPT = cli_option("--no-headers", default=False,
479                        action="store_true", dest="no_headers",
480                        help="Don't display column headers")
481
482 SEP_OPT = cli_option("--separator", default=None,
483                      action="store", dest="separator",
484                      help=("Separator between output fields"
485                            " (defaults to one space)"))
486
487 USEUNITS_OPT = cli_option("--units", default=None,
488                           dest="units", choices=('h', 'm', 'g', 't'),
489                           help="Specify units for output (one of hmgt)")
490
491 FIELDS_OPT = cli_option("-o", "--output", dest="output", action="store",
492                         type="string", metavar="FIELDS",
493                         help="Comma separated list of output fields")
494
495 FORCE_OPT = cli_option("-f", "--force", dest="force", action="store_true",
496                        default=False, help="Force the operation")
497
498 CONFIRM_OPT = cli_option("--yes", dest="confirm", action="store_true",
499                          default=False, help="Do not require confirmation")
500
501 TAG_SRC_OPT = cli_option("--from", dest="tags_source",
502                          default=None, help="File with tag names")
503
504 SUBMIT_OPT = cli_option("--submit", dest="submit_only",
505                         default=False, action="store_true",
506                         help=("Submit the job and return the job ID, but"
507                               " don't wait for the job to finish"))
508
509 SYNC_OPT = cli_option("--sync", dest="do_locking",
510                       default=False, action="store_true",
511                       help=("Grab locks while doing the queries"
512                             " in order to ensure more consistent results"))
513
514 _DRY_RUN_OPT = cli_option("--dry-run", default=False,
515                           action="store_true",
516                           help=("Do not execute the operation, just run the"
517                                 " check steps and verify it it could be"
518                                 " executed"))
519
520 VERBOSE_OPT = cli_option("-v", "--verbose", default=False,
521                          action="store_true",
522                          help="Increase the verbosity of the operation")
523
524 DEBUG_SIMERR_OPT = cli_option("--debug-simulate-errors", default=False,
525                               action="store_true", dest="simulate_errors",
526                               help="Debugging option that makes the operation"
527                               " treat most runtime checks as failed")
528
529 NWSYNC_OPT = cli_option("--no-wait-for-sync", dest="wait_for_sync",
530                         default=True, action="store_false",
531                         help="Don't wait for sync (DANGEROUS!)")
532
533 DISK_TEMPLATE_OPT = cli_option("-t", "--disk-template", dest="disk_template",
534                                help="Custom disk setup (diskless, file,"
535                                " plain or drbd)",
536                                default=None, metavar="TEMPL",
537                                choices=list(constants.DISK_TEMPLATES))
538
539 NONICS_OPT = cli_option("--no-nics", default=False, action="store_true",
540                         help="Do not create any network cards for"
541                         " the instance")
542
543 FILESTORE_DIR_OPT = cli_option("--file-storage-dir", dest="file_storage_dir",
544                                help="Relative path under default cluster-wide"
545                                " file storage dir to store file-based disks",
546                                default=None, metavar="<DIR>")
547
548 FILESTORE_DRIVER_OPT = cli_option("--file-driver", dest="file_driver",
549                                   help="Driver to use for image files",
550                                   default="loop", metavar="<DRIVER>",
551                                   choices=list(constants.FILE_DRIVER))
552
553 IALLOCATOR_OPT = cli_option("-I", "--iallocator", metavar="<NAME>",
554                             help="Select nodes for the instance automatically"
555                             " using the <NAME> iallocator plugin",
556                             default=None, type="string",
557                             completion_suggest=OPT_COMPL_ONE_IALLOCATOR)
558
559 OS_OPT = cli_option("-o", "--os-type", dest="os", help="What OS to run",
560                     metavar="<os>",
561                     completion_suggest=OPT_COMPL_ONE_OS)
562
563 BACKEND_OPT = cli_option("-B", "--backend-parameters", dest="beparams",
564                          type="keyval", default={},
565                          help="Backend parameters")
566
567 HVOPTS_OPT =  cli_option("-H", "--hypervisor-parameters", type="keyval",
568                          default={}, dest="hvparams",
569                          help="Hypervisor parameters")
570
571 HYPERVISOR_OPT = cli_option("-H", "--hypervisor-parameters", dest="hypervisor",
572                             help="Hypervisor and hypervisor options, in the"
573                             " format hypervisor:option=value,option=value,...",
574                             default=None, type="identkeyval")
575
576 HVLIST_OPT = cli_option("-H", "--hypervisor-parameters", dest="hvparams",
577                         help="Hypervisor and hypervisor options, in the"
578                         " format hypervisor:option=value,option=value,...",
579                         default=[], action="append", type="identkeyval")
580
581 NOIPCHECK_OPT = cli_option("--no-ip-check", dest="ip_check", default=True,
582                            action="store_false",
583                            help="Don't check that the instance's IP"
584                            " is alive")
585
586 NET_OPT = cli_option("--net",
587                      help="NIC parameters", default=[],
588                      dest="nics", action="append", type="identkeyval")
589
590 DISK_OPT = cli_option("--disk", help="Disk parameters", default=[],
591                       dest="disks", action="append", type="identkeyval")
592
593 DISKIDX_OPT = cli_option("--disks", dest="disks", default=None,
594                          help="Comma-separated list of disks"
595                          " indices to act on (e.g. 0,2) (optional,"
596                          " defaults to all disks)")
597
598 OS_SIZE_OPT = cli_option("-s", "--os-size", dest="sd_size",
599                          help="Enforces a single-disk configuration using the"
600                          " given disk size, in MiB unless a suffix is used",
601                          default=None, type="unit", metavar="<size>")
602
603 IGNORE_CONSIST_OPT = cli_option("--ignore-consistency",
604                                 dest="ignore_consistency",
605                                 action="store_true", default=False,
606                                 help="Ignore the consistency of the disks on"
607                                 " the secondary")
608
609 NONLIVE_OPT = cli_option("--non-live", dest="live",
610                          default=True, action="store_false",
611                          help="Do a non-live migration (this usually means"
612                          " freeze the instance, save the state, transfer and"
613                          " only then resume running on the secondary node)")
614
615 NODE_PLACEMENT_OPT = cli_option("-n", "--node", dest="node",
616                                 help="Target node and optional secondary node",
617                                 metavar="<pnode>[:<snode>]",
618                                 completion_suggest=OPT_COMPL_INST_ADD_NODES)
619
620 NODE_LIST_OPT = cli_option("-n", "--node", dest="nodes", default=[],
621                            action="append", metavar="<node>",
622                            help="Use only this node (can be used multiple"
623                            " times, if not given defaults to all nodes)",
624                            completion_suggest=OPT_COMPL_ONE_NODE)
625
626 SINGLE_NODE_OPT = cli_option("-n", "--node", dest="node", help="Target node",
627                              metavar="<node>",
628                              completion_suggest=OPT_COMPL_ONE_NODE)
629
630 NOSTART_OPT = cli_option("--no-start", dest="start", default=True,
631                          action="store_false",
632                          help="Don't start the instance after creation")
633
634 SHOWCMD_OPT = cli_option("--show-cmd", dest="show_command",
635                          action="store_true", default=False,
636                          help="Show command instead of executing it")
637
638 CLEANUP_OPT = cli_option("--cleanup", dest="cleanup",
639                          default=False, action="store_true",
640                          help="Instead of performing the migration, try to"
641                          " recover from a failed cleanup. This is safe"
642                          " to run even if the instance is healthy, but it"
643                          " will create extra replication traffic and "
644                          " disrupt briefly the replication (like during the"
645                          " migration")
646
647 STATIC_OPT = cli_option("-s", "--static", dest="static",
648                         action="store_true", default=False,
649                         help="Only show configuration data, not runtime data")
650
651 ALL_OPT = cli_option("--all", dest="show_all",
652                      default=False, action="store_true",
653                      help="Show info on all instances on the cluster."
654                      " This can take a long time to run, use wisely")
655
656 SELECT_OS_OPT = cli_option("--select-os", dest="select_os",
657                            action="store_true", default=False,
658                            help="Interactive OS reinstall, lists available"
659                            " OS templates for selection")
660
661 IGNORE_FAILURES_OPT = cli_option("--ignore-failures", dest="ignore_failures",
662                                  action="store_true", default=False,
663                                  help="Remove the instance from the cluster"
664                                  " configuration even if there are failures"
665                                  " during the removal process")
666
667 NEW_SECONDARY_OPT = cli_option("-n", "--new-secondary", dest="dst_node",
668                                help="Specifies the new secondary node",
669                                metavar="NODE", default=None,
670                                completion_suggest=OPT_COMPL_ONE_NODE)
671
672 ON_PRIMARY_OPT = cli_option("-p", "--on-primary", dest="on_primary",
673                             default=False, action="store_true",
674                             help="Replace the disk(s) on the primary"
675                             " node (only for the drbd template)")
676
677 ON_SECONDARY_OPT = cli_option("-s", "--on-secondary", dest="on_secondary",
678                               default=False, action="store_true",
679                               help="Replace the disk(s) on the secondary"
680                               " node (only for the drbd template)")
681
682 AUTO_REPLACE_OPT = cli_option("-a", "--auto", dest="auto",
683                               default=False, action="store_true",
684                               help="Automatically replace faulty disks"
685                               " (only for the drbd template)")
686
687 IGNORE_SIZE_OPT = cli_option("--ignore-size", dest="ignore_size",
688                              default=False, action="store_true",
689                              help="Ignore current recorded size"
690                              " (useful for forcing activation when"
691                              " the recorded size is wrong)")
692
693 SRC_NODE_OPT = cli_option("--src-node", dest="src_node", help="Source node",
694                           metavar="<node>",
695                           completion_suggest=OPT_COMPL_ONE_NODE)
696
697 SRC_DIR_OPT = cli_option("--src-dir", dest="src_dir", help="Source directory",
698                          metavar="<dir>")
699
700 SECONDARY_IP_OPT = cli_option("-s", "--secondary-ip", dest="secondary_ip",
701                               help="Specify the secondary ip for the node",
702                               metavar="ADDRESS", default=None)
703
704 READD_OPT = cli_option("--readd", dest="readd",
705                        default=False, action="store_true",
706                        help="Readd old node after replacing it")
707
708 NOSSH_KEYCHECK_OPT = cli_option("--no-ssh-key-check", dest="ssh_key_check",
709                                 default=True, action="store_false",
710                                 help="Disable SSH key fingerprint checking")
711
712
713 MC_OPT = cli_option("-C", "--master-candidate", dest="master_candidate",
714                     choices=_YESNO, default=None, metavar=_YORNO,
715                     help="Set the master_candidate flag on the node")
716
717 OFFLINE_OPT = cli_option("-O", "--offline", dest="offline", metavar=_YORNO,
718                          choices=_YESNO, default=None,
719                          help="Set the offline flag on the node")
720
721 DRAINED_OPT = cli_option("-D", "--drained", dest="drained", metavar=_YORNO,
722                          choices=_YESNO, default=None,
723                          help="Set the drained flag on the node")
724
725 ALLOCATABLE_OPT = cli_option("--allocatable", dest="allocatable",
726                              choices=_YESNO, default=None, metavar=_YORNO,
727                              help="Set the allocatable flag on a volume")
728
729 NOLVM_STORAGE_OPT = cli_option("--no-lvm-storage", dest="lvm_storage",
730                                help="Disable support for lvm based instances"
731                                " (cluster-wide)",
732                                action="store_false", default=True)
733
734 ENABLED_HV_OPT = cli_option("--enabled-hypervisors",
735                             dest="enabled_hypervisors",
736                             help="Comma-separated list of hypervisors",
737                             type="string", default=None)
738
739 NIC_PARAMS_OPT = cli_option("-N", "--nic-parameters", dest="nicparams",
740                             type="keyval", default={},
741                             help="NIC parameters")
742
743 CP_SIZE_OPT = cli_option("-C", "--candidate-pool-size", default=None,
744                          dest="candidate_pool_size", type="int",
745                          help="Set the candidate pool size")
746
747 VG_NAME_OPT = cli_option("-g", "--vg-name", dest="vg_name",
748                          help="Enables LVM and specifies the volume group"
749                          " name (cluster-wide) for disk allocation [xenvg]",
750                          metavar="VG", default=None)
751
752 YES_DOIT_OPT = cli_option("--yes-do-it", dest="yes_do_it",
753                           help="Destroy cluster", action="store_true")
754
755 NOVOTING_OPT = cli_option("--no-voting", dest="no_voting",
756                           help="Skip node agreement check (dangerous)",
757                           action="store_true", default=False)
758
759 MAC_PREFIX_OPT = cli_option("-m", "--mac-prefix", dest="mac_prefix",
760                             help="Specify the mac prefix for the instance IP"
761                             " addresses, in the format XX:XX:XX",
762                             metavar="PREFIX",
763                             default=None)
764
765 MASTER_NETDEV_OPT = cli_option("--master-netdev", dest="master_netdev",
766                                help="Specify the node interface (cluster-wide)"
767                                " on which the master IP address will be added "
768                                " [%s]" % constants.DEFAULT_BRIDGE,
769                                metavar="NETDEV",
770                                default=constants.DEFAULT_BRIDGE)
771
772
773 GLOBAL_FILEDIR_OPT = cli_option("--file-storage-dir", dest="file_storage_dir",
774                                 help="Specify the default directory (cluster-"
775                                 "wide) for storing the file-based disks [%s]" %
776                                 constants.DEFAULT_FILE_STORAGE_DIR,
777                                 metavar="DIR",
778                                 default=constants.DEFAULT_FILE_STORAGE_DIR)
779
780 NOMODIFY_ETCHOSTS_OPT = cli_option("--no-etc-hosts", dest="modify_etc_hosts",
781                                    help="Don't modify /etc/hosts",
782                                    action="store_false", default=True)
783
784
785 def _ParseArgs(argv, commands, aliases):
786   """Parser for the command line arguments.
787
788   This function parses the arguments and returns the function which
789   must be executed together with its (modified) arguments.
790
791   @param argv: the command line
792   @param commands: dictionary with special contents, see the design
793       doc for cmdline handling
794   @param aliases: dictionary with command aliases {'alias': 'target, ...}
795
796   """
797   if len(argv) == 0:
798     binary = "<command>"
799   else:
800     binary = argv[0].split("/")[-1]
801
802   if len(argv) > 1 and argv[1] == "--version":
803     ToStdout("%s (ganeti) %s", binary, constants.RELEASE_VERSION)
804     # Quit right away. That way we don't have to care about this special
805     # argument. optparse.py does it the same.
806     sys.exit(0)
807
808   if len(argv) < 2 or not (argv[1] in commands or
809                            argv[1] in aliases):
810     # let's do a nice thing
811     sortedcmds = commands.keys()
812     sortedcmds.sort()
813
814     ToStdout("Usage: %s {command} [options...] [argument...]", binary)
815     ToStdout("%s <command> --help to see details, or man %s", binary, binary)
816     ToStdout("")
817
818     # compute the max line length for cmd + usage
819     mlen = max([len(" %s" % cmd) for cmd in commands])
820     mlen = min(60, mlen) # should not get here...
821
822     # and format a nice command list
823     ToStdout("Commands:")
824     for cmd in sortedcmds:
825       cmdstr = " %s" % (cmd,)
826       help_text = commands[cmd][4]
827       help_lines = textwrap.wrap(help_text, 79 - 3 - mlen)
828       ToStdout("%-*s - %s", mlen, cmdstr, help_lines.pop(0))
829       for line in help_lines:
830         ToStdout("%-*s   %s", mlen, "", line)
831
832     ToStdout("")
833
834     return None, None, None
835
836   # get command, unalias it, and look it up in commands
837   cmd = argv.pop(1)
838   if cmd in aliases:
839     if cmd in commands:
840       raise errors.ProgrammerError("Alias '%s' overrides an existing"
841                                    " command" % cmd)
842
843     if aliases[cmd] not in commands:
844       raise errors.ProgrammerError("Alias '%s' maps to non-existing"
845                                    " command '%s'" % (cmd, aliases[cmd]))
846
847     cmd = aliases[cmd]
848
849   func, args_def, parser_opts, usage, description = commands[cmd]
850   parser = OptionParser(option_list=parser_opts + [_DRY_RUN_OPT],
851                         description=description,
852                         formatter=TitledHelpFormatter(),
853                         usage="%%prog %s %s" % (cmd, usage))
854   parser.disable_interspersed_args()
855   options, args = parser.parse_args()
856
857   if not _CheckArguments(cmd, args_def, args):
858     return None, None, None
859
860   return func, options, args
861
862
863 def _CheckArguments(cmd, args_def, args):
864   """Verifies the arguments using the argument definition.
865
866   Algorithm:
867
868     1. Abort with error if values specified by user but none expected.
869
870     1. For each argument in definition
871
872       1. Keep running count of minimum number of values (min_count)
873       1. Keep running count of maximum number of values (max_count)
874       1. If it has an unlimited number of values
875
876         1. Abort with error if it's not the last argument in the definition
877
878     1. If last argument has limited number of values
879
880       1. Abort with error if number of values doesn't match or is too large
881
882     1. Abort with error if user didn't pass enough values (min_count)
883
884   """
885   if args and not args_def:
886     ToStderr("Error: Command %s expects no arguments", cmd)
887     return False
888
889   min_count = None
890   max_count = None
891   check_max = None
892
893   last_idx = len(args_def) - 1
894
895   for idx, arg in enumerate(args_def):
896     if min_count is None:
897       min_count = arg.min
898     elif arg.min is not None:
899       min_count += arg.min
900
901     if max_count is None:
902       max_count = arg.max
903     elif arg.max is not None:
904       max_count += arg.max
905
906     if idx == last_idx:
907       check_max = (arg.max is not None)
908
909     elif arg.max is None:
910       raise errors.ProgrammerError("Only the last argument can have max=None")
911
912   if check_max:
913     # Command with exact number of arguments
914     if (min_count is not None and max_count is not None and
915         min_count == max_count and len(args) != min_count):
916       ToStderr("Error: Command %s expects %d argument(s)", cmd, min_count)
917       return False
918
919     # Command with limited number of arguments
920     if max_count is not None and len(args) > max_count:
921       ToStderr("Error: Command %s expects only %d argument(s)",
922                cmd, max_count)
923       return False
924
925   # Command with some required arguments
926   if min_count is not None and len(args) < min_count:
927     ToStderr("Error: Command %s expects at least %d argument(s)",
928              cmd, min_count)
929     return False
930
931   return True
932
933
934 def SplitNodeOption(value):
935   """Splits the value of a --node option.
936
937   """
938   if value and ':' in value:
939     return value.split(':', 1)
940   else:
941     return (value, None)
942
943
944 def UsesRPC(fn):
945   def wrapper(*args, **kwargs):
946     rpc.Init()
947     try:
948       return fn(*args, **kwargs)
949     finally:
950       rpc.Shutdown()
951   return wrapper
952
953
954 def AskUser(text, choices=None):
955   """Ask the user a question.
956
957   @param text: the question to ask
958
959   @param choices: list with elements tuples (input_char, return_value,
960       description); if not given, it will default to: [('y', True,
961       'Perform the operation'), ('n', False, 'Do no do the operation')];
962       note that the '?' char is reserved for help
963
964   @return: one of the return values from the choices list; if input is
965       not possible (i.e. not running with a tty, we return the last
966       entry from the list
967
968   """
969   if choices is None:
970     choices = [('y', True, 'Perform the operation'),
971                ('n', False, 'Do not perform the operation')]
972   if not choices or not isinstance(choices, list):
973     raise errors.ProgrammerError("Invalid choices argument to AskUser")
974   for entry in choices:
975     if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
976       raise errors.ProgrammerError("Invalid choices element to AskUser")
977
978   answer = choices[-1][1]
979   new_text = []
980   for line in text.splitlines():
981     new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
982   text = "\n".join(new_text)
983   try:
984     f = file("/dev/tty", "a+")
985   except IOError:
986     return answer
987   try:
988     chars = [entry[0] for entry in choices]
989     chars[-1] = "[%s]" % chars[-1]
990     chars.append('?')
991     maps = dict([(entry[0], entry[1]) for entry in choices])
992     while True:
993       f.write(text)
994       f.write('\n')
995       f.write("/".join(chars))
996       f.write(": ")
997       line = f.readline(2).strip().lower()
998       if line in maps:
999         answer = maps[line]
1000         break
1001       elif line == '?':
1002         for entry in choices:
1003           f.write(" %s - %s\n" % (entry[0], entry[2]))
1004         f.write("\n")
1005         continue
1006   finally:
1007     f.close()
1008   return answer
1009
1010
1011 class JobSubmittedException(Exception):
1012   """Job was submitted, client should exit.
1013
1014   This exception has one argument, the ID of the job that was
1015   submitted. The handler should print this ID.
1016
1017   This is not an error, just a structured way to exit from clients.
1018
1019   """
1020
1021
1022 def SendJob(ops, cl=None):
1023   """Function to submit an opcode without waiting for the results.
1024
1025   @type ops: list
1026   @param ops: list of opcodes
1027   @type cl: luxi.Client
1028   @param cl: the luxi client to use for communicating with the master;
1029              if None, a new client will be created
1030
1031   """
1032   if cl is None:
1033     cl = GetClient()
1034
1035   job_id = cl.SubmitJob(ops)
1036
1037   return job_id
1038
1039
1040 def PollJob(job_id, cl=None, feedback_fn=None):
1041   """Function to poll for the result of a job.
1042
1043   @type job_id: job identified
1044   @param job_id: the job to poll for results
1045   @type cl: luxi.Client
1046   @param cl: the luxi client to use for communicating with the master;
1047              if None, a new client will be created
1048
1049   """
1050   if cl is None:
1051     cl = GetClient()
1052
1053   prev_job_info = None
1054   prev_logmsg_serial = None
1055
1056   while True:
1057     result = cl.WaitForJobChange(job_id, ["status"], prev_job_info,
1058                                  prev_logmsg_serial)
1059     if not result:
1060       # job not found, go away!
1061       raise errors.JobLost("Job with id %s lost" % job_id)
1062
1063     # Split result, a tuple of (field values, log entries)
1064     (job_info, log_entries) = result
1065     (status, ) = job_info
1066
1067     if log_entries:
1068       for log_entry in log_entries:
1069         (serial, timestamp, _, message) = log_entry
1070         if callable(feedback_fn):
1071           feedback_fn(log_entry[1:])
1072         else:
1073           encoded = utils.SafeEncode(message)
1074           ToStdout("%s %s", time.ctime(utils.MergeTime(timestamp)), encoded)
1075         prev_logmsg_serial = max(prev_logmsg_serial, serial)
1076
1077     # TODO: Handle canceled and archived jobs
1078     elif status in (constants.JOB_STATUS_SUCCESS,
1079                     constants.JOB_STATUS_ERROR,
1080                     constants.JOB_STATUS_CANCELING,
1081                     constants.JOB_STATUS_CANCELED):
1082       break
1083
1084     prev_job_info = job_info
1085
1086   jobs = cl.QueryJobs([job_id], ["status", "opstatus", "opresult"])
1087   if not jobs:
1088     raise errors.JobLost("Job with id %s lost" % job_id)
1089
1090   status, opstatus, result = jobs[0]
1091   if status == constants.JOB_STATUS_SUCCESS:
1092     return result
1093   elif status in (constants.JOB_STATUS_CANCELING,
1094                   constants.JOB_STATUS_CANCELED):
1095     raise errors.OpExecError("Job was canceled")
1096   else:
1097     has_ok = False
1098     for idx, (status, msg) in enumerate(zip(opstatus, result)):
1099       if status == constants.OP_STATUS_SUCCESS:
1100         has_ok = True
1101       elif status == constants.OP_STATUS_ERROR:
1102         errors.MaybeRaise(msg)
1103         if has_ok:
1104           raise errors.OpExecError("partial failure (opcode %d): %s" %
1105                                    (idx, msg))
1106         else:
1107           raise errors.OpExecError(str(msg))
1108     # default failure mode
1109     raise errors.OpExecError(result)
1110
1111
1112 def SubmitOpCode(op, cl=None, feedback_fn=None):
1113   """Legacy function to submit an opcode.
1114
1115   This is just a simple wrapper over the construction of the processor
1116   instance. It should be extended to better handle feedback and
1117   interaction functions.
1118
1119   """
1120   if cl is None:
1121     cl = GetClient()
1122
1123   job_id = SendJob([op], cl)
1124
1125   op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
1126
1127   return op_results[0]
1128
1129
1130 def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
1131   """Wrapper around SubmitOpCode or SendJob.
1132
1133   This function will decide, based on the 'opts' parameter, whether to
1134   submit and wait for the result of the opcode (and return it), or
1135   whether to just send the job and print its identifier. It is used in
1136   order to simplify the implementation of the '--submit' option.
1137
1138   It will also add the dry-run parameter from the options passed, if true.
1139
1140   """
1141   if opts and opts.dry_run:
1142     op.dry_run = opts.dry_run
1143   if opts and opts.submit_only:
1144     job_id = SendJob([op], cl=cl)
1145     raise JobSubmittedException(job_id)
1146   else:
1147     return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
1148
1149
1150 def GetClient():
1151   # TODO: Cache object?
1152   try:
1153     client = luxi.Client()
1154   except luxi.NoMasterError:
1155     master, myself = ssconf.GetMasterAndMyself()
1156     if master != myself:
1157       raise errors.OpPrereqError("This is not the master node, please connect"
1158                                  " to node '%s' and rerun the command" %
1159                                  master)
1160     else:
1161       raise
1162   return client
1163
1164
1165 def FormatError(err):
1166   """Return a formatted error message for a given error.
1167
1168   This function takes an exception instance and returns a tuple
1169   consisting of two values: first, the recommended exit code, and
1170   second, a string describing the error message (not
1171   newline-terminated).
1172
1173   """
1174   retcode = 1
1175   obuf = StringIO()
1176   msg = str(err)
1177   if isinstance(err, errors.ConfigurationError):
1178     txt = "Corrupt configuration file: %s" % msg
1179     logging.error(txt)
1180     obuf.write(txt + "\n")
1181     obuf.write("Aborting.")
1182     retcode = 2
1183   elif isinstance(err, errors.HooksAbort):
1184     obuf.write("Failure: hooks execution failed:\n")
1185     for node, script, out in err.args[0]:
1186       if out:
1187         obuf.write("  node: %s, script: %s, output: %s\n" %
1188                    (node, script, out))
1189       else:
1190         obuf.write("  node: %s, script: %s (no output)\n" %
1191                    (node, script))
1192   elif isinstance(err, errors.HooksFailure):
1193     obuf.write("Failure: hooks general failure: %s" % msg)
1194   elif isinstance(err, errors.ResolverError):
1195     this_host = utils.HostInfo.SysName()
1196     if err.args[0] == this_host:
1197       msg = "Failure: can't resolve my own hostname ('%s')"
1198     else:
1199       msg = "Failure: can't resolve hostname '%s'"
1200     obuf.write(msg % err.args[0])
1201   elif isinstance(err, errors.OpPrereqError):
1202     obuf.write("Failure: prerequisites not met for this"
1203                " operation:\n%s" % msg)
1204   elif isinstance(err, errors.OpExecError):
1205     obuf.write("Failure: command execution error:\n%s" % msg)
1206   elif isinstance(err, errors.TagError):
1207     obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
1208   elif isinstance(err, errors.JobQueueDrainError):
1209     obuf.write("Failure: the job queue is marked for drain and doesn't"
1210                " accept new requests\n")
1211   elif isinstance(err, errors.JobQueueFull):
1212     obuf.write("Failure: the job queue is full and doesn't accept new"
1213                " job submissions until old jobs are archived\n")
1214   elif isinstance(err, errors.TypeEnforcementError):
1215     obuf.write("Parameter Error: %s" % msg)
1216   elif isinstance(err, errors.ParameterError):
1217     obuf.write("Failure: unknown/wrong parameter name '%s'" % msg)
1218   elif isinstance(err, errors.GenericError):
1219     obuf.write("Unhandled Ganeti error: %s" % msg)
1220   elif isinstance(err, luxi.NoMasterError):
1221     obuf.write("Cannot communicate with the master daemon.\nIs it running"
1222                " and listening for connections?")
1223   elif isinstance(err, luxi.TimeoutError):
1224     obuf.write("Timeout while talking to the master daemon. Error:\n"
1225                "%s" % msg)
1226   elif isinstance(err, luxi.ProtocolError):
1227     obuf.write("Unhandled protocol error while talking to the master daemon:\n"
1228                "%s" % msg)
1229   elif isinstance(err, JobSubmittedException):
1230     obuf.write("JobID: %s\n" % err.args[0])
1231     retcode = 0
1232   else:
1233     obuf.write("Unhandled exception: %s" % msg)
1234   return retcode, obuf.getvalue().rstrip('\n')
1235
1236
1237 def GenericMain(commands, override=None, aliases=None):
1238   """Generic main function for all the gnt-* commands.
1239
1240   Arguments:
1241     - commands: a dictionary with a special structure, see the design doc
1242                 for command line handling.
1243     - override: if not None, we expect a dictionary with keys that will
1244                 override command line options; this can be used to pass
1245                 options from the scripts to generic functions
1246     - aliases: dictionary with command aliases {'alias': 'target, ...}
1247
1248   """
1249   # save the program name and the entire command line for later logging
1250   if sys.argv:
1251     binary = os.path.basename(sys.argv[0]) or sys.argv[0]
1252     if len(sys.argv) >= 2:
1253       binary += " " + sys.argv[1]
1254       old_cmdline = " ".join(sys.argv[2:])
1255     else:
1256       old_cmdline = ""
1257   else:
1258     binary = "<unknown program>"
1259     old_cmdline = ""
1260
1261   if aliases is None:
1262     aliases = {}
1263
1264   try:
1265     func, options, args = _ParseArgs(sys.argv, commands, aliases)
1266   except errors.ParameterError, err:
1267     result, err_msg = FormatError(err)
1268     ToStderr(err_msg)
1269     return 1
1270
1271   if func is None: # parse error
1272     return 1
1273
1274   if override is not None:
1275     for key, val in override.iteritems():
1276       setattr(options, key, val)
1277
1278   utils.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
1279                      stderr_logging=True, program=binary)
1280
1281   if old_cmdline:
1282     logging.info("run with arguments '%s'", old_cmdline)
1283   else:
1284     logging.info("run with no arguments")
1285
1286   try:
1287     result = func(options, args)
1288   except (errors.GenericError, luxi.ProtocolError,
1289           JobSubmittedException), err:
1290     result, err_msg = FormatError(err)
1291     logging.exception("Error during command processing")
1292     ToStderr(err_msg)
1293
1294   return result
1295
1296
1297 def GenerateTable(headers, fields, separator, data,
1298                   numfields=None, unitfields=None,
1299                   units=None):
1300   """Prints a table with headers and different fields.
1301
1302   @type headers: dict
1303   @param headers: dictionary mapping field names to headers for
1304       the table
1305   @type fields: list
1306   @param fields: the field names corresponding to each row in
1307       the data field
1308   @param separator: the separator to be used; if this is None,
1309       the default 'smart' algorithm is used which computes optimal
1310       field width, otherwise just the separator is used between
1311       each field
1312   @type data: list
1313   @param data: a list of lists, each sublist being one row to be output
1314   @type numfields: list
1315   @param numfields: a list with the fields that hold numeric
1316       values and thus should be right-aligned
1317   @type unitfields: list
1318   @param unitfields: a list with the fields that hold numeric
1319       values that should be formatted with the units field
1320   @type units: string or None
1321   @param units: the units we should use for formatting, or None for
1322       automatic choice (human-readable for non-separator usage, otherwise
1323       megabytes); this is a one-letter string
1324
1325   """
1326   if units is None:
1327     if separator:
1328       units = "m"
1329     else:
1330       units = "h"
1331
1332   if numfields is None:
1333     numfields = []
1334   if unitfields is None:
1335     unitfields = []
1336
1337   numfields = utils.FieldSet(*numfields)
1338   unitfields = utils.FieldSet(*unitfields)
1339
1340   format_fields = []
1341   for field in fields:
1342     if headers and field not in headers:
1343       # TODO: handle better unknown fields (either revert to old
1344       # style of raising exception, or deal more intelligently with
1345       # variable fields)
1346       headers[field] = field
1347     if separator is not None:
1348       format_fields.append("%s")
1349     elif numfields.Matches(field):
1350       format_fields.append("%*s")
1351     else:
1352       format_fields.append("%-*s")
1353
1354   if separator is None:
1355     mlens = [0 for name in fields]
1356     format = ' '.join(format_fields)
1357   else:
1358     format = separator.replace("%", "%%").join(format_fields)
1359
1360   for row in data:
1361     if row is None:
1362       continue
1363     for idx, val in enumerate(row):
1364       if unitfields.Matches(fields[idx]):
1365         try:
1366           val = int(val)
1367         except ValueError:
1368           pass
1369         else:
1370           val = row[idx] = utils.FormatUnit(val, units)
1371       val = row[idx] = str(val)
1372       if separator is None:
1373         mlens[idx] = max(mlens[idx], len(val))
1374
1375   result = []
1376   if headers:
1377     args = []
1378     for idx, name in enumerate(fields):
1379       hdr = headers[name]
1380       if separator is None:
1381         mlens[idx] = max(mlens[idx], len(hdr))
1382         args.append(mlens[idx])
1383       args.append(hdr)
1384     result.append(format % tuple(args))
1385
1386   for line in data:
1387     args = []
1388     if line is None:
1389       line = ['-' for _ in fields]
1390     for idx in xrange(len(fields)):
1391       if separator is None:
1392         args.append(mlens[idx])
1393       args.append(line[idx])
1394     result.append(format % tuple(args))
1395
1396   return result
1397
1398
1399 def FormatTimestamp(ts):
1400   """Formats a given timestamp.
1401
1402   @type ts: timestamp
1403   @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
1404
1405   @rtype: string
1406   @return: a string with the formatted timestamp
1407
1408   """
1409   if not isinstance (ts, (tuple, list)) or len(ts) != 2:
1410     return '?'
1411   sec, usec = ts
1412   return time.strftime("%F %T", time.localtime(sec)) + ".%06d" % usec
1413
1414
1415 def ParseTimespec(value):
1416   """Parse a time specification.
1417
1418   The following suffixed will be recognized:
1419
1420     - s: seconds
1421     - m: minutes
1422     - h: hours
1423     - d: day
1424     - w: weeks
1425
1426   Without any suffix, the value will be taken to be in seconds.
1427
1428   """
1429   value = str(value)
1430   if not value:
1431     raise errors.OpPrereqError("Empty time specification passed")
1432   suffix_map = {
1433     's': 1,
1434     'm': 60,
1435     'h': 3600,
1436     'd': 86400,
1437     'w': 604800,
1438     }
1439   if value[-1] not in suffix_map:
1440     try:
1441       value = int(value)
1442     except ValueError:
1443       raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1444   else:
1445     multiplier = suffix_map[value[-1]]
1446     value = value[:-1]
1447     if not value: # no data left after stripping the suffix
1448       raise errors.OpPrereqError("Invalid time specification (only"
1449                                  " suffix passed)")
1450     try:
1451       value = int(value) * multiplier
1452     except ValueError:
1453       raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1454   return value
1455
1456
1457 def GetOnlineNodes(nodes, cl=None, nowarn=False):
1458   """Returns the names of online nodes.
1459
1460   This function will also log a warning on stderr with the names of
1461   the online nodes.
1462
1463   @param nodes: if not empty, use only this subset of nodes (minus the
1464       offline ones)
1465   @param cl: if not None, luxi client to use
1466   @type nowarn: boolean
1467   @param nowarn: by default, this function will output a note with the
1468       offline nodes that are skipped; if this parameter is True the
1469       note is not displayed
1470
1471   """
1472   if cl is None:
1473     cl = GetClient()
1474
1475   result = cl.QueryNodes(names=nodes, fields=["name", "offline"],
1476                          use_locking=False)
1477   offline = [row[0] for row in result if row[1]]
1478   if offline and not nowarn:
1479     ToStderr("Note: skipping offline node(s): %s" % ", ".join(offline))
1480   return [row[0] for row in result if not row[1]]
1481
1482
1483 def _ToStream(stream, txt, *args):
1484   """Write a message to a stream, bypassing the logging system
1485
1486   @type stream: file object
1487   @param stream: the file to which we should write
1488   @type txt: str
1489   @param txt: the message
1490
1491   """
1492   if args:
1493     args = tuple(args)
1494     stream.write(txt % args)
1495   else:
1496     stream.write(txt)
1497   stream.write('\n')
1498   stream.flush()
1499
1500
1501 def ToStdout(txt, *args):
1502   """Write a message to stdout only, bypassing the logging system
1503
1504   This is just a wrapper over _ToStream.
1505
1506   @type txt: str
1507   @param txt: the message
1508
1509   """
1510   _ToStream(sys.stdout, txt, *args)
1511
1512
1513 def ToStderr(txt, *args):
1514   """Write a message to stderr only, bypassing the logging system
1515
1516   This is just a wrapper over _ToStream.
1517
1518   @type txt: str
1519   @param txt: the message
1520
1521   """
1522   _ToStream(sys.stderr, txt, *args)
1523
1524
1525 class JobExecutor(object):
1526   """Class which manages the submission and execution of multiple jobs.
1527
1528   Note that instances of this class should not be reused between
1529   GetResults() calls.
1530
1531   """
1532   def __init__(self, cl=None, verbose=True):
1533     self.queue = []
1534     if cl is None:
1535       cl = GetClient()
1536     self.cl = cl
1537     self.verbose = verbose
1538     self.jobs = []
1539
1540   def QueueJob(self, name, *ops):
1541     """Record a job for later submit.
1542
1543     @type name: string
1544     @param name: a description of the job, will be used in WaitJobSet
1545     """
1546     self.queue.append((name, ops))
1547
1548   def SubmitPending(self):
1549     """Submit all pending jobs.
1550
1551     """
1552     results = self.cl.SubmitManyJobs([row[1] for row in self.queue])
1553     for ((status, data), (name, _)) in zip(results, self.queue):
1554       self.jobs.append((status, data, name))
1555
1556   def GetResults(self):
1557     """Wait for and return the results of all jobs.
1558
1559     @rtype: list
1560     @return: list of tuples (success, job results), in the same order
1561         as the submitted jobs; if a job has failed, instead of the result
1562         there will be the error message
1563
1564     """
1565     if not self.jobs:
1566       self.SubmitPending()
1567     results = []
1568     if self.verbose:
1569       ok_jobs = [row[1] for row in self.jobs if row[0]]
1570       if ok_jobs:
1571         ToStdout("Submitted jobs %s", ", ".join(ok_jobs))
1572     for submit_status, jid, name in self.jobs:
1573       if not submit_status:
1574         ToStderr("Failed to submit job for %s: %s", name, jid)
1575         results.append((False, jid))
1576         continue
1577       if self.verbose:
1578         ToStdout("Waiting for job %s for %s...", jid, name)
1579       try:
1580         job_result = PollJob(jid, cl=self.cl)
1581         success = True
1582       except (errors.GenericError, luxi.ProtocolError), err:
1583         _, job_result = FormatError(err)
1584         success = False
1585         # the error message will always be shown, verbose or not
1586         ToStderr("Job %s for %s has failed: %s", jid, name, job_result)
1587
1588       results.append((success, job_result))
1589     return results
1590
1591   def WaitOrShow(self, wait):
1592     """Wait for job results or only print the job IDs.
1593
1594     @type wait: boolean
1595     @param wait: whether to wait or not
1596
1597     """
1598     if wait:
1599       return self.GetResults()
1600     else:
1601       if not self.jobs:
1602         self.SubmitPending()
1603       for status, result, name in self.jobs:
1604         if status:
1605           ToStdout("%s: %s", result, name)
1606         else:
1607           ToStderr("Failure for %s: %s", name, result)