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