This is an automated email from the ASF dual-hosted git repository.

dahn pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cloudstack.git


The following commit(s) were added to refs/heads/master by this push:
     new 750abf3  FEATURE-3823: kvm agent hooks (#3839)
750abf3 is described below

commit 750abf355171303f3f9edc8dd464b166f270b812
Author: Bitworks LLC <[email protected]>
AuthorDate: Sat Mar 14 15:22:08 2020 +0700

    FEATURE-3823: kvm agent hooks (#3839)
---
 agent/conf/agent.properties                        | 25 ++++++
 plugins/hypervisors/kvm/pom.xml                    |  5 ++
 .../kvm/resource/LibvirtComputingResource.java     | 70 ++++++++++++++++
 .../kvm/resource/LibvirtKvmAgentHook.java          | 76 +++++++++++++++++
 .../wrapper/LibvirtStartCommandWrapper.java        | 32 +++++++-
 .../wrapper/LibvirtStopCommandWrapper.java         | 13 +++
 .../kvm/resource/LibvirtKvmAgentHookTest.java      | 94 ++++++++++++++++++++++
 7 files changed, 314 insertions(+), 1 deletion(-)

diff --git a/agent/conf/agent.properties b/agent/conf/agent.properties
index bb9bf40..85c85a5 100644
--- a/agent/conf/agent.properties
+++ b/agent/conf/agent.properties
@@ -97,6 +97,31 @@ domr.scripts.dir=scripts/network/domr/kvm
 # migration will finish quickly.  Less than 1 means disabled.
 #vm.migrate.pauseafter=0
 
+# Agent hooks is the way to override default agent behavior to extend the 
functionality without excessive coding
+# for a custom deployment. The first hook promoted is 
libvirt-vm-xml-transformer which allows provider to modify
+# VM XML specification before send to libvirt. Hooks are implemented in Groovy 
and must be implemented in the way
+# to keep default CS behaviour is something goes wrong.
+# All hooks are located in a special directory defined in 'agent.hooks.basedir'
+#
+# agent.hooks.basedir=/etc/cloudstack/agent/hooks
+
+# every hook has two major attributes - script name, specified in 
'agent.hooks.*.script' and method name
+# specified in 'agent.hooks.*.method'.
+
+# Libvirt XML transformer hook does XML-to-XML transformation which provider 
can use to add/remove/modify some
+# sort of attributes in Libvirt XML domain specification.
+# 
agent.hooks.libvirt_vm_xml_transformer.script=libvirt-vm-xml-transformer.groovy
+# agent.hooks.libvirt_vm_xml_transformer.method=transform
+#
+# The hook is called right after libvirt successfuly launched VM
+# agent.hooks.libvirt_vm_on_start.script=libvirt-vm-state-change.groovy
+# agent.hooks.libvirt_vm_on_start.method=onStart
+#
+# The hook is called right after libvirt successfuly stopped VM
+# agent.hooks.libvirt_vm_on_stop.script=libvirt-vm-state-change.groovy
+# agent.hooks.libvirt_vm_on_stop.method=onStop
+#
+
 # set the type of bridge used on the hypervisor, this defines what commands 
the resource 
 # will use to setup networking. Currently supported NATIVE, OPENVSWITCH
 #network.bridge.type=native
diff --git a/plugins/hypervisors/kvm/pom.xml b/plugins/hypervisors/kvm/pom.xml
index 3c3a635..9d0c786 100644
--- a/plugins/hypervisors/kvm/pom.xml
+++ b/plugins/hypervisors/kvm/pom.xml
@@ -29,6 +29,11 @@
     </parent>
     <dependencies>
         <dependency>
+            <groupId>org.codehaus.groovy</groupId>
+            <artifactId>groovy-all</artifactId>
+            <version>${cs.groovy.version}</version>
+        </dependency>
+        <dependency>
             <groupId>commons-io</groupId>
             <artifactId>commons-io</artifactId>
         </dependency>
diff --git 
a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java
 
b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java
index fd9075e..79958ef 100644
--- 
a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java
+++ 
b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java
@@ -289,6 +289,18 @@ public class LibvirtComputingResource extends 
ServerResourceBase implements Serv
     protected String _rngPath = "/dev/random";
     protected int _rngRatePeriod = 1000;
     protected int _rngRateBytes = 2048;
+    protected String _agentHooksBasedir = "/etc/cloudstack/agent/hooks";
+
+    protected String _agentHooksLibvirtXmlScript = 
"libvirt-vm-xml-transformer.groovy";
+    protected String _agentHooksLibvirtXmlMethod = "transform";
+
+    protected String _agentHooksVmOnStartScript = 
"libvirt-vm-state-change.groovy";
+    protected String _agentHooksVmOnStartMethod = "onStart";
+
+    protected String _agentHooksVmOnStopScript = 
"libvirt-vm-state-change.groovy";
+    protected String _agentHooksVmOnStopMethod = "onStop";
+
+
     protected File _qemuSocketsPath;
     private final String _qemuGuestAgentSocketName = "org.qemu.guest_agent.0";
     protected WatchDogAction _watchDogAction = WatchDogAction.NONE;
@@ -391,6 +403,18 @@ public class LibvirtComputingResource extends 
ServerResourceBase implements Serv
         return new ExecutionResult(true, null);
     }
 
+    public LibvirtKvmAgentHook getTransformer() throws IOException {
+        return new LibvirtKvmAgentHook(_agentHooksBasedir, 
_agentHooksLibvirtXmlScript, _agentHooksLibvirtXmlMethod);
+    }
+
+    public LibvirtKvmAgentHook getStartHook() throws IOException {
+        return new LibvirtKvmAgentHook(_agentHooksBasedir, 
_agentHooksVmOnStartScript, _agentHooksVmOnStartMethod);
+    }
+
+    public LibvirtKvmAgentHook getStopHook() throws IOException {
+        return new LibvirtKvmAgentHook(_agentHooksBasedir, 
_agentHooksVmOnStopScript, _agentHooksVmOnStopMethod);
+    }
+
     public LibvirtUtilitiesHelper getLibvirtUtilitiesHelper() {
         return libvirtUtilitiesHelper;
     }
@@ -1097,6 +1121,8 @@ public class LibvirtComputingResource extends 
ServerResourceBase implements Serv
         value = (String) params.get("vm.migrate.pauseafter");
         _migratePauseAfter = NumbersUtil.parseInt(value, -1);
 
+        configureAgentHooks(params);
+
         value = (String)params.get("vm.migrate.speed");
         _migrateSpeed = NumbersUtil.parseInt(value, -1);
         if (_migrateSpeed == -1) {
@@ -1155,6 +1181,50 @@ public class LibvirtComputingResource extends 
ServerResourceBase implements Serv
         return true;
     }
 
+    private void configureAgentHooks(final Map<String, Object> params) {
+        String value = (String) params.get("agent.hooks.basedir");
+        if (null != value) {
+            _agentHooksBasedir = value;
+        }
+        s_logger.debug("agent.hooks.basedir is " + _agentHooksBasedir);
+
+        value = (String) 
params.get("agent.hooks.libvirt_vm_xml_transformer.script");
+        if (null != value) {
+            _agentHooksLibvirtXmlScript = value;
+        }
+        s_logger.debug("agent.hooks.libvirt_vm_xml_transformer.script is " + 
_agentHooksLibvirtXmlScript);
+
+        value = (String) 
params.get("agent.hooks.libvirt_vm_xml_transformer.method");
+        if (null != value) {
+            _agentHooksLibvirtXmlMethod = value;
+        }
+        s_logger.debug("agent.hooks.libvirt_vm_xml_transformer.method is " + 
_agentHooksLibvirtXmlMethod);
+
+        value = (String) params.get("agent.hooks.libvirt_vm_on_start.script");
+        if (null != value) {
+            _agentHooksVmOnStartScript = value;
+        }
+        s_logger.debug("agent.hooks.libvirt_vm_on_start.script is " + 
_agentHooksVmOnStartScript);
+
+        value = (String) params.get("agent.hooks.libvirt_vm_on_start.method");
+        if (null != value) {
+            _agentHooksVmOnStartMethod = value;
+        }
+        s_logger.debug("agent.hooks.libvirt_vm_on_start.method is " + 
_agentHooksVmOnStartMethod);
+
+        value = (String) params.get("agent.hooks.libvirt_vm_on_stop.script");
+        if (null != value) {
+            _agentHooksVmOnStopScript = value;
+        }
+        s_logger.debug("agent.hooks.libvirt_vm_on_stop.script is " + 
_agentHooksVmOnStopScript);
+
+        value = (String) params.get("agent.hooks.libvirt_vm_on_stop.method");
+        if (null != value) {
+            _agentHooksVmOnStopMethod = value;
+        }
+        s_logger.debug("agent.hooks.libvirt_vm_on_stop.method is " + 
_agentHooksVmOnStopMethod);
+    }
+
     private void loadUefiProperties() throws FileNotFoundException {
 
         if (_uefiProperties != null && 
_uefiProperties.getProperty("guest.loader.legacy") != null) {
diff --git 
a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtKvmAgentHook.java
 
b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtKvmAgentHook.java
new file mode 100644
index 0000000..3627d6e
--- /dev/null
+++ 
b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtKvmAgentHook.java
@@ -0,0 +1,76 @@
+// 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.
+
+package com.cloud.hypervisor.kvm.resource;
+
+import groovy.lang.Binding;
+import groovy.lang.GroovyObject;
+import groovy.util.GroovyScriptEngine;
+import groovy.util.ResourceException;
+import groovy.util.ScriptException;
+import org.apache.log4j.Logger;
+import org.codehaus.groovy.runtime.metaclass.MissingMethodExceptionNoStack;
+
+import java.io.File;
+import java.io.IOException;
+
+public class LibvirtKvmAgentHook {
+    private final String script;
+    private final String method;
+    private final GroovyScriptEngine gse;
+    private final Binding binding = new Binding();
+
+    private static final Logger s_logger = 
Logger.getLogger(LibvirtKvmAgentHook.class);
+
+    public LibvirtKvmAgentHook(String path, String script, String method) 
throws IOException {
+        this.script = script;
+        this.method = method;
+        File full_path = new File(path, script);
+        if (!full_path.canRead()) {
+            s_logger.warn("Groovy script '" + full_path.toString() + "' is not 
available. Transformations will not be applied.");
+            this.gse = null;
+        } else {
+            this.gse = new GroovyScriptEngine(path);
+        }
+    }
+
+    public boolean isInitialized() {
+        return this.gse != null;
+    }
+
+    public Object handle(Object arg) throws ResourceException, ScriptException 
{
+        if (!isInitialized()) {
+            s_logger.warn("Groovy scripting engine is not initialized. Data 
transformation skipped.");
+            return arg;
+        }
+
+        GroovyObject cls = (GroovyObject) this.gse.run(this.script, binding);
+        if (null == cls) {
+            s_logger.warn("Groovy object is not received from script '" + 
this.script + "'.");
+            return arg;
+        } else {
+            Object[] params = {s_logger, arg};
+            try {
+                Object res = cls.invokeMethod(this.method, params);
+                return res;
+            } catch (MissingMethodExceptionNoStack e) {
+                s_logger.error("Error occured when calling method from groovy 
script, {}", e);
+                return arg;
+            }
+        }
+    }
+}
diff --git 
a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStartCommandWrapper.java
 
b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStartCommandWrapper.java
index 068b24e..dbb9571 100644
--- 
a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStartCommandWrapper.java
+++ 
b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStartCommandWrapper.java
@@ -35,6 +35,7 @@ import 
com.cloud.agent.resource.virtualnetwork.VirtualRoutingResource;
 import com.cloud.exception.InternalErrorException;
 import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource;
 import com.cloud.hypervisor.kvm.resource.LibvirtVMDef;
+import com.cloud.hypervisor.kvm.resource.LibvirtKvmAgentHook;
 import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager;
 import com.cloud.network.Networks.TrafficType;
 import com.cloud.resource.CommandWrapper;
@@ -79,7 +80,10 @@ public final class LibvirtStartCommandWrapper extends 
CommandWrapper<StartComman
             libvirtComputingResource.createVifs(vmSpec, vm);
 
             s_logger.debug("starting " + vmName + ": " + vm.toString());
-            libvirtComputingResource.startVM(conn, vmName, vm.toString());
+            String vmInitialSpecification = vm.toString();
+            String vmFinalSpecification = 
performXmlTransformHook(vmInitialSpecification, libvirtComputingResource);
+            libvirtComputingResource.startVM(conn, vmName, 
vmFinalSpecification);
+            performAgentStartHook(vmName, libvirtComputingResource);
 
             libvirtComputingResource.applyDefaultNetworkRules(conn, vmSpec, 
false);
 
@@ -136,4 +140,30 @@ public final class LibvirtStartCommandWrapper extends 
CommandWrapper<StartComman
             }
         }
     }
+
+    private void performAgentStartHook(String vmName, LibvirtComputingResource 
libvirtComputingResource) {
+        try {
+            LibvirtKvmAgentHook onStartHook = 
libvirtComputingResource.getStartHook();
+            onStartHook.handle(vmName);
+        } catch (Exception e) {
+            s_logger.warn("Exception occurred when handling LibVirt VM onStart 
hook: {}", e);
+        }
+    }
+
+    private String performXmlTransformHook(String vmInitialSpecification, 
final LibvirtComputingResource libvirtComputingResource) {
+        String vmFinalSpecification;
+        try {
+            // if transformer fails, everything must go as it's just skipped.
+            LibvirtKvmAgentHook t = libvirtComputingResource.getTransformer();
+            vmFinalSpecification = (String) t.handle(vmInitialSpecification);
+            if (null == vmFinalSpecification) {
+                s_logger.warn("Libvirt XML transformer returned NULL, will use 
XML specification unchanged.");
+                vmFinalSpecification = vmInitialSpecification;
+            }
+        } catch(Exception e) {
+            s_logger.warn("Exception occurred when handling LibVirt XML 
transformer hook: {}", e);
+            vmFinalSpecification = vmInitialSpecification;
+        }
+        return vmFinalSpecification;
+    }
 }
diff --git 
a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStopCommandWrapper.java
 
b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStopCommandWrapper.java
index ad12971..cb57dbc 100644
--- 
a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStopCommandWrapper.java
+++ 
b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStopCommandWrapper.java
@@ -24,6 +24,7 @@ import java.util.List;
 import java.util.Map;
 
 import com.cloud.agent.api.to.DpdkTO;
+import com.cloud.hypervisor.kvm.resource.LibvirtKvmAgentHook;
 import com.cloud.utils.Pair;
 import com.cloud.utils.script.Script;
 import com.cloud.utils.ssh.SshHelper;
@@ -92,6 +93,8 @@ public final class LibvirtStopCommandWrapper extends 
CommandWrapper<StopCommand,
             libvirtComputingResource.destroyNetworkRulesForVM(conn, vmName);
             final String result = libvirtComputingResource.stopVM(conn, 
vmName, command.isForceStop());
 
+            performAgentStopHook(vmName, libvirtComputingResource);
+
             if (result == null) {
                 if (disks != null && disks.size() > 0) {
                     for (final DiskDef disk : disks) {
@@ -147,4 +150,14 @@ public final class LibvirtStopCommandWrapper extends 
CommandWrapper<StopCommand,
             return new StopAnswer(command, e.getMessage(), false);
         }
     }
+
+    private void performAgentStopHook(String vmName, final 
LibvirtComputingResource libvirtComputingResource) {
+        try {
+            LibvirtKvmAgentHook onStopHook = 
libvirtComputingResource.getStopHook();
+            onStopHook.handle(vmName);
+        } catch (Exception e) {
+            s_logger.warn("Exception occurred when handling LibVirt VM onStop 
hook: {}", e);
+        }
+    }
+
 }
diff --git 
a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtKvmAgentHookTest.java
 
b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtKvmAgentHookTest.java
new file mode 100644
index 0000000..1f63914
--- /dev/null
+++ 
b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtKvmAgentHookTest.java
@@ -0,0 +1,94 @@
+// 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.
+
+package com.cloud.hypervisor.kvm.resource;
+
+import groovy.util.ResourceException;
+import groovy.util.ScriptException;
+import junit.framework.TestCase;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.UUID;
+
+public class LibvirtKvmAgentHookTest extends TestCase {
+
+    private final String source = "<xml />";
+    private final String dir = "/tmp";
+    private final String script = "xml-transform-test.groovy";
+    private final String method = "transform";
+    private final String methodNull = "transform2";
+    private final String testImpl = "package groovy\n" +
+            "\n" +
+            "class BaseTransform {\n" +
+            "    String transform(Object logger, String xml) {\n" +
+            "        return xml + xml\n" +
+            "    }\n" +
+            "    String transform2(Object logger, String xml) {\n" +
+            "        return null\n" +
+            "    }\n" +
+            "}\n" +
+            "\n" +
+            "new BaseTransform()\n" +
+            "\n";
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        PrintWriter pw = new PrintWriter(new File(dir, script));
+        pw.println(testImpl);
+        pw.close();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        new File(dir, script).delete();
+        super.tearDown();
+    }
+
+    public void testTransform() throws IOException, ResourceException, 
ScriptException {
+        LibvirtKvmAgentHook t = new LibvirtKvmAgentHook(dir, script, method);
+        assertEquals(t.isInitialized(), true);
+        String result = (String)t.handle(source);
+        assertEquals(result, source + source);
+    }
+
+    public void testWrongMethod() throws IOException, ResourceException, 
ScriptException {
+        LibvirtKvmAgentHook t = new LibvirtKvmAgentHook(dir, script, 
"methodX");
+        assertEquals(t.isInitialized(), true);
+        assertEquals(t.handle(source), source);
+    }
+
+    public void testNullMethod() throws IOException, ResourceException, 
ScriptException {
+        LibvirtKvmAgentHook t = new LibvirtKvmAgentHook(dir, script, 
methodNull);
+        assertEquals(t.isInitialized(), true);
+        assertEquals(t.handle(source), null);
+    }
+
+    public void testWrongScript() throws IOException, ResourceException, 
ScriptException {
+        LibvirtKvmAgentHook t = new LibvirtKvmAgentHook(dir, 
"wrong-script.groovy", method);
+        assertEquals(t.isInitialized(), false);
+        assertEquals(t.handle(source), source);
+    }
+
+    public void testWrongDir() throws IOException, ResourceException, 
ScriptException {
+        LibvirtKvmAgentHook t = new LibvirtKvmAgentHook("/" + 
UUID.randomUUID().toString() + "-dir", script, method);
+        assertEquals(t.isInitialized(), false);
+        assertEquals(t.handle(source), source);
+    }
+}

Reply via email to