This is an automated email from the ASF dual-hosted git repository.

dahn pushed a commit to branch 4.16
in repository https://gitbox.apache.org/repos/asf/cloudstack.git


The following commit(s) were added to refs/heads/4.16 by this push:
     new 2bd1dc1  Enable resetting config values to default value (#4230)
2bd1dc1 is described below

commit 2bd1dc1e149777ac42fb077bffb64bfa56c68ac4
Author: Rakesh <[email protected]>
AuthorDate: Mon Jan 3 21:55:30 2022 +0100

    Enable resetting config values to default value (#4230)
    
    * Enable resetting config values to default value
    
    Provide reset button to zone,cluster,domain,account,
    primary and secondary storage so that config values
    can be reset to default value
    
    * fix ui issue
    
    * Update test/integration/smoke/test_reset_configuration_settings.py
    
    * Update test/integration/smoke/test_reset_configuration_settings.py
    
    Co-authored-by: Rakesh Venkatesh <[email protected]>
    Co-authored-by: dahn <[email protected]>
---
 .../cloud/configuration/ConfigurationService.java  |  10 +
 .../org/apache/cloudstack/api/ApiConstants.java    |   1 +
 .../api/command/admin/config/ResetCfgCmd.java      | 166 ++++++++++
 .../java/com/cloud/dc/ClusterDetailsDaoImpl.java   |  28 +-
 .../configuration/ConfigurationManagerImpl.java    | 204 +++++++++++-
 .../com/cloud/server/ManagementServerImpl.java     |   2 +
 .../cloud/vpc/MockConfigurationManagerImpl.java    |   7 +
 .../smoke/test_reset_configuration_settings.py     | 367 +++++++++++++++++++++
 tools/marvin/marvin/lib/base.py                    |  20 ++
 ui/public/locales/en.json                          |   2 +
 ui/src/components/view/ListView.vue                |  23 ++
 ui/src/components/view/SettingsTab.vue             |  28 ++
 12 files changed, 849 insertions(+), 9 deletions(-)

diff --git 
a/api/src/main/java/com/cloud/configuration/ConfigurationService.java 
b/api/src/main/java/com/cloud/configuration/ConfigurationService.java
index 38d9d32..87d2d2f 100644
--- a/api/src/main/java/com/cloud/configuration/ConfigurationService.java
+++ b/api/src/main/java/com/cloud/configuration/ConfigurationService.java
@@ -18,6 +18,7 @@ package com.cloud.configuration;
 
 import java.util.List;
 
+import org.apache.cloudstack.api.command.admin.config.ResetCfgCmd;
 import org.apache.cloudstack.api.command.admin.config.UpdateCfgCmd;
 import 
org.apache.cloudstack.api.command.admin.network.CreateManagementNetworkIpRangeCmd;
 import 
org.apache.cloudstack.api.command.admin.network.CreateNetworkOfferingCmd;
@@ -77,6 +78,15 @@ public interface ConfigurationService {
     Configuration updateConfiguration(UpdateCfgCmd cmd) throws 
InvalidParameterValueException;
 
     /**
+     * Resets a configuration entry with default value
+     *
+     * @param cmd
+     *            - the command wrapping name parameter
+     * @return updated configuration object if successful
+     */
+    Pair<Configuration, String> resetConfiguration(ResetCfgCmd cmd) throws 
InvalidParameterValueException;
+
+    /**
      * Create a service offering through the API
      *
      * @param cmd
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 b19819c..b9a809f 100644
--- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
+++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
@@ -714,6 +714,7 @@ public class ApiConstants {
     public static final String VM_SNAPSHOT_MEMORY = "snapshotmemory";
     public static final String VM_SNAPSHOT_QUIESCEVM = "quiescevm";
     public static final String IMAGE_STORE_UUID = "imagestoreuuid";
+    public static final String IMAGE_STORE_ID = "imagestoreid";
     public static final String GUEST_VM_CIDR = "guestvmcidr";
     public static final String NETWORK_CIDR = "networkcidr";
     public static final String RESERVED_IP_RANGE = "reservediprange";
diff --git 
a/api/src/main/java/org/apache/cloudstack/api/command/admin/config/ResetCfgCmd.java
 
b/api/src/main/java/org/apache/cloudstack/api/command/admin/config/ResetCfgCmd.java
new file mode 100644
index 0000000..28a69ce
--- /dev/null
+++ 
b/api/src/main/java/org/apache/cloudstack/api/command/admin/config/ResetCfgCmd.java
@@ -0,0 +1,166 @@
+// 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.
+package org.apache.cloudstack.api.command.admin.config;
+
+import org.apache.cloudstack.api.APICommand;
+import org.apache.cloudstack.api.ApiArgValidator;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.ApiErrorCode;
+import org.apache.cloudstack.api.BaseCmd;
+import org.apache.cloudstack.api.Parameter;
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.response.ImageStoreResponse;
+import org.apache.cloudstack.framework.config.ConfigKey;
+import org.apache.log4j.Logger;
+
+import org.apache.cloudstack.api.response.AccountResponse;
+import org.apache.cloudstack.api.response.ClusterResponse;
+import org.apache.cloudstack.api.response.ConfigurationResponse;
+import org.apache.cloudstack.api.response.DomainResponse;
+import org.apache.cloudstack.api.response.StoragePoolResponse;
+import org.apache.cloudstack.api.response.ZoneResponse;
+import org.apache.cloudstack.config.Configuration;
+
+import com.cloud.user.Account;
+import com.cloud.utils.Pair;
+
+@APICommand(name = "resetConfiguration", description = "Resets a 
configuration. The configuration will be set to default value for global 
setting, and removed from account_details or domain_details for Account/Domain 
settings", responseObject = ConfigurationResponse.class,
+        requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, 
since = "4.16.0")
+public class ResetCfgCmd extends BaseCmd {
+    public static final Logger s_logger = 
Logger.getLogger(ResetCfgCmd.class.getName());
+    private static final String s_name = "resetconfigurationresponse";
+
+    /////////////////////////////////////////////////////
+    //////////////// API parameters /////////////////////
+    /////////////////////////////////////////////////////
+
+    @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, required = 
true, description = "the name of the configuration", validations = 
{ApiArgValidator.NotNullOrEmpty})
+    private String cfgName;
+
+    @Parameter(name = ApiConstants.ZONE_ID,
+               type = CommandType.UUID,
+               entityType = ZoneResponse.class,
+               description = "the ID of the Zone to reset the parameter value 
for corresponding zone")
+    private Long zoneId;
+
+    @Parameter(name = ApiConstants.CLUSTER_ID,
+               type = CommandType.UUID,
+               entityType = ClusterResponse.class,
+               description = "the ID of the Cluster to reset the parameter 
value for corresponding cluster")
+    private Long clusterId;
+
+    @Parameter(name = ApiConstants.STORAGE_ID,
+               type = CommandType.UUID,
+               entityType = StoragePoolResponse.class,
+               description = "the ID of the Storage pool to reset the 
parameter value for corresponding storage pool")
+    private Long storagePoolId;
+
+    @Parameter(name = ApiConstants.DOMAIN_ID,
+            type = CommandType.UUID,
+            entityType = DomainResponse.class,
+            description = "the ID of the Domain to reset the parameter value 
for corresponding domain")
+    private Long domainId;
+
+    @Parameter(name = ApiConstants.ACCOUNT_ID,
+               type = CommandType.UUID,
+               entityType = AccountResponse.class,
+               description = "the ID of the Account to reset the parameter 
value for corresponding account")
+    private Long accountId;
+
+    @Parameter(name = ApiConstants.IMAGE_STORE_ID,
+            type = CommandType.UUID,
+            entityType = ImageStoreResponse.class,
+            description = "the ID of the Image Store to reset the parameter 
value for corresponding image store")
+    private Long imageStoreId;
+
+    /////////////////////////////////////////////////////
+    /////////////////// Accessors ///////////////////////
+    /////////////////////////////////////////////////////
+
+    public String getCfgName() {
+        return cfgName;
+    }
+
+    public Long getZoneId() {
+        return zoneId;
+    }
+
+    public Long getClusterId() {
+        return clusterId;
+    }
+
+    public Long getStoragepoolId() {
+        return storagePoolId;
+    }
+
+    public Long getDomainId() {
+        return domainId;
+    }
+
+    public Long getAccountId() {
+        return accountId;
+    }
+
+    public Long getImageStoreId() {
+        return imageStoreId;
+    }
+
+    /////////////////////////////////////////////////////
+    /////////////// API Implementation///////////////////
+    /////////////////////////////////////////////////////
+
+    @Override
+    public String getCommandName() {
+        return s_name;
+    }
+
+    @Override
+    public long getEntityOwnerId() {
+        return Account.ACCOUNT_ID_SYSTEM;
+    }
+
+    @Override
+    public void execute() {
+        Pair<Configuration, String> cfg = 
_configService.resetConfiguration(this);
+        if (cfg != null) {
+            ConfigurationResponse response = 
_responseGenerator.createConfigurationResponse(cfg.first());
+            response.setResponseName(getCommandName());
+            if (getZoneId() != null) {
+                response.setScope(ConfigKey.Scope.Zone.name());
+            }
+            if (getClusterId() != null) {
+                response.setScope(ConfigKey.Scope.Cluster.name());
+            }
+            if (getStoragepoolId() != null) {
+                response.setScope(ConfigKey.Scope.StoragePool.name());
+            }
+            if (getDomainId() != null) {
+                response.setScope(ConfigKey.Scope.Domain.name());
+            }
+            if (getAccountId() != null) {
+                response.setScope(ConfigKey.Scope.Account.name());
+            }
+            if (getImageStoreId() != null) {
+                response.setScope(ConfigKey.Scope.ImageStore.name());
+            }
+            response.setValue(cfg.second());
+            this.setResponseObject(response);
+        } else {
+            throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed 
to reset config");
+        }
+    }
+}
diff --git 
a/engine/schema/src/main/java/com/cloud/dc/ClusterDetailsDaoImpl.java 
b/engine/schema/src/main/java/com/cloud/dc/ClusterDetailsDaoImpl.java
index a932322..c2058ad 100644
--- a/engine/schema/src/main/java/com/cloud/dc/ClusterDetailsDaoImpl.java
+++ b/engine/schema/src/main/java/com/cloud/dc/ClusterDetailsDaoImpl.java
@@ -35,6 +35,11 @@ public class ClusterDetailsDaoImpl extends 
GenericDaoBase<ClusterDetailsVO, Long
     protected final SearchBuilder<ClusterDetailsVO> ClusterSearch;
     protected final SearchBuilder<ClusterDetailsVO> DetailSearch;
 
+    private final String CpuOverprovisioningFactor = 
"cpu.overprovisioning.factor";
+    private final String MemoryOverprovisioningFactor = 
"mem.overprovisioning.factor";
+    private final String CpuOverCommitRatio = "cpuOvercommitRatio";
+    private final String MemoryOverCommitRatio = "memoryOvercommitRatio";
+
     protected ClusterDetailsDaoImpl() {
         ClusterSearch = createSearchBuilder();
         ClusterSearch.and("clusterId", ClusterSearch.entity().getClusterId(), 
SearchCriteria.Op.EQ);
@@ -50,12 +55,7 @@ public class ClusterDetailsDaoImpl extends 
GenericDaoBase<ClusterDetailsVO, Long
     public ClusterDetailsVO findDetail(long clusterId, String name) {
         SearchCriteria<ClusterDetailsVO> sc = DetailSearch.create();
         // This is temporary fix to support list/update configuration api for 
cpu and memory overprovisioning ratios
-        if (name.equalsIgnoreCase("cpu.overprovisioning.factor")) {
-            name = "cpuOvercommitRatio";
-        }
-        if (name.equalsIgnoreCase("mem.overprovisioning.factor")) {
-            name = "memoryOvercommitRatio";
-        }
+        name = getCpuMemoryOvercommitRatio(name);
         sc.setParameters("clusterId", clusterId);
         sc.setParameters("name", name);
 
@@ -103,11 +103,13 @@ public class ClusterDetailsDaoImpl extends 
GenericDaoBase<ClusterDetailsVO, Long
         expunge(sc);
 
         for (Map.Entry<String, String> detail : details.entrySet()) {
+            String name = detail.getKey();
+            name = getCpuMemoryOvercommitRatio(name);
             String value = detail.getValue();
             if ("password".equals(detail.getKey())) {
                 value = DBEncryptionUtil.encrypt(value);
             }
-            ClusterDetailsVO vo = new ClusterDetailsVO(clusterId, 
detail.getKey(), value);
+            ClusterDetailsVO vo = new ClusterDetailsVO(clusterId, name, value);
             persist(vo);
         }
         txn.commit();
@@ -115,6 +117,7 @@ public class ClusterDetailsDaoImpl extends 
GenericDaoBase<ClusterDetailsVO, Long
 
     @Override
     public void persist(long clusterId, String name, String value) {
+        name = getCpuMemoryOvercommitRatio(name);
         TransactionLegacy txn = TransactionLegacy.currentTxn();
         txn.start();
         SearchCriteria<ClusterDetailsVO> sc = DetailSearch.create();
@@ -147,4 +150,15 @@ public class ClusterDetailsDaoImpl extends 
GenericDaoBase<ClusterDetailsVO, Long
             dcName = tokens[3];
         return dcName;
     }
+
+    private String getCpuMemoryOvercommitRatio(String name) {
+        if (name.equalsIgnoreCase(CpuOverprovisioningFactor)) {
+            name = CpuOverCommitRatio;
+        }
+        if (name.equalsIgnoreCase(MemoryOverprovisioningFactor)) {
+            name = MemoryOverCommitRatio;
+        }
+
+        return name;
+    }
 }
diff --git 
a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java 
b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java
index 1380621..1bcc3d9 100755
--- a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java
+++ b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java
@@ -30,10 +30,12 @@ import java.util.Comparator;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.ListIterator;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.Optional;
 import java.util.Set;
 import java.util.UUID;
 import java.util.Vector;
@@ -51,6 +53,7 @@ import 
org.apache.cloudstack.agent.lb.IndirectAgentLBServiceImpl;
 import org.apache.cloudstack.annotation.AnnotationService;
 import org.apache.cloudstack.annotation.dao.AnnotationDao;
 import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.command.admin.config.ResetCfgCmd;
 import org.apache.cloudstack.api.command.admin.config.UpdateCfgCmd;
 import 
org.apache.cloudstack.api.command.admin.network.CreateManagementNetworkIpRangeCmd;
 import 
org.apache.cloudstack.api.command.admin.network.CreateNetworkOfferingCmd;
@@ -105,6 +108,7 @@ import org.apache.cloudstack.region.dao.RegionDao;
 import org.apache.cloudstack.resourcedetail.DiskOfferingDetailVO;
 import org.apache.cloudstack.resourcedetail.dao.DiskOfferingDetailsDao;
 import org.apache.cloudstack.storage.datastore.db.ImageStoreDao;
+import org.apache.cloudstack.storage.datastore.db.ImageStoreDetailVO;
 import org.apache.cloudstack.storage.datastore.db.ImageStoreDetailsDao;
 import org.apache.cloudstack.storage.datastore.db.ImageStoreVO;
 import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
@@ -630,9 +634,16 @@ public class ConfigurationManagerImpl extends ManagerBase 
implements Configurati
                 if (cluster == null) {
                     throw new InvalidParameterValueException("unable to find 
cluster by id " + resourceId);
                 }
-                ClusterDetailsVO clusterDetailsVO = 
_clusterDetailsDao.findDetail(resourceId, name);
+                String newName = name;
+                if (name.equalsIgnoreCase("cpu.overprovisioning.factor")) {
+                    newName = "cpuOvercommitRatio";
+                }
+                if (name.equalsIgnoreCase("mem.overprovisioning.factor")) {
+                    newName = "memoryOvercommitRatio";
+                }
+                ClusterDetailsVO clusterDetailsVO = 
_clusterDetailsDao.findDetail(resourceId, newName);
                 if (clusterDetailsVO == null) {
-                    clusterDetailsVO = new ClusterDetailsVO(resourceId, name, 
value);
+                    clusterDetailsVO = new ClusterDetailsVO(resourceId, 
newName, value);
                     _clusterDetailsDao.persist(clusterDetailsVO);
                 } else {
                     clusterDetailsVO.setValue(value);
@@ -879,6 +890,159 @@ public class ConfigurationManagerImpl extends ManagerBase 
implements Configurati
         }
     }
 
+    private ParamCountPair getParamCount(Map<String, Long> scopeMap) {
+        Long id = null;
+        int paramCount = 0;
+        String scope = ConfigKey.Scope.Global.toString();
+
+        for (var entry : scopeMap.entrySet()) {
+            if (entry.getValue() != null) {
+                id = entry.getValue();
+                scope = entry.getKey();
+                paramCount++;
+            }
+        }
+
+        return new ParamCountPair(id, paramCount, scope);
+    }
+
+    @Override
+    @ActionEvent(eventType = EventTypes.EVENT_CONFIGURATION_VALUE_EDIT, 
eventDescription = "resetting configuration")
+    public Pair<Configuration, String> resetConfiguration(final ResetCfgCmd 
cmd) throws InvalidParameterValueException {
+        final Long userId = CallContext.current().getCallingUserId();
+        final String name = cmd.getCfgName();
+        final Long zoneId = cmd.getZoneId();
+        final Long clusterId = cmd.getClusterId();
+        final Long storagepoolId = cmd.getStoragepoolId();
+        final Long accountId = cmd.getAccountId();
+        final Long domainId = cmd.getDomainId();
+        final Long imageStoreId = cmd.getImageStoreId();
+        Optional optionalValue;
+        final ConfigKey<?> configKey = _configDepot.get(name);
+        if (configKey == null) {
+            s_logger.warn("Probably the component manager where configuration 
variable " + name + " is defined needs to implement Configurable interface");
+            throw new InvalidParameterValueException("Config parameter with 
name " + name + " doesn't exist");
+        }
+        String defaultValue = configKey.defaultValue();
+        String category = configKey.category();
+        String configScope = configKey.scope().toString();
+
+        String scope = "";
+        Map<String, Long> scopeMap = new LinkedHashMap<>();
+
+        Long id = null;
+        int paramCountCheck = 0;
+
+        scopeMap.put(ConfigKey.Scope.Zone.toString(), zoneId);
+        scopeMap.put(ConfigKey.Scope.Cluster.toString(), clusterId);
+        scopeMap.put(ConfigKey.Scope.Domain.toString(), domainId);
+        scopeMap.put(ConfigKey.Scope.Account.toString(), accountId);
+        scopeMap.put(ConfigKey.Scope.StoragePool.toString(), storagepoolId);
+        scopeMap.put(ConfigKey.Scope.ImageStore.toString(), imageStoreId);
+
+        ParamCountPair paramCountPair = getParamCount(scopeMap);
+        id = paramCountPair.getId();
+        paramCountCheck = paramCountPair.getParamCount();
+        scope = paramCountPair.getScope();
+
+        if (paramCountCheck > 1) {
+            throw new InvalidParameterValueException("cannot handle multiple 
IDs, provide only one ID corresponding to the scope");
+        }
+
+        if (scope != null && !scope.equals(ConfigKey.Scope.Global.toString()) 
&& !configScope.contains(scope)) {
+            throw new InvalidParameterValueException("Invalid scope id 
provided for the parameter " + name);
+        }
+
+        String newValue = null;
+        switch (ConfigKey.Scope.valueOf(scope)) {
+            case Zone:
+                final DataCenterVO zone = _zoneDao.findById(id);
+                if (zone == null) {
+                    throw new InvalidParameterValueException("unable to find 
zone by id " + id);
+                }
+                _dcDetailsDao.removeDetail(id, name);
+                optionalValue = Optional.ofNullable(configKey.valueIn(id));
+                newValue = optionalValue.isPresent() ? 
optionalValue.get().toString() : defaultValue;
+                break;
+
+            case Cluster:
+                final ClusterVO cluster = _clusterDao.findById(id);
+                if (cluster == null) {
+                    throw new InvalidParameterValueException("unable to find 
cluster by id " + id);
+                }
+                ClusterDetailsVO clusterDetailsVO = 
_clusterDetailsDao.findDetail(id, name);
+                newValue = configKey.value().toString();
+                if (name.equalsIgnoreCase("cpu.overprovisioning.factor") || 
name.equalsIgnoreCase("mem.overprovisioning.factor")) {
+                    _clusterDetailsDao.persist(id, name, newValue);
+                } else if (clusterDetailsVO != null) {
+                    _clusterDetailsDao.remove(clusterDetailsVO.getId());
+                }
+                optionalValue = Optional.ofNullable(configKey.valueIn(id));
+                newValue = optionalValue.isPresent() ? 
optionalValue.get().toString() : defaultValue;
+                break;
+
+            case StoragePool:
+                final StoragePoolVO pool = _storagePoolDao.findById(id);
+                if (pool == null) {
+                    throw new InvalidParameterValueException("unable to find 
storage pool by id " + id);
+                }
+                _storagePoolDetailsDao.removeDetail(id, name);
+                optionalValue = Optional.ofNullable(configKey.valueIn(id));
+                newValue = optionalValue.isPresent() ? 
optionalValue.get().toString() : defaultValue;
+                break;
+
+            case Domain:
+                final DomainVO domain = _domainDao.findById(id);
+                if (domain == null) {
+                    throw new InvalidParameterValueException("unable to find 
domain by id " + id);
+                }
+                DomainDetailVO domainDetailVO = 
_domainDetailsDao.findDetail(id, name);
+                if (domainDetailVO != null) {
+                    _domainDetailsDao.remove(domainDetailVO.getId());
+                }
+                optionalValue = Optional.ofNullable(configKey.valueIn(id));
+                newValue = optionalValue.isPresent() ? 
optionalValue.get().toString() : defaultValue;
+                break;
+
+            case Account:
+                final AccountVO account = _accountDao.findById(id);
+                if (account == null) {
+                    throw new InvalidParameterValueException("unable to find 
account by id " + id);
+                }
+                AccountDetailVO accountDetailVO = 
_accountDetailsDao.findDetail(id, name);
+                if (accountDetailVO != null) {
+                    _accountDetailsDao.remove(accountDetailVO.getId());
+                }
+                optionalValue = Optional.ofNullable(configKey.valueIn(id));
+                newValue = optionalValue.isPresent() ? 
optionalValue.get().toString() : defaultValue;
+                break;
+
+            case ImageStore:
+                final ImageStoreVO imageStoreVO = _imageStoreDao.findById(id);
+                if (imageStoreVO == null) {
+                    throw new InvalidParameterValueException("unable to find 
the image store by id " + id);
+                }
+                ImageStoreDetailVO imageStoreDetailVO = 
_imageStoreDetailsDao.findDetail(id, name);
+                if (imageStoreDetailVO != null) {
+                    _imageStoreDetailsDao.remove(imageStoreDetailVO.getId());
+                }
+                optionalValue = Optional.ofNullable(configKey.valueIn(id));
+                newValue = optionalValue.isPresent() ? 
optionalValue.get().toString() : defaultValue;
+                break;
+
+            default:
+                if (!_configDao.update(name, category, defaultValue)) {
+                    s_logger.error("Failed to reset configuration option, 
name: " + name + ", defaultValue:" + defaultValue);
+                    throw new CloudRuntimeException("Failed to reset 
configuration value. Please contact Cloud Support.");
+                }
+                optionalValue = Optional.ofNullable(configKey.value());
+                newValue = optionalValue.isPresent() ? 
optionalValue.get().toString() : defaultValue;
+        }
+
+        CallContext.current().setEventDetails(" Name: " + name + " New Value: 
" + (name.toLowerCase().contains("password") ? "*****" : defaultValue == null ? 
"" : defaultValue));
+        return new Pair<Configuration, String>(_configDao.findByName(name), 
newValue);
+    }
+
     private String validateConfigurationValue(final String name, String value, 
final String scope) {
 
         final ConfigurationVO cfg = _configDao.findByName(name);
@@ -7056,4 +7220,40 @@ public class ConfigurationManagerImpl extends 
ManagerBase implements Configurati
                 ENABLE_ACCOUNT_SETTINGS_FOR_DOMAIN, 
ENABLE_DOMAIN_SETTINGS_FOR_CHILD_DOMAIN
         };
     }
+
+    static class ParamCountPair {
+        private Long id;
+        private int paramCount;
+        private String scope;
+
+        public ParamCountPair(Long id, int paramCount, String scope) {
+            this.id = id;
+            this.paramCount = paramCount;
+            this.scope = scope;
+        }
+
+        public Long getId() {
+            return id;
+        }
+
+        public void setId(Long id) {
+            this.id = id;
+        }
+
+        public int getParamCount() {
+            return paramCount;
+        }
+
+        public void setParamCount(int paramCount) {
+            this.paramCount = paramCount;
+        }
+
+        public String getScope() {
+            return scope;
+        }
+
+        public void setScope(String scope) {
+            this.scope = scope;
+        }
+    }
 }
diff --git a/server/src/main/java/com/cloud/server/ManagementServerImpl.java 
b/server/src/main/java/com/cloud/server/ManagementServerImpl.java
index 2b79ff0..790a570 100644
--- a/server/src/main/java/com/cloud/server/ManagementServerImpl.java
+++ b/server/src/main/java/com/cloud/server/ManagementServerImpl.java
@@ -74,6 +74,7 @@ import 
org.apache.cloudstack.api.command.admin.cluster.UpdateClusterCmd;
 import org.apache.cloudstack.api.command.admin.config.ListCfgsByCmd;
 import 
org.apache.cloudstack.api.command.admin.config.ListDeploymentPlannersCmd;
 import 
org.apache.cloudstack.api.command.admin.config.ListHypervisorCapabilitiesCmd;
+import org.apache.cloudstack.api.command.admin.config.ResetCfgCmd;
 import org.apache.cloudstack.api.command.admin.config.UpdateCfgCmd;
 import 
org.apache.cloudstack.api.command.admin.config.UpdateHypervisorCapabilitiesCmd;
 import 
org.apache.cloudstack.api.command.admin.direct.download.RevokeTemplateDirectDownloadCertificateCmd;
@@ -3009,6 +3010,7 @@ public class ManagementServerImpl extends ManagerBase 
implements ManagementServe
         cmdList.add(ListHypervisorCapabilitiesCmd.class);
         cmdList.add(UpdateCfgCmd.class);
         cmdList.add(UpdateHypervisorCapabilitiesCmd.class);
+        cmdList.add(ResetCfgCmd.class);
         cmdList.add(CreateDomainCmd.class);
         cmdList.add(DeleteDomainCmd.class);
         cmdList.add(ListDomainChildrenCmd.class);
diff --git 
a/server/src/test/java/com/cloud/vpc/MockConfigurationManagerImpl.java 
b/server/src/test/java/com/cloud/vpc/MockConfigurationManagerImpl.java
index 112d20b..81831e3 100644
--- a/server/src/test/java/com/cloud/vpc/MockConfigurationManagerImpl.java
+++ b/server/src/test/java/com/cloud/vpc/MockConfigurationManagerImpl.java
@@ -23,6 +23,7 @@ import java.util.Set;
 import javax.inject.Inject;
 import javax.naming.ConfigurationException;
 
+import org.apache.cloudstack.api.command.admin.config.ResetCfgCmd;
 import org.apache.cloudstack.api.command.admin.config.UpdateCfgCmd;
 import 
org.apache.cloudstack.api.command.admin.network.CreateManagementNetworkIpRangeCmd;
 import 
org.apache.cloudstack.api.command.admin.network.CreateNetworkOfferingCmd;
@@ -615,4 +616,10 @@ public class MockConfigurationManagerImpl extends 
ManagerBase implements Configu
         return null;
     }
 
+    @Override
+    public Pair<Configuration, String> resetConfiguration(ResetCfgCmd cmd) 
throws InvalidParameterValueException {
+        // TODO Auto-generated method stub
+        return null;
+    }
+
 }
diff --git a/test/integration/smoke/test_reset_configuration_settings.py 
b/test/integration/smoke/test_reset_configuration_settings.py
new file mode 100644
index 0000000..edec6e4
--- /dev/null
+++ b/test/integration/smoke/test_reset_configuration_settings.py
@@ -0,0 +1,367 @@
+# 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.
+
+# Import Local Modules
+from nose.plugins.attrib import attr
+from marvin.cloudstackTestCase import cloudstackTestCase, unittest
+from marvin.lib.utils import (validateList,
+                              cleanup_resources)
+from marvin.lib.base import (Account,
+                             Configurations,
+                             Domain,
+                             Cluster,
+                             StoragePool)
+from marvin.lib.common import (get_domain,
+                               get_zone)
+
+class TestRestConfigurationSettings(cloudstackTestCase):
+    @classmethod
+    def setUpClass(self):
+        self.testClient = super(
+            TestRestConfigurationSettings,
+            self).getClsTestClient()
+        self.apiclient = self.testClient.getApiClient()
+        self.testdata = self.testClient.getParsedTestDataConfig()
+
+        # Get Zone, Domain
+        self.domain = get_domain(self.apiclient)
+        self.zone = get_zone(self.apiclient, self.testClient.getZoneForTests())
+        self._cleanup = []
+        return
+
+    @classmethod
+    def tearDownClass(cls):
+        super(TestRestConfigurationSettings, cls).tearDownClass()
+        return
+
+    def setUp(self):
+        self.apiclient = self.testClient.getApiClient()
+        self.dbclient = self.testClient.getDbConnection()
+        self.cleanup = []
+
+        return
+
+    def tearDown(self):
+        super(TestRestConfigurationSettings, self).tearDown()
+        return
+
+    @attr(tags=["advanced", "advancedsg"], required_hardware="false")
+    def test_01_test_settings_for_domain(self):
+        """
+        1. Get the default value for the setting in domain scope
+        2. Change the default value to new value
+        3. Make sure updated value is same as new value
+        4. Reset the config value
+        5. Make sure that current value is same as default value
+        :return:
+        """
+        config_name="ldap.basedn"
+        #1. Get default value
+        configs = Configurations.list(
+            self.apiclient,
+            name=config_name
+        )
+        self.assertIsNotNone(configs, "Fail to get domain setting %s " % 
config_name)
+
+        orig_value = str(configs[0].value)
+        new_value = "testing"
+
+        #2. Update to new value
+        Configurations.update(
+            self.apiclient,
+            name=config_name,
+            value=new_value,
+            domainid=self.domain.id
+        )
+
+        configs = Configurations.list(
+            self.apiclient,
+            name=config_name,
+            domainid=self.domain.id
+        )
+        self.assertIsNotNone(configs, "Fail to get domain setting %s " % 
config_name)
+
+        #3. validate they are same
+        self.assertEqual(new_value,
+                         str(configs[0].value),
+                         "Failed to set new config value")
+
+        #4. Reset the value
+        Configurations.reset(
+            self.apiclient,
+            name=config_name,
+            domainid=self.domain.id
+        )
+
+        #5. Make sure its same as original value
+        configs = Configurations.list(
+            self.apiclient,
+            name=config_name,
+            domainid=self.domain.id
+        )
+        self.assertIsNotNone(configs, "Fail to get domain setting %s " % 
config_name)
+
+        self.assertEqual(orig_value,
+                         str(configs[0].value),
+                         "Failed to reset the value")
+
+    @attr(tags=["advanced", "advancedsg"], required_hardware="false")
+    def test_02_test_settings_for_account(self):
+        """
+        1. Get the default value for the setting in account scope
+        2. Change the default value to new value
+        3. Make sure updated value is same as new value
+        4. Reset the config value
+        5. Make sure that current value is same as default value
+        :return:
+        """
+        accounts = Account.list(
+            self.apiclient,
+            domainid=self.domain.id,
+            listall=True
+        )
+
+        self.assertIsNotNone(accounts[0],
+                             "There should be atleast 1 account in the zone")
+
+        config_name = "enable.additional.vm.configuration"
+        #1. Get the default value
+        configs = Configurations.list(
+            self.apiclient,
+            name=config_name
+        )
+        self.assertIsNotNone(configs, "Fail to get account setting %s " % 
config_name)
+
+        orig_value = str(configs[0].value)
+        new_value = "true"
+
+        Configurations.update(
+            self.apiclient,
+            name=config_name,
+            value=new_value,
+            accountid=accounts[0].id
+        )
+
+        configs = Configurations.list(
+            self.apiclient,
+            name=config_name,
+            accountid=accounts[0].id
+        )
+        self.assertIsNotNone(configs, "Fail to get account setting %s " % 
config_name)
+
+        self.assertEqual(new_value,
+                         str(configs[0].value),
+                         "Failed to set new config value")
+
+        Configurations.reset(
+            self.apiclient,
+            name=config_name,
+            accountid=accounts[0].id
+        )
+
+        configs = Configurations.list(
+            self.apiclient,
+            name=config_name,
+            accountid=accounts[0].id
+        )
+        self.assertIsNotNone(configs, "Fail to get account setting %s " % 
config_name)
+
+        self.assertEqual(orig_value,
+                         str(configs[0].value),
+                         "Failed to reset the value")
+
+    @attr(tags=["advanced", "advancedsg"], required_hardware="false")
+    def test_03_test_settings_for_cluster(self):
+        """
+        1. Get the default value for the setting in cluster scope
+        2. Change the default value to new value
+        3. Make sure updated value is same as new value
+        4. Reset the config value
+        5. Make sure that current value is same as default value
+        :return:
+        """
+        cluster = Cluster.list(
+            self.apiclient
+        )
+
+        self.assertIsNotNone(cluster[0],
+                             "There should be atleast 1 cluster in the zone")
+
+        config_name = "cluster.storage.operations.exclude"
+        configs = Configurations.list(
+            self.apiclient,
+            name=config_name,
+            clusterid=cluster[0].id
+        )
+        self.assertIsNotNone(configs, "Fail to get cluster setting %s " % 
config_name)
+
+        orig_value = str(configs[0].value)
+        new_value = "true"
+
+        Configurations.update(
+            self.apiclient,
+            name=config_name,
+            value=new_value,
+            clusterid=cluster[0].id
+        )
+
+        configs = Configurations.list(
+            self.apiclient,
+            name=config_name,
+            clusterid=cluster[0].id
+        )
+        self.assertIsNotNone(configs, "Fail to get cluster setting %s " % 
config_name)
+
+        self.assertEqual(new_value,
+                         str(configs[0].value),
+                         "Failed to set new config value")
+
+        Configurations.reset(
+            self.apiclient,
+            name=config_name,
+            clusterid=cluster[0].id
+        )
+
+        configs = Configurations.list(
+            self.apiclient,
+            name=config_name,
+            clusterid=cluster[0].id
+        )
+        self.assertIsNotNone(configs, "Fail to get cluster setting %s " % 
config_name)
+
+        self.assertEqual(orig_value,
+                         str(configs[0].value),
+                         "Failed to reset the value")
+
+    @attr(tags=["advanced", "advancedsg"], required_hardware="false")
+    def test_04_test_settings_for_storage(self):
+        """
+        1. Get the default value for the setting in storage scope
+        2. Change the default value to new value
+        3. Make sure updated value is same as new value
+        4. Reset the config value
+        5. Make sure that current value is same as default value
+        :return:
+        """
+        storage = StoragePool.list(
+            self.apiclient
+        )
+
+        self.assertIsNotNone(storage[0],
+                             "There should be atleast 1 primary storage pool 
in the zone")
+
+        config_name = "vmware.create.full.clone"
+        configs = Configurations.list(
+            self.apiclient,
+            name=config_name,
+            storageid=storage[0].id
+        )
+        self.assertIsNotNone(configs, "Fail to get storage pool setting %s " % 
config_name)
+
+        orig_value = str(configs[0].value)
+        new_value = 'false'
+
+        Configurations.update(
+            self.apiclient,
+            name=config_name,
+            value=new_value,
+            storageid=storage[0].id
+        )
+
+        configs = Configurations.list(
+            self.apiclient,
+            name=config_name,
+            storageid=storage[0].id
+        )
+        self.assertIsNotNone(configs, "Fail to get storage pool setting %s " % 
config_name)
+
+        self.assertEqual(new_value,
+                         (configs[0].value),
+                         "Failed to set new config value")
+
+        Configurations.reset(
+            self.apiclient,
+            name=config_name,
+            storageid=storage[0].id
+        )
+
+        configs = Configurations.list(
+            self.apiclient,
+            name=config_name,
+            storage=storage[0].id
+        )
+        self.assertIsNotNone(configs, "Fail to get storage pool setting %s " % 
config_name)
+
+        self.assertEqual(orig_value,
+                         (configs[0].value),
+                         "Failed to reset the value for storage pool")
+
+    @attr(tags=["advanced", "advancedsg"], required_hardware="false")
+    def test_05_test_settings_for_zone(self):
+        """
+        1. Get the default value for the setting in zone scope
+        2. Change the default value to new value
+        3. Make sure updated value is same as new value
+        4. Reset the config value
+        5. Make sure that current value is same as default value
+        :return:
+        """
+        config_name = "enable.dynamic.scale.vm"
+        configs = Configurations.list(
+            self.apiclient,
+            name=config_name,
+            zoneid=self.zone.id
+        )
+        self.assertIsNotNone(configs, "Fail to get zone setting %s " % 
config_name)
+
+        orig_value = str(configs[0].value)
+        new_value = 'true'
+
+        Configurations.update(
+            self.apiclient,
+            name=config_name,
+            value=new_value,
+            zoneid=self.zone.id
+        )
+
+        configs = Configurations.list(
+            self.apiclient,
+            name=config_name,
+            zoneid=self.zone.id
+        )
+        self.assertIsNotNone(configs, "Fail to get ol setting %s " % 
config_name)
+
+        self.assertEqual(new_value,
+                         (configs[0].value),
+                         "Failed to set new config value")
+
+        Configurations.reset(
+            self.apiclient,
+            name=config_name,
+            zoneid=self.zone.id
+        )
+
+        configs = Configurations.list(
+            self.apiclient,
+            name=config_name,
+            zoneid=self.zone.id
+        )
+        self.assertIsNotNone(configs, "Fail to get zone setting %s " % 
config_name)
+
+        self.assertEqual(orig_value,
+                         (configs[0].value),
+                         "Failed to reset the value for zone")
diff --git a/tools/marvin/marvin/lib/base.py b/tools/marvin/marvin/lib/base.py
index b8c1138..910e8bd 100755
--- a/tools/marvin/marvin/lib/base.py
+++ b/tools/marvin/marvin/lib/base.py
@@ -4273,6 +4273,26 @@ class Configurations:
         return (apiclient.listCapabilities(cmd))
 
 
+    @classmethod
+    def reset(cls, apiclient, name, zoneid=None, clusterid=None, 
storageid=None, domainid=None, accountid=None):
+        """Resets the specified configuration to original value"""
+
+        cmd = resetConfiguration.resetConfigurationCmd()
+        cmd.name = name
+
+        if zoneid:
+            cmd.zoneid = zoneid
+        if clusterid:
+            cmd.clusterid = clusterid
+        if storageid:
+            cmd.storageid = storageid
+        if domainid:
+            cmd.domainid = domainid
+        if accountid:
+            cmd.accountid = accountid
+
+        apiclient.resetConfiguration(cmd)
+
 class NetScaler:
     """Manage external netscaler device"""
 
diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json
index 135630e..9dbbd85 100644
--- a/ui/public/locales/en.json
+++ b/ui/public/locales/en.json
@@ -1863,6 +1863,7 @@
 "label.reservedsystemnetmask": "Reserved system netmask",
 "label.reservedsystemstartip": "Start Reserved system IP",
 "label.reset": "Reset",
+"label.reset.config.value": "Reset to default value",
 "label.reset.ssh.key.pair": "Reset SSH Key Pair",
 "label.reset.ssh.key.pair.on.vm": "Reset SSH Key Pair on VM",
 "label.reset.to.default": "Reset to default",
@@ -2943,6 +2944,7 @@
 "message.error.rados.user": "Please enter RADOS User",
 "message.error.remove.nic": "There was an error",
 "message.error.remove.secondary.ipaddress": "There was an error removing the 
secondary IP Address",
+"message.error.reset.config": "Unable to reset config to default value",
 "message.error.required.input": "Please enter input",
 "message.error.retrieve.kubeconfig": "Unable to retrieve Kubernetes cluster 
config",
 "message.error.s3nfs.path": "Please enter S3 NFS Path",
diff --git a/ui/src/components/view/ListView.vue 
b/ui/src/components/view/ListView.vue
index 7ea3e39..2665035 100644
--- a/ui/src/components/view/ListView.vue
+++ b/ui/src/components/view/ListView.vue
@@ -348,6 +348,12 @@
         v-if="editableValueKey === record.key"
         iconType="check-circle"
         iconTwoToneColor="#52c41a" />
+      <tooltip-button
+        :tooltip="$t('label.reset.config.value')"
+        @click="resetConfig(record)"
+        v-if="editableValueKey !== record.key"
+        icon="reload"
+        :disabled="!('updateConfiguration' in $store.getters.apis)" />
     </template>
     <template slot="tariffActions" slot-scope="text, record">
       <tooltip-button
@@ -526,6 +532,23 @@ export default {
         this.$emit('refresh')
       })
     },
+    resetConfig (item) {
+      api('resetConfiguration', {
+        name: item.name
+      }).then(() => {
+        const message = `${this.$t('label.setting')} ${item.name} 
${this.$t('label.reset.config.value')}`
+        this.$message.success(message)
+      }).catch(error => {
+        console.error(error)
+        this.$message.error(this.$t('message.error.reset.config'))
+        this.$notification.error({
+          message: this.$t('label.error'),
+          description: this.$t('message.error.reset.config')
+        })
+      }).finally(() => {
+        this.$emit('refresh')
+      })
+    },
     editValue (record) {
       this.editableValueKey = record.key
       this.editableValue = record.value
diff --git a/ui/src/components/view/SettingsTab.vue 
b/ui/src/components/view/SettingsTab.vue
index 349d516..36bf11d 100644
--- a/ui/src/components/view/SettingsTab.vue
+++ b/ui/src/components/view/SettingsTab.vue
@@ -64,6 +64,12 @@
             v-if="editableValueKey === index"
             iconType="check-circle"
             iconTwoToneColor="#52c41a" />
+          <tooltip-button
+            :tooltip="$t('label.reset.config.value')"
+            @click="resetConfig(item)"
+            v-if="editableValueKey !== index"
+            icon="reload"
+            :disabled="!('updateConfiguration' in $store.getters.apis)" />
         </div>
       </a-list-item>
     </a-list>
@@ -182,6 +188,28 @@ export default {
     handleSearch (value) {
       this.filter = value
       this.fetchData()
+    },
+    resetConfig (item) {
+      this.tabLoading = true
+      api('resetConfiguration', {
+        [this.scopeKey]: this.resource.id,
+        name: item.name
+      }).then(() => {
+        const message = `${this.$t('label.setting')} ${item.name} 
${this.$t('label.reset.config.value')}`
+        this.$message.success(message)
+      }).catch(error => {
+        console.error(error)
+        this.$message.error(this.$t('message.error.reset.config'))
+        this.$notification.error({
+          message: this.$t('label.error'),
+          description: this.$t('message.error.reset.config')
+        })
+      }).finally(() => {
+        this.tabLoading = false
+        this.fetchData(() => {
+          this.editableValueKey = null
+        })
+      })
     }
   }
 }

Reply via email to