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