Redis is a data structure store and so you can use it as a database. In Redis, there can be multiple databases identified by a number and the number of the default database is 0.

The commands you run through the Redis plug-in target the default Redis database unless you specify otherwise.
Jedis jedis = getPool().getResource(); 
jedis.select(3); //Selects database with index 3 
jedis.set("plugin:tutorial", "Using another database"); 

You can add to the plug-in the option to select a database that is different from the default one using either of the two available methods.

Method Description
Expose database index as part of the scripting API. When a user calls a set method, for example, they can pass the database index as an extra function argument.
Present a new inventory object, Database, that wraps the index. You must move all current methods from the Connection to the Database model object.

The following diagram displays the model of the second method.

Selecting a database by introducing a new Database inventory object, and then moving the methods from the Connection object to the Database object.

You define the Database object in the model package. The Database object that you invoke instead of get, set, or other methods, includes the following code.

package com.vmware.o11n.plugin.redis.model;

import com.vmware.o11n.sdk.modeldriven.extension.ExtensionMethod;
import org.springframework.util.Assert;
import redis.clients.jedis.Jedis;

public class Database {
    private final Connection connection; 
    private final int index; 
  
    public Database(Connection connection, int index) { 
        Assert.notNull(connection, "Connection cannot be null."); 
        Assert.isTrue(index >= 0, "Index must be a positive number."); 
  
  
        this.connection = connection; 
        this.index = index; 
    } 
    public String getDisplayName() { 
        return "db" + index; 
    } 
    public int getIndex() { 
        return index; 
    } 
    @ExtensionMethod 
    public String ping() { 
        try (Jedis jedis = connection.getResource(index)) { 
            return jedis.ping(); 
        } 
    } 
    @ExtensionMethod 
    public String set(String key, String value) { 
        try (Jedis jedis = connection.getResource(index)) { 
            return jedis.set(key, value); 
        } 
    } 
    @ExtensionMethod 
    public String get(String key) { 
        try (Jedis jedis = connection.getResource(index)) { 
            return jedis.get(key); 
        } 
    } 
} 

The Connection object also requires some changes. Some of them include removing methods, such as get, set, and append.

@Component 
@Qualifier(value = "connection") 
@Scope(value = "prototype") 
public class Connection implements Findable { 
    private static final int DEFAULT_REDIS_DATABASE_INDEX = 0; 
  
    /* 
     * The connectionInfo which stands behind this live connection. 
     */ 
  
    private ConnectionInfo connectionInfo; 
    private Map<Integer, Database> databases = null; 
... 
    public List<Database> getDatabases() { 
        if (databases == null) { 
            databases = new HashMap<>(16); 
            //Issue a call to Redis, to see how many databases are configured, default is 16 
            List<String> configs = getResource(DEFAULT_REDIS_DATABASE_INDEX).configGet("databases"); 
            int numberOfInstances = Integer.parseInt(configs.get(1)); 
            for (int index = 0; index < numberOfInstances; index++) { 
                databases.put(index, new Database(this, index)); 
            } 
        } 
        return new ArrayList<>(databases.values()); 
    } 
  
    @ExtensionMethod 
    public Database getDatabase(int index) { 
        return getDatabases().get(index); 
    } 
  
    @ExtensionMethod 
    public Database getDefaultDatabase() { 
        return getDatabase(DEFAULT_REDIS_DATABASE_INDEX); 
    } 
... 
} 

The modified Connection model initializes a map of database instances. You can find a database by its index. The getDatabases() method invokes a configuration command against the Redis instance and retrieves the count of the supported database instances. By default, the number of supported instances is 16.

You use CustomMapping to add a relation between a Connection object and a Database object.

    @Override 
    public void define() { 
      //@formatter:off 
... 
        wrap(Database.class). 
            andFind(). 
            using(DatabaseFinder.class). 
            withIcon("database.png"); 
... 
        relate(Connection.class). 
            to(Database.class). 
            using(ConnectionHasDatabases.class). 
            as("databases"); 
      //@formatter:on 
    } 
}
Note: After you add a new inventory object, you must create a new Finder implementation and a new Relater implementation.
public class DatabaseFinder implements ObjectFinder<Database> { 
    @Autowired 
    private ConnectionRepository connectionRepository; 
  
  
    @Override 
    public Database find(PluginContext ctx, String type, Sid id) { 
        Connection connection = connectionRepository.findLiveConnection(id); 
        if (connection != null) { 
            return connection.getDatabase((int) id.getLong("dbid", 0)); 
        } 
        return null; 
    } 
  
  
    @Override 
    public List<FoundObject<Database>> query(PluginContext ctx, String type, String query) { 
        //Return null for now 
        return null; 
    } 
  
  
    @Override 
    public Sid assignId(Database obj, Sid relatedObject) { 
        return relatedObject.with("dbid", obj.getIndex()); 
    } 
} 

Although the finder of the Database object is similar to ConnectionFinder, some major differences exist between these finders. While the Connection object is related to the root of the inventory tree and does not have a parent object, the Database object is a child object of the Connection object. When you invoke the assignId method, the relatedObject argument is the ID of the parent object, or the ID of the Connection object. You can track a child object by the ID of its parent object.

The relatedObject.with("dbid", obj.getIndex()); implementation creates a new ID based on the connection ID. The Database object ID includes the Connection object ID and the index of the database instance. By using this method, you identify a single database instance among all Connection objects.

The following diagram shows the relations between several layers of objects and their corresponding IDs.

Relations between several layers of objects and their corresponding IDs.

For example, Connection 1 has an ID l34Kow5TTI2mBC38YMLL8Q. Connection 1 also has sub objects: obj1, obj2, and obj3, whose natural IDs are 1, 2, and 3 respectively. obj3 has a set of subobjects, namely subobj1 and subobj2.

There are three types of objects, so you need three finders – for Connections, for obj and for subobj objects.

The assignId method for the connection returns only the ID of the connection. The assignId method for the finder of the first-level objects, obj, returns the ID of the connection and the ID of the obj. The assignId method for the finder of the subobj objects returns the ID of the parent object and the ID of the subobject.

A structure, similar to a map, stores the values of the ID for each object.

Note: During checkpointing, the Automation Orchestrator server stores only the ID of the object, the method that mixes the IDs makes it possible to retrieve the connection ID of a subobject and, at the same time, retrieve the ID of the Connection object.

Defining the finder of the Database object is not enough to show the database objects in the inventory tree. By using CustomMapping, you must define the parent object of the Database object and pass it to the Automation Orchestrator platform. By introducing the ConnectionHasDatabase class, you can find a set of databases for a certain connection.

public class ConnectionHasDatabases implements ObjectRelater<Database> { 
  
  
    @Autowired 
    private ConnectionRepository connectionRepository; 
  
  
    @Override 
    public List<Database> findChildren(PluginContext ctx, String relation, String parentType, Sid parentId) { 
        Connection connection = connectionRepository.findLiveConnection(parentId); 
        if (connection != null) { 
            return connection.getDatabases(); 
        } 
        return Collections.emptyList(); 
    } 
} 

Similarly to using the ConnectionFinder class, when you know the ID of the Connection object, which is a parent object, you can use ConnectionRepository to find the connection instance. To retrieve the result, you must invoke the getDatabases() method.

If you run the workflow from the Wrap the Client section, you receive the following error message: TypeError: Cannot find function set in object DynamicWrapper (Instance) : [RedisConnection]-[class com.vmware.o11n.plugin.redis_gen.Connection_Wrapper] -- VALUE :.

To resolve the error, you must recreate the scripting of the Testing Redis Connection workflow.
connection.defaultDatabase.set("plugin:tutorial", "Testing redis connection - success"); 
var result = connection.defaultDatabase.get("plugin:tutorial"); 
System.log(result); 

The modified workflow uses the same Redis:Connection connection parameter but retrieves the default database.

Writing to the file system

Using the file system to store data is not recommended, because the data is not synchronized between Automation Orchestrator nodes and is not automatically migrated.

If you need to write data for testing purposes, such as checking which node is running the current script, place all files in a folder dedicated to your plug-in under /var/run/vco. Ensure that you include the short name of your plug-in in the name of the folder.

Keep in mind that the customer might have additional restrictions and this might not work on all Automation Orchestrator installations.

Releasing new versions of the plug-in

When a new version of the same plug-in is released, keep in mind the following to ensure that you customer's content is compatible with the new version.

  • Workflow IDs, scripting action categories, and names must not be changed. They are used as unique identifiers and changing them introduces entirely new workflows or actions when the customer upgrades.
  • Avoid breaking changes. If you need to add a new parameter to an action or a workflow, make sure that the action or workflow continues to work without that new parameter.
  • Avoid removing API scriptable objects or methods in such objects.
  • Update the versions of any workflows and actions that you changed.
  • If you need to introduce significant changes, where possible, keep the old workflows, actions or scriptable objects, and mark them as Deprecated in the description, while adding new ones with the changes.