Convert patchviasocket to python (removes perl dependency for KVM agent)

As requested here: https://github.com/apache/cloudstack/pull/1495

No scripts are using perl so that install requirement can be removed.
The new scripts are using standard python packages only.
Includes extensive unit test.


Project: http://git-wip-us.apache.org/repos/asf/cloudstack/repo
Commit: http://git-wip-us.apache.org/repos/asf/cloudstack/commit/0acd3c12
Tree: http://git-wip-us.apache.org/repos/asf/cloudstack/tree/0acd3c12
Diff: http://git-wip-us.apache.org/repos/asf/cloudstack/diff/0acd3c12

Branch: refs/heads/master
Commit: 0acd3c12a2c40d4ccf89e4d531cf8de7c9d69d2a
Parents: 570b676
Author: Sverrir A. Berg <sver...@greenqloud.com>
Authored: Tue May 3 15:17:59 2016 +0000
Committer: Sverrir Berg <sver...@greenqloud.com>
Committed: Fri May 20 15:42:34 2016 +0000

----------------------------------------------------------------------
 .../kvm/resource/LibvirtComputingResource.java  |   4 +-
 scripts/installer/windows/client.wxs            |   2 +-
 scripts/vm/hypervisor/kvm/patchviasocket.pl     |  58 --------
 scripts/vm/hypervisor/kvm/patchviasocket.py     |  80 +++++++++++
 .../vm/hypervisor/kvm/test_patchviasocket.py    | 142 +++++++++++++++++++
 5 files changed, 225 insertions(+), 61 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cloudstack/blob/0acd3c12/plugins/hypervisors/kvm/src/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java
----------------------------------------------------------------------
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 17b3fd5..eea16ae 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");

http://git-wip-us.apache.org/repos/asf/cloudstack/blob/0acd3c12/scripts/installer/windows/client.wxs
----------------------------------------------------------------------
diff --git a/scripts/installer/windows/client.wxs 
b/scripts/installer/windows/client.wxs
index 414d813..f5aec48 100644
--- a/scripts/installer/windows/client.wxs
+++ b/scripts/installer/windows/client.wxs
@@ -1055,7 +1055,7 @@
                                             <File 
Id="fil47212822DDAFFCD71C79615CF4583BAF" KeyPath="yes" 
Source="!(wix.SourceClient)\WEB-INF\classes\scripts\vm\hypervisor\kvm\kvmheartbeat.sh"
 />
                                         </Component>
                                         <Component 
Id="cmpF2BBDD336FEC0B34B3C744ACF1E4B959" 
Guid="{56D8ECF7-49F8-4B26-A8F4-662252C0A647}">
-                                            <File 
Id="filD809C7F728AC5D1BD36E7DB403BFA141" KeyPath="yes" 
Source="!(wix.SourceClient)\WEB-INF\classes\scripts\vm\hypervisor\kvm\patchviasocket.pl"
 />
+                                            <File 
Id="filD809C7F728AC5D1BD36E7DB403BFA141" KeyPath="yes" 
Source="!(wix.SourceClient)\WEB-INF\classes\scripts\vm\hypervisor\kvm\patchviasocket.py"
 />
                                         </Component>
                                         <Component 
Id="cmp2F4D4D81563D153E86B0A652A83D363A" 
Guid="{7BFC7637-E33D-4BC4-8B25-3CDEA601110C}">
                                             <File 
Id="fil349420D6088A01C9F63E27634623F5BE" KeyPath="yes" 
Source="!(wix.SourceClient)\WEB-INF\classes\scripts\vm\hypervisor\kvm\setup_agent.sh"
 />

http://git-wip-us.apache.org/repos/asf/cloudstack/blob/0acd3c12/scripts/vm/hypervisor/kvm/patchviasocket.pl
----------------------------------------------------------------------
diff --git a/scripts/vm/hypervisor/kvm/patchviasocket.pl 
b/scripts/vm/hypervisor/kvm/patchviasocket.pl
deleted file mode 100755
index 7bcd245..0000000
--- 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 = <FILE>;
-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;
-

http://git-wip-us.apache.org/repos/asf/cloudstack/blob/0acd3c12/scripts/vm/hypervisor/kvm/patchviasocket.py
----------------------------------------------------------------------
diff --git a/scripts/vm/hypervisor/kvm/patchviasocket.py 
b/scripts/vm/hypervisor/kvm/patchviasocket.py
new file mode 100755
index 0000000..d9616c9
--- /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))

http://git-wip-us.apache.org/repos/asf/cloudstack/blob/0acd3c12/scripts/vm/hypervisor/kvm/test_patchviasocket.py
----------------------------------------------------------------------
diff --git a/scripts/vm/hypervisor/kvm/test_patchviasocket.py 
b/scripts/vm/hypervisor/kvm/test_patchviasocket.py
new file mode 100755
index 0000000..074b159
--- /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()

Reply via email to