218e62ea1baa88194fb0fff00bfd181e2d3edb21
[ganeti-local] / lib / bootstrap.py
1 #
2 #
3
4 # Copyright (C) 2006, 2007, 2008 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 sha
29 import re
30 import logging
31
32 from ganeti import rpc
33 from ganeti import ssh
34 from ganeti import utils
35 from ganeti import errors
36 from ganeti import config
37 from ganeti import constants
38 from ganeti import objects
39 from ganeti import ssconf
40
41 from ganeti.rpc import RpcRunner
42
43 def _InitSSHSetup(node):
44   """Setup the SSH configuration for the cluster.
45
46
47   This generates a dsa keypair for root, adds the pub key to the
48   permitted hosts and adds the hostkey to its own known hosts.
49
50   Args:
51     node: the name of this host as a fqdn
52
53   """
54   priv_key, pub_key, auth_keys = ssh.GetUserFiles(constants.GANETI_RUNAS)
55
56   for name in priv_key, pub_key:
57     if os.path.exists(name):
58       utils.CreateBackup(name)
59     utils.RemoveFile(name)
60
61   result = utils.RunCmd(["ssh-keygen", "-t", "dsa",
62                          "-f", priv_key,
63                          "-q", "-N", ""])
64   if result.failed:
65     raise errors.OpExecError("Could not generate ssh keypair, error %s" %
66                              result.output)
67
68   f = open(pub_key, 'r')
69   try:
70     utils.AddAuthorizedKey(auth_keys, f.read(8192))
71   finally:
72     f.close()
73
74
75 def _InitGanetiServerSetup():
76   """Setup the necessary configuration for the initial node daemon.
77
78   This creates the nodepass file containing the shared password for
79   the cluster and also generates the SSL certificate.
80
81   """
82   # Create pseudo random password
83   randpass = utils.GenerateSecret()
84
85   # and write it into the config file
86   utils.WriteFile(constants.CLUSTER_PASSWORD_FILE,
87                   data="%s\n" % randpass, mode=0400)
88
89   result = utils.RunCmd(["openssl", "req", "-new", "-newkey", "rsa:1024",
90                          "-days", str(365*5), "-nodes", "-x509",
91                          "-keyout", constants.SSL_CERT_FILE,
92                          "-out", constants.SSL_CERT_FILE, "-batch"])
93   if result.failed:
94     raise errors.OpExecError("could not generate server ssl cert, command"
95                              " %s had exitcode %s and error message %s" %
96                              (result.cmd, result.exit_code, result.output))
97
98   os.chmod(constants.SSL_CERT_FILE, 0400)
99
100   result = utils.RunCmd([constants.NODE_INITD_SCRIPT, "restart"])
101
102   if result.failed:
103     raise errors.OpExecError("Could not start the node daemon, command %s"
104                              " had exitcode %s and error %s" %
105                              (result.cmd, result.exit_code, result.output))
106
107
108 def InitCluster(cluster_name, hypervisor_type, mac_prefix, def_bridge,
109                 master_netdev, file_storage_dir,
110                 secondary_ip=None,
111                 vg_name=None):
112   """Initialise the cluster.
113
114   """
115   if config.ConfigWriter.IsCluster():
116     raise errors.OpPrereqError("Cluster is already initialised")
117
118   if hypervisor_type == constants.HT_XEN_HVM:
119     if not os.path.exists(constants.VNC_PASSWORD_FILE):
120       raise errors.OpPrereqError("Please prepare the cluster VNC"
121                                  "password file %s" %
122                                  constants.VNC_PASSWORD_FILE)
123
124   hostname = utils.HostInfo()
125
126   if hostname.ip.startswith("127."):
127     raise errors.OpPrereqError("This host's IP resolves to the private"
128                                " range (%s). Please fix DNS or %s." %
129                                (hostname.ip, constants.ETC_HOSTS))
130
131   if not utils.OwnIpAddress(hostname.ip):
132     raise errors.OpPrereqError("Inconsistency: this host's name resolves"
133                                " to %s,\nbut this ip address does not"
134                                " belong to this host."
135                                " Aborting." % hostname.ip)
136
137   clustername = utils.HostInfo(cluster_name)
138
139   if utils.TcpPing(clustername.ip, constants.DEFAULT_NODED_PORT,
140                    timeout=5):
141     raise errors.OpPrereqError("Cluster IP already active. Aborting.")
142
143   if secondary_ip:
144     if not utils.IsValidIP(secondary_ip):
145       raise errors.OpPrereqError("Invalid secondary ip given")
146     if (secondary_ip != hostname.ip and
147         not utils.OwnIpAddress(secondary_ip)):
148       raise errors.OpPrereqError("You gave %s as secondary IP,"
149                                  " but it does not belong to this host." %
150                                  secondary_ip)
151   else:
152     secondary_ip = hostname.ip
153
154   if vg_name is not None:
155     # Check if volume group is valid
156     vgstatus = utils.CheckVolumeGroupSize(utils.ListVolumeGroups(), vg_name,
157                                           constants.MIN_VG_SIZE)
158     if vgstatus:
159       raise errors.OpPrereqError("Error: %s\nspecify --no-lvm-storage if"
160                                  " you are not using lvm" % vgstatus)
161
162   file_storage_dir = os.path.normpath(file_storage_dir)
163
164   if not os.path.isabs(file_storage_dir):
165     raise errors.OpPrereqError("The file storage directory you passed is"
166                                " not an absolute path.")
167
168   if not os.path.exists(file_storage_dir):
169     try:
170       os.makedirs(file_storage_dir, 0750)
171     except OSError, err:
172       raise errors.OpPrereqError("Cannot create file storage directory"
173                                  " '%s': %s" %
174                                  (file_storage_dir, err))
175
176   if not os.path.isdir(file_storage_dir):
177     raise errors.OpPrereqError("The file storage directory '%s' is not"
178                                " a directory." % file_storage_dir)
179
180   if not re.match("^[0-9a-z]{2}:[0-9a-z]{2}:[0-9a-z]{2}$", mac_prefix):
181     raise errors.OpPrereqError("Invalid mac prefix given '%s'" % mac_prefix)
182
183   if hypervisor_type not in constants.HYPER_TYPES:
184     raise errors.OpPrereqError("Invalid hypervisor type given '%s'" %
185                                hypervisor_type)
186
187   result = utils.RunCmd(["ip", "link", "show", "dev", master_netdev])
188   if result.failed:
189     raise errors.OpPrereqError("Invalid master netdev given (%s): '%s'" %
190                                (master_netdev,
191                                 result.output.strip()))
192
193   if not (os.path.isfile(constants.NODE_INITD_SCRIPT) and
194           os.access(constants.NODE_INITD_SCRIPT, os.X_OK)):
195     raise errors.OpPrereqError("Init.d script '%s' missing or not"
196                                " executable." % constants.NODE_INITD_SCRIPT)
197
198   # set up the inter-node password and certificate
199   _InitGanetiServerSetup()
200
201   # set up ssh config and /etc/hosts
202   f = open(constants.SSH_HOST_RSA_PUB, 'r')
203   try:
204     sshline = f.read()
205   finally:
206     f.close()
207   sshkey = sshline.split(" ")[1]
208
209   utils.AddHostToEtcHosts(hostname.name)
210   _InitSSHSetup(hostname.name)
211
212   # init of cluster config file
213   cluster_config = objects.Cluster(
214     serial_no=1,
215     rsahostkeypub=sshkey,
216     highest_used_port=(constants.FIRST_DRBD_PORT - 1),
217     mac_prefix=mac_prefix,
218     volume_group_name=vg_name,
219     default_bridge=def_bridge,
220     tcpudp_port_pool=set(),
221     hypervisor=hypervisor_type,
222     master_node=hostname.name,
223     master_ip=clustername.ip,
224     master_netdev=master_netdev,
225     cluster_name=clustername.name,
226     file_storage_dir=file_storage_dir,
227     )
228   master_node_config = objects.Node(name=hostname.name,
229                                     primary_ip=hostname.ip,
230                                     secondary_ip=secondary_ip)
231
232   cfg = InitConfig(constants.CONFIG_VERSION,
233                    cluster_config, master_node_config)
234   ssh.WriteKnownHostsFile(cfg, constants.SSH_KNOWN_HOSTS_FILE)
235
236   # start the master ip
237   # TODO: Review rpc call from bootstrap
238   RpcRunner.call_node_start_master(hostname.name, True)
239
240
241 def InitConfig(version, cluster_config, master_node_config,
242                cfg_file=constants.CLUSTER_CONF_FILE):
243   """Create the initial cluster configuration.
244
245   It will contain the current node, which will also be the master
246   node, and no instances.
247
248   @type version: int
249   @param version: Configuration version
250   @type cluster_config: objects.Cluster
251   @param cluster_config: Cluster configuration
252   @type master_node_config: objects.Node
253   @param master_node_config: Master node configuration
254   @type file_name: string
255   @param file_name: Configuration file path
256
257   @rtype: ssconf.SimpleConfigWriter
258   @returns: Initialized config instance
259
260   """
261   nodes = {
262     master_node_config.name: master_node_config,
263     }
264
265   config_data = objects.ConfigData(version=version,
266                                    cluster=cluster_config,
267                                    nodes=nodes,
268                                    instances={},
269                                    serial_no=1)
270   cfg = ssconf.SimpleConfigWriter.FromDict(config_data.ToDict(), cfg_file)
271   cfg.Save()
272
273   return cfg
274
275
276 def FinalizeClusterDestroy(master):
277   """Execute the last steps of cluster destroy
278
279   This function shuts down all the daemons, completing the destroy
280   begun in cmdlib.LUDestroyOpcode.
281
282   """
283   if not RpcRunner.call_node_stop_master(master, True):
284     logging.warning("Could not disable the master role")
285   if not RpcRunner.call_node_leave_cluster(master):
286     logging.warning("Could not shutdown the node daemon and cleanup the node")
287
288
289 def SetupNodeDaemon(node, ssh_key_check):
290   """Add a node to the cluster.
291
292   This function must be called before the actual opcode, and will ssh
293   to the remote node, copy the needed files, and start ganeti-noded,
294   allowing the master to do the rest via normal rpc calls.
295
296   Args:
297     node: fully qualified domain name for the new node
298
299   """
300   cfg = ssconf.SimpleConfigReader()
301   sshrunner = ssh.SshRunner(cfg.GetClusterName())
302   gntpass = utils.GetNodeDaemonPassword()
303   if not re.match('^[a-zA-Z0-9.]{1,64}$', gntpass):
304     raise errors.OpExecError("ganeti password corruption detected")
305   f = open(constants.SSL_CERT_FILE)
306   try:
307     gntpem = f.read(8192)
308   finally:
309     f.close()
310   # in the base64 pem encoding, neither '!' nor '.' are valid chars,
311   # so we use this to detect an invalid certificate; as long as the
312   # cert doesn't contain this, the here-document will be correctly
313   # parsed by the shell sequence below
314   if re.search('^!EOF\.', gntpem, re.MULTILINE):
315     raise errors.OpExecError("invalid PEM encoding in the SSL certificate")
316   if not gntpem.endswith("\n"):
317     raise errors.OpExecError("PEM must end with newline")
318
319   # set up inter-node password and certificate and restarts the node daemon
320   # and then connect with ssh to set password and start ganeti-noded
321   # note that all the below variables are sanitized at this point,
322   # either by being constants or by the checks above
323   mycommand = ("umask 077 && "
324                "echo '%s' > '%s' && "
325                "cat > '%s' << '!EOF.' && \n"
326                "%s!EOF.\n%s restart" %
327                (gntpass, constants.CLUSTER_PASSWORD_FILE,
328                 constants.SSL_CERT_FILE, gntpem,
329                 constants.NODE_INITD_SCRIPT))
330
331   result = sshrunner.Run(node, 'root', mycommand, batch=False,
332                          ask_key=ssh_key_check,
333                          use_cluster_key=False,
334                          strict_host_check=ssh_key_check)
335   if result.failed:
336     raise errors.OpExecError("Remote command on node %s, error: %s,"
337                              " output: %s" %
338                              (node, result.fail_reason, result.output))
339
340   return 0
341
342
343 def MasterFailover():
344   """Failover the master node.
345
346   This checks that we are not already the master, and will cause the
347   current master to cease being master, and the non-master to become
348   new master.
349
350   """
351   cfg = ssconf.SimpleConfigWriter()
352
353   new_master = utils.HostInfo().name
354   old_master = cfg.GetMasterNode()
355
356   if old_master == new_master:
357     raise errors.OpPrereqError("This commands must be run on the node"
358                                " where you want the new master to be."
359                                " %s is already the master" %
360                                old_master)
361   # end checks
362
363   rcode = 0
364
365   logging.info("setting master to %s, old master: %s", new_master, old_master)
366
367   if not RpcRunner.call_node_stop_master(old_master, True):
368     logging.error("could disable the master role on the old master"
369                  " %s, please disable manually", old_master)
370
371   cfg.SetMasterNode(new_master)
372   cfg.Save()
373
374   # Here we have a phase where no master should be running
375
376   if not RpcRunner.call_upload_file(cfg.GetNodeList(),
377                                     constants.CLUSTER_CONF_FILE):
378     logging.error("could not distribute the new simple store master file"
379                   " to the other nodes, please check.")
380
381   if not RpcRunner.call_node_start_master(new_master, True):
382     logging.error("could not start the master role on the new master"
383                   " %s, please check", new_master)
384     rcode = 1
385
386   return rcode
387
388
389 def GatherMasterVotes(node_list):
390   """Check the agreement on who is the master.
391
392   This function will return a list of (node, number of votes), ordered
393   by the number of votes. Errors will be denoted by the key 'None'.
394
395   Note that the sum of votes is the number of nodes this machine
396   knows, whereas the number of entries in the list could be different
397   (if some nodes vote for another master).
398
399   We remove ourselves from the list since we know that (bugs aside)
400   since we use the same source for configuration information for both
401   backend and boostrap, we'll always vote for ourselves.
402
403   @type node_list: list
404   @param node_list: the list of nodes to query for master info; the current
405       node wil be removed if it is in the list
406   @rtype: list
407   @return: list of (node, votes)
408
409   """
410   myself = utils.HostInfo().name
411   try:
412     node_list.remove(myself)
413   except ValueError:
414     pass
415   if not node_list:
416     # no nodes left (eventually after removing myself)
417     return []
418   results = rpc.RpcRunner.call_master_info(node_list)
419   if not isinstance(results, dict):
420     # this should not happen (unless internal error in rpc)
421     logging.critical("Can't complete rpc call, aborting master startup")
422     return [(None, len(node_list))]
423   positive = negative = 0
424   other_masters = {}
425   votes = {}
426   for node in results:
427     if not isinstance(results[node], (tuple, list)) or len(results[node]) < 3:
428       # here the rpc layer should have already logged errors
429       if None not in votes:
430         votes[None] = 0
431       votes[None] += 1
432       continue
433     master_node = results[node][2]
434     if master_node not in votes:
435       votes[master_node] = 0
436     votes[master_node] += 1
437
438   vote_list = [v for v in votes.items()]
439   # sort first on number of votes then on name, since we want None
440   # sorted later if we have the half of the nodes not responding, and
441   # half voting all for the same master
442   vote_list.sort(key=lambda x: (x[1], x[0]), reverse=True)
443
444   return vote_list