Statistics
| Branch: | Tag: | Revision:

root / lib / client / gnt_node.py @ d9a22528

History | View | Annotate | Download (37.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
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
from cStringIO import StringIO
44

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

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

    
55

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

    
59

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

    
71

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

    
75

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

    
87

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

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

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

    
108
_MODIFIABLE_STORAGE_TYPES = constants.MODIFIABLE_STORAGE_FIELDS.keys()
109

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

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

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

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

    
127

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

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

    
138

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

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

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

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

    
158

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

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

169
  """
170
  result = []
171

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

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

    
182
  return result
183

    
184

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

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

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

    
199
  host_keys = _ReadSshKeys(constants.SSH_DAEMON_KEYFILES)
200

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

    
204
  root_keys = _ReadSshKeys(root_keyfiles)
205

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

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

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

    
220

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

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

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

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

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

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

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

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

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

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

    
280
  hv_state = dict(opts.hv_state)
281

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

    
290

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

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

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

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

    
306
  cl = GetClient(query=True)
307

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

    
313

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

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

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

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

    
329

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

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

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

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

    
357
  # Determine affected instances
358
  fields = []
359

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

    
365
  cl = GetClient()
366

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

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

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

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

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

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

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

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

    
407
  return rcode
408

    
409

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

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

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

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

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

    
436
  pinst = utils.NiceSort(pinst)
437

    
438
  retcode = 0
439

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

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

    
459

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

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

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

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

    
477
  pinst = utils.NiceSort(pinst)
478

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

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

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

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

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

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

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

    
517
  return rcode
518

    
519

    
520
def ShowNodeConfig(opts, args):
521
  """Show node information.
522

523
  @param opts: the command line options selected by the user
524
  @type args: list
525
  @param args: should either be an empty list, in which case
526
      we show information about all nodes, or should contain
527
      a list of nodes to be queried for information
528
  @rtype: int
529
  @return: the desired exit code
530

531
  """
532
  cl = GetClient(query=True)
533
  result = cl.QueryNodes(fields=["name", "pip", "sip",
534
                                 "pinst_list", "sinst_list",
535
                                 "master_candidate", "drained", "offline",
536
                                 "master_capable", "vm_capable", "powered",
537
                                 "ndparams", "custom_ndparams"],
538
                         names=args, use_locking=False)
539

    
540
  for (name, primary_ip, secondary_ip, pinst, sinst, is_mc, drained, offline,
541
       master_capable, vm_capable, powered, ndparams,
542
       ndparams_custom) in result:
543
    ToStdout("Node name: %s", name)
544
    ToStdout("  primary ip: %s", primary_ip)
545
    ToStdout("  secondary ip: %s", secondary_ip)
546
    ToStdout("  master candidate: %s", is_mc)
547
    ToStdout("  drained: %s", drained)
548
    ToStdout("  offline: %s", offline)
549
    if powered is not None:
550
      ToStdout("  powered: %s", powered)
551
    ToStdout("  master_capable: %s", master_capable)
552
    ToStdout("  vm_capable: %s", vm_capable)
553
    if vm_capable:
554
      if pinst:
555
        ToStdout("  primary for instances:")
556
        for iname in utils.NiceSort(pinst):
557
          ToStdout("    - %s", iname)
558
      else:
559
        ToStdout("  primary for no instances")
560
      if sinst:
561
        ToStdout("  secondary for instances:")
562
        for iname in utils.NiceSort(sinst):
563
          ToStdout("    - %s", iname)
564
      else:
565
        ToStdout("  secondary for no instances")
566
    ToStdout("  node parameters:")
567
    buf = StringIO()
568
    FormatParameterDict(buf, ndparams_custom, ndparams, level=2)
569
    ToStdout(buf.getvalue().rstrip("\n"))
570

    
571
  return 0
572

    
573

    
574
def RemoveNode(opts, args):
575
  """Remove a node from the cluster.
576

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

584
  """
585
  op = opcodes.OpNodeRemove(node_name=args[0])
586
  SubmitOpCode(op, opts=opts)
587
  return 0
588

    
589

    
590
def PowercycleNode(opts, args):
591
  """Remove a node from the cluster.
592

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

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

    
606
  op = opcodes.OpNodePowercycle(node_name=node, force=opts.force)
607
  result = SubmitOrSend(op, opts)
608
  if result:
609
    ToStderr(result)
610
  return 0
611

    
612

    
613
def PowerNode(opts, args):
614
  """Change/ask power state of a node.
615

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

623
  """
624
  command = args.pop(0)
625

    
626
  if opts.no_headers:
627
    headers = None
628
  else:
629
    headers = {"node": "Node", "status": "Status"}
630

    
631
  if command not in _LIST_POWER_COMMANDS:
632
    ToStderr("power subcommand %s not supported." % command)
633
    return constants.EXIT_FAILURE
634

    
635
  oob_command = "power-%s" % command
636

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

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

    
653
  opcodelist.append(opcodes.OpOobCommand(node_names=args,
654
                                         command=oob_command,
655
                                         ignore_status=opts.ignore_status,
656
                                         timeout=opts.oob_timeout,
657
                                         power_delay=opts.power_delay))
658

    
659
  cli.SetGenericOpcodeOpts(opcodelist, opts)
660

    
661
  job_id = cli.SendJob(opcodelist)
662

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

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

    
687
  data = GenerateTable(separator=opts.separator, headers=headers,
688
                       fields=["node", "status"], data=data)
689

    
690
  for line in data:
691
    ToStdout(line)
692

    
693
  if errs:
694
    return constants.EXIT_FAILURE
695
  else:
696
    return constants.EXIT_SUCCESS
697

    
698

    
699
def Health(opts, args):
700
  """Show health of a node using OOB.
701

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

709
  """
710
  op = opcodes.OpOobCommand(node_names=args, command=constants.OOB_HEALTH,
711
                            timeout=opts.oob_timeout)
712
  result = SubmitOpCode(op, opts=opts)
713

    
714
  if opts.no_headers:
715
    headers = None
716
  else:
717
    headers = {"node": "Node", "status": "Status"}
718

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

    
733
  data = GenerateTable(separator=opts.separator, headers=headers,
734
                       fields=["node", "status"], data=data)
735

    
736
  for line in data:
737
    ToStdout(line)
738

    
739
  if errs:
740
    return constants.EXIT_FAILURE
741
  else:
742
    return constants.EXIT_SUCCESS
743

    
744

    
745
def ListVolumes(opts, args):
746
  """List logical volumes on node(s).
747

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

756
  """
757
  selected_fields = ParseFields(opts.output, _LIST_VOL_DEF_FIELDS)
758

    
759
  op = opcodes.OpNodeQueryvols(nodes=args, output_fields=selected_fields)
760
  output = SubmitOpCode(op, opts=opts)
761

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

    
769
  unitfields = ["size"]
770

    
771
  numfields = ["size"]
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 ListStorage(opts, args):
784
  """List physical volumes on node(s).
785

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

794
  """
795
  # TODO: Default to ST_FILE if LVM is disabled on the cluster
796
  if opts.user_storage_type is None:
797
    opts.user_storage_type = constants.ST_LVM_PV
798

    
799
  storage_type = ConvertStorageType(opts.user_storage_type)
800

    
801
  selected_fields = ParseFields(opts.output, _LIST_STOR_DEF_FIELDS)
802

    
803
  op = opcodes.OpNodeQueryStorage(nodes=args,
804
                                  storage_type=storage_type,
805
                                  output_fields=selected_fields)
806
  output = SubmitOpCode(op, opts=opts)
807

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

    
821
  unitfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
822
  numfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
823

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

    
835
  data = GenerateTable(separator=opts.separator, headers=headers,
836
                       fields=selected_fields, unitfields=unitfields,
837
                       numfields=numfields, data=output, units=opts.units)
838

    
839
  for line in data:
840
    ToStdout(line)
841

    
842
  return 0
843

    
844

    
845
def ModifyStorage(opts, args):
846
  """Modify storage volume on a node.
847

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

854
  """
855
  (node_name, user_storage_type, volume_name) = args
856

    
857
  storage_type = ConvertStorageType(user_storage_type)
858

    
859
  changes = {}
860

    
861
  if opts.allocatable is not None:
862
    changes[constants.SF_ALLOCATABLE] = opts.allocatable
863

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

    
873

    
874
def RepairStorage(opts, args):
875
  """Repairs a storage volume on a node.
876

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

883
  """
884
  (node_name, user_storage_type, volume_name) = args
885

    
886
  storage_type = ConvertStorageType(user_storage_type)
887

    
888
  op = opcodes.OpRepairNodeStorage(node_name=node_name,
889
                                   storage_type=storage_type,
890
                                   name=volume_name,
891
                                   ignore_consistency=opts.ignore_consistency)
892
  SubmitOrSend(op, opts)
893

    
894

    
895
def SetNodeParams(opts, args):
896
  """Modifies a node.
897

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

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

    
913
  if opts.disk_state:
914
    disk_state = utils.FlatToDict(opts.disk_state)
915
  else:
916
    disk_state = {}
917

    
918
  hv_state = dict(opts.hv_state)
919

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

    
934
  # even if here we process the result, we allow submit only
935
  result = SubmitOrSend(op, opts)
936

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

    
943

    
944
def RestrictedCommand(opts, args):
945
  """Runs a remote command on node(s).
946

947
  @param opts: Command line options selected by user
948
  @type args: list
949
  @param args: Command line arguments
950
  @rtype: int
951
  @return: Exit code
952

953
  """
954
  cl = GetClient()
955

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

    
963
  op = opcodes.OpRestrictedCommand(command=args[0], nodes=nodes,
964
                                   use_locking=opts.do_locking)
965
  result = SubmitOrSend(op, opts, cl=cl)
966

    
967
  exit_code = constants.EXIT_SUCCESS
968

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

    
982
  return exit_code
983

    
984

    
985
class ReplyStatus(object):
986
  """Class holding a reply status for synchronous confd clients.
987

988
  """
989
  def __init__(self):
990
    self.failure = True
991
    self.answer = False
992

    
993

    
994
def ListDrbd(opts, args):
995
  """Modifies a node.
996

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

1003
  """
1004
  if len(args) != 1:
1005
    ToStderr("Please give one (and only one) node.")
1006
    return constants.EXIT_FAILURE
1007

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

    
1013
  status = ReplyStatus()
1014

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

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

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

    
1055
  DoConfdRequestReply(req)
1056

    
1057
  if status.failure:
1058
    return constants.EXIT_FAILURE
1059

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

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

    
1073
  return constants.EXIT_SUCCESS
1074

    
1075

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

    
1198
#: dictionary with aliases for commands
1199
aliases = {
1200
  "show": "info",
1201
  }
1202

    
1203

    
1204
def Main():
1205
  return GenericMain(commands, aliases=aliases,
1206
                     override={"tag_type": constants.TAG_NODE},
1207
                     env_override=_ENV_OVERRIDE)