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 }