Statistics
| Branch: | Tag: | Revision:

root / lib / client / gnt_node.py @ fb251c2c

History | View | Annotate | Download (37.5 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
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=False)
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
  # note: if this starts using RPC fields, and we haven't yet fixed
533
  # hconfd, then we should revert to query=False
534
  cl = GetClient(query=True)
535
  result = cl.QueryNodes(fields=["name", "pip", "sip",
536
                                 "pinst_list", "sinst_list",
537
                                 "master_candidate", "drained", "offline",
538
                                 "master_capable", "vm_capable", "powered",
539
                                 "ndparams", "custom_ndparams"],
540
                         names=args, use_locking=False)
541

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

    
573
  return 0
574

    
575

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

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

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

    
591

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

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

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

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

    
614

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

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

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

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

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

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

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

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

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

    
661
  cli.SetGenericOpcodeOpts(opcodelist, opts)
662

    
663
  job_id = cli.SendJob(opcodelist)
664

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

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

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

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

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

    
700

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

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

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

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

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

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

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

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

    
746

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

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

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

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

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

    
771
  unitfields = ["size"]
772

    
773
  numfields = ["size"]
774

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

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

    
782
  return 0
783

    
784

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

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

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

    
801
  storage_type = ConvertStorageType(opts.user_storage_type)
802

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

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

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

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

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

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

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

    
844
  return 0
845

    
846

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

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

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

    
859
  storage_type = ConvertStorageType(user_storage_type)
860

    
861
  changes = {}
862

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

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

    
875

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

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

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

    
888
  storage_type = ConvertStorageType(user_storage_type)
889

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

    
896

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

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

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

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

    
920
  hv_state = dict(opts.hv_state)
921

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

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

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

    
945

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

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

955
  """
956
  cl = GetClient()
957

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

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

    
969
  exit_code = constants.EXIT_SUCCESS
970

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

    
984
  return exit_code
985

    
986

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

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

    
995

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

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

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

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

    
1015
  status = ReplyStatus()
1016

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

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

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

    
1057
  DoConfdRequestReply(req)
1058

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

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

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

    
1075
  return constants.EXIT_SUCCESS
1076

    
1077

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

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

    
1205

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