Statistics
| Branch: | Tag: | Revision:

root / scripts / gnt-node @ 5af3da74

History | View | Annotate | Download (19.3 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=False)
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=False)
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"],
218
                           use_locking=False)
219
    dst_node = result[0][0]
220

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

    
229
  sinst = utils.NiceSort(sinst)
230

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

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

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

    
249

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

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

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

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

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

    
274
  pinst = utils.NiceSort(pinst)
275

    
276
  retcode = 0
277

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

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

    
296

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

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

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

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

    
312
  pinst = utils.NiceSort(pinst)
313

    
314
  retcode = 0
315

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

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

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

    
335

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

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

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

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

    
375
  return 0
376

    
377

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

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

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

    
393

    
394
def PowercycleNode(opts, args):
395
  """Remove a node from the cluster.
396

    
397
  @param opts: the command line options selected by the user
398
  @type args: list
399
  @param args: should contain only one element, the name of
400
      the node to be removed
401
  @rtype: int
402
  @return: the desired exit code
403

    
404
  """
405
  node = args[0]
406
  if (not opts.confirm and
407
      not AskUser("Are you sure you want to hard powercycle node %s?" % node)):
408
    return 2
409

    
410
  op = opcodes.OpPowercycleNode(node_name=node, force=opts.force)
411
  result = SubmitOpCode(op)
412
  ToStderr(result)
413
  return 0
414

    
415

    
416
def ListVolumes(opts, args):
417
  """List logical volumes on node(s).
418

    
419
  @param opts: the command line options selected by the user
420
  @type args: list
421
  @param args: should either be an empty list, in which case
422
      we list data for all nodes, or contain a list of nodes
423
      to display data only for those
424
  @rtype: int
425
  @return: the desired exit code
426

    
427
  """
428
  if opts.output is None:
429
    selected_fields = ["node", "phys", "vg",
430
                       "name", "size", "instance"]
431
  else:
432
    selected_fields = opts.output.split(",")
433

    
434
  op = opcodes.OpQueryNodeVolumes(nodes=args, output_fields=selected_fields)
435
  output = SubmitOpCode(op)
436

    
437
  if not opts.no_headers:
438
    headers = {"node": "Node", "phys": "PhysDev",
439
               "vg": "VG", "name": "Name",
440
               "size": "Size", "instance": "Instance"}
441
  else:
442
    headers = None
443

    
444
  unitfields = ["size"]
445

    
446
  numfields = ["size"]
447

    
448
  data = GenerateTable(separator=opts.separator, headers=headers,
449
                       fields=selected_fields, unitfields=unitfields,
450
                       numfields=numfields, data=output, units=opts.units)
451

    
452
  for line in data:
453
    ToStdout(line)
454

    
455
  return 0
456

    
457

    
458
def SetNodeParams(opts, args):
459
  """Modifies a node.
460

    
461
  @param opts: the command line options selected by the user
462
  @type args: list
463
  @param args: should contain only one element, the node name
464
  @rtype: int
465
  @return: the desired exit code
466

    
467
  """
468
  if [opts.master_candidate, opts.drained, opts.offline].count(None) == 3:
469
    ToStderr("Please give at least one of the parameters.")
470
    return 1
471

    
472
  if opts.master_candidate is not None:
473
    candidate = opts.master_candidate == 'yes'
474
  else:
475
    candidate = None
476
  if opts.offline is not None:
477
    offline = opts.offline == 'yes'
478
  else:
479
    offline = None
480

    
481
  if opts.drained is not None:
482
    drained = opts.drained == 'yes'
483
  else:
484
    drained = None
485
  op = opcodes.OpSetNodeParams(node_name=args[0],
486
                               master_candidate=candidate,
487
                               offline=offline,
488
                               drained=drained,
489
                               force=opts.force)
490

    
491
  # even if here we process the result, we allow submit only
492
  result = SubmitOrSend(op, opts)
493

    
494
  if result:
495
    ToStdout("Modified node %s", args[0])
496
    for param, data in result:
497
      ToStdout(" - %-5s -> %s", param, data)
498
  return 0
499

    
500

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

    
569
              make_option("-O", "--offline", dest="offline", metavar="yes|no",
570
                          choices=('yes', 'no'), default=None,
571
                          help="Set the offline flag on the node"),
572
              make_option("-D", "--drained", dest="drained", metavar="yes|no",
573
                          choices=('yes', 'no'), default=None,
574
                          help="Set the drained flag on the node"),
575
              ],
576
             "<instance>", "Alters the parameters of an instance"),
577
  'powercycle': (PowercycleNode, ARGS_ONE, [DEBUG_OPT, FORCE_OPT, CONFIRM_OPT],
578
                 "<node_name>", "Tries to forcefully powercycle a node"),
579
  'remove': (RemoveNode, ARGS_ONE, [DEBUG_OPT],
580
             "<node_name>", "Removes a node from the cluster"),
581
  'volumes': (ListVolumes, ARGS_ANY,
582
              [DEBUG_OPT, NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT],
583
              "[<node_name>...]", "List logical volumes on node(s)"),
584
  'list-tags': (ListTags, ARGS_ONE, [DEBUG_OPT],
585
                "<node_name>", "List the tags of the given node"),
586
  'add-tags': (AddTags, ARGS_ATLEAST(1), [DEBUG_OPT, TAG_SRC_OPT],
587
               "<node_name> tag...", "Add tags to the given node"),
588
  'remove-tags': (RemoveTags, ARGS_ATLEAST(1), [DEBUG_OPT, TAG_SRC_OPT],
589
                  "<node_name> tag...", "Remove tags from the given node"),
590
  }
591

    
592

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