Statistics
| Branch: | Tag: | Revision:

root / lib / client / gnt_node.py @ 6d846d0e

History | View | Annotate | Download (38.1 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
import tempfile
32

    
33
from ganeti.cli import *
34
from ganeti import cli
35
from ganeti import bootstrap
36
from ganeti import opcodes
37
from ganeti import utils
38
from ganeti import constants
39
from ganeti import errors
40
from ganeti import netutils
41
from ganeti import pathutils
42
from ganeti import serializer
43
from ganeti import ssh
44
from cStringIO import StringIO
45

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

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

    
56

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

    
60

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

    
72

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

    
76

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

    
88

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

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

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

    
109
_MODIFIABLE_STORAGE_TYPES = constants.MODIFIABLE_STORAGE_FIELDS.keys()
110

    
111

    
112
_OOB_COMMAND_ASK = frozenset([constants.OOB_POWER_OFF,
113
                              constants.OOB_POWER_CYCLE])
114

    
115

    
116
_ENV_OVERRIDE = frozenset(["list"])
117

    
118

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

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

    
129

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

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

    
140

    
141
def _TryReadFile(path):
142
  """Tries to read a file.
143

144
  If the file is not found, C{None} is returned.
145

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

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

    
160

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

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

171
  """
172
  result = []
173

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

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

    
184
  return result
185

    
186

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

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

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

    
201
  cmd = [pathutils.PREPARE_NODE_JOIN]
202

    
203
  # Pass --debug/--verbose to the external script if set on our invocation
204
  if options.debug:
205
    cmd.append("--debug")
206

    
207
  if options.verbose:
208
    cmd.append("--verbose")
209

    
210
  host_keys = _ReadSshKeys(constants.SSH_DAEMON_KEYFILES)
211

    
212
  (_, root_keyfiles) = \
213
    ssh.GetAllUserFiles(constants.SSH_LOGIN_USER, mkdir=False, dircheck=False)
214

    
215
  root_keys = _ReadSshKeys(root_keyfiles)
216

    
217
  (_, cert_pem) = \
218
    utils.ExtractX509Certificate(utils.ReadFile(pathutils.NODED_CERT_FILE))
219

    
220
  data = {
221
    constants.SSHS_CLUSTER_NAME: cluster_name,
222
    constants.SSHS_NODE_DAEMON_CERTIFICATE: cert_pem,
223
    constants.SSHS_SSH_HOST_KEY: host_keys,
224
    constants.SSHS_SSH_ROOT_KEY: root_keys,
225
    }
226

    
227
  srun = ssh.SshRunner(cluster_name)
228
  scmd = srun.BuildCmd(node, constants.SSH_LOGIN_USER,
229
                       utils.ShellQuoteArgs(cmd),
230
                       batch=False, ask_key=options.ssh_key_check,
231
                       strict_host_check=options.ssh_key_check, quiet=False,
232
                       use_cluster_key=False)
233

    
234
  tempfh = tempfile.TemporaryFile()
235
  try:
236
    tempfh.write(serializer.DumpJson(data))
237
    tempfh.seek(0)
238

    
239
    result = utils.RunCmd(scmd, interactive=True, input_fd=tempfh)
240
  finally:
241
    tempfh.close()
242

    
243
  if result.failed:
244
    raise errors.OpExecError("Command '%s' failed: %s" %
245
                             (result.cmd, result.fail_reason))
246

    
247

    
248
@UsesRPC
249
def AddNode(opts, args):
250
  """Add a node to the cluster.
251

252
  @param opts: the command line options selected by the user
253
  @type args: list
254
  @param args: should contain only one element, the new node name
255
  @rtype: int
256
  @return: the desired exit code
257

258
  """
259
  cl = GetClient()
260
  node = netutils.GetHostname(name=args[0]).name
261
  readd = opts.readd
262

    
263
  try:
264
    output = cl.QueryNodes(names=[node], fields=["name", "sip", "master"],
265
                           use_locking=False)
266
    node_exists, sip, is_master = output[0]
267
  except (errors.OpPrereqError, errors.OpExecError):
268
    node_exists = ""
269
    sip = None
270

    
271
  if readd:
272
    if not node_exists:
273
      ToStderr("Node %s not in the cluster"
274
               " - please retry without '--readd'", node)
275
      return 1
276
    if is_master:
277
      ToStderr("Node %s is the master, cannot readd", node)
278
      return 1
279
  else:
280
    if node_exists:
281
      ToStderr("Node %s already in the cluster (as %s)"
282
               " - please retry with '--readd'", node, node_exists)
283
      return 1
284
    sip = opts.secondary_ip
285

    
286
  # read the cluster name from the master
287
  (cluster_name, ) = cl.QueryConfigValues(["cluster_name"])
288

    
289
  if not readd and opts.node_setup:
290
    ToStderr("-- WARNING -- \n"
291
             "Performing this operation is going to replace the ssh daemon"
292
             " keypair\n"
293
             "on the target machine (%s) with the ones of the"
294
             " current one\n"
295
             "and grant full intra-cluster ssh root access to/from it\n", node)
296

    
297
  if opts.node_setup:
298
    _SetupSSH(opts, cluster_name, node)
299

    
300
  bootstrap.SetupNodeDaemon(cluster_name, node, opts.ssh_key_check)
301

    
302
  if opts.disk_state:
303
    disk_state = utils.FlatToDict(opts.disk_state)
304
  else:
305
    disk_state = {}
306

    
307
  hv_state = dict(opts.hv_state)
308

    
309
  op = opcodes.OpNodeAdd(node_name=args[0], secondary_ip=sip,
310
                         readd=opts.readd, group=opts.nodegroup,
311
                         vm_capable=opts.vm_capable, ndparams=opts.ndparams,
312
                         master_capable=opts.master_capable,
313
                         disk_state=disk_state,
314
                         hv_state=hv_state)
315
  SubmitOpCode(op, opts=opts)
316

    
317

    
318
def ListNodes(opts, args):
319
  """List nodes and their properties.
320

321
  @param opts: the command line options selected by the user
322
  @type args: list
323
  @param args: nodes to list, or empty for all
324
  @rtype: int
325
  @return: the desired exit code
326

327
  """
328
  selected_fields = ParseFields(opts.output, _LIST_DEF_FIELDS)
329

    
330
  fmtoverride = dict.fromkeys(["pinst_list", "sinst_list", "tags"],
331
                              (",".join, False))
332

    
333
  cl = GetClient(query=True)
334

    
335
  return GenericList(constants.QR_NODE, selected_fields, args, opts.units,
336
                     opts.separator, not opts.no_headers,
337
                     format_override=fmtoverride, verbose=opts.verbose,
338
                     force_filter=opts.force_filter, cl=cl)
339

    
340

    
341
def ListNodeFields(opts, args):
342
  """List node fields.
343

344
  @param opts: the command line options selected by the user
345
  @type args: list
346
  @param args: fields to list, or empty for all
347
  @rtype: int
348
  @return: the desired exit code
349

350
  """
351
  cl = GetClient(query=True)
352

    
353
  return GenericListFields(constants.QR_NODE, args, opts.separator,
354
                           not opts.no_headers, cl=cl)
355

    
356

    
357
def EvacuateNode(opts, args):
358
  """Relocate all secondary instance from a node.
359

360
  @param opts: the command line options selected by the user
361
  @type args: list
362
  @param args: should be an empty list
363
  @rtype: int
364
  @return: the desired exit code
365

366
  """
367
  if opts.dst_node is not None:
368
    ToStderr("New secondary node given (disabling iallocator), hence evacuating"
369
             " secondary instances only.")
370
    opts.secondary_only = True
371
    opts.primary_only = False
372

    
373
  if opts.secondary_only and opts.primary_only:
374
    raise errors.OpPrereqError("Only one of the --primary-only and"
375
                               " --secondary-only options can be passed",
376
                               errors.ECODE_INVAL)
377
  elif opts.primary_only:
378
    mode = constants.NODE_EVAC_PRI
379
  elif opts.secondary_only:
380
    mode = constants.NODE_EVAC_SEC
381
  else:
382
    mode = constants.NODE_EVAC_ALL
383

    
384
  # Determine affected instances
385
  fields = []
386

    
387
  if not opts.secondary_only:
388
    fields.append("pinst_list")
389
  if not opts.primary_only:
390
    fields.append("sinst_list")
391

    
392
  cl = GetClient()
393

    
394
  qcl = GetClient(query=True)
395
  result = qcl.QueryNodes(names=args, fields=fields, use_locking=False)
396
  qcl.Close()
397

    
398
  instances = set(itertools.chain(*itertools.chain(*itertools.chain(result))))
399

    
400
  if not instances:
401
    # No instances to evacuate
402
    ToStderr("No instances to evacuate on node(s) %s, exiting.",
403
             utils.CommaJoin(args))
404
    return constants.EXIT_SUCCESS
405

    
406
  if not (opts.force or
407
          AskUser("Relocate instance(s) %s from node(s) %s?" %
408
                  (utils.CommaJoin(utils.NiceSort(instances)),
409
                   utils.CommaJoin(args)))):
410
    return constants.EXIT_CONFIRMATION
411

    
412
  # Evacuate node
413
  op = opcodes.OpNodeEvacuate(node_name=args[0], mode=mode,
414
                              remote_node=opts.dst_node,
415
                              iallocator=opts.iallocator,
416
                              early_release=opts.early_release)
417
  result = SubmitOrSend(op, opts, cl=cl)
418

    
419
  # Keep track of submitted jobs
420
  jex = JobExecutor(cl=cl, opts=opts)
421

    
422
  for (status, job_id) in result[constants.JOB_IDS_KEY]:
423
    jex.AddJobId(None, status, job_id)
424

    
425
  results = jex.GetResults()
426
  bad_cnt = len([row for row in results if not row[0]])
427
  if bad_cnt == 0:
428
    ToStdout("All instances evacuated successfully.")
429
    rcode = constants.EXIT_SUCCESS
430
  else:
431
    ToStdout("There were %s errors during the evacuation.", bad_cnt)
432
    rcode = constants.EXIT_FAILURE
433

    
434
  return rcode
435

    
436

    
437
def FailoverNode(opts, args):
438
  """Failover all primary instance on a node.
439

440
  @param opts: the command line options selected by the user
441
  @type args: list
442
  @param args: should be an empty list
443
  @rtype: int
444
  @return: the desired exit code
445

446
  """
447
  cl = GetClient()
448
  force = opts.force
449
  selected_fields = ["name", "pinst_list"]
450

    
451
  # these fields are static data anyway, so it doesn't matter, but
452
  # locking=True should be safer
453
  qcl = GetClient(query=True)
454
  result = cl.QueryNodes(names=args, fields=selected_fields,
455
                         use_locking=False)
456
  qcl.Close()
457
  node, pinst = result[0]
458

    
459
  if not pinst:
460
    ToStderr("No primary instances on node %s, exiting.", node)
461
    return 0
462

    
463
  pinst = utils.NiceSort(pinst)
464

    
465
  retcode = 0
466

    
467
  if not force and not AskUser("Fail over instance(s) %s?" %
468
                               (",".join("'%s'" % name for name in pinst))):
469
    return 2
470

    
471
  jex = JobExecutor(cl=cl, opts=opts)
472
  for iname in pinst:
473
    op = opcodes.OpInstanceFailover(instance_name=iname,
474
                                    ignore_consistency=opts.ignore_consistency,
475
                                    iallocator=opts.iallocator)
476
    jex.QueueJob(iname, op)
477
  results = jex.GetResults()
478
  bad_cnt = len([row for row in results if not row[0]])
479
  if bad_cnt == 0:
480
    ToStdout("All %d instance(s) failed over successfully.", len(results))
481
  else:
482
    ToStdout("There were errors during the failover:\n"
483
             "%d error(s) out of %d instance(s).", bad_cnt, len(results))
484
  return retcode
485

    
486

    
487
def MigrateNode(opts, args):
488
  """Migrate all primary instance on a node.
489

490
  """
491
  cl = GetClient()
492
  force = opts.force
493
  selected_fields = ["name", "pinst_list"]
494

    
495
  qcl = GetClient(query=True)
496
  result = cl.QueryNodes(names=args, fields=selected_fields, use_locking=False)
497
  qcl.Close()
498
  ((node, pinst), ) = result
499

    
500
  if not pinst:
501
    ToStdout("No primary instances on node %s, exiting." % node)
502
    return 0
503

    
504
  pinst = utils.NiceSort(pinst)
505

    
506
  if not (force or
507
          AskUser("Migrate instance(s) %s?" %
508
                  utils.CommaJoin(utils.NiceSort(pinst)))):
509
    return constants.EXIT_CONFIRMATION
510

    
511
  # this should be removed once --non-live is deprecated
512
  if not opts.live and opts.migration_mode is not None:
513
    raise errors.OpPrereqError("Only one of the --non-live and "
514
                               "--migration-mode options can be passed",
515
                               errors.ECODE_INVAL)
516
  if not opts.live: # --non-live passed
517
    mode = constants.HT_MIGRATION_NONLIVE
518
  else:
519
    mode = opts.migration_mode
520

    
521
  op = opcodes.OpNodeMigrate(node_name=args[0], mode=mode,
522
                             iallocator=opts.iallocator,
523
                             target_node=opts.dst_node,
524
                             allow_runtime_changes=opts.allow_runtime_chgs,
525
                             ignore_ipolicy=opts.ignore_ipolicy)
526

    
527
  result = SubmitOrSend(op, opts, cl=cl)
528

    
529
  # Keep track of submitted jobs
530
  jex = JobExecutor(cl=cl, opts=opts)
531

    
532
  for (status, job_id) in result[constants.JOB_IDS_KEY]:
533
    jex.AddJobId(None, status, job_id)
534

    
535
  results = jex.GetResults()
536
  bad_cnt = len([row for row in results if not row[0]])
537
  if bad_cnt == 0:
538
    ToStdout("All instances migrated successfully.")
539
    rcode = constants.EXIT_SUCCESS
540
  else:
541
    ToStdout("There were %s errors during the node migration.", bad_cnt)
542
    rcode = constants.EXIT_FAILURE
543

    
544
  return rcode
545

    
546

    
547
def ShowNodeConfig(opts, args):
548
  """Show node information.
549

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

558
  """
559
  cl = GetClient(query=True)
560
  result = cl.QueryNodes(fields=["name", "pip", "sip",
561
                                 "pinst_list", "sinst_list",
562
                                 "master_candidate", "drained", "offline",
563
                                 "master_capable", "vm_capable", "powered",
564
                                 "ndparams", "custom_ndparams"],
565
                         names=args, use_locking=False)
566

    
567
  for (name, primary_ip, secondary_ip, pinst, sinst, is_mc, drained, offline,
568
       master_capable, vm_capable, powered, ndparams,
569
       ndparams_custom) in result:
570
    ToStdout("Node name: %s", name)
571
    ToStdout("  primary ip: %s", primary_ip)
572
    ToStdout("  secondary ip: %s", secondary_ip)
573
    ToStdout("  master candidate: %s", is_mc)
574
    ToStdout("  drained: %s", drained)
575
    ToStdout("  offline: %s", offline)
576
    if powered is not None:
577
      ToStdout("  powered: %s", powered)
578
    ToStdout("  master_capable: %s", master_capable)
579
    ToStdout("  vm_capable: %s", vm_capable)
580
    if vm_capable:
581
      if pinst:
582
        ToStdout("  primary for instances:")
583
        for iname in utils.NiceSort(pinst):
584
          ToStdout("    - %s", iname)
585
      else:
586
        ToStdout("  primary for no instances")
587
      if sinst:
588
        ToStdout("  secondary for instances:")
589
        for iname in utils.NiceSort(sinst):
590
          ToStdout("    - %s", iname)
591
      else:
592
        ToStdout("  secondary for no instances")
593
    ToStdout("  node parameters:")
594
    buf = StringIO()
595
    FormatParameterDict(buf, ndparams_custom, ndparams, level=2)
596
    ToStdout(buf.getvalue().rstrip("\n"))
597

    
598
  return 0
599

    
600

    
601
def RemoveNode(opts, args):
602
  """Remove a node from the cluster.
603

604
  @param opts: the command line options selected by the user
605
  @type args: list
606
  @param args: should contain only one element, the name of
607
      the node to be removed
608
  @rtype: int
609
  @return: the desired exit code
610

611
  """
612
  op = opcodes.OpNodeRemove(node_name=args[0])
613
  SubmitOpCode(op, opts=opts)
614
  return 0
615

    
616

    
617
def PowercycleNode(opts, args):
618
  """Remove a node from the cluster.
619

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

627
  """
628
  node = args[0]
629
  if (not opts.confirm and
630
      not AskUser("Are you sure you want to hard powercycle node %s?" % node)):
631
    return 2
632

    
633
  op = opcodes.OpNodePowercycle(node_name=node, force=opts.force)
634
  result = SubmitOrSend(op, opts)
635
  if result:
636
    ToStderr(result)
637
  return 0
638

    
639

    
640
def PowerNode(opts, args):
641
  """Change/ask power state of a node.
642

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

650
  """
651
  command = args.pop(0)
652

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

    
658
  if command not in _LIST_POWER_COMMANDS:
659
    ToStderr("power subcommand %s not supported." % command)
660
    return constants.EXIT_FAILURE
661

    
662
  oob_command = "power-%s" % command
663

    
664
  if oob_command in _OOB_COMMAND_ASK:
665
    if not args:
666
      ToStderr("Please provide at least one node for this command")
667
      return constants.EXIT_FAILURE
668
    elif not opts.force and not ConfirmOperation(args, "nodes",
669
                                                 "power %s" % command):
670
      return constants.EXIT_FAILURE
671
    assert len(args) > 0
672

    
673
  opcodelist = []
674
  if not opts.ignore_status and oob_command == constants.OOB_POWER_OFF:
675
    # TODO: This is a little ugly as we can't catch and revert
676
    for node in args:
677
      opcodelist.append(opcodes.OpNodeSetParams(node_name=node, offline=True,
678
                                                auto_promote=opts.auto_promote))
679

    
680
  opcodelist.append(opcodes.OpOobCommand(node_names=args,
681
                                         command=oob_command,
682
                                         ignore_status=opts.ignore_status,
683
                                         timeout=opts.oob_timeout,
684
                                         power_delay=opts.power_delay))
685

    
686
  cli.SetGenericOpcodeOpts(opcodelist, opts)
687

    
688
  job_id = cli.SendJob(opcodelist)
689

    
690
  # We just want the OOB Opcode status
691
  # If it fails PollJob gives us the error message in it
692
  result = cli.PollJob(job_id)[-1]
693

    
694
  errs = 0
695
  data = []
696
  for node_result in result:
697
    (node_tuple, data_tuple) = node_result
698
    (_, node_name) = node_tuple
699
    (data_status, data_node) = data_tuple
700
    if data_status == constants.RS_NORMAL:
701
      if oob_command == constants.OOB_POWER_STATUS:
702
        if data_node[constants.OOB_POWER_STATUS_POWERED]:
703
          text = "powered"
704
        else:
705
          text = "unpowered"
706
        data.append([node_name, text])
707
      else:
708
        # We don't expect data here, so we just say, it was successfully invoked
709
        data.append([node_name, "invoked"])
710
    else:
711
      errs += 1
712
      data.append([node_name, cli.FormatResultError(data_status, True)])
713

    
714
  data = GenerateTable(separator=opts.separator, headers=headers,
715
                       fields=["node", "status"], data=data)
716

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

    
720
  if errs:
721
    return constants.EXIT_FAILURE
722
  else:
723
    return constants.EXIT_SUCCESS
724

    
725

    
726
def Health(opts, args):
727
  """Show health of a node using OOB.
728

729
  @param opts: the command line options selected by the user
730
  @type args: list
731
  @param args: should contain only one element, the name of
732
      the node to be removed
733
  @rtype: int
734
  @return: the desired exit code
735

736
  """
737
  op = opcodes.OpOobCommand(node_names=args, command=constants.OOB_HEALTH,
738
                            timeout=opts.oob_timeout)
739
  result = SubmitOpCode(op, opts=opts)
740

    
741
  if opts.no_headers:
742
    headers = None
743
  else:
744
    headers = {"node": "Node", "status": "Status"}
745

    
746
  errs = 0
747
  data = []
748
  for node_result in result:
749
    (node_tuple, data_tuple) = node_result
750
    (_, node_name) = node_tuple
751
    (data_status, data_node) = data_tuple
752
    if data_status == constants.RS_NORMAL:
753
      data.append([node_name, "%s=%s" % tuple(data_node[0])])
754
      for item, status in data_node[1:]:
755
        data.append(["", "%s=%s" % (item, status)])
756
    else:
757
      errs += 1
758
      data.append([node_name, cli.FormatResultError(data_status, True)])
759

    
760
  data = GenerateTable(separator=opts.separator, headers=headers,
761
                       fields=["node", "status"], data=data)
762

    
763
  for line in data:
764
    ToStdout(line)
765

    
766
  if errs:
767
    return constants.EXIT_FAILURE
768
  else:
769
    return constants.EXIT_SUCCESS
770

    
771

    
772
def ListVolumes(opts, args):
773
  """List logical volumes on node(s).
774

775
  @param opts: the command line options selected by the user
776
  @type args: list
777
  @param args: should either be an empty list, in which case
778
      we list data for all nodes, or contain a list of nodes
779
      to display data only for those
780
  @rtype: int
781
  @return: the desired exit code
782

783
  """
784
  selected_fields = ParseFields(opts.output, _LIST_VOL_DEF_FIELDS)
785

    
786
  op = opcodes.OpNodeQueryvols(nodes=args, output_fields=selected_fields)
787
  output = SubmitOpCode(op, opts=opts)
788

    
789
  if not opts.no_headers:
790
    headers = {"node": "Node", "phys": "PhysDev",
791
               "vg": "VG", "name": "Name",
792
               "size": "Size", "instance": "Instance"}
793
  else:
794
    headers = None
795

    
796
  unitfields = ["size"]
797

    
798
  numfields = ["size"]
799

    
800
  data = GenerateTable(separator=opts.separator, headers=headers,
801
                       fields=selected_fields, unitfields=unitfields,
802
                       numfields=numfields, data=output, units=opts.units)
803

    
804
  for line in data:
805
    ToStdout(line)
806

    
807
  return 0
808

    
809

    
810
def ListStorage(opts, args):
811
  """List physical volumes on node(s).
812

813
  @param opts: the command line options selected by the user
814
  @type args: list
815
  @param args: should either be an empty list, in which case
816
      we list data for all nodes, or contain a list of nodes
817
      to display data only for those
818
  @rtype: int
819
  @return: the desired exit code
820

821
  """
822
  # TODO: Default to ST_FILE if LVM is disabled on the cluster
823
  if opts.user_storage_type is None:
824
    opts.user_storage_type = constants.ST_LVM_PV
825

    
826
  storage_type = ConvertStorageType(opts.user_storage_type)
827

    
828
  selected_fields = ParseFields(opts.output, _LIST_STOR_DEF_FIELDS)
829

    
830
  op = opcodes.OpNodeQueryStorage(nodes=args,
831
                                  storage_type=storage_type,
832
                                  output_fields=selected_fields)
833
  output = SubmitOpCode(op, opts=opts)
834

    
835
  if not opts.no_headers:
836
    headers = {
837
      constants.SF_NODE: "Node",
838
      constants.SF_TYPE: "Type",
839
      constants.SF_NAME: "Name",
840
      constants.SF_SIZE: "Size",
841
      constants.SF_USED: "Used",
842
      constants.SF_FREE: "Free",
843
      constants.SF_ALLOCATABLE: "Allocatable",
844
      }
845
  else:
846
    headers = None
847

    
848
  unitfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
849
  numfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
850

    
851
  # change raw values to nicer strings
852
  for row in output:
853
    for idx, field in enumerate(selected_fields):
854
      val = row[idx]
855
      if field == constants.SF_ALLOCATABLE:
856
        if val:
857
          val = "Y"
858
        else:
859
          val = "N"
860
      row[idx] = str(val)
861

    
862
  data = GenerateTable(separator=opts.separator, headers=headers,
863
                       fields=selected_fields, unitfields=unitfields,
864
                       numfields=numfields, data=output, units=opts.units)
865

    
866
  for line in data:
867
    ToStdout(line)
868

    
869
  return 0
870

    
871

    
872
def ModifyStorage(opts, args):
873
  """Modify storage volume on a node.
874

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

881
  """
882
  (node_name, user_storage_type, volume_name) = args
883

    
884
  storage_type = ConvertStorageType(user_storage_type)
885

    
886
  changes = {}
887

    
888
  if opts.allocatable is not None:
889
    changes[constants.SF_ALLOCATABLE] = opts.allocatable
890

    
891
  if changes:
892
    op = opcodes.OpNodeModifyStorage(node_name=node_name,
893
                                     storage_type=storage_type,
894
                                     name=volume_name,
895
                                     changes=changes)
896
    SubmitOrSend(op, opts)
897
  else:
898
    ToStderr("No changes to perform, exiting.")
899

    
900

    
901
def RepairStorage(opts, args):
902
  """Repairs a storage volume on a node.
903

904
  @param opts: the command line options selected by the user
905
  @type args: list
906
  @param args: should contain 3 items: node name, storage type and volume name
907
  @rtype: int
908
  @return: the desired exit code
909

910
  """
911
  (node_name, user_storage_type, volume_name) = args
912

    
913
  storage_type = ConvertStorageType(user_storage_type)
914

    
915
  op = opcodes.OpRepairNodeStorage(node_name=node_name,
916
                                   storage_type=storage_type,
917
                                   name=volume_name,
918
                                   ignore_consistency=opts.ignore_consistency)
919
  SubmitOrSend(op, opts)
920

    
921

    
922
def SetNodeParams(opts, args):
923
  """Modifies a node.
924

925
  @param opts: the command line options selected by the user
926
  @type args: list
927
  @param args: should contain only one element, the node name
928
  @rtype: int
929
  @return: the desired exit code
930

931
  """
932
  all_changes = [opts.master_candidate, opts.drained, opts.offline,
933
                 opts.master_capable, opts.vm_capable, opts.secondary_ip,
934
                 opts.ndparams]
935
  if (all_changes.count(None) == len(all_changes) and
936
      not (opts.hv_state or opts.disk_state)):
937
    ToStderr("Please give at least one of the parameters.")
938
    return 1
939

    
940
  if opts.disk_state:
941
    disk_state = utils.FlatToDict(opts.disk_state)
942
  else:
943
    disk_state = {}
944

    
945
  hv_state = dict(opts.hv_state)
946

    
947
  op = opcodes.OpNodeSetParams(node_name=args[0],
948
                               master_candidate=opts.master_candidate,
949
                               offline=opts.offline,
950
                               drained=opts.drained,
951
                               master_capable=opts.master_capable,
952
                               vm_capable=opts.vm_capable,
953
                               secondary_ip=opts.secondary_ip,
954
                               force=opts.force,
955
                               ndparams=opts.ndparams,
956
                               auto_promote=opts.auto_promote,
957
                               powered=opts.node_powered,
958
                               hv_state=hv_state,
959
                               disk_state=disk_state)
960

    
961
  # even if here we process the result, we allow submit only
962
  result = SubmitOrSend(op, opts)
963

    
964
  if result:
965
    ToStdout("Modified node %s", args[0])
966
    for param, data in result:
967
      ToStdout(" - %-5s -> %s", param, data)
968
  return 0
969

    
970

    
971
def RestrictedCommand(opts, args):
972
  """Runs a remote command on node(s).
973

974
  @param opts: Command line options selected by user
975
  @type args: list
976
  @param args: Command line arguments
977
  @rtype: int
978
  @return: Exit code
979

980
  """
981
  cl = GetClient()
982

    
983
  if len(args) > 1 or opts.nodegroup:
984
    # Expand node names
985
    nodes = GetOnlineNodes(nodes=args[1:], cl=cl, nodegroup=opts.nodegroup)
986
  else:
987
    raise errors.OpPrereqError("Node group or node names must be given",
988
                               errors.ECODE_INVAL)
989

    
990
  op = opcodes.OpRestrictedCommand(command=args[0], nodes=nodes,
991
                                   use_locking=opts.do_locking)
992
  result = SubmitOrSend(op, opts, cl=cl)
993

    
994
  exit_code = constants.EXIT_SUCCESS
995

    
996
  for (node, (status, text)) in zip(nodes, result):
997
    ToStdout("------------------------------------------------")
998
    if status:
999
      if opts.show_machine_names:
1000
        for line in text.splitlines():
1001
          ToStdout("%s: %s", node, line)
1002
      else:
1003
        ToStdout("Node: %s", node)
1004
        ToStdout(text)
1005
    else:
1006
      exit_code = constants.EXIT_FAILURE
1007
      ToStdout(text)
1008

    
1009
  return exit_code
1010

    
1011

    
1012
class ReplyStatus(object):
1013
  """Class holding a reply status for synchronous confd clients.
1014

1015
  """
1016
  def __init__(self):
1017
    self.failure = True
1018
    self.answer = False
1019

    
1020

    
1021
def ListDrbd(opts, args):
1022
  """Modifies a node.
1023

1024
  @param opts: the command line options selected by the user
1025
  @type args: list
1026
  @param args: should contain only one element, the node name
1027
  @rtype: int
1028
  @return: the desired exit code
1029

1030
  """
1031
  if len(args) != 1:
1032
    ToStderr("Please give one (and only one) node.")
1033
    return constants.EXIT_FAILURE
1034

    
1035
  if not constants.ENABLE_CONFD:
1036
    ToStderr("Error: this command requires confd support, but it has not"
1037
             " been enabled at build time.")
1038
    return constants.EXIT_FAILURE
1039

    
1040
  status = ReplyStatus()
1041

    
1042
  def ListDrbdConfdCallback(reply):
1043
    """Callback for confd queries"""
1044
    if reply.type == confd_client.UPCALL_REPLY:
1045
      answer = reply.server_reply.answer
1046
      reqtype = reply.orig_request.type
1047
      if reqtype == constants.CONFD_REQ_NODE_DRBD:
1048
        if reply.server_reply.status != constants.CONFD_REPL_STATUS_OK:
1049
          ToStderr("Query gave non-ok status '%s': %s" %
1050
                   (reply.server_reply.status,
1051
                    reply.server_reply.answer))
1052
          status.failure = True
1053
          return
1054
        if not confd.HTNodeDrbd(answer):
1055
          ToStderr("Invalid response from server: expected %s, got %s",
1056
                   confd.HTNodeDrbd, answer)
1057
          status.failure = True
1058
        else:
1059
          status.failure = False
1060
          status.answer = answer
1061
      else:
1062
        ToStderr("Unexpected reply %s!?", reqtype)
1063
        status.failure = True
1064

    
1065
  node = args[0]
1066
  hmac = utils.ReadFile(pathutils.CONFD_HMAC_KEY)
1067
  filter_callback = confd_client.ConfdFilterCallback(ListDrbdConfdCallback)
1068
  counting_callback = confd_client.ConfdCountingCallback(filter_callback)
1069
  cf_client = confd_client.ConfdClient(hmac, [constants.IP4_ADDRESS_LOCALHOST],
1070
                                       counting_callback)
1071
  req = confd_client.ConfdClientRequest(type=constants.CONFD_REQ_NODE_DRBD,
1072
                                        query=node)
1073

    
1074
  def DoConfdRequestReply(req):
1075
    counting_callback.RegisterQuery(req.rsalt)
1076
    cf_client.SendRequest(req, async=False)
1077
    while not counting_callback.AllAnswered():
1078
      if not cf_client.ReceiveReply():
1079
        ToStderr("Did not receive all expected confd replies")
1080
        break
1081

    
1082
  DoConfdRequestReply(req)
1083

    
1084
  if status.failure:
1085
    return constants.EXIT_FAILURE
1086

    
1087
  fields = ["node", "minor", "instance", "disk", "role", "peer"]
1088
  if opts.no_headers:
1089
    headers = None
1090
  else:
1091
    headers = {"node": "Node", "minor": "Minor", "instance": "Instance",
1092
               "disk": "Disk", "role": "Role", "peer": "PeerNode"}
1093

    
1094
  data = GenerateTable(separator=opts.separator, headers=headers,
1095
                       fields=fields, data=sorted(status.answer),
1096
                       numfields=["minor"])
1097
  for line in data:
1098
    ToStdout(line)
1099

    
1100
  return constants.EXIT_SUCCESS
1101

    
1102

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

    
1225
#: dictionary with aliases for commands
1226
aliases = {
1227
  "show": "info",
1228
  }
1229

    
1230

    
1231
def Main():
1232
  return GenericMain(commands, aliases=aliases,
1233
                     override={"tag_type": constants.TAG_NODE},
1234
                     env_override=_ENV_OVERRIDE)