/* * * * Copyright 2000-2014 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.connector; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.util.Pair; import com.vmware.vim25.*; import com.vmware.vim25.mo.*; import com.vmware.vim25.mo.util.MorUtil; import java.net.InetAddress; import java.net.MalformedURLException; import java.net.URL; import java.net.UnknownHostException; import java.rmi.RemoteException; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; import jetbrains.buildServer.Used; import jetbrains.buildServer.clouds.CloudException; import jetbrains.buildServer.clouds.CloudInstanceUserData; import jetbrains.buildServer.clouds.InstanceStatus; import jetbrains.buildServer.clouds.base.connector.AbstractInstance; import jetbrains.buildServer.clouds.base.errors.TypedCloudErrorInfo; import jetbrains.buildServer.clouds.server.CloudInstancesProvider; import jetbrains.buildServer.clouds.vmware.VmwareCloudImage; import jetbrains.buildServer.clouds.vmware.VmwareCloudImageDetails; import jetbrains.buildServer.clouds.vmware.VmwareCloudInstance; import jetbrains.buildServer.clouds.vmware.VmwareConstants; import jetbrains.buildServer.clouds.vmware.connector.beans.FolderBean; import jetbrains.buildServer.clouds.vmware.connector.beans.ResourcePoolBean; import jetbrains.buildServer.clouds.vmware.errors.VmwareCheckedCloudException; import jetbrains.buildServer.serverSide.TeamCityProperties; import jetbrains.buildServer.serverSide.crypt.EncryptUtil; import jetbrains.buildServer.util.StringUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import static jetbrains.buildServer.clouds.vmware.VMWarePropertiesNames.*; import static jetbrains.buildServer.clouds.vmware.connector.VmwareUtils.isSpecial; /** * @author Sergey.Pak * Date: 4/17/2014 * Time: 11:32 AM */ public class VMWareApiConnectorImpl implements VMWareApiConnector { private static final Logger LOG = Logger.getInstance(VMWareApiConnectorImpl.class.getName()); private static final String VM_TYPE = VirtualMachine.class.getSimpleName(); private static final String LINUX_GUEST_FAMILY = "linuxGuest"; private static final Pattern FQDN_PATTERN = Pattern.compile("[^\\.]+\\.(.+)"); private static final Pattern RESPOOL_PATTERN = Pattern.compile("resgroup-\\d+"); private static final Pattern FOLDER_PATTERN = Pattern.compile("group-v\\d+"); private static final String FOLDER_TYPE = Folder.class.getSimpleName(); private static final String RESPOOL_TYPE = ResourcePool.class.getSimpleName(); private static final String SPEC_FOLDER = "vm"; private static final String SPEC_RESPOOL = "Resources"; private static final long SHUTDOWN_TIMEOUT = 60 * 1000; private final URL myInstanceURL; private final String myUsername; private final String myPassword; private ServiceInstance myServiceInstance; private final String myDomain; // short living cache private static final Cache<Pair<String, String>, String> MANAGED_ENTITIES_NAMES_CACHE = CacheBuilder.newBuilder() .expireAfterWrite(20, TimeUnit.SECONDS) .build(); @Nullable private final String myServerUUID; // it can be null, when we create a temporary api connector for a short-term use (for example, when we prepopulate information on create/edit cloud profile page @Nullable private final String myProfileId; // we also create a separate connector for controller and which doesn't need this field @Nullable private final CloudInstancesProvider myInstancesProvider; public VMWareApiConnectorImpl(@NotNull final URL instanceURL, @NotNull final String username, @NotNull final String password, @Nullable final String serverUUID, @Nullable final String profileId, @Nullable final CloudInstancesProvider instancesProvider){ myInstanceURL = instanceURL; myUsername = username; myPassword = password; myServerUUID = serverUUID; myProfileId = profileId; myInstancesProvider = instancesProvider; myDomain = getTCServerDomain(); if (myDomain == null){ LOG.info("Unable to determine server domain. Linux guest hostname customization is disabled"); } else { LOG.info("Domain is " + myDomain + ". Will use the Linux guest hostname customization"); } } private synchronized Folder getRootFolder() throws VmwareCheckedCloudException { try { if (myServiceInstance != null) { final SessionManager sessionManager = myServiceInstance.getSessionManager(); if (sessionManager == null || sessionManager.getCurrentSession() == null) { myServiceInstance = null; } } } catch (Exception ex){ ex.printStackTrace(); myServiceInstance = null; } if (myServiceInstance == null){ try { myServiceInstance = new ServiceInstance(myInstanceURL, myUsername, myPassword, true, 10*1000, 30*1000); } catch (MalformedURLException e) { throw new VmwareCheckedCloudException("Invalid server URL", e); } catch (RemoteException e) { throw new VmwareCheckedCloudException(e); } } return myServiceInstance.getRootFolder(); } private boolean isId(String idName, Class instanceType){ if (instanceType == ResourcePool.class) { return RESPOOL_PATTERN.matcher(idName).matches(); } else if (instanceType == Folder.class) { return FOLDER_PATTERN.matcher(idName).matches(); } else { return false; } } @Nullable protected <T extends ManagedEntity> T findEntityByIdNameNullableOld(@NotNull final String idName, @NotNull final Class<T> instanceType, @Nullable final Datacenter dc) throws VmwareCheckedCloudException { try { if (isId(idName, instanceType)) { ManagedObjectReference mor = new ManagedObjectReference(); mor.setType(instanceType.getSimpleName()); mor.setVal(idName); return createExactManagedEntity(mor); } else { return searchManagedEntity(idName, instanceType, dc); } } catch (RemoteException e) { throw new VmwareCheckedCloudException(e); } } protected <T extends ManagedEntity> T createExactManagedEntity(final ManagedObjectReference mor) { return (T)MorUtil.createExactManagedEntity(myServiceInstance.getServerConnection(), mor); } protected <T extends ManagedEntity> T searchManagedEntity(final @NotNull String idName, final @NotNull Class<T> instanceType, final @Nullable Datacenter dc) throws RemoteException, VmwareCheckedCloudException { if (dc == null) { return (T)new InventoryNavigator(getRootFolder()).searchManagedEntity(instanceType.getSimpleName(), idName); } else { return (T)new InventoryNavigator(dc).searchManagedEntity(instanceType.getSimpleName(), idName); } } @NotNull protected <T extends ManagedEntity> Pair<T,Datacenter> findEntityByIdNameOld(String idName, Class<T> instanceType) throws VmwareCheckedCloudException { final AtomicReference<VmwareCheckedCloudException> exceptionRef = new AtomicReference<>(); final Optional<Pair<T, Datacenter>> any = findAllEntitiesOld(Datacenter.class) .stream() .map( dc -> { try { final T e = findEntityByIdNameNullableOld(idName, instanceType, dc); return (e != null) ? Pair.create(e, dc) : null; } catch (VmwareCheckedCloudException e) { LOG.warnAndDebugDetails("An exception while searching", e); exceptionRef.set(e); return null; } }) .filter(Objects::nonNull) .findAny(); if (exceptionRef.get() != null) { throw exceptionRef.get(); } if (!any.isPresent() ) { throw new VmwareCheckedCloudException(String.format("Unable to find %s '%s'", instanceType.getSimpleName(), idName)); } return any.get(); } protected Collection<VmwareInstance> findAllVirtualMachines() throws VmwareCheckedCloudException { final AtomicReference<VmwareCheckedCloudException> exceptionRef = new AtomicReference<>(); final Collection<VmwareInstance> result = findWithDatacenter(dc -> { final String datacenterId = dc.getMOR().getVal(); try { final ObjectContent[] ocs = getObjectContents(dc, new String[][]{{ "VirtualMachine", "name", "config.extraConfig", "config.template" , "config.changeVersion" , "runtime.powerState", "runtime.bootTime", "guest.ipAddress", "parent" },}); if (ocs == null){ return Stream.empty(); } return Arrays.stream(ocs) .map(oc->{ final Map<String, Object> mappedProperties = Arrays.stream(oc.getPropSet()).collect(Collectors.toMap( DynamicProperty::getName, DynamicProperty::getVal )); final String vmName = String.valueOf(mappedProperties.get("name")); try { return new VmwareInstance( vmName, oc.getObj().getVal(), ((ArrayOfOptionValue)mappedProperties.get("config.extraConfig")).getOptionValue(), (VirtualMachinePowerState)mappedProperties.get("runtime.powerState"), (Boolean)mappedProperties.get("config.template"), String.valueOf(mappedProperties.get("config.changeVersion")), (Calendar)mappedProperties.get("runtime.bootTime"), (String)mappedProperties.get("guest.ipAddress"), (ManagedObjectReference)mappedProperties.get("parent"), datacenterId ); } catch (Exception ex) { LOG.debug("Unable to process VM with name '" + vmName + "'. Not all properties are available"); return null; }}).filter(Objects::nonNull); } catch (RemoteException e) { LOG.warnAndDebugDetails("An error occurred while searching for all folders", e); exceptionRef.set(new VmwareCheckedCloudException(e)); return Stream.empty(); } }); if (exceptionRef.get() != null){ throw exceptionRef.get(); } LOG.debug( String.format("[%s]. All instances: [%s]" , myProfileId, String.join(",", result .stream() .map(VmwareInstance::getName).collect(Collectors.toList()) ) ) ); return result; } protected Map<String, VmwareInstance> findAllVirtualMachinesAsMap() throws VmwareCheckedCloudException{ return findAllVirtualMachines().stream().collect(Collectors.toMap(VmwareInstance::getName, Function.identity())); } @NotNull protected VmwareInstance findVirtualMachineOrThrowException(String vmName) throws VmwareCheckedCloudException { final VmwareInstance vmwareInstance = findAllVirtualMachinesAsMap().get(vmName); if (vmwareInstance == null) { throw new VmwareCheckedCloudException(String.format("Unable to find VirtualMachine by name '%s'", vmName)); } return vmwareInstance; } protected Collection<FolderBean> findAllFolders() throws VmwareCheckedCloudException { final AtomicReference<VmwareCheckedCloudException> exceptionRef = new AtomicReference<>(); final Collection<FolderBean> result = findWithDatacenter(dc -> { try { final ObjectContent[] ocs = getObjectContents(dc, new String[][]{{ "Folder", "name", "childType", "parent" },}); if (ocs == null){ return Stream.empty(); } final String datacenterId = dc.getMOR().getVal(); return Arrays.stream(ocs).map(oc -> { try { final Map<String, Object> mappedProperties = Arrays.stream(oc.getPropSet()).collect(Collectors.toMap( DynamicProperty::getName, DynamicProperty::getVal )); final String simpleName = String.valueOf(mappedProperties.get("name")); final ManagedObjectReference parent = (ManagedObjectReference)mappedProperties.get("parent"); LOG.debug("Found folder with name '" + simpleName + "'. Parent: " + (parent == null ? "null" : parent.toString())); if (simpleName.equals(SPEC_FOLDER)) { LOG.debug("The folder is a special folder. Skipping it..."); return null; } final String[] childTypes = ((ArrayOfString)mappedProperties.get("childType")).getString(); boolean skip = true; for (String childType : childTypes) { if (VM_TYPE.equals(childType)) { skip = false; break; } } if (skip) { LOG.debug("The folder cannot contain VMs. Skipping it..."); return null; } final String fullFolderPath = getFullPath(simpleName, oc.obj, parent, dc); LOG.debug("Calculated path: " + fullFolderPath); return new FolderBean(oc.obj, simpleName, fullFolderPath, childTypes, parent, datacenterId ); } catch (Exception ex) { LOG.warnAndDebugDetails("Error getting folder details", ex); return null; } }); } catch (RemoteException e) { LOG.warnAndDebugDetails("An error occurred while searching for all folders", e); exceptionRef.set(new VmwareCheckedCloudException(e)); return Stream.empty(); } }); return result; } protected Collection<ResourcePoolBean> findAllResourcePools() throws VmwareCheckedCloudException { final AtomicReference<VmwareCheckedCloudException> exceptionRef = new AtomicReference<>(); final Collection<ResourcePoolBean> result = findWithDatacenter(dc -> { try { final String datacenterId = dc.getMOR().getVal(); final ObjectContent[] ocs = getObjectContents(dc, new String[][]{{"ResourcePool", "name", "parent"},}); if (ocs == null){ return Stream.empty(); } return Arrays.stream(ocs).map(oc -> { final Map<String, Object> mappedProperties = Arrays.stream(oc.getPropSet()).collect(Collectors.toMap( DynamicProperty::getName, DynamicProperty::getVal )); final String simpleName = String.valueOf(mappedProperties.get("name")); final ManagedObjectReference parent = (ManagedObjectReference)mappedProperties.get("parent"); LOG.debug("Found respool with name '" + simpleName + "'. Parent: " + (parent == null ? "null" : parent.toString())); if ("Resources".equals(simpleName) && parent != null && !oc.obj.getType().equals(parent.getVal())){ LOG.debug("The pool is a special pool. Skipping it..."); return null; } final String path = getFullPath(simpleName, oc.obj, parent, dc); LOG.debug("Calculated path: " + path); return new ResourcePoolBean(oc.obj, simpleName, path, parent, datacenterId ); }); } catch (RemoteException e) { LOG.warnAndDebugDetails("An error occurred while searching for all resource pools", e); exceptionRef.set(new VmwareCheckedCloudException(e)); return Stream.empty(); } }); if (exceptionRef.get() != null){ throw exceptionRef.get(); } return result; } //protected 4 tests protected ObjectContent[] getObjectContents(final Datacenter dc, final String[][] typeinfo) throws RemoteException { return new InventoryNavigator(dc).retrieveObjectContents(typeinfo, true); } private <T extends VmwareManagedEntity> Collection<T> findWithDatacenter( Function<Datacenter, Stream<T>> mapper) throws VmwareCheckedCloudException { return findAllEntitiesOld(Datacenter.class).stream().flatMap(mapper).filter(Objects::nonNull).collect(Collectors.toList()); } protected <T extends ManagedEntity> Collection<T> findAllEntitiesOld(Class<T> instanceType) throws VmwareCheckedCloudException { final ManagedEntity[] managedEntities; try { managedEntities = new InventoryNavigator(getRootFolder()) .searchManagedEntities(new String[][]{{instanceType.getSimpleName(), "name"},}, true); } catch (RemoteException e) { throw new VmwareCheckedCloudException(e); } List<T> retval = new ArrayList<T>(); for (ManagedEntity managedEntity : managedEntities) { retval.add((T)managedEntity); } return retval; } protected <T extends ManagedEntity> Map<String, T> findAllEntitiesAsMapOld(Class<T> instanceType) throws VmwareCheckedCloudException { final ManagedEntity[] managedEntities; try { managedEntities = new InventoryNavigator(getRootFolder()) .searchManagedEntities(new String[][]{{instanceType.getSimpleName(), "name"},}, true); } catch (RemoteException e) { throw new VmwareCheckedCloudException(e); } Map<String, T> retval = new HashMap<String, T>(); for (ManagedEntity managedEntity : managedEntities) { try { retval.put(managedEntity.getName(), (T)managedEntity); } catch (Exception ex){} } return retval; } @NotNull public List<VmwareInstance> getVirtualMachines(boolean filterClones) throws VmwareCheckedCloudException { final Collection<VmwareInstance> allVms = findAllVirtualMachines(); return allVms.stream() .filter(vm -> vm.isInitialized() && (!filterClones || !vm.isClone())) .sorted() .collect(Collectors.toList()); } @Override @NotNull public <R extends AbstractInstance> Map<String, R> fetchInstances(@NotNull final VmwareCloudImage image) throws VmwareCheckedCloudException { Map<VmwareCloudImage, Map<String, R>> imageMap = fetchInstances(Collections.singleton(image)); Map<String, R> res = imageMap.get(image); return res == null ? Collections.emptyMap() : res; } @Override @NotNull public <R extends AbstractInstance> Map<VmwareCloudImage, Map<String, R>> fetchInstances(@NotNull final Collection<VmwareCloudImage> images) throws VmwareCheckedCloudException { Map<VmwareCloudImage, Map<String, R>> result = new HashMap<>(); List<VmwareCloudImage> unprocessed = new ArrayList<>(); final Map<String, VmwareInstance> allVmsAsMap = findAllVirtualMachines() .stream() .collect(Collectors.toMap(VmwareInstance::getName, Function.identity())); for (VmwareCloudImage image: images) { final VmwareCloudImageDetails imageDetails = image.getImageDetails(); if(imageDetails.getBehaviour().isUseOriginal()){ final VmwareInstance vmInstance = allVmsAsMap.get(imageDetails.getSourceVmName()); if (vmInstance == null){ throw new VmwareCheckedCloudException(String.format("Unable to find VirtualMachine '%s'", imageDetails.getSourceVmName())); } result.put(image, Collections.singletonMap(image.getName(), (R)vmInstance)); } else { unprocessed.add(image); } } if (unprocessed.isEmpty()) return result; Map<String, VmwareCloudImage> imageNameMap = new HashMap<>(); for (VmwareCloudImage image: unprocessed) { imageNameMap.put(image.getName(), image); } for (VmwareInstance vmInstance : allVmsAsMap.values()) { try { final String instanceImage = vmInstance.getImageName(); VmwareCloudImage image = imageNameMap.get(instanceImage); if (image != null) { Map<String, R> imageInstancesMap = result.get(image); if (imageInstancesMap == null) { imageInstancesMap = new HashMap<>(); final String serverUUID = vmInstance.getServerUUID(); if (StringUtil.isNotEmpty(serverUUID) && !serverUUID.equals(myServerUUID)) { LOG.debug(String.format("Instance '%s' belongs to server with another UUID('%s'). Our UUID is '%s'", vmInstance.getName(), serverUUID, myServerUUID)); continue; } result.put(image, imageInstancesMap); } imageInstancesMap.put(vmInstance.getName(), (R)vmInstance); } } catch (Exception ex) { LOG.debug("Unable to process VirtualMachine" + vmInstance.getId()); } } return result; } @NotNull @Override public Map<String, String> getCustomizationSpecs() { final Map<String,String> retval = new HashMap<>(); try { final CustomizationSpecManager specManager = myServiceInstance.getCustomizationSpecManager(); if (specManager == null) return retval; final CustomizationSpecInfo[] specs = specManager.getInfo(); if (specs != null) { for (CustomizationSpecInfo spec : specs) { retval.put(spec.getName(), spec.getType()); } } } catch (Exception ex){ LOG.warnAndDebugDetails("Can't get customization specs", ex); } return retval; } @Override public CustomizationSpec getCustomizationSpec(final String name) throws VmwareCheckedCloudException { final CustomizationSpecManager specManager = myServiceInstance.getCustomizationSpecManager(); if (specManager == null){ throw new VmwareCheckedCloudException("Customization Spec in not available: '" + name + "'"); } try { return specManager.getCustomizationSpec(name).getSpec(); } catch (RemoteException e) { throw new VmwareCheckedCloudException("Unable to get Customization Spec: '" + name + "'" , e); } } @Used("Tests") public Map<String, String> getVMParams(@NotNull final String vmName) throws VmwareCheckedCloudException { return findVirtualMachineOrThrowException(vmName).getProperties(); } @NotNull public List<FolderBean> getFolders() throws VmwareCheckedCloudException { final Collection<FolderBean> allFolders = findAllFolders(); return allFolders.stream().filter(this::canContainVMs).collect(Collectors.toList()); } @NotNull public List<ResourcePoolBean> getResourcePools() throws VmwareCheckedCloudException { final Collection<ResourcePoolBean> pools = findAllResourcePools(); return pools.stream() .filter(rp->!isSpecial(rp)) .sorted() .collect(Collectors.toList()); } private boolean canContainVMs(final FolderBean folder) { final String[] childTypes = folder.getChildType(); for (String childType : childTypes) { if (VM_TYPE.equals(childType)) { return true; } } return false; } private String getFullPath(@NotNull final String entityName, @NotNull final ManagedObjectReference mor, @Nullable final ManagedObjectReference firstParent, @Nullable final Datacenter dc){ final String uniqueName = String.format("%s (%s)", entityName, mor.getVal()); if (firstParent == null) { return uniqueName; } try { final String morPath = getFullMORPath(createExactManagedEntity(firstParent), dc); if (StringUtil.isEmpty(morPath)) { return uniqueName; } else { return morPath + "/" + entityName; } } catch (Exception ex){ LOG.warnAndDebugDetails("Can't calculate full path for " + uniqueName, ex); return uniqueName; } } @Nullable private String getFullFolderPath(final ManagedObjectReference mor, final Datacenter dc) { ManagedEntity entity; try { entity = findEntityByIdNameNullableOld(mor.getVal(), Folder.class, dc); if (entity != null) { return getFullMORPath(entity, dc); } else { return null; } } catch (VmwareCheckedCloudException e) { return mor.getVal(); } } @Nullable private String getResourcePoolPath(final ManagedObjectReference mor, final Datacenter dc) { ManagedEntity entity; try { entity = findEntityByIdNameNullableOld(mor.getVal(), ResourcePool.class, dc); if (entity != null) { return getFullMORPath(entity, dc); } else { return null; } } catch (VmwareCheckedCloudException e) { return mor.getVal(); } } private String getFullMORPath(@NotNull final ManagedEntity entity, @Nullable final Datacenter dc) { final ManagedObjectReference mor = entity.getMOR(); final Pair<String, String> morPair = Pair.create(mor.getType(), mor.getVal()); final String existingPath = MANAGED_ENTITIES_NAMES_CACHE.getIfPresent(morPair); if (existingPath != null) return existingPath; final ManagedEntity parent = entity.getParent(); final String entityName = entity.getName(); boolean skipName = (mor.getType().equals(FOLDER_TYPE) && (entityName.equals(SPEC_FOLDER) || !FOLDER_PATTERN.matcher(morPair.getSecond()).matches() )) || (mor.getType().equals(RESPOOL_TYPE) && entityName.equals(SPEC_RESPOOL)); if (parent == null){ final String name = skipName ? "" : entityName; MANAGED_ENTITIES_NAMES_CACHE.put(morPair, name); return name; } else { final String fullMORPath = getFullMORPath(parent, dc); final String delimiter = fullMORPath.isEmpty() ? "" : "/"; final String name = skipName ? fullMORPath : fullMORPath + delimiter + entityName; MANAGED_ENTITIES_NAMES_CACHE.put(morPair, name); return name; } } @NotNull private Map<String, VirtualMachineSnapshotTree> getSnapshotList(final VirtualMachine vm) { if (vm.getSnapshot() == null) { return Collections.emptyMap(); } final VirtualMachineSnapshotTree[] rootSnapshotList = vm.getSnapshot().getRootSnapshotList(); return snapshotNames(rootSnapshotList); } public Map<String, VirtualMachineSnapshotTree> getSnapshotList(final String vmName) throws VmwareCheckedCloudException { return getSnapshotList(findEntityByIdNameOld(vmName, VirtualMachine.class).getFirst()); } @Nullable public String getLatestSnapshot(@NotNull final String vmName, @NotNull final String snapshotNameMask) throws VmwareCheckedCloudException { if (VmwareConstants.CURRENT_STATE.equals(snapshotNameMask)){ return VmwareConstants.CURRENT_STATE; } final Map<String, VirtualMachineSnapshotTree> snapshotList = getSnapshotList(vmName); return getLatestSnapshot(snapshotNameMask, snapshotList); } private boolean containsDuplicates(Collection<? extends VmwareManagedEntity> entities){ final Set<String> names = new HashSet<String>(); for (VmwareManagedEntity entity : entities) { if (!names.add(entity.getName())){ return true; } } return false; } private String getLatestSnapshot(final String snapshotNameMask, final Map<String, VirtualMachineSnapshotTree> snapshotList) { if (snapshotNameMask == null) return null; if (!snapshotNameMask.contains("*") && !snapshotNameMask.contains("?")) { return snapshotList.containsKey(snapshotNameMask) ? snapshotNameMask : null; } Date latestTime = new Date(0); String latestSnapshotName = null; for (Map.Entry<String, VirtualMachineSnapshotTree> entry : snapshotList.entrySet()) { final String snapshotNameMaskRegex = StringUtil.convertWildcardToRegexp(snapshotNameMask); final Pattern pattern = Pattern.compile(snapshotNameMaskRegex); if (pattern.matcher(entry.getKey()).matches()) { final Date snapshotTime = entry.getValue().getCreateTime().getTime(); if (latestTime.before(snapshotTime)) { latestTime = snapshotTime; latestSnapshotName = entry.getKey(); } } } return latestSnapshotName; } @Nullable public Task startInstance(@NotNull final VmwareCloudInstance instance, @NotNull final String agentName, @NotNull final CloudInstanceUserData userData) throws VmwareCheckedCloudException, InterruptedException { final VirtualMachine vm = findEntityByIdNameOld(instance.getInstanceId(), VirtualMachine.class).getFirst(); if (vm != null) { try { return vm.powerOnVM_Task(null); } catch (RemoteException e) { throw new VmwareCheckedCloudException(e); } } else { instance.updateErrors(new TypedCloudErrorInfo(String.format("Instance %s doesn't exist", instance.getInstanceId()))); } return null; } public Task reconfigureInstance(@NotNull final VmwareCloudInstance instance, @NotNull final String agentName, @NotNull final CloudInstanceUserData userData) throws VmwareCheckedCloudException { final VirtualMachine vm = findEntityByIdNameOld(instance.getInstanceId(), VirtualMachine.class).getFirst(); final VirtualMachineConfigSpec spec = new VirtualMachineConfigSpec(); spec.setExtraConfig(new OptionValue[]{ createOptionValue(AGENT_NAME, agentName), createOptionValue(INSTANCE_NAME, instance.getInstanceId()), createOptionValue(AUTH_TOKEN, userData.getAuthToken()), createOptionValue(SERVER_URL, userData.getServerAddress()), createOptionValue(IMAGE_NAME, instance.getImageId()), createOptionValue(USER_DATA, userData.serialize()) }); try { return vm.reconfigVM_Task(spec); } catch (RemoteException e) { throw new VmwareCheckedCloudException(e); } } @Nullable @Override public Task cloneAndStartVm(@NotNull final VmwareCloudInstance instance) throws VmwareCheckedCloudException { final VmwareCloudImageDetails imageDetails = instance.getImage().getImageDetails(); LOG.info(String.format("Attempting to clone VM %s into %s", imageDetails.getSourceVmName(), instance.getName())); final Pair<VirtualMachine, Datacenter> pair = findEntityByIdNameOld(imageDetails.getSourceVmName(), VirtualMachine.class); final VirtualMachine vm = pair.getFirst(); final Datacenter datacenter = pair.getSecond(); final VirtualMachineConfigSpec config = new VirtualMachineConfigSpec(); final VirtualMachineCloneSpec cloneSpec = new VirtualMachineCloneSpec(); final VirtualMachineRelocateSpec location = new VirtualMachineRelocateSpec(); cloneSpec.setPowerOn(true); cloneSpec.setLocation(location); cloneSpec.setConfig(config); final boolean disableOsCustomization = TeamCityProperties.getBoolean(VmwareConstants.DISABLE_OS_CUSTOMIZATION); if (!VmwareConstants.DEFAULT_RESOURCE_POOL.equals(imageDetails.getResourcePoolId())) { final ResourcePool pool = findEntityByIdNameNullableOld(imageDetails.getResourcePoolId(), ResourcePool.class, datacenter); if (pool != null) { location.setPool(pool.getMOR()); } else { LOG.warn(String.format("Unable to find resource pool %s at datacenter %s. Will clone at the image resource pool instead" , imageDetails.getResourcePoolId() , datacenter == null? "<not provided>": datacenter.getName())); } } final Map<String, VirtualMachineSnapshotTree> snapshotList = getSnapshotList(vm); final String snapshotName = instance.getSnapshotName(); if (imageDetails.useCurrentVersion() || StringUtil.isEmpty(snapshotName)) { LOG.info("Snapshot name is not specified. Will clone latest VM state"); } else { final VirtualMachineSnapshotTree obj = snapshotList.get(snapshotName); final ManagedObjectReference snapshot = obj == null ? null : obj.getSnapshot(); cloneSpec.setSnapshot(snapshot); if (snapshot != null) { if (TeamCityProperties.getBooleanOrTrue(VmwareConstants.USE_LINKED_CLONE)) { LOG.info("Using linked clone. Snapshot name: " + snapshotName); location.setDiskMoveType(VirtualMachineRelocateDiskMoveOptions.createNewChildDiskBacking.name()); } else { LOG.info("Using full clone. Snapshot name: " + snapshotName); } } else { final String errorText = "Unable to find snapshot " + snapshotName; throw new VmwareCheckedCloudException(errorText); } } final VirtualMachineConfigInfo vmConfig = vm.getConfig(); config.setExtraConfig(new OptionValue[]{ createOptionValue(TEAMCITY_VMWARE_CLONED_INSTANCE, "true"), createOptionValue(TEAMCITY_VMWARE_IMAGE_SOURCE_VM_NAME, imageDetails.getSourceVmName()), createOptionValue(TEAMCITY_VMWARE_IMAGE_SOURCE_ID, imageDetails.getSourceId()), createOptionValue(TEAMCITY_VMWARE_IMAGE_SNAPSHOT, snapshotName), createOptionValue(TEAMCITY_VMWARE_IMAGE_CHANGE_VERSION, vmConfig.getChangeVersion()), createOptionValue(TEAMCITY_VMWARE_PROFILE_ID, StringUtil.emptyIfNull(myProfileId)), createOptionValue(TEAMCITY_VMWARE_SERVER_UUID, StringUtil.emptyIfNull(myServerUUID)) }); final GuestInfo guest = vm.getGuest(); String guestFamily = guest != null ? guest.getGuestFamily() : null; if (guestFamily == null){ final String guestFullName = vmConfig.getGuestFullName(); if (guestFullName != null && guestFullName.contains("Linux")){ guestFamily = LINUX_GUEST_FAMILY; } } if (StringUtil.isNotEmpty(imageDetails.getCustomizationSpec())){ LOG.info(String.format("Will use Customization Spec '%s' to clone %s into %s" , imageDetails.getCustomizationSpec(), imageDetails.getSourceVmName(), instance.getName())); cloneSpec.setCustomization(getCustomizationSpec(imageDetails.getCustomizationSpec())); } else if (!disableOsCustomization && myDomain != null && LINUX_GUEST_FAMILY.equals(guestFamily)){ LOG.info("Will use basic Linux customization (will customize hostname)"); // TODO: remove later, after all profiles are updated to use customization spec final CustomizationSpec customization = new CustomizationSpec(); final CustomizationLinuxPrep linuxPrep = new CustomizationLinuxPrep(); final CustomizationLinuxOptions linuxOptions = new CustomizationLinuxOptions(); linuxPrep.setHostName(new CustomizationVirtualMachineName()); linuxPrep.setDomain(myDomain); customization.setIdentity(linuxPrep); customization.setOptions(linuxOptions); customization.setGlobalIPSettings(new CustomizationGlobalIPSettings()); final CustomizationAdapterMapping mapping = new CustomizationAdapterMapping(); final CustomizationIPSettings ipSettings = new CustomizationIPSettings(); mapping.setAdapter(ipSettings); ipSettings.setIp(new CustomizationDhcpIpGenerator()); customization.setNicSettingMap(new CustomizationAdapterMapping[]{mapping}); cloneSpec.setCustomization(customization); } try { final Folder folder = findEntityByIdNameNullableOld(imageDetails.getFolderId(), Folder.class, datacenter); if (folder != null) { return vm.cloneVM_Task(folder, instance.getName(), cloneSpec); } else { String dcName = datacenter == null ? "root" : datacenter.getName(); throw new VmwareCheckedCloudException( String.format("Unable to find folder %s in datacenter %s", imageDetails.getFolderId(), dcName) ); } } catch (RemoteException e) { throw new VmwareCheckedCloudException(e); } } protected static Map<String, VirtualMachineSnapshotTree> snapshotNames(@Nullable final VirtualMachineSnapshotTree[] trees) { final Map<String, VirtualMachineSnapshotTree> treeNames = new HashMap<String, VirtualMachineSnapshotTree>(); if (trees != null) { for (final VirtualMachineSnapshotTree tree : trees) { treeNames.put(tree.getName(), tree); treeNames.putAll(snapshotNames(tree.getChildSnapshotList())); } } return treeNames; } private OptionValue createOptionValue(@NotNull final String key, @Nullable final String value) { final OptionValue optionValue = new OptionValue(); optionValue.setKey(key); optionValue.setValue(value == null ? "" : value); return optionValue; } public Task stopInstance(@NotNull final VmwareCloudInstance instance) { instance.setStatus(InstanceStatus.STOPPING); try { VirtualMachine vm = findEntityByIdNameOld(instance.getInstanceId(), VirtualMachine.class).getFirst(); if (getInstanceStatus(vm) == InstanceStatus.STOPPED) { return emptyTask(); } return doShutdown(instance, vm); } catch (Exception ex) { instance.updateErrors(TypedCloudErrorInfo.fromException(ex)); throw new CloudException(ex.getMessage(),ex); } } private Task doShutdown(@NotNull final VmwareCloudInstance instance, @NotNull final VirtualMachine vm) throws VmwareCheckedCloudException { try { guestShutdown(instance, vm); final long shutdownStartTime = System.currentTimeMillis(); return new Task(null, null){ private final TaskInfo myInfo = new TaskInfo(); {myInfo.setState(TaskInfoState.running);} @Override public String waitForTask() throws RemoteException, InterruptedException { if (waitForStatus(shutdownStartTime, 5000) != InstanceStatus.STOPPED) { myInfo.setState(TaskInfoState.error); } else { myInfo.setState(TaskInfoState.success); } return myInfo.getState().name(); } @Override public String waitForTask(final int runningDelayInMillSecond, final int queuedDelayInMillSecond) throws RemoteException, InterruptedException { if (runningDelayInMillSecond >= (System.currentTimeMillis() - shutdownStartTime)){ return waitForTask(); } else { final InstanceStatus instanceStatus = waitForStatus(runningDelayInMillSecond, 5000); if (instanceStatus == InstanceStatus.STOPPED){ myInfo.setState(TaskInfoState.success); } } return myInfo.getState().name(); } @Override public TaskInfo getTaskInfo() throws RemoteException { try { final InstanceStatus instanceStatus = waitForStatus(0, 5000); if (instanceStatus == InstanceStatus.STOPPED){ myInfo.setState(TaskInfoState.success); } } catch (InterruptedException e) { e.printStackTrace(); } return myInfo; } public InstanceStatus waitForStatus(long maxWaitTime, long delay) throws RemoteException, InterruptedException { //TODO rework try { VirtualMachine vmCopy = findEntityByIdNameOld(instance.getInstanceId(), VirtualMachine.class).getFirst(); final long startHere = System.currentTimeMillis(); while (getInstanceStatus(vmCopy) != InstanceStatus.STOPPED && (System.currentTimeMillis() - shutdownStartTime) < SHUTDOWN_TIMEOUT) { if ((System.currentTimeMillis() - startHere) >= maxWaitTime) { break; } Thread.sleep(delay); vmCopy = findEntityByIdNameOld(instance.getInstanceId(), VirtualMachine.class).getFirst(); } return getInstanceStatus(vmCopy); } catch (VmwareCheckedCloudException e) { throw new RemoteException(e.getMessage(), e); } } @Override public void cancelTask() throws RemoteException { // do nothing; } }; } catch (RemoteException e) { LOG.info("Will attempt to force shutdown due to error: " + e.toString()); try { return forceShutdown(vm); } catch (RemoteException e1) { throw new VmwareCheckedCloudException(e1); } } } private void guestShutdown(final VmwareCloudInstance instance, final VirtualMachine vm) throws RemoteException { try { vm.shutdownGuest(); } catch (ToolsUnavailable e) { LOG.warn(String.format("Guest tools not installed or unavailable for '%s'", instance.getName())); throw e; } catch (InvalidState e) { final VirtualMachineRuntimeInfo runtime = vm.getRuntime(); final String powerStateInfo = runtime==null ? "no runtime info" : runtime.getPowerState().name(); LOG.warn(String.format("Invalid power state for '%s': %s", instance.getName(), powerStateInfo)); throw e; } catch (TaskInProgress e) { LOG.warn(String.format("Already task in progress for '%s': '%s'", instance.getName(), e.getTask().getType())); throw e; } catch (RuntimeFault runtimeFault) { LOG.warn(String.format("Runtime fault in guest shutdown for '%s': '%s'", instance.getName(), runtimeFault.toString())); throw runtimeFault; } } private Task forceShutdown(@NotNull final VirtualMachine vm) throws RemoteException { return vm.powerOffVM_Task(); } @Override public Task deleteInstance(final VmwareCloudInstance instance) { LOG.info("Will delete instance " + instance.getName()); try { final VirtualMachine vm = findEntityByIdNameOld(instance.getInstanceId(), VirtualMachine.class).getFirst(); return vm.destroy_Task(); } catch (Exception e) { // stacktrace goes to SDK details, so no value of dumping it to log here LOG.warnAndDebugDetails("An error occured during deleting instance " + instance.getName(), e); instance.updateErrors(TypedCloudErrorInfo.fromException(e)); } return emptyTask(); } public void restartInstance(VmwareCloudInstance instance) throws VmwareCheckedCloudException { final VirtualMachine vm = findEntityByIdNameOld(instance.getInstanceId(), VirtualMachine.class).getFirst(); try { vm.rebootGuest(); } catch (RemoteException e) { throw new VmwareCheckedCloudException(e); } } public boolean checkVirtualMachineExists(@NotNull final String vmName) { try { return findEntityByIdNameNullableOld(vmName, VirtualMachine.class, null) != null; } catch (VmwareCheckedCloudException e) { return false; } } public void processImageInstances(@NotNull final VmwareCloudImage image, @NotNull final VmwareInstanceProcessor processor) { try { final Map<String, VmwareInstance> instances = fetchInstances(image); for (VmwareInstance instance : instances.values()) { processor.process(instance); } } catch (VmwareCheckedCloudException e) { LOG.warnAndDebugDetails("Unable to process image instances", e); } } @NotNull public VmwareInstance getInstanceDetails(String instanceName) throws VmwareCheckedCloudException { return findVirtualMachineOrThrowException(instanceName); } @Nullable private String getOptionValue(@NotNull final VirtualMachine vm, @NotNull final String optionName) { final VirtualMachineConfigInfo config = vm.getConfig(); if (config == null) return null; final OptionValue[] extraConfig = config.getExtraConfig(); for (OptionValue option : extraConfig) { if (optionName.equals(option.getKey())) { return String.valueOf(option.getValue()); } } return null; } @NotNull public InstanceStatus getInstanceStatus(@NotNull final VirtualMachine vm) { if (vm.getRuntime() == null || vm.getRuntime().getPowerState() == VirtualMachinePowerState.poweredOff) { return InstanceStatus.STOPPED; } if (vm.getRuntime().getPowerState() == VirtualMachinePowerState.poweredOn) { return InstanceStatus.RUNNING; } return InstanceStatus.UNKNOWN; } public void dispose(){ try { if (myServiceInstance != null) { final ServerConnection serverConnection = myServiceInstance.getServerConnection(); if (serverConnection != null) serverConnection.logout(); } } catch (Exception ex){} } public void test() throws VmwareCheckedCloudException { getRootFolder(); } @NotNull @Override public String getKey() { return getKey(myInstanceURL, myUsername, myPassword); } @NotNull @Override public Map<String, InstanceStatus> getInstanceStatusesIfExists(@NotNull Set<String> instanceNames) { try { return findAllVirtualMachines() .stream() .filter(vm->instanceNames.contains(vm.getName())) .collect(Collectors.toMap(VmwareInstance::getName, VmwareInstance::getInstanceStatus)); } catch (VmwareCheckedCloudException e) { LOG.debug(e.toString()); return instanceNames.stream().collect(Collectors.toMap(Function.identity(), in-> InstanceStatus.ERROR)); } } @NotNull public TypedCloudErrorInfo[] checkImage(@NotNull final VmwareCloudImage image) { final VmwareCloudImageDetails imageDetails = image.getImageDetails(); final String vmName = imageDetails.getSourceVmName(); try { final VirtualMachine vm = findEntityByIdNameNullableOld(vmName, VirtualMachine.class, null); if (vm == null){ return new TypedCloudErrorInfo[]{new TypedCloudErrorInfo("NoVM", "No such VM: " + vmName)}; } if (!imageDetails.getBehaviour().isUseOriginal() && !imageDetails.useCurrentVersion()) { final String snapshotName = imageDetails.getSnapshotName(); final Map<String, VirtualMachineSnapshotTree> snapshotList = getSnapshotList(vm); final String latestSnapshot = getLatestSnapshot(snapshotName, snapshotList); if (StringUtil.isNotEmpty(snapshotName) && latestSnapshot == null) { return new TypedCloudErrorInfo[]{new TypedCloudErrorInfo("NoSnapshot", "No such snapshot: " + snapshotName)}; } image.updateActualSnapshotName(latestSnapshot); if (myInstancesProvider != null) { final Collection<VmwareCloudInstance> instances = image.getInstances(); for (VmwareCloudInstance instance : instances) { if (!StringUtil.areEqual(instance.getSnapshotName(), latestSnapshot)) { myInstancesProvider.markInstanceExpired(instance); } } } else { LOG.debug("CloudInstancesProvider is null"); } } } catch (VmwareCheckedCloudException e) { return new TypedCloudErrorInfo[]{TypedCloudErrorInfo.fromException(e)}; } return new TypedCloudErrorInfo[0]; } @NotNull public TypedCloudErrorInfo[] checkInstance(@NotNull final VmwareCloudInstance instance) { return new TypedCloudErrorInfo[0]; } private static <T extends ManagedEntity> T getParentOfType(ManagedEntity entity, Class<T> parentType){ while(entity != null){ if (parentType.isAssignableFrom(entity.getClass())){ return (T)entity; } entity = entity.getParent(); } return null; } @Nullable private static String getTCServerDomain(){ try { final String fqdn = InetAddress.getLocalHost().getCanonicalHostName(); final Matcher matcher = FQDN_PATTERN.matcher(fqdn); if (matcher.matches()) { return matcher.group(1); } else { return null; } } catch (UnknownHostException ex){ LOG.info("Unable to resolve FQDN. Linux hostname customization will be disabled: " + ex.toString()); return null; } } private static Task emptyTask(){ return new Task(null, null) { @Override public TaskInfo getTaskInfo() throws RemoteException { final TaskInfo taskInfo = new TaskInfo(); taskInfo.setState(TaskInfoState.success); return taskInfo; } @Override public String waitForTask() throws RemoteException, InterruptedException { return Task.SUCCESS; } }; } public static String getKey(@NotNull final URL serverUrl, @NotNull final String username, @NotNull final String pwd){ return String.format("%s_%s%s", serverUrl.toString().toLowerCase(), username.toLowerCase(), EncryptUtil.scramble(pwd)); } }