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