Fix double use of PRIORITY_OPT in gnt-node migrate
[ganeti-local] / lib / client / gnt_node.py
1 #
2 #
3
4 # Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012 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 """Node related commands"""
22
23 # pylint: disable=W0401,W0613,W0614,C0103
24 # W0401: Wildcard import ganeti.cli
25 # W0613: Unused argument, since all functions follow the same API
26 # W0614: Unused import %s from wildcard import (since we need cli)
27 # C0103: Invalid name gnt-node
28
29 import itertools
30
31 from ganeti.cli import *
32 from ganeti import cli
33 from ganeti import bootstrap
34 from ganeti import opcodes
35 from ganeti import utils
36 from ganeti import constants
37 from ganeti import errors
38 from ganeti import netutils
39 from cStringIO import StringIO
40
41 from ganeti import confd
42 from ganeti.confd import client as confd_client
43
44 #: default list of field for L{ListNodes}
45 _LIST_DEF_FIELDS = [
46   "name", "dtotal", "dfree",
47   "mtotal", "mnode", "mfree",
48   "pinst_cnt", "sinst_cnt",
49   ]
50
51
52 #: Default field list for L{ListVolumes}
53 _LIST_VOL_DEF_FIELDS = ["node", "phys", "vg", "name", "size", "instance"]
54
55
56 #: default list of field for L{ListStorage}
57 _LIST_STOR_DEF_FIELDS = [
58   constants.SF_NODE,
59   constants.SF_TYPE,
60   constants.SF_NAME,
61   constants.SF_SIZE,
62   constants.SF_USED,
63   constants.SF_FREE,
64   constants.SF_ALLOCATABLE,
65   ]
66
67
68 #: default list of power commands
69 _LIST_POWER_COMMANDS = ["on", "off", "cycle", "status"]
70
71
72 #: headers (and full field list) for L{ListStorage}
73 _LIST_STOR_HEADERS = {
74   constants.SF_NODE: "Node",
75   constants.SF_TYPE: "Type",
76   constants.SF_NAME: "Name",
77   constants.SF_SIZE: "Size",
78   constants.SF_USED: "Used",
79   constants.SF_FREE: "Free",
80   constants.SF_ALLOCATABLE: "Allocatable",
81   }
82
83
84 #: User-facing storage unit types
85 _USER_STORAGE_TYPE = {
86   constants.ST_FILE: "file",
87   constants.ST_LVM_PV: "lvm-pv",
88   constants.ST_LVM_VG: "lvm-vg",
89   }
90
91 _STORAGE_TYPE_OPT = \
92   cli_option("-t", "--storage-type",
93              dest="user_storage_type",
94              choices=_USER_STORAGE_TYPE.keys(),
95              default=None,
96              metavar="STORAGE_TYPE",
97              help=("Storage type (%s)" %
98                    utils.CommaJoin(_USER_STORAGE_TYPE.keys())))
99
100 _REPAIRABLE_STORAGE_TYPES = \
101   [st for st, so in constants.VALID_STORAGE_OPERATIONS.iteritems()
102    if constants.SO_FIX_CONSISTENCY in so]
103
104 _MODIFIABLE_STORAGE_TYPES = constants.MODIFIABLE_STORAGE_FIELDS.keys()
105
106
107 _OOB_COMMAND_ASK = frozenset([constants.OOB_POWER_OFF,
108                               constants.OOB_POWER_CYCLE])
109
110
111 _ENV_OVERRIDE = frozenset(["list"])
112
113
114 NONODE_SETUP_OPT = cli_option("--no-node-setup", default=True,
115                               action="store_false", dest="node_setup",
116                               help=("Do not make initial SSH setup on remote"
117                                     " node (needs to be done manually)"))
118
119 IGNORE_STATUS_OPT = cli_option("--ignore-status", default=False,
120                                action="store_true", dest="ignore_status",
121                                help=("Ignore the Node(s) offline status"
122                                      " (potentially DANGEROUS)"))
123
124
125 def ConvertStorageType(user_storage_type):
126   """Converts a user storage type to its internal name.
127
128   """
129   try:
130     return _USER_STORAGE_TYPE[user_storage_type]
131   except KeyError:
132     raise errors.OpPrereqError("Unknown storage type: %s" % user_storage_type,
133                                errors.ECODE_INVAL)
134
135
136 def _RunSetupSSH(options, nodes):
137   """Wrapper around utils.RunCmd to call setup-ssh
138
139   @param options: The command line options
140   @param nodes: The nodes to setup
141
142   """
143
144   assert nodes, "Empty node list"
145
146   cmd = [constants.SETUP_SSH]
147
148   # Pass --debug|--verbose to the external script if set on our invocation
149   # --debug overrides --verbose
150   if options.debug:
151     cmd.append("--debug")
152   elif options.verbose:
153     cmd.append("--verbose")
154   if not options.ssh_key_check:
155     cmd.append("--no-ssh-key-check")
156   if options.force_join:
157     cmd.append("--force-join")
158
159   cmd.extend(nodes)
160
161   result = utils.RunCmd(cmd, interactive=True)
162
163   if result.failed:
164     errmsg = ("Command '%s' failed with exit code %s; output %r" %
165               (result.cmd, result.exit_code, result.output))
166     raise errors.OpExecError(errmsg)
167
168
169 @UsesRPC
170 def AddNode(opts, args):
171   """Add a node to the cluster.
172
173   @param opts: the command line options selected by the user
174   @type args: list
175   @param args: should contain only one element, the new node name
176   @rtype: int
177   @return: the desired exit code
178
179   """
180   cl = GetClient()
181   node = netutils.GetHostname(name=args[0]).name
182   readd = opts.readd
183
184   try:
185     output = cl.QueryNodes(names=[node], fields=["name", "sip", "master"],
186                            use_locking=False)
187     node_exists, sip, is_master = output[0]
188   except (errors.OpPrereqError, errors.OpExecError):
189     node_exists = ""
190     sip = None
191
192   if readd:
193     if not node_exists:
194       ToStderr("Node %s not in the cluster"
195                " - please retry without '--readd'", node)
196       return 1
197     if is_master:
198       ToStderr("Node %s is the master, cannot readd", node)
199       return 1
200   else:
201     if node_exists:
202       ToStderr("Node %s already in the cluster (as %s)"
203                " - please retry with '--readd'", node, node_exists)
204       return 1
205     sip = opts.secondary_ip
206
207   # read the cluster name from the master
208   output = cl.QueryConfigValues(["cluster_name"])
209   cluster_name = output[0]
210
211   if not readd and opts.node_setup:
212     ToStderr("-- WARNING -- \n"
213              "Performing this operation is going to replace the ssh daemon"
214              " keypair\n"
215              "on the target machine (%s) with the ones of the"
216              " current one\n"
217              "and grant full intra-cluster ssh root access to/from it\n", node)
218
219   if opts.node_setup:
220     _RunSetupSSH(opts, [node])
221
222   bootstrap.SetupNodeDaemon(cluster_name, node, opts.ssh_key_check)
223
224   if opts.disk_state:
225     disk_state = utils.FlatToDict(opts.disk_state)
226   else:
227     disk_state = {}
228
229   hv_state = dict(opts.hv_state)
230
231   op = opcodes.OpNodeAdd(node_name=args[0], secondary_ip=sip,
232                          readd=opts.readd, group=opts.nodegroup,
233                          vm_capable=opts.vm_capable, ndparams=opts.ndparams,
234                          master_capable=opts.master_capable,
235                          disk_state=disk_state,
236                          hv_state=hv_state)
237   SubmitOpCode(op, opts=opts)
238
239
240 def ListNodes(opts, args):
241   """List nodes and their properties.
242
243   @param opts: the command line options selected by the user
244   @type args: list
245   @param args: nodes to list, or empty for all
246   @rtype: int
247   @return: the desired exit code
248
249   """
250   selected_fields = ParseFields(opts.output, _LIST_DEF_FIELDS)
251
252   fmtoverride = dict.fromkeys(["pinst_list", "sinst_list", "tags"],
253                               (",".join, False))
254
255   return GenericList(constants.QR_NODE, selected_fields, args, opts.units,
256                      opts.separator, not opts.no_headers,
257                      format_override=fmtoverride, verbose=opts.verbose,
258                      force_filter=opts.force_filter)
259
260
261 def ListNodeFields(opts, args):
262   """List node fields.
263
264   @param opts: the command line options selected by the user
265   @type args: list
266   @param args: fields to list, or empty for all
267   @rtype: int
268   @return: the desired exit code
269
270   """
271   return GenericListFields(constants.QR_NODE, args, opts.separator,
272                            not opts.no_headers)
273
274
275 def EvacuateNode(opts, args):
276   """Relocate all secondary instance from a node.
277
278   @param opts: the command line options selected by the user
279   @type args: list
280   @param args: should be an empty list
281   @rtype: int
282   @return: the desired exit code
283
284   """
285   if opts.dst_node is not None:
286     ToStderr("New secondary node given (disabling iallocator), hence evacuating"
287              " secondary instances only.")
288     opts.secondary_only = True
289     opts.primary_only = False
290
291   if opts.secondary_only and opts.primary_only:
292     raise errors.OpPrereqError("Only one of the --primary-only and"
293                                " --secondary-only options can be passed",
294                                errors.ECODE_INVAL)
295   elif opts.primary_only:
296     mode = constants.NODE_EVAC_PRI
297   elif opts.secondary_only:
298     mode = constants.NODE_EVAC_SEC
299   else:
300     mode = constants.NODE_EVAC_ALL
301
302   # Determine affected instances
303   fields = []
304
305   if not opts.secondary_only:
306     fields.append("pinst_list")
307   if not opts.primary_only:
308     fields.append("sinst_list")
309
310   cl = GetClient()
311
312   result = cl.QueryNodes(names=args, fields=fields, use_locking=False)
313   instances = set(itertools.chain(*itertools.chain(*itertools.chain(result))))
314
315   if not instances:
316     # No instances to evacuate
317     ToStderr("No instances to evacuate on node(s) %s, exiting.",
318              utils.CommaJoin(args))
319     return constants.EXIT_SUCCESS
320
321   if not (opts.force or
322           AskUser("Relocate instance(s) %s from node(s) %s?" %
323                   (utils.CommaJoin(utils.NiceSort(instances)),
324                    utils.CommaJoin(args)))):
325     return constants.EXIT_CONFIRMATION
326
327   # Evacuate node
328   op = opcodes.OpNodeEvacuate(node_name=args[0], mode=mode,
329                               remote_node=opts.dst_node,
330                               iallocator=opts.iallocator,
331                               early_release=opts.early_release)
332   result = SubmitOrSend(op, opts, cl=cl)
333
334   # Keep track of submitted jobs
335   jex = JobExecutor(cl=cl, opts=opts)
336
337   for (status, job_id) in result[constants.JOB_IDS_KEY]:
338     jex.AddJobId(None, status, job_id)
339
340   results = jex.GetResults()
341   bad_cnt = len([row for row in results if not row[0]])
342   if bad_cnt == 0:
343     ToStdout("All instances evacuated successfully.")
344     rcode = constants.EXIT_SUCCESS
345   else:
346     ToStdout("There were %s errors during the evacuation.", bad_cnt)
347     rcode = constants.EXIT_FAILURE
348
349   return rcode
350
351
352 def FailoverNode(opts, args):
353   """Failover all primary instance on a node.
354
355   @param opts: the command line options selected by the user
356   @type args: list
357   @param args: should be an empty list
358   @rtype: int
359   @return: the desired exit code
360
361   """
362   cl = GetClient()
363   force = opts.force
364   selected_fields = ["name", "pinst_list"]
365
366   # these fields are static data anyway, so it doesn't matter, but
367   # locking=True should be safer
368   result = cl.QueryNodes(names=args, fields=selected_fields,
369                          use_locking=False)
370   node, pinst = result[0]
371
372   if not pinst:
373     ToStderr("No primary instances on node %s, exiting.", node)
374     return 0
375
376   pinst = utils.NiceSort(pinst)
377
378   retcode = 0
379
380   if not force and not AskUser("Fail over instance(s) %s?" %
381                                (",".join("'%s'" % name for name in pinst))):
382     return 2
383
384   jex = JobExecutor(cl=cl, opts=opts)
385   for iname in pinst:
386     op = opcodes.OpInstanceFailover(instance_name=iname,
387                                     ignore_consistency=opts.ignore_consistency,
388                                     iallocator=opts.iallocator)
389     jex.QueueJob(iname, op)
390   results = jex.GetResults()
391   bad_cnt = len([row for row in results if not row[0]])
392   if bad_cnt == 0:
393     ToStdout("All %d instance(s) failed over successfully.", len(results))
394   else:
395     ToStdout("There were errors during the failover:\n"
396              "%d error(s) out of %d instance(s).", bad_cnt, len(results))
397   return retcode
398
399
400 def MigrateNode(opts, args):
401   """Migrate all primary instance on a node.
402
403   """
404   cl = GetClient()
405   force = opts.force
406   selected_fields = ["name", "pinst_list"]
407
408   result = cl.QueryNodes(names=args, fields=selected_fields, use_locking=False)
409   ((node, pinst), ) = result
410
411   if not pinst:
412     ToStdout("No primary instances on node %s, exiting." % node)
413     return 0
414
415   pinst = utils.NiceSort(pinst)
416
417   if not (force or
418           AskUser("Migrate instance(s) %s?" %
419                   utils.CommaJoin(utils.NiceSort(pinst)))):
420     return constants.EXIT_CONFIRMATION
421
422   # this should be removed once --non-live is deprecated
423   if not opts.live and opts.migration_mode is not None:
424     raise errors.OpPrereqError("Only one of the --non-live and "
425                                "--migration-mode options can be passed",
426                                errors.ECODE_INVAL)
427   if not opts.live: # --non-live passed
428     mode = constants.HT_MIGRATION_NONLIVE
429   else:
430     mode = opts.migration_mode
431
432   op = opcodes.OpNodeMigrate(node_name=args[0], mode=mode,
433                              iallocator=opts.iallocator,
434                              target_node=opts.dst_node,
435                              allow_runtime_changes=opts.allow_runtime_chgs,
436                              ignore_ipolicy=opts.ignore_ipolicy)
437
438   result = SubmitOrSend(op, opts, cl=cl)
439
440   # Keep track of submitted jobs
441   jex = JobExecutor(cl=cl, opts=opts)
442
443   for (status, job_id) in result[constants.JOB_IDS_KEY]:
444     jex.AddJobId(None, status, job_id)
445
446   results = jex.GetResults()
447   bad_cnt = len([row for row in results if not row[0]])
448   if bad_cnt == 0:
449     ToStdout("All instances migrated successfully.")
450     rcode = constants.EXIT_SUCCESS
451   else:
452     ToStdout("There were %s errors during the node migration.", bad_cnt)
453     rcode = constants.EXIT_FAILURE
454
455   return rcode
456
457
458 def ShowNodeConfig(opts, args):
459   """Show node information.
460
461   @param opts: the command line options selected by the user
462   @type args: list
463   @param args: should either be an empty list, in which case
464       we show information about all nodes, or should contain
465       a list of nodes to be queried for information
466   @rtype: int
467   @return: the desired exit code
468
469   """
470   cl = GetClient()
471   result = cl.QueryNodes(fields=["name", "pip", "sip",
472                                  "pinst_list", "sinst_list",
473                                  "master_candidate", "drained", "offline",
474                                  "master_capable", "vm_capable", "powered",
475                                  "ndparams", "custom_ndparams"],
476                          names=args, use_locking=False)
477
478   for (name, primary_ip, secondary_ip, pinst, sinst, is_mc, drained, offline,
479        master_capable, vm_capable, powered, ndparams,
480        ndparams_custom) in result:
481     ToStdout("Node name: %s", name)
482     ToStdout("  primary ip: %s", primary_ip)
483     ToStdout("  secondary ip: %s", secondary_ip)
484     ToStdout("  master candidate: %s", is_mc)
485     ToStdout("  drained: %s", drained)
486     ToStdout("  offline: %s", offline)
487     if powered is not None:
488       ToStdout("  powered: %s", powered)
489     ToStdout("  master_capable: %s", master_capable)
490     ToStdout("  vm_capable: %s", vm_capable)
491     if vm_capable:
492       if pinst:
493         ToStdout("  primary for instances:")
494         for iname in utils.NiceSort(pinst):
495           ToStdout("    - %s", iname)
496       else:
497         ToStdout("  primary for no instances")
498       if sinst:
499         ToStdout("  secondary for instances:")
500         for iname in utils.NiceSort(sinst):
501           ToStdout("    - %s", iname)
502       else:
503         ToStdout("  secondary for no instances")
504     ToStdout("  node parameters:")
505     buf = StringIO()
506     FormatParameterDict(buf, ndparams_custom, ndparams, level=2)
507     ToStdout(buf.getvalue().rstrip("\n"))
508
509   return 0
510
511
512 def RemoveNode(opts, args):
513   """Remove a node from the cluster.
514
515   @param opts: the command line options selected by the user
516   @type args: list
517   @param args: should contain only one element, the name of
518       the node to be removed
519   @rtype: int
520   @return: the desired exit code
521
522   """
523   op = opcodes.OpNodeRemove(node_name=args[0])
524   SubmitOpCode(op, opts=opts)
525   return 0
526
527
528 def PowercycleNode(opts, args):
529   """Remove a node from the cluster.
530
531   @param opts: the command line options selected by the user
532   @type args: list
533   @param args: should contain only one element, the name of
534       the node to be removed
535   @rtype: int
536   @return: the desired exit code
537
538   """
539   node = args[0]
540   if (not opts.confirm and
541       not AskUser("Are you sure you want to hard powercycle node %s?" % node)):
542     return 2
543
544   op = opcodes.OpNodePowercycle(node_name=node, force=opts.force)
545   result = SubmitOrSend(op, opts)
546   if result:
547     ToStderr(result)
548   return 0
549
550
551 def PowerNode(opts, args):
552   """Change/ask power state of a node.
553
554   @param opts: the command line options selected by the user
555   @type args: list
556   @param args: should contain only one element, the name of
557       the node to be removed
558   @rtype: int
559   @return: the desired exit code
560
561   """
562   command = args.pop(0)
563
564   if opts.no_headers:
565     headers = None
566   else:
567     headers = {"node": "Node", "status": "Status"}
568
569   if command not in _LIST_POWER_COMMANDS:
570     ToStderr("power subcommand %s not supported." % command)
571     return constants.EXIT_FAILURE
572
573   oob_command = "power-%s" % command
574
575   if oob_command in _OOB_COMMAND_ASK:
576     if not args:
577       ToStderr("Please provide at least one node for this command")
578       return constants.EXIT_FAILURE
579     elif not opts.force and not ConfirmOperation(args, "nodes",
580                                                  "power %s" % command):
581       return constants.EXIT_FAILURE
582     assert len(args) > 0
583
584   opcodelist = []
585   if not opts.ignore_status and oob_command == constants.OOB_POWER_OFF:
586     # TODO: This is a little ugly as we can't catch and revert
587     for node in args:
588       opcodelist.append(opcodes.OpNodeSetParams(node_name=node, offline=True,
589                                                 auto_promote=opts.auto_promote))
590
591   opcodelist.append(opcodes.OpOobCommand(node_names=args,
592                                          command=oob_command,
593                                          ignore_status=opts.ignore_status,
594                                          timeout=opts.oob_timeout,
595                                          power_delay=opts.power_delay))
596
597   cli.SetGenericOpcodeOpts(opcodelist, opts)
598
599   job_id = cli.SendJob(opcodelist)
600
601   # We just want the OOB Opcode status
602   # If it fails PollJob gives us the error message in it
603   result = cli.PollJob(job_id)[-1]
604
605   errs = 0
606   data = []
607   for node_result in result:
608     (node_tuple, data_tuple) = node_result
609     (_, node_name) = node_tuple
610     (data_status, data_node) = data_tuple
611     if data_status == constants.RS_NORMAL:
612       if oob_command == constants.OOB_POWER_STATUS:
613         if data_node[constants.OOB_POWER_STATUS_POWERED]:
614           text = "powered"
615         else:
616           text = "unpowered"
617         data.append([node_name, text])
618       else:
619         # We don't expect data here, so we just say, it was successfully invoked
620         data.append([node_name, "invoked"])
621     else:
622       errs += 1
623       data.append([node_name, cli.FormatResultError(data_status, True)])
624
625   data = GenerateTable(separator=opts.separator, headers=headers,
626                        fields=["node", "status"], data=data)
627
628   for line in data:
629     ToStdout(line)
630
631   if errs:
632     return constants.EXIT_FAILURE
633   else:
634     return constants.EXIT_SUCCESS
635
636
637 def Health(opts, args):
638   """Show health of a node using OOB.
639
640   @param opts: the command line options selected by the user
641   @type args: list
642   @param args: should contain only one element, the name of
643       the node to be removed
644   @rtype: int
645   @return: the desired exit code
646
647   """
648   op = opcodes.OpOobCommand(node_names=args, command=constants.OOB_HEALTH,
649                             timeout=opts.oob_timeout)
650   result = SubmitOpCode(op, opts=opts)
651
652   if opts.no_headers:
653     headers = None
654   else:
655     headers = {"node": "Node", "status": "Status"}
656
657   errs = 0
658   data = []
659   for node_result in result:
660     (node_tuple, data_tuple) = node_result
661     (_, node_name) = node_tuple
662     (data_status, data_node) = data_tuple
663     if data_status == constants.RS_NORMAL:
664       data.append([node_name, "%s=%s" % tuple(data_node[0])])
665       for item, status in data_node[1:]:
666         data.append(["", "%s=%s" % (item, status)])
667     else:
668       errs += 1
669       data.append([node_name, cli.FormatResultError(data_status, True)])
670
671   data = GenerateTable(separator=opts.separator, headers=headers,
672                        fields=["node", "status"], data=data)
673
674   for line in data:
675     ToStdout(line)
676
677   if errs:
678     return constants.EXIT_FAILURE
679   else:
680     return constants.EXIT_SUCCESS
681
682
683 def ListVolumes(opts, args):
684   """List logical volumes on node(s).
685
686   @param opts: the command line options selected by the user
687   @type args: list
688   @param args: should either be an empty list, in which case
689       we list data for all nodes, or contain a list of nodes
690       to display data only for those
691   @rtype: int
692   @return: the desired exit code
693
694   """
695   selected_fields = ParseFields(opts.output, _LIST_VOL_DEF_FIELDS)
696
697   op = opcodes.OpNodeQueryvols(nodes=args, output_fields=selected_fields)
698   output = SubmitOpCode(op, opts=opts)
699
700   if not opts.no_headers:
701     headers = {"node": "Node", "phys": "PhysDev",
702                "vg": "VG", "name": "Name",
703                "size": "Size", "instance": "Instance"}
704   else:
705     headers = None
706
707   unitfields = ["size"]
708
709   numfields = ["size"]
710
711   data = GenerateTable(separator=opts.separator, headers=headers,
712                        fields=selected_fields, unitfields=unitfields,
713                        numfields=numfields, data=output, units=opts.units)
714
715   for line in data:
716     ToStdout(line)
717
718   return 0
719
720
721 def ListStorage(opts, args):
722   """List physical volumes on node(s).
723
724   @param opts: the command line options selected by the user
725   @type args: list
726   @param args: should either be an empty list, in which case
727       we list data for all nodes, or contain a list of nodes
728       to display data only for those
729   @rtype: int
730   @return: the desired exit code
731
732   """
733   # TODO: Default to ST_FILE if LVM is disabled on the cluster
734   if opts.user_storage_type is None:
735     opts.user_storage_type = constants.ST_LVM_PV
736
737   storage_type = ConvertStorageType(opts.user_storage_type)
738
739   selected_fields = ParseFields(opts.output, _LIST_STOR_DEF_FIELDS)
740
741   op = opcodes.OpNodeQueryStorage(nodes=args,
742                                   storage_type=storage_type,
743                                   output_fields=selected_fields)
744   output = SubmitOpCode(op, opts=opts)
745
746   if not opts.no_headers:
747     headers = {
748       constants.SF_NODE: "Node",
749       constants.SF_TYPE: "Type",
750       constants.SF_NAME: "Name",
751       constants.SF_SIZE: "Size",
752       constants.SF_USED: "Used",
753       constants.SF_FREE: "Free",
754       constants.SF_ALLOCATABLE: "Allocatable",
755       }
756   else:
757     headers = None
758
759   unitfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
760   numfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
761
762   # change raw values to nicer strings
763   for row in output:
764     for idx, field in enumerate(selected_fields):
765       val = row[idx]
766       if field == constants.SF_ALLOCATABLE:
767         if val:
768           val = "Y"
769         else:
770           val = "N"
771       row[idx] = str(val)
772
773   data = GenerateTable(separator=opts.separator, headers=headers,
774                        fields=selected_fields, unitfields=unitfields,
775                        numfields=numfields, data=output, units=opts.units)
776
777   for line in data:
778     ToStdout(line)
779
780   return 0
781
782
783 def ModifyStorage(opts, args):
784   """Modify storage volume on a node.
785
786   @param opts: the command line options selected by the user
787   @type args: list
788   @param args: should contain 3 items: node name, storage type and volume name
789   @rtype: int
790   @return: the desired exit code
791
792   """
793   (node_name, user_storage_type, volume_name) = args
794
795   storage_type = ConvertStorageType(user_storage_type)
796
797   changes = {}
798
799   if opts.allocatable is not None:
800     changes[constants.SF_ALLOCATABLE] = opts.allocatable
801
802   if changes:
803     op = opcodes.OpNodeModifyStorage(node_name=node_name,
804                                      storage_type=storage_type,
805                                      name=volume_name,
806                                      changes=changes)
807     SubmitOrSend(op, opts)
808   else:
809     ToStderr("No changes to perform, exiting.")
810
811
812 def RepairStorage(opts, args):
813   """Repairs a storage volume on a node.
814
815   @param opts: the command line options selected by the user
816   @type args: list
817   @param args: should contain 3 items: node name, storage type and volume name
818   @rtype: int
819   @return: the desired exit code
820
821   """
822   (node_name, user_storage_type, volume_name) = args
823
824   storage_type = ConvertStorageType(user_storage_type)
825
826   op = opcodes.OpRepairNodeStorage(node_name=node_name,
827                                    storage_type=storage_type,
828                                    name=volume_name,
829                                    ignore_consistency=opts.ignore_consistency)
830   SubmitOrSend(op, opts)
831
832
833 def SetNodeParams(opts, args):
834   """Modifies a node.
835
836   @param opts: the command line options selected by the user
837   @type args: list
838   @param args: should contain only one element, the node name
839   @rtype: int
840   @return: the desired exit code
841
842   """
843   all_changes = [opts.master_candidate, opts.drained, opts.offline,
844                  opts.master_capable, opts.vm_capable, opts.secondary_ip,
845                  opts.ndparams]
846   if (all_changes.count(None) == len(all_changes) and
847       not (opts.hv_state or opts.disk_state)):
848     ToStderr("Please give at least one of the parameters.")
849     return 1
850
851   if opts.disk_state:
852     disk_state = utils.FlatToDict(opts.disk_state)
853   else:
854     disk_state = {}
855
856   hv_state = dict(opts.hv_state)
857
858   op = opcodes.OpNodeSetParams(node_name=args[0],
859                                master_candidate=opts.master_candidate,
860                                offline=opts.offline,
861                                drained=opts.drained,
862                                master_capable=opts.master_capable,
863                                vm_capable=opts.vm_capable,
864                                secondary_ip=opts.secondary_ip,
865                                force=opts.force,
866                                ndparams=opts.ndparams,
867                                auto_promote=opts.auto_promote,
868                                powered=opts.node_powered,
869                                hv_state=hv_state,
870                                disk_state=disk_state)
871
872   # even if here we process the result, we allow submit only
873   result = SubmitOrSend(op, opts)
874
875   if result:
876     ToStdout("Modified node %s", args[0])
877     for param, data in result:
878       ToStdout(" - %-5s -> %s", param, data)
879   return 0
880
881
882 class ReplyStatus(object):
883   """Class holding a reply status for synchronous confd clients.
884
885   """
886   def __init__(self):
887     self.failure = True
888     self.answer = False
889
890
891 def ListDrbd(opts, args):
892   """Modifies a node.
893
894   @param opts: the command line options selected by the user
895   @type args: list
896   @param args: should contain only one element, the node name
897   @rtype: int
898   @return: the desired exit code
899
900   """
901   if len(args) != 1:
902     ToStderr("Please give one (and only one) node.")
903     return constants.EXIT_FAILURE
904
905   if not constants.ENABLE_CONFD:
906     ToStderr("Error: this command requires confd support, but it has not"
907              " been enabled at build time.")
908     return constants.EXIT_FAILURE
909
910   if not constants.HS_CONFD:
911     ToStderr("Error: this command requires the Haskell version of confd,"
912              " but it has not been enabled at build time.")
913     return constants.EXIT_FAILURE
914
915   status = ReplyStatus()
916
917   def ListDrbdConfdCallback(reply):
918     """Callback for confd queries"""
919     if reply.type == confd_client.UPCALL_REPLY:
920       answer = reply.server_reply.answer
921       reqtype = reply.orig_request.type
922       if reqtype == constants.CONFD_REQ_NODE_DRBD:
923         if reply.server_reply.status != constants.CONFD_REPL_STATUS_OK:
924           ToStderr("Query gave non-ok status '%s': %s" %
925                    (reply.server_reply.status,
926                     reply.server_reply.answer))
927           status.failure = True
928           return
929         if not confd.HTNodeDrbd(answer):
930           ToStderr("Invalid response from server: expected %s, got %s",
931                    confd.HTNodeDrbd, answer)
932           status.failure = True
933         else:
934           status.failure = False
935           status.answer = answer
936       else:
937         ToStderr("Unexpected reply %s!?", reqtype)
938         status.failure = True
939
940   node = args[0]
941   hmac = utils.ReadFile(constants.CONFD_HMAC_KEY)
942   filter_callback = confd_client.ConfdFilterCallback(ListDrbdConfdCallback)
943   counting_callback = confd_client.ConfdCountingCallback(filter_callback)
944   cf_client = confd_client.ConfdClient(hmac, [constants.IP4_ADDRESS_LOCALHOST],
945                                        counting_callback)
946   req = confd_client.ConfdClientRequest(type=constants.CONFD_REQ_NODE_DRBD,
947                                         query=node)
948
949   def DoConfdRequestReply(req):
950     counting_callback.RegisterQuery(req.rsalt)
951     cf_client.SendRequest(req, async=False)
952     while not counting_callback.AllAnswered():
953       if not cf_client.ReceiveReply():
954         ToStderr("Did not receive all expected confd replies")
955         break
956
957   DoConfdRequestReply(req)
958
959   if status.failure:
960     return constants.EXIT_FAILURE
961
962   fields = ["node", "minor", "instance", "disk", "role", "peer"]
963   if opts.no_headers:
964     headers = None
965   else:
966     headers = {"node": "Node", "minor": "Minor", "instance": "Instance",
967                "disk": "Disk", "role": "Role", "peer": "PeerNode"}
968
969   data = GenerateTable(separator=opts.separator, headers=headers,
970                        fields=fields, data=sorted(status.answer),
971                        numfields=["minor"])
972   for line in data:
973     ToStdout(line)
974
975   return constants.EXIT_SUCCESS
976
977 commands = {
978   "add": (
979     AddNode, [ArgHost(min=1, max=1)],
980     [SECONDARY_IP_OPT, READD_OPT, NOSSH_KEYCHECK_OPT, NODE_FORCE_JOIN_OPT,
981      NONODE_SETUP_OPT, VERBOSE_OPT, NODEGROUP_OPT, PRIORITY_OPT,
982      CAPAB_MASTER_OPT, CAPAB_VM_OPT, NODE_PARAMS_OPT, HV_STATE_OPT,
983      DISK_STATE_OPT],
984     "[-s ip] [--readd] [--no-ssh-key-check] [--force-join]"
985     " [--no-node-setup] [--verbose]"
986     " <node_name>",
987     "Add a node to the cluster"),
988   "evacuate": (
989     EvacuateNode, ARGS_ONE_NODE,
990     [FORCE_OPT, IALLOCATOR_OPT, NEW_SECONDARY_OPT, EARLY_RELEASE_OPT,
991      PRIORITY_OPT, PRIMARY_ONLY_OPT, SECONDARY_ONLY_OPT, SUBMIT_OPT],
992     "[-f] {-I <iallocator> | -n <dst>} [-p | -s] [options...] <node>",
993     "Relocate the primary and/or secondary instances from a node"),
994   "failover": (
995     FailoverNode, ARGS_ONE_NODE, [FORCE_OPT, IGNORE_CONSIST_OPT,
996                                   IALLOCATOR_OPT, PRIORITY_OPT],
997     "[-f] <node>",
998     "Stops the primary instances on a node and start them on their"
999     " secondary node (only for instances with drbd disk template)"),
1000   "migrate": (
1001     MigrateNode, ARGS_ONE_NODE,
1002     [FORCE_OPT, NONLIVE_OPT, MIGRATION_MODE_OPT, DST_NODE_OPT,
1003      IALLOCATOR_OPT, PRIORITY_OPT, IGNORE_IPOLICY_OPT,
1004      NORUNTIME_CHGS_OPT, SUBMIT_OPT],
1005     "[-f] <node>",
1006     "Migrate all the primary instance on a node away from it"
1007     " (only for instances of type drbd)"),
1008   "info": (
1009     ShowNodeConfig, ARGS_MANY_NODES, [],
1010     "[<node_name>...]", "Show information about the node(s)"),
1011   "list": (
1012     ListNodes, ARGS_MANY_NODES,
1013     [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, VERBOSE_OPT,
1014      FORCE_FILTER_OPT],
1015     "[nodes...]",
1016     "Lists the nodes in the cluster. The available fields can be shown using"
1017     " the \"list-fields\" command (see the man page for details)."
1018     " The default field list is (in order): %s." %
1019     utils.CommaJoin(_LIST_DEF_FIELDS)),
1020   "list-fields": (
1021     ListNodeFields, [ArgUnknown()],
1022     [NOHDR_OPT, SEP_OPT],
1023     "[fields...]",
1024     "Lists all available fields for nodes"),
1025   "modify": (
1026     SetNodeParams, ARGS_ONE_NODE,
1027     [FORCE_OPT, SUBMIT_OPT, MC_OPT, DRAINED_OPT, OFFLINE_OPT,
1028      CAPAB_MASTER_OPT, CAPAB_VM_OPT, SECONDARY_IP_OPT,
1029      AUTO_PROMOTE_OPT, DRY_RUN_OPT, PRIORITY_OPT, NODE_PARAMS_OPT,
1030      NODE_POWERED_OPT, HV_STATE_OPT, DISK_STATE_OPT],
1031     "<node_name>", "Alters the parameters of a node"),
1032   "powercycle": (
1033     PowercycleNode, ARGS_ONE_NODE,
1034     [FORCE_OPT, CONFIRM_OPT, DRY_RUN_OPT, PRIORITY_OPT, SUBMIT_OPT],
1035     "<node_name>", "Tries to forcefully powercycle a node"),
1036   "power": (
1037     PowerNode,
1038     [ArgChoice(min=1, max=1, choices=_LIST_POWER_COMMANDS),
1039      ArgNode()],
1040     [SUBMIT_OPT, AUTO_PROMOTE_OPT, PRIORITY_OPT, IGNORE_STATUS_OPT,
1041      FORCE_OPT, NOHDR_OPT, SEP_OPT, OOB_TIMEOUT_OPT, POWER_DELAY_OPT],
1042     "on|off|cycle|status [nodes...]",
1043     "Change power state of node by calling out-of-band helper."),
1044   "remove": (
1045     RemoveNode, ARGS_ONE_NODE, [DRY_RUN_OPT, PRIORITY_OPT],
1046     "<node_name>", "Removes a node from the cluster"),
1047   "volumes": (
1048     ListVolumes, [ArgNode()],
1049     [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, PRIORITY_OPT],
1050     "[<node_name>...]", "List logical volumes on node(s)"),
1051   "list-storage": (
1052     ListStorage, ARGS_MANY_NODES,
1053     [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, _STORAGE_TYPE_OPT,
1054      PRIORITY_OPT],
1055     "[<node_name>...]", "List physical volumes on node(s). The available"
1056     " fields are (see the man page for details): %s." %
1057     (utils.CommaJoin(_LIST_STOR_HEADERS))),
1058   "modify-storage": (
1059     ModifyStorage,
1060     [ArgNode(min=1, max=1),
1061      ArgChoice(min=1, max=1, choices=_MODIFIABLE_STORAGE_TYPES),
1062      ArgFile(min=1, max=1)],
1063     [ALLOCATABLE_OPT, DRY_RUN_OPT, PRIORITY_OPT, SUBMIT_OPT],
1064     "<node_name> <storage_type> <name>", "Modify storage volume on a node"),
1065   "repair-storage": (
1066     RepairStorage,
1067     [ArgNode(min=1, max=1),
1068      ArgChoice(min=1, max=1, choices=_REPAIRABLE_STORAGE_TYPES),
1069      ArgFile(min=1, max=1)],
1070     [IGNORE_CONSIST_OPT, DRY_RUN_OPT, PRIORITY_OPT, SUBMIT_OPT],
1071     "<node_name> <storage_type> <name>",
1072     "Repairs a storage volume on a node"),
1073   "list-tags": (
1074     ListTags, ARGS_ONE_NODE, [],
1075     "<node_name>", "List the tags of the given node"),
1076   "add-tags": (
1077     AddTags, [ArgNode(min=1, max=1), ArgUnknown()],
1078     [TAG_SRC_OPT, PRIORITY_OPT, SUBMIT_OPT],
1079     "<node_name> tag...", "Add tags to the given node"),
1080   "remove-tags": (
1081     RemoveTags, [ArgNode(min=1, max=1), ArgUnknown()],
1082     [TAG_SRC_OPT, PRIORITY_OPT, SUBMIT_OPT],
1083     "<node_name> tag...", "Remove tags from the given node"),
1084   "health": (
1085     Health, ARGS_MANY_NODES,
1086     [NOHDR_OPT, SEP_OPT, PRIORITY_OPT, OOB_TIMEOUT_OPT],
1087     "[<node_name>...]", "List health of node(s) using out-of-band"),
1088   "list-drbd": (
1089     ListDrbd, ARGS_ONE_NODE,
1090     [NOHDR_OPT, SEP_OPT],
1091     "[<node_name>]", "Query the list of used DRBD minors on the given node"),
1092   }
1093
1094 #: dictionary with aliases for commands
1095 aliases = {
1096   "show": "info",
1097   }
1098
1099
1100 def Main():
1101   return GenericMain(commands, aliases=aliases,
1102                      override={"tag_type": constants.TAG_NODE},
1103                      env_override=_ENV_OVERRIDE)