diff --git a/slapos/recipe/kvm/__init__.py b/slapos/recipe/kvm/__init__.py index 1a6cfd6697d72e842a98a67a2b0f8ed5709702cb..c06db3aa971bf565bbe302b6108c1502dab15ff1 100644 --- a/slapos/recipe/kvm/__init__.py +++ b/slapos/recipe/kvm/__init__.py @@ -41,40 +41,40 @@ class Recipe(GenericBaseRecipe): '"virtio" value.' self.options['disk-type'] = 'virtio' - config = dict( - tap_interface=self.options['tap'], - vnc_ip=self.options['vnc-ip'], - vnc_port=self.options['vnc-port'], - nbd_ip=self.options['nbd-host'], - nbd_port=self.options['nbd-port'], - nbd2_ip=self.options.get('nbd2-host', ''), - nbd2_port=self.options.get('nbd2-port', 1024), - disk_path=self.options['disk-path'], - disk_size=self.options['disk-size'], - disk_type=self.options['disk-type'], - mac_address=self.options['mac-address'], - smp_count=self.options['smp-count'], - ram_size=self.options['ram-size'], - socket_path=self.options['socket-path'], - pid_file_path=self.options['pid-path'], - python_path=sys.executable, - shell_path=self.options['shell-path'], - qemu_path=self.options['qemu-path'], - qemu_img_path=self.options['qemu-img-path'], - vnc_passwd=self.options['passwd'], - default_disk_image=self.options['default-disk-image'], - ) + self.options['python-path'] = sys.executable + + path_list = [] + + if not self.isTrueValue(self.options.get('use-tap')): + # XXX This could be done using Jinja. + for port in self.options['nat-rules'].split(): + ipv6_port = int(port) + 10000 + tunnel_path = self.createExecutable( + '%s-%sto%s' % (self.options['6tunnel-wrapper-path'], port, ipv6_port), + self.substituteTemplate( + self.getTemplateFilename('6to4.in'), + { + 'ipv6': self.options['ipv6'], + 'ipv6_port': ipv6_port, + 'ipv4': self.options['ipv4'], + 'ipv4_port': port, + 'shell_path': self.options['shell-path'], + '6tunnel_path': self.options['6tunnel-path'], + }, + ), + ) + path_list.append(tunnel_path) - # Runners runner_path = self.createExecutable( - self.options['runner-path'], - self.substituteTemplate(self.getTemplateFilename('kvm_run.in'), - config)) + self.options['runner-path'], + self.substituteTemplate(self.getTemplateFilename('kvm_run.in'), + self.options)) + path_list.append(runner_path) controller_path = self.createExecutable( self.options['controller-path'], self.substituteTemplate(self.getTemplateFilename('kvm_controller_run.in'), - config)) + self.options)) - return [runner_path, controller_path] + return path_list diff --git a/slapos/recipe/kvm/template/6to4.in b/slapos/recipe/kvm/template/6to4.in new file mode 100644 index 0000000000000000000000000000000000000000..f829959323e142199005b9f00c73cd6b4bad6246 --- /dev/null +++ b/slapos/recipe/kvm/template/6to4.in @@ -0,0 +1,5 @@ +#!%(shell_path)s +# BEWARE: This file is operated by slapgrid +# BEWARE: It will be overwritten automatically +exec %(6tunnel_path)s -6 -4 -d -l %(ipv6)s %(ipv6_port)s %(ipv4)s %(ipv4_port)s + diff --git a/slapos/recipe/kvm/template/kvm_controller_run.in b/slapos/recipe/kvm/template/kvm_controller_run.in index a2c3ce66da1f99698aaee06efa12171dca038aa6..6d426a5019c27118d2490e050127141b5db7994a 100644 --- a/slapos/recipe/kvm/template/kvm_controller_run.in +++ b/slapos/recipe/kvm/template/kvm_controller_run.in @@ -1,4 +1,4 @@ -#!%(python_path)s +#!%(python-path)s # BEWARE: This file is operated by slapgrid # BEWARE: It will be overwritten automatically @@ -6,12 +6,15 @@ import socket import time +socket_path = '%(socket-path)s' +vnc_password = '%(vnc-passwd)s' + # Connect to KVM qmp socket so = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) connected = False while not connected: try: - so.connect('%(socket_path)s') + so.connect(socket_path) except socket.error: time.sleep(1) else: @@ -25,7 +28,7 @@ data = so.recv(1024) # Set VNC password so.send('{ "execute": "change", ' \ '"arguments": { "device": "vnc", "target": "password", ' \ - ' "arg": "%(vnc_passwd)s" } }') + ' "arg": "' + vnc_password + '" } }') data = so.recv(1024) # Finish diff --git a/slapos/recipe/kvm/template/kvm_run.in b/slapos/recipe/kvm/template/kvm_run.in index 9f4a4bd764edb2f0ef80fcc61d21a992e8302865..516d96af1008a20d58ef68759bb7e25314a29a49 100644 --- a/slapos/recipe/kvm/template/kvm_run.in +++ b/slapos/recipe/kvm/template/kvm_run.in @@ -1,14 +1,42 @@ -#!%(python_path)s +#!%(python-path)s # BEWARE: This file is operated by slapgrid # BEWARE: It will be overwritten automatically -# Echo client program +import hashlib import os import socket import subprocess +import urllib -# XXX: give all of this through parameter, don't use this as template -default_disk_image = '%(default_disk_image)s' +# XXX: give all of this through parameter, don't use this as template, but as module +qemu_img_path = '%(qemu-img-path)s' +qemu_path = '%(qemu-path)s' +disk_size = '%(disk-size)s' +disk_type = '%(disk-type)s' +socket_path = '%(socket-path)s' +nbd_list = (('%(nbd-host)s', %(nbd-port)s), ('%(nbd2-host)s', %(nbd2-port)s)) +default_disk_image = '%(default-disk-image)s' +disk_path = '%(disk-path)s' +virtual_hard_drive_url = '%(virtual-hard-drive-url)s'.strip() +virtual_hard_drive_md5_url = '%(virtual-hard-drive-md5-url)s'.strip() +nat_rules = '%(nat-rules)s'.strip() +use_tap = '%(use-tap)s' +tap_interface = '%(tap-interface)s' +listen_ip = '%(ipv4)s' +mac_address = '%(mac-address)s' +smp_count = '%(smp-count)s' +ram_size = '%(ram-size)s' +pid_file_path = '%(pid-file-path)s' + +def md5Checksum(file_path): + with open(file_path, 'rb') as fh: + m = hashlib.md5() + while True: + data = fh.read(8192) + if not data: + break + m.update(data) + return m.hexdigest() def getSocketStatus(host, port): s = None @@ -29,28 +57,44 @@ def getSocketStatus(host, port): break return s +# Download existing hard drive if needed at first boot +if not os.path.exists(disk_path) and virtual_hard_drive_url != '': + urllib.urlretrieve(virtual_hard_drive_url, disk_path) + local_md5sum = md5Checksum(disk_path) + md5sum = urllib.urlopen(virtual_hard_drive_md5_url).read().strip() + if local_md5sum != md5sum: + os.remove(disk_path) + raise Exception('MD5 mismatch.') + # Create disk if doesn't exist # XXX: move to Buildout profile -disk_path = '%(disk_path)s' if not os.path.exists(disk_path): - subprocess.Popen(['%(qemu_img_path)s', 'create' ,'-f', 'qcow2', - disk_path, '%(disk_size)sG']) + subprocess.Popen([qemu_img_path, 'create' ,'-f', 'qcow2', + disk_path, '%%sG' %% disk_size]) -kvm_argument_list = ['%(qemu_path)s', - '-enable-kvm', '-net', 'nic,macaddr=%(mac_address)s', - '-net', 'tap,ifname=%(tap_interface)s,script=no,downscript=no', - '-smp', '%(smp_count)s', - '-m', '%(ram_size)s', - '-drive', 'file=%(disk_path)s,if=%(disk_type)s', - '-vnc', '%(vnc_ip)s:1,ipv4,password', +# Generate network parameters +# XXX: use_tap should be a boolean +if use_tap == 'True': + qemu_network_parameter = 'tap,ifname=%%s,script=no,downscript=no' %% tap_interface +else: + qemu_network_parameter = 'user,' + ','.join('hostfwd=tcp:%%s:%%s-:%%s' %% (listen_ip, int(port) + 10000, port) for port in nat_rules.split()) + +kvm_argument_list = [qemu_path, + '-enable-kvm', '-net', 'nic,macaddr=%%s' %% mac_address, + '-net', qemu_network_parameter, + '-smp', smp_count, + '-m', ram_size, + '-drive', 'file=%%s,if=%%s' %% (disk_path, disk_type), + '-vnc', '%%s:1,ipv4,password' %% listen_ip, '-boot', 'menu=on', - '-qmp', 'unix:%(socket_path)s,server', - '-pidfile', '%(pid_file_path)s', + '-qmp', 'unix:%%s,server' %% socket_path, + '-pidfile', pid_file_path, ] -# Try to connect to NBD server (and second nbd if defined) -for nbd_ip, nbd_port in ( - ('%(nbd_ip)s', %(nbd_port)s), ('%(nbd2_ip)s', %(nbd2_port)s)): +# Try to connect to NBD server (and second nbd if defined). +# If not available, don't even specify it in qemu command line parameters. +# Reason: if qemu starts with unavailable NBD drive, it will just crash. +for nbd_ip, nbd_port in nbd_list: if nbd_ip and nbd_port: s = getSocketStatus(nbd_ip, nbd_port) if s is None: @@ -61,11 +105,10 @@ for nbd_ip, nbd_port in ( kvm_argument_list.extend([ '-drive', 'file=nbd:[%%s]:%%s,media=cdrom' %% (nbd_ip, nbd_port)]) - # If no NBD is specified/available: use internal disk image else: kvm_argument_list.extend([ '-drive', 'file=%%s,media=cdrom' %% default_disk_image ]) -os.execv('%(qemu_path)s', kvm_argument_list) +os.execv(qemu_path, kvm_argument_list) diff --git a/software/kvm/common.cfg b/software/kvm/common.cfg index 09f91f817382ed0320eb4e6a6a1fc390b5a599c4..014c1c9de5e243b943d44c337f65d82bfd4c6067 100644 --- a/software/kvm/common.cfg +++ b/software/kvm/common.cfg @@ -1,6 +1,7 @@ [buildout] extends = + ../../component/6tunnel/buildout.cfg ../../component/curl/buildout.cfg ../../component/dash/buildout.cfg ../../component/dcron/buildout.cfg @@ -84,7 +85,7 @@ mode = 0644 [template-kvm] recipe = slapos.recipe.template url = ${:_profile_base_location_}/instance-kvm.cfg.in -md5sum = f7c0e2172dac4ee70daae50f38d610ef +#md5sum = c3c888c78bbff334135be9e8ad5885a9 output = ${buildout:directory}/template-kvm.cfg mode = 0644 diff --git a/software/kvm/instance-kvm-input-schema.json b/software/kvm/instance-kvm-input-schema.json index 03e1251adf2791628b235bfd5349dd7b81d17480..cd71320a1baee2ff17818e4603dbf6143a8e5cb6 100644 --- a/software/kvm/instance-kvm-input-schema.json +++ b/software/kvm/instance-kvm-input-schema.json @@ -1,5 +1,8 @@ { - "name": "Input Parameters", + "type": "object", + "$schema": "http://json-schema.org/draft-04/schema", + + "title": "Input Parameters", "properties": { "ram-size": { "title": "RAM size", @@ -7,7 +10,7 @@ "type": "integer", "default": 1024, "minimum": 128, - "divisibleBy": 128, + "multipleOf": 128, "maximum": 16384 }, "disk-size": { @@ -34,7 +37,6 @@ "maximum": 8 }, - "nbd-host": { "title": "NBD hostname", "description": "hostname (or IP) of the NBD server containing the boot image.", @@ -65,6 +67,25 @@ "maximum": 65535 }, + "virtual-hard-drive-url": { + "title": "Existing disk image URL", + "description": "If specified, will download an existing disk image (qcow2, raw, ...), and will use it as main virtual hard drive. Can be used to download and use an already installed and customized virtual hard drive.", + "format": "uri", + "type": "string", + }, + + "use-tap": { + "title": "Use QEMU TAP network interface", + "description": "Use QEMU TAP network interface, requires a bridge on SlapOS Node. If false, use user-mode network stack (NAT).", + "type": "boolean", + "default": false + }, + "nat-rules": { + "title": "List of rules for NAT of QEMU user mode network stack.", + "description": "List of rules for NAT of QEMU user mode network stack, as comma-separated list of ports. For each port specified, it will redirect port x of the VM (example: 80) to the port x + 10000 of the public IPv6 (example: 10080). Defaults to \"22 80 443\". Ignored if \"use-tap\" parameter is enabled.", + "type": "string", + }, + "frontend-instance-guid": { "title": "Frontend Instance ID", diff --git a/software/kvm/instance-kvm-output-schema.json b/software/kvm/instance-kvm-output-schema.json index 725ba9f25a8bb29ef293eb4cfcbc7133ed28e170..51f3578a15dbbeafd24aca3d84a29add1b1aa8e9 100644 --- a/software/kvm/instance-kvm-output-schema.json +++ b/software/kvm/instance-kvm-output-schema.json @@ -13,13 +13,6 @@ "description": "URL used to connect to the service.", "type": "uri", "required": false - }, - - "password": { - "title": "Password", - "description": "Password used to authenticate in the service webpage.", - "type": "uri", - "required": true } } } diff --git a/software/kvm/instance-kvm.cfg.in b/software/kvm/instance-kvm.cfg.in index a49656e623932816c588d66f4ba04e58f59862b7..b4050a94b95b0974c155686dc9e7d356be71d142 100644 --- a/software/kvm/instance-kvm.cfg.in +++ b/software/kvm/instance-kvm.cfg.in @@ -45,32 +45,56 @@ recipe = slapos.cookbook:generate.password storage-path = $${directory:srv}/passwd bytes = 8 + [kvm-instance] # XXX-Cedric: change "KVM" recipe to simple "create wrappers". No need for this -# Specific code +# Specific code. It needs Jinja. recipe = slapos.cookbook:kvm -vnc-ip = $${slap-network-information:local-ipv4} + +vnc-passwd = $${gen-passwd:passwd} + +ipv4 = $${slap-network-information:local-ipv4} +ipv6 = $${slap-network-information:global-ipv6} +vnc-ip = $${:ipv4} + vnc-port = 5901 + +# XXX-Cedric: should be named "default-cdrom-iso" default-disk-image = ${debian-amd64-netinst.iso:location}/${debian-amd64-netinst.iso:filename} nbd-host = $${slap-parameter:nbd-host} nbd-port = $${slap-parameter:nbd-port} nbd2-host = $${slap-parameter:nbd2-host} nbd2-port = $${slap-parameter:nbd2-port} -tap = $${slap-network-information:network-interface} + +tap-interface = $${slap-network-information:network-interface} + disk-path = $${directory:srv}/virtual.qcow2 disk-size = $${slap-parameter:disk-size} disk-type = $${slap-parameter:disk-type} + socket-path = $${directory:var}/qmp_socket -pid-path = $${directory:run}/pid_file +pid-file-path = $${directory:run}/pid_file + smp-count = $${slap-parameter:cpu-count} ram-size = $${slap-parameter:ram-size} mac-address = $${create-mac:mac-address} + +# XXX-Cedric: should be named runner-wrapper-path and controller-wrapper-path runner-path = $${directory:services}/kvm controller-path = $${directory:scripts}/kvm_controller + +use-tap = $${slap-parameter:use-tap} +nat-rules = $${slap-parameter:nat-rules} +6tunnel-wrapper-path = $${directory:services}/6tunnel + +virtual-hard-drive-url = $${slap-parameter:virtual-hard-drive-url} +virtual-hard-drive-md5-url = $${slap-parameter:virtual-hard-drive-md5-url} + shell-path = ${dash:location}/bin/dash qemu-path = ${kvm:location}/bin/qemu-system-x86_64 qemu-img-path = ${kvm:location}/bin/qemu-img -passwd = $${gen-passwd:passwd} +6tunnel-path = ${6tunnel:location}/bin/6tunnel + [kvm-promise] recipe = slapos.cookbook:check_port_listening @@ -188,8 +212,8 @@ sla-instance_guid = $${slap-parameter:frontend-instance-guid} [publish-connection-information] recipe = slapos.cookbook:publish -backend-url = https://[$${novnc-instance:ip}]:$${novnc-instance:port}/vnc_auto.html?host=[$${novnc-instance:ip}]&port=$${novnc-instance:port}&encrypt=1&password=$${kvm-instance:passwd} -url = $${request-slave-frontend:connection-url}/vnc_auto.html?host=$${request-slave-frontend:connection-domainname}&port=$${request-slave-frontend:connection-port}&encrypt=1&path=$${request-slave-frontend:connection-resource}&password=$${kvm-instance:passwd} +backend-url = https://[$${novnc-instance:ip}]:$${novnc-instance:port}/vnc_auto.html?host=[$${novnc-instance:ip}]&port=$${novnc-instance:port}&encrypt=1&password=$${kvm-instance:vnc-passwd} +url = $${request-slave-frontend:connection-url}/vnc_auto.html?host=$${request-slave-frontend:connection-domainname}&port=$${request-slave-frontend:connection-port}&encrypt=1&path=$${request-slave-frontend:connection-resource}&password=$${kvm-instance:vnc-passwd} [frontend-promise] @@ -214,3 +238,9 @@ disk-size = 10 disk-type = virtio cpu-count = 1 + +nat-rules = 22 80 443 +use-tap = False + +virtual-hard-drive-url = +virtual-hard-drive-md5-url =