Revert "Generate new ca_email for ssl cert issuer for each software instance."
[slapos.git] / slapos / recipe / kvm / __init__.py
1 ##############################################################################
2 #
3 # Copyright (c) 2010 Vifib SARL and Contributors. All Rights Reserved.
4 #
5 # WARNING: This program as such is intended to be used by professional
6 # programmers who take the whole responsibility of assessing all potential
7 # consequences resulting from its eventual inadequacies and bugs
8 # End users who are looking for a ready-to-use solution with commercial
9 # guarantees and support are strongly adviced to contract a Free Software
10 # Service Company
11 #
12 # This program is Free Software; you can redistribute it and/or
13 # modify it under the terms of the GNU General Public License
14 # as published by the Free Software Foundation; either version 3
15 # of the License, or (at your option) any later version.
16 #
17 # This program is distributed in the hope that it will be useful,
18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # GNU General Public License for more details.
21 #
22 # You should have received a copy of the GNU General Public License
23 # along with this program; if not, write to the Free Software
24 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
25 #
26 ##############################################################################
27 import os
28 import sys
29 from slapos.recipe.librecipe import BaseSlapRecipe
30 import subprocess
31 import binascii
32 import random
33 import zc.buildout
34 import pkg_resources
35 import ConfigParser
36 import hashlib
37
38 class Recipe(BaseSlapRecipe):
39
40 # To avoid magic numbers
41 VNC_BASE_PORT = 5900
42
43 def _install(self):
44 """
45 Set the connection dictionnary for the computer partition and create a list
46 of paths to the different wrappers
47
48 Parameters : none
49
50 Returns : List path_list
51 """
52 self.path_list = []
53
54 self.requirements, self.ws = self.egg.working_set()
55 self.cron_d = self.installCrond()
56
57 self.ca_conf = self.installCertificateAuthority()
58 self.key_path, self.certificate_path = self.requestCertificate('noVNC')
59
60 # Install the socket_connection_attempt script
61 catcher = zc.buildout.easy_install.scripts(
62 [('check_port_listening', 'slapos.recipe.kvm.socket_connection_attempt',
63 'connection_attempt')],
64 self.ws,
65 sys.executable,
66 self.bin_directory,
67 )
68 # Save the check_port_listening script path
69 check_port_listening_script = catcher[0]
70 # Get the port_listening_promise template path, and save it
71 self.port_listening_promise_path = pkg_resources.resource_filename(
72 __name__, 'template/port_listening_promise.in')
73 self.port_listening_promise_conf = dict(
74 check_port_listening_script=check_port_listening_script,
75 )
76
77 kvm_conf = self.installKvm(vnc_ip = self.getLocalIPv4Address())
78
79 vnc_port = Recipe.VNC_BASE_PORT + kvm_conf['vnc_display']
80
81 noVNC_conf = self.installNoVnc(source_ip = self.getGlobalIPv6Address(),
82 source_port = 6080,
83 target_ip = kvm_conf['vnc_ip'],
84 target_port = vnc_port)
85
86 self.linkBinary()
87 self.computer_partition.setConnectionDict(dict(
88 url = "https://[%s]:%s/vnc_auto.html?host=[%s]&port=%s&encrypt=1" % (
89 noVNC_conf['source_ip'],
90 noVNC_conf['source_port'],
91 noVNC_conf['source_ip'],
92 noVNC_conf['source_port']),
93 password = kvm_conf['vnc_passwd']))
94
95 return self.path_list
96
97 def installKvm(self, vnc_ip):
98 """
99 Create kvm configuration dictionnary and instanciate a wrapper for kvm and
100 kvm controller
101
102 Parameters : IP the vnc server is listening on
103
104 Returns : Dictionnary kvm_conf
105 """
106 kvm_conf = dict(vnc_ip = vnc_ip)
107
108 connection_found = False
109 for tap_interface, dummy in self.parameter_dict['ip_list']:
110 # Get an ip associated to a tap interface
111 if tap_interface:
112 connection_found = True
113 if not connection_found:
114 raise NotImplementedError("Do not support ip without tap interface")
115
116 kvm_conf['tap_interface'] = tap_interface
117
118 # Disk path
119 kvm_conf['disk_path'] = os.path.join(self.data_root_directory,
120 'virtual.qcow2')
121 kvm_conf['socket_path'] = os.path.join(self.var_directory, 'qmp_socket')
122 # XXX Weak password
123 ##XXX -Vivien: add an option to generate one password for all instances
124 # and/or to input it yourself
125 kvm_conf['vnc_passwd'] = binascii.hexlify(os.urandom(4))
126
127 #XXX pid_file path, database_path, path to python binary and xml path
128 kvm_conf['pid_file_path'] = os.path.join(self.run_directory, 'pid_file')
129 kvm_conf['database_path'] = os.path.join(self.data_root_directory,
130 'slapmonitor_database')
131 kvm_conf['python_path'] = sys.executable
132 kvm_conf['qemu_path'] = self.options['qemu_path']
133 #xml_path = os.path.join(self.var_directory, 'slapreport.xml' )
134
135 # Create disk if needed
136 if not os.path.exists(kvm_conf['disk_path']):
137 retcode = subprocess.call(["%s create -f qcow2 %s %iG" % (
138 self.options['qemu_img_path'], kvm_conf['disk_path'],
139 int(self.options['disk_size']))], shell=True)
140 if retcode != 0:
141 raise OSError, "Disk creation failed!"
142
143 # Options nbd_ip and nbd_port are provided by slapos master
144 kvm_conf['nbd_ip'] = self.parameter_dict['nbd_ip']
145 kvm_conf['nbd_port'] = self.parameter_dict['nbd_port']
146
147 # First octet has to represent a locally administered address
148 octet_list = [254] + [random.randint(0x00, 0xff) for x in range(5)]
149 kvm_conf['mac_address'] = ':'.join(['%02x' % x for x in octet_list])
150
151 kvm_conf['hostname'] = "slaposkvm"
152 kvm_conf['smp_count'] = self.options['smp_count']
153 kvm_conf['ram_size'] = self.options['ram_size']
154
155 kvm_conf['vnc_display'] = 1
156
157 # Instanciate KVM
158 kvm_template_location = pkg_resources.resource_filename(
159 __name__, os.path.join(
160 'template', 'kvm_run.in'))
161
162 kvm_runner_path = self.createRunningWrapper("kvm",
163 self.substituteTemplate(kvm_template_location,
164 kvm_conf))
165
166 self.path_list.append(kvm_runner_path)
167
168 # Instanciate KVM controller
169 kvm_controller_template_location = pkg_resources.resource_filename(
170 __name__, os.path.join(
171 'template',
172 'kvm_controller_run.in' ))
173
174 kvm_controller_runner_path = self.createRunningWrapper("kvm_controller",
175 self.substituteTemplate(kvm_controller_template_location,
176 kvm_conf))
177
178 self.path_list.append(kvm_controller_runner_path)
179
180 # Instanciate Slapmonitor
181 ##slapmonitor_runner_path = self.instanciate_wrapper("slapmonitor",
182 # [database_path, pid_file_path, python_path])
183 # Instanciate Slapreport
184 ##slapreport_runner_path = self.instanciate_wrapper("slapreport",
185 # [database_path, python_path])
186
187 # Add VNC promise
188 self.port_listening_promise_conf.update(
189 hostname=kvm_conf['vnc_ip'],
190 port=Recipe.VNC_BASE_PORT + kvm_conf['vnc_display'],
191 )
192 self.createPromiseWrapper("vnc_promise",
193 self.substituteTemplate(self.port_listening_promise_path,
194 self.port_listening_promise_conf,
195 )
196 )
197
198 return kvm_conf
199
200 def installNoVnc(self, source_ip, source_port, target_ip, target_port):
201 """
202 Create noVNC configuration dictionnary and instanciate Websockify proxy
203
204 Parameters : IP of the proxy, port on which is situated the proxy,
205 IP of the vnc server, port on which is situated the vnc server,
206 path to python binary
207
208 Returns : noVNC configuration dictionnary
209 """
210
211 noVNC_conf = {}
212
213 noVNC_conf['source_ip'] = source_ip
214 noVNC_conf['source_port'] = source_port
215
216 execute_arguments = [[
217 self.options['websockify'].strip(),
218 '--web',
219 self.options['noVNC_location'],
220 '--key=%s' % (self.key_path),
221 '--cert=%s' % (self.certificate_path),
222 '--ssl-only',
223 '%s:%s' % (source_ip, source_port),
224 '%s:%s' % (target_ip, target_port)],
225 [self.certificate_path, self.key_path]]
226
227 self.path_list.extend(zc.buildout.easy_install.scripts([('websockify',
228 'slapos.recipe.librecipe.execute', 'execute_wait')], self.ws, sys.executable,
229 self.wrapper_directory, arguments=execute_arguments))
230
231 # Add noVNC promise
232 self.port_listening_promise_conf.update(hostname=noVNC_conf['source_ip'],
233 port=noVNC_conf['source_port'],
234 )
235 self.createPromiseWrapper("novnc_promise",
236 self.substituteTemplate(self.port_listening_promise_path,
237 self.port_listening_promise_conf,
238 )
239 )
240
241 return noVNC_conf
242
243 def linkBinary(self):
244 """Links binaries to instance's bin directory for easier exposal"""
245 for linkline in self.options.get('link_binary_list', '').splitlines():
246 if not linkline:
247 continue
248 target = linkline.split()
249 if len(target) == 1:
250 target = target[0]
251 path, linkname = os.path.split(target)
252 else:
253 linkname = target[1]
254 target = target[0]
255 link = os.path.join(self.bin_directory, linkname)
256 if os.path.lexists(link):
257 if not os.path.islink(link):
258 raise zc.buildout.UserError(
259 'Target link already %r exists but it is not link' % link)
260 os.unlink(link)
261 os.symlink(target, link)
262 self.logger.debug('Created link %r -> %r' % (link, target))
263 self.path_list.append(link)
264
265 def installCertificateAuthority(self, ca_country_code='XX',
266 ca_email='xx@example.com', ca_state='State', ca_city='City',
267 ca_company='Company'):
268 backup_path = self.createBackupDirectory('ca')
269 self.ca_dir = os.path.join(self.data_root_directory, 'ca')
270 self._createDirectory(self.ca_dir)
271 self.ca_request_dir = os.path.join(self.ca_dir, 'requests')
272 self._createDirectory(self.ca_request_dir)
273 config = dict(ca_dir=self.ca_dir, request_dir=self.ca_request_dir)
274 self.ca_private = os.path.join(self.ca_dir, 'private')
275 self.ca_certs = os.path.join(self.ca_dir, 'certs')
276 self.ca_crl = os.path.join(self.ca_dir, 'crl')
277 self.ca_newcerts = os.path.join(self.ca_dir, 'newcerts')
278 self.ca_key_ext = '.key'
279 self.ca_crt_ext = '.crt'
280 for d in [self.ca_private, self.ca_crl, self.ca_newcerts, self.ca_certs]:
281 self._createDirectory(d)
282 for f in ['crlnumber', 'serial']:
283 if not os.path.exists(os.path.join(self.ca_dir, f)):
284 open(os.path.join(self.ca_dir, f), 'w').write('01')
285 if not os.path.exists(os.path.join(self.ca_dir, 'index.txt')):
286 open(os.path.join(self.ca_dir, 'index.txt'), 'w').write('')
287 openssl_configuration = os.path.join(self.ca_dir, 'openssl.cnf')
288 config.update(
289 working_directory=self.ca_dir,
290 country_code=ca_country_code,
291 state=ca_state,
292 city=ca_city,
293 company=ca_company,
294 email_address=ca_email,
295 )
296 self._writeFile(openssl_configuration, pkg_resources.resource_string(
297 __name__, 'template/openssl.cnf.ca.in') % config)
298 self.path_list.extend(zc.buildout.easy_install.scripts([
299 ('certificate_authority',
300 __name__ + '.certificate_authority', 'runCertificateAuthority')],
301 self.ws, sys.executable, self.wrapper_directory, arguments=[dict(
302 openssl_configuration=openssl_configuration,
303 openssl_binary=self.options['openssl_binary'],
304 certificate=os.path.join(self.ca_dir, 'cacert.pem'),
305 key=os.path.join(self.ca_private, 'cakey.pem'),
306 crl=os.path.join(self.ca_crl),
307 request_dir=self.ca_request_dir
308 )]))
309 # configure backup
310 backup_cron = os.path.join(self.cron_d, 'ca_rdiff_backup')
311 open(backup_cron, 'w').write(
312 '''0 0 * * * %(rdiff_backup)s %(source)s %(destination)s'''%dict(
313 rdiff_backup=self.options['rdiff_backup_binary'],
314 source=self.ca_dir,
315 destination=backup_path))
316 self.path_list.append(backup_cron)
317
318 return dict(
319 ca_certificate=os.path.join(config['ca_dir'], 'cacert.pem'),
320 ca_crl=os.path.join(config['ca_dir'], 'crl'),
321 certificate_authority_path=config['ca_dir']
322 )
323
324 def requestCertificate(self, name):
325 hash = hashlib.sha512(name).hexdigest()
326 key = os.path.join(self.ca_private, hash + self.ca_key_ext)
327 certificate = os.path.join(self.ca_certs, hash + self.ca_crt_ext)
328 parser = ConfigParser.RawConfigParser()
329 parser.add_section('certificate')
330 parser.set('certificate', 'name', name)
331 parser.set('certificate', 'key_file', key)
332 parser.set('certificate', 'certificate_file', certificate)
333 parser.write(open(os.path.join(self.ca_request_dir, hash), 'w'))
334 return key, certificate
335
336 def installCrond(self):
337 timestamps = self.createDataDirectory('cronstamps')
338 cron_output = os.path.join(self.log_directory, 'cron-output')
339 self._createDirectory(cron_output)
340 catcher = zc.buildout.easy_install.scripts([('catchcron',
341 __name__ + '.catdatefile', 'catdatefile')], self.ws, sys.executable,
342 self.bin_directory, arguments=[cron_output])[0]
343 self.path_list.append(catcher)
344 cron_d = os.path.join(self.etc_directory, 'cron.d')
345 crontabs = os.path.join(self.etc_directory, 'crontabs')
346 self._createDirectory(cron_d)
347 self._createDirectory(crontabs)
348 # Use execute from erp5.
349 wrapper = zc.buildout.easy_install.scripts([('crond',
350 'slapos.recipe.librecipe.execute', 'execute')], self.ws, sys.executable,
351 self.wrapper_directory, arguments=[
352 self.options['dcrond_binary'].strip(), '-s', cron_d, '-c', crontabs,
353 '-t', timestamps, '-f', '-l', '5', '-M', catcher]
354 )[0]
355 self.path_list.append(wrapper)
356 return cron_d