diff --git a/api/src/main/java/org/apache/cloudstack/api/response/NetworkResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/NetworkResponse.java index a80317c83cd..db811ffbe2d 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/NetworkResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/NetworkResponse.java @@ -196,6 +196,10 @@ public class NetworkResponse extends BaseResponseWithAssociatedNetwork implement @Param(description = "true network requires restart") private Boolean restartRequired; + @SerializedName(ApiConstants.SPECIFY_VLAN) + @Param(description = "true if network supports specifying vlan, false otherwise") + private Boolean specifyVlan; + @SerializedName(ApiConstants.SPECIFY_IP_RANGES) @Param(description = "true if network supports specifying ip ranges, false otherwise") private Boolean specifyIpRanges; @@ -516,6 +520,10 @@ public class NetworkResponse extends BaseResponseWithAssociatedNetwork implement this.restartRequired = restartRequired; } + public void setSpecifyVlan(Boolean specifyVlan) { + this.specifyVlan = specifyVlan; + } + public void setSpecifyIpRanges(Boolean specifyIpRanges) { this.specifyIpRanges = specifyIpRanges; } diff --git a/engine/schema/src/main/java/com/cloud/user/UserAccountVO.java b/engine/schema/src/main/java/com/cloud/user/UserAccountVO.java index d204f67dc93..e4fcbad6b02 100644 --- a/engine/schema/src/main/java/com/cloud/user/UserAccountVO.java +++ b/engine/schema/src/main/java/com/cloud/user/UserAccountVO.java @@ -33,11 +33,11 @@ import javax.persistence.Table; import javax.persistence.Transient; import org.apache.cloudstack.api.InternalIdentity; +import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; +import org.apache.commons.lang3.StringUtils; import com.cloud.utils.db.Encrypt; import com.cloud.utils.db.GenericDao; -import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; -import org.apache.commons.lang3.StringUtils; @Entity @Table(name = "user") @@ -131,12 +131,6 @@ public class UserAccountVO implements UserAccount, InternalIdentity { public UserAccountVO() { } - @Override - public String toString() { - return String.format("UserAccount %s.", ReflectionToStringBuilderUtils.reflectOnlySelectedFields - (this, "id", "uuid", "username", "accountName")); - } - @Override public long getId() { return id; @@ -379,4 +373,10 @@ public class UserAccountVO implements UserAccount, InternalIdentity { public void setDetails(Map details) { this.details = details; } + + @Override + public String toString() { + return String.format("UserAccount %s.", ReflectionToStringBuilderUtils.reflectOnlySelectedFields + (this, "id", "uuid", "username", "accountName")); + } } diff --git a/engine/schema/src/main/resources/META-INF/db/procedures/cloud.idempotent_update_api_permission.sql b/engine/schema/src/main/resources/META-INF/db/procedures/cloud.idempotent_update_api_permission.sql new file mode 100644 index 00000000000..c53e0067061 --- /dev/null +++ b/engine/schema/src/main/resources/META-INF/db/procedures/cloud.idempotent_update_api_permission.sql @@ -0,0 +1,52 @@ +-- 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. + +DROP PROCEDURE IF EXISTS `cloud`.`IDEMPOTENT_UPDATE_API_PERMISSION`; + +CREATE PROCEDURE `cloud`.`IDEMPOTENT_UPDATE_API_PERMISSION` ( + IN role VARCHAR(255), + IN rule VARCHAR(255), + IN permission VARCHAR(255) +) +BEGIN + DECLARE role_id BIGINT(20) UNSIGNED +; DECLARE max_sort_order BIGINT(20) UNSIGNED + +; SELECT `r`.`id` INTO role_id + FROM `cloud`.`roles` `r` + WHERE `r`.`name` = role + AND `r`.`is_default` = 1 + +; SELECT MAX(`rp`.`sort_order`) INTO max_sort_order + FROM `cloud`.`role_permissions` `rp` + WHERE `rp`.`role_id` = role_id + +; IF NOT EXISTS ( + SELECT * FROM `cloud`.`role_permissions` `rp` + WHERE `rp`.`role_id` = role_id + AND `rp`.`rule` = rule + ) THEN + UPDATE `cloud`.`role_permissions` `rp` + SET `rp`.`sort_order` = max_sort_order + 1 + WHERE `rp`.`sort_order` = max_sort_order + AND `rp`.`role_id` = role_id + +; INSERT INTO `cloud`.`role_permissions` + (uuid, role_id, rule, permission, sort_order) + VALUES (uuid(), role_id, rule, permission, max_sort_order) +; END IF +;END; diff --git a/engine/schema/src/main/resources/META-INF/db/schema-41910to41920.sql b/engine/schema/src/main/resources/META-INF/db/schema-41910to41920.sql index 2ce8ea99bd1..12ead739d84 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-41910to41920.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-41910to41920.sql @@ -21,3 +21,25 @@ -- Add last_id to the volumes table CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.volumes', 'last_id', 'bigint(20) unsigned DEFAULT NULL'); + +-- Grant access to 2FA APIs for the "Read-Only User - Default" role + +CALL `cloud`.`IDEMPOTENT_UPDATE_API_PERMISSION`('Read-Only User - Default', 'setupUserTwoFactorAuthentication', 'ALLOW'); +CALL `cloud`.`IDEMPOTENT_UPDATE_API_PERMISSION`('Read-Only User - Default', 'validateUserTwoFactorAuthenticationCode', 'ALLOW'); +CALL `cloud`.`IDEMPOTENT_UPDATE_API_PERMISSION`('Read-Only User - Default', 'listUserTwoFactorAuthenticatorProviders', 'ALLOW'); + +-- Grant access to 2FA APIs for the "Support User - Default" role + +CALL `cloud`.`IDEMPOTENT_UPDATE_API_PERMISSION`('Support User - Default', 'setupUserTwoFactorAuthentication', 'ALLOW'); +CALL `cloud`.`IDEMPOTENT_UPDATE_API_PERMISSION`('Support User - Default', 'validateUserTwoFactorAuthenticationCode', 'ALLOW'); +CALL `cloud`.`IDEMPOTENT_UPDATE_API_PERMISSION`('Support User - Default', 'listUserTwoFactorAuthenticatorProviders', 'ALLOW'); + +-- Grant access to 2FA APIs for the "Read-Only Admin - Default" role + +CALL `cloud`.`IDEMPOTENT_UPDATE_API_PERMISSION`('Read-Only Admin - Default', 'setupUserTwoFactorAuthentication', 'ALLOW'); +CALL `cloud`.`IDEMPOTENT_UPDATE_API_PERMISSION`('Read-Only Admin - Default', 'validateUserTwoFactorAuthenticationCode', 'ALLOW'); + +-- Grant access to 2FA APIs for the "Support Admin - Default" role + +CALL `cloud`.`IDEMPOTENT_UPDATE_API_PERMISSION`('Support Admin - Default', 'setupUserTwoFactorAuthentication', 'ALLOW'); +CALL `cloud`.`IDEMPOTENT_UPDATE_API_PERMISSION`('Support Admin - Default', 'validateUserTwoFactorAuthenticationCode', 'ALLOW'); diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42000to42010.sql b/engine/schema/src/main/resources/META-INF/db/schema-42000to42010.sql index 976ef217832..92e0dbb5b2a 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42000to42010.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42000to42010.sql @@ -22,6 +22,7 @@ -- Add column api_key_access to user and account tables CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.user', 'api_key_access', 'boolean DEFAULT NULL COMMENT "is api key access allowed for the user" AFTER `secret_key`'); CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.account', 'api_key_access', 'boolean DEFAULT NULL COMMENT "is api key access allowed for the account" '); +CALL `cloud_usage`.`IDEMPOTENT_ADD_COLUMN`('cloud_usage.account', 'api_key_access', 'boolean DEFAULT NULL COMMENT "is api key access allowed for the account" '); -- Modify index for mshost_peer DELETE FROM `cloud`.`mshost_peer`; diff --git a/engine/storage/src/main/java/org/apache/cloudstack/storage/allocator/AbstractStoragePoolAllocator.java b/engine/storage/src/main/java/org/apache/cloudstack/storage/allocator/AbstractStoragePoolAllocator.java index 63524ccb6db..2c034d8429a 100644 --- a/engine/storage/src/main/java/org/apache/cloudstack/storage/allocator/AbstractStoragePoolAllocator.java +++ b/engine/storage/src/main/java/org/apache/cloudstack/storage/allocator/AbstractStoragePoolAllocator.java @@ -124,18 +124,24 @@ public abstract class AbstractStoragePoolAllocator extends AdapterBase implement protected List reorderPoolsByCapacity(DeploymentPlan plan, List pools) { Long zoneId = plan.getDataCenterId(); Long clusterId = plan.getClusterId(); - short capacityType; if (CollectionUtils.isEmpty(pools)) { return null; } - if (pools.get(0).getPoolType().isShared()) { + short capacityType = Capacity.CAPACITY_TYPE_LOCAL_STORAGE; + String storageType = "local"; + StoragePool storagePool = pools.get(0); + if (storagePool.isShared()) { capacityType = Capacity.CAPACITY_TYPE_STORAGE_ALLOCATED; - } else { - capacityType = Capacity.CAPACITY_TYPE_LOCAL_STORAGE; + storageType = "shared"; } + logger.debug(String.format( + "Filtering storage pools by capacity type [%s] as the first storage pool of the list, with name [%s] and ID [%s], is a [%s] storage.", + capacityType, storagePool.getName(), storagePool.getUuid(), storageType + )); + List poolIdsByCapacity = capacityDao.orderHostsByFreeCapacity(zoneId, clusterId, capacityType); logger.debug(String.format("List of pools in descending order of available capacity [%s].", poolIdsByCapacity)); @@ -221,6 +227,8 @@ public abstract class AbstractStoragePoolAllocator extends AdapterBase implement } List reorderStoragePoolsBasedOnAlgorithm(List pools, DeploymentPlan plan, Account account) { + logger.debug(String.format("Using allocation algorithm [%s] to reorder pools.", allocationAlgorithm)); + if (allocationAlgorithm.equals("random") || allocationAlgorithm.equals("userconcentratedpod_random") || (account == null)) { reorderRandomPools(pools); } else if (StringUtils.equalsAny(allocationAlgorithm, "userdispersing", "firstfitleastconsumed")) { diff --git a/packaging/el8/cloud.spec b/packaging/el8/cloud.spec index fbbb7abe350..244f4431a3b 100644 --- a/packaging/el8/cloud.spec +++ b/packaging/el8/cloud.spec @@ -71,7 +71,7 @@ Requires: (openssh-clients or openssh) Requires: (nfs-utils or nfs-client) Requires: iproute Requires: wget -Requires: mysql +Requires: (mysql or mariadb) Requires: sudo Requires: /sbin/service Requires: /sbin/chkconfig diff --git a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterScaleWorker.java b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterScaleWorker.java index de85e6231f2..4d50ef7e1f8 100644 --- a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterScaleWorker.java +++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterScaleWorker.java @@ -203,7 +203,7 @@ public class KubernetesClusterScaleWorker extends KubernetesClusterResourceModif retryCounter++; try { Pair result = SshHelper.sshExecute(ipAddress, port, getControlNodeLoginUser(), - pkFile, null, String.format("sudo /opt/bin/kubectl drain %s --ignore-daemonsets --delete-local-data", hostName), + pkFile, null, String.format("sudo /opt/bin/kubectl drain %s --ignore-daemonsets --delete-emptydir-data", hostName), 10000, 10000, 60000); if (!result.first()) { logger.warn("Draining node: {} on VM: {} in Kubernetes cluster: {} unsuccessful", hostName, userVm, kubernetesCluster); diff --git a/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/MockAccountManager.java b/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/MockAccountManager.java index 7d27e6b77ce..12741e0996a 100644 --- a/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/MockAccountManager.java +++ b/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/MockAccountManager.java @@ -527,6 +527,10 @@ public class MockAccountManager extends ManagerBase implements AccountManager { @Override public void checkApiAccess(Account account, String command) throws PermissionDeniedException { + } + @Override + public UserAccount clearUserTwoFactorAuthenticationInSetupStateOnLogin(UserAccount user) { + return null; } } diff --git a/scripts/util/create-kubernetes-binaries-iso.sh b/scripts/util/create-kubernetes-binaries-iso.sh index d5fb014f220..e4364054d2a 100755 --- a/scripts/util/create-kubernetes-binaries-iso.sh +++ b/scripts/util/create-kubernetes-binaries-iso.sh @@ -53,7 +53,7 @@ echo "Downloading Kubernetes tools ${RELEASE}..." k8s_dir="${working_dir}/k8s" mkdir -p "${k8s_dir}" cd "${k8s_dir}" -curl -L --remote-name-all https://storage.googleapis.com/kubernetes-release/release/${RELEASE}/bin/linux/amd64/{kubeadm,kubelet,kubectl} +curl -L --remote-name-all https://dl.k8s.io/release/${RELEASE}/bin/linux/amd64/{kubeadm,kubelet,kubectl} kubeadm_file_permissions=`stat --format '%a' kubeadm` chmod +x kubeadm diff --git a/server/src/main/java/com/cloud/api/ApiResponseHelper.java b/server/src/main/java/com/cloud/api/ApiResponseHelper.java index 810f0abd7e0..fcc4444670c 100644 --- a/server/src/main/java/com/cloud/api/ApiResponseHelper.java +++ b/server/src/main/java/com/cloud/api/ApiResponseHelper.java @@ -2565,6 +2565,7 @@ public class ApiResponseHelper implements ResponseGenerator { response.setIsSystem(networkOffering.isSystemOnly()); response.setNetworkOfferingAvailability(networkOffering.getAvailability().toString()); response.setIsPersistent(networkOffering.isPersistent()); + response.setSpecifyVlan(networkOffering.isSpecifyVlan()); if (Network.GuestType.Isolated.equals(network.getGuestType()) && network.getVpcId() == null) { response.setEgressDefaultPolicy(networkOffering.isEgressDefaultPolicy()); } diff --git a/server/src/main/java/com/cloud/api/ApiServer.java b/server/src/main/java/com/cloud/api/ApiServer.java index 6016b24502e..8964001a5d0 100644 --- a/server/src/main/java/com/cloud/api/ApiServer.java +++ b/server/src/main/java/com/cloud/api/ApiServer.java @@ -1197,7 +1197,7 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer domainId = userDomain.getId(); } - final UserAccount userAcct = accountMgr.authenticateUser(username, password, domainId, loginIpAddress, requestParameters); + UserAccount userAcct = accountMgr.authenticateUser(username, password, domainId, loginIpAddress, requestParameters); if (userAcct != null) { final String timezone = userAcct.getTimezone(); float offsetInHrs = 0f; @@ -1242,6 +1242,7 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer session.setAttribute("timezoneoffset", Float.valueOf(offsetInHrs).toString()); } + userAcct = accountMgr.clearUserTwoFactorAuthenticationInSetupStateOnLogin(userAcct); boolean is2faEnabled = false; if (userAcct.isUser2faEnabled() || (Boolean.TRUE.equals(AccountManagerImpl.enableUserTwoFactorAuthentication.valueIn(userAcct.getDomainId())) && Boolean.TRUE.equals(AccountManagerImpl.mandateUserTwoFactorAuthentication.valueIn(userAcct.getDomainId())))) { is2faEnabled = true; diff --git a/server/src/main/java/com/cloud/storage/StorageManagerImpl.java b/server/src/main/java/com/cloud/storage/StorageManagerImpl.java index 4ca5bf2eb92..8e99e3429ba 100644 --- a/server/src/main/java/com/cloud/storage/StorageManagerImpl.java +++ b/server/src/main/java/com/cloud/storage/StorageManagerImpl.java @@ -62,8 +62,8 @@ import org.apache.cloudstack.api.command.admin.storage.DeleteObjectStoragePoolCm import org.apache.cloudstack.api.command.admin.storage.DeletePoolCmd; import org.apache.cloudstack.api.command.admin.storage.DeleteSecondaryStagingStoreCmd; import org.apache.cloudstack.api.command.admin.storage.SyncStoragePoolCmd; -import org.apache.cloudstack.api.command.admin.storage.UpdateObjectStoragePoolCmd; import org.apache.cloudstack.api.command.admin.storage.UpdateImageStoreCmd; +import org.apache.cloudstack.api.command.admin.storage.UpdateObjectStoragePoolCmd; import org.apache.cloudstack.api.command.admin.storage.UpdateStoragePoolCmd; import org.apache.cloudstack.api.command.admin.storage.heuristics.CreateSecondaryStorageSelectorCmd; import org.apache.cloudstack.api.command.admin.storage.heuristics.RemoveSecondaryStorageSelectorCmd; @@ -234,8 +234,8 @@ import com.cloud.user.dao.UserDao; import com.cloud.utils.DateUtil; import com.cloud.utils.NumbersUtil; import com.cloud.utils.Pair; -import com.cloud.utils.UriUtils; import com.cloud.utils.StringUtils; +import com.cloud.utils.UriUtils; import com.cloud.utils.component.ComponentContext; import com.cloud.utils.component.ManagerBase; import com.cloud.utils.concurrency.NamedThreadFactory; @@ -499,8 +499,8 @@ public class StorageManagerImpl extends ManagerBase implements StorageManager, C public boolean isLocalStorageActiveOnHost(Long hostId) { List storagePoolHostRefs = _storagePoolHostDao.listByHostId(hostId); for (StoragePoolHostVO storagePoolHostRef : storagePoolHostRefs) { - StoragePoolVO PrimaryDataStoreVO = _storagePoolDao.findById(storagePoolHostRef.getPoolId()); - if (PrimaryDataStoreVO.getPoolType() == StoragePoolType.LVM || PrimaryDataStoreVO.getPoolType() == StoragePoolType.EXT) { + StoragePoolVO primaryDataStoreVO = _storagePoolDao.findById(storagePoolHostRef.getPoolId()); + if (primaryDataStoreVO != null && (primaryDataStoreVO.getPoolType() == StoragePoolType.LVM || primaryDataStoreVO.getPoolType() == StoragePoolType.EXT)) { SearchBuilder volumeSB = volumeDao.createSearchBuilder(); volumeSB.and("poolId", volumeSB.entity().getPoolId(), SearchCriteria.Op.EQ); volumeSB.and("removed", volumeSB.entity().getRemoved(), SearchCriteria.Op.NULL); @@ -511,7 +511,7 @@ public class StorageManagerImpl extends ManagerBase implements StorageManager, C volumeSB.join("activeVmSB", activeVmSB, volumeSB.entity().getInstanceId(), activeVmSB.entity().getId(), JoinBuilder.JoinType.INNER); SearchCriteria volumeSC = volumeSB.create(); - volumeSC.setParameters("poolId", PrimaryDataStoreVO.getId()); + volumeSC.setParameters("poolId", primaryDataStoreVO.getId()); volumeSC.setParameters("state", Volume.State.Expunging, Volume.State.Destroy); volumeSC.setJoinParameters("activeVmSB", "state", State.Starting, State.Running, State.Stopping, State.Migrating); @@ -2171,9 +2171,9 @@ public class StorageManagerImpl extends ManagerBase implements StorageManager, C // poolId is null only if volume is destroyed, which has been checked // before. assert poolId != null; - StoragePoolVO PrimaryDataStoreVO = _storagePoolDao.findById(poolId); - assert PrimaryDataStoreVO != null; - return PrimaryDataStoreVO.getUuid(); + StoragePoolVO primaryDataStoreVO = _storagePoolDao.findById(poolId); + assert primaryDataStoreVO != null; + return primaryDataStoreVO.getUuid(); } @Override @@ -2724,8 +2724,8 @@ public class StorageManagerImpl extends ManagerBase implements StorageManager, C } CapacityVO capacity = new CapacityVO(poolId, zoneId, podId, clusterId, 0, 0, Capacity.CAPACITY_TYPE_STORAGE); - for (StoragePoolVO pool : pools) { - StorageStats stats = ApiDBUtils.getStoragePoolStatistics(pool.getId()); + for (StoragePoolVO primaryDataStoreVO : pools) { + StorageStats stats = ApiDBUtils.getStoragePoolStatistics(primaryDataStoreVO.getId()); if (stats == null) { continue; } diff --git a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java index 2b6c7a06709..a371a064701 100644 --- a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java +++ b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java @@ -133,7 +133,9 @@ import com.cloud.dc.ClusterDetailsDao; import com.cloud.dc.DataCenter; import com.cloud.dc.DataCenterVO; import com.cloud.dc.Pod; +import com.cloud.dc.dao.ClusterDao; import com.cloud.dc.dao.DataCenterDao; +import com.cloud.dc.dao.HostPodDao; import com.cloud.domain.Domain; import com.cloud.domain.dao.DomainDao; import com.cloud.event.ActionEvent; @@ -153,6 +155,7 @@ import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.hypervisor.HypervisorCapabilitiesVO; import com.cloud.hypervisor.dao.HypervisorCapabilitiesDao; import com.cloud.offering.DiskOffering; +import com.cloud.org.Cluster; import com.cloud.org.Grouping; import com.cloud.projects.Project; import com.cloud.projects.ProjectManager; @@ -322,6 +325,8 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic @Inject private VmWorkJobDao _workJobDao; @Inject + ClusterDao clusterDao; + @Inject private ClusterDetailsDao _clusterDetailsDao; @Inject private StorageManager storageMgr; @@ -347,6 +352,8 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic protected StoragePoolDetailsDao storagePoolDetailsDao; @Inject private BackupDao backupDao; + @Inject + HostPodDao podDao; protected Gson _gson; @@ -2436,17 +2443,10 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic return attachVolumeToVM(command.getVirtualMachineId(), command.getId(), command.getDeviceId(), false); } - private Volume orchestrateAttachVolumeToVM(Long vmId, Long volumeId, Long deviceId) { - VolumeInfo volumeToAttach = volFactory.getVolume(volumeId); - - if (volumeToAttach.isAttachedVM()) { - throw new CloudRuntimeException("This volume is already attached to a VM."); - } - - UserVmVO vm = _userVmDao.findById(vmId); + protected VolumeVO getVmExistingVolumeForVolumeAttach(UserVmVO vm, VolumeInfo volumeToAttach) { VolumeVO existingVolumeOfVm = null; VMTemplateVO template = _templateDao.findById(vm.getTemplateId()); - List rootVolumesOfVm = _volsDao.findByInstanceAndType(vmId, Volume.Type.ROOT); + List rootVolumesOfVm = _volsDao.findByInstanceAndType(vm.getId(), Volume.Type.ROOT); if (rootVolumesOfVm.size() > 1 && template != null && !template.isDeployAsIs()) { throw new CloudRuntimeException("The VM " + vm.getHostName() + " has more than one ROOT volume and is in an invalid state."); } else { @@ -2454,7 +2454,7 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic existingVolumeOfVm = rootVolumesOfVm.get(0); } else { // locate data volume of the vm - List diskVolumesOfVm = _volsDao.findByInstanceAndType(vmId, Volume.Type.DATADISK); + List diskVolumesOfVm = _volsDao.findByInstanceAndType(vm.getId(), Volume.Type.DATADISK); for (VolumeVO diskVolume : diskVolumesOfVm) { if (diskVolume.getState() != Volume.State.Allocated) { existingVolumeOfVm = diskVolume; @@ -2463,40 +2463,88 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic } } } - if (logger.isTraceEnabled()) { - if (existingVolumeOfVm != null) { - logger.trace("attaching volume {} to a VM {} with an existing volume {} on primary storage {}", - volumeToAttach, vm, existingVolumeOfVm, _storagePoolDao.findById(existingVolumeOfVm.getPoolId())); + if (existingVolumeOfVm == null) { + if (logger.isTraceEnabled()) { + logger.trace(String.format("No existing volume found for VM (%s/%s) to attach volume %s/%s", + vm.getName(), vm.getUuid(), + volumeToAttach.getName(), volumeToAttach.getUuid())); } + return null; } + if (logger.isTraceEnabled()) { + String msg = "attaching volume %s/%s to a VM (%s/%s) with an existing volume %s/%s on primary storage %s"; + logger.trace(String.format(msg, + volumeToAttach.getName(), volumeToAttach.getUuid(), + vm.getName(), vm.getUuid(), + existingVolumeOfVm.getName(), existingVolumeOfVm.getUuid(), + existingVolumeOfVm.getPoolId())); + } + return existingVolumeOfVm; + } - HypervisorType rootDiskHyperType = vm.getHypervisorType(); - HypervisorType volumeToAttachHyperType = _volsDao.getHypervisorType(volumeToAttach.getId()); + protected StoragePool getPoolForAllocatedOrUploadedVolumeForAttach(final VolumeInfo volumeToAttach, final UserVmVO vm) { + DataCenter zone = _dcDao.findById(vm.getDataCenterId()); + Pair clusterHostId = virtualMachineManager.findClusterAndHostIdForVm(vm, false); + long podId = vm.getPodIdToDeployIn(); + if (clusterHostId.first() != null) { + Cluster cluster = clusterDao.findById(clusterHostId.first()); + podId = cluster.getPodId(); + } + Pod pod = podDao.findById(podId); + DiskOfferingVO offering = _diskOfferingDao.findById(volumeToAttach.getDiskOfferingId()); + DiskProfile diskProfile = new DiskProfile(volumeToAttach.getId(), volumeToAttach.getVolumeType(), + volumeToAttach.getName(), volumeToAttach.getId(), volumeToAttach.getSize(), offering.getTagsArray(), + offering.isUseLocalStorage(), offering.isRecreatable(), + volumeToAttach.getTemplateId()); + diskProfile.setHyperType(vm.getHypervisorType()); + StoragePool pool = _volumeMgr.findStoragePool(diskProfile, zone, pod, clusterHostId.first(), + clusterHostId.second(), vm, Collections.emptySet()); + if (pool == null) { + throw new CloudRuntimeException(String.format("Failed to find a primary storage for volume in state: %s", volumeToAttach.getState())); + } + return pool; + } + protected VolumeInfo createVolumeOnPrimaryForAttachIfNeeded(final VolumeInfo volumeToAttach, final UserVmVO vm, VolumeVO existingVolumeOfVm) { VolumeInfo newVolumeOnPrimaryStorage = volumeToAttach; - + boolean volumeOnSecondary = volumeToAttach.getState() == Volume.State.Uploaded; + if (!Arrays.asList(Volume.State.Allocated, Volume.State.Uploaded).contains(volumeToAttach.getState())) { + return newVolumeOnPrimaryStorage; + } //don't create volume on primary storage if its being attached to the vm which Root's volume hasn't been created yet - StoragePoolVO destPrimaryStorage = null; + StoragePool destPrimaryStorage = null; if (existingVolumeOfVm != null && !existingVolumeOfVm.getState().equals(Volume.State.Allocated)) { destPrimaryStorage = _storagePoolDao.findById(existingVolumeOfVm.getPoolId()); if (logger.isTraceEnabled() && destPrimaryStorage != null) { logger.trace("decided on target storage: {}", destPrimaryStorage); } } - - boolean volumeOnSecondary = volumeToAttach.getState() == Volume.State.Uploaded; - - if (destPrimaryStorage != null && (volumeToAttach.getState() == Volume.State.Allocated || volumeOnSecondary)) { - try { - if (volumeOnSecondary && destPrimaryStorage.getPoolType() == Storage.StoragePoolType.PowerFlex) { - throw new InvalidParameterValueException("Cannot attach uploaded volume, this operation is unsupported on storage pool type " + destPrimaryStorage.getPoolType()); - } - newVolumeOnPrimaryStorage = _volumeMgr.createVolumeOnPrimaryStorage(vm, volumeToAttach, rootDiskHyperType, destPrimaryStorage); - } catch (NoTransitionException e) { - logger.debug("Failed to create volume on primary storage", e); - throw new CloudRuntimeException("Failed to create volume on primary storage", e); - } + if (destPrimaryStorage == null) { + destPrimaryStorage = getPoolForAllocatedOrUploadedVolumeForAttach(volumeToAttach, vm); } + try { + if (volumeOnSecondary && Storage.StoragePoolType.PowerFlex.equals(destPrimaryStorage.getPoolType())) { + throw new InvalidParameterValueException("Cannot attach uploaded volume, this operation is unsupported on storage pool type " + destPrimaryStorage.getPoolType()); + } + newVolumeOnPrimaryStorage = _volumeMgr.createVolumeOnPrimaryStorage(vm, volumeToAttach, + vm.getHypervisorType(), destPrimaryStorage); + } catch (NoTransitionException e) { + logger.debug("Failed to create volume on primary storage", e); + throw new CloudRuntimeException("Failed to create volume on primary storage", e); + } + return newVolumeOnPrimaryStorage; + } + + private Volume orchestrateAttachVolumeToVM(Long vmId, Long volumeId, Long deviceId) { + VolumeInfo volumeToAttach = volFactory.getVolume(volumeId); + + if (volumeToAttach.isAttachedVM()) { + throw new CloudRuntimeException("This volume is already attached to a VM."); + } + + UserVmVO vm = _userVmDao.findById(vmId); + VolumeVO existingVolumeOfVm = getVmExistingVolumeForVolumeAttach(vm, volumeToAttach); + VolumeInfo newVolumeOnPrimaryStorage = createVolumeOnPrimaryForAttachIfNeeded(volumeToAttach, vm, existingVolumeOfVm); // reload the volume from db newVolumeOnPrimaryStorage = volFactory.getVolume(newVolumeOnPrimaryStorage.getId()); @@ -2515,19 +2563,17 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic StoragePoolVO vmRootVolumePool = _storagePoolDao.findById(existingVolumeOfVm.getPoolId()); try { + HypervisorType volumeToAttachHyperType = _volsDao.getHypervisorType(volumeToAttach.getId()); newVolumeOnPrimaryStorage = _volumeMgr.moveVolume(newVolumeOnPrimaryStorage, vmRootVolumePool.getDataCenterId(), vmRootVolumePool.getPodId(), vmRootVolumePool.getClusterId(), volumeToAttachHyperType); - } catch (ConcurrentOperationException e) { - logger.debug("move volume failed", e); - throw new CloudRuntimeException("move volume failed", e); - } catch (StorageUnavailableException e) { + } catch (ConcurrentOperationException | StorageUnavailableException e) { logger.debug("move volume failed", e); throw new CloudRuntimeException("move volume failed", e); } } VolumeVO newVol = _volsDao.findById(newVolumeOnPrimaryStorage.getId()); // Getting the fresh vm object in case of volume migration to check the current state of VM - if (moveVolumeNeeded || volumeOnSecondary) { + if (moveVolumeNeeded) { vm = _userVmDao.findById(vmId); if (vm == null) { throw new InvalidParameterValueException("VM not found."); @@ -2712,9 +2758,6 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic if (!_volsDao.findByInstanceAndDeviceId(vm.getId(), 0).isEmpty()) { throw new InvalidParameterValueException("Vm already has root volume attached to it"); } - if (volumeToAttach.getState() == Volume.State.Uploaded) { - throw new InvalidParameterValueException("No support for Root volume attach in state " + Volume.State.Uploaded); - } } } diff --git a/server/src/main/java/com/cloud/user/AccountManager.java b/server/src/main/java/com/cloud/user/AccountManager.java index 1e5526688b7..640eaa00076 100644 --- a/server/src/main/java/com/cloud/user/AccountManager.java +++ b/server/src/main/java/com/cloud/user/AccountManager.java @@ -20,7 +20,6 @@ import java.net.InetAddress; import java.util.List; import java.util.Map; -import com.cloud.api.auth.SetupUserTwoFactorAuthenticationCmd; import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.api.command.admin.account.UpdateAccountCmd; import org.apache.cloudstack.api.command.admin.user.DeleteUserCmd; @@ -31,6 +30,7 @@ import org.apache.cloudstack.auth.UserTwoFactorAuthenticator; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.Configurable; +import com.cloud.api.auth.SetupUserTwoFactorAuthenticationCmd; import com.cloud.api.query.vo.ControlledViewEntity; import com.cloud.exception.ConcurrentOperationException; import com.cloud.exception.ResourceUnavailableException; @@ -202,5 +202,8 @@ public interface AccountManager extends AccountService, Configurable { void validateUserPasswordAndUpdateIfNeeded(String newPassword, UserVO user, String currentPassword, boolean skipCurrentPassValidation); - void checkApiAccess(Account caller, String command); + void checkApiAccess(Account caller, String command); + + UserAccount clearUserTwoFactorAuthenticationInSetupStateOnLogin(UserAccount user); + } diff --git a/server/src/main/java/com/cloud/user/AccountManagerImpl.java b/server/src/main/java/com/cloud/user/AccountManagerImpl.java index ea2df5d1530..1422f39f1fe 100644 --- a/server/src/main/java/com/cloud/user/AccountManagerImpl.java +++ b/server/src/main/java/com/cloud/user/AccountManagerImpl.java @@ -41,7 +41,6 @@ import javax.crypto.spec.SecretKeySpec; import javax.inject.Inject; import javax.naming.ConfigurationException; -import com.cloud.host.dao.HostDao; import org.apache.cloudstack.acl.APIChecker; import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.acl.InfrastructureEntity; @@ -120,6 +119,7 @@ import com.cloud.exception.InvalidParameterValueException; import com.cloud.exception.OperationTimedoutException; import com.cloud.exception.PermissionDeniedException; import com.cloud.exception.ResourceUnavailableException; +import com.cloud.host.dao.HostDao; import com.cloud.network.IpAddress; import com.cloud.network.IpAddressManager; import com.cloud.network.Network; @@ -1378,7 +1378,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M */ private void checkRoleEscalation(Account caller, Account requested) { if (logger.isDebugEnabled()) { - logger.debug(String.format("checking if user of account %s [%s] with role-id [%d] can create an account of type %s [%s] with role-id [%d]", + logger.debug(String.format("Checking if user of account %s [%s] with role-id [%d] can create an account of type %s [%s] with role-id [%d]", caller.getAccountName(), caller.getUuid(), caller.getRoleId(), @@ -1392,12 +1392,13 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M checkApiAccess(apiCheckers, requested, command); } catch (PermissionDeniedException pde) { if (logger.isTraceEnabled()) { - logger.trace(String.format("checking for permission to \"%s\" is irrelevant as it is not requested for %s [%s]", + logger.trace(String.format( + "Checking for permission to \"%s\" is irrelevant as it is not requested for %s [%s]", command, - pde.getAccount().getAccountName(), - pde.getAccount().getUuid(), - pde.getEntitiesInViolation() - )); + requested.getAccountName(), + requested.getUuid() + ) + ); } continue; } @@ -3566,4 +3567,26 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M return userTwoFactorAuthenticationProvidersMap.get(name.toLowerCase()); } + @Override + public UserAccount clearUserTwoFactorAuthenticationInSetupStateOnLogin(UserAccount user) { + return Transaction.execute((TransactionCallback) status -> { + if (!user.isUser2faEnabled() && StringUtils.isBlank(user.getUser2faProvider())) { + return user; + } + UserDetailVO userDetailVO = _userDetailsDao.findDetail(user.getId(), UserDetailVO.Setup2FADetail); + if (userDetailVO != null && UserAccountVO.Setup2FAstatus.VERIFIED.name().equals(userDetailVO.getValue())) { + return user; + } + logger.info("Clearing 2FA configurations for {} as it is still in setup on a new login request", user); + if (userDetailVO != null) { + _userDetailsDao.remove(userDetailVO.getId()); + } + UserAccountVO userAccountVO = _userAccountDao.findById(user.getId()); + userAccountVO.setUser2faEnabled(false); + userAccountVO.setUser2faProvider(null); + userAccountVO.setKeyFor2fa(null); + _userAccountDao.update(user.getId(), userAccountVO); + return userAccountVO; + }); + } } diff --git a/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java b/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java index 9630b341bc9..23b79e4ab03 100644 --- a/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java +++ b/server/src/test/java/com/cloud/storage/VolumeApiServiceImplTest.java @@ -41,7 +41,6 @@ import java.util.List; import java.util.UUID; import java.util.concurrent.ExecutionException; -import com.cloud.server.ManagementService; import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.acl.SecurityChecker.AccessType; import org.apache.cloudstack.api.command.user.volume.CheckAndRepairVolumeCmd; @@ -94,8 +93,12 @@ import com.cloud.api.query.dao.ServiceOfferingJoinDao; import com.cloud.configuration.ConfigurationManager; import com.cloud.configuration.Resource; import com.cloud.configuration.Resource.ResourceType; +import com.cloud.dc.ClusterVO; import com.cloud.dc.DataCenterVO; +import com.cloud.dc.HostPodVO; +import com.cloud.dc.dao.ClusterDao; import com.cloud.dc.dao.DataCenterDao; +import com.cloud.dc.dao.HostPodDao; import com.cloud.event.EventTypes; import com.cloud.event.UsageEventUtils; import com.cloud.exception.InvalidParameterValueException; @@ -108,6 +111,7 @@ import com.cloud.org.Grouping; import com.cloud.projects.Project; import com.cloud.projects.ProjectManager; import com.cloud.serializer.GsonHelper; +import com.cloud.server.ManagementService; import com.cloud.server.TaggedResourceService; import com.cloud.service.ServiceOfferingVO; import com.cloud.service.dao.ServiceOfferingDao; @@ -130,10 +134,12 @@ import com.cloud.utils.Pair; import com.cloud.utils.db.TransactionLegacy; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.fsm.NoTransitionException; +import com.cloud.vm.DiskProfile; import com.cloud.vm.UserVmManager; import com.cloud.vm.UserVmVO; import com.cloud.vm.VirtualMachine; import com.cloud.vm.VirtualMachine.State; +import com.cloud.vm.VirtualMachineManager; import com.cloud.vm.dao.UserVmDao; import com.cloud.vm.dao.VMInstanceDao; import com.cloud.vm.snapshot.VMSnapshotVO; @@ -209,6 +215,15 @@ public class VolumeApiServiceImplTest { private DataStoreManager dataStoreMgr; @Mock private SnapshotHelper snapshotHelper; + @Mock + VirtualMachineManager virtualMachineManager; + @Mock + HostPodDao podDao; + @Mock + ClusterDao clusterDao; + @Mock + VolumeOrchestrationService volumeOrchestrationService; + private DetachVolumeCmd detachCmd = new DetachVolumeCmd(); private Class _detachCmdClass = detachCmd.getClass(); @@ -250,9 +265,6 @@ public class VolumeApiServiceImplTest { @Mock private ConfigurationManager _configMgr; - @Mock - private VolumeOrchestrationService _volumeMgr; - @Mock private ManagementService managementService; @@ -1925,4 +1937,236 @@ public class VolumeApiServiceImplTest { verify(volumeServiceMock, times(0)).resize(any(VolumeInfo.class)); } } + private UserVmVO getMockedVm() { + UserVmVO vm = Mockito.mock(UserVmVO.class); + Mockito.when(vm.getId()).thenReturn(1L); + Mockito.when(vm.getTemplateId()).thenReturn(10L); + Mockito.when(vm.getHostName()).thenReturn("test-vm"); + return vm; + } + + private VMTemplateVO getMockedTemplate() { + VMTemplateVO template = Mockito.mock(VMTemplateVO.class); + Mockito.when(template.isDeployAsIs()).thenReturn(false); + return template; + } + + @Test(expected = CloudRuntimeException.class) + public void testGetVmExistingVolumeForVolumeAttach_MultipleRootVolumes_ThrowsException() { + UserVmVO vm = getMockedVm(); + VMTemplateVO template = getMockedTemplate(); + when(templateDao.findById(10L)).thenReturn(template); + when(volumeDaoMock.findByInstanceAndType(1L, Volume.Type.ROOT)) + .thenReturn(Arrays.asList(Mockito.mock(VolumeVO.class), Mockito.mock(VolumeVO.class))); + volumeApiServiceImpl.getVmExistingVolumeForVolumeAttach(vm, Mockito.mock(VolumeInfo.class)); + } + + @Test + public void testGetVmExistingVolumeForVolumeAttach_SingleRootVolume() { + UserVmVO vm = getMockedVm(); + VMTemplateVO template = getMockedTemplate(); + VolumeVO rootVolume = Mockito.mock(VolumeVO.class); + Mockito.when(rootVolume.getId()).thenReturn(20L); + Mockito.when(templateDao.findById(10L)).thenReturn(template); + Mockito.when(volumeDaoMock.findByInstanceAndType(1L, Volume.Type.ROOT)) + .thenReturn(Collections.singletonList(rootVolume)); + VolumeVO result = volumeApiServiceImpl.getVmExistingVolumeForVolumeAttach(vm, Mockito.mock(VolumeInfo.class)); + Assert.assertNotNull(result); + Assert.assertEquals(20L, result.getId()); + } + + private VolumeVO getMockedDataVolume() { + VolumeVO volume = Mockito.mock(VolumeVO.class); + Mockito.when(volume.getId()).thenReturn(30L); + Mockito.when(volume.getState()).thenReturn(Volume.State.Ready); + return volume; + } + + @Test + public void testGetVmExistingVolumeForVolumeAttach_NoRootVolume_DataDiskAvailable() { + UserVmVO vm = getMockedVm(); + VMTemplateVO template = getMockedTemplate(); + VolumeVO dataDisk = getMockedDataVolume(); + List rootVolumes = Collections.emptyList(); + List dataVolumes = Collections.singletonList(dataDisk); + Mockito.when(templateDao.findById(10L)).thenReturn(template); + Mockito.when(volumeDaoMock.findByInstanceAndType(1L, Volume.Type.ROOT)).thenReturn(rootVolumes); + Mockito.when(volumeDaoMock.findByInstanceAndType(1L, Volume.Type.DATADISK)).thenReturn(dataVolumes); + VolumeVO result = volumeApiServiceImpl.getVmExistingVolumeForVolumeAttach(vm, Mockito.mock(VolumeInfo.class)); + Assert.assertNotNull(result); + Assert.assertEquals(30L, result.getId()); + } + + @Test + public void testGetVmExistingVolumeForVolumeAttach_NoVolumesAtAll() { + UserVmVO vm = getMockedVm(); + VMTemplateVO template = getMockedTemplate(); + Mockito.when(templateDao.findById(10L)).thenReturn(template); + Mockito.when(volumeDaoMock.findByInstanceAndType(1L, Volume.Type.ROOT)).thenReturn(Collections.emptyList()); + Mockito.when(volumeDaoMock.findByInstanceAndType(1L, Volume.Type.DATADISK)).thenReturn(Collections.emptyList()); + VolumeVO result = volumeApiServiceImpl.getVmExistingVolumeForVolumeAttach(vm, Mockito.mock(VolumeInfo.class)); + Assert.assertNull(result); + } + + private void mockDiskOffering() { + DiskOfferingVO offering = Mockito.mock(DiskOfferingVO.class); + Mockito.when(_diskOfferingDao.findById(1L)).thenReturn(offering); + Mockito.when(offering.isUseLocalStorage()).thenReturn(true); + Mockito.when(offering.isRecreatable()).thenReturn(false); + } + + private DataCenterVO mockZone() { + DataCenterVO zone = Mockito.mock(DataCenterVO.class); + Mockito.when(_dcDao.findById(1L)).thenReturn(zone); + return zone; + } + + @Test + public void testGetPoolForAllocatedOrUploadedVolumeForAttach_Success() { + VolumeInfo volumeToAttach = Mockito.mock(VolumeInfo.class); + UserVmVO vm = Mockito.mock(UserVmVO.class); + ClusterVO cluster = Mockito.mock(ClusterVO.class); + HostPodVO pod = Mockito.mock(HostPodVO.class); + DataCenterVO zone = mockZone(); + mockDiskOffering(); + StoragePool pool = Mockito.mock(StoragePool.class); + when(vm.getDataCenterId()).thenReturn(1L); + when(virtualMachineManager.findClusterAndHostIdForVm(vm, false)).thenReturn(new Pair<>(1L, 2L)); + when(clusterDao.findById(1L)).thenReturn(cluster); + when(cluster.getPodId()).thenReturn(1L); + when(podDao.findById(1L)).thenReturn(pod); + when(volumeToAttach.getDiskOfferingId()).thenReturn(1L); + when(volumeOrchestrationService.findStoragePool(any(DiskProfile.class), eq(zone), eq(pod), eq(1L), eq(2L), eq(vm), eq(Collections.emptySet()))) + .thenReturn(pool); + StoragePool result = volumeApiServiceImpl.getPoolForAllocatedOrUploadedVolumeForAttach(volumeToAttach, vm); + Assert.assertNotNull(result); + Assert.assertEquals(pool, result); + } + + @Test(expected = CloudRuntimeException.class) + public void testGetPoolForAllocatedOrUploadedVolumeForAttach_NoPoolFound_ThrowsException() { + VolumeInfo volumeToAttach = Mockito.mock(VolumeInfo.class); + UserVmVO vm = Mockito.mock(UserVmVO.class); + DataCenterVO zone = mockZone(); + Pair clusterHostId = new Pair<>(1L, 2L); + ClusterVO cluster = Mockito.mock(ClusterVO.class); + HostPodVO pod = Mockito.mock(HostPodVO.class); + mockDiskOffering(); + when(vm.getDataCenterId()).thenReturn(1L); + when(clusterDao.findById(1L)).thenReturn(cluster); + when(virtualMachineManager.findClusterAndHostIdForVm(vm, false)).thenReturn(clusterHostId); + when(podDao.findById(anyLong())).thenReturn(pod); + when(volumeToAttach.getDiskOfferingId()).thenReturn(1L); + when(volumeOrchestrationService.findStoragePool(any(DiskProfile.class), eq(zone), eq(pod), eq(1L), eq(2L), eq(vm), eq(Collections.emptySet()))) + .thenReturn(null); + volumeApiServiceImpl.getPoolForAllocatedOrUploadedVolumeForAttach(volumeToAttach, vm); + } + + @Test + public void testGetPoolForAllocatedOrUploadedVolumeForAttach_NoCluster() { + VolumeInfo volumeToAttach = Mockito.mock(VolumeInfo.class); + UserVmVO vm = Mockito.mock(UserVmVO.class); + DataCenterVO zone = mockZone(); + HostPodVO pod = Mockito.mock(HostPodVO.class); + mockDiskOffering(); + StoragePool pool = Mockito.mock(StoragePool.class); + when(vm.getDataCenterId()).thenReturn(1L); + when(vm.getPodIdToDeployIn()).thenReturn(2L); + when(virtualMachineManager.findClusterAndHostIdForVm(vm, false)).thenReturn(new Pair<>(null, 2L)); + when(podDao.findById(2L)).thenReturn(pod); + when(volumeToAttach.getDiskOfferingId()).thenReturn(1L); + when(volumeOrchestrationService.findStoragePool(any(DiskProfile.class), eq(zone), eq(pod), eq(null), eq(2L), eq(vm), eq(Collections.emptySet()))) + .thenReturn(pool); + StoragePool result = volumeApiServiceImpl.getPoolForAllocatedOrUploadedVolumeForAttach(volumeToAttach, vm); + Assert.assertNotNull(result); + Assert.assertEquals(pool, result); + } + + + @Test + public void testCreateVolumeOnSecondaryForAttachIfNeeded_VolumeNotAllocatedOrUploaded() { + VolumeInfo volumeToAttach = Mockito.mock(VolumeInfo.class); + Mockito.when(volumeToAttach.getState()).thenReturn(Volume.State.Ready); + VolumeInfo result = volumeApiServiceImpl.createVolumeOnPrimaryForAttachIfNeeded( + volumeToAttach, Mockito.mock(UserVmVO.class), null); + Assert.assertSame(volumeToAttach, result); + Mockito.verifyNoInteractions(primaryDataStoreDaoMock, volumeOrchestrationService); + } + + @Test + public void testCreateVolumeOnSecondaryForAttachIfNeeded_ExistingVolumeDeterminesStoragePool() { + VolumeInfo volumeToAttach = Mockito.mock(VolumeInfo.class); + Mockito.when(volumeToAttach.getState()).thenReturn(Volume.State.Uploaded); + UserVmVO vm = Mockito.mock(UserVmVO.class); + VolumeVO existingVolume = Mockito.mock(VolumeVO.class); + Mockito.when(existingVolume.getState()).thenReturn(Volume.State.Ready); + when(existingVolume.getPoolId()).thenReturn(1L); + StoragePoolVO destPrimaryStorage = Mockito.mock(StoragePoolVO.class); + Mockito.when(destPrimaryStorage.getPoolType()).thenReturn(Storage.StoragePoolType.NetworkFilesystem); + Mockito.when(primaryDataStoreDaoMock.findById(1L)).thenReturn(destPrimaryStorage); + VolumeInfo newVolumeOnPrimaryStorage = Mockito.mock(VolumeInfo.class); + try { + Mockito.when(volumeOrchestrationService.createVolumeOnPrimaryStorage(vm, volumeToAttach, vm.getHypervisorType(), destPrimaryStorage)) + .thenReturn(newVolumeOnPrimaryStorage); + } catch (NoTransitionException nte) { + Assert.fail(nte.getMessage()); + } + VolumeInfo result = volumeApiServiceImpl.createVolumeOnPrimaryForAttachIfNeeded(volumeToAttach, vm, existingVolume); + Assert.assertSame(newVolumeOnPrimaryStorage, result); + Mockito.verify(primaryDataStoreDaoMock).findById(1L); + } + + @Test + public void testCreateVolumeOnPrimaryForAttachIfNeeded_UsesGetPoolForAttach() { + VolumeInfo volumeToAttach = Mockito.mock(VolumeInfo.class); + Mockito.when(volumeToAttach.getState()).thenReturn(Volume.State.Allocated); + UserVmVO vm = Mockito.mock(UserVmVO.class); + StoragePool destPrimaryStorage = Mockito.mock(StoragePool.class); + Mockito.doReturn(destPrimaryStorage).when(volumeApiServiceImpl) + .getPoolForAllocatedOrUploadedVolumeForAttach(volumeToAttach, vm); + VolumeInfo newVolumeOnPrimaryStorage = Mockito.mock(VolumeInfo.class); + try { + Mockito.when(volumeOrchestrationService.createVolumeOnPrimaryStorage( + vm, volumeToAttach, vm.getHypervisorType(), destPrimaryStorage)) + .thenReturn(newVolumeOnPrimaryStorage); + } catch (NoTransitionException nte) { + Assert.fail(nte.getMessage()); + } + VolumeInfo result = volumeApiServiceImpl.createVolumeOnPrimaryForAttachIfNeeded(volumeToAttach, vm, null); + Assert.assertSame(newVolumeOnPrimaryStorage, result); + verify(volumeApiServiceImpl).getPoolForAllocatedOrUploadedVolumeForAttach(volumeToAttach, vm); + } + + @Test(expected = InvalidParameterValueException.class) + public void testCreateVolumeOnPrimaryForAttachIfNeeded_UnsupportedPoolType_ThrowsException() { + VolumeInfo volumeToAttach = Mockito.mock(VolumeInfo.class); + when(volumeToAttach.getState()).thenReturn(Volume.State.Uploaded); + UserVmVO vm = Mockito.mock(UserVmVO.class); + StoragePool destPrimaryStorage = Mockito.mock(StoragePool.class); + when(destPrimaryStorage.getPoolType()).thenReturn(Storage.StoragePoolType.PowerFlex); + Mockito.doReturn(destPrimaryStorage).when(volumeApiServiceImpl) + .getPoolForAllocatedOrUploadedVolumeForAttach(volumeToAttach, vm); + volumeApiServiceImpl.createVolumeOnPrimaryForAttachIfNeeded(volumeToAttach, vm, null); + } + + @Test + public void testCreateVolumeOnSecondaryForAttachIfNeeded_CreateVolumeFails_ThrowsException() { + VolumeInfo volumeToAttach = Mockito.mock(VolumeInfo.class); + Mockito.when(volumeToAttach.getState()).thenReturn(Volume.State.Uploaded); + UserVmVO vm = Mockito.mock(UserVmVO.class); + StoragePool destPrimaryStorage = Mockito.mock(StoragePool.class); + Mockito.when(destPrimaryStorage.getPoolType()).thenReturn(Storage.StoragePoolType.NetworkFilesystem); + Mockito.doReturn(destPrimaryStorage).when(volumeApiServiceImpl) + .getPoolForAllocatedOrUploadedVolumeForAttach(volumeToAttach, vm); + try { + Mockito.when(volumeOrchestrationService.createVolumeOnPrimaryStorage(vm, volumeToAttach, vm.getHypervisorType(), destPrimaryStorage)) + .thenThrow(new NoTransitionException("Mocked exception")); + } catch (NoTransitionException nte) { + Assert.fail(nte.getMessage()); + } + CloudRuntimeException exception = Assert.assertThrows(CloudRuntimeException.class, () -> + volumeApiServiceImpl.createVolumeOnPrimaryForAttachIfNeeded(volumeToAttach, vm, null) + ); + Assert.assertTrue(exception.getMessage().contains("Failed to create volume on primary storage")); + } } diff --git a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java index 0f53b907251..61a273c8946 100644 --- a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java +++ b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java @@ -26,8 +26,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import com.cloud.event.ActionEventUtils; - import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.acl.Role; import org.apache.cloudstack.acl.RoleService; @@ -43,23 +41,25 @@ import org.apache.cloudstack.auth.UserAuthenticator.ActionOnFailedAuthentication import org.apache.cloudstack.auth.UserTwoFactorAuthenticator; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.resourcedetail.UserDetailVO; import org.apache.cloudstack.webhook.WebhookHelper; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.MockedStatic; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; - import org.springframework.beans.factory.NoSuchBeanDefinitionException; import com.cloud.acl.DomainChecker; import com.cloud.api.auth.SetupUserTwoFactorAuthenticationCmd; import com.cloud.domain.Domain; import com.cloud.domain.DomainVO; +import com.cloud.event.ActionEventUtils; import com.cloud.exception.ConcurrentOperationException; import com.cloud.exception.InvalidParameterValueException; import com.cloud.exception.PermissionDeniedException; @@ -1356,4 +1356,54 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase { Mockito.when(_projectAccountDao.listAdministratedProjectIds(accountId)).thenReturn(managedProjectIds); accountManagerImpl.checkIfAccountManagesProjects(accountId); } + + @Test + public void testClearUser2FA_When2FADisabled_NoChanges() { + UserAccount user = Mockito.mock(UserAccount.class); + Mockito.when(user.isUser2faEnabled()).thenReturn(false); + Mockito.when(user.getUser2faProvider()).thenReturn(null); + UserAccount result = accountManagerImpl.clearUserTwoFactorAuthenticationInSetupStateOnLogin(user); + Assert.assertSame(user, result); + Mockito.verifyNoInteractions(userDetailsDaoMock, userAccountDaoMock); + } + + @Test + public void testClearUser2FA_When2FAInVerifiedState_NoChanges() { + UserAccount user = Mockito.mock(UserAccount.class); + Mockito.when(user.getId()).thenReturn(1L); + Mockito.when(user.isUser2faEnabled()).thenReturn(true); + UserDetailVO userDetail = new UserDetailVO(); + userDetail.setValue(UserAccountVO.Setup2FAstatus.VERIFIED.name()); + Mockito.when(userDetailsDaoMock.findDetail(1L, UserDetailVO.Setup2FADetail)).thenReturn(userDetail); + UserAccount result = accountManagerImpl.clearUserTwoFactorAuthenticationInSetupStateOnLogin(user); + Assert.assertSame(user, result); + Mockito.verify(userDetailsDaoMock).findDetail(1L, UserDetailVO.Setup2FADetail); + Mockito.verifyNoMoreInteractions(userDetailsDaoMock, userAccountDaoMock); + } + + @Test + public void testClearUser2FA_When2FAInSetupState_Disable2FA() { + UserAccount user = Mockito.mock(UserAccount.class); + Mockito.when(user.getId()).thenReturn(1L); + Mockito.when(user.isUser2faEnabled()).thenReturn(true); + UserDetailVO userDetail = new UserDetailVO(); + userDetail.setValue(UserAccountVO.Setup2FAstatus.ENABLED.name()); + UserAccountVO userAccountVO = new UserAccountVO(); + userAccountVO.setId(1L); + Mockito.when(userDetailsDaoMock.findDetail(1L, UserDetailVO.Setup2FADetail)).thenReturn(userDetail); + Mockito.when(userAccountDaoMock.findById(1L)).thenReturn(userAccountVO); + UserAccount result = accountManagerImpl.clearUserTwoFactorAuthenticationInSetupStateOnLogin(user); + Assert.assertNotNull(result); + Assert.assertFalse(result.isUser2faEnabled()); + Assert.assertNull(result.getUser2faProvider()); + Mockito.verify(userDetailsDaoMock).findDetail(1L, UserDetailVO.Setup2FADetail); + Mockito.verify(userDetailsDaoMock).remove(Mockito.anyLong()); + Mockito.verify(userAccountDaoMock).findById(1L); + ArgumentCaptor captor = ArgumentCaptor.forClass(UserAccountVO.class); + Mockito.verify(userAccountDaoMock).update(Mockito.eq(1L), captor.capture()); + UserAccountVO updatedUser = captor.getValue(); + Assert.assertFalse(updatedUser.isUser2faEnabled()); + Assert.assertNull(updatedUser.getUser2faProvider()); + Assert.assertNull(updatedUser.getKeyFor2fa()); + } } diff --git a/server/src/test/java/com/cloud/user/MockAccountManagerImpl.java b/server/src/test/java/com/cloud/user/MockAccountManagerImpl.java index 30324b41986..62a95380a15 100644 --- a/server/src/test/java/com/cloud/user/MockAccountManagerImpl.java +++ b/server/src/test/java/com/cloud/user/MockAccountManagerImpl.java @@ -16,6 +16,27 @@ // under the License. package com.cloud.user; +import java.net.InetAddress; +import java.util.List; +import java.util.Map; + +import javax.naming.ConfigurationException; + +import org.apache.cloudstack.acl.ControlledEntity; +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.acl.SecurityChecker.AccessType; +import org.apache.cloudstack.api.command.admin.account.CreateAccountCmd; +import org.apache.cloudstack.api.command.admin.account.UpdateAccountCmd; +import org.apache.cloudstack.api.command.admin.user.DeleteUserCmd; +import org.apache.cloudstack.api.command.admin.user.GetUserKeysCmd; +import org.apache.cloudstack.api.command.admin.user.MoveUserCmd; +import org.apache.cloudstack.api.command.admin.user.RegisterCmd; +import org.apache.cloudstack.api.command.admin.user.UpdateUserCmd; +import org.apache.cloudstack.api.response.UserTwoFactorAuthenticationSetupResponse; +import org.apache.cloudstack.auth.UserTwoFactorAuthenticator; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.springframework.stereotype.Component; + import com.cloud.api.auth.SetupUserTwoFactorAuthenticationCmd; import com.cloud.api.query.vo.ControlledViewEntity; import com.cloud.dc.DataCenter; @@ -34,25 +55,6 @@ import com.cloud.utils.component.Manager; import com.cloud.utils.component.ManagerBase; import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; -import org.apache.cloudstack.acl.ControlledEntity; -import org.apache.cloudstack.acl.RoleType; -import org.apache.cloudstack.acl.SecurityChecker.AccessType; -import org.apache.cloudstack.api.command.admin.account.CreateAccountCmd; -import org.apache.cloudstack.api.command.admin.account.UpdateAccountCmd; -import org.apache.cloudstack.api.command.admin.user.DeleteUserCmd; -import org.apache.cloudstack.api.command.admin.user.GetUserKeysCmd; -import org.apache.cloudstack.api.command.admin.user.MoveUserCmd; -import org.apache.cloudstack.api.command.admin.user.RegisterCmd; -import org.apache.cloudstack.api.command.admin.user.UpdateUserCmd; -import org.apache.cloudstack.api.response.UserTwoFactorAuthenticationSetupResponse; -import org.apache.cloudstack.auth.UserTwoFactorAuthenticator; -import org.apache.cloudstack.framework.config.ConfigKey; -import org.springframework.stereotype.Component; - -import javax.naming.ConfigurationException; -import java.net.InetAddress; -import java.util.List; -import java.util.Map; @Component public class MockAccountManagerImpl extends ManagerBase implements Manager, AccountManager { @@ -496,4 +498,9 @@ public class MockAccountManagerImpl extends ManagerBase implements Manager, Acco @Override public void validateUserPasswordAndUpdateIfNeeded(String newPassword, UserVO user, String currentPassword, boolean skipCurrentPassValidation) { } + + @Override + public UserAccount clearUserTwoFactorAuthenticationInSetupStateOnLogin(UserAccount user) { + return null; + } } diff --git a/ui/src/components/view/ListView.vue b/ui/src/components/view/ListView.vue index 6d7b2301d65..a0b304c8148 100644 --- a/ui/src/components/view/ListView.vue +++ b/ui/src/components/view/ListView.vue @@ -33,7 +33,7 @@ - {{ $t('label.' + String(getColumTitle(column)).toLowerCase()) }} + {{ $t('label.' + String(getColumnTitle(column)).toLowerCase()) }} @@ -1005,16 +1005,16 @@ export default { return host.state }, getColumnKey (name) { - if (typeof name === 'object') { - name = Object.keys(name).includes('field') ? name.field : name.customTitle + if (typeof name !== 'object' || name === null) { + return name } - return name + return name.field ?? name.customTitle ?? Object.keys(name)[0] }, - getColumTitle (name) { - if (typeof name === 'object') { - name = Object.keys(name).includes('customTitle') ? name.customTitle : name.field + getColumnTitle (name) { + if (typeof name !== 'object' || name === null) { + return name } - return name + return name.customTitle ?? name.field ?? Object.keys(name)[0] }, handleResizeColumn (w, col) { col.width = w diff --git a/ui/src/components/view/SearchView.vue b/ui/src/components/view/SearchView.vue index dc0a1280b96..27142e521ba 100644 --- a/ui/src/components/view/SearchView.vue +++ b/ui/src/components/view/SearchView.vue @@ -680,6 +680,9 @@ export default { if (accountIndex > -1) { this.fields[accountIndex].loading = false } + if (hypervisorIndex > -1) { + this.fields[hypervisorIndex].loading = false + } if (imageStoreIndex > -1) { this.fields[imageStoreIndex].loading = false } diff --git a/ui/src/config/section/network.js b/ui/src/config/section/network.js index 2fee9fca319..f3a706bbcc1 100644 --- a/ui/src/config/section/network.js +++ b/ui/src/config/section/network.js @@ -140,7 +140,10 @@ export default { icon: 'edit-outlined', label: 'label.update.network', dataView: true, - disabled: (record, user) => { return (record.account !== user.userInfo.account && !['Admin', 'DomainAdmin'].includes(user.userInfo.roletype)) }, + disabled: (record, user) => { + return (!record.projectid && (record.account !== user.userInfo.account && !['Admin', 'DomainAdmin'].includes(user.userInfo.roletype))) || + (record.type === 'Shared' && record.specifyvlan && !['Admin'].includes(user.userInfo.roletype)) + }, popup: true, component: shallowRef(defineAsyncComponent(() => import('@/views/network/UpdateNetwork.vue'))) }, @@ -150,7 +153,10 @@ export default { label: 'label.restart.network', message: 'message.restart.network', dataView: true, - disabled: (record, user) => { return (record.account !== user.userInfo.account && !['Admin', 'DomainAdmin'].includes(user.userInfo.roletype)) }, + disabled: (record, user) => { + return (!record.projectid && (record.account !== user.userInfo.account && !['Admin', 'DomainAdmin'].includes(user.userInfo.roletype))) || + (record.type === 'Shared' && record.specifyvlan && !['Admin'].includes(user.userInfo.roletype)) + }, args: (record, store, isGroupAction) => { var fields = [] if (isGroupAction || record.vpcid == null) { @@ -189,7 +195,10 @@ export default { label: 'label.action.delete.network', message: 'message.action.delete.network', dataView: true, - disabled: (record, user) => { return (record.account !== user.userInfo.account && !['Admin', 'DomainAdmin'].includes(user.userInfo.roletype)) }, + disabled: (record, user) => { + return (!record.projectid && (record.account !== user.userInfo.account && !['Admin', 'DomainAdmin'].includes(user.userInfo.roletype))) || + (record.type === 'Shared' && record.specifyvlan && !['Admin'].includes(user.userInfo.roletype)) + }, groupAction: true, popup: true, groupMap: (selection) => { return selection.map(x => { return { id: x } }) } diff --git a/ui/src/views/iam/DomainView.vue b/ui/src/views/iam/DomainView.vue index 997f900bbf7..ed875151a13 100644 --- a/ui/src/views/iam/DomainView.vue +++ b/ui/src/views/iam/DomainView.vue @@ -56,6 +56,7 @@ :loading="loading" :tabs="$route.meta.tabs" /> @@ -52,6 +53,7 @@ @keydown.esc="editableValueKey = null" @pressEnter="updateConfigurationValue(configrecord)" @change="value => setConfigurationEditable(configrecord, value)" + @keydown="e => handleInputNumberKeyDown(e, true)" /> @@ -87,6 +89,7 @@ @keydown.esc="editableValueKey = null" @pressEnter="updateConfigurationValue(configrecord)" @change="value => setConfigurationEditable(configrecord, value)" + @keydown="e => handleInputNumberKeyDown(e, true)" /> @@ -365,6 +368,26 @@ export default { } else { this.editableValueKey = null } + }, + handleInputNumberKeyDown (event, isDecimal) { + const allowedCodes = ['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Minus'] + + if (isDecimal) { + allowedCodes.push('Period') + } + + if ( + event.getModifierState('Control') || + event.getModifierState('Meta') || + event.getModifierState('Alt') + ) { + return + } + + const isValid = allowedCodes.includes(event.code) || !isNaN(event.key) + if (!isValid) { + event.preventDefault() + } } } }