You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ambari.apache.org by yu...@apache.org on 2014/08/30 01:41:00 UTC

git commit: AMBARI-6125. Ambari Groovy client enhancements. (Janos Matyas and Krisztian Horvath via yusaku)

Repository: ambari
Updated Branches:
  refs/heads/trunk 1af0a4417 -> 733f0345c


AMBARI-6125. Ambari Groovy client enhancements. (Janos Matyas and Krisztian Horvath via yusaku)


Project: http://git-wip-us.apache.org/repos/asf/ambari/repo
Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/733f0345
Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/733f0345
Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/733f0345

Branch: refs/heads/trunk
Commit: 733f0345c5b726a5a4164214e740102ba8d7a8d1
Parents: 1af0a44
Author: Yusaku Sako <yu...@hortonworks.com>
Authored: Fri Aug 29 16:39:44 2014 -0700
Committer: Yusaku Sako <yu...@hortonworks.com>
Committed: Fri Aug 29 16:39:44 2014 -0700

----------------------------------------------------------------------
 ambari-client/groovy-client/pom.xml             |   4 +
 .../ambari/groovy/client/AmbariClient.groovy    | 474 +++++++++++++++++--
 .../client/InvalidBlueprintException.groovy     |  28 ++
 .../InvalidHostGroupHostAssociation.groovy      |  39 ++
 .../resources/blueprints/hdp-multinode-default  | 182 +++++++
 .../resources/blueprints/hdp-singlenode-default | 133 ++++++
 .../resources/blueprints/lambda-architecture    |   8 +-
 .../resources/blueprints/multi-node-hdfs-yarn   |   4 +-
 .../resources/blueprints/single-node-hdfs-yarn  |   2 +-
 .../groovy/client/AmbariBlueprintsTest.groovy   |  95 ++++
 .../ambari/groovy/client/AmbariHostsTest.groovy |  34 +-
 .../groovy/client/AmbariRecommendTest.groovy    | 150 ++++++
 .../ambari/groovy/client/TestResources.groovy   |   2 +
 .../src/test/resources/blueprint-config.json    |  61 +++
 .../test/resources/hdp-multinode-default.json   | 200 ++++++++
 .../test/resources/hdp-multinode-default2.json  | 164 +++++++
 .../resources/multi-node-hdfs-yarn-config.json  |  89 ++++
 .../test/resources/multi-node-hdfs-yarn.json    |  83 ++++
 .../ambari/shell/commands/ClusterCommands.java  |   3 +-
 .../ambari/shell/flash/InstallProgress.java     |   2 +-
 .../shell/commands/ClusterCommandsTest.java     |   5 +-
 21 files changed, 1719 insertions(+), 43 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ambari/blob/733f0345/ambari-client/groovy-client/pom.xml
----------------------------------------------------------------------
diff --git a/ambari-client/groovy-client/pom.xml b/ambari-client/groovy-client/pom.xml
index 9600cd7..fbedbd1 100644
--- a/ambari-client/groovy-client/pom.xml
+++ b/ambari-client/groovy-client/pom.xml
@@ -53,6 +53,10 @@
       <artifactId>spock-core</artifactId>
       <version>0.7-groovy-2.0</version>
     </dependency>
+    <dependency>
+      <groupId>commons-io</groupId>
+      <artifactId>commons-io</artifactId>
+    </dependency>
   </dependencies>
   <build>
     <plugins>

http://git-wip-us.apache.org/repos/asf/ambari/blob/733f0345/ambari-client/groovy-client/src/main/groovy/org/apache/ambari/groovy/client/AmbariClient.groovy
----------------------------------------------------------------------
diff --git a/ambari-client/groovy-client/src/main/groovy/org/apache/ambari/groovy/client/AmbariClient.groovy b/ambari-client/groovy-client/src/main/groovy/org/apache/ambari/groovy/client/AmbariClient.groovy
index 85c52a4..1a515f5 100644
--- a/ambari-client/groovy-client/src/main/groovy/org/apache/ambari/groovy/client/AmbariClient.groovy
+++ b/ambari-client/groovy-client/src/main/groovy/org/apache/ambari/groovy/client/AmbariClient.groovy
@@ -23,6 +23,7 @@ import groovy.util.logging.Slf4j
 import groovyx.net.http.ContentType
 import groovyx.net.http.HttpResponseException
 import groovyx.net.http.RESTClient
+import org.apache.commons.io.IOUtils
 import org.apache.http.NoHttpResponseException
 import org.apache.http.client.ClientProtocolException
 import java.net.ConnectException
@@ -37,6 +38,7 @@ class AmbariClient {
 
   private static final int PAD = 30
   private static final int OK_RESPONSE = 200
+  private static final String SLAVE = "slave_"
   boolean debugEnabled = false;
   def RESTClient ambari
   def slurper = new JsonSlurper()
@@ -92,6 +94,203 @@ class AmbariClient {
   }
 
   /**
+   * Adds a registered host to the cluster.
+   *
+   * @param hostName new node's hostname
+   * @throws HttpResponseException if the node is not registered with ambari
+   */
+  def addHost(String hostName) throws HttpResponseException {
+    if (debugEnabled) {
+      println "[DEBUG] POST ${ambari.getUri()}clusters/${getClusterName()}/hosts/$hostName"
+    }
+    ambari.post(path: "clusters/${getClusterName()}/hosts/$hostName", { it })
+  }
+
+  /**
+   * Decommission and remove a host from the cluster.
+   * NOTE: this is a synchronous call, it wont return until all
+   * requests are finished
+   *
+   * Steps:
+   *  1, decommission services
+   *  2, stop services
+   *  3, delete host components
+   *  4, delete host
+   *  5, restart services
+   *
+   * @param hostName host to be deleted
+   */
+  def removeHost(String hostName) {
+    def components = getHostComponentsMap(hostName).keySet() as List
+
+    // decommission
+    if (components.contains("NODEMANAGER")) {
+      decommissionNodeManager(hostName)
+    }
+    if (components.contains("DATANODE")) {
+      decommissionDataNode(hostName)
+    }
+
+    // stop services
+    def requests = stopComponentsOnHost(hostName, components)
+    waitForRequestsToFinish(requests.values() as List)
+
+    // delete host components
+    deleteHostComponents(hostName, components)
+
+    // delete host
+    deleteHost(hostName)
+
+    // restart zookeper
+    def id = restartServiceComponents("ZOOKEEPER", ["ZOOKEEPER_SERVER"])
+    waitForRequestsToFinish([id])
+
+    // restart nagios
+    if (getServiceComponentsMap().containsKey("NAGIOS")) {
+      id = restartServiceComponents("NAGIOS", ["NAGIOS_SERVER"])
+      waitForRequestsToFinish([id])
+    }
+  }
+
+  /**
+   * Does not return until all the requests are finished.
+   * @param requestIds ids of the requests
+   */
+  def waitForRequestsToFinish(List<Integer> requestIds) {
+    def stopped = false
+    while (!stopped) {
+      def state = true
+      for (int id : requestIds) {
+        if (getRequestProgress(id) != 100.0) {
+          state = false;
+          break;
+        }
+      }
+      stopped = state
+      Thread.sleep(2000)
+    }
+  }
+
+  /**
+   * Decommission the data node on a given host.
+   *
+   * @return id of the request to keep track its progress
+   */
+  def int decommissionDataNode(String host) {
+    decommission(host, "DATANODE", "HDFS", "NAMENODE")
+  }
+
+  /**
+   * Decommission the node manager on a given host.
+   *
+   * @return id of the request to keep track its progress
+   */
+  def int decommissionNodeManager(String host) {
+    decommission(host, "NODEMANAGER", "YARN", "RESOURCEMANAGER")
+  }
+
+  /**
+   * Decommission a host component on a given host.
+   *
+   * @param host hostName where the component is installed to
+   * @param slaveName slave to be decommissioned
+   * @param serviceName where the slave belongs to
+   * @param componentName where the slave belongs to
+   * @return id of the request to keep track its progress
+   */
+  def int decommission(String host, String slaveName, String serviceName, String componentName) {
+    def requestInfo = [
+      command   : "DECOMMISSION",
+      context   : "Decommission $slaveName",
+      parameters: ["slave_type": slaveName, "excluded_hosts": host]
+    ]
+    def filter = [
+      ["service_name": serviceName, "component_name": componentName]
+    ]
+    Map bodyMap = [
+      "RequestInfo"              : requestInfo,
+      "Requests/resource_filters": filter
+    ]
+    ambari.post(path: "clusters/${getClusterName()}/requests", body: new JsonBuilder(bodyMap).toPrettyString(), {
+      getRequestId(it)
+    })
+  }
+
+  /**
+   * Deletes the components from the host.
+   */
+  def deleteHostComponents(String hostName, List<String> components) {
+    components.each {
+      ambari.delete(path: "clusters/${getClusterName()}/hosts/$hostName/host_components/$it")
+    }
+  }
+
+  /**
+   * Deletes the host from the cluster.
+   */
+  def deleteHost(String hostName) {
+    ambari.delete(path: "clusters/${getClusterName()}/hosts/$hostName")
+  }
+
+  /**
+   * Install all the components from a given blueprint's host group. The services must be installed
+   * in order to install its components. It is recommended to use the same blueprint's host group from which
+   * the cluster was created.
+   *
+   * @param hostName components will be installed on this host
+   * @param blueprint id of the blueprint
+   * @param hostGroup host group of the blueprint
+   * @return map of the component names and their request id since its an async call
+   */
+  def Map<String, Integer> installComponentsToHost(String hostName, String blueprint, String hostGroup) throws HttpResponseException {
+    def bpMap = getBlueprint(blueprint)
+    def components = bpMap?.host_groups?.find { it.name.equals(hostGroup) }?.components?.collect { it.name }
+    if (components) {
+      return installComponentsToHost(hostName, components)
+    } else {
+      return [:]
+    }
+  }
+
+  /**
+   * Installs the given components to the given host.
+   * Only existing service components can be installed.
+   *
+   * @param hostName host to install the component to
+   * @param components components to be installed
+   * @throws HttpResponseException in case the component's service is not installed
+   * @return map of the component names and their request id since its an async call
+   */
+  def Map<String, Integer> installComponentsToHost(String hostName, List<String> components) throws HttpResponseException {
+    def resp = [:]
+    components.each {
+      addComponentToHost(hostName, it)
+      resp << [(it): setComponentState(hostName, it, "INSTALLED")]
+    }
+    resp
+  }
+
+  /**
+   * Starts the given components on a host.
+   *
+   * @return map of the component names and their request id since its an async call
+   * @throws HttpResponseException in case the component is not found
+   */
+  def Map<String, Integer> startComponentsOnHost(String hostName, List<String> components) throws HttpResponseException {
+    setComponentsState(hostName, components, "STARTED")
+  }
+
+  /**
+   * Stops the given components on a host.
+   *
+   * @return map of the component names and their request id since its an async call
+   * @throws HttpResponseException in case the component is not found
+   */
+  def Map<String, Integer> stopComponentsOnHost(String hostName, List<String> components) throws HttpResponseException {
+    setComponentsState(hostName, components, "INSTALLED")
+  }
+
+  /**
    * Checks whether the blueprint exists or not.
    *
    * @param id id of the blueprint
@@ -178,27 +377,34 @@ class AmbariClient {
    * @param blueprint id of the blueprint
    * @return recommended assignments
    */
-  def Map<String, List<String>> recommendAssignments(String blueprint) {
+  def Map<String, List<String>> recommendAssignments(String blueprint) throws InvalidHostGroupHostAssociation {
     def result = [:]
     def hostNames = getHostNames().keySet() as List
     def groups = getBlueprint(blueprint)?.host_groups?.collect { ["name": it.name, "cardinality": it.cardinality] }
     if (hostNames && groups) {
       def groupSize = groups.size()
       def hostSize = hostNames.size()
-      if (hostSize == groupSize) {
-        def i = 0
-        result = groups.collectEntries { [(it.name): [hostNames[i++]]] }
-      } else if (groupSize == 2 && hostSize > 2) {
-        def grouped = groups.groupBy { it.cardinality }
-        if (grouped["1"] && grouped["1"].size() == 1) {
-          groups.each {
-            if (it["cardinality"] == "1") {
-              result << [(it["name"]): [hostNames[0]]]
-            } else {
-              result << [(it["name"]): hostNames.subList(1, hostSize)]
-            }
+      if (hostSize == 1 && groupSize == 1) {
+        result = [(groups[0].name): [hostNames[0]]]
+      } else if (hostSize >= groupSize) {
+        int i = 0
+        groups.findAll { !it.name.toLowerCase().startsWith(SLAVE) }.each {
+          result << [(it.name): [hostNames[i++]]]
+        }
+        def slaves = groups.findAll { it.name.toLowerCase().startsWith(SLAVE) }
+        if (slaves) {
+          int k = 0
+          for (int j = i; j < hostSize; j++) {
+            result[slaves[k].name] = result[slaves[k].name] ?: []
+            result[slaves[k].name] << hostNames[j]
+            result << [(slaves[k].name): result[slaves[k++].name]]
+            k = k == slaves.size ? 0 : k
           }
+        } else {
+          throw new InvalidHostGroupHostAssociation("At least one '$SLAVE' is required", groupSize)
         }
+      } else {
+        throw new InvalidHostGroupHostAssociation("At least $groupSize host is required", groupSize)
       }
     }
     return result
@@ -228,11 +434,49 @@ class AmbariClient {
    * Adds a blueprint to the Ambari server. Exception is thrown if fails.
    *
    * @param json blueprint as json
+   * @return blueprint json
    * @throws HttpResponseException in case of error
    */
-  def void addBlueprint(String json) throws HttpResponseException {
+  def String addBlueprint(String json) throws HttpResponseException {
+    addBlueprint(json, [:])
+  }
+
+  /**
+   * Adds a blueprint with the desired configurations.
+   * 
+   * @param json blueprint to be added
+   * @param configurations blueprint will be extended with these configurations
+   * @return the extended blueprint as json
+   */
+  def String addBlueprint(String json, Map<String, Map<String, String>> configurations) throws HttpResponseException {
     if (json) {
-      postBlueprint(json)
+      def text = slurper.parseText(json)
+      def bpMap = extendBlueprintConfiguration(text, configurations)
+      def builder = new JsonBuilder(bpMap)
+      def resultJson = builder.toPrettyString()
+      postBlueprint(resultJson)
+      resultJson
+    }
+  }
+
+  /**
+   * Only validates the multinode blueprints, at least 1 slave host group must exist.
+   * Throws an exception if the blueprint is not valid.
+   *
+   * @param json blueprint json
+   * @throws InvalidBlueprintException if the blueprint is not valid
+   */
+  def void validateBlueprint(String json) throws InvalidBlueprintException {
+    if (json) {
+      def bpMap = slurper.parseText(json)
+      if (bpMap?.host_groups?.size > 1) {
+        def find = bpMap.host_groups.find { it.name.toLowerCase().startsWith(SLAVE) }
+        if (!find) {
+          throw new InvalidBlueprintException("At least one '$SLAVE' host group is required.")
+        }
+      }
+    } else {
+      throw new InvalidBlueprintException("No blueprint specified")
     }
   }
 
@@ -246,6 +490,8 @@ class AmbariClient {
     addBlueprint(getResourceContent("blueprints/single-node-hdfs-yarn"))
     addBlueprint(getResourceContent("blueprints/lambda-architecture"))
     addBlueprint(getResourceContent("blueprints/warmup"))
+    addBlueprint(getResourceContent("blueprints/hdp-singlenode-default"))
+    addBlueprint(getResourceContent("blueprints/hdp-multinode-default"))
   }
 
   /**
@@ -301,6 +547,24 @@ class AmbariClient {
   }
 
   /**
+   * Modify an existing configuration. Be ware you'll have to provide the whole configuration
+   * otherwise properties might get lost.
+   *
+   * @param type type of the configuration e.g capacity-scheduler
+   * @param properties properties to be used
+   */
+  def modifyConfiguration(String type, Map<String, String> properties) {
+    Map bodyMap = [
+      "Clusters": ["desired_config": ["type": type, "tag": "version${System.currentTimeMillis()}", "properties": properties]]
+    ]
+    def Map<String, ?> putRequestMap = [:]
+    putRequestMap.put('requestContentType', ContentType.URLENC)
+    putRequestMap.put('path', "clusters/${getClusterName()}")
+    putRequestMap.put('body', new JsonBuilder(bodyMap).toPrettyString());
+    ambari.put(putRequestMap)
+  }
+
+  /**
    * Returns a pre-formatted String of the clusters.
    *
    * @return pre-formatted cluster list
@@ -322,18 +586,18 @@ class AmbariClient {
   }
 
   /**
-   * Returns the install progress state. If the install failed -1 returned.
+   * Returns the requests progress.
    *
    * @param request request id; default is 1
    * @return progress in percentage
    */
-  def BigDecimal getInstallProgress(request = 1) {
+  def BigDecimal getRequestProgress(request = 1) {
     def response = getAllResources("requests/$request", "Requests")
-    def String status = response.Requests?.request_status
+    def String status = response?.Requests?.request_status
     if (status && status.equals("FAILED")) {
       return new BigDecimal(-1)
     }
-    return response.Requests?.progress_percent
+    return response?.Requests?.progress_percent
   }
 
   /**
@@ -358,7 +622,9 @@ class AmbariClient {
   }
 
   /**
-   * Returns the available host names and its states.
+   * Returns the available host names and their states. It also
+   * contains hosts which are not part of the cluster, but are connected
+   * to ambari.
    *
    * @return hostname state association
    */
@@ -367,6 +633,15 @@ class AmbariClient {
   }
 
   /**
+   * Returns the names of the hosts which have the given state. It also
+   * contains hosts which are not part of the cluster, but are connected
+   * to ambari.
+   */
+  def Map<String, String> getHostNamesByState(String state) {
+    getHostNames().findAll { it.value == state }
+  }
+
+  /**
    * Returns a pre-formatted list of the hosts.
    *
    * @return pre-formatted String
@@ -458,7 +733,7 @@ class AmbariClient {
    */
   def Map<String, String> getHostComponentsMap(host) {
     def result = getHostComponents(host)?.items?.collectEntries { [(it.HostRoles.component_name): it.HostRoles.state] }
-    result ?: new HashMap()
+    result ?: [:]
   }
 
   /**
@@ -472,11 +747,11 @@ class AmbariClient {
     return getRawResource(resourceRequestMap)
   }
 
-/**
- * Returns a map with service configurations. The keys are the service names, values are maps with <propertyName, propertyValue> entries
- *
- * @return a Map with entries of format <servicename, Map<property, value>>
- */
+  /**
+   * Returns a map with service configurations. The keys are the service names, values are maps with <propertyName, propertyValue> entries
+   *
+   * @return a Map with entries of format <servicename, Map<property, value>>
+   */
   def Map<String, Map<String, String>> getServiceConfigMap() {
     def Map<String, Integer> serviceToTags = new HashMap<>()
 
@@ -499,12 +774,22 @@ class AmbariClient {
     return finalMap
   }
 
-  def startAllServices() {
+  /**
+   * Starts all the services.
+   *
+   * @return id of the request since its an async call
+   */
+  def int startAllServices() {
     log.debug("Starting all services ...")
     manageAllServices("Start All Services", "STARTED")
   }
 
-  def stopAllServices() {
+  /**
+   * Stops all the services.
+   *
+   * @return id of the request since its an async call
+   */
+  def int stopAllServices() {
     log.debug("Stopping all services ...")
     manageAllServices("Stop All Services", "INSTALLED")
   }
@@ -517,6 +802,61 @@ class AmbariClient {
     return servicesStatus(false)
   }
 
+  /**
+   * Returns the public hostnames of the hosts which the host components are installed to.
+   */
+  def List<String> getPublicHostNames(String hostComponent) {
+    def hosts = getInternalHostNames(hostComponent)
+    if (hosts) {
+      return hosts.collect() { resolveInternalHostName(it) }
+    } else {
+      return []
+    }
+  }
+
+  /**
+   * Returns the internal hostnames of the hosts which the host components are installed to.
+   */
+  def List<String> getInternalHostNames(String hostComponent) {
+    def hosts = []
+    getClusterHosts().each {
+      if (getHostComponentsMap(it).keySet().contains(hostComponent)) {
+        hosts << it
+      }
+    }
+    hosts
+  }
+
+  /**
+   * Restarts the given components of a service.
+   */
+  def int restartServiceComponents(String service, List<String> components) {
+    def filter = components.collect {
+      ["service_name": service, "component_name": it, "hosts": getInternalHostNames(it).join(",")]
+    }
+    Map bodyMap = [
+      "RequestInfo"              : [command: "RESTART", context: "Restart $service components $components"],
+      "Requests/resource_filters": filter
+    ]
+    ambari.post(path: "clusters/${getClusterName()}/requests", body: new JsonBuilder(bodyMap).toPrettyString(), {
+      getRequestId(it)
+    })
+  }
+
+  /**
+   * Returns the names of the hosts which are in the cluster.
+   */
+  def List<String> getClusterHosts() {
+    slurp("clusters/${getClusterName()}")?.hosts?.Hosts?.host_name
+  }
+
+  /**
+   * Resolves an internal hostname to a public one.
+   */
+  def String resolveInternalHostName(String internalHostName) {
+    slurp("clusters/${getClusterName()}/hosts/$internalHostName")?.Hosts?.public_host_name
+  }
+
   def private boolean servicesStatus(boolean starting) {
     def String status = (starting) ? "STARTED" : "INSTALLED"
     Map serviceComponents = getServicesMap();
@@ -540,7 +880,8 @@ class AmbariClient {
     putRequestMap.put('query', ['params/run_smoke_test': 'false'])
     putRequestMap.put('body', builder.toPrettyString());
 
-    ambari.put(putRequestMap)
+    def reponse = ambari.put(putRequestMap)
+    slurper.parseText(reponse.getAt("responseData")?.getAt("str"))?.Requests?.id
   }
 
   private def processServiceVersions(Map<String, Integer> serviceToVersions, String service, def version) {
@@ -617,7 +958,7 @@ class AmbariClient {
    */
   private getSlurpedResource(Map resourceRequestMap) {
     def rawResource = getRawResource(resourceRequestMap)
-    def slurpedResource = (rawResource) ? slurper.parseText(rawResource) : null
+    def slurpedResource = (rawResource != null) ? slurper.parseText(rawResource) : rawResource
     return slurpedResource
   }
 
@@ -716,6 +1057,38 @@ class AmbariClient {
     getAllResources("services", "ServiceInfo")
   }
 
+  private def addComponentToHost(String hostName, String component) {
+    if (debugEnabled) {
+      println "[DEBUG] POST ${ambari.getUri()}clusters/${getClusterName()}/hosts/$hostName/host_components"
+    }
+    ambari.post(path: "clusters/${getClusterName()}/hosts/$hostName/host_components/${component.toUpperCase()}", { it })
+  }
+
+  private def Map<String, Integer> setComponentsState(String hostName, List<String> components, String state)
+    throws HttpResponseException {
+    def resp = [:]
+    components.each {
+      resp << [(it): setComponentState(hostName, it, state)]
+    }
+    return resp
+  }
+
+  private def setComponentState(String hostName, String component, String state) {
+    if (debugEnabled) {
+      println "[DEBUG] PUT ${ambari.getUri()}clusters/${getClusterName()}/hosts/$hostName/host_components/$component"
+    }
+    Map bodyMap = [
+      HostRoles  : [state: state.toUpperCase()],
+      RequestInfo: [context: "${component.toUpperCase()} ${state.toUpperCase()}"]
+    ]
+    def Map<String, ?> putRequestMap = [:]
+    putRequestMap.put('requestContentType', ContentType.URLENC)
+    putRequestMap.put('path', "clusters/${getClusterName()}/hosts/$hostName/host_components/${component.toUpperCase()}")
+    putRequestMap.put('body', new JsonBuilder(bodyMap).toPrettyString());
+    def reponse = ambari.put(putRequestMap)
+    slurper.parseText(reponse.getAt("responseData")?.getAt("str"))?.Requests?.id
+  }
+
   /**
    * Returns the properties of the host components as a Map parsed from the Ambari response json.
    *
@@ -730,4 +1103,43 @@ class AmbariClient {
     getClass().getClassLoader().getResourceAsStream(name)?.text
   }
 
-}
\ No newline at end of file
+  private def extendBlueprintConfiguration(Map blueprintMap, Map newConfigs) {
+    def configurations = blueprintMap.configurations
+    if (!configurations) {
+      if (newConfigs) {
+        def conf = []
+        newConfigs.each { conf << [(it.key): it.value] }
+        blueprintMap << ["configurations": conf]
+      }
+      return blueprintMap
+    }
+    newConfigs.each {
+      def site = it.key
+      def index = indexOfConfig(configurations, site)
+      if (index == -1) {
+        configurations << ["$site": it.value]
+      } else {
+        def existingConf = configurations.get(index)
+        existingConf."$site" << it.value
+      }
+    }
+    blueprintMap
+  }
+
+  private int indexOfConfig(List<Map> configurations, String site) {
+    def index = 0
+    for (Map conf : configurations) {
+      if (conf.containsKey(site)) {
+        return index;
+      }
+      index++
+    }
+    return -1;
+  }
+
+  private def int getRequestId(def responseDecorator) {
+    def resp = IOUtils.toString(new InputStreamReader(responseDecorator.entity.content.wrappedStream))
+    slurper.parseText(resp)?.Requests?.id
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/733f0345/ambari-client/groovy-client/src/main/groovy/org/apache/ambari/groovy/client/InvalidBlueprintException.groovy
----------------------------------------------------------------------
diff --git a/ambari-client/groovy-client/src/main/groovy/org/apache/ambari/groovy/client/InvalidBlueprintException.groovy b/ambari-client/groovy-client/src/main/groovy/org/apache/ambari/groovy/client/InvalidBlueprintException.groovy
new file mode 100644
index 0000000..36a7e62
--- /dev/null
+++ b/ambari-client/groovy-client/src/main/groovy/org/apache/ambari/groovy/client/InvalidBlueprintException.groovy
@@ -0,0 +1,28 @@
+/**
+ * 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.groovy.client
+
+/**
+ * Thrown when the blueprint validation fails.
+ */
+public class InvalidBlueprintException extends Exception {
+
+  public InvalidBlueprintException(String message) {
+    super(message)
+  }
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/733f0345/ambari-client/groovy-client/src/main/groovy/org/apache/ambari/groovy/client/InvalidHostGroupHostAssociation.groovy
----------------------------------------------------------------------
diff --git a/ambari-client/groovy-client/src/main/groovy/org/apache/ambari/groovy/client/InvalidHostGroupHostAssociation.groovy b/ambari-client/groovy-client/src/main/groovy/org/apache/ambari/groovy/client/InvalidHostGroupHostAssociation.groovy
new file mode 100644
index 0000000..1483eb9
--- /dev/null
+++ b/ambari-client/groovy-client/src/main/groovy/org/apache/ambari/groovy/client/InvalidHostGroupHostAssociation.groovy
@@ -0,0 +1,39 @@
+/**
+ * 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.groovy.client
+
+/**
+ * Thrown when the host group and host association offends the criteria of
+ * recommendation.
+ */
+public class InvalidHostGroupHostAssociation extends Exception {
+
+  /**
+   * For recommendation at least host group number of host is expected.
+   */
+  private final int minRequiredHost
+
+  public InvalidHostGroupHostAssociation(String message, int minRequiredHost) {
+    super(message)
+    this.minRequiredHost = minRequiredHost
+  }
+
+  public int getMinRequiredHost() {
+    return minRequiredHost
+  }
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/733f0345/ambari-client/groovy-client/src/main/resources/blueprints/hdp-multinode-default
----------------------------------------------------------------------
diff --git a/ambari-client/groovy-client/src/main/resources/blueprints/hdp-multinode-default b/ambari-client/groovy-client/src/main/resources/blueprints/hdp-multinode-default
new file mode 100644
index 0000000..4247c41
--- /dev/null
+++ b/ambari-client/groovy-client/src/main/resources/blueprints/hdp-multinode-default
@@ -0,0 +1,182 @@
+{
+    "configurations" : [
+        {
+            "global" : {
+                "nagios_contact" : "admin@localhost"
+            }
+        }
+    ],
+    "host_groups" : [
+        {
+            "name" : "master_1",
+            "components" : [
+                {
+                    "name" : "NAMENODE"
+                },
+                {
+                    "name" : "ZOOKEEPER_SERVER"
+                },
+                {
+                    "name" : "HBASE_MASTER"
+                },
+                {
+                    "name" : "GANGLIA_SERVER"
+                },
+                {
+                    "name" : "HDFS_CLIENT"
+                },
+                {
+                    "name" : "YARN_CLIENT"
+                },
+                {
+                    "name" : "HCAT"
+                },
+                {
+                    "name" : "GANGLIA_MONITOR"
+                }
+            ],
+            "cardinality" : "1"
+        },
+        {
+            "name" : "master_2",
+            "components" : [
+
+                {
+                    "name" : "ZOOKEEPER_CLIENT"
+                },
+                {
+                    "name" : "HISTORYSERVER"
+                },
+                {
+                    "name" : "HIVE_SERVER"
+                },
+                {
+                    "name" : "SECONDARY_NAMENODE"
+                },
+                {
+                    "name" : "HIVE_METASTORE"
+                },
+                {
+                    "name" : "HDFS_CLIENT"
+                },
+                {
+                    "name" : "HIVE_CLIENT"
+                },
+                {
+                    "name" : "YARN_CLIENT"
+                },
+                {
+                    "name" : "MYSQL_SERVER"
+                },
+                {
+                    "name" : "GANGLIA_MONITOR"
+                },
+                {
+                    "name" : "WEBHCAT_SERVER"
+                }
+            ],
+            "cardinality" : "1"
+        },
+        {
+            "name" : "master_3",
+            "components" : [
+                {
+                    "name" : "RESOURCEMANAGER"
+                },
+                {
+                    "name" : "ZOOKEEPER_SERVER"
+                },
+                {
+                    "name" : "GANGLIA_MONITOR"
+                }
+            ],
+            "cardinality" : "1"
+        },
+        {
+            "name" : "master_4",
+            "components" : [
+                {
+                    "name" : "OOZIE_SERVER"
+                },
+                {
+                    "name" : "ZOOKEEPER_SERVER"
+                },
+                {
+                    "name" : "GANGLIA_MONITOR"
+                }
+            ],
+            "cardinality" : "1"
+        },
+        {
+            "name" : "slave_1",
+            "components" : [
+                {
+                    "name" : "HBASE_REGIONSERVER"
+                },
+                {
+                    "name" : "NODEMANAGER"
+                },
+                {
+                    "name" : "DATANODE"
+                },
+                {
+                    "name" : "GANGLIA_MONITOR"
+                }
+            ],
+            "cardinality" : "${slavesCount}"
+        },
+        {
+            "name" : "gateway",
+            "components" : [
+                {
+                    "name" : "AMBARI_SERVER"
+                },
+                {
+                    "name" : "NAGIOS_SERVER"
+                },
+                {
+                    "name" : "GANGLIA_SERVER"
+                },
+                {
+                    "name" : "ZOOKEEPER_CLIENT"
+                },
+                {
+                    "name" : "PIG"
+                },
+                {
+                    "name" : "OOZIE_CLIENT"
+                },
+                {
+                    "name" : "HBASE_CLIENT"
+                },
+                {
+                    "name" : "HCAT"
+                },
+                {
+                    "name" : "SQOOP"
+                },
+                {
+                    "name" : "HDFS_CLIENT"
+                },
+                {
+                    "name" : "HIVE_CLIENT"
+                },
+                {
+                    "name" : "YARN_CLIENT"
+                },
+                {
+                    "name" : "MAPREDUCE2_CLIENT"
+                },
+                {
+                    "name" : "GANGLIA_MONITOR"
+                }
+            ],
+            "cardinality" : "1"
+        }
+    ],
+    "Blueprints" : {
+        "blueprint_name" : "hdp-multinode-default",
+        "stack_name" : "HDP",
+        "stack_version" : "2.1"
+    }
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/733f0345/ambari-client/groovy-client/src/main/resources/blueprints/hdp-singlenode-default
----------------------------------------------------------------------
diff --git a/ambari-client/groovy-client/src/main/resources/blueprints/hdp-singlenode-default b/ambari-client/groovy-client/src/main/resources/blueprints/hdp-singlenode-default
new file mode 100644
index 0000000..aa752de
--- /dev/null
+++ b/ambari-client/groovy-client/src/main/resources/blueprints/hdp-singlenode-default
@@ -0,0 +1,133 @@
+{
+    "configurations" : [
+        {
+            "global" : {
+                "nagios_contact" : "admin@localhost"
+            }
+        }
+    ],
+    "host_groups" : [
+        {
+            "name" : "master",
+            "components" : [
+                {
+                    "name" : "STORM_REST_API"
+                },
+                {
+                    "name" : "PIG"
+                },
+                {
+                    "name" : "HISTORYSERVER"
+                },
+                {
+                    "name" : "HBASE_REGIONSERVER"
+                },
+                {
+                    "name" : "OOZIE_CLIENT"
+                },
+                {
+                    "name" : "HBASE_CLIENT"
+                },
+                {
+                    "name" : "NAMENODE"
+                },
+                {
+                    "name" : "SUPERVISOR"
+                },
+                {
+                    "name" : "FALCON_SERVER"
+                },
+                {
+                    "name" : "HCAT"
+                },
+                {
+                    "name" : "AMBARI_SERVER"
+                },
+                {
+                    "name" : "APP_TIMELINE_SERVER"
+                },
+                {
+                    "name" : "HDFS_CLIENT"
+                },
+                {
+                    "name" : "HIVE_CLIENT"
+                },
+                {
+                    "name" : "NODEMANAGER"
+                },
+                {
+                    "name" : "DATANODE"
+                },
+                {
+                    "name" : "WEBHCAT_SERVER"
+                },
+                {
+                    "name" : "RESOURCEMANAGER"
+                },
+                {
+                    "name" : "ZOOKEEPER_SERVER"
+                },
+                {
+                    "name" : "ZOOKEEPER_CLIENT"
+                },
+                {
+                    "name" : "STORM_UI_SERVER"
+                },
+                {
+                    "name" : "HBASE_MASTER"
+                },
+                {
+                    "name" : "HIVE_SERVER"
+                },
+                {
+                    "name" : "OOZIE_SERVER"
+                },
+                {
+                    "name" : "FALCON_CLIENT"
+                },
+                {
+                    "name" : "NAGIOS_SERVER"
+                },
+                {
+                    "name" : "SECONDARY_NAMENODE"
+                },
+                {
+                    "name" : "TEZ_CLIENT"
+                },
+                {
+                    "name" : "HIVE_METASTORE"
+                },
+                {
+                    "name" : "GANGLIA_SERVER"
+                },
+                {
+                    "name" : "SQOOP"
+                },
+                {
+                    "name" : "YARN_CLIENT"
+                },
+                {
+                    "name" : "MAPREDUCE2_CLIENT"
+                },
+                {
+                    "name" : "MYSQL_SERVER"
+                },
+                {
+                    "name" : "GANGLIA_MONITOR"
+                },
+                {
+                    "name" : "DRPC_SERVER"
+                },
+                {
+                    "name" : "NIMBUS"
+                }
+            ],
+            "cardinality" : "1"
+        }
+    ],
+    "Blueprints" : {
+        "blueprint_name" : "hdp-singlenode-default",
+        "stack_name" : "HDP",
+        "stack_version" : "2.1"
+    }
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/733f0345/ambari-client/groovy-client/src/main/resources/blueprints/lambda-architecture
----------------------------------------------------------------------
diff --git a/ambari-client/groovy-client/src/main/resources/blueprints/lambda-architecture b/ambari-client/groovy-client/src/main/resources/blueprints/lambda-architecture
index 3deec4c..1750e59 100644
--- a/ambari-client/groovy-client/src/main/resources/blueprints/lambda-architecture
+++ b/ambari-client/groovy-client/src/main/resources/blueprints/lambda-architecture
@@ -8,7 +8,7 @@
   ],
   "host_groups": [
     {
-      "name": "host_group_1",
+      "name": "master_1",
       "components": [
         {
           "name": "ZOOKEEPER_SERVER"
@@ -65,7 +65,7 @@
       "cardinality": "1"
     },
     {
-      "name": "host_group_2",
+      "name": "slave_1",
       "components": [
         {
           "name": "ZOOKEEPER_SERVER"
@@ -122,7 +122,7 @@
       "cardinality": "1"
     },
     {
-      "name": "host_group_3",
+      "name": "slave_2",
       "components": [
         {
           "name": "ZOOKEEPER_SERVER"
@@ -172,4 +172,4 @@
     "stack_name": "HDP",
     "stack_version": "2.1"
   }
-}
\ No newline at end of file
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/733f0345/ambari-client/groovy-client/src/main/resources/blueprints/multi-node-hdfs-yarn
----------------------------------------------------------------------
diff --git a/ambari-client/groovy-client/src/main/resources/blueprints/multi-node-hdfs-yarn b/ambari-client/groovy-client/src/main/resources/blueprints/multi-node-hdfs-yarn
index 27a602a..6d6a364 100644
--- a/ambari-client/groovy-client/src/main/resources/blueprints/multi-node-hdfs-yarn
+++ b/ambari-client/groovy-client/src/main/resources/blueprints/multi-node-hdfs-yarn
@@ -32,7 +32,7 @@
       "cardinality": "1"
     },
     {
-      "name": "slaves",
+      "name": "slave_1",
       "components": [
         {
           "name": "DATANODE"
@@ -64,4 +64,4 @@
     "stack_name": "HDP",
     "stack_version": "2.1"
   }
-}
\ No newline at end of file
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/733f0345/ambari-client/groovy-client/src/main/resources/blueprints/single-node-hdfs-yarn
----------------------------------------------------------------------
diff --git a/ambari-client/groovy-client/src/main/resources/blueprints/single-node-hdfs-yarn b/ambari-client/groovy-client/src/main/resources/blueprints/single-node-hdfs-yarn
index 46ca508..3772a7f 100644
--- a/ambari-client/groovy-client/src/main/resources/blueprints/single-node-hdfs-yarn
+++ b/ambari-client/groovy-client/src/main/resources/blueprints/single-node-hdfs-yarn
@@ -1,7 +1,7 @@
 {
   "host_groups" : [
     {
-      "name" : "host_group_1",
+      "name" : "master",
       "components" : [
       {
         "name" : "NAMENODE"

http://git-wip-us.apache.org/repos/asf/ambari/blob/733f0345/ambari-client/groovy-client/src/test/groovy/org/apache/ambari/groovy/client/AmbariBlueprintsTest.groovy
----------------------------------------------------------------------
diff --git a/ambari-client/groovy-client/src/test/groovy/org/apache/ambari/groovy/client/AmbariBlueprintsTest.groovy b/ambari-client/groovy-client/src/test/groovy/org/apache/ambari/groovy/client/AmbariBlueprintsTest.groovy
index b467c56..d47b1a2 100644
--- a/ambari-client/groovy-client/src/test/groovy/org/apache/ambari/groovy/client/AmbariBlueprintsTest.groovy
+++ b/ambari-client/groovy-client/src/test/groovy/org/apache/ambari/groovy/client/AmbariBlueprintsTest.groovy
@@ -17,11 +17,14 @@
  */
 package org.apache.ambari.groovy.client
 
+import groovy.json.JsonSlurper
 import groovy.util.logging.Slf4j
 
 @Slf4j
 class AmbariBlueprintsTest extends AbstractAmbariClientTest {
 
+  def slurper = new JsonSlurper()
+
   private enum Scenario {
     CLUSTERS, NO_CLUSTERS, BLUEPRINT_EXISTS, NO_BLUEPRINT, HOSTS, NO_HOSTS
   }
@@ -138,6 +141,98 @@ class AmbariBlueprintsTest extends AbstractAmbariClientTest {
     [:] == result
   }
 
+  def "test validate blueprint"() {
+    given:
+    def json = getClass().getClassLoader().getResourceAsStream("blueprint.json").text
+
+    when:
+    ambari.validateBlueprint(json)
+
+    then:
+    noExceptionThrown()
+  }
+
+  def "test validate blueprint no slaves_"() {
+    given:
+    def json = getClass().getClassLoader().getResourceAsStream("hdp-multinode-default2.json").text
+
+    when:
+    ambari.validateBlueprint(json)
+
+    then:
+    thrown(InvalidBlueprintException)
+  }
+
+  def "test validate blueprint with uppercase SLAVE_"() {
+    given:
+    def json = getClass().getClassLoader().getResourceAsStream("hdp-multinode-default.json").text
+
+    when:
+    ambari.validateBlueprint(json)
+
+    then:
+    notThrown(InvalidBlueprintException)
+  }
+
+  def "test validate blueprint for null json"() {
+    when:
+    ambari.validateBlueprint(null)
+
+    then:
+    thrown(InvalidBlueprintException)
+  }
+
+  def "test add blueprint with configuration"() {
+    given:
+    def json = getClass().getClassLoader().getResourceAsStream("blueprint.json").text
+    ambari.metaClass.postBlueprint = { String blueprint -> return }
+
+    when:
+    def config = [
+      "yarn-site": ["property-key": "property-value", "yarn.nodemanager.local-dirs": "/mnt/fs1/,/mnt/fs2/"],
+      "hdfs-site": ["dfs.datanode.data.dir": "/mnt/fs1/,/mnt/fs2/"]
+    ]
+    def blueprint = ambari.addBlueprint(json, config)
+
+    then:
+    def expected = slurper.parseText(getClass().getClassLoader().getResourceAsStream("blueprint-config.json").text)
+    def actual = slurper.parseText(blueprint)
+    actual == expected
+  }
+
+  def "test add blueprint with existing configuration"() {
+    given:
+    def json = getClass().getClassLoader().getResourceAsStream("multi-node-hdfs-yarn.json").text
+    ambari.metaClass.postBlueprint = { String blueprint -> return }
+
+    when:
+    def config = [
+      "yarn-site": ["property-key": "property-value", "yarn.nodemanager.local-dirs": "apple"],
+      "hdfs-site": ["dfs.datanode.data.dir": "/mnt/fs1/,/mnt/fs2/"],
+      "core-site": ["fs.defaultFS": "localhost:9000"]
+    ]
+    def blueprint = ambari.addBlueprint(json, config)
+
+    then:
+    def expected = slurper.parseText(getClass().getClassLoader().getResourceAsStream("multi-node-hdfs-yarn-config.json").text)
+    def actual = slurper.parseText(blueprint)
+    actual == expected
+  }
+
+  def "test add blueprint with empty configuration"() {
+    given:
+    def json = getClass().getClassLoader().getResourceAsStream("blueprint.json").text
+    ambari.metaClass.postBlueprint = { String blueprint -> return }
+
+    when:
+    def blueprint = ambari.addBlueprint(json, [:])
+
+    then:
+    def expected = slurper.parseText(json)
+    def actual = slurper.parseText(blueprint)
+    actual == expected
+  }
+
   def protected String selectResponseJson(Map resourceRequestMap, String scenarioStr) {
     def thePath = resourceRequestMap.get("path");
     def query = resourceRequestMap.get("query");

http://git-wip-us.apache.org/repos/asf/ambari/blob/733f0345/ambari-client/groovy-client/src/test/groovy/org/apache/ambari/groovy/client/AmbariHostsTest.groovy
----------------------------------------------------------------------
diff --git a/ambari-client/groovy-client/src/test/groovy/org/apache/ambari/groovy/client/AmbariHostsTest.groovy b/ambari-client/groovy-client/src/test/groovy/org/apache/ambari/groovy/client/AmbariHostsTest.groovy
index 80d2f51..7efb95d 100644
--- a/ambari-client/groovy-client/src/test/groovy/org/apache/ambari/groovy/client/AmbariHostsTest.groovy
+++ b/ambari-client/groovy-client/src/test/groovy/org/apache/ambari/groovy/client/AmbariHostsTest.groovy
@@ -59,9 +59,39 @@ class AmbariHostsTest extends AbstractAmbariClientTest {
     ] == result
   }
 
+  def "install host components to a host from an existing valid blueprint"() {
+    given:
+    mockResponses(Scenario.CLUSTERS.name())
+    ambari.metaClass.addComponentToHost = { String host, String component -> return null }
+    ambari.metaClass.setComponentState = { String host, String component, String state -> return 10 }
+
+    when:
+    def result = ambari.installComponentsToHost("amb0", "hdp-multinode-default", "slave_1")
+
+    then:
+    [
+      "HBASE_REGIONSERVER": 10,
+      "NODEMANAGER"       : 10,
+      "DATANODE"          : 10,
+      "GANGLIA_MONITOR"   : 10
+    ] == result
+  }
+
+  def "install host components to a host from an existing valid blueprint but invalid group"() {
+    given:
+    mockResponses(Scenario.CLUSTERS.name())
+    ambari.metaClass.addComponentToHost = { String host, String component -> return null }
+    ambari.metaClass.setComponentState = { String host, String component, String state -> return null }
+
+    when:
+    def result = ambari.installComponentsToHost("amb0", "hdp-multinode-default", "slave_2")
+
+    then:
+    [:] == result
+  }
+
   def protected String selectResponseJson(Map resourceRequestMap, String scenarioStr) {
     def thePath = resourceRequestMap.get("path");
-    def query = resourceRequestMap.get("query");
     def Scenario scenario = Scenario.valueOf(scenarioStr)
     def json = null
     if (thePath == TestResources.CLUSTERS.uri()) {
@@ -71,6 +101,8 @@ class AmbariHostsTest extends AbstractAmbariClientTest {
       }
     } else if (thePath == TestResources.HOST_COMPONENTS.uri()) {
       json = "host-components.json"
+    } else if (thePath == TestResources.BLUEPRINT_MULTI.uri) {
+      json = "hdp-multinode-default.json"
     } else {
       log.error("Unsupported resource path: {}", thePath)
     }

http://git-wip-us.apache.org/repos/asf/ambari/blob/733f0345/ambari-client/groovy-client/src/test/groovy/org/apache/ambari/groovy/client/AmbariRecommendTest.groovy
----------------------------------------------------------------------
diff --git a/ambari-client/groovy-client/src/test/groovy/org/apache/ambari/groovy/client/AmbariRecommendTest.groovy b/ambari-client/groovy-client/src/test/groovy/org/apache/ambari/groovy/client/AmbariRecommendTest.groovy
new file mode 100644
index 0000000..9f7ff03
--- /dev/null
+++ b/ambari-client/groovy-client/src/test/groovy/org/apache/ambari/groovy/client/AmbariRecommendTest.groovy
@@ -0,0 +1,150 @@
+/**
+ * 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.groovy.client
+
+import groovy.util.logging.Slf4j
+
+@Slf4j
+class AmbariRecommendTest extends AbstractAmbariClientTest {
+
+  private enum Scenario {
+    SINGLE_NODE_BLUEPRINT, MULTI_NODE_BLUEPRINT, MULTI_NODE_BLUEPRINT2
+  }
+
+  def "test recommend for single node"() {
+    given:
+    mockResponses(Scenario.SINGLE_NODE_BLUEPRINT.name())
+    ambari.metaClass.getHostNames = { return ["amb0": "HEALTHY"] }
+
+    when:
+    def result = ambari.recommendAssignments("single-node-hdfs-yarn")
+
+    then:
+    [host_group_1: ["amb0"]] == result
+  }
+
+  def "test recommend for invalid host number"() {
+    given:
+    mockResponses(Scenario.MULTI_NODE_BLUEPRINT.name())
+    ambari.metaClass.getHostNames = { return ["amb0": "HEALTHY"] }
+
+    when:
+    def result
+    try {
+      result = ambari.recommendAssignments("hdp-multinode-default")
+    } catch (InvalidHostGroupHostAssociation e) {
+      result = e.getMinRequiredHost()
+    }
+
+    then:
+    result == 7
+  }
+
+  def "test recommend for no slave group"() {
+    given:
+    mockResponses(Scenario.MULTI_NODE_BLUEPRINT2.name())
+    ambari.metaClass.getHostNames = {
+      return [
+        "amb0": "HEALTHY",
+        "amb1": "HEALTHY",
+        "amb2": "HEALTHY",
+        "amb3": "HEALTHY",
+        "amb4": "HEALTHY",
+        "amb5": "HEALTHY",
+        "amb6": "HEALTHY",
+        "amb7": "HEALTHY",
+        "amb8": "HEALTHY",
+        "amb9": "HEALTHY",
+        "am10": "HEALTHY",
+        "am10": "HEALTHY",
+        "am20": "HEALTHY",
+        "am30": "HEALTHY",
+        "am40": "HEALTHY",
+      ]
+    }
+
+    when:
+    def result
+    def msg
+    try {
+      result = ambari.recommendAssignments("hdp-multinode-default2")
+    } catch (InvalidHostGroupHostAssociation e) {
+      msg = e.getMessage()
+      result = e.getMinRequiredHost()
+    }
+
+    then:
+    result == 5
+    msg == "At least one 'slave_' is required"
+  }
+
+  def "test recommend for multi node"() {
+    given:
+    mockResponses(Scenario.MULTI_NODE_BLUEPRINT.name())
+    ambari.metaClass.getHostNames = {
+      return [
+        "amb0": "HEALTHY",
+        "amb1": "HEALTHY",
+        "amb2": "HEALTHY",
+        "amb3": "HEALTHY",
+        "amb4": "HEALTHY",
+        "amb5": "HEALTHY",
+        "amb6": "HEALTHY",
+        "amb7": "HEALTHY",
+        "amb8": "HEALTHY",
+        "amb9": "HEALTHY",
+        "am10": "HEALTHY",
+        "am10": "HEALTHY",
+        "am20": "HEALTHY",
+        "am30": "HEALTHY",
+        "am40": "HEALTHY",
+      ]
+    }
+
+    when:
+    def result = ambari.recommendAssignments("hdp-multinode-default")
+
+    then:
+    [master_1: ["amb0"],
+     master_2: ["amb1"],
+     master_3: ["amb2"],
+     master_4: ["amb3"],
+     gateway : ["amb4"],
+     slave_1 : ["amb5", "amb7", "amb9", "am20", "am40"],
+     SLAVE_2 : ["amb6", "amb8", "am10", "am30"]
+    ] == result
+  }
+
+  def protected String selectResponseJson(Map resourceRequestMap, String scenarioStr) {
+    def thePath = resourceRequestMap.get("path");
+    def query = resourceRequestMap.get("query");
+    def Scenario scenario = Scenario.valueOf(scenarioStr)
+    def json = null
+    if (thePath == TestResources.BLUEPRINT.uri()) {
+      json = "blueprint.json"
+    } else if (thePath == TestResources.BLUEPRINT_MULTI.uri()) {
+      json = "hdp-multinode-default.json"
+    } else if (thePath == TestResources.BLUEPRINT_MULTI2.uri()) {
+      json = "hdp-multinode-default2.json"
+    } else {
+      log.error("Unsupported resource path: {}", thePath)
+    }
+    return json
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/733f0345/ambari-client/groovy-client/src/test/groovy/org/apache/ambari/groovy/client/TestResources.groovy
----------------------------------------------------------------------
diff --git a/ambari-client/groovy-client/src/test/groovy/org/apache/ambari/groovy/client/TestResources.groovy b/ambari-client/groovy-client/src/test/groovy/org/apache/ambari/groovy/client/TestResources.groovy
index f9ee519..716d700 100644
--- a/ambari-client/groovy-client/src/test/groovy/org/apache/ambari/groovy/client/TestResources.groovy
+++ b/ambari-client/groovy-client/src/test/groovy/org/apache/ambari/groovy/client/TestResources.groovy
@@ -23,6 +23,8 @@ enum TestResources {
   CONFIGURATIONS("http://localhost:8080/api/v1/clusters/MySingleNodeCluster/configurations"),
   BLUEPRINTS("http://localhost:8080/api/v1/blueprints"),
   BLUEPRINT("http://localhost:8080/api/v1/blueprints/single-node-hdfs-yarn"),
+  BLUEPRINT_MULTI("http://localhost:8080/api/v1/blueprints/hdp-multinode-default"),
+  BLUEPRINT_MULTI2("http://localhost:8080/api/v1/blueprints/hdp-multinode-default2"),
   INEXISTENT_BLUEPRINT("http://localhost:8080/api/v1/blueprints/inexistent-blueprint"),
   HOSTS("http://localhost:8080/api/v1/hosts"),
   TASKS("http://localhost:8080/api/v1/clusters/MySingleNodeCluster/requests/1"),

http://git-wip-us.apache.org/repos/asf/ambari/blob/733f0345/ambari-client/groovy-client/src/test/resources/blueprint-config.json
----------------------------------------------------------------------
diff --git a/ambari-client/groovy-client/src/test/resources/blueprint-config.json b/ambari-client/groovy-client/src/test/resources/blueprint-config.json
new file mode 100644
index 0000000..bf3f67d
--- /dev/null
+++ b/ambari-client/groovy-client/src/test/resources/blueprint-config.json
@@ -0,0 +1,61 @@
+{
+    "Blueprints": {
+        "blueprint_name": "single-node-hdfs-yarn",
+        "stack_version": "2.0",
+        "stack_name": "HDP"
+    },
+    "configurations": [
+        {
+            "yarn-site": {
+                "property-key": "property-value",
+                "yarn.nodemanager.local-dirs": "/mnt/fs1/,/mnt/fs2/"
+            }
+        },
+        {
+            "hdfs-site": {
+                "dfs.datanode.data.dir": "/mnt/fs1/,/mnt/fs2/"
+            }
+        }
+    ],
+    "host_groups": [
+        {
+            "name": "host_group_1",
+            "components": [
+                {
+                    "name": "NAMENODE"
+                },
+                {
+                    "name": "SECONDARY_NAMENODE"
+                },
+                {
+                    "name": "DATANODE"
+                },
+                {
+                    "name": "HDFS_CLIENT"
+                },
+                {
+                    "name": "RESOURCEMANAGER"
+                },
+                {
+                    "name": "NODEMANAGER"
+                },
+                {
+                    "name": "YARN_CLIENT"
+                },
+                {
+                    "name": "HISTORYSERVER"
+                },
+                {
+                    "name": "MAPREDUCE2_CLIENT"
+                },
+                {
+                    "name": "ZOOKEEPER_SERVER"
+                },
+                {
+                    "name": "ZOOKEEPER_CLIENT"
+                }
+            ],
+            "cardinality": "1"
+        }
+    ]
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ambari/blob/733f0345/ambari-client/groovy-client/src/test/resources/hdp-multinode-default.json
----------------------------------------------------------------------
diff --git a/ambari-client/groovy-client/src/test/resources/hdp-multinode-default.json b/ambari-client/groovy-client/src/test/resources/hdp-multinode-default.json
new file mode 100644
index 0000000..f6c7f3f
--- /dev/null
+++ b/ambari-client/groovy-client/src/test/resources/hdp-multinode-default.json
@@ -0,0 +1,200 @@
+{
+    "configurations" : [
+        {
+            "global" : {
+                "nagios_contact" : "admin@localhost"
+            }
+        }
+    ],
+    "host_groups" : [
+        {
+            "name" : "master_1",
+            "components" : [
+                {
+                    "name" : "NAMENODE"
+                },
+                {
+                    "name" : "ZOOKEEPER_SERVER"
+                },
+                {
+                    "name" : "HBASE_MASTER"
+                },
+                {
+                    "name" : "GANGLIA_SERVER"
+                },
+                {
+                    "name" : "HDFS_CLIENT"
+                },
+                {
+                    "name" : "YARN_CLIENT"
+                },
+                {
+                    "name" : "HCAT"
+                },
+                {
+                    "name" : "GANGLIA_MONITOR"
+                }
+            ],
+            "cardinality" : "1"
+        },
+        {
+            "name" : "master_2",
+            "components" : [
+
+                {
+                    "name" : "ZOOKEEPER_CLIENT"
+                },
+                {
+                    "name" : "HISTORYSERVER"
+                },
+                {
+                    "name" : "HIVE_SERVER"
+                },
+                {
+                    "name" : "SECONDARY_NAMENODE"
+                },
+                {
+                    "name" : "HIVE_METASTORE"
+                },
+                {
+                    "name" : "HDFS_CLIENT"
+                },
+                {
+                    "name" : "HIVE_CLIENT"
+                },
+                {
+                    "name" : "YARN_CLIENT"
+                },
+                {
+                    "name" : "MYSQL_SERVER"
+                },
+                {
+                    "name" : "GANGLIA_MONITOR"
+                },
+                {
+                    "name" : "WEBHCAT_SERVER"
+                }
+            ],
+            "cardinality" : "1"
+        },
+        {
+            "name" : "master_3",
+            "components" : [
+                {
+                    "name" : "RESOURCEMANAGER"
+                },
+                {
+                    "name" : "ZOOKEEPER_SERVER"
+                },
+                {
+                    "name" : "GANGLIA_MONITOR"
+                }
+            ],
+            "cardinality" : "1"
+        },
+        {
+            "name" : "master_4",
+            "components" : [
+                {
+                    "name" : "OOZIE_SERVER"
+                },
+                {
+                    "name" : "ZOOKEEPER_SERVER"
+                },
+                {
+                    "name" : "GANGLIA_MONITOR"
+                }
+            ],
+            "cardinality" : "1"
+        },
+        {
+            "name" : "slave_1",
+            "components" : [
+                {
+                    "name" : "HBASE_REGIONSERVER"
+                },
+                {
+                    "name" : "NODEMANAGER"
+                },
+                {
+                    "name" : "DATANODE"
+                },
+                {
+                    "name" : "GANGLIA_MONITOR"
+                }
+            ],
+            "cardinality" : "${slavesCount}"
+        },
+        {
+                    "name" : "SLAVE_2",
+                    "components" : [
+                        {
+                            "name" : "HBASE_REGIONSERVER"
+                        },
+                        {
+                            "name" : "NODEMANAGER"
+                        },
+                        {
+                            "name" : "DATANODE"
+                        },
+                        {
+                            "name" : "GANGLIA_MONITOR"
+                        }
+                    ],
+                    "cardinality" : "${slavesCount}"
+                },
+        {
+            "name" : "gateway",
+            "components" : [
+                {
+                    "name" : "AMBARI_SERVER"
+                },
+                {
+                    "name" : "NAGIOS_SERVER"
+                },
+                {
+                    "name" : "GANGLIA_SERVER"
+                },
+                {
+                    "name" : "ZOOKEEPER_CLIENT"
+                },
+                {
+                    "name" : "PIG"
+                },
+                {
+                    "name" : "OOZIE_CLIENT"
+                },
+                {
+                    "name" : "HBASE_CLIENT"
+                },
+                {
+                    "name" : "HCAT"
+                },
+                {
+                    "name" : "SQOOP"
+                },
+                {
+                    "name" : "HDFS_CLIENT"
+                },
+                {
+                    "name" : "HIVE_CLIENT"
+                },
+                {
+                    "name" : "YARN_CLIENT"
+                },
+                {
+                    "name" : "MAPREDUCE2_CLIENT"
+                },
+                {
+                    "name" : "GANGLIA_MONITOR"
+                }
+            ],
+            "cardinality" : "1"
+        }
+    ],
+    "Blueprints" : {
+        "blueprint_name" : "hdp-multinode-default",
+        "stack_name" : "HDP",
+        "stack_version" : "2.1"
+    }
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/733f0345/ambari-client/groovy-client/src/test/resources/hdp-multinode-default2.json
----------------------------------------------------------------------
diff --git a/ambari-client/groovy-client/src/test/resources/hdp-multinode-default2.json b/ambari-client/groovy-client/src/test/resources/hdp-multinode-default2.json
new file mode 100644
index 0000000..4db3680
--- /dev/null
+++ b/ambari-client/groovy-client/src/test/resources/hdp-multinode-default2.json
@@ -0,0 +1,164 @@
+{
+    "configurations" : [
+        {
+            "global" : {
+                "nagios_contact" : "admin@localhost"
+            }
+        }
+    ],
+    "host_groups" : [
+        {
+            "name" : "master_1",
+            "components" : [
+                {
+                    "name" : "NAMENODE"
+                },
+                {
+                    "name" : "ZOOKEEPER_SERVER"
+                },
+                {
+                    "name" : "HBASE_MASTER"
+                },
+                {
+                    "name" : "GANGLIA_SERVER"
+                },
+                {
+                    "name" : "HDFS_CLIENT"
+                },
+                {
+                    "name" : "YARN_CLIENT"
+                },
+                {
+                    "name" : "HCAT"
+                },
+                {
+                    "name" : "GANGLIA_MONITOR"
+                }
+            ],
+            "cardinality" : "1"
+        },
+        {
+            "name" : "master_2",
+            "components" : [
+
+                {
+                    "name" : "ZOOKEEPER_CLIENT"
+                },
+                {
+                    "name" : "HISTORYSERVER"
+                },
+                {
+                    "name" : "HIVE_SERVER"
+                },
+                {
+                    "name" : "SECONDARY_NAMENODE"
+                },
+                {
+                    "name" : "HIVE_METASTORE"
+                },
+                {
+                    "name" : "HDFS_CLIENT"
+                },
+                {
+                    "name" : "HIVE_CLIENT"
+                },
+                {
+                    "name" : "YARN_CLIENT"
+                },
+                {
+                    "name" : "MYSQL_SERVER"
+                },
+                {
+                    "name" : "GANGLIA_MONITOR"
+                },
+                {
+                    "name" : "WEBHCAT_SERVER"
+                }
+            ],
+            "cardinality" : "1"
+        },
+        {
+            "name" : "master_3",
+            "components" : [
+                {
+                    "name" : "RESOURCEMANAGER"
+                },
+                {
+                    "name" : "ZOOKEEPER_SERVER"
+                },
+                {
+                    "name" : "GANGLIA_MONITOR"
+                }
+            ],
+            "cardinality" : "1"
+        },
+        {
+            "name" : "master_4",
+            "components" : [
+                {
+                    "name" : "OOZIE_SERVER"
+                },
+                {
+                    "name" : "ZOOKEEPER_SERVER"
+                },
+                {
+                    "name" : "GANGLIA_MONITOR"
+                }
+            ],
+            "cardinality" : "1"
+        },
+        {
+            "name" : "gateway",
+            "components" : [
+                {
+                    "name" : "AMBARI_SERVER"
+                },
+                {
+                    "name" : "NAGIOS_SERVER"
+                },
+                {
+                    "name" : "GANGLIA_SERVER"
+                },
+                {
+                    "name" : "ZOOKEEPER_CLIENT"
+                },
+                {
+                    "name" : "PIG"
+                },
+                {
+                    "name" : "OOZIE_CLIENT"
+                },
+                {
+                    "name" : "HBASE_CLIENT"
+                },
+                {
+                    "name" : "HCAT"
+                },
+                {
+                    "name" : "SQOOP"
+                },
+                {
+                    "name" : "HDFS_CLIENT"
+                },
+                {
+                    "name" : "HIVE_CLIENT"
+                },
+                {
+                    "name" : "YARN_CLIENT"
+                },
+                {
+                    "name" : "MAPREDUCE2_CLIENT"
+                },
+                {
+                    "name" : "GANGLIA_MONITOR"
+                }
+            ],
+            "cardinality" : "1"
+        }
+    ],
+    "Blueprints" : {
+        "blueprint_name" : "hdp-multinode-default",
+        "stack_name" : "HDP",
+        "stack_version" : "2.1"
+    }
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/733f0345/ambari-client/groovy-client/src/test/resources/multi-node-hdfs-yarn-config.json
----------------------------------------------------------------------
diff --git a/ambari-client/groovy-client/src/test/resources/multi-node-hdfs-yarn-config.json b/ambari-client/groovy-client/src/test/resources/multi-node-hdfs-yarn-config.json
new file mode 100644
index 0000000..16f4938
--- /dev/null
+++ b/ambari-client/groovy-client/src/test/resources/multi-node-hdfs-yarn-config.json
@@ -0,0 +1,89 @@
+{
+  "configurations": [
+    {
+      "global": {
+        "nagios_contact": "admin@localhost"
+      }
+    },
+    {
+      "hdfs-site": {
+        "dfs.datanode.data.dir": "/mnt/fs1/,/mnt/fs2/"
+      }
+    },
+    {
+      "yarn-site": {
+        "yarn.nodemanager.local-dirs": "apple",
+        "property-key": "property-value"
+      }
+    },
+    {
+          "core-site": {
+            "fs.defaultFS": "localhost:9000"
+          }
+        }
+  ],
+  "host_groups": [
+    {
+      "name": "master",
+      "components": [
+        {
+          "name": "NAMENODE"
+        },
+        {
+          "name": "GANGLIA_SERVER"
+        },
+        {
+          "name": "HISTORYSERVER"
+        },
+        {
+          "name": "SECONDARY_NAMENODE"
+        },
+        {
+          "name": "RESOURCEMANAGER"
+        },
+        {
+          "name": "HISTORYSERVER"
+        },
+        {
+          "name": "NAGIOS_SERVER"
+        },
+        {
+          "name": "ZOOKEEPER_SERVER"
+        }
+      ],
+      "cardinality": "1"
+    },
+    {
+      "name": "slave_1",
+      "components": [
+        {
+          "name": "DATANODE"
+        },
+        {
+          "name": "GANGLIA_MONITOR"
+        },
+        {
+          "name": "HDFS_CLIENT"
+        },
+        {
+          "name": "NODEMANAGER"
+        },
+        {
+          "name": "YARN_CLIENT"
+        },
+        {
+          "name": "MAPREDUCE2_CLIENT"
+        },
+        {
+          "name": "ZOOKEEPER_CLIENT"
+        }
+      ],
+      "cardinality": "2"
+    }
+  ],
+  "Blueprints": {
+    "blueprint_name": "multi-node-hdfs-yarn",
+    "stack_name": "HDP",
+    "stack_version": "2.1"
+  }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ambari/blob/733f0345/ambari-client/groovy-client/src/test/resources/multi-node-hdfs-yarn.json
----------------------------------------------------------------------
diff --git a/ambari-client/groovy-client/src/test/resources/multi-node-hdfs-yarn.json b/ambari-client/groovy-client/src/test/resources/multi-node-hdfs-yarn.json
new file mode 100644
index 0000000..fce02ad
--- /dev/null
+++ b/ambari-client/groovy-client/src/test/resources/multi-node-hdfs-yarn.json
@@ -0,0 +1,83 @@
+{
+  "configurations": [
+    {
+      "global": {
+        "nagios_contact": "admin@localhost"
+      }
+    },
+    {
+      "hdfs-site": {
+        "dfs.datanode.data.dir": "/mnt/fs1/,/mnt/fs2/"
+      }
+    },
+    {
+      "yarn-site": {
+        "yarn.nodemanager.local-dirs": "/mnt/fs1/,/mnt/fs2/"
+      }
+    }
+  ],
+  "host_groups": [
+    {
+      "name": "master",
+      "components": [
+        {
+          "name": "NAMENODE"
+        },
+        {
+          "name": "GANGLIA_SERVER"
+        },
+        {
+          "name": "HISTORYSERVER"
+        },
+        {
+          "name": "SECONDARY_NAMENODE"
+        },
+        {
+          "name": "RESOURCEMANAGER"
+        },
+        {
+          "name": "HISTORYSERVER"
+        },
+        {
+          "name": "NAGIOS_SERVER"
+        },
+        {
+          "name": "ZOOKEEPER_SERVER"
+        }
+      ],
+      "cardinality": "1"
+    },
+    {
+      "name": "slave_1",
+      "components": [
+        {
+          "name": "DATANODE"
+        },
+        {
+          "name": "GANGLIA_MONITOR"
+        },
+        {
+          "name": "HDFS_CLIENT"
+        },
+        {
+          "name": "NODEMANAGER"
+        },
+        {
+          "name": "YARN_CLIENT"
+        },
+        {
+          "name": "MAPREDUCE2_CLIENT"
+        },
+        {
+          "name": "ZOOKEEPER_CLIENT"
+        }
+      ],
+      "cardinality": "2"
+    }
+  ],
+  "Blueprints": {
+    "blueprint_name": "multi-node-hdfs-yarn",
+    "stack_name": "HDP",
+    "stack_version": "2.1"
+  }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ambari/blob/733f0345/ambari-shell/ambari-groovy-shell/src/main/java/org/apache/ambari/shell/commands/ClusterCommands.java
----------------------------------------------------------------------
diff --git a/ambari-shell/ambari-groovy-shell/src/main/java/org/apache/ambari/shell/commands/ClusterCommands.java b/ambari-shell/ambari-groovy-shell/src/main/java/org/apache/ambari/shell/commands/ClusterCommands.java
index dafdb85..a773c3c 100644
--- a/ambari-shell/ambari-groovy-shell/src/main/java/org/apache/ambari/shell/commands/ClusterCommands.java
+++ b/ambari-shell/ambari-groovy-shell/src/main/java/org/apache/ambari/shell/commands/ClusterCommands.java
@@ -26,6 +26,7 @@ import java.util.List;
 import java.util.Map;
 
 import org.apache.ambari.groovy.client.AmbariClient;
+import org.apache.ambari.groovy.client.InvalidHostGroupHostAssociation;
 import org.apache.ambari.shell.completion.Blueprint;
 import org.apache.ambari.shell.completion.Host;
 import org.apache.ambari.shell.flash.FlashService;
@@ -148,7 +149,7 @@ public class ClusterCommands implements CommandMarker {
    * @return prints the auto assignments
    */
   @CliCommand(value = "cluster autoAssign", help = "Automatically assigns hosts to different host groups base on the provided strategy")
-  public String autoAssign() {
+  public String autoAssign() throws InvalidHostGroupHostAssociation {
     Map<String, List<String>> assignments = client.recommendAssignments(context.getFocusValue());
     if (!assignments.isEmpty()) {
       hostGroups = assignments;

http://git-wip-us.apache.org/repos/asf/ambari/blob/733f0345/ambari-shell/ambari-groovy-shell/src/main/java/org/apache/ambari/shell/flash/InstallProgress.java
----------------------------------------------------------------------
diff --git a/ambari-shell/ambari-groovy-shell/src/main/java/org/apache/ambari/shell/flash/InstallProgress.java b/ambari-shell/ambari-groovy-shell/src/main/java/org/apache/ambari/shell/flash/InstallProgress.java
index 69164ea..dbf8e65 100644
--- a/ambari-shell/ambari-groovy-shell/src/main/java/org/apache/ambari/shell/flash/InstallProgress.java
+++ b/ambari-shell/ambari-groovy-shell/src/main/java/org/apache/ambari/shell/flash/InstallProgress.java
@@ -45,7 +45,7 @@ public class InstallProgress extends AbstractFlash {
   public String getText() {
     StringBuilder sb = new StringBuilder();
     if (!done) {
-      BigDecimal progress = client.getInstallProgress();
+      BigDecimal progress = client.getRequestProgress();
       if (progress != null) {
         BigDecimal decimal = progress.setScale(2, BigDecimal.ROUND_HALF_UP);
         int intValue = decimal.intValue();

http://git-wip-us.apache.org/repos/asf/ambari/blob/733f0345/ambari-shell/ambari-groovy-shell/src/test/java/org/apache/ambari/shell/commands/ClusterCommandsTest.java
----------------------------------------------------------------------
diff --git a/ambari-shell/ambari-groovy-shell/src/test/java/org/apache/ambari/shell/commands/ClusterCommandsTest.java b/ambari-shell/ambari-groovy-shell/src/test/java/org/apache/ambari/shell/commands/ClusterCommandsTest.java
index 777d05d..ab0608b 100644
--- a/ambari-shell/ambari-groovy-shell/src/test/java/org/apache/ambari/shell/commands/ClusterCommandsTest.java
+++ b/ambari-shell/ambari-groovy-shell/src/test/java/org/apache/ambari/shell/commands/ClusterCommandsTest.java
@@ -35,6 +35,7 @@ import java.util.List;
 import java.util.Map;
 
 import org.apache.ambari.groovy.client.AmbariClient;
+import org.apache.ambari.groovy.client.InvalidHostGroupHostAssociation;
 import org.apache.ambari.shell.completion.Blueprint;
 import org.apache.ambari.shell.completion.Host;
 import org.apache.ambari.shell.flash.FlashService;
@@ -250,7 +251,7 @@ public class ClusterCommandsTest {
   }
 
   @Test
-  public void testAutoAssignForEmptyResult() {
+  public void testAutoAssignForEmptyResult() throws InvalidHostGroupHostAssociation {
     Map<String, List<String>> hostGroups = singletonMap("group1", asList("host1"));
     ReflectionTestUtils.setField(clusterCommands, "hostGroups", hostGroups);
     when(context.getFocusValue()).thenReturn("blueprint");
@@ -263,7 +264,7 @@ public class ClusterCommandsTest {
   }
 
   @Test
-  public void testAutoAssign() {
+  public void testAutoAssign() throws InvalidHostGroupHostAssociation {
     Map<String, List<String>> hostGroups = singletonMap("group1", asList("host1"));
     Map<String, List<String>> newAssignments = singletonMap("group1", asList("host1"));
     ReflectionTestUtils.setField(clusterCommands, "hostGroups", hostGroups);