Statistics
| Branch: | Tag: | Revision:

root / lib / client / gnt_node.py @ 5a904197

History | View | Annotate | Download (37.8 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
    node_exists, sip, is_master, ssh_port = output[0]
257
  except (errors.OpPrereqError, errors.OpExecError):
258
    node_exists = ""
259
    sip = None
260

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

    
276
  # read the cluster name from the master
277
  (cluster_name, ) = cl.QueryConfigValues(["cluster_name"])
278

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

    
287
  if opts.node_setup:
288
    _SetupSSH(opts, cluster_name, node, ssh_port)
289

    
290
  bootstrap.SetupNodeDaemon(opts, cluster_name, node, ssh_port)
291

    
292
  if opts.disk_state:
293
    disk_state = utils.FlatToDict(opts.disk_state)
294
  else:
295
    disk_state = {}
296

    
297
  hv_state = dict(opts.hv_state)
298

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

    
307

    
308
def ListNodes(opts, args):
309
  """List nodes and their properties.
310

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

317
  """
318
  selected_fields = ParseFields(opts.output, _LIST_DEF_FIELDS)
319

    
320
  fmtoverride = dict.fromkeys(["pinst_list", "sinst_list", "tags"],
321
                              (",".join, False))
322

    
323
  cl = GetClient(query=True)
324

    
325
  return GenericList(constants.QR_NODE, selected_fields, args, opts.units,
326
                     opts.separator, not opts.no_headers,
327
                     format_override=fmtoverride, verbose=opts.verbose,
328
                     force_filter=opts.force_filter, cl=cl)
329

    
330

    
331
def ListNodeFields(opts, args):
332
  """List node fields.
333

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

340
  """
341
  cl = GetClient(query=True)
342

    
343
  return GenericListFields(constants.QR_NODE, args, opts.separator,
344
                           not opts.no_headers, cl=cl)
345

    
346

    
347
def EvacuateNode(opts, args):
348
  """Relocate all secondary instance from a node.
349

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

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

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

    
374
  # Determine affected instances
375
  fields = []
376

    
377
  if not opts.secondary_only:
378
    fields.append("pinst_list")
379
  if not opts.primary_only:
380
    fields.append("sinst_list")
381

    
382
  cl = GetClient()
383

    
384
  qcl = GetClient(query=True)
385
  result = qcl.QueryNodes(names=args, fields=fields, use_locking=False)
386
  qcl.Close()
387

    
388
  instances = set(itertools.chain(*itertools.chain(*itertools.chain(result))))
389

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

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

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

    
409
  # Keep track of submitted jobs
410
  jex = JobExecutor(cl=cl, opts=opts)
411

    
412
  for (status, job_id) in result[constants.JOB_IDS_KEY]:
413
    jex.AddJobId(None, status, job_id)
414

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

    
424
  return rcode
425

    
426

    
427
def FailoverNode(opts, args):
428
  """Failover all primary instance on a node.
429

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

436
  """
437
  cl = GetClient()
438
  force = opts.force
439
  selected_fields = ["name", "pinst_list"]
440

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

    
449
  if not pinst:
450
    ToStderr("No primary instances on node %s, exiting.", node)
451
    return 0
452

    
453
  pinst = utils.NiceSort(pinst)
454

    
455
  retcode = 0
456

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

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

    
476

    
477
def MigrateNode(opts, args):
478
  """Migrate all primary instance on a node.
479

480
  """
481
  cl = GetClient()
482
  force = opts.force
483
  selected_fields = ["name", "pinst_list"]
484

    
485
  qcl = GetClient(query=True)
486
  result = qcl.QueryNodes(names=args, fields=selected_fields, use_locking=False)
487
  qcl.Close()
488
  ((node, pinst), ) = result
489

    
490
  if not pinst:
491
    ToStdout("No primary instances on node %s, exiting." % node)
492
    return 0
493

    
494
  pinst = utils.NiceSort(pinst)
495

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

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

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

    
517
  result = SubmitOrSend(op, opts, cl=cl)
518

    
519
  # Keep track of submitted jobs
520
  jex = JobExecutor(cl=cl, opts=opts)
521

    
522
  for (status, job_id) in result[constants.JOB_IDS_KEY]:
523
    jex.AddJobId(None, status, job_id)
524

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

    
534
  return rcode
535

    
536

    
537
def _FormatNodeInfo(node_info):
538
  """Format node information for L{cli.PrintGenericInfo()}.
539

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

    
568

    
569
def ShowNodeConfig(opts, args):
570
  """Show node information.
571

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

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

    
594

    
595
def RemoveNode(opts, args):
596
  """Remove a node from the cluster.
597

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

605
  """
606
  op = opcodes.OpNodeRemove(node_name=args[0])
607
  SubmitOpCode(op, opts=opts)
608
  return 0
609

    
610

    
611
def PowercycleNode(opts, args):
612
  """Remove a node from the cluster.
613

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

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

    
627
  op = opcodes.OpNodePowercycle(node_name=node, force=opts.force)
628
  result = SubmitOrSend(op, opts)
629
  if result:
630
    ToStderr(result)
631
  return 0
632

    
633

    
634
def PowerNode(opts, args):
635
  """Change/ask power state of a node.
636

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

644
  """
645
  command = args.pop(0)
646

    
647
  if opts.no_headers:
648
    headers = None
649
  else:
650
    headers = {"node": "Node", "status": "Status"}
651

    
652
  if command not in _LIST_POWER_COMMANDS:
653
    ToStderr("power subcommand %s not supported." % command)
654
    return constants.EXIT_FAILURE
655

    
656
  oob_command = "power-%s" % command
657

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

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

    
674
  opcodelist.append(opcodes.OpOobCommand(node_names=args,
675
                                         command=oob_command,
676
                                         ignore_status=opts.ignore_status,
677
                                         timeout=opts.oob_timeout,
678
                                         power_delay=opts.power_delay))
679

    
680
  cli.SetGenericOpcodeOpts(opcodelist, opts)
681

    
682
  job_id = cli.SendJob(opcodelist)
683

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

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

    
708
  data = GenerateTable(separator=opts.separator, headers=headers,
709
                       fields=["node", "status"], data=data)
710

    
711
  for line in data:
712
    ToStdout(line)
713

    
714
  if errs:
715
    return constants.EXIT_FAILURE
716
  else:
717
    return constants.EXIT_SUCCESS
718

    
719

    
720
def Health(opts, args):
721
  """Show health of a node using OOB.
722

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

730
  """
731
  op = opcodes.OpOobCommand(node_names=args, command=constants.OOB_HEALTH,
732
                            timeout=opts.oob_timeout)
733
  result = SubmitOpCode(op, opts=opts)
734

    
735
  if opts.no_headers:
736
    headers = None
737
  else:
738
    headers = {"node": "Node", "status": "Status"}
739

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

    
754
  data = GenerateTable(separator=opts.separator, headers=headers,
755
                       fields=["node", "status"], data=data)
756

    
757
  for line in data:
758
    ToStdout(line)
759

    
760
  if errs:
761
    return constants.EXIT_FAILURE
762
  else:
763
    return constants.EXIT_SUCCESS
764

    
765

    
766
def ListVolumes(opts, args):
767
  """List logical volumes on node(s).
768

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

777
  """
778
  selected_fields = ParseFields(opts.output, _LIST_VOL_DEF_FIELDS)
779

    
780
  op = opcodes.OpNodeQueryvols(nodes=args, output_fields=selected_fields)
781
  output = SubmitOpCode(op, opts=opts)
782

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

    
790
  unitfields = ["size"]
791

    
792
  numfields = ["size"]
793

    
794
  data = GenerateTable(separator=opts.separator, headers=headers,
795
                       fields=selected_fields, unitfields=unitfields,
796
                       numfields=numfields, data=output, units=opts.units)
797

    
798
  for line in data:
799
    ToStdout(line)
800

    
801
  return 0
802

    
803

    
804
def ListStorage(opts, args):
805
  """List physical volumes on node(s).
806

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

815
  """
816
  selected_fields = ParseFields(opts.output, _LIST_STOR_DEF_FIELDS)
817

    
818
  op = opcodes.OpNodeQueryStorage(nodes=args,
819
                                  storage_type=opts.user_storage_type,
820
                                  output_fields=selected_fields)
821
  output = SubmitOpCode(op, opts=opts)
822

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

    
836
  unitfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
837
  numfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
838

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

    
850
  data = GenerateTable(separator=opts.separator, headers=headers,
851
                       fields=selected_fields, unitfields=unitfields,
852
                       numfields=numfields, data=output, units=opts.units)
853

    
854
  for line in data:
855
    ToStdout(line)
856

    
857
  return 0
858

    
859

    
860
def ModifyStorage(opts, args):
861
  """Modify storage volume on a node.
862

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

869
  """
870
  (node_name, user_storage_type, volume_name) = args
871

    
872
  storage_type = ConvertStorageType(user_storage_type)
873

    
874
  changes = {}
875

    
876
  if opts.allocatable is not None:
877
    changes[constants.SF_ALLOCATABLE] = opts.allocatable
878

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

    
888

    
889
def RepairStorage(opts, args):
890
  """Repairs a storage volume on a node.
891

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

898
  """
899
  (node_name, user_storage_type, volume_name) = args
900

    
901
  storage_type = ConvertStorageType(user_storage_type)
902

    
903
  op = opcodes.OpRepairNodeStorage(node_name=node_name,
904
                                   storage_type=storage_type,
905
                                   name=volume_name,
906
                                   ignore_consistency=opts.ignore_consistency)
907
  SubmitOrSend(op, opts)
908

    
909

    
910
def SetNodeParams(opts, args):
911
  """Modifies a node.
912

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

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

    
928
  if opts.disk_state:
929
    disk_state = utils.FlatToDict(opts.disk_state)
930
  else:
931
    disk_state = {}
932

    
933
  hv_state = dict(opts.hv_state)
934

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

    
949
  # even if here we process the result, we allow submit only
950
  result = SubmitOrSend(op, opts)
951

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

    
958

    
959
def RestrictedCommand(opts, args):
960
  """Runs a remote command on node(s).
961

962
  @param opts: Command line options selected by user
963
  @type args: list
964
  @param args: Command line arguments
965
  @rtype: int
966
  @return: Exit code
967

968
  """
969
  cl = GetClient()
970

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

    
978
  op = opcodes.OpRestrictedCommand(command=args[0], nodes=nodes,
979
                                   use_locking=opts.do_locking)
980
  result = SubmitOrSend(op, opts, cl=cl)
981

    
982
  exit_code = constants.EXIT_SUCCESS
983

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

    
997
  return exit_code
998

    
999

    
1000
class ReplyStatus(object):
1001
  """Class holding a reply status for synchronous confd clients.
1002

1003
  """
1004
  def __init__(self):
1005
    self.failure = True
1006
    self.answer = False
1007

    
1008

    
1009
def ListDrbd(opts, args):
1010
  """Modifies a node.
1011

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

1018
  """
1019
  if len(args) != 1:
1020
    ToStderr("Please give one (and only one) node.")
1021
    return constants.EXIT_FAILURE
1022

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

    
1028
  status = ReplyStatus()
1029

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

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

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

    
1070
  DoConfdRequestReply(req)
1071

    
1072
  if status.failure:
1073
    return constants.EXIT_FAILURE
1074

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

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

    
1088
  return constants.EXIT_SUCCESS
1089

    
1090

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

    
1215
#: dictionary with aliases for commands
1216
aliases = {
1217
  "show": "info",
1218
  }
1219

    
1220

    
1221
def Main():
1222
  return GenericMain(commands, aliases=aliases,
1223
                     override={"tag_type": constants.TAG_NODE},
1224
                     env_override=_ENV_OVERRIDE)