Statistics
| Branch: | Tag: | Revision:

root / scripts / gnt-node @ c120ff34

History | View | Annotate | Download (18.6 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 readd:
104
    # clear the offline and drain flags on the node
105
    ToStdout("Resetting the 'offline' and 'drained' flags due to re-add")
106
    op = opcodes.OpSetNodeParams(node_name=node, force=True,
107
                                 offline=False, drained=False)
108

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

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

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

    
128

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

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

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

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

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

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

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

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

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

    
182
  return 0
183

    
184

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

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

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

    
198
  dst_node = opts.dst_node
199
  iallocator = opts.iallocator
200

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

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

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

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

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

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

    
230
  sinst = utils.NiceSort(sinst)
231

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

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

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

    
250

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

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

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

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

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

    
275
  pinst = utils.NiceSort(pinst)
276

    
277
  retcode = 0
278

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

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

    
297

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

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

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

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

    
313
  pinst = utils.NiceSort(pinst)
314

    
315
  retcode = 0
316

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

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

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

    
336

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

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

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

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

    
376
  return 0
377

    
378

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

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

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

    
394

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

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

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

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

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

    
423
  unitfields = ["size"]
424

    
425
  numfields = ["size"]
426

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

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

    
434
  return 0
435

    
436

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

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

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

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

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

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

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

    
479

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

    
548
              make_option("-O", "--offline", dest="offline", metavar="yes|no",
549
                          choices=('yes', 'no'), default=None,
550
                          help="Set the offline flag on the node"),
551
              make_option("-D", "--drained", dest="drained", metavar="yes|no",
552
                          choices=('yes', 'no'), default=None,
553
                          help="Set the drained flag on the node"),
554
              ],
555
             "<instance>", "Alters the parameters of an instance"),
556
  'remove': (RemoveNode, ARGS_ONE, [DEBUG_OPT],
557
             "<node_name>", "Removes a node from the cluster"),
558
  'volumes': (ListVolumes, ARGS_ANY,
559
              [DEBUG_OPT, NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT],
560
              "[<node_name>...]", "List logical volumes on node(s)"),
561
  'list-tags': (ListTags, ARGS_ONE, [DEBUG_OPT],
562
                "<node_name>", "List the tags of the given node"),
563
  'add-tags': (AddTags, ARGS_ATLEAST(1), [DEBUG_OPT, TAG_SRC_OPT],
564
               "<node_name> tag...", "Add tags to the given node"),
565
  'remove-tags': (RemoveTags, ARGS_ATLEAST(1), [DEBUG_OPT, TAG_SRC_OPT],
566
                  "<node_name> tag...", "Remove tags from the given node"),
567
  }
568

    
569

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