From 5acdeea0d1aae6af62281392705c7e8c57a29362 Mon Sep 17 00:00:00 2001 From: vprusakovs Date: Sun, 5 Mar 2023 20:01:03 +0200 Subject: [PATCH] 1.1.0 (2023-03-05) ------------------ * [new] ciscoconfparse --- CHANGELOG.rst | 5 + Pipfile | 1 + README.rst | 109 ++++++++++- examples/ccp.py | 77 ++++++++ examples/ip_pool.py | 76 ++++++++ fortigate_api/ccp.py | 283 +++++++++++++++++++++++++++ pyproject.toml | 7 +- requirements.txt | Bin 2258 -> 2306 bytes tests/ccp_/__init__.py | 0 tests/ccp_/helpers__ccp.py | 384 +++++++++++++++++++++++++++++++++++++ tests/ccp_/test__ccp.py | 208 ++++++++++++++++++++ 11 files changed, 1141 insertions(+), 9 deletions(-) create mode 100644 examples/ccp.py create mode 100644 examples/ip_pool.py create mode 100644 fortigate_api/ccp.py create mode 100644 tests/ccp_/__init__.py create mode 100644 tests/ccp_/helpers__ccp.py create mode 100644 tests/ccp_/test__ccp.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index fca22e6c..68a47cf6 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,11 @@ CHANGELOG ========= +1.1.0 (2023-03-05) +------------------ +* [new] ciscoconfparse + + 1.0.2 (2023-02-07) ------------------ * [fix] ccsrftoken for fortios v7 diff --git a/Pipfile b/Pipfile index 7267e1c0..e5413d5f 100644 --- a/Pipfile +++ b/Pipfile @@ -9,6 +9,7 @@ python_version = "3.8" [packages] requests = "==2.28.*" # 2.28.1 netmiko = "==4.1.*" # 4.1.2 +ciscoconfparse = "==1.7.*" # 1.7.18 [dev-packages] mypy = "==0.*" # 0.982 diff --git a/README.rst b/README.rst index b3ce11c9..7a8a7a9c 100644 --- a/README.rst +++ b/README.rst @@ -3,19 +3,31 @@ :target: https://pypi.python.org/pypi/fortigate-api .. image:: https://img.shields.io/pypi/pyversions/fortigate-api.svg :target: https://pypi.python.org/pypi/fortigate-api +.. image:: https://img.shields.io/github/last-commit/vladimirs-git/fortigate-api + :target: https://pypi.python.org/pypi/fortigate-api fortigate-api ============= Python package to configure Fortigate (Fortios) devices using REST API and SSH. -With this package, you can create, delete, get, update any objects in the Fortigate. -The most commonly used `Objects`_ are implemented in the `FortigateAPI`_ methods, but you can -manipulate any other objects that can be accessed through the REST API using the `Fortigate`_ -methods. You can also get and change the Fortigate configuration through SSH. +With this package, you can change objects in the Fortigate. The most commonly used `Objects`_ +are implemented in the `FortigateAPI`_ methods, but you can manipulate any other objects +that can be accessed through the REST API using the `Fortigate`_ methods. +You can also get and change the Fortigate configuration through SSH. + +Main features: + +- REST API to create, delete, get, update objects. Move policy before, after other policy. +- SSH Netmiko connector to work with CLI commands. +- CiscoConfParse to search and modify commands in config. +- Usage examples in `./examples`_ + +---------------------------------------------------------------------------------------------------- .. contents:: **Contents** :local: +---------------------------------------------------------------------------------------------------- Requirements ------------ @@ -36,7 +48,7 @@ or install the package from github.com release .. code:: bash - pip install https://github.com/vladimirs-git/fortigate-api/archive/refs/tags/1.0.2.tar.gz + pip install https://github.com/vladimirs-git/fortigate-api/archive/refs/tags/1.1.0.tar.gz or install the package from github.com repository @@ -45,6 +57,8 @@ or install the package from github.com repository pip install git+https://github.com/vladimirs-git/fortigate-api +---------------------------------------------------------------------------------------------------- + Objects ------- The objects implemented in `FortigateAPI`_. @@ -57,40 +71,73 @@ access to any other objects is available via `Fortigate`_. Object GUI and REST API URL to the object, FortiOS v6.4 =================== ================================================================================ `Address`_ https://hostname/ng/firewall/address + https://hostname/api/v2/cmdb/firewall/address/ + `AddressGroup`_ https://hostname/ng/firewall/address + https://hostname/api/v2/cmdb/firewall/addrgrp/ + `Antivirus`_ https://hostname/ng/utm/antivirus/profile + https://hostname/api/v2/cmdb/antivirus/profile/ + `Application`_ https://hostname/ng/utm/appctrl/sensor + https://hostname/api/v2/cmdb/application/list/ + `DhcpServer`_ https://hostname/ng/interface/edit/{name} + https://hostname/api/v2/cmdb/system.dhcp/server/ + `Interface`_ https://hostname/ng/interface + https://hostname/api/v2/cmdb/system/interface/ + `InternetService`_ https://hostname/ng/firewall/internet_service + https://hostname/api/v2/cmdb/firewall/internet-service/ + `IpPool`_ https://hostname/ng/firewall/ip-pool + https://hostname/api/v2/cmdb/firewall/ippool/ + `Policy`_ https://hostname/ng/firewall/policy/policy/standard + https://hostname/api/v2/cmdb/firewall/policy/ + `Schedule`_ https://hostname/ng/firewall/schedule + https://hostname/api/v2/cmdb/firewall.schedule/onetime/ + `Service`_ https://hostname/ng/firewall/service + https://hostname/api/v2/cmdb/firewall.service/custom/ + `ServiceCategory`_ https://hostname/ng/firewall/service + https://hostname/api/v2/cmdb/firewall.service/category/ + `ServiceGroup`_ https://hostname/ng/firewall/service + https://hostname/api/v2/cmdb/firewall.service/group/ + `SnmpCommunity`_ https://hostname/ng/system/snmp + https://hostname/api/v2/cmdb/system.snmp/community/ + `VirtualIp`_ https://hostname/ng/firewall/virtual-ip + https://hostname/api/v2/cmdb/firewall/vip/ + `Zone`_ https://hostname/ng/interface + https://hostname/api/v2/cmdb/system/zone/ =================== ================================================================================ +---------------------------------------------------------------------------------------------------- + FortigateAPI ------------ **FortigateAPI(host, username, password, scheme, port, timeout, vdom)** @@ -111,6 +158,7 @@ vdom *str* Name of virtual domain (default "root") =============== ======= ============================================================================ +---------------------------------------------------------------------------------------------------- Address ------- @@ -217,6 +265,7 @@ Address examples: `./examples/address.py`_ +---------------------------------------------------------------------------------------------------- AddressGroup ------------ @@ -321,6 +370,7 @@ AddressGroup examples: `./examples/address_group.py`_ +---------------------------------------------------------------------------------------------------- Antivirus --------- @@ -339,6 +389,7 @@ FortiOS v6.4 data example `./examples/yml/antivirus.yml`_ **FortigateAPI.antivirus.update(data, uid)** +---------------------------------------------------------------------------------------------------- Application ----------- @@ -357,6 +408,7 @@ FortiOS v6.4 data example `./examples/yml/application.yml`_ **FortigateAPI.antivirus.update(data, uid)** +---------------------------------------------------------------------------------------------------- DhcpServer ---------- @@ -377,6 +429,7 @@ FortiOS v6.4 data example `./examples/yml/dhcp_server.yml`_ DhcpServer examples `./examples/dhcp_server.py`_ +---------------------------------------------------------------------------------------------------- Interface --------- @@ -427,6 +480,7 @@ Interface examples: `./examples/interface.py`_ +---------------------------------------------------------------------------------------------------- InternetService --------------- @@ -445,6 +499,7 @@ FortiOS v6.4 data example `./examples/yml/internet_service.yml`_ **FortigateAPI.internet_service.update(data, uid)** +---------------------------------------------------------------------------------------------------- IpPool ------ @@ -463,6 +518,14 @@ FortiOS v6.4 data example `./examples/yml/ip_pool.yml`_ **FortigateAPI.ip_pool.update(data, uid)** +Examples +........ +IpPool examples: + +`./examples/ip_pool.py`_ + + +---------------------------------------------------------------------------------------------------- Policy ------ @@ -596,6 +659,7 @@ Policy Extended Filter examples: `./examples/policy_extended_filter.py`_ +---------------------------------------------------------------------------------------------------- Schedule -------- @@ -614,6 +678,7 @@ FortiOS v6.4 data example `./examples/yml/schedule.yml`_ **FortigateAPI.schedule.update(data, uid)** +---------------------------------------------------------------------------------------------------- Service ------- @@ -632,6 +697,7 @@ FortiOS v6.4 data example `./examples/yml/service.yml`_ **FortigateAPI.service.update(data, uid)** +---------------------------------------------------------------------------------------------------- ServiceCategory --------------- @@ -650,6 +716,7 @@ FortiOS v6.4 data example `./examples/yml/service_category.yml`_ **FortigateAPI.service_category.update(data, uid)** +---------------------------------------------------------------------------------------------------- ServiceGroup ------------ @@ -668,6 +735,7 @@ FortiOS v6.4 data example `./examples/yml/service_group.yml`_ **FortigateAPI.service_group.update(data, uid)** +---------------------------------------------------------------------------------------------------- SnmpCommunity ------------- @@ -702,6 +770,7 @@ Examples SnmpCommunity examples `./examples/snmp_community.py`_ +---------------------------------------------------------------------------------------------------- VirtualIP --------- @@ -720,6 +789,7 @@ FortiOS v6.4 data example `./examples/yml/virtual_ip.yml`_ **FortigateAPI.virtual_ip.update(data, uid)** +---------------------------------------------------------------------------------------------------- Zone ---- @@ -738,6 +808,7 @@ FortiOS v6.4 data example `./examples/yml/zone.yml`_ **FortigateAPI.zone.update(data, uid)** +---------------------------------------------------------------------------------------------------- Fortigate --------- @@ -857,6 +928,7 @@ Fortigate examples: `./examples/fortigate.py`_ +---------------------------------------------------------------------------------------------------- SSH --- @@ -925,7 +997,30 @@ SSH examples: `./examples/ssh.py`_ +---------------------------------------------------------------------------------------------------- + +CiscoConfParse +-------------- +Helper that parses the Fortigate configuration to find and modify command lines. +CiscoConfParse doesn't natively support Fortigate configuration, +but after some tweaking in this package it has become a good tool to play with Fortigate config lines. +For more information, see the documentation for the JunosCfgLine object at https://github.com/mpenning/ciscoconfparse + + +Examples +........ +CiscoConfParse examples: + +- get config from the Fortigate by SSH +- create CiscoConfParse object +- filter all JunosCfgLine objects of wan interfaces +- print some data in CiscoConfParse objects +- filter all wan interfaces blocks + +`./examples/ccp.py`_ + +.. _`./examples`: ./examples .. _`./examples/yml/address.yml`: ./examples/yml/address.yml .. _`./examples/yml/address_group.yml`: ./examples/yml/address_group.yml .. _`./examples/yml/antivirus.yml`: ./examples/yml/antivirus.yml @@ -945,10 +1040,12 @@ SSH examples: .. _`./examples/address.py`: ./examples/address.py .. _`./examples/address_group.py`: ./examples/address_group.py +.. _`./examples/ccp.py`: ./examples/ccp.py .. _`./examples/dhcp_server.py`: ./examples/dhcp_server.py .. _`./examples/fortigate.py`: ./examples/fortigate.py .. _`./examples/interface.py`: ./examples/interface.py +.. _`./examples/ip_pool.py`: ./examples/ip_pool.py .. _`./examples/policy.py`: ./examples/policy.py .. _`./examples/policy_extended_filter.py`: ./examples/policy_extended_filter.py .. _`./examples/snmp_community.py`: ./examples/snmp_community.py -.. _`./examples/ssh.py`: ./examples/ssh.py \ No newline at end of file +.. _`./examples/ssh.py`: ./examples/ssh.py diff --git a/examples/ccp.py b/examples/ccp.py new file mode 100644 index 00000000..f0e1b2a5 --- /dev/null +++ b/examples/ccp.py @@ -0,0 +1,77 @@ +"""CiscoConfParse examples +- get config from the Fortigate by SSH +- create CiscoConfParse object +- filter all JunosCfgLine objects of wan interfaces +- print some data in CiscoConfParse objects +- filter all wan interfaces blocks + +for more details see https://github.com/mpenning/ciscoconfparse +""" +from pprint import pprint + +from fortigate_api import FortigateAPI +from fortigate_api import ccp + +HOST = "host" +USERNAME = "username" +PASSWORD = "password" + +fgt = FortigateAPI(host=HOST, username=USERNAME, password=PASSWORD) +fgt.login() + +# get config from the Fortigate by SSH +config = fgt.ssh.send_command("show system interface") +print(config) +# config system interface +# edit "wan1" +# set vdom "root" +# set mode dhcp +# set status down +# set type physical +# set role wan +# set snmp-index 3 +# next +# edit "wan2" +# set vdom "root" +# set mode dhcp +# set status down +# set type physical +# set role wan +# set snmp-index 4 +# next +# end + +# create CiscoConfParse object +# filter all JunosCfgLine objects of wan interfaces +parser = ccp.FgtConfParse(config=config) +interfaces = ccp.find_by_re_keys(ccp=parser, keys=["config system interface", r"edit .wan\d"]) +pprint(interfaces) +# [, +# ] + +# print some data in CiscoConfParse objects +for interface in interfaces: + print(interface.text) + print([o.text for o in interface.all_children if o.text.find("snmp-index")][0]) +# edit "wan1" +# set vdom "root" +# edit "wan2" +# set vdom "root" + +# filter all wan interfaces blocks +blocks = ccp.find_re_blocks(ccp=parser, regex=r"edit .wan\d") +pprint(blocks) +# [' edit "wan1"\n' +# ' set vdom "root"\n' +# ' set mode dhcp\n' +# ' set status down\n' +# ' set type physical\n' +# ' set role wan\n' +# ' set snmp-index 3', +# ' edit "wan2"\n' +# ' set vdom "root"\n' +# ' set mode dhcp\n' +# ' set status down\n' +# ' set type physical\n' +# ' set role wan\n' +# ' set snmp-index 4'] diff --git a/examples/ip_pool.py b/examples/ip_pool.py new file mode 100644 index 00000000..89ec00ce --- /dev/null +++ b/examples/ip_pool.py @@ -0,0 +1,76 @@ +"""IP-Pool examples +""" + +from pprint import pprint + +from fortigate_api import FortigateAPI + +HOST = "host" +USERNAME = "username" +PASSWORD = "password" + +fgt = FortigateAPI(host=HOST, username=USERNAME, password=PASSWORD) +fgt.login() + +print("\nGets all ip-pool in vdom \"root\" from the Fortigate") +ip_pools = fgt.ip_pool.get() +pprint(ip_pools) +# [{'arp-intf': '', +# 'arp-reply': 'enable', +# 'associated-interface': '', +# 'block-size': 128, +# 'comments': '', +# 'endip': '10.0.0.1', +# 'name': 'NAT-Source-01', +# 'num-blocks-per-user': 8, +# 'pba-timeout': 30, +# 'permit-any-host': 'disable', +# 'q_origin_key': 'NAT-Source-01', +# 'source-endip': '0.0.0.0', +# 'source-startip': '0.0.0.0', +# 'startip': '10.0.0.1', +# 'type': 'overload'}, +# ... + + +print("\nGets filtered ip_pools by name (unique identifier)") +ip_pools = fgt.ip_pool.get(uid="NAT-Source-01") +pprint(ip_pools) +# [{'arp-intf': '', +# 'arp-reply': 'enable', +# 'associated-interface': '', +# 'block-size': 128, +# 'comments': '', +# 'endip': '10.0.0.1', +# 'name': 'NAT-Source-01', +# 'num-blocks-per-user': 8, +# 'pba-timeout': 30, +# 'permit-any-host': 'disable', +# 'q_origin_key': 'NAT-Source-01', +# 'source-endip': '0.0.0.0', +# 'source-startip': '0.0.0.0', +# 'startip': '10.0.0.1', +# 'type': 'overload'}] + +print("\nFilters ip_pools by operator equals \"==\"") +ip_pools = fgt.ip_pool.get(filter="name==NAT-Source-01") +print(f"ip_pools count={len(ip_pools)}") # ip_pools count=1 + +print("\nFilters ip_pools by operator contains \"=@\"") +ip_pools = fgt.ip_pool.get(filter="name=@NAT-") +print(f"ip_pools count={len(ip_pools)}") # ip_pools count=5 + +print("\nFilters ip_pools by multiple conditions") +ip_pools = fgt.ip_pool.get(filter=["name=@NAT-", "endip=@10.0.0."]) +print(f"ip_pools count={len(ip_pools)}") # ip_pools count=2 + +print("\nUpdates ip_pool data in the Fortigate") +data = dict(name="NAT-Source-01", comments="description") +response = fgt.ip_pool.update(uid="NAT-Source-01", data=data) +print("ip_pool.update", response) # ip_pool.update + +print("\nChecks for presence of ip_pool in the Fortigate") +response = fgt.ip_pool.is_exist(uid="NAT-Source-01") +print("ip_pool.is_exist", response) # ip_pool.is_exist True + +fgt.logout() diff --git a/fortigate_api/ccp.py b/fortigate_api/ccp.py new file mode 100644 index 00000000..f6c2a7bc --- /dev/null +++ b/fortigate_api/ccp.py @@ -0,0 +1,283 @@ +"""CiscoConfParse +Helper that parses the Fortigate config to find and change config commands. +For more details see https://github.com/mpenning/ciscoconfparse +""" +import re +from typing import List + +from ciscoconfparse import CiscoConfParse, JunosCfgLine # type: ignore + +from fortigate_api import str_ +from fortigate_api.types_ import LStr, T2Str, T3Str, UStr + +LJunosCfgLine = List[JunosCfgLine] + + +class FgtConfParse(CiscoConfParse): + """CiscoConfParse adapted for Fortigate""" + + def __init__(self, config: UStr, comment="#", encoding="UTF-8", **kwargs): + """FgtConfParse + :: + :param config: Fortigate config + :type config: str, List[str] + :param comment: Comment delimiter (default "#") + :type comment: str + :param kwargs: Other CiscoConfParse parameters + :return: CiscoConfParse object with JunosCfgLine lines + """ + config_: str = self._init_config(config) + config_ = convert_fgt_to_junos(config_) + lines = config_.splitlines() + kwargs.update(config=lines, comment=comment, encoding=encoding, syntax="junos") + super().__init__(**kwargs) + + @staticmethod + def _init_config(config: UStr) -> str: + """Init Fortigate config""" + if isinstance(config, str): + return config + if not isinstance(config, (list, tuple)): + raise TypeError(f"{config=} {str} expected") + for line in config: + if not isinstance(line, str): + raise TypeError(f"{line=} {str} expected") + return "\n".join(config) + + +# ============================= function ============================= + +def convert_fgt_to_junos(config: str) -> str: + """Convert Fortigate commands to Junos syntax, to parse by ciscoconfparse + :param config: Fortigate syntax config + :return config: CiscoConfParse junos syntax config + :example: + config = " + config system interface + edit "mgmt" + set ip 10.0.0.1 255.255.255.0 + next + end + " + convert_fgt_to_junos(config) -> " + config system interface { + edit "mgmt" { + set ip 10.0.0.1 255.255.255.0 + } + next + } + end + " + """ + spaces = r"^(\s+)?" + re_config = f"({spaces}config .+)" + re_end = f"{spaces}(end)" + re_edit = f"({spaces}edit .+)" + re_next = f"{spaces}(next)" + re_comment = f"$|{spaces}#" + + fgt_lines: LStr = [s.rstrip() for s in config.splitlines()] + fgt_lines = [s for s in fgt_lines if not re.match(re_comment, s)] + + junos_lines: LStr = [] + for line in fgt_lines: + # config + if re.match(re_config, line): + line += " {" + # end + elif re.match(re_end, line): + result = re.findall(re_end, line) + indent, end_ = result[0][:2] + line = indent + "}\n" + indent + "end" + # edit + elif re.match(re_edit, line): + line += " {" + # next + elif re.match(re_next, line): + result = re.findall(re_next, line) + indent, next_ = result[0][:2] + line = indent + "}\n" + indent + "next" + junos_lines.append(line) + + return "\n".join(junos_lines) + + +def findall(regex: str, obj: JunosCfgLine, flags=0) -> LStr: + """Parses substrings from *JunosCfgLine* objects by regex + :: + :param regex: Regex pattern with 1 group + :param obj: JunosCfgLine object + :param flags: re.findall flags + :return: List of interested substrings + """ + string = "\n".join(obj.ioscfg) + values = re.findall(pattern=regex, string=string, flags=flags) + return values + + +def findall1(regex: str, obj: JunosCfgLine, flags=0) -> str: + """Parses substring from *JunosCfgLine* objects by regex + :: + :param regex: Regex pattern with 1 group + :param obj: JunosCfgLine object + :param flags: re.findall flags + :return: Interested substring + """ + value = "" + for item in obj.ioscfg: + if value := str_.findall1(pattern=regex, string=item, flags=flags): + break + return value + + +def findall2(regex: str, obj: JunosCfgLine, flags=0) -> T2Str: + """Parses 2 substrings from *JunosCfgLine* objects by regex + :: + :param regex: Regex pattern with 2 groups + :param obj: JunosCfgLine object + :param flags: re.findall flags + :return: Interested substrings + """ + value1, value2 = "", "" + for item in obj.ioscfg: + value1, value2 = str_.findall2(pattern=regex, string=item, flags=flags) + if value1: + break + return value1, value2 + + +def findall3(regex: str, obj: JunosCfgLine) -> T3Str: + """Parses 3 substrings from *JunosCfgLine* objects by regex + :: + :param regex: Regex pattern with 3 groups + :param obj: JunosCfgLine object + :return: Interested substrings + """ + value1, value2, value3 = "", "", "" + for item in obj.ioscfg: + value1, value2, value3 = str_.findall3(pattern=regex, string=item) + if value1: + break + return value1, value2, value3 + + +def find_by_keys(ccp: CiscoConfParse, keys: LStr) -> LJunosCfgLine: + """Finds object by keys in geneology + :: + :param ccp: CiscoConfParse object + :param keys: Geneology keys to find object + :return: List of JunosCfgLine objects + :rtype: List[JunosCfgLine] + :example: + keys = ["config system global", "edit \"wan1\""] + find_by_keys(ccp, keys) -> [] + """ + if not keys: + return [] + *keys_parent, key_last = keys + spaces = r"^(\s+)?" + regex = f"{spaces}{key_last}$" + + objs_w_child: LJunosCfgLine = [] + for obj_ in ccp.find_objects(regex): + if isinstance(obj_, JunosCfgLine): + objs_w_child.append(obj_) + + results: LJunosCfgLine = [] + for obj in objs_w_child: + all_parents: LStr = [o.text.strip() for o in obj.all_parents] + if keys_parent == all_parents: + results.append(obj) + return results + + +def find_by_re_keys(ccp: CiscoConfParse, keys: LStr) -> LJunosCfgLine: + """Finds object by keys regex in geneology + :: + :param ccp: CiscoConfParse object + :param keys: Geneology regex keys to find object + :return: List of JunosCfgLine objects + :rtype: List[JunosCfgLine] + :example: + keys = ["config system global", r"edit .wan[12]."] + find_by_keys(ccp, keys) -> [, + ] + """ + if not keys: + return [] + *re_parents, re_last = keys + + keys_last = [] + for line in ccp.ioscfg: + if re.search(re_last, line): + if line not in keys_last: + keys_last.append(line) + + objs_w_child: LJunosCfgLine = [] + for key_last in keys_last: + for obj_ in ccp.find_objects(key_last): + if isinstance(obj_, JunosCfgLine): + objs_w_child.append(obj_) + + results: LJunosCfgLine = [] + for obj in objs_w_child: + all_parents: LStr = [o.text for o in obj.all_parents] + if len(re_parents) == len(all_parents): + if not re_parents: + results.append(obj) + continue + for idx, re_parent in enumerate(re_parents): + parent = all_parents[idx] + if re.search(re_parent, parent): + results.append(obj) + break + return results + + +def find_children(ccp: CiscoConfParse, keys: LStr) -> LJunosCfgLine: + """Finds children object by geneology keys + :: + :param ccp: CiscoConfParse object + :param keys: Geneology keys to find object + :return: List of children JunosCfgLine objects + :rtype: List[JunosCfgLine] + :example: + keys = ["config system global", "edit \"wan1\""] + find_children(ccp, keys) -> ["set vdom \"root\"", + "set ip 10.0.1.1 255.255.255.0", + ...] + """ + return [o for oo in find_by_keys(ccp, keys) for o in oo.children] + + +def join_children(obj: JunosCfgLine) -> str: + """Join all children of JunosCfgLine object + :: + :param obj: JunosCfgLine object + :return: joined text lines of all children + :rtype: str + """ + lines = [o.text for o in obj.all_children] + return "\n".join(lines) + + +def find_re_blocks(ccp: CiscoConfParse, regex: str) -> LStr: + """Works similar to CiscoConfParse.find_block(), but returns list of config sections" + :: + :param ccp: CiscoConfParse object + :param regex: Regex pattern to find + :return: Lines of all_children in found block + :example: + regex = "edit \"wan1\"" + find_children(ccp, keys) -> ["set vdom \"root\"", + "set ip 10.0.1.1 255.255.255.0", + ...] + """ + blocks: LStr = [] + parents: LJunosCfgLine = ccp.find_objects(regex) + for parent in parents: + all_children: LStr = [o.text for o in parent.all_children] + all_children.insert(0, parent.text) + block = "\n".join(all_children) + blocks.append(block) + return blocks diff --git a/pyproject.toml b/pyproject.toml index 0f2e7b0c..83a6fcc0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "fortigate_api" -version = "1.0.2" +version = "1.1.0" authors = [ { name="Vladimir Prusakov", email="vladimir.prusakovs@gmail.com" }, ] @@ -9,8 +9,9 @@ readme = "README.rst" license = { text="MIT" } requires-python = ">=3.8" dependencies = [ - "requests", + "requests ~= 2.28", "netmiko ~= 4.1", + "ciscoconfparse ~= 1.7", ] keywords = ["fortigate", "api", "fortios", "firewall", "networking", "telecommunication"] classifiers = [ @@ -30,7 +31,7 @@ classifiers = [ "Homepage" = "https://github.com/vladimirs-git/fortigate-api" "Repository" = "https://github.com/vladimirs-git/fortigate-api" "Bug Tracker" = "https://github.com/vladimirs-git/fortigate-api/issues" -"Download URL" = "https://github.com/vladimirs-git/fortigate-api/archive/refs/tags/1.0.2.tar.gz" +"Download URL" = "https://github.com/vladimirs-git/fortigate-api/archive/refs/tags/1.1.0.tar.gz" [tool.setuptools.packages.find] include = ["fortigate_api"] [tool.setuptools.package-data] diff --git a/requirements.txt b/requirements.txt index 5211360d3eecc45b15dec1c18cd697683136c2b5..8813ad4156a15165fd05d8f259ba5a9c3435bfa0 100644 GIT binary patch delta 56 zcmca4*d(;!5{E%DLncEpLo!1?80RshF%&Q)G86&DQW") + + def test_valid__convert_fgt_to_junos(self): + """ccp.convert_fgt_to_junos()""" + for config, expected in [ + ("", ""), + (CONFIG_FGT_DUMMY, CONFIG_JFGT_DUMMY), + (CONFIG_FGT, CONFIG_JFGT), + ]: + actual = ccp.convert_fgt_to_junos(config=config) + self.assertEqual(expected, actual, msg=f"{config=}") + + def test_valid__findall(self): + """ccp.findall()""" + lines = CONFIG_FGT_DUMMY.splitlines() + ccp_o = ccp.FgtConfParse(config=lines) + obj = ccp_o.ConfigObjs.all_parents[3] + + for regex, expected in [ + ("(typo)", []), + (r"edit (E\d)", ["E1", "E2"]), + ]: + actual = ccp.findall(regex=regex, obj=obj) + self.assertEqual(expected, actual, msg=f"{regex=}") + + def test_valid__findall1(self): + """ccp.findall1()""" + lines = CONFIG_FGT_DUMMY.splitlines() + ccp_o = ccp.FgtConfParse(config=lines) + obj = ccp_o.ConfigObjs.all_parents[3] + + for regex, expected in [ + ("(typo)", ""), + (r"edit (E\d)", "E1"), + ]: + actual = ccp.findall1(regex=regex, obj=obj) + self.assertEqual(expected, actual, msg=f"{regex=}") + + def test_valid__findall2(self): + """ccp.findall2()""" + lines = CONFIG_FGT_DUMMY.splitlines() + ccp_o = ccp.FgtConfParse(config=lines) + obj = ccp_o.ConfigObjs.all_parents[3] + + for regex, expected in [ + ("(typo)(typo)", ("", "")), + (r"edit (E)(\d)", ("E", "1")), + ]: + actual = ccp.findall2(regex=regex, obj=obj) + self.assertEqual(expected, actual, msg=f"{regex=}") + + def test_valid__findall3(self): + """ccp.findall3()""" + lines = CONFIG_FGT_DUMMY.splitlines() + ccp_o = ccp.FgtConfParse(config=lines) + obj = ccp_o.ConfigObjs.all_parents[3] + + for regex, expected in [ + ("(typo)(typo)(typo)", ("", "", "")), + (r"(edit)\s+(E)(\d)", ("edit", "E", "1")), + ]: + actual = ccp.findall3(regex=regex, obj=obj) + self.assertEqual(expected, actual, msg=f"{regex=}") + + def test_valid__find_by_keys__junos(self): + """ccp.find_by_keys() for junos""" + lines = CONFIG_JUNOS.splitlines() + ccp_o = ccp.CiscoConfParse(config=lines, comment="#", syntax="junos") + + for keys, exp_parents, expected in [ + ([], [], []), + (["A1"], [], ["A1"]), + (["A2"], [], []), + (["A1", "B1"], ["A1"], ["B1"]), + (["A1", "B2"], ["A1"], ["B2"]), + (["A1", "B1", "C1"], ["A1", "B1"], ["C1"]), + (["A1", "B1", "C2"], ["A1", "B1"], ["C2"]), + (["A1", "B1", "C1", "D2"], ["A1", "B1", "C1"], ["D2"]), + ]: + actual_lo = ccp.find_by_keys(ccp=ccp_o, keys=keys) + + actual_parents = [o.text.strip() for lo in actual_lo for o in lo.all_parents] + self.assertEqual(exp_parents, actual_parents, msg=f"{keys=}") + actual = [o.text.strip() for o in actual_lo] + self.assertEqual(expected, actual, msg=f"{keys=}") + + def test_valid__find_by_keys__fgt(self): + """ccp.find_by_keys() for Fortigate""" + ccp_o = ccp.FgtConfParse(config=CONFIG_FGT_DUMMY) + + for keys, exp_parents, expected in [ + ([], [], []), + (["config system A1"], [], ["config system A1"]), + (["config system A2"], [], ["config system A2"]), + (["config system typo"], [], []), + (["config system A1", "set value B1"], ["config system A1"], ["set value B1"]), + (["config system A2", "edit \"B1\""], ["config system A2"], ["edit \"B1\""]), + (["config system A2", "edit \"B1\"", "set value \"C1\""], + ["config system A2", "edit \"B1\""], ["set value \"C1\""]), + (["config system A3", "edit B1", "config D-1", "edit E2"], + ["config system A3", "edit B1", "config D-1"], ["edit E2"]), + ]: + objs = ccp.find_by_keys(ccp=ccp_o, keys=keys) + actual = [o.text.strip() for lo in objs for o in lo.all_parents] + self.assertEqual(exp_parents, actual, msg=f"{keys=}") + actual = [o.text.strip() for o in objs] + self.assertEqual(expected, actual, msg=f"{keys=}") + + def test_valid__find_children(self): + """ccp.find_children()""" + ccp_o = ccp.FgtConfParse(config=CONFIG_FGT_DUMMY) + + for keys, expected in [ + ([], []), + (["config system A1"], + ["set value B1", "set buffer \"", "line B2 1", "line B2 2", "\""]), + (["config system A2", "set value B1"], []), + (["config system A2", "set buffer \""], []), + (["config system A2", "edit \"B1\""], ["set value \"C1\""]), + (["config system A2", "edit \"B1\"", "set value \"C1\""], []), + (["config system A3", "edit B1", "config D-1", "edit E2"], ["set value F1"]), + ]: + objs = ccp.find_children(ccp=ccp_o, keys=keys) + actual = [o.text.strip() for o in objs] + self.assertEqual(expected, actual, msg=f"{keys=}") + + def test_valid__join_children(self): + """ccp.join_children()""" + lines = CONFIG_FGT_DUMMY.splitlines() + ccp_o = ccp.FgtConfParse(config=lines) + parent1 = ccp_o.ConfigObjs.all_parents[1] + parent2 = ccp_o.ConfigObjs.all_parents[2] + for parent, expected in [ + (parent1, JOINED1), + (parent2, JOINED2), + ]: + actual = ccp.join_children(obj=parent) + self.assertEqual(expected, actual, msg=f"{parent=}") + + def test_valid__find_re_blocks(self): + """ccp.find_re_blocks()""" + lines = CONFIG_FGT_DUMMY.splitlines() + ccp_o = ccp.FgtConfParse(config=lines) + + for regex, expected in [ + ("config system A[12]", [BLOCK_A1, BLOCK_A2]), + ("edit E2", [BLOCK_E2]), + ]: + actual = ccp.find_re_blocks(ccp=ccp_o, regex=regex) + self.assertEqual(expected, actual, msg=f"{regex=}") + + def test_valid__find_by_re_keys(self): + """ccp.find_by_re_keys() for Fortigate""" + ccp_o = ccp.FgtConfParse(config=CONFIG_FGT_DUMMY) + + for keys, exp_parents, expected in [ + ([], [], []), + (["config system A[1]"], [], ["config system A1"]), + (["config system A[12]"], [], ["config system A1", "config system A2"]), + ([r"config system A\d", r"set value B\d"], ["config system A1"], ["set value B1"]), + ([r"config system A\d", r"edit B\d", r"config D.+", r"edit E\d"], + ["config system A3", "edit B1", "config D-1", + "config system A3", "edit B1", "config D-1"], ["edit E1", "edit E2"]), + ]: + objs = ccp.find_by_re_keys(ccp=ccp_o, keys=keys) + actual = [o.text.strip() for lo in objs for o in lo.all_parents] + self.assertEqual(exp_parents, actual, msg=f"{keys=}") + actual = [o.text.strip() for o in objs] + self.assertEqual(expected, actual, msg=f"{keys=}") + + +if __name__ == "__main__": + unittest.main()