Implement job summary in gnt-job list
[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 import sys
23 from optparse import make_option
24
25 from ganeti.cli import *
26 from ganeti import opcodes
27 from ganeti import logger
28 from ganeti import utils
29 from ganeti import constants
30 from ganeti import errors
31 from ganeti import bootstrap
32
33
34 _LIST_DEF_FIELDS = [
35   "name", "dtotal", "dfree",
36   "mtotal", "mnode", "mfree",
37   "pinst_cnt", "sinst_cnt",
38   ]
39
40
41 def AddNode(opts, args):
42   """Add node cli-to-processor bridge.
43
44   """
45   dns_data = utils.HostInfo(args[0])
46   node = dns_data.name
47
48   if not opts.readd:
49     op = opcodes.OpQueryNodes(output_fields=['name'], names=[node])
50     try:
51       output = SubmitOpCode(op)
52     except (errors.OpPrereqError, errors.OpExecError):
53       pass
54     else:
55       logger.ToStderr("Node %s already in the cluster (as %s)"
56                       " - please use --readd" % (args[0], output[0][0]))
57       return 1
58
59   logger.ToStderr("-- WARNING -- \n"
60     "Performing this operation is going to replace the ssh daemon keypair\n"
61     "on the target machine (%s) with the ones of the current one\n"
62     "and grant full intra-cluster ssh root access to/from it\n" % node)
63
64   bootstrap.SetupNodeDaemon(node, opts.ssh_key_check)
65
66   op = opcodes.OpAddNode(node_name=args[0], secondary_ip=opts.secondary_ip,
67                          readd=opts.readd)
68   SubmitOpCode(op)
69
70
71 def ListNodes(opts, args):
72   """List nodes and their properties.
73
74   """
75   if opts.output is None:
76     selected_fields = _LIST_DEF_FIELDS
77   elif opts.output.startswith("+"):
78     selected_fields = _LIST_DEF_FIELDS + opts.output[1:].split(",")
79   else:
80     selected_fields = opts.output.split(",")
81
82   output = GetClient().QueryNodes([], selected_fields)
83
84   if not opts.no_headers:
85     headers = {
86       "name": "Node", "pinst_cnt": "Pinst", "sinst_cnt": "Sinst",
87       "pinst_list": "PriInstances", "sinst_list": "SecInstances",
88       "pip": "PrimaryIP", "sip": "SecondaryIP",
89       "dtotal": "DTotal", "dfree": "DFree",
90       "mtotal": "MTotal", "mnode": "MNode", "mfree": "MFree",
91       "bootid": "BootID",
92       "ctotal": "CTotal",
93       "tags": "Tags",
94       "serial_no": "SerialNo",
95       }
96   else:
97     headers = None
98
99   if opts.human_readable:
100     unitfields = ["dtotal", "dfree", "mtotal", "mnode", "mfree"]
101   else:
102     unitfields = None
103
104   numfields = ["dtotal", "dfree",
105                "mtotal", "mnode", "mfree",
106                "pinst_cnt", "sinst_cnt",
107                "ctotal", "serial_no"]
108
109   list_type_fields = ("pinst_list", "sinst_list", "tags")
110   # change raw values to nicer strings
111   for row in output:
112     for idx, field in enumerate(selected_fields):
113       val = row[idx]
114       if field in list_type_fields:
115         val = ",".join(val)
116       elif val is None:
117         val = "?"
118       row[idx] = str(val)
119
120   data = GenerateTable(separator=opts.separator, headers=headers,
121                        fields=selected_fields, unitfields=unitfields,
122                        numfields=numfields, data=output)
123   for line in data:
124     logger.ToStdout(line)
125
126   return 0
127
128
129 def EvacuateNode(opts, args):
130   """Relocate all secondary instance from a node.
131
132   """
133   force = opts.force
134   selected_fields = ["name", "sinst_list"]
135   src_node, dst_node = args
136
137   op = opcodes.OpQueryNodes(output_fields=selected_fields, names=[src_node])
138   result = SubmitOpCode(op)
139   src_node, sinst = result[0]
140   op = opcodes.OpQueryNodes(output_fields=["name"], names=[dst_node])
141   result = SubmitOpCode(op)
142   dst_node = result[0][0]
143
144   if src_node == dst_node:
145     raise errors.OpPrereqError("Evacuate node needs different source and"
146                                " target nodes (node %s given twice)" %
147                                src_node)
148
149   if not sinst:
150     logger.ToStderr("No secondary instances on node %s, exiting." % src_node)
151     return constants.EXIT_SUCCESS
152
153   sinst = utils.NiceSort(sinst)
154
155   retcode = constants.EXIT_SUCCESS
156
157   if not force and not AskUser("Relocate instance(s) %s from node\n"
158                                " %s to node\n %s?" %
159                                (",".join("'%s'" % name for name in sinst),
160                                src_node, dst_node)):
161     return constants.EXIT_CONFIRMATION
162
163   good_cnt = bad_cnt = 0
164   for iname in sinst:
165     op = opcodes.OpReplaceDisks(instance_name=iname,
166                                 remote_node=dst_node,
167                                 mode=constants.REPLACE_DISK_ALL,
168                                 disks=["sda", "sdb"])
169     try:
170       logger.ToStdout("Replacing disks for instance %s" % iname)
171       SubmitOpCode(op)
172       logger.ToStdout("Instance %s has been relocated" % iname)
173       good_cnt += 1
174     except errors.GenericError, err:
175       nret, msg = FormatError(err)
176       retcode |= nret
177       logger.ToStderr("Error replacing disks for instance %s: %s" %
178                       (iname, msg))
179       bad_cnt += 1
180
181   if retcode == constants.EXIT_SUCCESS:
182     logger.ToStdout("All %d instance(s) relocated successfully." % good_cnt)
183   else:
184     logger.ToStdout("There were errors during the relocation:\n"
185                     "%d error(s) out of %d instance(s)." %
186                     (bad_cnt, good_cnt + bad_cnt))
187   return retcode
188
189
190 def FailoverNode(opts, args):
191   """Failover all primary instance on a node.
192
193   """
194   force = opts.force
195   selected_fields = ["name", "pinst_list"]
196
197   op = opcodes.OpQueryNodes(output_fields=selected_fields, names=args)
198   result = SubmitOpCode(op)
199   node, pinst = result[0]
200
201   if not pinst:
202     logger.ToStderr("No primary instances on node %s, exiting." % node)
203     return 0
204
205   pinst = utils.NiceSort(pinst)
206
207   retcode = 0
208
209   if not force and not AskUser("Fail over instance(s) %s?" %
210                                (",".join("'%s'" % name for name in pinst))):
211     return 2
212
213   good_cnt = bad_cnt = 0
214   for iname in pinst:
215     op = opcodes.OpFailoverInstance(instance_name=iname,
216                                     ignore_consistency=opts.ignore_consistency)
217     try:
218       logger.ToStdout("Failing over instance %s" % iname)
219       SubmitOpCode(op)
220       logger.ToStdout("Instance %s has been failed over" % iname)
221       good_cnt += 1
222     except errors.GenericError, err:
223       nret, msg = FormatError(err)
224       retcode |= nret
225       logger.ToStderr("Error failing over instance %s: %s" % (iname, msg))
226       bad_cnt += 1
227
228   if retcode == 0:
229     logger.ToStdout("All %d instance(s) failed over successfully." % good_cnt)
230   else:
231     logger.ToStdout("There were errors during the failover:\n"
232                     "%d error(s) out of %d instance(s)." %
233                     (bad_cnt, good_cnt + bad_cnt))
234   return retcode
235
236
237 def ShowNodeConfig(opts, args):
238   """Show node information.
239
240   """
241   op = opcodes.OpQueryNodes(output_fields=["name", "pip", "sip",
242                                            "pinst_list", "sinst_list"],
243                             names=args)
244   result = SubmitOpCode(op)
245
246   for name, primary_ip, secondary_ip, pinst, sinst in result:
247     logger.ToStdout("Node name: %s" % name)
248     logger.ToStdout("  primary ip: %s" % primary_ip)
249     logger.ToStdout("  secondary ip: %s" % secondary_ip)
250     if pinst:
251       logger.ToStdout("  primary for instances:")
252       for iname in pinst:
253         logger.ToStdout("    - %s" % iname)
254     else:
255       logger.ToStdout("  primary for no instances")
256     if sinst:
257       logger.ToStdout("  secondary for instances:")
258       for iname in sinst:
259         logger.ToStdout("    - %s" % iname)
260     else:
261       logger.ToStdout("  secondary for no instances")
262
263   return 0
264
265
266 def RemoveNode(opts, args):
267   """Remove node cli-to-processor bridge."""
268   op = opcodes.OpRemoveNode(node_name=args[0])
269   SubmitOpCode(op)
270
271
272 def ListVolumes(opts, args):
273   """List logical volumes on node(s).
274
275   """
276   if opts.output is None:
277     selected_fields = ["node", "phys", "vg",
278                        "name", "size", "instance"]
279   else:
280     selected_fields = opts.output.split(",")
281
282   op = opcodes.OpQueryNodeVolumes(nodes=args, output_fields=selected_fields)
283   output = SubmitOpCode(op)
284
285   if not opts.no_headers:
286     headers = {"node": "Node", "phys": "PhysDev",
287                "vg": "VG", "name": "Name",
288                "size": "Size", "instance": "Instance"}
289   else:
290     headers = None
291
292   if opts.human_readable:
293     unitfields = ["size"]
294   else:
295     unitfields = None
296
297   numfields = ["size"]
298
299   data = GenerateTable(separator=opts.separator, headers=headers,
300                        fields=selected_fields, unitfields=unitfields,
301                        numfields=numfields, data=output)
302
303   for line in data:
304     logger.ToStdout(line)
305
306   return 0
307
308
309 commands = {
310   'add': (AddNode, ARGS_ONE,
311           [DEBUG_OPT,
312            make_option("-s", "--secondary-ip", dest="secondary_ip",
313                        help="Specify the secondary ip for the node",
314                        metavar="ADDRESS", default=None),
315            make_option("--readd", dest="readd",
316                        default=False, action="store_true",
317                        help="Readd old node after replacing it"),
318            make_option("--no-ssh-key-check", dest="ssh_key_check",
319                        default=True, action="store_false",
320                        help="Disable SSH key fingerprint checking"),
321            ],
322           "[-s ip] [--readd] [--no-ssh-key-check] <node_name>",
323           "Add a node to the cluster"),
324   'evacuate': (EvacuateNode, ARGS_FIXED(2),
325                [DEBUG_OPT, FORCE_OPT],
326                "[-f] <src> <dst>",
327                "Relocate the secondary instances from the first node"
328                " to the second one (only for instances with drbd disk template"
329                ),
330   'failover': (FailoverNode, ARGS_ONE,
331                [DEBUG_OPT, FORCE_OPT,
332                 make_option("--ignore-consistency", dest="ignore_consistency",
333                             action="store_true", default=False,
334                             help="Ignore the consistency of the disks on"
335                             " the secondary"),
336                 ],
337                "[-f] <node>",
338                "Stops the primary instances on a node and start them on their"
339                " secondary node (only for instances with drbd disk template)"),
340   'info': (ShowNodeConfig, ARGS_ANY, [DEBUG_OPT],
341            "[<node_name>...]", "Show information about the node(s)"),
342   'list': (ListNodes, ARGS_NONE,
343            [DEBUG_OPT, NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT,
344             SUBMIT_OPT],
345            "", "Lists the nodes in the cluster. The available fields"
346            " are (see the man page for details): name, pinst_cnt, pinst_list,"
347            " sinst_cnt, sinst_list, pip, sip, dtotal, dfree, mtotal, mnode,"
348            " mfree, bootid, cpu_count, serial_no."
349            " The default field list is"
350            " (in order): %s." % ", ".join(_LIST_DEF_FIELDS),
351            ),
352   'remove': (RemoveNode, ARGS_ONE, [DEBUG_OPT],
353              "<node_name>", "Removes a node from the cluster"),
354   'volumes': (ListVolumes, ARGS_ANY,
355               [DEBUG_OPT, NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT],
356               "[<node_name>...]", "List logical volumes on node(s)"),
357   'list-tags': (ListTags, ARGS_ONE, [DEBUG_OPT],
358                 "<node_name>", "List the tags of the given node"),
359   'add-tags': (AddTags, ARGS_ATLEAST(1), [DEBUG_OPT, TAG_SRC_OPT],
360                "<node_name> tag...", "Add tags to the given node"),
361   'remove-tags': (RemoveTags, ARGS_ATLEAST(1), [DEBUG_OPT, TAG_SRC_OPT],
362                   "<node_name> tag...", "Remove tags from the given node"),
363   }
364
365
366 if __name__ == '__main__':
367   sys.exit(GenericMain(commands, override={"tag_type": constants.TAG_NODE}))