Statistics
| Branch: | Tag: | Revision:

root / lib / client / gnt_cluster.py @ 87bc7ca8

History | View | Annotate | Download (31.1 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007, 2010, 2011 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
"""Cluster related commands"""
22

    
23
# pylint: disable-msg=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-cluster
28

    
29
import os.path
30
import time
31
import OpenSSL
32

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

    
44

    
45
@UsesRPC
46
def InitCluster(opts, args):
47
  """Initialize the cluster.
48

49
  @param opts: the command line options selected by the user
50
  @type args: list
51
  @param args: should contain only one element, the desired
52
      cluster name
53
  @rtype: int
54
  @return: the desired exit code
55

56
  """
57
  if not opts.lvm_storage and opts.vg_name:
58
    ToStderr("Options --no-lvm-storage and --vg-name conflict.")
59
    return 1
60

    
61
  vg_name = opts.vg_name
62
  if opts.lvm_storage and not opts.vg_name:
63
    vg_name = constants.DEFAULT_VG
64

    
65
  if not opts.drbd_storage and opts.drbd_helper:
66
    ToStderr("Options --no-drbd-storage and --drbd-usermode-helper conflict.")
67
    return 1
68

    
69
  drbd_helper = opts.drbd_helper
70
  if opts.drbd_storage and not opts.drbd_helper:
71
    drbd_helper = constants.DEFAULT_DRBD_HELPER
72

    
73
  master_netdev = opts.master_netdev
74
  if master_netdev is None:
75
    master_netdev = constants.DEFAULT_BRIDGE
76

    
77
  hvlist = opts.enabled_hypervisors
78
  if hvlist is None:
79
    hvlist = constants.DEFAULT_ENABLED_HYPERVISOR
80
  hvlist = hvlist.split(",")
81

    
82
  hvparams = dict(opts.hvparams)
83
  beparams = opts.beparams
84
  nicparams = opts.nicparams
85

    
86
  # prepare beparams dict
87
  beparams = objects.FillDict(constants.BEC_DEFAULTS, beparams)
88
  utils.ForceDictType(beparams, constants.BES_PARAMETER_TYPES)
89

    
90
  # prepare nicparams dict
91
  nicparams = objects.FillDict(constants.NICC_DEFAULTS, nicparams)
92
  utils.ForceDictType(nicparams, constants.NICS_PARAMETER_TYPES)
93

    
94
  # prepare ndparams dict
95
  if opts.ndparams is None:
96
    ndparams = dict(constants.NDC_DEFAULTS)
97
  else:
98
    ndparams = objects.FillDict(constants.NDC_DEFAULTS, opts.ndparams)
99
    utils.ForceDictType(ndparams, constants.NDS_PARAMETER_TYPES)
100

    
101
  # prepare hvparams dict
102
  for hv in constants.HYPER_TYPES:
103
    if hv not in hvparams:
104
      hvparams[hv] = {}
105
    hvparams[hv] = objects.FillDict(constants.HVC_DEFAULTS[hv], hvparams[hv])
106
    utils.ForceDictType(hvparams[hv], constants.HVS_PARAMETER_TYPES)
107

    
108
  if opts.candidate_pool_size is None:
109
    opts.candidate_pool_size = constants.MASTER_POOL_SIZE_DEFAULT
110

    
111
  if opts.mac_prefix is None:
112
    opts.mac_prefix = constants.DEFAULT_MAC_PREFIX
113

    
114
  uid_pool = opts.uid_pool
115
  if uid_pool is not None:
116
    uid_pool = uidpool.ParseUidPool(uid_pool)
117

    
118
  if opts.prealloc_wipe_disks is None:
119
    opts.prealloc_wipe_disks = False
120

    
121
  try:
122
    primary_ip_version = int(opts.primary_ip_version)
123
  except (ValueError, TypeError), err:
124
    ToStderr("Invalid primary ip version value: %s" % str(err))
125
    return 1
126

    
127
  bootstrap.InitCluster(cluster_name=args[0],
128
                        secondary_ip=opts.secondary_ip,
129
                        vg_name=vg_name,
130
                        mac_prefix=opts.mac_prefix,
131
                        master_netdev=master_netdev,
132
                        file_storage_dir=opts.file_storage_dir,
133
                        shared_file_storage_dir=opts.shared_file_storage_dir,
134
                        enabled_hypervisors=hvlist,
135
                        hvparams=hvparams,
136
                        beparams=beparams,
137
                        nicparams=nicparams,
138
                        ndparams=ndparams,
139
                        candidate_pool_size=opts.candidate_pool_size,
140
                        modify_etc_hosts=opts.modify_etc_hosts,
141
                        modify_ssh_setup=opts.modify_ssh_setup,
142
                        maintain_node_health=opts.maintain_node_health,
143
                        drbd_helper=drbd_helper,
144
                        uid_pool=uid_pool,
145
                        default_iallocator=opts.default_iallocator,
146
                        primary_ip_version=primary_ip_version,
147
                        prealloc_wipe_disks=opts.prealloc_wipe_disks,
148
                        )
149
  op = opcodes.OpClusterPostInit()
150
  SubmitOpCode(op, opts=opts)
151
  return 0
152

    
153

    
154
@UsesRPC
155
def DestroyCluster(opts, args):
156
  """Destroy the cluster.
157

158
  @param opts: the command line options selected by the user
159
  @type args: list
160
  @param args: should be an empty list
161
  @rtype: int
162
  @return: the desired exit code
163

164
  """
165
  if not opts.yes_do_it:
166
    ToStderr("Destroying a cluster is irreversible. If you really want"
167
             " destroy this cluster, supply the --yes-do-it option.")
168
    return 1
169

    
170
  op = opcodes.OpClusterDestroy()
171
  master = SubmitOpCode(op, opts=opts)
172
  # if we reached this, the opcode didn't fail; we can proceed to
173
  # shutdown all the daemons
174
  bootstrap.FinalizeClusterDestroy(master)
175
  return 0
176

    
177

    
178
def RenameCluster(opts, args):
179
  """Rename the cluster.
180

181
  @param opts: the command line options selected by the user
182
  @type args: list
183
  @param args: should contain only one element, the new cluster name
184
  @rtype: int
185
  @return: the desired exit code
186

187
  """
188
  cl = GetClient()
189

    
190
  (cluster_name, ) = cl.QueryConfigValues(["cluster_name"])
191

    
192
  new_name = args[0]
193
  if not opts.force:
194
    usertext = ("This will rename the cluster from '%s' to '%s'. If you are"
195
                " connected over the network to the cluster name, the"
196
                " operation is very dangerous as the IP address will be"
197
                " removed from the node and the change may not go through."
198
                " Continue?") % (cluster_name, new_name)
199
    if not AskUser(usertext):
200
      return 1
201

    
202
  op = opcodes.OpClusterRename(name=new_name)
203
  result = SubmitOpCode(op, opts=opts, cl=cl)
204

    
205
  if result:
206
    ToStdout("Cluster renamed from '%s' to '%s'", cluster_name, result)
207

    
208
  return 0
209

    
210

    
211
def RedistributeConfig(opts, args):
212
  """Forces push of the cluster configuration.
213

214
  @param opts: the command line options selected by the user
215
  @type args: list
216
  @param args: empty list
217
  @rtype: int
218
  @return: the desired exit code
219

220
  """
221
  op = opcodes.OpClusterRedistConf()
222
  SubmitOrSend(op, opts)
223
  return 0
224

    
225

    
226
def ShowClusterVersion(opts, args):
227
  """Write version of ganeti software to the standard output.
228

229
  @param opts: the command line options selected by the user
230
  @type args: list
231
  @param args: should be an empty list
232
  @rtype: int
233
  @return: the desired exit code
234

235
  """
236
  cl = GetClient()
237
  result = cl.QueryClusterInfo()
238
  ToStdout("Software version: %s", result["software_version"])
239
  ToStdout("Internode protocol: %s", result["protocol_version"])
240
  ToStdout("Configuration format: %s", result["config_version"])
241
  ToStdout("OS api version: %s", result["os_api_version"])
242
  ToStdout("Export interface: %s", result["export_version"])
243
  return 0
244

    
245

    
246
def ShowClusterMaster(opts, args):
247
  """Write name of master node to the standard output.
248

249
  @param opts: the command line options selected by the user
250
  @type args: list
251
  @param args: should be an empty list
252
  @rtype: int
253
  @return: the desired exit code
254

255
  """
256
  master = bootstrap.GetMaster()
257
  ToStdout(master)
258
  return 0
259

    
260

    
261
def _PrintGroupedParams(paramsdict, level=1, roman=False):
262
  """Print Grouped parameters (be, nic, disk) by group.
263

264
  @type paramsdict: dict of dicts
265
  @param paramsdict: {group: {param: value, ...}, ...}
266
  @type level: int
267
  @param level: Level of indention
268

269
  """
270
  indent = "  " * level
271
  for item, val in sorted(paramsdict.items()):
272
    if isinstance(val, dict):
273
      ToStdout("%s- %s:", indent, item)
274
      _PrintGroupedParams(val, level=level + 1, roman=roman)
275
    elif roman and isinstance(val, int):
276
      ToStdout("%s  %s: %s", indent, item, compat.TryToRoman(val))
277
    else:
278
      ToStdout("%s  %s: %s", indent, item, val)
279

    
280

    
281
def ShowClusterConfig(opts, args):
282
  """Shows cluster information.
283

284
  @param opts: the command line options selected by the user
285
  @type args: list
286
  @param args: should be an empty list
287
  @rtype: int
288
  @return: the desired exit code
289

290
  """
291
  cl = GetClient()
292
  result = cl.QueryClusterInfo()
293

    
294
  ToStdout("Cluster name: %s", result["name"])
295
  ToStdout("Cluster UUID: %s", result["uuid"])
296

    
297
  ToStdout("Creation time: %s", utils.FormatTime(result["ctime"]))
298
  ToStdout("Modification time: %s", utils.FormatTime(result["mtime"]))
299

    
300
  ToStdout("Master node: %s", result["master"])
301

    
302
  ToStdout("Architecture (this node): %s (%s)",
303
           result["architecture"][0], result["architecture"][1])
304

    
305
  if result["tags"]:
306
    tags = utils.CommaJoin(utils.NiceSort(result["tags"]))
307
  else:
308
    tags = "(none)"
309

    
310
  ToStdout("Tags: %s", tags)
311

    
312
  ToStdout("Default hypervisor: %s", result["default_hypervisor"])
313
  ToStdout("Enabled hypervisors: %s",
314
           utils.CommaJoin(result["enabled_hypervisors"]))
315

    
316
  ToStdout("Hypervisor parameters:")
317
  _PrintGroupedParams(result["hvparams"])
318

    
319
  ToStdout("OS-specific hypervisor parameters:")
320
  _PrintGroupedParams(result["os_hvp"])
321

    
322
  ToStdout("OS parameters:")
323
  _PrintGroupedParams(result["osparams"])
324

    
325
  ToStdout("Hidden OSes: %s", utils.CommaJoin(result["hidden_os"]))
326
  ToStdout("Blacklisted OSes: %s", utils.CommaJoin(result["blacklisted_os"]))
327

    
328
  ToStdout("Cluster parameters:")
329
  ToStdout("  - candidate pool size: %s",
330
            compat.TryToRoman(result["candidate_pool_size"],
331
                              convert=opts.roman_integers))
332
  ToStdout("  - master netdev: %s", result["master_netdev"])
333
  ToStdout("  - lvm volume group: %s", result["volume_group_name"])
334
  if result["reserved_lvs"]:
335
    reserved_lvs = utils.CommaJoin(result["reserved_lvs"])
336
  else:
337
    reserved_lvs = "(none)"
338
  ToStdout("  - lvm reserved volumes: %s", reserved_lvs)
339
  ToStdout("  - drbd usermode helper: %s", result["drbd_usermode_helper"])
340
  ToStdout("  - file storage path: %s", result["file_storage_dir"])
341
  ToStdout("  - shared file storage path: %s",
342
           result["shared_file_storage_dir"])
343
  ToStdout("  - maintenance of node health: %s",
344
           result["maintain_node_health"])
345
  ToStdout("  - uid pool: %s",
346
            uidpool.FormatUidPool(result["uid_pool"],
347
                                  roman=opts.roman_integers))
348
  ToStdout("  - default instance allocator: %s", result["default_iallocator"])
349
  ToStdout("  - primary ip version: %d", result["primary_ip_version"])
350
  ToStdout("  - preallocation wipe disks: %s", result["prealloc_wipe_disks"])
351

    
352
  ToStdout("Default node parameters:")
353
  _PrintGroupedParams(result["ndparams"], roman=opts.roman_integers)
354

    
355
  ToStdout("Default instance parameters:")
356
  _PrintGroupedParams(result["beparams"], roman=opts.roman_integers)
357

    
358
  ToStdout("Default nic parameters:")
359
  _PrintGroupedParams(result["nicparams"], roman=opts.roman_integers)
360

    
361
  return 0
362

    
363

    
364
def ClusterCopyFile(opts, args):
365
  """Copy a file from master to some nodes.
366

367
  @param opts: the command line options selected by the user
368
  @type args: list
369
  @param args: should contain only one element, the path of
370
      the file to be copied
371
  @rtype: int
372
  @return: the desired exit code
373

374
  """
375
  filename = args[0]
376
  if not os.path.exists(filename):
377
    raise errors.OpPrereqError("No such filename '%s'" % filename,
378
                               errors.ECODE_INVAL)
379

    
380
  cl = GetClient()
381

    
382
  cluster_name = cl.QueryConfigValues(["cluster_name"])[0]
383

    
384
  results = GetOnlineNodes(nodes=opts.nodes, cl=cl, filter_master=True,
385
                           secondary_ips=opts.use_replication_network)
386

    
387
  srun = ssh.SshRunner(cluster_name=cluster_name)
388
  for node in results:
389
    if not srun.CopyFileToNode(node, filename):
390
      ToStderr("Copy of file %s to node %s failed", filename, node)
391

    
392
  return 0
393

    
394

    
395
def RunClusterCommand(opts, args):
396
  """Run a command on some nodes.
397

398
  @param opts: the command line options selected by the user
399
  @type args: list
400
  @param args: should contain the command to be run and its arguments
401
  @rtype: int
402
  @return: the desired exit code
403

404
  """
405
  cl = GetClient()
406

    
407
  command = " ".join(args)
408

    
409
  nodes = GetOnlineNodes(nodes=opts.nodes, cl=cl)
410

    
411
  cluster_name, master_node = cl.QueryConfigValues(["cluster_name",
412
                                                    "master_node"])
413

    
414
  srun = ssh.SshRunner(cluster_name=cluster_name)
415

    
416
  # Make sure master node is at list end
417
  if master_node in nodes:
418
    nodes.remove(master_node)
419
    nodes.append(master_node)
420

    
421
  for name in nodes:
422
    result = srun.Run(name, "root", command)
423
    ToStdout("------------------------------------------------")
424
    ToStdout("node: %s", name)
425
    ToStdout("%s", result.output)
426
    ToStdout("return code = %s", result.exit_code)
427

    
428
  return 0
429

    
430

    
431
def VerifyCluster(opts, args):
432
  """Verify integrity of cluster, performing various test on nodes.
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
  skip_checks = []
442
  if opts.skip_nplusone_mem:
443
    skip_checks.append(constants.VERIFY_NPLUSONE_MEM)
444
  op = opcodes.OpClusterVerify(skip_checks=skip_checks,
445
                               verbose=opts.verbose,
446
                               error_codes=opts.error_codes,
447
                               debug_simulate_errors=opts.simulate_errors)
448
  if SubmitOpCode(op, opts=opts):
449
    return 0
450
  else:
451
    return 1
452

    
453

    
454
def VerifyDisks(opts, args):
455
  """Verify integrity of cluster disks.
456

457
  @param opts: the command line options selected by the user
458
  @type args: list
459
  @param args: should be an empty list
460
  @rtype: int
461
  @return: the desired exit code
462

463
  """
464
  cl = GetClient()
465

    
466
  op = opcodes.OpClusterVerifyDisks()
467
  result = SubmitOpCode(op, opts=opts, cl=cl)
468
  if not isinstance(result, (list, tuple)) or len(result) != 3:
469
    raise errors.ProgrammerError("Unknown result type for OpClusterVerifyDisks")
470

    
471
  bad_nodes, instances, missing = result
472

    
473
  retcode = constants.EXIT_SUCCESS
474

    
475
  if bad_nodes:
476
    for node, text in bad_nodes.items():
477
      ToStdout("Error gathering data on node %s: %s",
478
               node, utils.SafeEncode(text[-400:]))
479
      retcode |= 1
480
      ToStdout("You need to fix these nodes first before fixing instances")
481

    
482
  if instances:
483
    for iname in instances:
484
      if iname in missing:
485
        continue
486
      op = opcodes.OpInstanceActivateDisks(instance_name=iname)
487
      try:
488
        ToStdout("Activating disks for instance '%s'", iname)
489
        SubmitOpCode(op, opts=opts, cl=cl)
490
      except errors.GenericError, err:
491
        nret, msg = FormatError(err)
492
        retcode |= nret
493
        ToStderr("Error activating disks for instance %s: %s", iname, msg)
494

    
495
  if missing:
496
    for iname, ival in missing.iteritems():
497
      all_missing = compat.all(x[0] in bad_nodes for x in ival)
498
      if all_missing:
499
        ToStdout("Instance %s cannot be verified as it lives on"
500
                 " broken nodes", iname)
501
      else:
502
        ToStdout("Instance %s has missing logical volumes:", iname)
503
        ival.sort()
504
        for node, vol in ival:
505
          if node in bad_nodes:
506
            ToStdout("\tbroken node %s /dev/%s", node, vol)
507
          else:
508
            ToStdout("\t%s /dev/%s", node, vol)
509

    
510
    ToStdout("You need to run replace or recreate disks for all the above"
511
             " instances, if this message persist after fixing nodes.")
512
    retcode |= 1
513

    
514
  return retcode
515

    
516

    
517
def RepairDiskSizes(opts, args):
518
  """Verify sizes of cluster disks.
519

520
  @param opts: the command line options selected by the user
521
  @type args: list
522
  @param args: optional list of instances to restrict check to
523
  @rtype: int
524
  @return: the desired exit code
525

526
  """
527
  op = opcodes.OpClusterRepairDiskSizes(instances=args)
528
  SubmitOpCode(op, opts=opts)
529

    
530

    
531
@UsesRPC
532
def MasterFailover(opts, args):
533
  """Failover the master node.
534

535
  This command, when run on a non-master node, will cause the current
536
  master to cease being master, and the non-master to become new
537
  master.
538

539
  @param opts: the command line options selected by the user
540
  @type args: list
541
  @param args: should be an empty list
542
  @rtype: int
543
  @return: the desired exit code
544

545
  """
546
  if opts.no_voting:
547
    usertext = ("This will perform the failover even if most other nodes"
548
                " are down, or if this node is outdated. This is dangerous"
549
                " as it can lead to a non-consistent cluster. Check the"
550
                " gnt-cluster(8) man page before proceeding. Continue?")
551
    if not AskUser(usertext):
552
      return 1
553

    
554
  return bootstrap.MasterFailover(no_voting=opts.no_voting)
555

    
556

    
557
def MasterPing(opts, args):
558
  """Checks if the master is alive.
559

560
  @param opts: the command line options selected by the user
561
  @type args: list
562
  @param args: should be an empty list
563
  @rtype: int
564
  @return: the desired exit code
565

566
  """
567
  try:
568
    cl = GetClient()
569
    cl.QueryClusterInfo()
570
    return 0
571
  except Exception: # pylint: disable-msg=W0703
572
    return 1
573

    
574

    
575
def SearchTags(opts, args):
576
  """Searches the tags on all the cluster.
577

578
  @param opts: the command line options selected by the user
579
  @type args: list
580
  @param args: should contain only one element, the tag pattern
581
  @rtype: int
582
  @return: the desired exit code
583

584
  """
585
  op = opcodes.OpTagsSearch(pattern=args[0])
586
  result = SubmitOpCode(op, opts=opts)
587
  if not result:
588
    return 1
589
  result = list(result)
590
  result.sort()
591
  for path, tag in result:
592
    ToStdout("%s %s", path, tag)
593

    
594

    
595
def _RenewCrypto(new_cluster_cert, new_rapi_cert, rapi_cert_filename,
596
                 new_confd_hmac_key, new_cds, cds_filename,
597
                 force):
598
  """Renews cluster certificates, keys and secrets.
599

600
  @type new_cluster_cert: bool
601
  @param new_cluster_cert: Whether to generate a new cluster certificate
602
  @type new_rapi_cert: bool
603
  @param new_rapi_cert: Whether to generate a new RAPI certificate
604
  @type rapi_cert_filename: string
605
  @param rapi_cert_filename: Path to file containing new RAPI certificate
606
  @type new_confd_hmac_key: bool
607
  @param new_confd_hmac_key: Whether to generate a new HMAC key
608
  @type new_cds: bool
609
  @param new_cds: Whether to generate a new cluster domain secret
610
  @type cds_filename: string
611
  @param cds_filename: Path to file containing new cluster domain secret
612
  @type force: bool
613
  @param force: Whether to ask user for confirmation
614

615
  """
616
  if new_rapi_cert and rapi_cert_filename:
617
    ToStderr("Only one of the --new-rapi-certficate and --rapi-certificate"
618
             " options can be specified at the same time.")
619
    return 1
620

    
621
  if new_cds and cds_filename:
622
    ToStderr("Only one of the --new-cluster-domain-secret and"
623
             " --cluster-domain-secret options can be specified at"
624
             " the same time.")
625
    return 1
626

    
627
  if rapi_cert_filename:
628
    # Read and verify new certificate
629
    try:
630
      rapi_cert_pem = utils.ReadFile(rapi_cert_filename)
631

    
632
      OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM,
633
                                      rapi_cert_pem)
634
    except Exception, err: # pylint: disable-msg=W0703
635
      ToStderr("Can't load new RAPI certificate from %s: %s" %
636
               (rapi_cert_filename, str(err)))
637
      return 1
638

    
639
    try:
640
      OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, rapi_cert_pem)
641
    except Exception, err: # pylint: disable-msg=W0703
642
      ToStderr("Can't load new RAPI private key from %s: %s" %
643
               (rapi_cert_filename, str(err)))
644
      return 1
645

    
646
  else:
647
    rapi_cert_pem = None
648

    
649
  if cds_filename:
650
    try:
651
      cds = utils.ReadFile(cds_filename)
652
    except Exception, err: # pylint: disable-msg=W0703
653
      ToStderr("Can't load new cluster domain secret from %s: %s" %
654
               (cds_filename, str(err)))
655
      return 1
656
  else:
657
    cds = None
658

    
659
  if not force:
660
    usertext = ("This requires all daemons on all nodes to be restarted and"
661
                " may take some time. Continue?")
662
    if not AskUser(usertext):
663
      return 1
664

    
665
  def _RenewCryptoInner(ctx):
666
    ctx.feedback_fn("Updating certificates and keys")
667
    bootstrap.GenerateClusterCrypto(new_cluster_cert, new_rapi_cert,
668
                                    new_confd_hmac_key,
669
                                    new_cds,
670
                                    rapi_cert_pem=rapi_cert_pem,
671
                                    cds=cds)
672

    
673
    files_to_copy = []
674

    
675
    if new_cluster_cert:
676
      files_to_copy.append(constants.NODED_CERT_FILE)
677

    
678
    if new_rapi_cert or rapi_cert_pem:
679
      files_to_copy.append(constants.RAPI_CERT_FILE)
680

    
681
    if new_confd_hmac_key:
682
      files_to_copy.append(constants.CONFD_HMAC_KEY)
683

    
684
    if new_cds or cds:
685
      files_to_copy.append(constants.CLUSTER_DOMAIN_SECRET_FILE)
686

    
687
    if files_to_copy:
688
      for node_name in ctx.nonmaster_nodes:
689
        ctx.feedback_fn("Copying %s to %s" %
690
                        (", ".join(files_to_copy), node_name))
691
        for file_name in files_to_copy:
692
          ctx.ssh.CopyFileToNode(node_name, file_name)
693

    
694
  RunWhileClusterStopped(ToStdout, _RenewCryptoInner)
695

    
696
  ToStdout("All requested certificates and keys have been replaced."
697
           " Running \"gnt-cluster verify\" now is recommended.")
698

    
699
  return 0
700

    
701

    
702
def RenewCrypto(opts, args):
703
  """Renews cluster certificates, keys and secrets.
704

705
  """
706
  return _RenewCrypto(opts.new_cluster_cert,
707
                      opts.new_rapi_cert,
708
                      opts.rapi_cert,
709
                      opts.new_confd_hmac_key,
710
                      opts.new_cluster_domain_secret,
711
                      opts.cluster_domain_secret,
712
                      opts.force)
713

    
714

    
715
def SetClusterParams(opts, args):
716
  """Modify the cluster.
717

718
  @param opts: the command line options selected by the user
719
  @type args: list
720
  @param args: should be an empty list
721
  @rtype: int
722
  @return: the desired exit code
723

724
  """
725
  if not (not opts.lvm_storage or opts.vg_name or
726
          not opts.drbd_storage or opts.drbd_helper or
727
          opts.enabled_hypervisors or opts.hvparams or
728
          opts.beparams or opts.nicparams or opts.ndparams or
729
          opts.candidate_pool_size is not None or
730
          opts.uid_pool is not None or
731
          opts.maintain_node_health is not None or
732
          opts.add_uids is not None or
733
          opts.remove_uids is not None or
734
          opts.default_iallocator is not None or
735
          opts.reserved_lvs is not None or
736
          opts.master_netdev is not None or
737
          opts.prealloc_wipe_disks is not None):
738
    ToStderr("Please give at least one of the parameters.")
739
    return 1
740

    
741
  vg_name = opts.vg_name
742
  if not opts.lvm_storage and opts.vg_name:
743
    ToStderr("Options --no-lvm-storage and --vg-name conflict.")
744
    return 1
745

    
746
  if not opts.lvm_storage:
747
    vg_name = ""
748

    
749
  drbd_helper = opts.drbd_helper
750
  if not opts.drbd_storage and opts.drbd_helper:
751
    ToStderr("Options --no-drbd-storage and --drbd-usermode-helper conflict.")
752
    return 1
753

    
754
  if not opts.drbd_storage:
755
    drbd_helper = ""
756

    
757
  hvlist = opts.enabled_hypervisors
758
  if hvlist is not None:
759
    hvlist = hvlist.split(",")
760

    
761
  # a list of (name, dict) we can pass directly to dict() (or [])
762
  hvparams = dict(opts.hvparams)
763
  for hv_params in hvparams.values():
764
    utils.ForceDictType(hv_params, constants.HVS_PARAMETER_TYPES)
765

    
766
  beparams = opts.beparams
767
  utils.ForceDictType(beparams, constants.BES_PARAMETER_TYPES)
768

    
769
  nicparams = opts.nicparams
770
  utils.ForceDictType(nicparams, constants.NICS_PARAMETER_TYPES)
771

    
772
  ndparams = opts.ndparams
773
  if ndparams is not None:
774
    utils.ForceDictType(ndparams, constants.NDS_PARAMETER_TYPES)
775

    
776
  mnh = opts.maintain_node_health
777

    
778
  uid_pool = opts.uid_pool
779
  if uid_pool is not None:
780
    uid_pool = uidpool.ParseUidPool(uid_pool)
781

    
782
  add_uids = opts.add_uids
783
  if add_uids is not None:
784
    add_uids = uidpool.ParseUidPool(add_uids)
785

    
786
  remove_uids = opts.remove_uids
787
  if remove_uids is not None:
788
    remove_uids = uidpool.ParseUidPool(remove_uids)
789

    
790
  if opts.reserved_lvs is not None:
791
    if opts.reserved_lvs == "":
792
      opts.reserved_lvs = []
793
    else:
794
      opts.reserved_lvs = utils.UnescapeAndSplit(opts.reserved_lvs, sep=",")
795

    
796
  op = opcodes.OpClusterSetParams(vg_name=vg_name,
797
                                  drbd_helper=drbd_helper,
798
                                  enabled_hypervisors=hvlist,
799
                                  hvparams=hvparams,
800
                                  os_hvp=None,
801
                                  beparams=beparams,
802
                                  nicparams=nicparams,
803
                                  ndparams=ndparams,
804
                                  candidate_pool_size=opts.candidate_pool_size,
805
                                  maintain_node_health=mnh,
806
                                  uid_pool=uid_pool,
807
                                  add_uids=add_uids,
808
                                  remove_uids=remove_uids,
809
                                  default_iallocator=opts.default_iallocator,
810
                                  prealloc_wipe_disks=opts.prealloc_wipe_disks,
811
                                  master_netdev=opts.master_netdev,
812
                                  reserved_lvs=opts.reserved_lvs)
813
  SubmitOpCode(op, opts=opts)
814
  return 0
815

    
816

    
817
def QueueOps(opts, args):
818
  """Queue operations.
819

820
  @param opts: the command line options selected by the user
821
  @type args: list
822
  @param args: should contain only one element, the subcommand
823
  @rtype: int
824
  @return: the desired exit code
825

826
  """
827
  command = args[0]
828
  client = GetClient()
829
  if command in ("drain", "undrain"):
830
    drain_flag = command == "drain"
831
    client.SetQueueDrainFlag(drain_flag)
832
  elif command == "info":
833
    result = client.QueryConfigValues(["drain_flag"])
834
    if result[0]:
835
      val = "set"
836
    else:
837
      val = "unset"
838
    ToStdout("The drain flag is %s" % val)
839
  else:
840
    raise errors.OpPrereqError("Command '%s' is not valid." % command,
841
                               errors.ECODE_INVAL)
842

    
843
  return 0
844

    
845

    
846
def _ShowWatcherPause(until):
847
  if until is None or until < time.time():
848
    ToStdout("The watcher is not paused.")
849
  else:
850
    ToStdout("The watcher is paused until %s.", time.ctime(until))
851

    
852

    
853
def WatcherOps(opts, args):
854
  """Watcher operations.
855

856
  @param opts: the command line options selected by the user
857
  @type args: list
858
  @param args: should contain only one element, the subcommand
859
  @rtype: int
860
  @return: the desired exit code
861

862
  """
863
  command = args[0]
864
  client = GetClient()
865

    
866
  if command == "continue":
867
    client.SetWatcherPause(None)
868
    ToStdout("The watcher is no longer paused.")
869

    
870
  elif command == "pause":
871
    if len(args) < 2:
872
      raise errors.OpPrereqError("Missing pause duration", errors.ECODE_INVAL)
873

    
874
    result = client.SetWatcherPause(time.time() + ParseTimespec(args[1]))
875
    _ShowWatcherPause(result)
876

    
877
  elif command == "info":
878
    result = client.QueryConfigValues(["watcher_pause"])
879
    _ShowWatcherPause(result[0])
880

    
881
  else:
882
    raise errors.OpPrereqError("Command '%s' is not valid." % command,
883
                               errors.ECODE_INVAL)
884

    
885
  return 0
886

    
887

    
888
commands = {
889
  'init': (
890
    InitCluster, [ArgHost(min=1, max=1)],
891
    [BACKEND_OPT, CP_SIZE_OPT, ENABLED_HV_OPT, GLOBAL_FILEDIR_OPT,
892
     HVLIST_OPT, MAC_PREFIX_OPT, MASTER_NETDEV_OPT, NIC_PARAMS_OPT,
893
     NOLVM_STORAGE_OPT, NOMODIFY_ETCHOSTS_OPT, NOMODIFY_SSH_SETUP_OPT,
894
     SECONDARY_IP_OPT, VG_NAME_OPT, MAINTAIN_NODE_HEALTH_OPT,
895
     UIDPOOL_OPT, DRBD_HELPER_OPT, NODRBD_STORAGE_OPT,
896
     DEFAULT_IALLOCATOR_OPT, PRIMARY_IP_VERSION_OPT, PREALLOC_WIPE_DISKS_OPT,
897
     NODE_PARAMS_OPT, GLOBAL_SHARED_FILEDIR_OPT],
898
    "[opts...] <cluster_name>", "Initialises a new cluster configuration"),
899
  'destroy': (
900
    DestroyCluster, ARGS_NONE, [YES_DOIT_OPT],
901
    "", "Destroy cluster"),
902
  'rename': (
903
    RenameCluster, [ArgHost(min=1, max=1)],
904
    [FORCE_OPT, DRY_RUN_OPT],
905
    "<new_name>",
906
    "Renames the cluster"),
907
  'redist-conf': (
908
    RedistributeConfig, ARGS_NONE, [SUBMIT_OPT, DRY_RUN_OPT, PRIORITY_OPT],
909
    "", "Forces a push of the configuration file and ssconf files"
910
    " to the nodes in the cluster"),
911
  'verify': (
912
    VerifyCluster, ARGS_NONE,
913
    [VERBOSE_OPT, DEBUG_SIMERR_OPT, ERROR_CODES_OPT, NONPLUS1_OPT,
914
     DRY_RUN_OPT, PRIORITY_OPT],
915
    "", "Does a check on the cluster configuration"),
916
  'verify-disks': (
917
    VerifyDisks, ARGS_NONE, [PRIORITY_OPT],
918
    "", "Does a check on the cluster disk status"),
919
  'repair-disk-sizes': (
920
    RepairDiskSizes, ARGS_MANY_INSTANCES, [DRY_RUN_OPT, PRIORITY_OPT],
921
    "", "Updates mismatches in recorded disk sizes"),
922
  'master-failover': (
923
    MasterFailover, ARGS_NONE, [NOVOTING_OPT],
924
    "", "Makes the current node the master"),
925
  'master-ping': (
926
    MasterPing, ARGS_NONE, [],
927
    "", "Checks if the master is alive"),
928
  'version': (
929
    ShowClusterVersion, ARGS_NONE, [],
930
    "", "Shows the cluster version"),
931
  'getmaster': (
932
    ShowClusterMaster, ARGS_NONE, [],
933
    "", "Shows the cluster master"),
934
  'copyfile': (
935
    ClusterCopyFile, [ArgFile(min=1, max=1)],
936
    [NODE_LIST_OPT, USE_REPL_NET_OPT],
937
    "[-n node...] <filename>", "Copies a file to all (or only some) nodes"),
938
  'command': (
939
    RunClusterCommand, [ArgCommand(min=1)],
940
    [NODE_LIST_OPT],
941
    "[-n node...] <command>", "Runs a command on all (or only some) nodes"),
942
  'info': (
943
    ShowClusterConfig, ARGS_NONE, [ROMAN_OPT],
944
    "[--roman]", "Show cluster configuration"),
945
  'list-tags': (
946
    ListTags, ARGS_NONE, [], "", "List the tags of the cluster"),
947
  'add-tags': (
948
    AddTags, [ArgUnknown()], [TAG_SRC_OPT, PRIORITY_OPT],
949
    "tag...", "Add tags to the cluster"),
950
  'remove-tags': (
951
    RemoveTags, [ArgUnknown()], [TAG_SRC_OPT, PRIORITY_OPT],
952
    "tag...", "Remove tags from the cluster"),
953
  'search-tags': (
954
    SearchTags, [ArgUnknown(min=1, max=1)], [PRIORITY_OPT], "",
955
    "Searches the tags on all objects on"
956
    " the cluster for a given pattern (regex)"),
957
  'queue': (
958
    QueueOps,
959
    [ArgChoice(min=1, max=1, choices=["drain", "undrain", "info"])],
960
    [], "drain|undrain|info", "Change queue properties"),
961
  'watcher': (
962
    WatcherOps,
963
    [ArgChoice(min=1, max=1, choices=["pause", "continue", "info"]),
964
     ArgSuggest(min=0, max=1, choices=["30m", "1h", "4h"])],
965
    [],
966
    "{pause <timespec>|continue|info}", "Change watcher properties"),
967
  'modify': (
968
    SetClusterParams, ARGS_NONE,
969
    [BACKEND_OPT, CP_SIZE_OPT, ENABLED_HV_OPT, HVLIST_OPT, MASTER_NETDEV_OPT,
970
     NIC_PARAMS_OPT, NOLVM_STORAGE_OPT, VG_NAME_OPT, MAINTAIN_NODE_HEALTH_OPT,
971
     UIDPOOL_OPT, ADD_UIDS_OPT, REMOVE_UIDS_OPT, DRBD_HELPER_OPT,
972
     NODRBD_STORAGE_OPT, DEFAULT_IALLOCATOR_OPT, RESERVED_LVS_OPT,
973
     DRY_RUN_OPT, PRIORITY_OPT, PREALLOC_WIPE_DISKS_OPT, NODE_PARAMS_OPT],
974
    "[opts...]",
975
    "Alters the parameters of the cluster"),
976
  "renew-crypto": (
977
    RenewCrypto, ARGS_NONE,
978
    [NEW_CLUSTER_CERT_OPT, NEW_RAPI_CERT_OPT, RAPI_CERT_OPT,
979
     NEW_CONFD_HMAC_KEY_OPT, FORCE_OPT,
980
     NEW_CLUSTER_DOMAIN_SECRET_OPT, CLUSTER_DOMAIN_SECRET_OPT],
981
    "[opts...]",
982
    "Renews cluster certificates, keys and secrets"),
983
  }
984

    
985

    
986
#: dictionary with aliases for commands
987
aliases = {
988
  'masterfailover': 'master-failover',
989
}
990

    
991

    
992
def Main():
993
  return GenericMain(commands, override={"tag_type": constants.TAG_CLUSTER},
994
                     aliases=aliases)