001/** 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018 019package org.apache.hadoop.hbase; 020 021import java.io.IOException; 022import java.net.URI; 023import java.util.HashMap; 024import java.util.Locale; 025import java.util.Map; 026import javax.ws.rs.client.Client; 027import javax.ws.rs.client.ClientBuilder; 028import javax.ws.rs.client.Entity; 029import javax.ws.rs.client.Invocation; 030import javax.ws.rs.client.WebTarget; 031import javax.ws.rs.core.MediaType; 032import javax.ws.rs.core.Response; 033import javax.ws.rs.core.UriBuilder; 034import javax.xml.ws.http.HTTPException; 035import org.apache.hadoop.conf.Configuration; 036import org.apache.hadoop.conf.Configured; 037import org.apache.hadoop.hbase.util.GsonUtil; 038import org.apache.hadoop.util.ReflectionUtils; 039import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature; 040import org.slf4j.Logger; 041import org.slf4j.LoggerFactory; 042 043import org.apache.hbase.thirdparty.com.google.gson.Gson; 044import org.apache.hbase.thirdparty.com.google.gson.JsonElement; 045import org.apache.hbase.thirdparty.com.google.gson.JsonObject; 046 047/** 048 * A ClusterManager implementation designed to control Cloudera Manager (http://www.cloudera.com) 049 * clusters via REST API. This API uses HTTP GET requests against the cluster manager server to 050 * retrieve information and POST/PUT requests to perform actions. As a simple example, to retrieve a 051 * list of hosts from a CM server with login credentials admin:admin, a simple curl command would be 052 * curl -X POST -H "Content-Type:application/json" -u admin:admin \ 053 * "http://this.is.my.server.com:7180/api/v8/hosts" 054 * 055 * This command would return a JSON result, which would need to be parsed to retrieve relevant 056 * information. This action and many others are covered by this class. 057 * 058 * A note on nomenclature: while the ClusterManager interface uses a ServiceType enum when 059 * referring to things like RegionServers and DataNodes, cluster managers often use different 060 * terminology. As an example, Cloudera Manager (http://www.cloudera.com) would refer to a 061 * RegionServer as a "role" of the HBase "service." It would further refer to "hbase" as the 062 * "serviceType." Apache Ambari (http://ambari.apache.org) would call the RegionServer a 063 * "component" of the HBase "service." 064 * 065 * This class will defer to the ClusterManager terminology in methods that it implements from 066 * that interface, but uses Cloudera Manager's terminology when dealing with its API directly. 067 */ 068public class RESTApiClusterManager extends Configured implements ClusterManager { 069 // Properties that need to be in the Configuration object to interact with the REST API cluster 070 // manager. Most easily defined in hbase-site.xml, but can also be passed on the command line. 071 private static final String REST_API_CLUSTER_MANAGER_HOSTNAME = 072 "hbase.it.clustermanager.restapi.hostname"; 073 private static final String REST_API_CLUSTER_MANAGER_USERNAME = 074 "hbase.it.clustermanager.restapi.username"; 075 private static final String REST_API_CLUSTER_MANAGER_PASSWORD = 076 "hbase.it.clustermanager.restapi.password"; 077 private static final String REST_API_CLUSTER_MANAGER_CLUSTER_NAME = 078 "hbase.it.clustermanager.restapi.clustername"; 079 080 private static final Gson GSON = GsonUtil.createGson().create(); 081 082 // Some default values for the above properties. 083 private static final String DEFAULT_SERVER_HOSTNAME = "http://localhost:7180"; 084 private static final String DEFAULT_SERVER_USERNAME = "admin"; 085 private static final String DEFAULT_SERVER_PASSWORD = "admin"; 086 private static final String DEFAULT_CLUSTER_NAME = "Cluster 1"; 087 088 // Fields for the hostname, username, password, and cluster name of the cluster management server 089 // to be used. 090 private String serverHostname; 091 private String serverUsername; 092 private String serverPassword; 093 private String clusterName; 094 095 // Each version of Cloudera Manager supports a particular API versions. Version 6 of this API 096 // provides all the features needed by this class. 097 private static final String API_VERSION = "v6"; 098 099 // Client instances are expensive, so use the same one for all our REST queries. 100 private Client client = ClientBuilder.newClient(); 101 102 // An instance of HBaseClusterManager is used for methods like the kill, resume, and suspend 103 // because cluster managers don't tend to implement these operations. 104 private ClusterManager hBaseClusterManager; 105 106 private static final Logger LOG = LoggerFactory.getLogger(RESTApiClusterManager.class); 107 108 RESTApiClusterManager() { 109 hBaseClusterManager = ReflectionUtils.newInstance(HBaseClusterManager.class, 110 new IntegrationTestingUtility().getConfiguration()); 111 } 112 113 @Override 114 public void setConf(Configuration conf) { 115 super.setConf(conf); 116 if (conf == null) { 117 // Configured gets passed null before real conf. Why? I don't know. 118 return; 119 } 120 serverHostname = conf.get(REST_API_CLUSTER_MANAGER_HOSTNAME, DEFAULT_SERVER_HOSTNAME); 121 serverUsername = conf.get(REST_API_CLUSTER_MANAGER_USERNAME, DEFAULT_SERVER_USERNAME); 122 serverPassword = conf.get(REST_API_CLUSTER_MANAGER_PASSWORD, DEFAULT_SERVER_PASSWORD); 123 clusterName = conf.get(REST_API_CLUSTER_MANAGER_CLUSTER_NAME, DEFAULT_CLUSTER_NAME); 124 125 // Add filter to Client instance to enable server authentication. 126 client.register(HttpAuthenticationFeature.basic(serverUsername, serverPassword)); 127 } 128 129 @Override 130 public void start(ServiceType service, String hostname, int port) throws IOException { 131 performClusterManagerCommand(service, hostname, RoleCommand.START); 132 } 133 134 @Override 135 public void stop(ServiceType service, String hostname, int port) throws IOException { 136 performClusterManagerCommand(service, hostname, RoleCommand.STOP); 137 } 138 139 @Override 140 public void restart(ServiceType service, String hostname, int port) throws IOException { 141 performClusterManagerCommand(service, hostname, RoleCommand.RESTART); 142 } 143 144 @Override 145 public boolean isRunning(ServiceType service, String hostname, int port) throws IOException { 146 String serviceName = getServiceName(roleServiceType.get(service)); 147 String hostId = getHostId(hostname); 148 String roleState = getRoleState(serviceName, service.toString(), hostId); 149 String healthSummary = getHealthSummary(serviceName, service.toString(), hostId); 150 boolean isRunning = false; 151 152 // Use Yoda condition to prevent NullPointerException. roleState will be null if the "service 153 // type" does not exist on the specified hostname. 154 if ("STARTED".equals(roleState) && "GOOD".equals(healthSummary)) { 155 isRunning = true; 156 } 157 158 return isRunning; 159 } 160 161 @Override 162 public void kill(ServiceType service, String hostname, int port) throws IOException { 163 hBaseClusterManager.kill(service, hostname, port); 164 } 165 166 @Override 167 public void suspend(ServiceType service, String hostname, int port) throws IOException { 168 hBaseClusterManager.suspend(service, hostname, port); 169 } 170 171 @Override 172 public void resume(ServiceType service, String hostname, int port) throws IOException { 173 hBaseClusterManager.resume(service, hostname, port); 174 } 175 176 177 // Convenience method to execute command against role on hostname. Only graceful commands are 178 // supported since cluster management APIs don't tend to let you SIGKILL things. 179 private void performClusterManagerCommand(ServiceType role, String hostname, RoleCommand command) 180 throws IOException { 181 LOG.info("Performing " + command + " command against " + role + " on " + hostname + "..."); 182 String serviceName = getServiceName(roleServiceType.get(role)); 183 String hostId = getHostId(hostname); 184 String roleName = getRoleName(serviceName, role.toString(), hostId); 185 doRoleCommand(serviceName, roleName, command); 186 } 187 188 // Performing a command (e.g. starting or stopping a role) requires a POST instead of a GET. 189 private void doRoleCommand(String serviceName, String roleName, RoleCommand roleCommand) { 190 URI uri = UriBuilder.fromUri(serverHostname) 191 .path("api") 192 .path(API_VERSION) 193 .path("clusters") 194 .path(clusterName) 195 .path("services") 196 .path(serviceName) 197 .path("roleCommands") 198 .path(roleCommand.toString()) 199 .build(); 200 String body = "{ \"items\": [ \"" + roleName + "\" ] }"; 201 LOG.info("Executing POST against " + uri + " with body " + body + "..."); 202 WebTarget webTarget = client.target(uri); 203 Invocation.Builder invocationBuilder = webTarget.request(MediaType.APPLICATION_JSON); 204 Response response = invocationBuilder.post(Entity.json(body)); 205 int statusCode = response.getStatus(); 206 if (statusCode != Response.Status.OK.getStatusCode()) { 207 throw new HTTPException(statusCode); 208 } 209 } 210 211 // Possible healthSummary values include "GOOD" and "BAD." 212 private String getHealthSummary(String serviceName, String roleType, String hostId) 213 throws IOException { 214 return getRolePropertyValue(serviceName, roleType, hostId, "healthSummary"); 215 } 216 217 // This API uses a hostId to execute host-specific commands; get one from a hostname. 218 private String getHostId(String hostname) throws IOException { 219 String hostId = null; 220 221 URI uri = 222 UriBuilder.fromUri(serverHostname).path("api").path(API_VERSION).path("hosts").build(); 223 JsonElement hosts = getJsonNodeFromURIGet(uri); 224 if (hosts != null) { 225 // Iterate through the list of hosts, stopping once you've reached the requested hostname. 226 for (JsonElement host : hosts.getAsJsonArray()) { 227 if (host.getAsJsonObject().get("hostname").getAsString().equals(hostname)) { 228 hostId = host.getAsJsonObject().get("hostId").getAsString(); 229 break; 230 } 231 } 232 } else { 233 hostId = null; 234 } 235 236 return hostId; 237 } 238 239 // Execute GET against URI, returning a JsonNode object to be traversed. 240 private JsonElement getJsonNodeFromURIGet(URI uri) throws IOException { 241 LOG.info("Executing GET against " + uri + "..."); 242 WebTarget webTarget = client.target(uri); 243 Invocation.Builder invocationBuilder = webTarget.request(MediaType.APPLICATION_JSON); 244 Response response = invocationBuilder.get(); 245 int statusCode = response.getStatus(); 246 if (statusCode != Response.Status.OK.getStatusCode()) { 247 throw new HTTPException(statusCode); 248 } 249 // This API folds information as the value to an "items" attribute. 250 return GSON.toJsonTree(response.readEntity(String.class)).getAsJsonObject().get("items"); 251 } 252 253 // This API assigns a unique role name to each host's instance of a role. 254 private String getRoleName(String serviceName, String roleType, String hostId) 255 throws IOException { 256 return getRolePropertyValue(serviceName, roleType, hostId, "name"); 257 } 258 259 // Get the value of a property from a role on a particular host. 260 private String getRolePropertyValue(String serviceName, String roleType, String hostId, 261 String property) throws IOException { 262 String roleValue = null; 263 URI uri = UriBuilder.fromUri(serverHostname).path("api").path(API_VERSION).path("clusters") 264 .path(clusterName).path("services").path(serviceName).path("roles").build(); 265 JsonElement roles = getJsonNodeFromURIGet(uri); 266 if (roles != null) { 267 // Iterate through the list of roles, stopping once the requested one is found. 268 for (JsonElement role : roles.getAsJsonArray()) { 269 JsonObject roleObj = role.getAsJsonObject(); 270 if (roleObj.get("hostRef").getAsJsonObject().get("hostId").getAsString().equals(hostId) && 271 roleObj.get("type").getAsString().toLowerCase(Locale.ROOT) 272 .equals(roleType.toLowerCase(Locale.ROOT))) { 273 roleValue = roleObj.get(property).getAsString(); 274 break; 275 } 276 } 277 } 278 279 return roleValue; 280 } 281 282 // Possible roleState values include "STARTED" and "STOPPED." 283 private String getRoleState(String serviceName, String roleType, String hostId) 284 throws IOException { 285 return getRolePropertyValue(serviceName, roleType, hostId, "roleState"); 286 } 287 288 // Convert a service (e.g. "HBASE," "HDFS") into a service name (e.g. "HBASE-1," "HDFS-1"). 289 private String getServiceName(Service service) throws IOException { 290 String serviceName = null; 291 URI uri = UriBuilder.fromUri(serverHostname).path("api").path(API_VERSION).path("clusters") 292 .path(clusterName).path("services").build(); 293 JsonElement services = getJsonNodeFromURIGet(uri); 294 if (services != null) { 295 // Iterate through the list of services, stopping once the requested one is found. 296 for (JsonElement serviceEntry : services.getAsJsonArray()) { 297 if (serviceEntry.getAsJsonObject().get("type").getAsString().equals(service.toString())) { 298 serviceName = serviceEntry.getAsJsonObject().get("name").getAsString(); 299 break; 300 } 301 } 302 } 303 304 return serviceName; 305 } 306 307 /* 308 * Some enums to guard against bad calls. 309 */ 310 311 // The RoleCommand enum is used by the doRoleCommand method to guard against non-existent methods 312 // being invoked on a given role. 313 // TODO: Integrate zookeeper and hdfs related failure injections (Ref: HBASE-14261). 314 private enum RoleCommand { 315 START, STOP, RESTART; 316 317 // APIs tend to take commands in lowercase, so convert them to save the trouble later. 318 @Override 319 public String toString() { 320 return name().toLowerCase(Locale.ROOT); 321 } 322 } 323 324 // ClusterManager methods take a "ServiceType" object (e.g. "HBASE_MASTER," "HADOOP_NAMENODE"). 325 // These "service types," which cluster managers call "roles" or "components," need to be mapped 326 // to their corresponding service (e.g. "HBase," "HDFS") in order to be controlled. 327 private static Map<ServiceType, Service> roleServiceType = new HashMap<>(); 328 static { 329 roleServiceType.put(ServiceType.HADOOP_NAMENODE, Service.HDFS); 330 roleServiceType.put(ServiceType.HADOOP_DATANODE, Service.HDFS); 331 roleServiceType.put(ServiceType.HADOOP_JOBTRACKER, Service.MAPREDUCE); 332 roleServiceType.put(ServiceType.HADOOP_TASKTRACKER, Service.MAPREDUCE); 333 roleServiceType.put(ServiceType.HBASE_MASTER, Service.HBASE); 334 roleServiceType.put(ServiceType.HBASE_REGIONSERVER, Service.HBASE); 335 } 336 337 private enum Service { 338 HBASE, HDFS, MAPREDUCE 339 } 340}