From 9ff819da2c5a66d3c2fb6a5c71fb626c7126c5f6 Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Fri, 10 May 2019 23:42:19 +0530 Subject: [PATCH] systemvm: new qemu-guest-agent based patching for KVM (#3278) This introduces a new patching script for patching systemvms on KVM using qemu-guest-agent that runs inside the systemvm on startup. This also removes the vport device which was previously used by the legacy patching script and instead uses the modern and new uniform guest agent vport for host-guest communication. Also updates the sytemvmtemplate build config to use the latest Debian 9.9.0 iso. Signed-off-by: Rohit Yadav --- .../resource/LibvirtComputingResource.java | 22 +-- .../wrapper/LibvirtStartCommandWrapper.java | 7 +- .../resource/LibvirtDomainXMLParserTest.java | 22 +-- .../kvm/resource/LibvirtVMDefTest.java | 6 +- scripts/installer/windows/client.wxs | 2 +- scripts/vm/hypervisor/kvm/patch.sh | 80 ++++++++++ scripts/vm/hypervisor/kvm/patchviasocket.py | 76 --------- .../vm/hypervisor/kvm/test_patchviasocket.py | 144 ------------------ .../opt/cloud/bin/setup/cloud-early-config | 52 +++---- systemvm/debian/root/.ssh/authorized_keys | 1 - systemvm/pom.xml | 7 - .../scripts/configure_systemvm_services.sh | 2 +- .../appliance/systemvmtemplate/template.json | 4 +- 13 files changed, 123 insertions(+), 302 deletions(-) create mode 100755 scripts/vm/hypervisor/kvm/patch.sh delete mode 100755 scripts/vm/hypervisor/kvm/patchviasocket.py delete mode 100755 scripts/vm/hypervisor/kvm/test_patchviasocket.py delete mode 100644 systemvm/debian/root/.ssh/authorized_keys diff --git a/plugins/hypervisors/kvm/src/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java b/plugins/hypervisors/kvm/src/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java index 652f6431f0e..f5f38edbb2e 100644 --- a/plugins/hypervisors/kvm/src/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java +++ b/plugins/hypervisors/kvm/src/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java @@ -47,7 +47,6 @@ import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; -import com.cloud.resource.RequestWrapper; import org.apache.cloudstack.storage.to.PrimaryDataStoreTO; import org.apache.cloudstack.storage.to.TemplateObjectTO; import org.apache.cloudstack.storage.to.VolumeObjectTO; @@ -146,6 +145,7 @@ import com.cloud.hypervisor.kvm.storage.KVMStorageProcessor; import com.cloud.network.Networks.BroadcastDomainType; import com.cloud.network.Networks.RouterPrivateIpStrategy; import com.cloud.network.Networks.TrafficType; +import com.cloud.resource.RequestWrapper; import com.cloud.resource.ServerResource; import com.cloud.resource.ServerResourceBase; import com.cloud.storage.JavaStorageLayer; @@ -199,7 +199,7 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv private String _modifyVlanPath; private String _versionstringpath; - private String _patchViaSocketPath; + private String _patchScriptPath; private String _createvmPath; private String _manageSnapshotPath; private String _resizeVolumePath; @@ -682,9 +682,9 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv throw new ConfigurationException("Unable to find versions.sh"); } - _patchViaSocketPath = Script.findScript(kvmScriptsDir + "/patch/", "patchviasocket.py"); - if (_patchViaSocketPath == null) { - throw new ConfigurationException("Unable to find patchviasocket.py"); + _patchScriptPath = Script.findScript(kvmScriptsDir, "patch.sh"); + if (_patchScriptPath == null) { + throw new ConfigurationException("Unable to find patch.sh"); } _heartBeatPath = Script.findScript(kvmScriptsDir, "kvmheartbeat.sh"); @@ -1362,13 +1362,13 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv } public boolean passCmdLine(final String vmName, final String cmdLine) throws InternalErrorException { - final Script command = new Script(_patchViaSocketPath, 5 * 1000, s_logger); + final Script command = new Script(_patchScriptPath, 30 * 1000, s_logger); String result; command.add("-n", vmName); - command.add("-p", cmdLine.replaceAll(" ", "%")); + command.add("-c", cmdLine); result = command.execute(); if (result != null) { - s_logger.error("passcmd failed:" + result); + s_logger.error("Passing cmdline failed:" + result); return false; } return true; @@ -2141,12 +2141,6 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv final SerialDef serial = new SerialDef("pty", null, (short)0); devices.addDevice(serial); - /* Add a VirtIO channel for SystemVMs for communication and provisioning */ - if (vmTO.getType() != VirtualMachine.Type.User) { - devices.addDevice(new ChannelDef(vmTO.getName() + ".vport", ChannelDef.ChannelType.UNIX, - new File(_qemuSocketsPath + "/" + vmTO.getName() + ".agent"))); - } - if (_rngEnable) { final RngDef rngDevice = new RngDef(_rngPath, _rngBackendModel, _rngRateBytes, _rngRatePeriod); devices.addDevice(rngDevice); diff --git a/plugins/hypervisors/kvm/src/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStartCommandWrapper.java b/plugins/hypervisors/kvm/src/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStartCommandWrapper.java index 5a75f078f9e..18abb1c77b4 100644 --- a/plugins/hypervisors/kvm/src/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStartCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStartCommandWrapper.java @@ -115,7 +115,6 @@ public final class LibvirtStartCommandWrapper extends CommandWrapper" + "" + "" + - "" + - "" + - "" + - "" + - "
" + - "" + "" + "" + "" + @@ -215,14 +206,9 @@ public class LibvirtDomainXMLParserTest extends TestCase { assertEquals(channelType, channels.get(i).getChannelType()); } - /* SSVM provisioning port/channel */ - assertEquals(channelState, channels.get(0).getChannelState()); - assertEquals(new File(ssvmAgentPath), channels.get(0).getPath()); - assertEquals(ssvmAgentName, channels.get(0).getName()); - /* Qemu Guest Agent port/channel */ - assertEquals(new File(guestAgentPath), channels.get(1).getPath()); - assertEquals(guestAgentName, channels.get(1).getName()); + assertEquals(new File(guestAgentPath), channels.get(0).getPath()); + assertEquals(guestAgentName, channels.get(0).getName()); List ifs = parser.getInterfaces(); for (int i = 0; i < ifs.size(); i++) { diff --git a/plugins/hypervisors/kvm/test/com/cloud/hypervisor/kvm/resource/LibvirtVMDefTest.java b/plugins/hypervisors/kvm/test/com/cloud/hypervisor/kvm/resource/LibvirtVMDefTest.java index b391b94e740..6a06c11fca7 100644 --- a/plugins/hypervisors/kvm/test/com/cloud/hypervisor/kvm/resource/LibvirtVMDefTest.java +++ b/plugins/hypervisors/kvm/test/com/cloud/hypervisor/kvm/resource/LibvirtVMDefTest.java @@ -21,13 +21,13 @@ package com.cloud.hypervisor.kvm.resource; import java.io.File; -import junit.framework.TestCase; - import com.cloud.hypervisor.kvm.resource.LibvirtVMDef.ChannelDef; import com.cloud.hypervisor.kvm.resource.LibvirtVMDef.DiskDef; import com.cloud.hypervisor.kvm.resource.LibvirtVMDef.SCSIDef; import com.cloud.utils.Pair; +import junit.framework.TestCase; + public class LibvirtVMDefTest extends TestCase { public void testInterfaceEtehrnet() { @@ -180,7 +180,7 @@ public class LibvirtVMDefTest extends TestCase { public void testChannelDef() { ChannelDef.ChannelType type = ChannelDef.ChannelType.UNIX; ChannelDef.ChannelState state = ChannelDef.ChannelState.CONNECTED; - String name = "v-136-VM.vport"; + String name = "v-136-VM.org.qemu.guest_agent.0"; File path = new File("/var/lib/libvirt/qemu/" + name); ChannelDef channelDef = new ChannelDef(name, type, state, path); diff --git a/scripts/installer/windows/client.wxs b/scripts/installer/windows/client.wxs index ee09744fc7b..c54b76cc475 100644 --- a/scripts/installer/windows/client.wxs +++ b/scripts/installer/windows/client.wxs @@ -1046,7 +1046,7 @@ - + diff --git a/scripts/vm/hypervisor/kvm/patch.sh b/scripts/vm/hypervisor/kvm/patch.sh new file mode 100755 index 00000000000..e7c79fd9a73 --- /dev/null +++ b/scripts/vm/hypervisor/kvm/patch.sh @@ -0,0 +1,80 @@ +#!/bin/bash +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +set -e + +# Get the VM name and cmdline +while getopts "n:c:h" opt; do + case ${opt} in + n ) + name=$OPTARG + ;; + c ) + cmdline=$(echo $OPTARG | base64 -w 0) + ;; + h ) + echo "Usage: $0 -n [VM name] -c [command line]" + exit 0 + ;; + esac +done + +SSHKEY_FILE="/root/.ssh/id_rsa.pub.cloud" +if [ ! -e $SSHKEY_FILE ]; then + echo "SSH public key file $SSHKEY_FILE not found!" + exit 1 +fi + +if ! which virsh > /dev/null; then + echo "Libvirt CLI 'virsh' not found" + exit 1 +fi + +# Read the SSH public key +sshkey=$(cat $SSHKEY_FILE | base64 -w 0) + +# Method to send and write payload inside the VM +send_file() { + local name=${1} + local path=${2} + local content=${@:3} + local fd=$(virsh qemu-agent-command $name "{\"execute\":\"guest-file-open\", \"arguments\":{\"path\":\"$path\",\"mode\":\"w+\"}}" | sed 's/[^:]*:\([^}]*\).*/\1/') + virsh qemu-agent-command $name "{\"execute\":\"guest-file-write\", \"arguments\":{\"handle\":$fd,\"buf-b64\":\"$content\"}}" > /dev/null + virsh qemu-agent-command $name "{\"execute\":\"guest-file-close\", \"arguments\":{\"handle\":$fd}}" > /dev/null +} + +# Wait for the guest agent to come online +while ! virsh qemu-agent-command $name '{"execute":"guest-ping"}' >/dev/null 2>&1 +do + sleep 0.1 +done + +# Test guest agent sanity +while [ "$(virsh qemu-agent-command $name '{"execute":"guest-sync","arguments":{"id":1234567890}}' 2>/dev/null)" != '{"return":1234567890}' ] +do + sleep 0.1 +done + +# Write ssh public key +send_file $name "/root/.ssh/authorized_keys" $sshkey + +# Fix ssh public key permission +virsh qemu-agent-command $name '{"execute":"guest-exec","arguments":{"path":"chmod","arg":["go-rwx","/root/.ssh/authorized_keys"]}}' > /dev/null + +# Write cmdline payload +send_file $name "/var/cache/cloud/cmdline" $cmdline diff --git a/scripts/vm/hypervisor/kvm/patchviasocket.py b/scripts/vm/hypervisor/kvm/patchviasocket.py deleted file mode 100755 index c971d5dcc58..00000000000 --- a/scripts/vm/hypervisor/kvm/patchviasocket.py +++ /dev/null @@ -1,76 +0,0 @@ -#!/usr/bin/env python -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. - -# -# This script connects to the system vm socket and writes the -# authorized_keys and cmdline data to it. The system VM then -# reads it from /dev/vport0p1 in cloud_early_config -# - -import argparse -import os -import socket - -SOCK_FILE = "/var/lib/libvirt/qemu/{name}.agent" -PUB_KEY_FILE = "/root/.ssh/id_rsa.pub.cloud" -MESSAGE = "pubkey:{key}\ncmdline:{cmdline}\n" - - -def send_to_socket(sock_file, key_file, cmdline): - if not os.path.exists(key_file): - print("ERROR: ssh public key not found on host at {0}".format(key_file)) - return 1 - - try: - with open(key_file, "r") as f: - pub_key = f.read() - except IOError as e: - print("ERROR: unable to open {0} - {1}".format(key_file, e.strerror)) - return 1 - - # Keep old substitution from perl code: - cmdline = cmdline.replace("%", " ") - - msg = MESSAGE.format(key=pub_key, cmdline=cmdline) - - if not os.path.exists(sock_file): - print("ERROR: {0} socket not found".format(sock_file)) - return 1 - - try: - s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - s.connect(sock_file) - s.sendall(msg) - s.close() - except IOError as e: - print("ERROR: unable to connect to {0} - {1}".format(sock_file, e.strerror)) - return 1 - - return 0 # Success - - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Send configuration to system VM socket") - parser.add_argument("-n", "--name", required=True, help="Name of VM") - parser.add_argument("-p", "--cmdline", required=True, help="Command line") - - arguments = parser.parse_args() - - socket_file = SOCK_FILE.format(name=arguments.name) - - exit(send_to_socket(socket_file, PUB_KEY_FILE, arguments.cmdline)) diff --git a/scripts/vm/hypervisor/kvm/test_patchviasocket.py b/scripts/vm/hypervisor/kvm/test_patchviasocket.py deleted file mode 100755 index 6b411d32246..00000000000 --- a/scripts/vm/hypervisor/kvm/test_patchviasocket.py +++ /dev/null @@ -1,144 +0,0 @@ -#!/usr/bin/env python -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. - -import patchviasocket - -import getpass -import os -import socket -import tempfile -import time -import threading -import unittest - -KEY_DATA = "I luv\nCloudStack\n" -CMD_DATA = "/run/this-for-me --please=TRUE! very%quickly" -NON_EXISTING_FILE = "must-not-exist" - - -def write_key_file(): - _, tmpfile = tempfile.mkstemp(".sck") - with open(tmpfile, "w") as f: - f.write(KEY_DATA) - return tmpfile - - -class SocketThread(threading.Thread): - def __init__(self): - super(SocketThread, self).__init__() - self._data = "" - self._folder = tempfile.mkdtemp(".sck") - self._file = os.path.join(self._folder, "socket") - self._ready = False - - def data(self): - return self._data - - def file(self): - return self._file - - def wait_until_ready(self): - while not self._ready: - time.sleep(0.050) - - def run(self): - TIMEOUT = 0.314 # Very short time for tests that don't write to socket. - MAX_SIZE = 10 * 1024 - - s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - try: - s.bind(self._file) - s.listen(1) - s.settimeout(TIMEOUT) - try: - self._ready = True - client, address = s.accept() - self._data = client.recv(MAX_SIZE) - client.close() - except socket.timeout: - pass - finally: - s.close() - os.remove(self._file) - os.rmdir(self._folder) - - -class TestPatchViaSocket(unittest.TestCase): - def setUp(self): - self._key_file = write_key_file() - - self._unreadable = write_key_file() - os.chmod(self._unreadable, 0) - - self.assertFalse(os.path.exists(NON_EXISTING_FILE)) - self.assertNotEqual("root", getpass.getuser(), "must be non-root user (to test access denied errors)") - - def tearDown(self): - os.remove(self._key_file) - os.remove(self._unreadable) - - def test_write_to_socket(self): - reader = SocketThread() - reader.start() - reader.wait_until_ready() - self.assertEquals(0, patchviasocket.send_to_socket(reader.file(), self._key_file, CMD_DATA)) - reader.join() - data = reader.data() - self.assertIn(KEY_DATA, data) - self.assertIn(CMD_DATA.replace("%", " "), data) - self.assertNotIn("LUV", data) - self.assertNotIn("very%quickly", data) # Testing substitution - - def test_host_key_error(self): - reader = SocketThread() - reader.start() - reader.wait_until_ready() - self.assertEquals(1, patchviasocket.send_to_socket(reader.file(), NON_EXISTING_FILE, CMD_DATA)) - reader.join() # timeout - - def test_host_key_access_denied(self): - reader = SocketThread() - reader.start() - reader.wait_until_ready() - self.assertEquals(1, patchviasocket.send_to_socket(reader.file(), self._unreadable, CMD_DATA)) - reader.join() # timeout - - def test_nonexistant_socket_error(self): - reader = SocketThread() - reader.start() - reader.wait_until_ready() - self.assertEquals(1, patchviasocket.send_to_socket(NON_EXISTING_FILE, self._key_file, CMD_DATA)) - reader.join() # timeout - - def test_invalid_socket_error(self): - reader = SocketThread() - reader.start() - reader.wait_until_ready() - self.assertEquals(1, patchviasocket.send_to_socket(self._key_file, self._key_file, CMD_DATA)) - reader.join() # timeout - - def test_access_denied_socket_error(self): - reader = SocketThread() - reader.start() - reader.wait_until_ready() - self.assertEquals(1, patchviasocket.send_to_socket(self._unreadable, self._key_file, CMD_DATA)) - reader.join() # timeout - - -if __name__ == '__main__': - unittest.main() diff --git a/systemvm/debian/opt/cloud/bin/setup/cloud-early-config b/systemvm/debian/opt/cloud/bin/setup/cloud-early-config index 8ba7701de8a..6aa0314040c 100755 --- a/systemvm/debian/opt/cloud/bin/setup/cloud-early-config +++ b/systemvm/debian/opt/cloud/bin/setup/cloud-early-config @@ -90,41 +90,29 @@ get_boot_params() { sed -i "s/%/ /g" $CMDLINE ;; kvm) - VPORT=$(find /dev/virtio-ports -type l -name '*.vport' 2>/dev/null|head -1) - - if [ -z "$VPORT" ]; then - log_it "No suitable VirtIO port was found in /dev/virtio-ports" && exit 2 + # Use any old cmdline as backup only + if [ -s $CMDLINE ]; then + mv $CMDLINE $CMDLINE.old + log_it "Found a non-empty old cmdline file" fi - - if [ ! -e "$VPORT" ]; then - log_it "${VPORT} not loaded, perhaps guest kernel is too old." && exit 2 - fi - - local factor=2 - local progress=1 - for i in {1..5} - do - while read line; do - if [[ $line == cmdline:* ]]; then - cmd=${line//cmdline:/} - echo $cmd > $CMDLINE - elif [[ $line == pubkey:* ]]; then - pubkey=${line//pubkey:/} - echo $pubkey > /var/cache/cloud/authorized_keys - echo $pubkey > /root/.ssh/authorized_keys - fi - done < $VPORT - # In case of reboot we do not send the boot args again. - # So, no need to wait for them, as the boot args are already set at startup - if [ -s $CMDLINE ] - then - log_it "Found a non empty cmdline file. Will now exit the loop and proceed with configuration." - break; + systemctl enable --now qemu-guest-agent + # Wait for $CMDLINE file to be written by the qemu-guest-agent + for i in {1..60}; do + if [ -s $CMDLINE ]; then + log_it "Received a new non-empty cmdline file from qemu-guest-agent" + break fi - sleep ${progress}s - progress=$[ progress * factor ] + sleep 1 done - chmod go-rwx /root/.ssh/authorized_keys + # Use any old cmdline only when new cmdline is not received + if [ -e $CMDLINE.old ]; then + if [ -s $CMDLINE ]; then + rm $CMDLINE.old + else + mv $CMDLINE.old $CMDLINE + log_it "Using old cmdline file, VM was perhaps rebooted." + fi + fi ;; vmware) vmtoolsd --cmd 'machine.id.get' > $CMDLINE diff --git a/systemvm/debian/root/.ssh/authorized_keys b/systemvm/debian/root/.ssh/authorized_keys deleted file mode 100644 index c09f6379a34..00000000000 --- a/systemvm/debian/root/.ssh/authorized_keys +++ /dev/null @@ -1 +0,0 @@ -ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA2RIE3hgSAD8zULuyE7KDW9EKh2oVbNGY7iSL/VI5xHLISKh4e8ksTshWjlGBtrUCnuzR7y2BUxZ65RI8XkB1fEDxcOU4/0lVPvJYDSsGveXoOgpLwOtKRoGLgjFUGzBQlj2s6YaYQxoNTqtBVkDIH6ekPNq0Q38hRrFcsVIk1sFo5ejuvFxt2wx6APcFIQtHSNezEDO0GVUScDU1N1YEMMv1PU3M/SrcezkXrGl/efF3kWtY9L5xm7sojHMCCqsI38r8ogof67F7JdWRXM6Nl3VzkdCBzWGcyAl+cYfjzgOiBGXyAyYBk8qqzJjKwUOtdjfRvCyowA/0xBwMW1T7PQ== diff --git a/systemvm/pom.xml b/systemvm/pom.xml index 55fc12b9d95..a72e3e9aac7 100644 --- a/systemvm/pom.xml +++ b/systemvm/pom.xml @@ -87,12 +87,6 @@ agent.zip - - debian/root/.ssh - - authorized_keys - - @@ -167,7 +161,6 @@ systemvm.iso agent.zip cloud-scripts.tgz - authorized_keys diff --git a/tools/appliance/systemvmtemplate/scripts/configure_systemvm_services.sh b/tools/appliance/systemvmtemplate/scripts/configure_systemvm_services.sh index 48695014bd9..78d868d8b0f 100644 --- a/tools/appliance/systemvmtemplate/scripts/configure_systemvm_services.sh +++ b/tools/appliance/systemvmtemplate/scripts/configure_systemvm_services.sh @@ -19,7 +19,7 @@ set -e set -x -CLOUDSTACK_RELEASE=4.11.2 +CLOUDSTACK_RELEASE=4.11.3 function configure_apache2() { # Enable ssl, rewrite and auth diff --git a/tools/appliance/systemvmtemplate/template.json b/tools/appliance/systemvmtemplate/template.json index 1c62b1e0942..083a66c9cfa 100644 --- a/tools/appliance/systemvmtemplate/template.json +++ b/tools/appliance/systemvmtemplate/template.json @@ -38,8 +38,8 @@ "disk_interface": "virtio", "net_device": "virtio-net", - "iso_url": "https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/debian-9.6.0-amd64-netinst.iso", - "iso_checksum": "fcd77acbd46f33e0a266faf284acc1179ab0a3719e4b8abebac555307aa978aa242d7052c8d41e1a5fc6d1b30bc6ca6d62269e71526b71c9d5199b13339f0e25", + "iso_url": "https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/debian-9.9.0-amd64-netinst.iso", + "iso_checksum": "42d9818abc4a08681dc0638f07e7aeb35d0c44646ab1e5b05a31a71d76c99da52b6192db9a3e852171ac78c2ba6b110b337c0b562c7be3d32e86a105023a6a0c", "iso_checksum_type": "sha512", "vm_name": "systemvmtemplate",