Statistics
| Branch: | Tag: | Revision:

root / scripts / gnt-node @ b21d8c7f

History | View | Annotate | Download (18.2 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 not readd:
104
    ToStderr("-- WARNING -- \n"
105
             "Performing this operation is going to replace the ssh daemon"
106
             " keypair\n"
107
             "on the target machine (%s) with the ones of the"
108
             " current one\n"
109
             "and grant full intra-cluster ssh root access to/from it\n", node)
110

    
111
  bootstrap.SetupNodeDaemon(cluster_name, node, opts.ssh_key_check)
112

    
113
  op = opcodes.OpAddNode(node_name=args[0], secondary_ip=sip,
114
                         readd=opts.readd)
115
  SubmitOpCode(op)
116

    
117

    
118
def ListNodes(opts, args):
119
  """List nodes and their properties.
120

    
121
  @param opts: the command line options selected by the user
122
  @type args: list
123
  @param args: should be an empty list
124
  @rtype: int
125
  @return: the desired exit code
126

    
127
  """
128
  if opts.output is None:
129
    selected_fields = _LIST_DEF_FIELDS
130
  elif opts.output.startswith("+"):
131
    selected_fields = _LIST_DEF_FIELDS + opts.output[1:].split(",")
132
  else:
133
    selected_fields = opts.output.split(",")
134

    
135
  output = GetClient().QueryNodes(args, selected_fields, opts.do_locking)
136

    
137
  if not opts.no_headers:
138
    headers = _LIST_HEADERS
139
  else:
140
    headers = None
141

    
142
  unitfields = ["dtotal", "dfree", "mtotal", "mnode", "mfree"]
143

    
144
  numfields = ["dtotal", "dfree",
145
               "mtotal", "mnode", "mfree",
146
               "pinst_cnt", "sinst_cnt",
147
               "ctotal", "serial_no"]
148

    
149
  list_type_fields = ("pinst_list", "sinst_list", "tags")
150
  # change raw values to nicer strings
151
  for row in output:
152
    for idx, field in enumerate(selected_fields):
153
      val = row[idx]
154
      if field in list_type_fields:
155
        val = ",".join(val)
156
      elif field in ('master', 'master_candidate', 'offline', 'drained'):
157
        if val:
158
          val = 'Y'
159
        else:
160
          val = 'N'
161
      elif val is None:
162
        val = "?"
163
      row[idx] = str(val)
164

    
165
  data = GenerateTable(separator=opts.separator, headers=headers,
166
                       fields=selected_fields, unitfields=unitfields,
167
                       numfields=numfields, data=output, units=opts.units)
168
  for line in data:
169
    ToStdout(line)
170

    
171
  return 0
172

    
173

    
174
def EvacuateNode(opts, args):
175
  """Relocate all secondary instance from a node.
176

    
177
  @param opts: the command line options selected by the user
178
  @type args: list
179
  @param args: should be an empty list
180
  @rtype: int
181
  @return: the desired exit code
182

    
183
  """
184
  cl = GetClient()
185
  force = opts.force
186

    
187
  dst_node = opts.dst_node
188
  iallocator = opts.iallocator
189

    
190
  cnt = [dst_node, iallocator].count(None)
191
  if cnt != 1:
192
    raise errors.OpPrereqError("One and only one of the -n and -i"
193
                               " options must be passed")
194

    
195
  selected_fields = ["name", "sinst_list"]
196
  src_node = args[0]
197

    
198
  result = cl.QueryNodes(names=[src_node], fields=selected_fields,
199
                         use_locking=False)
200
  src_node, sinst = result[0]
201

    
202
  if not sinst:
203
    ToStderr("No secondary instances on node %s, exiting.", src_node)
204
    return constants.EXIT_SUCCESS
205

    
206
  if dst_node is not None:
207
    result = cl.QueryNodes(names=[dst_node], fields=["name"],
208
                           use_locking=False)
209
    dst_node = result[0][0]
210

    
211
    if src_node == dst_node:
212
      raise errors.OpPrereqError("Evacuate node needs different source and"
213
                                 " target nodes (node %s given twice)" %
214
                                 src_node)
215
    txt_msg = "to node %s" % dst_node
216
  else:
217
    txt_msg = "using iallocator %s" % iallocator
218

    
219
  sinst = utils.NiceSort(sinst)
220

    
221
  if not force and not AskUser("Relocate instance(s) %s from node\n"
222
                               " %s %s?" %
223
                               (",".join("'%s'" % name for name in sinst),
224
                               src_node, txt_msg)):
225
    return constants.EXIT_CONFIRMATION
226

    
227
  op = opcodes.OpEvacuateNode(node_name=args[0], remote_node=dst_node,
228
                              iallocator=iallocator)
229
  SubmitOpCode(op, cl=cl)
230

    
231

    
232
def FailoverNode(opts, args):
233
  """Failover all primary instance on a node.
234

    
235
  @param opts: the command line options selected by the user
236
  @type args: list
237
  @param args: should be an empty list
238
  @rtype: int
239
  @return: the desired exit code
240

    
241
  """
242
  cl = GetClient()
243
  force = opts.force
244
  selected_fields = ["name", "pinst_list"]
245

    
246
  # these fields are static data anyway, so it doesn't matter, but
247
  # locking=True should be safer
248
  result = cl.QueryNodes(names=args, fields=selected_fields,
249
                         use_locking=False)
250
  node, pinst = result[0]
251

    
252
  if not pinst:
253
    ToStderr("No primary instances on node %s, exiting.", node)
254
    return 0
255

    
256
  pinst = utils.NiceSort(pinst)
257

    
258
  retcode = 0
259

    
260
  if not force and not AskUser("Fail over instance(s) %s?" %
261
                               (",".join("'%s'" % name for name in pinst))):
262
    return 2
263

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

    
278

    
279
def MigrateNode(opts, args):
280
  """Migrate all primary instance on a node.
281

    
282
  """
283
  cl = GetClient()
284
  force = opts.force
285
  selected_fields = ["name", "pinst_list"]
286

    
287
  result = cl.QueryNodes(names=args, fields=selected_fields, use_locking=False)
288
  node, pinst = result[0]
289

    
290
  if not pinst:
291
    ToStdout("No primary instances on node %s, exiting." % node)
292
    return 0
293

    
294
  pinst = utils.NiceSort(pinst)
295

    
296
  retcode = 0
297

    
298
  if not force and not AskUser("Migrate instance(s) %s?" %
299
                               (",".join("'%s'" % name for name in pinst))):
300
    return 2
301

    
302
  op = opcodes.OpMigrateNode(node_name=args[0], live=opts.live)
303
  SubmitOpCode(op, cl=cl)
304

    
305

    
306
def ShowNodeConfig(opts, args):
307
  """Show node information.
308

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

    
317
  """
318
  cl = GetClient()
319
  result = cl.QueryNodes(fields=["name", "pip", "sip",
320
                                 "pinst_list", "sinst_list",
321
                                 "master_candidate", "drained", "offline"],
322
                         names=args, use_locking=False)
323

    
324
  for (name, primary_ip, secondary_ip, pinst, sinst,
325
       is_mc, drained, offline) in result:
326
    ToStdout("Node name: %s", name)
327
    ToStdout("  primary ip: %s", primary_ip)
328
    ToStdout("  secondary ip: %s", secondary_ip)
329
    ToStdout("  master candidate: %s", is_mc)
330
    ToStdout("  drained: %s", drained)
331
    ToStdout("  offline: %s", offline)
332
    if pinst:
333
      ToStdout("  primary for instances:")
334
      for iname in utils.NiceSort(pinst):
335
        ToStdout("    - %s", iname)
336
    else:
337
      ToStdout("  primary for no instances")
338
    if sinst:
339
      ToStdout("  secondary for instances:")
340
      for iname in utils.NiceSort(sinst):
341
        ToStdout("    - %s", iname)
342
    else:
343
      ToStdout("  secondary for no instances")
344

    
345
  return 0
346

    
347

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

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

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

    
363

    
364
def PowercycleNode(opts, args):
365
  """Remove a node from the cluster.
366

    
367
  @param opts: the command line options selected by the user
368
  @type args: list
369
  @param args: should contain only one element, the name of
370
      the node to be removed
371
  @rtype: int
372
  @return: the desired exit code
373

    
374
  """
375
  node = args[0]
376
  if (not opts.confirm and
377
      not AskUser("Are you sure you want to hard powercycle node %s?" % node)):
378
    return 2
379

    
380
  op = opcodes.OpPowercycleNode(node_name=node, force=opts.force)
381
  result = SubmitOpCode(op)
382
  ToStderr(result)
383
  return 0
384

    
385

    
386
def ListVolumes(opts, args):
387
  """List logical volumes on node(s).
388

    
389
  @param opts: the command line options selected by the user
390
  @type args: list
391
  @param args: should either be an empty list, in which case
392
      we list data for all nodes, or contain a list of nodes
393
      to display data only for those
394
  @rtype: int
395
  @return: the desired exit code
396

    
397
  """
398
  if opts.output is None:
399
    selected_fields = ["node", "phys", "vg",
400
                       "name", "size", "instance"]
401
  else:
402
    selected_fields = opts.output.split(",")
403

    
404
  op = opcodes.OpQueryNodeVolumes(nodes=args, output_fields=selected_fields)
405
  output = SubmitOpCode(op)
406

    
407
  if not opts.no_headers:
408
    headers = {"node": "Node", "phys": "PhysDev",
409
               "vg": "VG", "name": "Name",
410
               "size": "Size", "instance": "Instance"}
411
  else:
412
    headers = None
413

    
414
  unitfields = ["size"]
415

    
416
  numfields = ["size"]
417

    
418
  data = GenerateTable(separator=opts.separator, headers=headers,
419
                       fields=selected_fields, unitfields=unitfields,
420
                       numfields=numfields, data=output, units=opts.units)
421

    
422
  for line in data:
423
    ToStdout(line)
424

    
425
  return 0
426

    
427

    
428
def SetNodeParams(opts, args):
429
  """Modifies a node.
430

    
431
  @param opts: the command line options selected by the user
432
  @type args: list
433
  @param args: should contain only one element, the node name
434
  @rtype: int
435
  @return: the desired exit code
436

    
437
  """
438
  if [opts.master_candidate, opts.drained, opts.offline].count(None) == 3:
439
    ToStderr("Please give at least one of the parameters.")
440
    return 1
441

    
442
  if opts.master_candidate is not None:
443
    candidate = opts.master_candidate == 'yes'
444
  else:
445
    candidate = None
446
  if opts.offline is not None:
447
    offline = opts.offline == 'yes'
448
  else:
449
    offline = None
450

    
451
  if opts.drained is not None:
452
    drained = opts.drained == 'yes'
453
  else:
454
    drained = None
455
  op = opcodes.OpSetNodeParams(node_name=args[0],
456
                               master_candidate=candidate,
457
                               offline=offline,
458
                               drained=drained,
459
                               force=opts.force)
460

    
461
  # even if here we process the result, we allow submit only
462
  result = SubmitOrSend(op, opts)
463

    
464
  if result:
465
    ToStdout("Modified node %s", args[0])
466
    for param, data in result:
467
      ToStdout(" - %-5s -> %s", param, data)
468
  return 0
469

    
470

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

    
539
              make_option("-O", "--offline", dest="offline", metavar="yes|no",
540
                          choices=('yes', 'no'), default=None,
541
                          help="Set the offline flag on the node"),
542
              make_option("-D", "--drained", dest="drained", metavar="yes|no",
543
                          choices=('yes', 'no'), default=None,
544
                          help="Set the drained flag on the node"),
545
              ],
546
             "<instance>", "Alters the parameters of an instance"),
547
  'powercycle': (PowercycleNode, ARGS_ONE, [DEBUG_OPT, FORCE_OPT, CONFIRM_OPT],
548
                 "<node_name>", "Tries to forcefully powercycle a node"),
549
  'remove': (RemoveNode, ARGS_ONE, [DEBUG_OPT],
550
             "<node_name>", "Removes a node from the cluster"),
551
  'volumes': (ListVolumes, ARGS_ANY,
552
              [DEBUG_OPT, NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT],
553
              "[<node_name>...]", "List logical volumes on node(s)"),
554
  'list-tags': (ListTags, ARGS_ONE, [DEBUG_OPT],
555
                "<node_name>", "List the tags of the given node"),
556
  'add-tags': (AddTags, ARGS_ATLEAST(1), [DEBUG_OPT, TAG_SRC_OPT],
557
               "<node_name> tag...", "Add tags to the given node"),
558
  'remove-tags': (RemoveTags, ARGS_ATLEAST(1), [DEBUG_OPT, TAG_SRC_OPT],
559
                  "<node_name> tag...", "Remove tags from the given node"),
560
  }
561

    
562

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