DNS resolve OCVS SDDC resources from a private MS DNS Server [Automation]

Business Requirement

I was working on setting up an OCVS environment for one of the biggest stock exchanges in the APAC region. The customer’s requirement was to join the VC deployed by OCVS to a custom stock exchange domain privately hosted in the customer’s on-premises environment. I thought the process was simple.

  1. Create a listener and forwarder in OCI DNS.
  2. Create a rule to forward the DNS traffic for your custom domain (fluffyclouds.com in my case ) to the private DNS server.
  3. Create the OCI DNS Zones hosting OCVS components as conditional forwarders in my private MS AD.
  4. Test DNS resolution is working, follow the standard VC URL rename and domain join process.

Challenge

Well the process looks straightforward and there is a formal Oracle Guide to do this but it can be challenging to follow in a complex environment where the customer has a number of DNS zones. Finding the correct DNS zones for the OCVS components manually, was like finding something to watch on Netflix!

A lot of our customers are new to Oracle Cloud and OCVS, it would be unfair for us to assume that the customers will be able to easily find the private zones esp if you have a lot of them.

Solution

After I spent an all-nighter gathering the required information to create these conditional forwarders, I thought there could be a better way of doing this. That’s where I decided to use my new love Python to solve this problem.

In this article, I am using the naming conventions

Private Microsoft DNS Server IP192.168.100.11
Private Microsoft DNS Server Domain Namefluffyclouds.com
OCI Listener FQDNocvslistner.subnet10261946.ocvs1.oraclevcn.com
OCI Listener IP10.1.254.211
OCI Forwarder FQDNocvsfwdr.subnet10261946.ocvs1.oraclevcn.com
OCI Forwarder IP10.1.254.41
DNS for OCVSDefault OCI DNS

For keeping the things simple, we will manually create the following:

  1. OCI Listener
  2. OCI Forwarder
  3. Rule to forward any fluffyclouds.com traffic to my private DNS server 192.169.100.11

We can also use the Python SDK to do this, but I already had an existing DNS FWD/RCVR so I am skipping that part.

Listener and Forwarder Created

Listener and Forwarder

DNS Foward Rule Created for fluffyclouds.com

Forwarding Rule

Solution Automation

Now comes the fun part of the solution!

I decided to start with simple user inputs as shown below. “vcn-compartment-id” will be blank if the VCN is deployed in the same compartment as OCVS and that’s all the user needs to provide, automation will take care of the rest!

{
    "ocvs-compartment-id": "ocid1.compartment.oc1..xxxxxxxxxxxxxxxxxxx",
    "vcn-compartment-id": "",
    "vcn-name":"yyyyyyyyy"
}

Automation workflow

From the user inputs provided on a high level following tasks will be executed

  1. Use Python SDK to query the SDDC API
  2. Gather the SDDC details and push them to a JSON file.
  3. Gather ESXi details and push them to JSON.
  4. Get the VCN ID with the VCN name provided by the user.
  5. Find the default DNS resolver for the VCN.
  6. Get all the private OCI zones in the resolver.
  7. Iterate through each zone record to find a match with the SDDC elements.
  8. Push the matches to the JSON along with the reverse lookup zone information for ESXi and DNS Resolver / Listener.
  9. Write the JSON file to the local OS.

Here’s the process flow for the solution.

Solution Workflow

This is the script that does that magic for you!

I have added the running comments to explain the code.

import oci
import json
from datetime import datetime

start = datetime.now()
config = oci.config.from_file("~/.oci/configp", "DEFAULT")
# open the JSON file
f = open('/Users/barjindersingh/.oci/dnsinputs/userInputs.json')
# parse the JSON file
try:
    data = json.load(f)
except json.decoder.JSONDecodeError:
    print("Invalid documentTemplate JSON")

# User Input
compartment_ocvs = data["ocvs-compartment-id"]
if (data["vcn-compartment-id"]):
    compartment_vcn = data["vcn-compartment-id"]
else:
    compartment_vcn = compartment_ocvs
vcnName = data["vcn-name"]
# Global Variables
processInputs = {}
selected_resolver_id = ""
viewId = ""
esxiName = ""
sddc_id = ""
################  GET VCN ID with Name Start  ####################
# Initialize service client with default config file
core_client = oci.core.VirtualNetworkClient(config)
list_vcns_response = core_client.list_vcns(
    compartment_id=compartment_ocvs,
    limit=473,
    display_name= vcnName
)
# Get the data from response
vcnId = list_vcns_response.data[0].id
################  GET VCN ID with Name End  ####################

#################### SDDC Section Begin ###########################
# Initialize service client with default config file
ocvp_client = oci.ocvp.SddcClient(config)
# Send the request to service, some parameters are not required, see API
# doc for more info
list_sddcs_response = ocvp_client.list_sddcs(
    compartment_id=compartment_ocvs,
    sort_order="DESC",
    sort_by="displayName"
)
# Add the SDDC details to a JSON file.
for item in list_sddcs_response.data.items:
    processInputs[str(item.nsx_manager_fqdn).split("-")[0]] = item.nsx_manager_fqdn
    processInputs[str(item.vcenter_fqdn).split("-")[0]] = item.vcenter_fqdn
    processInputs[str(item.hcx_fqdn).split("-")[0]] = item.hcx_fqdn
    sddc_id= item.id
#################### SDDC Section End ###########################

#################### ESXi Section Begin ###########################
# Initialize service client with default config file
ocvp_client = oci.ocvp.EsxiHostClient(config)
# Send the request to ESXi service
list_esxi_hosts_response = ocvp_client.list_esxi_hosts(
    sddc_id=sddc_id)
# Get the data from response
# Loop through the ESXi hosts and the display names to the JSON File
for item in list_esxi_hosts_response.data.items:
    processInputs["ESXi-"+str(item.display_name).split("-")[1]] = item.display_name
#################### ESXi Section END ###########################

##################### DNS Section##################################################
# Create DNS Client
dns_client = oci.dns.DnsClient(config)
# Get a list of all the resolvers.
list_resolvers_response = dns_client.list_resolvers(
    compartment_id=compartment_vcn,
    limit=37,
    sort_order="DESC",
    sort_by="displayName",
    lifecycle_state="ACTIVE",
    scope="PRIVATE")
# Get the default view id for the listed VCN
for x in list_resolvers_response.data:
    if (x.attached_vcn_id == vcnId):
        selected_resolver_id = x.id
        viewId = x.default_view_id
# Get Default VCN Resolver
get_resolver_response = dns_client.get_resolver(
    resolver_id=selected_resolver_id,
    scope="PRIVATE")
# Update the JSON file with forwarders and resolvers
for endpoint in get_resolver_response.data.endpoints:
    if(endpoint.is_forwarding):
        processInputs["DNSForwarder-" + str(endpoint.forwarding_address).split(".")[3]] = endpoint.name
    elif(endpoint.is_listening):
        processInputs["DNSListner-" + str(endpoint.listening_address).split(".")[3]] = endpoint.name
# Get all the DNS Zones with the view ID
all_zones = oci.pagination.list_call_get_all_results(
    dns_client.list_zones, compartment_vcn, scope="PRIVATE", view_id=viewId)

#Loop through the zones and zone records to find a match and update the JSON file
try:
    for k, v in processInputs.items():
        for y in all_zones.data:
            zoneRecord = dns_client.get_zone_records(y.id)
            for b in zoneRecord.data.items:
                if str(b.rdata).split(".")[0].lower() == str(v).lower():
                    processInputs[k]["reverse"] = {
                        "fqdn": b.domain, "zone": y.name, "rdata": b.rdata, "id": y.id}
                elif str(b.domain).lower() == str(v).lower() or str(b.domain).split(".")[0].lower() == str(v).lower():
                    processInputs[k] = {"fqdn": b.domain,
                                        "zone": y.name, "rdata": b.rdata, "id": y.id}
except: 
    print("OOps! An exception occurred finding the DNZ Zone Information")
#Print the JSON File. 
print(json.dumps(processInputs, indent=4))
# Serializing json 
json_object = json.dumps(processInputs, indent = 4)
  
# Writing to sample.json
with open("OCVS-DNS-Zones.json", "w") as outfile:
    outfile.write(json_object)
#calculate the execution time
end = datetime.now()
print("The time of execution of above program is :",
      str(end-start)[5:])

Below is the JSON output of the script. As you can see it lists the FQDN, Zone, Rdata, ID, and Reverse Zones for the SDDC components from the OCI DNS Private Zones. These inputs would be required to configure in the private MS DNS server.

{
    "nsx": {
        "fqdn": "nsx-ocvs1.sddc.nrt.oci.oraclecloud.com",
        "zone": "sddc.nrt.oci.oraclecloud.com",
        "rdata": "10.1.3.xx",
        "id": "ocid1.dns-zone.oc1.ap-tokyo-1.xxxxxx"
    },
    "vcenter": {
        "fqdn": "vcenter-ocvs1.sddc.nrt.oci.oraclecloud.com",
        "zone": "sddc.nrt.oci.oraclecloud.com",
        "rdata": "10.1.3.xx",
        "id": "ocid1.dns-zone.oc1.ap-tokyo-1.xxxxx"
    },
    "hcxmgr": {
        "fqdn": "hcxmgr-ocvs1.sddc.nrt.oci.oraclecloud.com",
        "zone": "sddc.nrt.oci.oraclecloud.com",
        "rdata": "10.1.3.xx",
        "id": "ocid1.dns-zone.oc1.ap-tokyo-1.xxxxxx"
    },
    "ESXi-3": {
        "fqdn": "ocvs1-3.sub09090413461.ocvs1.oraclevcn.com",
        "zone": "sub09090413461.ocvs1.oraclevcn.com",
        "rdata": "10.1.0.xx",
        "id": "ocid1.dns-zone.oc1.ap-tokyo-1.xxxxxx",
        "reverse": {
            "fqdn": "xx.0.1.10.in-addr.arpa",
            "zone": "1.10.in-addr.arpa",
            "rdata": "ocvs1-3.sub09090413461.ocvs1.oraclevcn.com.",
            "id": "ocid1.dns-zone.oc1.ap-tokyo-1.xxxx"
        }
    },
    "ESXi-2": {
        "fqdn": "ocvs1-2.sub09090413461.ocvs1.oraclevcn.com",
        "zone": "sub09090413461.ocvs1.oraclevcn.com",
        "rdata": "10.1.0.xx",
        "id": "ocid1.dns-zone.oc1.ap-tokyo-1.xxxxx",
        "reverse": {
            "fqdn": "xx.0.1.10.in-addr.arpa",
            "zone": "1.10.in-addr.arpa",
            "rdata": "ocvs1-2.sub09090413461.ocvs1.oraclevcn.com.",
            "id": "ocid1.dns-zone.oc1.ap-tokyo-1.xxxx"
        }
    },
    "ESXi-1": {
        "fqdn": "ocvs1-1.sub09090413461.ocvs1.oraclevcn.com",
        "zone": "sub09090413461.ocvs1.oraclevcn.com",
        "rdata": "10.1.0.xx",
        "id": "ocid1.dns-zone.oc1.ap-tokyo-1.xxxx",
        "reverse": {
            "fqdn": "xx.0.1.10.in-addr.arpa",
            "zone": "1.10.in-addr.arpa",
            "rdata": "ocvs1-1.sub09090413461.ocvs1.oraclevcn.com.",
            "id": "ocid1.dns-zone.oc1.ap-tokyo-1.xxxx"
        }
    },
    "DNSForwarder-41": {
        "fqdn": "ocvsfwdr.subnet10261946.ocvs1.oraclevcn.com",
        "zone": "subnet10261946.ocvs1.oraclevcn.com",
        "rdata": "10.1.254.xx",
        "id": "ocid1.dns-zone.oc1.ap-tokyo-1.xxxx",
        "reverse": {
            "fqdn": "xx.254.1.10.in-addr.arpa",
            "zone": "1.10.in-addr.arpa",
            "rdata": "ocvsfwdr.subnet10261946.ocvs1.oraclevcn.com.",
            "id": "ocid1.dns-zone.oc1.ap-tokyo-1.axxxx"
        }
    },
    "DNSListner-211": {
        "fqdn": "ocvslistner.subnet10261946.ocvs1.oraclevcn.com",
        "zone": "subnet10261946.ocvs1.oraclevcn.com",
        "rdata": "10.1.254.xx",
        "id": "ocid1.dns-zone.oc1.ap-tokyo-1.aaaaaaaapvivuqj6dcey75gsj65gfh33mqiv4bvlkfhznzpa7ag63rpemw7a",
        "reverse": {
            "fqdn": "xx.254.1.10.in-addr.arpa",
            "zone": "1.10.in-addr.arpa",
            "rdata": "ocvslistner.subnet10261946.ocvs1.oraclevcn.com.",
            "id": "ocid1.dns-zone.oc1.ap-tokyo-1.xxxx"
        }
    }
}

The reason for using JSON as an output format is that it’s portable. I can use the python pypsrp module to configure these DNS zones with automation and use the above JSON as an input file.

Microsoft DNS Configuration

Now that, all the pre-requisites are in a single JSON file, the next steps are easy peasy!

Create a Reverse Lookup Zone aligned with the Oracle Cloud Infrastructure (OCI) Listener and create a pointer record for the Listener IP.

Create the conditional forwarders for the SDDC Components, ESXi Servers, and Reverse Zones for ESXi. All the entries should be pointing to the OCVS listener FQDN and IP Address created in the article above.

Conditional Forwarding

Each conditional forwarder configuration.

Conditional Forwarder Configuration

After the DNS zones are created, I am able to resolve the ESXi hosts, NSX manager and all other components from my fluffyclouds.com domain.

C:\Users\Administrator>ping -a 10.1.0.xx
Pinging ocvs1-1.sub09090413461.ocvs1.oraclevcn.com [10.1.0.xx] with 32 bytes of data:
Reply from 10.1.0.xx: bytes=32 time<1ms TTL=62
Reply from 10.1.0.xx: bytes=32 time=1ms TTL=62
Reply from 10.1.0.xx: bytes=32 time<1ms TTL=62
Reply from 10.1.0.xx: bytes=32 time<1ms TTL=62
C:\Users\Administrator>ping ocvs1-2.sub09090413461.ocvs1.oraclevcn.com
Pinging ocvs1-2.sub09090413461.ocvs1.oraclevcn.com [10.1.0.xx] with 32 bytes of data:
Reply from 10.1.0.xx: bytes=32 time<1ms TTL=62
Reply from 10.1.0.xx: bytes=32 time<1ms TTL=62
Reply from 10.1.0.xx: bytes=32 time=1ms TTL=62
Reply from 10.1.0.xx: bytes=32 time=1ms TTL=62
C:\Users\Administrator>ping ocvsfwdr.subnet10261946.ocvs1.oraclevcn.com
Pinging ocvsfwdr.subnet10261946.ocvs1.oraclevcn.com [10.1.254.xx] with 32 bytes of data:
C:\Users\Administrator>ping nsx-ocvs1.sddc.nrt.oci.oraclecloud.com
Pinging nsx-ocvs1.sddc.nrt.oci.oraclecloud.com [10.1.3.xxx] with 32 bytes of data:
Reply from 10.1.3.xxx: bytes=32 time<1ms TTL=62
Reply from 10.1.3.xxx: bytes=32 time<1ms TTL=62
Reply from 10.1.3.xxx: bytes=32 time<1ms TTL=62
Reply from 10.1.3.xxx: bytes=32 time<1ms TTL=62

Conclusion

Oracle Cloud is a full-blown cloud platform with a range of services for every organisation large or small and Python SDK for OCI is a powerful tool to automate the cloud. There is a lot that can be automated with easy SDK options. I hope this article will help you save some time and effort. Keep reading!

This Post Has One Comment

  1. Mayur gadge

    Hi barji,

    Very nice article i can relate this problem.

    Regards,
    Mayur

Leave a Reply