Modify cli.JobExecutor to use SubmitManyJobs
[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 from optparse import make_option
28
29 from ganeti.cli import *
30 from ganeti import cli
31 from ganeti import opcodes
32 from ganeti import utils
33 from ganeti import constants
34 from ganeti import errors
35 from ganeti import bootstrap
36
37
38 #: default list of field for L{ListNodes}
39 _LIST_DEF_FIELDS = [
40   "name", "dtotal", "dfree",
41   "mtotal", "mnode", "mfree",
42   "pinst_cnt", "sinst_cnt",
43   ]
44
45 #: headers (and full field list for L{ListNodes}
46 _LIST_HEADERS = {
47   "name": "Node", "pinst_cnt": "Pinst", "sinst_cnt": "Sinst",
48   "pinst_list": "PriInstances", "sinst_list": "SecInstances",
49   "pip": "PrimaryIP", "sip": "SecondaryIP",
50   "dtotal": "DTotal", "dfree": "DFree",
51   "mtotal": "MTotal", "mnode": "MNode", "mfree": "MFree",
52   "bootid": "BootID",
53   "ctotal": "CTotal", "cnodes": "CNodes", "csockets": "CSockets",
54   "tags": "Tags",
55   "serial_no": "SerialNo",
56   "master_candidate": "MasterC",
57   "master": "IsMaster",
58   "offline": "Offline", "drained": "Drained",
59   }
60
61
62 @UsesRPC
63 def AddNode(opts, args):
64   """Add a node to the cluster.
65
66   @param opts: the command line options selected by the user
67   @type args: list
68   @param args: should contain only one element, the new node name
69   @rtype: int
70   @return: the desired exit code
71
72   """
73   cl = GetClient()
74   dns_data = utils.HostInfo(args[0])
75   node = dns_data.name
76   readd = opts.readd
77
78   try:
79     output = cl.QueryNodes(names=[node], fields=['name', 'sip'],
80                            use_locking=False)
81     node_exists, sip = output[0]
82   except (errors.OpPrereqError, errors.OpExecError):
83     node_exists = ""
84     sip = None
85
86   if readd:
87     if not node_exists:
88       ToStderr("Node %s not in the cluster"
89                " - please retry without '--readd'", node)
90       return 1
91   else:
92     if node_exists:
93       ToStderr("Node %s already in the cluster (as %s)"
94                " - please retry with '--readd'", node, node_exists)
95       return 1
96     sip = opts.secondary_ip
97
98   # read the cluster name from the master
99   output = cl.QueryConfigValues(['cluster_name'])
100   cluster_name = output[0]
101
102   if readd:
103     # clear the offline and drain flags on the node
104     ToStdout("Resetting the 'offline' and 'drained' flags due to re-add")
105     op = opcodes.OpSetNodeParams(node_name=node, force=True,
106                                  offline=False, drained=False)
107
108     result = SubmitOpCode(op, cl=cl)
109     if result:
110       ToStdout("Modified:")
111       for param, data in result:
112         ToStdout(" - %-5s -> %s", param, data)
113   else:
114     ToStderr("-- WARNING -- \n"
115              "Performing this operation is going to replace the ssh daemon"
116              " keypair\n"
117              "on the target machine (%s) with the ones of the"
118              " current one\n"
119              "and grant full intra-cluster ssh root access to/from it\n", node)
120
121   bootstrap.SetupNodeDaemon(cluster_name, node, opts.ssh_key_check)
122
123   op = opcodes.OpAddNode(node_name=args[0], secondary_ip=sip,
124                          readd=opts.readd)
125   SubmitOpCode(op)
126
127
128 def ListNodes(opts, args):
129   """List nodes and their properties.
130
131   @param opts: the command line options selected by the user
132   @type args: list
133   @param args: should be an empty list
134   @rtype: int
135   @return: the desired exit code
136
137   """
138   if opts.output is None:
139     selected_fields = _LIST_DEF_FIELDS
140   elif opts.output.startswith("+"):
141     selected_fields = _LIST_DEF_FIELDS + opts.output[1:].split(",")
142   else:
143     selected_fields = opts.output.split(",")
144
145   output = GetClient().QueryNodes(args, selected_fields, opts.do_locking)
146
147   if not opts.no_headers:
148     headers = _LIST_HEADERS
149   else:
150     headers = None
151
152   unitfields = ["dtotal", "dfree", "mtotal", "mnode", "mfree"]
153
154   numfields = ["dtotal", "dfree",
155                "mtotal", "mnode", "mfree",
156                "pinst_cnt", "sinst_cnt",
157                "ctotal", "serial_no"]
158
159   list_type_fields = ("pinst_list", "sinst_list", "tags")
160   # change raw values to nicer strings
161   for row in output:
162     for idx, field in enumerate(selected_fields):
163       val = row[idx]
164       if field in list_type_fields:
165         val = ",".join(val)
166       elif field in ('master', 'master_candidate', 'offline', 'drained'):
167         if val:
168           val = 'Y'
169         else:
170           val = 'N'
171       elif val is None:
172         val = "?"
173       row[idx] = str(val)
174
175   data = GenerateTable(separator=opts.separator, headers=headers,
176                        fields=selected_fields, unitfields=unitfields,
177                        numfields=numfields, data=output, units=opts.units)
178   for line in data:
179     ToStdout(line)
180
181   return 0
182
183
184 def EvacuateNode(opts, args):
185   """Relocate all secondary instance from a node.
186
187   @param opts: the command line options selected by the user
188   @type args: list
189   @param args: should be an empty list
190   @rtype: int
191   @return: the desired exit code
192
193   """
194   cl = GetClient()
195   force = opts.force
196
197   dst_node = opts.dst_node
198   iallocator = opts.iallocator
199
200   cnt = [dst_node, iallocator].count(None)
201   if cnt != 1:
202     raise errors.OpPrereqError("One and only one of the -n and -i"
203                                " options must be passed")
204
205   selected_fields = ["name", "sinst_list"]
206   src_node = args[0]
207
208   result = cl.QueryNodes(names=[src_node], fields=selected_fields,
209                          use_locking=False)
210   src_node, sinst = result[0]
211
212   if not sinst:
213     ToStderr("No secondary instances on node %s, exiting.", src_node)
214     return constants.EXIT_SUCCESS
215
216   if dst_node is not None:
217     result = cl.QueryNodes(names=[dst_node], fields=["name"],
218                            use_locking=False)
219     dst_node = result[0][0]
220
221     if src_node == dst_node:
222       raise errors.OpPrereqError("Evacuate node needs different source and"
223                                  " target nodes (node %s given twice)" %
224                                  src_node)
225     txt_msg = "to node %s" % dst_node
226   else:
227     txt_msg = "using iallocator %s" % iallocator
228
229   sinst = utils.NiceSort(sinst)
230
231   if not force and not AskUser("Relocate instance(s) %s from node\n"
232                                " %s %s?" %
233                                (",".join("'%s'" % name for name in sinst),
234                                src_node, txt_msg)):
235     return constants.EXIT_CONFIRMATION
236
237   ops = []
238   for iname in sinst:
239     op = opcodes.OpReplaceDisks(instance_name=iname,
240                                 remote_node=dst_node,
241                                 mode=constants.REPLACE_DISK_CHG,
242                                 iallocator=iallocator,
243                                 disks=[])
244     ops.append(op)
245
246   job_id = cli.SendJob(ops, cl=cl)
247   cli.PollJob(job_id, cl=cl)
248
249
250 def FailoverNode(opts, args):
251   """Failover all primary instance on a node.
252
253   @param opts: the command line options selected by the user
254   @type args: list
255   @param args: should be an empty list
256   @rtype: int
257   @return: the desired exit code
258
259   """
260   cl = GetClient()
261   force = opts.force
262   selected_fields = ["name", "pinst_list"]
263
264   # these fields are static data anyway, so it doesn't matter, but
265   # locking=True should be safer
266   result = cl.QueryNodes(names=args, fields=selected_fields,
267                          use_locking=False)
268   node, pinst = result[0]
269
270   if not pinst:
271     ToStderr("No primary instances on node %s, exiting.", node)
272     return 0
273
274   pinst = utils.NiceSort(pinst)
275
276   retcode = 0
277
278   if not force and not AskUser("Fail over instance(s) %s?" %
279                                (",".join("'%s'" % name for name in pinst))):
280     return 2
281
282   jex = JobExecutor(cl=cl)
283   for iname in pinst:
284     op = opcodes.OpFailoverInstance(instance_name=iname,
285                                     ignore_consistency=opts.ignore_consistency)
286     jex.QueueJob(iname, op)
287   results = jex.GetResults()
288   bad_cnt = len([row for row in results if not row[0]])
289   if bad_cnt == 0:
290     ToStdout("All %d instance(s) failed over successfully.", len(results))
291   else:
292     ToStdout("There were errors during the failover:\n"
293              "%d error(s) out of %d instance(s).", bad_cnt, len(results))
294   return retcode
295
296
297 def MigrateNode(opts, args):
298   """Migrate all primary instance on a node.
299
300   """
301   cl = GetClient()
302   force = opts.force
303   selected_fields = ["name", "pinst_list"]
304
305   result = cl.QueryNodes(names=args, fields=selected_fields, use_locking=False)
306   node, pinst = result[0]
307
308   if not pinst:
309     ToStdout("No primary instances on node %s, exiting." % node)
310     return 0
311
312   pinst = utils.NiceSort(pinst)
313
314   retcode = 0
315
316   if not force and not AskUser("Migrate instance(s) %s?" %
317                                (",".join("'%s'" % name for name in pinst))):
318     return 2
319
320   jex = JobExecutor(cl=cl)
321   for iname in pinst:
322     op = opcodes.OpMigrateInstance(instance_name=iname, live=opts.live,
323                                    cleanup=False)
324     jex.QueueJob(iname, op)
325
326   results = jex.GetResults()
327   bad_cnt = len([row for row in results if not row[0]])
328   if bad_cnt == 0:
329     ToStdout("All %d instance(s) migrated successfully.", len(results))
330   else:
331     ToStdout("There were errors during the migration:\n"
332              "%d error(s) out of %d instance(s).", bad_cnt, len(results))
333   return retcode
334
335
336 def ShowNodeConfig(opts, args):
337   """Show node information.
338
339   @param opts: the command line options selected by the user
340   @type args: list
341   @param args: should either be an empty list, in which case
342       we show information about all nodes, or should contain
343       a list of nodes to be queried for information
344   @rtype: int
345   @return: the desired exit code
346
347   """
348   cl = GetClient()
349   result = cl.QueryNodes(fields=["name", "pip", "sip",
350                                  "pinst_list", "sinst_list",
351                                  "master_candidate", "drained", "offline"],
352                          names=args, use_locking=False)
353
354   for (name, primary_ip, secondary_ip, pinst, sinst,
355        is_mc, drained, offline) in result:
356     ToStdout("Node name: %s", name)
357     ToStdout("  primary ip: %s", primary_ip)
358     ToStdout("  secondary ip: %s", secondary_ip)
359     ToStdout("  master candidate: %s", is_mc)
360     ToStdout("  drained: %s", drained)
361     ToStdout("  offline: %s", offline)
362     if pinst:
363       ToStdout("  primary for instances:")
364       for iname in utils.NiceSort(pinst):
365         ToStdout("    - %s", iname)
366     else:
367       ToStdout("  primary for no instances")
368     if sinst:
369       ToStdout("  secondary for instances:")
370       for iname in utils.NiceSort(sinst):
371         ToStdout("    - %s", iname)
372     else:
373       ToStdout("  secondary for no instances")
374
375   return 0
376
377
378 def RemoveNode(opts, args):
379   """Remove a node from the cluster.
380
381   @param opts: the command line options selected by the user
382   @type args: list
383   @param args: should contain only one element, the name of
384       the node to be removed
385   @rtype: int
386   @return: the desired exit code
387
388   """
389   op = opcodes.OpRemoveNode(node_name=args[0])
390   SubmitOpCode(op)
391   return 0
392
393
394 def ListVolumes(opts, args):
395   """List logical volumes on node(s).
396
397   @param opts: the command line options selected by the user
398   @type args: list
399   @param args: should either be an empty list, in which case
400       we list data for all nodes, or contain a list of nodes
401       to display data only for those
402   @rtype: int
403   @return: the desired exit code
404
405   """
406   if opts.output is None:
407     selected_fields = ["node", "phys", "vg",
408                        "name", "size", "instance"]
409   else:
410     selected_fields = opts.output.split(",")
411
412   op = opcodes.OpQueryNodeVolumes(nodes=args, output_fields=selected_fields)
413   output = SubmitOpCode(op)
414
415   if not opts.no_headers:
416     headers = {"node": "Node", "phys": "PhysDev",
417                "vg": "VG", "name": "Name",
418                "size": "Size", "instance": "Instance"}
419   else:
420     headers = None
421
422   unitfields = ["size"]
423
424   numfields = ["size"]
425
426   data = GenerateTable(separator=opts.separator, headers=headers,
427                        fields=selected_fields, unitfields=unitfields,
428                        numfields=numfields, data=output, units=opts.units)
429
430   for line in data:
431     ToStdout(line)
432
433   return 0
434
435
436 def SetNodeParams(opts, args):
437   """Modifies a node.
438
439   @param opts: the command line options selected by the user
440   @type args: list
441   @param args: should contain only one element, the node name
442   @rtype: int
443   @return: the desired exit code
444
445   """
446   if [opts.master_candidate, opts.drained, opts.offline].count(None) == 3:
447     ToStderr("Please give at least one of the parameters.")
448     return 1
449
450   if opts.master_candidate is not None:
451     candidate = opts.master_candidate == 'yes'
452   else:
453     candidate = None
454   if opts.offline is not None:
455     offline = opts.offline == 'yes'
456   else:
457     offline = None
458
459   if opts.drained is not None:
460     drained = opts.drained == 'yes'
461   else:
462     drained = None
463   op = opcodes.OpSetNodeParams(node_name=args[0],
464                                master_candidate=candidate,
465                                offline=offline,
466                                drained=drained,
467                                force=opts.force)
468
469   # even if here we process the result, we allow submit only
470   result = SubmitOrSend(op, opts)
471
472   if result:
473     ToStdout("Modified node %s", args[0])
474     for param, data in result:
475       ToStdout(" - %-5s -> %s", param, data)
476   return 0
477
478
479 commands = {
480   'add': (AddNode, ARGS_ONE,
481           [DEBUG_OPT,
482            make_option("-s", "--secondary-ip", dest="secondary_ip",
483                        help="Specify the secondary ip for the node",
484                        metavar="ADDRESS", default=None),
485            make_option("--readd", dest="readd",
486                        default=False, action="store_true",
487                        help="Readd old node after replacing it"),
488            make_option("--no-ssh-key-check", dest="ssh_key_check",
489                        default=True, action="store_false",
490                        help="Disable SSH key fingerprint checking"),
491            ],
492           "[-s ip] [--readd] [--no-ssh-key-check] <node_name>",
493           "Add a node to the cluster"),
494   'evacuate': (EvacuateNode, ARGS_ONE,
495                [DEBUG_OPT, FORCE_OPT,
496                 make_option("-n", "--new-secondary", dest="dst_node",
497                             help="New secondary node", metavar="NODE",
498                             default=None),
499                 make_option("-I", "--iallocator", metavar="<NAME>",
500                             help="Select new secondary for the instance"
501                             " automatically using the"
502                             " <NAME> iallocator plugin",
503                             default=None, type="string"),
504                 ],
505                "[-f] {-I <iallocator> | -n <dst>} <node>",
506                "Relocate the secondary instances from a node"
507                " to other nodes (only for instances with drbd disk template)"),
508   'failover': (FailoverNode, ARGS_ONE,
509                [DEBUG_OPT, FORCE_OPT,
510                 make_option("--ignore-consistency", dest="ignore_consistency",
511                             action="store_true", default=False,
512                             help="Ignore the consistency of the disks on"
513                             " the secondary"),
514                 ],
515                "[-f] <node>",
516                "Stops the primary instances on a node and start them on their"
517                " secondary node (only for instances with drbd disk template)"),
518   'migrate': (MigrateNode, ARGS_ONE,
519                [DEBUG_OPT, FORCE_OPT,
520                 make_option("--non-live", dest="live",
521                             default=True, action="store_false",
522                             help="Do a non-live migration (this usually means"
523                             " freeze the instance, save the state,"
524                             " transfer and only then resume running on the"
525                             " secondary node)"),
526                 ],
527                "[-f] <node>",
528                "Migrate all the primary instance on a node away from it"
529                " (only for instances of type drbd)"),
530   'info': (ShowNodeConfig, ARGS_ANY, [DEBUG_OPT],
531            "[<node_name>...]", "Show information about the node(s)"),
532   'list': (ListNodes, ARGS_ANY,
533            [DEBUG_OPT, NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, SYNC_OPT],
534            "[nodes...]",
535            "Lists the nodes in the cluster. The available fields"
536            " are (see the man page for details): %s"
537            " The default field list is (in order): %s." %
538            (", ".join(_LIST_HEADERS), ", ".join(_LIST_DEF_FIELDS))),
539   'modify': (SetNodeParams, ARGS_ONE,
540              [DEBUG_OPT, FORCE_OPT,
541               SUBMIT_OPT,
542               make_option("-C", "--master-candidate", dest="master_candidate",
543                           choices=('yes', 'no'), default=None,
544                           metavar="yes|no",
545                           help="Set the master_candidate flag on the node"),
546
547               make_option("-O", "--offline", dest="offline", metavar="yes|no",
548                           choices=('yes', 'no'), default=None,
549                           help="Set the offline flag on the node"),
550               make_option("-D", "--drained", dest="drained", metavar="yes|no",
551                           choices=('yes', 'no'), default=None,
552                           help="Set the drained flag on the node"),
553               ],
554              "<instance>", "Alters the parameters of an instance"),
555   'remove': (RemoveNode, ARGS_ONE, [DEBUG_OPT],
556              "<node_name>", "Removes a node from the cluster"),
557   'volumes': (ListVolumes, ARGS_ANY,
558               [DEBUG_OPT, NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT],
559               "[<node_name>...]", "List logical volumes on node(s)"),
560   'list-tags': (ListTags, ARGS_ONE, [DEBUG_OPT],
561                 "<node_name>", "List the tags of the given node"),
562   'add-tags': (AddTags, ARGS_ATLEAST(1), [DEBUG_OPT, TAG_SRC_OPT],
563                "<node_name> tag...", "Add tags to the given node"),
564   'remove-tags': (RemoveTags, ARGS_ATLEAST(1), [DEBUG_OPT, TAG_SRC_OPT],
565                   "<node_name> tag...", "Remove tags from the given node"),
566   }
567
568
569 if __name__ == '__main__':
570   sys.exit(GenericMain(commands, override={"tag_type": constants.TAG_NODE}))