From 24a1298e5979455f90e322d534364ba9ef571d49 Mon Sep 17 00:00:00 2001 From: "Ivan A. Kudryavtsev" Date: Tue, 28 Jan 2020 13:36:16 +0700 Subject: [PATCH 1/2] Implemented KVM hooks. --- agent/conf/agent.properties | 25 +++++ plugins/hypervisors/kvm/pom.xml | 5 + .../resource/LibvirtComputingResource.java | 70 ++++++++++++++ .../kvm/resource/LibvirtKvmAgentHook.java | 76 +++++++++++++++ .../wrapper/LibvirtStartCommandWrapper.java | 47 ++++++++-- .../wrapper/LibvirtStopCommandWrapper.java | 13 +++ .../kvm/resource/LibvirtKvmAgentHookTest.java | 94 +++++++++++++++++++ 7 files changed, 321 insertions(+), 9 deletions(-) create mode 100644 plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtKvmAgentHook.java create mode 100644 plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtKvmAgentHookTest.java diff --git a/agent/conf/agent.properties b/agent/conf/agent.properties index 7472e3922277..330a4c562dab 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 76db243ee6b7..12fbe8ed33ae 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 @@ -280,6 +280,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; @@ -382,6 +394,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; } @@ -1061,6 +1085,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) { @@ -1097,6 +1123,50 @@ public boolean configure(final String name, final Map params) th 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); + } + protected void configureDiskActivityChecks(final Map params) { _diskActivityCheckEnabled = Boolean.parseBoolean((String)params.get("vm.diskactivity.checkenabled")); if (_diskActivityCheckEnabled) { 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 9c97bd4770fc..c2808e5e8675 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 @@ -19,14 +19,6 @@ package com.cloud.hypervisor.kvm.resource.wrapper; -import java.net.URISyntaxException; -import java.util.List; - -import org.apache.log4j.Logger; -import org.libvirt.Connect; -import org.libvirt.DomainInfo.DomainState; -import org.libvirt.LibvirtException; - import com.cloud.agent.api.Answer; import com.cloud.agent.api.StartAnswer; import com.cloud.agent.api.StartCommand; @@ -36,12 +28,20 @@ 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.IsolationType; import com.cloud.network.Networks.TrafficType; import com.cloud.resource.CommandWrapper; import com.cloud.resource.ResourceWrapper; import com.cloud.vm.VirtualMachine; +import org.apache.log4j.Logger; +import org.libvirt.Connect; +import org.libvirt.DomainInfo.DomainState; +import org.libvirt.LibvirtException; + +import java.net.URISyntaxException; +import java.util.List; @ResourceWrapper(handles = StartCommand.class) public final class LibvirtStartCommandWrapper extends CommandWrapper { @@ -81,7 +81,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); for (final NicTO nic : nics) { if (nic.isSecurityGroupEnabled() || nic.getIsolationUri() != null && nic.getIsolationUri().getScheme().equalsIgnoreCase(IsolationType.Ec2.toString())) { @@ -158,4 +161,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); + } +} From 5a6c54a1ac4a0cb328369b1c2439e7f1662e1361 Mon Sep 17 00:00:00 2001 From: "Ivan A. Kudryavtsev" Date: Fri, 13 Mar 2020 16:29:26 +0700 Subject: [PATCH 2/2] Fix --- .../kvm/resource/wrapper/LibvirtStartCommandWrapper.java | 1 - 1 file changed, 1 deletion(-) 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 5ad3f73f5f26..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 @@ -37,7 +37,6 @@ 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.IsolationType; import com.cloud.network.Networks.TrafficType; import com.cloud.resource.CommandWrapper; import com.cloud.resource.ResourceWrapper;