Aria Automation IPAM Integration - the Easy Way
How to implement external IPAM integration via Orchestrator
Table of Contents
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