Statistics
| Branch: | Tag: | Revision:

root / lib / client / gnt_node.py @ 929efcc3

History | View | Annotate | Download (37.9 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013 Google Inc.
5
#
6
# This program is free software; you can redistribute it and/or modify
7
# it under the terms of the GNU General Public License as published by
8
# the Free Software Foundation; either version 2 of the License, or
9
# (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful, but
12
# WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14
# General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19
# 02110-1301, USA.
20

    
21
"""Node related commands"""
22

    
23
# pylint: disable=W0401,W0613,W0614,C0103
24
# W0401: Wildcard import ganeti.cli
25
# W0613: Unused argument, since all functions follow the same API
26
# W0614: Unused import %s from wildcard import (since we need cli)
27
# C0103: Invalid name gnt-node
28

    
29
import itertools
30
import errno
31

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

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

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

    
54

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

    
58

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

    
70

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

    
74

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

    
86

    
87
#: User-facing storage unit types
88
_USER_STORAGE_TYPE = {
89
  constants.ST_FILE: "file",
90
  constants.ST_LVM_PV: "lvm-pv",
91
  constants.ST_LVM_VG: "lvm-vg",
92
  constants.ST_SHARED_FILE: "sharedfile",
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, ssh_port):
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
  @type ssh_port: int
194
  @param ssh_port: Destination node ssh port
195

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

    
201
  host_keys = _ReadSshKeys(constants.SSH_DAEMON_KEYFILES)
202

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

    
206
  root_keys = _ReadSshKeys(root_keyfiles)
207

    
208
  (_, cert_pem) = \
209
    utils.ExtractX509Certificate(utils.ReadFile(pathutils.NODED_CERT_FILE))
210

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

    
218
  bootstrap.RunNodeSetupCmd(cluster_name, node, pathutils.PREPARE_NODE_JOIN,
219
                            options.debug, options.verbose, False,
220
                            options.ssh_key_check, options.ssh_key_check,
221
                            ssh_port, data)
222

    
223

    
224
@UsesRPC
225
def AddNode(opts, args):
226
  """Add a node to the cluster.
227

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

234
  """
235
  cl = GetClient()
236
  query_cl = GetClient(query=True)
237
  node = netutils.GetHostname(name=args[0]).name
238
  readd = opts.readd
239

    
240
  # Retrieve relevant parameters of the node group.
241
  ssh_port = None
242
  try:
243
    # Passing [] to QueryGroups means query the default group:
244
    node_groups = [opts.nodegroup] if opts.nodegroup is not None else []
245
    output = query_cl.QueryGroups(names=node_groups, fields=["ndp/ssh_port"],
246
                                  use_locking=False)
247
    (ssh_port, ) = output[0]
248
  except (errors.OpPrereqError, errors.OpExecError):
249
    pass
250

    
251
  try:
252
    output = query_cl.QueryNodes(names=[node],
253
                                 fields=["name", "sip", "master",
254
                                         "ndp/ssh_port"],
255
                                 use_locking=False)
256
    if len(output) == 0:
257
      node_exists = ""
258
      sip = None
259
    else:
260
      node_exists, sip, is_master, ssh_port = output[0]
261
  except (errors.OpPrereqError, errors.OpExecError):
262
    node_exists = ""
263
    sip = None
264

    
265
  if readd:
266
    if not node_exists:
267
      ToStderr("Node %s not in the cluster"
268
               " - please retry without '--readd'", node)
269
      return 1
270
    if is_master:
271
      ToStderr("Node %s is the master, cannot readd", node)
272
      return 1
273
  else:
274
    if node_exists:
275
      ToStderr("Node %s already in the cluster (as %s)"
276
               " - please retry with '--readd'", node, node_exists)
277
      return 1
278
    sip = opts.secondary_ip
279

    
280
  # read the cluster name from the master
281
  (cluster_name, ) = cl.QueryConfigValues(["cluster_name"])
282

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

    
291
  if opts.node_setup:
292
    _SetupSSH(opts, cluster_name, node, ssh_port)
293

    
294
  bootstrap.SetupNodeDaemon(opts, cluster_name, node, ssh_port)
295

    
296
  if opts.disk_state:
297
    disk_state = utils.FlatToDict(opts.disk_state)
298
  else:
299
    disk_state = {}
300

    
301
  hv_state = dict(opts.hv_state)
302

    
303
  op = opcodes.OpNodeAdd(node_name=args[0], secondary_ip=sip,
304
                         readd=opts.readd, group=opts.nodegroup,
305
                         vm_capable=opts.vm_capable, ndparams=opts.ndparams,
306
                         master_capable=opts.master_capable,
307
                         disk_state=disk_state,
308
                         hv_state=hv_state)
309
  SubmitOpCode(op, opts=opts)
310

    
311

    
312
def ListNodes(opts, args):
313
  """List nodes and their properties.
314

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

321
  """
322
  selected_fields = ParseFields(opts.output, _LIST_DEF_FIELDS)
323

    
324
  fmtoverride = dict.fromkeys(["pinst_list", "sinst_list", "tags"],
325
                              (",".join, False))
326

    
327
  cl = GetClient(query=True)
328

    
329
  return GenericList(constants.QR_NODE, selected_fields, args, opts.units,
330
                     opts.separator, not opts.no_headers,
331
                     format_override=fmtoverride, verbose=opts.verbose,
332
                     force_filter=opts.force_filter, cl=cl)
333

    
334

    
335
def ListNodeFields(opts, args):
336
  """List node fields.
337

338
  @param opts: the command line options selected by the user
339
  @type args: list
340
  @param args: fields to list, or empty for all
341
  @rtype: int
342
  @return: the desired exit code
343

344
  """
345
  cl = GetClient(query=True)
346

    
347
  return GenericListFields(constants.QR_NODE, args, opts.separator,
348
                           not opts.no_headers, cl=cl)
349

    
350

    
351
def EvacuateNode(opts, args):
352
  """Relocate all secondary instance from a node.
353

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

360
  """
361
  if opts.dst_node is not None:
362
    ToStderr("New secondary node given (disabling iallocator), hence evacuating"
363
             " secondary instances only.")
364
    opts.secondary_only = True
365
    opts.primary_only = False
366

    
367
  if opts.secondary_only and opts.primary_only:
368
    raise errors.OpPrereqError("Only one of the --primary-only and"
369
                               " --secondary-only options can be passed",
370
                               errors.ECODE_INVAL)
371
  elif opts.primary_only:
372
    mode = constants.NODE_EVAC_PRI
373
  elif opts.secondary_only:
374
    mode = constants.NODE_EVAC_SEC
375
  else:
376
    mode = constants.NODE_EVAC_ALL
377

    
378
  # Determine affected instances
379
  fields = []
380

    
381
  if not opts.secondary_only:
382
    fields.append("pinst_list")
383
  if not opts.primary_only:
384
    fields.append("sinst_list")
385

    
386
  cl = GetClient()
387

    
388
  qcl = GetClient(query=True)
389
  result = qcl.QueryNodes(names=args, fields=fields, use_locking=False)
390
  qcl.Close()
391

    
392
  instances = set(itertools.chain(*itertools.chain(*itertools.chain(result))))
393

    
394
  if not instances:
395
    # No instances to evacuate
396
    ToStderr("No instances to evacuate on node(s) %s, exiting.",
397
             utils.CommaJoin(args))
398
    return constants.EXIT_SUCCESS
399

    
400
  if not (opts.force or
401
          AskUser("Relocate instance(s) %s from node(s) %s?" %
402
                  (utils.CommaJoin(utils.NiceSort(instances)),
403
                   utils.CommaJoin(args)))):
404
    return constants.EXIT_CONFIRMATION
405

    
406
  # Evacuate node
407
  op = opcodes.OpNodeEvacuate(node_name=args[0], mode=mode,
408
                              remote_node=opts.dst_node,
409
                              iallocator=opts.iallocator,
410
                              early_release=opts.early_release)
411
  result = SubmitOrSend(op, opts, cl=cl)
412

    
413
  # Keep track of submitted jobs
414
  jex = JobExecutor(cl=cl, opts=opts)
415

    
416
  for (status, job_id) in result[constants.JOB_IDS_KEY]:
417
    jex.AddJobId(None, status, job_id)
418

    
419
  results = jex.GetResults()
420
  bad_cnt = len([row for row in results if not row[0]])
421
  if bad_cnt == 0:
422
    ToStdout("All instances evacuated successfully.")
423
    rcode = constants.EXIT_SUCCESS
424
  else:
425
    ToStdout("There were %s errors during the evacuation.", bad_cnt)
426
    rcode = constants.EXIT_FAILURE
427

    
428
  return rcode
429

    
430

    
431
def FailoverNode(opts, args):
432
  """Failover all primary instance on a node.
433

434
  @param opts: the command line options selected by the user
435
  @type args: list
436
  @param args: should be an empty list
437
  @rtype: int
438
  @return: the desired exit code
439

440
  """
441
  cl = GetClient()
442
  force = opts.force
443
  selected_fields = ["name", "pinst_list"]
444

    
445
  # these fields are static data anyway, so it doesn't matter, but
446
  # locking=True should be safer
447
  qcl = GetClient(query=True)
448
  result = qcl.QueryNodes(names=args, fields=selected_fields,
449
                          use_locking=False)
450
  qcl.Close()
451
  node, pinst = result[0]
452

    
453
  if not pinst:
454
    ToStderr("No primary instances on node %s, exiting.", node)
455
    return 0
456

    
457
  pinst = utils.NiceSort(pinst)
458

    
459
  retcode = 0
460

    
461
  if not force and not AskUser("Fail over instance(s) %s?" %
462
                               (",".join("'%s'" % name for name in pinst))):
463
    return 2
464

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

    
480

    
481
def MigrateNode(opts, args):
482
  """Migrate all primary instance on a node.
483

484
  """
485
  cl = GetClient()
486
  force = opts.force
487
  selected_fields = ["name", "pinst_list"]
488

    
489
  qcl = GetClient(query=True)
490
  result = qcl.QueryNodes(names=args, fields=selected_fields, use_locking=False)
491
  qcl.Close()
492
  ((node, pinst), ) = result
493

    
494
  if not pinst:
495
    ToStdout("No primary instances on node %s, exiting." % node)
496
    return 0
497

    
498
  pinst = utils.NiceSort(pinst)
499

    
500
  if not (force or
501
          AskUser("Migrate instance(s) %s?" %
502
                  utils.CommaJoin(utils.NiceSort(pinst)))):
503
    return constants.EXIT_CONFIRMATION
504

    
505
  # this should be removed once --non-live is deprecated
506
  if not opts.live and opts.migration_mode is not None:
507
    raise errors.OpPrereqError("Only one of the --non-live and "
508
                               "--migration-mode options can be passed",
509
                               errors.ECODE_INVAL)
510
  if not opts.live: # --non-live passed
511
    mode = constants.HT_MIGRATION_NONLIVE
512
  else:
513
    mode = opts.migration_mode
514

    
515
  op = opcodes.OpNodeMigrate(node_name=args[0], mode=mode,
516
                             iallocator=opts.iallocator,
517
                             target_node=opts.dst_node,
518
                             allow_runtime_changes=opts.allow_runtime_chgs,
519
                             ignore_ipolicy=opts.ignore_ipolicy)
520

    
521
  result = SubmitOrSend(op, opts, cl=cl)
522

    
523
  # Keep track of submitted jobs
524
  jex = JobExecutor(cl=cl, opts=opts)
525

    
526
  for (status, job_id) in result[constants.JOB_IDS_KEY]:
527
    jex.AddJobId(None, status, job_id)
528

    
529
  results = jex.GetResults()
530
  bad_cnt = len([row for row in results if not row[0]])
531
  if bad_cnt == 0:
532
    ToStdout("All instances migrated successfully.")
533
    rcode = constants.EXIT_SUCCESS
534
  else:
535
    ToStdout("There were %s errors during the node migration.", bad_cnt)
536
    rcode = constants.EXIT_FAILURE
537

    
538
  return rcode
539

    
540

    
541
def _FormatNodeInfo(node_info):
542
  """Format node information for L{cli.PrintGenericInfo()}.
543

544
  """
545
  (name, primary_ip, secondary_ip, pinst, sinst, is_mc, drained, offline,
546
   master_capable, vm_capable, powered, ndparams, ndparams_custom) = node_info
547
  info = [
548
    ("Node name", name),
549
    ("primary ip", primary_ip),
550
    ("secondary ip", secondary_ip),
551
    ("master candidate", is_mc),
552
    ("drained", drained),
553
    ("offline", offline),
554
    ]
555
  if powered is not None:
556
    info.append(("powered", powered))
557
  info.extend([
558
    ("master_capable", master_capable),
559
    ("vm_capable", vm_capable),
560
    ])
561
  if vm_capable:
562
    info.extend([
563
      ("primary for instances",
564
       [iname for iname in utils.NiceSort(pinst)]),
565
      ("secondary for instances",
566
       [iname for iname in utils.NiceSort(sinst)]),
567
      ])
568
  info.append(("node parameters",
569
               FormatParamsDictInfo(ndparams_custom, ndparams)))
570
  return info
571

    
572

    
573
def ShowNodeConfig(opts, args):
574
  """Show node information.
575

576
  @param opts: the command line options selected by the user
577
  @type args: list
578
  @param args: should either be an empty list, in which case
579
      we show information about all nodes, or should contain
580
      a list of nodes to be queried for information
581
  @rtype: int
582
  @return: the desired exit code
583

584
  """
585
  cl = GetClient(query=True)
586
  result = cl.QueryNodes(fields=["name", "pip", "sip",
587
                                 "pinst_list", "sinst_list",
588
                                 "master_candidate", "drained", "offline",
589
                                 "master_capable", "vm_capable", "powered",
590
                                 "ndparams", "custom_ndparams"],
591
                         names=args, use_locking=False)
592
  PrintGenericInfo([
593
    _FormatNodeInfo(node_info)
594
    for node_info in result
595
    ])
596
  return 0
597

    
598

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

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

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

    
614

    
615
def PowercycleNode(opts, args):
616
  """Remove a node from the cluster.
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
  node = args[0]
627
  if (not opts.confirm and
628
      not AskUser("Are you sure you want to hard powercycle node %s?" % node)):
629
    return 2
630

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

    
637

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

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

648
  """
649
  command = args.pop(0)
650

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

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

    
660
  oob_command = "power-%s" % command
661

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

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

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

    
684
  cli.SetGenericOpcodeOpts(opcodelist, opts)
685

    
686
  job_id = cli.SendJob(opcodelist)
687

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

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

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

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

    
718
  if errs:
719
    return constants.EXIT_FAILURE
720
  else:
721
    return constants.EXIT_SUCCESS
722

    
723

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

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

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

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

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

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

    
761
  for line in data:
762
    ToStdout(line)
763

    
764
  if errs:
765
    return constants.EXIT_FAILURE
766
  else:
767
    return constants.EXIT_SUCCESS
768

    
769

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

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

781
  """
782
  selected_fields = ParseFields(opts.output, _LIST_VOL_DEF_FIELDS)
783

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

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

    
794
  unitfields = ["size"]
795

    
796
  numfields = ["size"]
797

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

    
802
  for line in data:
803
    ToStdout(line)
804

    
805
  return 0
806

    
807

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

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

819
  """
820
  selected_fields = ParseFields(opts.output, _LIST_STOR_DEF_FIELDS)
821

    
822
  op = opcodes.OpNodeQueryStorage(nodes=args,
823
                                  storage_type=opts.user_storage_type,
824
                                  output_fields=selected_fields)
825
  output = SubmitOpCode(op, opts=opts)
826

    
827
  if not opts.no_headers:
828
    headers = {
829
      constants.SF_NODE: "Node",
830
      constants.SF_TYPE: "Type",
831
      constants.SF_NAME: "Name",
832
      constants.SF_SIZE: "Size",
833
      constants.SF_USED: "Used",
834
      constants.SF_FREE: "Free",
835
      constants.SF_ALLOCATABLE: "Allocatable",
836
      }
837
  else:
838
    headers = None
839

    
840
  unitfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
841
  numfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
842

    
843
  # change raw values to nicer strings
844
  for row in output:
845
    for idx, field in enumerate(selected_fields):
846
      val = row[idx]
847
      if field == constants.SF_ALLOCATABLE:
848
        if val:
849
          val = "Y"
850
        else:
851
          val = "N"
852
      row[idx] = str(val)
853

    
854
  data = GenerateTable(separator=opts.separator, headers=headers,
855
                       fields=selected_fields, unitfields=unitfields,
856
                       numfields=numfields, data=output, units=opts.units)
857

    
858
  for line in data:
859
    ToStdout(line)
860

    
861
  return 0
862

    
863

    
864
def ModifyStorage(opts, args):
865
  """Modify storage volume on a node.
866

867
  @param opts: the command line options selected by the user
868
  @type args: list
869
  @param args: should contain 3 items: node name, storage type and volume name
870
  @rtype: int
871
  @return: the desired exit code
872

873
  """
874
  (node_name, user_storage_type, volume_name) = args
875

    
876
  storage_type = ConvertStorageType(user_storage_type)
877

    
878
  changes = {}
879

    
880
  if opts.allocatable is not None:
881
    changes[constants.SF_ALLOCATABLE] = opts.allocatable
882

    
883
  if changes:
884
    op = opcodes.OpNodeModifyStorage(node_name=node_name,
885
                                     storage_type=storage_type,
886
                                     name=volume_name,
887
                                     changes=changes)
888
    SubmitOrSend(op, opts)
889
  else:
890
    ToStderr("No changes to perform, exiting.")
891

    
892

    
893
def RepairStorage(opts, args):
894
  """Repairs a storage volume on a node.
895

896
  @param opts: the command line options selected by the user
897
  @type args: list
898
  @param args: should contain 3 items: node name, storage type and volume name
899
  @rtype: int
900
  @return: the desired exit code
901

902
  """
903
  (node_name, user_storage_type, volume_name) = args
904

    
905
  storage_type = ConvertStorageType(user_storage_type)
906

    
907
  op = opcodes.OpRepairNodeStorage(node_name=node_name,
908
                                   storage_type=storage_type,
909
                                   name=volume_name,
910
                                   ignore_consistency=opts.ignore_consistency)
911
  SubmitOrSend(op, opts)
912

    
913

    
914
def SetNodeParams(opts, args):
915
  """Modifies a node.
916

917
  @param opts: the command line options selected by the user
918
  @type args: list
919
  @param args: should contain only one element, the node name
920
  @rtype: int
921
  @return: the desired exit code
922

923
  """
924
  all_changes = [opts.master_candidate, opts.drained, opts.offline,
925
                 opts.master_capable, opts.vm_capable, opts.secondary_ip,
926
                 opts.ndparams]
927
  if (all_changes.count(None) == len(all_changes) and
928
      not (opts.hv_state or opts.disk_state)):
929
    ToStderr("Please give at least one of the parameters.")
930
    return 1
931

    
932
  if opts.disk_state:
933
    disk_state = utils.FlatToDict(opts.disk_state)
934
  else:
935
    disk_state = {}
936

    
937
  hv_state = dict(opts.hv_state)
938

    
939
  op = opcodes.OpNodeSetParams(node_name=args[0],
940
                               master_candidate=opts.master_candidate,
941
                               offline=opts.offline,
942
                               drained=opts.drained,
943
                               master_capable=opts.master_capable,
944
                               vm_capable=opts.vm_capable,
945
                               secondary_ip=opts.secondary_ip,
946
                               force=opts.force,
947
                               ndparams=opts.ndparams,
948
                               auto_promote=opts.auto_promote,
949
                               powered=opts.node_powered,
950
                               hv_state=hv_state,
951
                               disk_state=disk_state)
952

    
953
  # even if here we process the result, we allow submit only
954
  result = SubmitOrSend(op, opts)
955

    
956
  if result:
957
    ToStdout("Modified node %s", args[0])
958
    for param, data in result:
959
      ToStdout(" - %-5s -> %s", param, data)
960
  return 0
961

    
962

    
963
def RestrictedCommand(opts, args):
964
  """Runs a remote command on node(s).
965

966
  @param opts: Command line options selected by user
967
  @type args: list
968
  @param args: Command line arguments
969
  @rtype: int
970
  @return: Exit code
971

972
  """
973
  cl = GetClient()
974

    
975
  if len(args) > 1 or opts.nodegroup:
976
    # Expand node names
977
    nodes = GetOnlineNodes(nodes=args[1:], cl=cl, nodegroup=opts.nodegroup)
978
  else:
979
    raise errors.OpPrereqError("Node group or node names must be given",
980
                               errors.ECODE_INVAL)
981

    
982
  op = opcodes.OpRestrictedCommand(command=args[0], nodes=nodes,
983
                                   use_locking=opts.do_locking)
984
  result = SubmitOrSend(op, opts, cl=cl)
985

    
986
  exit_code = constants.EXIT_SUCCESS
987

    
988
  for (node, (status, text)) in zip(nodes, result):
989
    ToStdout("------------------------------------------------")
990
    if status:
991
      if opts.show_machine_names:
992
        for line in text.splitlines():
993
          ToStdout("%s: %s", node, line)
994
      else:
995
        ToStdout("Node: %s", node)
996
        ToStdout(text)
997
    else:
998
      exit_code = constants.EXIT_FAILURE
999
      ToStdout(text)
1000

    
1001
  return exit_code
1002

    
1003

    
1004
class ReplyStatus(object):
1005
  """Class holding a reply status for synchronous confd clients.
1006

1007
  """
1008
  def __init__(self):
1009
    self.failure = True
1010
    self.answer = False
1011

    
1012

    
1013
def ListDrbd(opts, args):
1014
  """Modifies a node.
1015

1016
  @param opts: the command line options selected by the user
1017
  @type args: list
1018
  @param args: should contain only one element, the node name
1019
  @rtype: int
1020
  @return: the desired exit code
1021

1022
  """
1023
  if len(args) != 1:
1024
    ToStderr("Please give one (and only one) node.")
1025
    return constants.EXIT_FAILURE
1026

    
1027
  if not constants.ENABLE_CONFD:
1028
    ToStderr("Error: this command requires confd support, but it has not"
1029
             " been enabled at build time.")
1030
    return constants.EXIT_FAILURE
1031

    
1032
  status = ReplyStatus()
1033

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

    
1057
  node = args[0]
1058
  hmac = utils.ReadFile(pathutils.CONFD_HMAC_KEY)
1059
  filter_callback = confd_client.ConfdFilterCallback(ListDrbdConfdCallback)
1060
  counting_callback = confd_client.ConfdCountingCallback(filter_callback)
1061
  cf_client = confd_client.ConfdClient(hmac, [constants.IP4_ADDRESS_LOCALHOST],
1062
                                       counting_callback)
1063
  req = confd_client.ConfdClientRequest(type=constants.CONFD_REQ_NODE_DRBD,
1064
                                        query=node)
1065

    
1066
  def DoConfdRequestReply(req):
1067
    counting_callback.RegisterQuery(req.rsalt)
1068
    cf_client.SendRequest(req, async=False)
1069
    while not counting_callback.AllAnswered():
1070
      if not cf_client.ReceiveReply():
1071
        ToStderr("Did not receive all expected confd replies")
1072
        break
1073

    
1074
  DoConfdRequestReply(req)
1075

    
1076
  if status.failure:
1077
    return constants.EXIT_FAILURE
1078

    
1079
  fields = ["node", "minor", "instance", "disk", "role", "peer"]
1080
  if opts.no_headers:
1081
    headers = None
1082
  else:
1083
    headers = {"node": "Node", "minor": "Minor", "instance": "Instance",
1084
               "disk": "Disk", "role": "Role", "peer": "PeerNode"}
1085

    
1086
  data = GenerateTable(separator=opts.separator, headers=headers,
1087
                       fields=fields, data=sorted(status.answer),
1088
                       numfields=["minor"])
1089
  for line in data:
1090
    ToStdout(line)
1091

    
1092
  return constants.EXIT_SUCCESS
1093

    
1094

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

    
1219
#: dictionary with aliases for commands
1220
aliases = {
1221
  "show": "info",
1222
  }
1223

    
1224

    
1225
def Main():
1226
  return GenericMain(commands, aliases=aliases,
1227
                     override={"tag_type": constants.TAG_NODE},
1228
                     env_override=_ENV_OVERRIDE)