Loading lncc.py +97 −33 Original line number Diff line number Diff line #!/usr/bin/env python3 """Lightweight NETCONF Controlleur (LNCC) """Lightweight NETCONF Controller (LNCC) Emulates a real SDN Controlleur by pushing NETCONF operations, ... License ======= Not yet OpenSource licensed, it's still on Orange property as of May 6th 2022 TODO(Jean): Working with legal to make it OpenSource, probably BSD-3-Clause Software Name : Lightweight NETCONF Controlleur Software Name : Lightweight NETCONF Controller (LNCC) Version: 0.3.0 SPDX-FileCopyrightText: Copyright (c) 2022 Jean Rebiffé, Orange Innovation Networks SPDX-License-Identifier: BSD-3-Clause SPDX-FileType: SOURCE SPDX-FileContributor: Jean Rebiffe, Orange Innovation Networks This software is confidential and proprietary information of Orange. You shall not disclose such Confidential Information and shall use it only in accordance with the terms of the agreement you entered into. Unauthorized copying of this file, via any medium is strictly prohibited. If you are Orange employee you shall use this software in accordance with the Orange Source Charter (http://infoportal-opensource.innov.intraorange/en/orange-source-charter/) This software is distributed under the BSD 3-Clause "New" or "Revised" License, the text of which is available at https://opensource.org/licenses/BSD-3-Clause or see the "license.txt" file for more details. Author: Jean Rebiffe <jean.rebiffe@orange.com> Author: Jean Rebiffe, Orange Innovation Networks Software description: Emulates a real SDN Controlleur by pushing NETCONF operations, ... """ __version__ = "0.2" __version__ = "0.3.0" __author__ = "Jean Rebiffe, Orange Innovation Networks, 2022" import argparse Loading Loading @@ -52,8 +50,11 @@ class TemplateError(LnccError): pass # TODO(Jean): Should be keep unsafe_hash=True? @dataclass # (unsafe_hash=True) # https://datatracker.ietf.org/doc/html/rfc8342#section-5.1 DATASTORES = {"running", "candidate", "startup", "intended"} @dataclass class NetworkElement: """Represent a single Network Element managable with NETCONF Loading Loading @@ -90,9 +91,12 @@ class NetworkElement: LOGGER.debug("Start: NETCONF connect() to %s", self.name) try: self.conn = manager.connect_ssh(**self.params, device_params={ "name": "huaweiyang" }) hostkey_verify=False) # TODO(Jean): fix vendor-specific hook - needed on my lab # self.conn = manager.connect_ssh(**self.params, # device_params={ # "name": "huaweiyang" # }) except ncclient.NCClientError as err: LOGGER.warning("Failed: NETCONF connect() to %s: Reason: %s", self.name, err) Loading Loading @@ -122,6 +126,8 @@ class NetworkElement: try: self.replies[reply] = func(**kwargs) # print(self.replies[reply].data_ele) # print(vars(self.replies[reply].data_ele)) except ncclient.NCClientError as err: LOGGER.warning( "Failed: NETCONF <%s> on %s. Reason: %s", Loading Loading @@ -244,6 +250,7 @@ class NetworkElementGroup(collections.UserDict): # TODO(Jean): Refactoring needed? tables = list() for name, state in self.items(): # print(state) tmp_tb = state.tables[table].copy() tmp_tb["origin"] = name tables.append(tmp_tb) Loading Loading @@ -276,6 +283,9 @@ class LnccCli(cmd2.Cmd): tables: Dict[str, pandas.DataFrame] = dict() logging_config = None intro = ("Welcome to the Lightweight NETCONF Controller lncc.py\n" "Type 'help' for more information") def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) logg = cmd2.Settable( Loading Loading @@ -526,27 +536,42 @@ class LnccCli(cmd2.Cmd): nargs="*", required=True) _nc_edit_parser.add_argument("--format") _nc_edit_parser.add_argument("--target") _nc_edit_parser.add_argument("--default-operation") _nc_edit_parser.add_argument("--test-option") _nc_edit_parser.add_argument("--error-option") _nc_edit_parser.add_argument( "--target", choices=DATASTORES, help="Name of the configuration datastore being edited", ) _nc_edit_parser.add_argument("--default-operation", choices={"merge", "replace", "none"}) _nc_edit_parser.add_argument( "--test-option", choices={"test-then-set", "set", "test-only"}, help="NE checks a complete configuration for syntactical " "and semantic errors before applying the configuration", ) _nc_edit_parser.add_argument( "--error-option", choices={"stop-on-error", "continue-on-error", "rollback-on-error"}, ) _nc_edit_parser.set_defaults(operation="edit-config") _nc_copy_parser = _netconf_subparsers.add_parser("copy-config") _nc_copy_parser.add_argument("--target", required=True) _nc_copy_parser.add_argument("--source", required=True) _nc_copy_parser.add_argument("--target", required=True, choices=DATASTORES) _nc_copy_parser.add_argument("--source", required=True, choices=DATASTORES) _nc_copy_parser.set_defaults(operation="copy-config") _nc_delete_parser = _netconf_subparsers.add_parser("delete-config") _nc_delete_parser.add_argument("--target", required=True) _nc_delete_parser.add_argument("--target", required=True, choices=DATASTORES) _nc_delete_parser.set_defaults(operation="delete-config") _nc_lock_parser = _netconf_subparsers.add_parser("lock") _nc_lock_parser.add_argument("--target") _nc_lock_parser.add_argument("--target", choices=DATASTORES) _nc_lock_parser.set_defaults(operation="lock") _nc_unlock_parser = _netconf_subparsers.add_parser("unlock") _nc_unlock_parser.add_argument("--target") _nc_unlock_parser.add_argument("--target", choices=DATASTORES) _nc_unlock_parser.set_defaults(operation="unlock") _nc_close_parser = _netconf_subparsers.add_parser("close-session") Loading @@ -572,7 +597,7 @@ class LnccCli(cmd2.Cmd): _nc_cancel_commit_parser.set_defaults(operation="cancel-commit") _nc_validate_parser = _netconf_subparsers.add_parser("validate") _nc_validate_parser.add_argument("--target") _nc_validate_parser.add_argument("--target", choices=DATASTORES) _nc_validate_parser.set_defaults(operation="validate") @cmd2.with_argparser(netconf_parser) Loading Loading @@ -634,8 +659,7 @@ class LnccCli(cmd2.Cmd): def table_concat(self, args: argparse.Namespace) -> None: assert args.table kwargs = {"table": args.table} self.nes.table_concat(**kwargs) self.nes.table_concat(table=args.table) self.pfeedback(f"Table '{args.table}' available for group") def table_to_excel(self, args: argparse.Namespace) -> None: Loading @@ -645,7 +669,7 @@ class LnccCli(cmd2.Cmd): with args.file: self.nes.table_to_excel(args.file, args.tables) self.pfeedback(f"Wrote Excel file {args.file.name}, " f"with {len(args.tables)} tabs: args.tables") f"with {len(args.tables)} tabs: {args.tables}") table_parser = cmd2.Cmd2ArgumentParser() _table_subparsers = table_parser.add_subparsers(title="subcommands") Loading Loading @@ -687,6 +711,46 @@ class LnccCli(cmd2.Cmd): self.pfeedback(f"Sleeping for {args.seconds} seconds") time.sleep(args.seconds) write_parser = cmd2.Cmd2ArgumentParser() write_parser.add_argument("--table", default="get") write_parser.add_argument("--reply", metavar="REPLY-NAME") write_parser.add_argument("--xsl-transform", type=argparse.FileType("r")) write_parser.add_argument("--file", metavar="EXCEL-FILE", type=argparse.FileType("wb")) @cmd2.with_argparser(write_parser) @cmd2.with_category("Input & output handling") def do_write(self, args: argparse.Namespace): assert hasattr(args, "table") assert args.table kwargs = {"table": args.table} kwargs["reply"] = args.reply or args.table if args.xsl_transform: with args.xsl_transform: kwargs["xsl_transform"] = args.xsl_transform.read() self.nes.table_from_xml(**kwargs) try: self.nes.table_concat(table=args.table) except ValueError as err: self.perror(f"Cannot concat tables {err}") self.pwarning("Did you forget the --xsl-transform?") return if args.file: with args.file: self.nes.table_to_excel(args.file, [args.table]) self.pfeedback(f"Wrote Excel file {args.file.name}, " f"with tab: {args.table}") else: self.poutput(self.nes.tables[args.table]) self.pfeedback(f"Displayed {kwargs['reply']}") def main(): app = LnccCli(persistent_history_file=".lncc-history.json.xz", Loading Loading
lncc.py +97 −33 Original line number Diff line number Diff line #!/usr/bin/env python3 """Lightweight NETCONF Controlleur (LNCC) """Lightweight NETCONF Controller (LNCC) Emulates a real SDN Controlleur by pushing NETCONF operations, ... License ======= Not yet OpenSource licensed, it's still on Orange property as of May 6th 2022 TODO(Jean): Working with legal to make it OpenSource, probably BSD-3-Clause Software Name : Lightweight NETCONF Controlleur Software Name : Lightweight NETCONF Controller (LNCC) Version: 0.3.0 SPDX-FileCopyrightText: Copyright (c) 2022 Jean Rebiffé, Orange Innovation Networks SPDX-License-Identifier: BSD-3-Clause SPDX-FileType: SOURCE SPDX-FileContributor: Jean Rebiffe, Orange Innovation Networks This software is confidential and proprietary information of Orange. You shall not disclose such Confidential Information and shall use it only in accordance with the terms of the agreement you entered into. Unauthorized copying of this file, via any medium is strictly prohibited. If you are Orange employee you shall use this software in accordance with the Orange Source Charter (http://infoportal-opensource.innov.intraorange/en/orange-source-charter/) This software is distributed under the BSD 3-Clause "New" or "Revised" License, the text of which is available at https://opensource.org/licenses/BSD-3-Clause or see the "license.txt" file for more details. Author: Jean Rebiffe <jean.rebiffe@orange.com> Author: Jean Rebiffe, Orange Innovation Networks Software description: Emulates a real SDN Controlleur by pushing NETCONF operations, ... """ __version__ = "0.2" __version__ = "0.3.0" __author__ = "Jean Rebiffe, Orange Innovation Networks, 2022" import argparse Loading Loading @@ -52,8 +50,11 @@ class TemplateError(LnccError): pass # TODO(Jean): Should be keep unsafe_hash=True? @dataclass # (unsafe_hash=True) # https://datatracker.ietf.org/doc/html/rfc8342#section-5.1 DATASTORES = {"running", "candidate", "startup", "intended"} @dataclass class NetworkElement: """Represent a single Network Element managable with NETCONF Loading Loading @@ -90,9 +91,12 @@ class NetworkElement: LOGGER.debug("Start: NETCONF connect() to %s", self.name) try: self.conn = manager.connect_ssh(**self.params, device_params={ "name": "huaweiyang" }) hostkey_verify=False) # TODO(Jean): fix vendor-specific hook - needed on my lab # self.conn = manager.connect_ssh(**self.params, # device_params={ # "name": "huaweiyang" # }) except ncclient.NCClientError as err: LOGGER.warning("Failed: NETCONF connect() to %s: Reason: %s", self.name, err) Loading Loading @@ -122,6 +126,8 @@ class NetworkElement: try: self.replies[reply] = func(**kwargs) # print(self.replies[reply].data_ele) # print(vars(self.replies[reply].data_ele)) except ncclient.NCClientError as err: LOGGER.warning( "Failed: NETCONF <%s> on %s. Reason: %s", Loading Loading @@ -244,6 +250,7 @@ class NetworkElementGroup(collections.UserDict): # TODO(Jean): Refactoring needed? tables = list() for name, state in self.items(): # print(state) tmp_tb = state.tables[table].copy() tmp_tb["origin"] = name tables.append(tmp_tb) Loading Loading @@ -276,6 +283,9 @@ class LnccCli(cmd2.Cmd): tables: Dict[str, pandas.DataFrame] = dict() logging_config = None intro = ("Welcome to the Lightweight NETCONF Controller lncc.py\n" "Type 'help' for more information") def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) logg = cmd2.Settable( Loading Loading @@ -526,27 +536,42 @@ class LnccCli(cmd2.Cmd): nargs="*", required=True) _nc_edit_parser.add_argument("--format") _nc_edit_parser.add_argument("--target") _nc_edit_parser.add_argument("--default-operation") _nc_edit_parser.add_argument("--test-option") _nc_edit_parser.add_argument("--error-option") _nc_edit_parser.add_argument( "--target", choices=DATASTORES, help="Name of the configuration datastore being edited", ) _nc_edit_parser.add_argument("--default-operation", choices={"merge", "replace", "none"}) _nc_edit_parser.add_argument( "--test-option", choices={"test-then-set", "set", "test-only"}, help="NE checks a complete configuration for syntactical " "and semantic errors before applying the configuration", ) _nc_edit_parser.add_argument( "--error-option", choices={"stop-on-error", "continue-on-error", "rollback-on-error"}, ) _nc_edit_parser.set_defaults(operation="edit-config") _nc_copy_parser = _netconf_subparsers.add_parser("copy-config") _nc_copy_parser.add_argument("--target", required=True) _nc_copy_parser.add_argument("--source", required=True) _nc_copy_parser.add_argument("--target", required=True, choices=DATASTORES) _nc_copy_parser.add_argument("--source", required=True, choices=DATASTORES) _nc_copy_parser.set_defaults(operation="copy-config") _nc_delete_parser = _netconf_subparsers.add_parser("delete-config") _nc_delete_parser.add_argument("--target", required=True) _nc_delete_parser.add_argument("--target", required=True, choices=DATASTORES) _nc_delete_parser.set_defaults(operation="delete-config") _nc_lock_parser = _netconf_subparsers.add_parser("lock") _nc_lock_parser.add_argument("--target") _nc_lock_parser.add_argument("--target", choices=DATASTORES) _nc_lock_parser.set_defaults(operation="lock") _nc_unlock_parser = _netconf_subparsers.add_parser("unlock") _nc_unlock_parser.add_argument("--target") _nc_unlock_parser.add_argument("--target", choices=DATASTORES) _nc_unlock_parser.set_defaults(operation="unlock") _nc_close_parser = _netconf_subparsers.add_parser("close-session") Loading @@ -572,7 +597,7 @@ class LnccCli(cmd2.Cmd): _nc_cancel_commit_parser.set_defaults(operation="cancel-commit") _nc_validate_parser = _netconf_subparsers.add_parser("validate") _nc_validate_parser.add_argument("--target") _nc_validate_parser.add_argument("--target", choices=DATASTORES) _nc_validate_parser.set_defaults(operation="validate") @cmd2.with_argparser(netconf_parser) Loading Loading @@ -634,8 +659,7 @@ class LnccCli(cmd2.Cmd): def table_concat(self, args: argparse.Namespace) -> None: assert args.table kwargs = {"table": args.table} self.nes.table_concat(**kwargs) self.nes.table_concat(table=args.table) self.pfeedback(f"Table '{args.table}' available for group") def table_to_excel(self, args: argparse.Namespace) -> None: Loading @@ -645,7 +669,7 @@ class LnccCli(cmd2.Cmd): with args.file: self.nes.table_to_excel(args.file, args.tables) self.pfeedback(f"Wrote Excel file {args.file.name}, " f"with {len(args.tables)} tabs: args.tables") f"with {len(args.tables)} tabs: {args.tables}") table_parser = cmd2.Cmd2ArgumentParser() _table_subparsers = table_parser.add_subparsers(title="subcommands") Loading Loading @@ -687,6 +711,46 @@ class LnccCli(cmd2.Cmd): self.pfeedback(f"Sleeping for {args.seconds} seconds") time.sleep(args.seconds) write_parser = cmd2.Cmd2ArgumentParser() write_parser.add_argument("--table", default="get") write_parser.add_argument("--reply", metavar="REPLY-NAME") write_parser.add_argument("--xsl-transform", type=argparse.FileType("r")) write_parser.add_argument("--file", metavar="EXCEL-FILE", type=argparse.FileType("wb")) @cmd2.with_argparser(write_parser) @cmd2.with_category("Input & output handling") def do_write(self, args: argparse.Namespace): assert hasattr(args, "table") assert args.table kwargs = {"table": args.table} kwargs["reply"] = args.reply or args.table if args.xsl_transform: with args.xsl_transform: kwargs["xsl_transform"] = args.xsl_transform.read() self.nes.table_from_xml(**kwargs) try: self.nes.table_concat(table=args.table) except ValueError as err: self.perror(f"Cannot concat tables {err}") self.pwarning("Did you forget the --xsl-transform?") return if args.file: with args.file: self.nes.table_to_excel(args.file, [args.table]) self.pfeedback(f"Wrote Excel file {args.file.name}, " f"with tab: {args.table}") else: self.poutput(self.nes.tables[args.table]) self.pfeedback(f"Displayed {kwargs['reply']}") def main(): app = LnccCli(persistent_history_file=".lncc-history.json.xz", Loading