Most Automation Orchestrator plug-ins require an endpoint configuration. An endpoint is a location where the plug-in stores connection details for the instances, with which Automation Orchestrator communicates.
Procedure
- Under the {plug-in-home}/o11nplugin-redis-core/src/main/java/com/vmware/o11n/plugin/redis folder, create a directory with the name model.
This directory constitutes the com.vmware.o11n.plugin.redis.model package of the plug-in.
- Create an endpoint configuration.
The following example shows a plain old Java object ( POJO) that is used to store connection details for the plug-in endpoint, such as connection name, host name, and host port.
package com.vmware.o11n.plugin.redis.model; import org.springframework.util.Assert; import com.vmware.o11n.sdk.modeldriven.Sid; /** * Object for all the redis connection details. */ public class ConnectionInfo { /** * Name of the connection as defined by the user when creating a connection */ private String name; /** * ID of the connection */ private final Sid id; /** * Service URI of the third party system */ private String host; /** * Port of the connection, defaults the to redis default port number */ private int port = 6379; public ConnectionInfo() { this.id = Sid.unique(); } /* * Verify that each ConnectionInfo has an ID. */ public ConnectionInfo(Sid id) { super(); Assert.notNull(id, "Id cannot be null."); this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getHost() { return host; } public void setHost(String host) { this.host = host; } public Sid getId() { return id; } public void setPort(int port) { this.port = port; } public int getPort() { return port; } @Override public String toString() { return "ConnectionInfo [name=" + name + ", id=" + id + ", uri=" + host + ", port=" + port + "]"; } }
- Specify an ID, so that the default constructor can find a unique ID.
Plug-ins use
Sid
instead of a string ID. When you build hierarchies of object, each child object must know its parent ID. ASid
wraps multiple key-value objects and you can identify a single object by different properties.For example, a single configuration can have multiple connections, each connection has a list of locations and each location has a list of virtual machine images. When you run certain operation, for example delete, on a virtual machine image, you must know the specific connection, to which you run the command. By using aSid
, you can identify the virtual machine image object by connection ID, name, and location. - Save the endpoint configuration as a ConnectionInfo.java file in the model directory.
- Wrap
ConnectionInfo
toConnection
to expose the actual object as a scripting objectpackage com.vmware.o11n.plugin.redis.model; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; @Component @Qualifier(value = "connection") @Scope(value = "prototype") public class Connection { /* * The connectionInfo which stands behind this live connection. */ private ConnectionInfo connectionInfo; /* * There is no default constructor, the Connection must be initialized only * with a connection info argument. */ public Connection(ConnectionInfo info) { init(info); } public synchronized ConnectionInfo getConnectionInfo() { return connectionInfo; } public String getDisplayName() { return getConnectionInfo().getName() + " [" + getConnectionInfo().getHost() + "]"; } /* * Updates this connection with the provided info. This operation will * destroy the existing third party client, causing all associated * operations to fail. */ public synchronized void update(ConnectionInfo connectionInfo) { if (this.connectionInfo != null && !connectionInfo.getId().equals(this.connectionInfo.getId())) { throw new IllegalArgumentException("Cannot update using different id"); } destroy(); init(connectionInfo); } private void init(ConnectionInfo connectionInfo) { this.connectionInfo = connectionInfo; } public synchronized void destroy() { // Destroy the third party client } }
Note: After theConnection
object is exposed as a scripting object, it's also exposed as an inventory object. - Save the
Connection
object as a Connection.java file in the model directory. - Under the {plug-in-home}/o11nplugin-redis-core/src/main/java/com/vmware/o11n/plugin/redis folder, create a directory with name config.
This directory constitutes the com.vmware.o11n.plugin.redis.config package of the plug-in.
- Add a connection, persister, and interface for object messaging to the com.vmware.o11n.plugin.redis.config package.
- Add
ConnectionPersister
and save it as a ConnectionPersister.java file.Note:ConnectionPersister
invokes the Automation Orchestrator Persistence SDK.package com.vmware.o11n.plugin.redis.config; import java.util.List; import com.vmware.o11n.plugin.redis.model.ConnectionInfo; import com.vmware.o11n.sdk.modeldriven.Sid; public interface ConnectionPersister { /** * Returns a collection of all stored configurations (resources under a * folder with the plug-in name) */ public List<ConnectionInfo> findAll(); /** * Returns a collection by its ID or null if not found */ public ConnectionInfo findById(Sid id); /** * Stores a connection info or updates it if already available. The * persister checks the availability of a connection by its ID */ public ConnectionInfo save(ConnectionInfo connection); /** * Deletes a connection info. The persister will use the ID of the * connection */ public void delete(ConnectionInfo connectionInfo); /** * Allows us to subscribe to the events of the persister. For example, if a * connection is deleted, the persister will trigger an event, notifying all * subscribers. This is an implementation of the observer pattern. */ void addChangeListener(ConfigurationChangeListener listener); /** * Forces the persister to read all the configurations and trigger the * events. This method is invoked when the plug-in is loaded on server * start-up. */ public void load(); }
- Add
DefaultConnectionPersister
and save it as a DefaultConnectionPersister.java file.package com.vmware.o11n.plugin.redis.config; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import org.apache.commons.lang.Validate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import com.vmware.o11n.plugin.redis.model.ConnectionInfo; import com.vmware.o11n.sdk.modeldriven.Sid; import ch.dunes.vso.sdk.endpoints.IEndpointConfiguration; import ch.dunes.vso.sdk.endpoints.IEndpointConfigurationService; @Component public class DefaultConnectionPersister implements ConnectionPersister { private static final Logger log = LoggerFactory.getLogger(DefaultConnectionPersister.class); /** * A list of listeners, who have subscribed to any configuration events, * such as connection updates and deletions. */ private final Collection<ConfigurationChangeListener> listeners; public DefaultConnectionPersister() { // Initialise the listeners listeners = new CopyOnWriteArrayList<ConfigurationChangeListener>(); } /* * Constants of the key names under which the connection values will be * stored. */ private static final String ID = "connectionId"; private static final String NAME = "name"; private static final String SERVICE_HOST = "serviceHost"; private static final String SERVICE_PORT = "servicePort"; /* * The platform-provided service for configuration persistence */ @Autowired private IEndpointConfigurationService endpointConfigurationService; /* * Returns a collection of all stored configurations for this plug-in only * The service is aware of the plug-in name, thus will return only * configurations for this plug-in. */ @Override public List<ConnectionInfo> findAll() { Collection<IEndpointConfiguration> configs; try { // Use the configuration service to retrieve all configurations. // The service is aware of the plug-in name, thus will return only // configurations for this plug-in. configs = endpointConfigurationService.getEndpointConfigurations(); List<ConnectionInfo> result = new ArrayList<>(configs.size()); // Iterate all the connections for (IEndpointConfiguration config : configs) { // Convert the IEndpointConfiguration to our domain object - the // ConnectionInfo ConnectionInfo connectionInfo = getConnectionInfo(config); if (connectionInfo != null) { log.debug("Adding connection info to result map: " + connectionInfo); result.add(connectionInfo); } } return result; } catch (IOException e) { log.debug("Error reading connections.", e); throw new RuntimeException(e); } } /* * Returns a ConnectionInfo by its ID The service is aware of the plug-in * name, thus cannot return a configuration for another plug-in. */ @Override public ConnectionInfo findById(Sid id) { // Sanity checks Validate.notNull(id, "Sid cannot be null."); IEndpointConfiguration endpointConfiguration; try { // Use the configuration service to retrieve the configuration // service by its ID endpointConfiguration = endpointConfigurationService.getEndpointConfiguration(id.toString()); // Convert the IEndpointConfiguration to our domain object - the // ConnectionInfo return getConnectionInfo(endpointConfiguration); } catch (IOException e) { log.debug("Error finding connection by id: " + id.toString(), e); throw new RuntimeException(e); } } /** * Save or update a connection info. The service is aware of the plug-in * name, thus cannot save the configuration under the name of another * plug-in. */ @Override public ConnectionInfo save(ConnectionInfo connectionInfo) { // Sanity checks Validate.notNull(connectionInfo, "Connection info cannot be null."); Validate.notNull(connectionInfo.getId(), "Connection info must have an id."); // Additional validation - in this case we want the name of the // connection to be unique validateConnectionName(connectionInfo); try { // Find a connection with the provided ID. We don't expect to have // an empty ID IEndpointConfiguration endpointConfiguration = endpointConfigurationService .getEndpointConfiguration(connectionInfo.getId().toString()); // If the configuration is null, then we are performing a save // operation if (endpointConfiguration == null) { // Use the configuration service to create a new (empty) // IEndpointConfiguration. // In this case, we are responsible for assigning the ID of the // configuration, // which is done in the constructor of the ConnectionInfo endpointConfiguration = endpointConfigurationService .newEndpointConfiguration(connectionInfo.getId().toString()); } // Convert the ConnectionInfo the IEndpointConfiguration addConnectionInfoToConfig(endpointConfiguration, connectionInfo); // Use the configuration service to save the endpoint configuration endpointConfigurationService.saveEndpointConfiguration(endpointConfiguration); // Fire an event to all subscribers, that we have updated a // configuration. // Pass the entire connectionInfo object and let the subscribers // decide if they need to do something fireConnectionUpdated(connectionInfo); return connectionInfo; } catch (IOException e) { log.error("Error saving connection " + connectionInfo, e); throw new RuntimeException(e); } } /** * Delete a connection info. The service is aware of the plug-in name, thus * cannot delete a configuration from another plug-in. */ @Override public void delete(ConnectionInfo connectionInfo) { try { // Use the configuration service to delete the connection info. The // service uses the ID endpointConfigurationService.deleteEndpointConfiguration(connectionInfo.getId().toString()); // Fire an event to all subscribers, that we have deleted a // configuration. // Pass the entire connectionInfo object and let the subscribers // decide if they need to do something fireConnectionRemoved(connectionInfo); } catch (IOException e) { log.error("Error deleting endpoint configuration: " + connectionInfo, e); throw new RuntimeException(e); } } /** * This method is used to load the entire configuration set of the plug-in. * As a second step we fire a notification to all subscribers. This method * is used when the plug-in is being loaded (on server startup). */ @Override public void load() { List<ConnectionInfo> findAll = findAll(); for (ConnectionInfo connectionInfo : findAll) { fireConnectionUpdated(connectionInfo); } } /** * Attach a configuration listener which will be called when a connection is * created/updated/deleted */ @Override public void addChangeListener(ConfigurationChangeListener listener) { listeners.add(listener); } /* * A helper method which iterates all event subscribers and fires the update * notification for the provided connection info. */ private void fireConnectionUpdated(ConnectionInfo connectionInfo) { for (ConfigurationChangeListener li : listeners) { li.connectionUpdated(connectionInfo); } } /* * A helper method which iterates all event subscribers and fires the delete * notification for the provided connection info. */ private void fireConnectionRemoved(ConnectionInfo connectionInfo) { for (ConfigurationChangeListener li : listeners) { li.connectionRemoved(connectionInfo); } } /* * A helper method which converts our domain object the ConnectionInfo to an * IEndpointConfiguration */ private void addConnectionInfoToConfig(IEndpointConfiguration config, ConnectionInfo info) { try { config.setString(ID, info.getId().toString()); config.setString(NAME, info.getName()); config.setString(SERVICE_HOST, info.getHost()); config.setInt(SERVICE_PORT, info.getPort()); } catch (Exception e) { log.error("Error converting ConnectionInfo to IEndpointConfiguration.", e); throw new RuntimeException(e); } } /* * A helper method which converts the IEndpointConfiguration to our domain * object the ConnectionInfo */ private ConnectionInfo getConnectionInfo(IEndpointConfiguration config) { ConnectionInfo info = null; try { Sid id = Sid.valueOf(config.getString(ID)); info = new ConnectionInfo(id); info.setName(config.getString(NAME)); info.setHost(config.getString(SERVICE_HOST)); info.setPort(config.getAsInteger(SERVICE_PORT)); } catch (IllegalArgumentException e) { log.warn("Cannot convert IEndpointConfiguration to ConnectionInfo: " + config.getId(), e); } return info; } /* * A helper method which validates if a connection with the same name * already exists. */ private void validateConnectionName(ConnectionInfo connectionInfo) { ConnectionInfo configurationByName = getConfigurationByName(connectionInfo.getName()); if (configurationByName != null && !configurationByName.getId().toString().equals(connectionInfo.getId().toString())) { throw new RuntimeException("Connection with the same name already exists: " + connectionInfo); } } /* * A helper method which gets a connection by name. */ private ConnectionInfo getConfigurationByName(String name) { Validate.notNull(name, "Connection name cannot be null."); Collection<ConnectionInfo> findAllClientInfos = findAll(); for (ConnectionInfo info : findAllClientInfos) { if (name.equals(info.getName())) { return info; } } return null; } }
TheDefaultConnectionPersister
class includes theIEndpointConfigurationService service
that is specific for Automation Orchestrator.IEndpointConfigurationService
stores in the resources of the Automation Orchestrator platform simple key-value objects calledIEndpointConfiguration
objects. This service saves and deletes the configuration, and also validates connection configurations, if these configurations have a unique name system-wide. - Add a collection of
ConfigurationChangeListener
interfaces. - Save the interface as a ConfigurationChangeListener.java file in the configuration directory.
You can use the
ConfigurationChangeListener
interfaces as an extension point of theIEndpointConfigurationService
service.package com.vmware.o11n.plugin.redis.config; import com.vmware.o11n.plugin.redis.model.ConnectionInfo; /** * An extension point of the configuration persister. Serves the role of an * Observer when a certain connection is created, modified or deleted. */ public interface ConfigurationChangeListener { /** * Invoked when a connection is updated or created */ void connectionUpdated(ConnectionInfo info); /** * Invoked when the ConnectionInfo input is deleted */ void connectionRemoved(ConnectionInfo info); }
This way, you implement the Observer pattern from Creating the Configuration Change Listener. If you want to cache all live connections, you must know which of the connections are deleted, so that you can remove them from the local cache.
- Create a local cache by using
ConnectionRepository
saved as a ConnectionRepository.java file in the configuration directory.package com.vmware.o11n.plugin.redis.config; import java.util.Collection; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.springframework.beans.BeansException; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.stereotype.Component; import com.vmware.o11n.plugin.redis.model.Connection; import com.vmware.o11n.plugin.redis.model.ConnectionInfo; import com.vmware.o11n.sdk.modeldriven.Sid; @Component public class ConnectionRepository implements ApplicationContextAware, InitializingBean, ConfigurationChangeListener { /* * Injecting the ConnectionPersister */ @Autowired private ConnectionPersister persister; private ApplicationContext context; /* * The local map (cache) of live connections */ private final Map<Sid, Connection> connections; public ConnectionRepository() { connections = new ConcurrentHashMap<Sid, Connection>(); } /** * Returns a live connection by its ID or null if no such connection has * been initialised by this ID */ public Connection findLiveConnection(Sid anyId) { return connections.get(anyId.getId()); } /** * Returns all live connections from the local cache */ public Collection<Connection> findAll() { return connections.values(); } /* * Spring-specifics - storing a reference to the spring context */ @Override public void setApplicationContext(ApplicationContext context) throws BeansException { this.context = context; } /* * Spring specifics - this method is being called automatically by the * spring container after all the fields are set and before the bean is * being provided for usage. This method will be called when the plug-in is * being loaded - on server start-up. */ @Override public void afterPropertiesSet() throws Exception { // Subscribing the Repository for any configuration changes that occur // in the Persister persister.addChangeListener(this); // Initialising the Persister. By doing that, the persister will invoke // the connectionUpdated() method // and since we are subscribed to those events, the local cache will be // populated with all the available connections. persister.load(); } private Connection createConnection(ConnectionInfo info) { // This call will create a new spring-managed bean from the context return (Connection) context.getBean("connection", info); } /* * This method will be called from the ConnectionPersister when a new * connection is added or an existing one is updated. */ @Override public void connectionUpdated(ConnectionInfo info) { Connection live = connections.get(info.getId()); if (live != null) { live.update(info); } else { // connection just added, create it live = createConnection(info); connections.put(info.getId(), live); } } /* * This method will be called from the ConnectionPersister when a connection * is removed. */ @Override public void connectionRemoved(ConnectionInfo info) { Connection live = connections.remove(info.getId()); if (live != null) { live.destroy(); } } }
You can useConnectionRepository
as a local cache to read a single connection or all available connections. A data structure keeps all connections and every time the platform requests a connection, the local cache retrieves the same connection instance.Note: You can useConnectionRepository
for read operations andConnectionPersister
for create, update, and delete operations. When a connection is created, the persister notifies all listeners. For example,ConnectionRepository
and the local cache stores every new connection.For performance reasons, usingConnectionRepository
is the preferred method for handling connections. Reading connections withIEndpointConfigurationService
might lead to an unnecessary increase in plug-in load.
- Add
- Use
ConnectionManager
from the singleton directory to expose the configuration-specific options in the scripting.package com.vmware.o11n.plugin.redis.singleton; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; import com.vmware.o11n.plugin.redis.config.ConnectionPersister; import com.vmware.o11n.plugin.redis.model.ConnectionInfo; import com.vmware.o11n.plugin.sdk.spring.platform.GlobalPluginNotificationHandler; /** * A scripting singleton for managing redis connections. */ @Component @Scope(value = "prototype") public class ConnectionManager { @Autowired private ConnectionPersister persister; /* * A vRO SDK object which is responsible for notifying the platform of * changes in the inventory of the plug-in */ @Autowired private GlobalPluginNotificationHandler notificationHandler; /** * This method creates a redis connection by the provided name, host and * port. * * @param connectionName * the name of the connection, only one with the same name can * exist * @param redisHost * the redis host, cannot be null * @param redisPort * redis port * @return the ID of the newly created connection */ public String save(String connectionName, String redisHost, int redisPort) { ConnectionInfo info = new ConnectionInfo(); info.setName(connectionName); info.setHost(redisHost); if (redisPort > 0) { info.setPort(redisPort); } // Save the connection through the persister info = persister.save(info); // Invalidate all elements of the redis inventory notificationHandler.notifyElementsInvalidate(); // Return the ID of the newly created connection return info.getId().toString(); } }
When you invoke the save method of the singleton scripting object, a newConnectionInfo
class is constructed. You save the connection by using the newly created persister.Note: You must annotate the class, otherwise the dependency injection does not work. - Build the plug-in again and deploy it to the Automation Orchestrator server. See Deploy a Plug-In.