/**
* 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.ambari.server.state.stack.upgrade;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;
import org.apache.ambari.server.serveraction.upgrades.ConfigureAction;
import org.apache.ambari.server.state.Cluster;
import org.apache.ambari.server.state.stack.ConfigUpgradePack;
import org.apache.ambari.server.state.stack.upgrade.ConfigUpgradeChangeDefinition.ConfigurationKeyValue;
import org.apache.ambari.server.state.stack.upgrade.ConfigUpgradeChangeDefinition.Insert;
import org.apache.ambari.server.state.stack.upgrade.ConfigUpgradeChangeDefinition.Replace;
import org.apache.ambari.server.state.stack.upgrade.ConfigUpgradeChangeDefinition.Transfer;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
/**
* The {@link ConfigureTask} represents a configuration change. This task
* contains id of change. Change definitions are located in a separate file (config
* upgrade pack). IDs of change definitions share the same namespace within all
* stacks
* <p/>
*
* <pre>
* {@code
* <task xsi:type="configure" id="hdp_2_3_0_0-UpdateHiveConfig"/>
* }
* </pre>
*
*/
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name="configure")
public class ConfigureTask extends ServerSideActionTask {
private static Logger LOG = LoggerFactory.getLogger(ConfigureTask.class);
/**
* The key that represents the configuration type to change (ie hdfs-site).
*/
public static final String PARAMETER_CONFIG_TYPE = "configure-task-config-type";
/**
* Setting key/value pairs can be several per task, so they're passed in as a
* json-ified list of objects.
*/
public static final String PARAMETER_KEY_VALUE_PAIRS = "configure-task-key-value-pairs";
/**
* Transfers can be several per task, so they're passed in as a json-ified
* list of objects.
*/
public static final String PARAMETER_TRANSFERS = "configure-task-transfers";
/**
* Replacements can be several per task, so they're passed in as a json-ified list of
* objects.
*/
public static final String PARAMETER_REPLACEMENTS = "configure-task-replacements";
/**
* Insertions can be several per task, so they're passed in as a json-ified
* list of objects.
*/
public static final String PARAMETER_INSERTIONS = "configure-task-insertions";
public static final String actionVerb = "Configuring";
/**
* Gson
*/
private Gson m_gson = new Gson();
/**
* Constructor.
*/
public ConfigureTask() {
implClass = ConfigureAction.class.getName();
}
private Task.Type type = Task.Type.CONFIGURE;
@XmlAttribute(name = "id")
public String id;
/**
* {@inheritDoc}
*/
@Override
public Type getType() {
return type;
}
@Override
public StageWrapper.Type getStageWrapperType() {
return StageWrapper.Type.SERVER_SIDE_ACTION;
}
@Override
public String getActionVerb() {
return actionVerb;
}
/**
* This getter is intended to be used only from tests. In production,
* getConfigurationChanges() logic should be used instead
* @return id of config upgrade change definition as defined in upgrade pack
*/
public String getId() {
return id;
}
/**
* Gets the summary of the task or {@code null}.
*
* @return the task summary or {@code null}.
*/
public String getSummary(ConfigUpgradePack configUpgradePack) {
if(StringUtils.isNotBlank(id) && null != configUpgradePack){
ConfigUpgradeChangeDefinition definition = configUpgradePack.enumerateConfigChangesByID().get(id);
if (null != definition && StringUtils.isNotBlank(definition.summary)) {
return definition.summary;
}
}
return super.getSummary();
}
/**
* Gets a map containing the following properties pertaining to the
* configuration value to change:
* <ul>
* <li>{@link #PARAMETER_CONFIG_TYPE} - the configuration type (ie hdfs-site)</li>
* <li>{@link #PARAMETER_KEY_VALUE_PAIRS} - key/value pairs for the
* configurations</li>
* <li>{@link #PARAMETER_KEY_VALUE_PAIRS} - key/value pairs for the
* configurations</li>
* <li>{@link #PARAMETER_TRANSFERS} - COPY/MOVE/DELETE changes</li>
* <li>{@link #PARAMETER_REPLACEMENTS} - value replacements</li>
* </ul>
*
* @param cluster
* the cluster to use when retrieving conditional properties to test
* against (not {@code null}).
* @return the a map containing the changes to make. This could potentially be
* an empty map if no conditions are met. Callers should decide how to
* handle a configuration task that is unable to set any configuration
* values.
*/
public Map<String, String> getConfigurationChanges(Cluster cluster,
ConfigUpgradePack configUpgradePack) {
Map<String, String> configParameters = new HashMap<>();
if (id == null || id.isEmpty()) {
LOG.warn("Config task id is not defined, skipping config change");
return configParameters;
}
if (configUpgradePack == null) {
LOG.warn("Config upgrade pack is not defined, skipping config change");
return configParameters;
}
// extract config change definition, referenced by current ConfigureTask
ConfigUpgradeChangeDefinition definition = configUpgradePack.enumerateConfigChangesByID().get(id);
if (definition == null) {
LOG.warn(String.format("Can not resolve config change definition by id %s, " +
"skipping config change", id));
return configParameters;
}
// this task is not a condition task, so process the other elements normally
if (null != definition.getConfigType()) {
configParameters.put(PARAMETER_CONFIG_TYPE, definition.getConfigType());
}
// for every <set key=foo value=bar/> add it to this list
if (null != definition.getKeyValuePairs() && !definition.getKeyValuePairs().isEmpty()) {
List<ConfigurationKeyValue> allowedSets = getValidSets(cluster, definition.getConfigType(), definition.getKeyValuePairs());
configParameters.put(ConfigureTask.PARAMETER_KEY_VALUE_PAIRS,
m_gson.toJson(allowedSets));
}
// transfers
List<Transfer> transfers = definition.getTransfers();
if (null != transfers && !transfers.isEmpty()) {
List<Transfer> allowedTransfers = getValidTransfers(cluster, definition.getConfigType(), definition.getTransfers());
configParameters.put(ConfigureTask.PARAMETER_TRANSFERS, m_gson.toJson(allowedTransfers));
}
// replacements
List<Replace> replacements = new ArrayList<>();
replacements.addAll(definition.getReplacements());
//Fetch the replacements that used regex to find a string
replacements.addAll(definition.getRegexReplacements(cluster));
if( null != replacements && !replacements.isEmpty() ){
List<Replace> allowedReplacements = getValidReplacements(cluster, definition.getConfigType(), replacements);
configParameters.put(ConfigureTask.PARAMETER_REPLACEMENTS, m_gson.toJson(allowedReplacements));
}
// inserts
List<Insert> insertions = definition.getInsertions();
if (!insertions.isEmpty()) {
configParameters.put(ConfigureTask.PARAMETER_INSERTIONS, m_gson.toJson(insertions));
}
return configParameters;
}
private List<Replace> getValidReplacements(Cluster cluster, String configType, List<Replace> replacements){
List<Replace> allowedReplacements= new ArrayList<>();
for(Replace replacement: replacements){
if(isValidConditionSettings(cluster, configType, replacement.key,
replacement.ifKey, replacement.ifType, replacement.ifValue, replacement.ifKeyState)) {
allowedReplacements.add(replacement);
}
}
return allowedReplacements;
}
private List<ConfigurationKeyValue> getValidSets(Cluster cluster, String configType, List<ConfigurationKeyValue> sets){
List<ConfigurationKeyValue> allowedSets = new ArrayList<>();
for(ConfigurationKeyValue configurationKeyValue: sets){
if(isValidConditionSettings(cluster, configType, configurationKeyValue.key,
configurationKeyValue.ifKey, configurationKeyValue.ifType, configurationKeyValue.ifValue, configurationKeyValue.ifKeyState)) {
allowedSets.add(configurationKeyValue);
}
}
return allowedSets;
}
private List<Transfer> getValidTransfers(Cluster cluster, String configType, List<Transfer> transfers){
List<Transfer> allowedTransfers = new ArrayList<>();
for (Transfer transfer : transfers) {
String key = "";
if(transfer.operation == TransferOperation.DELETE) {
key = transfer.deleteKey;
} else {
key = transfer.fromKey;
}
if(isValidConditionSettings(cluster, configType, key,
transfer.ifKey, transfer.ifType, transfer.ifValue, transfer.ifKeyState)) {
allowedTransfers.add(transfer);
}
}
return allowedTransfers;
}
/**
* Sanity check for invalid attribute settings on if-key, if-value, if-key-state, if-site
* Regardless whether it's set, transfer, or replace, the condition attributes are the same
* So the same logic can be used to determine if the operation is allowed or not.
* */
private boolean isValidConditionSettings(Cluster cluster, String configType, String targetPropertyKey,
String ifKey, String ifType, String ifValue, PropertyKeyState ifKeyState){
//Operation is always valid if there are no conditions specified
boolean isValid = false;
boolean ifKeyIsNotBlank = StringUtils.isNotBlank(ifKey);
boolean ifTypeIsNotBlank = StringUtils.isNotBlank(ifType);
boolean ifValueIsNotNull = (null != ifValue);
boolean ifKeyStateIsValid = (PropertyKeyState.PRESENT == ifKeyState || PropertyKeyState.ABSENT == ifKeyState);
if(ifKeyIsNotBlank && ifTypeIsNotBlank && (ifValueIsNotNull || ifKeyStateIsValid)) {
// allow if the condition has ifKey, ifType and either ifValue or ifKeyState
isValid = true;
} else if (!ifKeyIsNotBlank && !ifTypeIsNotBlank && !ifValueIsNotNull && !ifKeyStateIsValid) {
//no condition, allow
isValid = true;
}
return isValid;
}
}