Statistics
| Branch: | Tag: | Revision:

root / lib / client / gnt_node.py @ 651ce6a3

History | View | Annotate | Download (38.7 kB)

1
#
2
#
3

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

    
21
"""Node related commands"""
22

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

    
29
import itertools
30
import errno
31

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

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

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

    
54

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

    
58

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

    
70

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

    
74

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

    
86

    
87
#: User-facing storage unit types
88
_USER_STORAGE_TYPE = {
89
  constants.ST_FILE: "file",
90
  constants.ST_LVM_PV: "lvm-pv",
91
  constants.ST_LVM_VG: "lvm-vg",
92
  }
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
  try:
253
    # Passing [] to QueryGroups means query the default group:
254
    node_groups = [opts.nodegroup] if opts.nodegroup is not None else []
255
    output = cl.QueryGroups(names=node_groups, fields=["ndp/ssh_port"],
256
                            use_locking=False)
257
    (ssh_port, ) = output[0]
258
  except (errors.OpPrereqError, errors.OpExecError):
259
    pass
260

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

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

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

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

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

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

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

    
306
  hv_state = dict(opts.hv_state)
307

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

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

    
326

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

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

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

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

    
342
  cl = GetClient(query=True)
343

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

    
349

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

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

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

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

    
365

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

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

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

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

    
393
  # Determine affected instances
394
  fields = []
395

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

    
401
  cl = GetClient()
402

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

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

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

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

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

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

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

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

    
443
  return rcode
444

    
445

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

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

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

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

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

    
472
  pinst = utils.NiceSort(pinst)
473

    
474
  retcode = 0
475

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

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

    
495

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

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

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

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

    
513
  pinst = utils.NiceSort(pinst)
514

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

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

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

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

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

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

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

    
553
  return rcode
554

    
555

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

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

    
587

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

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

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

    
613

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

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

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

    
629

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

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

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

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

    
652

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

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

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

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

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

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

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

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

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

    
699
  cli.SetGenericOpcodeOpts(opcodelist, opts)
700

    
701
  job_id = cli.SendJob(opcodelist)
702

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

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

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

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

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

    
738

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

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

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

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

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

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

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

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

    
784

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

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

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

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

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

    
809
  unitfields = ["size"]
810

    
811
  numfields = ["size"]
812

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

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

    
820
  return 0
821

    
822

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

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

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

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

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

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

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

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

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

    
876
  return 0
877

    
878

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

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

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

    
891
  storage_type = ConvertStorageType(user_storage_type)
892

    
893
  changes = {}
894

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

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

    
907

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

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

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

    
920
  storage_type = ConvertStorageType(user_storage_type)
921

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

    
928

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

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

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

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

    
952
  hv_state = dict(opts.hv_state)
953

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

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

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

    
977

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

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

987
  """
988
  cl = GetClient()
989

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

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

    
1001
  exit_code = constants.EXIT_SUCCESS
1002

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

    
1016
  return exit_code
1017

    
1018

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

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

    
1027

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

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

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

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

    
1047
  status = ReplyStatus()
1048

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

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

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

    
1089
  DoConfdRequestReply(req)
1090

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

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

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

    
1107
  return constants.EXIT_SUCCESS
1108

    
1109

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

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

    
1240

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