Statistics
| Branch: | Tag: | Revision:

root / scripts / gnt-node @ b59252fe

History | View | Annotate | Download (18.2 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", "cnodes": "CNodes", "csockets": "CSockets",
54
  "tags": "Tags",
55
  "serial_no": "SerialNo",
56
  "master_candidate": "MasterC",
57
  "master": "IsMaster",
58
  "offline": "Offline", "drained": "Drained",
59
  "role": "Role",
60
  }
61

    
62

    
63
@UsesRPC
64
def AddNode(opts, args):
65
  """Add a node to the cluster.
66

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

    
73
  """
74
  cl = GetClient()
75
  dns_data = utils.HostInfo(args[0])
76
  node = dns_data.name
77
  readd = opts.readd
78

    
79
  try:
80
    output = cl.QueryNodes(names=[node], fields=['name', 'sip'],
81
                           use_locking=False)
82
    node_exists, sip = output[0]
83
  except (errors.OpPrereqError, errors.OpExecError):
84
    node_exists = ""
85
    sip = None
86

    
87
  if readd:
88
    if not node_exists:
89
      ToStderr("Node %s not in the cluster"
90
               " - please retry without '--readd'", node)
91
      return 1
92
  else:
93
    if node_exists:
94
      ToStderr("Node %s already in the cluster (as %s)"
95
               " - please retry with '--readd'", node, node_exists)
96
      return 1
97
    sip = opts.secondary_ip
98

    
99
  # read the cluster name from the master
100
  output = cl.QueryConfigValues(['cluster_name'])
101
  cluster_name = output[0]
102

    
103
  if not readd:
104
    ToStderr("-- WARNING -- \n"
105
             "Performing this operation is going to replace the ssh daemon"
106
             " keypair\n"
107
             "on the target machine (%s) with the ones of the"
108
             " current one\n"
109
             "and grant full intra-cluster ssh root access to/from it\n", node)
110

    
111
  bootstrap.SetupNodeDaemon(cluster_name, node, opts.ssh_key_check)
112

    
113
  op = opcodes.OpAddNode(node_name=args[0], secondary_ip=sip,
114
                         readd=opts.readd)
115
  SubmitOpCode(op)
116

    
117

    
118
def ListNodes(opts, args):
119
  """List nodes and their properties.
120

    
121
  @param opts: the command line options selected by the user
122
  @type args: list
123
  @param args: should be an empty list
124
  @rtype: int
125
  @return: the desired exit code
126

    
127
  """
128
  if opts.output is None:
129
    selected_fields = _LIST_DEF_FIELDS
130
  elif opts.output.startswith("+"):
131
    selected_fields = _LIST_DEF_FIELDS + opts.output[1:].split(",")
132
  else:
133
    selected_fields = opts.output.split(",")
134

    
135
  output = GetClient().QueryNodes(args, selected_fields, opts.do_locking)
136

    
137
  if not opts.no_headers:
138
    headers = _LIST_HEADERS
139
  else:
140
    headers = None
141

    
142
  unitfields = ["dtotal", "dfree", "mtotal", "mnode", "mfree"]
143

    
144
  numfields = ["dtotal", "dfree",
145
               "mtotal", "mnode", "mfree",
146
               "pinst_cnt", "sinst_cnt",
147
               "ctotal", "serial_no"]
148

    
149
  list_type_fields = ("pinst_list", "sinst_list", "tags")
150
  # change raw values to nicer strings
151
  for row in output:
152
    for idx, field in enumerate(selected_fields):
153
      val = row[idx]
154
      if field in list_type_fields:
155
        val = ",".join(val)
156
      elif field in ('master', 'master_candidate', 'offline', 'drained'):
157
        if val:
158
          val = 'Y'
159
        else:
160
          val = 'N'
161
      elif val is None:
162
        val = "?"
163
      row[idx] = str(val)
164

    
165
  data = GenerateTable(separator=opts.separator, headers=headers,
166
                       fields=selected_fields, unitfields=unitfields,
167
                       numfields=numfields, data=output, units=opts.units)
168
  for line in data:
169
    ToStdout(line)
170

    
171
  return 0
172

    
173

    
174
def EvacuateNode(opts, args):
175
  """Relocate all secondary instance from a node.
176

    
177
  @param opts: the command line options selected by the user
178
  @type args: list
179
  @param args: should be an empty list
180
  @rtype: int
181
  @return: the desired exit code
182

    
183
  """
184
  cl = GetClient()
185
  force = opts.force
186

    
187
  dst_node = opts.dst_node
188
  iallocator = opts.iallocator
189

    
190
  cnt = [dst_node, iallocator].count(None)
191
  if cnt != 1:
192
    raise errors.OpPrereqError("One and only one of the -n and -i"
193
                               " options must be passed")
194

    
195
  selected_fields = ["name", "sinst_list"]
196
  src_node = args[0]
197

    
198
  result = cl.QueryNodes(names=[src_node], fields=selected_fields,
199
                         use_locking=False)
200
  src_node, sinst = result[0]
201

    
202
  if not sinst:
203
    ToStderr("No secondary instances on node %s, exiting.", src_node)
204
    return constants.EXIT_SUCCESS
205

    
206
  if dst_node is not None:
207
    result = cl.QueryNodes(names=[dst_node], fields=["name"],
208
                           use_locking=False)
209
    dst_node = result[0][0]
210

    
211
    if src_node == dst_node:
212
      raise errors.OpPrereqError("Evacuate node needs different source and"
213
                                 " target nodes (node %s given twice)" %
214
                                 src_node)
215
    txt_msg = "to node %s" % dst_node
216
  else:
217
    txt_msg = "using iallocator %s" % iallocator
218

    
219
  sinst = utils.NiceSort(sinst)
220

    
221
  if not force and not AskUser("Relocate instance(s) %s from node\n"
222
                               " %s %s?" %
223
                               (",".join("'%s'" % name for name in sinst),
224
                               src_node, txt_msg)):
225
    return constants.EXIT_CONFIRMATION
226

    
227
  ops = []
228
  for iname in sinst:
229
    op = opcodes.OpReplaceDisks(instance_name=iname,
230
                                remote_node=dst_node,
231
                                mode=constants.REPLACE_DISK_CHG,
232
                                iallocator=iallocator,
233
                                disks=[])
234
    ops.append(op)
235

    
236
  job_id = cli.SendJob(ops, cl=cl)
237
  cli.PollJob(job_id, cl=cl)
238

    
239

    
240
def FailoverNode(opts, args):
241
  """Failover all primary instance on a node.
242

    
243
  @param opts: the command line options selected by the user
244
  @type args: list
245
  @param args: should be an empty list
246
  @rtype: int
247
  @return: the desired exit code
248

    
249
  """
250
  cl = GetClient()
251
  force = opts.force
252
  selected_fields = ["name", "pinst_list"]
253

    
254
  # these fields are static data anyway, so it doesn't matter, but
255
  # locking=True should be safer
256
  result = cl.QueryNodes(names=args, fields=selected_fields,
257
                         use_locking=False)
258
  node, pinst = result[0]
259

    
260
  if not pinst:
261
    ToStderr("No primary instances on node %s, exiting.", node)
262
    return 0
263

    
264
  pinst = utils.NiceSort(pinst)
265

    
266
  retcode = 0
267

    
268
  if not force and not AskUser("Fail over instance(s) %s?" %
269
                               (",".join("'%s'" % name for name in pinst))):
270
    return 2
271

    
272
  jex = JobExecutor(cl=cl)
273
  for iname in pinst:
274
    op = opcodes.OpFailoverInstance(instance_name=iname,
275
                                    ignore_consistency=opts.ignore_consistency)
276
    jex.QueueJob(iname, op)
277
  results = jex.GetResults()
278
  bad_cnt = len([row for row in results if not row[0]])
279
  if bad_cnt == 0:
280
    ToStdout("All %d instance(s) failed over successfully.", len(results))
281
  else:
282
    ToStdout("There were errors during the failover:\n"
283
             "%d error(s) out of %d instance(s).", bad_cnt, len(results))
284
  return retcode
285

    
286

    
287
def MigrateNode(opts, args):
288
  """Migrate all primary instance on a node.
289

    
290
  """
291
  cl = GetClient()
292
  force = opts.force
293
  selected_fields = ["name", "pinst_list"]
294

    
295
  result = cl.QueryNodes(names=args, fields=selected_fields, use_locking=False)
296
  node, pinst = result[0]
297

    
298
  if not pinst:
299
    ToStdout("No primary instances on node %s, exiting." % node)
300
    return 0
301

    
302
  pinst = utils.NiceSort(pinst)
303

    
304
  retcode = 0
305

    
306
  if not force and not AskUser("Migrate instance(s) %s?" %
307
                               (",".join("'%s'" % name for name in pinst))):
308
    return 2
309

    
310
  jex = JobExecutor(cl=cl)
311
  for iname in pinst:
312
    op = opcodes.OpMigrateInstance(instance_name=iname, live=opts.live,
313
                                   cleanup=False)
314
    jex.QueueJob(iname, op)
315

    
316
  results = jex.GetResults()
317
  bad_cnt = len([row for row in results if not row[0]])
318
  if bad_cnt == 0:
319
    ToStdout("All %d instance(s) migrated successfully.", len(results))
320
  else:
321
    ToStdout("There were errors during the migration:\n"
322
             "%d error(s) out of %d instance(s).", bad_cnt, len(results))
323
  return retcode
324

    
325

    
326
def ShowNodeConfig(opts, args):
327
  """Show node information.
328

    
329
  @param opts: the command line options selected by the user
330
  @type args: list
331
  @param args: should either be an empty list, in which case
332
      we show information about all nodes, or should contain
333
      a list of nodes to be queried for information
334
  @rtype: int
335
  @return: the desired exit code
336

    
337
  """
338
  cl = GetClient()
339
  result = cl.QueryNodes(fields=["name", "pip", "sip",
340
                                 "pinst_list", "sinst_list",
341
                                 "master_candidate", "drained", "offline"],
342
                         names=args, use_locking=False)
343

    
344
  for (name, primary_ip, secondary_ip, pinst, sinst,
345
       is_mc, drained, offline) in result:
346
    ToStdout("Node name: %s", name)
347
    ToStdout("  primary ip: %s", primary_ip)
348
    ToStdout("  secondary ip: %s", secondary_ip)
349
    ToStdout("  master candidate: %s", is_mc)
350
    ToStdout("  drained: %s", drained)
351
    ToStdout("  offline: %s", offline)
352
    if pinst:
353
      ToStdout("  primary for instances:")
354
      for iname in utils.NiceSort(pinst):
355
        ToStdout("    - %s", iname)
356
    else:
357
      ToStdout("  primary for no instances")
358
    if sinst:
359
      ToStdout("  secondary for instances:")
360
      for iname in utils.NiceSort(sinst):
361
        ToStdout("    - %s", iname)
362
    else:
363
      ToStdout("  secondary for no instances")
364

    
365
  return 0
366

    
367

    
368
def RemoveNode(opts, args):
369
  """Remove a node from the cluster.
370

    
371
  @param opts: the command line options selected by the user
372
  @type args: list
373
  @param args: should contain only one element, the name of
374
      the node to be removed
375
  @rtype: int
376
  @return: the desired exit code
377

    
378
  """
379
  op = opcodes.OpRemoveNode(node_name=args[0])
380
  SubmitOpCode(op)
381
  return 0
382

    
383

    
384
def ListVolumes(opts, args):
385
  """List logical volumes on node(s).
386

    
387
  @param opts: the command line options selected by the user
388
  @type args: list
389
  @param args: should either be an empty list, in which case
390
      we list data for all nodes, or contain a list of nodes
391
      to display data only for those
392
  @rtype: int
393
  @return: the desired exit code
394

    
395
  """
396
  if opts.output is None:
397
    selected_fields = ["node", "phys", "vg",
398
                       "name", "size", "instance"]
399
  else:
400
    selected_fields = opts.output.split(",")
401

    
402
  op = opcodes.OpQueryNodeVolumes(nodes=args, output_fields=selected_fields)
403
  output = SubmitOpCode(op)
404

    
405
  if not opts.no_headers:
406
    headers = {"node": "Node", "phys": "PhysDev",
407
               "vg": "VG", "name": "Name",
408
               "size": "Size", "instance": "Instance"}
409
  else:
410
    headers = None
411

    
412
  unitfields = ["size"]
413

    
414
  numfields = ["size"]
415

    
416
  data = GenerateTable(separator=opts.separator, headers=headers,
417
                       fields=selected_fields, unitfields=unitfields,
418
                       numfields=numfields, data=output, units=opts.units)
419

    
420
  for line in data:
421
    ToStdout(line)
422

    
423
  return 0
424

    
425

    
426
def SetNodeParams(opts, args):
427
  """Modifies a node.
428

    
429
  @param opts: the command line options selected by the user
430
  @type args: list
431
  @param args: should contain only one element, the node name
432
  @rtype: int
433
  @return: the desired exit code
434

    
435
  """
436
  if [opts.master_candidate, opts.drained, opts.offline].count(None) == 3:
437
    ToStderr("Please give at least one of the parameters.")
438
    return 1
439

    
440
  if opts.master_candidate is not None:
441
    candidate = opts.master_candidate == 'yes'
442
  else:
443
    candidate = None
444
  if opts.offline is not None:
445
    offline = opts.offline == 'yes'
446
  else:
447
    offline = None
448

    
449
  if opts.drained is not None:
450
    drained = opts.drained == 'yes'
451
  else:
452
    drained = None
453
  op = opcodes.OpSetNodeParams(node_name=args[0],
454
                               master_candidate=candidate,
455
                               offline=offline,
456
                               drained=drained,
457
                               force=opts.force)
458

    
459
  # even if here we process the result, we allow submit only
460
  result = SubmitOrSend(op, opts)
461

    
462
  if result:
463
    ToStdout("Modified node %s", args[0])
464
    for param, data in result:
465
      ToStdout(" - %-5s -> %s", param, data)
466
  return 0
467

    
468

    
469
commands = {
470
  'add': (AddNode, ARGS_ONE,
471
          [DEBUG_OPT,
472
           make_option("-s", "--secondary-ip", dest="secondary_ip",
473
                       help="Specify the secondary ip for the node",
474
                       metavar="ADDRESS", default=None),
475
           make_option("--readd", dest="readd",
476
                       default=False, action="store_true",
477
                       help="Readd old node after replacing it"),
478
           make_option("--no-ssh-key-check", dest="ssh_key_check",
479
                       default=True, action="store_false",
480
                       help="Disable SSH key fingerprint checking"),
481
           ],
482
          "[-s ip] [--readd] [--no-ssh-key-check] <node_name>",
483
          "Add a node to the cluster"),
484
  'evacuate': (EvacuateNode, ARGS_ONE,
485
               [DEBUG_OPT, FORCE_OPT,
486
                make_option("-n", "--new-secondary", dest="dst_node",
487
                            help="New secondary node", metavar="NODE",
488
                            default=None),
489
                make_option("-I", "--iallocator", metavar="<NAME>",
490
                            help="Select new secondary for the instance"
491
                            " automatically using the"
492
                            " <NAME> iallocator plugin",
493
                            default=None, type="string"),
494
                ],
495
               "[-f] {-I <iallocator> | -n <dst>} <node>",
496
               "Relocate the secondary instances from a node"
497
               " to other nodes (only for instances with drbd disk template)"),
498
  'failover': (FailoverNode, ARGS_ONE,
499
               [DEBUG_OPT, FORCE_OPT,
500
                make_option("--ignore-consistency", dest="ignore_consistency",
501
                            action="store_true", default=False,
502
                            help="Ignore the consistency of the disks on"
503
                            " the secondary"),
504
                ],
505
               "[-f] <node>",
506
               "Stops the primary instances on a node and start them on their"
507
               " secondary node (only for instances with drbd disk template)"),
508
  'migrate': (MigrateNode, ARGS_ONE,
509
               [DEBUG_OPT, FORCE_OPT,
510
                make_option("--non-live", dest="live",
511
                            default=True, action="store_false",
512
                            help="Do a non-live migration (this usually means"
513
                            " freeze the instance, save the state,"
514
                            " transfer and only then resume running on the"
515
                            " secondary node)"),
516
                ],
517
               "[-f] <node>",
518
               "Migrate all the primary instance on a node away from it"
519
               " (only for instances of type drbd)"),
520
  'info': (ShowNodeConfig, ARGS_ANY, [DEBUG_OPT],
521
           "[<node_name>...]", "Show information about the node(s)"),
522
  'list': (ListNodes, ARGS_ANY,
523
           [DEBUG_OPT, NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, SYNC_OPT],
524
           "[nodes...]",
525
           "Lists the nodes in the cluster. The available fields"
526
           " are (see the man page for details): %s"
527
           " The default field list is (in order): %s." %
528
           (", ".join(_LIST_HEADERS), ", ".join(_LIST_DEF_FIELDS))),
529
  'modify': (SetNodeParams, ARGS_ONE,
530
             [DEBUG_OPT, FORCE_OPT,
531
              SUBMIT_OPT,
532
              make_option("-C", "--master-candidate", dest="master_candidate",
533
                          choices=('yes', 'no'), default=None,
534
                          metavar="yes|no",
535
                          help="Set the master_candidate flag on the node"),
536

    
537
              make_option("-O", "--offline", dest="offline", metavar="yes|no",
538
                          choices=('yes', 'no'), default=None,
539
                          help="Set the offline flag on the node"),
540
              make_option("-D", "--drained", dest="drained", metavar="yes|no",
541
                          choices=('yes', 'no'), default=None,
542
                          help="Set the drained flag on the node"),
543
              ],
544
             "<instance>", "Alters the parameters of an instance"),
545
  'remove': (RemoveNode, ARGS_ONE, [DEBUG_OPT],
546
             "<node_name>", "Removes a node from the cluster"),
547
  'volumes': (ListVolumes, ARGS_ANY,
548
              [DEBUG_OPT, NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT],
549
              "[<node_name>...]", "List logical volumes on node(s)"),
550
  'list-tags': (ListTags, ARGS_ONE, [DEBUG_OPT],
551
                "<node_name>", "List the tags of the given node"),
552
  'add-tags': (AddTags, ARGS_ATLEAST(1), [DEBUG_OPT, TAG_SRC_OPT],
553
               "<node_name> tag...", "Add tags to the given node"),
554
  'remove-tags': (RemoveTags, ARGS_ATLEAST(1), [DEBUG_OPT, TAG_SRC_OPT],
555
                  "<node_name> tag...", "Remove tags from the given node"),
556
  }
557

    
558

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