diff --git a/cosmic-core/plugins/hypervisor/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceProperties.java b/cosmic-core/plugins/hypervisor/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceProperties.java index 9b58e38d56..012eb686f5 100644 --- a/cosmic-core/plugins/hypervisor/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceProperties.java +++ b/cosmic-core/plugins/hypervisor/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceProperties.java @@ -545,7 +545,7 @@ public static class Constants { public static final String DEFAULT_VM_RNG_PATH = "/dev/random"; public static final String SCRIPT_MODIFY_VLAN = "modifyvlan.sh"; public static final String SCRIPT_VERSIONS = "versions.sh"; - public static final String SCRIPT_PATCH_VIA_SOCKET = "patchviasocket.pl"; + public static final String SCRIPT_PATCH_VIA_SOCKET = "patchviasocket.py"; public static final String SCRIPT_KVM_HEART_BEAT = "kvmheartbeat.sh"; public static final String SCRIPT_CREATE_VM = "createvm.sh"; public static final String SCRIPT_MANAGE_SNAPSHOT = "managesnapshot.sh"; diff --git a/cosmic-core/scripts/src/main/resources/scripts/vm/hypervisor/kvm/patchviasocket.pl b/cosmic-core/scripts/src/main/resources/scripts/vm/hypervisor/kvm/patchviasocket.pl deleted file mode 100755 index e2679a5383..0000000000 --- a/cosmic-core/scripts/src/main/resources/scripts/vm/hypervisor/kvm/patchviasocket.pl +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/perl -w - -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; - -print "Sending: $msg\n"; -print "To: $sockfile\n"; -my $runCmd="echo \"$msg\n\" | /usr/bin/socat - UNIX-CONNECT:$sockfile"; -my $exit_code=system($runCmd); -if($exit_code!=0) -{ - print "Command $runCmd failed with an exit code of $exit_code.\n"; - exit($exit_code >> 8); -} -else -{ - print "Command $runCmd successful!\n"; -} \ No newline at end of file diff --git a/cosmic-core/scripts/src/main/resources/scripts/vm/hypervisor/kvm/patchviasocket.py b/cosmic-core/scripts/src/main/resources/scripts/vm/hypervisor/kvm/patchviasocket.py new file mode 100644 index 0000000000..c971d5dcc5 --- /dev/null +++ b/cosmic-core/scripts/src/main/resources/scripts/vm/hypervisor/kvm/patchviasocket.py @@ -0,0 +1,76 @@ +#!/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/cosmic-core/scripts/src/test/resources/scripts/vm/hypervisor/kvm/test_patchviasocket.py b/cosmic-core/scripts/src/test/resources/scripts/vm/hypervisor/kvm/test_patchviasocket.py new file mode 100644 index 0000000000..6b411d3224 --- /dev/null +++ b/cosmic-core/scripts/src/test/resources/scripts/vm/hypervisor/kvm/test_patchviasocket.py @@ -0,0 +1,144 @@ +#!/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()