ambari-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From yus...@apache.org
Subject [40/51] [partial] AMBARI-7718. Rebase branch-windows-dev against trunk. (Jayush Luniya and Florian Barca via yusaku)
Date Thu, 16 Oct 2014 20:12:04 GMT
http://git-wip-us.apache.org/repos/asf/ambari/blob/9213dcca/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..6d94c5d 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,16 +747,17 @@ 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>>
- */
-  def Map<String, Map<String, String>> getServiceConfigMap() {
+  /**
+   * 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(String type = "") {
     def Map<String, Integer> serviceToTags = new HashMap<>()
 
     //get services and last versions configurations
-    Map<String, ?> configsResourceRequestMap = getResourceRequestMap("clusters/${getClusterName()}/configurations", [:])
+    def path = "clusters/${getClusterName()}/configurations"
+    Map<String, ?> configsResourceRequestMap = getResourceRequestMap(path, type ? ["type": type] : [:])
     def rawConfigs = getSlurpedResource(configsResourceRequestMap)
 
     rawConfigs?.items.collect { object ->
@@ -499,14 +775,44 @@ 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")
+    manageService("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")
+    manageService("Stop All Services", "INSTALLED")
+  }
+
+  /**
+   * Starts the given service.
+   *
+   * @param service name of the service
+   * @return id of the request
+   */
+  def int startService(String service) {
+    manageService("Starting $service", "STARTED", service)
+  }
+
+  /**
+   * Stops the given service.
+   *
+   * @param service name of the service
+   * @return id of the request
+   */
+  def int stopService(String service) {
+    manageService("Stopping $service", "INSTALLED", service)
   }
 
   def boolean servicesStarted() {
@@ -517,6 +823,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();
@@ -528,19 +889,24 @@ class AmbariClient {
     return allInState;
   }
 
-  def private manageAllServices(String context, String state) {
+  def private manageService(String context, String state, String service = "") {
     Map bodyMap = [
       RequestInfo: [context: context],
       ServiceInfo: [state: state]
     ]
     JsonBuilder builder = new JsonBuilder(bodyMap)
+    def path = "${ambari.getUri()}clusters/${getClusterName()}/services"
+    if (service) {
+      path += "/$service"
+    }
     def Map<String, ?> putRequestMap = [:]
     putRequestMap.put('requestContentType', ContentType.URLENC)
-    putRequestMap.put('path', "${ambari.getUri()}" + "clusters/${getClusterName()}/services")
+    putRequestMap.put('path', path)
     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 +983,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 +1082,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 +1128,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/9213dcca/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/9213dcca/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/9213dcca/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..0a76808
--- /dev/null
+++ b/ambari-client/groovy-client/src/main/resources/blueprints/hdp-multinode-default
@@ -0,0 +1,179 @@
+{
+    "configurations" : [
+        {
+            "nagios-env" : {
+                "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" : "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/9213dcca/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..34fa5a0
--- /dev/null
+++ b/ambari-client/groovy-client/src/main/resources/blueprints/hdp-singlenode-default
@@ -0,0 +1,133 @@
+{
+    "configurations" : [
+        {
+            "nagios-env" : {
+                "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/9213dcca/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..26fa576 100644
--- a/ambari-client/groovy-client/src/main/resources/blueprints/lambda-architecture
+++ b/ambari-client/groovy-client/src/main/resources/blueprints/lambda-architecture
@@ -1,14 +1,14 @@
 {
   "configurations": [
     {
-      "global": {
-        "nagios_contact": "me@my-awesome-domain.example"
-      }
+        "nagios-env" : {
+            "nagios_contact" : "admin@localhost"
+        }
     }
   ],
   "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"
@@ -101,9 +101,6 @@
           "name": "YARN_CLIENT"
         },
         {
-          "name" : "APP_TIMELINE_SERVER"
-        },
-        {
           "name": "MAPREDUCE2_CLIENT"
         },
         {
@@ -122,7 +119,7 @@
       "cardinality": "1"
     },
     {
-      "name": "host_group_3",
+      "name": "slave_2",
       "components": [
         {
           "name": "ZOOKEEPER_SERVER"
@@ -158,9 +155,6 @@
           "name": "DATANODE"
         },
         {
-          "name" : "APP_TIMELINE_SERVER"
-        },
-        {
           "name": "GANGLIA_MONITOR"
         }
       ],
@@ -172,4 +166,4 @@
     "stack_name": "HDP",
     "stack_version": "2.1"
   }
-}
\ No newline at end of file
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/9213dcca/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..82f3042 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
@@ -1,9 +1,9 @@
 {
   "configurations": [
     {
-      "global": {
-        "nagios_contact": "me@my-awesome-domain.example"
-      }
+        "nagios-env" : {
+            "nagios_contact" : "admin@localhost"
+        }
     }
   ],
   "host_groups": [
@@ -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/9213dcca/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..768741b 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,14 +1,14 @@
 {
   "host_groups" : [
     {
-      "name" : "host_group_1",
+      "name" : "master",
       "components" : [
       {
         "name" : "NAMENODE"
       },
       {
         "name" : "SECONDARY_NAMENODE"
-      },       
+      },
       {
         "name" : "DATANODE"
       },

http://git-wip-us.apache.org/repos/asf/ambari/blob/9213dcca/ambari-client/groovy-client/src/main/resources/blueprints/warmup
----------------------------------------------------------------------
diff --git a/ambari-client/groovy-client/src/main/resources/blueprints/warmup b/ambari-client/groovy-client/src/main/resources/blueprints/warmup
index 8b745d6..e6587c1 100644
--- a/ambari-client/groovy-client/src/main/resources/blueprints/warmup
+++ b/ambari-client/groovy-client/src/main/resources/blueprints/warmup
@@ -1,9 +1,9 @@
 {
   "configurations": [
     {
-      "global": {
-        "nagios_contact": "me@my-awesome-domain.example"
-      }
+        "nagios-env" : {
+            "nagios_contact" : "admin@localhost"
+        }
     }
   ],
   "host_groups": [
@@ -91,4 +91,4 @@
     "stack_name": "HDP",
     "stack_version": "2.1"
   }
-}
\ No newline at end of file
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/9213dcca/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/9213dcca/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/9213dcca/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/9213dcca/ambari-client/groovy-client/src/test/groovy/org/apache/ambari/groovy/client/AmbariServicesTest.groovy
----------------------------------------------------------------------
diff --git a/ambari-client/groovy-client/src/test/groovy/org/apache/ambari/groovy/client/AmbariServicesTest.groovy b/ambari-client/groovy-client/src/test/groovy/org/apache/ambari/groovy/client/AmbariServicesTest.groovy
index ce8060f..e798789 100644
--- a/ambari-client/groovy-client/src/test/groovy/org/apache/ambari/groovy/client/AmbariServicesTest.groovy
+++ b/ambari-client/groovy-client/src/test/groovy/org/apache/ambari/groovy/client/AmbariServicesTest.groovy
@@ -17,11 +17,14 @@
  */
 package org.apache.ambari.groovy.client
 
+import groovy.json.JsonSlurper
 import groovy.util.logging.Slf4j
 
 @Slf4j
 class AmbariServicesTest extends AbstractAmbariClientTest {
 
+  def slurper = new JsonSlurper()
+
   private enum Scenario {
     SERVICES, NO_SERVICES, NO_SERVICE_COMPONENTS
   }
@@ -114,6 +117,66 @@ class AmbariServicesTest extends AbstractAmbariClientTest {
     !result
   }
 
+  def "test stop all services"() {
+    given:
+    def context
+    ambari.metaClass.getClusterName = { return "cluster" }
+    ambari.getAmbari().metaClass.put = { Map request ->
+      context = request
+    }
+    ambari.getSlurper().metaClass.parseText { String text -> return ["Requests": ["id": 1]] }
+
+    when:
+    def id = ambari.stopAllServices()
+
+    then:
+    1 == id
+    context.path == "http://localhost:8080/api/v1/clusters/cluster/services"
+    def body = slurper.parseText(context.body)
+    body.RequestInfo.context == "Stop All Services"
+    body.ServiceInfo.state == "INSTALLED"
+  }
+
+  def "test start service ZOOKEEPER"() {
+    given:
+    def context
+    ambari.metaClass.getClusterName = { return "cluster" }
+    ambari.getAmbari().metaClass.put = { Map request ->
+      context = request
+    }
+    ambari.getSlurper().metaClass.parseText { String text -> return ["Requests": ["id": 1]] }
+
+    when:
+    def id = ambari.startService("ZOOKEEPER")
+
+    then:
+    1 == id
+    context.path == "http://localhost:8080/api/v1/clusters/cluster/services/ZOOKEEPER"
+    def body = slurper.parseText(context.body)
+    body.RequestInfo.context == "Starting ZOOKEEPER"
+    body.ServiceInfo.state == "STARTED"
+  }
+
+  def "test stop service ZOOKEEPER"() {
+    given:
+    def context
+    ambari.metaClass.getClusterName = { return "cluster" }
+    ambari.getAmbari().metaClass.put = { Map request ->
+      context = request
+    }
+    ambari.getSlurper().metaClass.parseText { String text -> return ["Requests": ["id": 1]] }
+
+    when:
+    def id = ambari.stopService("ZOOKEEPER")
+
+    then:
+    1 == id
+    context.path == "http://localhost:8080/api/v1/clusters/cluster/services/ZOOKEEPER"
+    def body = slurper.parseText(context.body)
+    body.RequestInfo.context == "Stopping ZOOKEEPER"
+    body.ServiceInfo.state == "INSTALLED"
+  }
+
   def private String selectResponseJson(Map resourceRequestMap, String scenarioStr) {
     def thePath = resourceRequestMap.get("path");
     def Scenario scenario = Scenario.valueOf(scenarioStr)

http://git-wip-us.apache.org/repos/asf/ambari/blob/9213dcca/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/9213dcca/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/9213dcca/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/9213dcca/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/9213dcca/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/9213dcca/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/9213dcca/ambari-client/python-client/src/main/python/ambari_client/ambari_api.py
----------------------------------------------------------------------
diff --git a/ambari-client/python-client/src/main/python/ambari_client/ambari_api.py b/ambari-client/python-client/src/main/python/ambari_client/ambari_api.py
index a79d907..87972f9 100644
--- a/ambari-client/python-client/src/main/python/ambari_client/ambari_api.py
+++ b/ambari-client/python-client/src/main/python/ambari_client/ambari_api.py
@@ -145,6 +145,22 @@ class AmbariClient(RestResource):
         """
         return cluster._create_cluster(self, cluster_name, version)
 
+    def create_cluster_from_blueprint(self, cluster_name, blueprint_name,
+                                      host_groups, configurations=None,
+                                      default_password=None):
+        """
+        Create a new cluster.
+        @param cluster_name: Cluster cluster_name
+        @param blueprint_name: the name of the blueprint
+        @param host_groups: an array of host_group information
+        @param configurations: an array of configuration overrides
+        @param default_password: the default password to use for all password-requiring services
+        @return  ClusterModel object.
+        """
+        return cluster._create_cluster_from_blueprint(self, cluster_name,
+            blueprint_name, host_groups, configurations=configurations,
+            default_password=default_password)
+
     def delete_cluster(self, cluster_name):
         """
         Delete a cluster
@@ -240,7 +256,7 @@ class AmbariClient(RestResource):
         """
         return status._get_N_requests(self, cluster_name, noOfrequest)
 
-    def get_blueprint(self, blueprint_name=None):
+    def get_blueprint(self, blueprint_name):
         """
         get blueprint
         @param blueprint_name:blueprint_name name.
@@ -256,7 +272,7 @@ class AmbariClient(RestResource):
         """
         return blueprint.get_cluster_blueprint(self, cluster_name)
 
-    def delete_blueprint(self, blueprint_name=None):
+    def delete_blueprint(self, blueprint_name):
         """
         get blueprint
         @param blueprint_name:blueprint_name name.
@@ -264,13 +280,13 @@ class AmbariClient(RestResource):
         """
         return blueprint.delete_blueprint(self, blueprint_name)
 
-    def create_blueprint(self, blueprint_name=None):
+    def create_blueprint(self, blueprint_name, blueprint_schema):
         """
         get blueprint
         @param blueprint_name:blueprint_name name.
         @return: A BlueprintModel object
         """
-        return blueprint.create_blueprint(self, blueprint_name)
+        return blueprint.create_blueprint(self, blueprint_name, blueprint_schema)
 
 
 def get_root_resource(

http://git-wip-us.apache.org/repos/asf/ambari/blob/9213dcca/ambari-client/python-client/src/main/python/ambari_client/core/coreutils.py
----------------------------------------------------------------------
diff --git a/ambari-client/python-client/src/main/python/ambari_client/core/coreutils.py b/ambari-client/python-client/src/main/python/ambari_client/core/coreutils.py
index 278df2e..3f92d43 100644
--- a/ambari-client/python-client/src/main/python/ambari_client/core/coreutils.py
+++ b/ambari-client/python-client/src/main/python/ambari_client/core/coreutils.py
@@ -14,3 +14,14 @@
 #  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 #  See the License for the specific language governing permissions and
 #  limitations under the License.
+
+import re
+
+def normalize_all_caps(name):
+	"""
+	This converts all caps words into normal case.
+	i.e. 'NAGIOS_SERVER' becomes 'Nagios Server'
+	"""
+	normalized = name.lower()
+	normalized = re.sub('_(\w)', lambda match: ' ' + match.group(1).upper(), normalized)
+	return normalized[0].upper() + normalized[1:]

http://git-wip-us.apache.org/repos/asf/ambari/blob/9213dcca/ambari-client/python-client/src/main/python/ambari_client/core/http_client.py
----------------------------------------------------------------------
diff --git a/ambari-client/python-client/src/main/python/ambari_client/core/http_client.py b/ambari-client/python-client/src/main/python/ambari_client/core/http_client.py
index 39b9961..7f7e526 100644
--- a/ambari-client/python-client/src/main/python/ambari_client/core/http_client.py
+++ b/ambari-client/python-client/src/main/python/ambari_client/core/http_client.py
@@ -99,6 +99,7 @@ class HttpClient(object):
                     (path,))
                 payload = None
 
+	self.c.unsetopt(pycurl.CUSTOMREQUEST)
         buf = cStringIO.StringIO()
         self.c.setopt(pycurl.WRITEFUNCTION, buf.write)
         self.c.setopt(pycurl.SSL_VERIFYPEER, 0)

http://git-wip-us.apache.org/repos/asf/ambari/blob/9213dcca/ambari-client/python-client/src/main/python/ambari_client/model/blueprint.py
----------------------------------------------------------------------
diff --git a/ambari-client/python-client/src/main/python/ambari_client/model/blueprint.py b/ambari-client/python-client/src/main/python/ambari_client/model/blueprint.py
index e207a6b..21b0b33 100644
--- a/ambari-client/python-client/src/main/python/ambari_client/model/blueprint.py
+++ b/ambari-client/python-client/src/main/python/ambari_client/model/blueprint.py
@@ -88,16 +88,16 @@ def delete_blueprint(resource_root, blueprint_name):
         "NO_KEY")
 
 
-def create_blueprint(resource_root, blueprint_name, json_data):
+def create_blueprint(resource_root, blueprint_name, blueprint_schema):
     """
     Create a blueprint
     @param root_resource: The root Resource.
     @param blueprint_name: blueprint_name
-    @param json_data: blueprint  json
+    @param blueprint_schema: blueprint  json
     @return: An ClusterModel object
     """
     path = paths.BLUEPRINT_PATH % blueprint_name
-    resp = resource_root.post(path=path, payload=json_data)
+    resp = resource_root.post(path=path, payload=blueprint_schema)
     return utils.ModelUtils.create_model(
         status.StatusModel,
         resp,

http://git-wip-us.apache.org/repos/asf/ambari/blob/9213dcca/ambari-client/python-client/src/main/python/ambari_client/model/cluster.py
----------------------------------------------------------------------
diff --git a/ambari-client/python-client/src/main/python/ambari_client/model/cluster.py b/ambari-client/python-client/src/main/python/ambari_client/model/cluster.py
index 87cd86e..f24b064 100644
--- a/ambari-client/python-client/src/main/python/ambari_client/model/cluster.py
+++ b/ambari-client/python-client/src/main/python/ambari_client/model/cluster.py
@@ -72,6 +72,37 @@ def _create_cluster(root_resource, cluster_name, version):
         "NO_KEY")
 
 
+def _create_cluster_from_blueprint(root_resource, cluster_name, blueprint_name,
+                                   host_groups, configurations=None,
+                                   default_password=None):
+    """
+    Create a cluster
+    @param root_resource: The root Resource.
+    @param cluster_name: Cluster cluster_name
+    @param blueprint_name: the name of the blueprint
+    @param host_groups: an array of host_group information
+    @param configurations: an array of configuration overrides
+    @param default_password: the default password to use for all password-requiring services
+    @return: An StatusModel object
+    """
+    data = {
+      "blueprint" : blueprint_name,
+      "host_groups" : host_groups,
+    }
+    if configurations is not None:
+        data['configurations'] = configurations
+    if default_password is not None:
+        data['default_password'] = default_password
+
+    path = paths.CLUSTERS_PATH + "/%s" % (cluster_name)
+    resp = root_resource.post(path=path, payload=data)
+    return utils.ModelUtils.create_model(
+        status.StatusModel,
+        resp,
+        root_resource,
+        "NO_KEY")
+
+
 def _delete_cluster(root_resource, cluster_name):
     """
     Delete a cluster by name

http://git-wip-us.apache.org/repos/asf/ambari/blob/9213dcca/ambari-client/python-client/src/main/python/ambari_client/model/component.py
----------------------------------------------------------------------
diff --git a/ambari-client/python-client/src/main/python/ambari_client/model/component.py b/ambari-client/python-client/src/main/python/ambari_client/model/component.py
index 5e04fc8..858b68f 100644
--- a/ambari-client/python-client/src/main/python/ambari_client/model/component.py
+++ b/ambari-client/python-client/src/main/python/ambari_client/model/component.py
@@ -16,6 +16,7 @@
 #  limitations under the License.
 
 import logging
+from ambari_client.core.coreutils import normalize_all_caps
 from ambari_client.model.base_model import BaseModel, ModelList
 from ambari_client.model import paths, utils, status
 
@@ -73,9 +74,22 @@ def _get_service_component(
         resource_root,
         "ServiceComponentInfo")
 
+def _delete_host_component(
+        resource_root,
+        cluster_name ,
+        host_name ,
+        component_name):
+    path = paths.HOSTS_COMPONENT_PATH % (
+        cluster_name, host_name , component_name)
+    resp = resource_root.delete(path)
+    return utils.ModelUtils.create_model(
+        status.StatusModel,
+        resp,
+        resource_root,
+        "NO_KEY")
 
-class ComponentModel(BaseModel):
 
+class ComponentModel(BaseModel):
     """
     The ComponentModel class
     """
@@ -124,3 +138,70 @@ class ComponentModel(BaseModel):
                 self._get_cluster_name(), self.host_name, self.component_name) + "?fields=metrics"
         metricjson = self._get_resource_root().get(metricpath)
         return metricjson
+
+
+    def delete(self):
+        return _delete_host_component(self._get_resource_root(), self._get_cluster_name(), self.host_name, self.component_name)
+
+    def install(self):
+        data = {
+            "RequestInfo": {
+                "context": "Install %s" % normalize_all_caps(self.component_name),
+            },
+            "HostRoles": {
+                "state": "INSTALLED",
+            },
+        }
+        root_resource = self._get_resource_root()
+        resp = root_resource.put(path=self._path() + '/' + self.component_name, payload=data)
+        return utils.ModelUtils.create_model(status.StatusModel, resp, root_resource, "NO_KEY")
+
+    def start(self):
+        data = {
+            "RequestInfo": {
+                "context": "Start %s" % normalize_all_caps(self.component_name),
+            },
+            "HostRoles": {
+                "state": "STARTED",
+            },
+        }
+        root_resource = self._get_resource_root()
+        resp = root_resource.put(path=self._path() + '/' + self.component_name, payload=data)
+        return utils.ModelUtils.create_model(status.StatusModel, resp, root_resource, "NO_KEY")
+
+    def stop(self):
+        data = {
+            "RequestInfo": {
+                "context": "Stop %s" % normalize_all_caps(self.component_name),
+            },
+            "HostRoles": {
+                "state": "INSTALLED",
+            },
+        }
+        root_resource = self._get_resource_root()
+        resp = root_resource.put(path=self._path() + '/' + self.component_name, payload=data)
+        return utils.ModelUtils.create_model(status.StatusModel, resp, root_resource, "NO_KEY")
+
+    def restart(self):
+        # need to move this to utils, handle _ gracefully
+        data = {
+            "RequestInfo": {
+                "command": "RESTART",
+                "context": "Restart %s" % normalize_all_caps(self.component_name),
+                "operation_level": {
+                    "level": "SERVICE",
+                    "cluster_name": self._get_cluster_name(),
+                    "service_name": self.service_name,
+
+                },
+            },
+            "Requests/resource_filters": [{
+                "service_name": self.service_name,
+                "component_name": self.component_name,
+                "hosts": self.host_name,
+            }],
+        }
+        root_resource = self._get_resource_root()
+        path = paths.CLUSTER_REQUESTS_PATH % self._get_cluster_name()
+        resp = root_resource.post(path=path, payload=data)
+        return utils.ModelUtils.create_model(status.StatusModel, resp, root_resource, "NO_KEY")


Mime
View raw message