VCF Automation offers a polyglot programming environment, with JavaScript, PowerShell and Python as programming languages. We have the opportunity to expand this range to include additional programming languages. This post shows how the
TypeScript programming language can be seamlessly used.
Integrate TypeScript Programming Language
Here are the necessary actions required for this:
- testTypeScript: Contains the TypeScript source code.
- getActionAsText: JavaScript that reads the TypeScript source code.
- handler: JavaScript to call the transpilation with the TypeScript compiler.
- transpileTypeScript: JavaScript to transpile TypeScript code.
- invokeTypeScript: JavaScript to invoke TypeScript code.
- invokeMixScript: JavaScript to invoke TypeScript code with VCF Automation objects.
To get the source code move your mouse pointer into the source code section and press on the upper right side the copy button. This copies the source code to the clipboard and now it can be pasted and saved e.g. in an editor on the target system.
You can download the sources also it from my
GitHub account.
Invoke TypeScript
The following flowchart shows the steps for executing TypeScript code.
Invoke MixScript
The following flowchart shows the steps for executing TypeScript code using VCF Automation objects. By using the REST interface it is possible to build an action that can be executed like the standard. This means that all objects available in VCF Automation can also be used.
Save TypeScript Code as Action
In the first step we build an action, who we called testTypeScript, in which we store the TypeScript code to be executed. We simply save the TypeScript source code as an action, regardless if errors are displayed or not.
function addNumbers(a: number, b: number) {
return a + b;
}
function helloWorld(name: string, age: number) {
return "Hello World from " + name + " (" + age.toString() + ")";
}
function main(name: string, age: number) {
const sum: number = addNumbers(10, 15);
const hello: string = helloWorld(name, age);
return "Sum of the two numbers is: " + sum.toString() + "\n" + hello;
}
main(in_name, in_age);
|
As we can see, this code doesn't do anything special. It is a standard example that is used in many internet sources. The addNumbers function adds only two numbers and the result is returned. The helloWorld function delivers a text with the input parameters. Not imaginative, but sufficient for our example.
Read Action as Text
In the second step we build a JavaScript action, called getActionAsText. As the name suggests, the content of an action is read out as text. With this action we read the TypeScript code and pass it for transpilation or execution.
Here all actions are filtered, first by the module name and then by the action name, to return the content of the specified action.
Transpile TypeScript to JavaScript
To provide the functionality of transpiling, we need to provide the TypeScript compiler with an interface in the third step.
/**
* Handler to call the transpilation.
*
* @param {string} typescriptSource
* @param {string} actionName
* @returns {Properties}
*
* @author Stefan Schnell <mail@stefan-schnell.de>
* @license MIT
* @version 0.7.0
*
* Checked with Aria Automation 8.13.1 and 8.16.0
*/
exports.handler = (context, inputs, callback) => {
"use strict";
const os = require("node:os");
const fs = require("node:fs");
const typescript = require('./typescript.js');
// Compiler options for transpilation to JavaScript for the Rhino engine
const compilerOptions = {
module: typescript.ModuleKind.CommonJS,
target: typescript.ScriptTarget.ES5,
strictNullChecks: true
};
// Checking the TypeScript code and error output if present
const inMemoryFileName = `${os.tmpdir()}/${inputs.actionName}.ts`;
try {
fs.accessSync(fileName, fs.constants.F_OK);
fs.rmSync(fileName, { force: true } );
} catch (exception) {
}
const host = typescript.createCompilerHost(compilerOptions);
host.writeFile(inMemoryFileName, inputs.typescriptSource);
const program =
typescript.createProgram([inMemoryFileName], compilerOptions, host);
const emitResult = program.emit();
const allDiagnostics =
typescript.getPreEmitDiagnostics(program).concat(emitResult.diagnostics);
let diagnosticMessages = "";
if (allDiagnostics.length > 0) {
allDiagnostics.forEach(diagnostic => {
if (diagnostic.file) {
let { line, character } = typescript.getLineAndCharacterOfPosition(
diagnostic.file,
diagnostic.start
);
let message = typescript.flattenDiagnosticMessageText(
diagnostic.messageText,
"\n"
);
diagnosticMessages +=
`[Line ${line + 1}, Column ${character + 1}] ${message}\n`;
} else {
diagnosticMessages += typescript.flattenDiagnosticMessageText(
diagnostic.messageText,
"\n"
);
}
});
}
// Transpile TypeScript to JavaScript
const javascriptSource = typescript.transpileModule(
inputs.typescriptSource,
compilerOptions
);
callback(undefined, {
status: "done",
javascriptSource: javascriptSource.outputText,
diagnosticMessages: diagnosticMessages
});
};
|
The handler has two essentially blocks. The first defines the compiler options and the second do the transpilation. The block for checking the TypeScript code should only help to find errors that have occurred.
Now the handler with the TypeScript compiler and the necessary declaration files must be packed into a zip package. This package can now be loaded into VMware Aria Automation and provided as an action.
Hint: To get the TypeScript compiler and the definition files,
download the latest release of TypeScript.
Hint: With the release 5.3.2 and highter of TypeScript is it better to add also the declaration files lib.dom.d.ts, lib.scripthost.d.ts and lib.webworker.importscripts.d.ts. Transpiling works perfectly without these files, but otherwise a diagnostic message occurs, that one of these files are missing.
To be able to easily use this transpilation, in conjunction with TypeScript code stored in an action, here an action that makes this possible.
/**
* Transpiles TypeScript code.
*
* @function transpileTypeScript
*
* @param {string} in_moduleName - Module name which contains the action
* @param {string} in_actionName - Action name which contains TypeScript code
* @param {boolean} in_showOutput - Flag whether output should be displayed
* @returns {string} result - Result of TypeScript code transpilation
*
* @author Stefan Schnell <mail@stefan-schnell.de>
* @license MIT
* @version 0.7.0
*
* @example
* var in_moduleName = "de.stschnell";
* var in_actionName = "testTypeScript";
* var in_showOutput = false;
*
* Checked with Aria Automation 8.13.1 and 8.16.0
*/
var _transpileTypeScriptNS = {
main : function(moduleName, actionName, showOutput) {
if (typeof showOutput !== "boolean") {
showOutput = false;
}
try {
var typescriptSource = System.getModule("de.stschnell")
.getActionAsText(moduleName, actionName);
if (showOutput) {
System.log(typescriptSource);
}
var transpileResult = System.getModule("de.stschnell.typescript")
.transpileTypeScriptToJavaScript(typescriptSource, actionName);
if (showOutput) {
System.log(transpileResult.javascriptSource);
}
System.log(transpileResult.diagnosticMessages);
return transpileResult.javascriptSource;
} catch (exception) {
System.log(exception);
}
}
};
if (
String(in_moduleName).trim() !== "" &&
String(in_actionName).trim() !== ""
) {
return _transpileTypeScriptNS.main(
in_moduleName,
in_actionName,
in_showOutput
);
} else {
throw new Error("in_moduleName or in_actionName argument can not be null");
}
|
Here an example of a transpilation. In the upper area the TypeScript source code and in the lower area the JavaScript code generated by the transpilation.
2024-03-09 05:01:30.708 +02:00INFO(de.stschnell/transpileTypeScript)
function addNumbers(a: number, b: number) {
return a + b;
}
function helloWorld(name: string, age: number) {
return "Hello World from " + name + " (" + age.toString() + ")";
}
function main(name: string, age: number) {
const sum: number = addNumbers(10, 15);
const hello: string = helloWorld(name, age);
return "Sum of the two numbers is: " + sum.toString() + "\n" + hello;
}
main(in_name, in_age);
2024-03-09 05:01:32.087 +02:00INFO(de.stschnell/transpileTypeScript)
function addNumbers(a, b) {
return a + b;
}
function helloWorld(name, age) {
return "Hello World from " + name + " (" + age.toString() + ")";
}
function main() {
var sum = addNumbers(10, 15);
var hello = helloWorld(name, age);
return "Sum of the two numbers is: " + sum.toString() + "\n" + hello;
}
main(in_name, in_age);
2024-03-09 05:01:32.313 +02:00INFO(de.stschnell/transpileTypeScript)
[Line 15, Column 6] Cannot find name 'in_name'.
[Line 15, Column 15] Cannot find name 'in_age'.
|
Invoke TypeScript
In fourth step the transpiled TypeScript code is executed with the Rhino Engine from VMware Aria Automation. With this action we achieve a seamless embedding of TypeScript.
Hint: For this approach it is necessary to set the system property com.vmware.scripting.javascript.allow-native-object to true or add in the shutter file the org.mozilla.javascript.* package, or a superset of it.
Hint: The parameters are defined in a JSON string as a collection of key and value pairs. The values can be the primitive data types string, number, boolean and null.
/**
* Invokes TypeScript code which is stored in an action.
*
* @function invokeTypeScript
*
* @param {string} in_moduleName - Module name which contains the action
* @param {string} in_actionName - Action name which contains TypeScript code
* @param {string} in_parameters - Parameters as JSON string
* @param {boolean} in_showOutput - Flag whether output should be displayed
* @returns {Any} result - Result of TypeScript code execution
*
* @author Stefan Schnell <mail@stefan-schnell.de>
* @license MIT
* @version 0.7.0
*
* @example
* var in_moduleName = "de.stschnell";
* var in_actionName = "testTypeScript";
* var in_params = "{\"in_name\": \"\\\"Stefan\\\"\", \"in_age\": 42}";
* var in_showOutput = false;
*
* Set com.vmware.scripting.javascript.allow-native-object in the
* system properties to true.
*
* Checked with Aria Automation 8.16.0
*/
var _invokeTypeScriptNS = {
main : function(moduleName, actionName, parameters, showOutput) {
if (typeof parameters !== "string") {
parameters = "";
}
if (typeof showOutput !== "boolean") {
showOutput = false;
}
// var cx =
// org.mozilla.javascript.ContextFactory.getGlobal().enterContext();
var cx = org.mozilla.javascript.Context.enter();
try {
// Reads the TypeScript source
var typescriptSource = System.getModule("de.stschnell")
.getActionAsText(moduleName, actionName);
if (showOutput) {
System.log(typescriptSource);
}
if (typescriptSource.trim() !== "") {
// Transpiles the TypeScript to JavaScript
var transpileResult = System.getModule("de.stschnell.typescript")
.transpileTypeScriptToJavaScript(typescriptSource, actionName);
if (showOutput) {
System.log(transpileResult.diagnosticMessages);
}
var javascriptSource = transpileResult.javascriptSource;
// Add input parameters to JavaScript code
if (parameters.trim() !== "") {
var addParameters = "";
var jsonParameters = JSON.parse(parameters);
Object.keys(jsonParameters).forEach( function(key) {
var parameter = "var " + key + " = " + jsonParameters[key] + ";";
addParameters += parameter + "\n";
});
javascriptSource = addParameters + "\n" + javascriptSource;
}
if (showOutput) {
System.log(javascriptSource);
}
// Executes the JavaScript
var scope = cx.initStandardObjects();
var sourceName = "export";
var lineNumber = 1;
var securityDomain = null;
var result = cx.evaluateString(
scope,
javascriptSource,
sourceName,
lineNumber,
securityDomain
);
if (showOutput) {
switch (typeof result) {
case "string" :
case "number" :
case "boolean" :
System.log(String(result));
break;
case "function" :
break;
case "object" :
if (result.constructor !== undefined) {
switch (result.constructor.name) {
case "String" :
case "Number" :
case "Boolean" :
System.log(result);
break;
case "Array" :
System.log("Array: " + String(result));
break;
case "Date" :
System.log("Date: " + String(result));
break;
case "Function" :
break;
case "Object" :
System.log(JSON.stringify(result));
break;
case "RegExp" :
System.log("RegExp: " + String(result));
break;
default :
break;
}
}
break;
default :
break;
}
}
return result;
}
} catch (exception) {
System.log(exception);
} finally {
cx.exit();
}
}
};
if (
String(in_moduleName).trim() !== "" &&
String(in_actionName).trim() !== ""
) {
return _invokeTypeScriptNS.main(
in_moduleName,
in_actionName,
in_parameters,
in_showOutput
);
} else {
throw new Error("in_moduleName or in_actionName argument can not be null");
}
|
Here is the example result of an execution.
Invoke TypeScript with Aria Automation Objects
The final and fifth step shows how TypeScript can also be used in conjunction with Aria Automation objects. The approach is very simple, the transpiled source code is saved as an action and this is then executed. Creation, execution and deletion of the action are done via the REST interface of Aria Automation.
Hint: The parameters are defined in a JSON object as an array of name, type and value. The values can be the primitive data types string, number, boolean and null.
/**
* Invokes TypeScript code mixed with Aria Automation objects.
*
* @function invokeMixScript
*
* @param {string} in_userName - Name of the user
* @param {SecureString} in_password - Password of the user
* @param {string} in_moduleName - Name of the module which contains the action
* @param {string} in_actionName - Name of the action
* @param {Array.<{name:string, type:string, value:{objectType:{value:Any}}}>}
* in_parameters - Parameters as JSON
* @param {string} in_outputType - Type of the return value
* @returns {Properties}
*
* @author Stefan Schnell <mail@stefan-schnell.de>
* @license MIT
* @version 0.7.0
*
* Set com.vmware.scripting.javascript.allow-native-object in the
* system properties to true.
*
* Checked with Aria Automation 8.16.0
*/
var _actionActivities = function() {
this._url = null;
var fqdn = this.getFQDN();
if (fqdn !== null) {
this._url = "https://" + fqdn;
} else {
throw new Error("Error at FQDN detection");
}
this._httpRestHost = null;
if (this._url !== null) {
this._httpRestHost = RESTHostManager.createTransientHostFrom(
RESTHostManager.createHost("dynamicRequest")
);
this._httpRestHost.operationTimeout = 60;
this._httpRestHost.connectionTimeout = 30;
this._httpRestHost.hostVerification = false;
this._httpRestHost.url = this._url;
}
this.bearerToken = null;
};
_actionActivities.prototype = {
/**
* Detects the Full Qualified Domain Name (FQDN)
*
* @function getFQDN
*
* @returns {string} FQDN
*/
getFQDN : function() {
var fqdn = "";
var jvmOpts = java.lang.System.getenv("JVM_OPTS");
if (jvmOpts !== null) {
var options = jvmOpts.split(" ");
options.forEach( function(option) {
if (option.substring(0, 19) === "-Dvco.app.hostname=") {
fqdn = option.substring(19, option.length);
}
});
}
if (fqdn !== "") {
return fqdn;
} else {
return null;
}
},
/**
* Retrieves Bearer token
*
* @function retrieveBearerToken
*
* @param {string} username - Name of the user
* @param {string} password - Password of the user
*/
retrieveBearerToken : function(username, password) {
if (this._url === null) {
return;
}
var httpRestHost = this._httpRestHost.clone();
var jsonLogin = {
"username": username,
"password": password
};
var login = JSON.stringify(jsonLogin);
var request = httpRestHost.createRequest(
"POST",
"/csp/gateway/am/api/login?access_token",
login
);
request.contentType = "application/json";
var response = request.execute();
if (response.statusCode === 200) {
var oRefreshToken = JSON.parse(response.contentAsString);
var refreshToken = "{\"refreshToken\":\"" +
oRefreshToken.refresh_token + "\"}";
request = httpRestHost.createRequest(
"POST",
"/iaas/api/login",
refreshToken
);
request.contentType = "application/json";
response = request.execute();
if (response.statusCode === 200) {
var oBearerToken = JSON.parse(response.contentAsString);
this.bearerToken = oBearerToken.token;
} else {
System.error("Error at retrieving bearer token");
}
}
},
/**
* Creates a new action
*
* @function createAction
*
* @param {string} moduleName - Module name that should contain the action
* @param {string} actionName - Name of the action to be created
* @param {string} code - Source code in the action
* @param {Array.<{name:string, type:string, value:{objectType:{value:Any}}}>}
* in_parameters - Parameters as JSON
* @param {string} outputType - Type of return value
* @returns {string} actionId
*/
createAction : function(
moduleName,
actionName,
code,
parameters,
outputType
) {
var _parameters = [];
if (parameters instanceof Array) {
_parameters = JSON.parse(JSON.stringify(parameters));
}
if (typeof outputType !== "string") {
outputType = "string";
}
var httpRestHost = this._httpRestHost.clone();
// Deletes value column from parameters array
if (_parameters.length > 0) {
_parameters.forEach( function(parameter) {
delete parameter.value;
});
}
var jsonScript = {
"name": actionName,
"module": moduleName,
"version": "0.1.0",
"description": "Test",
"script": code,
"input-parameters": _parameters,
"output-type": outputType
};
var script = JSON.stringify(jsonScript);
// API Documentation > Orchestrator > Actions Service
var request = httpRestHost.createRequest(
"POST",
"/vco/api/actions?uniqueName=false",
script
);
request.contentType = "application/json";
request.setHeader("Accept", "application/json");
request.setHeader("Authorization", "Bearer " + this.bearerToken);
var response = request.execute();
if (response.statusCode === 201) {
var jsonResponse = JSON.parse(response.contentAsString);
return jsonResponse.id;
} else {
System.error("Error creating action");
throw new Error("Error creating action");
}
},
/**
* Runs an action
*
* @function executeAction
*
* @param {string} actionId
* @param {Array.<{name:string, type:string, value:{objectType:{value:Any}}}>}
* in_parameters - Parameters as JSON
* @returns {string} executionId
*/
executeAction : function(actionId, parameters) {
if (!Array.isArray(parameters)) {
parameters = [];
}
var httpRestHost = this._httpRestHost.clone();
var jsonBody = {
"parameters": parameters,
"async-execution": false
};
var body = JSON.stringify(jsonBody);
// API Documentation > Orchestrator > Actions Service
var request = httpRestHost.createRequest(
"POST",
"/vco/api/actions/" + actionId + "/executions",
body
);
request.contentType = "application/json";
request.setHeader("Accept", "application/json");
request.setHeader("Authorization", "Bearer " + this.bearerToken);
var response = request.execute();
if (response.statusCode === 200) {
var jsonResponse = JSON.parse(response.contentAsString);
return jsonResponse;
} else {
System.log("Error at execution\n" + response.contentAsString);
}
},
/**
* Retrieves the definition of an action
* Hint: In this case this call is used to check if the action has
* been created.
*
* @function getAction
*
* @param {string} actionId
* @returns {number}
*/
getAction : function(actionId) {
var httpRestHost = this._httpRestHost.clone();
var request = httpRestHost.createRequest(
"GET",
"/vco/api/actions/" + actionId
);
request.setHeader("Accept", "application/json");
request.setHeader("Authorization", "Bearer " + this.bearerToken);
var response = request.execute();
return response.statusCode;
},
/**
* Gets the action run logs
*
* @function getActionLog
*
* @param {string} executionId
* @returns {string} log
*/
getActionLog : function(executionId) {
var httpRestHost = this._httpRestHost.clone();
var request = httpRestHost.createRequest(
"GET",
"/vco/api/actions/" + executionId + "/logs?maxResult=2147483647"
);
request.setHeader("Accept", "application/json");
request.setHeader("Authorization", "Bearer " + this.bearerToken);
var response = request.execute();
if (response.statusCode === 200) {
return response.contentAsString;
} else {
System.log("Error at get log");
}
},
/**
* Deletes an action
*
* @function deleteAction
*
* @param {string} actionId
*/
deleteAction : function(actionId) {
var httpRestHost = this._httpRestHost.clone();
var request = httpRestHost.createRequest(
"DELETE",
"/vco/api/actions/" + actionId
);
request.setHeader("Authorization", "Bearer " + this.bearerToken);
var response = request.execute();
if (response.statusCode !== 200) {
System.log("Error at deletion");
}
}
};
function main(
userName,
password,
moduleName,
actionName,
parameters,
outputType
) {
if (
String(userName).trim() !== "" &&
String(password).trim() !== "" &&
String(moduleName).trim() !== "" &&
String(actionName).trim() !== ""
) {
// Retrieve Bearer token
var actionActivities = new _actionActivities();
actionActivities.retrieveBearerToken(userName, password);
if (actionActivities.bearerToken === null) {
throw new Error("No Bearer token available");
}
var code = System.getModule("de.stschnell").getActionAsText(
moduleName,
actionName
);
if (String(code).trim() === "") {
throw new Error("The action contains no code");
}
var transpiledCode = System.getModule("de.stschnell")
.transpileTypeScriptToJavaScript(code, actionName).javascriptSource;
// Create action with random name in temp module
// Hint: The module temp must exist
var tempActionName = "x" + System.nextUUID().replace(/-/g, "_");
var actionId =
actionActivities.createAction(
"temp",
tempActionName,
transpiledCode,
parameters,
outputType
);
// Wait until action exists
while (actionActivities.getAction(actionId) !== 200) {
System.sleep(1000);
}
// Execute action
var response = actionActivities.executeAction(
actionId,
parameters
);
var executionId = response["execution-id"];
// Delete action
actionActivities.deleteAction(actionId);
System.sleep(2500);
// Read action log
var actionLog = actionActivities.getActionLog(executionId);
return {
"returnType": response["type"],
"returnValue": JSON.stringify(response["value"]),
"executionId": executionId,
"actionLog": actionLog
};
} else {
throw new Error(
"userName, password, moduleName or actionName argument can not be null"
);
}
}
// Main
return main(
in_userName,
in_password,
in_moduleName,
in_actionName,
in_parameters,
in_outputType
);
|
Here is an example that uses the system object.
function helloWorld(name: string) {
return "Hello World from " + name;
}
let multilineString: string = `
This is a multiline string.
In TypeScript, we use backticks.
It makes the code more readable.
`;
System.log(multilineString)
enum CardinalDirections {
North,
South,
East,
West
}
let currentDirection = CardinalDirections.North;
System.warn("Current Direction is: " + currentDirection);
System.error(helloWorld(in_name);
|
When transpiling from TypeScript to JavaScript, the use of the system objects is output as errors.
The following approach reads the single entries of the JSON object.
var userName = "holadmin";
var password = "VMware1!";
var moduleName = "de.stschnell";
var actionName = "testTypeScript";
var parameters = [
{ "name": "in_name", "type": "string", "value": {
"string": { "value": "Stefan" }
} }
];
var outputType = "string";
var result = System.getModule("de.stschnell").invokeMixScript(
userName,
password,
moduleName,
actionName,
parameters,
outputType
).actionLog;
var jsonResult = JSON.parse(result).logs;
Object.keys(jsonResult).forEach( function(item) {
System.log(jsonResult[item].entry["short-description"]);
});
|
Conclusion
This approach shows that it is possible to integrate TypeScript directly into VMware Aria Automation, and also save the source code. On this way we can use TypeScript seamlessly in VMware Aria Automation. We can also use it to cover two development scenarios. On the one hand we can use the integrated transpiler to validate our TypeScript code and to transform it to JavaScript. On the other hand we can also execute the TypeScript code directly on different ways, and one of them with the full use of all Aria Automation objects.
Addendum
Presentation at VMUG Community Event
On September the 10
th 2024 I presented the results of the TypeScript implementation at the VMware User Group (VMUG) Community Event in Kaiserlautern.
Open presentation