Statistics
| Branch: | Tag: | Revision:

root / scripts / gnt-node @ c9d443ea

History | View | Annotate | Download (17.7 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

    
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', 'drained'):
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
                                 "master_candidate", "drained", "offline"],
328
                         names=args, use_locking=True)
329

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

    
351
  return 0
352

    
353

    
354
def RemoveNode(opts, args):
355
  """Remove a node from the cluster.
356

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

    
364
  """
365
  op = opcodes.OpRemoveNode(node_name=args[0])
366
  SubmitOpCode(op)
367
  return 0
368

    
369

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

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

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

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

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

    
398
  unitfields = ["size"]
399

    
400
  numfields = ["size"]
401

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

    
406
  for line in data:
407
    ToStdout(line)
408

    
409
  return 0
410

    
411

    
412
def SetNodeParams(opts, args):
413
  """Modifies a node.
414

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

    
421
  """
422
  if [opts.master_candidate, opts.drained, opts.offline].count(None) == 3:
423
    ToStderr("Please give at least one of the parameters.")
424
    return 1
425

    
426
  if opts.master_candidate is not None:
427
    candidate = opts.master_candidate == 'yes'
428
  else:
429
    candidate = None
430
  if opts.offline is not None:
431
    offline = opts.offline == 'yes'
432
  else:
433
    offline = None
434

    
435
  if opts.drained is not None:
436
    drained = opts.drained == 'yes'
437
  else:
438
    drained = None
439
  op = opcodes.OpSetNodeParams(node_name=args[0],
440
                               master_candidate=candidate,
441
                               offline=offline,
442
                               drained=drained,
443
                               force=opts.force)
444

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

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

    
454

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

    
542

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