Convert ssh.py
[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(ss):
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   Args:
81     ss: A WritableSimpleStore
82
83   """
84   # Create pseudo random password
85   randpass = utils.GenerateSecret()
86   # and write it into sstore
87   ss.SetKey(ss.SS_NODED_PASS, randpass)
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_HVM31:
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.TcpPing(hostname.ip, constants.DEFAULT_NODED_PORT,
132                        source=constants.LOCALHOST_IP_ADDRESS):
133     raise errors.OpPrereqError("Inconsistency: this host's name resolves"
134                                " to %s,\nbut this ip address does not"
135                                " belong to this host."
136                                " Aborting." % hostname.ip)
137
138   clustername = utils.HostInfo(cluster_name)
139
140   if utils.TcpPing(clustername.ip, constants.DEFAULT_NODED_PORT,
141                    timeout=5):
142     raise errors.OpPrereqError("Cluster IP already active. Aborting.")
143
144   if secondary_ip:
145     if not utils.IsValidIP(secondary_ip):
146       raise errors.OpPrereqError("Invalid secondary ip given")
147     if (secondary_ip != hostname.ip and
148         (not utils.TcpPing(secondary_ip, constants.DEFAULT_NODED_PORT,
149                            source=constants.LOCALHOST_IP_ADDRESS))):
150       raise errors.OpPrereqError("You gave %s as secondary IP,"
151                                  " but it does not belong to this host." %
152                                  secondary_ip)
153   else:
154     secondary_ip = hostname.ip
155
156   if vg_name is not None:
157     # Check if volume group is valid
158     vgstatus = utils.CheckVolumeGroupSize(utils.ListVolumeGroups(), vg_name,
159                                           constants.MIN_VG_SIZE)
160     if vgstatus:
161       raise errors.OpPrereqError("Error: %s\nspecify --no-lvm-storage if"
162                                  " you are not using lvm" % vgstatus)
163
164   file_storage_dir = os.path.normpath(file_storage_dir)
165
166   if not os.path.isabs(file_storage_dir):
167     raise errors.OpPrereqError("The file storage directory you passed is"
168                                " not an absolute path.")
169
170   if not os.path.exists(file_storage_dir):
171     try:
172       os.makedirs(file_storage_dir, 0750)
173     except OSError, err:
174       raise errors.OpPrereqError("Cannot create file storage directory"
175                                  " '%s': %s" %
176                                  (file_storage_dir, err))
177
178   if not os.path.isdir(file_storage_dir):
179     raise errors.OpPrereqError("The file storage directory '%s' is not"
180                                " a directory." % file_storage_dir)
181
182   if not re.match("^[0-9a-z]{2}:[0-9a-z]{2}:[0-9a-z]{2}$", mac_prefix):
183     raise errors.OpPrereqError("Invalid mac prefix given '%s'" % mac_prefix)
184
185   if hypervisor_type not in constants.HYPER_TYPES:
186     raise errors.OpPrereqError("Invalid hypervisor type given '%s'" %
187                                hypervisor_type)
188
189   result = utils.RunCmd(["ip", "link", "show", "dev", master_netdev])
190   if result.failed:
191     raise errors.OpPrereqError("Invalid master netdev given (%s): '%s'" %
192                                (master_netdev,
193                                 result.output.strip()))
194
195   if not (os.path.isfile(constants.NODE_INITD_SCRIPT) and
196           os.access(constants.NODE_INITD_SCRIPT, os.X_OK)):
197     raise errors.OpPrereqError("Init.d script '%s' missing or not"
198                                " executable." % constants.NODE_INITD_SCRIPT)
199
200   # set up the simple store
201   ss = ssconf.WritableSimpleStore()
202   ss.SetKey(ss.SS_HYPERVISOR, hypervisor_type)
203   ss.SetKey(ss.SS_MASTER_NODE, hostname.name)
204   ss.SetKey(ss.SS_MASTER_IP, clustername.ip)
205   ss.SetKey(ss.SS_MASTER_NETDEV, master_netdev)
206   ss.SetKey(ss.SS_CLUSTER_NAME, clustername.name)
207   ss.SetKey(ss.SS_FILE_STORAGE_DIR, file_storage_dir)
208   ss.SetKey(ss.SS_CONFIG_VERSION, constants.CONFIG_VERSION)
209
210   # set up the inter-node password and certificate
211   _InitGanetiServerSetup(ss)
212
213   # set up ssh config and /etc/hosts
214   f = open(constants.SSH_HOST_RSA_PUB, 'r')
215   try:
216     sshline = f.read()
217   finally:
218     f.close()
219   sshkey = sshline.split(" ")[1]
220
221   utils.AddHostToEtcHosts(hostname.name)
222   _InitSSHSetup(hostname.name)
223
224   # init of cluster config file
225   cluster_config = objects.Cluster(
226     serial_no=1,
227     rsahostkeypub=sshkey,
228     highest_used_port=(constants.FIRST_DRBD_PORT - 1),
229     mac_prefix=mac_prefix,
230     volume_group_name=vg_name,
231     default_bridge=def_bridge,
232     tcpudp_port_pool=set(),
233     hypervisor=hypervisor_type,
234     master_node=hostname.name,
235     master_ip=clustername.ip,
236     master_netdev=master_netdev,
237     cluster_name=clustername.name,
238     file_storage_dir=file_storage_dir,
239     )
240   master_node_config = objects.Node(name=hostname.name,
241                                     primary_ip=hostname.ip,
242                                     secondary_ip=secondary_ip)
243   cfg = config.ConfigWriter()
244   cfg.InitConfig(constants.CONFIG_VERSION, cluster_config, master_node_config)
245
246   ssh.WriteKnownHostsFile(cfg, constants.SSH_KNOWN_HOSTS_FILE)
247
248   # start the master ip
249   # TODO: Review rpc call from bootstrap
250   rpc.call_node_start_master(hostname.name, True)
251
252
253 def FinalizeClusterDestroy(master):
254   """Execute the last steps of cluster destroy
255
256   This function shuts down all the daemons, completing the destroy
257   begun in cmdlib.LUDestroyOpcode.
258
259   """
260   if not rpc.call_node_stop_master(master, True):
261     logging.warning("Could not disable the master role")
262   if not rpc.call_node_leave_cluster(master):
263     logging.warning("Could not shutdown the node daemon and cleanup the node")
264
265
266 def SetupNodeDaemon(node, ssh_key_check):
267   """Add a node to the cluster.
268
269   This function must be called before the actual opcode, and will ssh
270   to the remote node, copy the needed files, and start ganeti-noded,
271   allowing the master to do the rest via normal rpc calls.
272
273   Args:
274     node: fully qualified domain name for the new node
275
276   """
277   cfg = ssconf.SimpleConfigReader()
278   sshrunner = ssh.SshRunner(cfg)
279   ss = ssconf.SimpleStore()
280   gntpass = ss.GetNodeDaemonPassword()
281   if not re.match('^[a-zA-Z0-9.]{1,64}$', gntpass):
282     raise errors.OpExecError("ganeti password corruption detected")
283   f = open(constants.SSL_CERT_FILE)
284   try:
285     gntpem = f.read(8192)
286   finally:
287     f.close()
288   # in the base64 pem encoding, neither '!' nor '.' are valid chars,
289   # so we use this to detect an invalid certificate; as long as the
290   # cert doesn't contain this, the here-document will be correctly
291   # parsed by the shell sequence below
292   if re.search('^!EOF\.', gntpem, re.MULTILINE):
293     raise errors.OpExecError("invalid PEM encoding in the SSL certificate")
294   if not gntpem.endswith("\n"):
295     raise errors.OpExecError("PEM must end with newline")
296
297   # set up inter-node password and certificate and restarts the node daemon
298   # and then connect with ssh to set password and start ganeti-noded
299   # note that all the below variables are sanitized at this point,
300   # either by being constants or by the checks above
301   mycommand = ("umask 077 && "
302                "echo '%s' > '%s' && "
303                "cat > '%s' << '!EOF.' && \n"
304                "%s!EOF.\n%s restart" %
305                (gntpass, ss.KeyToFilename(ss.SS_NODED_PASS),
306                 constants.SSL_CERT_FILE, gntpem,
307                 constants.NODE_INITD_SCRIPT))
308
309   result = sshrunner.Run(node, 'root', mycommand, batch=False,
310                          ask_key=ssh_key_check,
311                          use_cluster_key=False,
312                          strict_host_check=ssh_key_check)
313   if result.failed:
314     raise errors.OpExecError("Remote command on node %s, error: %s,"
315                              " output: %s" %
316                              (node, result.fail_reason, result.output))
317
318   return 0
319
320
321 def MasterFailover():
322   """Failover the master node.
323
324   This checks that we are not already the master, and will cause the
325   current master to cease being master, and the non-master to become
326   new master.
327
328   """
329   ss = ssconf.WritableSimpleStore()
330
331   new_master = utils.HostInfo().name
332   old_master = ss.GetMasterNode()
333
334   if old_master == new_master:
335     raise errors.OpPrereqError("This commands must be run on the node"
336                                " where you want the new master to be."
337                                " %s is already the master" %
338                                old_master)
339   # end checks
340
341   rcode = 0
342
343   logging.info("setting master to %s, old master: %s", new_master, old_master)
344
345   if not rpc.call_node_stop_master(old_master, True):
346     logging.error("could disable the master role on the old master"
347                  " %s, please disable manually", old_master)
348
349   ss.SetKey(ss.SS_MASTER_NODE, new_master)
350
351   cfg = config.ConfigWriter()
352
353   if not rpc.call_upload_file(cfg.GetNodeList(),
354                               ss.KeyToFilename(ss.SS_MASTER_NODE)):
355     logging.error("could not distribute the new simple store master file"
356                   " to the other nodes, please check.")
357
358   if not rpc.call_node_start_master(new_master, True):
359     logging.error("could not start the master role on the new master"
360                   " %s, please check", new_master)
361     rcode = 1
362
363   return rcode