/* * Copyright 2000-2020 JetBrains s.r.o. * * Licensed 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 jetbrains.buildServer.clouds.vmware; import com.intellij.openapi.diagnostic.Logger; import com.intellij.util.Function; import com.vmware.vim25.mo.Task; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.util.*; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import jetbrains.buildServer.clouds.*; import jetbrains.buildServer.clouds.base.AbstractCloudImage; import jetbrains.buildServer.clouds.base.connector.AbstractInstance; import jetbrains.buildServer.clouds.base.connector.CloudAsyncTaskExecutor; import jetbrains.buildServer.clouds.base.connector.TaskCallbackHandler; import jetbrains.buildServer.clouds.base.errors.TypedCloudErrorInfo; import jetbrains.buildServer.clouds.vmware.connector.VMWareApiConnector; import jetbrains.buildServer.clouds.vmware.connector.VmwareInstance; import jetbrains.buildServer.clouds.vmware.connector.VmwareTaskWrapper; import jetbrains.buildServer.clouds.vmware.errors.VmwareCheckedCloudException; import jetbrains.buildServer.serverSide.TeamCityProperties; import jetbrains.buildServer.util.FileUtil; import jetbrains.buildServer.util.IdentifiersGenerator; import jetbrains.buildServer.util.StringUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; /** * @author Sergey.Pak * Date: 4/15/2014 * Time: 3:58 PM */ public class VmwareCloudImage extends AbstractCloudImage<VmwareCloudInstance, VmwareCloudImageDetails>{ private static final Logger LOG = Logger.getInstance(VmwareCloudImage.class.getName()); // consider <Clone;Delete> instances orphaned (won't be deleted), if they were stopped more than 5 minutes ago private static final long STOPPED_ORPHANED_TIMEOUT = 5*60*1000l; private final VMWareApiConnector myApiConnector; @NotNull private final CloudAsyncTaskExecutor myAsyncTaskExecutor; private final VmwareCloudImageDetails myImageDetails; private final AtomicReference<VmwareSourceState> myActualSourceState; private final File myIdxFile; private final CloudProfile myProfile; private final AtomicInteger myIdxCounter = new AtomicInteger(0); private final AtomicBoolean myIdxTouched = new AtomicBoolean(false); public VmwareCloudImage(@NotNull final VMWareApiConnector apiConnector, @NotNull final VmwareCloudImageDetails imageDetails, @NotNull final CloudAsyncTaskExecutor asyncTaskExecutor, @NotNull final File idxStorage, @NotNull final CloudProfile profile) { super(imageDetails.getSourceId(), imageDetails.getSourceId()); myImageDetails = imageDetails; myApiConnector = apiConnector; myAsyncTaskExecutor = asyncTaskExecutor; myProfile = profile; myActualSourceState = new AtomicReference<>(); myIdxFile = new File(idxStorage, imageDetails.getSourceId() + ".idx"); try { if (!myIdxFile.exists()) { myIdxCounter.set(1); myIdxTouched.set(true); storeIdx(); } else { myIdxCounter.set(Integer.parseInt(FileUtil.readText(myIdxFile))); } } catch (Exception e) { LOG.warnAndDebugDetails(String.format("Unable to process idx file '%s'. Will reset the index for " + imageDetails.getSourceId(), myIdxFile.getAbsolutePath()), e); myIdxCounter.set(1); } asyncTaskExecutor.scheduleWithFixedDelay("Store idx", ()->{ storeIdx(); }, 5000, 5000, TimeUnit.MILLISECONDS); } @NotNull public String getSnapshotName() { return myImageDetails.getSnapshotName(); } @Nullable private VmwareCloudInstance getExistingInstanceToStart(@NotNull final VmwareSourceState currentSourceState) throws VmwareCheckedCloudException { final VmwareInstance imageVm = myApiConnector.getInstanceDetails(myImageDetails.getSourceVmName()); final AtomicReference<VmwareCloudInstance> candidate = new AtomicReference<VmwareCloudInstance>(); processStoppedInstances(new Function<VmwareInstance, Boolean>() { public Boolean fun(final VmwareInstance vmInstance) { final String vmName = vmInstance.getName(); final VmwareCloudInstance instance = findInstanceById(vmName); if (instance != null) { if (myImageDetails.useCurrentVersion()) { if (imageVm.getChangeVersion() == null || !imageVm.getChangeVersion().equals(vmInstance.getChangeVersion())) { LOG.info(String.format("Change version for %s is outdated: '%s' vs '%s'", vmName, vmInstance.getChangeVersion(), imageVm.getChangeVersion())); deleteInstance(instance); return false; } } else { final VmwareSourceState vmSourceState = vmInstance.getVmSourceState(); if (!vmSourceState.equals(currentSourceState)){ LOG.info(String.format("Source for VM %s has been changed: %s", vmName, currentSourceState.getDiffMessage(vmSourceState))); deleteInstance(instance); return false; } } } LOG.info("Will use existing VM with name " + vmName); candidate.set(instance); return true; } }); if (candidate.get() != null){ return candidate.get(); } return null; } private VmwareCloudInstance getStartableInstanceFast(){ if (!canStartNewInstance()){ throw new QuotaException("Unable to start more instances of image " + getName()); } if (myImageDetails.getBehaviour().isUseOriginal()) { LOG.info("Won't create a new instance - using original"); return findInstanceById(myImageDetails.getSourceId()); } if (myImageDetails.getBehaviour().isDeleteAfterStop()){ // will clone into new instance final String newVmName = generateNewVmName(); final StartingVmwareCloudInstance instance = new StartingVmwareCloudInstance(this, newVmName); addInstance(instance); return instance; } final UnresolvedVmwareCloudInstance instance = new UnresolvedVmwareCloudInstance(this); addInstance(instance); return instance; } @Override public synchronized VmwareCloudInstance startNewInstance(@NotNull final CloudInstanceUserData cloudInstanceUserData) throws QuotaException{ final VmwareCloudInstance instanceCandidate = getStartableInstanceFast(); instanceCandidate.setStatus(InstanceStatus.SCHEDULED_TO_START); myAsyncTaskExecutor.submit("Preparing to start new instance...", () -> { VmwareInstance sourceVm = null; try { boolean willClone = true; VmwareCloudInstance instance = instanceCandidate; if (myImageDetails.getBehaviour().isUseOriginal()){ willClone = false; } else { sourceVm = myApiConnector.getInstanceDetails(myImageDetails.getSourceVmName()); final VmwareSourceState sourceState; if (instanceCandidate.getSourceState().getSnapshotName() == null) { String latestSnapshotName; latestSnapshotName = myApiConnector.getLatestSnapshot(sourceVm.getId(), myImageDetails.getSnapshotName()); if (latestSnapshotName == null){ if (!myImageDetails.useCurrentVersion()) { updateErrors(new TypedCloudErrorInfo("No such snapshot: " + getSnapshotName())); LOG.warn("Unable to find snapshot: " + myImageDetails.getSnapshotName() + ". Won't start " + instanceCandidate.getInstanceId()); return; } } sourceState = VmwareSourceState.from(latestSnapshotName, sourceVm.getId()); instanceCandidate.setSourceState(sourceState); } else { sourceState = sourceVm.getVmSourceState(); } if (!instance.isReady()) { assert sourceState != null; // need to resolve the real instance VmwareCloudInstance existingInstanceToStart = getExistingInstanceToStart(sourceState); if (existingInstanceToStart != null) { removeInstance(instance.getInstanceId()); instance = existingInstanceToStart; willClone = false; } else { final String newVmName = generateNewVmName(); instance.setName(newVmName); instance.setInstanceId(newVmName); instance.setSourceState(sourceState); instance.setReady(true); } } final int instancesCount = getInstances().size(); LOG.info("Should clone into " + instance.getName() + ": " + willClone + ". Already have instances: " + instancesCount); if (willClone && myImageDetails.getMaxInstances() < instancesCount) { LOG.info("Cannot clone - instances limit exceeded. Will try to clean up some old instances"); cleanupOldInstances(); // don't attempt to start so far removeInstance(instance.getInstanceId()); return; } } if (willClone) { final VmwareCloudInstance finalInstance = instance; myAsyncTaskExecutor.executeAsync( new VmwareTaskWrapper(() -> myApiConnector.cloneAndStartVm(finalInstance), "Clone and start instance " + instance.getName()), new ImageStatusTaskWrapper(instance) { @Override public void onSuccess() { reconfigureVmTask(finalInstance, cloudInstanceUserData); } @Override public void onError(final Throwable th) { super.onError(th); } }); } else { startVM(instance, cloudInstanceUserData); } } catch (Exception ex) { ex.printStackTrace(); LOG.warnAndDebugDetails("Unexpected error while trying to start vSphere cloud instance", ex); } }); return instanceCandidate; } private void cleanupOldInstances() { final long stoppedOrphanedTimeout = TeamCityProperties.getLong("teamcity.vmware.stopped.orphaned.timeout", STOPPED_ORPHANED_TIMEOUT); final Date considerTime = new Date(System.currentTimeMillis() - stoppedOrphanedTimeout); processStoppedInstances(new Function<VmwareInstance, Boolean>() { public Boolean fun(final VmwareInstance vmInstance) { final String vmName = vmInstance.getName(); final VmwareCloudInstance instance = findInstanceById(vmName); if (instance != null && instance.getStatusUpdateTime().before(considerTime)){ LOG.info(String.format("VM %s was orphaned and will be deleted", vmName)); deleteInstance(instance); return true; } return false; } }); } private synchronized void startVM(@NotNull final VmwareCloudInstance instance, @NotNull final CloudInstanceUserData cloudInstanceUserData) { instance.setStartDate(new Date()); instance.setStatus(InstanceStatus.STARTING); myAsyncTaskExecutor.executeAsync(new VmwareTaskWrapper(new Callable<Task>() { public Task call() throws Exception { return myApiConnector.startInstance(instance, instance.getName(), cloudInstanceUserData); } }, "Start instance " + instance.getName()) , new ImageStatusTaskWrapper(instance) { @Override public void onSuccess() { reconfigureVmTask(instance, cloudInstanceUserData); } }); } private synchronized void reconfigureVmTask(@NotNull final VmwareCloudInstance instance, @NotNull final CloudInstanceUserData cloudInstanceUserData) { myAsyncTaskExecutor.executeAsync(new VmwareTaskWrapper(new Callable<Task>() { public Task call() throws Exception { return myApiConnector.reconfigureInstance(instance, instance.getName(), cloudInstanceUserData); } }, "Reconfigure " + instance.getName()) , new ImageStatusTaskWrapper(instance) { @Override public void onSuccess() { instance.setStatus(InstanceStatus.RUNNING); instance.setStartDate(new Date()); instance.updateErrors(); LOG.info("Reconfiguration of '" + instance.getInstanceId() + "' is finished. Instance started successfully"); } @Override public void onError(final Throwable th) { LOG.warnAndDebugDetails("Can't reconfigure '" + instance.getInstanceId() +"'. Instance will be terminated", th); terminateInstance(instance); } }); } public void terminateInstance(@NotNull final VmwareCloudInstance instance) { LOG.info("Stopping instance " + instance.getName()); instance.setStatus(InstanceStatus.SCHEDULED_TO_STOP); myAsyncTaskExecutor.executeAsync(new VmwareTaskWrapper(new Callable<Task>() { public Task call() throws Exception { return myApiConnector.stopInstance(instance); } }, "Stop " + instance.getName()), new ImageStatusTaskWrapper(instance){ @Override public void onComplete() { instance.setStatus(InstanceStatus.STOPPED); if (myImageDetails.getBehaviour().isDeleteAfterStop()) { // we only destroy proper instances. deleteInstance(instance); } } }); } private void deleteInstance(@NotNull final VmwareCloudInstance instance){ if (instance.getErrorInfo() == null) { LOG.info("Will delete instance " + instance.getName()); myAsyncTaskExecutor.executeAsync(new VmwareTaskWrapper(new Callable<Task>() { public Task call() throws Exception { return myApiConnector.deleteInstance(instance); } }, "Delete " + instance.getName()), new ImageStatusTaskWrapper(instance) { @Override public void onSuccess() { removeInstance(instance.getName()); } }); } else { LOG.warn(String.format("Won't delete instance %s with error: %s (%s)", instance.getName(), instance.getErrorInfo().getMessage(), instance.getErrorInfo().getDetailedMessage())); } } @NotNull @Override public synchronized CanStartNewInstanceResult canStartNewInstanceWithDetails() { if (getErrorInfo() != null){ LOG.debug("Can't start new instance, if image is erroneous"); return CanStartNewInstanceResult.no("Image is erroneous."); } final String sourceId = myImageDetails.getSourceId(); if (myImageDetails.getBehaviour().isUseOriginal()) { final VmwareCloudInstance myInstance = findInstanceById(sourceId); if (myInstance == null) { return CanStartNewInstanceResult.no("Can't find original instance by id " + sourceId); } if (myInstance.getStatus() == InstanceStatus.STOPPED) { return CanStartNewInstanceResult.yes(); } return CanStartNewInstanceResult.no("Original instance with id " + sourceId + " is not being stopped"); } final boolean countStoppedVmsInLimit = TeamCityProperties.getBoolean(VmwareConstants.CONSIDER_STOPPED_VMS_LIMIT) && myImageDetails.getBehaviour().isDeleteAfterStop(); final List<String> consideredInstances = new ArrayList<String>(); for (VmwareCloudInstance instance : getInstances()) { if (instance.getStatus() != InstanceStatus.STOPPED || countStoppedVmsInLimit) consideredInstances.add(instance.getInstanceId()); } final boolean canStartMore = consideredInstances.size() < myImageDetails.getMaxInstances(); final String message = String.format("[%s] Instances count: %d %s, can start more: %s", sourceId, consideredInstances.size(), Arrays.toString(consideredInstances.toArray()), String.valueOf(canStartMore)); LOG.debug(message); return canStartMore ? CanStartNewInstanceResult.yes() : CanStartNewInstanceResult.no("Image instance limit exceeded"); } @Override public void restartInstance(@NotNull final VmwareCloudInstance instance) { throw new UnsupportedOperationException("Restart not implemented"); } protected String generateNewVmName() { String newVmName; do { newVmName = String.format("%s-%d", getId(), myIdxCounter.getAndIncrement()); myIdxTouched.set(true); } while (getInstanceIds().contains(newVmName)); LOG.info("Will create a new VM with name " + newVmName); return newVmName; } public VmwareCloudImageDetails getImageDetails() { return myImageDetails; } @NotNull @Override protected VmwareCloudInstance createInstanceFromReal(final AbstractInstance realInstance) { final VmwareInstance vmwareInstance = (VmwareInstance) realInstance; return new VmwareCloudInstance(this, realInstance.getName(), vmwareInstance.getVmSourceState()); } private void processStoppedInstances(final Function<VmwareInstance, Boolean> function) { myApiConnector.processImageInstances(this, new VMWareApiConnector.VmwareInstanceProcessor() { public void process(final VmwareInstance vmInstance) { if (vmInstance.getInstanceStatus() == InstanceStatus.STOPPED) { final String vmName = vmInstance.getName(); final VmwareCloudInstance instance = findInstanceById(vmName); if (instance == null) { LOG.warn("Unable to find instance " + vmName + " in myInstances."); return; } // checking if this instance is already starting. if (instance.getStatus() != InstanceStatus.STOPPED) return; // currently value is ignore function.fun(vmInstance); } } }); } @Nullable @Override public Integer getAgentPoolId() { return myImageDetails.getAgentPoolId(); } public CloudProfile getProfile() { return myProfile; } void storeIdx() { if (myIdxTouched.compareAndSet(true, false)){ synchronized (myIdxFile) { try { FileUtil.writeViaTmpFile(myIdxFile, new ByteArrayInputStream(String.valueOf(myIdxCounter.get()).getBytes()), FileUtil.IOAction.DO_NOTHING); } catch (IOException ignored) {} } } } private static class ImageStatusTaskWrapper extends TaskCallbackHandler { @NotNull protected final VmwareCloudInstance myInstance; public ImageStatusTaskWrapper(@NotNull final VmwareCloudInstance instance) { myInstance = instance; } @Override public void onError(final Throwable th) { myInstance.setStatus(InstanceStatus.ERROR); if (th != null) { myInstance.updateErrors(TypedCloudErrorInfo.fromException(th)); LOG.warnAndDebugDetails("An error occurred: " + th.getLocalizedMessage() + " during processing " + myInstance.getName(), th); th.printStackTrace(); } else { myInstance.updateErrors(new TypedCloudErrorInfo("Unknown error during processing instance " + myInstance.getName())); LOG.warn("Unknown error during processing " + myInstance.getName()); } } } public void updateActualSourceState(@NotNull final VmwareSourceState state){ if (StringUtil.isNotEmpty(state.getSnapshotName()) && !state.equals(myActualSourceState.get())){ myActualSourceState.set(state); LOG.info("Updated actual vm source state name for " + myImageDetails.getSourceId() + " to " + state); } } }