top of page
Search
Marco

Using hidden APIs for Group-Based Access Control in DNA Center | Part 1/2 |





If you're utilizing APIs to streamline your daily tasks with DNA Center, you may have wondered where to find endpoints for Group-Based Access Control Policies. Unfortunately, there are currently no available endpoints, except possibly some in beta state.

However, I was curious about how the Graphical User Interface (GUI) was able to accomplish this and was confident that it was possible. Using my skills from when I first started exploring SDN with ACI, I used an API Inspector and was able to get to my goal.



Disclaimer!:


I am using undocumented APIs in this post. Do NOT use this in your production network. You may have disruptions. Also make sure you have a backup before you start. I am not responsible for any damage which is resulting of this information.







To get the informations i wanted i opened the GUI and created a Security Group Tags (SGTs).




Nothing too fancy here. But after i´ve done the configuration i opened the API Inspector and was looking for some noteworthy POST/PATCH/PUT calls. And i found what i was looking for!



By clicking on the blue symbol, you can download the body in JSON format. Let's take a closer look at the contents.



[
	{
        "name": "Blog_User1",
        "securityGroupTag": 22,
        "description": "",
        "vnAgnostic": false,
        "propagateToAci": false,
        "scalableGroupType": "USER_DEVICE"
    }
]

Checkpot! I figured out a procedure of how i can go ahead. I will use this technique many time from this point onwards. I will not show this on every subsequent call.


After that i noticed that huge number of GET request to the endpoint "v2/data/customer-facing-service/summary/scalablegroup/access?getNextSgt=true". I wondered what this is doing and it turn out, that it will return the next available scalable group tag value. This means I don't need to manually specify a decimal, as the DNA Center will handle the task for me.


I was wondering too, why no VN was mentioned in this call. So i tried to use this call with Postman.



And this happened. A SGT without any VN association. This is even not possible with the GUI. Interesting.


I couldn´t find the right API call to change that either. But i saw some GET calls. So i tried my luck and changed the results from the GET call, changed the values and used the same endpoint from the GET call but this time using PUT. Turns out this was the a way to archive this task.


PUT https://{{dnacip}}/dna/intent/api/v1/virtual-network

{
    "virtualNetworkName": "BlogVN",
    "isGuestVirtualNetwork": false,
    "scalableGroupNames": [
        "Blog_User1",
        "Blog_User2
    ],
	"vManageVpnId": ""
 
}

Be aware, that this will override you whole bindings for the entire VN! No matter how many SGTs were in this VN, after the call given in the code above, only the two mentioned SGTs will be there. All other SGTs will be gone.


I optimized the json by extracting all unnecessary information from it, leaving only the essential fields. After heavy testing, I discovered a successful approach. However, it is important to note that you must retain the "policyscope" and "priority" fields for it to work correctly. For whatever reason.


I think you got how this was figured out. So i will directly jump into my script! Otherwise this post would get too heavy.


My goal was to use an Excel file to specify all parameters. There are some limitations in this early stage. It is expected that the contracts and virtual networks are already configured and recorded in the file. Every time you add or delete contracts or virtual networks on the DNA Center, you have to synchronize the data in the Excel File manually. The user can then create as many SGTs as he/she wants and specify the corresponding VN to it. Creating a new virtual network is a lot more that only this anyways (BGP AF on border, subinterfaces, rules on the Fusionfirewall,..). I know it is possible to have a SGT in multiple VNs but for the sake of this demonstration, we are limited to a one to one ratio. This is done by a drop-down list, to make sure the related VN is correct. After that, it is time to create as many rules as desired. All options (source SGT, destination SGT and contract respectively) are chosen by a drop-down list as well. Also to make sure no typos are made.




First of all we need to lead the Excel file into the Python script


#load two user configurable worksheets
workbook = openpyxl.load_workbook(filename="set_policy_matrix.xlsx")
sheet_policies = workbook['Policies']
sheet_sgt = workbook['SGT']

The next step is to synchronize the Security Group Tags (SGTs) listed in the Excel file with the existing ones on the DNAC. To achieve this, we retrieve all the SGTs currently on the DNAC and compare them with those in the loaded file. If a SGT exists in the file but not on the DNAC, it will be added to the DNA Center. However, it's worth noting that this process only works in one direction, meaning it will not remove any SGTs that are present on the DNAC but not in the file.



def sync_SGTs(headers, sheet_sgt):
    #get all SGTs and create a list of all SGT names
    URL=credentials.base_url+'/api/v2/data/customer-facing-service/
        scalablegroup/access'
    
    response=requests.get(URL,headers=headers, verify=False).json()
    
    currentSGT=[]
    for sgt in response['response']:
        currentSGT.append(sgt['name'])

    #convert the rows from the sheet into a list with tuples. Start with 
    #row two and max col two to only get only values
    #and filter "None" Fields
    inputSGT = []
    sgtVNTable = {}
    for row in sheet_sgt.iter_rows(min_row=2, max_col=2, 
    values_only=True):
        if not None in row:
          inputSGT.append(row[0])
          sgtVNTable.update({f"{row[0]}":f"{row[1]}"})
    
    #compare the SGTs of the Excel to the current SGTs on the DNAC
    diffSGTs = set(inputSGT).difference(set(currentSGT))
    
    #create the missing SGTs
    for sgt in diffSGTs:
        payload = json.dumps(
            [
              {
               "name":f"{sgt}",
               "securityGroupTag":f"{getNextFreeSGTValue(headers)}",
               "description":"",
               "vnAgnostic": False,
               "propagateToAci":False,
               "scalableGroupType":"USER_DEVICE"
              }
            ]
          )
        
        response = requests.post(URL, headers=headers, data=payload, 
                                 verify=False)
        time.sleep(5)
        setVNsforSGT(headers, sgt, sgtVNTable[sgt])

We use tuples instead of lists, because the response of all SGTs is a tuple as well. So no conversion is needed before comparing. I´m using the set function because of the difference method, which gives me all values that are only in one of the two tuples. We can use the same URL for both, getting all SGTs and post new SGTs.

We still need to tell the DNA Center which SGT belongs to which virtual network. Per iteration we give the DNA Center five seconds to process the request use another method.

Note: the more professional way would be to use the task id in the response and use another API to check repeatedly the status until it finished the task.

At the beginning of the code above, we not only loaded the SGTs from the Excel file but also created a dictionary, which lists all SGTs with their VN: {SGTx : VNy}.


def setVNsforSGT(headers, sgt, VNs):
    #put the SGT in a VN. Up on creation via API die SGT is in none of the 
    #Virutal Networks
    #This is not possible by using the GUI
    #todo: optimize this call. possible to send alls SGTs per VN within 
    #one call
    URL = credentials.base_url+"/dna/intent/api/v1/virtual-network"
    
    VN = requests.get(credentials.base_url+f"/dna/intent/api/v1/
                      virtual-network?virtualNetworkName={VNs}", 
                      headers=headers, verify=False).json()

    #SGT get all SGTs and add the user input. 
    VN["scalableGroupNames"].append(sgt)
    
    #build own header and replace the SGT List 
    payload = {
        "virtualNetworkName": f"{VNs}",
        "isGuestVirtualNetwork": False,
        "scalableGroupNames": [],
        "vManageVpnId": ""
        }
    
    payload["scalableGroupNames"]=VN["scalableGroupNames"]
        
    payload = json.dumps(payload)
    
    response = requests.put(URL, headers=headers, data=payload, 
                            verify=False)
    return 

Now it's time to get the policy done.


def setPolicy(headers, sheet_Policies):

    URL = credentials.base_url+f"/api/v2/data/
          customer-facing-service/policy/access"

    #read the excel file and put it in variables
    for row in sheet_Policies.iter_rows(min_row=2, max_col=3, 
    values_only=True):
        if not None in row:
            srcSGT = row[0]
            dstSGT = row[1]
            contract = row[2]

        #deletion of current Policy in case it is already configure. 
        #with PUT is a lot of code and effort. Using Post on an existing 
        #Policy would throw an error
        deletePolicy(headers, srcSGT, dstSGT)

        #build header

        payload = json.dumps(
          [
             {
                "policyScope": "2827e5bf-d291-3d54-aeda-3e21b29a9d5d",
                        "priority": 65535,
                        "name": f"{srcSGT}-{dstSGT}",
                        "contract": 
                        {
                            "idRef": f"{getContractID(headers, contract)}"
                        },
                        "producer": 
                         {
                            "scalableGroup":
                                            [
                                             {
                                              "idRef": f"getSGTID(headers, 
                                                         srcSGT)"
                                             }
                                            ]      
                         },
                        "consumer": 
                        {
                            "scalableGroup": 
                                           [
                                            {
                                            "idRef": f"{getSGTID(headers, 
                                                        dstSGT)}"
                                            }
                                           ]   
                        },
                        "description": ""
                 }
              ]
            )
            response = requests.post(URL, headers=headers, data=payload, 
                                     verify=False)
            if response.status_code >= 200 and 
               response.status_code <= 300:
               
                 print(f"Contract: {srcSGT}-{dstSGT} successfull")      



"When naming, I use the source and destination Security Group Tag (SGT) separated by a hyphen, making it easier to locate and delete for future contracts if needed


Python file:


Excel:


But if you are willing to rebuild this code, I strongly recommend to use seperate jinja2 files for all the payloads. This will reduce the code in the main file and make it more scalable. I put the payload bodies in the code for better clarity in this post. So you don't have to jump up and down on this post.


To realize this, every payload body will be stored in a *.j2 file. Looking like this:


[
	{
		 "name":"{{sgt}}",
		 "securityGroupTag":"{{sgtValue}}",
		 "description":"",
		 "vnAgnostic": false,
		 "propagateToAci": false,
		 "scalableGroupType":"USER_DEVICE"
	}
]

! python: False jinja2: false !


"In Jinja2, variables are declared using double curly brackets {{ }} around the variable name.

To use Jinja2 in your Python code, you need to import both the jinja2 and json libraries. Additionally, you need to specify the location of all the Jinja2 templates, in this case in the "Templates" folder.


allTemplates = jinja2.Environment(loader=jinja2.FileSystemLoader('Templates/'))
newContractTemp = allTemplates.get_template('new_contract.j2')

For every variable in the j2 file wie need a key-value pair in a dict.


vars = {
    "SGT" : "Blog_User1",
    "securityGroupTag" : 66
}

And finally you fill the variables in the .j2 file with the values from the dictionary.


rendered = newContractTemp.render(vars)
>>print(rendered)

[
	{
		 "name":"Blog_User1",
		 "securityGroupTag":"66",
		 "description":"",
		 "vnAgnostic": false,
		 "propagateToAci": false,
		 "scalableGroupType":"USER_DEVICE"
	}
]



Version using Jinja2 templates:


This was a whole lot of work. Therefore i really hope this can help you in your project.


Stay tuned for the second part!

110 views0 comments

Comments


Beitrag: Blog2 Post
bottom of page