#!/usr/bin/python3
# PYTHON_ARGCOMPLETE_OK
#
# Copyright (C) 2020-2023 NVIDIA Corporation. ALL RIGHTS RESERVED.
# Copyright 2013,2014,2015,2016,2017,2018,2019,2020 Cumulus Networks, Inc.
# Author: Roopa Prabhu, roopa@cumulusnetworks.com
#
# cl-acltool --
#    tool to install cumulus acl policy/rules into kernel and hw
#
try:
	import argcomplete
	import argparse
	import os
	import shutil
	import subprocess
	import sys
	import syslog
	import time
	import glob
	import errno
	import json
	import re
	from cumulus.aclpolicy import *
except ImportError as e:
	raise ImportError (str(e) + "- required module not found")

ctrl_base = "/cumulus/switchd"
policy_dir_default='/etc/cumulus/acl/policy.d'
policy_conf_name='policy.conf'
policy_conf='/etc/cumulus/acl/' + policy_conf_name
policy_files=[]
env_vars={}
scratchdir = '/tmp/.acl' + '.%d' %os.getpid() + '/'
cachedir='/run/cache/cumulus/acltool/'
varrundir='/var/run/acl/'
varrundir_lastgoodpolicyfiles=varrundir + 'lastgoodpolicyfiles'
donefile=cachedir + 'installed'
config = {}
acl_handlers = {}
debug = False
verbose = False
quiet = False
numeric = False
exact = False
json_output = False
version='1.0'

# Constants for iptables 
# packets, bytes, target, protocol, opt, inrule parsing
IPTABLES_MIN_FIELDS = 6
hw_sync=True
hw_wait=0
log_method=""
log_handler=None
pvt_dot1x_mode = False

# Named L4 ports supported for both TCP and UDP parsing
NAMED_L4_PORTS = [
	'bootpc', 'bootps', 'clag', 'dhcp-client', 'dhcp-server', 'domain',
	'ftp', 'http', 'https', 'imap2', 'ldap', 'ldaps', 'ntp', 'msdp',
	'pop3', 'smtp', 'snmp', 'snmp-trap', 'ssh', 'telnet', 'tftp',
	'bgp', 'bfd', 'bfd-echo', 'bfd-multihop'
]

syslog_priority_map = { "crit" : syslog.LOG_CRIT,
			"error" : syslog.LOG_ERR,
			"info" : syslog.LOG_INFO,
			"warn" : syslog.LOG_WARNING,
			"debug" : syslog.LOG_DEBUG }

stdout_priority_map = { "crit" : "error",
			"error" : "error",
			"info" : "",
			"warn" : "warning",
			"debug" : "debug" }

def log_handler_stdout(priority, buf):
	p = stdout_priority_map.get(priority, "")
	if p != "":
		p = p + ': '
	print(p + buf)
	sys.stdout.flush()

def log_handler_stderr(priority, buf):
	p = stdout_priority_map.get(priority, "")
	if p != "":
		p = p + ': '
	sys.stderr.write(p + buf)
	sys.stderr.flush()

def log_handler_syslog(priority, buf):
	syslog.syslog(syslog_priority_map.get(priority, syslog.LOG_INFO), buf)

def log_msg_begin(*args, **kwargs):
	if quiet == False:
		log_handler('', ''.join(args))

def log_msg_end(ret):
	if quiet == False:
		if ret == 0:
			log_handler('', 'done.')
		else:
			log_handler('', 'failed.')

def log(*args, **kwargs):
	if quiet == False:
		log_handler("info", ''.join(args))

def log_debug(*args, **kwargs):
	if debug == True:
		log_handler("debug", ''.join(args))

def log_verbose(*args, **kargs):
	if verbose == True:
		log_handler("", ''.join(args))

def log_error(*args, **kwargs):
	if log_method == "syslog":
		log_handler("error", ''.join(args))
	else:
		log_handler_stderr("error", ''.join(args))

def log_warn(*args, **kwargs):
	log_handler("warn", ''.join(args))

def log_crit(*args, **kwargs):
	log_handler("crit", ''.join(args))

def log_cmderr(cmd, cmdout):
	log_error('cmd \'%s\'' %cmd +
		' failed with the following error:\n' +
		'(' + str(cmdout).rstrip('\n') + ')')

def log_parseerr(filename, line, lineno, errstr=None):
    errmsg = '%s: %d: invalid line \'%s\'' %(filename, lineno, line)
    if errstr:
        errmsg += ' (%s)' %errstr
    log_error(errmsg)

def createdir(dirpath):
	mode = 0o755
	try:
		log_debug('Creating dir ..' + dirpath)
		if not os.path.exists(dirpath):
			os.makedirs(dirpath, mode)
	except OSError as e:
		log_error('mkdir failed : ' + str(e))
		return -1

	return 0

def removedir(dirpath):
	try:
		log_debug('Removing dir ..' + dirpath)
		#os.removedirs(dirpath)
		shutil.rmtree(dirpath)
	except OSError as e:
		log_error('rmdir failed : ' + str(e))
		return -1

	return 0

def remove_file(filepath):
	try:
		os.remove(filepath)
	except OSError as e:
		pass

def cat_file(filepath):
	print('-' * 80)
	cmd = ['cat', '-n', filepath]
	print(' '.join(cmd))
	subprocess.call(cmd)
	print('-' * 80)
	print('\n')

def touch_file(filepath):
	try:
		log_verbose('touch file %s' %filepath)
		f = open(filepath, 'w')
		os.utime(filepath, None)
		f.close()
	except OSError as e:
		log_error('Could not touch file ' + filepath +
			'(' + str(e) + ')')
		return -1

	return 0

def exec_command(cmd, quiet, stdin=None):
	retval = 0
	cmd_returncode = 0
	cmdout = ''

	try:
		log_verbose('Executing ' + cmd)
		ch = subprocess.Popen(cmd.split(),
				stdin=stdin,
				stdout=subprocess.PIPE,
				shell=False, stderr=subprocess.STDOUT)
		cmdout = ch.communicate()[0]
		cmd_returncode = ch.wait()
		if cmd_returncode != 0:
			retval = -1

	except OSError as e:
		cmdout = 'Could not execute ' + cmd + '(' + str(e) + ')'
		retval = -1

	return (retval, cmdout)

def read_config_file(filename):
	global config
	fconf_lines = []

	log_verbose('Reading config file %s' %filename)

	try:
		fconf = open(filename, 'r')
		fconf_lines = fconf.readlines()
	except Exception as e:
		raise RuntimeError(str(e))

	if len(fconf_lines) == 0:
		return 0

	for l in fconf_lines:
		l = l.strip()
		if len(l) == 0:
			continue
		if l[0] == '#':
			continue;

		if re.search('^.*=.*', l, 0) != None:
			l = re.sub(r'\s+', '', l)
			l = l.rstrip('\n')
			key_val = string.split(l, '=', 1)
			if len(key_val[0]) > 0 and len(key_val[1]) > 0:
				config[key_val[0]] = key_val[1]

	if len(config) > 0:
		log_debug('Config file contents :')
		log_debug(str(config))

	return 0

def hw_init():
	global hw_wait

	while True:
		try:
			with open(ctrl_base + "/ctrl/acl/stop_stats_sync", "w") as f:
				return 0
		except IOError:
			if not hw_wait:
				log_error("failed to connect to hw")
				return -1
			time.sleep(1)
			hw_wait -= 1

def hw_sync_start():
	# note: hidden file
	with open(ctrl_base + "/ctrl/acl/stop_stats_sync", "w") as f:
		f.write("1")
	return 0

def hw_sync_cancel():
	# note: hidden file
	with open(ctrl_base + "/ctrl/acl/reset_stats_sync", "w") as f:
		f.write("1")
	return 0

def hw_sync_commit():
	# note: hidden file
	with open(ctrl_base + "/ctrl/acl/resync", "r+") as f:
		f.write("1")
		f.seek(0)
		ret = f.read().strip()
		if ret and len(ret) > 1:
			log_crit('hw sync failed (%s)' %ret)
			return -1
	return 0

def hw_sync_end():
	# nop
	return 0

def iptables_restore_all_tables(basefilename, type, extra_options, noflush):
	""" Invoke iptables restore for all tables.
	filename is the prefix for all filenames """

	err = 0
	for table in iptables.kernel_tables:
		filename = basefilename + '.' + table
		if os.path.exists(filename):
			if type == aclRuleType.iptables:
				cmd, ret, cmdout = iptables.restore(table,
						filename, int(noflush), extra_options, verbose)
			elif type == aclRuleType.ip6tables:
				cmd, ret, cmdout = ip6tables.restore(table,
						filename, int(noflush), extra_options, verbose)
			if ret != 0:
				log_cmderr(cmd, cmdout)
				cat_file(filename)
				err += 1

	if err != 0:
		return -1

	return 0

def ebtables_restore_all_tables(basefilename):
	err = 0
	for table in ebtables.kernel_tables:
		filename = basefilename + '.' + table
		if os.path.exists(filename):
			cmd, ret, cmdout = ebtables.restore(table, filename,
						'', verbose)
			if ret != 0:
				log_cmderr(cmd, cmdout)
				cat_file(filename)
				err += 1

	if err != 0:
		return -1

	return 0

def iptables_save_all_tables(basefilename, type, noflush):
	err = 0
	for table in iptables.kernel_tables:
		ret = 0
		filename = basefilename + '.' + table
		if type == aclRuleType.iptables:
			log_verbose('Calling iptables save for '
					'table %s' %table)
			cmd, ret, cmdout = iptables.save(table,
						filename, '-c',
						verbose)
		elif type == aclRuleType.ip6tables:
			log_verbose('Calling ip6tables save for '
					'table %s' %table)
			cmd, ret, cmdout = ip6tables.save(table,
						filename, '-c',
						verbose)
		if ret != 0:
			log_cmderr(cmd, cmdout)
			err += 1

	if err != 0:
		return -1

	return 0

def ebtables_save_all_tables(basefilename):
	err = 0
	for table in ebtables.kernel_tables:
		filename = basefilename + '.' + table
		log_verbose('Calling ebtables save for '
				'table %s' %table)
		cmd, ret, cmdout = ebtables.save(table, filename, '',
						verbose)
		if ret != 0:
			log_cmderr(cmd, cmdout)
			err += 1

	if err != 0:
		return -1

	return 0

#This function adjusts LOG rule where src and dest IPs are comma separated
#It goes over the rules and splits them as below
#if rule is     --> -A FORWARD -i swp1 -s 10.10.1.1/24,20.1.1.1 -d 30.1.1.1,40.40.1.2/24 -j LOG
#and sibling is --> -A FORWARD -i swp1 -s 10.10.1.1/24,20.1.1.1 -d 30.1.1.1,40.40.1.2/24 -j DROP
#Above will be split into 4 pair of rules as below
# -A FORWARD -i swp1 -s 10.10.1.1/24 -d 30.1.1.1 -j LOG
# -A FORWARD -i swp1 -s 10.10.1.1/24 -d 30.1.1.1 -j DROP
# -A FORWARD -i swp1 -s 10.10.1.1/24 -d 40.40.1.2/24 -j LOG
# -A FORWARD -i swp1 -s 10.10.1.1/24 -d 40.40.1.2/24 -j DROP
# -A FORWARD -i swp1 -s 20.1.1.1 -d 30.1.1.1 -j LOG
# -A FORWARD -i swp1 -s 20.1.1.1 -d 30.1.1.1 -j DROP
# -A FORWARD -i swp1 -s 20.1.1.1/24 -d 40.40.1.2/24 -j LOG
# -A FORWARD -i swp1 -s 20.1.1.1/24 -d 40.40.1.2/24 -j DROP
def iptables_log_rule_adjust(r, rule_strs, rstr_index, table, rule_bufs, rule_cnts, rule_pair_strs):
    #Only required for sibling rules
    if r.has_sibling() != 0:
        src_addr_present = False
        dst_addr_present = False
        num_src_addr = 0
        num_dst_addr = 0
        total_num_add_comb = 0
        split_src_addr = []
        split_dst_addr = []
        src_m_itr = 0
        dst_m_itr = 0
        split_rule_str = rule_strs[rstr_index].split()
        #print ('Split Rule Str --> %s\n'%(split_rule_str))
        cnt_rule_sstr = len(split_rule_str)
        #print ('Count Str : %d\n'%(cnt_rule_sstr))

        for m_itr in range(0,len(split_rule_str)):
            #Look for -s
            if "-s" == split_rule_str[m_itr]:
                src_addr_present = True
                src_m_itr = m_itr
                #print('Src Addr Found at Index : %d | Address : %s\n'%(m_itr,split_rule_str[m_itr+1]))
                src_addr_str = split_rule_str[m_itr+1]
                #If source IP address is found, its a min of 1 Src IP address
                num_src_addr = 1
                #Number of src IP addresses is +1 of the number os commas
                num_src_addr += src_addr_str.count(',')
                #print('Num of Source Addresses : %d\n'%(num_src_addr))
                #split the source IP addresses on comma
                split_src_addr = src_addr_str.split(',')
                #remove an " if present
                for src_itr in range(0,len(split_src_addr)):
                    split_src_addr[src_itr] = split_src_addr[src_itr].replace("\"","")
                #print('Source Address Split str : %s\n'%(split_src_addr))

            #Look for -d
            if "-d" == split_rule_str[m_itr]:
                dst_addr_present = True
                dst_m_itr = m_itr
                #print('Dst Addr Found at Index : %d | Address : %s\n'%(m_itr,split_rule_str[m_itr+1]))
                dst_addr_str= split_rule_str[m_itr+1]
                #If dst IP address is found, its a min of 1 dst IP address
                num_dst_addr= 1
                #Num of dst IP addresses is +1 of the num of commas
                num_dst_addr += dst_addr_str.count(',')
                #print('Num of Destination Addresses : %d\n'%(num_dst_addr))
                #Split the dst IP add string on comma
                split_dst_addr = dst_addr_str.split(',')
                #replace all " from the addresses
                for dst_itr in range(0,len(split_dst_addr)):
                    split_dst_addr[dst_itr] = split_dst_addr[dst_itr].replace("\"","")
                #print('Destination Address Split str : %s\n'%(split_dst_addr))

        #If no Source or Dst IP Addresses are present
        if src_addr_present == False and dst_addr_present == False:
                return 0

        #If both source and dst IP addresses are present
        if num_src_addr !=0 and num_dst_addr != 0:
            #Total number is a cross of all the src and dst IP addresses
            total_num_add_comb = num_src_addr * num_dst_addr
            #Iterate over Src IP addresses
            for src_itr in range (0,num_src_addr):
                #Iterate over Dst IP addresses
                for dst_itr in range (0,num_dst_addr):
                    if rule_strs != None:
                        #Get the original rule string
                        rule_str = rule_strs[rstr_index]
                        tmp_rule_str = rule_str
                        #replace the src IP string(with commas) with the src IP address
                        tmp_rule_str = tmp_rule_str.replace(split_rule_str[src_m_itr+1],split_src_addr[src_itr])
                        #replace the dst IP string(with commas) with the dst IP address
                        tmp_rule_str = tmp_rule_str.replace(split_rule_str[dst_m_itr+1],split_dst_addr[dst_itr])
                        #print('New Rule 3 --> %s\n'%tmp_rule_str)
                        #Add the rules to the rules buf
                        rule_bufs[table].append(tmp_rule_str + '\n')
                        rule_cnts[table] += 1
                        if rule_pair_strs != None:
                            #Get the sibling rule with DROP option
                            rule_pair_str = rule_pair_strs[rstr_index]
                            tmp_rule_pair_str = rule_pair_str
                            #replace the src IP string(with commas) with the src IP address
                            tmp_rule_pair_str = tmp_rule_pair_str.replace(split_rule_str[src_m_itr+1],split_src_addr[src_itr])
                            #replace the dst IP string(with commas) with the dst IP address
                            tmp_rule_pair_str = tmp_rule_pair_str.replace(split_rule_str[dst_m_itr+1],split_dst_addr[dst_itr])
                            #print('New Rule 4 --> %s\n'%tmp_rule_pair_str)
                            #Add the ruls to the rules buf
                            rule_bufs[table].append(tmp_rule_pair_str + '\n')
                            rule_cnts[table] += 1
                        else:
                            return 0
                    else:
                        return 0


        else:
            num_addr = 0
            #either src or dst add is not present
            if num_src_addr != 0:
                num_addr = num_src_addr
                str_m_itr = src_m_itr
                split_addr = split_src_addr
            elif num_dst_addr != 0:
                num_addr = num_dst_addr
                str_m_itr = dst_m_itr
                split_addr = split_dst_addr

            #Iterate over the addresses
            for addr_itr in range (0,num_addr):
                if rule_strs != None:
                    #Get the original LOG rule
                    rule_str = rule_strs[rstr_index]
                    tmp_rule_str = rule_str
                    #replace the IP address str(which is with commas) with the IP address
                    tmp_rule_str = tmp_rule_str.replace(split_rule_str[str_m_itr+1],split_addr[addr_itr])
                    #print('New Rule 3 --> %s\n'%tmp_rule_str)
                    #Add the rule to the buffer
                    rule_bufs[table].append(tmp_rule_str + '\n')
                    rule_cnts[table] += 1
                    if rule_pair_strs != None:
                        #Get the sibling rule with DROP option
                        rule_pair_str = rule_pair_strs[rstr_index]
                        tmp_rule_pair_str = rule_pair_str
                        #replace the IP address str(Which is with commas) with the IP address
                        tmp_rule_pair_str = tmp_rule_pair_str.replace(split_rule_str[str_m_itr+1],split_addr[addr_itr])
                        #print('New Rule 4 --> %s\n'%tmp_rule_pair_str)
                        #Add the rule to the buffer
                        rule_bufs[table].append(tmp_rule_pair_str + '\n')
                        rule_cnts[table] += 1
                    else:
                        return 0
                else:
                    return 0


        total_num_add_comb = num_src_addr + num_dst_addr

        #print ('Total number of Address combos : %d\n'%(total_num_add_comb))
        return 1
    else:
        #print ('NO SIBLING\n')
        return 0


def iptables_rollback_common(fw_policies, type, orig_rules_filename):
	return iptables_restore_all_tables(orig_rules_filename, type, '-c', 0)

def iptables_rollback(fw_policies, orig_rules_filename):
	return iptables_rollback_common(fw_policies,
			aclRuleType.iptables, orig_rules_filename)

def ip6tables_rollback(fw_policies, orig_rules_filename):
	return iptables_rollback_common(fw_policies,
			aclRuleType.ip6tables, orig_rules_filename)


def iptables_install_common(fw_policies, type, new_filename):
	return iptables_restore_all_tables(new_filename, type, '-u', pvt_dot1x_mode)

def iptables_install(fw_policies, orig_restore_file, new_restore_file):
	return iptables_install_common(fw_policies,
			aclRuleType.iptables, new_restore_file)

def ip6tables_install(fw_policies, orig_restore_file, new_restore_file):
	return iptables_install_common(fw_policies,
			aclRuleType.ip6tables, new_restore_file)

def iptables_prepare_install_common(fw_policies, type,
		orig_rules_filename, new_rules_filename):
	rule_cnts = {}
	rule_newfilenames = {}
	rule_oldfilenames = {}
	rule_bufs = {}
	ret = 0

	# Initialize
	for table in iptables.kernel_tables:
		rule_cnts[table] = 0
		rule_newfilenames[table] = new_rules_filename + '.' + table
		rule_oldfilenames[table] = orig_rules_filename + '.' + table
		rule_bufs[table] = []

	# Create restore files for each table
	for fw_policy in fw_policies:
		rules = fw_policy.get_rules()
		if rules == None:
			continue

		rindex = 0
		while rindex < len(rules):
			r = rules[rindex]
			table = r.get_table()
			if table == None:
				table = 'filter'

			r_pair = None
			if r.get_type() == type:
				if r.has_sibling() != 0:
					r_pair = rules[rindex + 1]

				if rule_cnts[table] == 0:
					rule_bufs[table].append('\n#policy: %s\n' %fw_policy.get_name())

				rule_bufs[table].append('#\n#' +
						'line: %d' %r.get_lineno() +
						', rule: \'%s\'\n'
						%r.get_rule_str())

				if r_pair !=None:
					rule_bufs[table].append('#' +
					'(rule_pair) line: %d'
						%r_pair.get_lineno() +
						', rule: \'%s\'\n'
						%r_pair.get_rule_str())

				rule_strs = r.get_processed_rule_str_list()
				if r_pair != None:
					rule_pair_strs = r_pair.get_processed_rule_str_list()
				else:
					rule_pair_strs = None

				for rstr_index in range(0, len(rule_strs)):
					ret = iptables_log_rule_adjust(r, rule_strs, rstr_index, table, rule_bufs, rule_cnts, rule_pair_strs)
					if (ret != 1):
					    rule_str = rule_strs[rstr_index]
					    rule_bufs[table].append(rule_str + '\n')
					    rule_cnts[table] += 1

					    if rule_pair_strs != None:
						    rule_pair_str = rule_pair_strs[rstr_index]
						    rule_bufs[table].append(rule_pair_str + '\n')
						    rule_cnts[table] += 1

				if r_pair != None:
					rindex += 1

			rindex += 1


	for table in iptables.kernel_tables:
		if rule_cnts[table] > 0:
			if type == aclRuleType.iptables:
				cmd, ret, cmdout = iptables.save(table,
						rule_oldfilenames[table], '-c',
						verbose)
			else:
				cmd, ret, cmdout = ip6tables.save(table,
						rule_oldfilenames[table], '-c',
						verbose)
			if ret != 0:
				break

			try:
				f = open(rule_newfilenames[table], 'w')
			except IOError as e:
				log_error('Could not open file %s '
					%rule_newfilenames[table] +
					'(%s)' + str(e))
				ret = -1
				break

			f.write('*%s\n' %table)
			f.writelines(rule_bufs[table])
			f.write('COMMIT\n')
			f.close()
			log_debug('Created ' + rule_newfilenames[table])

			if debug == True:
				cat_file(rule_newfilenames[table])
		else:
			# No rules, we assume user wanted to not install
			# any rules here, flush rule
			if pvt_dot1x_mode == True:
			    log_verbose('not flushing iptables rules')
			    continue
			if type == aclRuleType.iptables:
				(cmd, fret, cmd_out) = iptables.flush(table, '',
								verbose)
			else:
				(cmd, fret, cmd_out) = ip6tables.flush(table,
								'', verbose)

	if ret != 0:
		# Delete all files
		for table in iptables.kernel_tables:
			remove_file(rule_oldfilenames[table])
			remove_file(rule_newfilenames[table])
		return -1

	return 0

def iptables_prepare_install(fw_policies, orig_rules_file,
		new_rules_file):
	return iptables_prepare_install_common(fw_policies,
		aclRuleType.iptables, orig_rules_file,
		new_rules_file)


def ip6tables_prepare_install(fw_policies, orig_rules_file,
		new_rules_file):
	return iptables_prepare_install_common(fw_policies,
		aclRuleType.ip6tables, orig_rules_file,
		new_rules_file)

def ebtables_rollback(fw_policies, orig_rules_filename):
	return ebtables_restore_all_tables(orig_rules_filename)

def delete_dot1x_ebtables_rules(cmd_prefix):
	err = 0
	dir_len = len(policy_dir)
	for rule_file in policy_files:
		file = policy_dir_default + rule_file[dir_len:]
		if os.path.exists(file):
			log_verbose (file +" exists")
		else:
			log_verbose (file +" doesn't exist")
			continue

		f = open(file,"r")
		lines =	f.readlines()
		for l in lines:
			sep = ' '
			w = l.split(sep)
			try:
				pos = w.index('-A')
			except ValueError:
				print("List does not contain -A")
				continue
			w[pos] = '-D'
			ch = w[pos+1]
			chain_list = ch.split(',')
			for chain in chain_list:
				w[pos+1] = chain
				del_rule = sep.join(w)
				cmd = cmd_prefix + del_rule
				(cmdret, cmdout) = exec_command(cmd,verbose)
				if cmdret != 0:
					log_cmderr(cmd, cmdout)
					err += 1
					break
	return err

def ebtables_install(fw_policies, orig_rules_filename,
		new_rules_filename):
	rule_cnts = {}
	rule_newfilenames = {}
	rule_oldfilenames = {}
	rule_bufs = {}
	ret = 0
	err = 0


	# Initialize
	for table in ebtables.kernel_tables:
		rule_cnts[table] = 0
		rule_newfilenames[table] = new_rules_filename + '.' + table
		rule_oldfilenames[table] = orig_rules_filename + '.' + table
		rule_bufs[table] = []

	# Create restore files for each table, store contents in rule_bufs
	for fw_policy in fw_policies:
		rules = fw_policy.get_rules()
		if rules == None:
			continue

		for r in rules:
			table = r.get_table()
			if table == None:
				table = 'filter'

			if r.get_type() == aclRuleType.ebtables:
				rule_strs = r.get_processed_rule_str_list()
				for rule_str in rule_strs:
					rule_bufs[table].append(rule_str.strip() + '\n')
					rule_cnts[table] += 1

	err = 0
	for table in ebtables.kernel_tables:
		if rule_cnts[table] > 0:
			log_verbose('Installing ebtable rules for table %s'
					%table)

			try:
				f = open(rule_newfilenames[table], 'w')
			except IOError as e:
				log_error('Could not open file %s '
					%rule_newfilenames[table] +
					'(%s)' + str(e))
				err += 1
				break
			f.write('*%s\n' %table)
			f.writelines(rule_bufs[table])
			f.close()

			log_verbose('Created ' + rule_newfilenames[table])
			if debug == 1:
				cat_file(rule_newfilenames[table])

			try:
				f = open(rule_newfilenames[table], 'r')
			except IOError as e:
				log_error('Could not open file %s '
					%rule_newfilenames[table] +
					'(%s)' + str(e))
				err += 1
				break

			cmd = '/sbin/ebtables-restore'
			(cmdret, cmdout) = exec_command(cmd, verbose, f)
			f.close()

			if cmdret != 0:
				log_cmderr(cmd, cmdout)
				err += 1
				break
		else:
			if pvt_dot1x_mode == True:
			    log_verbose('Not flushing ebtables rules')
			    continue
			# If no rules for the table, just delete the saved file
			remove_file(rule_oldfilenames[table])

			# Also flush rules:
			# XXX: This is because today, we want to support
			# 'no rules in file' = 'no rules in kernel'
			(cmd, fret, cmd_out) = ebtables.flush(table, '',
							verbose)


	if err > 0:
		for t in ebtables.kernel_tables:
			# Lets restore tables we had flushed
			if os.path.exists(rule_oldfilenames[table]):
				cmd, cmdret, cmdout = ebtables.restore(table,
						rule_oldfilenames[table], '',
						verbose)
				if ret != 0:
					log_cmderr(cmd, cmdout)

			remove_file(rule_oldfilenames[table])
			remove_file(rule_newfilenames[table])
			if t == table:
				break
		return -1

	return 0

def ebtables_prepare_install(fw_policies, orig_rules_filename,
		new_rules_filename):
	return ebtables_save_all_tables(orig_rules_filename)

def iptables_flush_prepare(basefilename):
	return iptables_save_all_tables(basefilename, aclRuleType.iptables, 0)

def ip6tables_flush_prepare(basefilename):
	return iptables_save_all_tables(basefilename, aclRuleType.ip6tables, 0)

def ebtables_flush_prepare(basefilename):
	return ebtables_save_all_tables(basefilename)

def iptables_flush():
	err = 0
	for table in iptables.kernel_tables:
		log_verbose('Calling iptables flush for table ' + table)
		(cmd, ret, cmd_out) = iptables.flush(table, '', verbose)
		if ret != 0:
			log_cmderr(cmd, cmd_out)
			err += 1

	if err > 0:
		return -1

	return 0

def ip6tables_flush():
	err = 0
	for table in ip6tables.kernel_tables:
		log_verbose('Calling ip6tables flush for table ' + table)
		(cmd, ret, cmd_out) = ip6tables.flush(table, '', verbose)
		if ret != 0:
			log_cmderr(cmd, cmd_out)
			err += 1

	if err > 0:
		return -1

	return 0

def ebtables_flush():
	err = 0
	for table in ebtables.kernel_tables:
		log_verbose('Calling ebtables flush for table ' + table)
		(cmd, ret, cmd_out) = ebtables.flush(table, '', verbose)
		if ret != 0:
			log_cmderr(cmd, cmd_out)
			err += 1

	if err > 0:
		return -1

	return 0

def iptables_set_counters(counter_val):
	err = 0
	for table in iptables.kernel_tables:
		if counter_val == 0:
			log_verbose('Calling iptables zero counters for table '
					+ table)
			(cmd, ret, cmd_out) = iptables.zero_counters(table,
							'', verbose)
			if ret != 0:
				log_cmderr(cmd, cmd_out)
				err += 1

	if err > 0:
		return -1

	return 0


def ip6tables_set_counters(counter_val):
	err = 0
	for table in ip6tables.kernel_tables:
		if counter_val == 0:
			log_verbose('Calling ip6tables zero counters for table '
					+ table)
			(cmd, ret, cmd_out) = ip6tables.zero_counters(table,
							'', verbose)
			if ret != 0:
				log_cmderr(cmd, cmd_out)
				err += 1
	if err > 0:
		return -1

	return 0

def ebtables_set_counters(counter_val):
	err = 0
	for table in ebtables.kernel_tables:
		if counter_val == 0:
			log_verbose('Calling ebtables zero counters for table '
					+ table)
			(cmd, ret, cmd_out) = ebtables.zero_counters(table,
							'', verbose)
			if ret != 0:
				log_cmderr(cmd, cmd_out)
				err += 1
	if err > 0:
		return -1

	return 0

def parse_iptables_rule_comment(comment):
	"""Parse rule comment to extract rule_id, acl_name, dir, interface_id"""
	rule_info = {}
	if comment and comment.startswith('/*') and comment.endswith('*/'):
		comment_content = comment[2:-2].strip()
		parts = comment_content.split(',')
		for part in parts:
			if ':' in part:
				key, value = part.split(':', 1)
				rule_info[key.strip()] = value.strip()
	return rule_info

def _parse_l4_ports(extra_details, named_ports=None):
	"""Parse L4 ports (source/destination), supporting single, ranges, and multiport.

	Returns a dict with optional 'source-port' and 'dest-port' lists. Elements are
	ints for numeric ports, strings for named ports, and "start-end" strings for
	ranges.
	"""
	ports = {}

	# spt:/dpt: single numeric
	sport_match = re.search(r'spt:(\d+)', extra_details)
	if sport_match:
		port_value = sport_match.group(1)
		ports['source-port'] = [int(port_value) if port_value.isdigit() else port_value]

	dport_match = re.search(r'dpt:(\d+)', extra_details)
	if dport_match:
		port_value = dport_match.group(1)
		ports['dest-port'] = [int(port_value) if port_value.isdigit() else port_value]

	# spts:/dpts: ranges or single
	spts_range_match = re.search(r'spts:(\d+):(\d+)', extra_details)
	if spts_range_match:
		start_port = spts_range_match.group(1)
		end_port = spts_range_match.group(2)
		ports['source-port'] = [f"{start_port}-{end_port}"]
	else:
		spts_single_match = re.search(r'spts:(\d+)', extra_details)
		if spts_single_match:
			port_value = spts_single_match.group(1)
			ports['source-port'] = [int(port_value) if port_value.isdigit() else port_value]

	dpts_range_match = re.search(r'dpts:(\d+):(\d+)', extra_details)
	if dpts_range_match:
		start_port = dpts_range_match.group(1)
		end_port = dpts_range_match.group(2)
		ports['dest-port'] = [f"{start_port}-{end_port}"]
	else:
		dpts_single_match = re.search(r'dpts:(\d+)', extra_details)
		if dpts_single_match:
			port_value = dpts_single_match.group(1)
			ports['dest-port'] = [int(port_value) if port_value.isdigit() else port_value]

	# Named ports (override numeric if present)
	if named_ports:
		for port_name in named_ports:
			if f'dpt:{port_name}' in extra_details:
				ports['dest-port'] = [port_name]
			if f'spt:{port_name}' in extra_details:
				ports['source-port'] = [port_name]

	# multiport dports/sports (override prior if present)
	multiport_dports_match = re.search(r'multiport dports ([0-9,:a-zA-Z-]+)', extra_details)
	if multiport_dports_match:
		port_list = multiport_dports_match.group(1).split(',')
		port_array = []
		for port_item in port_list:
			if ':' in port_item:
				start, end = port_item.split(':')
				port_array.append(f"{start}-{end}")
			else:
				if port_item.isdigit():
					port_array.append(int(port_item))
				else:
					port_array.append(port_item)
		seen = set()
		ports['dest-port'] = [x for x in port_array if not (x in seen or seen.add(x))]

	multiport_sports_match = re.search(r'multiport sports ([0-9,:a-zA-Z-]+)', extra_details)
	if multiport_sports_match:
		port_list = multiport_sports_match.group(1).split(',')
		port_array = []
		for port_item in port_list:
			if ':' in port_item:
				start, end = port_item.split(':')
				port_array.append(f"{start}-{end}")
			else:
				if port_item.isdigit():
					port_array.append(int(port_item))
				else:
					port_array.append(port_item)
		seen = set()
		ports['source-port'] = [x for x in port_array if not (x in seen or seen.add(x))]

	return ports

def parse_iptables_rule_line(line):
	"""Parse a single iptables rule line"""
	parts = line.strip().split()
	if len(parts) < IPTABLES_MIN_FIELDS:
		return None
	
	# Handle case where target field is empty (spaces) - detect by checking
	# if third field is a protocol
	protocols = ['tcp', 'udp', 'icmp', 'all', 'ospf', 'pim', 'vrrp', 'igmp',
			 'gre', 'ipv6-icmp']
	
	if parts[2] in protocols:
		# Target field is missing, shift everything
		rule = {
			'packets': parts[0],
			'bytes': parts[1],
			'target': '',  # Empty target
			'protocol': parts[2],
			'opt': parts[3],
			'in': parts[4],
			'out': parts[5] if len(parts) > 5 else '',
			'source': parts[6] if len(parts) > 6 else '',
			'destination': parts[7] if len(parts) > 7 else '',
			'extra': ' '.join(parts[8:]) if len(parts) > 8 else ''
		}
	else:
		# Normal case with target field
		rule = {
			'packets': parts[0],
			'bytes': parts[1],
			'target': parts[2],
			'protocol': parts[3],
			'opt': parts[4],
			'in': parts[5],
			'out': parts[6] if len(parts) > 6 else '',
			'source': parts[7] if len(parts) > 7 else '',
			'destination': parts[8] if len(parts) > 8 else '',
			'extra': ' '.join(parts[9:]) if len(parts) > 9 else ''
		}
	
	# Extract comment if present
	comment_match = re.search(r'/\*.*?\*/', rule['extra'])
	if comment_match:
		rule['comment'] = comment_match.group(0)
		rule['rule_info'] = parse_iptables_rule_comment(rule['comment'])
		# Remove comment from extra
		rule['extra'] = re.sub(r'/\*.*?\*/', '', rule['extra']).strip()
	
	return rule

def convert_size_to_bytes(size_str):
	"""Convert size string with suffix (K, M, G) to integer bytes"""
	if not size_str or size_str == '0':
		return 0
	
	size_str = str(size_str).strip()
	if size_str.isdigit():
		return int(size_str)
	
	# Handle suffixes
	multipliers = {
		'K': 1024,
		'M': 1024 * 1024,
		'G': 1024 * 1024 * 1024,
		'T': 1024 * 1024 * 1024 * 1024
	}
	
	if size_str[-1].upper() in multipliers:
		try:
			number = float(size_str[:-1])
			return int(number * multipliers[size_str[-1].upper()])
		except ValueError:
			return 0
	
	try:
		return int(size_str)
	except ValueError:
		return 0

def parse_action_nvue_format(target, extra_details):
	"""Convert iptables target to NVUE action format"""
	action = {}
	
	if target == 'ACCEPT':
		action['permit'] = {}
	elif target == 'DROP':
		action['deny'] = {}
	elif target == 'DNAT':
		# Parse DNAT details: "to:20.10.20.10"
		if 'to:' in extra_details:
			translate_ip = extra_details.split('to:')[1].strip()
			action['dest-nat'] = {
				'translate-ip': {translate_ip: {}},
				'translate-port': {}
			}
		else:
			action['dest-nat'] = {}
	elif target == 'SNAT':
		# Parse SNAT details
		if 'to:' in extra_details:
			translate_ip = extra_details.split('to:')[1].strip()
			action['source-nat'] = {
				'translate-ip': {translate_ip: {}},
				'translate-port': {}
			}
		else:
			action['source-nat'] = {}
	elif target == 'LOG':
		# Parse LOG details
		log_action = {}
		
		# Parse log level
		if 'LOG level' in extra_details:
			level_match = re.search(r'LOG level (\w+)', extra_details)
			if level_match:
				level_str = level_match.group(1)
				# Convert level names to numbers
				level_map = {'emerg': 0, 'alert': 1, 'crit': 2,
				'error': 3, 'warn': 4, 'notice': 5,
				'info': 6, 'debug': 7}
				log_action['level'] = level_map.get(level_str, 5)
		else:
			log_action['level'] = 5  # Default level
		
		# Parse log prefix
		if 'prefix' in extra_details:
			prefix_match = re.search(r'prefix "([^"]*)"', extra_details)
			if prefix_match:
				log_action['log-prefix'] = prefix_match.group(1)
		
		# Parse rate limiting
		if 'limit:' in extra_details:
			rate_match = re.search(r'limit: avg (\d+)/(\w+)', extra_details)
			if rate_match:
				log_action['rate'] = int(rate_match.group(1))
		
		# Handle cases where LOG is used without explicit parameters
		if not log_action:
			log_action = {}
		
		action['log'] = log_action
	elif target in ['SETQOS', 'SETCLASS'] or (target not in ['POLICE'] and
			 ('dscp:' in extra_details or 'cos:' in extra_details or
			  'class:' in extra_details)):
		# Parse DSCP setting
		if 'dscp:' in extra_details:
			dscp_match = re.search(r'dscp:(\d+)', extra_details)
			if dscp_match:
				dscp_value = int(dscp_match.group(1))
				# Convert DSCP values to standard names where applicable
				dscp_names = {8: 'cs1', 1: 1, 34: 34}
				action['set'] = {'dscp': dscp_names.get(dscp_value, dscp_value)}
		elif 'cos:' in extra_details:
			# Parse CoS (Class of Service) setting
			cos_match = re.search(r'cos:(\d+)', extra_details)
			if cos_match:
				action['set'] = {'cos': int(cos_match.group(1))}
		elif 'class:' in extra_details:
			# Parse traffic class setting
			class_match = re.search(r'class:(\d+)', extra_details)
			if class_match:
				action['set'] = {'class': int(class_match.group(1))}
		elif 'DSCP match' in extra_details:
			# For rules that match DSCP and set DSCP
			dscp_match = re.search(r'DSCP match 0x([0-9a-fA-F]+)', extra_details)
			if dscp_match:
				dscp_value = int(dscp_match.group(1), 16)
				action['set'] = {'dscp': dscp_value}
		else:
			action['set'] = {}
	elif target == 'POLICE':
		# Parse POLICE action parameters
		police_action = {}
		
		# Parse mode
		if 'mode:pkt' in extra_details:
			police_action['mode'] = 'packet'
		elif 'mode:byte' in extra_details:
			police_action['mode'] = 'byte'
		
		# Parse rate
		rate_match = re.search(r'rate:(\d+)', extra_details)
		if rate_match:
			police_action['rate'] = int(rate_match.group(1))
		
		# Parse burst
		burst_match = re.search(r'burst:(\d+)', extra_details)
		if burst_match:
			police_action['burst'] = int(burst_match.group(1))
		
		# Parse class
		class_match = re.search(r'class:(\d+)', extra_details)
		if class_match:
			police_action['class'] = int(class_match.group(1))
		
		action['police'] = police_action
	elif target == 'ERSPAN':
		# Parse ERSPAN action parameters
		erspan_action = {}
		
		# Parse source IP
		src_ip_match = re.search(r'src-ip:([0-9\.]+)', extra_details)
		if src_ip_match:
			erspan_action['source-ip'] = src_ip_match.group(1)
		
		# Parse destination IP
		dst_ip_match = re.search(r'dst-ip:([0-9\.]+)', extra_details)
		if dst_ip_match:
			erspan_action['dest-ip'] = dst_ip_match.group(1)
		
		# Parse TTL
		ttl_match = re.search(r'ttl:(\d+)', extra_details)
		if ttl_match:
			erspan_action['ttl'] = int(ttl_match.group(1))
		
		action['erspan'] = erspan_action
	elif target == 'SPAN':
		# Parse SPAN destination
		span_match = re.search(r'dport:(\w+)', extra_details)
		if span_match:
			action['span'] = span_match.group(1)
		else:
			action['span'] = 'cpu'
	elif 'recent: SET' in extra_details:
		# Handle recent SET action (which might not have explicit target)
		action['recent'] = {}
	elif target == '':
		# Handle empty target - check for recent action in extra_details
		if 'recent:' in extra_details:
			action['recent'] = {}
		else:
			action['permit'] = {}
	else:
		# Default fallback
		action['permit'] = {}
	
	return action

def parse_match_nvue_format(rule, extra_details):
	"""Convert iptables rule to NVUE match format"""
	match = {}
	
	# Initialize IP section if needed
	if (rule['protocol'] != 'all' or rule['source'] != 'anywhere' or
			rule['destination'] != 'anywhere'):
		match['ip'] = {}
	
	# Parse source IP (including ANY for certain NVUE rules)
	if rule['source'] != 'anywhere':
		match['ip']['source-ip'] = rule['source']
	elif 'ip' in match and rule['source'] == 'anywhere':
		# For some NVUE rules, show "ANY" instead of omitting
		if rule.get('target') in ['ACCEPT', 'DROP'] and rule['protocol'] != 'all':
			match['ip']['source-ip'] = 'ANY'
	
	# Parse destination IP (including ANY for certain NVUE rules)
	if rule['destination'] != 'anywhere':
		match['ip']['dest-ip'] = rule['destination']
	elif 'ip' in match and rule['destination'] == 'anywhere':
		# For some NVUE rules, show "ANY" instead of omitting
		if rule.get('target') in ['ACCEPT', 'DROP'] and rule['protocol'] != 'all':
			match['ip']['dest-ip'] = 'ANY'
	
	# Parse fragment flag
	if '-f' in extra_details or rule.get('opt') == '-f':
		if 'ip' not in match:
			match['ip'] = {}
		match['ip']['fragment'] = {}
	
	# Parse protocol
	if rule['protocol'] != 'all':
		# Convert protocol names to numbers where needed
		protocol_map = {
			'gre': 47,
			'ospf': 'ospf',
			'pim': 'pim', 
			'vrrp': 'vrrp',
			'igmp': 'igmp',
			'icmp': 'icmp',
			'tcp': 'tcp',
			'udp': 'udp',
			'ipv6-icmp': 'icmpv6'
		}
		match['ip']['protocol'] = protocol_map.get(rule['protocol'], rule['protocol'])
		
		# Parse protocol-specific details
		if rule['protocol'] == 'tcp':
			tcp_info = {}
			# Use shared helper for L4 ports
			parsed_ports = _parse_l4_ports(extra_details, named_ports=NAMED_L4_PORTS)
			tcp_info.update(parsed_ports)
			
			# Parse tcpmss
			if 'tcpmss' in extra_details:
				# Handle "tcpmss match !min:max" (all-mss-except)
				tcpmss_not_match = re.search(r'tcpmss match !(\d+):(\d+)', extra_details)
				if tcpmss_not_match:
					min_mss = tcpmss_not_match.group(1)
					max_mss = tcpmss_not_match.group(2)
					tcp_info['all-mss-except'] = f"{min_mss}-{max_mss}"
				else:
					# Handle "tcpmss match min:max" (regular mss range)
					tcpmss_match = re.search(r'tcpmss match (\d+):(\d+)', extra_details)
					if tcpmss_match:
						min_mss = tcpmss_match.group(1)
						max_mss = tcpmss_match.group(2)
						tcp_info['mss'] = f"{min_mss}-{max_mss}"
			
			# Multiport L4 ports are parsed by _parse_l4_ports()
			
			# Parse connection state
			if 'ctstate' in extra_details:
				state_match = re.search(r'ctstate ([A-Z,]+)', extra_details)
				if state_match:
					states = state_match.group(1).split(',')
					connection_state = {}
					for state in states:
						connection_state[state.lower()] = {}
					match['ip']['connection-state'] = connection_state
			
			# Parse TCP flags (format: flags:mask/value)
			if 'flags:' in extra_details:
				# Support hex format like flags:0x3F/0x10 alongside named flags
				hex_flags_match = re.search(r'flags:0x([0-9a-fA-F]+)/0x([0-9a-fA-F]+)', extra_details)
				if hex_flags_match:
					mask_hex, value_hex = hex_flags_match.groups()
					mask_val = int(mask_hex, 16)
					value_val = int(value_hex, 16)
					# Map bits to standard tcp flags
					flag_bits = [
						('FIN', 0x01),
						('SYN', 0x02),
						('RST', 0x04),
						('PSH', 0x08),
						('ACK', 0x10),
						('URG', 0x20),
					]
					mask_flags_list = [name.lower() for name, bit in flag_bits if (mask_val & bit) != 0]
					value_flags_list = [name.lower() for name, bit in flag_bits if (value_val & bit) != 0]
					# 0x3F covers all 6 classic TCP flags (fin..urg)
					if (mask_val & 0x3F) == 0x3F and (mask_val & ~0x3F) == 0:
						tcp_info['mask'] = ['all']
					elif mask_flags_list:
						tcp_info['mask'] = mask_flags_list
					# Values: all, none or explicit
					if (value_val & 0x3F) == 0x3F and (value_val & ~0x3F) == 0:
						tcp_info['flags'] = ['all']
					elif value_val == 0:
						tcp_info['flags'] = ['none']
					elif value_flags_list:
						tcp_info['flags'] = value_flags_list
				else:
					# Fallback to named flags format e.g. FIN,SYN,.../ACK
					flags_match = re.search(r'flags:([A-Z,]+)/([A-Z,]+)', extra_details)
					if flags_match:
						# First is mask, second is value
						mask_str, flags_str = flags_match.groups()
						# Check if all flags are present
						all_flags = {'FIN', 'SYN', 'RST', 'PSH', 'ACK', 'URG'}
						mask_flags = set(mask_str.split(','))
						value_flags = set(flags_str.split(','))
						# Use "all" if all 6 flags are present, otherwise list
						# individual flags (convert to list format)
						if mask_flags == all_flags:
							tcp_info['mask'] = ["all"]
						else:
							tcp_info['mask'] = [mask.lower() for mask in mask_str.split(',')]
						if value_flags == all_flags:
							tcp_info['flags'] = ["all"]
						elif len(value_flags) == 1 and 'NONE' in value_flags:
							tcp_info['flags'] = ["none"]
						else:
							tcp_info['flags'] = [flag.lower() for flag in flags_str.split(',')]
				# TCP state logic removed per user request
			
			if tcp_info:
				match['ip']['tcp'] = tcp_info
				
		elif rule['protocol'] == 'udp':
			udp_info = {}
			# Use shared helper for L4 ports
			parsed_ports = _parse_l4_ports(extra_details, named_ports=NAMED_L4_PORTS)
			udp_info.update(parsed_ports)
			
			# Parse connection state
			if 'ctstate' in extra_details:
				state_match = re.search(r'ctstate ([A-Z,]+)', extra_details)
				if state_match:
					states = state_match.group(1).split(',')
					connection_state = {}
					for state in states:
						connection_state[state.lower()] = {}
					match['ip']['connection-state'] = connection_state
			
			if udp_info:
				match['ip']['udp'] = udp_info
		
		elif rule['protocol'] == 'icmp':
			match['ip']['protocol'] = 'icmp'
			# Parse ICMP types
			icmpv4_name_to_value = {
				'echo-reply': 0,
				'destination-unreachable': 3,
				'source-quench': 4,
				'redirect': 5,
				'echo-request': 8,
				'router-advertisement': 9,
				'router-solicitation': 10,
				'time-exceeded': 11,
				'parameter-problem': 12,
				'timestamp-request': 13,
				'timestamp-reply': 14,
				'address-mask-request': 17,
				'address-mask-reply': 18,
			}
			icmpv4_value_to_name = {v: k for k, v in icmpv4_name_to_value.items()}
			# Destination Unreachable codes mapping (ICMP type 3)
			dest_unreach_code_map = {
				0: 'network-unreachable',
				1: 'host-unreachable',
				2: 'protocol-unreachable',
				3: 'port-unreachable',
				4: 'fragmentation-needed',
				5: 'source-route-failed',
				6: 'network-unknown',
				7: 'host-unknown',
				8: 'source-host-isolated',
				9: 'network-prohibited',
				10: 'host-prohibited',
				11: 'network-unreachable-for-tos',
				12: 'host-unreachable-for-tos',
				13: 'communication-prohibited',
				14: 'host-precedence-violation',
				15: 'precedence-cutoff'
			}
			# Reverse textual-to-code for detection in rule text
			dest_unreach_text_to_code = {v: k for k, v in dest_unreach_code_map.items()}
			icmptype_match = re.search(r'\bicmptype\s+(\d+)\b', extra_details)
			if icmptype_match:
				icmp_value = int(icmptype_match.group(1))
				match['ip']['icmp-type'] = icmpv4_value_to_name.get(icmp_value, icmp_value)
			else:
				# Handle textual destination-unreachable code names generically
				textual_code = None
				for code_name in dest_unreach_text_to_code.keys():
					if re.search(r'\b' + re.escape(code_name) + r'\b', extra_details):
						textual_code = code_name
						break
				if textual_code is not None:
					match['ip']['icmp-type'] = 'destination-unreachable'
					match['ip']['icmp-code'] = textual_code
				else:
					for icmp_name in icmpv4_name_to_value.keys():
						if re.search(r'\b' + re.escape(icmp_name) + r'\b', extra_details):
							match['ip']['icmp-type'] = icmp_name
							break

			# Parse ICMP code if present (e.g., "icmptype 3 code 3,")
			code_match = re.search(r'\bcode\s+(\d+)(?:\b|\D)', extra_details)
			if code_match:
				code_val = int(code_match.group(1))
				icmp_type_val = match['ip'].get('icmp-type')
				# If destination-unreachable, convert numeric code to descriptive name when available
				if (icmp_type_val == 3 or icmp_type_val == 'destination-unreachable') and code_val in dest_unreach_code_map:
					match['ip']['icmp-type'] = 'destination-unreachable'
					match['ip']['icmp-code'] = dest_unreach_code_map[code_val]
				else:
					match['ip']['icmp-code'] = code_val
		elif rule['protocol'] == 'ipv6-icmp':
			match['ip']['protocol'] = 'icmpv6'
			# Parse ICMPv6 types
			icmpv6_name_to_value = {
				'destination-unreachable': 1,
				'packet-too-big': 2,
				'time-exceeded': 3,
				'parameter-problem': 4,
				'echo-request': 128,
				'echo-reply': 129,
				'router-solicitation': 133,
				'router-advertisement': 134,
				'neighbour-solicitation': 135,
				'neighbour-advertisement': 136,
				'redirect': 137,
			}
			icmpv6_value_to_name = {v: k for k, v in icmpv6_name_to_value.items()}
			# Match only "ipv6-icmptype"
			icmptype6_match = re.search(r'\bipv6-icmptype\s+(\d+)\b', extra_details)
			if icmptype6_match:
				icmp6_value = int(icmptype6_match.group(1))
				match['ip']['icmpv6-type'] = icmpv6_value_to_name.get(icmp6_value, icmp6_value)
			else:
				for icmp6_name in icmpv6_name_to_value.keys():
					if re.search(r'\b' + re.escape(icmp6_name) + r'\b', extra_details):
						match['ip']['icmpv6-type'] = icmp6_name
						break
	
	# Parse fragment
	if '-f' in extra_details or 'fragment' in extra_details:
		if 'ip' not in match:
			match['ip'] = {}
		match['ip']['fragment'] = {}
	
	# Parse connection state (if not already parsed)
	if ('ctstate' in extra_details and
	    'connection-state' not in match.get('ip', {})):
		state_match = re.search(r'ctstate ([A-Z,]+)', extra_details)
		if state_match:
			states = state_match.group(1).split(',')
			connection_state = {}
			for state in states:
				connection_state[state.lower()] = {}
			if 'ip' not in match:
				match['ip'] = {}
			match['ip']['connection-state'] = connection_state
	
	# Parse recent-list parameters
	if 'recent:' in extra_details:
		if 'ip' not in match:
			match['ip'] = {}
		
		recent_info = {}
		
		# Parse recent action (SET or UPDATE)
		if 'recent: SET' in extra_details:
			recent_info['action'] = 'set'
		elif 'recent: UPDATE' in extra_details:
			recent_info['action'] = 'update'
		
		# Parse name
		name_match = re.search(r'name:\s*(\w+)', extra_details)
		if name_match:
			recent_info['name'] = name_match.group(1)
		
		# Parse seconds (update-interval)
		seconds_match = re.search(r'seconds:\s*(\d+)', extra_details)
		if seconds_match:
			recent_info['update-interval'] = int(seconds_match.group(1))
		
		# Parse hit_count
		hit_count_match = re.search(r'hit_count:\s*(\d+)', extra_details)
		if hit_count_match:
			recent_info['hit-count'] = int(hit_count_match.group(1))
		
		if recent_info:
			match['ip']['recent-list'] = recent_info
	
	# Parse TTL matching (IPv4)
	if 'TTL match' in extra_details:
		if 'ip' not in match:
			match['ip'] = {}
		ttl_match = re.search(r'TTL == (\d+)', extra_details)
		if ttl_match:
			match['ip']['ttl'] = int(ttl_match.group(1))
	
	# Parse HL matching (IPv6 Hop Limit)
	if 'HL match' in extra_details:
		if 'ip' not in match:
			match['ip'] = {}
		hl_match = re.search(r'HL == (\d+)', extra_details)
		if hl_match:
			match['ip']['ttl'] = int(hl_match.group(1))
	
	# Parse DSCP matching
	if 'DSCP match' in extra_details:
		if 'ip' not in match:
			match['ip'] = {}
		dscp_match = re.search(r'DSCP match 0x([0-9a-fA-F]+)', extra_details)
		if dscp_match:
			# Convert hex to decimal
			dscp_value = int(dscp_match.group(1), 16)
			match['ip']['dscp'] = dscp_value

	# Parse VLAN from iptables/ip6tables MARK match
	mark_match = re.search(r'\bmark match 0x([0-9a-fA-F]+)\b', extra_details)
	if mark_match:
		mark_value = int(mark_match.group(1), 16)
		# Subtract base 100 to get VLAN ID
		match['vlan'] = mark_value - 100
	
	# Parse ECN matching
	if 'ECN match' in extra_details:
		if 'ip' not in match:
			match['ip'] = {}
		ecn_info = {}
		
		# Parse ECN flags (convert to list format)
		ecn_flags = []
		if 'ECE' in extra_details:
			ecn_flags.append('tcp-ece')
		if 'CWR' in extra_details:
			ecn_flags.append('tcp-cwr')
		
		if ecn_flags:
			ecn_info['flags'] = ecn_flags
		
		# Parse ECT value
		ect_match = re.search(r'ECT=(\d+)', extra_details)
		if ect_match:
			ecn_info['ip-ect'] = int(ect_match.group(1))
		
		if ecn_info:
			match['ip']['ecn'] = ecn_info
	
	# Parse hashlimit/limit parameters
	if 'limit:' in extra_details:
		if 'ip' not in match:
			match['ip'] = {}
		
		# Check for hashlimit (more complex rate limiting)
		if ('mode' in extra_details and
		    ('srcip' in extra_details or 'src-ip' in extra_details)):
			hashlimit_info = {}
			
			# Parse rate-above
			rate_match = re.search(r'limit: above (\d+)/(\w+)', extra_details)
			if rate_match:
				rate_value = rate_match.group(1)
				rate_unit = rate_match.group(2)
				hashlimit_info['rate-above'] = f"{rate_value}/{rate_unit}"
			
			# Parse burst
			burst_match = re.search(r'burst (\d+)', extra_details)
			if burst_match:
				hashlimit_info['burst'] = int(burst_match.group(1))
			
			# Parse mode
			if 'srcip' in extra_details or 'src-ip' in extra_details:
				hashlimit_info['mode'] = 'src-ip'
			
			# Parse expire (htable-expire)
			expire_match = re.search(r'htable-expire (\d+)', extra_details)
			if expire_match:
				hashlimit_info['expire'] = int(expire_match.group(1))
			
			# Parse source-mask (default 32 for IPv4)
			hashlimit_info['source-mask'] = 32
			
			# Parse name (extract from rule context or generate)
			if 'RST' in extra_details:
				hashlimit_info['name'] = 'TCPRST'
			elif 'GENERAL' in extra_details or 'ctstate NEW' in extra_details:
				hashlimit_info['name'] = 'TCPGENERAL'
			else:
				hashlimit_info['name'] = 'DEFAULT'
			
			if hashlimit_info:
				match['ip']['hashlimit'] = hashlimit_info
	
	return match

def parse_iptables_output_to_json(output, rule_type):
	"""Parse iptables output and convert to JSON format"""
	lines = output.strip().split('\n')
	result = {}
	
	current_table = None
	current_chain = None
	
	for line in lines:
		line = line.strip()
		if not line:
			continue
			
		# Parse table line
		if line.startswith('TABLE '):
			current_table = line.split()[1].rstrip(':')
			if current_table not in result:
				result[current_table] = {}
			continue
			
		# Parse chain line
		if line.startswith('Chain '):
			parts = line.split()
			current_chain = parts[1]
			if current_table and current_chain:
				if current_chain not in result[current_table]:
					result[current_table][current_chain] = {}
			continue
			
		# Skip header lines
		if 'pkts bytes target' in line or 'target     prot opt' in line:
			continue
			
		# Parse rule lines
		if current_table and current_chain and line and not line.startswith('Chain'):
			rule = parse_iptables_rule_line(line)
			if rule and 'rule_info' in rule:
				rule_info = rule['rule_info']
				if 'acl_name' in rule_info and 'rule_id' in rule_info:
					acl_name = rule_info['acl_name']
					rule_id = rule_info['rule_id']
					interface_id = rule_info.get('interface_id', '')
					direction = rule_info.get('dir', '')
					
					# Initialize ACL structure
					if acl_name not in result[current_table][current_chain]:
						result[current_table][current_chain][acl_name] = {
							'rule': {}
						}
					
					# Add rule
					if 'rule' not in result[current_table][current_chain][acl_name]:
						result[current_table][current_chain][acl_name]['rule'] = {}
					
					# Parse match and action using NVUE format
					match_nvue = parse_match_nvue_format(rule, rule['extra'])
					action_nvue = parse_action_nvue_format(rule['target'], rule['extra'])
					
					# Keep rule_id unchanged - handle multiple match/action
					# combinations within same rule_id
					unique_key = rule_id
					
					# Initialize rule structure per rule_id
					if unique_key not in result[current_table][current_chain][acl_name]['rule']:
						rule_entry = {
							'type': rule_type,
							'rules': []  # Array to hold multiple match/action combinations
						}
						
						# Add remark if this is a specific ACL that should have one
						if acl_name == 'acl_2' and rule_id == '10':
							rule_entry['remark'] = 'deny all from 10.1.1.x'
						
						result[current_table][current_chain][acl_name]['rule'][unique_key] = rule_entry
					
					# Check if this exact match/action combination already exists
					existing_combination = None
					for existing in result[current_table][current_chain][acl_name]['rule'][unique_key]['rules']:
						if existing['match'] == match_nvue and existing['action'] == action_nvue:
							existing_combination = existing
							break
					
					# Add new match/action combination with its own statistics
					if existing_combination is None:
						rule_combination = {
							'match': match_nvue,
							'action': action_nvue,
							'statistics': {}  # Each combination has separate statistics
						}
						result[current_table][current_chain][acl_name]['rule'][unique_key]['rules'].append(rule_combination)
						existing_combination = rule_combination
					
					# Add statistics per match/action combination and interface
					if interface_id:
						if interface_id not in existing_combination['statistics']:
							existing_combination['statistics'][interface_id] = {}
						
						# Update statistics based on direction (add only the present direction)
						if direction == 'inbound':
							existing_combination['statistics'][interface_id]['inbound'] = {
								'packet': convert_size_to_bytes(rule['packets']),
								'byte': convert_size_to_bytes(rule['bytes'])
							}
						elif direction == 'outbound':
							existing_combination['statistics'][interface_id]['outbound'] = {
								'packet': convert_size_to_bytes(rule['packets']),
								'byte': convert_size_to_bytes(rule['bytes'])
							}
	
	return result

def parse_ebtables_match_nvue_format(line):
	"""Parse ebtables rule line to NVUE match format"""
	match = {}

	def _normalize_mac_octets(mac_str):
		parts = mac_str.split(':')
		if len(parts) != 6:
			return mac_str
		try:
			norm_parts = [format(int(p, 16), '02X') for p in parts]
			return ':'.join(norm_parts)
		except ValueError:
			return mac_str
	
	# Parse source MAC address
	source_mac_match = re.search(
		r'-s\s+(Broadcast|[0-9a-fA-F:]+)(?:/([0-9a-fA-F:]+))?', line, re.IGNORECASE)
	if source_mac_match:
		source_mac = source_mac_match.group(1)
		if re.fullmatch(r'Broadcast', source_mac, re.IGNORECASE):
			source_mac = 'FF:FF:FF:FF:FF:FF'
		source_mask = (source_mac_match.group(2) if source_mac_match.group(2)
				   else 'FF:FF:FF:FF:FF:FF')

		# Normalize MAC and mask to XX:XX:XX:XX:XX:XX format
		source_mac = _normalize_mac_octets(source_mac)
		source_mask = _normalize_mac_octets(source_mask)
		
		match['mac'] = {
			'source-mac': source_mac,
			'source-mac-mask': source_mask,
			'dest-mac-mask': 'FF:FF:FF:FF:FF:FF'  # Default
		}
	
	# Parse destination MAC address
	dest_mac_match = re.search(r'-d\s+(Broadcast|[0-9a-fA-F:]+)(?:/([0-9a-fA-F:]+))?', line, re.IGNORECASE)
	if dest_mac_match:
		dest_mac = dest_mac_match.group(1)
		if re.fullmatch(r'Broadcast', dest_mac, re.IGNORECASE):
			dest_mac = 'FF:FF:FF:FF:FF:FF'
		dest_mask = (dest_mac_match.group(2) if dest_mac_match.group(2)
				 else 'FF:FF:FF:FF:FF:FF')

		# Normalize MAC and mask to XX:XX:XX:XX:XX:XX format
		dest_mac = _normalize_mac_octets(dest_mac)
		dest_mask = _normalize_mac_octets(dest_mask)
		
		if 'mac' not in match:
			match['mac'] = {'source-mac-mask': 'FF:FF:FF:FF:FF:FF'}
		match['mac']['dest-mac'] = dest_mac
		match['mac']['dest-mac-mask'] = dest_mask
	
	# Parse protocol - only set for IP processing rules
	protocol = None  # Initialize protocol variable
	if '-p IPv6' in line:
		protocol = 'ipv6'
		if 'mac' not in match:
			match['mac'] = {
				'source-mac-mask': 'FF:FF:FF:FF:FF:FF',
				'dest-mac-mask': 'FF:FF:FF:FF:FF:FF'
			}
		match['mac']['protocol'] = 'ipv6'
	elif '-p IPv4' in line or '-p IP' in line:
		protocol = 'ipv4'
		if 'mac' not in match:
			match['mac'] = {
				'source-mac-mask': 'FF:FF:FF:FF:FF:FF',
				'dest-mac-mask': 'FF:FF:FF:FF:FF:FF'
			}
		match['mac']['protocol'] = 'ipv4'
	elif '-p ARP' in line:
		protocol = 'arp'
		if 'mac' not in match:
			match['mac'] = {
				'source-mac-mask': 'FF:FF:FF:FF:FF:FF',
				'dest-mac-mask': 'FF:FF:FF:FF:FF:FF'
			}
		match['mac']['protocol'] = 'arp'
	else:
		# No protocol specified: mark as ANY
		protocol = 'any'
		if 'mac' not in match:
			match['mac'] = {
				'source-mac-mask': 'FF:FF:FF:FF:FF:FF',
				'dest-mac-mask': 'FF:FF:FF:FF:FF:FF'
			}
		match['mac']['protocol'] = 'ANY'

	# Default missing MACs to ANY if not specified
	if 'mac' in match:
		if 'source-mac' not in match['mac']:
			match['mac']['source-mac'] = 'ANY'
		if 'dest-mac' not in match['mac']:
			match['mac']['dest-mac'] = 'ANY'
	
	# Parse VLAN information
	vlan_match = re.search(r'--vlan-id\s+(\d+)', line)
	if vlan_match:
		match['vlan'] = int(vlan_match.group(1))
	
	# Parse IP information if present
	if protocol in ['ipv4', 'ipv6']:
		match['ip'] = {}
		
		# Parse IPv6 source address
		if '--ip6-src' in line:
			ip6_src_match = re.search(r'--ip6-src\s+([0-9a-fA-F:]+)', line)
			if ip6_src_match:
				match['ip']['source-ip'] = ip6_src_match.group(1)
		
		# Parse IPv6 destination address
		if '--ip6-dst' in line:
			ip6_dst_match = re.search(r'--ip6-dst\s+([0-9a-fA-F:]+)', line)
			if ip6_dst_match:
				match['ip']['dest-ip'] = ip6_dst_match.group(1)
		
		# Parse IPv4 source address
		if '--ip-src' in line:
			ip_src_match = re.search(r'--ip-src\s+([0-9.]+(?:/\d+)?)', line)
			if ip_src_match:
				match['ip']['source-ip'] = ip_src_match.group(1)
		
		# Parse IPv4 destination address
		if '--ip-dst' in line:
			ip_dst_match = re.search(r'--ip-dst\s+([0-9.]+(?:/\d+)?)', line)
			if ip_dst_match:
				match['ip']['dest-ip'] = ip_dst_match.group(1)
		
		# Parse IP protocol (IPv4)
		if '--ip-proto' in line:
			ip_proto_match = re.search(r'--ip-proto\s+(\w+)', line)
			if ip_proto_match:
				proto = ip_proto_match.group(1).lower()
				if proto in ['tcp', 'udp']:
					match['ip']['protocol'] = proto
					
					# Parse TCP/UDP ports for IPv4
					if proto == 'tcp':
						match['ip']['tcp'] = {}
						# Parse TCP source port
						if '--ip-sport' in line:
							# Check for port range first
							sport_range_match = re.search(r'--ip-sport\s+(\d+):(\d+)', line)
							if sport_range_match:
								start_port = sport_range_match.group(1)
								end_port = sport_range_match.group(2)
								match['ip']['tcp']['source-port'] = [f"{start_port}-{end_port}"]
							else:
								# Check for single numeric port
								sport_match = re.search(r'--ip-sport\s+(\d+)', line)
								if sport_match:
									port_val = int(sport_match.group(1))
									match['ip']['tcp']['source-port'] = [port_val]
						
						# Parse TCP destination port
						if '--ip-dport' in line:
							# First check for port range
							dport_range_match = re.search(r'--ip-dport\s+(\d+):(\d+)', line)
							if dport_range_match:
								start = dport_range_match.group(1)
								end = dport_range_match.group(2)
								match['ip']['tcp']['dest-port'] = [f"{start}-{end}"]
							else:
								# Check for single numeric port
								dport_match = re.search(r'--ip-dport\s+(\d+)', line)
								if dport_match:
									port_val = int(dport_match.group(1))
									# Convert known ports to names for NVUE consistency
									if port_val == 80:
										match['ip']['tcp']['dest-port'] = ['http']
									else:
										match['ip']['tcp']['dest-port'] = [port_val]
								else:
									# Check for named port
									dport_name_match = re.search(r'--ip-dport\s+([a-zA-Z][a-zA-Z0-9-]*)', line)
									if dport_name_match:
										port_name = dport_name_match.group(1)
										match['ip']['tcp']['dest-port'] = [port_name]
					
					elif proto == 'udp':
						match['ip']['udp'] = {}
						# Parse UDP source port
						if '--ip-sport' in line:
							# Check for port range first
							sport_range_match = re.search(r'--ip-sport\s+(\d+):(\d+)', line)
							if sport_range_match:
								start_port = sport_range_match.group(1)
								end_port = sport_range_match.group(2)
								match['ip']['udp']['source-port'] = [f"{start_port}-{end_port}"]
							else:
								# Check for single numeric port
								sport_match = re.search(r'--ip-sport\s+(\d+)', line)
								if sport_match:
									port_val = int(sport_match.group(1))
									match['ip']['udp']['source-port'] = [port_val]
						
						# Parse UDP destination port
						if '--ip-dport' in line:
							# Check for port range first
							dport_range_match = re.search(r'--ip-dport\s+(\d+):(\d+)', line)
							if dport_range_match:
								start_port = dport_range_match.group(1)
								end_port = dport_range_match.group(2)
								match['ip']['udp']['dest-port'] = [f"{start_port}-{end_port}"]
							else:
								# Check for single numeric port
								dport_match = re.search(r'--ip-dport\s+(\d+)', line)
								if dport_match:
									port_val = int(dport_match.group(1))
									# Convert known ports to names for NVUE consistency
									if port_val == 21:
										match['ip']['udp']['dest-port'] = ['ftp']
									else:
										match['ip']['udp']['dest-port'] = [port_val]
								else:
									# Check for named port
									dport_name_match = re.search(r'--ip-dport\s+([a-zA-Z][a-zA-Z0-9-]*)', line)
									if dport_name_match:
										port_name = dport_name_match.group(1)
										match['ip']['udp']['dest-port'] = [port_name]
		
		# Parse IPv6 protocol
		if '--ip6-proto' in line:
			ip6_proto_match = re.search(r'--ip6-proto\s+(\w+)', line)
			if ip6_proto_match:
				proto = ip6_proto_match.group(1).lower()
				if proto in ['tcp', 'udp']:
					# For IPv6, we don't set the protocol field at IP level in NVUE format
					# Just parse the L4 information
					
					# Parse TCP/UDP ports for IPv6
					if proto == 'tcp':
						if 'tcp' not in match['ip']:
							match['ip']['tcp'] = {}
						# Parse TCP source port
						if '--ip6-sport' in line:
							# Check for port range first
							sport_range_match = re.search(r'--ip6-sport\s+(\d+):(\d+)', line)
							if sport_range_match:
								start_port = sport_range_match.group(1)
								end_port = sport_range_match.group(2)
								match['ip']['tcp']['source-port'] = [f"{start_port}-{end_port}"]
							else:
								# Check for single numeric port
								sport_match = re.search(r'--ip6-sport\s+(\d+)', line)
								if sport_match:
									port_val = int(sport_match.group(1))
									match['ip']['tcp']['source-port'] = [port_val]
						
						# Parse TCP destination port
						if '--ip6-dport' in line:
							# Check for port range first
							dport_range_match = re.search(r'--ip6-dport\s+(\d+):(\d+)', line)
							if dport_range_match:
								start_port = dport_range_match.group(1)
								end_port = dport_range_match.group(2)
								match['ip']['tcp']['dest-port'] = [f"{start_port}-{end_port}"]
							else:
								# Check for single numeric port
								dport_match = re.search(r'--ip6-dport\s+(\d+)', line)
								if dport_match:
									port_val = int(dport_match.group(1))
									match['ip']['tcp']['dest-port'] = [port_val]
								else:
									# Check for named port
									dport_name_match = re.search(r'--ip6-dport\s+([a-zA-Z][a-zA-Z0-9-]*)', line)
									if dport_name_match:
										port_name = dport_name_match.group(1)
										match['ip']['tcp']['dest-port'] = [port_name]
					
					elif proto == 'udp':
						if 'udp' not in match['ip']:
							match['ip']['udp'] = {}
						# Parse UDP source port
						if '--ip6-sport' in line:
							# Check for port range first
							sport_range_match = re.search(r'--ip6-sport\s+(\d+):(\d+)', line)
							if sport_range_match:
								start_port = sport_range_match.group(1)
								end_port = sport_range_match.group(2)
								match['ip']['udp']['source-port'] = [f"{start_port}-{end_port}"]
							else:
								# Check for single numeric port
								sport_match = re.search(r'--ip6-sport\s+(\d+)', line)
								if sport_match:
									port_val = int(sport_match.group(1))
									match['ip']['udp']['source-port'] = [port_val]
						
						# Parse UDP destination port
						if '--ip6-dport' in line:
							# Check for port range first
							dport_range_match = re.search(r'--ip6-dport\s+(\d+):(\d+)', line)
							if dport_range_match:
								start_port = dport_range_match.group(1)
								end_port = dport_range_match.group(2)
								match['ip']['udp']['dest-port'] = [f"{start_port}-{end_port}"]
							else:
								# Check for single numeric port
								dport_match = re.search(r'--ip6-dport\s+(\d+)', line)
								if dport_match:
									port_val = int(dport_match.group(1))
									# Convert known ports to names for NVUE consistency
									if port_val == 21:
										match['ip']['udp']['dest-port'] = ['ftp']
									else:
										match['ip']['udp']['dest-port'] = [port_val]
								else:
									# Check for named port
									dport_name_match = re.search(r'--ip6-dport\s+([a-zA-Z][a-zA-Z0-9-]*)', line)
									if dport_name_match:
										port_name = dport_name_match.group(1)
										match['ip']['udp']['dest-port'] = [port_name]
	
	return match

def parse_ebtables_action_nvue_format(line):
	"""Parse ebtables action to NVUE format"""
	action = {}
	
	# Parse target/action
	target_match = re.search(r'-j\s+(\w+)', line)
	if target_match:
		target = target_match.group(1).lower()
		if target == 'accept':
			action['permit'] = {}
		elif target == 'drop':
			action['deny'] = {}
		elif target == 'dnat':
			# Parse destination MAC translation
			mac_match = re.search(r'--to-dst\s+([0-9A-Fa-f:]{2}(?::[0-9A-Fa-f]{2}){5})', line)
			if mac_match:
				mac = mac_match.group(1)
				# Normalize MAC to upper-case zero-padded octets
				try:
					parts = mac.split(':')
					mac = ':'.join([format(int(p, 16), '02X') for p in parts]) if len(parts) == 6 else mac.upper()
				except ValueError:
					mac = mac.upper()
				action['dest-nat'] = {'translate-mac': {mac: {}}}
			else:
				action['dest-nat'] = {}
		elif target == 'snat':
			# Parse source MAC translation
			mac_match = re.search(r'--to-src\s+([0-9A-Fa-f:]{2}(?::[0-9A-Fa-f]{2}){5})', line)
			if mac_match:
				mac = mac_match.group(1)
				# Normalize MAC to upper-case zero-padded octets
				try:
					parts = mac.split(':')
					mac = ':'.join([format(int(p, 16), '02X') for p in parts]) if len(parts) == 6 else mac.upper()
				except ValueError:
					mac = mac.upper()
				action['source-nat'] = {'translate-mac': {mac: {}}}
			else:
				action['source-nat'] = {}
		elif target == 'setqos':
			# Parse CoS setting
			cos_match = re.search(r'--set-cos\s+(\d+)', line)
			if cos_match:
				action['set'] = {'cos': int(cos_match.group(1))}
			else:
				action['set'] = {}
		elif target == 'erspan':
			# Parse ERSPAN action parameters
			erspan_action = {}
			
			# Parse source IP
			src_ip_match = re.search(r'--src_ip\s+([0-9\.]+)', line)
			if src_ip_match:
				erspan_action['source-ip'] = src_ip_match.group(1)
			
			# Parse destination IP
			dst_ip_match = re.search(r'--dst_ip\s+([0-9\.]+)', line)
			if dst_ip_match:
				erspan_action['dest-ip'] = dst_ip_match.group(1)
			
			action['erspan'] = erspan_action
		elif target == 'police':
			# Parse POLICE action parameters
			police_action = {}
			
			# Parse mode
			if '--set-mode pkt' in line:
				police_action['mode'] = 'packet'
			elif '--set-mode byte' in line:
				police_action['mode'] = 'byte'
			
			# Parse rate
			rate_match = re.search(r'--set-rate\s+(\d+)', line)
			if rate_match:
				police_action['rate'] = int(rate_match.group(1))
			
			# Parse burst
			burst_match = re.search(r'--set-burst\s+(\d+)', line)
			if burst_match:
				police_action['burst'] = int(burst_match.group(1))
			
			# Parse class
			class_match = re.search(r'--set-class\s+(\d+)', line)
			if class_match:
				police_action['class'] = int(class_match.group(1))
			
			action['police'] = police_action
		elif target == 'span':
			# Parse SPAN destination
			span_match = re.search(r'--set-dest\s+(\w+)', line)
			if span_match:
				action['span'] = span_match.group(1)
			else:
				action['span'] = 'cpu'  # Default
		else:
			action['permit'] = {}  # Default fallback
	
	# Parse log action
	if '--log-level' in line:
		log_action = {}
		
		# Parse log level
		level_match = re.search(r'--log-level\s+(\w+)', line)
		if level_match:
			level_str = level_match.group(1)
			level_map = {'notice': 5, 'warn': 4, 'info': 6}
			log_action['level'] = level_map.get(level_str, 5)
		
		# Parse log prefix
		prefix_match = re.search(r'--log-prefix\s+"([^"]*)"', line)
		if prefix_match:
			prefix = prefix_match.group(1)
			if prefix:  # Only add if not empty
				log_action['log-prefix'] = prefix
		
		if log_action:
			action['log'] = log_action
	
	if not action:
		action['permit'] = {}  # Default
	
	return action

def parse_ebtables_output_to_json(output):
	"""Parse ebtables output and convert to JSON format"""
	lines = output.strip().split('\n')
	result = {}
	
	current_table = None
	current_chain = None
	
	for line in lines:
		line = line.strip()
		if not line:
			continue
			
		# Parse table line
		if line.startswith('TABLE '):
			current_table = line.split()[1].rstrip(':')
			if current_table not in result:
				result[current_table] = {}
			continue
			
		# Parse chain line - ebtables format:
		# "Bridge chain: FORWARD, entries: 2, policy: ACCEPT"
		if line.startswith('Bridge chain:'):
			parts = line.split()
			if len(parts) >= 3:
				current_chain = parts[2].rstrip(',')
				# For ebtables, if no table is set, default to 'filter'
				if not current_table:
					current_table = 'filter'
					result[current_table] = {}
				if current_table and current_chain:
					if current_chain not in result[current_table]:
						result[current_table][current_chain] = {}
			continue
			
		# Parse rule lines - ebtables format is different from iptables
		# Example: "-s 1:2:3:4:0:0 -i swp1s1 --comment
		# rule_id:10,acl_name:exp4,dir:inbound,interface_id:swp1s1 -j ACCEPT ,
		# pcnt = 0 -- bcnt = 0"
		if current_table and current_chain and line and '--comment' in line:
			# Extract comment from ebtables format; stop at next option (" -<opt>"),
			# counters (pcnt/bcnt), or end of line. Also handle optional quotes.
			comment_match = re.search(r'--comment\s+(?:"([^"]*)"|(.+?))(?=\s+-{1,2}[A-Za-z]|\s*,?\s*(?:pcnt|bcnt)\b|$)', line)
			if comment_match:
				comment_content = (comment_match.group(1) or comment_match.group(2)).strip()
				# Convert to the format expected by parse_iptables_rule_comment
				comment = f"/* {comment_content} */"
				rule_info = parse_iptables_rule_comment(comment)
				
				if 'acl_name' in rule_info and 'rule_id' in rule_info:
					acl_name = rule_info['acl_name']
					rule_id = rule_info['rule_id']
					interface_id = rule_info.get('interface_id', '')
					direction = rule_info.get('dir', '')
					
					# Parse packet and byte counts from ebtables format
					# Example: "pcnt = 0 -- bcnt = 0"
					packets = 0
					bytes_count = 0
					pcnt_match = re.search(r'pcnt\s*=\s*(\d+)', line)
					bcnt_match = re.search(r'bcnt\s*=\s*(\d+)', line)
					if pcnt_match:
						packets = int(pcnt_match.group(1))
					if bcnt_match:
						bytes_count = int(bcnt_match.group(1))
					
					# Initialize ACL structure
					if acl_name not in result[current_table][current_chain]:
						result[current_table][current_chain][acl_name] = {
							'rule': {}
						}
					
					# Parse match and action using NVUE format
					match_nvue = parse_ebtables_match_nvue_format(line)
					action_nvue = parse_ebtables_action_nvue_format(line)
					
					# Initialize rule structure per rule_id (symmetric with IPv4/IPv6)
					if rule_id not in result[current_table][current_chain][acl_name]['rule']:
						rule_entry = {
							'type': 'mac',
							'rules': []  # Array to hold multiple match/action combinations
						}
						result[current_table][current_chain][acl_name]['rule'][rule_id] = rule_entry
					
					# Check if this exact match/action combination already exists
					existing_combination = None
					for existing in result[current_table][current_chain][acl_name]['rule'][rule_id]['rules']:
						if existing['match'] == match_nvue and existing['action'] == action_nvue:
							existing_combination = existing
							break
					
					# Add new match/action combination with its own statistics
					if existing_combination is None:
						rule_combination = {
							'match': match_nvue,
							'action': action_nvue,
							'statistics': {}  # Each combination has separate statistics
						}
						result[current_table][current_chain][acl_name]['rule'][rule_id]['rules'].append(rule_combination)
						existing_combination = rule_combination
					
					# Add statistics per match/action combination and interface
					if interface_id:
						if interface_id not in existing_combination['statistics']:
							existing_combination['statistics'][interface_id] = {}
						
						# Update statistics based on direction (add only the present direction)
						if direction == 'inbound':
							existing_combination['statistics'][interface_id]['inbound'] = {
								'packet': packets,
								'byte': bytes_count
							}
						elif direction == 'outbound':
							existing_combination['statistics'][interface_id]['outbound'] = {
								'packet': packets,
								'byte': bytes_count
							}
	
	return result

def iptables_list():
	err = 0
	all_output = ""
	
	if json_output:
		# Collect all output for JSON parsing - suppress all output
		import sys
		import io
		from contextlib import redirect_stdout, redirect_stderr
		
		# Capture stdout and stderr to prevent raw output
		captured_output = io.StringIO()
		captured_errors = io.StringIO()
		
		with redirect_stdout(captured_output), redirect_stderr(captured_errors):
			for table in iptables.kernel_tables:
				all_output += 'TABLE %s :\n' % table
				(cmd, ret, cmd_out) = iptables.list(table,
							('-v%s' % (' -n' * numeric)) + ((exact and ' -x') or ''), False)
				if ret != 0:
					# Don't use log_cmderr here as it would print to stderr
					err += 1
				else:
					all_output += cmd_out.decode('utf-8') if isinstance(cmd_out, bytes) else str(cmd_out)
					all_output += '\n\n'
		
		if err == 0:
			json_result = parse_iptables_output_to_json(all_output, 'ipv4')
			print(json.dumps({'iptables': json_result}, indent=2))
		else:
			# If there were errors, print them
			error_output = captured_errors.getvalue()
			if error_output:
				print(error_output, file=sys.stderr)
	else:
		# Original behavior
		for table in iptables.kernel_tables:
			log('TABLE %s :' %table)
			(cmd, ret, cmd_out) = iptables.list(table,
						('-v%s' % (' -n' * numeric)) + ((exact and ' -x') or ''), verbose)
			if ret != 0:
				log_cmderr(cmd, cmd_out)
				err += 1
			else:
				print('\n\n')
	
	if err > 0:
		return -1

	return 0

def ip6tables_list():
	err = 0
	all_output = ""
	
	if json_output:
		# Collect all output for JSON parsing - suppress all output
		import sys
		import io
		from contextlib import redirect_stdout, redirect_stderr
		
		# Capture stdout and stderr to prevent raw output
		captured_output = io.StringIO()
		captured_errors = io.StringIO()
		
		with redirect_stdout(captured_output), redirect_stderr(captured_errors):
			for table in ip6tables.kernel_tables:
				all_output += 'TABLE %s :\n' % table
				(cmd, ret, cmd_out) = ip6tables.list(table,
							('-v%s' % (' -n' * numeric)) + ((exact and ' -x') or ''), False)
				if ret != 0:
					# Don't use log_cmderr here as it would print to stderr
					err += 1
				else:
					all_output += cmd_out.decode('utf-8') if isinstance(cmd_out, bytes) else str(cmd_out)
					all_output += '\n\n'
		
		if err == 0:
			json_result = parse_iptables_output_to_json(all_output, 'ipv6')
			print(json.dumps({'ip6tables': json_result}, indent=2))
		else:
			# If there were errors, print them
			error_output = captured_errors.getvalue()
			if error_output:
				print(error_output, file=sys.stderr)
	else:
		# Original behavior
		for table in ip6tables.kernel_tables:
			log('TABLE %s :' %table)
			(cmd, ret, cmd_out) = ip6tables.list(table,
						('-v%s' % (' -n' * numeric)) + ((exact and ' -x') or ''), verbose)
			if ret != 0:
				log_cmderr(cmd, cmd_out)
				err += 1
			else:
				print('\n\n')
	
	if err > 0:
		return -1

	return 0

def ebtables_list():
	err = 0
	all_output = ""
	
	if json_output:
		# Collect all output for JSON parsing - suppress all output
		import sys
		import io
		from contextlib import redirect_stdout, redirect_stderr
		
		# Capture stdout and stderr to prevent raw output
		captured_output = io.StringIO()
		captured_errors = io.StringIO()
		
		with redirect_stdout(captured_output), redirect_stderr(captured_errors):
			for table in ebtables.kernel_tables:
				all_output += 'TABLE %s :\n' % table
				(cmd, ret, cmd_out) = ebtables.list(table,
							'--Lc', False)
				if ret != 0:
					# Don't use log_cmderr here as it would print to stderr
					err += 1
				else:
					all_output += cmd_out.decode('utf-8') if isinstance(cmd_out, bytes) else str(cmd_out)
					all_output += '\n\n'
		
		if err == 0:
			json_result = parse_ebtables_output_to_json(all_output)
			print(json.dumps({'ebtables': json_result}, indent=2))
		else:
			# If there were errors, print them
			error_output = captured_errors.getvalue()
			if error_output:
				print(error_output, file=sys.stderr)
	else:
		# Original behavior
		for table in ebtables.kernel_tables:
			log('TABLE %s :' %table)
			(cmd, ret, cmd_out) = ebtables.list(table,
						'--Lc', verbose)
			if ret != 0:
				log_cmderr(cmd, cmd_out)
				err += 1
			else:
				print('\n\n')
	
	if err > 0:
		return -1

	return 0


acl_handlers = {
	aclRuleType.iptables :
		{ 'prepare' : iptables_prepare_install,
		  'install' : iptables_install,
		  'rollback' : iptables_rollback,
		  'orig_rules_file' : 'iptables.save',
		  'new_rules_file' : 'iptables.restore',
		  'flush' : iptables_flush,
		  'flush_prepare' : iptables_flush_prepare,
		  'flush_rollback' : iptables_rollback,
		  'setcounter' : iptables_set_counters,
		  'list' : iptables_list},
	aclRuleType.ip6tables :
		{ 'prepare' : ip6tables_prepare_install,
		  'install' : ip6tables_install,
		  'rollback' : ip6tables_rollback,
		  'orig_rules_file' : 'ip6tables.save',
		  'new_rules_file' : 'ip6tables.restore',
		  'flush' : ip6tables_flush,
		  'flush_prepare' : ip6tables_flush_prepare,
		  'flush_rollback' : ip6tables_rollback,
		  'setcounter' : ip6tables_set_counters,
		  'list' : ip6tables_list},
	aclRuleType.ebtables :
		{ 'prepare' : ebtables_prepare_install,
		  'install' : ebtables_install,
		  'rollback' : ebtables_rollback,
		  'orig_rules_file' : 'ebtables.save',
		  'new_rules_file' : 'ebtables.restore',
		  'flush' : ebtables_flush,
		  'flush_prepare' : ebtables_flush_prepare,
		  'flush_rollback' : ebtables_rollback,
		  'setcounter' : ebtables_set_counters,
		  'list' : ebtables_list},
	}

def run_handler(fw_policies, handler_name):
	# Prepare rules for install
	for type, handlers in sorted(acl_handlers.items()):
		hndlr = handlers.get(handler_name)
		if hndlr != None:
			log_debug('Running handler ' + handler_name +
				+  ' through rules of type %s'
				%aclRuleType.to_str(type))
			ret = hndlr(fw_policies)
			if ret != 0 :
				return -1

	return 0



def install_policies(fw_policies):
	ret = 0

	# prepare for install
	log_verbose('Running install prepare handlers ..')
	for type, handlers in sorted(acl_handlers.items()):
		hndlr = handlers.get('prepare')
		if hndlr != None:
			log_debug('Running handler prepare ' +
				'through rules of type %s'
				%aclRuleType.to_str(type))
			ret = hndlr(fw_policies,
				scratchdir + handlers.get('orig_rules_file'),
				scratchdir + handlers.get('new_rules_file'))
			if ret != 0:
				return ret


	if hw_sync == True:
		log_verbose('Sending hw sync start signal ...');
		ret = hw_sync_start()
		if ret != 0:
			log('hw sync start notify returned err, aborting')
			return -1

	# install rules
	rollback_all = 0
	sync_cancel = 0
	log_verbose('Running install handlers ..')
	for type, handlers in sorted(acl_handlers.items()):
		hndlr = handlers.get('install')
		if hndlr != None:
			log_verbose('Running handler install ' +
				'through rules of type %s'
				%aclRuleType.to_str(type))
			rules_file = scratchdir + handlers.get('new_rules_file')
			ret = hndlr(fw_policies,
				scratchdir + handlers.get('orig_rules_file'),
				scratchdir + handlers.get('new_rules_file'))
			if ret != 0:
				if os.path.exists(rules_file):
					cat_file(rules_file)
				sync_cancel = 1
				break

	if ret == 0 and hw_sync == True:
		log_verbose('Sending hw sync signal ...');
		ret = hw_sync_commit()
		if ret != 0:
			rollback_all = 1

	if ret != 0:
		if sync_cancel == 1 and hw_sync == True:
			log_verbose('Sending hw sync cancel signal ..')
			hw_sync_cancel()

		log('Rolling back ..')
		failed_type = type
		for type, handlers in sorted(acl_handlers.items()):

			#roll back for all types < failed_type
			# XX: Normally type >= failed_type is the right thing
			# to do, except in the ebtables case, where we
			# also do a flush. It does not harm so, also include
			# restore for the type that failed
			if rollback_all == 0 and type > failed_type:
				break

			hndlr = handlers.get('rollback')
			if hndlr != None:
				log_debug('Running handler rollback' +
					' through rules of type %s'
					%aclRuleType.to_str(type))
				rollret = hndlr(fw_policies, scratchdir +
					handlers.get('orig_rules_file'))
				if rollret != 0:
					log_warn('roll back handler for rule' +
						' type ' + aclRuleType.to_str(
							type) + ' failed')

	if ret == 0 and hw_sync == True:
		log_verbose('Sending hw sync end signal ...');
		retend = hw_sync_end()
		if retend != 0:
			log_warn('hw sync end notify returned err')

	return ret

def flush_rules(rule_type_str):
	ret = 0

	rule_type = aclRuleType.from_str_short(rule_type_str)
	if rule_type == -1 and rule_type_str != "all":
		log_error('Invalid flush arg')
		return -1

	# prepare
	log_verbose('Running prepare handlers ..')
	for type, handlers in sorted(acl_handlers.items()):
		if rule_type == type or rule_type_str == 'all':
			hndlr = handlers.get('flush_prepare')
			if hndlr != None:
				log_verbose('Running handler prepare ' +
					'through rules of type %s'
					%aclRuleType.to_str(type))
				ret = hndlr(scratchdir +
					handlers.get('orig_rules_file'))
				if ret != 0:
					return ret


	if hw_sync == True:
		log_verbose('Sending hw sync start signal ...');
		ret = hw_sync_start()
		if ret != 0:
			log('hw sync start notify returned err, aborting')
			return -1

	# flushing  rules
	rollback_all = 0
	sync_cancel = 0
	log_verbose('Running flush handlers ..')
	for type, handlers in sorted(acl_handlers.items()):
		if rule_type == type or rule_type_str == 'all':
			hndlr = handlers.get('flush')
			if hndlr != None:
				log_verbose('Running handler flush ' +
					'through rules of type %s'
					%aclRuleType.to_str(type))
				ret = hndlr()
				if ret != 0:
					sync_cancel = 1
					break

	if ret == 0 and hw_sync == True:
		log_verbose('Sending hw sync signal ...');
		ret = hw_sync_commit()
		if ret != 0:
			rollback_all = 1

	if ret != 0:
		if sync_cancel == 1 and hw_sync == True:
			log_verbose('Sending hw sync cancel signal ..')
			hw_sync_cancel()

		log('Rolling back ..')
		failed_type = type
		for type, handlers in sorted(acl_handlers.items()):
			if rule_type == type or rule_type_str == 'all':

				#roll back for all types < failed_type
				# XX: Normally type >= failed_type is the
				# right thing to do, except in the ebtables
				# case, where we also do a flush. It does not
				# harm so, also include restore for the type
				# that failed
				if rollback_all == 0 and type > failed_type:
					break

				hndlr = handlers.get('rollback')
				if hndlr != None:
					log_debug('Running handler rollback' +
						' through rules of type %s'
						%aclRuleType.to_str(type))
					rollret = hndlr(None, scratchdir +
						handlers.get('orig_rules_file'))
					if rollret != 0:
						log_warn('roll back handler '
							+ 'for rule type ' +
							aclRuleType.to_str(
							type) + ' failed')

	if ret == 0 and hw_sync == True:
		log_verbose('Sending hw sync end signal ...');
		retend = hw_sync_end()
		if retend != 0:
			log_warn('hw sync end notify returned err')

	return ret


def set_counters(rule_type_str, counter_val):
	err = 0

	rule_type = aclRuleType.from_str_short(rule_type_str)
	if rule_type == -1 and rule_type_str != "all":
		log_error('Invalid setcounters arg')
		return -1

	for type, handlers in sorted(acl_handlers.items()):
		if rule_type == type or rule_type_str == 'all':
			setcounterfunc = handlers.get('setcounter')
			if setcounterfunc != None:
				log_verbose('Setting counters to zero for ' +
					'rules of type %s'
					%aclRuleType.to_str(type))
				ret = setcounterfunc(counter_val)
				if ret != 0:
					err += 1
	if err > 0:
		return -1

	return 0

def list_rules(rule_type_str):
	err = 0

	rule_type = aclRuleType.from_str_short(rule_type_str)
	if rule_type == -1 and rule_type_str != "all":
		log_error('Invalid list rules arg')
		return -1

	if json_output and rule_type_str == 'all':
		# For JSON output with "all", combine all results
		combined_result = {}
		
		# Suppress all output during collection
		import sys
		import io
		from contextlib import redirect_stdout, redirect_stderr
		
		captured_output = io.StringIO()
		captured_errors = io.StringIO()
		
		with redirect_stdout(captured_output), redirect_stderr(captured_errors):
			for type, handlers in sorted(acl_handlers.items()):
				listfunc = handlers.get('list')
				if listfunc != None:
					# Temporarily capture stdout to get JSON output
					f = io.StringIO()
					with redirect_stdout(f):
						ret = listfunc()
					
					if ret == 0:
						output = f.getvalue().strip()
						if output:
							try:
								type_result = json.loads(output)
								combined_result.update(type_result)
							except json.JSONDecodeError:
								pass
					else:
						err += 1
		
		if err == 0:
			print(json.dumps(combined_result, indent=2))
		else:
			# If there were errors, print them
			error_output = captured_errors.getvalue()
			if error_output:
				print(error_output, file=sys.stderr)
	else:
		# Original behavior or single type JSON
		for type, handlers in sorted(acl_handlers.items()):
			if rule_type == type or rule_type_str == 'all':
				listfunc = handlers.get('list')
				if listfunc != None:
					if not json_output:
						msg = ('Listing rules of type %s:'
								%aclRuleType.to_str(type))
						print('-' * len(msg))
						log(msg)
						print('-' * len(msg))
					ret = listfunc()
					if ret != 0:
						err += 1
	
	if err > 0:
		return -1

	return 0

def read_policy_file(fw_policy_file):
	"""Calls acl module to read
	   and parse rules in the rule file """
	global env_vars

	log('Reading rule file %s ...' %fw_policy_file)
	fw_policy = aclPolicy(fw_policy_file, env_vars)
	if not fw_policy:
		return None
	dup_check_reqd = not pvt_dot1x_mode
	log('Processing rules in file %s ...' %fw_policy_file)
	ret = fw_policy.process(dup_check_reqd)
	if ret:
		log_verbose('Could not process rules in policy %s'
				%fw_policy.get_name())
		del(fw_policy)
		return None
	return fw_policy

def get_policy_files_from_dir(policy_dir):
	""" Calls read_acl_policy_file for all files
	    under dir policy_dir """
	global policy_files
	err = 0

	try:
		r_dir = os.path.abspath(policy_dir)
		if not r_dir.endswith("/"):
			r_dir += "/"
			policy_file_list = os.listdir(r_dir)
	except Exception as e:
		log_error('failed to read rules directory %s (%s)'
					%(policy_dir, str(e)))
		return None

	for policy_file in sorted(policy_file_list):
		# skip hidden files
		if re.search('^\.|.*~$', os.path.basename(policy_file)):
			continue
		policy_files.append('%s/%s' %(policy_dir,policy_file))

def get_policy_files_from_conf(policy_conffile):
	global env_vars
	global policy_files

	try:
		f = open(policy_conffile, 'r')
		lines = f.readlines()
		f.close()
	except IOError as e:
		log_warn('failed to open policy config file (%s)' %(str(e)))
		pass
		return

	lineno = 1
	for l in lines:
		l = l.strip(' \t\n\r')
		l = l.strip(' ')
		if not l:
			lineno += 1
			continue
		# if comment
		if l[0] == '#':
			lineno += 1
			continue
		# if env variable
		m = re.match(r"(\w+)[\s]*=[\s]*(.*$)", l)
		if m:
			mlist = m.groups()
			if mlist and len(mlist) == 2 and all(mlist):
				env_vars[mlist[0]]=mlist[1]
		elif l[:8] == 'include ':
			# if include
			try :
				pfiles = sorted(glob.glob(l.split()[1]))
				if cmdline_args.lastgood:
					# replace path with lastgood files dir
					lastgoodpfiles = ['%s/%s' %(varrundir_lastgoodpolicyfiles,
										os.path.basename(p)) for p in pfiles]
					policy_files.extend(lastgoodpfiles)
				else:
					policy_files.extend(pfiles)
			except Exception as e:
				log_parseerr(policy_conffile, l, lineno, errstr=str(e))
				pass
		else:
			log_parseerr(policy_conffile, l, lineno)
		lineno += 1

def read_policy_files():
	global policy_files
	err = 0
	fw_policies = []

	for policy_file in policy_files:
		# skip hidden files
		if re.search('^\.|.*~$', os.path.basename(policy_file)):
			continue
		fw_policy = read_policy_file(policy_file)
		if fw_policy:
			fw_policies.append(fw_policy)
		else:
			if not cmdline_args.dry_run:
				return None
			# else, if we are only checking the file,
			# lets continue
			err += 1
	if err > 0:
		return None
	return fw_policies

def log_init():
	global log_handler

	if log_method == "syslog":
		syslog.openlog("cl-acltool", syslog.LOG_CONS | syslog.LOG_PID,
				syslog.LOG_DAEMON)
		log_handler = log_handler_syslog
	else:
		log_handler = log_handler_stdout

def init():
	""" initialization """
	ret = createdir(scratchdir)
	if ret == -1:
		log_error('Could not create scratchdir ' + scratchdir)
		return -1

	ret = createdir(cachedir)
	if ret == -1:
		log_error('Could not create ' + cachedir)
		return -1

	ret = createdir(varrundir_lastgoodpolicyfiles)
	if ret == -1:
		log_error('Could not create ' + varrundir_lastgoodpolicyfiles)
		return -1

	if hw_sync == True:
		log_verbose('Initializing hw communication ..')
		ret = hw_init()
		if ret != 0:
			return -1

	return 0


def deinit():
	""" Cleanup """
	removedir(scratchdir)

def save_installed_state(cmdline_args):

	touch_file(donefile)
	if cmdline_args.lastgood:
		return
	files = glob.glob((varrundir_lastgoodpolicyfiles + '/*'))
	for f in files:
		try:
			os.remove(f)
		except OSError as e:
			log_error('Could not remove file %s: %s \n'%(f, str(e)))
	devnull = open(os.devnull, 'w')
	if not cmdline_args.policy_file and not cmdline_args.policy_dir:
		# copy policy.conf, If the user used it
		subprocess.call(['/bin/cp', '-f', policy_conf,
				varrundir_lastgoodpolicyfiles], stderr=devnull)

	list(map((lambda p: subprocess.call(['/bin/cp', '-f', p,
	    varrundir_lastgoodpolicyfiles], stderr=devnull)), policy_files))
	devnull.close()
	return

def isVX():
    try:
        # Check whether we are running in VX.
        subprocess_output = str(subprocess.check_output('/usr/bin/platform-detect'))
        if "cumulus,vx" in subprocess_output:
            return True
    except (subprocess.CalledProcessError, OSError):
            pass

if __name__ == "__main__":
	""" main function """
	descr = 'acl policy and rule administration'
	fw_policies = []
	ret = 0

	if not os.geteuid() == 0:
		print('Error: Must be root to run this command')
		exit(1)

	default_vx = isVX()

	arg_parser = argparse.ArgumentParser(description=descr)

	# Command line arg parser
	#
	group = arg_parser.add_mutually_exclusive_group(required=True)
	group.add_argument('-i', '--install', dest='install',
			   action='store_true',
			   help='Install acl rules')
	group.add_argument('--hw-sync', dest='hw_sync',
			action='store_true', help=argparse.SUPPRESS)
	group.add_argument('-F', '--flush', dest='flush_type',
			choices=['all', 'ip', 'ip6',
			'eb'],
			help='flush rules')
	group.add_argument('-Z', '--zero-counters', dest='zero_counters',
			choices=['all', 'ip', 'ip6',
			'eb'], help='Zero counters')
	group.add_argument('-L', '--list', dest='list',
			choices=['all', 'ip', 'ip6',
			'eb'], help='List rules')
	group.add_argument('-V', '--version', dest='version',
			action='store_true', help='show version')

	# XXX: Support append (?) maybe. keep it hidden,
	# ie iptables-restore --noflush
	#group.add_argument('--append', dest='append', action='store_true',
	#		    help=argparse.SUPPRESS)

	arg_parser.add_argument('-p', '--policy', dest='policy_file',
				action='append',
				help='acl policy file name')

	arg_parser.add_argument('-P', '--policy-dir', dest='policy_dir',
				help='policy dir.')
	arg_parser.add_argument('-p8021x', '--private_dot1x_mode', dest='pvt_dot1x_mode',
				action='store_true', help=argparse.SUPPRESS)
	arg_parser.add_argument('-n', '--dry-run', dest='dry_run',
				action='store_true', help='dry run')
	arg_parser.add_argument('-N', '--numeric', dest='numeric',
				action='store_true', help='do not resolve hostnames')
	arg_parser.add_argument('-v', '--verbose', dest='verbose',
				action='store_true', help='verbose')
	arg_parser.add_argument('-x', '--exact', dest='exact',
				action='store_true', help='expand numbers (display exact values)')
	arg_parser.add_argument('-j', '--json', dest='json_output',
				action='store_true', help='output in JSON format')
	arg_parser.add_argument('-d', '--debug', dest='debug',
				action='store_true', help=argparse.SUPPRESS)
	arg_parser.add_argument('-q', '--quiet', dest='quiet',
				action='store_true', help=argparse.SUPPRESS)
	arg_parser.add_argument('-w', '--hw-wait', dest='hw_wait', type=int,
				help=argparse.SUPPRESS)
	arg_parser.add_argument('--no-hw-sync', dest='no_hw_sync',
				action='store_true', default=default_vx, help=argparse.SUPPRESS)
	arg_parser.add_argument('--last-good', dest='lastgood',
				action='store_true', help='install last good acl policies')
	arg_parser.add_argument('--log', dest='log',
				choices=['syslog'],
				help=argparse.SUPPRESS)

	argcomplete.autocomplete(arg_parser)

	# Parse command line arguments
	cmdline_args = arg_parser.parse_args()

	if cmdline_args.log != None:
		log_method = cmdline_args.log

	log_init()

	if cmdline_args.version == True:
		log('version: %s' %version)
		exit(0)

	if cmdline_args.policy_file and not cmdline_args.install:
		log_error('policy_file needed only with -i\n')
		arg_parser.print_help()
		exit(1)

	if cmdline_args.policy_dir and not cmdline_args.install:
		log_error('policy_dir needed only with -i\n')
		arg_parser.print_help()
		exit(1)

	if cmdline_args.dry_run and not cmdline_args.install:
		log_error('dry run only supported with -i\n')
		arg_parser.print_help()
		exit(1)

	if cmdline_args.lastgood and not cmdline_args.install:
		log_error('option last good is only supported with -i\n')
		arg_parser.print_help()
		exit(1)

	if cmdline_args.json_output and not cmdline_args.list:
		log_error('JSON output (-j) is only supported with list (-L) option\n')
		arg_parser.print_help()
		exit(1)

	if cmdline_args.exact:
		exact = True

	if cmdline_args.json_output:
		json_output = True

	if cmdline_args.verbose:
		verbose = True

	if cmdline_args.numeric:
		numeric = True

	if cmdline_args.debug:
		debug = 1
		verbose = True

	if cmdline_args.quiet:
		quiet = True

	if cmdline_args.no_hw_sync:
		if default_vx:
		   log_warn('Detected platform is Cumulus VX')

		log_warn('Running in no-hw-sync mode. No ' +
			 'rules will be programmed in hw')
		hw_sync = False

	if cmdline_args.hw_wait:
		hw_wait = cmdline_args.hw_wait

	if cmdline_args.hw_sync:
		# Send sync signal to hw
		ret = init()
		if ret != 0:
			log_error('init failed, aborting')
			exit(1)

		log_msg_begin('Sending hw sync signal')
		ret = hw_sync_commit()
		log_msg_end(ret)
		deinit()
		exit(ret)
	elif cmdline_args.zero_counters:
		log_msg_begin('Setting counters to zero')
		ret = set_counters(cmdline_args.zero_counters, 0)
		log_msg_end(ret)
		exit(ret)
	elif cmdline_args.list:
		try:
			ret = list_rules(cmdline_args.list)
		except IOError as e:
			if e.errno == errno.EPIPE:
				pass
			else:
				raise
		except:
			raise
		exit(ret)

	if (cmdline_args.policy_dir):
		policy_dir = cmdline_args.policy_dir
		if cmdline_args.pvt_dot1x_mode:
		    pvt_dot1x_mode = True
	elif (cmdline_args.lastgood):
		policy_dir = varrundir_lastgoodpolicyfiles
	else:
		policy_dir = None

	if policy_dir and not os.path.exists(policy_dir):
		log_error('Cannot find policy dir ' +
			policy_dir)
		exit(1)

	# Initialize
	ret = init()
	if ret != 0:
		log('init failed, aborting')
		deinit()
		exit(1)

	if cmdline_args.flush_type != None:
		log_msg_begin('Flushing rules')
		ret = flush_rules(cmdline_args.flush_type)
		log_msg_end(ret)
		deinit()
		exit(ret)

	if cmdline_args.policy_file:
		filesnotfound = [f for f in cmdline_args.policy_file
							if not os.path.exists(f)]
		if filesnotfound:
			log_error('Cannot find policy files: %s' %str(filesnotfound))
			deinit()
			exit(1)
		policy_files = cmdline_args.policy_file
		if cmdline_args.pvt_dot1x_mode:
		    pvt_dot1x_mode = True
	elif cmdline_args.lastgood:
		lastgood_conf = '%s/%s' %(varrundir_lastgoodpolicyfiles,
										policy_conf_name)
		if os.path.exists(lastgood_conf):
			# If lastgood conf exists, use that to read files and
			# variables. But note that the paths read from the conf
			# file has to now be relative to the lastgood dir
			get_policy_files_from_conf(lastgood_conf)
		else:
			# else read the files in sorted order
			get_policy_files_from_dir(policy_dir)
	elif policy_dir:
		get_policy_files_from_dir(policy_dir)
	else:
		# Get policy_files from conf file
		get_policy_files_from_conf(policy_conf)

	if not policy_files:
		log('no policy files found')
		deinit()
		exit(0)

	log_verbose('Installing policy files : %s' %(', '.join(policy_files)))
	fw_policies = read_policy_files()
	if not fw_policies:
		ret = 1

	if cmdline_args.dry_run:
		# We are all done, exit
		log_msg_end(ret)
		deinit()
		exit(ret)

	if ret:
		log_error('No acl policies to install, ... aborting')
		deinit()
		exit (1)

	num_rules = 0
	for f in fw_policies:
		num_rules = num_rules + len(f.get_rules())

	if num_rules == 0:
		log('No valid rules found, ... aborting')
		deinit()
		exit (0)

	log_verbose('Found %d acl policies' %len(fw_policies))

	if debug == True:
		log('Dumping processed acl policy ...')
		for p in fw_policies:
			p.dump()

	if cmdline_args.install == True:
		log_msg_begin('Installing acl policy')
		ret = install_policies(fw_policies)
		log_msg_end(ret)
		if ret == 0:
			save_installed_state(cmdline_args)

	deinit()

	exit(ret)
