Reputation: 11
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" ]
Upvotes: 0
Views: 83
Reputation: 43097
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...
ciscoconfparse2 includes the IPv4Obj()
and IPv6Obj()
classes which do all this work for you
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)
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" ]
$
Upvotes: 0