Using Martini to run shell scripts such as Shell, Bash, Batch, or PowerShell.
How does it work?
The package’s resources directory contains several folders that correspond to a CLI type. The idea is to put the corresponding script file in their corresponding type, which will be then executed when a REST API request is made to the exposed Martini Service.
Before trying this demo, make sure that you have the correct file directory permissions, else, the resulting request will always respond with an error.
Implementation
This demo uses the default Groovy library put together to create a Martini compatible function that can be used to run scripts such as shell, bash, or PowerShell. The resulting function is then wrapped in a service exposed as a REST API. You may download the package from TORO Marketplace using the keyword demo014.
ScriptUtility Class
This is where all the magic happens. This class combines several private functions that make up the whole process for readability. Creating a method as a usable function in a service made possible by Groovy Services.
The main function executeScriptFile accepts the following inputs: scriptFilename, cliType , and args which are then sent to three separate private methods inside this class to construct the required cli command to execute the script file to be executed. The args input parameter is an optional parameter that can be provided for additional execution options.
@GloopObjectParameter("output{\n result#demo014.models.Result{\n}\n }\n")
static GloopModel executeScriptFile( String scriptFilename, String cliType, String args = '' ) {
String defaultScriptDir = getSystemDefaultScriptDir( cliType )
String osCli = getSystemCli( cliType )
return executeCommand( "${osCli} ${args == null ? '' : args} ${defaultScriptDir}${scriptFilename}" )
}
executeScriptFile method
In the executeScriptFile method, getSystemDefaultScriptDir is used to get the file path of the script from the correct cliType directory. This function call is necessary to accommodate different operating systems because operating systems use different file path formats e.g. windows uses backslash as a separator, while unix file systems use forward slashes.
private static String getSystemDefaultScriptDir( String cliType ) {
String packageHome = "${System.getProperty( 'toroesb.home' )}packages"
String os = System.getProperty( 'os.name' ).toLowerCase()
String scriptPath = "${packageHome}"
if ( os.contains( 'win' ) ) {
scriptPath += "\\demo014-rest-to-script-file\\resources\\${cliType}-scripts\\"
} else if ( os.contains( 'mac' ) || os.contains( 'nix' ) || os.contains( 'nux' ) || os.contains( 'aix' ) ) {
scriptPath += "/demo014-rest-to-script-file/resources/${cliType}-scripts/"
} else {
throw new UnsupportedOperationException( 'Operating system not supported' )
}
return scriptPath
}
getSystemDefaultScriptDir function
The getSystemCli is called to get the necessary command to use depending on the operating system where the script file will be executed. For instance, the cli command for running a PowerShell command in Windows is different from running a PowerShell command in MacOS: powershell.exe(Windows) vs pwsh(MacOS)
private static String getSystemCli( String cliType ) throws UnsupportedOperationException {
String osCli = ''
String osName = System.getProperty( 'os.name' ).toLowerCase()
if ( cliType.equalsIgnoreCase( 'powershell' ) ) {
if ( osName.contains( 'mac' )) {
osCli = "${'pwsh.macos.dir'.getPackageProperty()}/pwsh"
} else if ( osName.contains( 'nix' ) || osName.contains( 'nux' ) || osName.contains( 'aix' ) ) {
osCli = "${'pwsh.linux.dir'.getPackageProperty()}/pwsh"
} else if ( osName.contains( 'win' ) ) {
osCli = "${'pwsh.windows.dir'.getPackageProperty()}powershell.exe"
} else {
throw new UnsupportedOperationException( 'Operating system not supported.' )
}
} else if ( cliType.equalsIgnoreCase( 'cmd' ) ) {
if ( osName.contains( 'mac' ) || osName.contains( 'nix' ) || osName.contains( 'nux' ) || osName.contains( 'aix' ) ) {
throw new UnsupportedOperationException( "Batch file is not supported in the current operating system: ${osName}" )
} else if ( osName.contains( 'win' ) ) {
osCli = 'powershell.exe'
}
} else if ( cliType.equalsIgnoreCase( 'bash' ) || cliType.equalsIgnoreCase( 'shell' ) ) {
if ( osName.contains( 'win' ) ) {
throw new UnsupportedOperationException( "Operation not supported in the current operating system: ${osName}" )
} else {
osCli = ''
}
}
return osCli
}
getSystemCli function
Finally, after getting the file path of the script that corresponds to the operating system where the script that will be executed, the final command string is constructed and will be sent to the executeCommand function which will return the result whether the script has successfully executed or not.
private static GloopModel executeCommand( String commandString ) {
def sout = new StringBuilder(), serr = new StringBuilder()
Result result = new Result()
def sysCommand = commandString.execute()
sysCommand.waitForProcessOutput( sout, serr )
sysCommand.waitForOrKill( 1000 )
"\nout>\n${sout}\nerr>\n${serr}".debug()
result.errorOutput = serr.toString()
result.exitCode = sysCommand.exitValue()
result.successOutput = result.exitCode > 0 ? null : sout.toString()
result.commandHint = result.exitCode > 0 ? sout.toString() : null
return result.toGloopModel()
}
executeCommand function
Additionally, the executeScriptFile method uses Gloop Annotation to bind a Java Object to a Martini Data Model which is added as an inner class in the ScriptUtility Class. Thanks to this, our function is able to display a readable, and easy to map output object from the Groovy Service
ExecuteScript Service
This is a Martini Service that uses a Groovy exposed method as a function to execute. The service has a very straightforward implementation. It just calls the executeScriptFile function from the ScriptUtility Class.
And has the following input:
cliType: Can be any of the following: powershell, bash, shell, and cmd.
scriptFileName: The file name of the script to be executed. The name of the script file to be provided should include the extension e.g. hello-world.ps1
args: Additional arguments for fine-tuning the script execution.
Exposing ExecuteScript service as an API
The ExecuteScript service is then exposed as POST request via Martini REST API
By using the ExecuteScript service, we can then start sending POST requests to <martini-host>/sys_command/execute_script with the following JSON payload:
{
"cliType": "powershell",
"scriptFilename": "",
"args": ""
}
See it in action
Below is a GIF that showcases the demo to execute a PowerShell script using a Martini Service exposed as a REST API.
What’s happening in the GIF?
The command for manually executing the PowerShell script is manually executed in PowerShell to show a working script.
The POST request is then made to the Script Utility API to execute the PowerShell script, providing the same arguments used when it was executed manually via PowerShell terminal.
The POST request is sent again, but this time, providing invalid arguments to show that the error from executing the command is returned in the response.
Finally, the command is executed again, this time in the PowerShell terminal with the same invalid arguments to show that the error returned by the REST API is the same as the error returned by the PowerShell terminal.