diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index 2f0e4f16797..96034e0ba8b 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -30,6 +30,7 @@ public class ApiConstants { public static final String ALGORITHM = "algorithm"; public static final String ALIAS = "alias"; public static final String ALLOCATED_ONLY = "allocatedonly"; + public static final String ALLOW_USER_FORCE_STOP_VM = "allowuserforcestopvm"; public static final String ANNOTATION = "annotation"; public static final String API_KEY = "apikey"; public static final String ARCHIVED = "archived"; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/config/ListCapabilitiesCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/config/ListCapabilitiesCmd.java index cf25dfaf5b5..a807d2ad837 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/config/ListCapabilitiesCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/config/ListCapabilitiesCmd.java @@ -55,6 +55,7 @@ public class ListCapabilitiesCmd extends BaseCmd { response.setAllowUserExpungeRecoverVM((Boolean)capabilities.get("allowUserExpungeRecoverVM")); response.setAllowUserExpungeRecoverVolume((Boolean)capabilities.get("allowUserExpungeRecoverVolume")); response.setAllowUserViewAllDomainAccounts((Boolean)capabilities.get("allowUserViewAllDomainAccounts")); + response.setAllowUserForceStopVM((Boolean)capabilities.get(ApiConstants.ALLOW_USER_FORCE_STOP_VM)); response.setKubernetesServiceEnabled((Boolean)capabilities.get("kubernetesServiceEnabled")); response.setKubernetesClusterExperimentalFeaturesEnabled((Boolean)capabilities.get("kubernetesClusterExperimentalFeaturesEnabled")); response.setCustomHypervisorDisplayName((String) capabilities.get("customHypervisorDisplayName")); diff --git a/api/src/main/java/org/apache/cloudstack/api/response/CapabilitiesResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/CapabilitiesResponse.java index e4224c85e97..162386ee042 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/CapabilitiesResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/CapabilitiesResponse.java @@ -92,6 +92,10 @@ public class CapabilitiesResponse extends BaseResponse { @Param(description = "true if users can see all accounts within the same domain, false otherwise") private boolean allowUserViewAllDomainAccounts; + @SerializedName(ApiConstants.ALLOW_USER_FORCE_STOP_VM) + @Param(description = "true if users are allowed to force stop a vm, false otherwise", since = "4.20.0") + private boolean allowUserForceStopVM; + @SerializedName("kubernetesserviceenabled") @Param(description = "true if Kubernetes Service plugin is enabled, false otherwise") private boolean kubernetesServiceEnabled; @@ -192,6 +196,10 @@ public class CapabilitiesResponse extends BaseResponse { this.allowUserViewAllDomainAccounts = allowUserViewAllDomainAccounts; } + public void setAllowUserForceStopVM(boolean allowUserForceStopVM) { + this.allowUserForceStopVM = allowUserForceStopVM; + } + public void setKubernetesServiceEnabled(boolean kubernetesServiceEnabled) { this.kubernetesServiceEnabled = kubernetesServiceEnabled; } diff --git a/server/src/main/java/com/cloud/server/ManagementServerImpl.java b/server/src/main/java/com/cloud/server/ManagementServerImpl.java index fb3cf9bf193..fabf82e5182 100644 --- a/server/src/main/java/com/cloud/server/ManagementServerImpl.java +++ b/server/src/main/java/com/cloud/server/ManagementServerImpl.java @@ -4461,6 +4461,7 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe final boolean allowUserViewDestroyedVM = (QueryService.AllowUserViewDestroyedVM.valueIn(caller.getId()) | _accountService.isAdmin(caller.getId())); final boolean allowUserExpungeRecoverVM = (UserVmManager.AllowUserExpungeRecoverVm.valueIn(caller.getId()) | _accountService.isAdmin(caller.getId())); final boolean allowUserExpungeRecoverVolume = (VolumeApiServiceImpl.AllowUserExpungeRecoverVolume.valueIn(caller.getId()) | _accountService.isAdmin(caller.getId())); + final boolean allowUserForceStopVM = (UserVmManager.AllowUserForceStopVm.valueIn(caller.getId()) | _accountService.isAdmin(caller.getId())); final boolean allowUserViewAllDomainAccounts = (QueryService.AllowUserViewAllDomainAccounts.valueIn(caller.getDomainId())); @@ -4488,6 +4489,7 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe capabilities.put("allowUserExpungeRecoverVM", allowUserExpungeRecoverVM); capabilities.put("allowUserExpungeRecoverVolume", allowUserExpungeRecoverVolume); capabilities.put("allowUserViewAllDomainAccounts", allowUserViewAllDomainAccounts); + capabilities.put(ApiConstants.ALLOW_USER_FORCE_STOP_VM, allowUserForceStopVM); capabilities.put("kubernetesServiceEnabled", kubernetesServiceEnabled); capabilities.put("kubernetesClusterExperimentalFeaturesEnabled", kubernetesClusterExperimentalFeaturesEnabled); capabilities.put("customHypervisorDisplayName", HypervisorGuru.HypervisorCustomDisplayName.value()); diff --git a/server/src/main/java/com/cloud/vm/UserVmManager.java b/server/src/main/java/com/cloud/vm/UserVmManager.java index 0dc7a7bb73f..047bc8ea6d2 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManager.java +++ b/server/src/main/java/com/cloud/vm/UserVmManager.java @@ -51,12 +51,15 @@ public interface UserVmManager extends UserVmService { String EnableDynamicallyScaleVmCK = "enable.dynamic.scale.vm"; String AllowDiskOfferingChangeDuringScaleVmCK = "allow.diskoffering.change.during.scale.vm"; String AllowUserExpungeRecoverVmCK ="allow.user.expunge.recover.vm"; + String AllowUserForceStopVmCK = "allow.user.force.stop.vm"; ConfigKey EnableDynamicallyScaleVm = new ConfigKey("Advanced", Boolean.class, EnableDynamicallyScaleVmCK, "false", "Enables/Disables dynamically scaling a vm", true, ConfigKey.Scope.Zone); ConfigKey AllowDiskOfferingChangeDuringScaleVm = new ConfigKey("Advanced", Boolean.class, AllowDiskOfferingChangeDuringScaleVmCK, "false", "Determines whether to allow or disallow disk offering change for root volume during scaling of a stopped or running vm", true, ConfigKey.Scope.Zone); ConfigKey AllowUserExpungeRecoverVm = new ConfigKey("Advanced", Boolean.class, AllowUserExpungeRecoverVmCK, "false", "Determines whether users can expunge or recover their vm", true, ConfigKey.Scope.Account); + ConfigKey AllowUserForceStopVm = new ConfigKey("Advanced", Boolean.class, AllowUserForceStopVmCK, "true", + "Determines whether users are allowed to force stop a vm", true, ConfigKey.Scope.Account); ConfigKey DisplayVMOVFProperties = new ConfigKey("Advanced", Boolean.class, "vm.display.ovf.properties", "false", "Set display of VMs OVF properties as part of VM details", true); diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index 22ef809e5da..0dc0e84cbd4 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -5348,6 +5348,13 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir public void finalizeExpunge(VirtualMachine vm) { } + private void checkForceStopVmPermission(Account callingAccount) { + if (!AllowUserForceStopVm.valueIn(callingAccount.getId())) { + logger.error("Parameter [{}] can only be passed by Admin accounts or when the allow.user.force.stop.vm config is true for the account.", ApiConstants.FORCED); + throw new PermissionDeniedException("Account does not have the permission to force stop the vm."); + } + } + @Override @ActionEvent(eventType = EventTypes.EVENT_VM_STOP, eventDescription = "stopping Vm", async = true) public UserVm stopVirtualMachine(long vmId, boolean forced) throws ConcurrentOperationException { @@ -5365,6 +5372,10 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir throw new InvalidParameterValueException("unable to find a virtual machine with id " + vmId); } + if (forced) { + checkForceStopVmPermission(caller); + } + // check if vm belongs to AutoScale vm group in Disabled state autoScaleManager.checkIfVmActionAllowed(vmId); @@ -8467,7 +8478,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir return new ConfigKey[] {EnableDynamicallyScaleVm, AllowDiskOfferingChangeDuringScaleVm, AllowUserExpungeRecoverVm, VmIpFetchWaitInterval, VmIpFetchTrialMax, VmIpFetchThreadPoolMax, VmIpFetchTaskWorkers, AllowDeployVmIfGivenHostFails, EnableAdditionalVmConfig, DisplayVMOVFProperties, KvmAdditionalConfigAllowList, XenServerAdditionalConfigAllowList, VmwareAdditionalConfigAllowList, DestroyRootVolumeOnVmDestruction, - EnforceStrictResourceLimitHostTagCheck, StrictHostTags}; + EnforceStrictResourceLimitHostTagCheck, StrictHostTags, AllowUserForceStopVm}; } @Override diff --git a/ui/src/config/section/compute.js b/ui/src/config/section/compute.js index 4c5a61e3bdc..7c7480a7b75 100644 --- a/ui/src/config/section/compute.js +++ b/ui/src/config/section/compute.js @@ -133,7 +133,10 @@ export default { dataView: true, groupAction: true, groupMap: (selection, values) => { return selection.map(x => { return { id: x, forced: values.forced } }) }, - args: ['forced'], + args: (record, store, group) => { + return (['Admin'].includes(store.userInfo.roletype) || store.features.allowuserforcestopvm) + ? ['forced'] : [] + }, show: (record) => { return ['Running'].includes(record.state) } }, {