Statistics
| Branch: | Tag: | Revision:

root / lib / client / gnt_node.py @ 7acbda7b

History | View | Annotate | Download (34.4 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 cStringIO import StringIO
40

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

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

    
51

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

    
55

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

    
67

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

    
71

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

    
83

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

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

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

    
104
_MODIFIABLE_STORAGE_TYPES = constants.MODIFIABLE_STORAGE_FIELDS.keys()
105

    
106

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

    
110

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

    
113

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

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

    
124

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

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

    
135

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

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

142
  """
143

    
144
  assert nodes, "Empty node list"
145

    
146
  cmd = [constants.SETUP_SSH]
147

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

    
159
  cmd.extend(nodes)
160

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

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

    
168

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

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

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

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

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

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

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

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

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

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

    
229
  hv_state = dict(opts.hv_state)
230

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

    
239

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

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

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

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

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

    
260

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

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

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

    
274

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

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

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

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

    
302
  # Determine affected instances
303
  fields = []
304

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

    
310
  cl = GetClient()
311

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

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

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

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

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

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

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

    
349
  return rcode
350

    
351

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

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

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

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

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

    
376
  pinst = utils.NiceSort(pinst)
377

    
378
  retcode = 0
379

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

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

    
399

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

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

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

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

    
415
  pinst = utils.NiceSort(pinst)
416

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

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

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

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

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

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

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

    
455
  return rcode
456

    
457

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

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

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

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

    
509
  return 0
510

    
511

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

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

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

    
527

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

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

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

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

    
550

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

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

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

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

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

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

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

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

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

    
597
  cli.SetGenericOpcodeOpts(opcodelist, opts)
598

    
599
  job_id = cli.SendJob(opcodelist)
600

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

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

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

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

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

    
636

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

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

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

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

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

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

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

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

    
682

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

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

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

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

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

    
707
  unitfields = ["size"]
708

    
709
  numfields = ["size"]
710

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

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

    
718
  return 0
719

    
720

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

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

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

    
737
  storage_type = ConvertStorageType(opts.user_storage_type)
738

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

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

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

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

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

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

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

    
780
  return 0
781

    
782

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

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

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

    
795
  storage_type = ConvertStorageType(user_storage_type)
796

    
797
  changes = {}
798

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

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

    
811

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

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

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

    
824
  storage_type = ConvertStorageType(user_storage_type)
825

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

    
832

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

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

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

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

    
856
  hv_state = dict(opts.hv_state)
857

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

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

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

    
881

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

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

    
890

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

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

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

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

    
910
  if not constants.HS_CONFD:
911
    ToStderr("Error: this command requires the Haskell version of confd,"
912
             " but it has not been enabled at build time.")
913
    return constants.EXIT_FAILURE
914

    
915
  status = ReplyStatus()
916

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

    
940
  node = args[0]
941
  hmac = utils.ReadFile(constants.CONFD_HMAC_KEY)
942
  filter_callback = confd_client.ConfdFilterCallback(ListDrbdConfdCallback)
943
  counting_callback = confd_client.ConfdCountingCallback(filter_callback)
944
  cf_client = confd_client.ConfdClient(hmac, [constants.IP4_ADDRESS_LOCALHOST],
945
                                       counting_callback)
946
  req = confd_client.ConfdClientRequest(type=constants.CONFD_REQ_NODE_DRBD,
947
                                        query=node)
948

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

    
957
  DoConfdRequestReply(req)
958

    
959
  if status.failure:
960
    return constants.EXIT_FAILURE
961

    
962
  fields = ["node", "minor", "instance", "disk", "role", "peer"]
963
  headers = {"node": "Node", "minor": "Minor", "instance": "Instance",
964
             "disk": "Disk", "role": "Role", "peer": "PeerNode"}
965

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

    
972
  return constants.EXIT_SUCCESS
973

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

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

    
1096

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