Share This:

Writing BAO Adapters - Index

 

Introduction

If you've been following the series so far, you'll know that in Part 3 we created a skeleton actor adapter. This contained the core components required to register it on the grid, and little else. It didn't provide any actual actions we could trigger from our workflows, which is what we'll remedy this time around.


Adapter Configuration

When the adapter is activated on the grid, we can specify some parameters for global or generic options; this saves us entering or changing them on a per-adapter-call basis. You might run all SSH services on a single non-standard port, so it makes sense for that to be set at the adapter level. We will offer the following adapter configuration items:


  • Whether to check the target is alive by performing an ICMP ping first (defaults to “true”)
  • The port to use for SSH scans (defaults to “22”)
  • The port to use for SMB scans (defaults to “445”)


These are translated into an adapter configuration XML document, which gives us:

 

<config>
  <check-alive>true</check-alive>
  <ssh-port>22</ssh-port>
  <smb-port>445</smb-port>
</config>










It is possible to include the target hostname in here as well; however, for this use case I'm going to suggest that it doesn't make sense. For those who think it should be included, consider this an exercise for the reader


Defining Behaviour

Actor adapters are provided two key pieces of information when called: an action and a request.


The action is a (typically short) text string defining some relevant high level action type; for example, in a file-based adapter this might indicate “read” or “write”. We're free to use this for whatever we choose, and the AO adapter manager will simply pass this string over for us to interpret as we see fit.


The request is an XML document containing well-formed elements and values in a structure that you can determine. The document root element can be any valid element name and in this case we’ll go with “host-adapter-request”.


Let's assume that our adapter will build up over time into a suite of networking tools, so our choice of host identification is just one action among many; I know the previous choice of adapter name doesn't suggest this, but we’ll just throw logic out of the window for now. As such, we can either use the action parameter to dictate this choice – similar to the File adapter's choices of read, write, append, and delete – or we could structure our request XML so it includes an element which does this instead, as the VMware Infrastructure adapter does with its operation type element.


In this case we're going with an action and will call it “Identify Host”. If you don't supply this action when calling our adapter, it's going to refuse to do anything at all and will return an error.The adapter request for our adapter can contain four pieces of information:


  • The hostname or IP address to scan (mandatory)

  • Whether to check the target is alive by performing an ICMP ping first (optional, will over-ride the adapter configuration value)

  • The port to use for the SSH scan (optional, will over-ride the adapter configuration value)

  • The port to use for the SMB scan (optional, will over-ride the adapter configuration value)


A complete request might be structured like this (though note that when passing it to the adapter call in your workflow you must wrap it in “request-data” tags):


<host-adapter-request>
  <target>192.168.1.1</target>
  <check-alive>true</check-alive>
  <ssh-port>22</ssh-port>
  <smb-port>445</smb-port>
</host-adapter-request>










What's Our Status?

In the final paper-based step we need to decide what the output from the adapter call is going to look like. Typically an actor adapter will report back status information in two sections: one for the adapter (the metadata) and one for the action the adapter was asked to perform.


Keep in mind that whilst our adapter might complete successfully, meaning it performed the requested action, the action itself might be considered a failure; for example, if the target host doesn't respond. We should distinguish between these two types of error condition.


So we will return a status code that indicates what happened when trying to scan the remote machine, and I'm going to go with a multi-part approach. At the simplest level we will return a “status” element containing either of:


  • success: We were able to determine the remote host type
  • error: We weren't able to determine the remote host type

 

If we provide the error status we will include two additional fields: “error” containing a short error code and “description” providing some more detailed error information. These will be as follows:


  • DNSFAIL – We weren't able to resolve the hostname in DNS, so the host probably doesn't exist.
  • PINGFAIL – The ICMP ping didn't receive a response; the host may be offline, configured such that it doesn't respond to pings, or behind a router that filters out certain ICMP packets.
  • NETFAIL - An unknown network error has occurred.
  • UNKNOWNTYPE – We were not able to identify the host, suggesting that neither the SSH port nor the SMB port were open.


The ultimate goal is to identify whether the target machine is Windows or UNIX-like, so we will also return a host-type of WINDOWS or UNIX.


Based on the above, an adapter response might look like this:

 

<host-adapter-response>
  <metadata>
    <status>success</status>
  </metadata>
  <host-type>WINDOWS</host-type>
</host-adapter-response>









 

And an error response like this:

 

<host-adapter-response>
  <metadata>
    <status>error</status>
    <error>PINGFAIL</error>
    <description> An ICMP ping request to the target host did not receive a response</description>
  </metadata>
</host-adapter-response>









 

Expanding the Configuration Class

And so we get to the good stuff! Using the configuration definition above, the configuration class can be updated to define the permitted configuration parameters.


The code is getting a bit long to be fully included inline, so we’ll just look at the pertinent changes from the skeleton version. The full source is attached in the file HostIdentifierActorConfig.java.


The configuration parameter names, default values, and some error strings are defined first. For simplicity we’re not using proper internationalisation of any of the strings; they are just hard-coded in English. Internationalisation will be covered in a later article.


A call to initValidKeys() is added to the class constructors so that the adapter manager knows which configuration parameters are understood. For example:

 

// Constructor - when passed the adapter ID as a string
public HostIdentifierActorConfig(String adapterId) {
  super(adapterId);
  this.initValidKeys();
  System.out.println("Adapter config created with adapter ID: " + adapterId);
}









 

This is a new method that is defined later in the source file:

 

private void initValidKeys() {
  super.addValidKey(CONFIG_ALIVE);
  super.addValidKey(CONFIG_SSHPORT);
  super.addValidKey(CONFIG_SMBPORT);
}









 

Note that we don’t specify any required keys -- once a key has been defined as valid, you can then make it mandatory by calling, for example, super.addRequiredKey(CONFIG_ALIVE). If such required parameters are not specified in the adapter configuration, the adapter will not start.


Next we build up the validate() method so that it checks for ports in the valid TCP range 1-65535 (technically 0 is a valid port, but it’s reserved for special purposes). The actual check is farmed out to the helper method isValidPortString(), so the entries in validate() are like so:

 

// Validate SSH port is in the range 1-65535
if (!isValidPortString(super.getProperty(CONFIG_SSHPORT))) {
  throw new InvalidConfigurationException(new Message(INVALID_SSH_PORT, INVALID_SSH_PORT_DETAIL));
}










If the helper reports that the port number isn’t valid, we throw InvalidConfigurationException to advise the Adapter Manager that we’re not happy with the supplied config. If this happens, the adapter will not start.


Finally, for the configuration class at least, we add some getter/setter methods to allow retrieval of the configured or default parameter values as appropriate. These follow the form:

 

public int getSmbPort() {
  if (super.getProperty(CONFIG_SMBPORT) == null) {
    return DEFAULT_SMBPORT;
  }
  return super.getIntegerProperty(CONFIG_SMBPORT);
}









 

Each has a simple check whether a value has been set in the adapter configuration. If so, it is returned to the caller. If not, the default value is returned instead.


Fleshing Out the Actor

Now that the configuration class has been beefed up, it’s time to add some muscle to the actor adapter class itself. Again there are too many changes to include everything inline, so refer to the attached file HostIdentifierActor.java. The majority of our changes will be to the performAction() method and we will be adding code to:


  • Check the action that we’ve been passed.
  • Retrieve parameters from defaults, adapter configuration, or adapter request as appropriate.
  • Perform a ping check against the target host if check-alive is true.
  • Attempt a connection on the SSH port.
  • Attempt a connection on the SMB port.
  • Interpret the results from the above and build a suitable adapter response to return to the caller.


Retrieving the action is simple as the AdapterRequest class provides us with the getAction() method. At this point it might be helpful to take a look at the adapter request as defined in Dev Studio:


2014-07-11 15_23_19-Windows 7 Basic client [Running] - Oracle VM VirtualBox.png

And then a complete adapter request as it is passed to our adapter:

 

<adapter-request>
  <target-adapter>HostIdentifierAdapter</target-adapter>
  <peer-location>
    <location>this</location>
  </peer-location>
  <request-action>Identify Host</request-action>
  <request-data>
    <host-adapter-request>
      <target>192.168.66.11</target>
      <check-alive>true</check-alive>
    </host-adapter-request>
  </request-data>
</adapter-request>









 

This is what we get if we simply dump the whole request to XML and then display it as a string using adapterRequest.toXML().toPrettyString(). Since we can grab the request-action directly as described above, what we really want to play with is just the adapter request as passed in to the adapter call from workflow. With adapterRequest.getData() we can do this, and from adapterRequest.getData().toPrettyString() we get:

 

<host-adapter-request>
  <target>192.168.66.11</target>
  <check-alive>true</check-alive>
</host-adapter-request>









 

This is a bit easier to work with, since all of our parameters are now children of the root element. Therefore we can grab them easily using getChild() rather than having to navigate the XML tree or use XPath. This is how it’s done:


XML requestData = adapterRequest.getData();
String targetHost = requestData.getChild("target").getText();










To prevent Null Pointer Exceptions, we actually wrap up the getChild() method calls with checks to determine if the elements exist, and if not we grab the values from the adapter configuration.


The ping and port checks are using basic Java network functions, so I won’t cover them in detail here.

Some logic is applied to the results from the network checks to decide whether we were successful or not, and from that we build our adapter response XML. Lastly we create the AdapterResponse object which is passed back to the caller. These steps are performed using this code:

 

XML responseXml = this.buildResponseXml(status, errorCode, errorDescription, hostType);
AdapterResponse adapterResponse = new AdapterResponse(System.currentTimeMillis() - adapterStartTime, null, responseXml, Status.SUCCESS);
return adapterResponse;









 

Here’s a real example of the adapter output as seen in Dev Studio:


2014-07-11 15_20_47-Windows 7 Basic client [Running] - Oracle VM VirtualBox.png


It is worth noting at this point that the code has fairly minimal error checking and throws some adapter exception failures where we might want to be a bit more helpful by failing less dramatically. I’ve mentioned before that I don’t wish to hold this code up as a shining example of well-written Java, so please take it as a functional example.


Test, Test, and Test Some More

The JUnit test class has been updated to be a bit more thorough in checking the output from the configuration and adapter classes. This is attached as HostIdentifierActorTest.java. This test class still isn't particularly extensive, but it does perform some basic checks that might help prevent silly errors making it to your adapter on the grid (failing to pass an adapter response, for example).


Two resource files are included to support the test cases: config.xml and adapter-request.xml.


And Now For Something Completely Different

Here we are with a functional actor adapter that can be used to interrogate the OS type of remote machines (in a very simplistic way, granted). Hopefully this provides enough code for you to start working on some wonderful actor adapters of your own! In the next part of the series we’ll move on to monitor adapters which are a slightly different beast...