Statistics
| Branch: | Tag: | Revision:

root / lib / client / gnt_node.py @ 5f6d1b42

History | View | Annotate | Download (37.2 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013 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
import errno
31

    
32
from ganeti.cli import *
33
from ganeti import cli
34
from ganeti import bootstrap
35
from ganeti import opcodes
36
from ganeti import utils
37
from ganeti import constants
38
from ganeti import errors
39
from ganeti import netutils
40
from ganeti import pathutils
41
from ganeti import ssh
42
from ganeti import compat
43

    
44
from ganeti import confd
45
from ganeti.confd import client as confd_client
46

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

    
54

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

    
58

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

    
70

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

    
74

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

    
86

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

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

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

    
107
_MODIFIABLE_STORAGE_TYPES = constants.MODIFIABLE_STORAGE_FIELDS.keys()
108

    
109
_OOB_COMMAND_ASK = compat.UniqueFrozenset([
110
  constants.OOB_POWER_OFF,
111
  constants.OOB_POWER_CYCLE,
112
  ])
113

    
114
_ENV_OVERRIDE = compat.UniqueFrozenset(["list"])
115

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

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

    
126

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

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

    
137

    
138
def _TryReadFile(path):
139
  """Tries to read a file.
140

141
  If the file is not found, C{None} is returned.
142

143
  @type path: string
144
  @param path: Filename
145
  @rtype: None or string
146
  @todo: Consider adding a generic ENOENT wrapper
147

148
  """
149
  try:
150
    return utils.ReadFile(path)
151
  except EnvironmentError, err:
152
    if err.errno == errno.ENOENT:
153
      return None
154
    else:
155
      raise
156

    
157

    
158
def _ReadSshKeys(keyfiles, _tostderr_fn=ToStderr):
159
  """Reads SSH keys according to C{keyfiles}.
160

161
  @type keyfiles: dict
162
  @param keyfiles: Dictionary with keys of L{constants.SSHK_ALL} and two-values
163
    tuples (private and public key file)
164
  @rtype: list
165
  @return: List of three-values tuples (L{constants.SSHK_ALL}, private and
166
    public key as strings)
167

168
  """
169
  result = []
170

    
171
  for (kind, (private_file, public_file)) in keyfiles.items():
172
    private_key = _TryReadFile(private_file)
173
    public_key = _TryReadFile(public_file)
174

    
175
    if public_key and private_key:
176
      result.append((kind, private_key, public_key))
177
    elif public_key or private_key:
178
      _tostderr_fn("Couldn't find a complete set of keys for kind '%s'; files"
179
                   " '%s' and '%s'", kind, private_file, public_file)
180

    
181
  return result
182

    
183

    
184
def _SetupSSH(options, cluster_name, node):
185
  """Configures a destination node's SSH daemon.
186

187
  @param options: Command line options
188
  @type cluster_name
189
  @param cluster_name: Cluster name
190
  @type node: string
191
  @param node: Destination node name
192

193
  """
194
  if options.force_join:
195
    ToStderr("The \"--force-join\" option is no longer supported and will be"
196
             " ignored.")
197

    
198
  host_keys = _ReadSshKeys(constants.SSH_DAEMON_KEYFILES)
199

    
200
  (_, root_keyfiles) = \
201
    ssh.GetAllUserFiles(constants.SSH_LOGIN_USER, mkdir=False, dircheck=False)
202

    
203
  root_keys = _ReadSshKeys(root_keyfiles)
204

    
205
  (_, cert_pem) = \
206
    utils.ExtractX509Certificate(utils.ReadFile(pathutils.NODED_CERT_FILE))
207

    
208
  data = {
209
    constants.SSHS_CLUSTER_NAME: cluster_name,
210
    constants.SSHS_NODE_DAEMON_CERTIFICATE: cert_pem,
211
    constants.SSHS_SSH_HOST_KEY: host_keys,
212
    constants.SSHS_SSH_ROOT_KEY: root_keys,
213
    }
214

    
215
  bootstrap.RunNodeSetupCmd(cluster_name, node, pathutils.PREPARE_NODE_JOIN,
216
                            options.debug, options.verbose, False,
217
                            options.ssh_key_check, options.ssh_key_check, data)
218

    
219

    
220
@UsesRPC
221
def AddNode(opts, args):
222
  """Add a node to the cluster.
223

224
  @param opts: the command line options selected by the user
225
  @type args: list
226
  @param args: should contain only one element, the new node name
227
  @rtype: int
228
  @return: the desired exit code
229

230
  """
231
  cl = GetClient()
232
  node = netutils.GetHostname(name=args[0]).name
233
  readd = opts.readd
234

    
235
  try:
236
    output = cl.QueryNodes(names=[node], fields=["name", "sip", "master"],
237
                           use_locking=False)
238
    node_exists, sip, is_master = output[0]
239
  except (errors.OpPrereqError, errors.OpExecError):
240
    node_exists = ""
241
    sip = None
242

    
243
  if readd:
244
    if not node_exists:
245
      ToStderr("Node %s not in the cluster"
246
               " - please retry without '--readd'", node)
247
      return 1
248
    if is_master:
249
      ToStderr("Node %s is the master, cannot readd", node)
250
      return 1
251
  else:
252
    if node_exists:
253
      ToStderr("Node %s already in the cluster (as %s)"
254
               " - please retry with '--readd'", node, node_exists)
255
      return 1
256
    sip = opts.secondary_ip
257

    
258
  # read the cluster name from the master
259
  (cluster_name, ) = cl.QueryConfigValues(["cluster_name"])
260

    
261
  if not readd and opts.node_setup:
262
    ToStderr("-- WARNING -- \n"
263
             "Performing this operation is going to replace the ssh daemon"
264
             " keypair\n"
265
             "on the target machine (%s) with the ones of the"
266
             " current one\n"
267
             "and grant full intra-cluster ssh root access to/from it\n", node)
268

    
269
  if opts.node_setup:
270
    _SetupSSH(opts, cluster_name, node)
271

    
272
  bootstrap.SetupNodeDaemon(opts, cluster_name, node)
273

    
274
  if opts.disk_state:
275
    disk_state = utils.FlatToDict(opts.disk_state)
276
  else:
277
    disk_state = {}
278

    
279
  hv_state = dict(opts.hv_state)
280

    
281
  op = opcodes.OpNodeAdd(node_name=args[0], secondary_ip=sip,
282
                         readd=opts.readd, group=opts.nodegroup,
283
                         vm_capable=opts.vm_capable, ndparams=opts.ndparams,
284
                         master_capable=opts.master_capable,
285
                         disk_state=disk_state,
286
                         hv_state=hv_state)
287
  SubmitOpCode(op, opts=opts)
288

    
289

    
290
def ListNodes(opts, args):
291
  """List nodes and their properties.
292

293
  @param opts: the command line options selected by the user
294
  @type args: list
295
  @param args: nodes to list, or empty for all
296
  @rtype: int
297
  @return: the desired exit code
298

299
  """
300
  selected_fields = ParseFields(opts.output, _LIST_DEF_FIELDS)
301

    
302
  fmtoverride = dict.fromkeys(["pinst_list", "sinst_list", "tags"],
303
                              (",".join, False))
304

    
305
  cl = GetClient(query=True)
306

    
307
  return GenericList(constants.QR_NODE, selected_fields, args, opts.units,
308
                     opts.separator, not opts.no_headers,
309
                     format_override=fmtoverride, verbose=opts.verbose,
310
                     force_filter=opts.force_filter, cl=cl)
311

    
312

    
313
def ListNodeFields(opts, args):
314
  """List node fields.
315

316
  @param opts: the command line options selected by the user
317
  @type args: list
318
  @param args: fields to list, or empty for all
319
  @rtype: int
320
  @return: the desired exit code
321

322
  """
323
  cl = GetClient(query=True)
324

    
325
  return GenericListFields(constants.QR_NODE, args, opts.separator,
326
                           not opts.no_headers, cl=cl)
327

    
328

    
329
def EvacuateNode(opts, args):
330
  """Relocate all secondary instance from a node.
331

332
  @param opts: the command line options selected by the user
333
  @type args: list
334
  @param args: should be an empty list
335
  @rtype: int
336
  @return: the desired exit code
337

338
  """
339
  if opts.dst_node is not None:
340
    ToStderr("New secondary node given (disabling iallocator), hence evacuating"
341
             " secondary instances only.")
342
    opts.secondary_only = True
343
    opts.primary_only = False
344

    
345
  if opts.secondary_only and opts.primary_only:
346
    raise errors.OpPrereqError("Only one of the --primary-only and"
347
                               " --secondary-only options can be passed",
348
                               errors.ECODE_INVAL)
349
  elif opts.primary_only:
350
    mode = constants.NODE_EVAC_PRI
351
  elif opts.secondary_only:
352
    mode = constants.NODE_EVAC_SEC
353
  else:
354
    mode = constants.NODE_EVAC_ALL
355

    
356
  # Determine affected instances
357
  fields = []
358

    
359
  if not opts.secondary_only:
360
    fields.append("pinst_list")
361
  if not opts.primary_only:
362
    fields.append("sinst_list")
363

    
364
  cl = GetClient()
365

    
366
  qcl = GetClient(query=True)
367
  result = qcl.QueryNodes(names=args, fields=fields, use_locking=False)
368
  qcl.Close()
369

    
370
  instances = set(itertools.chain(*itertools.chain(*itertools.chain(result))))
371

    
372
  if not instances:
373
    # No instances to evacuate
374
    ToStderr("No instances to evacuate on node(s) %s, exiting.",
375
             utils.CommaJoin(args))
376
    return constants.EXIT_SUCCESS
377

    
378
  if not (opts.force or
379
          AskUser("Relocate instance(s) %s from node(s) %s?" %
380
                  (utils.CommaJoin(utils.NiceSort(instances)),
381
                   utils.CommaJoin(args)))):
382
    return constants.EXIT_CONFIRMATION
383

    
384
  # Evacuate node
385
  op = opcodes.OpNodeEvacuate(node_name=args[0], mode=mode,
386
                              remote_node=opts.dst_node,
387
                              iallocator=opts.iallocator,
388
                              early_release=opts.early_release)
389
  result = SubmitOrSend(op, opts, cl=cl)
390

    
391
  # Keep track of submitted jobs
392
  jex = JobExecutor(cl=cl, opts=opts)
393

    
394
  for (status, job_id) in result[constants.JOB_IDS_KEY]:
395
    jex.AddJobId(None, status, job_id)
396

    
397
  results = jex.GetResults()
398
  bad_cnt = len([row for row in results if not row[0]])
399
  if bad_cnt == 0:
400
    ToStdout("All instances evacuated successfully.")
401
    rcode = constants.EXIT_SUCCESS
402
  else:
403
    ToStdout("There were %s errors during the evacuation.", bad_cnt)
404
    rcode = constants.EXIT_FAILURE
405

    
406
  return rcode
407

    
408

    
409
def FailoverNode(opts, args):
410
  """Failover all primary instance on a node.
411

412
  @param opts: the command line options selected by the user
413
  @type args: list
414
  @param args: should be an empty list
415
  @rtype: int
416
  @return: the desired exit code
417

418
  """
419
  cl = GetClient()
420
  force = opts.force
421
  selected_fields = ["name", "pinst_list"]
422

    
423
  # these fields are static data anyway, so it doesn't matter, but
424
  # locking=True should be safer
425
  qcl = GetClient(query=True)
426
  result = cl.QueryNodes(names=args, fields=selected_fields,
427
                         use_locking=False)
428
  qcl.Close()
429
  node, pinst = result[0]
430

    
431
  if not pinst:
432
    ToStderr("No primary instances on node %s, exiting.", node)
433
    return 0
434

    
435
  pinst = utils.NiceSort(pinst)
436

    
437
  retcode = 0
438

    
439
  if not force and not AskUser("Fail over instance(s) %s?" %
440
                               (",".join("'%s'" % name for name in pinst))):
441
    return 2
442

    
443
  jex = JobExecutor(cl=cl, opts=opts)
444
  for iname in pinst:
445
    op = opcodes.OpInstanceFailover(instance_name=iname,
446
                                    ignore_consistency=opts.ignore_consistency,
447
                                    iallocator=opts.iallocator)
448
    jex.QueueJob(iname, op)
449
  results = jex.GetResults()
450
  bad_cnt = len([row for row in results if not row[0]])
451
  if bad_cnt == 0:
452
    ToStdout("All %d instance(s) failed over successfully.", len(results))
453
  else:
454
    ToStdout("There were errors during the failover:\n"
455
             "%d error(s) out of %d instance(s).", bad_cnt, len(results))
456
  return retcode
457

    
458

    
459
def MigrateNode(opts, args):
460
  """Migrate all primary instance on a node.
461

462
  """
463
  cl = GetClient()
464
  force = opts.force
465
  selected_fields = ["name", "pinst_list"]
466

    
467
  qcl = GetClient(query=True)
468
  result = cl.QueryNodes(names=args, fields=selected_fields, use_locking=False)
469
  qcl.Close()
470
  ((node, pinst), ) = result
471

    
472
  if not pinst:
473
    ToStdout("No primary instances on node %s, exiting." % node)
474
    return 0
475

    
476
  pinst = utils.NiceSort(pinst)
477

    
478
  if not (force or
479
          AskUser("Migrate instance(s) %s?" %
480
                  utils.CommaJoin(utils.NiceSort(pinst)))):
481
    return constants.EXIT_CONFIRMATION
482

    
483
  # this should be removed once --non-live is deprecated
484
  if not opts.live and opts.migration_mode is not None:
485
    raise errors.OpPrereqError("Only one of the --non-live and "
486
                               "--migration-mode options can be passed",
487
                               errors.ECODE_INVAL)
488
  if not opts.live: # --non-live passed
489
    mode = constants.HT_MIGRATION_NONLIVE
490
  else:
491
    mode = opts.migration_mode
492

    
493
  op = opcodes.OpNodeMigrate(node_name=args[0], mode=mode,
494
                             iallocator=opts.iallocator,
495
                             target_node=opts.dst_node,
496
                             allow_runtime_changes=opts.allow_runtime_chgs,
497
                             ignore_ipolicy=opts.ignore_ipolicy)
498

    
499
  result = SubmitOrSend(op, opts, cl=cl)
500

    
501
  # Keep track of submitted jobs
502
  jex = JobExecutor(cl=cl, opts=opts)
503

    
504
  for (status, job_id) in result[constants.JOB_IDS_KEY]:
505
    jex.AddJobId(None, status, job_id)
506

    
507
  results = jex.GetResults()
508
  bad_cnt = len([row for row in results if not row[0]])
509
  if bad_cnt == 0:
510
    ToStdout("All instances migrated successfully.")
511
    rcode = constants.EXIT_SUCCESS
512
  else:
513
    ToStdout("There were %s errors during the node migration.", bad_cnt)
514
    rcode = constants.EXIT_FAILURE
515

    
516
  return rcode
517

    
518

    
519
def _FormatNodeInfo(node_info):
520
  """Format node information for L{cli.PrintGenericInfo()}.
521

522
  """
523
  (name, primary_ip, secondary_ip, pinst, sinst, is_mc, drained, offline,
524
   master_capable, vm_capable, powered, ndparams, ndparams_custom) = node_info
525
  info = [
526
    ("Node name", name),
527
    ("primary ip", primary_ip),
528
    ("secondary ip", secondary_ip),
529
    ("master candidate", is_mc),
530
    ("drained", drained),
531
    ("offline", offline),
532
    ]
533
  if powered is not None:
534
    info.append(("powered", powered))
535
  info.extend([
536
    ("master_capable", master_capable),
537
    ("vm_capable", vm_capable),
538
    ])
539
  if vm_capable:
540
    info.extend([
541
      ("primary for instances",
542
       [iname for iname in utils.NiceSort(pinst)]),
543
      ("secondary for instances",
544
       [iname for iname in utils.NiceSort(sinst)]),
545
      ])
546
  info.append(("node parameters",
547
               FormatParamsDictInfo(ndparams_custom, ndparams)))
548
  return info
549

    
550

    
551
def ShowNodeConfig(opts, args):
552
  """Show node information.
553

554
  @param opts: the command line options selected by the user
555
  @type args: list
556
  @param args: should either be an empty list, in which case
557
      we show information about all nodes, or should contain
558
      a list of nodes to be queried for information
559
  @rtype: int
560
  @return: the desired exit code
561

562
  """
563
  cl = GetClient(query=True)
564
  result = cl.QueryNodes(fields=["name", "pip", "sip",
565
                                 "pinst_list", "sinst_list",
566
                                 "master_candidate", "drained", "offline",
567
                                 "master_capable", "vm_capable", "powered",
568
                                 "ndparams", "custom_ndparams"],
569
                         names=args, use_locking=False)
570
  PrintGenericInfo([
571
    _FormatNodeInfo(node_info)
572
    for node_info in result
573
    ])
574
  return 0
575

    
576

    
577
def RemoveNode(opts, args):
578
  """Remove a node from the cluster.
579

580
  @param opts: the command line options selected by the user
581
  @type args: list
582
  @param args: should contain only one element, the name of
583
      the node to be removed
584
  @rtype: int
585
  @return: the desired exit code
586

587
  """
588
  op = opcodes.OpNodeRemove(node_name=args[0])
589
  SubmitOpCode(op, opts=opts)
590
  return 0
591

    
592

    
593
def PowercycleNode(opts, args):
594
  """Remove a node from the cluster.
595

596
  @param opts: the command line options selected by the user
597
  @type args: list
598
  @param args: should contain only one element, the name of
599
      the node to be removed
600
  @rtype: int
601
  @return: the desired exit code
602

603
  """
604
  node = args[0]
605
  if (not opts.confirm and
606
      not AskUser("Are you sure you want to hard powercycle node %s?" % node)):
607
    return 2
608

    
609
  op = opcodes.OpNodePowercycle(node_name=node, force=opts.force)
610
  result = SubmitOrSend(op, opts)
611
  if result:
612
    ToStderr(result)
613
  return 0
614

    
615

    
616
def PowerNode(opts, args):
617
  """Change/ask power state of a node.
618

619
  @param opts: the command line options selected by the user
620
  @type args: list
621
  @param args: should contain only one element, the name of
622
      the node to be removed
623
  @rtype: int
624
  @return: the desired exit code
625

626
  """
627
  command = args.pop(0)
628

    
629
  if opts.no_headers:
630
    headers = None
631
  else:
632
    headers = {"node": "Node", "status": "Status"}
633

    
634
  if command not in _LIST_POWER_COMMANDS:
635
    ToStderr("power subcommand %s not supported." % command)
636
    return constants.EXIT_FAILURE
637

    
638
  oob_command = "power-%s" % command
639

    
640
  if oob_command in _OOB_COMMAND_ASK:
641
    if not args:
642
      ToStderr("Please provide at least one node for this command")
643
      return constants.EXIT_FAILURE
644
    elif not opts.force and not ConfirmOperation(args, "nodes",
645
                                                 "power %s" % command):
646
      return constants.EXIT_FAILURE
647
    assert len(args) > 0
648

    
649
  opcodelist = []
650
  if not opts.ignore_status and oob_command == constants.OOB_POWER_OFF:
651
    # TODO: This is a little ugly as we can't catch and revert
652
    for node in args:
653
      opcodelist.append(opcodes.OpNodeSetParams(node_name=node, offline=True,
654
                                                auto_promote=opts.auto_promote))
655

    
656
  opcodelist.append(opcodes.OpOobCommand(node_names=args,
657
                                         command=oob_command,
658
                                         ignore_status=opts.ignore_status,
659
                                         timeout=opts.oob_timeout,
660
                                         power_delay=opts.power_delay))
661

    
662
  cli.SetGenericOpcodeOpts(opcodelist, opts)
663

    
664
  job_id = cli.SendJob(opcodelist)
665

    
666
  # We just want the OOB Opcode status
667
  # If it fails PollJob gives us the error message in it
668
  result = cli.PollJob(job_id)[-1]
669

    
670
  errs = 0
671
  data = []
672
  for node_result in result:
673
    (node_tuple, data_tuple) = node_result
674
    (_, node_name) = node_tuple
675
    (data_status, data_node) = data_tuple
676
    if data_status == constants.RS_NORMAL:
677
      if oob_command == constants.OOB_POWER_STATUS:
678
        if data_node[constants.OOB_POWER_STATUS_POWERED]:
679
          text = "powered"
680
        else:
681
          text = "unpowered"
682
        data.append([node_name, text])
683
      else:
684
        # We don't expect data here, so we just say, it was successfully invoked
685
        data.append([node_name, "invoked"])
686
    else:
687
      errs += 1
688
      data.append([node_name, cli.FormatResultError(data_status, True)])
689

    
690
  data = GenerateTable(separator=opts.separator, headers=headers,
691
                       fields=["node", "status"], data=data)
692

    
693
  for line in data:
694
    ToStdout(line)
695

    
696
  if errs:
697
    return constants.EXIT_FAILURE
698
  else:
699
    return constants.EXIT_SUCCESS
700

    
701

    
702
def Health(opts, args):
703
  """Show health of a node using OOB.
704

705
  @param opts: the command line options selected by the user
706
  @type args: list
707
  @param args: should contain only one element, the name of
708
      the node to be removed
709
  @rtype: int
710
  @return: the desired exit code
711

712
  """
713
  op = opcodes.OpOobCommand(node_names=args, command=constants.OOB_HEALTH,
714
                            timeout=opts.oob_timeout)
715
  result = SubmitOpCode(op, opts=opts)
716

    
717
  if opts.no_headers:
718
    headers = None
719
  else:
720
    headers = {"node": "Node", "status": "Status"}
721

    
722
  errs = 0
723
  data = []
724
  for node_result in result:
725
    (node_tuple, data_tuple) = node_result
726
    (_, node_name) = node_tuple
727
    (data_status, data_node) = data_tuple
728
    if data_status == constants.RS_NORMAL:
729
      data.append([node_name, "%s=%s" % tuple(data_node[0])])
730
      for item, status in data_node[1:]:
731
        data.append(["", "%s=%s" % (item, status)])
732
    else:
733
      errs += 1
734
      data.append([node_name, cli.FormatResultError(data_status, True)])
735

    
736
  data = GenerateTable(separator=opts.separator, headers=headers,
737
                       fields=["node", "status"], data=data)
738

    
739
  for line in data:
740
    ToStdout(line)
741

    
742
  if errs:
743
    return constants.EXIT_FAILURE
744
  else:
745
    return constants.EXIT_SUCCESS
746

    
747

    
748
def ListVolumes(opts, args):
749
  """List logical volumes on node(s).
750

751
  @param opts: the command line options selected by the user
752
  @type args: list
753
  @param args: should either be an empty list, in which case
754
      we list data for all nodes, or contain a list of nodes
755
      to display data only for those
756
  @rtype: int
757
  @return: the desired exit code
758

759
  """
760
  selected_fields = ParseFields(opts.output, _LIST_VOL_DEF_FIELDS)
761

    
762
  op = opcodes.OpNodeQueryvols(nodes=args, output_fields=selected_fields)
763
  output = SubmitOpCode(op, opts=opts)
764

    
765
  if not opts.no_headers:
766
    headers = {"node": "Node", "phys": "PhysDev",
767
               "vg": "VG", "name": "Name",
768
               "size": "Size", "instance": "Instance"}
769
  else:
770
    headers = None
771

    
772
  unitfields = ["size"]
773

    
774
  numfields = ["size"]
775

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

    
780
  for line in data:
781
    ToStdout(line)
782

    
783
  return 0
784

    
785

    
786
def ListStorage(opts, args):
787
  """List physical volumes on node(s).
788

789
  @param opts: the command line options selected by the user
790
  @type args: list
791
  @param args: should either be an empty list, in which case
792
      we list data for all nodes, or contain a list of nodes
793
      to display data only for those
794
  @rtype: int
795
  @return: the desired exit code
796

797
  """
798
  # TODO: Default to ST_FILE if LVM is disabled on the cluster
799
  if opts.user_storage_type is None:
800
    opts.user_storage_type = constants.ST_LVM_PV
801

    
802
  storage_type = ConvertStorageType(opts.user_storage_type)
803

    
804
  selected_fields = ParseFields(opts.output, _LIST_STOR_DEF_FIELDS)
805

    
806
  op = opcodes.OpNodeQueryStorage(nodes=args,
807
                                  storage_type=storage_type,
808
                                  output_fields=selected_fields)
809
  output = SubmitOpCode(op, opts=opts)
810

    
811
  if not opts.no_headers:
812
    headers = {
813
      constants.SF_NODE: "Node",
814
      constants.SF_TYPE: "Type",
815
      constants.SF_NAME: "Name",
816
      constants.SF_SIZE: "Size",
817
      constants.SF_USED: "Used",
818
      constants.SF_FREE: "Free",
819
      constants.SF_ALLOCATABLE: "Allocatable",
820
      }
821
  else:
822
    headers = None
823

    
824
  unitfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
825
  numfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
826

    
827
  # change raw values to nicer strings
828
  for row in output:
829
    for idx, field in enumerate(selected_fields):
830
      val = row[idx]
831
      if field == constants.SF_ALLOCATABLE:
832
        if val:
833
          val = "Y"
834
        else:
835
          val = "N"
836
      row[idx] = str(val)
837

    
838
  data = GenerateTable(separator=opts.separator, headers=headers,
839
                       fields=selected_fields, unitfields=unitfields,
840
                       numfields=numfields, data=output, units=opts.units)
841

    
842
  for line in data:
843
    ToStdout(line)
844

    
845
  return 0
846

    
847

    
848
def ModifyStorage(opts, args):
849
  """Modify storage volume on a node.
850

851
  @param opts: the command line options selected by the user
852
  @type args: list
853
  @param args: should contain 3 items: node name, storage type and volume name
854
  @rtype: int
855
  @return: the desired exit code
856

857
  """
858
  (node_name, user_storage_type, volume_name) = args
859

    
860
  storage_type = ConvertStorageType(user_storage_type)
861

    
862
  changes = {}
863

    
864
  if opts.allocatable is not None:
865
    changes[constants.SF_ALLOCATABLE] = opts.allocatable
866

    
867
  if changes:
868
    op = opcodes.OpNodeModifyStorage(node_name=node_name,
869
                                     storage_type=storage_type,
870
                                     name=volume_name,
871
                                     changes=changes)
872
    SubmitOrSend(op, opts)
873
  else:
874
    ToStderr("No changes to perform, exiting.")
875

    
876

    
877
def RepairStorage(opts, args):
878
  """Repairs a storage volume on a node.
879

880
  @param opts: the command line options selected by the user
881
  @type args: list
882
  @param args: should contain 3 items: node name, storage type and volume name
883
  @rtype: int
884
  @return: the desired exit code
885

886
  """
887
  (node_name, user_storage_type, volume_name) = args
888

    
889
  storage_type = ConvertStorageType(user_storage_type)
890

    
891
  op = opcodes.OpRepairNodeStorage(node_name=node_name,
892
                                   storage_type=storage_type,
893
                                   name=volume_name,
894
                                   ignore_consistency=opts.ignore_consistency)
895
  SubmitOrSend(op, opts)
896

    
897

    
898
def SetNodeParams(opts, args):
899
  """Modifies a node.
900

901
  @param opts: the command line options selected by the user
902
  @type args: list
903
  @param args: should contain only one element, the node name
904
  @rtype: int
905
  @return: the desired exit code
906

907
  """
908
  all_changes = [opts.master_candidate, opts.drained, opts.offline,
909
                 opts.master_capable, opts.vm_capable, opts.secondary_ip,
910
                 opts.ndparams]
911
  if (all_changes.count(None) == len(all_changes) and
912
      not (opts.hv_state or opts.disk_state)):
913
    ToStderr("Please give at least one of the parameters.")
914
    return 1
915

    
916
  if opts.disk_state:
917
    disk_state = utils.FlatToDict(opts.disk_state)
918
  else:
919
    disk_state = {}
920

    
921
  hv_state = dict(opts.hv_state)
922

    
923
  op = opcodes.OpNodeSetParams(node_name=args[0],
924
                               master_candidate=opts.master_candidate,
925
                               offline=opts.offline,
926
                               drained=opts.drained,
927
                               master_capable=opts.master_capable,
928
                               vm_capable=opts.vm_capable,
929
                               secondary_ip=opts.secondary_ip,
930
                               force=opts.force,
931
                               ndparams=opts.ndparams,
932
                               auto_promote=opts.auto_promote,
933
                               powered=opts.node_powered,
934
                               hv_state=hv_state,
935
                               disk_state=disk_state)
936

    
937
  # even if here we process the result, we allow submit only
938
  result = SubmitOrSend(op, opts)
939

    
940
  if result:
941
    ToStdout("Modified node %s", args[0])
942
    for param, data in result:
943
      ToStdout(" - %-5s -> %s", param, data)
944
  return 0
945

    
946

    
947
def RestrictedCommand(opts, args):
948
  """Runs a remote command on node(s).
949

950
  @param opts: Command line options selected by user
951
  @type args: list
952
  @param args: Command line arguments
953
  @rtype: int
954
  @return: Exit code
955

956
  """
957
  cl = GetClient()
958

    
959
  if len(args) > 1 or opts.nodegroup:
960
    # Expand node names
961
    nodes = GetOnlineNodes(nodes=args[1:], cl=cl, nodegroup=opts.nodegroup)
962
  else:
963
    raise errors.OpPrereqError("Node group or node names must be given",
964
                               errors.ECODE_INVAL)
965

    
966
  op = opcodes.OpRestrictedCommand(command=args[0], nodes=nodes,
967
                                   use_locking=opts.do_locking)
968
  result = SubmitOrSend(op, opts, cl=cl)
969

    
970
  exit_code = constants.EXIT_SUCCESS
971

    
972
  for (node, (status, text)) in zip(nodes, result):
973
    ToStdout("------------------------------------------------")
974
    if status:
975
      if opts.show_machine_names:
976
        for line in text.splitlines():
977
          ToStdout("%s: %s", node, line)
978
      else:
979
        ToStdout("Node: %s", node)
980
        ToStdout(text)
981
    else:
982
      exit_code = constants.EXIT_FAILURE
983
      ToStdout(text)
984

    
985
  return exit_code
986

    
987

    
988
class ReplyStatus(object):
989
  """Class holding a reply status for synchronous confd clients.
990

991
  """
992
  def __init__(self):
993
    self.failure = True
994
    self.answer = False
995

    
996

    
997
def ListDrbd(opts, args):
998
  """Modifies a node.
999

1000
  @param opts: the command line options selected by the user
1001
  @type args: list
1002
  @param args: should contain only one element, the node name
1003
  @rtype: int
1004
  @return: the desired exit code
1005

1006
  """
1007
  if len(args) != 1:
1008
    ToStderr("Please give one (and only one) node.")
1009
    return constants.EXIT_FAILURE
1010

    
1011
  if not constants.ENABLE_CONFD:
1012
    ToStderr("Error: this command requires confd support, but it has not"
1013
             " been enabled at build time.")
1014
    return constants.EXIT_FAILURE
1015

    
1016
  status = ReplyStatus()
1017

    
1018
  def ListDrbdConfdCallback(reply):
1019
    """Callback for confd queries"""
1020
    if reply.type == confd_client.UPCALL_REPLY:
1021
      answer = reply.server_reply.answer
1022
      reqtype = reply.orig_request.type
1023
      if reqtype == constants.CONFD_REQ_NODE_DRBD:
1024
        if reply.server_reply.status != constants.CONFD_REPL_STATUS_OK:
1025
          ToStderr("Query gave non-ok status '%s': %s" %
1026
                   (reply.server_reply.status,
1027
                    reply.server_reply.answer))
1028
          status.failure = True
1029
          return
1030
        if not confd.HTNodeDrbd(answer):
1031
          ToStderr("Invalid response from server: expected %s, got %s",
1032
                   confd.HTNodeDrbd, answer)
1033
          status.failure = True
1034
        else:
1035
          status.failure = False
1036
          status.answer = answer
1037
      else:
1038
        ToStderr("Unexpected reply %s!?", reqtype)
1039
        status.failure = True
1040

    
1041
  node = args[0]
1042
  hmac = utils.ReadFile(pathutils.CONFD_HMAC_KEY)
1043
  filter_callback = confd_client.ConfdFilterCallback(ListDrbdConfdCallback)
1044
  counting_callback = confd_client.ConfdCountingCallback(filter_callback)
1045
  cf_client = confd_client.ConfdClient(hmac, [constants.IP4_ADDRESS_LOCALHOST],
1046
                                       counting_callback)
1047
  req = confd_client.ConfdClientRequest(type=constants.CONFD_REQ_NODE_DRBD,
1048
                                        query=node)
1049

    
1050
  def DoConfdRequestReply(req):
1051
    counting_callback.RegisterQuery(req.rsalt)
1052
    cf_client.SendRequest(req, async=False)
1053
    while not counting_callback.AllAnswered():
1054
      if not cf_client.ReceiveReply():
1055
        ToStderr("Did not receive all expected confd replies")
1056
        break
1057

    
1058
  DoConfdRequestReply(req)
1059

    
1060
  if status.failure:
1061
    return constants.EXIT_FAILURE
1062

    
1063
  fields = ["node", "minor", "instance", "disk", "role", "peer"]
1064
  if opts.no_headers:
1065
    headers = None
1066
  else:
1067
    headers = {"node": "Node", "minor": "Minor", "instance": "Instance",
1068
               "disk": "Disk", "role": "Role", "peer": "PeerNode"}
1069

    
1070
  data = GenerateTable(separator=opts.separator, headers=headers,
1071
                       fields=fields, data=sorted(status.answer),
1072
                       numfields=["minor"])
1073
  for line in data:
1074
    ToStdout(line)
1075

    
1076
  return constants.EXIT_SUCCESS
1077

    
1078

    
1079
commands = {
1080
  "add": (
1081
    AddNode, [ArgHost(min=1, max=1)],
1082
    [SECONDARY_IP_OPT, READD_OPT, NOSSH_KEYCHECK_OPT, NODE_FORCE_JOIN_OPT,
1083
     NONODE_SETUP_OPT, VERBOSE_OPT, NODEGROUP_OPT, PRIORITY_OPT,
1084
     CAPAB_MASTER_OPT, CAPAB_VM_OPT, NODE_PARAMS_OPT, HV_STATE_OPT,
1085
     DISK_STATE_OPT],
1086
    "[-s ip] [--readd] [--no-ssh-key-check] [--force-join]"
1087
    " [--no-node-setup] [--verbose]"
1088
    " <node_name>",
1089
    "Add a node to the cluster"),
1090
  "evacuate": (
1091
    EvacuateNode, ARGS_ONE_NODE,
1092
    [FORCE_OPT, IALLOCATOR_OPT, NEW_SECONDARY_OPT, EARLY_RELEASE_OPT,
1093
     PRIORITY_OPT, PRIMARY_ONLY_OPT, SECONDARY_ONLY_OPT, SUBMIT_OPT],
1094
    "[-f] {-I <iallocator> | -n <dst>} [-p | -s] [options...] <node>",
1095
    "Relocate the primary and/or secondary instances from a node"),
1096
  "failover": (
1097
    FailoverNode, ARGS_ONE_NODE, [FORCE_OPT, IGNORE_CONSIST_OPT,
1098
                                  IALLOCATOR_OPT, PRIORITY_OPT],
1099
    "[-f] <node>",
1100
    "Stops the primary instances on a node and start them on their"
1101
    " secondary node (only for instances with drbd disk template)"),
1102
  "migrate": (
1103
    MigrateNode, ARGS_ONE_NODE,
1104
    [FORCE_OPT, NONLIVE_OPT, MIGRATION_MODE_OPT, DST_NODE_OPT,
1105
     IALLOCATOR_OPT, PRIORITY_OPT, IGNORE_IPOLICY_OPT,
1106
     NORUNTIME_CHGS_OPT, SUBMIT_OPT],
1107
    "[-f] <node>",
1108
    "Migrate all the primary instance on a node away from it"
1109
    " (only for instances of type drbd)"),
1110
  "info": (
1111
    ShowNodeConfig, ARGS_MANY_NODES, [],
1112
    "[<node_name>...]", "Show information about the node(s)"),
1113
  "list": (
1114
    ListNodes, ARGS_MANY_NODES,
1115
    [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, VERBOSE_OPT,
1116
     FORCE_FILTER_OPT],
1117
    "[nodes...]",
1118
    "Lists the nodes in the cluster. The available fields can be shown using"
1119
    " the \"list-fields\" command (see the man page for details)."
1120
    " The default field list is (in order): %s." %
1121
    utils.CommaJoin(_LIST_DEF_FIELDS)),
1122
  "list-fields": (
1123
    ListNodeFields, [ArgUnknown()],
1124
    [NOHDR_OPT, SEP_OPT],
1125
    "[fields...]",
1126
    "Lists all available fields for nodes"),
1127
  "modify": (
1128
    SetNodeParams, ARGS_ONE_NODE,
1129
    [FORCE_OPT, SUBMIT_OPT, MC_OPT, DRAINED_OPT, OFFLINE_OPT,
1130
     CAPAB_MASTER_OPT, CAPAB_VM_OPT, SECONDARY_IP_OPT,
1131
     AUTO_PROMOTE_OPT, DRY_RUN_OPT, PRIORITY_OPT, NODE_PARAMS_OPT,
1132
     NODE_POWERED_OPT, HV_STATE_OPT, DISK_STATE_OPT],
1133
    "<node_name>", "Alters the parameters of a node"),
1134
  "powercycle": (
1135
    PowercycleNode, ARGS_ONE_NODE,
1136
    [FORCE_OPT, CONFIRM_OPT, DRY_RUN_OPT, PRIORITY_OPT, SUBMIT_OPT],
1137
    "<node_name>", "Tries to forcefully powercycle a node"),
1138
  "power": (
1139
    PowerNode,
1140
    [ArgChoice(min=1, max=1, choices=_LIST_POWER_COMMANDS),
1141
     ArgNode()],
1142
    [SUBMIT_OPT, AUTO_PROMOTE_OPT, PRIORITY_OPT, IGNORE_STATUS_OPT,
1143
     FORCE_OPT, NOHDR_OPT, SEP_OPT, OOB_TIMEOUT_OPT, POWER_DELAY_OPT],
1144
    "on|off|cycle|status [nodes...]",
1145
    "Change power state of node by calling out-of-band helper."),
1146
  "remove": (
1147
    RemoveNode, ARGS_ONE_NODE, [DRY_RUN_OPT, PRIORITY_OPT],
1148
    "<node_name>", "Removes a node from the cluster"),
1149
  "volumes": (
1150
    ListVolumes, [ArgNode()],
1151
    [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, PRIORITY_OPT],
1152
    "[<node_name>...]", "List logical volumes on node(s)"),
1153
  "list-storage": (
1154
    ListStorage, ARGS_MANY_NODES,
1155
    [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, _STORAGE_TYPE_OPT,
1156
     PRIORITY_OPT],
1157
    "[<node_name>...]", "List physical volumes on node(s). The available"
1158
    " fields are (see the man page for details): %s." %
1159
    (utils.CommaJoin(_LIST_STOR_HEADERS))),
1160
  "modify-storage": (
1161
    ModifyStorage,
1162
    [ArgNode(min=1, max=1),
1163
     ArgChoice(min=1, max=1, choices=_MODIFIABLE_STORAGE_TYPES),
1164
     ArgFile(min=1, max=1)],
1165
    [ALLOCATABLE_OPT, DRY_RUN_OPT, PRIORITY_OPT, SUBMIT_OPT],
1166
    "<node_name> <storage_type> <name>", "Modify storage volume on a node"),
1167
  "repair-storage": (
1168
    RepairStorage,
1169
    [ArgNode(min=1, max=1),
1170
     ArgChoice(min=1, max=1, choices=_REPAIRABLE_STORAGE_TYPES),
1171
     ArgFile(min=1, max=1)],
1172
    [IGNORE_CONSIST_OPT, DRY_RUN_OPT, PRIORITY_OPT, SUBMIT_OPT],
1173
    "<node_name> <storage_type> <name>",
1174
    "Repairs a storage volume on a node"),
1175
  "list-tags": (
1176
    ListTags, ARGS_ONE_NODE, [],
1177
    "<node_name>", "List the tags of the given node"),
1178
  "add-tags": (
1179
    AddTags, [ArgNode(min=1, max=1), ArgUnknown()],
1180
    [TAG_SRC_OPT, PRIORITY_OPT, SUBMIT_OPT],
1181
    "<node_name> tag...", "Add tags to the given node"),
1182
  "remove-tags": (
1183
    RemoveTags, [ArgNode(min=1, max=1), ArgUnknown()],
1184
    [TAG_SRC_OPT, PRIORITY_OPT, SUBMIT_OPT],
1185
    "<node_name> tag...", "Remove tags from the given node"),
1186
  "health": (
1187
    Health, ARGS_MANY_NODES,
1188
    [NOHDR_OPT, SEP_OPT, PRIORITY_OPT, OOB_TIMEOUT_OPT],
1189
    "[<node_name>...]", "List health of node(s) using out-of-band"),
1190
  "list-drbd": (
1191
    ListDrbd, ARGS_ONE_NODE,
1192
    [NOHDR_OPT, SEP_OPT],
1193
    "[<node_name>]", "Query the list of used DRBD minors on the given node"),
1194
  "restricted-command": (
1195
    RestrictedCommand, [ArgUnknown(min=1, max=1)] + ARGS_MANY_NODES,
1196
    [SYNC_OPT, PRIORITY_OPT, SUBMIT_OPT, SHOW_MACHINE_OPT, NODEGROUP_OPT],
1197
    "<command> <node_name> [<node_name>...]",
1198
    "Executes a restricted command on node(s)"),
1199
  }
1200

    
1201
#: dictionary with aliases for commands
1202
aliases = {
1203
  "show": "info",
1204
  }
1205

    
1206

    
1207
def Main():
1208
  return GenericMain(commands, aliases=aliases,
1209
                     override={"tag_type": constants.TAG_NODE},
1210
                     env_override=_ENV_OVERRIDE)