FEATURE-3823: kvm agent hooks (#3839)

This commit is contained in:
Bitworks LLC 2020-03-14 15:22:08 +07:00 committed by GitHub
parent d4b537efa7
commit 750abf3551
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 314 additions and 1 deletions

View File

@ -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

View File

@ -28,6 +28,11 @@
<relativePath>../../pom.xml</relativePath>
</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>

View File

@ -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) {

View File

@ -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;
}
}
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}