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