Aria Automation IPAM Integration - the Easy Way

Share on:

How to implement external IPAM integration via Orchestrator

Motivation

vRealize Automation version 7.x external IPAM support was provided by Orchestrator actions/workflows that allowed an easy way to integrate external providers to the IaaS service. Aria Automation version 8.x changed this model, as it is a SAAS first application and the cloud version does not have Orchestrator. So the engineers came up with a container-based solution called VMware Aria Automation Third-Party IPAM SDK. This skeleton code allows to create custom IPAM providers running as extensibility actions (ABX) within containers. This approach is very different from what we had in v7.x and made the development/debugging more complex. We took the challenge and implemented a phpIPAM provider by the SDK. I cannot share the result but if you are interested there are some public implementations, e.g. https://github.com/jbowdre/phpIPAM-for-vRA8.

Having an on-premises Aria Automation (with Orchestrator) I wondered if the integration can implemented via vRO workflows. This would allow easier development process, more (existing) Orchestator integrations and better visibility of code runs. In the following chapters we'll reimplement our phpIPAM plugin functionality in pure Orchestrator JavaScript code.

The Cloud Template

Let's create a cloud template that has multiple virtual machines, with multiple interfaces to make sure our code would work with any possible combination.

 1formatVersion: 1
 2inputs:
 3  count:
 4    type: integer
 5    default: 1
 6resources:
 7  internal:
 8    type: Cloud.vSphere.Machine
 9    properties:
10      count: ${input.count}
11      image: photon
12      flavor: small
13      networks:
14        - network: ${resource.lan0.id}
15        - network: ${resource.lan1.id}
16  public:
17    type: Cloud.vSphere.Machine
18    properties:
19      count: ${input.count}
20      image: photon
21      flavor: small
22      networks:
23        - network: ${resource.dmz.id}
24  lan0:
25    type: Cloud.vSphere.Network
26    properties:
27      networkType: existing
28      constraints:
29        - tag: zone:lan
30  lan1:
31    type: Cloud.vSphere.Network
32    properties:
33      networkType: existing
34      constraints:
35        - tag: zone:mgt
36  dmz:
37    type: Cloud.vSphere.Network
38    properties:
39      networkType: existing
40      constraints:
41        - tag: zone:dmz
42

The internal VMs connect to the lan and mgt zones, while the public VMs connect to the dmz zone.

Network Profiles

We create 3 profiles. The Prod profile will cover the network connections of the internal VMs, while DMZ1 and DMZ2 profiles can provide networks for the public VMs. It is important to note that each VM with the same connection combination must use a single network profile, containing all the matching networks required to connect to.



About the profile selection process: the Provisioning Service can select Prod profile for the internal VMs, and can select from DMZ1 and DMZ2 for public VMs. Within Prod profile there are two networks that can connect to lan zone: net1-pr-nsxt-ls and net2-pp-nsxt-ls. mgt zone is only available via net3-mg-nsxt-ls. Also, within the DMZ profiles, only a single network can be selected. Let's see how these options are presented.

Event Broker Subscription

EBS is the super glue of Aria Automation: it allows us to subscribe to a particular event and run arbitrary code once it is triggered. In order to create IPAM integration and network selections for our IaaS cloud template, we need to use the Network Configure (network.configure) event topic for VM creation and Compute Post Removal (compute.removal.post) event topic for VM deletion. Both topics require workflows with the usual inputProperties input. The following outputs are available for Network Configure:


Important! Make sure to create the EBS with blocking settings, otherwise the return values are not used in the deployment process.

Many attributes of network configuration can be overriden in this deployment phase. For the sake of simplicity we will set only networkSelectionIds and addresses, the others will be configured on the network level (CIDR, gateways, DNS servers, etc.). They could also be provided by the IPAM solution, though.


Event Data

Let's create a workflow that is triggered at Network Configure topic with the inputProperties input and print out the input variable value to see what we need to process:

1System.log(JSON.stringify(inputProperties, null, 2));

When we request 2x2 VMs (count = 2) we'll see 2 runs of the workflow. The output of the first run:

 1{
 2  "componentId": "internal",
 3  "endpointId": "d098094c-5e88-4428-bcca-83e5ab6e9f6b",
 4  "externalIds": [
 5    "internal-342",                                      // first VM hostname 
 6    "internal-343"                                       // second VM hostname
 7  ],
 8  "blueprintId": "d2ce10f3-6671-4588-9bf7-9d4b47085865",
 9  "tags": {
10    "project": "Prod1"
11  },
12  "customProperties": {
13    "flavor": "small",
14    "neglectPowerOffVms": "false",
15    "image": "photon",
16    "zone_overlapping_migrated": "true",
17    "count": "2",                                        // two VMs requested
18    "project": "ffab9ffd-a072-4617-bbb4-54101b3b5dd1",
19    "flavorMappingName": "small",
20    "isSimulate": "false"
21  },
22  "networkProfileIds": [
23    "3aca5c1f-443f-42fa-9b4b-66e853ad11f3",              // first VM selected network profile
24    "3aca5c1f-443f-42fa-9b4b-66e853ad11f3"               // second VM selected network profile
25  ],
26  "componentTypeId": "Cloud.vSphere.Machine",
27  "requestId": "3bdde4cf-3b12-4b43-90d8-7ccf2c47b1b6",
28  "deploymentId": "fa4b9df4-9664-474a-a3f2-056db527e6e3",
29  "zoneId": "d8666ddd-804e-4587-a37e-58b72708743d",
30  "networkSelectionIds": [
31    [                                                    // first VM
32      [                                                  // first NIC
33        "c2168e8f-62aa-45e2-9dd9-502d22359214",          // matching network in network profile
34        "6bf0d8fb-6f39-4a43-b5d9-56a371030171"           // matching network in network profile
35      ],
36      [                                                  // second NIC
37        "dd65009b-3900-46ee-a6a2-d3226d755f4b"           // matching network in network profile
38      ]
39    ],
40    [                                                    // second VM
41      [                                                  // first NIC
42        "c2168e8f-62aa-45e2-9dd9-502d22359214",          // matching network in network profile
43        "6bf0d8fb-6f39-4a43-b5d9-56a371030171"           // matching network in network profile
44      ],
45      [                                                  // second NIC
46        "dd65009b-3900-46ee-a6a2-d3226d755f4b"           // matching network in network profile
47      ]
48    ]
49  ],
50  "projectId": "ffab9ffd-a072-4617-bbb4-54101b3b5dd1",
51  "resourceIds": [
52    "c084c3cb-2506-4477-b738-6debd7ac3827",
53    "483de42b-6a13-437e-90f9-e1e7a9cbc158"
54  ]
55}

The second run output is simpler, as the public VMs have only a single interface:

 1{
 2  "componentId": "public",
 3  "endpointId": "d098094c-5e88-4428-bcca-83e5ab6e9f6b",
 4  "externalIds": [
 5    "public-344",                                        // first VM hostname
 6    "public-345"                                         // second VM hostname
 7  ],
 8  "blueprintId": "d2ce10f3-6671-4588-9bf7-9d4b47085865",
 9  "tags": {
10    "project": "Prod1"
11  },
12  "customProperties": {
13    "flavor": "small",
14    "neglectPowerOffVms": "false",
15    "image": "photon",
16    "zone_overlapping_migrated": "true",
17    "count": "2",                                        // two VMs requested
18    "project": "ffab9ffd-a072-4617-bbb4-54101b3b5dd1",
19    "flavorMappingName": "small",
20    "isSimulate": "false"
21  },
22  "networkProfileIds": [
23    "14727fec-33cf-437d-8a20-91dcffef2717",              // first VM selected network profile
24    "14727fec-33cf-437d-8a20-91dcffef2717"               // second VM selected network profile
25  ],
26  "componentTypeId": "Cloud.vSphere.Machine",
27  "requestId": "3bdde4cf-3b12-4b43-90d8-7ccf2c47b1b6",
28  "deploymentId": "fa4b9df4-9664-474a-a3f2-056db527e6e3",
29  "zoneId": "d8666ddd-804e-4587-a37e-58b72708743d",
30  "networkSelectionIds": [
31    [                                                    // first VM
32      [                                                  // first NIC
33        "a9ec7502-5fb6-45fc-9f99-13f946a28b8f"           // matching network in network profile
34      ]
35    ],
36    [                                                    // second VM
37      [                                                  // first NIC
38        "a9ec7502-5fb6-45fc-9f99-13f946a28b8f"           // matching network in network profile
39      ]
40    ]
41  ],
42  "projectId": "ffab9ffd-a072-4617-bbb4-54101b3b5dd1",
43  "resourceIds": [
44    "e3a981a0-80fa-4b39-9c06-7c4d021b5f08",
45    "c1a79ccf-4600-4bb2-9ff9-5babdd63df40"
46  ]
47}

Matching networks against IPAM

We need to get further information about the matching networks and find them in IPAM to make the allocation or deallocation. Using the IaaS API we can get the needed data.

For Aria API we use the Orchestrator Plug-in for vRealize Automation.

The EBS system user does not have Cloud Assembly access and won't be able to read IaaS API, so we create a service user for that purpose with Read permissions:

and create a vRA endpoint within vRO:

To collect the network information of the sample subnet 6bf0d8fb-6f39-4a43-b5d9-56a371030171 we can run the following code:

1var vraHost = VraHostManager.findHostsByType("vra-onprem").filter(
2    function (host) {
3        return host.name == 'apiuser'  // find the vRA endpoint to use
4    }
5)[0];
6
7var fabricNetwork = VraEntitiesFinder.getFabricNetworks(vraHost, "id eq 6bf0d8fb-6f39-4a43-b5d9-56a371030171")[0];
8System.log(fabricNetwork);

The output:

 1DynamicWrapper (Instance) : [VraFabricNetwork]-[class com.vmware.o11n.plugin.vra_gen.FabricNetwork_Wrapper] -- VALUE : class FabricNetwork {
 2    owner: null
 3    links: {cloud-accounts=class Href {
 4        hrefs: [/iaas/api/cloud-accounts/d098094c-5e88-4428-bcca-83e5ab6e9f6b]
 5        href: null
 6    }, network-domain=class Href {
 7        hrefs: null
 8        href: /iaas/api/network-domains/c9932000429de93c72785683eb33e47d74936aaa
 9    }, self=class Href {
10        hrefs: null
11        href: /iaas/api/fabric-networks/6bf0d8fb-6f39-4a43-b5d9-56a371030171
12    }, region=class Href {
13        hrefs: null
14        href: /iaas/api/regions/850e477a-8d22-43bb-9a2e-f80aa1daafb6
15    }}
16    externalRegionId: Datacenter:datacenter-2
17    description: null
18    externalId: DistributedVirtualPortgroup:dvportgroup-49872
19    orgId: 360bc331-1360-4d76-8000-aae9f7491989
20    tags: [class Tag {
21        value: lan
22        key: zone
23    }]
24    createdAt: 2023-08-15
25    ipv6Cidr: null
26    cloudAccountIds: [d098094c-5e88-4428-bcca-83e5ab6e9f6b]
27    isDefault: null
28    customProperties: {}
29    name: net1-pr-nsxt-ls
30    isPublic: null
31    cidr: 10.32.101.0/24
32    id: 6bf0d8fb-6f39-4a43-b5d9-56a371030171
33    updatedAt: 2023-12-11
34}

As hightlighted, we can search in IPAM by network name, network CIDR or even by tags if we want.

phpIPAM Functions

The phpIPAM documentation explains how to use this IPAM solution via API. We'll need an app_id and a user in phpIPAM. To receive and send data via the API a token must be received and included in further REST client calls. We implement this with 2 functions:

 1function getIPAMtoken(restHost) {
 2  var request = restHost.createRequest("POST", "/user/", "");
 3  request.contentType = "application/json";
 4  var response = request.execute();
 5  if (response.statusCode < 400) return JSON.parse(response.contentAsString).data.token;
 6  else throw "phpIPAM token error:" + response.contentAsString;
 7}
 8
 9function IPAMrequest(restHost, token, path, method, data) {
10  var request = restHost.createRequest(method, path, data ? JSON.stringify(data) : "");
11  request.contentType = "application/json";
12  request.setHeader("phpipam-token", token);
13  var response = request.execute();
14  System.debug(response.contentAsString);
15  try {
16    return JSON.parse(response.contentAsString);
17  }
18  catch (e) {
19    throw "phpIPAM request error:" + response.contentAsString;
20  }
21}

The REST host is registered with Basic Authentication:


We can search for a subnet by CIDR on /api/app_id/subnets/cidr/{subnetCIDR}, and allocate an IP on /api/app_id/addresses/first_free/{subnetId}.

Expected workflow output

The IP allocation workflow can return quite a few parameters, we chose to implement 2 of them: setting the IP addresses by addresses

 1[
 2  [
 3    "10.32.102.34",
 4    "10.32.103.31"
 5  ],
 6  [
 7    "10.32.102.35",
 8    "10.32.103.32"
 9  ]
10]

and selecting the target network by networkSelectionIds.

 1[
 2  [
 3    [
 4      "c2168e8f-62aa-45e2-9dd9-502d22359214"
 5    ],
 6    [
 7      "dd65009b-3900-46ee-a6a2-d3226d755f4b"
 8    ]
 9  ],
10  [
11    [
12      "c2168e8f-62aa-45e2-9dd9-502d22359214"
13    ],
14    [
15      "dd65009b-3900-46ee-a6a2-d3226d755f4b"
16    ]
17  ]
18]

Putting IP Allocate code all together

Let's see the IP allocate workflow code. We have 3 nested loops: the outer is looping through the VMs, the next one is looping through the NICs and the inner one is looping through the networks and trying to get an IP. If the IP is granted, we quit the inner loop, otherwise we try the other networks selected in the network profile.

We assume that only a single IPAM network is found for a specific CIDR.

 1var vraHost = VraHostManager.findHostsByType("vra-onprem").filter(
 2    function (host) {
 3        return host.name == 'apiuser'  // find the vRA endpoint to use
 4    }
 5)[0];
 6
 7var ipamHost = RESTHostManager.getHosts()
 8  .map(function (hostid) {return RESTHostManager.getHost(hostid)})
 9  .filter(function (host) {return "phpIPAM" == host.name})[0];
10
11var ipamToken = getIPAMtoken(ipamHost);
12
13addresses = [];
14networkSelectionIds = [];
15
16for (vmIndex in inputProperties.resourceIds) {
17  System.log("Processing VM " + inputProperties.externalIds[vmIndex]);
18  addresses.push([]);
19  networkSelectionIds.push([]);
20
21  for (nicIndex in inputProperties.networkSelectionIds[vmIndex]) {
22    System.log("NIC" + nicIndex);
23    for each (networkId in inputProperties.networkSelectionIds[vmIndex][nicIndex]) {
24      var fabricNetwork = VraEntitiesFinder.getFabricNetworks(vraHost, "id eq " + networkId)[0];
25      System.log("Checking network " + fabricNetwork.name);
26      var response = IPAMrequest(ipamHost, ipamToken, "/subnets/cidr/" + fabricNetwork.cidr, "GET");
27      if (200 == response.code) {
28        var subnetId = response.data[0].id;
29        System.log("Subnet " + fabricNetwork.cidr + " found in IPAM with ID " + subnetId);
30        response = IPAMrequest(ipamHost, ipamToken, "/addresses/first_free/" + subnetId, "POST", 
31                                  {hostname: inputProperties.externalIds[vmIndex]});
32        if (201 == response.code) {
33          System.log("Reserved IP " + response.data);
34          addresses[vmIndex].push(response.data);
35          networkSelectionIds[vmIndex].push([networkId]);
36          break;
37        }
38        else System.log("No free IP found")
39      }
40      else System.log("Subnet " + fabricNetwork.cidr + " not found in IPAM");
41    }
42  }
43
44  System.log("---")
45}
46
47System.log(JSON.stringify(networkSelectionIds, null, 2));
48System.log(JSON.stringify(addresses, null, 2));

IP Release

Releasing is much simpler. The EBS workflow is run by VM and we need only the IPs from the inputProperties:

1{
2  "addresses": [
3    [
4      "10.32.102.34",
5      "10.32.103.31"
6    ]
7  ],
8  "componentId": "internal[0]",
9...

The code (no workflow outputs):

 1var ipamHost = RESTHostManager.getHosts()
 2  .map(function (hostid) {return RESTHostManager.getHost(hostid)})
 3  .filter(function (host) {return "phpIPAM" == host.name})[0];
 4
 5var ipamToken = getIPAMtoken(ipamHost);
 6
 7for each (ip in inputProperties.addresses[0]) {
 8    var response = IPAMrequest(ipamHost, ipamToken, "/addresses/search/" + ip, "GET");
 9    if (200 == response.code) {
10        var addressId = response.data[0].id;
11        System.log("IP " + ip + " found in IPAM with ID " + addressId);
12        response = IPAMrequest(ipamHost, ipamToken, "/addresses/" + addressId, "DELETE");
13        if (200 == response.code) System.log("Released IP " + ip);
14        else System.log("Could not release IP " + ip + " (" + response.contentAsString + ")");
15    }
16    else System.log("IP " + ip + " not found in IPAM");
17}

Conclusion

In less than 100 lines of code we could implement a fully functional phpIPAM integration:


Bear in mind this is just a sample code, not production ready (for the sake of simplicity). Error handling needs to be enhanced. Download the com.test.phpipam package containing the workflows from GitHub: https://github.com/kuklis/vro8-packages