ecproxy

5 minute readAutomation

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 to workingDirectory, sets COMMANDER_ 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 ' getDefaultWorkingDirectory '

Existing implementation

Return $ENV{COMMANDER_WORKSPACE_UNIX};

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. ecproxy is guaranteed to run in the workspace directory on the proxy agent, but it is not guaranteed that the proxy target has the same path to the workspace. For example, the workspace on a Windows proxy agent is not the same as the path a Unix proxy target uses to access the workspace—so the existing implementation of this operation simply returns the UNIX path to the workspace. However, if the proxy target has a different path for accessing the workspace, the existing implementation will give the wrong answer. Thus, a user can provide a different implementation that gives the right answer.

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

ssh_getDefaultTargetPort

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

host, port (optional)

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

my $context =< connect('myhost', 22)

Example 2

my $context =< connect('myhost')

SSH implementation function

ssh_connect

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 srcFile to the proxy target as tgtFile.

Arguments

context, srcFile (typically simple file-name), tgtFile (typically workingDirectory/file-name)

Returns

Nothing; on failure it does a 'die' with an appropriate error message.

Example

uploadFile($context, 'agent123.tmp', '/opt/work/joe/agent123.tmp')

SSH implementation function

ssh_uploadFile

generateWrapperScript

Description

Generates the script body that will run the command-file on the proxy-target in the workingDirectory.

Arguments

workingDirectory, cmd, cmdArg1, …​, cmdFileName (just the base-name, no directory)

Returns

A string containing the script to execute on the proxy target.

Example

generateWrapperScript('/opt/work/joe', 'perl', 'agent123.tmp')

SSH implementation function

Not SSH-specific, so the function is ` 'generateWrapperScript'`

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 sh, the wrapper script needs to be written in a language available on the target.

uploadWrapperScript

Description

Uploads the wrapper-script code to the proxy target.

Arguments

context workingDirectory. wrapperScriptBody

Returns

The path to the wrapper-script on the proxy target.

Example

my $wrapperFile =< uploadWrapperScript($context, '/opt/work/joe', 'cd /opt/work/joe; perl agent123.tmp')

SSH implementation function

3.1, 3.1.1: ssh_uploadWrapperScript

3.1.2 and later

Not SSH-specific anymore, so the function is ` 'uploadWrapperScript'`

NOTE:

  1. This function must generate a uniquely named file that will not conflict with other ecproxy invocations that might be occurring in parallel steps. The recommended approach is to generate a file name containing the job-step-id.

  2. Depending on the protocol and facilities available in the Perl implementation, you may or may not need to create a local tmp file to upload to the proxy target. If you do, record that fact in the context and clean up the local file in the cleanup operation. Setting the local wrapper file path in $context→{wrapperFile} is recommended because the default cleanup operation implementation looks for that string.

generateWrapperInvocationCommand

Description

Generates the command-line for running the wrapper script on the proxy target.

Arguments

remoteWrapperFile (path on proxy target)

Returns

A string containing the command-line for running the wrapper script file on the proxy target.

Example

my $wrapperCmdLine =< generateWrapperInvocationCommand($wrapperFile)

SSH implementation function

Not SSH-specific, so the function is 'generateWrapperInvocationCommand'

Existing implementation

Return "sh $remoteWrapperFile";

Reason to override

The default implementation of this function returns something like 'sh $wrapperFile'. If the wrapper script is not an `sh ` script, or if you want to pass different arguments to the shell, you must override this function.

runCommand

Description

Runs the given command-line on the proxy target.

Arguments:

context, cmdLine

Returns

exit-code from running the command on the proxy target, `undef ` if the command could not be run for some reason.

Example

runCommand($context, $wrapperCmdLine)

SSH implementation function

ssh_runCommand

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, cmdFile, wrapperFile ` (both are of the form `workingDirectory/file-name )

Returns

1 on success, undef ` on failure. `

Example

cleanupTarget($context, '/opt/work/joe/agent123.tmp', '/opt/work/joe/cmdwrapper.123.tmp')

SSH implementation function

3.1, 3.1.1: ssh_cleanup

3.1.2 and later

Not SSH-specific anymore, so the function is cleanup

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

host, `port ` (optional)

Returns

1 on success, `undef ` on failure. ` `

Example

ping('myhost', 22)

SSH implementation function

Not SSH-specific, so the function is ping.

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 ECPROXY_DEBUGFILE environment variable (if it exists). No-op otherwise.

Arguments

`message `

Example

mesg("myCleanup: about to delete $cmdFile on proxy target");

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

fileName

Returns

Contents of the file. If there is an error, it returns an empty string.

Example

my $data =< readFile("foo.txt");

writeFile

Description

Creates a local file containing data.

Arguments

fileName, data

Returns

1 on success, undef ` on failure. `

Example

writeFile("myWrapper.$ENV{COMMANDER_JOBSTEPID}.cmd", "perl foo.pl")

initDispatcher

Description

Initialize the operation dispatcher map to point to functions for the given protocol. For each operation, initDispatcher ` checks if a function named `protocol_operation exists, and if so, assigns that function as the implementation for that operation.

Arguments

protocol

Example

initDispatcher("ssh") sets the "connect" operation to run "ssh_connect", "uploadFile" =<> "ssh_uploadFile", etc.

setOperation

Description

Sets the implementation of an operation to be a particular function.

Arguments

operation, function. The 'function' argument may be the name of a function or a reference to a function.

Example

setOperation("ping", "my_ping"); sets the "ping" operation to run the "my_ping" function

Example

setOperation("ping", \&my_ping); same as above, but using a function ref

This function manipulates the gDispatcher hash, but provides a safe interface to it.

loadFile

Description

Load proxy customizations from a file.

Arguments

fileName

Example

loadFile("custom.pl")

setSSHKeyFiles

Description

Set the paths to the public and private key files that ssh will use to authenticate with the proxy target.

Arguments

publicKeyFile, privateKeyFile

Example

setSSHKeyFiles('c:\foo\pub.key', 'c:\foo\priv.key')

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

userName

Example

setSSHUser('user1')

By default, the user name 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

useMultipleSSHSessions()

Examples

Specify public/private key files for SSH
  1. Set the proxyCustomization property on the resource like this: ?setSSHKeyFiles('c:\foo\pub.key', 'c:\foo\priv.key');

  2. Set the ECPROXY_SSH_PRIVKEYFILE and ECPROXY_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 and clusterupload 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;