/**
* 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.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;
import org.apache.ambari.server.AmbariException;
import org.apache.ambari.server.stack.HostsType;
import org.apache.ambari.server.state.Cluster;
import org.apache.ambari.server.state.Host;
import org.apache.ambari.server.state.MaintenanceState;
import org.apache.ambari.server.state.UpgradeContext;
import org.apache.ambari.server.state.stack.UpgradePack.ProcessingComponent;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Objects;
/**
* Used to represent cluster-based operations.
*/
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name="cluster")
public class ClusterGrouping extends Grouping {
/**
* Logger.
*/
private static final Logger LOG = LoggerFactory.getLogger(ClusterGrouping.class);
/**
* Stages against a Service and Component, or the Server, that doesn't need a Processing Component.
*/
@XmlElement(name="execute-stage")
public List<ExecuteStage> executionStages;
@Override
public ClusterBuilder getBuilder() {
return new ClusterBuilder(this);
}
/**
* Represents a single-stage execution that happens as part of a cluster-wide
* upgrade or downgrade.
*/
public static class ExecuteStage {
@XmlAttribute(name="title")
public String title;
/**
* An optional ID which can be used to uniquely identified any execution
* stage.
*/
@XmlAttribute(name="id")
public String id;
@XmlElement(name="direction")
public Direction intendedDirection = null;
/**
* Optional service name, can be ""
*/
@XmlAttribute(name="service")
public String service;
/**
* Optional component name, can be ""
*/
@XmlAttribute(name="component")
public String component;
@XmlElement(name="task")
public Task task;
@XmlElement(name="scope")
public UpgradeScope scope = UpgradeScope.ANY;
/**
* A condition element with can prevent this stage from being scheduled in
* the upgrade.
*/
@XmlElement(name = "condition")
public Condition condition;
/**
* {@inheritDoc}
*/
@Override
public String toString() {
return Objects.toStringHelper(this).add("id", id).add("title",
title).omitNullValues().toString();
}
}
public class ClusterBuilder extends StageWrapperBuilder {
/**
* Constructor.
*
* @param grouping
* the upgrade/downgrade grouping (not {@code null}).
*/
private ClusterBuilder(Grouping grouping) {
super(grouping);
}
@Override
public void add(UpgradeContext ctx, HostsType hostsType, String service,
boolean clientOnly, ProcessingComponent pc, Map<String, String> params) {
// !!! no-op in this case
}
/**
* {@inheritDoc}
*/
@Override
public List<StageWrapper> build(UpgradeContext upgradeContext,
List<StageWrapper> stageWrappers) {
if (null == executionStages) {
return stageWrappers;
}
List<StageWrapper> results = new ArrayList<>(stageWrappers);
if (executionStages != null) {
for (ExecuteStage execution : executionStages) {
if (null != execution.intendedDirection
&& execution.intendedDirection != upgradeContext.getDirection()) {
continue;
}
// if there is a condition on the group, evaluate it and skip scheduling
// of this group if the condition has not been satisfied
if (null != execution.condition && !execution.condition.isSatisfied(upgradeContext)) {
LOG.info("Skipping {} while building upgrade orchestration due to {}", execution,
execution.condition);
continue;
}
Task task = execution.task;
StageWrapper wrapper = null;
switch (task.getType()) {
case MANUAL:
case SERVER_ACTION:
case CONFIGURE:
wrapper = getServerActionStageWrapper(upgradeContext, execution);
break;
case EXECUTE:
wrapper = getExecuteStageWrapper(upgradeContext, execution);
break;
default:
break;
}
if (null != wrapper) {
results.add(wrapper);
}
}
}
return results;
}
}
/**
* Return a Stage Wrapper for a server side action that runs on the server.
* @param ctx Upgrade Context
* @param execution Execution Stage
* @return Returns a Stage Wrapper
*/
private StageWrapper getServerActionStageWrapper(UpgradeContext ctx, ExecuteStage execution) {
String service = execution.service;
String component = execution.component;
Task task = execution.task;
Set<String> realHosts = Collections.emptySet();
if (StringUtils.isNotEmpty(service) && StringUtils.isNotEmpty(component)) {
HostsType hosts = ctx.getResolver().getMasterAndHosts(service, component);
if (null == hosts || hosts.hosts.isEmpty()) {
return null;
} else {
realHosts = new LinkedHashSet<>(hosts.hosts);
}
}
if (Task.Type.MANUAL == task.getType()) {
return new StageWrapper(
StageWrapper.Type.SERVER_SIDE_ACTION,
execution.title,
new TaskWrapper(service, component, realHosts, task));
} else {
return new StageWrapper(
StageWrapper.Type.SERVER_SIDE_ACTION,
execution.title,
new TaskWrapper(null, null, Collections.<String>emptySet(), task));
}
}
/**
* Return a Stage Wrapper for a task meant to execute code, typically on Ambari Server.
* @param ctx Upgrade Context
* @param execution Execution Stage
* @return Returns a Stage Wrapper, or null if a valid one could not be created.
*/
private StageWrapper getExecuteStageWrapper(UpgradeContext ctx, ExecuteStage execution) {
String service = execution.service;
String component = execution.component;
ExecuteTask et = (ExecuteTask) execution.task;
if (StringUtils.isNotBlank(service) && StringUtils.isNotBlank(component)) {
// !!! if the context is not scoped for the execute-stage, bail
if (!ctx.isScoped(execution.scope)) {
return null;
}
// !!! if the context is targeted and does not include the service, bail
if (!ctx.isServiceSupported(service)) {
return null;
}
// !!! FUTURE: check for component
HostsType hosts = ctx.getResolver().getMasterAndHosts(service, component);
if (hosts != null) {
Set<String> realHosts = new LinkedHashSet<>(hosts.hosts);
if (ExecuteHostType.MASTER == et.hosts && null != hosts.master) {
realHosts = Collections.singleton(hosts.master);
}
// Pick a random host.
if (ExecuteHostType.ANY == et.hosts && !hosts.hosts.isEmpty()) {
realHosts = Collections.singleton(hosts.hosts.iterator().next());
}
// Pick the first host sorted alphabetically (case insensitive)
if (ExecuteHostType.FIRST == et.hosts && !hosts.hosts.isEmpty()) {
List<String> sortedHosts = new ArrayList<>(hosts.hosts);
Collections.sort(sortedHosts, String.CASE_INSENSITIVE_ORDER);
realHosts = Collections.singleton(sortedHosts.get(0));
}
// !!! cannot execute against empty hosts (safety net)
if (realHosts.isEmpty()) {
return null;
}
return new StageWrapper(
StageWrapper.Type.RU_TASKS,
execution.title,
new TaskWrapper(service, component, realHosts, et));
}
} else if (null == service && null == component) {
// no service and no component will distributed the task to all healthy
// hosts not in maintenance mode
Cluster cluster = ctx.getCluster();
Set<String> hostNames = new HashSet<>();
for (Host host : ctx.getCluster().getHosts()) {
MaintenanceState maintenanceState = host.getMaintenanceState(cluster.getClusterId());
if (maintenanceState == MaintenanceState.OFF) {
hostNames.add(host.getHostName());
}
}
return new StageWrapper(
StageWrapper.Type.RU_TASKS,
execution.title,
new TaskWrapper(service, component, hostNames, et));
}
return null;
}
/**
* Attempts to merge the given cluster groupings. This merges the execute stages
* in an order specific manner.
*/
@Override
public void merge(Iterator<Grouping> iterator) throws AmbariException {
if (executionStages == null) {
executionStages = new ArrayList<>();
}
Map<String, List<ExecuteStage>> skippedStages = new HashMap<>();
while (iterator.hasNext()) {
Grouping next = iterator.next();
if (!(next instanceof ClusterGrouping)) {
throw new AmbariException("Invalid group type " + next.getClass().getSimpleName() + " expected cluster group");
}
ClusterGrouping clusterGroup = (ClusterGrouping) next;
boolean added = addGroupingStages(clusterGroup.executionStages, clusterGroup.addAfterGroupEntry);
if (added) {
addSkippedStages(skippedStages, clusterGroup.executionStages);
}
else {
// store these services until later
if (skippedStages.containsKey(next.addAfterGroupEntry)) {
List<ExecuteStage> tmp = skippedStages.get(clusterGroup.addAfterGroupEntry);
tmp.addAll(clusterGroup.executionStages);
}
else {
skippedStages.put(clusterGroup.addAfterGroupEntry, clusterGroup.executionStages);
}
}
}
}
/**
* Adds the given stages if the stage they are supposed to come after has been added.
*/
private boolean addGroupingStages(List<ExecuteStage> stagesToAdd, String after) {
if (after == null) {
executionStages.addAll(stagesToAdd);
return true;
}
else {
// Check the current stages, if the "after" stage is there then add these
for (int index = executionStages.size() - 1; index >= 0; index--) {
ExecuteStage stage = executionStages.get(index);
if ((stage.service != null && stage.service.equals(after)) || stage.title.equals(after)) {
executionStages.addAll(index + 1, stagesToAdd);
return true;
}
}
}
return false;
}
/**
* Adds the skipped stages if the stage they are supposed to come after has been added.
*/
private void addSkippedStages(Map<String, List<ExecuteStage>> skippedStages, List<ExecuteStage> stagesJustAdded) {
for (ExecuteStage stage : stagesJustAdded) {
if (skippedStages.containsKey(stage.service)) {
List<ExecuteStage> stagesToAdd = skippedStages.remove(stage.service);
addGroupingStages(stagesToAdd, stage.service);
addSkippedStages(skippedStages, stagesToAdd);
}
}
}
}