Statistics
| Branch: | Tag: | Revision:

root / lib / client / gnt_node.py @ fe5144b0

History | View | Annotate | Download (37.7 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
  node = netutils.GetHostname(name=args[0]).name
237
  readd = opts.readd
238

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

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

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

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

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

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

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

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

    
300
  hv_state = dict(opts.hv_state)
301

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

    
310

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

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

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

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

    
326
  cl = GetClient()
327

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

    
333

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

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

343
  """
344
  cl = GetClient()
345

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

    
349

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

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

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

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

    
377
  # Determine affected instances
378
  fields = []
379

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

    
385
  cl = GetClient()
386

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

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

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

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

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

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

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

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

    
427
  return rcode
428

    
429

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

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

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

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

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

    
456
  pinst = utils.NiceSort(pinst)
457

    
458
  retcode = 0
459

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

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

    
479

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

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

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

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

    
497
  pinst = utils.NiceSort(pinst)
498

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

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

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

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

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

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

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

    
537
  return rcode
538

    
539

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

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

    
571

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

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

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

    
597

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

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

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

    
613

    
614
def PowercycleNode(opts, args):
615
  """Remove a node from the cluster.
616

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

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

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

    
636

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

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

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

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

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

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

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

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

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

    
683
  cli.SetGenericOpcodeOpts(opcodelist, opts)
684

    
685
  job_id = cli.SendJob(opcodelist)
686

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

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

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

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

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

    
722

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

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

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

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

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

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

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

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

    
768

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

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

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

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

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

    
793
  unitfields = ["size"]
794

    
795
  numfields = ["size"]
796

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

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

    
804
  return 0
805

    
806

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

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

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

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

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

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

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

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

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

    
860
  return 0
861

    
862

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

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

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

    
875
  storage_type = ConvertStorageType(user_storage_type)
876

    
877
  changes = {}
878

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

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

    
891

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

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

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

    
904
  storage_type = ConvertStorageType(user_storage_type)
905

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

    
912

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

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

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

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

    
936
  hv_state = dict(opts.hv_state)
937

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

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

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

    
961

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

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

971
  """
972
  cl = GetClient()
973

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

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

    
985
  exit_code = constants.EXIT_SUCCESS
986

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

    
1000
  return exit_code
1001

    
1002

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

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

    
1011

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

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

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

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

    
1031
  status = ReplyStatus()
1032

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

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

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

    
1073
  DoConfdRequestReply(req)
1074

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

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

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

    
1091
  return constants.EXIT_SUCCESS
1092

    
1093

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

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

    
1223

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