Statistics
| Branch: | Tag: | Revision:

root / scripts / gnt-node @ 77921a95

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=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 ListVolumes(opts, args):
395
  """List logical volumes on node(s).
396

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

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

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

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

    
422
  unitfields = ["size"]
423

    
424
  numfields = ["size"]
425

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

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

    
433
  return 0
434

    
435

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

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

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

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

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

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

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

    
478

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

    
566

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