Statistics
| Branch: | Tag: | Revision:

root / scripts / gnt-node @ bc8e4a1a

History | View | Annotate | Download (17.1 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 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",
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
  op = opcodes.OpQueryNodes(output_fields=selected_fields, names=[src_node])
186
  result = cl.QueryNodes(names=[src_node], fields=selected_fields,
187
                         use_locking=True)
188
  src_node, sinst = result[0]
189

    
190
  if not sinst:
191
    ToStderr("No secondary instances on node %s, exiting.", src_node)
192
    return constants.EXIT_SUCCESS
193

    
194
  if dst_node is not None:
195
    result = cl.QueryNodes(names=[dst_node], fields=["name"], use_locking=True)
196
    dst_node = result[0][0]
197

    
198
    if src_node == dst_node:
199
      raise errors.OpPrereqError("Evacuate node needs different source and"
200
                                 " target nodes (node %s given twice)" %
201
                                 src_node)
202
    txt_msg = "to node %s" % dst_node
203
  else:
204
    txt_msg = "using iallocator %s" % iallocator
205

    
206
  sinst = utils.NiceSort(sinst)
207

    
208
  if not force and not AskUser("Relocate instance(s) %s from node\n"
209
                               " %s %s?" %
210
                               (",".join("'%s'" % name for name in sinst),
211
                               src_node, txt_msg)):
212
    return constants.EXIT_CONFIRMATION
213

    
214
  ops = []
215
  for iname in sinst:
216
    op = opcodes.OpReplaceDisks(instance_name=iname,
217
                                remote_node=dst_node,
218
                                mode=constants.REPLACE_DISK_CHG,
219
                                iallocator=iallocator,
220
                                disks=[])
221
    ops.append(op)
222

    
223
  job_id = cli.SendJob(ops, cl=cl)
224
  cli.PollJob(job_id, cl=cl)
225

    
226

    
227
def FailoverNode(opts, args):
228
  """Failover all primary instance on a node.
229

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

    
236
  """
237
  cl = GetClient()
238
  force = opts.force
239
  selected_fields = ["name", "pinst_list"]
240

    
241
  op = opcodes.OpQueryNodes(output_fields=selected_fields, names=args)
242
  result = SubmitOpCode(op, cl=cl)
243
  node, pinst = result[0]
244

    
245
  if not pinst:
246
    ToStderr("No primary instances on node %s, exiting.", node)
247
    return 0
248

    
249
  pinst = utils.NiceSort(pinst)
250

    
251
  retcode = 0
252

    
253
  if not force and not AskUser("Fail over instance(s) %s?" %
254
                               (",".join("'%s'" % name for name in pinst))):
255
    return 2
256

    
257
  jex = JobExecutor(cl=cl)
258
  for iname in pinst:
259
    op = opcodes.OpFailoverInstance(instance_name=iname,
260
                                    ignore_consistency=opts.ignore_consistency)
261
    jex.QueueJob(iname, op)
262
  results = jex.GetResults()
263
  bad_cnt = len([row for row in results if not row[0]])
264
  if bad_cnt == 0:
265
    ToStdout("All %d instance(s) failed over successfully.", len(results))
266
  else:
267
    ToStdout("There were errors during the failover:\n"
268
             "%d error(s) out of %d instance(s).", bad_cnt, len(results))
269
  return retcode
270

    
271

    
272
def MigrateNode(opts, args):
273
  """Migrate all primary instance on a node.
274

    
275
  """
276
  cl = GetClient()
277
  force = opts.force
278
  selected_fields = ["name", "pinst_list"]
279

    
280
  result = cl.QueryNodes(names=args, fields=selected_fields, use_locking=True)
281
  node, pinst = result[0]
282

    
283
  if not pinst:
284
    ToStdout("No primary instances on node %s, exiting." % node)
285
    return 0
286

    
287
  pinst = utils.NiceSort(pinst)
288

    
289
  retcode = 0
290

    
291
  if not force and not AskUser("Migrate instance(s) %s?" %
292
                               (",".join("'%s'" % name for name in pinst))):
293
    return 2
294

    
295
  jex = JobExecutor(cl=cl)
296
  for iname in pinst:
297
    op = opcodes.OpMigrateInstance(instance_name=iname, live=opts.live,
298
                                   cleanup=False)
299
    jex.QueueJob(iname, op)
300

    
301
  results = jex.GetResults()
302
  bad_cnt = len([row for row in results if not row[0]])
303
  if bad_cnt == 0:
304
    ToStdout("All %d instance(s) migrated successfully.", len(results))
305
  else:
306
    ToStdout("There were errors during the migration:\n"
307
             "%d error(s) out of %d instance(s).", bad_cnt, len(results))
308
  return retcode
309

    
310

    
311
def ShowNodeConfig(opts, args):
312
  """Show node information.
313

    
314
  @param opts: the command line options selected by the user
315
  @type args: list
316
  @param args: should either be an empty list, in which case
317
      we show information about all nodes, or should contain
318
      a list of nodes to be queried for information
319
  @rtype: int
320
  @return: the desired exit code
321

    
322
  """
323
  op = opcodes.OpQueryNodes(output_fields=["name", "pip", "sip",
324
                                           "pinst_list", "sinst_list"],
325
                            names=args)
326
  result = SubmitOpCode(op)
327

    
328
  for name, primary_ip, secondary_ip, pinst, sinst in result:
329
    ToStdout("Node name: %s", name)
330
    ToStdout("  primary ip: %s", primary_ip)
331
    ToStdout("  secondary ip: %s", secondary_ip)
332
    if pinst:
333
      ToStdout("  primary for instances:")
334
      for iname in pinst:
335
        ToStdout("    - %s", iname)
336
    else:
337
      ToStdout("  primary for no instances")
338
    if sinst:
339
      ToStdout("  secondary for instances:")
340
      for iname in sinst:
341
        ToStdout("    - %s", iname)
342
    else:
343
      ToStdout("  secondary for no instances")
344

    
345
  return 0
346

    
347

    
348
def RemoveNode(opts, args):
349
  """Remove a node from the cluster.
350

    
351
  @param opts: the command line options selected by the user
352
  @type args: list
353
  @param args: should contain only one element, the name of
354
      the node to be removed
355
  @rtype: int
356
  @return: the desired exit code
357

    
358
  """
359
  op = opcodes.OpRemoveNode(node_name=args[0])
360
  SubmitOpCode(op)
361
  return 0
362

    
363

    
364
def ListVolumes(opts, args):
365
  """List logical volumes on node(s).
366

    
367
  @param opts: the command line options selected by the user
368
  @type args: list
369
  @param args: should either be an empty list, in which case
370
      we list data for all nodes, or contain a list of nodes
371
      to display data only for those
372
  @rtype: int
373
  @return: the desired exit code
374

    
375
  """
376
  if opts.output is None:
377
    selected_fields = ["node", "phys", "vg",
378
                       "name", "size", "instance"]
379
  else:
380
    selected_fields = opts.output.split(",")
381

    
382
  op = opcodes.OpQueryNodeVolumes(nodes=args, output_fields=selected_fields)
383
  output = SubmitOpCode(op)
384

    
385
  if not opts.no_headers:
386
    headers = {"node": "Node", "phys": "PhysDev",
387
               "vg": "VG", "name": "Name",
388
               "size": "Size", "instance": "Instance"}
389
  else:
390
    headers = None
391

    
392
  unitfields = ["size"]
393

    
394
  numfields = ["size"]
395

    
396
  data = GenerateTable(separator=opts.separator, headers=headers,
397
                       fields=selected_fields, unitfields=unitfields,
398
                       numfields=numfields, data=output, units=opts.units)
399

    
400
  for line in data:
401
    ToStdout(line)
402

    
403
  return 0
404

    
405

    
406
def SetNodeParams(opts, args):
407
  """Modifies a node.
408

    
409
  @param opts: the command line options selected by the user
410
  @type args: list
411
  @param args: should contain only one element, the node name
412
  @rtype: int
413
  @return: the desired exit code
414

    
415
  """
416
  if opts.master_candidate is None and opts.offline is None:
417
    ToStderr("Please give at least one of the parameters.")
418
    return 1
419

    
420
  if opts.master_candidate is not None:
421
    candidate = opts.master_candidate == 'yes'
422
  else:
423
    candidate = None
424
  if opts.offline is not None:
425
    offline = opts.offline == 'yes'
426
  else:
427
    offline = None
428
  op = opcodes.OpSetNodeParams(node_name=args[0],
429
                               master_candidate=candidate,
430
                               offline=offline,
431
                               force=opts.force)
432

    
433
  # even if here we process the result, we allow submit only
434
  result = SubmitOrSend(op, opts)
435

    
436
  if result:
437
    ToStdout("Modified node %s", args[0])
438
    for param, data in result:
439
      ToStdout(" - %-5s -> %s", param, data)
440
  return 0
441

    
442

    
443
commands = {
444
  'add': (AddNode, ARGS_ONE,
445
          [DEBUG_OPT,
446
           make_option("-s", "--secondary-ip", dest="secondary_ip",
447
                       help="Specify the secondary ip for the node",
448
                       metavar="ADDRESS", default=None),
449
           make_option("--readd", dest="readd",
450
                       default=False, action="store_true",
451
                       help="Readd old node after replacing it"),
452
           make_option("--no-ssh-key-check", dest="ssh_key_check",
453
                       default=True, action="store_false",
454
                       help="Disable SSH key fingerprint checking"),
455
           ],
456
          "[-s ip] [--readd] [--no-ssh-key-check] <node_name>",
457
          "Add a node to the cluster"),
458
  'evacuate': (EvacuateNode, ARGS_ONE,
459
               [DEBUG_OPT, FORCE_OPT,
460
                make_option("-n", "--new-secondary", dest="dst_node",
461
                            help="New secondary node", metavar="NODE",
462
                            default=None),
463
                make_option("-i", "--iallocator", metavar="<NAME>",
464
                            help="Select new secondary for the instance"
465
                            " automatically using the"
466
                            " <NAME> iallocator plugin",
467
                            default=None, type="string"),
468
                ],
469
               "[-f] {-i <iallocator> | -n <dst>} <node>",
470
               "Relocate the secondary instances from a node"
471
               " to other nodes (only for instances with drbd disk template)"),
472
  'failover': (FailoverNode, ARGS_ONE,
473
               [DEBUG_OPT, FORCE_OPT,
474
                make_option("--ignore-consistency", dest="ignore_consistency",
475
                            action="store_true", default=False,
476
                            help="Ignore the consistency of the disks on"
477
                            " the secondary"),
478
                ],
479
               "[-f] <node>",
480
               "Stops the primary instances on a node and start them on their"
481
               " secondary node (only for instances with drbd disk template)"),
482
  'migrate': (MigrateNode, ARGS_ONE,
483
               [DEBUG_OPT, FORCE_OPT,
484
                make_option("--non-live", dest="live",
485
                            default=True, action="store_false",
486
                            help="Do a non-live migration (this usually means"
487
                            " freeze the instance, save the state,"
488
                            " transfer and only then resume running on the"
489
                            " secondary node)"),
490
                ],
491
               "[-f] <node>",
492
               "Migrate all the primary instance on a node away from it"
493
               " (only for instances of type drbd)"),
494
  'info': (ShowNodeConfig, ARGS_ANY, [DEBUG_OPT],
495
           "[<node_name>...]", "Show information about the node(s)"),
496
  'list': (ListNodes, ARGS_NONE,
497
           [DEBUG_OPT, NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT,
498
            SUBMIT_OPT, SYNC_OPT],
499
           "", "Lists the nodes in the cluster. The available fields"
500
           " are (see the man page for details): %s"
501
           " The default field list is (in order): %s." %
502
           (", ".join(_LIST_HEADERS), ", ".join(_LIST_DEF_FIELDS))),
503
  'modify': (SetNodeParams, ARGS_ONE,
504
             [DEBUG_OPT, FORCE_OPT,
505
              SUBMIT_OPT,
506
              make_option("-C", "--master-candidate", dest="master_candidate",
507
                          choices=('yes', 'no'), default=None,
508
                          help="Set the master_candidate flag on the node"),
509
              make_option("-O", "--offline", dest="offline",
510
                          choices=('yes', 'no'), default=None,
511
                          help="Set the offline flag on the node"),
512
              ],
513
             "<instance>", "Alters the parameters of an instance"),
514
  'remove': (RemoveNode, ARGS_ONE, [DEBUG_OPT],
515
             "<node_name>", "Removes a node from the cluster"),
516
  'volumes': (ListVolumes, ARGS_ANY,
517
              [DEBUG_OPT, NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT],
518
              "[<node_name>...]", "List logical volumes on node(s)"),
519
  'list-tags': (ListTags, ARGS_ONE, [DEBUG_OPT],
520
                "<node_name>", "List the tags of the given node"),
521
  'add-tags': (AddTags, ARGS_ATLEAST(1), [DEBUG_OPT, TAG_SRC_OPT],
522
               "<node_name> tag...", "Add tags to the given node"),
523
  'remove-tags': (RemoveTags, ARGS_ATLEAST(1), [DEBUG_OPT, TAG_SRC_OPT],
524
                  "<node_name> tag...", "Remove tags from the given node"),
525
  }
526

    
527

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