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

adoroszlai pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/ambari.git


The following commit(s) were added to refs/heads/trunk by this push:
     new 1431ab4  AMBARI-25006. Support component-level provision_action in Add 
Service request (#2725)
1431ab4 is described below

commit 1431ab44887c2ff7f9e94f8fcb42e24e3ce33800
Author: Doroszlai, Attila <[email protected]>
AuthorDate: Thu Dec 20 15:56:46 2018 +0100

    AMBARI-25006. Support component-level provision_action in Add Service 
request (#2725)
---
 .../AmbariCustomCommandExecutionHelper.java        |   5 +-
 .../controller/AmbariManagementControllerImpl.java |  12 +-
 .../internal/HostComponentResourceProvider.java    |  11 +-
 .../controller/internal/ProvisionAction.java       |  28 +-
 .../internal/ServiceResourceProvider.java          |   5 +-
 .../server/controller/predicate/OrPredicate.java   |   6 +
 .../server/controller/predicate/Predicates.java    |  50 ++++
 .../ambari/server/topology/ProvisionStep.java      |  87 ++++++
 .../server/topology/addservice/AddServiceInfo.java |  24 +-
 .../addservice/AddServiceOrchestrator.java         |  17 +-
 .../addservice}/AddServiceRequest.java             | 165 +---------
 .../server/topology/addservice/Component.java      | 115 +++++++
 .../ambari/server/topology/addservice/Host.java    |  68 +++++
 .../ProvisionActionPredicateBuilder.java           | 331 +++++++++++++++++++++
 .../topology/addservice/RequestValidator.java      |  36 ++-
 .../addservice/RequestValidatorFactory.java        |   1 -
 .../addservice/ResourceProviderAdapter.java        |  57 ++--
 .../ambari/server/topology/addservice/Service.java |  96 ++++++
 .../addservice}/AddServiceRequestTest.java         |  18 +-
 .../ProvisionActionPredicateBuilderTest.java       | 202 +++++++++++++
 .../topology/addservice/RequestValidatorTest.java  |  47 ++-
 .../addservice/StackAdvisorAdapterTest.java        |  33 +-
 .../test/resources/add_service_api/request1.json   |   3 +-
 23 files changed, 1137 insertions(+), 280 deletions(-)

diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/controller/AmbariCustomCommandExecutionHelper.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/controller/AmbariCustomCommandExecutionHelper.java
index 52dd6ce..741f9e6 100644
--- 
a/ambari-server/src/main/java/org/apache/ambari/server/controller/AmbariCustomCommandExecutionHelper.java
+++ 
b/ambari-server/src/main/java/org/apache/ambari/server/controller/AmbariCustomCommandExecutionHelper.java
@@ -66,6 +66,7 @@ import org.apache.ambari.server.api.services.AmbariMetaInfo;
 import org.apache.ambari.server.configuration.Configuration;
 import org.apache.ambari.server.controller.internal.RequestOperationLevel;
 import org.apache.ambari.server.controller.internal.RequestResourceFilter;
+import org.apache.ambari.server.controller.internal.RequestResourceProvider;
 import org.apache.ambari.server.controller.spi.Resource;
 import org.apache.ambari.server.metadata.ActionMetadata;
 import org.apache.ambari.server.orm.dao.HostRoleCommandDAO;
@@ -450,8 +451,8 @@ public class AmbariCustomCommandExecutionHelper {
         commandTimeout = Math.max(60, commandTimeout);
       }
 
-      if (requestParams != null && requestParams.containsKey("context")) {
-        String requestContext = requestParams.get("context");
+      if (requestParams != null && 
requestParams.containsKey(RequestResourceProvider.CONTEXT)) {
+        String requestContext = 
requestParams.get(RequestResourceProvider.CONTEXT);
         if (StringUtils.isNotEmpty(requestContext) && 
requestContext.toLowerCase().contains("rolling-restart")) {
           Config clusterEnvConfig = 
cluster.getDesiredConfigByType("cluster-env");
           if (clusterEnvConfig != null) {
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/controller/AmbariManagementControllerImpl.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/controller/AmbariManagementControllerImpl.java
index 36e2e14..82bd14f 100644
--- 
a/ambari-server/src/main/java/org/apache/ambari/server/controller/AmbariManagementControllerImpl.java
+++ 
b/ambari-server/src/main/java/org/apache/ambari/server/controller/AmbariManagementControllerImpl.java
@@ -127,6 +127,7 @@ import 
org.apache.ambari.server.controller.internal.DeleteStatusMetaData;
 import 
org.apache.ambari.server.controller.internal.HostComponentResourceProvider;
 import org.apache.ambari.server.controller.internal.RequestOperationLevel;
 import org.apache.ambari.server.controller.internal.RequestResourceFilter;
+import org.apache.ambari.server.controller.internal.RequestResourceProvider;
 import org.apache.ambari.server.controller.internal.RequestStageContainer;
 import org.apache.ambari.server.controller.internal.URLStreamProvider;
 import 
org.apache.ambari.server.controller.internal.WidgetLayoutResourceProvider;
@@ -276,11 +277,6 @@ public class AmbariManagementControllerImpl implements 
AmbariManagementControlle
       LoggerFactory.getLogger(AmbariManagementControllerImpl.class);
   private final static Logger configChangeLog = 
LoggerFactory.getLogger("configchange");
 
-  /**
-   * Property name of request context.
-   */
-  private static final String REQUEST_CONTEXT_PROPERTY = "context";
-
   private static final Type hostAttributesType =
           new TypeToken<Map<String, String>>() {}.getType();
 
@@ -2910,7 +2906,7 @@ public class AmbariManagementControllerImpl implements 
AmbariManagementControlle
       String clusterHostInfoJson = 
StageUtils.getGson().toJson(clusterHostInfo);
 
       Stage stage = createNewStage(requestStages.getLastStageId(), cluster,
-          requestStages.getId(), 
requestProperties.get(REQUEST_CONTEXT_PROPERTY),
+          requestStages.getId(), 
requestProperties.get(RequestResourceProvider.CONTEXT),
           "{}", null);
       boolean skipFailure = false;
       if (requestProperties.containsKey(Setting.SETTING_NAME_SKIP_FAILURE) && 
requestProperties.get(Setting.SETTING_NAME_SKIP_FAILURE).equalsIgnoreCase("true"))
 {
@@ -3810,7 +3806,7 @@ public class AmbariManagementControllerImpl implements 
AmbariManagementControlle
     }
     LOG.debug("Refresh include/exclude files action will be executed for " + 
serviceMasterMap);
     HashMap<String, String> requestProperties = new HashMap<>();
-    requestProperties.put("context", "Update Include/Exclude Files for " + 
serviceMasterMap.keySet().toString());
+    requestProperties.put(RequestResourceProvider.CONTEXT, "Update 
Include/Exclude Files for " + serviceMasterMap.keySet().toString());
     HashMap<String, String> params = new HashMap<>();
     params.put(AmbariCustomCommandExecutionHelper.UPDATE_FILES_ONLY, 
String.valueOf(isDecommission));
 
@@ -4164,7 +4160,7 @@ public class AmbariManagementControllerImpl implements 
AmbariManagementControlle
     String requestContext = "";
 
     if (requestProperties != null) {
-      requestContext = requestProperties.get(REQUEST_CONTEXT_PROPERTY);
+      requestContext = requestProperties.get(RequestResourceProvider.CONTEXT);
       if (requestContext == null) {
         // guice needs a non-null value as there is no way to mark this 
parameter @Nullable
         requestContext = "";
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/HostComponentResourceProvider.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/HostComponentResourceProvider.java
index 8ba2623..d223e55 100644
--- 
a/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/HostComponentResourceProvider.java
+++ 
b/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/HostComponentResourceProvider.java
@@ -20,6 +20,7 @@ package org.apache.ambari.server.controller.internal;
 import static 
org.apache.ambari.server.controller.AmbariManagementControllerImpl.CLUSTER_PHASE_INITIAL_INSTALL;
 import static 
org.apache.ambari.server.controller.AmbariManagementControllerImpl.CLUSTER_PHASE_INITIAL_START;
 import static 
org.apache.ambari.server.controller.AmbariManagementControllerImpl.CLUSTER_PHASE_PROPERTY;
+import static 
org.apache.ambari.server.controller.internal.RequestResourceProvider.CONTEXT;
 
 import java.util.ArrayList;
 import java.util.Collection;
@@ -175,8 +176,8 @@ public class HostComponentResourceProvider extends 
AbstractControllerResourcePro
   public static final String SKIP_INSTALL_FOR_COMPONENTS = 
"skipInstallForComponents";
   public static final String DO_NOT_SKIP_INSTALL_FOR_COMPONENTS = 
"dontSkipInstallForComponents";
   public static final String ALL_COMPONENTS = "ALL";
-  public static final String SKIP_INSTALL_FOR_ALL_COMPONENTS = 
joinComponentList(ImmutableSet.of(ALL_COMPONENTS));
-  public static final String DO_NOT_SKIP_INSTALL_FOR_ANY_COMPONENTS = 
joinComponentList(ImmutableSet.of());
+  public static final String FOR_ALL_COMPONENTS = 
joinComponentList(ImmutableSet.of(ALL_COMPONENTS));
+  public static final String FOR_NO_COMPONENTS = 
joinComponentList(ImmutableSet.of());
 
   /**
    * maintenance state helper
@@ -394,7 +395,7 @@ public class HostComponentResourceProvider extends 
AbstractControllerResourcePro
 
     installProperties.put(DESIRED_STATE, "INSTALLED");
     Map<String, String> requestInfo = new HashMap<>();
-    requestInfo.put("context", String.format("Install components on host %s", 
hostname));
+    requestInfo.put(CONTEXT, String.format("Install components on host %s", 
hostname));
     requestInfo.put(CLUSTER_PHASE_PROPERTY, CLUSTER_PHASE_INITIAL_INSTALL);
     // although the operation is really for a specific host, the level needs 
to be set to HostComponent
     // to make sure that any service in maintenance mode does not prevent 
install/start on the new host during scale-up
@@ -452,7 +453,7 @@ public class HostComponentResourceProvider extends 
AbstractControllerResourcePro
       
CLUSTER_PHASE_INITIAL_INSTALL.equals(requestProperties.get(CLUSTER_PHASE_PROPERTY))
 &&
       skipInstallForComponents != null &&
       (skipInstallForComponents.contains(searchString) ||
-        (skipInstallForComponents.equals(SKIP_INSTALL_FOR_ALL_COMPONENTS) &&
+        (skipInstallForComponents.equals(FOR_ALL_COMPONENTS) &&
           
!requestProperties.get(DO_NOT_SKIP_INSTALL_FOR_COMPONENTS).contains(searchString))
       );
   }
@@ -470,7 +471,7 @@ public class HostComponentResourceProvider extends 
AbstractControllerResourcePro
       UnsupportedPropertyException, NoSuchParentResourceException {
 
     Map<String, String> requestInfo = new HashMap<>();
-    requestInfo.put("context", String.format("Start components on host %s", 
hostName));
+    requestInfo.put(CONTEXT, String.format("Start components on host %s", 
hostName));
     requestInfo.put(CLUSTER_PHASE_PROPERTY, CLUSTER_PHASE_INITIAL_START);
     // see rationale for marking the operation as HostComponent-level at 
"Install components on host"
     
requestInfo.putAll(RequestOperationLevel.propertiesFor(Resource.Type.HostComponent,
 cluster));
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/ProvisionAction.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/ProvisionAction.java
index 193f724..4c34dfc 100644
--- 
a/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/ProvisionAction.java
+++ 
b/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/ProvisionAction.java
@@ -18,27 +18,33 @@
 
 package org.apache.ambari.server.controller.internal;
 
+import java.util.List;
+
+import org.apache.ambari.server.topology.ProvisionStep;
+
+import com.google.common.collect.ImmutableList;
+
 public enum ProvisionAction {
   INSTALL_ONLY {
     @Override
-    public boolean skipStart() {
-      return true;
+    public List<ProvisionStep> getSteps() {
+      return ImmutableList.of(ProvisionStep.INSTALL);
     }
   },
   START_ONLY {
     @Override
-    public boolean skipInstall() {
-      return true;
+    public List<ProvisionStep> getSteps() {
+      return ImmutableList.of(ProvisionStep.SKIP_INSTALL, ProvisionStep.START);
+    }
+  },
+  INSTALL_AND_START {
+    @Override
+    public List<ProvisionStep> getSteps() {
+      return ImmutableList.of(ProvisionStep.INSTALL, ProvisionStep.START);
     }
   },
-  INSTALL_AND_START, // Default action
   ;
 
-  public boolean skipInstall() {
-    return false;
-  }
+  public abstract List<ProvisionStep> getSteps();
 
-  public boolean skipStart() {
-    return false;
-  }
 }
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/ServiceResourceProvider.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/ServiceResourceProvider.java
index 751ca0e..7d0ec62 100644
--- 
a/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/ServiceResourceProvider.java
+++ 
b/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/ServiceResourceProvider.java
@@ -39,8 +39,6 @@ import org.apache.ambari.server.ParentObjectNotFoundException;
 import org.apache.ambari.server.RoleCommand;
 import org.apache.ambari.server.ServiceNotFoundException;
 import org.apache.ambari.server.api.services.AmbariMetaInfo;
-import org.apache.ambari.server.controller.AddServiceRequest;
-import org.apache.ambari.server.controller.AddServiceRequest.OperationType;
 import org.apache.ambari.server.controller.AmbariManagementController;
 import org.apache.ambari.server.controller.KerberosHelper;
 import org.apache.ambari.server.controller.MaintenanceStateHelper;
@@ -80,6 +78,7 @@ import org.apache.ambari.server.state.StackId;
 import org.apache.ambari.server.state.State;
 import org.apache.ambari.server.topology.STOMPComponentsDeleteHandler;
 import org.apache.ambari.server.topology.addservice.AddServiceOrchestrator;
+import org.apache.ambari.server.topology.addservice.AddServiceRequest;
 import org.apache.ambari.server.utils.LoggingPreconditions;
 import org.apache.ambari.spi.RepositoryType;
 import org.apache.commons.collections.CollectionUtils;
@@ -1235,7 +1234,7 @@ public class ServiceResourceProvider extends 
AbstractControllerResourceProvider
   }
 
   private static boolean isAddServiceRequest(Map<String, Object> properties) {
-    return 
OperationType.ADD_SERVICE.name().equals(properties.get(OPERATION_TYPE));
+    return 
AddServiceRequest.OperationType.ADD_SERVICE.name().equals(properties.get(OPERATION_TYPE));
   }
 
   private RequestStatusResponse processAddServiceRequest(Map<String, Object> 
requestProperties, Map<String, String> requestInfoProperties) throws 
NoSuchParentResourceException {
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/controller/predicate/OrPredicate.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/controller/predicate/OrPredicate.java
index ab96cdf..d0c43d3 100644
--- 
a/ambari-server/src/main/java/org/apache/ambari/server/controller/predicate/OrPredicate.java
+++ 
b/ambari-server/src/main/java/org/apache/ambari/server/controller/predicate/OrPredicate.java
@@ -21,6 +21,8 @@ import java.util.Arrays;
 import java.util.LinkedList;
 import java.util.List;
 
+import javax.annotation.Nonnull;
+
 import org.apache.ambari.server.controller.spi.Predicate;
 import org.apache.ambari.server.controller.spi.Resource;
 
@@ -40,6 +42,10 @@ public class OrPredicate extends ArrayPredicate {
   }
 
   public static Predicate instance(Predicate... predicates) {
+    return of(Arrays.asList(predicates));
+  }
+
+  public static Predicate of(@Nonnull Iterable<? extends Predicate> 
predicates) {
     List<Predicate> predicateList = new LinkedList<>();
 
     // Simplify the predicate array
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/controller/predicate/Predicates.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/controller/predicate/Predicates.java
new file mode 100644
index 0000000..dde91ca
--- /dev/null
+++ 
b/ambari-server/src/main/java/org/apache/ambari/server/controller/predicate/Predicates.java
@@ -0,0 +1,50 @@
+/*
+ * 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.controller.predicate;
+
+import java.util.Collection;
+import java.util.Optional;
+import java.util.function.Function;
+
+import org.apache.ambari.server.controller.spi.Predicate;
+
+public class Predicates {
+
+  /**
+   * Creates an {@link OrPredicate} of the given predicates if any.
+   *
+   * @param predicates collection of predicates to be OR-ed
+   * @return {@link Optional} of the {@code OrPredicate} if any predicates are 
given,
+   *   otherwise an empty {@code Optional}
+   */
+  public static Optional<Predicate> anyOf(Collection<? extends Predicate> 
predicates) {
+    return predicates != null && !predicates.isEmpty() ? 
Optional.of(OrPredicate.of(predicates)) : Optional.empty();
+  }
+
+  /**
+   * Creates a {@link Function} which, when called, creates an {@link 
AndPredicate} of
+   * its input and the {@code presetPredicate}.  The function can then be used 
to transform
+   * {@code Optional}s or streams.
+   *
+   * @param presetPredicate this predicate will be AND-ed with the input to 
the {@code Function}
+   * @return the {@code Function}
+   */
+  public static Function<Predicate, Predicate> and(Predicate presetPredicate) {
+    return predicate -> new AndPredicate(presetPredicate, predicate);
+  }
+}
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/topology/ProvisionStep.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/topology/ProvisionStep.java
new file mode 100644
index 0000000..3e649bbb
--- /dev/null
+++ 
b/ambari-server/src/main/java/org/apache/ambari/server/topology/ProvisionStep.java
@@ -0,0 +1,87 @@
+/*
+ * 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.topology;
+
+import static 
org.apache.ambari.server.controller.AmbariManagementControllerImpl.CLUSTER_PHASE_INITIAL_INSTALL;
+import static 
org.apache.ambari.server.controller.AmbariManagementControllerImpl.CLUSTER_PHASE_INITIAL_START;
+import static 
org.apache.ambari.server.controller.AmbariManagementControllerImpl.CLUSTER_PHASE_PROPERTY;
+import static 
org.apache.ambari.server.controller.internal.HostComponentResourceProvider.DO_NOT_SKIP_INSTALL_FOR_COMPONENTS;
+import static 
org.apache.ambari.server.controller.internal.HostComponentResourceProvider.FOR_ALL_COMPONENTS;
+import static 
org.apache.ambari.server.controller.internal.HostComponentResourceProvider.FOR_NO_COMPONENTS;
+import static 
org.apache.ambari.server.controller.internal.HostComponentResourceProvider.SKIP_INSTALL_FOR_COMPONENTS;
+
+import java.util.Map;
+
+import org.apache.ambari.server.state.State;
+
+import com.google.common.collect.ImmutableMap;
+
+/**
+ * Steps of service provisioning.
+ */
+public enum ProvisionStep {
+
+  INSTALL {
+    @Override
+    public State getDesiredStateToSet() {
+      return State.INSTALLED;
+    }
+
+    @Override
+    public Map<String, String> getProvisionProperties() {
+      return ImmutableMap.of(CLUSTER_PHASE_PROPERTY, 
CLUSTER_PHASE_INITIAL_INSTALL);
+    }
+  },
+
+  /**
+   * This special step is used for START_ONLY services/components, because 
state
+   * transition cannot skip INSTALLED in the INIT -> INSTALLED -> STARTED 
sequence.
+   */
+  SKIP_INSTALL {
+    @Override
+    public State getDesiredStateToSet() {
+      return State.INSTALLED;
+    }
+
+    @Override
+    public Map<String, String> getProvisionProperties() {
+      return ImmutableMap.of(
+        SKIP_INSTALL_FOR_COMPONENTS,        FOR_ALL_COMPONENTS,
+        DO_NOT_SKIP_INSTALL_FOR_COMPONENTS, FOR_NO_COMPONENTS,
+        CLUSTER_PHASE_PROPERTY,             CLUSTER_PHASE_INITIAL_INSTALL
+      );
+    }
+  },
+
+  START {
+    @Override
+    public State getDesiredStateToSet() {
+      return State.STARTED;
+    }
+
+    @Override
+    public Map<String, String> getProvisionProperties() {
+      return ImmutableMap.of(CLUSTER_PHASE_PROPERTY, 
CLUSTER_PHASE_INITIAL_START);
+    }
+  },
+  ;
+
+  public abstract State getDesiredStateToSet();
+  public abstract Map<String, String> getProvisionProperties();
+
+}
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/topology/addservice/AddServiceInfo.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/topology/addservice/AddServiceInfo.java
index 25b9a3b..1d675e5 100644
--- 
a/ambari-server/src/main/java/org/apache/ambari/server/topology/addservice/AddServiceInfo.java
+++ 
b/ambari-server/src/main/java/org/apache/ambari/server/topology/addservice/AddServiceInfo.java
@@ -23,7 +23,6 @@ import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
 
-import org.apache.ambari.server.controller.AddServiceRequest;
 import org.apache.ambari.server.controller.internal.RequestStageContainer;
 import org.apache.ambari.server.controller.internal.Stack;
 import org.apache.ambari.server.state.kerberos.KerberosDescriptor;
@@ -46,13 +45,24 @@ public final class AddServiceInfo {
   public AddServiceInfo(
     AddServiceRequest request,
     String clusterName,
+    RequestStageContainer stages,
     Stack stack,
     Configuration config,
-    KerberosDescriptor kerberosDescriptor,
+    Map<String, Map<String, Set<String>>> newServices
+  ) {
+    this(request, clusterName, stages, stack, config, newServices, null, null);
+  }
+
+  AddServiceInfo(
+    AddServiceRequest request,
+    String clusterName,
     RequestStageContainer stages,
-    Map<String, Map<String,
-    Set<String>>> newServices,
-    LayoutRecommendationInfo recommendationInfo) {
+    Stack stack,
+    Configuration config,
+    Map<String, Map<String, Set<String>>> newServices,
+    KerberosDescriptor kerberosDescriptor,
+    LayoutRecommendationInfo recommendationInfo
+  ) {
     this.request = request;
     this.clusterName = clusterName;
     this.stack = stack;
@@ -65,11 +75,11 @@ public final class AddServiceInfo {
 
   public AddServiceInfo withLayoutRecommendation(Map<String, Map<String, 
Set<String>>> services,
                                                  LayoutRecommendationInfo 
recommendation) {
-    return new AddServiceInfo(request, clusterName, stack, config, 
kerberosDescriptor, stages, services, recommendation);
+    return new AddServiceInfo(request, clusterName, stages, stack, config, 
services, kerberosDescriptor, recommendation);
   }
 
   public AddServiceInfo withConfig(Configuration newConfig) {
-    return new AddServiceInfo(request, clusterName, stack, newConfig, 
kerberosDescriptor, stages, newServices, recommendationInfo);
+    return new AddServiceInfo(request, clusterName, stages, stack, newConfig, 
newServices, kerberosDescriptor, recommendationInfo);
   }
 
   @Override
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/topology/addservice/AddServiceOrchestrator.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/topology/addservice/AddServiceOrchestrator.java
index 4ff1301..6eed1af 100644
--- 
a/ambari-server/src/main/java/org/apache/ambari/server/topology/addservice/AddServiceOrchestrator.java
+++ 
b/ambari-server/src/main/java/org/apache/ambari/server/topology/addservice/AddServiceOrchestrator.java
@@ -28,7 +28,6 @@ import javax.inject.Singleton;
 import org.apache.ambari.server.AmbariException;
 import org.apache.ambari.server.actionmanager.ActionManager;
 import org.apache.ambari.server.actionmanager.RequestFactory;
-import org.apache.ambari.server.controller.AddServiceRequest;
 import org.apache.ambari.server.controller.AmbariManagementController;
 import org.apache.ambari.server.controller.KerberosHelper;
 import org.apache.ambari.server.controller.RequestStatusResponse;
@@ -41,6 +40,7 @@ import org.apache.ambari.server.state.Service;
 import org.apache.ambari.server.state.State;
 import org.apache.ambari.server.state.kerberos.KerberosDescriptor;
 import org.apache.ambari.server.topology.Configuration;
+import org.apache.ambari.server.topology.ProvisionStep;
 import org.apache.ambari.server.utils.LoggingPreconditions;
 import org.apache.ambari.server.utils.StageUtils;
 import org.slf4j.Logger;
@@ -160,9 +160,8 @@ public class AddServiceOrchestrator {
     resourceProviders.createComponents(request);
 
     resourceProviders.updateServiceDesiredState(request, State.INSTALLED);
-    if (!request.getRequest().getProvisionAction().skipStart()) {
-      resourceProviders.updateServiceDesiredState(request, State.STARTED);
-    }
+    resourceProviders.updateServiceDesiredState(request, State.STARTED);
+
     resourceProviders.createHostComponents(request);
 
     configureKerberos(request, cluster, existingServices);
@@ -195,11 +194,13 @@ public class AddServiceOrchestrator {
   }
 
   private void createHostTasks(AddServiceInfo request) {
-    LOG.info("Creating host tasks for {}: {}", request, 
request.getRequest().getProvisionAction());
+    LOG.info("Creating host tasks for {}", request);
 
-    resourceProviders.updateHostComponentDesiredState(request, 
State.INSTALLED);
-    if (!request.getRequest().getProvisionAction().skipStart()) {
-      resourceProviders.updateHostComponentDesiredState(request, 
State.STARTED);
+    ProvisionActionPredicateBuilder predicates = new 
ProvisionActionPredicateBuilder(request);
+    for (ProvisionStep step : ProvisionStep.values()) {
+      predicates.getPredicate(step).ifPresent(predicate ->
+        resourceProviders.updateHostComponentDesiredState(request, predicate, 
step)
+      );
     }
 
     try {
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/controller/AddServiceRequest.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/topology/addservice/AddServiceRequest.java
similarity index 72%
rename from 
ambari-server/src/main/java/org/apache/ambari/server/controller/AddServiceRequest.java
rename to 
ambari-server/src/main/java/org/apache/ambari/server/topology/addservice/AddServiceRequest.java
index e169f82..e2148e0 100644
--- 
a/ambari-server/src/main/java/org/apache/ambari/server/controller/AddServiceRequest.java
+++ 
b/ambari-server/src/main/java/org/apache/ambari/server/topology/addservice/AddServiceRequest.java
@@ -16,11 +16,10 @@
  * limitations under the License.
  */
 
-package org.apache.ambari.server.controller;
+package org.apache.ambari.server.topology.addservice;
 
 import static java.util.Collections.emptySet;
 import static java.util.stream.Collectors.toMap;
-import static java.util.stream.Collectors.toSet;
 import static 
org.apache.ambari.server.controller.internal.BaseClusterRequest.PROVISION_ACTION_PROPERTY;
 import static 
org.apache.ambari.server.controller.internal.ClusterResourceProvider.CREDENTIALS;
 import static 
org.apache.ambari.server.controller.internal.ClusterResourceProvider.SECURITY;
@@ -30,9 +29,7 @@ import static 
org.apache.ambari.server.topology.Configurable.CONFIGURATIONS;
 
 import java.io.IOException;
 import java.io.UncheckedIOException;
-import java.util.Arrays;
 import java.util.Collection;
-import java.util.HashMap;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
@@ -104,25 +101,6 @@ public class AddServiceRequest {
     @JsonProperty(CREDENTIALS) Set<Credential> credentials,
     @JsonProperty(CONFIGURATIONS) Collection<? extends Map<String, ?>> configs
   ) {
-    this(operationType, recommendationStrategy, provisionAction, 
validationType, stackName, stackVersion, services, components,
-      security, credentials,
-      ConfigurableHelper.parseConfigs(configs)
-    );
-  }
-
-  private AddServiceRequest(
-    OperationType operationType,
-    ConfigRecommendationStrategy recommendationStrategy,
-    ProvisionAction provisionAction,
-    ValidationType validationType,
-    String stackName,
-    String stackVersion,
-    Set<Service> services,
-    Set<Component> components,
-    SecurityConfiguration security,
-    Set<Credential> credentials,
-    Configuration configuration
-  ) {
     this.operationType = null != operationType ? operationType : 
OperationType.ADD_SERVICE;
     this.recommendationStrategy = null != recommendationStrategy ? 
recommendationStrategy : ConfigRecommendationStrategy.defaultForAddService();
     this.provisionAction = null != provisionAction ? provisionAction : 
ProvisionAction.INSTALL_AND_START;
@@ -132,7 +110,7 @@ public class AddServiceRequest {
     this.services = null != services ? services : emptySet();
     this.components = null != components ? components : emptySet();
     this.security = security;
-    this.configuration = null != configuration ? configuration : new 
Configuration(new HashMap<>(), new HashMap<>());
+    this.configuration = null != configs ? 
ConfigurableHelper.parseConfigs(configs) : Configuration.newEmpty();
     this.credentials = null != credentials
       ? credentials.stream().collect(toMap(Credential::getAlias, 
Function.identity()))
       : ImmutableMap.of();
@@ -312,143 +290,4 @@ public class AddServiceRequest {
     public abstract boolean strictValidation();
   }
 
-  public static final class Component {
-
-    static final String COMPONENT_NAME = "name";
-    static final String HOSTS = "hosts";
-
-    private final String name;
-    private final Set<Host> hosts;
-
-    @JsonCreator
-    public Component(@JsonProperty(COMPONENT_NAME) String name, 
@JsonProperty(HOSTS) Set<Host> hosts) {
-      this.name = name;
-      this.hosts = hosts != null ? ImmutableSet.copyOf(hosts) : 
ImmutableSet.of();
-    }
-
-    public static Component of(String name, String... hosts) {
-      return new Component(name, 
Arrays.stream(hosts).map(Host::new).collect(toSet()));
-    }
-
-    @JsonProperty(COMPONENT_NAME)
-    @ApiModelProperty(name = COMPONENT_NAME)
-    public String getName() {
-      return name;
-    }
-
-    @JsonProperty(HOSTS)
-    @ApiModelProperty(name = HOSTS)
-    public Set<Host> getHosts() {
-      return hosts;
-    }
-
-    @Override
-    public boolean equals(Object o) {
-      if (this == o) {
-        return true;
-      }
-      if (o == null || getClass() != o.getClass()) {
-        return false;
-      }
-
-      Component other = (Component) o;
-
-      return Objects.equals(name, other.name) &&
-        Objects.equals(hosts, other.hosts);
-    }
-
-    @Override
-    public int hashCode() {
-      return Objects.hash(name, hosts);
-    }
-
-    @Override
-    public String toString() {
-      return name;
-    }
-  }
-
-  public static final class Host {
-
-    static final String FQDN = "fqdn";
-
-    private final String fqdn;
-
-    @JsonCreator
-    public Host(@JsonProperty(FQDN) String fqdn) {
-      this.fqdn = fqdn;
-    }
-
-    @JsonProperty(FQDN)
-    @ApiModelProperty(name = FQDN)
-    public String getFqdn() {
-      return fqdn;
-    }
-
-    @Override
-    public boolean equals(Object o) {
-      if (this == o) {
-        return true;
-      }
-      if (o == null || getClass() != o.getClass()) {
-        return false;
-      }
-
-      Host other = (Host) o;
-
-      return Objects.equals(fqdn, other.fqdn);
-    }
-
-    @Override
-    public int hashCode() {
-      return Objects.hashCode(fqdn);
-    }
-
-    @Override
-    public String toString() {
-      return "host: " + fqdn;
-    }
-
-  }
-
-  @ApiModel
-  public static final class Service {
-
-    static final String NAME = "name";
-
-    private final String name;
-
-    @JsonCreator
-    public Service(@JsonProperty(NAME) String name) {
-      this.name = name;
-    }
-
-    public static Service of(String name) {
-      return new Service(name);
-    }
-
-    @JsonProperty(NAME)
-    @ApiModelProperty(name = NAME)
-    public String getName() {
-      return name;
-    }
-
-    @Override
-    public boolean equals(Object o) {
-      if (this == o) return true;
-      if (o == null || getClass() != o.getClass()) return false;
-      Service service = (Service) o;
-      return Objects.equals(name, service.name);
-    }
-
-    @Override
-    public int hashCode() {
-      return Objects.hash(name);
-    }
-
-    @Override
-    public String toString() {
-      return name;
-    }
-  }
 }
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/topology/addservice/Component.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/topology/addservice/Component.java
new file mode 100644
index 0000000..d467b68
--- /dev/null
+++ 
b/ambari-server/src/main/java/org/apache/ambari/server/topology/addservice/Component.java
@@ -0,0 +1,115 @@
+/*
+ * 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.topology.addservice;
+
+import static java.util.stream.Collectors.toSet;
+import static 
org.apache.ambari.server.controller.internal.BaseClusterRequest.PROVISION_ACTION_PROPERTY;
+
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+
+import org.apache.ambari.annotations.ApiIgnore;
+import org.apache.ambari.server.controller.internal.ProvisionAction;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.collect.ImmutableSet;
+
+import io.swagger.annotations.ApiModelProperty;
+
+public final class Component {
+
+  private static final String COMPONENT_NAME = "name";
+  private static final String HOSTS = "hosts";
+
+  private final String name;
+  private final Set<Host> hosts;
+  private final ProvisionAction provisionAction;
+
+  @JsonCreator
+  public Component(
+    @JsonProperty(COMPONENT_NAME) String name,
+    @JsonProperty(PROVISION_ACTION_PROPERTY) ProvisionAction provisionAction,
+    @JsonProperty(HOSTS) Set<Host> hosts
+  ) {
+    this.name = name;
+    this.provisionAction = provisionAction;
+    this.hosts = hosts != null ? ImmutableSet.copyOf(hosts) : 
ImmutableSet.of();
+  }
+
+  public static Component of(String name, String... hosts) {
+    return of(name, null, hosts);
+  }
+
+  public static Component of(String name, ProvisionAction provisionAction, 
String... hosts) {
+    return new Component(name, provisionAction, 
Arrays.stream(hosts).map(Host::new).collect(toSet()));
+  }
+
+  @JsonProperty(COMPONENT_NAME)
+  @ApiModelProperty(name = COMPONENT_NAME)
+  public String getName() {
+    return name;
+  }
+
+  @JsonProperty(HOSTS)
+  @ApiModelProperty(name = HOSTS)
+  public Set<Host> getHosts() {
+    return hosts;
+  }
+
+  @JsonProperty(PROVISION_ACTION_PROPERTY)
+  @ApiModelProperty(name = PROVISION_ACTION_PROPERTY)
+  public ProvisionAction _getProvisionAction() {
+    return provisionAction;
+  }
+
+  @ApiIgnore
+  @JsonIgnore
+  public Optional<ProvisionAction> getProvisionAction() {
+    return Optional.ofNullable(provisionAction);
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+
+    Component other = (Component) o;
+
+    return Objects.equals(name, other.name) &&
+      Objects.equals(hosts, other.hosts) &&
+      Objects.equals(provisionAction, other.provisionAction);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(name, hosts, provisionAction);
+  }
+
+  @Override
+  public String toString() {
+    return name;
+  }
+}
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/topology/addservice/Host.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/topology/addservice/Host.java
new file mode 100644
index 0000000..3392a80
--- /dev/null
+++ 
b/ambari-server/src/main/java/org/apache/ambari/server/topology/addservice/Host.java
@@ -0,0 +1,68 @@
+/*
+ * 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.topology.addservice;
+
+import java.util.Objects;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import io.swagger.annotations.ApiModelProperty;
+
+public final class Host {
+
+  private static final String FQDN = "fqdn";
+
+  private final String fqdn;
+
+  @JsonCreator
+  public Host(@JsonProperty(FQDN) String fqdn) {
+    this.fqdn = fqdn;
+  }
+
+  @JsonProperty(FQDN)
+  @ApiModelProperty(name = FQDN)
+  public String getFqdn() {
+    return fqdn;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+
+    Host other = (Host) o;
+
+    return Objects.equals(fqdn, other.fqdn);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hashCode(fqdn);
+  }
+
+  @Override
+  public String toString() {
+    return "host: " + fqdn;
+  }
+
+}
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/topology/addservice/ProvisionActionPredicateBuilder.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/topology/addservice/ProvisionActionPredicateBuilder.java
new file mode 100644
index 0000000..26c8ea4
--- /dev/null
+++ 
b/ambari-server/src/main/java/org/apache/ambari/server/topology/addservice/ProvisionActionPredicateBuilder.java
@@ -0,0 +1,331 @@
+/*
+ * 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.topology.addservice;
+
+import static java.util.stream.Collectors.toMap;
+import static java.util.stream.Collectors.toSet;
+import static 
org.apache.ambari.server.controller.internal.HostComponentResourceProvider.CLUSTER_NAME;
+import static 
org.apache.ambari.server.controller.internal.HostComponentResourceProvider.COMPONENT_NAME;
+import static 
org.apache.ambari.server.controller.internal.HostComponentResourceProvider.HOST_NAME;
+import static 
org.apache.ambari.server.controller.internal.HostComponentResourceProvider.SERVICE_NAME;
+import static org.apache.ambari.server.controller.predicate.Predicates.and;
+import static org.apache.ambari.server.controller.predicate.Predicates.anyOf;
+
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Function;
+
+import org.apache.ambari.server.controller.internal.ProvisionAction;
+import org.apache.ambari.server.controller.predicate.EqualsPredicate;
+import org.apache.ambari.server.controller.spi.Predicate;
+import org.apache.ambari.server.topology.ProvisionStep;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+
+/**
+ * Builds predicates to be used in the "update host component state" requests, 
one for each {@link ProvisionStep}.
+ *
+ * By default all components are both installed and started (except client 
components, but that exception is handled in
+ * HostComponentResourceProvider, not here).  This can be customized at the 
request-, service-, and component levels in
+ * the Add Service request.  Actions are inherited at the lower levels, so the 
request-level action applies to all
+ * services that have no custom action given, and the effective service-level 
action applies to all components of the
+ * given service with similar restriction.  The request and each service can 
specify exactly one {@code ProvisionAction},
+ * but at the component level the same component may have different action for 
groups of hosts.
+ *
+ * The predicates need to filter by service name (to avoid affecting existing 
services), and apply any component-level
+ * overrides by further checking for component name and host name.
+ *
+ * Example:
+ * <pre>
+ * cluster_name=TEST
+ * AND
+ * (
+ *   (
+ *     service_name=AMBARI_METRICS
+ *     AND
+ *     (
+ *       component_name=METRICS_MONITOR
+ *       OR
+ *       component_name=METRICS_COLLECTOR
+ *     )
+ *   )
+ *   OR
+ *   (
+ *     service_name=KAFKA
+ *     AND
+ *     (
+ *       component_name=KAFKA_BROKER
+ *       AND
+ *       host_name=c7402
+ *     )
+ *   )
+ * )
+ * </pre>
+ */
+public class ProvisionActionPredicateBuilder {
+
+  private final Map<ProvisionStep, Predicate> predicates = new 
EnumMap<>(ProvisionStep.class);
+  private final AddServiceInfo request;
+  private final Map<String, ProvisionAction> customServiceActions;
+  private final Map<String, Map<String, Map<ProvisionAction, Set<String>>>> 
customComponentActions;
+  private final Map<ProvisionStep, List<Predicate>> servicePredicatesByStep;
+
+  public ProvisionActionPredicateBuilder(AddServiceInfo request) {
+    this.request = request;
+
+    customServiceActions = findServicesWithCustomAction();
+    customComponentActions = findComponentsWithCustomAction();
+    servicePredicatesByStep = createServicePredicates();
+    createGlobalPredicates();
+  }
+
+  public Optional<Predicate> getPredicate(ProvisionStep action) {
+    return Optional.ofNullable(predicates.get(action));
+  }
+
+  /**
+   * Creates a "global" predicate for each {@link ProvisionStep} in the form 
of:
+   * cluster_name=... AND (service1 predicate OR service2 predicate OR ...)
+   */
+  private void createGlobalPredicates() {
+    Preconditions.checkState(servicePredicatesByStep != null);
+
+    Function<Predicate, Predicate> andClusterNameMatches = 
and(clusterNameIs(request.clusterName()));
+    for (Map.Entry<ProvisionStep, List<Predicate>> entry : 
servicePredicatesByStep.entrySet()) {
+      ProvisionStep step = entry.getKey();
+      List<Predicate> servicePredicates = entry.getValue();
+      anyOf(servicePredicates).map(andClusterNameMatches).ifPresent(predicate 
-> predicates.put(step, predicate));
+    }
+  }
+
+  /**
+   * Creates predicates for each service for each {@link ProvisionStep} as 
necessary.
+   *
+   * @return step -> service predicates map
+   */
+  private Map<ProvisionStep, List<Predicate>> createServicePredicates() {
+    Preconditions.checkState(customServiceActions != null);
+    Preconditions.checkState(customComponentActions != null);
+
+    ProvisionAction requestAction = request.getRequest().getProvisionAction();
+    Map<ProvisionStep, List<Predicate>> servicePredicatesByStep = new 
EnumMap<>(ProvisionStep.class);
+
+    for (Map.Entry<String, Map<String, Set<String>>> serviceEntry : 
request.newServices().entrySet()) {
+      String serviceName = serviceEntry.getKey();
+      Map<String, Set<String>> hostsByComponent = serviceEntry.getValue();
+
+      ProvisionAction serviceAction = 
customServiceActions.getOrDefault(serviceName, requestAction);
+      Predicate serviceNamePredicate = serviceNameIs(serviceName);
+
+      Map<String, Map<ProvisionAction, Set<String>>> customActionByComponent = 
customComponentActions.get(serviceName);
+      if (customActionByComponent == null) {
+        classifyItem(serviceAction, serviceNamePredicate, 
servicePredicatesByStep);
+      } else {
+        Map<ProvisionStep, List<Predicate>> componentPredicatesByStep =
+          createComponentPredicates(serviceAction, hostsByComponent, 
customActionByComponent);
+
+        applyComponentOverrides(servicePredicatesByStep, serviceNamePredicate, 
componentPredicatesByStep);
+      }
+    }
+    return servicePredicatesByStep;
+  }
+
+  /**
+   * Creates a service-level predicate for each step in the form of:
+   * <pre>service_name=... AND (component1 predicate OR component2 predicate 
OR ...)</pre>
+   * The result is appended to the list of predicates corresponding to each 
step in {@code servicePredicatesByStep}.
+   *
+   * @param servicePredicatesByStep step -> service predicates
+   * @param serviceNamePredicate predicate for service_name=...
+   * @param componentPredicatesByStep step -> component predicates
+   */
+  private static void applyComponentOverrides(
+    Map<ProvisionStep, List<Predicate>> servicePredicatesByStep,
+    Predicate serviceNamePredicate,
+    Map<ProvisionStep, List<Predicate>> componentPredicatesByStep
+  ) {
+    Function<Predicate, Predicate> andServiceNameMatches = 
and(serviceNamePredicate);
+    for (Map.Entry<ProvisionStep, List<Predicate>> entry : 
componentPredicatesByStep.entrySet()) {
+      ProvisionStep step = entry.getKey();
+      List<Predicate> componentPredicates = entry.getValue();
+      
anyOf(componentPredicates).map(andServiceNameMatches).ifPresent(predicate ->
+        servicePredicatesByStep.computeIfAbsent(step, __ -> new 
LinkedList<>()).add(predicate)
+      );
+    }
+  }
+
+  /**
+   * Creates predicates for each component with custom action (one that does 
not match its parent service's action).
+   *
+   * @param serviceAction service-level action
+   * @param hostsByComponent component -> hosts map (all hosts, including ones 
for which no custom action was specified)
+   * @param customActionByComponent component -> action -> hosts mapping;
+   *   only contains components whose action does not match the upper-level 
(service or request) action
+   * @return step -> component predicates map
+   */
+  private static Map<ProvisionStep, List<Predicate>> createComponentPredicates(
+    ProvisionAction serviceAction,
+    Map<String, Set<String>> hostsByComponent,
+    Map<String, Map<ProvisionAction, Set<String>>> customActionByComponent
+  ) {
+    Map<ProvisionStep, List<Predicate>> componentPredicatesByStep = new 
EnumMap<>(ProvisionStep.class);
+
+    for (Map.Entry<String, Set<String>> componentEntry : 
hostsByComponent.entrySet()) {
+      String componentName = componentEntry.getKey();
+      Set<String> allHosts = componentEntry.getValue();
+      Map<ProvisionAction, Set<String>> hostsByAction = 
customActionByComponent.getOrDefault(componentName, ImmutableMap.of());
+
+      if (!hostsByAction.isEmpty()) {
+        Set<String> customActionHosts = new HashSet<>();
+        for (Map.Entry<ProvisionAction, Set<String>> e : 
hostsByAction.entrySet()) {
+          ProvisionAction componentAction = e.getKey();
+          Set<String> hosts = e.getValue();
+
+          Predicate componentPredicate = 
predicateForComponentHosts(componentName, hosts);
+          classifyItem(componentAction, componentPredicate, 
componentPredicatesByStep);
+
+          customActionHosts.addAll(hosts);
+        }
+
+        // apply service-level action to any hosts for which no explicit 
action was specified
+        Set<String> leftoverHosts = 
ImmutableSet.copyOf(Sets.difference(allHosts, customActionHosts));
+        if (!leftoverHosts.isEmpty()) {
+          Predicate componentPredicate = 
predicateForComponentHosts(componentName, leftoverHosts);
+          classifyItem(serviceAction, componentPredicate, 
componentPredicatesByStep);
+        }
+      } else {
+        Predicate componentPredicate = componentNameIs(componentName);
+        classifyItem(serviceAction, componentPredicate, 
componentPredicatesByStep);
+      }
+    }
+    return componentPredicatesByStep;
+  }
+
+  /**
+   * Maps services in the request by component.
+   *
+   * @return component -> service map
+   */
+  private Map<String, String> mapServicesByComponent() {
+    Map<String, String> serviceByComponent = new HashMap<>();
+    for (Map.Entry<String, Map<String, Set<String>>> e : 
request.newServices().entrySet()) {
+      String service = e.getKey();
+      for (String component : e.getValue().keySet()) {
+        serviceByComponent.put(component, service);
+      }
+    }
+    return serviceByComponent;
+  }
+
+  /**
+   * Finds all services for which custom provision action was specified in the 
request.
+   *
+   * @return service -> action map; only contains services whose action does 
not match the request-level action
+   */
+  private Map<String, ProvisionAction> findServicesWithCustomAction() {
+    ProvisionAction requestAction = request.getRequest().getProvisionAction();
+    return request.getRequest().getServices().stream()
+      .filter(service -> service.getProvisionAction().isPresent())
+      .filter(service -> !Objects.equals(requestAction, 
service.getProvisionAction().get()))
+      .collect(toMap(Service::getName, service -> 
service.getProvisionAction().get()));
+  }
+
+  /**
+   * Finds all host components for which custom provision action was specified 
in the request.
+   *
+   * @return service -> component -> action -> hosts mapping; only contains 
components whose action does not match the upper-level (service or request) 
action
+   */
+  private Map<String, Map<String, Map<ProvisionAction, Set<String>>>> 
findComponentsWithCustomAction() {
+    Preconditions.checkState(customServiceActions != null);
+
+    Map<String, String> serviceByComponent = mapServicesByComponent();
+
+    Map<String, Map<String, Map<ProvisionAction, Set<String>>>> result = new 
HashMap<>();
+    for (Component component : request.getRequest().getComponents()) {
+      component.getProvisionAction().ifPresent(componentAction -> {
+        String componentName = component.getName();
+        String serviceName = serviceByComponent.get(componentName);
+        ProvisionAction serviceAction = 
customServiceActions.getOrDefault(serviceName, 
request.getRequest().getProvisionAction());
+        if (!Objects.equals(serviceAction, componentAction)) {
+          result
+            .computeIfAbsent(serviceName, __ -> new HashMap<>())
+            .computeIfAbsent(componentName, __ -> new 
EnumMap<>(ProvisionAction.class))
+            .computeIfAbsent(componentAction, __ -> new HashSet<>())
+            
.addAll(component.getHosts().stream().map(Host::getFqdn).collect(toSet()));
+        }
+      });
+    }
+    return result;
+  }
+
+  /**
+   * Adds {@code item} to the list(s) it belongs to depending on {@code 
action}'s steps.
+   * For example if {@code action} is {@link 
ProvisionAction#INSTALL_AND_START}, then the
+   * {@code item} is added to both {@link ProvisionStep#INSTALL} and {@link 
ProvisionStep#START}
+   * lists, but not to the list for {@link ProvisionStep#SKIP_INSTALL}.
+   *
+   * @param action provision action
+   * @param item the item to add
+   * @param itemsByStep step -> list of items
+   */
+  private static <T> void classifyItem(ProvisionAction action, T item, 
Map<ProvisionStep, List<T>> itemsByStep) {
+    for (ProvisionStep step : action.getSteps()) {
+      itemsByStep.computeIfAbsent(step, __ -> new LinkedList<>()).add(item);
+    }
+  }
+
+  /**
+   * Creates a predicate in the form of:
+   * <pre>component_name=... AND (host_name=... OR host_name=... OR ...)</pre>
+   *
+   * @param componentName component name
+   * @param hosts set of host names
+   */
+  private static Predicate predicateForComponentHosts(String componentName, 
Set<String> hosts) {
+    Preconditions.checkNotNull(hosts);
+    Preconditions.checkArgument(!hosts.isEmpty());
+    Set<Predicate> hostPredicates = 
hosts.stream().map(ProvisionActionPredicateBuilder::hostnameIs).collect(toSet());
+    return 
anyOf(hostPredicates).map(and(componentNameIs(componentName))).get();
+  }
+
+  private static Predicate clusterNameIs(String clusterName) {
+    return new EqualsPredicate<>(CLUSTER_NAME, clusterName);
+  }
+
+  private static Predicate serviceNameIs(String serviceName) {
+    return new EqualsPredicate<>(SERVICE_NAME, serviceName);
+  }
+
+  private static Predicate componentNameIs(String componentName) {
+    return new EqualsPredicate<>(COMPONENT_NAME, componentName);
+  }
+
+  private static Predicate hostnameIs(String hostname) {
+    return new EqualsPredicate<>(HOST_NAME, hostname);
+  }
+}
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/topology/addservice/RequestValidator.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/topology/addservice/RequestValidator.java
index 29ac8c8..59302d1 100644
--- 
a/ambari-server/src/main/java/org/apache/ambari/server/topology/addservice/RequestValidator.java
+++ 
b/ambari-server/src/main/java/org/apache/ambari/server/topology/addservice/RequestValidator.java
@@ -17,11 +17,14 @@
  */
 package org.apache.ambari.server.topology.addservice;
 
+import static java.util.stream.Collectors.toList;
 import static java.util.stream.Collectors.toSet;
 
 import java.util.Collection;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.LinkedHashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
@@ -33,7 +36,6 @@ import javax.inject.Inject;
 import org.apache.ambari.server.AmbariException;
 import org.apache.ambari.server.actionmanager.ActionManager;
 import org.apache.ambari.server.actionmanager.RequestFactory;
-import org.apache.ambari.server.controller.AddServiceRequest;
 import org.apache.ambari.server.controller.AmbariManagementController;
 import org.apache.ambari.server.controller.internal.RequestStageContainer;
 import org.apache.ambari.server.controller.internal.Stack;
@@ -53,7 +55,10 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.HashMultiset;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Multiset;
+import com.google.common.collect.Multisets;
 import com.google.common.collect.Sets;
 import com.google.inject.assistedinject.Assisted;
 
@@ -116,9 +121,10 @@ public class RequestValidator {
     CHECK.checkState(!serviceInfoCreated.getAndSet(true), "Can create only one 
instance for each validated add service request");
 
     RequestStageContainer stages = new 
RequestStageContainer(actionManager.getNextRequestId(), null, requestFactory, 
actionManager);
-    AddServiceInfo validatedRequest = new AddServiceInfo(request, 
cluster.getClusterName(),
-      state.getStack(), state.getConfig(), state.getKerberosDescriptor(),
-      stages, state.getNewServices(), null);
+    AddServiceInfo validatedRequest = new AddServiceInfo(request, 
cluster.getClusterName(), stages,
+      state.getStack(), state.getConfig(), state.getNewServices(), 
state.getKerberosDescriptor(),
+      null
+    );
     stages.setRequestContext(validatedRequest.describe());
     return validatedRequest;
   }
@@ -206,11 +212,12 @@ public class RequestValidator {
   void validateServicesAndComponents() {
     Stack stack = state.getStack();
     Map<String, Map<String, Set<String>>> newServices = new LinkedHashMap<>();
+    Map<String, Map<String, Multiset<String>>> withAllHosts = new 
LinkedHashMap<>();
 
     Set<String> existingServices = cluster.getServices().keySet();
 
     // process service declarations
-    for (AddServiceRequest.Service service : request.getServices()) {
+    for (Service service : request.getServices()) {
       String serviceName = service.getName();
 
       CHECK.checkArgument(stack.getServices().contains(serviceName),
@@ -222,7 +229,7 @@ public class RequestValidator {
     }
 
     // process component declarations
-    for (AddServiceRequest.Component requestedComponent : 
request.getComponents()) {
+    for (Component requestedComponent : request.getComponents()) {
       String componentName = requestedComponent.getName();
       String serviceName = stack.getServiceForComponent(componentName);
 
@@ -231,12 +238,27 @@ public class RequestValidator {
       CHECK.checkArgument(!existingServices.contains(serviceName),
         "Service %s (for component %s) already exists in cluster %s", 
serviceName, componentName, cluster.getClusterName());
 
+      List<String> hosts = 
requestedComponent.getHosts().stream().map(Host::getFqdn).collect(toList());
       newServices.computeIfAbsent(serviceName, __ -> new HashMap<>())
-        .put(componentName, 
requestedComponent.getHosts().stream().map(AddServiceRequest.Host::getFqdn).collect(toSet()));
+        .computeIfAbsent(componentName, __ -> new HashSet<>())
+        .addAll(hosts);
+      withAllHosts.computeIfAbsent(serviceName, __ -> new HashMap<>())
+        .computeIfAbsent(componentName, __ -> HashMultiset.create())
+        .addAll(hosts);
     }
 
     CHECK.checkArgument(!newServices.isEmpty(), "Request should have at least 
one new service or component to be added");
 
+    newServices.forEach(
+      (service, components) -> components.forEach(
+        (component, hosts) -> {
+          Multiset<String> allHosts = withAllHosts.get(service).get(component);
+          Multisets.removeOccurrences(allHosts, hosts);
+          CHECK.checkArgument(allHosts.isEmpty(), "Some hosts appear multiple 
times for the same component (%s) in the request: %s", component, allHosts);
+        }
+      )
+    );
+
     state = state.withNewServices(newServices);
   }
 
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/topology/addservice/RequestValidatorFactory.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/topology/addservice/RequestValidatorFactory.java
index f23a9f8..95a6cb8 100644
--- 
a/ambari-server/src/main/java/org/apache/ambari/server/topology/addservice/RequestValidatorFactory.java
+++ 
b/ambari-server/src/main/java/org/apache/ambari/server/topology/addservice/RequestValidatorFactory.java
@@ -17,7 +17,6 @@
  */
 package org.apache.ambari.server.topology.addservice;
 
-import org.apache.ambari.server.controller.AddServiceRequest;
 import org.apache.ambari.server.state.Cluster;
 
 /**
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/topology/addservice/ResourceProviderAdapter.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/topology/addservice/ResourceProviderAdapter.java
index a33d617..95fcc17 100644
--- 
a/ambari-server/src/main/java/org/apache/ambari/server/topology/addservice/ResourceProviderAdapter.java
+++ 
b/ambari-server/src/main/java/org/apache/ambari/server/topology/addservice/ResourceProviderAdapter.java
@@ -19,11 +19,7 @@ package org.apache.ambari.server.topology.addservice;
 
 import static java.util.stream.Collectors.toList;
 import static java.util.stream.Collectors.toSet;
-import static 
org.apache.ambari.server.controller.AmbariManagementControllerImpl.CLUSTER_PHASE_PROPERTY;
-import static 
org.apache.ambari.server.controller.internal.HostComponentResourceProvider.DO_NOT_SKIP_INSTALL_FOR_ANY_COMPONENTS;
-import static 
org.apache.ambari.server.controller.internal.HostComponentResourceProvider.DO_NOT_SKIP_INSTALL_FOR_COMPONENTS;
-import static 
org.apache.ambari.server.controller.internal.HostComponentResourceProvider.SKIP_INSTALL_FOR_ALL_COMPONENTS;
-import static 
org.apache.ambari.server.controller.internal.HostComponentResourceProvider.SKIP_INSTALL_FOR_COMPONENTS;
+import static 
org.apache.ambari.server.controller.internal.RequestResourceProvider.CONTEXT;
 
 import java.util.List;
 import java.util.Map;
@@ -37,15 +33,12 @@ import javax.inject.Singleton;
 
 import org.apache.ambari.server.AmbariException;
 import org.apache.ambari.server.controller.AmbariManagementController;
-import org.apache.ambari.server.controller.AmbariManagementControllerImpl;
 import org.apache.ambari.server.controller.ClusterRequest;
 import org.apache.ambari.server.controller.ConfigurationRequest;
 import org.apache.ambari.server.controller.internal.ArtifactResourceProvider;
-import org.apache.ambari.server.controller.internal.ClusterResourceProvider;
 import org.apache.ambari.server.controller.internal.ComponentResourceProvider;
 import org.apache.ambari.server.controller.internal.CredentialResourceProvider;
 import 
org.apache.ambari.server.controller.internal.HostComponentResourceProvider;
-import org.apache.ambari.server.controller.internal.ProvisionAction;
 import org.apache.ambari.server.controller.internal.RequestImpl;
 import org.apache.ambari.server.controller.internal.RequestOperationLevel;
 import org.apache.ambari.server.controller.internal.ServiceResourceProvider;
@@ -64,13 +57,13 @@ import 
org.apache.ambari.server.controller.spi.SystemException;
 import org.apache.ambari.server.controller.spi.UnsupportedPropertyException;
 import org.apache.ambari.server.controller.utilities.ClusterControllerHelper;
 import org.apache.ambari.server.controller.utilities.PredicateBuilder;
-import org.apache.ambari.server.controller.utilities.PropertyHelper;
 import org.apache.ambari.server.security.authorization.AuthorizationException;
 import org.apache.ambari.server.state.ConfigHelper;
 import org.apache.ambari.server.state.State;
 import org.apache.ambari.server.state.kerberos.KerberosDescriptor;
 import org.apache.ambari.server.state.kerberos.KerberosDescriptorFactory;
 import org.apache.ambari.server.topology.Credential;
+import org.apache.ambari.server.topology.ProvisionStep;
 import org.apache.ambari.server.utils.LoggingPreconditions;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -197,26 +190,30 @@ public class ResourceProviderAdapter {
     Set<Map<String, Object>> properties = ImmutableSet.of(ImmutableMap.of(
       ServiceResourceProvider.SERVICE_SERVICE_STATE_PROPERTY_ID, 
desiredState.name()
     ));
-    Map<String, String> requestInfo = createRequestInfo(request.clusterName(), 
Resource.Type.Service).build();
-    Predicate predicate = predicateForNewServices(request, "ServiceInfo");
+    Map<String, String> requestInfo = 
RequestOperationLevel.propertiesFor(Resource.Type.Service, 
request.clusterName());
+    Predicate predicate = predicateForNewServices(request);
     updateResources(request, properties, Resource.Type.Service, predicate, 
requestInfo);
   }
 
-  public void updateHostComponentDesiredState(AddServiceInfo request, State 
desiredState) {
-    LOG.info("Updating host component desired state to {} for {}", 
desiredState, request);
+  public void updateHostComponentDesiredState(AddServiceInfo request, 
Predicate predicate, ProvisionStep step) {
+    State desiredState = step.getDesiredStateToSet();
+    LOG.info("Updating host component desired state to {} per {} for {}", 
desiredState, step, request);
+    LOG.debug("Using predicate {}", predicate);
 
     Set<Map<String, Object>> properties = ImmutableSet.of(ImmutableMap.of(
       HostComponentResourceProvider.STATE, desiredState.name(),
-      "context", String.format("Put new components to %s state", desiredState)
+      CONTEXT, String.format("Put new components to %s state", desiredState)
     ));
 
-    ImmutableMap.Builder<String, String> requestInfo = 
createRequestInfo(request.clusterName(), Resource.Type.HostComponent);
-    addProvisionProperties(requestInfo, desiredState, 
request.getRequest().getProvisionAction());
+    Map<String, String> requestInfo = new ImmutableMap.Builder<String, 
String>()
+      .putAll(RequestOperationLevel.propertiesFor(Resource.Type.HostComponent, 
request.clusterName()))
+      .putAll(step.getProvisionProperties())
+      .build();
 
     HostComponentResourceProvider rp = (HostComponentResourceProvider) 
getClusterController().ensureResourceProvider(Resource.Type.HostComponent);
-    Request internalRequest = createRequest(properties, requestInfo.build(), 
null);
+    Request internalRequest = createRequest(properties, requestInfo, null);
     try {
-      rp.doUpdateResources(request.getStages(), internalRequest, 
predicateForNewServices(request, HostComponentResourceProvider.HOST_ROLES), 
false, false, false);
+      rp.doUpdateResources(request.getStages(), internalRequest, predicate, 
false, false, false);
     } catch (UnsupportedPropertyException | SystemException | 
NoSuchParentResourceException | NoSuchResourceException e) {
       CHECK.wrapInUnchecked(e, RuntimeException::new, "Error updating host 
component desired state for %s", request);
     }
@@ -272,20 +269,6 @@ public class ResourceProviderAdapter {
     return new RequestImpl(propertyIds, properties, requestInfoProperties, 
null);
   }
 
-  private static ImmutableMap.Builder<String, String> createRequestInfo(String 
clusterName, Resource.Type resourceType) {
-    return new ImmutableMap.Builder<String, String>()
-      .put(RequestOperationLevel.OPERATION_LEVEL_ID, 
RequestOperationLevel.getExternalLevelName(resourceType.name()))
-      .put(RequestOperationLevel.OPERATION_CLUSTER_ID, clusterName);
-  }
-
-  private static void addProvisionProperties(ImmutableMap.Builder<String, 
String> requestInfo, State desiredState, ProvisionAction requestAction) {
-    if (desiredState == State.INSTALLED && requestAction.skipInstall()) {
-      requestInfo.put(SKIP_INSTALL_FOR_COMPONENTS, 
SKIP_INSTALL_FOR_ALL_COMPONENTS);
-      requestInfo.put(DO_NOT_SKIP_INSTALL_FOR_COMPONENTS, 
DO_NOT_SKIP_INSTALL_FOR_ANY_COMPONENTS);
-      requestInfo.put(CLUSTER_PHASE_PROPERTY, 
AmbariManagementControllerImpl.CLUSTER_PHASE_INITIAL_INSTALL);
-    }
-  }
-
   public static Map<String, String> 
requestInfoForKerberosDescriptor(KerberosDescriptor descriptor) {
     return ImmutableMap.of(Request.REQUEST_INFO_BODY_PROPERTY, 
ArtifactResourceProvider.toArtifactDataJson(descriptor.toMap()));
   }
@@ -440,13 +423,13 @@ public class ResourceProviderAdapter {
       .end().toPredicate();
   }
 
-  private static Predicate predicateForNewServices(AddServiceInfo request, 
String category) {
+  private static Predicate predicateForNewServices(AddServiceInfo request) {
     return new AndPredicate(
-      new EqualsPredicate<>(PropertyHelper.getPropertyId(category, 
ClusterResourceProvider.CLUSTER_NAME), request.clusterName()),
-      new OrPredicate(
+      new 
EqualsPredicate<>(ServiceResourceProvider.SERVICE_CLUSTER_NAME_PROPERTY_ID, 
request.clusterName()),
+      OrPredicate.of(
         request.newServices().keySet().stream()
-          .map(service -> new 
EqualsPredicate<>(PropertyHelper.getPropertyId(category, "service_name"), 
service))
-          .toArray(Predicate[]::new)
+          .map(service -> new 
EqualsPredicate<>(ServiceResourceProvider.SERVICE_SERVICE_NAME_PROPERTY_ID, 
service))
+          .collect(toList())
       )
     );
   }
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/topology/addservice/Service.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/topology/addservice/Service.java
new file mode 100644
index 0000000..e31b3d8
--- /dev/null
+++ 
b/ambari-server/src/main/java/org/apache/ambari/server/topology/addservice/Service.java
@@ -0,0 +1,96 @@
+/*
+ * 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.topology.addservice;
+
+import static 
org.apache.ambari.server.controller.internal.BaseClusterRequest.PROVISION_ACTION_PROPERTY;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import org.apache.ambari.annotations.ApiIgnore;
+import org.apache.ambari.server.controller.internal.ProvisionAction;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+
+@ApiModel
+public final class Service {
+
+  private static final String NAME = "name";
+
+  private final String name;
+  private final ProvisionAction provisionAction;
+
+  @JsonCreator
+  public Service(
+    @JsonProperty(NAME) String name,
+    @JsonProperty(PROVISION_ACTION_PROPERTY) ProvisionAction provisionAction
+  ) {
+    this.name = name;
+    this.provisionAction = provisionAction;
+  }
+
+  public static Service of(String name) {
+    return of(name, null);
+  }
+
+  public static Service of(String name, ProvisionAction provisionAction) {
+    return new Service(name, provisionAction);
+  }
+
+  @JsonProperty(NAME)
+  @ApiModelProperty(name = NAME)
+  public String getName() {
+    return name;
+  }
+
+  @JsonProperty(PROVISION_ACTION_PROPERTY)
+  @ApiModelProperty(name = PROVISION_ACTION_PROPERTY)
+  public ProvisionAction _getProvisionAction() {
+    return provisionAction;
+  }
+
+  @ApiIgnore
+  @JsonIgnore
+  public Optional<ProvisionAction> getProvisionAction() {
+    return Optional.ofNullable(provisionAction);
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    Service service = (Service) o;
+    return Objects.equals(name, service.name) &&
+      Objects.equals(provisionAction, service.provisionAction);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(name, provisionAction);
+  }
+
+  @Override
+  public String toString() {
+    return name;
+  }
+}
diff --git 
a/ambari-server/src/test/java/org/apache/ambari/server/controller/AddServiceRequestTest.java
 
b/ambari-server/src/test/java/org/apache/ambari/server/topology/addservice/AddServiceRequestTest.java
similarity index 92%
rename from 
ambari-server/src/test/java/org/apache/ambari/server/controller/AddServiceRequestTest.java
rename to 
ambari-server/src/test/java/org/apache/ambari/server/topology/addservice/AddServiceRequestTest.java
index 4b90374..9f0e331 100644
--- 
a/ambari-server/src/test/java/org/apache/ambari/server/controller/AddServiceRequestTest.java
+++ 
b/ambari-server/src/test/java/org/apache/ambari/server/topology/addservice/AddServiceRequestTest.java
@@ -16,16 +16,15 @@
  * limitations under the License.
  */
 
-package org.apache.ambari.server.controller;
+package org.apache.ambari.server.topology.addservice;
 
-import static org.apache.ambari.server.controller.AddServiceRequest.Component;
-import static 
org.apache.ambari.server.controller.AddServiceRequest.OperationType.ADD_SERVICE;
-import static org.apache.ambari.server.controller.AddServiceRequest.Service;
-import static 
org.apache.ambari.server.controller.AddServiceRequest.ValidationType.PERMISSIVE;
-import static 
org.apache.ambari.server.controller.AddServiceRequest.ValidationType.STRICT;
 import static 
org.apache.ambari.server.controller.internal.ProvisionAction.INSTALL_AND_START;
 import static 
org.apache.ambari.server.controller.internal.ProvisionAction.INSTALL_ONLY;
+import static 
org.apache.ambari.server.controller.internal.ProvisionAction.START_ONLY;
 import static 
org.apache.ambari.server.topology.ConfigRecommendationStrategy.ALWAYS_APPLY;
+import static 
org.apache.ambari.server.topology.addservice.AddServiceRequest.OperationType.ADD_SERVICE;
+import static 
org.apache.ambari.server.topology.addservice.AddServiceRequest.ValidationType.PERMISSIVE;
+import static 
org.apache.ambari.server.topology.addservice.AddServiceRequest.ValidationType.STRICT;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
@@ -101,11 +100,14 @@ public class AddServiceRequestTest {
       configuration.getProperties());
 
     assertEquals(
-      ImmutableSet.of(Component.of("NIMBUS", "c7401.ambari.apache.org", 
"c7402.ambari.apache.org"), Component.of("BEACON_SERVER", 
"c7402.ambari.apache.org", "c7403.ambari.apache.org")),
+      ImmutableSet.of(
+        Component.of("NIMBUS", START_ONLY, "c7401.ambari.apache.org", 
"c7402.ambari.apache.org"),
+        Component.of("BEACON_SERVER", "c7402.ambari.apache.org", 
"c7403.ambari.apache.org")
+      ),
       request.getComponents());
 
     assertEquals(
-      ImmutableSet.of(Service.of("STORM"), Service.of("BEACON")),
+      ImmutableSet.of(Service.of("STORM", INSTALL_AND_START), 
Service.of("BEACON")),
       request.getServices());
 
     assertEquals(
diff --git 
a/ambari-server/src/test/java/org/apache/ambari/server/topology/addservice/ProvisionActionPredicateBuilderTest.java
 
b/ambari-server/src/test/java/org/apache/ambari/server/topology/addservice/ProvisionActionPredicateBuilderTest.java
new file mode 100644
index 0000000..9a3bab6
--- /dev/null
+++ 
b/ambari-server/src/test/java/org/apache/ambari/server/topology/addservice/ProvisionActionPredicateBuilderTest.java
@@ -0,0 +1,202 @@
+/*
+ * 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.topology.addservice;
+
+import static java.util.stream.Collectors.toSet;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+import 
org.apache.ambari.server.controller.internal.HostComponentResourceProvider;
+import org.apache.ambari.server.controller.internal.ProvisionAction;
+import org.apache.ambari.server.controller.internal.RequestStageContainer;
+import org.apache.ambari.server.controller.internal.ResourceImpl;
+import org.apache.ambari.server.controller.spi.Predicate;
+import org.apache.ambari.server.controller.spi.Resource;
+import org.apache.ambari.server.topology.ProvisionStep;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+
+public class ProvisionActionPredicateBuilderTest {
+
+  private static final String CLUSTER_NAME = "TEST";
+  private static final Map<String, Map<String, Set<String>>> NEW_SERVICES = 
ImmutableMap.of(
+    "AMBARI_METRICS", ImmutableMap.of(
+      "METRICS_COLLECTOR", ImmutableSet.of("c7401"),
+      "METRICS_GRAFANA", ImmutableSet.of("c7403"),
+      "METRICS_MONITOR", ImmutableSet.of("c7401", "c7402", "c7403", "c7404", 
"c7405")
+    ),
+    "KAFKA", ImmutableMap.of(
+      "KAFKA_BROKER", ImmutableSet.of("c7402", "c7404")
+    ),
+    "ZOOKEEPER", ImmutableMap.of(
+      "ZOOKEEPER_SERVER", ImmutableSet.of("c7401", "c7402", "c7403"),
+      "ZOOKEEPER_CLIENT", ImmutableSet.of("c7404")
+    )
+  );
+  private static final RequestStageContainer STAGES = new 
RequestStageContainer(42L, null, null, null, null);
+
+  @Test
+  public void noCustomProvisionAction() {
+    AddServiceRequest request = createRequest(null, null, null);
+    AddServiceInfo info = new AddServiceInfo(request, CLUSTER_NAME, STAGES, 
null, null, NEW_SERVICES);
+    ProvisionActionPredicateBuilder builder = new 
ProvisionActionPredicateBuilder(info);
+
+    assertTrue(builder.getPredicate(ProvisionStep.INSTALL).isPresent());
+    assertFalse(builder.getPredicate(ProvisionStep.SKIP_INSTALL).isPresent());
+    assertTrue(builder.getPredicate(ProvisionStep.START).isPresent());
+
+    Predicate installPredicate = 
builder.getPredicate(ProvisionStep.INSTALL).get();
+    Predicate startPredicate = builder.getPredicate(ProvisionStep.START).get();
+
+    Set<Resource> allNewHostComponents = allHostComponents(NEW_SERVICES);
+    assertMatchesAll(installPredicate, allNewHostComponents);
+    assertMatchesAll(startPredicate, allNewHostComponents);
+
+    assertNoMatchForExistingComponents(installPredicate, startPredicate);
+  }
+
+  @Test
+  public void requestLevelStartOnly() {
+    AddServiceRequest request = createRequest(ProvisionAction.START_ONLY, 
null, null);
+    AddServiceInfo info = new AddServiceInfo(request, CLUSTER_NAME, STAGES, 
null, null, NEW_SERVICES);
+    ProvisionActionPredicateBuilder builder = new 
ProvisionActionPredicateBuilder(info);
+
+    assertEquals(Optional.empty(), 
builder.getPredicate(ProvisionStep.INSTALL));
+    assertTrue(builder.getPredicate(ProvisionStep.SKIP_INSTALL).isPresent());
+    assertTrue(builder.getPredicate(ProvisionStep.START).isPresent());
+
+    Predicate skipInstallPredicate = 
builder.getPredicate(ProvisionStep.SKIP_INSTALL).get();
+    Predicate startPredicate = builder.getPredicate(ProvisionStep.START).get();
+
+    Set<Resource> allNewHostComponents = allHostComponents(NEW_SERVICES);
+    assertMatchesAll(skipInstallPredicate, allNewHostComponents);
+    assertMatchesAll(startPredicate, allNewHostComponents);
+    assertNoMatchForExistingComponents(skipInstallPredicate, startPredicate);
+  }
+
+  @Test
+  public void customAtAllLevels() {
+    AddServiceRequest request = createRequest(ProvisionAction.START_ONLY,
+      ImmutableSet.of(
+        Service.of("AMBARI_METRICS", ProvisionAction.INSTALL_AND_START),
+        Service.of("KAFKA"),
+        Service.of("ZOOKEEPER", ProvisionAction.INSTALL_ONLY)
+      ),
+      ImmutableSet.of(
+        Component.of("KAFKA_BROKER", ProvisionAction.INSTALL_AND_START, 
"c7404"), // overrides request-level
+        // KAFKA_BROKER on c7402 added by layout recommendation inherits 
request-level
+        Component.of("METRICS_GRAFANA", ProvisionAction.START_ONLY, "c7403"), 
// overrides service-level
+        Component.of("METRICS_MONITOR", "c7401"), // inherit from service
+        Component.of("METRICS_MONITOR", ProvisionAction.INSTALL_AND_START, 
"c7402"), // matches service-level
+        // METRICS_MONITOR on c7403 added by layout recommendation, inherits 
service-level
+        Component.of("METRICS_MONITOR", ProvisionAction.INSTALL_ONLY, "c7404", 
"c7405") // overrides service-level
+      )
+    );
+    AddServiceInfo info = new AddServiceInfo(request, CLUSTER_NAME, STAGES, 
null, null, NEW_SERVICES);
+    ProvisionActionPredicateBuilder builder = new 
ProvisionActionPredicateBuilder(info);
+
+    assertTrue(builder.getPredicate(ProvisionStep.INSTALL).isPresent());
+    assertTrue(builder.getPredicate(ProvisionStep.SKIP_INSTALL).isPresent());
+    assertTrue(builder.getPredicate(ProvisionStep.START).isPresent());
+
+    Predicate installPredicate = 
builder.getPredicate(ProvisionStep.INSTALL).get();
+    Predicate skipInstallPredicate = 
builder.getPredicate(ProvisionStep.SKIP_INSTALL).get();
+    Predicate startPredicate = builder.getPredicate(ProvisionStep.START).get();
+
+    Map<String, Map<String, Set<String>>> installComponents = ImmutableMap.of(
+      "AMBARI_METRICS", ImmutableMap.of(
+        "METRICS_COLLECTOR", ImmutableSet.of("c7401"),
+        "METRICS_MONITOR", ImmutableSet.of("c7401", "c7402", "c7404", "c7405")
+      ),
+      "KAFKA", ImmutableMap.of(
+        "KAFKA_BROKER", ImmutableSet.of("c7404")
+      ),
+      "ZOOKEEPER", ImmutableMap.of(
+        "ZOOKEEPER_SERVER", ImmutableSet.of("c7401", "c7402", "c7403"),
+        "ZOOKEEPER_CLIENT", ImmutableSet.of("c7404")
+      )
+    );
+    Map<String, Map<String, Set<String>>> skipInstallComponents = 
ImmutableMap.of(
+      "AMBARI_METRICS", ImmutableMap.of(
+        "METRICS_GRAFANA", ImmutableSet.of("c7403")
+      ),
+      "KAFKA", ImmutableMap.of(
+        "KAFKA_BROKER", ImmutableSet.of("c7402")
+      )
+    );
+    Map<String, Map<String, Set<String>>> startComponents = ImmutableMap.of(
+      "AMBARI_METRICS", ImmutableMap.of(
+        "METRICS_COLLECTOR", ImmutableSet.of("c7401"),
+        "METRICS_GRAFANA", ImmutableSet.of("c7403"),
+        "METRICS_MONITOR", ImmutableSet.of("c7401", "c7402", "c7403")
+      ),
+      "KAFKA", ImmutableMap.of(
+        "KAFKA_BROKER", ImmutableSet.of("c7402", "c7404")
+      )
+    );
+    assertMatchesAll(installPredicate, allHostComponents(installComponents));
+    assertMatchesAll(skipInstallPredicate, 
allHostComponents(skipInstallComponents));
+    assertMatchesAll(startPredicate, allHostComponents(startComponents));
+    assertNoMatchForExistingComponents(skipInstallPredicate, startPredicate);
+  }
+
+  private static void assertNoMatchForExistingComponents(Predicate... 
predicates) {
+    Resource existingComponent = existingComponent();
+    for (Predicate predicate : predicates) {
+      assertFalse(predicate.evaluate(existingComponent));
+    }
+  }
+
+  private static Resource existingComponent() {
+    return hostComponent("HDFS", "NAMENODE", "c7401");
+  }
+
+  private static void assertMatchesAll(Predicate predicates, Collection<? 
extends Resource> resources) {
+    assertEquals(ImmutableSet.of(), resources.stream().filter(each -> 
!predicates.evaluate(each)).collect(toSet()));
+  }
+
+  private static AddServiceRequest createRequest(ProvisionAction 
provisionAction, Set<Service> services, Set<Component> components) {
+    return new AddServiceRequest(AddServiceRequest.OperationType.ADD_SERVICE, 
null, provisionAction, AddServiceRequest.ValidationType.DEFAULT, "HDP", "3.0", 
services, components, null, null, null);
+  }
+
+  private static Set<Resource> allHostComponents(Map<String, Map<String, 
Set<String>>> services) {
+    return services.entrySet().stream()
+      .flatMap(componentsOfService -> 
componentsOfService.getValue().entrySet().stream()
+        .flatMap(hostsOfComponent -> hostsOfComponent.getValue().stream()
+          .map(host -> hostComponent(componentsOfService.getKey(), 
hostsOfComponent.getKey(), host))))
+      .collect(toSet());
+  }
+
+  private static Resource hostComponent(String service, String component, 
String hostname) {
+    Resource hostComponent = new ResourceImpl(Resource.Type.HostComponent);
+    hostComponent.setProperty(HostComponentResourceProvider.CLUSTER_NAME, 
CLUSTER_NAME);
+    hostComponent.setProperty(HostComponentResourceProvider.SERVICE_NAME, 
service);
+    hostComponent.setProperty(HostComponentResourceProvider.COMPONENT_NAME, 
component);
+    hostComponent.setProperty(HostComponentResourceProvider.HOST_NAME, 
hostname);
+    return hostComponent;
+  }
+
+}
diff --git 
a/ambari-server/src/test/java/org/apache/ambari/server/topology/addservice/RequestValidatorTest.java
 
b/ambari-server/src/test/java/org/apache/ambari/server/topology/addservice/RequestValidatorTest.java
index 13fc281..96a0afa 100644
--- 
a/ambari-server/src/test/java/org/apache/ambari/server/topology/addservice/RequestValidatorTest.java
+++ 
b/ambari-server/src/test/java/org/apache/ambari/server/topology/addservice/RequestValidatorTest.java
@@ -23,6 +23,7 @@ import static java.util.stream.Collectors.toSet;
 import static org.apache.ambari.server.utils.Assertions.assertThrows;
 import static org.easymock.EasyMock.expect;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertSame;
@@ -33,12 +34,13 @@ import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
 import java.util.function.Function;
+import java.util.stream.Stream;
 
 import org.apache.ambari.server.AmbariException;
 import org.apache.ambari.server.actionmanager.ActionManager;
 import org.apache.ambari.server.actionmanager.RequestFactory;
-import org.apache.ambari.server.controller.AddServiceRequest;
 import org.apache.ambari.server.controller.AmbariManagementController;
+import org.apache.ambari.server.controller.internal.ProvisionAction;
 import org.apache.ambari.server.controller.internal.Stack;
 import org.apache.ambari.server.state.Cluster;
 import org.apache.ambari.server.state.ConfigHelper;
@@ -228,6 +230,43 @@ public class RequestValidatorTest extends EasyMockSupport {
   }
 
   @Test
+  public void handlesMultipleComponentInstances() {
+    expect(request.getComponents()).andReturn(
+      Stream.of("c7401", "c7402")
+        .map(hostname -> Component.of("KAFKA_BROKER", hostname))
+        .collect(toSet()));
+    validator.setState(RequestValidator.State.INITIAL.with(simpleMockStack()));
+    replayAll();
+
+    validator.validateServicesAndComponents();
+
+    Map<String, Map<String, Set<String>>> expectedNewServices = 
ImmutableMap.of(
+      "KAFKA", ImmutableMap.of("KAFKA_BROKER", ImmutableSet.of("c7401", 
"c7402"))
+    );
+    assertEquals(expectedNewServices, validator.getState().getNewServices());
+  }
+
+  @Test
+  public void rejectsMultipleOccurrencesOfSameHostForSameComponent() {
+    Set<String> duplicateHosts = ImmutableSet.of("c7402", "c7403");
+    Set<String> uniqueHosts = ImmutableSet.of("c7401", "c7404");
+    expect(request.getComponents()).andReturn(
+      ImmutableSet.of(
+        Component.of("KAFKA_BROKER", ProvisionAction.INSTALL_AND_START, 
"c7401", "c7402", "c7403"),
+        Component.of("KAFKA_BROKER", ProvisionAction.INSTALL_ONLY, "c7402", 
"c7403", "c7404")
+      )
+    );
+    validator.setState(RequestValidator.State.INITIAL.with(simpleMockStack()));
+    replayAll();
+
+    IllegalArgumentException e = assertThrows(IllegalArgumentException.class, 
validator::validateServicesAndComponents);
+    assertTrue(e.getMessage().contains("hosts appear multiple"));
+    duplicateHosts.forEach(host -> assertTrue(e.getMessage().contains(host)));
+    uniqueHosts.forEach(host -> assertFalse(e.getMessage().contains(host)));
+    assertNull(validator.getState().getNewServices());
+  }
+
+  @Test
   public void rejectsUnknownService() {
     String serviceName = "UNKNOWN_SERVICE";
     requestServices(false, serviceName);
@@ -525,7 +564,7 @@ public class RequestValidatorTest extends EasyMockSupport {
     Exception e = assertThrows(IllegalArgumentException.class, validation);
     if (expectedMessage != null) {
       assertTrue(e.getMessage().contains(expectedMessage));
-    };
+    }
 
     resetAll();
     setUp();
@@ -585,7 +624,7 @@ public class RequestValidatorTest extends EasyMockSupport {
   private void requestServices(boolean validated, String... services) {
     expect(request.getServices()).andReturn(
       Arrays.stream(services)
-        .map(AddServiceRequest.Service::of)
+        .map(org.apache.ambari.server.topology.addservice.Service::of)
         .collect(toSet())
     ).anyTimes();
     if (validated) {
@@ -607,7 +646,7 @@ public class RequestValidatorTest extends EasyMockSupport {
   private void requestComponents(String... components) {
     expect(request.getComponents()).andReturn(
       Arrays.stream(components)
-        .map(componentName -> AddServiceRequest.Component.of(componentName, 
"c7401.ambari.apache.org"))
+        .map(componentName -> Component.of(componentName, 
"c7401.ambari.apache.org"))
         .collect(toSet())
     );
   }
diff --git 
a/ambari-server/src/test/java/org/apache/ambari/server/topology/addservice/StackAdvisorAdapterTest.java
 
b/ambari-server/src/test/java/org/apache/ambari/server/topology/addservice/StackAdvisorAdapterTest.java
index 78f481e..5219acb 100644
--- 
a/ambari-server/src/test/java/org/apache/ambari/server/topology/addservice/StackAdvisorAdapterTest.java
+++ 
b/ambari-server/src/test/java/org/apache/ambari/server/topology/addservice/StackAdvisorAdapterTest.java
@@ -42,7 +42,6 @@ import 
org.apache.ambari.server.api.services.stackadvisor.StackAdvisorHelper;
 import org.apache.ambari.server.api.services.stackadvisor.StackAdvisorRequest;
 import 
org.apache.ambari.server.api.services.stackadvisor.recommendations.RecommendationResponse;
 import 
org.apache.ambari.server.api.services.stackadvisor.validations.ValidationResponse;
-import org.apache.ambari.server.controller.AddServiceRequest;
 import org.apache.ambari.server.controller.AmbariManagementController;
 import org.apache.ambari.server.controller.internal.Stack;
 import org.apache.ambari.server.state.Cluster;
@@ -178,8 +177,8 @@ public class StackAdvisorAdapterTest {
         "OOZIE_SERVER", ImmutableSet.of("c7401"),
         "OOZIE_CLIENT", ImmutableSet.of("c7403", "c7404")));
 
-    AddServiceInfo info = new 
AddServiceInfo(request(ConfigRecommendationStrategy.ALWAYS_APPLY), "c1", stack 
, Configuration.newEmpty(),
-      null, null, newServices, null); // No LayoutReommendationInfo -> needs 
to be calculated
+    AddServiceRequest request = 
request(ConfigRecommendationStrategy.ALWAYS_APPLY);
+    AddServiceInfo info = new AddServiceInfo(request, "c1", null, stack, 
Configuration.newEmpty(), newServices); // No LayoutReommendationInfo -> needs 
to be calculated
 
     LayoutRecommendationInfo layoutRecommendationInfo = 
adapter.getLayoutRecommendationInfo(info);
     layoutRecommendationInfo.getAllServiceLayouts();
@@ -361,7 +360,7 @@ public class StackAdvisorAdapterTest {
       "KAFKA",
       ImmutableMap.of("KAFKA_BROKER", emptySet()));
 
-    AddServiceInfo info = new AddServiceInfo(null, "c1", stack, 
org.apache.ambari.server.topology.Configuration.newEmpty(), null, null, 
newServices, null);
+    AddServiceInfo info = new AddServiceInfo(null, "c1", null, stack, 
org.apache.ambari.server.topology.Configuration.newEmpty(), newServices);
     AddServiceInfo infoWithRecommendations = adapter.recommendLayout(info);
 
     Map<String, Map<String, Set<String>>> expectedNewLayout = ImmutableMap.of(
@@ -392,8 +391,8 @@ public class StackAdvisorAdapterTest {
     userConfig.setParentConfiguration(clusterConfig);
     clusterConfig.setParentConfiguration(stackConfig);
 
-    AddServiceInfo info = new 
AddServiceInfo(request(ConfigRecommendationStrategy.ALWAYS_APPLY), "c1", stack 
, userConfig,
-      null, null, newServices, null); // No LayoutRecommendationInfo
+    AddServiceRequest request = 
request(ConfigRecommendationStrategy.ALWAYS_APPLY);
+    AddServiceInfo info = new AddServiceInfo(request, "c1", null, stack, 
userConfig, newServices); // No LayoutRecommendationInfo
     AddServiceInfo infoWithConfig = adapter.recommendConfigurations(info);
 
     Configuration recommendedConfig = infoWithConfig.getConfig();
@@ -442,8 +441,9 @@ public class StackAdvisorAdapterTest {
     clusterConfig.setParentConfiguration(stackConfig);
 
     LayoutRecommendationInfo layoutRecommendationInfo = new 
LayoutRecommendationInfo(new HashMap<>(), new HashMap<>()); // contents doesn't 
matter for the test
-    AddServiceInfo info = new 
AddServiceInfo(request(ConfigRecommendationStrategy.ALWAYS_APPLY), "c1", stack 
, userConfig,
-      null, null, newServices, layoutRecommendationInfo);
+    AddServiceRequest request = 
request(ConfigRecommendationStrategy.ALWAYS_APPLY);
+    AddServiceInfo info = new AddServiceInfo(request, "c1", null, stack, 
userConfig, newServices)
+      .withLayoutRecommendation(newServices, layoutRecommendationInfo);
     AddServiceInfo infoWithConfig = adapter.recommendConfigurations(info);
 
     Configuration recommendedConfig = infoWithConfig.getConfig();
@@ -492,8 +492,9 @@ public class StackAdvisorAdapterTest {
     clusterConfig.setParentConfiguration(stackConfig);
 
     LayoutRecommendationInfo layoutRecommendationInfo = new 
LayoutRecommendationInfo(new HashMap<>(), new HashMap<>()); // contents doesn't 
matter for the test
-    AddServiceInfo info = new 
AddServiceInfo(request(ConfigRecommendationStrategy.ALWAYS_APPLY_DONT_OVERRIDE_CUSTOM_VALUES),
 "c1", stack , userConfig,
-      null, null, newServices, layoutRecommendationInfo);
+    AddServiceRequest request = 
request(ConfigRecommendationStrategy.ALWAYS_APPLY_DONT_OVERRIDE_CUSTOM_VALUES);
+    AddServiceInfo info = new AddServiceInfo(request, "c1", null, stack, 
userConfig, newServices)
+      .withLayoutRecommendation(newServices, layoutRecommendationInfo);
     AddServiceInfo infoWithConfig = adapter.recommendConfigurations(info);
 
     assertSame(userConfig, infoWithConfig.getConfig()); // user config stays 
top priority
@@ -547,8 +548,9 @@ public class StackAdvisorAdapterTest {
     clusterConfig.setParentConfiguration(stackConfig);
 
     LayoutRecommendationInfo layoutRecommendationInfo = new 
LayoutRecommendationInfo(new HashMap<>(), new HashMap<>()); // contents doesn't 
matter for the test
-    AddServiceInfo info = new 
AddServiceInfo(request(ConfigRecommendationStrategy.NEVER_APPLY), "c1", stack , 
userConfig,
-      null, null, newServices, layoutRecommendationInfo);
+    AddServiceRequest request = 
request(ConfigRecommendationStrategy.NEVER_APPLY);
+    AddServiceInfo info = new AddServiceInfo(request, "c1", null, stack, 
userConfig, newServices)
+      .withLayoutRecommendation(newServices, layoutRecommendationInfo);
     AddServiceInfo infoWithConfig = adapter.recommendConfigurations(info);
 
     // No recommended config, no stack config
@@ -588,8 +590,9 @@ public class StackAdvisorAdapterTest {
     clusterConfig.setParentConfiguration(stackConfig);
 
     LayoutRecommendationInfo layoutRecommendationInfo = new 
LayoutRecommendationInfo(new HashMap<>(), new HashMap<>()); // contents doesn't 
matter for the test
-    AddServiceInfo info = new 
AddServiceInfo(request(ConfigRecommendationStrategy.ONLY_STACK_DEFAULTS_APPLY), 
"c1", stack , userConfig,
-      null, null, newServices, layoutRecommendationInfo);
+    AddServiceRequest request = 
request(ConfigRecommendationStrategy.ONLY_STACK_DEFAULTS_APPLY);
+    AddServiceInfo info = new AddServiceInfo(request, "c1", null, stack, 
userConfig, newServices)
+      .withLayoutRecommendation(newServices, layoutRecommendationInfo);
     AddServiceInfo infoWithConfig = adapter.recommendConfigurations(info);
     Configuration recommendedConfig = 
infoWithConfig.getConfig().getParentConfiguration();
 
@@ -656,7 +659,7 @@ public class StackAdvisorAdapterTest {
     assertEquals(recommendedConfigsForStackDefaults, recommendedConfigs);
   }
 
-  private AddServiceRequest request(ConfigRecommendationStrategy strategy) {
+  private static AddServiceRequest request(ConfigRecommendationStrategy 
strategy) {
     return new AddServiceRequest(null, strategy, null, null, null, null, null, 
null, null, null, null);
   }
 
diff --git a/ambari-server/src/test/resources/add_service_api/request1.json 
b/ambari-server/src/test/resources/add_service_api/request1.json
index 75fb12f..b4e56e2 100644
--- a/ambari-server/src/test/resources/add_service_api/request1.json
+++ b/ambari-server/src/test/resources/add_service_api/request1.json
@@ -7,13 +7,14 @@
   "stack_version" : "3.0",
 
   "services": [
-    { "name" : "STORM" },
+    { "name" : "STORM", "provision_action": "INSTALL_AND_START" },
     { "name" : "BEACON" }
   ],
 
   "components" : [
     {
       "name" : "NIMBUS",
+      "provision_action" : "START_ONLY",
       "hosts": [
         { "fqdn" : "c7401.ambari.apache.org" },
         { "fqdn" : "c7402.ambari.apache.org" }

Reply via email to