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()