JON 2.3 Scripted Group Deployments Using The CLI API

1. Preface

 

A common use case for management tools is to automate deployments of new or existing applications. This article is an attempt to create an easy script doing the following:

 

1. Find all JBoss EAP instances for a specified JON group

2. For each EAP instance do

3. Shut down EAP instance

4. If there's already an application deployed with the given name

4a. update the applications binary

4b. Else create a new deployment

5. Restart EAP instance

6. End loop

 

 

2. Prerequisites

You need the following tools and software

 

1. JBoss Operations Network (JON) 2.3 or up

2. JDK 1.6.0

3. Linux for the Bash based wrapper script

4. An Application to deploy (EAR / WAR)

5. JBoss EAP 4.2 or up

 

3. The JON Command Line API

Before we actually start implementing the script you should be familar with JavaScript and the JON CLI. So please have a look at the following resources to get a brief overview of the API:


http://www.redhat.com/docs/en-US/JBoss_ON/2.3/html/Installation_Guide/index.html

 

 

4. Creating the Script

Our script should have two command line parameters. The first should be the path of the new application which should be installed on the group. The second one is the name of the group itself. Now we should find a way to parse those parameters in our script:

 

As described here

http://www.redhat.com/docs/en-US/JBoss_ON/2.3/html/Installation_Guide/Installation_Guide-Working_with_the_CLI-Script_Arguments.html

 

It is fairly easy to use parameters:

 

if( args.length < 2 ) usage();

var fileName = args[0];
var groupName = args[1];

 

We would then like to check if the path is valid and if the current user can read it. This is done by using Java classes as shown here:

 

// check that the file exists and that we can read it
var file = new java.io.File(fileName);

if( !file.exists() ) {
    println(fileName + " does not exist!");
    usage();
}

if( !file.canRead() ) {
    println(fileName + " can't be read!");
    usage();
}

 

 

Now we have the group and a valid path to the new application. Lets have a look if the group really exists on my JON server:

 

// find resource group
var rgc = new ResourceGroupCriteria();
rgc.addFilterName(groupName);
rgc.fetchExplicitResources(true);
var groupList = ResourceGroupManager.findResourceGroupsByCriteria(rgc);

 

 

The important part here is the call to

rgc.fetchExplicitResources(true);

 

As we would like to traverse the resources which are part of the group. Now lets check if there is a group found:

 

if( groupList == null || groupList.size() != 1 ) {
    println("Can't find a resource group named " + groupName);
    usage();
}

var group = groupList.get(0);

println("  Found group: " + group.name );
println("  Group ID   : " + group.id );
println("  Description: " + group.description);

 

 

Now we have validated that there is a group with the specified name. Now check if the group contains explicit resources:

 

if( group.explicitResources == null || group.explicitResources.size() == 0 ) {
    println("  Group does not contain explicit resources --> exiting!" );
    usage();
}
var resourcesArray = group.explicitResources.toArray();

 

 

resourceArray now contains all resources which are part of the group. Now lets check if there are JBoss AS Server instances which we need to stop/restart before we deploy our application:

 


for( i in resourcesArray ) {
    var res = resourcesArray[i];
    var resType = res.resourceType.name;
    println("  Found resource " + res.name + " of type " + resType + " and ID " + res.id);
    
    if( resType != "JBossAS Server") {
        println("    ---> Resource not of required type. Exiting!");
        usage();
    }
    
    // get server resource to start/stop it and to redeploy application
    var server = ProxyFactory.getResource(res.id);
}

As you can see, we need a group with only JBossAS Server resource types as top level resources. Now "server" contains the JBossAS instance. But why are we re-reading the server? Because we need it fully populated. Internally, the CLI is using simple JPA persistence. And as you can imagine, it is necessary to not always fetch all dependant objects.

 

Next step is to traverse all the children of the server instance and find the resource name of our application:

 

    var children = server.children;
    for( c in children ) {
        var child = children[c];
        
        if( child.name == packageName ) {
        }
    }

 

"packageName" is the name of the application without version information and path as shown in the JON GUI as deployed applications.

 

Now we can easily create a backup of the original version of the application by simply do the following:

 

            println("    download old app to /tmp");
            child.retrieveBackingContent("/tmp/" + packageName + "_" + server.name + "_old");

In /tmp should now be a copy of the old application with the server name decoded in path.

 

Now it's time to shutdown the server and to upload the new application content to the server. This is done as follows:

 

            println("    stopping " + server.name + "....");
            try {
                server.shutdown();
            }
            catch( ex ) {
                println("   --> Caught " + ex );
            }

            
            println("    uploading new application code");
            child.updateBackingContent(fileName);
            
            println("    restarting " + server.name + "....." );

            try {
                server.start();
            }
            catch( ex ) {
                println("   --> Caught " + ex );
            }

You may wonder why I am surrounding the actual operation with a try / catch statement. This is currently due to a NullPointerException caused by the underlying API. But the server does what we want so it is save to ignore the exception.

 

 

5. Creating a new resource

So now we are able to update an existing application. But what happens if the server is new and does not already have the application deployed? This means, we need to use the CLI to create a new resource which will then be deployed to the JBoss Server.

 

Unfortunately, it is not that easy to create a new resource. The discussed approach does also only work for JBossAS Server instances and not for Tomcat servers. But changing this is should be easy.

 

We first need to get the resource type for the application. This depends on several paramters:

  1. The type of the application (i.e. WAR or EAR)
  2. The type of the container the app needs to be deployed on (Tomcat, JBoss AS etc.)
        var appType = ResourceTypeManager.getResourceTypeByNameAndPlugin( appTypeName, "JBossAS" );
        if( appType == null ) {
            println("  Could not find application type. Exit.");
            usage();
        }

 

Then we need the package type of the application. This also depends on the two parameters discussed above.

 

        var realPackageType = ContentManager.findPackageTypes( appTypeName, "JBossAS" );
        
        if( realPackageType == null ) {
            println("  Could not find JON's packageType. Exit.");
            usage();
        }

 

As you know, each resource in JON has some configuration parameters. The same is true for WARs or EARs deployed on a JBoss AS. In order to be able to create a new resource, we need to fill some parameters.

 

        // create deployConfig 
        var deployConfig = new Configuration();
        deployConfig.put( new PropertySimple("deployDirectory", "deploy"));
        deployConfig.put( new PropertySimple("deployZipped", "true"));
        deployConfig.put( new PropertySimple("createBackup", "false"));

 

You may wonder, where those property names come from. You can get a list of supported properties by the package type by calling this method:

 

var deployConfigDef = ConfigurationManager.getPackageTypeConfigurationDefinition(realPackageType.getId());

 

Now comes the real ugly part. In order to be able to create the resource, we need to provide the package bits&bytes as a byte array:

 

        var inputStream = new java.io.FileInputStream(file);
        var fileLength = file.length();
        var fileBytes = java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, fileLength);
        for (numRead=0, offset=0; ((numRead >= 0) && (offset < fileBytes.length)); offset += numRead ) {
            numRead = inputStream.read(fileBytes, offset, fileBytes.length - offset); 
        }

But then we're able to create the resource:

 

        ResourceFactoryManager.createPackageBackedResource(
            server.id,
            appType.id,
            packageName,
            null,  // pluginConfiguration
            packageName,
            packageVersion,
            null, // architectureId        
            deployConfig,
            fileBytes
        );

Please make sure that the given JBoss AS server instance is still running and that JON knows that it's running. Otherwise you'll get an Exception saying that the JON agent is not able to upload the binary content to the server.

 

That's it. Congratulations!

 

If you would like to have some more in deep knowledge of using the CLI, here's a link to a test script of the RHQ 1.3.GA release:

http://svn.rhq-project.org/repos/rhq/tags/RHQ_1_3_0_GA/modules/enterprise/remoting/scripts/src/test/script/org/rhq/enterprise/remoting/cli/test_ChannelManager.js

 

 

6. Conclusion

As you can see it is fairly easy to use the new scripting engine to automate many tasks of daily administration just by typing a few lines of JavaScript code into an editor.

 

The attached script can be used for own scripts. It was developed during a simple Proof Of Concept for a customer. The purpose was to show how the scripting engine JON 2.3 can be used and that it is possible to update 20 nodes of JBoss EAP in one go.