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