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