gnt-node: Use new opcode to evacuate nodes
[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   op = opcodes.OpEvacuateNode(node_name=args[0], remote_node=dst_node,
228                               iallocator=iallocator)
229   SubmitOpCode(op, cl=cl)
230
231
232 def FailoverNode(opts, args):
233   """Failover all primary instance on a node.
234
235   @param opts: the command line options selected by the user
236   @type args: list
237   @param args: should be an empty list
238   @rtype: int
239   @return: the desired exit code
240
241   """
242   cl = GetClient()
243   force = opts.force
244   selected_fields = ["name", "pinst_list"]
245
246   # these fields are static data anyway, so it doesn't matter, but
247   # locking=True should be safer
248   result = cl.QueryNodes(names=args, fields=selected_fields,
249                          use_locking=False)
250   node, pinst = result[0]
251
252   if not pinst:
253     ToStderr("No primary instances on node %s, exiting.", node)
254     return 0
255
256   pinst = utils.NiceSort(pinst)
257
258   retcode = 0
259
260   if not force and not AskUser("Fail over instance(s) %s?" %
261                                (",".join("'%s'" % name for name in pinst))):
262     return 2
263
264   jex = JobExecutor(cl=cl)
265   for iname in pinst:
266     op = opcodes.OpFailoverInstance(instance_name=iname,
267                                     ignore_consistency=opts.ignore_consistency)
268     jex.QueueJob(iname, op)
269   results = jex.GetResults()
270   bad_cnt = len([row for row in results if not row[0]])
271   if bad_cnt == 0:
272     ToStdout("All %d instance(s) failed over successfully.", len(results))
273   else:
274     ToStdout("There were errors during the failover:\n"
275              "%d error(s) out of %d instance(s).", bad_cnt, len(results))
276   return retcode
277
278
279 def MigrateNode(opts, args):
280   """Migrate all primary instance on a node.
281
282   """
283   cl = GetClient()
284   force = opts.force
285   selected_fields = ["name", "pinst_list"]
286
287   result = cl.QueryNodes(names=args, fields=selected_fields, use_locking=False)
288   node, pinst = result[0]
289
290   if not pinst:
291     ToStdout("No primary instances on node %s, exiting." % node)
292     return 0
293
294   pinst = utils.NiceSort(pinst)
295
296   retcode = 0
297
298   if not force and not AskUser("Migrate instance(s) %s?" %
299                                (",".join("'%s'" % name for name in pinst))):
300     return 2
301
302   jex = JobExecutor(cl=cl)
303   for iname in pinst:
304     op = opcodes.OpMigrateInstance(instance_name=iname, live=opts.live,
305                                    cleanup=False)
306     jex.QueueJob(iname, op)
307
308   results = jex.GetResults()
309   bad_cnt = len([row for row in results if not row[0]])
310   if bad_cnt == 0:
311     ToStdout("All %d instance(s) migrated successfully.", len(results))
312   else:
313     ToStdout("There were errors during the migration:\n"
314              "%d error(s) out of %d instance(s).", bad_cnt, len(results))
315   return retcode
316
317
318 def ShowNodeConfig(opts, args):
319   """Show node information.
320
321   @param opts: the command line options selected by the user
322   @type args: list
323   @param args: should either be an empty list, in which case
324       we show information about all nodes, or should contain
325       a list of nodes to be queried for information
326   @rtype: int
327   @return: the desired exit code
328
329   """
330   cl = GetClient()
331   result = cl.QueryNodes(fields=["name", "pip", "sip",
332                                  "pinst_list", "sinst_list",
333                                  "master_candidate", "drained", "offline"],
334                          names=args, use_locking=False)
335
336   for (name, primary_ip, secondary_ip, pinst, sinst,
337        is_mc, drained, offline) in result:
338     ToStdout("Node name: %s", name)
339     ToStdout("  primary ip: %s", primary_ip)
340     ToStdout("  secondary ip: %s", secondary_ip)
341     ToStdout("  master candidate: %s", is_mc)
342     ToStdout("  drained: %s", drained)
343     ToStdout("  offline: %s", offline)
344     if pinst:
345       ToStdout("  primary for instances:")
346       for iname in utils.NiceSort(pinst):
347         ToStdout("    - %s", iname)
348     else:
349       ToStdout("  primary for no instances")
350     if sinst:
351       ToStdout("  secondary for instances:")
352       for iname in utils.NiceSort(sinst):
353         ToStdout("    - %s", iname)
354     else:
355       ToStdout("  secondary for no instances")
356
357   return 0
358
359
360 def RemoveNode(opts, args):
361   """Remove a node from the cluster.
362
363   @param opts: the command line options selected by the user
364   @type args: list
365   @param args: should contain only one element, the name of
366       the node to be removed
367   @rtype: int
368   @return: the desired exit code
369
370   """
371   op = opcodes.OpRemoveNode(node_name=args[0])
372   SubmitOpCode(op)
373   return 0
374
375
376 def PowercycleNode(opts, args):
377   """Remove a node from the cluster.
378
379   @param opts: the command line options selected by the user
380   @type args: list
381   @param args: should contain only one element, the name of
382       the node to be removed
383   @rtype: int
384   @return: the desired exit code
385
386   """
387   node = args[0]
388   if (not opts.confirm and
389       not AskUser("Are you sure you want to hard powercycle node %s?" % node)):
390     return 2
391
392   op = opcodes.OpPowercycleNode(node_name=node, force=opts.force)
393   result = SubmitOpCode(op)
394   ToStderr(result)
395   return 0
396
397
398 def ListVolumes(opts, args):
399   """List logical volumes on node(s).
400
401   @param opts: the command line options selected by the user
402   @type args: list
403   @param args: should either be an empty list, in which case
404       we list data for all nodes, or contain a list of nodes
405       to display data only for those
406   @rtype: int
407   @return: the desired exit code
408
409   """
410   if opts.output is None:
411     selected_fields = ["node", "phys", "vg",
412                        "name", "size", "instance"]
413   else:
414     selected_fields = opts.output.split(",")
415
416   op = opcodes.OpQueryNodeVolumes(nodes=args, output_fields=selected_fields)
417   output = SubmitOpCode(op)
418
419   if not opts.no_headers:
420     headers = {"node": "Node", "phys": "PhysDev",
421                "vg": "VG", "name": "Name",
422                "size": "Size", "instance": "Instance"}
423   else:
424     headers = None
425
426   unitfields = ["size"]
427
428   numfields = ["size"]
429
430   data = GenerateTable(separator=opts.separator, headers=headers,
431                        fields=selected_fields, unitfields=unitfields,
432                        numfields=numfields, data=output, units=opts.units)
433
434   for line in data:
435     ToStdout(line)
436
437   return 0
438
439
440 def SetNodeParams(opts, args):
441   """Modifies a node.
442
443   @param opts: the command line options selected by the user
444   @type args: list
445   @param args: should contain only one element, the node name
446   @rtype: int
447   @return: the desired exit code
448
449   """
450   if [opts.master_candidate, opts.drained, opts.offline].count(None) == 3:
451     ToStderr("Please give at least one of the parameters.")
452     return 1
453
454   if opts.master_candidate is not None:
455     candidate = opts.master_candidate == 'yes'
456   else:
457     candidate = None
458   if opts.offline is not None:
459     offline = opts.offline == 'yes'
460   else:
461     offline = None
462
463   if opts.drained is not None:
464     drained = opts.drained == 'yes'
465   else:
466     drained = None
467   op = opcodes.OpSetNodeParams(node_name=args[0],
468                                master_candidate=candidate,
469                                offline=offline,
470                                drained=drained,
471                                force=opts.force)
472
473   # even if here we process the result, we allow submit only
474   result = SubmitOrSend(op, opts)
475
476   if result:
477     ToStdout("Modified node %s", args[0])
478     for param, data in result:
479       ToStdout(" - %-5s -> %s", param, data)
480   return 0
481
482
483 commands = {
484   'add': (AddNode, ARGS_ONE,
485           [DEBUG_OPT,
486            make_option("-s", "--secondary-ip", dest="secondary_ip",
487                        help="Specify the secondary ip for the node",
488                        metavar="ADDRESS", default=None),
489            make_option("--readd", dest="readd",
490                        default=False, action="store_true",
491                        help="Readd old node after replacing it"),
492            make_option("--no-ssh-key-check", dest="ssh_key_check",
493                        default=True, action="store_false",
494                        help="Disable SSH key fingerprint checking"),
495            ],
496           "[-s ip] [--readd] [--no-ssh-key-check] <node_name>",
497           "Add a node to the cluster"),
498   'evacuate': (EvacuateNode, ARGS_ONE,
499                [DEBUG_OPT, FORCE_OPT,
500                 make_option("-n", "--new-secondary", dest="dst_node",
501                             help="New secondary node", metavar="NODE",
502                             default=None),
503                 make_option("-I", "--iallocator", metavar="<NAME>",
504                             help="Select new secondary for the instance"
505                             " automatically using the"
506                             " <NAME> iallocator plugin",
507                             default=None, type="string"),
508                 ],
509                "[-f] {-I <iallocator> | -n <dst>} <node>",
510                "Relocate the secondary instances from a node"
511                " to other nodes (only for instances with drbd disk template)"),
512   'failover': (FailoverNode, ARGS_ONE,
513                [DEBUG_OPT, FORCE_OPT,
514                 make_option("--ignore-consistency", dest="ignore_consistency",
515                             action="store_true", default=False,
516                             help="Ignore the consistency of the disks on"
517                             " the secondary"),
518                 ],
519                "[-f] <node>",
520                "Stops the primary instances on a node and start them on their"
521                " secondary node (only for instances with drbd disk template)"),
522   'migrate': (MigrateNode, ARGS_ONE,
523                [DEBUG_OPT, FORCE_OPT,
524                 make_option("--non-live", dest="live",
525                             default=True, action="store_false",
526                             help="Do a non-live migration (this usually means"
527                             " freeze the instance, save the state,"
528                             " transfer and only then resume running on the"
529                             " secondary node)"),
530                 ],
531                "[-f] <node>",
532                "Migrate all the primary instance on a node away from it"
533                " (only for instances of type drbd)"),
534   'info': (ShowNodeConfig, ARGS_ANY, [DEBUG_OPT],
535            "[<node_name>...]", "Show information about the node(s)"),
536   'list': (ListNodes, ARGS_ANY,
537            [DEBUG_OPT, NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, SYNC_OPT],
538            "[nodes...]",
539            "Lists the nodes in the cluster. The available fields"
540            " are (see the man page for details): %s"
541            " The default field list is (in order): %s." %
542            (", ".join(_LIST_HEADERS), ", ".join(_LIST_DEF_FIELDS))),
543   'modify': (SetNodeParams, ARGS_ONE,
544              [DEBUG_OPT, FORCE_OPT,
545               SUBMIT_OPT,
546               make_option("-C", "--master-candidate", dest="master_candidate",
547                           choices=('yes', 'no'), default=None,
548                           metavar="yes|no",
549                           help="Set the master_candidate flag on the node"),
550
551               make_option("-O", "--offline", dest="offline", metavar="yes|no",
552                           choices=('yes', 'no'), default=None,
553                           help="Set the offline flag on the node"),
554               make_option("-D", "--drained", dest="drained", metavar="yes|no",
555                           choices=('yes', 'no'), default=None,
556                           help="Set the drained flag on the node"),
557               ],
558              "<instance>", "Alters the parameters of an instance"),
559   'powercycle': (PowercycleNode, ARGS_ONE, [DEBUG_OPT, FORCE_OPT, CONFIRM_OPT],
560                  "<node_name>", "Tries to forcefully powercycle a node"),
561   'remove': (RemoveNode, ARGS_ONE, [DEBUG_OPT],
562              "<node_name>", "Removes a node from the cluster"),
563   'volumes': (ListVolumes, ARGS_ANY,
564               [DEBUG_OPT, NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT],
565               "[<node_name>...]", "List logical volumes on node(s)"),
566   'list-tags': (ListTags, ARGS_ONE, [DEBUG_OPT],
567                 "<node_name>", "List the tags of the given node"),
568   'add-tags': (AddTags, ARGS_ATLEAST(1), [DEBUG_OPT, TAG_SRC_OPT],
569                "<node_name> tag...", "Add tags to the given node"),
570   'remove-tags': (RemoveTags, ARGS_ATLEAST(1), [DEBUG_OPT, TAG_SRC_OPT],
571                   "<node_name> tag...", "Remove tags from the given node"),
572   }
573
574
575 if __name__ == '__main__':
576   sys.exit(GenericMain(commands, override={"tag_type": constants.TAG_NODE}))