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

Iustin Pop's avatar
Iustin Pop committed
4
# Copyright (C) 2006, 2007, 2010, 2011, 2012 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=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
René Nussbaumer's avatar
René Nussbaumer committed
32
import itertools
Iustin Pop's avatar
Iustin Pop committed
33
34
35

from ganeti.cli import *
from ganeti import opcodes
36
from ganeti import constants
37
from ganeti import errors
38
from ganeti import utils
39
from ganeti import bootstrap
40
from ganeti import ssh
41
from ganeti import objects
42
from ganeti import uidpool
43
from ganeti import compat
René Nussbaumer's avatar
René Nussbaumer committed
44
45
46
47
48
49
50
51
52
53
54
from ganeti import netutils


ON_OPT = cli_option("--on", default=False,
                    action="store_true", dest="on",
                    help="Recover from an EPO")

GROUPS_OPT = cli_option("--groups", default=False,
                    action="store_true", dest="groups",
                    help="Arguments are node groups instead of nodes")

55
56
57
58
SHOW_MACHINE_OPT = cli_option("-M", "--show-machine-names", default=False,
                              action="store_true",
                              help="Show machine name for every line in output")

René Nussbaumer's avatar
René Nussbaumer committed
59
60
61
_EPO_PING_INTERVAL = 30 # 30 seconds between pings
_EPO_PING_TIMEOUT = 1 # 1 second
_EPO_REACHABLE_TIMEOUT = 15 * 60 # 15 minutes
Iustin Pop's avatar
Iustin Pop committed
62
63


64
@UsesRPC
Iustin Pop's avatar
Iustin Pop committed
65
66
67
def InitCluster(opts, args):
  """Initialize the cluster.

68
69
70
71
72
73
  @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
74
75

  """
76
  if not opts.lvm_storage and opts.vg_name:
77
    ToStderr("Options --no-lvm-storage and --vg-name conflict.")
78
79
80
81
82
83
    return 1

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

84
85
86
87
88
89
90
91
  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

92
93
94
95
  master_netdev = opts.master_netdev
  if master_netdev is None:
    master_netdev = constants.DEFAULT_BRIDGE

96
  hvlist = opts.enabled_hypervisors
97
98
  if hvlist is None:
    hvlist = constants.DEFAULT_ENABLED_HYPERVISOR
99
  hvlist = hvlist.split(",")
100

101
  hvparams = dict(opts.hvparams)
102
  beparams = opts.beparams
103
  nicparams = opts.nicparams
104

105
106
107
108
109
110
111
112
113
114
  diskparams = dict(opts.diskparams)

  # check the disk template types here, as we cannot rely on the type check done
  # by the opcode parameter types
  diskparams_keys = set(diskparams.keys())
  if not (diskparams_keys <= constants.DISK_TEMPLATES):
    unknown = utils.NiceSort(diskparams_keys - constants.DISK_TEMPLATES)
    ToStderr("Disk templates unknown: %s" % utils.CommaJoin(unknown))
    return 1

115
  # prepare beparams dict
116
  beparams = objects.FillDict(constants.BEC_DEFAULTS, beparams)
117
  utils.ForceDictType(beparams, constants.BES_PARAMETER_COMPAT)
118

119
120
121
122
  # prepare nicparams dict
  nicparams = objects.FillDict(constants.NICC_DEFAULTS, nicparams)
  utils.ForceDictType(nicparams, constants.NICS_PARAMETER_TYPES)

123
124
125
126
127
128
129
  # 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)

130
131
132
133
  # prepare hvparams dict
  for hv in constants.HYPER_TYPES:
    if hv not in hvparams:
      hvparams[hv] = {}
134
    hvparams[hv] = objects.FillDict(constants.HVC_DEFAULTS[hv], hvparams[hv])
135
    utils.ForceDictType(hvparams[hv], constants.HVS_PARAMETER_TYPES)
136

137
138
139
140
141
142
143
144
  # prepare diskparams dict
  for templ in constants.DISK_TEMPLATES:
    if templ not in diskparams:
      diskparams[templ] = {}
    diskparams[templ] = objects.FillDict(constants.DISK_DT_DEFAULTS[templ],
                                         diskparams[templ])
    utils.ForceDictType(diskparams[templ], constants.DISK_DT_TYPES)

145
  # prepare ipolicy dict
146
  ispecs_dts = opts.ipolicy_disk_templates # hate long var names
147
  ipolicy_raw = \
148
    objects.CreateIPolicyFromOpts(ispecs_mem_size=opts.ispecs_mem_size,
149
150
151
                                  ispecs_cpu_count=opts.ispecs_cpu_count,
                                  ispecs_disk_count=opts.ispecs_disk_count,
                                  ispecs_disk_size=opts.ispecs_disk_size,
152
                                  ispecs_nic_count=opts.ispecs_nic_count,
153
                                  ipolicy_disk_templates=ispecs_dts,
154
155
                                  fill_all=True)
  ipolicy = objects.FillIPolicy(constants.IPOLICY_DEFAULTS, ipolicy_raw)
156

157
158
159
  if opts.candidate_pool_size is None:
    opts.candidate_pool_size = constants.MASTER_POOL_SIZE_DEFAULT

160
161
162
  if opts.mac_prefix is None:
    opts.mac_prefix = constants.DEFAULT_MAC_PREFIX

163
164
165
166
  uid_pool = opts.uid_pool
  if uid_pool is not None:
    uid_pool = uidpool.ParseUidPool(uid_pool)

167
168
169
  if opts.prealloc_wipe_disks is None:
    opts.prealloc_wipe_disks = False

170
171
172
173
  external_ip_setup_script = opts.use_external_mip_script
  if external_ip_setup_script is None:
    external_ip_setup_script = False

174
175
176
177
178
179
  try:
    primary_ip_version = int(opts.primary_ip_version)
  except (ValueError, TypeError), err:
    ToStderr("Invalid primary ip version value: %s" % str(err))
    return 1

180
181
182
183
184
185
186
187
  master_netmask = opts.master_netmask
  try:
    if master_netmask is not None:
      master_netmask = int(master_netmask)
  except (ValueError, TypeError), err:
    ToStderr("Invalid master netmask value: %s" % str(err))
    return 1

188
189
190
191
192
193
194
  if opts.disk_state:
    disk_state = utils.FlatToDict(opts.disk_state)
  else:
    disk_state = {}

  hv_state = dict(opts.hv_state)

195
196
197
198
  bootstrap.InitCluster(cluster_name=args[0],
                        secondary_ip=opts.secondary_ip,
                        vg_name=vg_name,
                        mac_prefix=opts.mac_prefix,
199
                        master_netmask=master_netmask,
200
                        master_netdev=master_netdev,
201
                        file_storage_dir=opts.file_storage_dir,
202
                        shared_file_storage_dir=opts.shared_file_storage_dir,
203
204
                        enabled_hypervisors=hvlist,
                        hvparams=hvparams,
205
                        beparams=beparams,
206
                        nicparams=nicparams,
207
                        ndparams=ndparams,
208
                        diskparams=diskparams,
209
                        ipolicy=ipolicy,
210
                        candidate_pool_size=opts.candidate_pool_size,
211
                        modify_etc_hosts=opts.modify_etc_hosts,
212
                        modify_ssh_setup=opts.modify_ssh_setup,
213
                        maintain_node_health=opts.maintain_node_health,
214
                        drbd_helper=drbd_helper,
215
                        uid_pool=uid_pool,
216
                        default_iallocator=opts.default_iallocator,
217
                        primary_ip_version=primary_ip_version,
218
                        prealloc_wipe_disks=opts.prealloc_wipe_disks,
219
                        use_external_mip_script=external_ip_setup_script,
220
221
                        hv_state=hv_state,
                        disk_state=disk_state,
222
                        )
223
  op = opcodes.OpClusterPostInit()
224
  SubmitOpCode(op, opts=opts)
Iustin Pop's avatar
Iustin Pop committed
225
226
227
  return 0


228
@UsesRPC
Iustin Pop's avatar
Iustin Pop committed
229
230
231
def DestroyCluster(opts, args):
  """Destroy the cluster.

232
233
234
235
236
  @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
237

Iustin Pop's avatar
Iustin Pop committed
238
239
  """
  if not opts.yes_do_it:
240
241
    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
242
243
    return 1

244
  op = opcodes.OpClusterDestroy()
245
  master = SubmitOpCode(op, opts=opts)
Iustin Pop's avatar
Iustin Pop committed
246
247
248
  # 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
249
250
251
  return 0


252
253
254
def RenameCluster(opts, args):
  """Rename the cluster.

255
256
257
258
259
  @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
260
261

  """
262
263
264
265
266
  cl = GetClient()

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

  new_name = args[0]
267
  if not opts.force:
268
269
270
271
272
    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)
273
    if not AskUser(usertext):
274
275
      return 1

276
  op = opcodes.OpClusterRename(name=new_name)
277
278
  result = SubmitOpCode(op, opts=opts, cl=cl)

279
280
  if result:
    ToStdout("Cluster renamed from '%s' to '%s'", cluster_name, result)
281

282
283
284
  return 0


285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
def ActivateMasterIp(opts, args):
  """Activates the master IP.

  """
  op = opcodes.OpClusterActivateMasterIp()
  SubmitOpCode(op)
  return 0


def DeactivateMasterIp(opts, args):
  """Deactivates the master IP.

  """
  if not opts.confirm:
    usertext = ("This will disable the master IP. All the open connections to"
                " the master IP will be closed. To reach the master you will"
                " need to use its node IP."
                " Continue?")
    if not AskUser(usertext):
      return 1

  op = opcodes.OpClusterDeactivateMasterIp()
  SubmitOpCode(op)
  return 0


311
312
313
314
315
316
317
318
319
320
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

  """
321
  op = opcodes.OpClusterRedistConf()
322
323
324
325
  SubmitOrSend(op, opts)
  return 0


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

329
330
331
332
333
  @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
334
335

  """
336
337
  cl = GetClient()
  result = cl.QueryClusterInfo()
338
339
340
341
342
  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
343
344
345
346
347
348
  return 0


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

349
350
351
352
353
  @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
354
355

  """
356
357
  master = bootstrap.GetMaster()
  ToStdout(master)
Iustin Pop's avatar
Iustin Pop committed
358
359
  return 0

360

Guido Trotter's avatar
Guido Trotter committed
361
def _PrintGroupedParams(paramsdict, level=1, roman=False):
362
363
364
365
  """Print Grouped parameters (be, nic, disk) by group.

  @type paramsdict: dict of dicts
  @param paramsdict: {group: {param: value, ...}, ...}
366
367
  @type level: int
  @param level: Level of indention
368
369

  """
370
  indent = "  " * level
371
  for item, val in sorted(paramsdict.items()):
372
373
    if isinstance(val, dict):
      ToStdout("%s- %s:", indent, item)
Guido Trotter's avatar
Guido Trotter committed
374
375
376
      _PrintGroupedParams(val, level=level + 1, roman=roman)
    elif roman and isinstance(val, int):
      ToStdout("%s  %s: %s", indent, item, compat.TryToRoman(val))
377
378
    else:
      ToStdout("%s  %s: %s", indent, item, val)
Iustin Pop's avatar
Iustin Pop committed
379

380

Iustin Pop's avatar
Iustin Pop committed
381
382
383
def ShowClusterConfig(opts, args):
  """Shows cluster information.

384
385
386
387
388
389
  @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
390
  """
391
392
  cl = GetClient()
  result = cl.QueryClusterInfo()
Iustin Pop's avatar
Iustin Pop committed
393

394
  ToStdout("Cluster name: %s", result["name"])
395
  ToStdout("Cluster UUID: %s", result["uuid"])
Iustin Pop's avatar
Iustin Pop committed
396

397
398
399
  ToStdout("Creation time: %s", utils.FormatTime(result["ctime"]))
  ToStdout("Modification time: %s", utils.FormatTime(result["mtime"]))

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

402
403
  ToStdout("Architecture (this node): %s (%s)",
           result["architecture"][0], result["architecture"][1])
Iustin Pop's avatar
Iustin Pop committed
404

405
  if result["tags"]:
406
    tags = utils.CommaJoin(utils.NiceSort(result["tags"]))
407
408
409
410
411
  else:
    tags = "(none)"

  ToStdout("Tags: %s", tags)

412
  ToStdout("Default hypervisor: %s", result["default_hypervisor"])
413
414
  ToStdout("Enabled hypervisors: %s",
           utils.CommaJoin(result["enabled_hypervisors"]))
415

416
  ToStdout("Hypervisor parameters:")
417
  _PrintGroupedParams(result["hvparams"])
418

419
  ToStdout("OS-specific hypervisor parameters:")
420
421
  _PrintGroupedParams(result["os_hvp"])

422
423
424
  ToStdout("OS parameters:")
  _PrintGroupedParams(result["osparams"])

425
426
427
  ToStdout("Hidden OSes: %s", utils.CommaJoin(result["hidden_os"]))
  ToStdout("Blacklisted OSes: %s", utils.CommaJoin(result["blacklisted_os"]))

428
  ToStdout("Cluster parameters:")
Guido Trotter's avatar
Guido Trotter committed
429
430
431
  ToStdout("  - candidate pool size: %s",
            compat.TryToRoman(result["candidate_pool_size"],
                              convert=opts.roman_integers))
432
  ToStdout("  - master netdev: %s", result["master_netdev"])
433
  ToStdout("  - master netmask: %s", result["master_netmask"])
434
435
  ToStdout("  - use external master IP address setup script: %s",
           result["use_external_mip_script"])
436
  ToStdout("  - lvm volume group: %s", result["volume_group_name"])
437
438
439
440
441
  if result["reserved_lvs"]:
    reserved_lvs = utils.CommaJoin(result["reserved_lvs"])
  else:
    reserved_lvs = "(none)"
  ToStdout("  - lvm reserved volumes: %s", reserved_lvs)
442
  ToStdout("  - drbd usermode helper: %s", result["drbd_usermode_helper"])
443
  ToStdout("  - file storage path: %s", result["file_storage_dir"])
444
445
  ToStdout("  - shared file storage path: %s",
           result["shared_file_storage_dir"])
446
447
  ToStdout("  - maintenance of node health: %s",
           result["maintain_node_health"])
Guido Trotter's avatar
Guido Trotter committed
448
449
450
  ToStdout("  - uid pool: %s",
            uidpool.FormatUidPool(result["uid_pool"],
                                  roman=opts.roman_integers))
451
  ToStdout("  - default instance allocator: %s", result["default_iallocator"])
452
  ToStdout("  - primary ip version: %d", result["primary_ip_version"])
453
  ToStdout("  - preallocation wipe disks: %s", result["prealloc_wipe_disks"])
454
  ToStdout("  - OS search path: %s", utils.CommaJoin(constants.OS_SEARCH_PATH))
455

456
457
458
  ToStdout("Default node parameters:")
  _PrintGroupedParams(result["ndparams"], roman=opts.roman_integers)

459
  ToStdout("Default instance parameters:")
Guido Trotter's avatar
Guido Trotter committed
460
  _PrintGroupedParams(result["beparams"], roman=opts.roman_integers)
461
462

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

465
  ToStdout("Instance policy - limits for instances:")
466
  for key in constants.IPOLICY_ISPECS:
467
468
    ToStdout("  - %s", key)
    _PrintGroupedParams(result["ipolicy"][key], roman=opts.roman_integers)
469
  ToStdout("  - enabled disk templates: %s",
470
           utils.CommaJoin(result["ipolicy"][constants.IPOLICY_DTS]))
471
472
  for key in constants.IPOLICY_PARAMETERS:
    ToStdout("  - %s: %s", key, result["ipolicy"][key])
473

Iustin Pop's avatar
Iustin Pop committed
474
475
476
477
478
479
  return 0


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

480
481
482
483
484
485
  @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
486
487

  """
488
489
  filename = args[0]
  if not os.path.exists(filename):
490
491
    raise errors.OpPrereqError("No such filename '%s'" % filename,
                               errors.ECODE_INVAL)
492

Iustin Pop's avatar
Iustin Pop committed
493
494
495
496
  cl = GetClient()

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

497
  results = GetOnlineNodes(nodes=opts.nodes, cl=cl, filter_master=True,
498
499
                           secondary_ips=opts.use_replication_network,
                           nodegroup=opts.nodegroup)
Michael Hanselmann's avatar
Michael Hanselmann committed
500

Iustin Pop's avatar
Iustin Pop committed
501
  srun = ssh.SshRunner(cluster_name=cluster_name)
502
503
  for node in results:
    if not srun.CopyFileToNode(node, filename):
504
      ToStderr("Copy of file %s to node %s failed", filename, node)
505

Iustin Pop's avatar
Iustin Pop committed
506
507
508
509
510
511
  return 0


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

512
513
514
515
516
  @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
517
518

  """
Iustin Pop's avatar
Iustin Pop committed
519
  cl = GetClient()
Michael Hanselmann's avatar
Michael Hanselmann committed
520

Iustin Pop's avatar
Iustin Pop committed
521
  command = " ".join(args)
522

523
  nodes = GetOnlineNodes(nodes=opts.nodes, cl=cl, nodegroup=opts.nodegroup)
Iustin Pop's avatar
Iustin Pop committed
524
525
526

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

Iustin Pop's avatar
Iustin Pop committed
528
  srun = ssh.SshRunner(cluster_name=cluster_name)
529

Michael Hanselmann's avatar
Michael Hanselmann committed
530
  # Make sure master node is at list end
531
532
533
534
535
536
  if master_node in nodes:
    nodes.remove(master_node)
    nodes.append(master_node)

  for name in nodes:
    result = srun.Run(name, "root", command)
537
    ToStdout("------------------------------------------------")
538
539
540
541
542
543
    if opts.show_machine_names:
      for line in result.output.splitlines():
        ToStdout("%s: %s", name, line)
    else:
      ToStdout("node: %s", name)
      ToStdout("%s", result.output)
544
    ToStdout("return code = %s", result.exit_code)
545
546

  return 0
Iustin Pop's avatar
Iustin Pop committed
547
548
549
550
551


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

552
553
554
555
556
  @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
557
558

  """
Iustin Pop's avatar
Iustin Pop committed
559
  skip_checks = []
560

561
562
  if opts.skip_nplusone_mem:
    skip_checks.append(constants.VERIFY_NPLUSONE_MEM)
563

564
  cl = GetClient()
565

566
567
568
569
  op = opcodes.OpClusterVerify(verbose=opts.verbose,
                               error_codes=opts.error_codes,
                               debug_simulate_errors=opts.simulate_errors,
                               skip_checks=skip_checks,
570
                               ignore_errors=opts.ignore_errors,
571
572
                               group_name=opts.nodegroup)
  result = SubmitOpCode(op, cl=cl, opts=opts)
573

574
575
576
577
578
  # Keep track of submitted jobs
  jex = JobExecutor(cl=cl, opts=opts)

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

580
  results = jex.GetResults()
581
582
583
584
585
586
587
588
589
590
591
592

  (bad_jobs, bad_results) = \
    map(len,
        # Convert iterators to lists
        map(list,
            # Count errors
            map(compat.partial(itertools.ifilterfalse, bool),
                # Convert result to booleans in a tuple
                zip(*((job_success, len(op_results) == 1 and op_results[0])
                      for (job_success, op_results) in results)))))

  if bad_jobs == 0 and bad_results == 0:
593
    rcode = constants.EXIT_SUCCESS
Guido Trotter's avatar
Guido Trotter committed
594
  else:
595
    rcode = constants.EXIT_FAILURE
596
597
    if bad_jobs > 0:
      ToStdout("%s job(s) failed while verifying the cluster.", bad_jobs)
598
599

  return rcode
Iustin Pop's avatar
Iustin Pop committed
600
601


602
603
604
def VerifyDisks(opts, args):
  """Verify integrity of cluster disks.

605
606
607
608
609
  @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
610
611

  """
612
613
  cl = GetClient()

614
  op = opcodes.OpClusterVerifyDisks()
615

616
617
618
619
620
621
622
  result = SubmitOpCode(op, cl=cl, opts=opts)

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

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

624
  retcode = constants.EXIT_SUCCESS
625

626
627
628
629
630
631
632
  for (status, result) in jex.GetResults():
    if not status:
      ToStdout("Job failed: %s", result)
      continue

    ((bad_nodes, instances, missing), ) = result

633
634
    for node, text in bad_nodes.items():
      ToStdout("Error gathering data on node %s: %s",
635
               node, utils.SafeEncode(text[-400:]))
636
      retcode = constants.EXIT_FAILURE
637
      ToStdout("You need to fix these nodes first before fixing instances")
638

639
    for iname in instances:
640
641
      if iname in missing:
        continue
642
      op = opcodes.OpInstanceActivateDisks(instance_name=iname)
643
      try:
644
        ToStdout("Activating disks for instance '%s'", iname)
645
        SubmitOpCode(op, opts=opts, cl=cl)
646
647
648
      except errors.GenericError, err:
        nret, msg = FormatError(err)
        retcode |= nret
649
        ToStderr("Error activating disks for instance %s: %s", iname, msg)
650

651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
    if missing:
      for iname, ival in missing.iteritems():
        all_missing = compat.all(x[0] in bad_nodes for x in ival)
        if all_missing:
          ToStdout("Instance %s cannot be verified as it lives on"
                   " broken nodes", iname)
        else:
          ToStdout("Instance %s has missing logical volumes:", iname)
          ival.sort()
          for node, vol in ival:
            if node in bad_nodes:
              ToStdout("\tbroken node %s /dev/%s", node, vol)
            else:
              ToStdout("\t%s /dev/%s", node, vol)

      ToStdout("You need to replace or recreate disks for all the above"
               " instances if this message persists after fixing broken nodes.")
      retcode = constants.EXIT_FAILURE
669
670
671
672

  return retcode


673
674
675
676
677
678
679
680
681
682
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

  """
683
  op = opcodes.OpClusterRepairDiskSizes(instances=args)
684
  SubmitOpCode(op, opts=opts)
685
686


687
@UsesRPC
Iustin Pop's avatar
Iustin Pop committed
688
689
690
691
692
693
694
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.

695
696
697
698
699
700
  @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
701
  """
702
703
704
705
706
707
708
709
710
  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
711
712


Iustin Pop's avatar
Iustin Pop committed
713
714
715
716
717
718
719
720
721
722
723
724
725
726
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
727
  except Exception: # pylint: disable=W0703
Iustin Pop's avatar
Iustin Pop committed
728
729
730
    return 1


Iustin Pop's avatar
Iustin Pop committed
731
732
733
def SearchTags(opts, args):
  """Searches the tags on all the cluster.

734
735
736
737
738
739
  @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
740
  """
741
  op = opcodes.OpTagsSearch(pattern=args[0])
742
  result = SubmitOpCode(op, opts=opts)
Iustin Pop's avatar
Iustin Pop committed
743
744
745
746
747
  if not result:
    return 1
  result = list(result)
  result.sort()
  for path, tag in result:
748
    ToStdout("%s %s", path, tag)
Iustin Pop's avatar
Iustin Pop committed
749
750


751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
def _ReadAndVerifyCert(cert_filename, verify_private_key=False):
  """Reads and verifies an X509 certificate.

  @type cert_filename: string
  @param cert_filename: the path of the file containing the certificate to
                        verify encoded in PEM format
  @type verify_private_key: bool
  @param verify_private_key: whether to verify the private key in addition to
                             the public certificate
  @rtype: string
  @return: a string containing the PEM-encoded certificate.

  """
  try:
    pem = utils.ReadFile(cert_filename)
  except IOError, err:
    raise errors.X509CertError(cert_filename,
                               "Unable to read certificate: %s" % str(err))

  try:
    OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, pem)
  except Exception, err:
    raise errors.X509CertError(cert_filename,
                               "Unable to load certificate: %s" % str(err))

  if verify_private_key:
    try:
      OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, pem)
    except Exception, err:
      raise errors.X509CertError(cert_filename,
                                 "Unable to load private key: %s" % str(err))

  return pem


def _RenewCrypto(new_cluster_cert, new_rapi_cert, #pylint: disable=R0911
                 rapi_cert_filename, new_spice_cert, spice_cert_filename,
                 spice_cacert_filename, new_confd_hmac_key, new_cds,
                 cds_filename, force):
790
791
792
793
794
795
796
797
  """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
798
799
800
801
802
803
804
  @type new_spice_cert: bool
  @param new_spice_cert: Whether to generate a new SPICE certificate
  @type spice_cert_filename: string
  @param spice_cert_filename: Path to file containing new SPICE certificate
  @type spice_cacert_filename: string
  @param spice_cacert_filename: Path to file containing the certificate of the
                                CA that signed the SPICE certificate
805
806
  @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
807
808
809
810
  @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
811
812
813
814
815
  @type force: bool
  @param force: Whether to ask user for confirmation

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

Michael Hanselmann's avatar
Michael Hanselmann committed
820
821
822
823
824
825
  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

826
827
828
829
  if new_spice_cert and (spice_cert_filename or spice_cacert_filename):
    ToStderr("When using --new-spice-certificate, the --spice-certificate"
             " and --spice-ca-certificate must not be used.")
    return 1
830

831
832
833
834
  if bool(spice_cacert_filename) ^ bool(spice_cert_filename):
    ToStderr("Both --spice-certificate and --spice-ca-certificate must be"
             " specified.")
    return 1
835

836
837
838
839
840
841
842
843
844
845
  rapi_cert_pem, spice_cert_pem, spice_cacert_pem = (None, None, None)
  try:
    if rapi_cert_filename:
      rapi_cert_pem = _ReadAndVerifyCert(rapi_cert_filename, True)
    if spice_cert_filename:
      spice_cert_pem = _ReadAndVerifyCert(spice_cert_filename, True)
      spice_cacert_pem = _ReadAndVerifyCert(spice_cacert_filename)
  except errors.X509CertError, err:
    ToStderr("Unable to load X509 certificate from %s: %s", err[0], err[1])
    return 1
846

Michael Hanselmann's avatar
Michael Hanselmann committed
847
848
849
  if cds_filename:
    try:
      cds = utils.ReadFile(cds_filename)
850
    except Exception, err: # pylint: disable=W0703
Michael Hanselmann's avatar
Michael Hanselmann committed
851
852
853
854
855
856
      ToStderr("Can't load new cluster domain secret from %s: %s" %
               (cds_filename, str(err)))
      return 1
  else:
    cds = None

857
858
859
860
861
862
863
864
  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")
865
866
867
    bootstrap.GenerateClusterCrypto(new_cluster_cert,
                                    new_rapi_cert,
                                    new_spice_cert,
868
                                    new_confd_hmac_key,
Michael Hanselmann's avatar
Michael Hanselmann committed
869
870
                                    new_cds,
                                    rapi_cert_pem=rapi_cert_pem,
871
872
                                    spice_cert_pem=spice_cert_pem,
                                    spice_cacert_pem=spice_cacert_pem,
Michael Hanselmann's avatar
Michael Hanselmann committed
873
                                    cds=cds)
874
875
876
877

    files_to_copy = []

    if new_cluster_cert:
878
      files_to_copy.append(constants.NODED_CERT_FILE)
879
880
881
882

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

883
884
885
886
    if new_spice_cert or spice_cert_pem:
      files_to_copy.append(constants.SPICE_CERT_FILE)
      files_to_copy.append(constants.SPICE_CACERT_FILE)

887
888
    if new_confd_hmac_key:
      files_to_copy.append(constants.CONFD_HMAC_KEY)
889

Michael Hanselmann's avatar
Michael Hanselmann committed
890
891
892
    if new_cds or cds:
      files_to_copy.append(constants.CLUSTER_DOMAIN_SECRET_FILE)

893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
    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,
915
916
917
                      opts.new_spice_cert,
                      opts.spice_cert,
                      opts.spice_cacert,
918
                      opts.new_confd_hmac_key,
Michael Hanselmann's avatar
Michael Hanselmann committed
919
920
                      opts.new_cluster_domain_secret,
                      opts.cluster_domain_secret,
921
922
923
                      opts.force)


924
925
926
def SetClusterParams(opts, args):
  """Modify the cluster.

927
928
929
930
931
  @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
932
933

  """
934
  if not (not opts.lvm_storage or opts.vg_name or
935
          not opts.drbd_storage or opts.drbd_helper or
936
          opts.enabled_hypervisors or opts.hvparams or
937
938
          opts.beparams or opts.nicparams or
          opts.ndparams or opts.diskparams or
939
          opts.candidate_pool_size is not None or
940
          opts.uid_pool is not None or
941
942
          opts.maintain_node_health is not None or
          opts.add_uids is not None or
943
          opts.remove_uids is not None or
944
          opts.default_iallocator is not None or
945
          opts.reserved_lvs is not None or
946
          opts.master_netdev is not None or
947
          opts.master_netmask is not None or
948
          opts.use_external_mip_script is not None or
949
950
          opts.prealloc_wipe_disks is not None or
          opts.hv_state or
951
952
953
954
955
956
          opts.disk_state or
          opts.ispecs_mem_size is not None or
          opts.ispecs_cpu_count is not None or
          opts.ispecs_disk_count is not None or
          opts.ispecs_disk_size is not None or
          opts.ispecs_nic_count is not None):
957
    ToStderr("Please give at least one of the parameters.")
958
959
960
961
    return 1

  vg_name = opts.vg_name
  if not opts.lvm_storage and opts.vg_name:
962
    ToStderr("Options --no-lvm-storage and --vg-name conflict.")
963
    return 1
964
965
966

  if not opts.lvm_storage:
    vg_name = ""
967

968
969
970
971
972
973
974
975
  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 = ""

976
977
978
979
  hvlist = opts.enabled_hypervisors
  if hvlist is not None:
    hvlist = hvlist.split(",")

980
981
  # a list of (name, dict) we can pass directly to dict() (or [])
  hvparams = dict(opts.hvparams)
Iustin Pop's avatar
Iustin Pop committed
982
  for hv_params in hvparams.values():
983
    utils.ForceDictType(hv_params, constants.HVS_PARAMETER_TYPES)
984

985
986
  diskparams = dict(opts.diskparams)

Iustin Pop's avatar
Iustin Pop committed
987
  for dt_params in diskparams.values():
988
989
    utils.ForceDictType(dt_params, constants.DISK_DT_TYPES)

990
  beparams = opts.beparams
991
  utils.ForceDictType(beparams, constants.BES_PARAMETER_COMPAT)
992

993
994
995
  nicparams = opts.nicparams
  utils.ForceDictType(nicparams, constants.NICS_PARAMETER_TYPES)

996
997
998
  ndparams = opts.ndparams
  if ndparams is not None:
    utils.ForceDictType(ndparams, constants.NDS_PARAMETER_TYPES)
999

1000
  ispecs_dts = opts.ipolicy_disk_templates
1001
1002
1003
1004
1005
  ipolicy = \
    objects.CreateIPolicyFromOpts(ispecs_mem_size=opts.ispecs_mem_size,
                                  ispecs_cpu_count=opts.ispecs_cpu_count,
                                  ispecs_disk_count=opts.ispecs_disk_count,
                                  ispecs_disk_size=opts.ispecs_disk_size,
1006
                                  ispecs_nic_count=opts.ispecs_nic_count,
1007
                                  ipolicy_disk_templates=ispecs_dts)
1008

1009
1010
  mnh = opts.maintain_node_health

1011
1012
1013
1014
  uid_pool = opts.uid_pool
  if uid_pool is not None:
    uid_pool = uidpool.ParseUidPool(uid_pool)

1015
1016
1017
1018
1019
1020
1021
1022
  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)

1023
1024
1025
1026
1027
1028
  if opts.reserved_lvs is not None:
    if opts.reserved_lvs == "":
      opts.reserved_lvs = []
    else:
      opts.reserved_lvs = utils.UnescapeAndSplit(opts.reserved_lvs, sep=",")

1029
1030
1031
1032
1033
1034
1035
  if opts.master_netmask is not None:
    try:
      opts.master_netmask = int(opts.master_netmask)
    except ValueError:
      ToStderr("The --master-netmask option expects an int parameter.")
      return 1

1036
1037
  ext_ip_script = opts.use_external_mip_script

1038
1039
1040
1041
1042
1043
1044
  if opts.disk_state:
    disk_state = utils.FlatToDict(opts.disk_state)
  else:
    disk_state = {}

  hv_state = dict(opts.hv_state)

1045
  op = opcodes.OpClusterSetParams(vg_name=vg_name,
1046
                                  drbd_helper=drbd_helper,
1047
1048
                                  enabled_hypervisors=hvlist,
                                  hvparams=hvparams,
1049
                                  os_hvp=None,
1050
                                  beparams=beparams,
1051
                                  nicparams=nicparams,
1052
                                  ndparams=ndparams,
1053
                                  diskparams=diskparams,
1054
                                  ipolicy=ipolicy,
1055
                                  candidate_pool_size=opts.candidate_pool_size,
1056
                                  maintain_node_health=mnh,
1057
1058
                                  uid_pool=uid_pool,
                                  add_uids=add_uids,
1059
                                  remove_uids=remove_uids,
1060
                                  default_iallocator=opts.default_iallocator,
1061
                                  prealloc_wipe_disks=opts.prealloc_wipe_disks,
1062
                                  master_netdev=opts.master_netdev,
1063
                                  master_netmask=opts.master_netmask,
1064
1065
                                  reserved_lvs=opts.reserved_lvs,
                                  use_external_mip_script=ext_ip_script,
1066
1067
                                  hv_state=hv_state,
                                  disk_state=disk_state,
1068
                                  )
1069
  SubmitOpCode(op, opts=opts)
Manuel Franceschini's avatar