View Javadoc

1   /**
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  
19  package org.apache.hadoop.hbase;
20  
21  import com.fasterxml.jackson.databind.JsonNode;
22  import com.fasterxml.jackson.databind.ObjectMapper;
23  import com.sun.jersey.api.client.Client;
24  import com.sun.jersey.api.client.ClientResponse;
25  import com.sun.jersey.api.client.filter.HTTPBasicAuthFilter;
26  
27  import org.apache.commons.logging.Log;
28  import org.apache.commons.logging.LogFactory;
29  import org.apache.hadoop.conf.Configuration;
30  import org.apache.hadoop.conf.Configured;
31  import org.apache.hadoop.util.ReflectionUtils;
32  
33  import javax.ws.rs.core.MediaType;
34  import javax.ws.rs.core.Response;
35  import javax.ws.rs.core.UriBuilder;
36  import javax.xml.ws.http.HTTPException;
37  import java.io.IOException;
38  import java.net.URI;
39  import java.util.HashMap;
40  import java.util.Locale;
41  import java.util.Map;
42  
43  /**
44   * A ClusterManager implementation designed to control Cloudera Manager (http://www.cloudera.com)
45   * clusters via REST API. This API uses HTTP GET requests against the cluster manager server to
46   * retrieve information and POST/PUT requests to perform actions. As a simple example, to retrieve a
47   * list of hosts from a CM server with login credentials admin:admin, a simple curl command would be
48   *     curl -X POST -H "Content-Type:application/json" -u admin:admin \
49   *         "http://this.is.my.server.com:7180/api/v8/hosts"
50   *
51   * This command would return a JSON result, which would need to be parsed to retrieve relevant
52   * information. This action and many others are covered by this class.
53   *
54   * A note on nomenclature: while the ClusterManager interface uses a ServiceType enum when
55   * referring to things like RegionServers and DataNodes, cluster managers often use different
56   * terminology. As an example, Cloudera Manager (http://www.cloudera.com) would refer to a
57   * RegionServer as a "role" of the HBase "service." It would further refer to "hbase" as the
58   * "serviceType." Apache Ambari (http://ambari.apache.org) would call the RegionServer a
59   * "component" of the HBase "service."
60   *
61   * This class will defer to the ClusterManager terminology in methods that it implements from
62   * that interface, but uses Cloudera Manager's terminology when dealing with its API directly.
63   */
64  public class RESTApiClusterManager extends Configured implements ClusterManager {
65    // Properties that need to be in the Configuration object to interact with the REST API cluster
66    // manager. Most easily defined in hbase-site.xml, but can also be passed on the command line.
67    private static final String REST_API_CLUSTER_MANAGER_HOSTNAME =
68        "hbase.it.clustermanager.restapi.hostname";
69    private static final String REST_API_CLUSTER_MANAGER_USERNAME =
70        "hbase.it.clustermanager.restapi.username";
71    private static final String REST_API_CLUSTER_MANAGER_PASSWORD =
72        "hbase.it.clustermanager.restapi.password";
73    private static final String REST_API_CLUSTER_MANAGER_CLUSTER_NAME =
74        "hbase.it.clustermanager.restapi.clustername";
75    private static final String REST_API_DELEGATE_CLUSTER_MANAGER =
76      "hbase.it.clustermanager.restapi.delegate";
77  
78    // Some default values for the above properties.
79    private static final String DEFAULT_SERVER_HOSTNAME = "http://localhost:7180";
80    private static final String DEFAULT_SERVER_USERNAME = "admin";
81    private static final String DEFAULT_SERVER_PASSWORD = "admin";
82    private static final String DEFAULT_CLUSTER_NAME = "Cluster 1";
83  
84    // Fields for the hostname, username, password, and cluster name of the cluster management server
85    // to be used.
86    private String serverHostname;
87    private String clusterName;
88  
89    // Each version of Cloudera Manager supports a particular API versions. Version 6 of this API
90    // provides all the features needed by this class.
91    private static final String API_VERSION = "v6";
92  
93    // Client instances are expensive, so use the same one for all our REST queries.
94    private Client client = Client.create();
95  
96    // An instance of HBaseClusterManager is used for methods like the kill, resume, and suspend
97    // because cluster managers don't tend to implement these operations.
98    private ClusterManager hBaseClusterManager;
99  
100   private static final Log LOG = LogFactory.getLog(RESTApiClusterManager.class);
101 
102   RESTApiClusterManager() { }
103 
104   @Override
105   public void setConf(Configuration conf) {
106     super.setConf(conf);
107     if (conf == null) {
108       // Configured gets passed null before real conf. Why? I don't know.
109       return;
110     }
111 
112     final Class<? extends ClusterManager> clazz = conf.getClass(REST_API_DELEGATE_CLUSTER_MANAGER,
113       HBaseClusterManager.class, ClusterManager.class);
114     hBaseClusterManager = ReflectionUtils.newInstance(clazz, conf);
115 
116     serverHostname = conf.get(REST_API_CLUSTER_MANAGER_HOSTNAME, DEFAULT_SERVER_HOSTNAME);
117     clusterName = conf.get(REST_API_CLUSTER_MANAGER_CLUSTER_NAME, DEFAULT_CLUSTER_NAME);
118     String serverUsername = conf.get(REST_API_CLUSTER_MANAGER_USERNAME, DEFAULT_SERVER_USERNAME);
119     String serverPassword = conf.get(REST_API_CLUSTER_MANAGER_PASSWORD, DEFAULT_SERVER_PASSWORD);
120 
121     // Add filter to Client instance to enable server authentication.
122     client.addFilter(new HTTPBasicAuthFilter(serverUsername, serverPassword));
123   }
124 
125   @Override
126   public void start(ServiceType service, String hostname, int port) throws IOException {
127     performClusterManagerCommand(service, hostname, RoleCommand.START);
128   }
129 
130   @Override
131   public void stop(ServiceType service, String hostname, int port) throws IOException {
132     performClusterManagerCommand(service, hostname, RoleCommand.STOP);
133   }
134 
135   @Override
136   public void restart(ServiceType service, String hostname, int port) throws IOException {
137     performClusterManagerCommand(service, hostname, RoleCommand.RESTART);
138   }
139 
140   @Override
141   public boolean isRunning(ServiceType service, String hostname, int port) throws IOException {
142     String serviceName = getServiceName(roleServiceType.get(service));
143     String hostId = getHostId(hostname);
144     String roleState = getRoleState(serviceName, service.toString(), hostId);
145     String healthSummary = getHealthSummary(serviceName, service.toString(), hostId);
146     boolean isRunning = false;
147 
148     // Use Yoda condition to prevent NullPointerException. roleState will be null if the "service
149     // type" does not exist on the specified hostname.
150     if ("STARTED".equals(roleState) && "GOOD".equals(healthSummary)) {
151       isRunning = true;
152     }
153 
154     return isRunning;
155   }
156 
157   @Override
158   public void kill(ServiceType service, String hostname, int port) throws IOException {
159     hBaseClusterManager.kill(service, hostname, port);
160   }
161 
162   @Override
163   public void suspend(ServiceType service, String hostname, int port) throws IOException {
164     hBaseClusterManager.suspend(service, hostname, port);
165   }
166 
167   @Override
168   public void resume(ServiceType service, String hostname, int port) throws IOException {
169     hBaseClusterManager.resume(service, hostname, port);
170   }
171 
172 
173   // Convenience method to execute command against role on hostname. Only graceful commands are
174   // supported since cluster management APIs don't tend to let you SIGKILL things.
175   private void performClusterManagerCommand(ServiceType role, String hostname, RoleCommand command)
176       throws IOException {
177     LOG.info("Performing " + command + " command against " + role + " on " + hostname + "...");
178     String serviceName = getServiceName(roleServiceType.get(role));
179     String hostId = getHostId(hostname);
180     String roleName = getRoleName(serviceName, role.toString(), hostId);
181     doRoleCommand(serviceName, roleName, command);
182   }
183 
184   // Performing a command (e.g. starting or stopping a role) requires a POST instead of a GET.
185   private void doRoleCommand(String serviceName, String roleName, RoleCommand roleCommand) {
186     URI uri = UriBuilder.fromUri(serverHostname)
187         .path("api")
188         .path(API_VERSION)
189         .path("clusters")
190         .path(clusterName)
191         .path("services")
192         .path(serviceName)
193         .path("roleCommands")
194         .path(roleCommand.toString())
195         .build();
196     String body = "{ \"items\": [ \"" + roleName + "\" ] }";
197     LOG.info("Executing POST against " + uri + " with body " + body + "...");
198     ClientResponse response = client.resource(uri)
199         .type(MediaType.APPLICATION_JSON)
200         .post(ClientResponse.class, body);
201 
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     ClientResponse response = client.resource(uri)
243         .accept(MediaType.APPLICATION_JSON_TYPE)
244         .get(ClientResponse.class);
245 
246     int statusCode = response.getStatus();
247     if (statusCode != Response.Status.OK.getStatusCode()) {
248       throw new HTTPException(statusCode);
249     }
250     // This API folds information as the value to an "items" attribute.
251     return new ObjectMapper().readTree(response.getEntity(String.class)).get("items");
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<ServiceType, Service>();
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 }