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}