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