diff --git a/agent/conf/agent.properties b/agent/conf/agent.properties
index bb9bf4071b2d..85c85a55bab7 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 3c3a63585dd5..9d0c786cbb4e 100644
--- a/plugins/hypervisors/kvm/pom.xml
+++ b/plugins/hypervisors/kvm/pom.xml
@@ -28,6 +28,11 @@
../../pom.xml
+
+ org.codehaus.groovy
+ groovy-all
+ ${cs.groovy.version}
+
commons-io
commons-io
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 fd9075e88908..79958ef8ea43 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 ExecutionResult cleanupCommand(final NetworkElementCommand cmd) {
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 boolean configure(final String name, final Map params) th
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 boolean configureHostParams(final Map params) {
return true;
}
+ private void configureAgentHooks(final Map 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 000000000000..3627d6e2a07c
--- /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 068b24e3d54c..dbb9571cea31 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.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 Answer execute(final StartCommand command, final LibvirtComputingResource
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 Answer execute(final StartCommand command, final LibvirtComputingResource
}
}
}
+
+ 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 ad129710152e..cb57dbc0ec29 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.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 Answer execute(final StopCommand command, final LibvirtComputingResource
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 Answer execute(final StopCommand command, final LibvirtComputingResource
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 000000000000..1f6391486cda
--- /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 = "";
+ 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);
+ }
+}