Statistics
| Branch: | Tag: | Revision:

root / scripts / gnt-node @ 40ef0ed6

History | View | Annotate | Download (16.4 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 opcodes
31
from ganeti import utils
32
from ganeti import constants
33
from ganeti import errors
34
from ganeti import bootstrap
35

    
36

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

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

    
60

    
61
@UsesRPC
62
def AddNode(opts, args):
63
  """Add a node to the cluster.
64

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

    
71
  """
72
  cl = GetClient()
73
  dns_data = utils.HostInfo(args[0])
74
  node = dns_data.name
75

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

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

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

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

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

    
103

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

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

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

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

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

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

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

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

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

    
157
  return 0
158

    
159

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

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

    
169
  """
170
  cl = GetClient()
171
  force = opts.force
172
  selected_fields = ["name", "sinst_list"]
173
  src_node, dst_node = args
174

    
175
  op = opcodes.OpQueryNodes(output_fields=selected_fields, names=[src_node])
176
  result = SubmitOpCode(op, cl=cl)
177
  src_node, sinst = result[0]
178
  op = opcodes.OpQueryNodes(output_fields=["name"], names=[dst_node])
179
  result = SubmitOpCode(op, cl=cl)
180
  dst_node = result[0][0]
181

    
182
  if src_node == dst_node:
183
    raise errors.OpPrereqError("Evacuate node needs different source and"
184
                               " target nodes (node %s given twice)" %
185
                               src_node)
186

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

    
191
  sinst = utils.NiceSort(sinst)
192

    
193
  if not force and not AskUser("Relocate instance(s) %s from node\n"
194
                               " %s to node\n %s?" %
195
                               (",".join("'%s'" % name for name in sinst),
196
                               src_node, dst_node)):
197
    return constants.EXIT_CONFIRMATION
198

    
199
  jex = JobExecutor()
200
  for iname in sinst:
201
    op = opcodes.OpReplaceDisks(instance_name=iname,
202
                                remote_node=dst_node,
203
                                mode=constants.REPLACE_DISK_CHG,
204
                                disks=[])
205
    jex.QueueJob(iname, op)
206

    
207
  results = jex.GetResults()
208

    
209
  bad_cnt = len([row for row in results if not row[0]])
210
  if bad_cnt == 0:
211
    ToStdout("All %d instance(s) relocated successfully.", len(results))
212
    retcode = constants.EXIT_SUCCESS
213
  else:
214
    ToStdout("There were errors during the relocation:\n"
215
             "%d error(s) out of %d instance(s).", bad_cnt, len(results))
216
    retcode = constants.EXIT_FAILURE
217
  return retcode
218

    
219

    
220
def FailoverNode(opts, args):
221
  """Failover all primary instance on a node.
222

    
223
  @param opts: the command line options selected by the user
224
  @type args: list
225
  @param args: should be an empty list
226
  @rtype: int
227
  @return: the desired exit code
228

    
229
  """
230
  cl = GetClient()
231
  force = opts.force
232
  selected_fields = ["name", "pinst_list"]
233

    
234
  op = opcodes.OpQueryNodes(output_fields=selected_fields, names=args)
235
  result = SubmitOpCode(op, cl=cl)
236
  node, pinst = result[0]
237

    
238
  if not pinst:
239
    ToStderr("No primary instances on node %s, exiting.", node)
240
    return 0
241

    
242
  pinst = utils.NiceSort(pinst)
243

    
244
  retcode = 0
245

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

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

    
264

    
265
def MigrateNode(opts, args):
266
  """Migrate all primary instance on a node.
267

    
268
  """
269
  cl = GetClient()
270
  force = opts.force
271
  selected_fields = ["name", "pinst_list"]
272

    
273
  result = cl.QueryNodes(names=args, fields=selected_fields)
274
  node, pinst = result[0]
275

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

    
280
  pinst = utils.NiceSort(pinst)
281

    
282
  retcode = 0
283

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

    
288
  jex = JobExecutor(cl=cl)
289
  for iname in pinst:
290
    op = opcodes.OpMigrateInstance(instance_name=iname, live=opts.live,
291
                                   cleanup=False)
292
    jex.QueueJob(iname, op)
293

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

    
303

    
304
def ShowNodeConfig(opts, args):
305
  """Show node information.
306

    
307
  @param opts: the command line options selected by the user
308
  @type args: list
309
  @param args: should either be an empty list, in which case
310
      we show information about all nodes, or should contain
311
      a list of nodes to be queried for information
312
  @rtype: int
313
  @return: the desired exit code
314

    
315
  """
316
  op = opcodes.OpQueryNodes(output_fields=["name", "pip", "sip",
317
                                           "pinst_list", "sinst_list"],
318
                            names=args)
319
  result = SubmitOpCode(op)
320

    
321
  for name, primary_ip, secondary_ip, pinst, sinst in result:
322
    ToStdout("Node name: %s", name)
323
    ToStdout("  primary ip: %s", primary_ip)
324
    ToStdout("  secondary ip: %s", secondary_ip)
325
    if pinst:
326
      ToStdout("  primary for instances:")
327
      for iname in pinst:
328
        ToStdout("    - %s", iname)
329
    else:
330
      ToStdout("  primary for no instances")
331
    if sinst:
332
      ToStdout("  secondary for instances:")
333
      for iname in sinst:
334
        ToStdout("    - %s", iname)
335
    else:
336
      ToStdout("  secondary for no instances")
337

    
338
  return 0
339

    
340

    
341
def RemoveNode(opts, args):
342
  """Remove a node from the cluster.
343

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

    
351
  """
352
  op = opcodes.OpRemoveNode(node_name=args[0])
353
  SubmitOpCode(op)
354
  return 0
355

    
356

    
357
def ListVolumes(opts, args):
358
  """List logical volumes on node(s).
359

    
360
  @param opts: the command line options selected by the user
361
  @type args: list
362
  @param args: should either be an empty list, in which case
363
      we list data for all nodes, or contain a list of nodes
364
      to display data only for those
365
  @rtype: int
366
  @return: the desired exit code
367

    
368
  """
369
  if opts.output is None:
370
    selected_fields = ["node", "phys", "vg",
371
                       "name", "size", "instance"]
372
  else:
373
    selected_fields = opts.output.split(",")
374

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

    
378
  if not opts.no_headers:
379
    headers = {"node": "Node", "phys": "PhysDev",
380
               "vg": "VG", "name": "Name",
381
               "size": "Size", "instance": "Instance"}
382
  else:
383
    headers = None
384

    
385
  unitfields = ["size"]
386

    
387
  numfields = ["size"]
388

    
389
  data = GenerateTable(separator=opts.separator, headers=headers,
390
                       fields=selected_fields, unitfields=unitfields,
391
                       numfields=numfields, data=output, units=opts.units)
392

    
393
  for line in data:
394
    ToStdout(line)
395

    
396
  return 0
397

    
398

    
399
def SetNodeParams(opts, args):
400
  """Modifies a node.
401

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

    
408
  """
409
  if opts.master_candidate is None and opts.offline is None:
410
    ToStderr("Please give at least one of the parameters.")
411
    return 1
412

    
413
  if opts.master_candidate is not None:
414
    candidate = opts.master_candidate == 'yes'
415
  else:
416
    candidate = None
417
  if opts.offline is not None:
418
    offline = opts.offline == 'yes'
419
  else:
420
    offline = None
421
  op = opcodes.OpSetNodeParams(node_name=args[0],
422
                               master_candidate=candidate,
423
                               offline=offline,
424
                               force=opts.force)
425

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

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

    
435

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

    
512

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