001/*
002 * Copyright (c) 2004-2010 The Regents of the University of California.
003 * All rights reserved.
004 *
005 * '$Author: welker $'
006 * '$Date: 2010-05-06 05:21:26 +0000 (Thu, 06 May 2010) $' 
007 * '$Revision: 24234 $'
008 * 
009 * Permission is hereby granted, without written agreement and without
010 * license or royalty fees, to use, copy, modify, and distribute this
011 * software and its documentation for any purpose, provided that the above
012 * copyright notice and the following two paragraphs appear in all copies
013 * of this software.
014 *
015 * IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
016 * FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
017 * ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
018 * THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
019 * SUCH DAMAGE.
020 *
021 * THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
022 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
023 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
024 * PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
025 * CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
026 * ENHANCEMENTS, OR MODIFICATIONS.
027 *
028 */
029
030package org.kepler.actor.ssh;
031
032import java.io.ByteArrayOutputStream;
033
034import org.apache.commons.logging.Log;
035import org.apache.commons.logging.LogFactory;
036import org.kepler.ssh.ExecException;
037import org.kepler.ssh.ExecFactory;
038import org.kepler.ssh.ExecInterface;
039
040import ptolemy.actor.TypedAtomicActor;
041import ptolemy.actor.TypedIOPort;
042import ptolemy.actor.parameters.PortParameter;
043import ptolemy.data.BooleanToken;
044import ptolemy.data.IntToken;
045import ptolemy.data.StringToken;
046import ptolemy.data.expr.Parameter;
047import ptolemy.data.expr.SingletonParameter;
048import ptolemy.data.type.BaseType;
049import ptolemy.kernel.CompositeEntity;
050import ptolemy.kernel.util.Attribute;
051import ptolemy.kernel.util.IllegalActionException;
052import ptolemy.kernel.util.NameDuplicationException;
053
054//////////////////////////////////////////////////////////////////////////
055//// ExecuteCmd 
056/**
057 * <p>
058 * Connects to a remote host using Ssh protocol (or does nothing for the local
059 * host) and executes a command. It returns the stdout, stderr and exit code
060 * after the command terminates.
061 * 
062 * </p>
063 * <p>
064 * This actor uses the org.kepler.ssh package to have longlasting connections.
065 * 
066 * </p>
067 * <p>
068 * If the <i>target</i> is empty string or equals <b>local</b>, the Java Runtime
069 * will be used for local execution instead of ssh. It behaves similarly to
070 * other local command-line exec actors of Kepler but you do not need to change
071 * your workflow for remote/local executions by using this actor.
072 * 
073 * </p>
074 * <p>
075 * If the <i>timeoutSeconds</i> is set greater than zero, the command will be
076 * timeouted after the specified amount of time (in seconds).
077 * 
078 * </p>
079 * <p>
080 * In case there is an ssh connection related error (or timeout) the
081 * <i>exitcode</i> will be -32767, <i>errors</i> will contain the error message,
082 * <i>stdout</i> and <i>stderr</i> will be empty string.
083 * 
084 * </p>
085 * <p>
086 * To ensure fixed rate of token production for SDF, the actor emits an empty
087 * string on <i>errors</i> if the command is executed without ssh related
088 * errors.
089 * 
090 * </p>
091 * <p>
092 * If <i>cleanupAfterError</i> is set, the remote process and its children will
093 * be killed (provided, we have the connection still alive). Very useful in case
094 * of timeout because that leaves remote processes running. Use only when
095 * connecting to a unix machine. In case of <i>local</i>, this flag is not used.
096 * 
097 * </p>
098 * <p>
099 * Streaming of output during the command execution is not supported by this
100 * actor.
101 * 
102 * </p>
103 * <p>
104 * <b>Third party operation</b><br>
105 * If the remote command is expected to ask for a password (or passphrase when
106 * connecting to a remote host with public-key authentication) set the expert
107 * parameter <i>thirdParty</i> for the user@host:port of that third party (it
108 * can be the same as <i>target</i> if a sudo command is executed).
109 * 
110 * </p>
111 * <p>
112 * The authentication to the third party should be the same from the target host
113 * and from Kepler's local host. Kepler authenticates (by opening a channel) to
114 * the third party and then it provides the password/passphrase used for the
115 * authentication to the command on the target host. Therefore, this actor
116 * cannot be used to reach a remote host through a proxy machine and execute a
117 * command there.
118 * 
119 * </p>
120 * <p>
121 * The third party execution can be used e.g. to execute and ssh/scp command
122 * that connects to another host, also reachable from Kepler's host, to execute
123 * external data transfer commands (bbcp, GridFTP, SRM-Lite etc) or sudo
124 * commands.
125 * 
126 * </p>
127 * <p>
128 * The actor will first authenticate Kepler to the third party host (if not yet
129 * done by other actors, e.g. SshSession). During the execution of the command,
130 * it looks for the appearance of the string 'password' or 'passphrase' in the
131 * stdout/stderr streams (case-insensitively). If such string is found, it
132 * writes the authentication code stored within Kepler used for the
133 * authentication. Therefore, the command must read the password on the standard
134 * input, not directly from the terminal device. This process is performed only
135 * once!
136 * 
137 * </p>
138 * <p>
139 * The underlying java code does not have pseudo-terminal emulation, so if you
140 * cannot force the command to read passwords from the stdin (e.g. scp command),
141 * you have to use an external tool to execute the command through a
142 * pseudo-terminal. <b>ptyexec</b> is provided in the org.kepler.ssh package, a
143 * C program, that should be compiled and put into the path on the target
144 * machine. Then you can execute <i>"ptyexec scp ..."</i>.
145 * </p>
146 * 
147 * @author Norbert Podhorszki
148 * @version $Revision: 24234 $
149 * @category.name remote
150 * @category.name connection
151 * @category.name external execution
152 */
153
154public class ExecuteCmd extends TypedAtomicActor {
155
156        /**
157         * Construct an ExecuteCmd actor with the given container and name. Create
158         * the parameters, initialize their values.
159         * 
160         * @param container
161         *            The container.
162         * @param name
163         *            The name of this actor.
164         * @exception IllegalActionException
165         *                If the entity cannot be contained by the proposed
166         *                container.
167         * @exception NameDuplicationException
168         *                If the container already has an actor with this name.
169         */
170        public ExecuteCmd(CompositeEntity container, String name)
171                        throws NameDuplicationException, IllegalActionException {
172                super(container, name);
173
174                // target selects the machine where to connect to
175                target = new PortParameter(this, "target", new StringToken(
176                                "[user@]host[:port]"));
177                new Parameter(target.getPort(), "_showName", BooleanToken.TRUE);
178
179                command = new TypedIOPort(this, "command", true, false);
180                command.setTypeEquals(BaseType.STRING);
181                new Parameter(command, "_showName", BooleanToken.TRUE);
182
183                stdout = new TypedIOPort(this, "stdout", false, true);
184                stdout.setTypeEquals(BaseType.STRING);
185                new Parameter(stdout, "_showName", BooleanToken.TRUE);
186
187                stderr = new TypedIOPort(this, "stderr", false, true);
188                stderr.setTypeEquals(BaseType.STRING);
189                new Parameter(stderr, "_showName", BooleanToken.TRUE);
190
191                exitcode = new TypedIOPort(this, "exitcode", false, true);
192                exitcode.setTypeEquals(BaseType.INT);
193                new Parameter(exitcode, "_showName", BooleanToken.TRUE);
194
195                errors = new TypedIOPort(this, "errors", false, true);
196                errors.setTypeEquals(BaseType.STRING);
197                new Parameter(errors, "_showName", BooleanToken.TRUE);
198
199                timeoutSeconds = new Parameter(this, "timeoutSeconds", new IntToken(0));
200                timeoutSeconds.setTypeEquals(BaseType.INT);
201
202                cleanupAfterError = new Parameter(this, "cleanupAfterError",
203                                new BooleanToken(false));
204                cleanupAfterError.setTypeEquals(BaseType.BOOLEAN);
205
206                /*
207                 * isThirdPartyOperation = new Parameter(this, "isThirdPartyOperation",
208                 * new BooleanToken(false));
209                 * isThirdPartyOperation.setTypeEquals(BaseType.BOOLEAN);
210                 */
211
212                /*
213                 * Hidden, expert PortParameter, directed to SOUTH by default to not to
214                 * disturb the port layout of the actor
215                 */
216                thirdParty = new PortParameter(this, "thirdParty", new StringToken(""));
217                /*
218                 * shownameTP = new SingletonParameter(thirdParty.getPort(),
219                 * "_showname"); shownameTP.setToken(BooleanToken.FALSE); hideTP = new
220                 * SingletonParameter(thirdParty.getPort(), "_hide");
221                 * hideTP.setToken(BooleanToken.TRUE);
222                 */
223                new Parameter(thirdParty.getPort(), "_showName", BooleanToken.FALSE);
224                new Parameter(thirdParty.getPort(), "_hide", BooleanToken.TRUE);
225                // DOES NOT WORK: new Parameter(thirdParty.getPort(), "_cardinal", new
226                // StringToken("SOUTH"));
227
228                /*
229                 * streamingMode = new Parameter(this, "streaming mode", new
230                 * BooleanToken(false)); streamingMode.setTypeEquals(BaseType.BOOLEAN);
231                 */
232
233                _attachText("_iconDescription", "<svg>\n" + "<rect x=\"0\" y=\"0\" "
234                                + "width=\"75\" height=\"50\" style=\"fill:blue\"/>\n"
235                                + "<text x=\"5\" y=\"30\""
236                                + "style=\"font-size:14; fill:yellow; font-family:SansSerif\">"
237                                + "ExecCmd</text>\n" + "</svg>\n");
238        }
239
240        // //////////////// Public ports and parameters ///////////////////////
241
242        /**
243         * Target in user@host:port format. If user is not provided, the local
244         * username will be used. If port is not provided, the default port 22 will
245         * be applied. If target is "local" or empty string, the command will be
246         * executed locally, using Java Runtime.
247         */
248        public PortParameter target;
249
250        /**
251         * The command to be executed on the remote host. It needs to be provided as
252         * a string.
253         */
254        public TypedIOPort command;
255
256        /**
257         * Third party target in user@host:port format. If user is not provided, the
258         * local username will be used. If port is not provided, the default port 22
259         * will be applied.
260         */
261        public PortParameter thirdParty;
262
263        /** _hide parameter of thirdParty. */
264        public SingletonParameter hideTP;
265
266        /** _showname parameter of thirdParty. */
267        public SingletonParameter shownameTP;
268
269        /**
270         * Output of the command as it would output to the standard shell output.
271         */
272        public TypedIOPort stdout;
273
274        /**
275         * The error that were reported by the remote execution or while connecting.
276         */
277        public TypedIOPort stderr;
278
279        /**
280         * The exit code of the command.
281         */
282        public TypedIOPort exitcode;
283
284        /**
285         * The string representation of all the errors that happened during the
286         * execution of the actor, if there are any.
287         */
288        public TypedIOPort errors;
289
290        /**
291         * Timeout in seconds for the command to be executed. 0 means waiting
292         * indefinitely for command termination.
293         */
294        public Parameter timeoutSeconds;
295
296        /**
297         * Enforce killing remote process(es) after an error or timeout. Unix
298         * specific solution is used, therefore you should not set this flag if
299         * connecting to other servers. But it is very useful for unix as timeout
300         * leaves processes living there, and sometimes errors too. All processes
301         * belonging to the same group as the remote command (i.e. its children)
302         * will be killed.
303         */
304        public Parameter cleanupAfterError;
305
306        /**
307         * Specifying whether the output should be sent in a streaming mode.
308         * Streaming is not implemented yet.
309         */
310        public Parameter streamingMode;
311
312        /**
313         * Specifying whether third party is to be defined. If false, the
314         * portparameter thirdParty is hidden, otherwise it is shown.
315         */
316        public Parameter isThirdPartyOperation;
317
318        // /////////////////////////////////////////////////////////////////
319        // // public methods ////
320
321        /**
322         * If the specified attribute is <i>showTriggerPort</i>, then get the value
323         * of it and re-render the trigger port. If it is true, show the trigger
324         * port; if it is false, hide the trigger port.
325         * 
326         * @param attribute
327         *            The attribute that has changed.
328         * @exception IllegalActionException.
329         */
330        public void attributeChanged(Attribute attribute)
331                        throws IllegalActionException {
332                if (attribute == isThirdPartyOperation) {
333                        BooleanToken useTP = (BooleanToken) isThirdPartyOperation
334                                        .getToken();
335                        log.debug("flag isThirdPartyOperation has changed to "
336                                        + useTP.booleanValue());
337                        try {
338                                if (useTP.booleanValue()) {
339                                        thirdParty.setContainer(this);
340                                        thirdParty.getPort().setContainer(this);
341                                } else {
342                                        thirdParty.getPort().setContainer(null);
343                                        thirdParty.setContainer(null);
344                                }
345                                // hideTP.setToken(useTP.not());
346                                // shownameTP.setToken(useTP);
347                        } catch (NameDuplicationException ndex) {
348                                log.error("Trouble with thirdParty portparameter: "
349                                                + ndex.getMessage());
350                        }
351                }
352        }
353
354        /**
355         * Send the token in the <i>value</i> parameter to the output.
356         * 
357         * @exception IllegalActionException
358         *                If it is thrown by the send() method sending out the
359         *                token.
360         */
361        public void fire() throws IllegalActionException {
362                super.fire();
363
364                // process inputs
365                target.update();
366                StringToken tg = (StringToken) target.getToken();
367                String strTarget = tg.stringValue();
368                String strCommand = ((StringToken) command.get(0)).stringValue();
369                int timeout = ((IntToken) timeoutSeconds.getToken()).intValue();
370                boolean cleanup = ((BooleanToken) cleanupAfterError.getToken())
371                                .booleanValue();
372                /*
373                 * boolean streaming = ((BooleanToken)
374                 * streamingMode.getToken()).booleanValue();
375                 */
376
377                // third party target
378                thirdParty.update();
379                String strThirdParty = ((StringToken) thirdParty.getToken())
380                                .stringValue();
381
382                int exitCode = 0;
383                ByteArrayOutputStream cmdStdout = new ByteArrayOutputStream();
384                ByteArrayOutputStream cmdStderr = new ByteArrayOutputStream();
385
386                // execute command
387                try {
388                        // get the execution object
389            log.info("Get exec object for " + strTarget);
390                        ExecInterface execObj = ExecFactory.getExecObject(strTarget);
391                        
392                        execObj.setTimeout(timeout, false, false);
393                        execObj.setForcedCleanUp(cleanup);
394                        
395                        log.info("Exec cmd: " + strCommand);
396                        exitCode = execObj.executeCmd(strCommand, cmdStdout, cmdStderr,
397                                        strThirdParty);
398
399                } catch (ExecException e) {
400                        String errText = new String("ExecuteCmd error:\n" + e.getMessage());
401
402                        if (isDebugging)
403                                log.debug(errText);
404
405                        stdout.send(0, new StringToken(""));
406                        stderr.send(0, new StringToken(""));
407                        exitcode.send(0, new IntToken(-32767));
408                        errors.send(0, new StringToken(errText));
409                        return;
410                }
411
412                if (isDebugging)
413                        log.debug("exit code = " + exitCode);
414
415                // send stdout, stderr and empty string as internal errors
416                exitcode.send(0, new IntToken(exitCode));
417                stdout.send(0, new StringToken(cmdStdout.toString()));
418                stderr.send(0, new StringToken(cmdStderr.toString()));
419                errors.send(0, new StringToken(""));
420
421        } // end-method fire()
422
423        private static final Log log = LogFactory
424                        .getLog(ExecuteCmd.class.getName());
425        private static final boolean isDebugging = log.isDebugEnabled();
426}
427
428// vim: sw=4 ts=4 et