/** * 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.hadoop.hbase; import com.sun.jersey.api.client.Client; import com.sun.jersey.api.client.ClientResponse; import com.sun.jersey.api.client.filter.HTTPBasicAuthFilter; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.conf.Configured; import org.apache.hadoop.util.ReflectionUtils; import org.codehaus.jackson.JsonNode; import org.codehaus.jackson.map.ObjectMapper; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; import javax.xml.ws.http.HTTPException; import java.io.IOException; import java.net.URI; import java.util.HashMap; import java.util.Locale; import java.util.Map; /** * A ClusterManager implementation designed to control Cloudera Manager (http://www.cloudera.com) * clusters via REST API. This API uses HTTP GET requests against the cluster manager server to * retrieve information and POST/PUT requests to perform actions. As a simple example, to retrieve a * list of hosts from a CM server with login credentials admin:admin, a simple curl command would be * curl -X POST -H "Content-Type:application/json" -u admin:admin \ * "http://this.is.my.server.com:7180/api/v8/hosts" * * This command would return a JSON result, which would need to be parsed to retrieve relevant * information. This action and many others are covered by this class. * * A note on nomenclature: while the ClusterManager interface uses a ServiceType enum when * referring to things like RegionServers and DataNodes, cluster managers often use different * terminology. As an example, Cloudera Manager (http://www.cloudera.com) would refer to a * RegionServer as a "role" of the HBase "service." It would further refer to "hbase" as the * "serviceType." Apache Ambari (http://ambari.apache.org) would call the RegionServer a * "component" of the HBase "service." * * This class will defer to the ClusterManager terminology in methods that it implements from * that interface, but uses Cloudera Manager's terminology when dealing with its API directly. */ public class RESTApiClusterManager extends Configured implements ClusterManager { // Properties that need to be in the Configuration object to interact with the REST API cluster // manager. Most easily defined in hbase-site.xml, but can also be passed on the command line. private static final String REST_API_CLUSTER_MANAGER_HOSTNAME = "hbase.it.clustermanager.restapi.hostname"; private static final String REST_API_CLUSTER_MANAGER_USERNAME = "hbase.it.clustermanager.restapi.username"; private static final String REST_API_CLUSTER_MANAGER_PASSWORD = "hbase.it.clustermanager.restapi.password"; private static final String REST_API_CLUSTER_MANAGER_CLUSTER_NAME = "hbase.it.clustermanager.restapi.clustername"; // Some default values for the above properties. private static final String DEFAULT_SERVER_HOSTNAME = "http://localhost:7180"; private static final String DEFAULT_SERVER_USERNAME = "admin"; private static final String DEFAULT_SERVER_PASSWORD = "admin"; private static final String DEFAULT_CLUSTER_NAME = "Cluster 1"; // Fields for the hostname, username, password, and cluster name of the cluster management server // to be used. private String serverHostname; private String serverUsername; private String serverPassword; private String clusterName; // Each version of Cloudera Manager supports a particular API versions. Version 6 of this API // provides all the features needed by this class. private static final String API_VERSION = "v6"; // Client instances are expensive, so use the same one for all our REST queries. private Client client = Client.create(); // An instance of HBaseClusterManager is used for methods like the kill, resume, and suspend // because cluster managers don't tend to implement these operations. private ClusterManager hBaseClusterManager; private static final Log LOG = LogFactory.getLog(RESTApiClusterManager.class); RESTApiClusterManager() { hBaseClusterManager = ReflectionUtils.newInstance(HBaseClusterManager.class, new IntegrationTestingUtility().getConfiguration()); } @Override public void setConf(Configuration conf) { super.setConf(conf); if (conf == null) { // Configured gets passed null before real conf. Why? I don't know. return; } serverHostname = conf.get(REST_API_CLUSTER_MANAGER_HOSTNAME, DEFAULT_SERVER_HOSTNAME); serverUsername = conf.get(REST_API_CLUSTER_MANAGER_USERNAME, DEFAULT_SERVER_USERNAME); serverPassword = conf.get(REST_API_CLUSTER_MANAGER_PASSWORD, DEFAULT_SERVER_PASSWORD); clusterName = conf.get(REST_API_CLUSTER_MANAGER_CLUSTER_NAME, DEFAULT_CLUSTER_NAME); // Add filter to Client instance to enable server authentication. client.addFilter(new HTTPBasicAuthFilter(serverUsername, serverPassword)); } @Override public void start(ServiceType service, String hostname, int port) throws IOException { performClusterManagerCommand(service, hostname, RoleCommand.START); } @Override public void stop(ServiceType service, String hostname, int port) throws IOException { performClusterManagerCommand(service, hostname, RoleCommand.STOP); } @Override public void restart(ServiceType service, String hostname, int port) throws IOException { performClusterManagerCommand(service, hostname, RoleCommand.RESTART); } @Override public boolean isRunning(ServiceType service, String hostname, int port) throws IOException { String serviceName = getServiceName(roleServiceType.get(service)); String hostId = getHostId(hostname); String roleState = getRoleState(serviceName, service.toString(), hostId); String healthSummary = getHealthSummary(serviceName, service.toString(), hostId); boolean isRunning = false; // Use Yoda condition to prevent NullPointerException. roleState will be null if the "service // type" does not exist on the specified hostname. if ("STARTED".equals(roleState) && "GOOD".equals(healthSummary)) { isRunning = true; } return isRunning; } @Override public void kill(ServiceType service, String hostname, int port) throws IOException { hBaseClusterManager.kill(service, hostname, port); } @Override public void suspend(ServiceType service, String hostname, int port) throws IOException { hBaseClusterManager.suspend(service, hostname, port); } @Override public void resume(ServiceType service, String hostname, int port) throws IOException { hBaseClusterManager.resume(service, hostname, port); } // Convenience method to execute command against role on hostname. Only graceful commands are // supported since cluster management APIs don't tend to let you SIGKILL things. private void performClusterManagerCommand(ServiceType role, String hostname, RoleCommand command) throws IOException { LOG.info("Performing " + command + " command against " + role + " on " + hostname + "..."); String serviceName = getServiceName(roleServiceType.get(role)); String hostId = getHostId(hostname); String roleName = getRoleName(serviceName, role.toString(), hostId); doRoleCommand(serviceName, roleName, command); } // Performing a command (e.g. starting or stopping a role) requires a POST instead of a GET. private void doRoleCommand(String serviceName, String roleName, RoleCommand roleCommand) { URI uri = UriBuilder.fromUri(serverHostname) .path("api") .path(API_VERSION) .path("clusters") .path(clusterName) .path("services") .path(serviceName) .path("roleCommands") .path(roleCommand.toString()) .build(); String body = "{ \"items\": [ \"" + roleName + "\" ] }"; LOG.info("Executing POST against " + uri + " with body " + body + "..."); ClientResponse response = client.resource(uri) .type(MediaType.APPLICATION_JSON) .post(ClientResponse.class, body); int statusCode = response.getStatus(); if (statusCode != Response.Status.OK.getStatusCode()) { throw new HTTPException(statusCode); } } // Possible healthSummary values include "GOOD" and "BAD." private String getHealthSummary(String serviceName, String roleType, String hostId) throws IOException { return getRolePropertyValue(serviceName, roleType, hostId, "healthSummary"); } // This API uses a hostId to execute host-specific commands; get one from a hostname. private String getHostId(String hostname) throws IOException { String hostId = null; URI uri = UriBuilder.fromUri(serverHostname) .path("api") .path(API_VERSION) .path("hosts") .build(); JsonNode hosts = getJsonNodeFromURIGet(uri); if (hosts != null) { // Iterate through the list of hosts, stopping once you've reached the requested hostname. for (JsonNode host : hosts) { if (host.get("hostname").getTextValue().equals(hostname)) { hostId = host.get("hostId").getTextValue(); break; } } } else { hostId = null; } return hostId; } // Execute GET against URI, returning a JsonNode object to be traversed. private JsonNode getJsonNodeFromURIGet(URI uri) throws IOException { LOG.info("Executing GET against " + uri + "..."); ClientResponse response = client.resource(uri) .accept(MediaType.APPLICATION_JSON_TYPE) .get(ClientResponse.class); int statusCode = response.getStatus(); if (statusCode != Response.Status.OK.getStatusCode()) { throw new HTTPException(statusCode); } // This API folds information as the value to an "items" attribute. return new ObjectMapper().readTree(response.getEntity(String.class)).get("items"); } // This API assigns a unique role name to each host's instance of a role. private String getRoleName(String serviceName, String roleType, String hostId) throws IOException { return getRolePropertyValue(serviceName, roleType, hostId, "name"); } // Get the value of a property from a role on a particular host. private String getRolePropertyValue(String serviceName, String roleType, String hostId, String property) throws IOException { String roleValue = null; URI uri = UriBuilder.fromUri(serverHostname) .path("api") .path(API_VERSION) .path("clusters") .path(clusterName) .path("services") .path(serviceName) .path("roles") .build(); JsonNode roles = getJsonNodeFromURIGet(uri); if (roles != null) { // Iterate through the list of roles, stopping once the requested one is found. for (JsonNode role : roles) { if (role.get("hostRef").get("hostId").getTextValue().equals(hostId) && role.get("type") .getTextValue() .toLowerCase(Locale.ROOT) .equals(roleType.toLowerCase(Locale.ROOT))) { roleValue = role.get(property).getTextValue(); break; } } } return roleValue; } // Possible roleState values include "STARTED" and "STOPPED." private String getRoleState(String serviceName, String roleType, String hostId) throws IOException { return getRolePropertyValue(serviceName, roleType, hostId, "roleState"); } // Convert a service (e.g. "HBASE," "HDFS") into a service name (e.g. "HBASE-1," "HDFS-1"). private String getServiceName(Service service) throws IOException { String serviceName = null; URI uri = UriBuilder.fromUri(serverHostname) .path("api") .path(API_VERSION) .path("clusters") .path(clusterName) .path("services") .build(); JsonNode services = getJsonNodeFromURIGet(uri); if (services != null) { // Iterate through the list of services, stopping once the requested one is found. for (JsonNode serviceEntry : services) { if (serviceEntry.get("type").getTextValue().equals(service.toString())) { serviceName = serviceEntry.get("name").getTextValue(); break; } } } return serviceName; } /* * Some enums to guard against bad calls. */ // The RoleCommand enum is used by the doRoleCommand method to guard against non-existent methods // being invoked on a given role. // TODO: Integrate zookeeper and hdfs related failure injections (Ref: HBASE-14261). private enum RoleCommand { START, STOP, RESTART; // APIs tend to take commands in lowercase, so convert them to save the trouble later. @Override public String toString() { return name().toLowerCase(Locale.ROOT); } } // ClusterManager methods take a "ServiceType" object (e.g. "HBASE_MASTER," "HADOOP_NAMENODE"). // These "service types," which cluster managers call "roles" or "components," need to be mapped // to their corresponding service (e.g. "HBase," "HDFS") in order to be controlled. private static Map<ServiceType, Service> roleServiceType = new HashMap<>(); static { roleServiceType.put(ServiceType.HADOOP_NAMENODE, Service.HDFS); roleServiceType.put(ServiceType.HADOOP_DATANODE, Service.HDFS); roleServiceType.put(ServiceType.HADOOP_JOBTRACKER, Service.MAPREDUCE); roleServiceType.put(ServiceType.HADOOP_TASKTRACKER, Service.MAPREDUCE); roleServiceType.put(ServiceType.HBASE_MASTER, Service.HBASE); roleServiceType.put(ServiceType.HBASE_REGIONSERVER, Service.HBASE); } private enum Service { HBASE, HDFS, MAPREDUCE } }