package org.zstack.kvm;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.util.UriComponentsBuilder;
import org.zstack.compute.host.HostGlobalConfig;
import org.zstack.core.CoreGlobalProperty;
import org.zstack.core.ansible.AnsibleFacade;
import org.zstack.core.cloudbus.*;
import org.zstack.core.componentloader.PluginRegistry;
import org.zstack.core.config.GlobalConfig;
import org.zstack.core.config.GlobalConfigException;
import org.zstack.core.config.GlobalConfigUpdateExtensionPoint;
import org.zstack.core.config.GlobalConfigValidatorExtensionPoint;
import org.zstack.core.db.DatabaseFacade;
import org.zstack.core.db.SimpleQuery;
import org.zstack.core.notification.N;
import org.zstack.core.thread.AsyncThread;
import org.zstack.header.AbstractService;
import org.zstack.header.Component;
import org.zstack.header.exception.CloudRuntimeException;
import org.zstack.header.host.*;
import org.zstack.header.managementnode.ManagementNodeReadyExtensionPoint;
import org.zstack.header.message.Message;
import org.zstack.header.message.MessageReply;
import org.zstack.header.message.NeedReplyMessage;
import org.zstack.header.network.l2.L2NetworkType;
import org.zstack.header.rest.RESTFacade;
import org.zstack.header.rest.SyncHttpCallHandler;
import org.zstack.header.volume.MaxDataVolumeNumberExtensionPoint;
import org.zstack.header.volume.VolumeConstant;
import org.zstack.header.volume.VolumeFormat;
import org.zstack.kvm.KVMAgentCommands.ReconnectMeCmd;
import org.zstack.utils.CollectionUtils;
import org.zstack.utils.SizeUtils;
import org.zstack.utils.Utils;
import org.zstack.utils.function.Function;
import org.zstack.utils.logging.CLogger;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class KVMHostFactory extends AbstractService implements HypervisorFactory, Component,
ManagementNodeReadyExtensionPoint, MaxDataVolumeNumberExtensionPoint {
private static final CLogger logger = Utils.getLogger(KVMHostFactory.class);
public static final HypervisorType hypervisorType = new HypervisorType(KVMConstant.KVM_HYPERVISOR_TYPE);
public static final VolumeFormat QCOW2_FORMAT = new VolumeFormat(VolumeConstant.VOLUME_FORMAT_QCOW2, hypervisorType);
public static final VolumeFormat RAW_FORMAT = new VolumeFormat(VolumeConstant.VOLUME_FORMAT_RAW, hypervisorType);
private List<KVMHostConnectExtensionPoint> connectExtensions = new ArrayList<>();
private Map<L2NetworkType, KVMCompleteNicInformationExtensionPoint> completeNicInfoExtensions = new HashMap<>();
private int maxDataVolumeNum;
static {
RAW_FORMAT.newFormatInputOutputMapping(hypervisorType, QCOW2_FORMAT.toString());
QCOW2_FORMAT.setFirstChoice(hypervisorType);
}
@Autowired
private DatabaseFacade dbf;
@Autowired
private PluginRegistry pluginRgty;
@Autowired
private AnsibleFacade asf;
@Autowired
private ResourceDestinationMaker destMaker;
@Autowired
private CloudBus bus;
@Autowired
private RESTFacade restf;
@Override
public HostVO createHost(HostVO vo, AddHostMessage msg) {
APIAddKVMHostMsg amsg = (APIAddKVMHostMsg) msg;
KVMHostVO kvo = new KVMHostVO(vo);
kvo.setUsername(amsg.getUsername());
kvo.setPassword(amsg.getPassword());
kvo.setPort(amsg.getSshPort());
kvo = dbf.persistAndRefresh(kvo);
return kvo;
}
@Override
public Host getHost(HostVO vo) {
KVMHostVO kvo = dbf.findByUuid(vo.getUuid(), KVMHostVO.class);
KVMHostContext context = getHostContext(vo.getUuid());
if (context == null) {
context = createHostContext(kvo);
}
return new KVMHost(kvo, context);
}
private List<String> getHostManagedByUs() {
int qun = 10000;
long amount = dbf.count(HostVO.class);
int times = (int) (amount / qun) + (amount % qun != 0 ? 1 : 0);
List<String> hostUuids = new ArrayList<String>();
int start = 0;
for (int i = 0; i < times; i++) {
SimpleQuery<KVMHostVO> q = dbf.createQuery(KVMHostVO.class);
q.select(HostVO_.uuid);
// disconnected host will be handled by HostManager
q.add(HostVO_.status, SimpleQuery.Op.EQ, HostStatus.Connected);
q.setLimit(qun);
q.setStart(start);
List<String> lst = q.listValue();
start += qun;
for (String huuid : lst) {
if (!destMaker.isManagedByUs(huuid)) {
continue;
}
hostUuids.add(huuid);
}
}
return hostUuids;
}
@Override
public HypervisorType getHypervisorType() {
return hypervisorType;
}
@Override
public HostInventory getHostInventory(HostVO vo) {
KVMHostVO kvo = vo instanceof KVMHostVO ? (KVMHostVO) vo : dbf.findByUuid(vo.getUuid(), KVMHostVO.class);
return KVMHostInventory.valueOf(kvo);
}
@Override
public HostInventory getHostInventory(String uuid) {
KVMHostVO vo = dbf.findByUuid(uuid, KVMHostVO.class);
return vo == null ? null : KVMHostInventory.valueOf(vo);
}
private void populateExtensions() {
connectExtensions = pluginRgty.getExtensionList(KVMHostConnectExtensionPoint.class);
for (KVMCompleteNicInformationExtensionPoint ext : pluginRgty.getExtensionList(KVMCompleteNicInformationExtensionPoint.class)) {
KVMCompleteNicInformationExtensionPoint old = completeNicInfoExtensions.get(ext.getL2NetworkTypeVmNicOn());
if (old != null) {
throw new CloudRuntimeException(String.format("duplicate KVMCompleteNicInformationExtensionPoint[%s, %s] for type[%s]",
old.getClass().getName(), ext.getClass().getName(), ext.getL2NetworkTypeVmNicOn()));
}
completeNicInfoExtensions.put(ext.getL2NetworkTypeVmNicOn(), ext);
}
}
public KVMCompleteNicInformationExtensionPoint getCompleteNicInfoExtension(L2NetworkType type) {
KVMCompleteNicInformationExtensionPoint extp = completeNicInfoExtensions.get(type);
if (extp == null) {
throw new IllegalArgumentException(String.format("unble to fine KVMCompleteNicInformationExtensionPoint supporting L2NetworkType[%s]", type));
}
return extp;
}
private void deployAnsibleModule() {
if (CoreGlobalProperty.UNIT_TEST_ON) {
return;
}
asf.deployModule(KVMConstant.ANSIBLE_MODULE_PATH, KVMConstant.ANSIBLE_PLAYBOOK_NAME);
}
@Override
public boolean start() {
deployAnsibleModule();
populateExtensions();
maxDataVolumeNum = KVMGlobalConfig.MAX_DATA_VOLUME_NUM.value(int.class);
KVMGlobalConfig.MAX_DATA_VOLUME_NUM.installUpdateExtension(new GlobalConfigUpdateExtensionPoint() {
@Override
public void updateGlobalConfig(GlobalConfig oldConfig, GlobalConfig newConfig) {
maxDataVolumeNum = newConfig.value(int.class);
}
});
KVMGlobalConfig.RESERVED_MEMORY_CAPACITY.installValidateExtension(new GlobalConfigValidatorExtensionPoint() {
@Override
public void validateGlobalConfig(String category, String name, String oldValue, String value) throws GlobalConfigException {
if (!SizeUtils.isSizeString(value)) {
throw new GlobalConfigException(String.format("%s only allows a size string." +
" A size string is a number with suffix 'T/t/G/g/M/m/K/k/B/b' or without suffix, but got %s",
KVMGlobalConfig.RESERVED_MEMORY_CAPACITY.getCanonicalName(), value));
}
}
});
KVMGlobalConfig.RESERVED_MEMORY_CAPACITY.installValidateExtension(new GlobalConfigValidatorExtensionPoint() {
@Override
public void validateGlobalConfig(String category, String name, String oldValue, String value) throws GlobalConfigException {
Long valueLong = SizeUtils.sizeStringToBytes(value);
Long _1t = SizeUtils.sizeStringToBytes("1T");
if (valueLong > _1t || valueLong < 0) {
throw new GlobalConfigException(String.format("Value %s cannot be greater than the 1TB" + " but got %s",
KVMGlobalConfig.RESERVED_MEMORY_CAPACITY.getCanonicalName(), value));
}
}
});
restf.registerSyncHttpCallHandler(KVMConstant.KVM_RECONNECT_ME, ReconnectMeCmd.class, new SyncHttpCallHandler<ReconnectMeCmd>() {
@Override
public String handleSyncHttpCall(ReconnectMeCmd cmd) {
N.New(HostVO.class, cmd.hostUuid).info_("the kvm host[uuid:%s] asks the management server to reconnect it for %s", cmd.hostUuid, cmd.reason);
ReconnectHostMsg msg = new ReconnectHostMsg();
msg.setHostUuid(cmd.hostUuid);
bus.makeTargetServiceIdByResourceUuid(msg, HostConstant.SERVICE_ID, cmd.hostUuid);
bus.send(msg);
return null;
}
});
return true;
}
@Override
public boolean stop() {
return true;
}
public List<KVMHostConnectExtensionPoint> getConnectExtensions() {
return connectExtensions;
}
public KVMHostContext createHostContext(KVMHostVO vo) {
UriComponentsBuilder ub = UriComponentsBuilder.newInstance();
ub.scheme(KVMGlobalProperty.AGENT_URL_SCHEME);
ub.host(vo.getManagementIp());
ub.port(KVMGlobalProperty.AGENT_PORT);
if (!"".equals(KVMGlobalProperty.AGENT_URL_ROOT_PATH)) {
ub.path(KVMGlobalProperty.AGENT_URL_ROOT_PATH);
}
String baseUrl = ub.build().toUriString();
KVMHostContext context = new KVMHostContext();
context.setInventory(KVMHostInventory.valueOf(vo));
context.setBaseUrl(baseUrl);
return context;
}
public KVMHostContext getHostContext(String hostUuid) {
KVMHostVO kvo = dbf.findByUuid(hostUuid, KVMHostVO.class);
return createHostContext(kvo);
}
@Override
public String getHypervisorTypeForMaxDataVolumeNumberExtension() {
return KVMConstant.KVM_HYPERVISOR_TYPE;
}
@Override
public int getMaxDataVolumeNumber() {
return maxDataVolumeNum;
}
@Override
@AsyncThread
public void managementNodeReady() {
if (CoreGlobalProperty.UNIT_TEST_ON) {
return;
}
if (!asf.isModuleChanged(KVMConstant.ANSIBLE_PLAYBOOK_NAME)) {
return;
}
// KVM hosts need to deploy new agent
// connect hosts even if they are ConnectionState is Connected
List<String> hostUuids = getHostManagedByUs();
if (hostUuids.isEmpty()) {
return;
}
logger.debug(String.format("need to connect kvm hosts because kvm agent changed, uuids:%s", hostUuids));
List<ConnectHostMsg> msgs = new ArrayList<ConnectHostMsg>();
for (String huuid : hostUuids) {
ConnectHostMsg msg = new ConnectHostMsg();
msg.setNewAdd(false);
msg.setUuid(huuid);
bus.makeTargetServiceIdByResourceUuid(msg, HostConstant.SERVICE_ID, huuid);
msgs.add(msg);
}
bus.send(msgs, HostGlobalConfig.HOST_LOAD_PARALLELISM_DEGREE.value(Integer.class), new CloudBusSteppingCallback(null) {
@Override
public void run(NeedReplyMessage msg, MessageReply reply) {
ConnectHostMsg cmsg = (ConnectHostMsg) msg;
if (!reply.isSuccess()) {
logger.warn(String.format("failed to connect kvm host[uuid:%s], %s", cmsg.getHostUuid(), reply.getError()));
} else {
logger.debug(String.format("successfully to connect kvm host[uuid:%s]", cmsg.getHostUuid()));
}
}
});
}
@Override
@MessageSafe
public void handleMessage(Message msg) {
if (msg instanceof APIKvmRunShellMsg) {
handle((APIKvmRunShellMsg) msg);
} else {
bus.dealWithUnknownMessage(msg);
}
}
private void handle(final APIKvmRunShellMsg msg) {
final APIKvmRunShellEvent evt = new APIKvmRunShellEvent(msg.getId());
final List<KvmRunShellMsg> kmsgs = CollectionUtils.transformToList(msg.getHostUuids(), new Function<KvmRunShellMsg, String>() {
@Override
public KvmRunShellMsg call(String arg) {
KvmRunShellMsg kmsg = new KvmRunShellMsg();
kmsg.setHostUuid(arg);
kmsg.setScript(msg.getScript());
bus.makeTargetServiceIdByResourceUuid(kmsg, HostConstant.SERVICE_ID, arg);
return kmsg;
}
});
bus.send(kmsgs, new CloudBusListCallBack(msg) {
@Override
public void run(List<MessageReply> replies) {
for (MessageReply r : replies) {
String hostUuid = kmsgs.get(replies.indexOf(r)).getHostUuid();
APIKvmRunShellEvent.ShellResult result = new APIKvmRunShellEvent.ShellResult();
if (!r.isSuccess()) {
result.setErrorCode(r.getError());
} else {
KvmRunShellReply kr = r.castReply();
result.setReturnCode(kr.getReturnCode());
result.setStderr(kr.getStderr());
result.setStdout(kr.getStdout());
}
evt.getInventory().put(hostUuid, result);
}
bus.publish(evt);
}
});
}
@Override
public String getId() {
return bus.makeLocalServiceId(KVMConstant.SERVICE_ID);
}
}