Statistics
| Branch: | Tag: | Revision:

root / scripts / gnt-node @ 4007f57d

History | View | Annotate | Download (19.9 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
  op = opcodes.OpEvacuateNode(node_name=args[0], remote_node=dst_node,
228
                              iallocator=iallocator)
229
  SubmitOpCode(op, cl=cl)
230

    
231

    
232
def FailoverNode(opts, args):
233
  """Failover all primary instance on a node.
234

    
235
  @param opts: the command line options selected by the user
236
  @type args: list
237
  @param args: should be an empty list
238
  @rtype: int
239
  @return: the desired exit code
240

    
241
  """
242
  cl = GetClient()
243
  force = opts.force
244
  selected_fields = ["name", "pinst_list"]
245

    
246
  # these fields are static data anyway, so it doesn't matter, but
247
  # locking=True should be safer
248
  result = cl.QueryNodes(names=args, fields=selected_fields,
249
                         use_locking=False)
250
  node, pinst = result[0]
251

    
252
  if not pinst:
253
    ToStderr("No primary instances on node %s, exiting.", node)
254
    return 0
255

    
256
  pinst = utils.NiceSort(pinst)
257

    
258
  retcode = 0
259

    
260
  if not force and not AskUser("Fail over instance(s) %s?" %
261
                               (",".join("'%s'" % name for name in pinst))):
262
    return 2
263

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

    
278

    
279
def MigrateNode(opts, args):
280
  """Migrate all primary instance on a node.
281

    
282
  """
283
  cl = GetClient()
284
  force = opts.force
285
  selected_fields = ["name", "pinst_list"]
286

    
287
  result = cl.QueryNodes(names=args, fields=selected_fields, use_locking=False)
288
  node, pinst = result[0]
289

    
290
  if not pinst:
291
    ToStdout("No primary instances on node %s, exiting." % node)
292
    return 0
293

    
294
  pinst = utils.NiceSort(pinst)
295

    
296
  retcode = 0
297

    
298
  if not force and not AskUser("Migrate instance(s) %s?" %
299
                               (",".join("'%s'" % name for name in pinst))):
300
    return 2
301

    
302
  op = opcodes.OpMigrateNode(node_name=args[0], live=opts.live)
303
  SubmitOpCode(op, cl=cl)
304

    
305

    
306
def ShowNodeConfig(opts, args):
307
  """Show node information.
308

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

    
317
  """
318
  cl = GetClient()
319
  result = cl.QueryNodes(fields=["name", "pip", "sip",
320
                                 "pinst_list", "sinst_list",
321
                                 "master_candidate", "drained", "offline"],
322
                         names=args, use_locking=False)
323

    
324
  for (name, primary_ip, secondary_ip, pinst, sinst,
325
       is_mc, drained, offline) in result:
326
    ToStdout("Node name: %s", name)
327
    ToStdout("  primary ip: %s", primary_ip)
328
    ToStdout("  secondary ip: %s", secondary_ip)
329
    ToStdout("  master candidate: %s", is_mc)
330
    ToStdout("  drained: %s", drained)
331
    ToStdout("  offline: %s", offline)
332
    if pinst:
333
      ToStdout("  primary for instances:")
334
      for iname in utils.NiceSort(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 utils.NiceSort(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 PowercycleNode(opts, args):
365
  """Remove a node from the cluster.
366

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

    
374
  """
375
  node = args[0]
376
  if (not opts.confirm and
377
      not AskUser("Are you sure you want to hard powercycle node %s?" % node)):
378
    return 2
379

    
380
  op = opcodes.OpPowercycleNode(node_name=node, force=opts.force)
381
  result = SubmitOpCode(op)
382
  ToStderr(result)
383
  return 0
384

    
385

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

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

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

    
404
  op = opcodes.OpQueryNodeVolumes(nodes=args, output_fields=selected_fields)
405
  output = SubmitOpCode(op)
406

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

    
414
  unitfields = ["size"]
415

    
416
  numfields = ["size"]
417

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

    
422
  for line in data:
423
    ToStdout(line)
424

    
425
  return 0
426

    
427

    
428
def ListPhysicalVolumes(opts, args):
429
  """List physical volumes on node(s).
430

    
431
  @param opts: the command line options selected by the user
432
  @type args: list
433
  @param args: should either be an empty list, in which case
434
      we list data for all nodes, or contain a list of nodes
435
      to display data only for those
436
  @rtype: int
437
  @return: the desired exit code
438

    
439
  """
440
  if opts.output is None:
441
    selected_fields = ["node", constants.SF_NAME, constants.SF_SIZE,
442
                       constants.SF_USED, constants.SF_FREE]
443
  else:
444
    selected_fields = opts.output.split(",")
445

    
446
  op = opcodes.OpQueryNodeStorage(nodes=args,
447
                                  storage_type=constants.ST_LVM_PV,
448
                                  output_fields=selected_fields)
449
  output = SubmitOpCode(op)
450

    
451
  if not opts.no_headers:
452
    headers = {
453
      "node": "Node",
454
      constants.SF_NAME: "Name",
455
      constants.SF_SIZE: "Size",
456
      constants.SF_USED: "Used",
457
      constants.SF_FREE: "Free",
458
      constants.SF_ALLOCATABLE: "Allocatable",
459
      }
460
  else:
461
    headers = None
462

    
463
  unitfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
464
  numfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
465

    
466
  data = GenerateTable(separator=opts.separator, headers=headers,
467
                       fields=selected_fields, unitfields=unitfields,
468
                       numfields=numfields, data=output, units=opts.units)
469

    
470
  for line in data:
471
    ToStdout(line)
472

    
473
  return 0
474

    
475

    
476
def SetNodeParams(opts, args):
477
  """Modifies a node.
478

    
479
  @param opts: the command line options selected by the user
480
  @type args: list
481
  @param args: should contain only one element, the node name
482
  @rtype: int
483
  @return: the desired exit code
484

    
485
  """
486
  if [opts.master_candidate, opts.drained, opts.offline].count(None) == 3:
487
    ToStderr("Please give at least one of the parameters.")
488
    return 1
489

    
490
  if opts.master_candidate is not None:
491
    candidate = opts.master_candidate == 'yes'
492
  else:
493
    candidate = None
494
  if opts.offline is not None:
495
    offline = opts.offline == 'yes'
496
  else:
497
    offline = None
498

    
499
  if opts.drained is not None:
500
    drained = opts.drained == 'yes'
501
  else:
502
    drained = None
503
  op = opcodes.OpSetNodeParams(node_name=args[0],
504
                               master_candidate=candidate,
505
                               offline=offline,
506
                               drained=drained,
507
                               force=opts.force)
508

    
509
  # even if here we process the result, we allow submit only
510
  result = SubmitOrSend(op, opts)
511

    
512
  if result:
513
    ToStdout("Modified node %s", args[0])
514
    for param, data in result:
515
      ToStdout(" - %-5s -> %s", param, data)
516
  return 0
517

    
518

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

    
587
              make_option("-O", "--offline", dest="offline", metavar="yes|no",
588
                          choices=('yes', 'no'), default=None,
589
                          help="Set the offline flag on the node"),
590
              make_option("-D", "--drained", dest="drained", metavar="yes|no",
591
                          choices=('yes', 'no'), default=None,
592
                          help="Set the drained flag on the node"),
593
              ],
594
             "<instance>", "Alters the parameters of an instance"),
595
  'powercycle': (PowercycleNode, ARGS_ONE, [DEBUG_OPT, FORCE_OPT, CONFIRM_OPT],
596
                 "<node_name>", "Tries to forcefully powercycle a node"),
597
  'remove': (RemoveNode, ARGS_ONE, [DEBUG_OPT],
598
             "<node_name>", "Removes a node from the cluster"),
599
  'volumes': (ListVolumes, ARGS_ANY,
600
              [DEBUG_OPT, NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT],
601
              "[<node_name>...]", "List logical volumes on node(s)"),
602
  'physical-volumes': (ListPhysicalVolumes, ARGS_ANY,
603
                       [DEBUG_OPT, NOHDR_OPT, SEP_OPT, USEUNITS_OPT,
604
                        FIELDS_OPT],
605
                       "[<node_name>...]",
606
                       "List physical volumes on node(s)"),
607
  'list-tags': (ListTags, ARGS_ONE, [DEBUG_OPT],
608
                "<node_name>", "List the tags of the given node"),
609
  'add-tags': (AddTags, ARGS_ATLEAST(1), [DEBUG_OPT, TAG_SRC_OPT],
610
               "<node_name> tag...", "Add tags to the given node"),
611
  'remove-tags': (RemoveTags, ARGS_ATLEAST(1), [DEBUG_OPT, TAG_SRC_OPT],
612
                  "<node_name> tag...", "Remove tags from the given node"),
613
  }
614

    
615

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