gnt_cluster.py 30.9 KB
Newer Older
1
#
Iustin Pop's avatar
Iustin Pop committed
2
3
#

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

Iustin Pop's avatar
Iustin Pop committed
21
"""Cluster related commands"""
Iustin Pop's avatar
Iustin Pop committed
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)
Iustin Pop's avatar
Iustin Pop committed
27
# C0103: Invalid name gnt-cluster
28

29
import os.path
30
import time
31
import OpenSSL
Iustin Pop's avatar
Iustin Pop committed
32
33
34

from ganeti.cli import *
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
Iustin Pop's avatar
Iustin Pop committed
43
44


45
@UsesRPC
Iustin Pop's avatar
Iustin Pop committed
46
47
48
def InitCluster(opts, args):
  """Initialize the cluster.

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

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

  vg_name = opts.vg_name
  if opts.lvm_storage and not opts.vg_name:
    vg_name = constants.DEFAULT_VG

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

  drbd_helper = opts.drbd_helper
  if opts.drbd_storage and not opts.drbd_helper:
    drbd_helper = constants.DEFAULT_DRBD_HELPER

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

77
  hvlist = opts.enabled_hypervisors
78
79
  if hvlist is None:
    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
91
92
93
  # prepare nicparams dict
  nicparams = objects.FillDict(constants.NICC_DEFAULTS, nicparams)
  utils.ForceDictType(nicparams, constants.NICS_PARAMETER_TYPES)

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

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

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

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

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

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

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

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


153
@UsesRPC
Iustin Pop's avatar
Iustin Pop committed
154
155
156
def DestroyCluster(opts, args):
  """Destroy the cluster.

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

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

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


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

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

  """
187
188
189
190
191
  cl = GetClient()

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

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

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

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

207
208
209
  return 0


210
211
212
213
214
215
216
217
218
219
def RedistributeConfig(opts, args):
  """Forces push of the cluster configuration.

  @param opts: the command line options selected by the user
  @type args: list
  @param args: empty list
  @rtype: int
  @return: the desired exit code

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


Iustin Pop's avatar
Iustin Pop committed
225
226
227
def ShowClusterVersion(opts, args):
  """Write version of ganeti software to the standard output.

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

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


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

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

  """
255
256
  master = bootstrap.GetMaster()
  ToStdout(master)
Iustin Pop's avatar
Iustin Pop committed
257
258
  return 0

259

Guido Trotter's avatar
Guido Trotter committed
260
def _PrintGroupedParams(paramsdict, level=1, roman=False):
261
262
263
264
  """Print Grouped parameters (be, nic, disk) by group.

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

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

279

Iustin Pop's avatar
Iustin Pop committed
280
281
282
def ShowClusterConfig(opts, args):
  """Shows cluster information.

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

Iustin Pop's avatar
Iustin Pop committed
289
  """
290
291
  cl = GetClient()
  result = cl.QueryClusterInfo()
Iustin Pop's avatar
Iustin Pop committed
292

293
  ToStdout("Cluster name: %s", result["name"])
294
  ToStdout("Cluster UUID: %s", result["uuid"])
Iustin Pop's avatar
Iustin Pop committed
295

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

299
  ToStdout("Master node: %s", result["master"])
Iustin Pop's avatar
Iustin Pop committed
300

301
302
  ToStdout("Architecture (this node): %s (%s)",
           result["architecture"][0], result["architecture"][1])
Iustin Pop's avatar
Iustin Pop committed
303

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

  ToStdout("Tags: %s", tags)

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

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

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

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

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

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

349
350
351
  ToStdout("Default node parameters:")
  _PrintGroupedParams(result["ndparams"], roman=opts.roman_integers)

352
  ToStdout("Default instance parameters:")
Guido Trotter's avatar
Guido Trotter committed
353
  _PrintGroupedParams(result["beparams"], roman=opts.roman_integers)
354
355

  ToStdout("Default nic parameters:")
Guido Trotter's avatar
Guido Trotter committed
356
  _PrintGroupedParams(result["nicparams"], roman=opts.roman_integers)
357

Iustin Pop's avatar
Iustin Pop committed
358
359
360
361
362
363
  return 0


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

364
365
366
367
368
369
  @param opts: the command line options selected by the user
  @type args: list
  @param args: should contain only one element, the path of
      the file to be copied
  @rtype: int
  @return: the desired exit code
Iustin Pop's avatar
Iustin Pop committed
370
371

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

Iustin Pop's avatar
Iustin Pop committed
377
378
379
380
  cl = GetClient()

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

381
382
  results = GetOnlineNodes(nodes=opts.nodes, cl=cl, filter_master=True,
                           secondary_ips=opts.use_replication_network)
Michael Hanselmann's avatar
Michael Hanselmann committed
383

Iustin Pop's avatar
Iustin Pop committed
384
  srun = ssh.SshRunner(cluster_name=cluster_name)
385
386
  for node in results:
    if not srun.CopyFileToNode(node, filename):
387
      ToStderr("Copy of file %s to node %s failed", filename, node)
388

Iustin Pop's avatar
Iustin Pop committed
389
390
391
392
393
394
  return 0


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

395
396
397
398
399
  @param opts: the command line options selected by the user
  @type args: list
  @param args: should contain the command to be run and its arguments
  @rtype: int
  @return: the desired exit code
Iustin Pop's avatar
Iustin Pop committed
400
401

  """
Iustin Pop's avatar
Iustin Pop committed
402
  cl = GetClient()
Michael Hanselmann's avatar
Michael Hanselmann committed
403

Iustin Pop's avatar
Iustin Pop committed
404
  command = " ".join(args)
405
406

  nodes = GetOnlineNodes(nodes=opts.nodes, cl=cl)
Iustin Pop's avatar
Iustin Pop committed
407
408
409

  cluster_name, master_node = cl.QueryConfigValues(["cluster_name",
                                                    "master_node"])
410

Iustin Pop's avatar
Iustin Pop committed
411
  srun = ssh.SshRunner(cluster_name=cluster_name)
412

Michael Hanselmann's avatar
Michael Hanselmann committed
413
  # Make sure master node is at list end
414
415
416
417
418
419
  if master_node in nodes:
    nodes.remove(master_node)
    nodes.append(master_node)

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

  return 0
Iustin Pop's avatar
Iustin Pop committed
426
427
428
429
430


def VerifyCluster(opts, args):
  """Verify integrity of cluster, performing various test on nodes.

431
432
433
434
435
  @param opts: the command line options selected by the user
  @type args: list
  @param args: should be an empty list
  @rtype: int
  @return: the desired exit code
Iustin Pop's avatar
Iustin Pop committed
436
437

  """
Iustin Pop's avatar
Iustin Pop committed
438
  skip_checks = []
439
440
  if opts.skip_nplusone_mem:
    skip_checks.append(constants.VERIFY_NPLUSONE_MEM)
441
  op = opcodes.OpClusterVerify(skip_checks=skip_checks,
442
                               verbose=opts.verbose,
443
444
                               error_codes=opts.error_codes,
                               debug_simulate_errors=opts.simulate_errors)
445
  if SubmitOpCode(op, opts=opts):
446
447
448
    return 0
  else:
    return 1
Iustin Pop's avatar
Iustin Pop committed
449
450


451
452
453
def VerifyDisks(opts, args):
  """Verify integrity of cluster disks.

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

  """
461
462
  cl = GetClient()

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

468
  bad_nodes, instances, missing = result
469

470
  retcode = constants.EXIT_SUCCESS
471

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

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

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

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

  return retcode


514
515
516
517
518
519
520
521
522
523
def RepairDiskSizes(opts, args):
  """Verify sizes of cluster disks.

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

  """
524
  op = opcodes.OpClusterRepairDiskSizes(instances=args)
525
  SubmitOpCode(op, opts=opts)
526
527


528
@UsesRPC
Iustin Pop's avatar
Iustin Pop committed
529
530
531
532
533
534
535
def MasterFailover(opts, args):
  """Failover the master node.

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

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

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

  return bootstrap.MasterFailover(no_voting=opts.no_voting)
Iustin Pop's avatar
Iustin Pop committed
552
553


Iustin Pop's avatar
Iustin Pop committed
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
def MasterPing(opts, args):
  """Checks if the master is alive.

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

  """
  try:
    cl = GetClient()
    cl.QueryClusterInfo()
    return 0
  except Exception: # pylint: disable-msg=W0703
    return 1


Iustin Pop's avatar
Iustin Pop committed
572
573
574
def SearchTags(opts, args):
  """Searches the tags on all the cluster.

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

Iustin Pop's avatar
Iustin Pop committed
581
  """
582
  op = opcodes.OpTagsSearch(pattern=args[0])
583
  result = SubmitOpCode(op, opts=opts)
Iustin Pop's avatar
Iustin Pop committed
584
585
586
587
588
  if not result:
    return 1
  result = list(result)
  result.sort()
  for path, tag in result:
589
    ToStdout("%s %s", path, tag)
Iustin Pop's avatar
Iustin Pop committed
590
591


592
def _RenewCrypto(new_cluster_cert, new_rapi_cert, rapi_cert_filename,
Michael Hanselmann's avatar
Michael Hanselmann committed
593
594
                 new_confd_hmac_key, new_cds, cds_filename,
                 force):
595
596
597
598
599
600
601
602
  """Renews cluster certificates, keys and secrets.

  @type new_cluster_cert: bool
  @param new_cluster_cert: Whether to generate a new cluster certificate
  @type new_rapi_cert: bool
  @param new_rapi_cert: Whether to generate a new RAPI certificate
  @type rapi_cert_filename: string
  @param rapi_cert_filename: Path to file containing new RAPI certificate
603
604
  @type new_confd_hmac_key: bool
  @param new_confd_hmac_key: Whether to generate a new HMAC key
Michael Hanselmann's avatar
Michael Hanselmann committed
605
606
607
608
  @type new_cds: bool
  @param new_cds: Whether to generate a new cluster domain secret
  @type cds_filename: string
  @param cds_filename: Path to file containing new cluster domain secret
609
610
611
612
613
614
615
616
617
  @type force: bool
  @param force: Whether to ask user for confirmation

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

Michael Hanselmann's avatar
Michael Hanselmann committed
618
619
620
621
622
623
  if new_cds and cds_filename:
    ToStderr("Only one of the --new-cluster-domain-secret and"
             " --cluster-domain-secret options can be specified at"
             " the same time.")
    return 1

624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
  if rapi_cert_filename:
    # Read and verify new certificate
    try:
      rapi_cert_pem = utils.ReadFile(rapi_cert_filename)

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

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

  else:
    rapi_cert_pem = None

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

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

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

    files_to_copy = []

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

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

678
679
    if new_confd_hmac_key:
      files_to_copy.append(constants.CONFD_HMAC_KEY)
680

Michael Hanselmann's avatar
Michael Hanselmann committed
681
682
683
    if new_cds or cds:
      files_to_copy.append(constants.CLUSTER_DOMAIN_SECRET_FILE)

684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
    if files_to_copy:
      for node_name in ctx.nonmaster_nodes:
        ctx.feedback_fn("Copying %s to %s" %
                        (", ".join(files_to_copy), node_name))
        for file_name in files_to_copy:
          ctx.ssh.CopyFileToNode(node_name, file_name)

  RunWhileClusterStopped(ToStdout, _RenewCryptoInner)

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

  return 0


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

  """
  return _RenewCrypto(opts.new_cluster_cert,
                      opts.new_rapi_cert,
                      opts.rapi_cert,
706
                      opts.new_confd_hmac_key,
Michael Hanselmann's avatar
Michael Hanselmann committed
707
708
                      opts.new_cluster_domain_secret,
                      opts.cluster_domain_secret,
709
710
711
                      opts.force)


712
713
714
def SetClusterParams(opts, args):
  """Modify the cluster.

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

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

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

  if not opts.lvm_storage:
    vg_name = ""
745

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

  if not opts.drbd_storage:
    drbd_helper = ""

754
755
756
757
  hvlist = opts.enabled_hypervisors
  if hvlist is not None:
    hvlist = hvlist.split(",")

758
759
  # a list of (name, dict) we can pass directly to dict() (or [])
  hvparams = dict(opts.hvparams)
Iustin Pop's avatar
Iustin Pop committed
760
  for hv_params in hvparams.values():
761
    utils.ForceDictType(hv_params, constants.HVS_PARAMETER_TYPES)
762
763

  beparams = opts.beparams
764
  utils.ForceDictType(beparams, constants.BES_PARAMETER_TYPES)
765

766
767
768
  nicparams = opts.nicparams
  utils.ForceDictType(nicparams, constants.NICS_PARAMETER_TYPES)

769
770
771
  ndparams = opts.ndparams
  if ndparams is not None:
    utils.ForceDictType(ndparams, constants.NDS_PARAMETER_TYPES)
772

773
774
  mnh = opts.maintain_node_health

775
776
777
778
  uid_pool = opts.uid_pool
  if uid_pool is not None:
    uid_pool = uidpool.ParseUidPool(uid_pool)

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

  remove_uids = opts.remove_uids
  if remove_uids is not None:
    remove_uids = uidpool.ParseUidPool(remove_uids)

787
788
789
790
791
792
  if opts.reserved_lvs is not None:
    if opts.reserved_lvs == "":
      opts.reserved_lvs = []
    else:
      opts.reserved_lvs = utils.UnescapeAndSplit(opts.reserved_lvs, sep=",")

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


814
815
816
def QueueOps(opts, args):
  """Queue operations.

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

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

840
841
  return 0

842

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


850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
def WatcherOps(opts, args):
  """Watcher operations.

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

  """
  command = args[0]
  client = GetClient()

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

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

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

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

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

  return 0


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

982

983
984
985
986
987
988
#: dictionary with aliases for commands
aliases = {
  'masterfailover': 'master-failover',
}


989
990
991
def Main():
  return GenericMain(commands, override={"tag_type": constants.TAG_CLUSTER},
                     aliases=aliases)