Statistics
| Branch: | Tag: | Revision:

root / scripts / gnt-node @ cc3bcec8

History | View | Annotate | Download (14.7 kB)

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 opcodes
31
from ganeti import utils
32
from ganeti import constants
33
from ganeti import errors
34
from ganeti import bootstrap
35

    
36

    
37
#: default list of field for L{ListNodes}
38
_LIST_DEF_FIELDS = [
39
  "name", "dtotal", "dfree",
40
  "mtotal", "mnode", "mfree",
41
  "pinst_cnt", "sinst_cnt",
42
  ]
43

    
44
#: headers (and full field list for L{ListNodes}
45
_LIST_HEADERS = {
46
  "name": "Node", "pinst_cnt": "Pinst", "sinst_cnt": "Sinst",
47
  "pinst_list": "PriInstances", "sinst_list": "SecInstances",
48
  "pip": "PrimaryIP", "sip": "SecondaryIP",
49
  "dtotal": "DTotal", "dfree": "DFree",
50
  "mtotal": "MTotal", "mnode": "MNode", "mfree": "MFree",
51
  "bootid": "BootID",
52
  "ctotal": "CTotal",
53
  "tags": "Tags",
54
  "serial_no": "SerialNo",
55
  "master_candidate": "MasterC",
56
  "master": "IsMaster",
57
  }
58

    
59

    
60
@UsesRPC
61
def AddNode(opts, args):
62
  """Add a node to the cluster.
63

    
64
  @param opts: the command line options selected by the user
65
  @type args: list
66
  @param args: should contain only one element, the new node name
67
  @rtype: int
68
  @return: the desired exit code
69

    
70
  """
71
  cl = GetClient()
72
  dns_data = utils.HostInfo(args[0])
73
  node = dns_data.name
74

    
75
  if not opts.readd:
76
    try:
77
      output = cl.QueryNodes(names=[node], fields=['name'])
78
    except (errors.OpPrereqError, errors.OpExecError):
79
      pass
80
    else:
81
      ToStderr("Node %s already in the cluster (as %s)"
82
               " - please use --readd", args[0], output[0][0])
83
      return 1
84

    
85
  # read the cluster name from the master
86
  output = cl.QueryConfigValues(['cluster_name'])
87
  cluster_name = output[0]
88

    
89
  ToStderr("-- WARNING -- \n"
90
           "Performing this operation is going to replace the ssh daemon"
91
           " keypair\n"
92
           "on the target machine (%s) with the ones of the"
93
           " current one\n"
94
           "and grant full intra-cluster ssh root access to/from it\n", node)
95

    
96
  bootstrap.SetupNodeDaemon(cluster_name, node, opts.ssh_key_check)
97

    
98
  op = opcodes.OpAddNode(node_name=args[0], secondary_ip=opts.secondary_ip,
99
                         readd=opts.readd)
100
  SubmitOpCode(op)
101

    
102

    
103
def ListNodes(opts, args):
104
  """List nodes and their properties.
105

    
106
  @param opts: the command line options selected by the user
107
  @type args: list
108
  @param args: should be an empty list
109
  @rtype: int
110
  @return: the desired exit code
111

    
112
  """
113
  if opts.output is None:
114
    selected_fields = _LIST_DEF_FIELDS
115
  elif opts.output.startswith("+"):
116
    selected_fields = _LIST_DEF_FIELDS + opts.output[1:].split(",")
117
  else:
118
    selected_fields = opts.output.split(",")
119

    
120
  output = GetClient().QueryNodes([], selected_fields)
121

    
122
  if not opts.no_headers:
123
    headers = _LIST_HEADERS
124
  else:
125
    headers = None
126

    
127
  unitfields = ["dtotal", "dfree", "mtotal", "mnode", "mfree"]
128

    
129
  numfields = ["dtotal", "dfree",
130
               "mtotal", "mnode", "mfree",
131
               "pinst_cnt", "sinst_cnt",
132
               "ctotal", "serial_no"]
133

    
134
  list_type_fields = ("pinst_list", "sinst_list", "tags")
135
  # change raw values to nicer strings
136
  for row in output:
137
    for idx, field in enumerate(selected_fields):
138
      val = row[idx]
139
      if field in list_type_fields:
140
        val = ",".join(val)
141
      elif field in ('master', 'master_candidate'):
142
        if val:
143
          val = 'Y'
144
        else:
145
          val = 'N'
146
      elif val is None:
147
        val = "?"
148
      row[idx] = str(val)
149

    
150
  data = GenerateTable(separator=opts.separator, headers=headers,
151
                       fields=selected_fields, unitfields=unitfields,
152
                       numfields=numfields, data=output, units=opts.units)
153
  for line in data:
154
    ToStdout(line)
155

    
156
  return 0
157

    
158

    
159
def EvacuateNode(opts, args):
160
  """Relocate all secondary instance from a node.
161

    
162
  @param opts: the command line options selected by the user
163
  @type args: list
164
  @param args: should be an empty list
165
  @rtype: int
166
  @return: the desired exit code
167

    
168
  """
169
  force = opts.force
170
  selected_fields = ["name", "sinst_list"]
171
  src_node, dst_node = args
172

    
173
  op = opcodes.OpQueryNodes(output_fields=selected_fields, names=[src_node])
174
  result = SubmitOpCode(op)
175
  src_node, sinst = result[0]
176
  op = opcodes.OpQueryNodes(output_fields=["name"], names=[dst_node])
177
  result = SubmitOpCode(op)
178
  dst_node = result[0][0]
179

    
180
  if src_node == dst_node:
181
    raise errors.OpPrereqError("Evacuate node needs different source and"
182
                               " target nodes (node %s given twice)" %
183
                               src_node)
184

    
185
  if not sinst:
186
    ToStderr("No secondary instances on node %s, exiting.", src_node)
187
    return constants.EXIT_SUCCESS
188

    
189
  sinst = utils.NiceSort(sinst)
190

    
191
  retcode = constants.EXIT_SUCCESS
192

    
193
  if not force and not AskUser("Relocate instance(s) %s from node\n"
194
                               " %s to node\n %s?" %
195
                               (",".join("'%s'" % name for name in sinst),
196
                               src_node, dst_node)):
197
    return constants.EXIT_CONFIRMATION
198

    
199
  good_cnt = bad_cnt = 0
200
  for iname in sinst:
201
    op = opcodes.OpReplaceDisks(instance_name=iname,
202
                                remote_node=dst_node,
203
                                mode=constants.REPLACE_DISK_ALL,
204
                                disks=["sda", "sdb"])
205
    try:
206
      ToStdout("Replacing disks for instance %s", iname)
207
      SubmitOpCode(op)
208
      ToStdout("Instance %s has been relocated", iname)
209
      good_cnt += 1
210
    except errors.GenericError, err:
211
      nret, msg = FormatError(err)
212
      retcode |= nret
213
      ToStderr("Error replacing disks for instance %s: %s", iname, msg)
214
      bad_cnt += 1
215

    
216
  if retcode == constants.EXIT_SUCCESS:
217
    ToStdout("All %d instance(s) relocated successfully.", good_cnt)
218
  else:
219
    ToStdout("There were errors during the relocation:\n"
220
             "%d error(s) out of %d instance(s).", bad_cnt, good_cnt + bad_cnt)
221
  return retcode
222

    
223

    
224
def FailoverNode(opts, args):
225
  """Failover all primary instance on a node.
226

    
227
  @param opts: the command line options selected by the user
228
  @type args: list
229
  @param args: should be an empty list
230
  @rtype: int
231
  @return: the desired exit code
232

    
233
  """
234
  force = opts.force
235
  selected_fields = ["name", "pinst_list"]
236

    
237
  op = opcodes.OpQueryNodes(output_fields=selected_fields, names=args)
238
  result = SubmitOpCode(op)
239
  node, pinst = result[0]
240

    
241
  if not pinst:
242
    ToStderr("No primary instances on node %s, exiting.", node)
243
    return 0
244

    
245
  pinst = utils.NiceSort(pinst)
246

    
247
  retcode = 0
248

    
249
  if not force and not AskUser("Fail over instance(s) %s?" %
250
                               (",".join("'%s'" % name for name in pinst))):
251
    return 2
252

    
253
  good_cnt = bad_cnt = 0
254
  for iname in pinst:
255
    op = opcodes.OpFailoverInstance(instance_name=iname,
256
                                    ignore_consistency=opts.ignore_consistency)
257
    try:
258
      ToStdout("Failing over instance %s", iname)
259
      SubmitOpCode(op)
260
      ToStdout("Instance %s has been failed over", iname)
261
      good_cnt += 1
262
    except errors.GenericError, err:
263
      nret, msg = FormatError(err)
264
      retcode |= nret
265
      ToStderr("Error failing over instance %s: %s", iname, msg)
266
      bad_cnt += 1
267

    
268
  if retcode == 0:
269
    ToStdout("All %d instance(s) failed over successfully.", good_cnt)
270
  else:
271
    ToStdout("There were errors during the failover:\n"
272
             "%d error(s) out of %d instance(s).", bad_cnt, good_cnt + bad_cnt)
273
  return retcode
274

    
275

    
276
def ShowNodeConfig(opts, args):
277
  """Show node information.
278

    
279
  @param opts: the command line options selected by the user
280
  @type args: list
281
  @param args: should either be an empty list, in which case
282
      we show information about all nodes, or should contain
283
      a list of nodes to be queried for information
284
  @rtype: int
285
  @return: the desired exit code
286

    
287
  """
288
  op = opcodes.OpQueryNodes(output_fields=["name", "pip", "sip",
289
                                           "pinst_list", "sinst_list"],
290
                            names=args)
291
  result = SubmitOpCode(op)
292

    
293
  for name, primary_ip, secondary_ip, pinst, sinst in result:
294
    ToStdout("Node name: %s", name)
295
    ToStdout("  primary ip: %s", primary_ip)
296
    ToStdout("  secondary ip: %s", secondary_ip)
297
    if pinst:
298
      ToStdout("  primary for instances:")
299
      for iname in pinst:
300
        ToStdout("    - %s", iname)
301
    else:
302
      ToStdout("  primary for no instances")
303
    if sinst:
304
      ToStdout("  secondary for instances:")
305
      for iname in sinst:
306
        ToStdout("    - %s", iname)
307
    else:
308
      ToStdout("  secondary for no instances")
309

    
310
  return 0
311

    
312

    
313
def RemoveNode(opts, args):
314
  """Remove a node from the cluster.
315

    
316
  @param opts: the command line options selected by the user
317
  @type args: list
318
  @param args: should contain only one element, the name of
319
      the node to be removed
320
  @rtype: int
321
  @return: the desired exit code
322

    
323
  """
324
  op = opcodes.OpRemoveNode(node_name=args[0])
325
  SubmitOpCode(op)
326
  return 0
327

    
328

    
329
def ListVolumes(opts, args):
330
  """List logical volumes on node(s).
331

    
332
  @param opts: the command line options selected by the user
333
  @type args: list
334
  @param args: should either be an empty list, in which case
335
      we list data for all nodes, or contain a list of nodes
336
      to display data only for those
337
  @rtype: int
338
  @return: the desired exit code
339

    
340
  """
341
  if opts.output is None:
342
    selected_fields = ["node", "phys", "vg",
343
                       "name", "size", "instance"]
344
  else:
345
    selected_fields = opts.output.split(",")
346

    
347
  op = opcodes.OpQueryNodeVolumes(nodes=args, output_fields=selected_fields)
348
  output = SubmitOpCode(op)
349

    
350
  if not opts.no_headers:
351
    headers = {"node": "Node", "phys": "PhysDev",
352
               "vg": "VG", "name": "Name",
353
               "size": "Size", "instance": "Instance"}
354
  else:
355
    headers = None
356

    
357
  unitfields = ["size"]
358

    
359
  numfields = ["size"]
360

    
361
  data = GenerateTable(separator=opts.separator, headers=headers,
362
                       fields=selected_fields, unitfields=unitfields,
363
                       numfields=numfields, data=output, units=opts.units)
364

    
365
  for line in data:
366
    ToStdout(line)
367

    
368
  return 0
369

    
370

    
371
def SetNodeParams(opts, args):
372
  """Modifies a node.
373

    
374
  @param opts: the command line options selected by the user
375
  @type args: list
376
  @param args: should contain only one element, the node name
377
  @rtype: int
378
  @return: the desired exit code
379

    
380
  """
381
  if opts.master_candidate is None:
382
    ToStderr("Please give at least one of the parameters.")
383
    return 1
384

    
385
  candidate = opts.master_candidate == 'yes'
386
  op = opcodes.OpSetNodeParams(node_name=args[0],
387
                              master_candidate=candidate,
388
                              force=opts.force)
389

    
390
  # even if here we process the result, we allow submit only
391
  result = SubmitOrSend(op, opts)
392

    
393
  if result:
394
    ToStdout("Modified node %s", args[0])
395
    for param, data in result:
396
      ToStdout(" - %-5s -> %s", param, data)
397
  return 0
398

    
399

    
400
commands = {
401
  'add': (AddNode, ARGS_ONE,
402
          [DEBUG_OPT,
403
           make_option("-s", "--secondary-ip", dest="secondary_ip",
404
                       help="Specify the secondary ip for the node",
405
                       metavar="ADDRESS", default=None),
406
           make_option("--readd", dest="readd",
407
                       default=False, action="store_true",
408
                       help="Readd old node after replacing it"),
409
           make_option("--no-ssh-key-check", dest="ssh_key_check",
410
                       default=True, action="store_false",
411
                       help="Disable SSH key fingerprint checking"),
412
           ],
413
          "[-s ip] [--readd] [--no-ssh-key-check] <node_name>",
414
          "Add a node to the cluster"),
415
  'evacuate': (EvacuateNode, ARGS_FIXED(2),
416
               [DEBUG_OPT, FORCE_OPT],
417
               "[-f] <src> <dst>",
418
               "Relocate the secondary instances from the first node"
419
               " to the second one (only for instances with drbd disk template"
420
               ),
421
  'failover': (FailoverNode, ARGS_ONE,
422
               [DEBUG_OPT, FORCE_OPT,
423
                make_option("--ignore-consistency", dest="ignore_consistency",
424
                            action="store_true", default=False,
425
                            help="Ignore the consistency of the disks on"
426
                            " the secondary"),
427
                ],
428
               "[-f] <node>",
429
               "Stops the primary instances on a node and start them on their"
430
               " secondary node (only for instances with drbd disk template)"),
431
  'info': (ShowNodeConfig, ARGS_ANY, [DEBUG_OPT],
432
           "[<node_name>...]", "Show information about the node(s)"),
433
  'list': (ListNodes, ARGS_NONE,
434
           [DEBUG_OPT, NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT,
435
            SUBMIT_OPT],
436
           "", "Lists the nodes in the cluster. The available fields"
437
           " are (see the man page for details): %s"
438
           " The default field list is (in order): %s." %
439
           (", ".join(_LIST_HEADERS), ", ".join(_LIST_DEF_FIELDS))),
440
  'modify': (SetNodeParams, ARGS_ONE,
441
             [DEBUG_OPT, FORCE_OPT,
442
              SUBMIT_OPT,
443
              make_option("-C", "--master-candidate", dest="master_candidate",
444
                          choices=('yes', 'no'), default=None,
445
                          help="Set the master_candidate flag on the node"),
446
              ],
447
             "<instance>", "Alters the parameters of an instance"),
448
  'remove': (RemoveNode, ARGS_ONE, [DEBUG_OPT],
449
             "<node_name>", "Removes a node from the cluster"),
450
  'volumes': (ListVolumes, ARGS_ANY,
451
              [DEBUG_OPT, NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT],
452
              "[<node_name>...]", "List logical volumes on node(s)"),
453
  'list-tags': (ListTags, ARGS_ONE, [DEBUG_OPT],
454
                "<node_name>", "List the tags of the given node"),
455
  'add-tags': (AddTags, ARGS_ATLEAST(1), [DEBUG_OPT, TAG_SRC_OPT],
456
               "<node_name> tag...", "Add tags to the given node"),
457
  'remove-tags': (RemoveTags, ARGS_ATLEAST(1), [DEBUG_OPT, TAG_SRC_OPT],
458
                  "<node_name> tag...", "Remove tags from the given node"),
459
  }
460

    
461

    
462
if __name__ == '__main__':
463
  sys.exit(GenericMain(commands, override={"tag_type": constants.TAG_NODE}))