Change indentation style in commands definitions
[ganeti-local] / scripts / gnt-node
1 #!/usr/bin/python
2 #
3
4 # Copyright (C) 2006, 2007, 2008 Google Inc.
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 # General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19 # 02110-1301, USA.
20
21
22 # pylint: disable-msg=W0401,W0614
23 # W0401: Wildcard import ganeti.cli
24 # W0614: Unused import %s from wildcard import (since we need cli)
25
26 import sys
27
28 from ganeti.cli import *
29 from ganeti import cli
30 from ganeti import opcodes
31 from ganeti import utils
32 from ganeti import constants
33 from ganeti import errors
34 from ganeti import bootstrap
35
36
37 #: default list of field for L{ListNodes}
38 _LIST_DEF_FIELDS = [
39   "name", "dtotal", "dfree",
40   "mtotal", "mnode", "mfree",
41   "pinst_cnt", "sinst_cnt",
42   ]
43
44 #: headers (and full field list for L{ListNodes}
45 _LIST_HEADERS = {
46   "name": "Node", "pinst_cnt": "Pinst", "sinst_cnt": "Sinst",
47   "pinst_list": "PriInstances", "sinst_list": "SecInstances",
48   "pip": "PrimaryIP", "sip": "SecondaryIP",
49   "dtotal": "DTotal", "dfree": "DFree",
50   "mtotal": "MTotal", "mnode": "MNode", "mfree": "MFree",
51   "bootid": "BootID",
52   "ctotal": "CTotal", "cnodes": "CNodes", "csockets": "CSockets",
53   "tags": "Tags",
54   "serial_no": "SerialNo",
55   "master_candidate": "MasterC",
56   "master": "IsMaster",
57   "offline": "Offline", "drained": "Drained",
58   "role": "Role",
59   "ctime": "CTime", "mtime": "MTime",
60   }
61
62 #: User-facing storage unit types
63 _USER_STORAGE_TYPE = {
64   constants.ST_FILE: "file",
65   constants.ST_LVM_PV: "lvm-pv",
66   constants.ST_LVM_VG: "lvm-vg",
67   }
68
69 _STORAGE_TYPE_OPT = \
70   cli_option("--storage-type",
71              dest="user_storage_type",
72              choices=_USER_STORAGE_TYPE.keys(),
73              default=None,
74              metavar="STORAGE_TYPE",
75              help=("Storage type (%s)" %
76                    utils.CommaJoin(_USER_STORAGE_TYPE.keys())))
77
78 _REPAIRABLE_STORAGE_TYPES = \
79   [st for st, so in constants.VALID_STORAGE_OPERATIONS.iteritems()
80    if constants.SO_FIX_CONSISTENCY in so]
81
82 _MODIFIABLE_STORAGE_TYPES = constants.MODIFIABLE_STORAGE_FIELDS.keys()
83
84
85 def ConvertStorageType(user_storage_type):
86   """Converts a user storage type to its internal name.
87
88   """
89   try:
90     return _USER_STORAGE_TYPE[user_storage_type]
91   except KeyError:
92     raise errors.OpPrereqError("Unknown storage type: %s" % user_storage_type)
93
94
95 @UsesRPC
96 def AddNode(opts, args):
97   """Add a node to the cluster.
98
99   @param opts: the command line options selected by the user
100   @type args: list
101   @param args: should contain only one element, the new node name
102   @rtype: int
103   @return: the desired exit code
104
105   """
106   cl = GetClient()
107   dns_data = utils.HostInfo(args[0])
108   node = dns_data.name
109   readd = opts.readd
110
111   try:
112     output = cl.QueryNodes(names=[node], fields=['name', 'sip'],
113                            use_locking=False)
114     node_exists, sip = output[0]
115   except (errors.OpPrereqError, errors.OpExecError):
116     node_exists = ""
117     sip = None
118
119   if readd:
120     if not node_exists:
121       ToStderr("Node %s not in the cluster"
122                " - please retry without '--readd'", node)
123       return 1
124   else:
125     if node_exists:
126       ToStderr("Node %s already in the cluster (as %s)"
127                " - please retry with '--readd'", node, node_exists)
128       return 1
129     sip = opts.secondary_ip
130
131   # read the cluster name from the master
132   output = cl.QueryConfigValues(['cluster_name'])
133   cluster_name = output[0]
134
135   if not readd:
136     ToStderr("-- WARNING -- \n"
137              "Performing this operation is going to replace the ssh daemon"
138              " keypair\n"
139              "on the target machine (%s) with the ones of the"
140              " current one\n"
141              "and grant full intra-cluster ssh root access to/from it\n", node)
142
143   bootstrap.SetupNodeDaemon(cluster_name, node, opts.ssh_key_check)
144
145   op = opcodes.OpAddNode(node_name=args[0], secondary_ip=sip,
146                          readd=opts.readd)
147   SubmitOpCode(op)
148
149
150 def ListNodes(opts, args):
151   """List nodes and their properties.
152
153   @param opts: the command line options selected by the user
154   @type args: list
155   @param args: should be an empty list
156   @rtype: int
157   @return: the desired exit code
158
159   """
160   if opts.output is None:
161     selected_fields = _LIST_DEF_FIELDS
162   elif opts.output.startswith("+"):
163     selected_fields = _LIST_DEF_FIELDS + opts.output[1:].split(",")
164   else:
165     selected_fields = opts.output.split(",")
166
167   output = GetClient().QueryNodes(args, selected_fields, opts.do_locking)
168
169   if not opts.no_headers:
170     headers = _LIST_HEADERS
171   else:
172     headers = None
173
174   unitfields = ["dtotal", "dfree", "mtotal", "mnode", "mfree"]
175
176   numfields = ["dtotal", "dfree",
177                "mtotal", "mnode", "mfree",
178                "pinst_cnt", "sinst_cnt",
179                "ctotal", "serial_no"]
180
181   list_type_fields = ("pinst_list", "sinst_list", "tags")
182   # change raw values to nicer strings
183   for row in output:
184     for idx, field in enumerate(selected_fields):
185       val = row[idx]
186       if field in list_type_fields:
187         val = ",".join(val)
188       elif field in ('master', 'master_candidate', 'offline', 'drained'):
189         if val:
190           val = 'Y'
191         else:
192           val = 'N'
193       elif field == "ctime" or field == "mtime":
194         val = utils.FormatTime(val)
195       elif val is None:
196         val = "?"
197       row[idx] = str(val)
198
199   data = GenerateTable(separator=opts.separator, headers=headers,
200                        fields=selected_fields, unitfields=unitfields,
201                        numfields=numfields, data=output, units=opts.units)
202   for line in data:
203     ToStdout(line)
204
205   return 0
206
207
208 def EvacuateNode(opts, args):
209   """Relocate all secondary instance from a node.
210
211   @param opts: the command line options selected by the user
212   @type args: list
213   @param args: should be an empty list
214   @rtype: int
215   @return: the desired exit code
216
217   """
218   cl = GetClient()
219   force = opts.force
220
221   dst_node = opts.dst_node
222   iallocator = opts.iallocator
223
224   cnt = [dst_node, iallocator].count(None)
225   if cnt != 1:
226     raise errors.OpPrereqError("One and only one of the -n and -I"
227                                " options must be passed")
228
229   selected_fields = ["name", "sinst_list"]
230   src_node = args[0]
231
232   result = cl.QueryNodes(names=[src_node], fields=selected_fields,
233                          use_locking=False)
234   src_node, sinst = result[0]
235
236   if not sinst:
237     ToStderr("No secondary instances on node %s, exiting.", src_node)
238     return constants.EXIT_SUCCESS
239
240   if dst_node is not None:
241     result = cl.QueryNodes(names=[dst_node], fields=["name"],
242                            use_locking=False)
243     dst_node = result[0][0]
244
245     if src_node == dst_node:
246       raise errors.OpPrereqError("Evacuate node needs different source and"
247                                  " target nodes (node %s given twice)" %
248                                  src_node)
249     txt_msg = "to node %s" % dst_node
250   else:
251     txt_msg = "using iallocator %s" % iallocator
252
253   sinst = utils.NiceSort(sinst)
254
255   if not force and not AskUser("Relocate instance(s) %s from node\n"
256                                " %s %s?" %
257                                (",".join("'%s'" % name for name in sinst),
258                                src_node, txt_msg)):
259     return constants.EXIT_CONFIRMATION
260
261   op = opcodes.OpEvacuateNode(node_name=args[0], remote_node=dst_node,
262                               iallocator=iallocator)
263   SubmitOpCode(op, cl=cl)
264
265
266 def FailoverNode(opts, args):
267   """Failover all primary instance on a node.
268
269   @param opts: the command line options selected by the user
270   @type args: list
271   @param args: should be an empty list
272   @rtype: int
273   @return: the desired exit code
274
275   """
276   cl = GetClient()
277   force = opts.force
278   selected_fields = ["name", "pinst_list"]
279
280   # these fields are static data anyway, so it doesn't matter, but
281   # locking=True should be safer
282   result = cl.QueryNodes(names=args, fields=selected_fields,
283                          use_locking=False)
284   node, pinst = result[0]
285
286   if not pinst:
287     ToStderr("No primary instances on node %s, exiting.", node)
288     return 0
289
290   pinst = utils.NiceSort(pinst)
291
292   retcode = 0
293
294   if not force and not AskUser("Fail over instance(s) %s?" %
295                                (",".join("'%s'" % name for name in pinst))):
296     return 2
297
298   jex = JobExecutor(cl=cl)
299   for iname in pinst:
300     op = opcodes.OpFailoverInstance(instance_name=iname,
301                                     ignore_consistency=opts.ignore_consistency)
302     jex.QueueJob(iname, op)
303   results = jex.GetResults()
304   bad_cnt = len([row for row in results if not row[0]])
305   if bad_cnt == 0:
306     ToStdout("All %d instance(s) failed over successfully.", len(results))
307   else:
308     ToStdout("There were errors during the failover:\n"
309              "%d error(s) out of %d instance(s).", bad_cnt, len(results))
310   return retcode
311
312
313 def MigrateNode(opts, args):
314   """Migrate all primary instance on a node.
315
316   """
317   cl = GetClient()
318   force = opts.force
319   selected_fields = ["name", "pinst_list"]
320
321   result = cl.QueryNodes(names=args, fields=selected_fields, use_locking=False)
322   node, pinst = result[0]
323
324   if not pinst:
325     ToStdout("No primary instances on node %s, exiting." % node)
326     return 0
327
328   pinst = utils.NiceSort(pinst)
329
330   retcode = 0
331
332   if not force and not AskUser("Migrate instance(s) %s?" %
333                                (",".join("'%s'" % name for name in pinst))):
334     return 2
335
336   op = opcodes.OpMigrateNode(node_name=args[0], live=opts.live)
337   SubmitOpCode(op, cl=cl)
338
339
340 def ShowNodeConfig(opts, args):
341   """Show node information.
342
343   @param opts: the command line options selected by the user
344   @type args: list
345   @param args: should either be an empty list, in which case
346       we show information about all nodes, or should contain
347       a list of nodes to be queried for information
348   @rtype: int
349   @return: the desired exit code
350
351   """
352   cl = GetClient()
353   result = cl.QueryNodes(fields=["name", "pip", "sip",
354                                  "pinst_list", "sinst_list",
355                                  "master_candidate", "drained", "offline"],
356                          names=args, use_locking=False)
357
358   for (name, primary_ip, secondary_ip, pinst, sinst,
359        is_mc, drained, offline) in result:
360     ToStdout("Node name: %s", name)
361     ToStdout("  primary ip: %s", primary_ip)
362     ToStdout("  secondary ip: %s", secondary_ip)
363     ToStdout("  master candidate: %s", is_mc)
364     ToStdout("  drained: %s", drained)
365     ToStdout("  offline: %s", offline)
366     if pinst:
367       ToStdout("  primary for instances:")
368       for iname in utils.NiceSort(pinst):
369         ToStdout("    - %s", iname)
370     else:
371       ToStdout("  primary for no instances")
372     if sinst:
373       ToStdout("  secondary for instances:")
374       for iname in utils.NiceSort(sinst):
375         ToStdout("    - %s", iname)
376     else:
377       ToStdout("  secondary for no instances")
378
379   return 0
380
381
382 def RemoveNode(opts, args):
383   """Remove a node from the cluster.
384
385   @param opts: the command line options selected by the user
386   @type args: list
387   @param args: should contain only one element, the name of
388       the node to be removed
389   @rtype: int
390   @return: the desired exit code
391
392   """
393   op = opcodes.OpRemoveNode(node_name=args[0])
394   SubmitOpCode(op)
395   return 0
396
397
398 def PowercycleNode(opts, args):
399   """Remove a node from the cluster.
400
401   @param opts: the command line options selected by the user
402   @type args: list
403   @param args: should contain only one element, the name of
404       the node to be removed
405   @rtype: int
406   @return: the desired exit code
407
408   """
409   node = args[0]
410   if (not opts.confirm and
411       not AskUser("Are you sure you want to hard powercycle node %s?" % node)):
412     return 2
413
414   op = opcodes.OpPowercycleNode(node_name=node, force=opts.force)
415   result = SubmitOpCode(op)
416   ToStderr(result)
417   return 0
418
419
420 def ListVolumes(opts, args):
421   """List logical volumes on node(s).
422
423   @param opts: the command line options selected by the user
424   @type args: list
425   @param args: should either be an empty list, in which case
426       we list data for all nodes, or contain a list of nodes
427       to display data only for those
428   @rtype: int
429   @return: the desired exit code
430
431   """
432   if opts.output is None:
433     selected_fields = ["node", "phys", "vg",
434                        "name", "size", "instance"]
435   else:
436     selected_fields = opts.output.split(",")
437
438   op = opcodes.OpQueryNodeVolumes(nodes=args, output_fields=selected_fields)
439   output = SubmitOpCode(op)
440
441   if not opts.no_headers:
442     headers = {"node": "Node", "phys": "PhysDev",
443                "vg": "VG", "name": "Name",
444                "size": "Size", "instance": "Instance"}
445   else:
446     headers = None
447
448   unitfields = ["size"]
449
450   numfields = ["size"]
451
452   data = GenerateTable(separator=opts.separator, headers=headers,
453                        fields=selected_fields, unitfields=unitfields,
454                        numfields=numfields, data=output, units=opts.units)
455
456   for line in data:
457     ToStdout(line)
458
459   return 0
460
461
462 def ListPhysicalVolumes(opts, args):
463   """List physical volumes on node(s).
464
465   @param opts: the command line options selected by the user
466   @type args: list
467   @param args: should either be an empty list, in which case
468       we list data for all nodes, or contain a list of nodes
469       to display data only for those
470   @rtype: int
471   @return: the desired exit code
472
473   """
474   # TODO: Default to ST_FILE if LVM is disabled on the cluster
475   if opts.user_storage_type is None:
476     opts.user_storage_type = constants.ST_LVM_PV
477
478   storage_type = ConvertStorageType(opts.user_storage_type)
479
480   default_fields = {
481     constants.ST_FILE: [
482       constants.SF_NAME,
483       constants.SF_USED,
484       constants.SF_FREE,
485       ],
486     constants.ST_LVM_PV: [
487       constants.SF_NAME,
488       constants.SF_SIZE,
489       constants.SF_USED,
490       constants.SF_FREE,
491       ],
492     constants.ST_LVM_VG: [
493       constants.SF_NAME,
494       constants.SF_SIZE,
495       ],
496   }
497
498   if opts.output is None:
499     selected_fields = ["node"]
500     selected_fields.extend(default_fields[storage_type])
501   else:
502     selected_fields = opts.output.split(",")
503
504   op = opcodes.OpQueryNodeStorage(nodes=args,
505                                   storage_type=storage_type,
506                                   output_fields=selected_fields)
507   output = SubmitOpCode(op)
508
509   if not opts.no_headers:
510     headers = {
511       "node": "Node",
512       constants.SF_NAME: "Name",
513       constants.SF_SIZE: "Size",
514       constants.SF_USED: "Used",
515       constants.SF_FREE: "Free",
516       constants.SF_ALLOCATABLE: "Allocatable",
517       }
518   else:
519     headers = None
520
521   unitfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
522   numfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
523
524   data = GenerateTable(separator=opts.separator, headers=headers,
525                        fields=selected_fields, unitfields=unitfields,
526                        numfields=numfields, data=output, units=opts.units)
527
528   for line in data:
529     ToStdout(line)
530
531   return 0
532
533
534 def ModifyVolume(opts, args):
535   """Modify storage volume on a node.
536
537   @param opts: the command line options selected by the user
538   @type args: list
539   @param args: should contain 3 items: node name, storage type and volume name
540   @rtype: int
541   @return: the desired exit code
542
543   """
544   (node_name, user_storage_type, volume_name) = args
545
546   storage_type = ConvertStorageType(user_storage_type)
547
548   changes = {}
549
550   if opts.allocatable is not None:
551     changes[constants.SF_ALLOCATABLE] = (opts.allocatable == "yes")
552
553   if changes:
554     op = opcodes.OpModifyNodeStorage(node_name=node_name,
555                                      storage_type=storage_type,
556                                      name=volume_name,
557                                      changes=changes)
558     SubmitOpCode(op)
559
560
561 def RepairVolume(opts, args):
562   """Repairs a storage volume on a node.
563
564   @param opts: the command line options selected by the user
565   @type args: list
566   @param args: should contain 3 items: node name, storage type and volume name
567   @rtype: int
568   @return: the desired exit code
569
570   """
571   (node_name, user_storage_type, volume_name) = args
572
573   storage_type = ConvertStorageType(user_storage_type)
574
575   op = opcodes.OpRepairNodeStorage(node_name=node_name,
576                                    storage_type=storage_type,
577                                    name=volume_name)
578   SubmitOpCode(op)
579
580
581 def SetNodeParams(opts, args):
582   """Modifies a node.
583
584   @param opts: the command line options selected by the user
585   @type args: list
586   @param args: should contain only one element, the node name
587   @rtype: int
588   @return: the desired exit code
589
590   """
591   if [opts.master_candidate, opts.drained, opts.offline].count(None) == 3:
592     ToStderr("Please give at least one of the parameters.")
593     return 1
594
595   if opts.master_candidate is not None:
596     candidate = opts.master_candidate == 'yes'
597   else:
598     candidate = None
599   if opts.offline is not None:
600     offline = opts.offline == 'yes'
601   else:
602     offline = None
603
604   if opts.drained is not None:
605     drained = opts.drained == 'yes'
606   else:
607     drained = None
608   op = opcodes.OpSetNodeParams(node_name=args[0],
609                                master_candidate=candidate,
610                                offline=offline,
611                                drained=drained,
612                                force=opts.force)
613
614   # even if here we process the result, we allow submit only
615   result = SubmitOrSend(op, opts)
616
617   if result:
618     ToStdout("Modified node %s", args[0])
619     for param, data in result:
620       ToStdout(" - %-5s -> %s", param, data)
621   return 0
622
623
624 commands = {
625   'add': (
626     AddNode, [ArgHost(min=1, max=1)],
627     [DEBUG_OPT, SECONDARY_IP_OPT, READD_OPT, NOSSH_KEYCHECK_OPT],
628     "[-s ip] [--readd] [--no-ssh-key-check] <node_name>",
629     "Add a node to the cluster"),
630   'evacuate': (
631     EvacuateNode, ARGS_ONE_NODE,
632     [DEBUG_OPT, FORCE_OPT, IALLOCATOR_OPT, NEW_SECONDARY_OPT],
633     "[-f] {-I <iallocator> | -n <dst>} <node>",
634     "Relocate the secondary instances from a node"
635     " to other nodes (only for instances with drbd disk template)"),
636   'failover': (
637     FailoverNode, ARGS_ONE_NODE,
638     [DEBUG_OPT, FORCE_OPT, IGNORE_CONSIST_OPT],
639     "[-f] <node>",
640     "Stops the primary instances on a node and start them on their"
641     " secondary node (only for instances with drbd disk template)"),
642   'migrate': (
643     MigrateNode, ARGS_ONE_NODE,
644     [DEBUG_OPT, FORCE_OPT, NONLIVE_OPT],
645     "[-f] <node>",
646     "Migrate all the primary instance on a node away from it"
647     " (only for instances of type drbd)"),
648   'info': (
649     ShowNodeConfig, ARGS_MANY_NODES, [DEBUG_OPT],
650     "[<node_name>...]", "Show information about the node(s)"),
651   'list': (
652     ListNodes, ARGS_MANY_NODES,
653     [DEBUG_OPT, NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, SYNC_OPT],
654     "[nodes...]",
655     "Lists the nodes in the cluster. The available fields are (see the man"
656     " page for details): %s. The default field list is (in order): %s." %
657     (", ".join(_LIST_HEADERS), ", ".join(_LIST_DEF_FIELDS))),
658   'modify': (
659     SetNodeParams, ARGS_ONE_NODE,
660     [DEBUG_OPT, FORCE_OPT, SUBMIT_OPT, MC_OPT, DRAINED_OPT, OFFLINE_OPT],
661     "<node_name>", "Alters the parameters of a node"),
662   'powercycle': (
663     PowercycleNode, ARGS_ONE_NODE,
664     [DEBUG_OPT, FORCE_OPT, CONFIRM_OPT],
665     "<node_name>", "Tries to forcefully powercycle a node"),
666   'remove': (
667     RemoveNode, ARGS_ONE_NODE, [DEBUG_OPT],
668     "<node_name>", "Removes a node from the cluster"),
669   'volumes': (
670     ListVolumes, [ArgNode()],
671     [DEBUG_OPT, NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT],
672     "[<node_name>...]", "List logical volumes on node(s)"),
673   'physical-volumes': (
674     ListPhysicalVolumes, ARGS_MANY_NODES,
675     [DEBUG_OPT, NOHDR_OPT, SEP_OPT, USEUNITS_OPT,
676      FIELDS_OPT, _STORAGE_TYPE_OPT],
677     "[<node_name>...]",
678     "List physical volumes on node(s)"),
679   'modify-volume': (
680     ModifyVolume,
681     [ArgNode(min=1, max=1),
682      ArgChoice(min=1, max=1, choices=_MODIFIABLE_STORAGE_TYPES),
683      ArgFile(min=1, max=1)],
684     [DEBUG_OPT, ALLOCATABLE_OPT],
685     "<node_name> <storage_type> <name>",
686     "Modify storage volume on a node"),
687   'repair-volume': (
688     RepairVolume,
689     [ArgNode(min=1, max=1),
690      ArgChoice(min=1, max=1, choices=_REPAIRABLE_STORAGE_TYPES),
691      ArgFile(min=1, max=1)],
692     [DEBUG_OPT],
693     "<node_name> <storage_type> <name>",
694     "Repairs a storage volume on a node"),
695   'list-tags': (
696     ListTags, ARGS_ONE_NODE, [DEBUG_OPT],
697     "<node_name>", "List the tags of the given node"),
698   'add-tags': (
699     AddTags, [ArgNode(min=1, max=1), ArgUnknown()],
700     [DEBUG_OPT, TAG_SRC_OPT],
701     "<node_name> tag...", "Add tags to the given node"),
702   'remove-tags': (
703     RemoveTags, [ArgNode(min=1, max=1), ArgUnknown()],
704     [DEBUG_OPT, TAG_SRC_OPT],
705     "<node_name> tag...", "Remove tags from the given node"),
706   }
707
708
709 if __name__ == '__main__':
710   sys.exit(GenericMain(commands, override={"tag_type": constants.TAG_NODE}))