Statistics
| Branch: | Tag: | Revision:

root / scripts / gnt-node @ 2e7b8369

History | View | Annotate | Download (17.1 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'], use_locking=True)
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, opts.do_locking)
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
  result = cl.QueryNodes(names=[src_node], fields=selected_fields,
186
                         use_locking=True)
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"], use_locking=True)
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
  # these fields are static data anyway, so it doesn't matter, but
241
  # locking=True should be safer
242
  result = cl.QueryNodes(names=args, fields=selected_fields,
243
                         use_locking=True)
244
  node, pinst = result[0]
245

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

    
250
  pinst = utils.NiceSort(pinst)
251

    
252
  retcode = 0
253

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

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

    
272

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

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

    
281
  result = cl.QueryNodes(names=args, fields=selected_fields, use_locking=True)
282
  node, pinst = result[0]
283

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

    
288
  pinst = utils.NiceSort(pinst)
289

    
290
  retcode = 0
291

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

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

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

    
311

    
312
def ShowNodeConfig(opts, args):
313
  """Show node information.
314

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

    
323
  """
324
  cl = GetClient()
325
  result = cl.QueryNodes(fields=["name", "pip", "sip",
326
                                 "pinst_list", "sinst_list"],
327
                         names=args, use_locking=True)
328

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

    
346
  return 0
347

    
348

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

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

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

    
364

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

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

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

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

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

    
393
  unitfields = ["size"]
394

    
395
  numfields = ["size"]
396

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

    
401
  for line in data:
402
    ToStdout(line)
403

    
404
  return 0
405

    
406

    
407
def SetNodeParams(opts, args):
408
  """Modifies a node.
409

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

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

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

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

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

    
443

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

    
528

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