0

I'm stuck with a simple script that's supposed to convert an input list of Cisco object configuration CLI commands into an equivalent list of commands in Palo Alto syntax.

It mostly works, however, it doesn't handle the definition of IPv6 network address entries.

Here is my function:

def cisco_to_paloalto(input_entries):
    input_text = "\n".join(input_entries)
    lines = input_text.strip().split('\n')
    paloalto_commands = []
    current_object_name = ""
    group_mapping = {}

    for line in lines:
        parts = line.split()
        if len(parts) < 2:
            continue 

        if parts[0] == 'object':
            if len(parts) >= 3:
                current_object_name = parts[2]
        elif parts[0] == 'subnet':
            if len(parts) >= 3:
                if ':' in parts[1]:  #IPv6 address
                    ipv6_address, cidr = parts[1].split('/')
                    paloalto_commands.append(f'set address "{current_object_name}" ip-netmask {ipv6_address}/{cidr}')
                else:  # IPv4 address
                    mask = cidr_to_mask(parts[2])  # IPv4 mask conversion
                    paloalto_commands.append(f'set address "{current_object_name}" ip-netmask {parts[1]}/{mask}')
        elif parts[0] == 'host':
            if ':' in parts[1]:  # IPv6 host
                paloalto_commands.append(f'set address "{current_object_name}" ip-netmask {parts[1]}/128')
            else:  # IPv4 host
                paloalto_commands.append(f'set address "{current_object_name}" ip-netmask {parts[1]}/32')
        elif parts[0] == 'fqdn':
            paloalto_commands.append(f'set address "{current_object_name}" fqdn {parts[1]}')
        elif parts[0] == 'object-group':
            group_name = parts[2]
            group_mapping[group_name] = []
        elif parts[0] == 'network-object':
            if group_name not in group_mapping:
                group_mapping[group_name] = []
            group_mapping[group_name].append(parts[2])

    for group_name, objects in group_mapping.items():
        paloalto_commands.append(f'set address-group "{group_name}" static [ ' + " ".join(f'"{obj}"' for obj in objects) + ' ]')

    return paloalto_commands

Here are my helper functions being used:

def convert_subnet_to_mask(subnet):
    if '/' in subnet:
        # Handle CIDR notation directly
        return int(subnet.split('/')[-1])
    else:
        # Handle dot-decimal notation
        return sum([bin(int(x)).count('1') for x in subnet.split('.')])

def is_valid_subnet_mask(mask):
    try:
        # Use a placeholder IP address with the mask to create an IPv4Interface with the ipaddress Python module
        ipaddress.IPv4Interface(f"192.0.2.0/{mask}")
        return True
    except ValueError:
        # If an exception is thrown, the mask is not valid.
        return False

def cidr_to_mask(mask):
    """
    Performs input validation, calculates the binary mask based on the CIDR value, and formats the result as a dotted decimal string (e.g., '/24' to '255.255.255.0'). If the input is a valid dotted-decimal mask it returns it directly.
    """
    if '.' in mask and is_valid_subnet_mask(mask):
        return mask
    
    try:
        mask = int(mask.lstrip('/'))

        if mask < 0 or mask > 32:
            return "Invalid input"  # Ensure all paths return a string

        # Calculate mask by shifting 1's and then converting to dotted decimal format
        cidr = (0xffffffff >> (32 - mask)) << (32 - mask)
        return f"{(cidr >> 24) & 0xff}.{(cidr >> 16) & 0xff}.{(cidr >> 8) & 0xff}.{cidr & 0xff}"
    except ValueError:
        return "255.255.255.255"

And here's how I call the function, take input and print the output:

while True:
       user_input = input()
       if user_input.lower() in ['done', 'end']:
           break
       input_entries.append(user_input)

   palo_commands = cisco_to_paloalto(input_entries)
   
   for command in palo_commands:
       print(command)

I'm rather new to this. Any criticism or suggestions are more than welcome.

The output that I am getting is missing the line for the configuration of the "Net_2820.1ec.900..m48_v6" IPv6 subnet - all other object definitions, including for the host IPv6 object are being done correctly.

This is the example input I give it:

object network Net_1.0.0.0m8
subnet 1.0.0.0 255.0.0.0
object network Net_2820.1ec.900..m48_v6
subnet 2820:1ec:900::/48
object network Host_2620.1ec.c..11_v6
host 2620:1ec:c::11

object-group network GrpTst
network-object object Net_1.0.0.0m8
network-object object Net_2820.1ec.900..m48_v6
network-object object Host_2620.1ec.c..11_v6

And I get the output missing the Ipv6 network command, while the host one is processed just fine.

set address "Net_1.0.0.0m8" ip-netmask 1.0.0.0/8
set address "Net_2820.1ec.900..m48_v6" ip-netmask 2820:1ec:900::/48
set address "Host_2620.1ec.c..11_v6" ip-netmask 2620:1ec:c::11/128

set address-group "GrpTst" static [ "Net_1.0.0.0m8" "Host_2620.1ec.c..11_v6" "FQDN_chatgpt.com" ]
7
  • For those of us who aren't familiar with networking, can you identify the exact output that you expected to see but was missing, and the exact input that you expected to produce that output? Commented Feb 18 at 23:07
  • Python has an ipaddress library that can do all this stuff, so you don't need to roll your own.
    – Barmar
    Commented Feb 18 at 23:25
  • @Barmar, the Python ipaddress library (written by Google) is buggy for IPv6. For example, IPv6 does not have broadcast, but it identifies an IPv6 broadcast address. There are multiple problems with it concerning IPv6, as it was written against obsolete RFCs.
    – Ron Maupin
    Commented Feb 19 at 1:44
  • @RonMaupin Don't let the perfect be the enemy of the good. If it works for basic parsing like this, it should be better than rolling their own.
    – Barmar
    Commented Feb 19 at 2:27
  • @Barmar, it really is not that hard to roll you own, which is what I did. I wrote this IPv6 regular expression that works well enough to be used in my IPvX IP calculator that I have implemented in multiple programming languages. When I used it with Python to prove something to someone, it worked well.
    – Ron Maupin
    Commented Feb 19 at 3:15

1 Answer 1

0

Properly render your Cisco text configuration with indentation

First, let's render the configuration as it's actually shown in the output of Cisco show runn (including command indentation)... proper indentation allows you to simplify your code by outsourcing to a dedicated networking command parser (such as ciscoconfparse2 - full disclosure: I am the author).

object network Net_1.0.0.0m8
 subnet 1.0.0.0 255.0.0.0
object network Net_2820.1ec.900..m48_v6
 subnet 2820:1ec:900::/48
object network Host_2620.1ec.c..11_v6
 host 2620:1ec:c::11

object-group network GrpTst
 network-object object Net_1.0.0.0m8
 network-object object Net_2820.1ec.900..m48_v6
 network-object object Host_2620.1ec.c..11_v6

Next, use ciscoconfparse2 to read the configuration... as mentioned in the comments, there are better solutions to IPv4 and IPv6 parsing than rolling-your-own from scratch...

IP parsing

ciscoconfparse2 includes the IPv4Obj() and IPv6Obj() classes which do all this work for you

Optimized code using ciscoconfparse2

import ipaddress

from ciscoconfparse2 import IPv4Obj, IPv6Obj
from ciscoconfparse2 import CiscoConfParse

class PaloAltoConfig:
    def __init__(self, cisco_config):
        self.palo_alto_lines = list()
        self.parse = CiscoConfParse(cisco_config.splitlines())

        self.convert_cisco_objects()
        self.palo_alto_lines.append('')
        self.convert_cisco_objectgroups()

    def convert_cisco_objects(self):
        """Convert Cisco ASA / Firepower object commands to Palo Alto set format"""

        # Read each Cisco 'object' command...
        for obj_cmd in self.parse.find_objects('^object '):
            obj_name = obj_cmd.split()[-1]                   # Get the obj name
            obj_subnet_host = obj_cmd.children[0]            # Get the subnet / host

            ###################################################
            # Get the obj_cmd IPv4 / IPv6 address
            ###################################################
            addr_mask = " ".join(obj_subnet_host.split()[1:])
            try:
                ip_obj = IPv4Obj(addr_mask)
            except ipaddress.AddressValueError:
                ip_obj = IPv6Obj(addr_mask)

            palo_alto_cmd = f'''set address "{obj_name}" ip-netmask {ip_obj.as_cidr_net}'''

            self.palo_alto_lines.append(palo_alto_cmd)


    def convert_cisco_objectgroups(self):
        """Convert Cisco ASA / Firepower object commands to Palo Alto set format"""
        # Read each Cisco 'object-group' command...
        for objgrp_cmd in self.parse.find_objects('^object-group '):
            objgrp_name = objgrp_cmd.split()[-1]          # Get the object-group name

            # Build a list for the object command names
            all_quoted_netobj_names = list()

            for netobj_cmd in objgrp_cmd.children:
                netobj_name = netobj_cmd.split()[-1]
                all_quoted_netobj_names.append(f'"{netobj_name}"')

            palo_alto_cmd = f'set address-group "{objgrp_name}" [ {" ".join(all_quoted_netobj_names)} ]'
            self.palo_alto_lines.append(palo_alto_cmd)

config = """
object network Net_1.0.0.0m8
 subnet 1.0.0.0 255.0.0.0
object network Net_2820.1ec.900..m48_v6
 subnet 2820:1ec:900::/48
object network Host_2620.1ec.c..11_v6
 host 2620:1ec:c::11

object-group network GrpTst
 network-object object Net_1.0.0.0m8
 network-object object Net_2820.1ec.900..m48_v6
 network-object object Host_2620.1ec.c..11_v6
"""


palo_alto_config = PaloAltoConfig(config)
for set_cmd in palo_alto_config.palo_alto_lines:
    print(set_cmd)

Correct answer output

When I run this, I get the desired IPv6 command in 'GrpTest'... all other results look correct as well.

$ python3 convert_cisco_to_paloalto.py
set address "Net_1.0.0.0m8" ip-netmask 1.0.0.0/8
set address "Net_2820.1ec.900..m48_v6" ip-netmask 2820:1ec:900::/48
set address "Host_2620.1ec.c..11_v6" ip-netmask 2620:1ec:c::11/128

set address-group "GrpTst" [ "Net_1.0.0.0m8" "Net_2820.1ec.900..m48_v6" "Host_2620.1ec.c..11_v6" ]
$

Not the answer you're looking for? Browse other questions tagged or ask your own question.