Statistics
| Branch: | Tag: | Revision:

root / scripts / gnt-node @ 07813a9e

History | View | Annotate | Download (18.5 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
  }
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
  readd = opts.readd
77

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

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

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

    
102
  if readd:
103
    # clear the offline and drain flags on the node
104
    ToStdout("Resetting the 'offline' and 'drained' flags due to re-add")
105
    op = opcodes.OpSetNodeParams(node_name=node, force=True,
106
                                 offline=False, drained=False)
107

    
108
    result = SubmitOpCode(op, cl=cl)
109
    if result:
110
      ToStdout("Modified:")
111
      for param, data in result:
112
        ToStdout(" - %-5s -> %s", param, data)
113
  else:
114
    ToStderr("-- WARNING -- \n"
115
             "Performing this operation is going to replace the ssh daemon"
116
             " keypair\n"
117
             "on the target machine (%s) with the ones of the"
118
             " current one\n"
119
             "and grant full intra-cluster ssh root access to/from it\n", node)
120

    
121
  bootstrap.SetupNodeDaemon(cluster_name, node, opts.ssh_key_check)
122

    
123
  op = opcodes.OpAddNode(node_name=args[0], secondary_ip=sip,
124
                         readd=opts.readd)
125
  SubmitOpCode(op)
126

    
127

    
128
def ListNodes(opts, args):
129
  """List nodes and their properties.
130

    
131
  @param opts: the command line options selected by the user
132
  @type args: list
133
  @param args: should be an empty list
134
  @rtype: int
135
  @return: the desired exit code
136

    
137
  """
138
  if opts.output is None:
139
    selected_fields = _LIST_DEF_FIELDS
140
  elif opts.output.startswith("+"):
141
    selected_fields = _LIST_DEF_FIELDS + opts.output[1:].split(",")
142
  else:
143
    selected_fields = opts.output.split(",")
144

    
145
  output = GetClient().QueryNodes(args, selected_fields, opts.do_locking)
146

    
147
  if not opts.no_headers:
148
    headers = _LIST_HEADERS
149
  else:
150
    headers = None
151

    
152
  unitfields = ["dtotal", "dfree", "mtotal", "mnode", "mfree"]
153

    
154
  numfields = ["dtotal", "dfree",
155
               "mtotal", "mnode", "mfree",
156
               "pinst_cnt", "sinst_cnt",
157
               "ctotal", "serial_no"]
158

    
159
  list_type_fields = ("pinst_list", "sinst_list", "tags")
160
  # change raw values to nicer strings
161
  for row in output:
162
    for idx, field in enumerate(selected_fields):
163
      val = row[idx]
164
      if field in list_type_fields:
165
        val = ",".join(val)
166
      elif field in ('master', 'master_candidate', 'offline', 'drained'):
167
        if val:
168
          val = 'Y'
169
        else:
170
          val = 'N'
171
      elif val is None:
172
        val = "?"
173
      row[idx] = str(val)
174

    
175
  data = GenerateTable(separator=opts.separator, headers=headers,
176
                       fields=selected_fields, unitfields=unitfields,
177
                       numfields=numfields, data=output, units=opts.units)
178
  for line in data:
179
    ToStdout(line)
180

    
181
  return 0
182

    
183

    
184
def EvacuateNode(opts, args):
185
  """Relocate all secondary instance from a node.
186

    
187
  @param opts: the command line options selected by the user
188
  @type args: list
189
  @param args: should be an empty list
190
  @rtype: int
191
  @return: the desired exit code
192

    
193
  """
194
  cl = GetClient()
195
  force = opts.force
196

    
197
  dst_node = opts.dst_node
198
  iallocator = opts.iallocator
199

    
200
  cnt = [dst_node, iallocator].count(None)
201
  if cnt != 1:
202
    raise errors.OpPrereqError("One and only one of the -n and -i"
203
                               " options must be passed")
204

    
205
  selected_fields = ["name", "sinst_list"]
206
  src_node = args[0]
207

    
208
  result = cl.QueryNodes(names=[src_node], fields=selected_fields,
209
                         use_locking=True)
210
  src_node, sinst = result[0]
211

    
212
  if not sinst:
213
    ToStderr("No secondary instances on node %s, exiting.", src_node)
214
    return constants.EXIT_SUCCESS
215

    
216
  if dst_node is not None:
217
    result = cl.QueryNodes(names=[dst_node], fields=["name"], use_locking=True)
218
    dst_node = result[0][0]
219

    
220
    if src_node == dst_node:
221
      raise errors.OpPrereqError("Evacuate node needs different source and"
222
                                 " target nodes (node %s given twice)" %
223
                                 src_node)
224
    txt_msg = "to node %s" % dst_node
225
  else:
226
    txt_msg = "using iallocator %s" % iallocator
227

    
228
  sinst = utils.NiceSort(sinst)
229

    
230
  if not force and not AskUser("Relocate instance(s) %s from node\n"
231
                               " %s %s?" %
232
                               (",".join("'%s'" % name for name in sinst),
233
                               src_node, txt_msg)):
234
    return constants.EXIT_CONFIRMATION
235

    
236
  ops = []
237
  for iname in sinst:
238
    op = opcodes.OpReplaceDisks(instance_name=iname,
239
                                remote_node=dst_node,
240
                                mode=constants.REPLACE_DISK_CHG,
241
                                iallocator=iallocator,
242
                                disks=[])
243
    ops.append(op)
244

    
245
  job_id = cli.SendJob(ops, cl=cl)
246
  cli.PollJob(job_id, cl=cl)
247

    
248

    
249
def FailoverNode(opts, args):
250
  """Failover all primary instance on a node.
251

    
252
  @param opts: the command line options selected by the user
253
  @type args: list
254
  @param args: should be an empty list
255
  @rtype: int
256
  @return: the desired exit code
257

    
258
  """
259
  cl = GetClient()
260
  force = opts.force
261
  selected_fields = ["name", "pinst_list"]
262

    
263
  # these fields are static data anyway, so it doesn't matter, but
264
  # locking=True should be safer
265
  result = cl.QueryNodes(names=args, fields=selected_fields,
266
                         use_locking=True)
267
  node, pinst = result[0]
268

    
269
  if not pinst:
270
    ToStderr("No primary instances on node %s, exiting.", node)
271
    return 0
272

    
273
  pinst = utils.NiceSort(pinst)
274

    
275
  retcode = 0
276

    
277
  if not force and not AskUser("Fail over instance(s) %s?" %
278
                               (",".join("'%s'" % name for name in pinst))):
279
    return 2
280

    
281
  jex = JobExecutor(cl=cl)
282
  for iname in pinst:
283
    op = opcodes.OpFailoverInstance(instance_name=iname,
284
                                    ignore_consistency=opts.ignore_consistency)
285
    jex.QueueJob(iname, op)
286
  results = jex.GetResults()
287
  bad_cnt = len([row for row in results if not row[0]])
288
  if bad_cnt == 0:
289
    ToStdout("All %d instance(s) failed over successfully.", len(results))
290
  else:
291
    ToStdout("There were errors during the failover:\n"
292
             "%d error(s) out of %d instance(s).", bad_cnt, len(results))
293
  return retcode
294

    
295

    
296
def MigrateNode(opts, args):
297
  """Migrate all primary instance on a node.
298

    
299
  """
300
  cl = GetClient()
301
  force = opts.force
302
  selected_fields = ["name", "pinst_list"]
303

    
304
  result = cl.QueryNodes(names=args, fields=selected_fields, use_locking=True)
305
  node, pinst = result[0]
306

    
307
  if not pinst:
308
    ToStdout("No primary instances on node %s, exiting." % node)
309
    return 0
310

    
311
  pinst = utils.NiceSort(pinst)
312

    
313
  retcode = 0
314

    
315
  if not force and not AskUser("Migrate instance(s) %s?" %
316
                               (",".join("'%s'" % name for name in pinst))):
317
    return 2
318

    
319
  jex = JobExecutor(cl=cl)
320
  for iname in pinst:
321
    op = opcodes.OpMigrateInstance(instance_name=iname, live=opts.live,
322
                                   cleanup=False)
323
    jex.QueueJob(iname, op)
324

    
325
  results = jex.GetResults()
326
  bad_cnt = len([row for row in results if not row[0]])
327
  if bad_cnt == 0:
328
    ToStdout("All %d instance(s) migrated successfully.", len(results))
329
  else:
330
    ToStdout("There were errors during the migration:\n"
331
             "%d error(s) out of %d instance(s).", bad_cnt, len(results))
332
  return retcode
333

    
334

    
335
def ShowNodeConfig(opts, args):
336
  """Show node information.
337

    
338
  @param opts: the command line options selected by the user
339
  @type args: list
340
  @param args: should either be an empty list, in which case
341
      we show information about all nodes, or should contain
342
      a list of nodes to be queried for information
343
  @rtype: int
344
  @return: the desired exit code
345

    
346
  """
347
  cl = GetClient()
348
  result = cl.QueryNodes(fields=["name", "pip", "sip",
349
                                 "pinst_list", "sinst_list",
350
                                 "master_candidate", "drained", "offline"],
351
                         names=args, use_locking=True)
352

    
353
  for (name, primary_ip, secondary_ip, pinst, sinst,
354
       is_mc, drained, offline) in result:
355
    ToStdout("Node name: %s", name)
356
    ToStdout("  primary ip: %s", primary_ip)
357
    ToStdout("  secondary ip: %s", secondary_ip)
358
    ToStdout("  master candidate: %s", is_mc)
359
    ToStdout("  drained: %s", drained)
360
    ToStdout("  offline: %s", offline)
361
    if pinst:
362
      ToStdout("  primary for instances:")
363
      for iname in utils.NiceSort(pinst):
364
        ToStdout("    - %s", iname)
365
    else:
366
      ToStdout("  primary for no instances")
367
    if sinst:
368
      ToStdout("  secondary for instances:")
369
      for iname in utils.NiceSort(sinst):
370
        ToStdout("    - %s", iname)
371
    else:
372
      ToStdout("  secondary for no instances")
373

    
374
  return 0
375

    
376

    
377
def RemoveNode(opts, args):
378
  """Remove a node from the cluster.
379

    
380
  @param opts: the command line options selected by the user
381
  @type args: list
382
  @param args: should contain only one element, the name of
383
      the node to be removed
384
  @rtype: int
385
  @return: the desired exit code
386

    
387
  """
388
  op = opcodes.OpRemoveNode(node_name=args[0])
389
  SubmitOpCode(op)
390
  return 0
391

    
392

    
393
def ListVolumes(opts, args):
394
  """List logical volumes on node(s).
395

    
396
  @param opts: the command line options selected by the user
397
  @type args: list
398
  @param args: should either be an empty list, in which case
399
      we list data for all nodes, or contain a list of nodes
400
      to display data only for those
401
  @rtype: int
402
  @return: the desired exit code
403

    
404
  """
405
  if opts.output is None:
406
    selected_fields = ["node", "phys", "vg",
407
                       "name", "size", "instance"]
408
  else:
409
    selected_fields = opts.output.split(",")
410

    
411
  op = opcodes.OpQueryNodeVolumes(nodes=args, output_fields=selected_fields)
412
  output = SubmitOpCode(op)
413

    
414
  if not opts.no_headers:
415
    headers = {"node": "Node", "phys": "PhysDev",
416
               "vg": "VG", "name": "Name",
417
               "size": "Size", "instance": "Instance"}
418
  else:
419
    headers = None
420

    
421
  unitfields = ["size"]
422

    
423
  numfields = ["size"]
424

    
425
  data = GenerateTable(separator=opts.separator, headers=headers,
426
                       fields=selected_fields, unitfields=unitfields,
427
                       numfields=numfields, data=output, units=opts.units)
428

    
429
  for line in data:
430
    ToStdout(line)
431

    
432
  return 0
433

    
434

    
435
def SetNodeParams(opts, args):
436
  """Modifies a node.
437

    
438
  @param opts: the command line options selected by the user
439
  @type args: list
440
  @param args: should contain only one element, the node name
441
  @rtype: int
442
  @return: the desired exit code
443

    
444
  """
445
  if [opts.master_candidate, opts.drained, opts.offline].count(None) == 3:
446
    ToStderr("Please give at least one of the parameters.")
447
    return 1
448

    
449
  if opts.master_candidate is not None:
450
    candidate = opts.master_candidate == 'yes'
451
  else:
452
    candidate = None
453
  if opts.offline is not None:
454
    offline = opts.offline == 'yes'
455
  else:
456
    offline = None
457

    
458
  if opts.drained is not None:
459
    drained = opts.drained == 'yes'
460
  else:
461
    drained = None
462
  op = opcodes.OpSetNodeParams(node_name=args[0],
463
                               master_candidate=candidate,
464
                               offline=offline,
465
                               drained=drained,
466
                               force=opts.force)
467

    
468
  # even if here we process the result, we allow submit only
469
  result = SubmitOrSend(op, opts)
470

    
471
  if result:
472
    ToStdout("Modified node %s", args[0])
473
    for param, data in result:
474
      ToStdout(" - %-5s -> %s", param, data)
475
  return 0
476

    
477

    
478
commands = {
479
  'add': (AddNode, ARGS_ONE,
480
          [DEBUG_OPT,
481
           make_option("-s", "--secondary-ip", dest="secondary_ip",
482
                       help="Specify the secondary ip for the node",
483
                       metavar="ADDRESS", default=None),
484
           make_option("--readd", dest="readd",
485
                       default=False, action="store_true",
486
                       help="Readd old node after replacing it"),
487
           make_option("--no-ssh-key-check", dest="ssh_key_check",
488
                       default=True, action="store_false",
489
                       help="Disable SSH key fingerprint checking"),
490
           ],
491
          "[-s ip] [--readd] [--no-ssh-key-check] <node_name>",
492
          "Add a node to the cluster"),
493
  'evacuate': (EvacuateNode, ARGS_ONE,
494
               [DEBUG_OPT, FORCE_OPT,
495
                make_option("-n", "--new-secondary", dest="dst_node",
496
                            help="New secondary node", metavar="NODE",
497
                            default=None),
498
                make_option("-I", "--iallocator", metavar="<NAME>",
499
                            help="Select new secondary for the instance"
500
                            " automatically using the"
501
                            " <NAME> iallocator plugin",
502
                            default=None, type="string"),
503
                ],
504
               "[-f] {-I <iallocator> | -n <dst>} <node>",
505
               "Relocate the secondary instances from a node"
506
               " to other nodes (only for instances with drbd disk template)"),
507
  'failover': (FailoverNode, ARGS_ONE,
508
               [DEBUG_OPT, FORCE_OPT,
509
                make_option("--ignore-consistency", dest="ignore_consistency",
510
                            action="store_true", default=False,
511
                            help="Ignore the consistency of the disks on"
512
                            " the secondary"),
513
                ],
514
               "[-f] <node>",
515
               "Stops the primary instances on a node and start them on their"
516
               " secondary node (only for instances with drbd disk template)"),
517
  'migrate': (MigrateNode, ARGS_ONE,
518
               [DEBUG_OPT, FORCE_OPT,
519
                make_option("--non-live", dest="live",
520
                            default=True, action="store_false",
521
                            help="Do a non-live migration (this usually means"
522
                            " freeze the instance, save the state,"
523
                            " transfer and only then resume running on the"
524
                            " secondary node)"),
525
                ],
526
               "[-f] <node>",
527
               "Migrate all the primary instance on a node away from it"
528
               " (only for instances of type drbd)"),
529
  'info': (ShowNodeConfig, ARGS_ANY, [DEBUG_OPT],
530
           "[<node_name>...]", "Show information about the node(s)"),
531
  'list': (ListNodes, ARGS_ANY,
532
           [DEBUG_OPT, NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, SYNC_OPT],
533
           "[nodes...]",
534
           "Lists the nodes in the cluster. The available fields"
535
           " are (see the man page for details): %s"
536
           " The default field list is (in order): %s." %
537
           (", ".join(_LIST_HEADERS), ", ".join(_LIST_DEF_FIELDS))),
538
  'modify': (SetNodeParams, ARGS_ONE,
539
             [DEBUG_OPT, FORCE_OPT,
540
              SUBMIT_OPT,
541
              make_option("-C", "--master-candidate", dest="master_candidate",
542
                          choices=('yes', 'no'), default=None,
543
                          help="Set the master_candidate flag on the node"),
544
              make_option("-O", "--offline", dest="offline",
545
                          choices=('yes', 'no'), default=None,
546
                          help="Set the offline flag on the node"),
547
              make_option("-D", "--drained", dest="drained",
548
                          choices=('yes', 'no'), default=None,
549
                          help="Set the drained flag on the node"),
550
              ],
551
             "<instance>", "Alters the parameters of an instance"),
552
  'remove': (RemoveNode, ARGS_ONE, [DEBUG_OPT],
553
             "<node_name>", "Removes a node from the cluster"),
554
  'volumes': (ListVolumes, ARGS_ANY,
555
              [DEBUG_OPT, NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT],
556
              "[<node_name>...]", "List logical volumes on node(s)"),
557
  'list-tags': (ListTags, ARGS_ONE, [DEBUG_OPT],
558
                "<node_name>", "List the tags of the given node"),
559
  'add-tags': (AddTags, ARGS_ATLEAST(1), [DEBUG_OPT, TAG_SRC_OPT],
560
               "<node_name> tag...", "Add tags to the given node"),
561
  'remove-tags': (RemoveTags, ARGS_ATLEAST(1), [DEBUG_OPT, TAG_SRC_OPT],
562
                  "<node_name> tag...", "Remove tags from the given node"),
563
  }
564

    
565

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