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 java.io.File;
22  import java.io.IOException;
23  import java.util.Map;
24  
25  import org.apache.commons.lang.StringUtils;
26  import org.apache.hadoop.classification.InterfaceAudience;
27  import org.apache.hadoop.conf.Configuration;
28  import org.apache.hadoop.hbase.HBaseClusterManager.CommandProvider.Operation;
29  import org.apache.hadoop.hbase.util.Pair;
30  import org.apache.hadoop.util.Shell;
31  
32  /**
33   * A default cluster manager for HBase. Uses SSH, and hbase shell scripts
34   * to manage the cluster. Assumes Unix-like commands are available like 'ps',
35   * 'kill', etc. Also assumes the user running the test has enough "power" to start & stop
36   * servers on the remote machines (for example, the test user could be the same user as the
37   * user the daemon isrunning as)
38   */
39  @InterfaceAudience.Private
40  public class HBaseClusterManager extends ClusterManager {
41    private String sshUserName;
42    private String sshOptions;
43  
44    /**
45     * The command format that is used to execute the remote command. Arguments:
46     * 1 SSH options, 2 user name , 3 "@" if username is set, 4 host, 5 original command.
47     */
48    private static final String DEFAULT_TUNNEL_CMD = "/usr/bin/ssh %1$s %2$s%3$s%4$s \"%5$s\"";
49    private String tunnelCmd;
50  
51    @Override
52    public void setConf(Configuration conf) {
53      super.setConf(conf);
54      if (conf == null) {
55        // Configured gets passed null before real conf. Why? I don't know.
56        return;
57      }
58      sshUserName = conf.get("hbase.it.clustermanager.ssh.user", "");
59      String extraSshOptions = conf.get("hbase.it.clustermanager.ssh.opts", "");
60      sshOptions = System.getenv("HBASE_SSH_OPTS");
61      if (!extraSshOptions.isEmpty()) {
62        sshOptions = StringUtils.join(new Object[] { sshOptions, extraSshOptions }, " ");
63      }
64      sshOptions = (sshOptions == null) ? "" : sshOptions;
65      tunnelCmd = conf.get("hbase.it.clustermanager.ssh.cmd", DEFAULT_TUNNEL_CMD);
66      LOG.info("Running with SSH user [" + sshUserName + "] and options [" + sshOptions + "]");
67    }
68  
69    /**
70     * Executes commands over SSH
71     */
72    protected class RemoteShell extends Shell.ShellCommandExecutor {
73      private String hostname;
74  
75      public RemoteShell(String hostname, String[] execString, File dir, Map<String, String> env,
76          long timeout) {
77        super(execString, dir, env, timeout);
78        this.hostname = hostname;
79      }
80  
81      public RemoteShell(String hostname, String[] execString, File dir, Map<String, String> env) {
82        super(execString, dir, env);
83        this.hostname = hostname;
84      }
85  
86      public RemoteShell(String hostname, String[] execString, File dir) {
87        super(execString, dir);
88        this.hostname = hostname;
89      }
90  
91      public RemoteShell(String hostname, String[] execString) {
92        super(execString);
93        this.hostname = hostname;
94      }
95  
96      public String[] getExecString() {
97        String at = sshUserName.isEmpty() ? "" : "@";
98        String remoteCmd = StringUtils.join(super.getExecString(), " ");
99        String cmd = String.format(tunnelCmd, sshOptions, sshUserName, at, hostname, remoteCmd);
100       LOG.info("Executing full command [" + cmd + "]");
101       return new String[] { "/usr/bin/env", "bash", "-c", cmd };
102     }
103 
104     @Override
105     public void execute() throws IOException {
106       super.execute();
107     }
108   }
109 
110   /**
111    * Provides command strings for services to be executed by Shell. CommandProviders are
112    * pluggable, and different deployments(windows, bigtop, etc) can be managed by
113    * plugging-in custom CommandProvider's or ClusterManager's.
114    */
115   static abstract class CommandProvider {
116 
117     enum Operation {
118       START, STOP, RESTART
119     }
120 
121     public abstract String getCommand(ServiceType service, Operation op);
122 
123     public String isRunningCommand(ServiceType service) {
124       return findPidCommand(service);
125     }
126 
127     protected String findPidCommand(ServiceType service) {
128       String servicePathFilter = "";
129       if (service == ServiceType.HBASE_MASTER || service == ServiceType.HBASE_REGIONSERVER) {
130         servicePathFilter = " | grep hbase";
131       }
132       return String.format("ps ux | grep %s %s | grep -v grep | tr -s ' ' | cut -d ' ' -f2",
133           service, servicePathFilter);
134     }
135 
136     public String signalCommand(ServiceType service, String signal) {
137       return String.format("%s | xargs kill -s %s", findPidCommand(service), signal);
138     }
139   }
140 
141   /**
142    * CommandProvider to manage the service using bin/hbase-* scripts
143    */
144   static class HBaseShellCommandProvider extends CommandProvider {
145     private String getHBaseHome() {
146       return System.getenv("HBASE_HOME");
147     }
148 
149     private String getConfig() {
150       String confDir = System.getenv("HBASE_CONF_DIR");
151       if (confDir != null) {
152         return String.format("--config %s", confDir);
153       }
154       return "";
155     }
156 
157     @Override
158     public String getCommand(ServiceType service, Operation op) {
159       return String.format("%s/bin/hbase-daemon.sh %s %s %s", getHBaseHome(), getConfig(),
160           op.toString().toLowerCase(), service);
161     }
162   }
163 
164   public HBaseClusterManager() {
165     super();
166   }
167 
168   protected CommandProvider getCommandProvider(ServiceType service) {
169     //TODO: make it pluggable, or auto-detect the best command provider, should work with
170     //hadoop daemons as well
171     return new HBaseShellCommandProvider();
172   }
173 
174   /**
175    * Execute the given command on the host using SSH
176    * @return pair of exit code and command output
177    * @throws IOException if something goes wrong.
178    */
179   private Pair<Integer, String> exec(String hostname, String... cmd) throws IOException {
180     LOG.info("Executing remote command: " + StringUtils.join(cmd, " ") + " , hostname:" + hostname);
181 
182     RemoteShell shell = new RemoteShell(hostname, cmd);
183     shell.execute();
184 
185     LOG.info("Executed remote command, exit code:" + shell.getExitCode()
186         + " , output:" + shell.getOutput());
187 
188     return new Pair<Integer, String>(shell.getExitCode(), shell.getOutput());
189   }
190 
191   private void exec(String hostname, ServiceType service, Operation op) throws IOException {
192     exec(hostname, getCommandProvider(service).getCommand(service, op));
193   }
194 
195   @Override
196   public void start(ServiceType service, String hostname) throws IOException {
197     exec(hostname, service, Operation.START);
198   }
199 
200   @Override
201   public void stop(ServiceType service, String hostname) throws IOException {
202     exec(hostname, service, Operation.STOP);
203   }
204 
205   @Override
206   public void restart(ServiceType service, String hostname) throws IOException {
207     exec(hostname, service, Operation.RESTART);
208   }
209 
210   @Override
211   public void signal(ServiceType service, String signal, String hostname) throws IOException {
212     exec(hostname, getCommandProvider(service).signalCommand(service, signal));
213   }
214 
215   @Override
216   public boolean isRunning(ServiceType service, String hostname) throws IOException {
217     String ret = exec(hostname, getCommandProvider(service).isRunningCommand(service))
218         .getSecond();
219     return ret.length() > 0;
220   }
221 
222 }