Skip to content

Commit

Permalink
Vastly speed up large CiscoRange() operations
Browse files Browse the repository at this point in the history
  • Loading branch information
mpenning committed Nov 15, 2023
1 parent 9cefd21 commit 9bb420e
Show file tree
Hide file tree
Showing 5 changed files with 177 additions and 147 deletions.
95 changes: 69 additions & 26 deletions ciscoconfparse/ccp_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -3016,7 +3016,7 @@ def __init__(self, interface_name=None, interface_dict=None, debug=False):
if isinstance(interface_name, str):
if debug is True:
logger.info(f"CiscoIOSInterface(interface_name='{interface_name}') was called")
elif isinstance(interface_name, CiscoIOSInterface):
elif isinstance(interface_name, (CiscoIOSInterface, CiscoIOSXRInterface)):
if debug is True:
logger.info(f"CiscoIOSInterface(interface_name={interface_name}) {type(interface_name)} was called")
# Re-parse the
Expand Down Expand Up @@ -3051,7 +3051,7 @@ def __init__(self, interface_name=None, interface_dict=None, debug=False):
logger.critical(error)
raise InvalidCiscoInterface(error)
else:
error = f"Could not create a CiscoIOSInterface() instance"
error = f"Could not create a CiscoIOSInterface() or CiscoIOSXRInterface instance"
logger.critical(error)
raise InvalidCiscoInterface(error)

Expand Down Expand Up @@ -4855,7 +4855,7 @@ def __init__(self, text="", result_type=str, empty=False, default_iter_attr='por
raise InvalidCiscoRange(error)

# Ensure that result_type is in the set of valid_result_types...
valid_result_types = {str, int, float, None}
valid_result_types = {str, int, float, None, CiscoIOSInterface, CiscoIOSXRInterface}
if not result_type in valid_result_types:
error = f'CiscoRange(text="{text}", result_type={result_type}) [text: {type(text)}] is not a valid result_type. Choose from {valid_result_types}'
logger.critical(error)
Expand All @@ -4876,7 +4876,11 @@ def __init__(self, text="", result_type=str, empty=False, default_iter_attr='por
self.range_str = ""
if isinstance(text, str) or empty is False:
if result_type is None:
self._list = self.parse_cisco_interfaces(text, debug=debug)
self._list = self.parse_cisco_interfaces(text, result_type=CiscoIOSInterface, debug=debug)
elif result_type is CiscoIOSInterface:
self._list = self.parse_cisco_interfaces(text, result_type=CiscoIOSInterface, debug=debug)
elif result_type is CiscoIOSXRInterface:
self._list = self.parse_cisco_interfaces(text, result_type=CiscoIOSXRInterface, debug=debug)
elif result_type is int:
self._list = self.parse_integers(text, debug=debug)
elif result_type is float:
Expand Down Expand Up @@ -4939,7 +4943,7 @@ def parse_integers(self, text, debug=False):
def parse_strings(self, text, debug=False):
"""Parse text input to CiscoRange(), such as CiscoRange('1-5,7', result_type=None). '1-5,7 will be parsed. By default, CiscoIOSInterface() instances are used when CiscoRange(result_type=None) is parsed.' An error is raised if the CiscoRange() cannot be parsed"""
self.result_type = str
return self.parse_cisco_interfaces(text=text, debug=debug)
return self.parse_cisco_interfaces(text=text, result_type=self.result_type, debug=debug)

# This method is on CiscoRange()
@logger.catch(reraise=True)
Expand All @@ -4949,12 +4953,19 @@ def parse_floats(self, text, debug=False):

# This method is on CiscoRange()
@logger.catch(reraise=True)
def parse_cisco_interfaces(self, text, debug=False):
def parse_cisco_interfaces(self, text, result_type, debug=False):
"""Parse text input to CiscoRange(), such as CiscoRange('Eth1/1-5,7', result_type=None). 'Eth1/1-5,7 will be parsed. By default, CiscoIOSInterface() objects are used when CiscoRange(result_type=None) is parsed.' An error is raised if the CiscoRange() cannot be parsed"""
self.result_type = None
range_str = ""
expanded_interfaces = []
csv_parts = text.split(",")

if result_type is CiscoIOSXRInterface:
_result_type = CiscoIOSXRInterface
else:
# Default to CiscoIOSInterface for **everything else**
_result_type = CiscoIOSInterface

for idx, _csv_part in enumerate(csv_parts):

if debug is True:
Expand All @@ -4966,9 +4977,9 @@ def parse_cisco_interfaces(self, text, debug=False):
##################################################################
if idx == 0:
# Set the begin_obj...
begin_obj = CiscoIOSInterface(_csv_part.split("-")[0], debug=debug)
begin_obj = _result_type(_csv_part.split("-")[0])
self.begin_obj = begin_obj
self.this_obj = CiscoIOSInterface(interface_dict=begin_obj.as_dict())
self.this_obj = _result_type(interface_dict=begin_obj.as_dict(), debug=debug)
intf_dict = begin_obj.as_dict()

##############################################################
Expand All @@ -4984,7 +4995,7 @@ def parse_cisco_interfaces(self, text, debug=False):
# Rebuild begin_obj with the interface_class
if isinstance(_interface_class, str):
intf_dict["interface_class"] = _interface_class
begin_obj = CiscoIOSInterface(interface_dict=intf_dict)
begin_obj = _result_type(interface_dict=intf_dict)
self.begin_obj = begin_obj

##################################################################
Expand All @@ -5011,15 +5022,15 @@ def parse_cisco_interfaces(self, text, debug=False):

if idx > 0:
if self.iterate_attribute == 'channel' and isinstance(begin_obj.channel, int):
self.this_obj.channel = CiscoIOSInterface(_csv_part.split("-")[0].strip(), debug=debug).channel
self.this_obj.channel = _result_type(_csv_part.split("-")[0].strip(), debug=debug).channel
elif self.iterate_attribute == 'subinterface' and isinstance(begin_obj.subinterface, int):
self.this_obj.subinterface = CiscoIOSInterface(_csv_part.split("-")[0].strip(), debug=debug).subinterface
self.this_obj.subinterface = _result_type(_csv_part.split("-")[0].strip(), debug=debug).subinterface
elif self.iterate_attribute == 'port' and isinstance(begin_obj.port, int):
self.this_obj.port = CiscoIOSInterface(_csv_part.split("-")[0].strip(), debug=debug).port
self.this_obj.port = _result_type(_csv_part.split("-")[0].strip(), debug=debug).port
elif self.iterate_attribute == 'card' and isinstance(begin_obj.card, int):
self.this_obj.card = CiscoIOSInterface(_csv_part.split("-")[0].strip(), debug=debug).card
self.this_obj.card = _result_type(_csv_part.split("-")[0].strip(), debug=debug).card
elif self.iterate_attribute == 'slot' and isinstance(begin_obj.card, int):
self.this_obj.slot = CiscoIOSInterface(_csv_part.split("-")[0].strip(), debug=debug).slot
self.this_obj.slot = _result_type(_csv_part.split("-")[0].strip(), debug=debug).slot
else:
raise NotImplementedError()

Expand All @@ -5042,7 +5053,7 @@ def parse_cisco_interfaces(self, text, debug=False):
if "-" in _csv_part:
if len(_csv_part.split("-")) == 2:
# Append a whole range of interfaces...
obj = CiscoIOSInterface(_csv_part.split("-")[0].strip(), debug=debug)
obj = _result_type(_csv_part.split("-")[0].strip(), debug=debug)
begin_ordinal = getattr(obj, self.iterate_attribute)
# parse end_ordinal from 'Eth1/2-4 multipoint'
# ref: https://stackoverflow.com/a/1450900
Expand Down Expand Up @@ -5276,6 +5287,31 @@ def __setitem__(self, ii, val):
else:
return self._list[ii]

def __add__(self, other):
if isinstance(other, CiscoRange):
self._list.extend(other._list)
self._list = sorted(set(self._list))
return self
else:
error = f'`{other}` must be a CiscoRange() instance; the received argument was {type(other)} instead of a CiscoRange()'
logger.error(error)
raise InvalidCiscoRange(error)

def __sub__(self, other):
if isinstance(other, CiscoRange):
for ii in other._list:
try:
self._list.remove(ii)
except ValueError:
# Skip the item if it does not exist...
pass
return self
else:
error = f'`{other}` must be a CiscoRange() instance; the received argument was {type(other)} instead of a CiscoRange()'
logger.error(error)
raise InvalidCiscoRange(error)


# This method is on CiscoRange()
@logger.catch(reraise=True)
def attribute_sort(self, target_list=None, attribute="sort_list", reverse=False):
Expand All @@ -5291,8 +5327,8 @@ def attribute_sort(self, target_list=None, attribute="sort_list", reverse=False)
else:
if len(target_list) > 0:
if isinstance(target_list, list):
if isinstance(target_list[0], CiscoIOSInterface):
# Sort CiscoIOSInterface() members
if isinstance(target_list[0], (CiscoIOSInterface, CiscoIOSXRInterface)):
# Sort CiscoIOSInterface() or CiscoIOSXRInterface members
new_list = sorted(target_list, key=lambda x: getattr(x, attribute), reverse=reverse)
elif isinstance(target_list[0], (int, float)):
# Sort int or float members
Expand Down Expand Up @@ -5398,8 +5434,11 @@ def remove(self, arg, ignore_errors=False, debug=False):
logger.error(error)
raise MismatchedType(error)
elif result_type is None:
# These should be CiscoIOSInterface() types...
new_list = [ii for ii in list_before if ii != arg]
# These are CiscoIOSInterface() or CiscoIOSXRInterface()
new_list = [CiscoIOSInterface(ii) for ii in list_before if ii != arg]
elif isinstance(result_type, (CiscoIOSInterface, CiscoIOSXRInterface)):
# These are CiscoIOSInterface() or CiscoIOSXRInterface()
new_list = [result_type(ii) for ii in list_before if ii != arg]
else:
try:
new_list = [result_type(ii) for ii in list_before if result_type(ii) != result_type(arg)]
Expand Down Expand Up @@ -5461,14 +5500,16 @@ def as_list(self, result_type="auto"):
return set()
elif result_type is None:
return [CiscoIOSInterface(ii) for ii in retval]
elif isinstance(result_type, (CiscoIOSInterface, CiscoIOSXRInterface)):
return [result_type(ii) for ii in retval]
elif result_type is str:
return [str(ii) for ii in retval]
elif result_type is int:
return [int(ii) for ii in retval]
elif result_type is float:
return [float(ii) for ii in retval]
else:
error = f"CiscoRange().as_list(result_type={result_type}) is not valid. Choose from {['auto', None, int, str, float]}. result_type: None will return CiscoIOSInterface() objects."
error = f"CiscoRange().as_list(result_type={result_type}) is not valid. Choose from {['auto', None, int, str, float, CiscoIOSInterface, CiscoIOSXRInterface]}. result_type: None will return CiscoIOSInterface() objects."
logger.critical(error)
raise ValueError(error)
except AttributeError as eee:
Expand All @@ -5492,6 +5533,8 @@ def as_set(self, result_type="auto"):
return list()
elif result_type is None:
return set([CiscoIOSInterface(ii) for ii in retval])
elif isinstance(result_type, (CiscoIOSInterface, CiscoIOSXRInterface)):
return set([result_type(ii) for ii in retval])
elif result_type is str:
return set([str(ii) for ii in retval])
elif result_type is int:
Expand Down Expand Up @@ -5522,28 +5565,28 @@ def as_compressed_str(self, debug=False):
retval = list()

# Handle CiscoIOSInterface() instances...
if self.member_type is CiscoIOSInterface:
if isinstance(self.member_type, (CiscoIOSInterface, CiscoIOSXRInterface)):
# Build a magic attribute dict so we can intelligently prepend slot/card/port/etc...
magic_string = "3141592653591892234109876543212345678"
this_obj = CiscoIOSInterface(self.text.split(",")[0])
this_obj = self.member_type(self.text.split(",")[0])
magic_dict = this_obj.as_dict()
for attr_name in ("channel", "subinterface", "port", "card", "slot",):
if magic_dict[attr_name] is not None:
if attr_name == self.iterate_attribute:
magic_dict[attr_name] = int(magic_string)
break
if debug is True:
logger.info(f"CiscoRange() calling CiscoIOSInterface(interface_dict={magic_dict}).as_compressed_str()")
obj = CiscoIOSInterface(interface_dict=magic_dict, debug=debug)
logger.info(f"CiscoRange() calling {self.member_type}(interface_dict={magic_dict}).as_compressed_str()")
obj = self.member_type(interface_dict=magic_dict, debug=debug)
if debug is True:
logger.success(f" CiscoRange() call to CiscoIOSInterface().as_compressed_str() with `interface_dict` parameter succeeded")
logger.success(f" CiscoRange() call to {self.member_type}().as_compressed_str() with `interface_dict` parameter succeeded")
prefix_str = str(obj).replace(magic_string, "")
prefix_str_len = len(prefix_str)

# Build a list of the relevant string iteration pieces...
input_str = []
for _, component in enumerate(self.as_list()):
input_str.append(getattr(CiscoIOSInterface(component), self.iterate_attribute))
input_str.append(getattr(self.member_type(component), self.iterate_attribute))
# Handle str() instances...
elif self.member_type is str:
prefix_str = ""
Expand Down
21 changes: 7 additions & 14 deletions ciscoconfparse/ciscoconfparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,10 @@
"junos",
)

ALL_BRACE_SYNTAX = {
"junos",
}


@logger.catch(reraise=True)
def get_version_number():
Expand Down Expand Up @@ -1338,11 +1342,7 @@ def find_interface_objects(self, intfspec, exactmatch=True):
raise ValueError(err_text)

retval = list()
if self.syntax in (
"ios",
"nxos",
"iosxr",
):
if self.syntax not in ALL_BRACE_SYNTAX:
if exactmatch is True:
for obj in self.find_objects("^interface"):
if intfspec.lower() in obj.abbvs:
Expand Down Expand Up @@ -3661,14 +3661,7 @@ def sync_diff(
continue

if remove_lines is True and action == "remove":
if _syntax in set(
{
"ios",
"nxos",
"iosxr",
"asa",
}
):
if _syntax not in ALL_BRACE_SYNTAX:
uu = re.search(uncfgspec, command)
remove_cmd = indent * " " + "no " + uu.group(0).strip()
# 'no no ' will become ''...
Expand Down Expand Up @@ -5506,7 +5499,7 @@ def bootstrap_obj_init_ng(self, text_list=None, debug=0):
#
# Build the banner_re regexp... at this point ios
# and nxos share the same method...
if syntax=="ios" or syntax=="nxos" or syntax=="iosxr":
if syntax not in ALL_BRACE_SYNTAX:
banner_re = self._build_banner_re_ios()
self._banner_mark_regex(banner_re)

Expand Down
Loading

0 comments on commit 9bb420e

Please sign in to comment.