Statistics
| Branch: | Tag: | Revision:

root / lib / bootstrap.py @ 60cc531d

History | View | Annotate | Download (42.5 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007, 2008, 2010, 2011, 2012 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

    
22
"""Functions to bootstrap a new cluster.
23

24
"""
25

    
26
import os
27
import os.path
28
import re
29
import logging
30
import time
31
import tempfile
32

    
33
from ganeti.cmdlib import cluster
34
import ganeti.rpc.node as rpc
35
from ganeti import ssh
36
from ganeti import utils
37
from ganeti import errors
38
from ganeti import config
39
from ganeti import constants
40
from ganeti import objects
41
from ganeti import ssconf
42
from ganeti import serializer
43
from ganeti import hypervisor
44
from ganeti.storage import drbd
45
from ganeti.storage import filestorage
46
from ganeti import netutils
47
from ganeti import luxi
48
from ganeti import jstore
49
from ganeti import pathutils
50

    
51

    
52
# ec_id for InitConfig's temporary reservation manager
53
_INITCONF_ECID = "initconfig-ecid"
54

    
55
#: After how many seconds daemon must be responsive
56
_DAEMON_READY_TIMEOUT = 10.0
57

    
58

    
59
def _InitSSHSetup():
60
  """Setup the SSH configuration for the cluster.
61

62
  This generates a dsa keypair for root, adds the pub key to the
63
  permitted hosts and adds the hostkey to its own known hosts.
64

65
  """
66
  priv_key, pub_key, auth_keys = ssh.GetUserFiles(constants.SSH_LOGIN_USER)
67

    
68
  for name in priv_key, pub_key:
69
    if os.path.exists(name):
70
      utils.CreateBackup(name)
71
    utils.RemoveFile(name)
72

    
73
  result = utils.RunCmd(["ssh-keygen", "-t", "dsa",
74
                         "-f", priv_key,
75
                         "-q", "-N", ""])
76
  if result.failed:
77
    raise errors.OpExecError("Could not generate ssh keypair, error %s" %
78
                             result.output)
79

    
80
  utils.AddAuthorizedKey(auth_keys, utils.ReadFile(pub_key))
81

    
82

    
83
def GenerateHmacKey(file_name):
84
  """Writes a new HMAC key.
85

86
  @type file_name: str
87
  @param file_name: Path to output file
88

89
  """
90
  utils.WriteFile(file_name, data="%s\n" % utils.GenerateSecret(), mode=0400,
91
                  backup=True)
92

    
93

    
94
def GenerateClusterCrypto(new_cluster_cert, new_rapi_cert, new_spice_cert,
95
                          new_confd_hmac_key, new_cds, new_node_client_cert,
96
                          rapi_cert_pem=None, spice_cert_pem=None,
97
                          spice_cacert_pem=None, cds=None,
98
                          nodecert_file=pathutils.NODED_CERT_FILE,
99
                          nodecert_client_file=pathutils.NODED_CLIENT_CERT_FILE,
100
                          rapicert_file=pathutils.RAPI_CERT_FILE,
101
                          spicecert_file=pathutils.SPICE_CERT_FILE,
102
                          spicecacert_file=pathutils.SPICE_CACERT_FILE,
103
                          hmackey_file=pathutils.CONFD_HMAC_KEY,
104
                          cds_file=pathutils.CLUSTER_DOMAIN_SECRET_FILE):
105
  """Updates the cluster certificates, keys and secrets.
106

107
  @type new_cluster_cert: bool
108
  @param new_cluster_cert: Whether to generate a new cluster certificate
109
  @type new_rapi_cert: bool
110
  @param new_rapi_cert: Whether to generate a new RAPI certificate
111
  @type new_spice_cert: bool
112
  @param new_spice_cert: Whether to generate a new SPICE certificate
113
  @type new_confd_hmac_key: bool
114
  @param new_confd_hmac_key: Whether to generate a new HMAC key
115
  @type new_cds: bool
116
  @param new_cds: Whether to generate a new cluster domain secret
117
  @type new_node_client_cert: bool
118
  @param new_node_client_cert: Whether to generate a new node (SSL)
119
    client certificate
120
  @type rapi_cert_pem: string
121
  @param rapi_cert_pem: New RAPI certificate in PEM format
122
  @type spice_cert_pem: string
123
  @param spice_cert_pem: New SPICE certificate in PEM format
124
  @type spice_cacert_pem: string
125
  @param spice_cacert_pem: Certificate of the CA that signed the SPICE
126
                           certificate, in PEM format
127
  @type cds: string
128
  @param cds: New cluster domain secret
129
  @type nodecert_file: string
130
  @param nodecert_file: optional override of the node cert file path
131
  @type nodecert_client_file: string
132
  @param nodecert_client_file: optional override of the node client certificate
133
    file path
134
  @type rapicert_file: string
135
  @param rapicert_file: optional override of the rapi cert file path
136
  @type spicecert_file: string
137
  @param spicecert_file: optional override of the spice cert file path
138
  @type spicecacert_file: string
139
  @param spicecacert_file: optional override of the spice CA cert file path
140
  @type hmackey_file: string
141
  @param hmackey_file: optional override of the hmac key file path
142

143
  """
144
  # noded SSL certificate
145
  utils.GenerateNewSslCert(
146
    new_cluster_cert, nodecert_file,
147
    "Generating new cluster certificate at %s" % nodecert_file)
148

    
149
  # noded client SSL certificate (to be used only by this very node)
150
  utils.GenerateNewSslCert(
151
    new_node_client_cert, nodecert_client_file,
152
    "Generating new node client certificate at %s" % nodecert_client_file)
153

    
154
  # confd HMAC key
155
  if new_confd_hmac_key or not os.path.exists(hmackey_file):
156
    logging.debug("Writing new confd HMAC key to %s", hmackey_file)
157
    GenerateHmacKey(hmackey_file)
158

    
159
  if rapi_cert_pem:
160
    # Assume rapi_pem contains a valid PEM-formatted certificate and key
161
    logging.debug("Writing RAPI certificate at %s", rapicert_file)
162
    utils.WriteFile(rapicert_file, data=rapi_cert_pem, backup=True)
163

    
164
  else:
165
    utils.GenerateNewSslCert(
166
      new_rapi_cert, rapicert_file,
167
      "Generating new RAPI certificate at %s" % rapicert_file)
168

    
169
  # SPICE
170
  spice_cert_exists = os.path.exists(spicecert_file)
171
  spice_cacert_exists = os.path.exists(spicecacert_file)
172
  if spice_cert_pem:
173
    # spice_cert_pem implies also spice_cacert_pem
174
    logging.debug("Writing SPICE certificate at %s", spicecert_file)
175
    utils.WriteFile(spicecert_file, data=spice_cert_pem, backup=True)
176
    logging.debug("Writing SPICE CA certificate at %s", spicecacert_file)
177
    utils.WriteFile(spicecacert_file, data=spice_cacert_pem, backup=True)
178
  elif new_spice_cert or not spice_cert_exists:
179
    if spice_cert_exists:
180
      utils.CreateBackup(spicecert_file)
181
    if spice_cacert_exists:
182
      utils.CreateBackup(spicecacert_file)
183

    
184
    logging.debug("Generating new self-signed SPICE certificate at %s",
185
                  spicecert_file)
186
    (_, cert_pem) = utils.GenerateSelfSignedSslCert(spicecert_file)
187

    
188
    # Self-signed certificate -> the public certificate is also the CA public
189
    # certificate
190
    logging.debug("Writing the public certificate to %s",
191
                  spicecert_file)
192
    utils.io.WriteFile(spicecacert_file, mode=0400, data=cert_pem)
193

    
194
  # Cluster domain secret
195
  if cds:
196
    logging.debug("Writing cluster domain secret to %s", cds_file)
197
    utils.WriteFile(cds_file, data=cds, backup=True)
198

    
199
  elif new_cds or not os.path.exists(cds_file):
200
    logging.debug("Generating new cluster domain secret at %s", cds_file)
201
    GenerateHmacKey(cds_file)
202

    
203

    
204
def _InitGanetiServerSetup(master_name):
205
  """Setup the necessary configuration for the initial node daemon.
206

207
  This creates the nodepass file containing the shared password for
208
  the cluster, generates the SSL certificate and starts the node daemon.
209

210
  @type master_name: str
211
  @param master_name: Name of the master node
212

213
  """
214
  # Generate cluster secrets
215
  GenerateClusterCrypto(True, False, False, False, False, True)
216

    
217
  result = utils.RunCmd([pathutils.DAEMON_UTIL, "start", constants.NODED])
218
  if result.failed:
219
    raise errors.OpExecError("Could not start the node daemon, command %s"
220
                             " had exitcode %s and error %s" %
221
                             (result.cmd, result.exit_code, result.output))
222

    
223
  _WaitForNodeDaemon(master_name)
224

    
225

    
226
def _WaitForNodeDaemon(node_name):
227
  """Wait for node daemon to become responsive.
228

229
  """
230
  def _CheckNodeDaemon():
231
    # Pylint bug <http://www.logilab.org/ticket/35642>
232
    # pylint: disable=E1101
233
    result = rpc.BootstrapRunner().call_version([node_name])[node_name]
234
    if result.fail_msg:
235
      raise utils.RetryAgain()
236

    
237
  try:
238
    utils.Retry(_CheckNodeDaemon, 1.0, _DAEMON_READY_TIMEOUT)
239
  except utils.RetryTimeout:
240
    raise errors.OpExecError("Node daemon on %s didn't answer queries within"
241
                             " %s seconds" % (node_name, _DAEMON_READY_TIMEOUT))
242

    
243

    
244
def _WaitForMasterDaemon():
245
  """Wait for master daemon to become responsive.
246

247
  """
248
  def _CheckMasterDaemon():
249
    try:
250
      cl = luxi.Client()
251
      (cluster_name, ) = cl.QueryConfigValues(["cluster_name"])
252
    except Exception:
253
      raise utils.RetryAgain()
254

    
255
    logging.debug("Received cluster name %s from master", cluster_name)
256

    
257
  try:
258
    utils.Retry(_CheckMasterDaemon, 1.0, _DAEMON_READY_TIMEOUT)
259
  except utils.RetryTimeout:
260
    raise errors.OpExecError("Master daemon didn't answer queries within"
261
                             " %s seconds" % _DAEMON_READY_TIMEOUT)
262

    
263

    
264
def _WaitForSshDaemon(hostname, port, family):
265
  """Wait for SSH daemon to become responsive.
266

267
  """
268
  hostip = netutils.GetHostname(name=hostname, family=family).ip
269

    
270
  def _CheckSshDaemon():
271
    if netutils.TcpPing(hostip, port, timeout=1.0, live_port_needed=True):
272
      logging.debug("SSH daemon on %s:%s (IP address %s) has become"
273
                    " responsive", hostname, port, hostip)
274
    else:
275
      raise utils.RetryAgain()
276

    
277
  try:
278
    utils.Retry(_CheckSshDaemon, 1.0, _DAEMON_READY_TIMEOUT)
279
  except utils.RetryTimeout:
280
    raise errors.OpExecError("SSH daemon on %s:%s (IP address %s) didn't"
281
                             " become responsive within %s seconds" %
282
                             (hostname, port, hostip, _DAEMON_READY_TIMEOUT))
283

    
284

    
285
def RunNodeSetupCmd(cluster_name, node, basecmd, debug, verbose,
286
                    use_cluster_key, ask_key, strict_host_check,
287
                    port, data):
288
  """Runs a command to configure something on a remote machine.
289

290
  @type cluster_name: string
291
  @param cluster_name: Cluster name
292
  @type node: string
293
  @param node: Node name
294
  @type basecmd: string
295
  @param basecmd: Base command (path on the remote machine)
296
  @type debug: bool
297
  @param debug: Enable debug output
298
  @type verbose: bool
299
  @param verbose: Enable verbose output
300
  @type use_cluster_key: bool
301
  @param use_cluster_key: See L{ssh.SshRunner.BuildCmd}
302
  @type ask_key: bool
303
  @param ask_key: See L{ssh.SshRunner.BuildCmd}
304
  @type strict_host_check: bool
305
  @param strict_host_check: See L{ssh.SshRunner.BuildCmd}
306
  @type port: int
307
  @param port: The SSH port of the remote machine or None for the default
308
  @param data: JSON-serializable input data for script (passed to stdin)
309

310
  """
311
  cmd = [basecmd]
312

    
313
  # Pass --debug/--verbose to the external script if set on our invocation
314
  if debug:
315
    cmd.append("--debug")
316

    
317
  if verbose:
318
    cmd.append("--verbose")
319

    
320
  if port is None:
321
    port = netutils.GetDaemonPort(constants.SSH)
322

    
323
  family = ssconf.SimpleStore().GetPrimaryIPFamily()
324
  srun = ssh.SshRunner(cluster_name,
325
                       ipv6=(family == netutils.IP6Address.family))
326
  scmd = srun.BuildCmd(node, constants.SSH_LOGIN_USER,
327
                       utils.ShellQuoteArgs(cmd),
328
                       batch=False, ask_key=ask_key, quiet=False,
329
                       strict_host_check=strict_host_check,
330
                       use_cluster_key=use_cluster_key,
331
                       port=port)
332

    
333
  tempfh = tempfile.TemporaryFile()
334
  try:
335
    tempfh.write(serializer.DumpJson(data))
336
    tempfh.seek(0)
337

    
338
    result = utils.RunCmd(scmd, interactive=True, input_fd=tempfh)
339
  finally:
340
    tempfh.close()
341

    
342
  if result.failed:
343
    raise errors.OpExecError("Command '%s' failed: %s" %
344
                             (result.cmd, result.fail_reason))
345

    
346
  _WaitForSshDaemon(node, port, family)
347

    
348

    
349
def _InitFileStorageDir(file_storage_dir):
350
  """Initialize if needed the file storage.
351

352
  @param file_storage_dir: the user-supplied value
353
  @return: either empty string (if file storage was disabled at build
354
      time) or the normalized path to the storage directory
355

356
  """
357
  file_storage_dir = os.path.normpath(file_storage_dir)
358

    
359
  if not os.path.isabs(file_storage_dir):
360
    raise errors.OpPrereqError("File storage directory '%s' is not an absolute"
361
                               " path" % file_storage_dir, errors.ECODE_INVAL)
362

    
363
  if not os.path.exists(file_storage_dir):
364
    try:
365
      os.makedirs(file_storage_dir, 0750)
366
    except OSError, err:
367
      raise errors.OpPrereqError("Cannot create file storage directory"
368
                                 " '%s': %s" % (file_storage_dir, err),
369
                                 errors.ECODE_ENVIRON)
370

    
371
  if not os.path.isdir(file_storage_dir):
372
    raise errors.OpPrereqError("The file storage directory '%s' is not"
373
                               " a directory." % file_storage_dir,
374
                               errors.ECODE_ENVIRON)
375

    
376
  return file_storage_dir
377

    
378

    
379
def _PrepareFileBasedStorage(
380
    enabled_disk_templates, file_storage_dir,
381
    default_dir, file_disk_template,
382
    init_fn=_InitFileStorageDir, acceptance_fn=None):
383
  """Checks if a file-base storage type is enabled and inits the dir.
384

385
  @type enabled_disk_templates: list of string
386
  @param enabled_disk_templates: list of enabled disk templates
387
  @type file_storage_dir: string
388
  @param file_storage_dir: the file storage directory
389
  @type default_dir: string
390
  @param default_dir: default file storage directory when C{file_storage_dir}
391
      is 'None'
392
  @type file_disk_template: string
393
  @param file_disk_template: a disk template whose storage type is 'ST_FILE' or
394
      'ST_SHARED_FILE'
395
  @rtype: string
396
  @returns: the name of the actual file storage directory
397

398
  """
399
  assert (file_disk_template in utils.storage.GetDiskTemplatesOfStorageTypes(
400
            constants.ST_FILE, constants.ST_SHARED_FILE
401
         ))
402

    
403
  if file_storage_dir is None:
404
    file_storage_dir = default_dir
405
  if not acceptance_fn:
406
    acceptance_fn = \
407
        lambda path: filestorage.CheckFileStoragePathAcceptance(
408
            path, exact_match_ok=True)
409

    
410
  cluster.CheckFileStoragePathVsEnabledDiskTemplates(
411
      logging.warning, file_storage_dir, enabled_disk_templates)
412

    
413
  file_storage_enabled = file_disk_template in enabled_disk_templates
414
  if file_storage_enabled:
415
    try:
416
      acceptance_fn(file_storage_dir)
417
    except errors.FileStoragePathError as e:
418
      raise errors.OpPrereqError(str(e))
419
    result_file_storage_dir = init_fn(file_storage_dir)
420
  else:
421
    result_file_storage_dir = file_storage_dir
422
  return result_file_storage_dir
423

    
424

    
425
def _PrepareFileStorage(
426
    enabled_disk_templates, file_storage_dir, init_fn=_InitFileStorageDir,
427
    acceptance_fn=None):
428
  """Checks if file storage is enabled and inits the dir.
429

430
  @see: C{_PrepareFileBasedStorage}
431

432
  """
433
  return _PrepareFileBasedStorage(
434
      enabled_disk_templates, file_storage_dir,
435
      pathutils.DEFAULT_FILE_STORAGE_DIR, constants.DT_FILE,
436
      init_fn=init_fn, acceptance_fn=acceptance_fn)
437

    
438

    
439
def _PrepareSharedFileStorage(
440
    enabled_disk_templates, file_storage_dir, init_fn=_InitFileStorageDir,
441
    acceptance_fn=None):
442
  """Checks if shared file storage is enabled and inits the dir.
443

444
  @see: C{_PrepareFileBasedStorage}
445

446
  """
447
  return _PrepareFileBasedStorage(
448
      enabled_disk_templates, file_storage_dir,
449
      pathutils.DEFAULT_SHARED_FILE_STORAGE_DIR, constants.DT_SHARED_FILE,
450
      init_fn=init_fn, acceptance_fn=acceptance_fn)
451

    
452

    
453
def _PrepareGlusterStorage(
454
    enabled_disk_templates, file_storage_dir, init_fn=_InitFileStorageDir,
455
    acceptance_fn=None):
456
  """Checks if gluster storage is enabled and inits the dir.
457

458
  @see: C{_PrepareFileBasedStorage}
459

460
  """
461
  return _PrepareFileBasedStorage(
462
      enabled_disk_templates, file_storage_dir,
463
      pathutils.DEFAULT_GLUSTER_STORAGE_DIR, constants.DT_GLUSTER,
464
      init_fn=init_fn, acceptance_fn=acceptance_fn)
465

    
466

    
467
def _InitCheckEnabledDiskTemplates(enabled_disk_templates):
468
  """Checks the sanity of the enabled disk templates.
469

470
  """
471
  if not enabled_disk_templates:
472
    raise errors.OpPrereqError("Enabled disk templates list must contain at"
473
                               " least one member", errors.ECODE_INVAL)
474
  invalid_disk_templates = \
475
    set(enabled_disk_templates) - constants.DISK_TEMPLATES
476
  if invalid_disk_templates:
477
    raise errors.OpPrereqError("Enabled disk templates list contains invalid"
478
                               " entries: %s" % invalid_disk_templates,
479
                               errors.ECODE_INVAL)
480

    
481

    
482
def _RestrictIpolicyToEnabledDiskTemplates(ipolicy, enabled_disk_templates):
483
  """Restricts the ipolicy's disk templates to the enabled ones.
484

485
  This function clears the ipolicy's list of allowed disk templates from the
486
  ones that are not enabled by the cluster.
487

488
  @type ipolicy: dict
489
  @param ipolicy: the instance policy
490
  @type enabled_disk_templates: list of string
491
  @param enabled_disk_templates: the list of cluster-wide enabled disk
492
    templates
493

494
  """
495
  assert constants.IPOLICY_DTS in ipolicy
496
  allowed_disk_templates = ipolicy[constants.IPOLICY_DTS]
497
  restricted_disk_templates = list(set(allowed_disk_templates)
498
                                   .intersection(set(enabled_disk_templates)))
499
  ipolicy[constants.IPOLICY_DTS] = restricted_disk_templates
500

    
501

    
502
def _InitCheckDrbdHelper(drbd_helper, drbd_enabled):
503
  """Checks the DRBD usermode helper.
504

505
  @type drbd_helper: string
506
  @param drbd_helper: name of the DRBD usermode helper that the system should
507
    use
508

509
  """
510
  if not drbd_enabled:
511
    return
512

    
513
  if drbd_helper is not None:
514
    try:
515
      curr_helper = drbd.DRBD8.GetUsermodeHelper()
516
    except errors.BlockDeviceError, err:
517
      raise errors.OpPrereqError("Error while checking drbd helper"
518
                                 " (disable drbd with --enabled-disk-templates"
519
                                 " if you are not using drbd): %s" % str(err),
520
                                 errors.ECODE_ENVIRON)
521
    if drbd_helper != curr_helper:
522
      raise errors.OpPrereqError("Error: requiring %s as drbd helper but %s"
523
                                 " is the current helper" % (drbd_helper,
524
                                                             curr_helper),
525
                                 errors.ECODE_INVAL)
526

    
527

    
528
def InitCluster(cluster_name, mac_prefix, # pylint: disable=R0913, R0914
529
                master_netmask, master_netdev, file_storage_dir,
530
                shared_file_storage_dir, gluster_storage_dir,
531
                candidate_pool_size, secondary_ip=None,
532
                vg_name=None, beparams=None, nicparams=None, ndparams=None,
533
                hvparams=None, diskparams=None, enabled_hypervisors=None,
534
                modify_etc_hosts=True, modify_ssh_setup=True,
535
                maintain_node_health=False, drbd_helper=None, uid_pool=None,
536
                default_iallocator=None, default_iallocator_params=None,
537
                primary_ip_version=None, ipolicy=None,
538
                prealloc_wipe_disks=False, use_external_mip_script=False,
539
                hv_state=None, disk_state=None, enabled_disk_templates=None):
540
  """Initialise the cluster.
541

542
  @type candidate_pool_size: int
543
  @param candidate_pool_size: master candidate pool size
544
  @type enabled_disk_templates: list of string
545
  @param enabled_disk_templates: list of disk_templates to be used in this
546
    cluster
547

548
  """
549
  # TODO: complete the docstring
550
  if config.ConfigWriter.IsCluster():
551
    raise errors.OpPrereqError("Cluster is already initialised",
552
                               errors.ECODE_STATE)
553

    
554
  if not enabled_hypervisors:
555
    raise errors.OpPrereqError("Enabled hypervisors list must contain at"
556
                               " least one member", errors.ECODE_INVAL)
557
  invalid_hvs = set(enabled_hypervisors) - constants.HYPER_TYPES
558
  if invalid_hvs:
559
    raise errors.OpPrereqError("Enabled hypervisors contains invalid"
560
                               " entries: %s" % invalid_hvs,
561
                               errors.ECODE_INVAL)
562

    
563
  _InitCheckEnabledDiskTemplates(enabled_disk_templates)
564

    
565
  try:
566
    ipcls = netutils.IPAddress.GetClassFromIpVersion(primary_ip_version)
567
  except errors.ProgrammerError:
568
    raise errors.OpPrereqError("Invalid primary ip version: %d." %
569
                               primary_ip_version, errors.ECODE_INVAL)
570

    
571
  hostname = netutils.GetHostname(family=ipcls.family)
572
  if not ipcls.IsValid(hostname.ip):
573
    raise errors.OpPrereqError("This host's IP (%s) is not a valid IPv%d"
574
                               " address." % (hostname.ip, primary_ip_version),
575
                               errors.ECODE_INVAL)
576

    
577
  if ipcls.IsLoopback(hostname.ip):
578
    raise errors.OpPrereqError("This host's IP (%s) resolves to a loopback"
579
                               " address. Please fix DNS or %s." %
580
                               (hostname.ip, pathutils.ETC_HOSTS),
581
                               errors.ECODE_ENVIRON)
582

    
583
  if not ipcls.Own(hostname.ip):
584
    raise errors.OpPrereqError("Inconsistency: this host's name resolves"
585
                               " to %s,\nbut this ip address does not"
586
                               " belong to this host" %
587
                               hostname.ip, errors.ECODE_ENVIRON)
588

    
589
  clustername = netutils.GetHostname(name=cluster_name, family=ipcls.family)
590

    
591
  if netutils.TcpPing(clustername.ip, constants.DEFAULT_NODED_PORT, timeout=5):
592
    raise errors.OpPrereqError("Cluster IP already active",
593
                               errors.ECODE_NOTUNIQUE)
594

    
595
  if not secondary_ip:
596
    if primary_ip_version == constants.IP6_VERSION:
597
      raise errors.OpPrereqError("When using a IPv6 primary address, a valid"
598
                                 " IPv4 address must be given as secondary",
599
                                 errors.ECODE_INVAL)
600
    secondary_ip = hostname.ip
601

    
602
  if not netutils.IP4Address.IsValid(secondary_ip):
603
    raise errors.OpPrereqError("Secondary IP address (%s) has to be a valid"
604
                               " IPv4 address." % secondary_ip,
605
                               errors.ECODE_INVAL)
606

    
607
  if not netutils.IP4Address.Own(secondary_ip):
608
    raise errors.OpPrereqError("You gave %s as secondary IP,"
609
                               " but it does not belong to this host." %
610
                               secondary_ip, errors.ECODE_ENVIRON)
611

    
612
  if master_netmask is not None:
613
    if not ipcls.ValidateNetmask(master_netmask):
614
      raise errors.OpPrereqError("CIDR netmask (%s) not valid for IPv%s " %
615
                                  (master_netmask, primary_ip_version),
616
                                 errors.ECODE_INVAL)
617
  else:
618
    master_netmask = ipcls.iplen
619

    
620
  if vg_name:
621
    # Check if volume group is valid
622
    vgstatus = utils.CheckVolumeGroupSize(utils.ListVolumeGroups(), vg_name,
623
                                          constants.MIN_VG_SIZE)
624
    if vgstatus:
625
      raise errors.OpPrereqError("Error: %s" % vgstatus, errors.ECODE_INVAL)
626

    
627
  drbd_enabled = constants.DT_DRBD8 in enabled_disk_templates
628
  _InitCheckDrbdHelper(drbd_helper, drbd_enabled)
629

    
630
  logging.debug("Stopping daemons (if any are running)")
631
  result = utils.RunCmd([pathutils.DAEMON_UTIL, "stop-all"])
632
  if result.failed:
633
    raise errors.OpExecError("Could not stop daemons, command %s"
634
                             " had exitcode %s and error '%s'" %
635
                             (result.cmd, result.exit_code, result.output))
636

    
637
  file_storage_dir = _PrepareFileStorage(enabled_disk_templates,
638
                                         file_storage_dir)
639
  shared_file_storage_dir = _PrepareSharedFileStorage(enabled_disk_templates,
640
                                                      shared_file_storage_dir)
641

    
642
  if not re.match("^[0-9a-z]{2}:[0-9a-z]{2}:[0-9a-z]{2}$", mac_prefix):
643
    raise errors.OpPrereqError("Invalid mac prefix given '%s'" % mac_prefix,
644
                               errors.ECODE_INVAL)
645

    
646
  if not nicparams.get('mode', None) == constants.NIC_MODE_OVS:
647
    # Do not do this check if mode=openvswitch, since the openvswitch is not
648
    # created yet
649
    result = utils.RunCmd(["ip", "link", "show", "dev", master_netdev])
650
    if result.failed:
651
      raise errors.OpPrereqError("Invalid master netdev given (%s): '%s'" %
652
                                 (master_netdev,
653
                                  result.output.strip()), errors.ECODE_INVAL)
654

    
655
  dirs = [(pathutils.RUN_DIR, constants.RUN_DIRS_MODE)]
656
  utils.EnsureDirs(dirs)
657

    
658
  objects.UpgradeBeParams(beparams)
659
  utils.ForceDictType(beparams, constants.BES_PARAMETER_TYPES)
660
  utils.ForceDictType(nicparams, constants.NICS_PARAMETER_TYPES)
661

    
662
  objects.NIC.CheckParameterSyntax(nicparams)
663

    
664
  full_ipolicy = objects.FillIPolicy(constants.IPOLICY_DEFAULTS, ipolicy)
665
  _RestrictIpolicyToEnabledDiskTemplates(full_ipolicy, enabled_disk_templates)
666

    
667
  if ndparams is not None:
668
    utils.ForceDictType(ndparams, constants.NDS_PARAMETER_TYPES)
669
  else:
670
    ndparams = dict(constants.NDC_DEFAULTS)
671

    
672
  # This is ugly, as we modify the dict itself
673
  # FIXME: Make utils.ForceDictType pure functional or write a wrapper
674
  # around it
675
  if hv_state:
676
    for hvname, hvs_data in hv_state.items():
677
      utils.ForceDictType(hvs_data, constants.HVSTS_PARAMETER_TYPES)
678
      hv_state[hvname] = objects.Cluster.SimpleFillHvState(hvs_data)
679
  else:
680
    hv_state = dict((hvname, constants.HVST_DEFAULTS)
681
                    for hvname in enabled_hypervisors)
682

    
683
  # FIXME: disk_state has no default values yet
684
  if disk_state:
685
    for storage, ds_data in disk_state.items():
686
      if storage not in constants.DS_VALID_TYPES:
687
        raise errors.OpPrereqError("Invalid storage type in disk state: %s" %
688
                                   storage, errors.ECODE_INVAL)
689
      for ds_name, state in ds_data.items():
690
        utils.ForceDictType(state, constants.DSS_PARAMETER_TYPES)
691
        ds_data[ds_name] = objects.Cluster.SimpleFillDiskState(state)
692

    
693
  # hvparams is a mapping of hypervisor->hvparams dict
694
  for hv_name, hv_params in hvparams.iteritems():
695
    utils.ForceDictType(hv_params, constants.HVS_PARAMETER_TYPES)
696
    hv_class = hypervisor.GetHypervisor(hv_name)
697
    hv_class.CheckParameterSyntax(hv_params)
698

    
699
  # diskparams is a mapping of disk-template->diskparams dict
700
  for template, dt_params in diskparams.items():
701
    param_keys = set(dt_params.keys())
702
    default_param_keys = set(constants.DISK_DT_DEFAULTS[template].keys())
703
    if not (param_keys <= default_param_keys):
704
      unknown_params = param_keys - default_param_keys
705
      raise errors.OpPrereqError("Invalid parameters for disk template %s:"
706
                                 " %s" % (template,
707
                                          utils.CommaJoin(unknown_params)),
708
                                 errors.ECODE_INVAL)
709
    utils.ForceDictType(dt_params, constants.DISK_DT_TYPES)
710
    if template == constants.DT_DRBD8 and vg_name is not None:
711
      # The default METAVG value is equal to the VG name set at init time,
712
      # if provided
713
      dt_params[constants.DRBD_DEFAULT_METAVG] = vg_name
714

    
715
  try:
716
    utils.VerifyDictOptions(diskparams, constants.DISK_DT_DEFAULTS)
717
  except errors.OpPrereqError, err:
718
    raise errors.OpPrereqError("While verify diskparam options: %s" % err,
719
                               errors.ECODE_INVAL)
720

    
721
  # set up ssh config and /etc/hosts
722
  rsa_sshkey = ""
723
  dsa_sshkey = ""
724
  if os.path.isfile(pathutils.SSH_HOST_RSA_PUB):
725
    sshline = utils.ReadFile(pathutils.SSH_HOST_RSA_PUB)
726
    rsa_sshkey = sshline.split(" ")[1]
727
  if os.path.isfile(pathutils.SSH_HOST_DSA_PUB):
728
    sshline = utils.ReadFile(pathutils.SSH_HOST_DSA_PUB)
729
    dsa_sshkey = sshline.split(" ")[1]
730
  if not rsa_sshkey and not dsa_sshkey:
731
    raise errors.OpPrereqError("Failed to find SSH public keys",
732
                               errors.ECODE_ENVIRON)
733

    
734
  if modify_etc_hosts:
735
    utils.AddHostToEtcHosts(hostname.name, hostname.ip)
736

    
737
  if modify_ssh_setup:
738
    _InitSSHSetup()
739

    
740
  if default_iallocator is not None:
741
    alloc_script = utils.FindFile(default_iallocator,
742
                                  constants.IALLOCATOR_SEARCH_PATH,
743
                                  os.path.isfile)
744
    if alloc_script is None:
745
      raise errors.OpPrereqError("Invalid default iallocator script '%s'"
746
                                 " specified" % default_iallocator,
747
                                 errors.ECODE_INVAL)
748
  elif constants.HTOOLS:
749
    # htools was enabled at build-time, we default to it
750
    if utils.FindFile(constants.IALLOC_HAIL,
751
                      constants.IALLOCATOR_SEARCH_PATH,
752
                      os.path.isfile):
753
      default_iallocator = constants.IALLOC_HAIL
754

    
755
  candidate_certs = {}
756

    
757
  now = time.time()
758

    
759
  # init of cluster config file
760
  cluster_config = objects.Cluster(
761
    serial_no=1,
762
    rsahostkeypub=rsa_sshkey,
763
    dsahostkeypub=dsa_sshkey,
764
    highest_used_port=(constants.FIRST_DRBD_PORT - 1),
765
    mac_prefix=mac_prefix,
766
    volume_group_name=vg_name,
767
    tcpudp_port_pool=set(),
768
    master_ip=clustername.ip,
769
    master_netmask=master_netmask,
770
    master_netdev=master_netdev,
771
    cluster_name=clustername.name,
772
    file_storage_dir=file_storage_dir,
773
    shared_file_storage_dir=shared_file_storage_dir,
774
    gluster_storage_dir=gluster_storage_dir,
775
    enabled_hypervisors=enabled_hypervisors,
776
    beparams={constants.PP_DEFAULT: beparams},
777
    nicparams={constants.PP_DEFAULT: nicparams},
778
    ndparams=ndparams,
779
    hvparams=hvparams,
780
    diskparams=diskparams,
781
    candidate_pool_size=candidate_pool_size,
782
    modify_etc_hosts=modify_etc_hosts,
783
    modify_ssh_setup=modify_ssh_setup,
784
    uid_pool=uid_pool,
785
    ctime=now,
786
    mtime=now,
787
    maintain_node_health=maintain_node_health,
788
    drbd_usermode_helper=drbd_helper,
789
    default_iallocator=default_iallocator,
790
    default_iallocator_params=default_iallocator_params,
791
    primary_ip_family=ipcls.family,
792
    prealloc_wipe_disks=prealloc_wipe_disks,
793
    use_external_mip_script=use_external_mip_script,
794
    ipolicy=full_ipolicy,
795
    hv_state_static=hv_state,
796
    disk_state_static=disk_state,
797
    enabled_disk_templates=enabled_disk_templates,
798
    candidate_certs=candidate_certs,
799
    )
800
  master_node_config = objects.Node(name=hostname.name,
801
                                    primary_ip=hostname.ip,
802
                                    secondary_ip=secondary_ip,
803
                                    serial_no=1,
804
                                    master_candidate=True,
805
                                    offline=False, drained=False,
806
                                    ctime=now, mtime=now,
807
                                    )
808
  InitConfig(constants.CONFIG_VERSION, cluster_config, master_node_config)
809
  cfg = config.ConfigWriter(offline=True)
810
  ssh.WriteKnownHostsFile(cfg, pathutils.SSH_KNOWN_HOSTS_FILE)
811
  cfg.Update(cfg.GetClusterInfo(), logging.error)
812
  ssconf.WriteSsconfFiles(cfg.GetSsconfValues())
813

    
814
  # set up the inter-node password and certificate
815
  _InitGanetiServerSetup(hostname.name)
816

    
817
  logging.debug("Starting daemons")
818
  result = utils.RunCmd([pathutils.DAEMON_UTIL, "start-all"])
819
  if result.failed:
820
    raise errors.OpExecError("Could not start daemons, command %s"
821
                             " had exitcode %s and error %s" %
822
                             (result.cmd, result.exit_code, result.output))
823

    
824
  _WaitForMasterDaemon()
825

    
826

    
827
def InitConfig(version, cluster_config, master_node_config,
828
               cfg_file=pathutils.CLUSTER_CONF_FILE):
829
  """Create the initial cluster configuration.
830

831
  It will contain the current node, which will also be the master
832
  node, and no instances.
833

834
  @type version: int
835
  @param version: configuration version
836
  @type cluster_config: L{objects.Cluster}
837
  @param cluster_config: cluster configuration
838
  @type master_node_config: L{objects.Node}
839
  @param master_node_config: master node configuration
840
  @type cfg_file: string
841
  @param cfg_file: configuration file path
842

843
  """
844
  uuid_generator = config.TemporaryReservationManager()
845
  cluster_config.uuid = uuid_generator.Generate([], utils.NewUUID,
846
                                                _INITCONF_ECID)
847
  master_node_config.uuid = uuid_generator.Generate([], utils.NewUUID,
848
                                                    _INITCONF_ECID)
849
  cluster_config.master_node = master_node_config.uuid
850
  nodes = {
851
    master_node_config.uuid: master_node_config,
852
    }
853
  default_nodegroup = objects.NodeGroup(
854
    uuid=uuid_generator.Generate([], utils.NewUUID, _INITCONF_ECID),
855
    name=constants.INITIAL_NODE_GROUP_NAME,
856
    members=[master_node_config.uuid],
857
    diskparams={},
858
    )
859
  nodegroups = {
860
    default_nodegroup.uuid: default_nodegroup,
861
    }
862
  now = time.time()
863
  config_data = objects.ConfigData(version=version,
864
                                   cluster=cluster_config,
865
                                   nodegroups=nodegroups,
866
                                   nodes=nodes,
867
                                   instances={},
868
                                   networks={},
869
                                   serial_no=1,
870
                                   ctime=now, mtime=now)
871
  utils.WriteFile(cfg_file,
872
                  data=serializer.Dump(config_data.ToDict()),
873
                  mode=0600)
874

    
875

    
876
def FinalizeClusterDestroy(master_uuid):
877
  """Execute the last steps of cluster destroy
878

879
  This function shuts down all the daemons, completing the destroy
880
  begun in cmdlib.LUDestroyOpcode.
881

882
  """
883
  cfg = config.ConfigWriter()
884
  modify_ssh_setup = cfg.GetClusterInfo().modify_ssh_setup
885
  runner = rpc.BootstrapRunner()
886

    
887
  master_name = cfg.GetNodeName(master_uuid)
888

    
889
  master_params = cfg.GetMasterNetworkParameters()
890
  master_params.uuid = master_uuid
891
  ems = cfg.GetUseExternalMipScript()
892
  result = runner.call_node_deactivate_master_ip(master_name, master_params,
893
                                                 ems)
894

    
895
  msg = result.fail_msg
896
  if msg:
897
    logging.warning("Could not disable the master IP: %s", msg)
898

    
899
  result = runner.call_node_stop_master(master_name)
900
  msg = result.fail_msg
901
  if msg:
902
    logging.warning("Could not disable the master role: %s", msg)
903

    
904
  result = runner.call_node_leave_cluster(master_name, modify_ssh_setup)
905
  msg = result.fail_msg
906
  if msg:
907
    logging.warning("Could not shutdown the node daemon and cleanup"
908
                    " the node: %s", msg)
909

    
910

    
911
def SetupNodeDaemon(opts, cluster_name, node, ssh_port):
912
  """Add a node to the cluster.
913

914
  This function must be called before the actual opcode, and will ssh
915
  to the remote node, copy the needed files, and start ganeti-noded,
916
  allowing the master to do the rest via normal rpc calls.
917

918
  @param cluster_name: the cluster name
919
  @param node: the name of the new node
920
  @param ssh_port: the SSH port of the new node
921

922
  """
923
  data = {
924
    constants.NDS_CLUSTER_NAME: cluster_name,
925
    constants.NDS_NODE_DAEMON_CERTIFICATE:
926
      utils.ReadFile(pathutils.NODED_CERT_FILE),
927
    constants.NDS_SSCONF: ssconf.SimpleStore().ReadAll(),
928
    constants.NDS_START_NODE_DAEMON: True,
929
    }
930

    
931
  RunNodeSetupCmd(cluster_name, node, pathutils.NODE_DAEMON_SETUP,
932
                  opts.debug, opts.verbose,
933
                  True, opts.ssh_key_check, opts.ssh_key_check,
934
                  ssh_port, data)
935

    
936
  _WaitForNodeDaemon(node)
937

    
938

    
939
def MasterFailover(no_voting=False):
940
  """Failover the master node.
941

942
  This checks that we are not already the master, and will cause the
943
  current master to cease being master, and the non-master to become
944
  new master.
945

946
  @type no_voting: boolean
947
  @param no_voting: force the operation without remote nodes agreement
948
                      (dangerous)
949

950
  """
951
  sstore = ssconf.SimpleStore()
952

    
953
  old_master, new_master = ssconf.GetMasterAndMyself(sstore)
954
  node_names = sstore.GetNodeList()
955
  mc_list = sstore.GetMasterCandidates()
956

    
957
  if old_master == new_master:
958
    raise errors.OpPrereqError("This commands must be run on the node"
959
                               " where you want the new master to be."
960
                               " %s is already the master" %
961
                               old_master, errors.ECODE_INVAL)
962

    
963
  if new_master not in mc_list:
964
    mc_no_master = [name for name in mc_list if name != old_master]
965
    raise errors.OpPrereqError("This node is not among the nodes marked"
966
                               " as master candidates. Only these nodes"
967
                               " can become masters. Current list of"
968
                               " master candidates is:\n"
969
                               "%s" % ("\n".join(mc_no_master)),
970
                               errors.ECODE_STATE)
971

    
972
  if not no_voting:
973
    vote_list = GatherMasterVotes(node_names)
974

    
975
    if vote_list:
976
      voted_master = vote_list[0][0]
977
      if voted_master is None:
978
        raise errors.OpPrereqError("Cluster is inconsistent, most nodes did"
979
                                   " not respond.", errors.ECODE_ENVIRON)
980
      elif voted_master != old_master:
981
        raise errors.OpPrereqError("I have a wrong configuration, I believe"
982
                                   " the master is %s but the other nodes"
983
                                   " voted %s. Please resync the configuration"
984
                                   " of this node." %
985
                                   (old_master, voted_master),
986
                                   errors.ECODE_STATE)
987
  # end checks
988

    
989
  rcode = 0
990

    
991
  logging.info("Setting master to %s, old master: %s", new_master, old_master)
992

    
993
  try:
994
    # instantiate a real config writer, as we now know we have the
995
    # configuration data
996
    cfg = config.ConfigWriter(accept_foreign=True)
997

    
998
    old_master_node = cfg.GetNodeInfoByName(old_master)
999
    if old_master_node is None:
1000
      raise errors.OpPrereqError("Could not find old master node '%s' in"
1001
                                 " cluster configuration." % old_master,
1002
                                 errors.ECODE_NOENT)
1003

    
1004
    cluster_info = cfg.GetClusterInfo()
1005
    new_master_node = cfg.GetNodeInfoByName(new_master)
1006
    if new_master_node is None:
1007
      raise errors.OpPrereqError("Could not find new master node '%s' in"
1008
                                 " cluster configuration." % new_master,
1009
                                 errors.ECODE_NOENT)
1010

    
1011
    cluster_info.master_node = new_master_node.uuid
1012
    # this will also regenerate the ssconf files, since we updated the
1013
    # cluster info
1014
    cfg.Update(cluster_info, logging.error)
1015
  except errors.ConfigurationError, err:
1016
    logging.error("Error while trying to set the new master: %s",
1017
                  str(err))
1018
    return 1
1019

    
1020
  # if cfg.Update worked, then it means the old master daemon won't be
1021
  # able now to write its own config file (we rely on locking in both
1022
  # backend.UploadFile() and ConfigWriter._Write(); hence the next
1023
  # step is to kill the old master
1024

    
1025
  logging.info("Stopping the master daemon on node %s", old_master)
1026

    
1027
  runner = rpc.BootstrapRunner()
1028
  master_params = cfg.GetMasterNetworkParameters()
1029
  master_params.uuid = old_master_node.uuid
1030
  ems = cfg.GetUseExternalMipScript()
1031
  result = runner.call_node_deactivate_master_ip(old_master,
1032
                                                 master_params, ems)
1033

    
1034
  msg = result.fail_msg
1035
  if msg:
1036
    logging.warning("Could not disable the master IP: %s", msg)
1037

    
1038
  result = runner.call_node_stop_master(old_master)
1039
  msg = result.fail_msg
1040
  if msg:
1041
    logging.error("Could not disable the master role on the old master"
1042
                  " %s, please disable manually: %s", old_master, msg)
1043

    
1044
  logging.info("Checking master IP non-reachability...")
1045

    
1046
  master_ip = sstore.GetMasterIP()
1047
  total_timeout = 30
1048

    
1049
  # Here we have a phase where no master should be running
1050
  def _check_ip():
1051
    if netutils.TcpPing(master_ip, constants.DEFAULT_NODED_PORT):
1052
      raise utils.RetryAgain()
1053

    
1054
  try:
1055
    utils.Retry(_check_ip, (1, 1.5, 5), total_timeout)
1056
  except utils.RetryTimeout:
1057
    logging.warning("The master IP is still reachable after %s seconds,"
1058
                    " continuing but activating the master on the current"
1059
                    " node will probably fail", total_timeout)
1060

    
1061
  if jstore.CheckDrainFlag():
1062
    logging.info("Undraining job queue")
1063
    jstore.SetDrainFlag(False)
1064

    
1065
  logging.info("Starting the master daemons on the new master")
1066

    
1067
  result = rpc.BootstrapRunner().call_node_start_master_daemons(new_master,
1068
                                                                no_voting)
1069
  msg = result.fail_msg
1070
  if msg:
1071
    logging.error("Could not start the master role on the new master"
1072
                  " %s, please check: %s", new_master, msg)
1073
    rcode = 1
1074

    
1075
  logging.info("Master failed over from %s to %s", old_master, new_master)
1076
  return rcode
1077

    
1078

    
1079
def GetMaster():
1080
  """Returns the current master node.
1081

1082
  This is a separate function in bootstrap since it's needed by
1083
  gnt-cluster, and instead of importing directly ssconf, it's better
1084
  to abstract it in bootstrap, where we do use ssconf in other
1085
  functions too.
1086

1087
  """
1088
  sstore = ssconf.SimpleStore()
1089

    
1090
  old_master, _ = ssconf.GetMasterAndMyself(sstore)
1091

    
1092
  return old_master
1093

    
1094

    
1095
def GatherMasterVotes(node_names):
1096
  """Check the agreement on who is the master.
1097

1098
  This function will return a list of (node, number of votes), ordered
1099
  by the number of votes. Errors will be denoted by the key 'None'.
1100

1101
  Note that the sum of votes is the number of nodes this machine
1102
  knows, whereas the number of entries in the list could be different
1103
  (if some nodes vote for another master).
1104

1105
  We remove ourselves from the list since we know that (bugs aside)
1106
  since we use the same source for configuration information for both
1107
  backend and boostrap, we'll always vote for ourselves.
1108

1109
  @type node_names: list
1110
  @param node_names: the list of nodes to query for master info; the current
1111
      node will be removed if it is in the list
1112
  @rtype: list
1113
  @return: list of (node, votes)
1114

1115
  """
1116
  myself = netutils.Hostname.GetSysName()
1117
  try:
1118
    node_names.remove(myself)
1119
  except ValueError:
1120
    pass
1121
  if not node_names:
1122
    # no nodes left (eventually after removing myself)
1123
    return []
1124
  results = rpc.BootstrapRunner().call_master_info(node_names)
1125
  if not isinstance(results, dict):
1126
    # this should not happen (unless internal error in rpc)
1127
    logging.critical("Can't complete rpc call, aborting master startup")
1128
    return [(None, len(node_names))]
1129
  votes = {}
1130
  for node_name in results:
1131
    nres = results[node_name]
1132
    data = nres.payload
1133
    msg = nres.fail_msg
1134
    fail = False
1135
    if msg:
1136
      logging.warning("Error contacting node %s: %s", node_name, msg)
1137
      fail = True
1138
    # for now we accept both length 3, 4 and 5 (data[3] is primary ip version
1139
    # and data[4] is the master netmask)
1140
    elif not isinstance(data, (tuple, list)) or len(data) < 3:
1141
      logging.warning("Invalid data received from node %s: %s",
1142
                      node_name, data)
1143
      fail = True
1144
    if fail:
1145
      if None not in votes:
1146
        votes[None] = 0
1147
      votes[None] += 1
1148
      continue
1149
    master_node = data[2]
1150
    if master_node not in votes:
1151
      votes[master_node] = 0
1152
    votes[master_node] += 1
1153

    
1154
  vote_list = [v for v in votes.items()]
1155
  # sort first on number of votes then on name, since we want None
1156
  # sorted later if we have the half of the nodes not responding, and
1157
  # half voting all for the same master
1158
  vote_list.sort(key=lambda x: (x[1], x[0]), reverse=True)
1159

    
1160
  return vote_list