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 17b3fd586ab..eea16ae9531 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
@@ -633,9 +633,9 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv
throw new ConfigurationException("Unable to find versions.sh");
}
- _patchViaSocketPath = Script.findScript(kvmScriptsDir + "/patch/", "patchviasocket.pl");
+ _patchViaSocketPath = Script.findScript(kvmScriptsDir + "/patch/", "patchviasocket.py");
if (_patchViaSocketPath == null) {
- throw new ConfigurationException("Unable to find patchviasocket.pl");
+ throw new ConfigurationException("Unable to find patchviasocket.py");
}
_heartBeatPath = Script.findScript(kvmScriptsDir, "kvmheartbeat.sh");
diff --git a/scripts/installer/windows/client.wxs b/scripts/installer/windows/client.wxs
index 414d8134759..f5aec48bde4 100644
--- a/scripts/installer/windows/client.wxs
+++ b/scripts/installer/windows/client.wxs
@@ -1055,7 +1055,7 @@
-
+
diff --git a/scripts/vm/hypervisor/kvm/patchviasocket.pl b/scripts/vm/hypervisor/kvm/patchviasocket.pl
deleted file mode 100755
index 7bcd245bc38..00000000000
--- a/scripts/vm/hypervisor/kvm/patchviasocket.pl
+++ /dev/null
@@ -1,58 +0,0 @@
-#!/usr/bin/perl -w
-# 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
-#############################################################
-
-use strict;
-use Getopt::Std;
-use IO::Socket;
-$|=1;
-
-my $opts = {};
-getopt('pn',$opts);
-my $name = $opts->{n};
-my $cmdline = $opts->{p};
-my $sockfile = "/var/lib/libvirt/qemu/$name.agent";
-my $pubkeyfile = "/root/.ssh/id_rsa.pub.cloud";
-
-if (! -S $sockfile) {
- print "ERROR: $sockfile socket not found\n";
- exit 1;
-}
-
-if (! -f $pubkeyfile) {
- print "ERROR: ssh public key not found on host at $pubkeyfile\n";
- exit 1;
-}
-
-open(FILE,$pubkeyfile) or die "ERROR: unable to open $pubkeyfile - $^E";
-my $key = ;
-close FILE;
-
-$cmdline =~ s/%/ /g;
-my $msg = "pubkey:" . $key . "\ncmdline:" . $cmdline;
-
-my $socket = IO::Socket::UNIX->new(Peer=>$sockfile,Type=>SOCK_STREAM)
- or die "ERROR: unable to connect to $sockfile - $^E\n";
-print $socket "$msg\n";
-close $socket;
-
diff --git a/scripts/vm/hypervisor/kvm/patchviasocket.py b/scripts/vm/hypervisor/kvm/patchviasocket.py
new file mode 100755
index 00000000000..d9616c9e8b9
--- /dev/null
+++ b/scripts/vm/hypervisor/kvm/patchviasocket.py
@@ -0,0 +1,80 @@
+#!/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 read_pub_key(key_file):
+ try:
+ if os.path.isfile(key_file):
+ with open(key_file, "r") as f:
+ return f.read()
+ except IOError:
+ return None
+
+
+def send_to_socket(sock_file, key_file, cmdline):
+ pub_key = read_pub_key(key_file)
+
+ if not pub_key:
+ print("ERROR: ssh public key not found on host at {0}".format(key_file))
+ 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
new file mode 100755
index 00000000000..074b159a7a6
--- /dev/null
+++ b/scripts/vm/hypervisor/kvm/test_patchviasocket.py
@@ -0,0 +1,142 @@
+#!/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.mktemp(".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._file = tempfile.mktemp(".sck")
+ 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)
+ 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
+ s.close()
+ os.remove(self._file)
+
+
+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_read_file(self):
+ pub_key = patchviasocket.read_pub_key(self._key_file)
+ self.assertEqual(KEY_DATA, pub_key)
+
+ def test_read_file_error(self):
+ self.assertIsNone(patchviasocket.read_pub_key(NON_EXISTING_FILE))
+ self.assertIsNone(patchviasocket.read_pub_key(self._unreadable))
+ self.assertIsNone(patchviasocket.read_pub_key("/tmp")) # folder is not a file
+
+ 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_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()