Statistics
| Branch: | Tag: | Revision:

root / lib / client / gnt_node.py @ 04520998

History | View | Annotate | Download (34.3 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012 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
"""Node related commands"""
22

    
23
# pylint: disable=W0401,W0613,W0614,C0103
24
# W0401: Wildcard import ganeti.cli
25
# W0613: Unused argument, since all functions follow the same API
26
# W0614: Unused import %s from wildcard import (since we need cli)
27
# C0103: Invalid name gnt-node
28

    
29
import itertools
30

    
31
from ganeti.cli import *
32
from ganeti import cli
33
from ganeti import bootstrap
34
from ganeti import opcodes
35
from ganeti import utils
36
from ganeti import constants
37
from ganeti import errors
38
from ganeti import netutils
39
from ganeti import pathutils
40
from cStringIO import StringIO
41

    
42
from ganeti import confd
43
from ganeti.confd import client as confd_client
44

    
45
#: default list of field for L{ListNodes}
46
_LIST_DEF_FIELDS = [
47
  "name", "dtotal", "dfree",
48
  "mtotal", "mnode", "mfree",
49
  "pinst_cnt", "sinst_cnt",
50
  ]
51

    
52

    
53
#: Default field list for L{ListVolumes}
54
_LIST_VOL_DEF_FIELDS = ["node", "phys", "vg", "name", "size", "instance"]
55

    
56

    
57
#: default list of field for L{ListStorage}
58
_LIST_STOR_DEF_FIELDS = [
59
  constants.SF_NODE,
60
  constants.SF_TYPE,
61
  constants.SF_NAME,
62
  constants.SF_SIZE,
63
  constants.SF_USED,
64
  constants.SF_FREE,
65
  constants.SF_ALLOCATABLE,
66
  ]
67

    
68

    
69
#: default list of power commands
70
_LIST_POWER_COMMANDS = ["on", "off", "cycle", "status"]
71

    
72

    
73
#: headers (and full field list) for L{ListStorage}
74
_LIST_STOR_HEADERS = {
75
  constants.SF_NODE: "Node",
76
  constants.SF_TYPE: "Type",
77
  constants.SF_NAME: "Name",
78
  constants.SF_SIZE: "Size",
79
  constants.SF_USED: "Used",
80
  constants.SF_FREE: "Free",
81
  constants.SF_ALLOCATABLE: "Allocatable",
82
  }
83

    
84

    
85
#: User-facing storage unit types
86
_USER_STORAGE_TYPE = {
87
  constants.ST_FILE: "file",
88
  constants.ST_LVM_PV: "lvm-pv",
89
  constants.ST_LVM_VG: "lvm-vg",
90
  }
91

    
92
_STORAGE_TYPE_OPT = \
93
  cli_option("-t", "--storage-type",
94
             dest="user_storage_type",
95
             choices=_USER_STORAGE_TYPE.keys(),
96
             default=None,
97
             metavar="STORAGE_TYPE",
98
             help=("Storage type (%s)" %
99
                   utils.CommaJoin(_USER_STORAGE_TYPE.keys())))
100

    
101
_REPAIRABLE_STORAGE_TYPES = \
102
  [st for st, so in constants.VALID_STORAGE_OPERATIONS.iteritems()
103
   if constants.SO_FIX_CONSISTENCY in so]
104

    
105
_MODIFIABLE_STORAGE_TYPES = constants.MODIFIABLE_STORAGE_FIELDS.keys()
106

    
107

    
108
_OOB_COMMAND_ASK = frozenset([constants.OOB_POWER_OFF,
109
                              constants.OOB_POWER_CYCLE])
110

    
111

    
112
_ENV_OVERRIDE = frozenset(["list"])
113

    
114

    
115
NONODE_SETUP_OPT = cli_option("--no-node-setup", default=True,
116
                              action="store_false", dest="node_setup",
117
                              help=("Do not make initial SSH setup on remote"
118
                                    " node (needs to be done manually)"))
119

    
120
IGNORE_STATUS_OPT = cli_option("--ignore-status", default=False,
121
                               action="store_true", dest="ignore_status",
122
                               help=("Ignore the Node(s) offline status"
123
                                     " (potentially DANGEROUS)"))
124

    
125

    
126
def ConvertStorageType(user_storage_type):
127
  """Converts a user storage type to its internal name.
128

129
  """
130
  try:
131
    return _USER_STORAGE_TYPE[user_storage_type]
132
  except KeyError:
133
    raise errors.OpPrereqError("Unknown storage type: %s" % user_storage_type,
134
                               errors.ECODE_INVAL)
135

    
136

    
137
def _RunSetupSSH(options, nodes):
138
  """Wrapper around utils.RunCmd to call setup-ssh
139

140
  @param options: The command line options
141
  @param nodes: The nodes to setup
142

143
  """
144

    
145
  assert nodes, "Empty node list"
146

    
147
  cmd = [pathutils.SETUP_SSH]
148

    
149
  # Pass --debug|--verbose to the external script if set on our invocation
150
  # --debug overrides --verbose
151
  if options.debug:
152
    cmd.append("--debug")
153
  elif options.verbose:
154
    cmd.append("--verbose")
155
  if not options.ssh_key_check:
156
    cmd.append("--no-ssh-key-check")
157
  if options.force_join:
158
    cmd.append("--force-join")
159

    
160
  cmd.extend(nodes)
161

    
162
  result = utils.RunCmd(cmd, interactive=True)
163

    
164
  if result.failed:
165
    errmsg = ("Command '%s' failed with exit code %s; output %r" %
166
              (result.cmd, result.exit_code, result.output))
167
    raise errors.OpExecError(errmsg)
168

    
169

    
170
@UsesRPC
171
def AddNode(opts, args):
172
  """Add a node to the cluster.
173

174
  @param opts: the command line options selected by the user
175
  @type args: list
176
  @param args: should contain only one element, the new node name
177
  @rtype: int
178
  @return: the desired exit code
179

180
  """
181
  cl = GetClient()
182
  node = netutils.GetHostname(name=args[0]).name
183
  readd = opts.readd
184

    
185
  try:
186
    output = cl.QueryNodes(names=[node], fields=["name", "sip", "master"],
187
                           use_locking=False)
188
    node_exists, sip, is_master = output[0]
189
  except (errors.OpPrereqError, errors.OpExecError):
190
    node_exists = ""
191
    sip = None
192

    
193
  if readd:
194
    if not node_exists:
195
      ToStderr("Node %s not in the cluster"
196
               " - please retry without '--readd'", node)
197
      return 1
198
    if is_master:
199
      ToStderr("Node %s is the master, cannot readd", node)
200
      return 1
201
  else:
202
    if node_exists:
203
      ToStderr("Node %s already in the cluster (as %s)"
204
               " - please retry with '--readd'", node, node_exists)
205
      return 1
206
    sip = opts.secondary_ip
207

    
208
  # read the cluster name from the master
209
  output = cl.QueryConfigValues(["cluster_name"])
210
  cluster_name = output[0]
211

    
212
  if not readd and opts.node_setup:
213
    ToStderr("-- WARNING -- \n"
214
             "Performing this operation is going to replace the ssh daemon"
215
             " keypair\n"
216
             "on the target machine (%s) with the ones of the"
217
             " current one\n"
218
             "and grant full intra-cluster ssh root access to/from it\n", node)
219

    
220
  if opts.node_setup:
221
    _RunSetupSSH(opts, [node])
222

    
223
  bootstrap.SetupNodeDaemon(cluster_name, node, opts.ssh_key_check)
224

    
225
  if opts.disk_state:
226
    disk_state = utils.FlatToDict(opts.disk_state)
227
  else:
228
    disk_state = {}
229

    
230
  hv_state = dict(opts.hv_state)
231

    
232
  op = opcodes.OpNodeAdd(node_name=args[0], secondary_ip=sip,
233
                         readd=opts.readd, group=opts.nodegroup,
234
                         vm_capable=opts.vm_capable, ndparams=opts.ndparams,
235
                         master_capable=opts.master_capable,
236
                         disk_state=disk_state,
237
                         hv_state=hv_state)
238
  SubmitOpCode(op, opts=opts)
239

    
240

    
241
def ListNodes(opts, args):
242
  """List nodes and their properties.
243

244
  @param opts: the command line options selected by the user
245
  @type args: list
246
  @param args: nodes to list, or empty for all
247
  @rtype: int
248
  @return: the desired exit code
249

250
  """
251
  selected_fields = ParseFields(opts.output, _LIST_DEF_FIELDS)
252

    
253
  fmtoverride = dict.fromkeys(["pinst_list", "sinst_list", "tags"],
254
                              (",".join, False))
255

    
256
  return GenericList(constants.QR_NODE, selected_fields, args, opts.units,
257
                     opts.separator, not opts.no_headers,
258
                     format_override=fmtoverride, verbose=opts.verbose,
259
                     force_filter=opts.force_filter)
260

    
261

    
262
def ListNodeFields(opts, args):
263
  """List node fields.
264

265
  @param opts: the command line options selected by the user
266
  @type args: list
267
  @param args: fields to list, or empty for all
268
  @rtype: int
269
  @return: the desired exit code
270

271
  """
272
  return GenericListFields(constants.QR_NODE, args, opts.separator,
273
                           not opts.no_headers)
274

    
275

    
276
def EvacuateNode(opts, args):
277
  """Relocate all secondary instance from a node.
278

279
  @param opts: the command line options selected by the user
280
  @type args: list
281
  @param args: should be an empty list
282
  @rtype: int
283
  @return: the desired exit code
284

285
  """
286
  if opts.dst_node is not None:
287
    ToStderr("New secondary node given (disabling iallocator), hence evacuating"
288
             " secondary instances only.")
289
    opts.secondary_only = True
290
    opts.primary_only = False
291

    
292
  if opts.secondary_only and opts.primary_only:
293
    raise errors.OpPrereqError("Only one of the --primary-only and"
294
                               " --secondary-only options can be passed",
295
                               errors.ECODE_INVAL)
296
  elif opts.primary_only:
297
    mode = constants.NODE_EVAC_PRI
298
  elif opts.secondary_only:
299
    mode = constants.NODE_EVAC_SEC
300
  else:
301
    mode = constants.NODE_EVAC_ALL
302

    
303
  # Determine affected instances
304
  fields = []
305

    
306
  if not opts.secondary_only:
307
    fields.append("pinst_list")
308
  if not opts.primary_only:
309
    fields.append("sinst_list")
310

    
311
  cl = GetClient()
312

    
313
  result = cl.QueryNodes(names=args, fields=fields, use_locking=False)
314
  instances = set(itertools.chain(*itertools.chain(*itertools.chain(result))))
315

    
316
  if not instances:
317
    # No instances to evacuate
318
    ToStderr("No instances to evacuate on node(s) %s, exiting.",
319
             utils.CommaJoin(args))
320
    return constants.EXIT_SUCCESS
321

    
322
  if not (opts.force or
323
          AskUser("Relocate instance(s) %s from node(s) %s?" %
324
                  (utils.CommaJoin(utils.NiceSort(instances)),
325
                   utils.CommaJoin(args)))):
326
    return constants.EXIT_CONFIRMATION
327

    
328
  # Evacuate node
329
  op = opcodes.OpNodeEvacuate(node_name=args[0], mode=mode,
330
                              remote_node=opts.dst_node,
331
                              iallocator=opts.iallocator,
332
                              early_release=opts.early_release)
333
  result = SubmitOrSend(op, opts, cl=cl)
334

    
335
  # Keep track of submitted jobs
336
  jex = JobExecutor(cl=cl, opts=opts)
337

    
338
  for (status, job_id) in result[constants.JOB_IDS_KEY]:
339
    jex.AddJobId(None, status, job_id)
340

    
341
  results = jex.GetResults()
342
  bad_cnt = len([row for row in results if not row[0]])
343
  if bad_cnt == 0:
344
    ToStdout("All instances evacuated successfully.")
345
    rcode = constants.EXIT_SUCCESS
346
  else:
347
    ToStdout("There were %s errors during the evacuation.", bad_cnt)
348
    rcode = constants.EXIT_FAILURE
349

    
350
  return rcode
351

    
352

    
353
def FailoverNode(opts, args):
354
  """Failover all primary instance on a node.
355

356
  @param opts: the command line options selected by the user
357
  @type args: list
358
  @param args: should be an empty list
359
  @rtype: int
360
  @return: the desired exit code
361

362
  """
363
  cl = GetClient()
364
  force = opts.force
365
  selected_fields = ["name", "pinst_list"]
366

    
367
  # these fields are static data anyway, so it doesn't matter, but
368
  # locking=True should be safer
369
  result = cl.QueryNodes(names=args, fields=selected_fields,
370
                         use_locking=False)
371
  node, pinst = result[0]
372

    
373
  if not pinst:
374
    ToStderr("No primary instances on node %s, exiting.", node)
375
    return 0
376

    
377
  pinst = utils.NiceSort(pinst)
378

    
379
  retcode = 0
380

    
381
  if not force and not AskUser("Fail over instance(s) %s?" %
382
                               (",".join("'%s'" % name for name in pinst))):
383
    return 2
384

    
385
  jex = JobExecutor(cl=cl, opts=opts)
386
  for iname in pinst:
387
    op = opcodes.OpInstanceFailover(instance_name=iname,
388
                                    ignore_consistency=opts.ignore_consistency,
389
                                    iallocator=opts.iallocator)
390
    jex.QueueJob(iname, op)
391
  results = jex.GetResults()
392
  bad_cnt = len([row for row in results if not row[0]])
393
  if bad_cnt == 0:
394
    ToStdout("All %d instance(s) failed over successfully.", len(results))
395
  else:
396
    ToStdout("There were errors during the failover:\n"
397
             "%d error(s) out of %d instance(s).", bad_cnt, len(results))
398
  return retcode
399

    
400

    
401
def MigrateNode(opts, args):
402
  """Migrate all primary instance on a node.
403

404
  """
405
  cl = GetClient()
406
  force = opts.force
407
  selected_fields = ["name", "pinst_list"]
408

    
409
  result = cl.QueryNodes(names=args, fields=selected_fields, use_locking=False)
410
  ((node, pinst), ) = result
411

    
412
  if not pinst:
413
    ToStdout("No primary instances on node %s, exiting." % node)
414
    return 0
415

    
416
  pinst = utils.NiceSort(pinst)
417

    
418
  if not (force or
419
          AskUser("Migrate instance(s) %s?" %
420
                  utils.CommaJoin(utils.NiceSort(pinst)))):
421
    return constants.EXIT_CONFIRMATION
422

    
423
  # this should be removed once --non-live is deprecated
424
  if not opts.live and opts.migration_mode is not None:
425
    raise errors.OpPrereqError("Only one of the --non-live and "
426
                               "--migration-mode options can be passed",
427
                               errors.ECODE_INVAL)
428
  if not opts.live: # --non-live passed
429
    mode = constants.HT_MIGRATION_NONLIVE
430
  else:
431
    mode = opts.migration_mode
432

    
433
  op = opcodes.OpNodeMigrate(node_name=args[0], mode=mode,
434
                             iallocator=opts.iallocator,
435
                             target_node=opts.dst_node,
436
                             allow_runtime_changes=opts.allow_runtime_chgs,
437
                             ignore_ipolicy=opts.ignore_ipolicy)
438

    
439
  result = SubmitOrSend(op, opts, cl=cl)
440

    
441
  # Keep track of submitted jobs
442
  jex = JobExecutor(cl=cl, opts=opts)
443

    
444
  for (status, job_id) in result[constants.JOB_IDS_KEY]:
445
    jex.AddJobId(None, status, job_id)
446

    
447
  results = jex.GetResults()
448
  bad_cnt = len([row for row in results if not row[0]])
449
  if bad_cnt == 0:
450
    ToStdout("All instances migrated successfully.")
451
    rcode = constants.EXIT_SUCCESS
452
  else:
453
    ToStdout("There were %s errors during the node migration.", bad_cnt)
454
    rcode = constants.EXIT_FAILURE
455

    
456
  return rcode
457

    
458

    
459
def ShowNodeConfig(opts, args):
460
  """Show node information.
461

462
  @param opts: the command line options selected by the user
463
  @type args: list
464
  @param args: should either be an empty list, in which case
465
      we show information about all nodes, or should contain
466
      a list of nodes to be queried for information
467
  @rtype: int
468
  @return: the desired exit code
469

470
  """
471
  cl = GetClient()
472
  result = cl.QueryNodes(fields=["name", "pip", "sip",
473
                                 "pinst_list", "sinst_list",
474
                                 "master_candidate", "drained", "offline",
475
                                 "master_capable", "vm_capable", "powered",
476
                                 "ndparams", "custom_ndparams"],
477
                         names=args, use_locking=False)
478

    
479
  for (name, primary_ip, secondary_ip, pinst, sinst, is_mc, drained, offline,
480
       master_capable, vm_capable, powered, ndparams,
481
       ndparams_custom) in result:
482
    ToStdout("Node name: %s", name)
483
    ToStdout("  primary ip: %s", primary_ip)
484
    ToStdout("  secondary ip: %s", secondary_ip)
485
    ToStdout("  master candidate: %s", is_mc)
486
    ToStdout("  drained: %s", drained)
487
    ToStdout("  offline: %s", offline)
488
    if powered is not None:
489
      ToStdout("  powered: %s", powered)
490
    ToStdout("  master_capable: %s", master_capable)
491
    ToStdout("  vm_capable: %s", vm_capable)
492
    if vm_capable:
493
      if pinst:
494
        ToStdout("  primary for instances:")
495
        for iname in utils.NiceSort(pinst):
496
          ToStdout("    - %s", iname)
497
      else:
498
        ToStdout("  primary for no instances")
499
      if sinst:
500
        ToStdout("  secondary for instances:")
501
        for iname in utils.NiceSort(sinst):
502
          ToStdout("    - %s", iname)
503
      else:
504
        ToStdout("  secondary for no instances")
505
    ToStdout("  node parameters:")
506
    buf = StringIO()
507
    FormatParameterDict(buf, ndparams_custom, ndparams, level=2)
508
    ToStdout(buf.getvalue().rstrip("\n"))
509

    
510
  return 0
511

    
512

    
513
def RemoveNode(opts, args):
514
  """Remove a node from the cluster.
515

516
  @param opts: the command line options selected by the user
517
  @type args: list
518
  @param args: should contain only one element, the name of
519
      the node to be removed
520
  @rtype: int
521
  @return: the desired exit code
522

523
  """
524
  op = opcodes.OpNodeRemove(node_name=args[0])
525
  SubmitOpCode(op, opts=opts)
526
  return 0
527

    
528

    
529
def PowercycleNode(opts, args):
530
  """Remove a node from the cluster.
531

532
  @param opts: the command line options selected by the user
533
  @type args: list
534
  @param args: should contain only one element, the name of
535
      the node to be removed
536
  @rtype: int
537
  @return: the desired exit code
538

539
  """
540
  node = args[0]
541
  if (not opts.confirm and
542
      not AskUser("Are you sure you want to hard powercycle node %s?" % node)):
543
    return 2
544

    
545
  op = opcodes.OpNodePowercycle(node_name=node, force=opts.force)
546
  result = SubmitOrSend(op, opts)
547
  if result:
548
    ToStderr(result)
549
  return 0
550

    
551

    
552
def PowerNode(opts, args):
553
  """Change/ask power state of a node.
554

555
  @param opts: the command line options selected by the user
556
  @type args: list
557
  @param args: should contain only one element, the name of
558
      the node to be removed
559
  @rtype: int
560
  @return: the desired exit code
561

562
  """
563
  command = args.pop(0)
564

    
565
  if opts.no_headers:
566
    headers = None
567
  else:
568
    headers = {"node": "Node", "status": "Status"}
569

    
570
  if command not in _LIST_POWER_COMMANDS:
571
    ToStderr("power subcommand %s not supported." % command)
572
    return constants.EXIT_FAILURE
573

    
574
  oob_command = "power-%s" % command
575

    
576
  if oob_command in _OOB_COMMAND_ASK:
577
    if not args:
578
      ToStderr("Please provide at least one node for this command")
579
      return constants.EXIT_FAILURE
580
    elif not opts.force and not ConfirmOperation(args, "nodes",
581
                                                 "power %s" % command):
582
      return constants.EXIT_FAILURE
583
    assert len(args) > 0
584

    
585
  opcodelist = []
586
  if not opts.ignore_status and oob_command == constants.OOB_POWER_OFF:
587
    # TODO: This is a little ugly as we can't catch and revert
588
    for node in args:
589
      opcodelist.append(opcodes.OpNodeSetParams(node_name=node, offline=True,
590
                                                auto_promote=opts.auto_promote))
591

    
592
  opcodelist.append(opcodes.OpOobCommand(node_names=args,
593
                                         command=oob_command,
594
                                         ignore_status=opts.ignore_status,
595
                                         timeout=opts.oob_timeout,
596
                                         power_delay=opts.power_delay))
597

    
598
  cli.SetGenericOpcodeOpts(opcodelist, opts)
599

    
600
  job_id = cli.SendJob(opcodelist)
601

    
602
  # We just want the OOB Opcode status
603
  # If it fails PollJob gives us the error message in it
604
  result = cli.PollJob(job_id)[-1]
605

    
606
  errs = 0
607
  data = []
608
  for node_result in result:
609
    (node_tuple, data_tuple) = node_result
610
    (_, node_name) = node_tuple
611
    (data_status, data_node) = data_tuple
612
    if data_status == constants.RS_NORMAL:
613
      if oob_command == constants.OOB_POWER_STATUS:
614
        if data_node[constants.OOB_POWER_STATUS_POWERED]:
615
          text = "powered"
616
        else:
617
          text = "unpowered"
618
        data.append([node_name, text])
619
      else:
620
        # We don't expect data here, so we just say, it was successfully invoked
621
        data.append([node_name, "invoked"])
622
    else:
623
      errs += 1
624
      data.append([node_name, cli.FormatResultError(data_status, True)])
625

    
626
  data = GenerateTable(separator=opts.separator, headers=headers,
627
                       fields=["node", "status"], data=data)
628

    
629
  for line in data:
630
    ToStdout(line)
631

    
632
  if errs:
633
    return constants.EXIT_FAILURE
634
  else:
635
    return constants.EXIT_SUCCESS
636

    
637

    
638
def Health(opts, args):
639
  """Show health of a node using OOB.
640

641
  @param opts: the command line options selected by the user
642
  @type args: list
643
  @param args: should contain only one element, the name of
644
      the node to be removed
645
  @rtype: int
646
  @return: the desired exit code
647

648
  """
649
  op = opcodes.OpOobCommand(node_names=args, command=constants.OOB_HEALTH,
650
                            timeout=opts.oob_timeout)
651
  result = SubmitOpCode(op, opts=opts)
652

    
653
  if opts.no_headers:
654
    headers = None
655
  else:
656
    headers = {"node": "Node", "status": "Status"}
657

    
658
  errs = 0
659
  data = []
660
  for node_result in result:
661
    (node_tuple, data_tuple) = node_result
662
    (_, node_name) = node_tuple
663
    (data_status, data_node) = data_tuple
664
    if data_status == constants.RS_NORMAL:
665
      data.append([node_name, "%s=%s" % tuple(data_node[0])])
666
      for item, status in data_node[1:]:
667
        data.append(["", "%s=%s" % (item, status)])
668
    else:
669
      errs += 1
670
      data.append([node_name, cli.FormatResultError(data_status, True)])
671

    
672
  data = GenerateTable(separator=opts.separator, headers=headers,
673
                       fields=["node", "status"], data=data)
674

    
675
  for line in data:
676
    ToStdout(line)
677

    
678
  if errs:
679
    return constants.EXIT_FAILURE
680
  else:
681
    return constants.EXIT_SUCCESS
682

    
683

    
684
def ListVolumes(opts, args):
685
  """List logical volumes on node(s).
686

687
  @param opts: the command line options selected by the user
688
  @type args: list
689
  @param args: should either be an empty list, in which case
690
      we list data for all nodes, or contain a list of nodes
691
      to display data only for those
692
  @rtype: int
693
  @return: the desired exit code
694

695
  """
696
  selected_fields = ParseFields(opts.output, _LIST_VOL_DEF_FIELDS)
697

    
698
  op = opcodes.OpNodeQueryvols(nodes=args, output_fields=selected_fields)
699
  output = SubmitOpCode(op, opts=opts)
700

    
701
  if not opts.no_headers:
702
    headers = {"node": "Node", "phys": "PhysDev",
703
               "vg": "VG", "name": "Name",
704
               "size": "Size", "instance": "Instance"}
705
  else:
706
    headers = None
707

    
708
  unitfields = ["size"]
709

    
710
  numfields = ["size"]
711

    
712
  data = GenerateTable(separator=opts.separator, headers=headers,
713
                       fields=selected_fields, unitfields=unitfields,
714
                       numfields=numfields, data=output, units=opts.units)
715

    
716
  for line in data:
717
    ToStdout(line)
718

    
719
  return 0
720

    
721

    
722
def ListStorage(opts, args):
723
  """List physical volumes on node(s).
724

725
  @param opts: the command line options selected by the user
726
  @type args: list
727
  @param args: should either be an empty list, in which case
728
      we list data for all nodes, or contain a list of nodes
729
      to display data only for those
730
  @rtype: int
731
  @return: the desired exit code
732

733
  """
734
  # TODO: Default to ST_FILE if LVM is disabled on the cluster
735
  if opts.user_storage_type is None:
736
    opts.user_storage_type = constants.ST_LVM_PV
737

    
738
  storage_type = ConvertStorageType(opts.user_storage_type)
739

    
740
  selected_fields = ParseFields(opts.output, _LIST_STOR_DEF_FIELDS)
741

    
742
  op = opcodes.OpNodeQueryStorage(nodes=args,
743
                                  storage_type=storage_type,
744
                                  output_fields=selected_fields)
745
  output = SubmitOpCode(op, opts=opts)
746

    
747
  if not opts.no_headers:
748
    headers = {
749
      constants.SF_NODE: "Node",
750
      constants.SF_TYPE: "Type",
751
      constants.SF_NAME: "Name",
752
      constants.SF_SIZE: "Size",
753
      constants.SF_USED: "Used",
754
      constants.SF_FREE: "Free",
755
      constants.SF_ALLOCATABLE: "Allocatable",
756
      }
757
  else:
758
    headers = None
759

    
760
  unitfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
761
  numfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
762

    
763
  # change raw values to nicer strings
764
  for row in output:
765
    for idx, field in enumerate(selected_fields):
766
      val = row[idx]
767
      if field == constants.SF_ALLOCATABLE:
768
        if val:
769
          val = "Y"
770
        else:
771
          val = "N"
772
      row[idx] = str(val)
773

    
774
  data = GenerateTable(separator=opts.separator, headers=headers,
775
                       fields=selected_fields, unitfields=unitfields,
776
                       numfields=numfields, data=output, units=opts.units)
777

    
778
  for line in data:
779
    ToStdout(line)
780

    
781
  return 0
782

    
783

    
784
def ModifyStorage(opts, args):
785
  """Modify storage volume on a node.
786

787
  @param opts: the command line options selected by the user
788
  @type args: list
789
  @param args: should contain 3 items: node name, storage type and volume name
790
  @rtype: int
791
  @return: the desired exit code
792

793
  """
794
  (node_name, user_storage_type, volume_name) = args
795

    
796
  storage_type = ConvertStorageType(user_storage_type)
797

    
798
  changes = {}
799

    
800
  if opts.allocatable is not None:
801
    changes[constants.SF_ALLOCATABLE] = opts.allocatable
802

    
803
  if changes:
804
    op = opcodes.OpNodeModifyStorage(node_name=node_name,
805
                                     storage_type=storage_type,
806
                                     name=volume_name,
807
                                     changes=changes)
808
    SubmitOrSend(op, opts)
809
  else:
810
    ToStderr("No changes to perform, exiting.")
811

    
812

    
813
def RepairStorage(opts, args):
814
  """Repairs a storage volume on a node.
815

816
  @param opts: the command line options selected by the user
817
  @type args: list
818
  @param args: should contain 3 items: node name, storage type and volume name
819
  @rtype: int
820
  @return: the desired exit code
821

822
  """
823
  (node_name, user_storage_type, volume_name) = args
824

    
825
  storage_type = ConvertStorageType(user_storage_type)
826

    
827
  op = opcodes.OpRepairNodeStorage(node_name=node_name,
828
                                   storage_type=storage_type,
829
                                   name=volume_name,
830
                                   ignore_consistency=opts.ignore_consistency)
831
  SubmitOrSend(op, opts)
832

    
833

    
834
def SetNodeParams(opts, args):
835
  """Modifies a node.
836

837
  @param opts: the command line options selected by the user
838
  @type args: list
839
  @param args: should contain only one element, the node name
840
  @rtype: int
841
  @return: the desired exit code
842

843
  """
844
  all_changes = [opts.master_candidate, opts.drained, opts.offline,
845
                 opts.master_capable, opts.vm_capable, opts.secondary_ip,
846
                 opts.ndparams]
847
  if (all_changes.count(None) == len(all_changes) and
848
      not (opts.hv_state or opts.disk_state)):
849
    ToStderr("Please give at least one of the parameters.")
850
    return 1
851

    
852
  if opts.disk_state:
853
    disk_state = utils.FlatToDict(opts.disk_state)
854
  else:
855
    disk_state = {}
856

    
857
  hv_state = dict(opts.hv_state)
858

    
859
  op = opcodes.OpNodeSetParams(node_name=args[0],
860
                               master_candidate=opts.master_candidate,
861
                               offline=opts.offline,
862
                               drained=opts.drained,
863
                               master_capable=opts.master_capable,
864
                               vm_capable=opts.vm_capable,
865
                               secondary_ip=opts.secondary_ip,
866
                               force=opts.force,
867
                               ndparams=opts.ndparams,
868
                               auto_promote=opts.auto_promote,
869
                               powered=opts.node_powered,
870
                               hv_state=hv_state,
871
                               disk_state=disk_state)
872

    
873
  # even if here we process the result, we allow submit only
874
  result = SubmitOrSend(op, opts)
875

    
876
  if result:
877
    ToStdout("Modified node %s", args[0])
878
    for param, data in result:
879
      ToStdout(" - %-5s -> %s", param, data)
880
  return 0
881

    
882

    
883
class ReplyStatus(object):
884
  """Class holding a reply status for synchronous confd clients.
885

886
  """
887
  def __init__(self):
888
    self.failure = True
889
    self.answer = False
890

    
891

    
892
def ListDrbd(opts, args):
893
  """Modifies a node.
894

895
  @param opts: the command line options selected by the user
896
  @type args: list
897
  @param args: should contain only one element, the node name
898
  @rtype: int
899
  @return: the desired exit code
900

901
  """
902
  if len(args) != 1:
903
    ToStderr("Please give one (and only one) node.")
904
    return constants.EXIT_FAILURE
905

    
906
  if not constants.ENABLE_CONFD:
907
    ToStderr("Error: this command requires confd support, but it has not"
908
             " been enabled at build time.")
909
    return constants.EXIT_FAILURE
910

    
911
  status = ReplyStatus()
912

    
913
  def ListDrbdConfdCallback(reply):
914
    """Callback for confd queries"""
915
    if reply.type == confd_client.UPCALL_REPLY:
916
      answer = reply.server_reply.answer
917
      reqtype = reply.orig_request.type
918
      if reqtype == constants.CONFD_REQ_NODE_DRBD:
919
        if reply.server_reply.status != constants.CONFD_REPL_STATUS_OK:
920
          ToStderr("Query gave non-ok status '%s': %s" %
921
                   (reply.server_reply.status,
922
                    reply.server_reply.answer))
923
          status.failure = True
924
          return
925
        if not confd.HTNodeDrbd(answer):
926
          ToStderr("Invalid response from server: expected %s, got %s",
927
                   confd.HTNodeDrbd, answer)
928
          status.failure = True
929
        else:
930
          status.failure = False
931
          status.answer = answer
932
      else:
933
        ToStderr("Unexpected reply %s!?", reqtype)
934
        status.failure = True
935

    
936
  node = args[0]
937
  hmac = utils.ReadFile(pathutils.CONFD_HMAC_KEY)
938
  filter_callback = confd_client.ConfdFilterCallback(ListDrbdConfdCallback)
939
  counting_callback = confd_client.ConfdCountingCallback(filter_callback)
940
  cf_client = confd_client.ConfdClient(hmac, [constants.IP4_ADDRESS_LOCALHOST],
941
                                       counting_callback)
942
  req = confd_client.ConfdClientRequest(type=constants.CONFD_REQ_NODE_DRBD,
943
                                        query=node)
944

    
945
  def DoConfdRequestReply(req):
946
    counting_callback.RegisterQuery(req.rsalt)
947
    cf_client.SendRequest(req, async=False)
948
    while not counting_callback.AllAnswered():
949
      if not cf_client.ReceiveReply():
950
        ToStderr("Did not receive all expected confd replies")
951
        break
952

    
953
  DoConfdRequestReply(req)
954

    
955
  if status.failure:
956
    return constants.EXIT_FAILURE
957

    
958
  fields = ["node", "minor", "instance", "disk", "role", "peer"]
959
  if opts.no_headers:
960
    headers = None
961
  else:
962
    headers = {"node": "Node", "minor": "Minor", "instance": "Instance",
963
               "disk": "Disk", "role": "Role", "peer": "PeerNode"}
964

    
965
  data = GenerateTable(separator=opts.separator, headers=headers,
966
                       fields=fields, data=sorted(status.answer),
967
                       numfields=["minor"])
968
  for line in data:
969
    ToStdout(line)
970

    
971
  return constants.EXIT_SUCCESS
972

    
973
commands = {
974
  "add": (
975
    AddNode, [ArgHost(min=1, max=1)],
976
    [SECONDARY_IP_OPT, READD_OPT, NOSSH_KEYCHECK_OPT, NODE_FORCE_JOIN_OPT,
977
     NONODE_SETUP_OPT, VERBOSE_OPT, NODEGROUP_OPT, PRIORITY_OPT,
978
     CAPAB_MASTER_OPT, CAPAB_VM_OPT, NODE_PARAMS_OPT, HV_STATE_OPT,
979
     DISK_STATE_OPT],
980
    "[-s ip] [--readd] [--no-ssh-key-check] [--force-join]"
981
    " [--no-node-setup] [--verbose]"
982
    " <node_name>",
983
    "Add a node to the cluster"),
984
  "evacuate": (
985
    EvacuateNode, ARGS_ONE_NODE,
986
    [FORCE_OPT, IALLOCATOR_OPT, NEW_SECONDARY_OPT, EARLY_RELEASE_OPT,
987
     PRIORITY_OPT, PRIMARY_ONLY_OPT, SECONDARY_ONLY_OPT, SUBMIT_OPT],
988
    "[-f] {-I <iallocator> | -n <dst>} [-p | -s] [options...] <node>",
989
    "Relocate the primary and/or secondary instances from a node"),
990
  "failover": (
991
    FailoverNode, ARGS_ONE_NODE, [FORCE_OPT, IGNORE_CONSIST_OPT,
992
                                  IALLOCATOR_OPT, PRIORITY_OPT],
993
    "[-f] <node>",
994
    "Stops the primary instances on a node and start them on their"
995
    " secondary node (only for instances with drbd disk template)"),
996
  "migrate": (
997
    MigrateNode, ARGS_ONE_NODE,
998
    [FORCE_OPT, NONLIVE_OPT, MIGRATION_MODE_OPT, DST_NODE_OPT,
999
     IALLOCATOR_OPT, PRIORITY_OPT, IGNORE_IPOLICY_OPT,
1000
     NORUNTIME_CHGS_OPT, SUBMIT_OPT],
1001
    "[-f] <node>",
1002
    "Migrate all the primary instance on a node away from it"
1003
    " (only for instances of type drbd)"),
1004
  "info": (
1005
    ShowNodeConfig, ARGS_MANY_NODES, [],
1006
    "[<node_name>...]", "Show information about the node(s)"),
1007
  "list": (
1008
    ListNodes, ARGS_MANY_NODES,
1009
    [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, VERBOSE_OPT,
1010
     FORCE_FILTER_OPT],
1011
    "[nodes...]",
1012
    "Lists the nodes in the cluster. The available fields can be shown using"
1013
    " the \"list-fields\" command (see the man page for details)."
1014
    " The default field list is (in order): %s." %
1015
    utils.CommaJoin(_LIST_DEF_FIELDS)),
1016
  "list-fields": (
1017
    ListNodeFields, [ArgUnknown()],
1018
    [NOHDR_OPT, SEP_OPT],
1019
    "[fields...]",
1020
    "Lists all available fields for nodes"),
1021
  "modify": (
1022
    SetNodeParams, ARGS_ONE_NODE,
1023
    [FORCE_OPT, SUBMIT_OPT, MC_OPT, DRAINED_OPT, OFFLINE_OPT,
1024
     CAPAB_MASTER_OPT, CAPAB_VM_OPT, SECONDARY_IP_OPT,
1025
     AUTO_PROMOTE_OPT, DRY_RUN_OPT, PRIORITY_OPT, NODE_PARAMS_OPT,
1026
     NODE_POWERED_OPT, HV_STATE_OPT, DISK_STATE_OPT],
1027
    "<node_name>", "Alters the parameters of a node"),
1028
  "powercycle": (
1029
    PowercycleNode, ARGS_ONE_NODE,
1030
    [FORCE_OPT, CONFIRM_OPT, DRY_RUN_OPT, PRIORITY_OPT, SUBMIT_OPT],
1031
    "<node_name>", "Tries to forcefully powercycle a node"),
1032
  "power": (
1033
    PowerNode,
1034
    [ArgChoice(min=1, max=1, choices=_LIST_POWER_COMMANDS),
1035
     ArgNode()],
1036
    [SUBMIT_OPT, AUTO_PROMOTE_OPT, PRIORITY_OPT, IGNORE_STATUS_OPT,
1037
     FORCE_OPT, NOHDR_OPT, SEP_OPT, OOB_TIMEOUT_OPT, POWER_DELAY_OPT],
1038
    "on|off|cycle|status [nodes...]",
1039
    "Change power state of node by calling out-of-band helper."),
1040
  "remove": (
1041
    RemoveNode, ARGS_ONE_NODE, [DRY_RUN_OPT, PRIORITY_OPT],
1042
    "<node_name>", "Removes a node from the cluster"),
1043
  "volumes": (
1044
    ListVolumes, [ArgNode()],
1045
    [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, PRIORITY_OPT],
1046
    "[<node_name>...]", "List logical volumes on node(s)"),
1047
  "list-storage": (
1048
    ListStorage, ARGS_MANY_NODES,
1049
    [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, _STORAGE_TYPE_OPT,
1050
     PRIORITY_OPT],
1051
    "[<node_name>...]", "List physical volumes on node(s). The available"
1052
    " fields are (see the man page for details): %s." %
1053
    (utils.CommaJoin(_LIST_STOR_HEADERS))),
1054
  "modify-storage": (
1055
    ModifyStorage,
1056
    [ArgNode(min=1, max=1),
1057
     ArgChoice(min=1, max=1, choices=_MODIFIABLE_STORAGE_TYPES),
1058
     ArgFile(min=1, max=1)],
1059
    [ALLOCATABLE_OPT, DRY_RUN_OPT, PRIORITY_OPT, SUBMIT_OPT],
1060
    "<node_name> <storage_type> <name>", "Modify storage volume on a node"),
1061
  "repair-storage": (
1062
    RepairStorage,
1063
    [ArgNode(min=1, max=1),
1064
     ArgChoice(min=1, max=1, choices=_REPAIRABLE_STORAGE_TYPES),
1065
     ArgFile(min=1, max=1)],
1066
    [IGNORE_CONSIST_OPT, DRY_RUN_OPT, PRIORITY_OPT, SUBMIT_OPT],
1067
    "<node_name> <storage_type> <name>",
1068
    "Repairs a storage volume on a node"),
1069
  "list-tags": (
1070
    ListTags, ARGS_ONE_NODE, [],
1071
    "<node_name>", "List the tags of the given node"),
1072
  "add-tags": (
1073
    AddTags, [ArgNode(min=1, max=1), ArgUnknown()],
1074
    [TAG_SRC_OPT, PRIORITY_OPT, SUBMIT_OPT],
1075
    "<node_name> tag...", "Add tags to the given node"),
1076
  "remove-tags": (
1077
    RemoveTags, [ArgNode(min=1, max=1), ArgUnknown()],
1078
    [TAG_SRC_OPT, PRIORITY_OPT, SUBMIT_OPT],
1079
    "<node_name> tag...", "Remove tags from the given node"),
1080
  "health": (
1081
    Health, ARGS_MANY_NODES,
1082
    [NOHDR_OPT, SEP_OPT, PRIORITY_OPT, OOB_TIMEOUT_OPT],
1083
    "[<node_name>...]", "List health of node(s) using out-of-band"),
1084
  "list-drbd": (
1085
    ListDrbd, ARGS_ONE_NODE,
1086
    [NOHDR_OPT, SEP_OPT],
1087
    "[<node_name>]", "Query the list of used DRBD minors on the given node"),
1088
  }
1089

    
1090
#: dictionary with aliases for commands
1091
aliases = {
1092
  "show": "info",
1093
  }
1094

    
1095

    
1096
def Main():
1097
  return GenericMain(commands, aliases=aliases,
1098
                     override={"tag_type": constants.TAG_NODE},
1099
                     env_override=_ENV_OVERRIDE)