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