A driver script with built-in support for SSH. Every major operation can be overridden by defining a Perl function in the Proxy Customization field on the New Proxy Resource panel, available from the Resources page (by specifying which operation this function re-implements. These operations must have certain "signatures" for the driver to invoke them properly—the operations are listed and described below. For more detail, see the SSH implementation in ecproxy.pl
.
ecproxy algorithm
ecproxy
invokes the operations detailed below to perform the following actions:
-
Uploads the command-file to a "
workingDirectory
" on the proxy target, using the protocol specified in the proxy config. Currently, only SSH is supported. -
Creates a wrapper
sh
shell script that changed directory toworkingDirectory
, setsCOMMANDER_
environment variables that exist in the proxy agent environment, and runs the command-file previously uploaded. -
Uploads the wrapper script to
workingDirectory
on the proxy target. -
Runs the wrapper script on the proxy target and streams its output to the proxy agent
stdout
. -
Deletes the local wrapper
shell
script, the remote wrapper, and remote command-file. -
Exits using the exit code of running the wrapper script.
ecproxy operations
getDefaultWorkingDirectory
Description |
Computes the default working directory where a command needs to run on the proxy target, if the step is not defined with a working directory. |
Arguments |
None |
Returns |
The path to a directory as it would be accessed on the proxy target. |
SSH implementation function |
Not SSH-specific, so the function is |
Existing implementation |
Return |
Reason to override |
If the "Working Directory" field is empty on a step that is going to run on a proxy target, the working directory for the step should be the workspace, just as it would be if the step were running on a non-proxy CloudBees CD/RO agent. |
This operation is applicable only if "Working Directory" is empty, and is used as the working directory on the proxy target in that case for running the command.
getDefaultTargetPort
Description |
Computes the default port where the proxy target is listening for this protocol. |
Arguments |
None |
Returns |
The default port. |
SSH implementation function |
|
This operation is applicable only if the resource definition specifies no port value.
connect
Description |
Opens a connection to the proxy target using the desired protocol. |
Arguments |
|
Returns |
"connection-context hash-ref" on successful connection. This context can contain anything other functions can use to perform their tasks (for example, a connection handle). |
Example 1 |
|
Example 2 |
|
SSH implementation function |
|
Because the port is optional, the implementation of connect can default to whatever is reasonable for the protocol. This means the 'Proxy Target Port' need not be specified in the CloudBees CD/RO web UI for proxied agents reachable on the default port.
|
uploadFile
Description |
Uploads the given |
Arguments |
context, |
Returns |
Nothing; on failure it does a 'die' with an appropriate error message. |
Example |
|
SSH implementation function |
|
generateWrapperScript
Description |
Generates the script body that will run the command-file on the proxy-target in the |
Arguments |
|
Returns |
A string containing the script to execute on the proxy target. |
Example |
generateWrapperScript ( |
SSH implementation function |
Not SSH-specific, so the function is |
Existing implementation |
cd workingDirectory; set COMMANDER_ environment variables; run command-file, properly quoting the command and args. |
Reason to override |
If the proxy target does not have |
uploadWrapperScript
Description |
Uploads the wrapper-script code to the proxy target. |
Arguments |
context |
Returns |
The path to the wrapper-script on the proxy target. |
Example |
|
SSH implementation function |
3.1, 3.1.1: |
3.1.2 and later |
Not SSH-specific anymore, so the function is |
|
generateWrapperInvocationCommand
Description |
Generates the command-line for running the wrapper script on the proxy target. |
Arguments |
|
Returns |
A string containing the command-line for running the wrapper script file on the proxy target. |
Example |
|
SSH implementation function |
Not SSH-specific, so the function is |
Existing implementation |
Return |
Reason to override |
The default implementation of this function returns something like |
runCommand
Description |
Runs the given command-line on the proxy target. |
Arguments: |
|
Returns |
exit-code from running the command on the proxy target, |
Example |
|
SSH implementation function |
|
cleanup
Description |
Performs any cleanup task after the command has completed on the proxy target. Typically, it deletes any locally created temp files and uploaded files on the proxy target. |
Arguments |
context, |
Returns |
|
Example |
|
SSH implementation function |
3.1, 3.1.1: |
3.1.2 and later |
Not SSH-specific anymore, so the function is |
The default implementation deletes the locally created wrapper script file whose path is stored in $context→{wrapperFile} , if it exists. Thus, if the uploadWrapperScript operation is overridden, it is recommended the overriding function set this attribute—that way, cleanup need not be overridden.
|
ping
Description |
A test to see if the proxy target is usable. |
Arguments |
|
Returns |
|
Example |
|
SSH implementation function |
Not SSH-specific, so the function is |
Existing implementation |
Opens a socket connection to the proxy target on the desired port. |
Reason to override |
The existing implementation may be deemed too simple for doing a ping; overriding ping to open a connection and do some protocol-specific handshaking might be more appropriate for some protocols/use cases. |
Available helper functions
To make proxy customization easier, ecproxy
provides the following helper functions.
mesg
Description |
Debug logging function. Writes to the file referenced in the |
Arguments |
|
Example |
|
This function automatically adds a newline to whatever it emits, so the caller does not have to incorporate a newline in message.
readFile
Description |
Reads a file. |
Arguments |
|
Returns |
Contents of the file. If there is an error, it returns an empty string. |
Example |
|
writeFile
Description |
Creates a local file containing data. |
Arguments |
|
Returns |
|
Example |
|
initDispatcher
Description |
Initialize the operation dispatcher map to point to functions for the given protocol. For each operation, |
Arguments |
|
Example |
|
setOperation
Description |
Sets the implementation of an operation to be a particular function. |
Arguments |
|
Example |
|
Example |
|
This function manipulates the gDispatcher
hash, but provides a safe interface to it.
loadFile
Description |
Load proxy customizations from a file. |
Arguments |
fileName |
Example |
|
setSSHKeyFiles
Description |
Set the paths to the public and private key files that ssh will use to authenticate with the proxy target. |
Arguments |
|
Example |
|
This is very useful on Windows proxies, where there is no reasonable default for ssh to use. |
setSSHUser
Description |
Set the name of the user to authenticate with the proxy target. |
Arguments |
|
Example |
|
By default, the username the agent is "running as" is used to log into the proxy target. If key-based authentication is configured on the target system such that agentUser can log into the user1 account on the proxy target, this function leverages that configuration.
|
useMultipleSSHSessions
Description |
Normally, ecproxy uses one ssh session with a number of "channels" to perform tasks like uploading files, running the command, and running a cleanup command on the proxy target. Some SSH servers do not allow this. This method configures ecproxy to use a separate SSH session for each operation; this requires authenticating with the SSH daemon on the proxy target several times, and thus it may perform worse than the single-session-multi-channel mode. |
Arguments |
None |
Example |
|
Examples
- Specify public/private key files for SSH
-
-
Set the
proxyCustomization
property on the resource like this:?setSSHKeyFiles('c:\foo\pub.key', 'c:\foo\priv.key');
-
Set the
ECPROXY_SSH_PRIVKEYFILE
andECPROXY_SSH_PUBKEYFILE
environment variables on the proxy agent as system environment variables.
-
- Override one of the operations
-
(for example, to enable SSH connection with username/password)
Set the proxyCustomization property on the resource like this: ?sub myConnect($$) {…} setOperation("connect", \&myConnect);
- Load proxy customizations from a file
-
rather than having all the logic in the `proxyCustomization ` property on the resource
Set the proxyCustomization property on the resource like this: ?loadFile('c:\foo\custom.pl');
- Implement a whole new protocol
-
Specify protocol as
myproto
and have a proxy customization block like this:
sub myproto_getDefaultTargetPort() { ... } sub myproto_connect($;$) { ... } sub myproto_uploadFile($$$) { ... } sub myproto_uploadWrapperScript($$$) { # Note: As of 3.1.2, the default implementation is likely good enough, so it may not be necessary to define this override .... } sub myproto_runCommand($$) { ... } sub myproto_cleanup($$$) { # Note: As of 3.1.2, the default implementation is likely good enough, so it may not be necessary to define this override .... } // Initialize the dispatcher to run these functionsinitDispatcher("myproto");
- Override ping to do a connect operation
-
(which does a full protocol handshake, authentication, and so on)
Write a specialized ping function for the proxy customization like this:
sub heavy_ping($$) { my ($host, $port) =< @_; return ssh_connect($host, $port);} setOperation("ping", \&heavy_ping);
Real world examples
ClusterExec
A basic integration for using clusterupload
and clusterexec
to reach a proxy target is here. It has been tested on a Windows target with a Cygwin installation. It will not work out-of-the-box because it makes the following assumptions:
-
The proxy target has
sh
and other UNIX tools, for example,rm
. -
The locations of the
clusterexec
andclusterupload
binaries are hard-coded at the top of the proxy customization.
To make this proxy customization work on a Windows machine that does not have Cygwin, the generateWrapperScript
operation would need to be overridden with a function that generates a cmd
batch script, and the generateWrapperInvocationCommand
operation would have to be overridden to generate a "cmd /c …"
command rather than "sh …"
.
MySQL
The idea here is that the proxy target need not be a host for running arbitrary commands. It could be a special entity like a db
. This integration uses the mysql clt
to run the step command (which should be SQL) on the db
referenced by the proxy target host and port.
A bare-bones integration with MySQL:
# Set the path to the mysql binary; if the directory is in the proxy agent's# PATH, this variable can simply contain the name of the executable. my $gMySQL =< "c:/cygwin/usr/local/tools/i686_win32/bin/mysql.exe"; sub mysql_getDefaultTargetPort() { return 3306; } sub mysql_connect($;$) { # This "protocol" implementation is just going to use the mysql # command-line tool, so just save off host/port. my $host =< $_[0]; my $port =< $_[1] || mysql_getDefaultTargetPort(); return {host =<> $host, port =<> $port}; } sub mysql_uploadFile($$$) { my ($context, $cmdFile, $rmtCmdFile) =< @_; # We do not need to upload the command-file to the proxy target. # We are going to run the mysql clt on the proxy agent to run # the query (contained in the local command-file), # so just save off the name of the command-file. $context->{cmdFile} =< $cmdFile;}sub mysql_uploadWrapperScript($$$) { my ($context, $workingDir, $wrapperScript) =< @_; # This has no meaning for this integration. No-op. } sub mysql_runCommand($$) { my ($context, $cmdLine) =< @_; # cmdLine is a command-line for running the wrapper script, which # has no meaning for this integration. We just want to run # 'mysql' for the desired host/port with the command-file. system("$gMySQL -D commander -h $context->{host} -P $context->{port} " . "-u commander -pcommander -e \"source $context->{cmdFile}\""); } sub mysql_cleanup($$$) { # We did not create any temp files. No-op. } # Initialize the dispatcher to run these functions initDispatcher("mysql");
Android
This example uses the adb
tool to upload files to the device and run commands on it. Initial testing has been only against the android emulator, but it is implemented in such a way that it should work against a real android device attached using USB to the proxy agent, or a device on the network.
A first attempt at proxying to android devices:
# Set the path to the adb binary; if the directory is in the proxy agent # PATH, this variable can simply contain the name of the executable. my $gADB =< "c:/android-sdk-windows-1.6_r1/tools/adb.exe"; android_getDefaultTargetPort() { # Not sure what a good meaningful value is here. return 0; } android_connect($;$) { # This "protocol" implementation uses the adb # command-line tool. Depending on the value of # host, construct the appropriate adb command-line # argument. my $host =< $_[0]; my $context =< {}; # if ($host eq "emulator") { if ($host eq "localhost") { # We want to talk to the emulator running on this host. $context->{targetArg} =< "-e"; } elsif ($host eq "usb") { # We want to talk to the single android device connected # to the computer via a USB. $context->{targetArg} =< "-d"; } else { # This must be the serial number of some device somewhere. $context->{targetArg} =< "-s $host"; } return $context; } android_uploadFile($$$) { my ($context, $srcFile, $tgtFile) =< @_; my($filename, $directories) =< fileparse($tgtFile); my $result =< `$gADB $context->{targetArg} push $srcFile "/data/tmp/$filename" 2>&1`; if ($? !=< 0) { die ("android_uploadFile: Error uploading file $srcFile to /data/tmp/$filename: $result\n"); } } android_runCommand($$) { my ($context, $cmdLine) =< @_; # cmdLine is a command-line for running the wrapper script, which # has no meaning for this integration. We just want to run # 'adb' for the desired device with the command-file. system("$gADB $context->{targetArg} shell $cmdLine"); } android_cleanup($$$) { my ($context, $remoteCmdFile, $remoteWrapperFile) =< @_; # This was copied from ssh_cleanup except that we do "rm", # not "rf -f". mesg("cleaning up"); # Delete the locally generate wrapper file. unlink($context->{"wrapperFile"}); # Delete the cmd-file and wrapper script file on the proxy target. $gDispatcher{"runCommand"}($context, "rm $remoteWrapperFile $remoteCmdFile"); } android_ping($;$) { my ($host, $port) =< @_; $port =< $gDispatcher{"getDefaultTargetPort"}() unless isPortValid($port); my $socket =< IO::Socket::INET->new(PeerAddr =<> $host, PeerPort =<> $port, Proto =<> "tcp", Type =<> SOCK_STREAM) or die "Couldn't connect to $host:$port : $@\n"; } # Initialize the dispatcher to run these functions initDispatcher("android"); 1;