Statistics
| Branch: | Tag: | Revision:

root / lib / client / gnt_node.py @ a9f33339

History | View | Annotate | Download (38.6 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, ssh_port):
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
  @type ssh_port: int
205
  @param ssh_port: Destination node ssh port
206

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

    
212
  host_keys = _ReadSshKeys(constants.SSH_DAEMON_KEYFILES)
213

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

    
217
  root_keys = _ReadSshKeys(root_keyfiles)
218

    
219
  (_, cert_pem) = \
220
    utils.ExtractX509Certificate(utils.ReadFile(pathutils.NODED_CERT_FILE))
221

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

    
229
  bootstrap.RunNodeSetupCmd(cluster_name, node, pathutils.PREPARE_NODE_JOIN,
230
                            options.debug, options.verbose, False,
231
                            options.ssh_key_check, options.ssh_key_check,
232
                            ssh_port, data)
233

    
234

    
235
@UsesRPC
236
def AddNode(opts, args):
237
  """Add a node to the cluster.
238

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

245
  """
246
  cl = GetClient()
247
  node = netutils.GetHostname(name=args[0]).name
248
  readd = opts.readd
249

    
250
  # Retrieve relevant parameters of the node group.
251
  ssh_port = None
252
  if opts.nodegroup:
253
    try:
254
      output = cl.QueryGroups(names=[opts.nodegroup], fields=["ndp/ssh_port"],
255
                              use_locking=False)
256
      (ssh_port, ) = output[0]
257
    except (errors.OpPrereqError, errors.OpExecError):
258
      pass
259

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

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

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

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

    
295
  if opts.node_setup:
296
    _SetupSSH(opts, cluster_name, node, ssh_port)
297

    
298
  bootstrap.SetupNodeDaemon(opts, cluster_name, node, ssh_port)
299

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

    
305
  hv_state = dict(opts.hv_state)
306

    
307
  if not opts.ndparams:
308
    ndparams = {constants.ND_OVS: opts.ovs,
309
                constants.ND_OVS_NAME: opts.ovs_name,
310
                constants.ND_OVS_LINK: opts.ovs_link}
311
  else:
312
    ndparams = opts.ndparams
313
    ndparams[constants.ND_OVS] = opts.ovs
314
    ndparams[constants.ND_OVS_NAME] = opts.ovs_name
315
    ndparams[constants.ND_OVS_LINK] = opts.ovs_link
316

    
317
  op = opcodes.OpNodeAdd(node_name=args[0], secondary_ip=sip,
318
                         readd=opts.readd, group=opts.nodegroup,
319
                         vm_capable=opts.vm_capable, ndparams=ndparams,
320
                         master_capable=opts.master_capable,
321
                         disk_state=disk_state,
322
                         hv_state=hv_state)
323
  SubmitOpCode(op, opts=opts)
324

    
325

    
326
def ListNodes(opts, args):
327
  """List nodes and their properties.
328

329
  @param opts: the command line options selected by the user
330
  @type args: list
331
  @param args: nodes to list, or empty for all
332
  @rtype: int
333
  @return: the desired exit code
334

335
  """
336
  selected_fields = ParseFields(opts.output, _LIST_DEF_FIELDS)
337

    
338
  fmtoverride = dict.fromkeys(["pinst_list", "sinst_list", "tags"],
339
                              (",".join, False))
340

    
341
  cl = GetClient(query=True)
342

    
343
  return GenericList(constants.QR_NODE, selected_fields, args, opts.units,
344
                     opts.separator, not opts.no_headers,
345
                     format_override=fmtoverride, verbose=opts.verbose,
346
                     force_filter=opts.force_filter, cl=cl)
347

    
348

    
349
def ListNodeFields(opts, args):
350
  """List node fields.
351

352
  @param opts: the command line options selected by the user
353
  @type args: list
354
  @param args: fields to list, or empty for all
355
  @rtype: int
356
  @return: the desired exit code
357

358
  """
359
  cl = GetClient(query=True)
360

    
361
  return GenericListFields(constants.QR_NODE, args, opts.separator,
362
                           not opts.no_headers, cl=cl)
363

    
364

    
365
def EvacuateNode(opts, args):
366
  """Relocate all secondary instance from a node.
367

368
  @param opts: the command line options selected by the user
369
  @type args: list
370
  @param args: should be an empty list
371
  @rtype: int
372
  @return: the desired exit code
373

374
  """
375
  if opts.dst_node is not None:
376
    ToStderr("New secondary node given (disabling iallocator), hence evacuating"
377
             " secondary instances only.")
378
    opts.secondary_only = True
379
    opts.primary_only = False
380

    
381
  if opts.secondary_only and opts.primary_only:
382
    raise errors.OpPrereqError("Only one of the --primary-only and"
383
                               " --secondary-only options can be passed",
384
                               errors.ECODE_INVAL)
385
  elif opts.primary_only:
386
    mode = constants.NODE_EVAC_PRI
387
  elif opts.secondary_only:
388
    mode = constants.NODE_EVAC_SEC
389
  else:
390
    mode = constants.NODE_EVAC_ALL
391

    
392
  # Determine affected instances
393
  fields = []
394

    
395
  if not opts.secondary_only:
396
    fields.append("pinst_list")
397
  if not opts.primary_only:
398
    fields.append("sinst_list")
399

    
400
  cl = GetClient()
401

    
402
  qcl = GetClient(query=True)
403
  result = qcl.QueryNodes(names=args, fields=fields, use_locking=False)
404
  qcl.Close()
405

    
406
  instances = set(itertools.chain(*itertools.chain(*itertools.chain(result))))
407

    
408
  if not instances:
409
    # No instances to evacuate
410
    ToStderr("No instances to evacuate on node(s) %s, exiting.",
411
             utils.CommaJoin(args))
412
    return constants.EXIT_SUCCESS
413

    
414
  if not (opts.force or
415
          AskUser("Relocate instance(s) %s from node(s) %s?" %
416
                  (utils.CommaJoin(utils.NiceSort(instances)),
417
                   utils.CommaJoin(args)))):
418
    return constants.EXIT_CONFIRMATION
419

    
420
  # Evacuate node
421
  op = opcodes.OpNodeEvacuate(node_name=args[0], mode=mode,
422
                              remote_node=opts.dst_node,
423
                              iallocator=opts.iallocator,
424
                              early_release=opts.early_release)
425
  result = SubmitOrSend(op, opts, cl=cl)
426

    
427
  # Keep track of submitted jobs
428
  jex = JobExecutor(cl=cl, opts=opts)
429

    
430
  for (status, job_id) in result[constants.JOB_IDS_KEY]:
431
    jex.AddJobId(None, status, job_id)
432

    
433
  results = jex.GetResults()
434
  bad_cnt = len([row for row in results if not row[0]])
435
  if bad_cnt == 0:
436
    ToStdout("All instances evacuated successfully.")
437
    rcode = constants.EXIT_SUCCESS
438
  else:
439
    ToStdout("There were %s errors during the evacuation.", bad_cnt)
440
    rcode = constants.EXIT_FAILURE
441

    
442
  return rcode
443

    
444

    
445
def FailoverNode(opts, args):
446
  """Failover all primary instance on a node.
447

448
  @param opts: the command line options selected by the user
449
  @type args: list
450
  @param args: should be an empty list
451
  @rtype: int
452
  @return: the desired exit code
453

454
  """
455
  cl = GetClient()
456
  force = opts.force
457
  selected_fields = ["name", "pinst_list"]
458

    
459
  # these fields are static data anyway, so it doesn't matter, but
460
  # locking=True should be safer
461
  qcl = GetClient(query=True)
462
  result = cl.QueryNodes(names=args, fields=selected_fields,
463
                         use_locking=False)
464
  qcl.Close()
465
  node, pinst = result[0]
466

    
467
  if not pinst:
468
    ToStderr("No primary instances on node %s, exiting.", node)
469
    return 0
470

    
471
  pinst = utils.NiceSort(pinst)
472

    
473
  retcode = 0
474

    
475
  if not force and not AskUser("Fail over instance(s) %s?" %
476
                               (",".join("'%s'" % name for name in pinst))):
477
    return 2
478

    
479
  jex = JobExecutor(cl=cl, opts=opts)
480
  for iname in pinst:
481
    op = opcodes.OpInstanceFailover(instance_name=iname,
482
                                    ignore_consistency=opts.ignore_consistency,
483
                                    iallocator=opts.iallocator)
484
    jex.QueueJob(iname, op)
485
  results = jex.GetResults()
486
  bad_cnt = len([row for row in results if not row[0]])
487
  if bad_cnt == 0:
488
    ToStdout("All %d instance(s) failed over successfully.", len(results))
489
  else:
490
    ToStdout("There were errors during the failover:\n"
491
             "%d error(s) out of %d instance(s).", bad_cnt, len(results))
492
  return retcode
493

    
494

    
495
def MigrateNode(opts, args):
496
  """Migrate all primary instance on a node.
497

498
  """
499
  cl = GetClient()
500
  force = opts.force
501
  selected_fields = ["name", "pinst_list"]
502

    
503
  qcl = GetClient(query=True)
504
  result = cl.QueryNodes(names=args, fields=selected_fields, use_locking=False)
505
  qcl.Close()
506
  ((node, pinst), ) = result
507

    
508
  if not pinst:
509
    ToStdout("No primary instances on node %s, exiting." % node)
510
    return 0
511

    
512
  pinst = utils.NiceSort(pinst)
513

    
514
  if not (force or
515
          AskUser("Migrate instance(s) %s?" %
516
                  utils.CommaJoin(utils.NiceSort(pinst)))):
517
    return constants.EXIT_CONFIRMATION
518

    
519
  # this should be removed once --non-live is deprecated
520
  if not opts.live and opts.migration_mode is not None:
521
    raise errors.OpPrereqError("Only one of the --non-live and "
522
                               "--migration-mode options can be passed",
523
                               errors.ECODE_INVAL)
524
  if not opts.live: # --non-live passed
525
    mode = constants.HT_MIGRATION_NONLIVE
526
  else:
527
    mode = opts.migration_mode
528

    
529
  op = opcodes.OpNodeMigrate(node_name=args[0], mode=mode,
530
                             iallocator=opts.iallocator,
531
                             target_node=opts.dst_node,
532
                             allow_runtime_changes=opts.allow_runtime_chgs,
533
                             ignore_ipolicy=opts.ignore_ipolicy)
534

    
535
  result = SubmitOrSend(op, opts, cl=cl)
536

    
537
  # Keep track of submitted jobs
538
  jex = JobExecutor(cl=cl, opts=opts)
539

    
540
  for (status, job_id) in result[constants.JOB_IDS_KEY]:
541
    jex.AddJobId(None, status, job_id)
542

    
543
  results = jex.GetResults()
544
  bad_cnt = len([row for row in results if not row[0]])
545
  if bad_cnt == 0:
546
    ToStdout("All instances migrated successfully.")
547
    rcode = constants.EXIT_SUCCESS
548
  else:
549
    ToStdout("There were %s errors during the node migration.", bad_cnt)
550
    rcode = constants.EXIT_FAILURE
551

    
552
  return rcode
553

    
554

    
555
def _FormatNodeInfo(node_info):
556
  """Format node information for L{cli.PrintGenericInfo()}.
557

558
  """
559
  (name, primary_ip, secondary_ip, pinst, sinst, is_mc, drained, offline,
560
   master_capable, vm_capable, powered, ndparams, ndparams_custom) = node_info
561
  info = [
562
    ("Node name", name),
563
    ("primary ip", primary_ip),
564
    ("secondary ip", secondary_ip),
565
    ("master candidate", is_mc),
566
    ("drained", drained),
567
    ("offline", offline),
568
    ]
569
  if powered is not None:
570
    info.append(("powered", powered))
571
  info.extend([
572
    ("master_capable", master_capable),
573
    ("vm_capable", vm_capable),
574
    ])
575
  if vm_capable:
576
    info.extend([
577
      ("primary for instances",
578
       [iname for iname in utils.NiceSort(pinst)]),
579
      ("secondary for instances",
580
       [iname for iname in utils.NiceSort(sinst)]),
581
      ])
582
  info.append(("node parameters",
583
               FormatParamsDictInfo(ndparams_custom, ndparams)))
584
  return info
585

    
586

    
587
def ShowNodeConfig(opts, args):
588
  """Show node information.
589

590
  @param opts: the command line options selected by the user
591
  @type args: list
592
  @param args: should either be an empty list, in which case
593
      we show information about all nodes, or should contain
594
      a list of nodes to be queried for information
595
  @rtype: int
596
  @return: the desired exit code
597

598
  """
599
  cl = GetClient(query=True)
600
  result = cl.QueryNodes(fields=["name", "pip", "sip",
601
                                 "pinst_list", "sinst_list",
602
                                 "master_candidate", "drained", "offline",
603
                                 "master_capable", "vm_capable", "powered",
604
                                 "ndparams", "custom_ndparams"],
605
                         names=args, use_locking=False)
606
  PrintGenericInfo([
607
    _FormatNodeInfo(node_info)
608
    for node_info in result
609
    ])
610
  return 0
611

    
612

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

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

623
  """
624
  op = opcodes.OpNodeRemove(node_name=args[0])
625
  SubmitOpCode(op, opts=opts)
626
  return 0
627

    
628

    
629
def PowercycleNode(opts, args):
630
  """Remove a node from the cluster.
631

632
  @param opts: the command line options selected by the user
633
  @type args: list
634
  @param args: should contain only one element, the name of
635
      the node to be removed
636
  @rtype: int
637
  @return: the desired exit code
638

639
  """
640
  node = args[0]
641
  if (not opts.confirm and
642
      not AskUser("Are you sure you want to hard powercycle node %s?" % node)):
643
    return 2
644

    
645
  op = opcodes.OpNodePowercycle(node_name=node, force=opts.force)
646
  result = SubmitOrSend(op, opts)
647
  if result:
648
    ToStderr(result)
649
  return 0
650

    
651

    
652
def PowerNode(opts, args):
653
  """Change/ask power state of a node.
654

655
  @param opts: the command line options selected by the user
656
  @type args: list
657
  @param args: should contain only one element, the name of
658
      the node to be removed
659
  @rtype: int
660
  @return: the desired exit code
661

662
  """
663
  command = args.pop(0)
664

    
665
  if opts.no_headers:
666
    headers = None
667
  else:
668
    headers = {"node": "Node", "status": "Status"}
669

    
670
  if command not in _LIST_POWER_COMMANDS:
671
    ToStderr("power subcommand %s not supported." % command)
672
    return constants.EXIT_FAILURE
673

    
674
  oob_command = "power-%s" % command
675

    
676
  if oob_command in _OOB_COMMAND_ASK:
677
    if not args:
678
      ToStderr("Please provide at least one node for this command")
679
      return constants.EXIT_FAILURE
680
    elif not opts.force and not ConfirmOperation(args, "nodes",
681
                                                 "power %s" % command):
682
      return constants.EXIT_FAILURE
683
    assert len(args) > 0
684

    
685
  opcodelist = []
686
  if not opts.ignore_status and oob_command == constants.OOB_POWER_OFF:
687
    # TODO: This is a little ugly as we can't catch and revert
688
    for node in args:
689
      opcodelist.append(opcodes.OpNodeSetParams(node_name=node, offline=True,
690
                                                auto_promote=opts.auto_promote))
691

    
692
  opcodelist.append(opcodes.OpOobCommand(node_names=args,
693
                                         command=oob_command,
694
                                         ignore_status=opts.ignore_status,
695
                                         timeout=opts.oob_timeout,
696
                                         power_delay=opts.power_delay))
697

    
698
  cli.SetGenericOpcodeOpts(opcodelist, opts)
699

    
700
  job_id = cli.SendJob(opcodelist)
701

    
702
  # We just want the OOB Opcode status
703
  # If it fails PollJob gives us the error message in it
704
  result = cli.PollJob(job_id)[-1]
705

    
706
  errs = 0
707
  data = []
708
  for node_result in result:
709
    (node_tuple, data_tuple) = node_result
710
    (_, node_name) = node_tuple
711
    (data_status, data_node) = data_tuple
712
    if data_status == constants.RS_NORMAL:
713
      if oob_command == constants.OOB_POWER_STATUS:
714
        if data_node[constants.OOB_POWER_STATUS_POWERED]:
715
          text = "powered"
716
        else:
717
          text = "unpowered"
718
        data.append([node_name, text])
719
      else:
720
        # We don't expect data here, so we just say, it was successfully invoked
721
        data.append([node_name, "invoked"])
722
    else:
723
      errs += 1
724
      data.append([node_name, cli.FormatResultError(data_status, True)])
725

    
726
  data = GenerateTable(separator=opts.separator, headers=headers,
727
                       fields=["node", "status"], data=data)
728

    
729
  for line in data:
730
    ToStdout(line)
731

    
732
  if errs:
733
    return constants.EXIT_FAILURE
734
  else:
735
    return constants.EXIT_SUCCESS
736

    
737

    
738
def Health(opts, args):
739
  """Show health of a node using OOB.
740

741
  @param opts: the command line options selected by the user
742
  @type args: list
743
  @param args: should contain only one element, the name of
744
      the node to be removed
745
  @rtype: int
746
  @return: the desired exit code
747

748
  """
749
  op = opcodes.OpOobCommand(node_names=args, command=constants.OOB_HEALTH,
750
                            timeout=opts.oob_timeout)
751
  result = SubmitOpCode(op, opts=opts)
752

    
753
  if opts.no_headers:
754
    headers = None
755
  else:
756
    headers = {"node": "Node", "status": "Status"}
757

    
758
  errs = 0
759
  data = []
760
  for node_result in result:
761
    (node_tuple, data_tuple) = node_result
762
    (_, node_name) = node_tuple
763
    (data_status, data_node) = data_tuple
764
    if data_status == constants.RS_NORMAL:
765
      data.append([node_name, "%s=%s" % tuple(data_node[0])])
766
      for item, status in data_node[1:]:
767
        data.append(["", "%s=%s" % (item, status)])
768
    else:
769
      errs += 1
770
      data.append([node_name, cli.FormatResultError(data_status, True)])
771

    
772
  data = GenerateTable(separator=opts.separator, headers=headers,
773
                       fields=["node", "status"], data=data)
774

    
775
  for line in data:
776
    ToStdout(line)
777

    
778
  if errs:
779
    return constants.EXIT_FAILURE
780
  else:
781
    return constants.EXIT_SUCCESS
782

    
783

    
784
def ListVolumes(opts, args):
785
  """List logical volumes on node(s).
786

787
  @param opts: the command line options selected by the user
788
  @type args: list
789
  @param args: should either be an empty list, in which case
790
      we list data for all nodes, or contain a list of nodes
791
      to display data only for those
792
  @rtype: int
793
  @return: the desired exit code
794

795
  """
796
  selected_fields = ParseFields(opts.output, _LIST_VOL_DEF_FIELDS)
797

    
798
  op = opcodes.OpNodeQueryvols(nodes=args, output_fields=selected_fields)
799
  output = SubmitOpCode(op, opts=opts)
800

    
801
  if not opts.no_headers:
802
    headers = {"node": "Node", "phys": "PhysDev",
803
               "vg": "VG", "name": "Name",
804
               "size": "Size", "instance": "Instance"}
805
  else:
806
    headers = None
807

    
808
  unitfields = ["size"]
809

    
810
  numfields = ["size"]
811

    
812
  data = GenerateTable(separator=opts.separator, headers=headers,
813
                       fields=selected_fields, unitfields=unitfields,
814
                       numfields=numfields, data=output, units=opts.units)
815

    
816
  for line in data:
817
    ToStdout(line)
818

    
819
  return 0
820

    
821

    
822
def ListStorage(opts, args):
823
  """List physical volumes on node(s).
824

825
  @param opts: the command line options selected by the user
826
  @type args: list
827
  @param args: should either be an empty list, in which case
828
      we list data for all nodes, or contain a list of nodes
829
      to display data only for those
830
  @rtype: int
831
  @return: the desired exit code
832

833
  """
834
  selected_fields = ParseFields(opts.output, _LIST_STOR_DEF_FIELDS)
835

    
836
  op = opcodes.OpNodeQueryStorage(nodes=args,
837
                                  storage_type=opts.user_storage_type,
838
                                  output_fields=selected_fields)
839
  output = SubmitOpCode(op, opts=opts)
840

    
841
  if not opts.no_headers:
842
    headers = {
843
      constants.SF_NODE: "Node",
844
      constants.SF_TYPE: "Type",
845
      constants.SF_NAME: "Name",
846
      constants.SF_SIZE: "Size",
847
      constants.SF_USED: "Used",
848
      constants.SF_FREE: "Free",
849
      constants.SF_ALLOCATABLE: "Allocatable",
850
      }
851
  else:
852
    headers = None
853

    
854
  unitfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
855
  numfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
856

    
857
  # change raw values to nicer strings
858
  for row in output:
859
    for idx, field in enumerate(selected_fields):
860
      val = row[idx]
861
      if field == constants.SF_ALLOCATABLE:
862
        if val:
863
          val = "Y"
864
        else:
865
          val = "N"
866
      row[idx] = str(val)
867

    
868
  data = GenerateTable(separator=opts.separator, headers=headers,
869
                       fields=selected_fields, unitfields=unitfields,
870
                       numfields=numfields, data=output, units=opts.units)
871

    
872
  for line in data:
873
    ToStdout(line)
874

    
875
  return 0
876

    
877

    
878
def ModifyStorage(opts, args):
879
  """Modify storage volume on a node.
880

881
  @param opts: the command line options selected by the user
882
  @type args: list
883
  @param args: should contain 3 items: node name, storage type and volume name
884
  @rtype: int
885
  @return: the desired exit code
886

887
  """
888
  (node_name, user_storage_type, volume_name) = args
889

    
890
  storage_type = ConvertStorageType(user_storage_type)
891

    
892
  changes = {}
893

    
894
  if opts.allocatable is not None:
895
    changes[constants.SF_ALLOCATABLE] = opts.allocatable
896

    
897
  if changes:
898
    op = opcodes.OpNodeModifyStorage(node_name=node_name,
899
                                     storage_type=storage_type,
900
                                     name=volume_name,
901
                                     changes=changes)
902
    SubmitOrSend(op, opts)
903
  else:
904
    ToStderr("No changes to perform, exiting.")
905

    
906

    
907
def RepairStorage(opts, args):
908
  """Repairs a storage volume on a node.
909

910
  @param opts: the command line options selected by the user
911
  @type args: list
912
  @param args: should contain 3 items: node name, storage type and volume name
913
  @rtype: int
914
  @return: the desired exit code
915

916
  """
917
  (node_name, user_storage_type, volume_name) = args
918

    
919
  storage_type = ConvertStorageType(user_storage_type)
920

    
921
  op = opcodes.OpRepairNodeStorage(node_name=node_name,
922
                                   storage_type=storage_type,
923
                                   name=volume_name,
924
                                   ignore_consistency=opts.ignore_consistency)
925
  SubmitOrSend(op, opts)
926

    
927

    
928
def SetNodeParams(opts, args):
929
  """Modifies a node.
930

931
  @param opts: the command line options selected by the user
932
  @type args: list
933
  @param args: should contain only one element, the node name
934
  @rtype: int
935
  @return: the desired exit code
936

937
  """
938
  all_changes = [opts.master_candidate, opts.drained, opts.offline,
939
                 opts.master_capable, opts.vm_capable, opts.secondary_ip,
940
                 opts.ndparams]
941
  if (all_changes.count(None) == len(all_changes) and
942
      not (opts.hv_state or opts.disk_state)):
943
    ToStderr("Please give at least one of the parameters.")
944
    return 1
945

    
946
  if opts.disk_state:
947
    disk_state = utils.FlatToDict(opts.disk_state)
948
  else:
949
    disk_state = {}
950

    
951
  hv_state = dict(opts.hv_state)
952

    
953
  op = opcodes.OpNodeSetParams(node_name=args[0],
954
                               master_candidate=opts.master_candidate,
955
                               offline=opts.offline,
956
                               drained=opts.drained,
957
                               master_capable=opts.master_capable,
958
                               vm_capable=opts.vm_capable,
959
                               secondary_ip=opts.secondary_ip,
960
                               force=opts.force,
961
                               ndparams=opts.ndparams,
962
                               auto_promote=opts.auto_promote,
963
                               powered=opts.node_powered,
964
                               hv_state=hv_state,
965
                               disk_state=disk_state)
966

    
967
  # even if here we process the result, we allow submit only
968
  result = SubmitOrSend(op, opts)
969

    
970
  if result:
971
    ToStdout("Modified node %s", args[0])
972
    for param, data in result:
973
      ToStdout(" - %-5s -> %s", param, data)
974
  return 0
975

    
976

    
977
def RestrictedCommand(opts, args):
978
  """Runs a remote command on node(s).
979

980
  @param opts: Command line options selected by user
981
  @type args: list
982
  @param args: Command line arguments
983
  @rtype: int
984
  @return: Exit code
985

986
  """
987
  cl = GetClient()
988

    
989
  if len(args) > 1 or opts.nodegroup:
990
    # Expand node names
991
    nodes = GetOnlineNodes(nodes=args[1:], cl=cl, nodegroup=opts.nodegroup)
992
  else:
993
    raise errors.OpPrereqError("Node group or node names must be given",
994
                               errors.ECODE_INVAL)
995

    
996
  op = opcodes.OpRestrictedCommand(command=args[0], nodes=nodes,
997
                                   use_locking=opts.do_locking)
998
  result = SubmitOrSend(op, opts, cl=cl)
999

    
1000
  exit_code = constants.EXIT_SUCCESS
1001

    
1002
  for (node, (status, text)) in zip(nodes, result):
1003
    ToStdout("------------------------------------------------")
1004
    if status:
1005
      if opts.show_machine_names:
1006
        for line in text.splitlines():
1007
          ToStdout("%s: %s", node, line)
1008
      else:
1009
        ToStdout("Node: %s", node)
1010
        ToStdout(text)
1011
    else:
1012
      exit_code = constants.EXIT_FAILURE
1013
      ToStdout(text)
1014

    
1015
  return exit_code
1016

    
1017

    
1018
class ReplyStatus(object):
1019
  """Class holding a reply status for synchronous confd clients.
1020

1021
  """
1022
  def __init__(self):
1023
    self.failure = True
1024
    self.answer = False
1025

    
1026

    
1027
def ListDrbd(opts, args):
1028
  """Modifies a node.
1029

1030
  @param opts: the command line options selected by the user
1031
  @type args: list
1032
  @param args: should contain only one element, the node name
1033
  @rtype: int
1034
  @return: the desired exit code
1035

1036
  """
1037
  if len(args) != 1:
1038
    ToStderr("Please give one (and only one) node.")
1039
    return constants.EXIT_FAILURE
1040

    
1041
  if not constants.ENABLE_CONFD:
1042
    ToStderr("Error: this command requires confd support, but it has not"
1043
             " been enabled at build time.")
1044
    return constants.EXIT_FAILURE
1045

    
1046
  status = ReplyStatus()
1047

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

    
1071
  node = args[0]
1072
  hmac = utils.ReadFile(pathutils.CONFD_HMAC_KEY)
1073
  filter_callback = confd_client.ConfdFilterCallback(ListDrbdConfdCallback)
1074
  counting_callback = confd_client.ConfdCountingCallback(filter_callback)
1075
  cf_client = confd_client.ConfdClient(hmac, [constants.IP4_ADDRESS_LOCALHOST],
1076
                                       counting_callback)
1077
  req = confd_client.ConfdClientRequest(type=constants.CONFD_REQ_NODE_DRBD,
1078
                                        query=node)
1079

    
1080
  def DoConfdRequestReply(req):
1081
    counting_callback.RegisterQuery(req.rsalt)
1082
    cf_client.SendRequest(req, async=False)
1083
    while not counting_callback.AllAnswered():
1084
      if not cf_client.ReceiveReply():
1085
        ToStderr("Did not receive all expected confd replies")
1086
        break
1087

    
1088
  DoConfdRequestReply(req)
1089

    
1090
  if status.failure:
1091
    return constants.EXIT_FAILURE
1092

    
1093
  fields = ["node", "minor", "instance", "disk", "role", "peer"]
1094
  if opts.no_headers:
1095
    headers = None
1096
  else:
1097
    headers = {"node": "Node", "minor": "Minor", "instance": "Instance",
1098
               "disk": "Disk", "role": "Role", "peer": "PeerNode"}
1099

    
1100
  data = GenerateTable(separator=opts.separator, headers=headers,
1101
                       fields=fields, data=sorted(status.answer),
1102
                       numfields=["minor"])
1103
  for line in data:
1104
    ToStdout(line)
1105

    
1106
  return constants.EXIT_SUCCESS
1107

    
1108

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

    
1234
#: dictionary with aliases for commands
1235
aliases = {
1236
  "show": "info",
1237
  }
1238

    
1239

    
1240
def Main():
1241
  return GenericMain(commands, aliases=aliases,
1242
                     override={"tag_type": constants.TAG_NODE},
1243
                     env_override=_ENV_OVERRIDE)