LUSetInstanceParams: nic parameters
[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 PowercycleNode(opts, args):
395   """Remove a node from the cluster.
396
397   @param opts: the command line options selected by the user
398   @type args: list
399   @param args: should contain only one element, the name of
400       the node to be removed
401   @rtype: int
402   @return: the desired exit code
403
404   """
405   node = args[0]
406   if (not opts.confirm and
407       not AskUser("Are you sure you want to hard powercycle node %s?" % node)):
408     return 2
409
410   op = opcodes.OpPowercycleNode(node_name=node, force=opts.force)
411   result = SubmitOpCode(op)
412   ToStderr(result)
413   return 0
414
415
416 def ListVolumes(opts, args):
417   """List logical volumes on node(s).
418
419   @param opts: the command line options selected by the user
420   @type args: list
421   @param args: should either be an empty list, in which case
422       we list data for all nodes, or contain a list of nodes
423       to display data only for those
424   @rtype: int
425   @return: the desired exit code
426
427   """
428   if opts.output is None:
429     selected_fields = ["node", "phys", "vg",
430                        "name", "size", "instance"]
431   else:
432     selected_fields = opts.output.split(",")
433
434   op = opcodes.OpQueryNodeVolumes(nodes=args, output_fields=selected_fields)
435   output = SubmitOpCode(op)
436
437   if not opts.no_headers:
438     headers = {"node": "Node", "phys": "PhysDev",
439                "vg": "VG", "name": "Name",
440                "size": "Size", "instance": "Instance"}
441   else:
442     headers = None
443
444   unitfields = ["size"]
445
446   numfields = ["size"]
447
448   data = GenerateTable(separator=opts.separator, headers=headers,
449                        fields=selected_fields, unitfields=unitfields,
450                        numfields=numfields, data=output, units=opts.units)
451
452   for line in data:
453     ToStdout(line)
454
455   return 0
456
457
458 def SetNodeParams(opts, args):
459   """Modifies a node.
460
461   @param opts: the command line options selected by the user
462   @type args: list
463   @param args: should contain only one element, the node name
464   @rtype: int
465   @return: the desired exit code
466
467   """
468   if [opts.master_candidate, opts.drained, opts.offline].count(None) == 3:
469     ToStderr("Please give at least one of the parameters.")
470     return 1
471
472   if opts.master_candidate is not None:
473     candidate = opts.master_candidate == 'yes'
474   else:
475     candidate = None
476   if opts.offline is not None:
477     offline = opts.offline == 'yes'
478   else:
479     offline = None
480
481   if opts.drained is not None:
482     drained = opts.drained == 'yes'
483   else:
484     drained = None
485   op = opcodes.OpSetNodeParams(node_name=args[0],
486                                master_candidate=candidate,
487                                offline=offline,
488                                drained=drained,
489                                force=opts.force)
490
491   # even if here we process the result, we allow submit only
492   result = SubmitOrSend(op, opts)
493
494   if result:
495     ToStdout("Modified node %s", args[0])
496     for param, data in result:
497       ToStdout(" - %-5s -> %s", param, data)
498   return 0
499
500
501 commands = {
502   'add': (AddNode, ARGS_ONE,
503           [DEBUG_OPT,
504            make_option("-s", "--secondary-ip", dest="secondary_ip",
505                        help="Specify the secondary ip for the node",
506                        metavar="ADDRESS", default=None),
507            make_option("--readd", dest="readd",
508                        default=False, action="store_true",
509                        help="Readd old node after replacing it"),
510            make_option("--no-ssh-key-check", dest="ssh_key_check",
511                        default=True, action="store_false",
512                        help="Disable SSH key fingerprint checking"),
513            ],
514           "[-s ip] [--readd] [--no-ssh-key-check] <node_name>",
515           "Add a node to the cluster"),
516   'evacuate': (EvacuateNode, ARGS_ONE,
517                [DEBUG_OPT, FORCE_OPT,
518                 make_option("-n", "--new-secondary", dest="dst_node",
519                             help="New secondary node", metavar="NODE",
520                             default=None),
521                 make_option("-I", "--iallocator", metavar="<NAME>",
522                             help="Select new secondary for the instance"
523                             " automatically using the"
524                             " <NAME> iallocator plugin",
525                             default=None, type="string"),
526                 ],
527                "[-f] {-I <iallocator> | -n <dst>} <node>",
528                "Relocate the secondary instances from a node"
529                " to other nodes (only for instances with drbd disk template)"),
530   'failover': (FailoverNode, ARGS_ONE,
531                [DEBUG_OPT, FORCE_OPT,
532                 make_option("--ignore-consistency", dest="ignore_consistency",
533                             action="store_true", default=False,
534                             help="Ignore the consistency of the disks on"
535                             " the secondary"),
536                 ],
537                "[-f] <node>",
538                "Stops the primary instances on a node and start them on their"
539                " secondary node (only for instances with drbd disk template)"),
540   'migrate': (MigrateNode, ARGS_ONE,
541                [DEBUG_OPT, FORCE_OPT,
542                 make_option("--non-live", dest="live",
543                             default=True, action="store_false",
544                             help="Do a non-live migration (this usually means"
545                             " freeze the instance, save the state,"
546                             " transfer and only then resume running on the"
547                             " secondary node)"),
548                 ],
549                "[-f] <node>",
550                "Migrate all the primary instance on a node away from it"
551                " (only for instances of type drbd)"),
552   'info': (ShowNodeConfig, ARGS_ANY, [DEBUG_OPT],
553            "[<node_name>...]", "Show information about the node(s)"),
554   'list': (ListNodes, ARGS_ANY,
555            [DEBUG_OPT, NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, SYNC_OPT],
556            "[nodes...]",
557            "Lists the nodes in the cluster. The available fields"
558            " are (see the man page for details): %s"
559            " The default field list is (in order): %s." %
560            (", ".join(_LIST_HEADERS), ", ".join(_LIST_DEF_FIELDS))),
561   'modify': (SetNodeParams, ARGS_ONE,
562              [DEBUG_OPT, FORCE_OPT,
563               SUBMIT_OPT,
564               make_option("-C", "--master-candidate", dest="master_candidate",
565                           choices=('yes', 'no'), default=None,
566                           metavar="yes|no",
567                           help="Set the master_candidate flag on the node"),
568
569               make_option("-O", "--offline", dest="offline", metavar="yes|no",
570                           choices=('yes', 'no'), default=None,
571                           help="Set the offline flag on the node"),
572               make_option("-D", "--drained", dest="drained", metavar="yes|no",
573                           choices=('yes', 'no'), default=None,
574                           help="Set the drained flag on the node"),
575               ],
576              "<instance>", "Alters the parameters of an instance"),
577   'powercycle': (PowercycleNode, ARGS_ONE, [DEBUG_OPT, FORCE_OPT, CONFIRM_OPT],
578                  "<node_name>", "Tries to forcefully powercycle a node"),
579   'remove': (RemoveNode, ARGS_ONE, [DEBUG_OPT],
580              "<node_name>", "Removes a node from the cluster"),
581   'volumes': (ListVolumes, ARGS_ANY,
582               [DEBUG_OPT, NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT],
583               "[<node_name>...]", "List logical volumes on node(s)"),
584   'list-tags': (ListTags, ARGS_ONE, [DEBUG_OPT],
585                 "<node_name>", "List the tags of the given node"),
586   'add-tags': (AddTags, ARGS_ATLEAST(1), [DEBUG_OPT, TAG_SRC_OPT],
587                "<node_name> tag...", "Add tags to the given node"),
588   'remove-tags': (RemoveTags, ARGS_ATLEAST(1), [DEBUG_OPT, TAG_SRC_OPT],
589                   "<node_name> tag...", "Remove tags from the given node"),
590   }
591
592
593 if __name__ == '__main__':
594   sys.exit(GenericMain(commands, override={"tag_type": constants.TAG_NODE}))