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