Statistics
| Branch: | Tag: | Revision:

root / scripts / gnt-node @ c4ed32cb

History | View | Annotate | Download (17 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",
54
  "tags": "Tags",
55
  "serial_no": "SerialNo",
56
  "master_candidate": "MasterC",
57
  "master": "IsMaster",
58
  "offline": "Offline",
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

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

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

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

    
98
  bootstrap.SetupNodeDaemon(cluster_name, node, opts.ssh_key_check)
99

    
100
  op = opcodes.OpAddNode(node_name=args[0], secondary_ip=opts.secondary_ip,
101
                         readd=opts.readd)
102
  SubmitOpCode(op)
103

    
104

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

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

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

    
122
  output = GetClient().QueryNodes([], selected_fields)
123

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

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

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

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

    
152
  data = GenerateTable(separator=opts.separator, headers=headers,
153
                       fields=selected_fields, unitfields=unitfields,
154
                       numfields=numfields, data=output, units=opts.units)
155
  for line in data:
156
    ToStdout(line)
157

    
158
  return 0
159

    
160

    
161
def EvacuateNode(opts, args):
162
  """Relocate all secondary instance from a node.
163

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

    
170
  """
171
  cl = GetClient()
172
  force = opts.force
173

    
174
  dst_node = opts.dst_node
175
  iallocator = opts.iallocator
176

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

    
182
  selected_fields = ["name", "sinst_list"]
183
  src_node = args[0]
184

    
185
  op = opcodes.OpQueryNodes(output_fields=selected_fields, names=[src_node])
186
  result = cl.QueryNodes(names=[src_node], fields=selected_fields)
187
  src_node, sinst = result[0]
188

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

    
193
  if dst_node is not None:
194
    result = cl.QueryNodes(names=[dst_node], fields=["name"])
195
    dst_node = result[0][0]
196

    
197
    if src_node == dst_node:
198
      raise errors.OpPrereqError("Evacuate node needs different source and"
199
                                 " target nodes (node %s given twice)" %
200
                                 src_node)
201
    txt_msg = "to node %s" % dst_node
202
  else:
203
    txt_msg = "using iallocator %s" % iallocator
204

    
205
  sinst = utils.NiceSort(sinst)
206

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

    
213
  ops = []
214
  for iname in sinst:
215
    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
  job_id = cli.SendJob(ops, cl=cl)
223
  cli.PollJob(job_id, cl=cl)
224

    
225

    
226
def FailoverNode(opts, args):
227
  """Failover all primary instance on a node.
228

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

    
235
  """
236
  cl = GetClient()
237
  force = opts.force
238
  selected_fields = ["name", "pinst_list"]
239

    
240
  op = opcodes.OpQueryNodes(output_fields=selected_fields, names=args)
241
  result = SubmitOpCode(op, cl=cl)
242
  node, pinst = result[0]
243

    
244
  if not pinst:
245
    ToStderr("No primary instances on node %s, exiting.", node)
246
    return 0
247

    
248
  pinst = utils.NiceSort(pinst)
249

    
250
  retcode = 0
251

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

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

    
270

    
271
def MigrateNode(opts, args):
272
  """Migrate all primary instance on a node.
273

    
274
  """
275
  cl = GetClient()
276
  force = opts.force
277
  selected_fields = ["name", "pinst_list"]
278

    
279
  result = cl.QueryNodes(names=args, fields=selected_fields)
280
  node, pinst = result[0]
281

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

    
286
  pinst = utils.NiceSort(pinst)
287

    
288
  retcode = 0
289

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

    
294
  jex = JobExecutor(cl=cl)
295
  for iname in pinst:
296
    op = opcodes.OpMigrateInstance(instance_name=iname, live=opts.live,
297
                                   cleanup=False)
298
    jex.QueueJob(iname, op)
299

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

    
309

    
310
def ShowNodeConfig(opts, args):
311
  """Show node information.
312

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

    
321
  """
322
  op = opcodes.OpQueryNodes(output_fields=["name", "pip", "sip",
323
                                           "pinst_list", "sinst_list"],
324
                            names=args)
325
  result = SubmitOpCode(op)
326

    
327
  for name, primary_ip, secondary_ip, pinst, sinst in result:
328
    ToStdout("Node name: %s", name)
329
    ToStdout("  primary ip: %s", primary_ip)
330
    ToStdout("  secondary ip: %s", secondary_ip)
331
    if pinst:
332
      ToStdout("  primary for instances:")
333
      for iname in pinst:
334
        ToStdout("    - %s", iname)
335
    else:
336
      ToStdout("  primary for no instances")
337
    if sinst:
338
      ToStdout("  secondary for instances:")
339
      for iname in sinst:
340
        ToStdout("    - %s", iname)
341
    else:
342
      ToStdout("  secondary for no instances")
343

    
344
  return 0
345

    
346

    
347
def RemoveNode(opts, args):
348
  """Remove a node from the cluster.
349

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

    
357
  """
358
  op = opcodes.OpRemoveNode(node_name=args[0])
359
  SubmitOpCode(op)
360
  return 0
361

    
362

    
363
def ListVolumes(opts, args):
364
  """List logical volumes on node(s).
365

    
366
  @param opts: the command line options selected by the user
367
  @type args: list
368
  @param args: should either be an empty list, in which case
369
      we list data for all nodes, or contain a list of nodes
370
      to display data only for those
371
  @rtype: int
372
  @return: the desired exit code
373

    
374
  """
375
  if opts.output is None:
376
    selected_fields = ["node", "phys", "vg",
377
                       "name", "size", "instance"]
378
  else:
379
    selected_fields = opts.output.split(",")
380

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

    
384
  if not opts.no_headers:
385
    headers = {"node": "Node", "phys": "PhysDev",
386
               "vg": "VG", "name": "Name",
387
               "size": "Size", "instance": "Instance"}
388
  else:
389
    headers = None
390

    
391
  unitfields = ["size"]
392

    
393
  numfields = ["size"]
394

    
395
  data = GenerateTable(separator=opts.separator, headers=headers,
396
                       fields=selected_fields, unitfields=unitfields,
397
                       numfields=numfields, data=output, units=opts.units)
398

    
399
  for line in data:
400
    ToStdout(line)
401

    
402
  return 0
403

    
404

    
405
def SetNodeParams(opts, args):
406
  """Modifies a node.
407

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

    
414
  """
415
  if opts.master_candidate is None and opts.offline is None:
416
    ToStderr("Please give at least one of the parameters.")
417
    return 1
418

    
419
  if opts.master_candidate is not None:
420
    candidate = opts.master_candidate == 'yes'
421
  else:
422
    candidate = None
423
  if opts.offline is not None:
424
    offline = opts.offline == 'yes'
425
  else:
426
    offline = None
427
  op = opcodes.OpSetNodeParams(node_name=args[0],
428
                               master_candidate=candidate,
429
                               offline=offline,
430
                               force=opts.force)
431

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

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

    
441

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

    
526

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