How to create a Microsoft DNS Dynamic Types plug-in ?

On the VMware blog I have explained how vRealize Automation Anything as a Service (XaaS) work with Orchestrator and how a vRO plug-in works.

In this tutorial I will walk you through how I managed to implement Microsoft DNS as a service I used for the screenshot examples on that blog including:

  • The Microsoft DNS dynamic types plug-in
  • DNS record as a service in vRA
  • DNS record as a blueprint component

This is the first public tutorial on how to create a Dynamic Types plug-in from scratch and the overall methodology used applies to other integrations.

Creating a Microsoft DNS plug-in

-1- The name space

First you need to run the “Define Namespace” workflow.

If you look at the plug-in inventory you will notice that Dynamic Types as a root element. All the namespaces you will create will be listed under this root element. The name should define your plug-in. In my case “Microsoft DNS”. This name will be used to prefix the type of your plug-in objects. For example “DynamicTypes:Microsoft DNS.zone”

The Dynamic Types namespaces

-2- Creating the types

Then you need to think about what objects you need in your inventory. This includes the parent folders and the objects. In my case:

  • Hosts : The parent folder containing all my DNS servers
  • Host : The DNS servers
  • Zones : the zones parent folders (I.E Forward lookup Zones, Reverse lookup Zones)
  • Zone : the zones
  • resourceRecords : the DNS records

This is my implementation but I could for example have decided to have different types for the zones parent folders, the zones (i.E forwardZone and reverseZone) and the records depending on their different types.

To adjust which type I needed I did a little bit of research checking the Microsoft DNS user interface and most importantly the Microsoft DNS API which is Powershell based. When I saw that for example that I could query all zones types and they returned the same properties I decided on this design with one object.

Microsoft DNS manager

Exploring the user interface and what the API can return for the different object is also important to determine what properties you want the object to have in vRO. If these properties are in the UI or returned by the PowerShell cmdlts this must be for a good reason. In my case I looked at the results of get-dnsServerZone and get-dnsServerResourceRecord.

Checking the properties returned by the API call. Micorsoft DNS has a PowerShell API.

You do not necessary have to know all the objects and their properties beforehand since you can always update these after but it will save you some time in the implementation.

Once you have your types listed and their important properties you can run the “Define Type” workflow for each type. You may want to do a bit or prep work before :

  • Besides the name of the type you have to provide the object properties and an icon for the inventory. You can either select one of the default icon in the library or do some icon captures and import these in vRO as resource elements. If you choose for the former you can always update the types after to change the icons.
  • The Define Type workflow will also ask you if you want to generate workflow stubs for the find and hasChildrenInRelation methods. You will also have to provide a workflow category for these. In my case I created a “Plug-in methods” category under a Microsoft DNS category.

I highly recommend using workflow stubs. The reason is that when you will test the plug-in you will see the workflow runs which will help you understand when the methods are called and see if they are successful or not.

Workflow stubs created by the 'Define Type' workflow

-3- Creating the relations

Now that we have objects we need to establish the parent to child relationships. This will create the inventory tree-view and call the hasChildrenInRelation and findRelation workflows when unfolding it.

Run the “Define Relation”. For relation name I always put [parentType]-[childType]. For example “hosts-host”.

Once done check the result under Dynamic Types / Type Hierarchy.

Type hierarchy as created by the 'Define relation' workflow

Now we need to implement the HasChildrenInRelation and findRelation methods for each object to be able to unfold the inventory under the namespace.

-4- Implementing the HasChildrenInRelation methods

Since we know every object will have children except the resourceRecord we can edit all the “Has Microsoft DNS-[objectName]” workflows scriptable task with result = true; and the “Has Microsoft DNS-resourceRecord” with result = false;

Editing Has Children In Relation

-5- Implementing the findRelation methods.

If you look at the findRelation workflow stubs you will see the workflow receive 3 inputs :

  • parentType
  • parentId
  • relationName

Lets implement the Find Relation workflow for each type.

Find Relation Microsoft DNS-hosts

There is something specific with the first object and its child relation under the Dynamic Types level : This same workflow will be called when unfolding the namespace to display the root folder “Hosts” and when unfolding the “Hosts” folder.

Find Relation Microsoft DNS-hosts is called to display these two levels

In order to determine what we need to return we are going to check relationName this way:

1
2
3
4
5
6
7
if (relationName == "namespace-children") {
  resultObjs = System.getModule("com.vmware.coe.microsoft.dns.dt").findAllMicrosoftDNSHosts("hosts");
}

if (relationName == "hosts-host") {
  resultObjs = System.getModule("com.vmware.coe.microsoft.dns.dt").findAllMicrosoftDNSHost("host");
}

Note that:

  • The name of the namespace to hosts relation name is predefined in the plug-in as “namespace-children”
  • I have created a com.vmware.coe.microsoft.dns.dt scripting module where I created findAllDNS* actions that I am calling here since for these particular objects findRelation returns the same objects as findAll.
  • The goal of the workflow is to return the resultObjs objects of typeArray/DynamicTypes:DynamicObject which is the generic object type for DynamicTypes object

Now lets have a look at the actions:Array/DynamicTypes:Microsoft DNS.hosts findAllMicrosoftDNSHosts(type)

1
2
resultObjs = new Array();
resultObjs.push(DynamicTypesManager.makeObject("Microsoft DNS", "hosts", "DNS", "DNS", new Array()));

We are basically calling the DynamicTypesManager makeObject method with passing the namespace, type, object ID “DNS”, object name “DNS” and an empty array for the object properties. Even if we return only one object we need to return this into an array since findRelation expects an array. For this folder we did not need to query the DNS server since there is always this root folder with this name so we can build it statically.

The findAllMicrosoftDNSHost action is a tricky one. We need to list the DNS hosts from somewhere. Since we will be leveraging the PowerShell plug-in to query the DNS host we can list the Powershell hosts that are DNS servers. For this we could run PowerShell commands but here I have adopted another strategy with using a “Add a DNS host” workflow.

This workflow has an input of type PowerShell host and a scriptable task where we set a custom property “dnsHost”.

1
Server.setCustomProperty(dnsHost,"dnsHost",true);

A vRO custom property is a convenient way to set a key, value pair (with the value being any object vRO can handle).

This means that as a pre-requisite we need to have a PowerShell host to the vRO inventory – this takes some configuration on the Windows side that is covered in the PowerShell plug-in guide. Then running the “Add DNS host” workflow so our DNS plug-in knows it has this host available.

Now let’s check the findAllDNSHost actions: Array/DynamicTypes:Microsoft DNS.host findAllMicrosoftDNSHost(type)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
var powershellHosts = Server.getObjectsWithCustomPropertyKey("dnsHost");
var resultObjs = new Array();

for each(var powershellHost in powershellHosts) {
  var object = DynamicTypesManager.makeObject("Microsoft DNS", "host", powershellHost.id, powershellHost.name, new Array());
  sess = powershellHost.openSession()
  sess.addCommandFromString('get-dnsServerSetting | ConvertTo-Json');
  var invResult = sess.invokePipeline();
  if (invResult.invocationState == 'Failed') {
    System.error(invResult.getErrors());
  } else {
    //System.log( invResult.getHostOutput() );
    var serverSetting = JSON.parse(invResult.getHostOutput());
    for each(var properties in serverSetting.CimInstanceProperties) {
      object.setProperty(properties.Name, properties.Value);
      //System.log(properties.Name + " = " + properties.Value);
    }
  }
  resultObjs.push(object);
}

Server.getObjectsWithCustomPropertyKey basically gets the DNS servers we added with the “Add a DNS host” workflow.

We are looping through these to create the host objects with makeObject.

We could have returned the resultObjs array at this point but since we have the PowerShell host handy I open a session with powerShellHost.openSession() and run the get-dnsServerSetting command to get some properties.

When running the command I use powerShell ConvertTo-Json to output the result in a format vRO can natively understand. JSON is JavaScript Object Notation, basically a way to describe a JavaScript object as text. With this I just have to do a

1
var serverSetting = JSON.parse(invResult.getHostOutput())

to get a javascript object that is much more convenient to extract properties

To understand the structure of the JSON file did copy and paste the text output to a JSON viewer such as jsonviewer.stack.nu

The DNS server settings opened in a JSON viewer

These DNS cmdlets always have the same format which is very convenient to parse. All object properties are both under the root object as static properties and as an array of CimInstanceProperties allowing to set the object properties with the setProperty method this way:

1
2
3
for each (var properties in serverSetting.CimInstanceProperties) {
  object.setProperty(properties.Name, properties.Value);
}

Finally I close the session with

1
powershellHost.closeSession( sess.getSessionId())

The try / catch finally blocks allows to close the session in case of success or failure of the script in the try block.

The cmdlet piped to ConvertTo-Json transformed into a JavaScript Object that we iterate through its properties is the pattern we will use for the other PowerShell objects. We would use the same methodology for other PowerShell integrations.

Find Relation Microsoft DNS-host

Next we need to be able to unfold the items under the DNS host. For this we need to edit the “Find Relation Microsoft DNS-Host” To mimic the Microsoft GUI we will return three folders like this:

1
2
3
4
5
6
7
if (relationName == "host-zones") {
 //Zones types
 resultObjs = new Array();
 resultObjs.push(DynamicTypesManager.makeObject("Microsoft DNS", "zones", parentId + "/forwardLookupZones", "Forward Lookup Zones", new Array()));
 resultObjs.push(DynamicTypesManager.makeObject("Microsoft DNS", "zones", parentId + "/reverseLookupZones", "Reverse Lookup Zones", new Array()));
 resultObjs.push(DynamicTypesManager.makeObject("Microsoft DNS", "zones", parentId + "/conditionalForwarders", "Conditional Forwarders", new Array()));
}

Note that we always prefix the object ID with the ID of the PowerShell server in order to:

  • Have unique IDs for objects with same ID postfix
  • Be able to use the PowerShell host ID to get the PowerShell host object to query it

This is a very important design implementation that I have successfully used many times when building plug-ins integration REST based hosts.

Next we need to be able to unfold the items under the zones type. For this we need to edit the “Find Relation Microsoft DNS-zones”

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
if (relationName == "zones-zone") {
  //Zones types
  resultObjs = new Array();
  var powershellHostId = parentId.split("/")[0];
  var zoneFolderType = parentId.split("/")[1];
  System.log("zoneFolderType : " + zoneFolderType);
  var powershellHost = Server.findForType("PowerShell:PowerShellHost", powershellHostId);

  var sess;
  try {  
    sess = powershellHost.openSession()  
    sess.addCommandFromString('get-DnsServerZone | ConvertTo-Json -Compress');  
    var invResult = sess.invokePipeline();
    if (invResult.invocationState  == 'Failed'){  
        System.error(invResult.getErrors());  
    } else {
      //System.log( invResult.getHostOutput() );
      var zones = JSON.parse(invResult.getHostOutput());
      for each (var zone in zones) {
        System.log(zone.ZoneName);
        var object = null;
        if (zoneFolderType == "conditionalForwarders" && zone.ZoneType == "Forwarder"){      
          object = DynamicTypesManager.makeObject("Microsoft DNS", "zone", powershellHostId + "/" + zone.ZoneName, zone.ZoneName, new Array());
        }
        if (zoneFolderType == "forwardLookupZones" && zone.ZoneType == "Primary" && zone.IsReverseLookupZone == false) {
          object = DynamicTypesManager.makeObject("Microsoft DNS", "zone", powershellHostId + "/" + zone.ZoneName, zone.ZoneName, new Array());
        }
        if (zoneFolderType == "reverseLookupZones" && zone.ZoneType == "Primary" && zone.IsReverseLookupZone == true) {
          object = DynamicTypesManager.makeObject("Microsoft DNS", "zone", powershellHostId + "/" + zone.ZoneName, zone.ZoneName, new Array());
        }
        if (object != null) {
          object.setProperty("ZoneType", zone.ZoneType);
          object.setProperty("IsAutoCreated", zone.IsAutoCreated);
          object.setProperty("IsDsIntegrated", zone.IsDsIntegrated);
          object.setProperty("IsReverseLookupZone", zone.IsReverseLookupZone);
          resultObjs.push(object);
        }
      }
    }
  } catch ( ex ) {  
    System.log (ex);  
  } finally {  
    if (sess) {  
      powershellHost.closeSession( sess.getSessionId());  
    }  
  }
}

First we get the powerShell object from the parent ID using the split command then the zoneFolderType that we will use later to filter the different types of zones (Forward, Reverse, conditional)Then we get the PowerSHellHost object using

1
Server.findForType("PowerShell:PowerShellHost", powershellHostId);

Then we get all DNS zones and convert the result to JSON with:

1
sess.addCommandFromString('get-DnsServerZone | ConvertTo-Json -Compress');

Note that I used the Compress parameter. This reduces the amount of space characters in the output which are only useful for having a nice indentation but useless for us as we convert this to a JavaScript object. Compressing will also allow the PowerShell query to run faster which will translate in unfolding the tree view quicker.

Once again we use a JSON viewer to see the output

Array of zone objects

The JSON is returned in the form of an array of Zone objects. As for the DNS server settings the properties of the zone object are accessible either via the CimInstanceProperties or directly using the properties name at the root of the object. This time I used the direct properties to check the type of zone so it match the parent folder and its properties.

I made the choice of using the zone name as the zone ID prefix since it will be useful for the next findRelation to know the zone name from the parentId that will be provided when finding the records from the zone names

Find Relation Microsoft DNS-zone

The last step to implement is to unfold the records

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
if (relationName == "zone-resourceRecord") {
  resultObjs = new Array();
  var powershellHostId = parentId.split("/")[0];
  var zoneName = parentId.split("/")[1];
  var powershellHost = Server.findForType("PowerShell:PowerShellHost", powershellHostId);  
  var sess;
  try {
    sess = powershellHost.openSession();  
    sess.addCommandFromString('get-DnsServerResourceRecord ' + zoneName + ' | ConvertTo-Json -depth 3 -Compress');  
    var invResult = sess.invokePipeline();
    if (invResult.invocationState  == 'Failed'){  
        System.error(invResult.getErrors());  
      }
      else { 
      //System.log( invResult.getHostOutput() );
      var resourceRecords = JSON.parse(invResult.getHostOutput());
      for each (var resourceRecord in resourceRecords) {
        var resourceRecordProperties = System.getModule("com.vmware.coe.microsoft.dns.dt").getMicrosoftDNSObjectProperties(resourceRecord);
        // Adding parent zone name
        resourceRecordProperties.put("Zone", zoneName);
        var name = resourceRecord.HostName;
        if (name == "@") name = "(same as parent folder)";
        var encodedId = System.getModule("com.vmware.coe.microsoft.dns.dt").getMicrosoftResourceRecordId(resourceRecordProperties) ;
        var object = DynamicTypesManager.makeObject("Microsoft DNS", "resourceRecord", powershellHost.id + "/" + encodedId, name, new Array());

        for each (var key in resourceRecordProperties.keys) {
          System.log(key + " : " + resourceRecordProperties.get(key));
          object.setProperty(key, resourceRecordProperties.get(key));
        }
        resultObjs.push(object);
      }
    }
  } catch ( ex ) {  
    System.log (ex);  
  } finally {  
    if (sess) {  
        powershellHost.closeSession( sess.getSessionId());  
    }
  }
}

The pattern here starts in a very similar way the zones find Relation.

1
'get-DnsServerResourceRecord ' + zoneName + ' | ConvertTo-Json -depth 3 -Compress'

will retrieve an arry of resource records

Please note the ‘-depth3’ here. ConvertTo-Json default depth is 2 meaning that if you have complex objects under the CimInstanceProperties these will be collapsed to save space which unfortunately in this case result in information loss. By adjusting the Depth to 3 I managed to not loose information anymore. This is a very important aspect when integrating with PowerShell.

Next I am iterating over the resourceRecords and passing these to the getMicrosoftDNSObjectProperties action that basically extract the complex resourceRecord properties in a vRO properties object having all values as strings.

Then I am adding the parent zone name as a property of the resourceRecord object. Knowing a record parent zone will be useful for operations on records next.

Then I have a new strategy here where I am generating a string ID from the resource record properties since there is no properties in the resourceRecord object that is unique. For example you can have 2 records with the same name. Only the combination of all properties is unique so this was the first reason I created this getMicrosoftResourceRecordId action. Please note that this action is using Base64 encoding to make sure the ID only contains valid characters.

The second reason if for reliability and efficiency of the “Find Microsoft DNS-resourceRecord By Id” workflow we will have to implement next. Since there is no unique ID it is not possible to effectively query a single record and there is the risk, at this leaf level of the tree view of starting a great number of requests on the PowerShell host which may be configured to not accept as many and will result in slow response times.

Now that we are done implementing all the find Relation workflows we can test unfolding the inventory and compare to the Microsoft DNS UI

The Microsoft DNS Manager UI reproduced in the Orchestrator inventory

You may notice there are a few more zones in the vRO inventory as the Microsoft UI hide some.

Implementing FindAll and findById

We still need findAll to use drop down lists as input and findById to be able to use a plug-in object within a scriptable task or action.

In order to test these methods with the different types we created we should create some test workflow for each type

Test workflows

Each workflow has:

  • One input of the given type.
  • The presentation property “Select value as list” -> This will trigger the findAll to display the list
  • A scriptable task with the input parameter
  • The following script to check the object name and properties:
1
2
3
4
5
6
7
8
System.log("Found object " + dynamicTypeObject.name + " with id " + dynamicTypeObject.id);
System.log("With properties : ");

for each (var  property in dynamicTypeObject.getOrderedPropertyNames()) {
  try {
    System.log("\t" + property + " = " + dynamicTypeObject.getProperty(property));
  } catch(e) {System.warn("Did not found property " + property)};  
}

I just had to create one and duplicate it with changing the type for each type.

Find All Microsoft DNS-hosts" and “Find All Microsoft DNS-host

Since I had already implemented the findAllMicrosoftDNSHost and findAllMicrosoftDNSHosts actions , I just had to call these in the “Find All Microsoft DNS-hosts” and “Find All Microsoft DNS-host” workflows

Find All Microsoft DNS-zones

1
2
3
4
5
6
7
8
var powershellHosts = Server.getObjectsWithCustomPropertyKey("dnsHost");
var resultObjs = new Array();
  
for each (var powershellHost in powershellHosts) {
  resultObjs.push(DynamicTypesManager.makeObject("Microsoft DNS", "zones", powershellHost.id + "/forwardLookupZones", "Forward Lookup Zones", new Array()));
  resultObjs.push(DynamicTypesManager.makeObject("Microsoft DNS", "zones", powershellHost.id + "/reverseLookupZones", "Reverse Lookup Zones", new Array()));
  resultObjs.push(DynamicTypesManager.makeObject("Microsoft DNS", "zones", powershellHost.id + "/conditionalForwarders", "Conditional Forwarders", new Array()));
}

Is pretty much a copy of the code we used for “Find Relation Microsoft DNS-host” except we do it for each DNS host

Find All Microsoft DNS-zone

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
var powershellHosts = Server.getObjectsWithCustomPropertyKey("dnsHost");
var resultObjs = new Array();

for each(var powershellHost in powershellHosts) {
    resultObjs = new Array();
    var sess;
    try {
        sess = powershellHost.openSession()
        sess.addCommandFromString('get-dnsServerZone| ConvertTo-Json -depth 3 -Compress');
        var invResult = sess.invokePipeline();
        if (invResult.invocationState == 'Failed') {
            System.error(invResult.getErrors());
        } else {
            //System.log( invResult.getHostOutput() );
            var zones = JSON.parse(invResult.getHostOutput());

            for each(var zone in zones) {
                var object = null;
                object = DynamicTypesManager.makeObject("Microsoft DNS", "zone", powershellHost.id + "/" + zone.ZoneName, zone.ZoneName, new Array());

        for each(var properties in zone.CimInstanceProperties) {
          if (properties.Value == null) object.setProperty(properties.Name, "");
          else object.setProperty(properties.Name, properties.Value);
          //System.log(properties.Name + " = " + properties.Value);  
        }
        resultObjs.push(object);
      }
        }
    } catch (ex) {
        System.log(ex);
    } finally {
        if (sess) {
            powershellHost.closeSession(sess.getSessionId());
        }
    }
}

Is very similar than the “Find Relation Microsoft DNS-zones” except we do not have to filter by zone type and that I am iterating over the CimInstanceProperties to set the properties dynamically.

Find All Microsoft DNS-resourceRecord

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
var powershellHosts = Server.getObjectsWithCustomPropertyKey("dnsHost");
var resultObjs = new Array();

for each(var powershellHost in powershellHosts) {
  resultObjs = new Array();
  var sess;
  try {
    sess = powershellHost.openSession();
    // We get the zones first instead of piping them since we want the zone to be a property of the object
    sess.addCommandFromString('get-dnsServerZone | ConvertTo-Json -depth 1 -Compress');

    var invResult = sess.invokePipeline();
    if (invResult.invocationState == 'Failed') {
      System.error(invResult.getErrors());
    } else {

      //System.log(invResult.getHostOutput() );
      var zones = JSON.parse(invResult.getHostOutput());

      for each(var zone in zones) {
        var zoneName = zone.ZoneName;
        sess.addCommandFromString('get-dnsServerResourceRecord ' + zoneName + ' | ConvertTo-Json -depth 3 -Compress'); //3 required to get proper recordData

        var invResult = sess.invokePipeline();
        if (invResult.invocationState == 'Failed') {
          System.error(invResult.getErrors());
        } else {
          //System.log( invResult.getHostOutput() );
          var resourceRecords = JSON.parse(invResult.getHostOutput());

          for each(var resourceRecord in resourceRecords) {            
            var resourceRecordProperties = System.getModule("com.vmware.coe.microsoft.dns.dt").getMicrosoftDNSObjectProperties(resourceRecord);
            // Adding parent zone name
            resourceRecordProperties.put("Zone", zoneName);
            var name = resourceRecord.HostName;
            if (name == "@") name = "(same as parent folder)";
            var encodedId = System.getModule("com.vmware.coe.microsoft.dns.dt").getMicrosoftResourceRecordId(resourceRecordProperties) ;
            var object = DynamicTypesManager.makeObject("Microsoft DNS", "resourceRecord", powershellHost.id + "/" + encodedId, name, new Array());

            for each (var key in resourceRecordProperties.keys) {
              System.log(key + " : " + resourceRecordProperties.get(key));
              object.setProperty(key, resourceRecordProperties.get(key));
            }
            resultObjs.push(object);
          }
        }
      }
    }
  } catch (ex) {
    System.log(ex);
  } finally {
    if (sess) {
      powershellHost.closeSession(sess.getSessionId());
    }
  }
}

The scripting is very similar to “Find All Microsoft DNS-zone”. The difference is that we make two PowerShell calls : one for the zones and one for the records since we must provide a zone for the get-dnsServerResourceRecord cmdlet. At first I managed it in a single call where I piped the zones but since I want to add a zone property for each record this was not working since the zone is not a property of the resource Record. Then I am generating the ID with the resourceRcord properties as explained before.

Find Microsoft DNS-hosts By Id

1
resultObj =  DynamicTypesManager.makeObject("Microsoft DNS", "hosts", "DNS", "DNS", new Array());

Straight forward and certainly useless since we will certainly never us the DNS-Hosts folder as input of any workflow.

Find Microsoft DNS-host By Id

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
var powershellHost = Server.findForType("PowerShell:PowerShellHost", id);
if (powershellHost != null) {
    var object = DynamicTypesManager.makeObject("Microsoft DNS", "host", id, powershellHost.name, new Array());
    try {
        sess = powershellHost.openSession()
        sess.addCommandFromString('get-dnsServerSetting | ConvertTo-Json');

        var invResult = sess.invokePipeline();
        if (invResult.invocationState == 'Failed') {
            System.error(invResult.getErrors());
        } else {
            //System.log( invResult.getHostOutput() );
            var serverSetting = JSON.parse(invResult.getHostOutput());
            for each(var properties in serverSetting.CimInstanceProperties) {
                object.setProperty(properties.Name, properties.Value);
                //System.log(properties.Name + " = " + properties.Value);  
            }
        }
    } catch (ex) {
        System.log(ex);
    } finally {
        if (sess) {
            powershellHost.closeSession(sess.getSessionId());

        }
    }
}

Same code as already completed except we query a single DNS host to gets its property settings.I could put this in an action to call it from the find All Microsoft DNS Host workflow.

Find Microsoft DNS-zones By Id

1
2
3
4
5
switch (id.split("/")[1]) {
  case "forwardLookupZones" : resultObj = DynamicTypesManager.makeObject("Microsoft DNS", "zones", id, "Forward Lookup Zones", new Array()); break;
  case "reverseLookupZones" : resultObj = DynamicTypesManager.makeObject("Microsoft DNS", "zones", id, "Reverse Lookup Zones", new Array()); break;
  case "conditionalForwarders" : resultObj = DynamicTypesManager.makeObject("Microsoft DNS", "zones", "Conditional Forwarders", new Array()); break;
}

Again very similar to the find All method.

Find Microsoft DNS-zone By Id

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
var powershellHost = Server.findForType("PowerShell:PowerShellHost", id.split("/")[0]);
if (powershellHost != null) {
  var sess;
  try {
    var zone = id.split("/")[1];
    sess = powershellHost.openSession();
    sess.addCommandFromString('get-dnsServerZone ' + zone + ' | ConvertTo-Json');
    var invResult = sess.invokePipeline();
    if (invResult.invocationState == 'Failed') {
      System.error(invResult.getErrors());
    } else {
      //System.log( invResult.getHostOutput() );
      var zone = JSON.parse(invResult.getHostOutput());
      object = DynamicTypesManager.makeObject("Microsoft DNS", "zone", powershellHost.id + "/" + zone.ZoneName, zone.ZoneName, new Array());

      for each(var properties in zone.CimInstanceProperties) {
        if (properties.Value == null) object.setProperty(properties.Name, "");
        else object.setProperty(properties.Name, properties.Value);
        //System.log(properties.Name + " = " + properties.Value);  
      }
    }
  } catch (ex) {
    System.log(ex);
  } finally {
    if (sess) {
      powershellHost.closeSession(sess.getSessionId());
    }
  }
  resultObj = object;
}

Here we leverage the fact we used the zone name as the ID. This allows passing it to get-dnsServerZone and get all the object properties.

Find Microsoft DNS-resourceRecord By Id

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
var Base64 = {

  // private property
  _keyStr: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",

  // public method for decoding
  decode: function(input) {
    var output = "";
    var chr1, chr2, chr3;
    var enc1, enc2, enc3, enc4;
    var i = 0;

    input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");

    while (i < input.length) {

      enc1 = this._keyStr.indexOf(input.charAt(i++));
      enc2 = this._keyStr.indexOf(input.charAt(i++));
      enc3 = this._keyStr.indexOf(input.charAt(i++));
      enc4 = this._keyStr.indexOf(input.charAt(i++));

      chr1 = (enc1 << 2) | (enc2 >> 4);
      chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
      chr3 = ((enc3 & 3) << 6) | enc4;

      output = output + String.fromCharCode(chr1);

      if (enc3 != 64) {
        output = output + String.fromCharCode(chr2);
      }
      if (enc4 != 64) {
        output = output + String.fromCharCode(chr3);
      }
    }

    output = Base64._utf8_decode(output);

    return output;
  },

  // private method for UTF-8 decoding
  _utf8_decode: function(utftext) {
    var string = "";
    var i = 0;
    var c = c1 = c2 = 0;

    while (i < utftext.length) {

      c = utftext.charCodeAt(i);

      if (c < 128) {
        string += String.fromCharCode(c);
        i++;
      } else if ((c > 191) && (c < 224)) {
        c2 = utftext.charCodeAt(i + 1);
        string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
        i += 2;
      } else {
        c2 = utftext.charCodeAt(i + 1);
        c3 = utftext.charCodeAt(i + 2);
        string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
        i += 3;
      }
    }

    return string;
  }
}

var decodedId = Base64.decode(id.split("/")[1]);
var resourceRecord = JSON.parse(decodedId);
var object = DynamicTypesManager.makeObject("Microsoft DNS", "resourceRecord", id, resourceRecord.HostName, new Array());

var idObj = JSON.parse(decodedId);

for (var propertyName in idObj) {
  //System.log("*** PropertyName : " + propertyName); 
  object.setProperty(propertyName, idObj[propertyName]);
}

resultObj = object;

Here is the most complex action. First we define a base 64 decode function. Then we use it to decode the ID as a JSON string representing the resource record. Then we use the resourceRecord JavaScript object to get its properties. This way we can manage to extract all properties without ever querying the DNS server for the information. It is a good way to avoid a lot of queries and reliability issues when there are different rcords for the same record name. The drawback is that these properties are as old as the findRelation or findAll call that created the ID for the resourceRecord.

Creating plug-in action methods

At this point we do have a functioning plug-in but while using workflows for development is convenient to understand when they are called and check they run properly it has some important drawbacks for an Orchestrator running in production. Plug-in methods can be called hundreds of times in minutes if not seconds. This is the case if you use an array of DynamicType objects that get iterated in the workflow. Running a workflow instead of an action has a lot of overhead. It will store many workflow runs in the Orchestrator database very quickly.The way to avoid that is to convert all workflows to actions and edit the types so their methods call actions instead of workflows. This will immediately have an great impact on the performance as well in the order of 10 to 20 times faster.So the first thing is to create all the actions in a scripting module and add the same inputs / ouptuts as in the workflows. Then copy paste the workflow scriptable task and not forget to return the result.

Plug-in action methods

If you check the “Update type” workflow you will notice it does not provide you with the option to change the worflow stubs to actions. This leaves us with exporting the Dynamic Types configuration, edit it and import it back again.You can export all the Dynamic Types configuration with saving to file the resource element under Library / DynamicTypes / Configuration

Save the Dynamic Types configuration to a file

Once saved you should always make a copy of it in case you make some unwanted / unsupported changes. It is easier to update it with the copy instead of editing it back.Open it in a text editor. For each object change the findByIdBinding, findAllBinding, hasChildrenInrelationBinding, findRelationBinding values from the workflow ID to the path of the scripting module and action name.

Bindings updated with actions

Once all types updated you can save and update the resource

Updating the Dynamic Types configuration

You can also use this method to do all your types icons, properties, relation changes or even create types.You should immediately feel a speed up when unfolding the inventory of using a list as input.

Providing CRUD workflowsNow that we have a functioning plug-in we will provide the end user with some functionality with some workflows to create, update / operate and delete DNS zones and records.

PowerShell workflows

To do so I first created these workflows with using the PowerShell host as input.

PowerShell workflows

Each of this workflow is written with the same scriptable task except for the PowerShell command and its parameters and matching workflow input parameters.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
if (powershellHost != null) {
  var sess;
  try {
    sess = powershellHost.openSession();
    var command = 'Add-DnsServerResourceRecordA -Name ' + name + ' -ZoneName ' + zoneName;

    if (allowUpdateAny == true) command += ' -AllowUpdateAny ';
    if (createPtr == true) command += ' -CreatePtr ';
    command += ' ' + ipAddress;
    command += ' -PassThru'
    command += ' | ConvertTo-Json -Compress';
    System.log(command);
    sess.addCommandFromString(command);
    var invResult = sess.invokePipeline();
    if (invResult.invocationState == 'Failed') {
      success = false;
      errorCode = invResult.getErrors().toString();
    } else {
      success = true;
      //System.log(invResult.getHostOutput());
      jsonOutput = invResult.getHostOutput();
    }
  } catch (ex) {
    System.log(ex);
  } finally {
    if (sess) {
      powershellHost.closeSession(sess.getSessionId());
    }
  }
} 

The scripting goes like this:

  • Opening a session on the PowerShell host coming from the workflow input
  • Creating the command string with the mandatory inputs.
  • Adding the optional inputs that I have set as boolean inputs in the workflow
  • Adding the Passthru option : This is what tells Powershell to return the object it just created.
  • Piping the result to ConvertTo-Json with the compress option
  • Assigning the JSON string as the output of the workflow
  • Finally closing the session

The delete workflows will not use the passThru since it does not need the objectIn terms of presentation the manfatory command parameters are set as mandatory inputs and the PowerShell host is set with “Show in inventory” so we can right click / run these workflows on the PowerShell host.

Plug-in workflows

For each PowerShell workflow I have created a matching workflow using plug-in types as inputs and outputs.

Plug-in workflows

Each of thse workflows is designed with the same patern:

  • The first scriptable task sets the powerShell host and may also get plug-in objects properties required for the PowerShell workflow
  • The PowerShell workflow runs and sets the JSON output
  • The Last scriptable tasks find and output the object from its ID and invalidates its parent object to refresh the inventory. In case of a delete operation no object need to be found.

    Plug-in workflows steps

Example 1 : “Add forward Primary zone"Getting the PowerShell host from the dnsHost object ID:

1
2
powershellHostId = dnsHost.id;
powershellHost = Server.findForType("PowerShell:PowerShellHost", powershellHostId);

Getting the zone object with its ID based on PowerShell host ID prefix and zone Name postfix, then ivalidating its parent object.

1
2
zone = Server.findForType("DynamicTypes:Microsoft DNS.zone", powershellHostId + "/" + name);
DynamicTypesManager.invalidate("Microsoft DNS", "zones", powershellHostId + "/forwardLookupZones");

Example 2 : “Add an alias (CNAME)“Getting the PowerShell host form the record ID prefix and setting PowerShell workflow input parameters from the record object properties:

1
2
3
4
powershellHostId = record.id.split("/")[0];
powershellHost = Server.findForType("PowerShell:PowerShellHost", powershellHostId);
zoneName = record.Zone;
hostNameAlias = record.name + "." + zoneName + ".";

Getting the newly record object with generating its ID from the JSON output , then ivalidating its parent object.

1
2
3
4
5
6
7
8
///The record id is made with the object properties that are encoded to make sure they do not contain any character not accepted in an ID
var resourceRecordProperties = System.getModule("com.vmware.coe.microsoft.dns.dt").getMicrosoftDNSObjectProperties(JSON.parse(jsonOutput));

// Generating the second part of the ID from the properties
var resourceRecordId = System.getModule("com.vmware.coe.microsoft.dns.dt").getMicrosoftResourceRecordId(resourceRecordProperties);

resourceRecord = Server.findForType("DynamicTypes:Microsoft DNS.resourceRecord", powershellHostId + "/" + resourceRecordId);
DynamicTypesManager.invalidate("Microsoft DNS", "zone", zoneName);

With this design the plug-in can be extended with other PowerShell commands mapping to plug-in workflows.

XaaS

Now that we have a plug-in we can create a new custom resource and use its CRUD workflows as a service.

Custom resource

The first step is to add a new custom resource in vRA. This basically maps a resource Name in vRA to a plug-in object in vRO.

Custom resource

XaaS blueprint

To be able to request a DNS record the next step is to add an XaaS Blueprint using “Add a DNS host (A) workflow

Auto generated Blueprint form

A part from selecting the workflow you need to set the Provisioned Resource to “DNS Host”

Setting the Provisioned resource in the XaaS blueprint

Once created, published and entitled you can test requesting a new DNS record from the catalog. You can customize the icon in Administration / Catalog Items

Testing the DNS hort record request

Add a DNS host catalog item

You will have to provide a zone, a name and IP address and if you want to create the PTR record and allow updates.

New Request

A few seconds later you get a new DNS entry that is under the Itams / Dynamic Types tab

New DNS host record item provisioned

Resource action

You can also add an action to Remove the record that maps to the “Remove a resource record” workflow. The important part here is to indicate this is a disposal type of action so vRA will remove the item once the workflow has been successfully ran.

Resource action with disposal option

Testing the Remove a DNS host record action

Then you can test the removal action from the items tab.

Remove a DNS Host

You now successfully manage the lifecycle of DNS host records within vRA with kepping records on who requested / removed a record and having end users managing this on their own.It is nice but does offer a lot of freedom on what IP and host name. We can automate further using an XaaS component

Software blueprint including DNS XaaS component

Now that we have XaaS components we can make these part of a software blueprint so DNS records can be created on the fly at deployment time, being displayed as part of a deployment component and deleted with the deployment. This basicly means we will manage the lifecycle of the application and the DNS record as one.

XaaS component workflow

The first requirement is to be able to start the workflow adding the record from the deployment. This is done by adding the XaaS Blueprint to the software Blueprint. However we cannot just use our existing XaaS blueprint since the designer passes the blueprint output fileds such as IP address and host name has an array of strings in a JSON format.To use with the designer I have created a wrapper workflow converting JSON inputs as the name and IP string that calls our “Add a host (A)” plug-in workflow. I have also added a zone drop down using strings instead of array of zones since vRA has an issue listing array of Dynamic Types.

Workflow wrapper for XaaS blueprint component

The scriptable task in the workflow contains the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
var ips = JSON.parse(ipJson);
ipAddress = ips[0];
var names = JSON.parse(nameJson);
name = names[0];

var hosts = Server.findAllForType("DynamicTypes:Microsoft DNS.host");
if (hosts.length == 1) zone = Server.findForType("DynamicTypes:Microsoft DNS.zone", hosts[0].id + "/" + zoneName);
else {
  var zones = System.getModule("com.vmware.coe.microsoft.dns.dt").getMicrosoftDNSForwardZones();
  for each (var zone in zones) {
    if (zone.name == zoneName) break;
  }  
}

At first we get the ipAddress and name from the JSON output. This is done using JSON.parse which will return an array which in my case will have a single element.The second part is to create a zone attribute from the zone string input to pass it to the add a host (A) workflow.For the end user confort I have also created an action that get all the Forward zones and return these as an array that I use with the presentation properties “Predefined list of elements"Finally I place this wotkflow in an XaaS components folder so I can identify the workflows in there as being designed for being used as XaaS components.

XaaS host service blueprint

Now that we have a workflow that can be used by the designer we can add it as a new XaaS blueprint. The important part here is the “Make available as a component in the design canvas”

XaaS blueprint for XaaS component

The next important thing to set is the provisioned resource and the component lifecycle tab.

XaaS component lifecycle

Here we set the Destroy workflow so whenever the deployment gets destroyed the record will be deleted as well.

Adding the XaaS component in the software blueprint

Next we need to edit our software blueprint(s) to include the DNS XaaS component. For this we drag and drop the XaaS component we created on the schema and for the ipJson and nameJson switch to the advanced view to define the value as fields from the blueprint output (IP address and Blueprint Name)

Binding the blueprint IP and host name outputs to the XaaS component inputs

Testing the software blueprint with XaaS component

We can now request our software blueprint with the DNS host record component. Note that the name and address fields are not shown to the end user since we bound them to the software blueprint. The end user here will select the zone, and the record option. In production we would likely have set constants defaults for these in the software blueprint designer. This screenshot also show the drop down I created for the zones list.

Software blueprint with XaaS DNS host record component

After we request this blueprint we get a new deployment that include the DNS record.

Deployment including the DNS host record

And when we destroy the deplyment the DNS record get deleted.Using XaaS component as part of the blueprint allows to manage the deployed hosts and their DNS record in one deployment. We could also add resource actions to operate the XaaS component within the deployment. This is a powerfull way to avoid having two different processes to manage the application and the DNS record which often result in having DNS records still existing but unused.

Conclusion

This is quite an extensive tutorial since I have included all major steps including scripting to develop the whole integration from scratch. However I am sharing the Dynamic Types plug-in I have created so you can use it without having to create your own and learn from it, eventually improve it or create other integrations.