Export the cpu nodes and sockets from Xen
[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",
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'):
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                          names=args, use_locking=True)
328
329   for name, primary_ip, secondary_ip, pinst, sinst in result:
330     ToStdout("Node name: %s", name)
331     ToStdout("  primary ip: %s", primary_ip)
332     ToStdout("  secondary ip: %s", secondary_ip)
333     if pinst:
334       ToStdout("  primary for instances:")
335       for iname in pinst:
336         ToStdout("    - %s", iname)
337     else:
338       ToStdout("  primary for no instances")
339     if sinst:
340       ToStdout("  secondary for instances:")
341       for iname in sinst:
342         ToStdout("    - %s", iname)
343     else:
344       ToStdout("  secondary for no instances")
345
346   return 0
347
348
349 def RemoveNode(opts, args):
350   """Remove a node from the cluster.
351
352   @param opts: the command line options selected by the user
353   @type args: list
354   @param args: should contain only one element, the name of
355       the node to be removed
356   @rtype: int
357   @return: the desired exit code
358
359   """
360   op = opcodes.OpRemoveNode(node_name=args[0])
361   SubmitOpCode(op)
362   return 0
363
364
365 def ListVolumes(opts, args):
366   """List logical volumes on node(s).
367
368   @param opts: the command line options selected by the user
369   @type args: list
370   @param args: should either be an empty list, in which case
371       we list data for all nodes, or contain a list of nodes
372       to display data only for those
373   @rtype: int
374   @return: the desired exit code
375
376   """
377   if opts.output is None:
378     selected_fields = ["node", "phys", "vg",
379                        "name", "size", "instance"]
380   else:
381     selected_fields = opts.output.split(",")
382
383   op = opcodes.OpQueryNodeVolumes(nodes=args, output_fields=selected_fields)
384   output = SubmitOpCode(op)
385
386   if not opts.no_headers:
387     headers = {"node": "Node", "phys": "PhysDev",
388                "vg": "VG", "name": "Name",
389                "size": "Size", "instance": "Instance"}
390   else:
391     headers = None
392
393   unitfields = ["size"]
394
395   numfields = ["size"]
396
397   data = GenerateTable(separator=opts.separator, headers=headers,
398                        fields=selected_fields, unitfields=unitfields,
399                        numfields=numfields, data=output, units=opts.units)
400
401   for line in data:
402     ToStdout(line)
403
404   return 0
405
406
407 def SetNodeParams(opts, args):
408   """Modifies a node.
409
410   @param opts: the command line options selected by the user
411   @type args: list
412   @param args: should contain only one element, the node name
413   @rtype: int
414   @return: the desired exit code
415
416   """
417   if opts.master_candidate is None and opts.offline is None:
418     ToStderr("Please give at least one of the parameters.")
419     return 1
420
421   if opts.master_candidate is not None:
422     candidate = opts.master_candidate == 'yes'
423   else:
424     candidate = None
425   if opts.offline is not None:
426     offline = opts.offline == 'yes'
427   else:
428     offline = None
429   op = opcodes.OpSetNodeParams(node_name=args[0],
430                                master_candidate=candidate,
431                                offline=offline,
432                                force=opts.force)
433
434   # even if here we process the result, we allow submit only
435   result = SubmitOrSend(op, opts)
436
437   if result:
438     ToStdout("Modified node %s", args[0])
439     for param, data in result:
440       ToStdout(" - %-5s -> %s", param, data)
441   return 0
442
443
444 commands = {
445   'add': (AddNode, ARGS_ONE,
446           [DEBUG_OPT,
447            make_option("-s", "--secondary-ip", dest="secondary_ip",
448                        help="Specify the secondary ip for the node",
449                        metavar="ADDRESS", default=None),
450            make_option("--readd", dest="readd",
451                        default=False, action="store_true",
452                        help="Readd old node after replacing it"),
453            make_option("--no-ssh-key-check", dest="ssh_key_check",
454                        default=True, action="store_false",
455                        help="Disable SSH key fingerprint checking"),
456            ],
457           "[-s ip] [--readd] [--no-ssh-key-check] <node_name>",
458           "Add a node to the cluster"),
459   'evacuate': (EvacuateNode, ARGS_ONE,
460                [DEBUG_OPT, FORCE_OPT,
461                 make_option("-n", "--new-secondary", dest="dst_node",
462                             help="New secondary node", metavar="NODE",
463                             default=None),
464                 make_option("-i", "--iallocator", metavar="<NAME>",
465                             help="Select new secondary for the instance"
466                             " automatically using the"
467                             " <NAME> iallocator plugin",
468                             default=None, type="string"),
469                 ],
470                "[-f] {-i <iallocator> | -n <dst>} <node>",
471                "Relocate the secondary instances from a node"
472                " to other nodes (only for instances with drbd disk template)"),
473   'failover': (FailoverNode, ARGS_ONE,
474                [DEBUG_OPT, FORCE_OPT,
475                 make_option("--ignore-consistency", dest="ignore_consistency",
476                             action="store_true", default=False,
477                             help="Ignore the consistency of the disks on"
478                             " the secondary"),
479                 ],
480                "[-f] <node>",
481                "Stops the primary instances on a node and start them on their"
482                " secondary node (only for instances with drbd disk template)"),
483   'migrate': (MigrateNode, ARGS_ONE,
484                [DEBUG_OPT, FORCE_OPT,
485                 make_option("--non-live", dest="live",
486                             default=True, action="store_false",
487                             help="Do a non-live migration (this usually means"
488                             " freeze the instance, save the state,"
489                             " transfer and only then resume running on the"
490                             " secondary node)"),
491                 ],
492                "[-f] <node>",
493                "Migrate all the primary instance on a node away from it"
494                " (only for instances of type drbd)"),
495   'info': (ShowNodeConfig, ARGS_ANY, [DEBUG_OPT],
496            "[<node_name>...]", "Show information about the node(s)"),
497   'list': (ListNodes, ARGS_NONE,
498            [DEBUG_OPT, NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT,
499             SUBMIT_OPT, SYNC_OPT],
500            "", "Lists the nodes in the cluster. The available fields"
501            " are (see the man page for details): %s"
502            " The default field list is (in order): %s." %
503            (", ".join(_LIST_HEADERS), ", ".join(_LIST_DEF_FIELDS))),
504   'modify': (SetNodeParams, ARGS_ONE,
505              [DEBUG_OPT, FORCE_OPT,
506               SUBMIT_OPT,
507               make_option("-C", "--master-candidate", dest="master_candidate",
508                           choices=('yes', 'no'), default=None,
509                           help="Set the master_candidate flag on the node"),
510               make_option("-O", "--offline", dest="offline",
511                           choices=('yes', 'no'), default=None,
512                           help="Set the offline flag on the node"),
513               ],
514              "<instance>", "Alters the parameters of an instance"),
515   'remove': (RemoveNode, ARGS_ONE, [DEBUG_OPT],
516              "<node_name>", "Removes a node from the cluster"),
517   'volumes': (ListVolumes, ARGS_ANY,
518               [DEBUG_OPT, NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT],
519               "[<node_name>...]", "List logical volumes on node(s)"),
520   'list-tags': (ListTags, ARGS_ONE, [DEBUG_OPT],
521                 "<node_name>", "List the tags of the given node"),
522   'add-tags': (AddTags, ARGS_ATLEAST(1), [DEBUG_OPT, TAG_SRC_OPT],
523                "<node_name> tag...", "Add tags to the given node"),
524   'remove-tags': (RemoveTags, ARGS_ATLEAST(1), [DEBUG_OPT, TAG_SRC_OPT],
525                   "<node_name> tag...", "Remove tags from the given node"),
526   }
527
528
529 if __name__ == '__main__':
530   sys.exit(GenericMain(commands, override={"tag_type": constants.TAG_NODE}))