gnt-node 17.4 KB
Newer Older
Iustin Pop's avatar
Iustin Pop committed
1
2
3
#!/usr/bin/python
#

4
# Copyright (C) 2006, 2007, 2008 Google Inc.
Iustin Pop's avatar
Iustin Pop committed
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.


22
23
24
25
# pylint: disable-msg=W0401,W0614
# W0401: Wildcard import ganeti.cli
# W0614: Unused import %s from wildcard import (since we need cli)

Iustin Pop's avatar
Iustin Pop committed
26
27
28
29
import sys
from optparse import make_option

from ganeti.cli import *
30
from ganeti import cli
Iustin Pop's avatar
Iustin Pop committed
31
32
from ganeti import opcodes
from ganeti import utils
33
from ganeti import constants
Iustin Pop's avatar
Iustin Pop committed
34
from ganeti import errors
35
from ganeti import bootstrap
Iustin Pop's avatar
Iustin Pop committed
36
37


38
#: default list of field for L{ListNodes}
39
40
41
42
43
44
_LIST_DEF_FIELDS = [
  "name", "dtotal", "dfree",
  "mtotal", "mnode", "mfree",
  "pinst_cnt", "sinst_cnt",
  ]

45
46
47
48
49
50
51
52
#: headers (and full field list for L{ListNodes}
_LIST_HEADERS = {
  "name": "Node", "pinst_cnt": "Pinst", "sinst_cnt": "Sinst",
  "pinst_list": "PriInstances", "sinst_list": "SecInstances",
  "pip": "PrimaryIP", "sip": "SecondaryIP",
  "dtotal": "DTotal", "dfree": "DFree",
  "mtotal": "MTotal", "mnode": "MNode", "mfree": "MFree",
  "bootid": "BootID",
53
  "ctotal": "CTotal", "cnodes": "CNodes", "csockets": "CSockets",
54
55
56
57
  "tags": "Tags",
  "serial_no": "SerialNo",
  "master_candidate": "MasterC",
  "master": "IsMaster",
58
  "offline": "Offline", "drained": "Drained",
59
60
  }

Michael Hanselmann's avatar
Michael Hanselmann committed
61

62
@UsesRPC
Iustin Pop's avatar
Iustin Pop committed
63
def AddNode(opts, args):
64
65
66
67
68
69
70
  """Add a node to the cluster.

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

  """
Iustin Pop's avatar
Iustin Pop committed
73
  cl = GetClient()
74
75
76
77
78
  dns_data = utils.HostInfo(args[0])
  node = dns_data.name

  if not opts.readd:
    try:
79
      output = cl.QueryNodes(names=[node], fields=['name'], use_locking=True)
80
81
82
    except (errors.OpPrereqError, errors.OpExecError):
      pass
    else:
83
84
      ToStderr("Node %s already in the cluster (as %s)"
               " - please use --readd", args[0], output[0][0])
85
86
      return 1

Iustin Pop's avatar
Iustin Pop committed
87
88
89
90
  # read the cluster name from the master
  output = cl.QueryConfigValues(['cluster_name'])
  cluster_name = output[0]

91
92
93
94
95
96
  ToStderr("-- WARNING -- \n"
           "Performing this operation is going to replace the ssh daemon"
           " keypair\n"
           "on the target machine (%s) with the ones of the"
           " current one\n"
           "and grant full intra-cluster ssh root access to/from it\n", node)
97

Iustin Pop's avatar
Iustin Pop committed
98
  bootstrap.SetupNodeDaemon(cluster_name, node, opts.ssh_key_check)
99

100
101
  op = opcodes.OpAddNode(node_name=args[0], secondary_ip=opts.secondary_ip,
                         readd=opts.readd)
Iustin Pop's avatar
Iustin Pop committed
102
103
104
105
106
107
  SubmitOpCode(op)


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

108
109
110
111
112
113
  @param opts: the command line options selected by the user
  @type args: list
  @param args: should be an empty list
  @rtype: int
  @return: the desired exit code

Iustin Pop's avatar
Iustin Pop committed
114
115
  """
  if opts.output is None:
116
117
118
    selected_fields = _LIST_DEF_FIELDS
  elif opts.output.startswith("+"):
    selected_fields = _LIST_DEF_FIELDS + opts.output[1:].split(",")
Iustin Pop's avatar
Iustin Pop committed
119
120
121
  else:
    selected_fields = opts.output.split(",")

Iustin Pop's avatar
Iustin Pop committed
122
  output = GetClient().QueryNodes([], selected_fields, opts.do_locking)
Iustin Pop's avatar
Iustin Pop committed
123
124

  if not opts.no_headers:
125
    headers = _LIST_HEADERS
126
127
128
  else:
    headers = None

129
  unitfields = ["dtotal", "dfree", "mtotal", "mnode", "mfree"]
130

131
132
  numfields = ["dtotal", "dfree",
               "mtotal", "mnode", "mfree",
133
               "pinst_cnt", "sinst_cnt",
134
               "ctotal", "serial_no"]
135

136
  list_type_fields = ("pinst_list", "sinst_list", "tags")
137
138
139
140
  # change raw values to nicer strings
  for row in output:
    for idx, field in enumerate(selected_fields):
      val = row[idx]
141
      if field in list_type_fields:
142
        val = ",".join(val)
143
      elif field in ('master', 'master_candidate', 'offline', 'drained'):
144
145
146
147
        if val:
          val = 'Y'
        else:
          val = 'N'
148
149
150
      elif val is None:
        val = "?"
      row[idx] = str(val)
151

152
153
  data = GenerateTable(separator=opts.separator, headers=headers,
                       fields=selected_fields, unitfields=unitfields,
154
                       numfields=numfields, data=output, units=opts.units)
155
  for line in data:
156
    ToStdout(line)
Iustin Pop's avatar
Iustin Pop committed
157
158
159
160

  return 0


Iustin Pop's avatar
Iustin Pop committed
161
162
163
def EvacuateNode(opts, args):
  """Relocate all secondary instance from a node.

164
165
166
167
168
169
  @param opts: the command line options selected by the user
  @type args: list
  @param args: should be an empty list
  @rtype: int
  @return: the desired exit code

Iustin Pop's avatar
Iustin Pop committed
170
  """
171
  cl = GetClient()
Iustin Pop's avatar
Iustin Pop committed
172
  force = opts.force
173
174
175
176
177
178
179
180
181

  dst_node = opts.dst_node
  iallocator = opts.iallocator

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

Iustin Pop's avatar
Iustin Pop committed
182
  selected_fields = ["name", "sinst_list"]
183
  src_node = args[0]
Iustin Pop's avatar
Iustin Pop committed
184

185
186
  result = cl.QueryNodes(names=[src_node], fields=selected_fields,
                         use_locking=True)
Iustin Pop's avatar
Iustin Pop committed
187
188
189
  src_node, sinst = result[0]

  if not sinst:
190
    ToStderr("No secondary instances on node %s, exiting.", src_node)
Iustin Pop's avatar
Iustin Pop committed
191
192
    return constants.EXIT_SUCCESS

193
  if dst_node is not None:
194
    result = cl.QueryNodes(names=[dst_node], fields=["name"], use_locking=True)
195
196
197
198
199
200
201
202
203
204
    dst_node = result[0][0]

    if src_node == dst_node:
      raise errors.OpPrereqError("Evacuate node needs different source and"
                                 " target nodes (node %s given twice)" %
                                 src_node)
    txt_msg = "to node %s" % dst_node
  else:
    txt_msg = "using iallocator %s" % iallocator

Iustin Pop's avatar
Iustin Pop committed
205
206
207
  sinst = utils.NiceSort(sinst)

  if not force and not AskUser("Relocate instance(s) %s from node\n"
208
                               " %s %s?" %
Iustin Pop's avatar
Iustin Pop committed
209
                               (",".join("'%s'" % name for name in sinst),
210
                               src_node, txt_msg)):
Iustin Pop's avatar
Iustin Pop committed
211
212
    return constants.EXIT_CONFIRMATION

213
  ops = []
Iustin Pop's avatar
Iustin Pop committed
214
215
  for iname in sinst:
    op = opcodes.OpReplaceDisks(instance_name=iname,
216
                                remote_node=dst_node,
217
                                mode=constants.REPLACE_DISK_CHG,
218
                                iallocator=iallocator,
219
                                disks=[])
220
    ops.append(op)
221

222
223
  job_id = cli.SendJob(ops, cl=cl)
  cli.PollJob(job_id, cl=cl)
Iustin Pop's avatar
Iustin Pop committed
224
225


Iustin Pop's avatar
Iustin Pop committed
226
227
228
def FailoverNode(opts, args):
  """Failover all primary instance on a node.

229
230
231
232
233
234
  @param opts: the command line options selected by the user
  @type args: list
  @param args: should be an empty list
  @rtype: int
  @return: the desired exit code

Iustin Pop's avatar
Iustin Pop committed
235
  """
236
  cl = GetClient()
Iustin Pop's avatar
Iustin Pop committed
237
238
239
  force = opts.force
  selected_fields = ["name", "pinst_list"]

240
241
242
243
  # these fields are static data anyway, so it doesn't matter, but
  # locking=True should be safer
  result = cl.QueryNodes(names=args, fields=selected_fields,
                         use_locking=True)
Iustin Pop's avatar
Iustin Pop committed
244
245
246
  node, pinst = result[0]

  if not pinst:
247
    ToStderr("No primary instances on node %s, exiting.", node)
Iustin Pop's avatar
Iustin Pop committed
248
249
250
251
252
253
254
255
256
257
    return 0

  pinst = utils.NiceSort(pinst)

  retcode = 0

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

258
  jex = JobExecutor(cl=cl)
Iustin Pop's avatar
Iustin Pop committed
259
260
261
  for iname in pinst:
    op = opcodes.OpFailoverInstance(instance_name=iname,
                                    ignore_consistency=opts.ignore_consistency)
262
263
264
265
266
    jex.QueueJob(iname, op)
  results = jex.GetResults()
  bad_cnt = len([row for row in results if not row[0]])
  if bad_cnt == 0:
    ToStdout("All %d instance(s) failed over successfully.", len(results))
Iustin Pop's avatar
Iustin Pop committed
267
  else:
268
    ToStdout("There were errors during the failover:\n"
269
             "%d error(s) out of %d instance(s).", bad_cnt, len(results))
Iustin Pop's avatar
Iustin Pop committed
270
271
272
  return retcode


Iustin Pop's avatar
Iustin Pop committed
273
274
275
276
277
278
279
280
def MigrateNode(opts, args):
  """Migrate all primary instance on a node.

  """
  cl = GetClient()
  force = opts.force
  selected_fields = ["name", "pinst_list"]

281
  result = cl.QueryNodes(names=args, fields=selected_fields, use_locking=True)
Iustin Pop's avatar
Iustin Pop committed
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
  node, pinst = result[0]

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

  pinst = utils.NiceSort(pinst)

  retcode = 0

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

  jex = JobExecutor(cl=cl)
  for iname in pinst:
    op = opcodes.OpMigrateInstance(instance_name=iname, live=opts.live,
                                   cleanup=False)
    jex.QueueJob(iname, op)

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


Iustin Pop's avatar
Iustin Pop committed
312
313
314
def ShowNodeConfig(opts, args):
  """Show node information.

315
316
317
318
319
320
321
322
  @param opts: the command line options selected by the user
  @type args: list
  @param args: should either be an empty list, in which case
      we show information about all nodes, or should contain
      a list of nodes to be queried for information
  @rtype: int
  @return: the desired exit code

Iustin Pop's avatar
Iustin Pop committed
323
  """
324
325
  cl = GetClient()
  result = cl.QueryNodes(fields=["name", "pip", "sip",
326
327
                                 "pinst_list", "sinst_list",
                                 "master_candidate", "drained", "offline"],
328
                         names=args, use_locking=True)
Iustin Pop's avatar
Iustin Pop committed
329

330
331
  for (name, primary_ip, secondary_ip, pinst, sinst,
       is_mc, drained, offline) in result:
332
333
334
    ToStdout("Node name: %s", name)
    ToStdout("  primary ip: %s", primary_ip)
    ToStdout("  secondary ip: %s", secondary_ip)
335
336
337
    ToStdout("  master candidate: %s", is_mc)
    ToStdout("  drained: %s", drained)
    ToStdout("  offline: %s", offline)
Iustin Pop's avatar
Iustin Pop committed
338
    if pinst:
339
      ToStdout("  primary for instances:")
Iustin Pop's avatar
Iustin Pop committed
340
      for iname in pinst:
341
        ToStdout("    - %s", iname)
Iustin Pop's avatar
Iustin Pop committed
342
    else:
343
      ToStdout("  primary for no instances")
Iustin Pop's avatar
Iustin Pop committed
344
    if sinst:
345
      ToStdout("  secondary for instances:")
Iustin Pop's avatar
Iustin Pop committed
346
      for iname in sinst:
347
        ToStdout("    - %s", iname)
Iustin Pop's avatar
Iustin Pop committed
348
    else:
349
      ToStdout("  secondary for no instances")
Iustin Pop's avatar
Iustin Pop committed
350
351
352
353
354

  return 0


def RemoveNode(opts, args):
355
356
357
358
359
360
361
362
363
364
  """Remove a node from the cluster.

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

  """
Iustin Pop's avatar
Iustin Pop committed
365
366
  op = opcodes.OpRemoveNode(node_name=args[0])
  SubmitOpCode(op)
367
  return 0
Iustin Pop's avatar
Iustin Pop committed
368
369


370
371
372
def ListVolumes(opts, args):
  """List logical volumes on node(s).

373
374
375
376
377
378
379
380
  @param opts: the command line options selected by the user
  @type args: list
  @param args: should either be an empty list, in which case
      we list data for all nodes, or contain a list of nodes
      to display data only for those
  @rtype: int
  @return: the desired exit code

381
382
383
384
385
386
387
388
389
390
391
  """
  if opts.output is None:
    selected_fields = ["node", "phys", "vg",
                       "name", "size", "instance"]
  else:
    selected_fields = opts.output.split(",")

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

  if not opts.no_headers:
392
393
394
395
396
397
    headers = {"node": "Node", "phys": "PhysDev",
               "vg": "VG", "name": "Name",
               "size": "Size", "instance": "Instance"}
  else:
    headers = None

398
  unitfields = ["size"]
399
400
401

  numfields = ["size"]

402
403
  data = GenerateTable(separator=opts.separator, headers=headers,
                       fields=selected_fields, unitfields=unitfields,
404
                       numfields=numfields, data=output, units=opts.units)
405
406

  for line in data:
407
    ToStdout(line)
408
409
410
411

  return 0


Iustin Pop's avatar
Iustin Pop committed
412
413
414
415
416
417
418
419
420
421
def SetNodeParams(opts, args):
  """Modifies a node.

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

  """
422
  if opts.master_candidate is None and opts.offline is None:
Iustin Pop's avatar
Iustin Pop committed
423
424
425
    ToStderr("Please give at least one of the parameters.")
    return 1

426
427
428
429
430
431
432
433
  if opts.master_candidate is not None:
    candidate = opts.master_candidate == 'yes'
  else:
    candidate = None
  if opts.offline is not None:
    offline = opts.offline == 'yes'
  else:
    offline = None
Iustin Pop's avatar
Iustin Pop committed
434
  op = opcodes.OpSetNodeParams(node_name=args[0],
435
436
437
                               master_candidate=candidate,
                               offline=offline,
                               force=opts.force)
Iustin Pop's avatar
Iustin Pop committed
438
439
440
441
442
443
444
445
446
447
448

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

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


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


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