001    /**
002     * 
003     */
004    package de.jw.cloud42.core.remoting;
005    
006    import java.io.BufferedReader;
007    import java.io.ByteArrayOutputStream;
008    import java.io.IOException;
009    import java.io.InputStream;
010    import java.io.InputStreamReader;
011    import java.io.OutputStream;
012    import java.text.DateFormat;
013    import java.text.SimpleDateFormat;
014    import java.util.Arrays;
015    import java.util.List;
016    import java.util.logging.Logger;
017    
018    import org.hibernate.type.EmbeddedComponentType;
019    
020    import com.trilead.ssh2.Connection;
021    import com.trilead.ssh2.SCPClient;
022    import com.trilead.ssh2.Session;
023    import com.trilead.ssh2.StreamGobbler;
024    
025    import de.jw.cloud42.core.domain.AwsCredentials;
026    import de.jw.cloud42.core.domain.Instance;
027    import de.jw.cloud42.core.domain.RemoteResult;
028    import de.jw.cloud42.core.domain.Settings;
029    import de.jw.cloud42.core.service.Cloud42BaseFunctions;
030    
031    /**
032     * 
033     * Class providing methods for remote control of AMI instances by using SSH.
034     * Also contains methods for up- and downloading files .
035     * 
036     * @author fbitzer
037     * 
038     */
039    public class RemoteControl {
040    
041            /**
042             * Folder for files temporary stored on a AMI instance.
043             */
044            private final String TEMP_REMOTE_FOLDER = "/tmp";
045            
046            /**
047             * Filename for temporary batch files.
048             */
049            private final String TEMP_BATCH_FILENAME = "tmp.sh";
050            
051            
052            /**
053             * Executes the given command on the given remote host.
054             * @param host Hostname or IP address of the target AMI instance.
055             * @param key RSA private key as String.
056             * @param command the command to execute.
057             *
058             * @return RemoteResult object containing the output of the command.
059             */
060            public RemoteResult executeCommand(String host, String key, String command) {
061    
062                    Connection conn = connect(host,key);
063                    
064                    RemoteResult result =  exec(conn,command);
065                    
066                    /* Close the connection */
067    
068                    if (conn != null) conn.close();
069    
070                    return result;
071                    
072            }
073                    
074            /**
075             * Executes a batch file / shell script on the instance.
076             * @param host Hostname or IP address of the target AMI instance.
077             * @param key RSA private key as String.
078             * @param batchData Shellscript to execute.
079             * @return RemoteResult object containing the output of the script.
080             */
081            public RemoteResult executeBatch(String host, String key, byte[] batchData) {
082                    
083                    Connection conn = connect(host, key);
084                    
085                    RemoteResult result = new RemoteResult();
086                    
087                    try {
088                            
089                            if (conn == null) {
090                                    throw new IOException("Connection failed.");
091                            }
092                    
093                            //Upload batch file                     
094                            RemoteResult r1 = doUpload(conn, batchData, TEMP_BATCH_FILENAME, TEMP_REMOTE_FOLDER);
095                            
096                            if (r1.getExceptionMessage() != null) {
097                                    throw new Exception("Uploading batch file failed due to an exception: " + r1.getExceptionMessage());
098                            }
099                            
100                            //execute the uploaded batch file
101                            
102                            //set rights before executing
103                            r1 = exec(conn, "chmod 774 " + TEMP_REMOTE_FOLDER + "/" + TEMP_BATCH_FILENAME);
104                            
105    
106                            if (!r1.getStdErr().equals("")) {
107                                    throw new Exception("Setting rights for batch file failed: " + r1.getStdErr());
108                            }
109                            if (r1.getExceptionMessage() != null) {
110                                    throw new Exception("Setting rights for batch file failed due to an exception: " + r1.getExceptionMessage());
111                            }
112                            
113                            result =  exec(conn, TEMP_REMOTE_FOLDER + "/" + TEMP_BATCH_FILENAME);
114                            
115                            
116                    } catch (Exception ex){
117                            
118                            result.setExceptionMessage(ex.getMessage());
119                            
120                    }
121                    
122                    //now delete temp file
123                    
124                    exec(conn, "rm -f " + TEMP_REMOTE_FOLDER + "/" + TEMP_BATCH_FILENAME);
125                    
126                    /* Close the connection */
127    
128                    if (conn != null) conn.close();
129    
130                    return result;
131                    
132                            
133            }
134            
135            
136            /**
137             * Uploads a file to an AMI instance.
138             * @param host Hostname or IP address of remote host.
139             * @param key RSA private key to use for authentification.
140             * @param targetDir Target folder on remote machine.
141             * @param targetFilename Target filename on remote machine.
142             * @param fileData The uploaded file.
143             * @return a RemoteResult encaplsulating error messages.
144             */
145            public RemoteResult uploadFile(String host, String key, String targetDir, String targetFilename, byte[] fileData){
146                    
147                    Connection conn = connect(host, key);
148                    
149                    RemoteResult result = new RemoteResult();
150                    
151                    try {
152                            
153                            if (conn == null) {
154                                    throw new IOException("Connection failed.");
155                            }
156                            
157                            //create target dir at first
158                            exec(conn, "mkdir -p " + targetDir);
159                            
160                            result = doUpload(conn, fileData, targetFilename, targetDir);
161                            
162                    } catch (Exception ex){
163                            
164                            ex.printStackTrace();
165                            
166                            result.setExceptionMessage(ex.getMessage());
167                            
168                    }
169                    
170                    if (conn != null) conn.close();
171    
172                    return result;
173            }
174            
175            
176            /**
177             * Uploads a file to an AMI instance from a given URL.
178             * Can be used to transfer files from S3 to an EC2 instance.
179             * Uses wget for file transfer.
180             * 
181             * @param host Hostname or IP address of remote host.
182             * @param key RSA private key to use for authentification.
183             * @param targetDir Target folder on remote machine.
184             * @param targetFilename Target filename on remote machine.
185             * @param url URL of file to transfer.
186             * @return a RemoteResult encaplsulating the output of the wget command.
187             */
188            public RemoteResult uploadFileFromURL(String host, String key, String targetDir, String targetFilename, String url){
189                    
190                    Connection conn = connect(host, key);
191                    
192                    RemoteResult result = new RemoteResult();
193                    
194                    try {
195                            
196                            if (conn == null) {
197                                    throw new IOException("Connection failed.");
198                            }
199                    
200                            
201                                    
202                            exec(conn, "mkdir -p " + targetDir);
203                            
204                            result = exec(conn, "wget " + url + " -O " + targetDir + "/" + targetFilename);
205                            
206                    } catch (Exception ex){
207                            
208                            ex.printStackTrace();
209                            
210                            
211                            result.setExceptionMessage(ex.getMessage());
212                            
213                    }
214                    
215                    if (conn != null) conn.close();
216    
217                    return result;
218            }
219            
220            /**
221             * Download a file from a remote host.
222             * 
223             * @param host Hostname.
224             * @param key RSA key to use.
225             * @param remoteFileName Absolute filename of file to download.
226             * @return File as byte array.
227             */
228            public byte[] downloadFile(String host, String key, String remoteFileName){
229                    
230                    Connection conn = connect(host, key);
231                    
232                    try {
233                            
234                            if (conn == null) {
235                                    throw new IOException("Connection failed.");
236                            }
237                    
238                            /* Create a SCPClient */
239                            SCPClient scp = conn.createSCPClient();
240                            
241                            ByteArrayOutputStream out = new ByteArrayOutputStream();
242    
243                            scp.get(remoteFileName, out);
244                            
245                            if (conn != null) conn.close();
246    
247                            
248                            return out.toByteArray();
249                            
250                    
251                    } catch (Exception ex){
252                            
253                            ex.printStackTrace();
254                            
255                            if (conn != null) conn.close();
256    
257                            
258                            return null;
259                            
260                    }
261                    
262            }
263            
264            /**
265             * Bundle a new AMI from an existing one by executing the neccessary statements.
266             * 
267             * @param dnsName
268             * @param key
269             * @param credentials
270             * @param targetBucket
271             * @param newImageName
272             * @param use64Bit
273             * @param notifyWhenFinished
274             * @param messageText
275             * @param messageInfo
276             * @param keyFile
277             * @param certFile
278             * @return RemoteResult with ExceptionMessage in case of error. Output of the single statements is NOT transferred.
279             */
280            public RemoteResult bundleImage(String dnsName, String key, AwsCredentials credentials, 
281                                                                    String targetBucket, String newImageName, boolean use64Bit, 
282                                                                    boolean notifyWhenFinished, String topic, String messageText, String messageInfo,
283                                                                    byte[] keyFile, byte[] certFile){
284                    
285                    Connection conn = connect(dnsName, key);
286                    
287                    RemoteResult result = new RemoteResult();
288                    result.setStdErr("");
289                    result.setStdOut("");
290                    result.setExitCode(0);
291                    
292                    try {
293                            
294                            if (conn == null) {
295                                    throw new IOException("Connection failed.");
296                            }
297                    
298                            //Copy pk file and certificate
299                            
300                            /* Create a SCPClient */
301                            SCPClient scp = conn.createSCPClient();
302                            
303                            //private key
304                            scp.put(keyFile, "pk.pem", "//mnt");
305                            
306                            //certificate
307                            scp.put(certFile, "cert.pem", "//mnt");
308                            
309                            String architecture;
310                            if (use64Bit) {
311                                    architecture = "x86_64";
312                            } else {
313                                    architecture = "i386";
314                            }
315                            
316                            RemoteResult tmpResult;
317                            
318                            //bundle AMI
319                            tmpResult = exec(conn,"ec2-bundle-vol -d //mnt -k //mnt//pk.pem -c //mnt//cert.pem -u " 
320                                            + credentials.getUserID() + " -r " + architecture + " -p " + newImageName);
321                            
322                            
323                            if (tmpResult.getExceptionMessage() != null) {
324                                    throw new Exception("ec2-bundle-vol failed with exception: " + tmpResult.getExceptionMessage());
325                            }
326                            
327                            if (tmpResult.getExitCode() != 0) {
328                                    throw new Exception("ec2-bundle-vol failed. Error output is: " + tmpResult.getStdErr());
329                            }
330                            
331                            //upload to S3
332                            tmpResult = exec(conn,"ec2-upload-bundle -b " + targetBucket + " -m //mnt//" 
333                                            + newImageName + ".manifest.xml -a " + credentials.getAwsAccessKeyId() 
334                                            + " -s " + credentials.getSecretAccessKey());
335                            
336                            
337                            if (tmpResult.getExceptionMessage() != null) {
338                                    throw new Exception("ec2-upload-bundle failed with an exception: " +tmpResult.getExceptionMessage());
339                            }
340                            
341                            if (tmpResult.getExitCode() != 0) {
342                                    throw new Exception("ec2-upload-bundle failed. Error output is: " + tmpResult.getStdErr());
343                            }
344                            
345                            
346                            //send notification if required
347                            if (notifyWhenFinished){
348                                    
349                                    //get instance-id at first
350                                    String instanceId = "";
351                                    //query instance metadata
352                                    tmpResult = exec(conn, "curl http://169.254.169.254/latest/meta-data/instance-id");
353                                    
354                                    
355                                    if (tmpResult.getExceptionMessage() != null) {
356                                            throw new Exception("Retreiving instance-id failed: " + tmpResult.getExceptionMessage());
357                                    }
358                                    
359                                    if (tmpResult.getStdOut()!= null) {
360                                            instanceId = tmpResult.getStdOut();
361                                    }
362                                    
363                                    //Format current date
364                                    DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
365                                    java.util.Date date = new java.util.Date();
366                                    String timestamp = dateFormat.format(date);
367    
368                                    //compose message
369                                    String message = "<message xmlns=\\\"http://cloud42.jw.de/message\\\">" +
370                                                                      "<topic>" + topic + "</topic>" + 
371                                                                      "<instanceId>" + instanceId + "</instanceId>" + 
372                                                                      "<timestamp>" + timestamp + "</timestamp>" + 
373                                                                      "<text>" + messageText + "</text>" + 
374                                                                      "<info>" + messageInfo + "</info>" +
375                                                                      "</message>";
376                                    
377                                    
378                                    //use the address of the Cloud42 notification endpoint to send the notifcation
379                                    String endpointAddress = Settings.getInstance().getEndpointAddress();
380                                    
381                                    
382                                    
383                                    if (endpointAddress != null){
384                                    
385                                            String command = "curl -H \"Content-Type: text/xml\" -i -d \"" 
386                                                                            + message + "\" " + endpointAddress;
387                                            
388                                            
389                                            tmpResult = exec(conn,command);
390                                            
391                                            if (tmpResult.getExitCode() != 0) {
392                                                    Logger.getAnonymousLogger().severe(
393                                                            "Bundling was completed but notification could not be sent.");  
394                                            }
395                                            
396                                    } else {
397                                            Logger.getAnonymousLogger().severe(
398                                                            "No endpoint address for Cloud42 notification endpoint was found. Notification could not be sent.");    
399                                    }
400                                    
401                                    if (tmpResult.getExceptionMessage() != null) {
402                                            throw new Exception("Notification failed with exception: " + tmpResult.getExceptionMessage());
403                                    }
404                                    
405                            }
406                            
407                            
408                            
409                    } catch (Exception ex){
410                            
411                            ex.printStackTrace();
412                            
413                            
414                            result.setExceptionMessage(ex.getMessage());
415                            result.setExitCode(1);
416                            
417                    }
418                    
419                    if (conn != null) conn.close();
420    
421                    return result;
422            }
423            
424            
425            /**
426             * Private helper function, etablishes a connection to a remote host.
427             * @param host Hostname or IP address.
428             * @param key Private RSA key for auhentification.
429             * @return Connection object representing the connection or null in case of error.
430             */
431            private Connection connect(String host, String key){
432                    
433                    try
434                    {
435                            
436                            /* Create a connection instance */
437    
438                            Connection conn = new Connection(host);
439    
440                            /* Now connect */
441    
442                            conn.connect();
443    
444                            /* Authenticate */
445    
446                            boolean isAuthenticated = conn.authenticateWithPublicKey("root", key.toCharArray(), "");
447    
448                            if (isAuthenticated == false)
449                                    throw new IOException("Authentication failed.");
450    
451                            return conn;
452                            
453                    } catch (IOException e){
454                            
455                            e.printStackTrace(System.err);
456                                    
457                            return null;
458                                    
459                    }
460            }
461            
462            /**
463             * 
464             * Executes the given command on a remote host using the given connection object.
465             * 
466             * @param conn the connection to use.
467             * @param command the command to execute.
468             * @return RemoteResult object containing the output of the command.
469             */
470            private RemoteResult exec(Connection conn, String command){
471                    
472                    RemoteResult result = new RemoteResult();
473                    try {
474                            /* Create a session */
475                            if (conn == null) {
476                                    throw new IOException("Connection failed.");
477                            }
478                    
479                                    
480                            Session sess = conn.openSession();
481            
482                            //This is the more complex approach, but it might be neccessary in some cases when a
483                            //special environment is needed.
484                            //See Trilead SSH FAQ included in distribution
485    //                      Session session = conn.openSession();
486    //                      
487    //                      session.requestPTY("bash");
488    //                      session.startShell();
489    //                      
490    //                      InputStream in = session.getStdout();
491    //                      OutputStream out = session.getStdin();
492    //                      InputStream error = session.getStderr();
493    //                              
494    //                      // Sending a command
495    //                      
496    //                      out.write((command + "\n").getBytes());
497    //
498    //                      // getting the result of the command
499    //                      BufferedReader br = new BufferedReader(new InputStreamReader(in));
500    //
501    //                      String txt = "";
502    //                      
503    //                      while (true)
504    //                      {
505    //                              String line = br.readLine();
506    //                              if (line == null)
507    //                                      break;
508    //                              txt = txt + line + "\n";
509    //                      }
510    //                      result.setOutput(txt);
511                            //!!!!!!!!!!!!!!
512                            
513                            
514                            
515                            sess.execCommand(command);
516            
517                            
518                            
519                            //read  stdout and stderr
520                            InputStream out;
521                            InputStream err;
522                            
523                            out = new StreamGobbler(sess.getStdout());
524                            err = new StreamGobbler(sess.getStderr());
525                            
526                            BufferedReader brOut = new BufferedReader(new InputStreamReader(out));
527                            BufferedReader brErr = new BufferedReader(new InputStreamReader(err));
528            
529            
530                            String outMessage = "";
531                            String errMessage = "";
532            
533                            while (true)
534                            {
535                                    String line = brOut.readLine();
536                                    if (line == null)
537                                            break;
538                                    outMessage = outMessage + line + "\n";
539                            }
540    //                      // remove last linebreak in order to return just the output without
541    //                      // an additional line
542    //                      if (!outMessage.equals("")){
543    //                              outMessage = outMessage.substring(0, outMessage.length() - 1);
544    //                      }
545                            while (true)
546                            {
547                                    String line = brErr.readLine();
548                                    if (line == null)
549                                            break;
550                                    errMessage = errMessage + line + "\n";
551                            }
552                            
553                            result.setStdErr(errMessage);
554                            result.setStdOut(outMessage);
555                            
556                            //set exit signal
557                            try {
558                                    result.setExitCode(sess.getExitStatus());
559                            } catch (Exception ex){
560                                    
561                            }
562                            
563                            
564                            //a boolean success tag would be nice, but it is not possible to
565                            //determine an execution's success by the outputs (some commands write to the stdErr in spite
566                            //of beeing executed successfully.
567                            
568    //                      if (outMessage.equals("") && errMessage.equals("")){
569    //                              
570    //                              result.setSuccess(true);
571    //                              //result.setOutput("");
572    //                              
573    //                      } else if (outMessage.equals("") && !errMessage.equals("")){
574    //                              
575    //                              result.setSuccess(false);
576    //                              result.setOutput(errMessage);
577    //                              
578    //                      } else if (!outMessage.equals("") && errMessage.equals("")){
579    //                              
580    //                              result.setSuccess(true);
581    //                              result.setOutput(outMessage);
582    //                              
583    //                      } else {
584    //                              //both messages contain values -> assume everything went fine
585    //                              result.setSuccess(true);
586    //                              result.setOutput(outMessage);
587    //                      }
588            
589                            /* Close this session */
590            
591                            sess.close();
592            
593                    
594                    } catch (Exception ex){
595                            
596                            result.setExceptionMessage(ex.getMessage());
597                            
598                    }
599                    
600                    return result;
601            }
602                    
603            
604            /**
605             * Uploads data by a SCP command.
606             * @param conn
607             * @param data
608             * @param fileName
609             * @param dir
610             * @return
611             */
612            private RemoteResult doUpload(Connection conn, byte[] data, String fileName, String dir){
613                    
614                    RemoteResult result = new RemoteResult();
615                    
616                    try {
617                            
618                            /* Create a SCPClient */
619                            SCPClient scp = conn.createSCPClient();
620                            
621                            scp.put(data, fileName, dir);
622                    
623                            result.setExitCode(0);
624                            
625                    } catch (Exception ex){
626                            
627                            result.setExceptionMessage(ex.getMessage());
628                            
629                            result.setExitCode(1);
630                    }
631                    
632                    return result;
633            }
634            
635    }