Statistics
| Branch: | Tag: | Revision:

root / lib / client / gnt_node.py @ 7a9dbd61

History | View | Annotate | Download (38.3 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
  }
93

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

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

    
107
_MODIFIABLE_STORAGE_TYPES = constants.MODIFIABLE_STORAGE_FIELDS.keys()
108

    
109
_OOB_COMMAND_ASK = compat.UniqueFrozenset([
110
  constants.OOB_POWER_OFF,
111
  constants.OOB_POWER_CYCLE,
112
  ])
113

    
114
_ENV_OVERRIDE = compat.UniqueFrozenset(["list"])
115

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

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

    
126
OVS_OPT = cli_option("--ovs", default=False, action="store_true", dest="ovs",
127
                     help=("Enable OpenvSwitch on the new node. This will"
128
                           " initialize OpenvSwitch during gnt-node add"))
129

    
130
OVS_NAME_OPT = cli_option("--ovs-name", action="store", dest="ovs_name",
131
                          type="string", default=None,
132
                          help=("Set name of OpenvSwitch to connect instances"))
133

    
134
OVS_LINK_OPT = cli_option("--ovs-link", action="store", dest="ovs_link",
135
                          type="string", default=None,
136
                          help=("Physical trunk interface for OpenvSwitch"))
137

    
138

    
139
def ConvertStorageType(user_storage_type):
140
  """Converts a user storage type to its internal name.
141

142
  """
143
  try:
144
    return _USER_STORAGE_TYPE[user_storage_type]
145
  except KeyError:
146
    raise errors.OpPrereqError("Unknown storage type: %s" % user_storage_type,
147
                               errors.ECODE_INVAL)
148

    
149

    
150
def _TryReadFile(path):
151
  """Tries to read a file.
152

153
  If the file is not found, C{None} is returned.
154

155
  @type path: string
156
  @param path: Filename
157
  @rtype: None or string
158
  @todo: Consider adding a generic ENOENT wrapper
159

160
  """
161
  try:
162
    return utils.ReadFile(path)
163
  except EnvironmentError, err:
164
    if err.errno == errno.ENOENT:
165
      return None
166
    else:
167
      raise
168

    
169

    
170
def _ReadSshKeys(keyfiles, _tostderr_fn=ToStderr):
171
  """Reads SSH keys according to C{keyfiles}.
172

173
  @type keyfiles: dict
174
  @param keyfiles: Dictionary with keys of L{constants.SSHK_ALL} and two-values
175
    tuples (private and public key file)
176
  @rtype: list
177
  @return: List of three-values tuples (L{constants.SSHK_ALL}, private and
178
    public key as strings)
179

180
  """
181
  result = []
182

    
183
  for (kind, (private_file, public_file)) in keyfiles.items():
184
    private_key = _TryReadFile(private_file)
185
    public_key = _TryReadFile(public_file)
186

    
187
    if public_key and private_key:
188
      result.append((kind, private_key, public_key))
189
    elif public_key or private_key:
190
      _tostderr_fn("Couldn't find a complete set of keys for kind '%s'; files"
191
                   " '%s' and '%s'", kind, private_file, public_file)
192

    
193
  return result
194

    
195

    
196
def _SetupSSH(options, cluster_name, node):
197
  """Configures a destination node's SSH daemon.
198

199
  @param options: Command line options
200
  @type cluster_name
201
  @param cluster_name: Cluster name
202
  @type node: string
203
  @param node: Destination node name
204

205
  """
206
  if options.force_join:
207
    ToStderr("The \"--force-join\" option is no longer supported and will be"
208
             " ignored.")
209

    
210
  host_keys = _ReadSshKeys(constants.SSH_DAEMON_KEYFILES)
211

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

    
215
  root_keys = _ReadSshKeys(root_keyfiles)
216

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

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

    
227
  bootstrap.RunNodeSetupCmd(cluster_name, node, pathutils.PREPARE_NODE_JOIN,
228
                            options.debug, options.verbose, False,
229
                            options.ssh_key_check, options.ssh_key_check, data)
230

    
231

    
232
@UsesRPC
233
def AddNode(opts, args):
234
  """Add a node to the cluster.
235

236
  @param opts: the command line options selected by the user
237
  @type args: list
238
  @param args: should contain only one element, the new node name
239
  @rtype: int
240
  @return: the desired exit code
241

242
  """
243
  cl = GetClient()
244
  node = netutils.GetHostname(name=args[0]).name
245
  readd = opts.readd
246

    
247
  try:
248
    output = cl.QueryNodes(names=[node], fields=["name", "sip", "master"],
249
                           use_locking=False)
250
    node_exists, sip, is_master = output[0]
251
  except (errors.OpPrereqError, errors.OpExecError):
252
    node_exists = ""
253
    sip = None
254

    
255
  if readd:
256
    if not node_exists:
257
      ToStderr("Node %s not in the cluster"
258
               " - please retry without '--readd'", node)
259
      return 1
260
    if is_master:
261
      ToStderr("Node %s is the master, cannot readd", node)
262
      return 1
263
  else:
264
    if node_exists:
265
      ToStderr("Node %s already in the cluster (as %s)"
266
               " - please retry with '--readd'", node, node_exists)
267
      return 1
268
    sip = opts.secondary_ip
269

    
270
  # read the cluster name from the master
271
  (cluster_name, ) = cl.QueryConfigValues(["cluster_name"])
272

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

    
281
  if opts.node_setup:
282
    _SetupSSH(opts, cluster_name, node)
283

    
284
  bootstrap.SetupNodeDaemon(opts, cluster_name, node)
285

    
286
  if opts.disk_state:
287
    disk_state = utils.FlatToDict(opts.disk_state)
288
  else:
289
    disk_state = {}
290

    
291
  hv_state = dict(opts.hv_state)
292

    
293
  if not opts.ndparams:
294
    ndparams = {constants.ND_OVS: opts.ovs,
295
                constants.ND_OVS_NAME: opts.ovs_name,
296
                constants.ND_OVS_LINK: opts.ovs_link}
297
  else:
298
    ndparams = opts.ndparams
299
    ndparams[constants.ND_OVS] = opts.ovs
300
    ndparams[constants.ND_OVS_NAME] = opts.ovs_name
301
    ndparams[constants.ND_OVS_LINK] = opts.ovs_link
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=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 = cl.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 = cl.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
  # TODO: Default to ST_FILE if LVM is disabled on the cluster
821
  if opts.user_storage_type is None:
822
    opts.user_storage_type = constants.ST_LVM_PV
823

    
824
  storage_type = ConvertStorageType(opts.user_storage_type)
825

    
826
  selected_fields = ParseFields(opts.output, _LIST_STOR_DEF_FIELDS)
827

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

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

    
846
  unitfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
847
  numfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
848

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

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

    
864
  for line in data:
865
    ToStdout(line)
866

    
867
  return 0
868

    
869

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

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

879
  """
880
  (node_name, user_storage_type, volume_name) = args
881

    
882
  storage_type = ConvertStorageType(user_storage_type)
883

    
884
  changes = {}
885

    
886
  if opts.allocatable is not None:
887
    changes[constants.SF_ALLOCATABLE] = opts.allocatable
888

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

    
898

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

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

908
  """
909
  (node_name, user_storage_type, volume_name) = args
910

    
911
  storage_type = ConvertStorageType(user_storage_type)
912

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

    
919

    
920
def SetNodeParams(opts, args):
921
  """Modifies a node.
922

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

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

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

    
943
  hv_state = dict(opts.hv_state)
944

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

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

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

    
968

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

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

978
  """
979
  cl = GetClient()
980

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

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

    
992
  exit_code = constants.EXIT_SUCCESS
993

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

    
1007
  return exit_code
1008

    
1009

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

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

    
1018

    
1019
def ListDrbd(opts, args):
1020
  """Modifies a node.
1021

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

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

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

    
1038
  status = ReplyStatus()
1039

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

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

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

    
1080
  DoConfdRequestReply(req)
1081

    
1082
  if status.failure:
1083
    return constants.EXIT_FAILURE
1084

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

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

    
1098
  return constants.EXIT_SUCCESS
1099

    
1100

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

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

    
1231

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