Loading lncc.py +97 −33 Original line number Original line Diff line number Diff line #!/usr/bin/env python3 #!/usr/bin/env python3 """Lightweight NETCONF Controlleur (LNCC) """Lightweight NETCONF Controller (LNCC) Emulates a real SDN Controlleur by pushing NETCONF operations, ... Emulates a real SDN Controlleur by pushing NETCONF operations, ... License License ======= ======= Not yet OpenSource licensed, it's still on Orange property as of May 6th 2022 Software Name : Lightweight NETCONF Controller (LNCC) TODO(Jean): Working with legal to make it OpenSource, probably BSD-3-Clause Version: 0.3.0 Software Name : Lightweight NETCONF Controlleur SPDX-FileCopyrightText: Copyright (c) 2022 Jean Rebiffé, Orange Innovation Networks 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. This software is distributed under the BSD 3-Clause "New" or "Revised" License, You shall not disclose such Confidential Information and shall use it only in the text of which is available at https://opensource.org/licenses/BSD-3-Clause accordance with the terms of the agreement you entered into. or see the "license.txt" file for more details. 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/) 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" __author__ = "Jean Rebiffe, Orange Innovation Networks, 2022" import argparse import argparse Loading Loading @@ -52,8 +50,11 @@ class TemplateError(LnccError): pass pass # TODO(Jean): Should be keep unsafe_hash=True? # https://datatracker.ietf.org/doc/html/rfc8342#section-5.1 @dataclass # (unsafe_hash=True) DATASTORES = {"running", "candidate", "startup", "intended"} @dataclass class NetworkElement: class NetworkElement: """Represent a single Network Element managable with NETCONF """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) LOGGER.debug("Start: NETCONF connect() to %s", self.name) try: try: self.conn = manager.connect_ssh(**self.params, self.conn = manager.connect_ssh(**self.params, device_params={ hostkey_verify=False) "name": "huaweiyang" # 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: except ncclient.NCClientError as err: LOGGER.warning("Failed: NETCONF connect() to %s: Reason: %s", LOGGER.warning("Failed: NETCONF connect() to %s: Reason: %s", self.name, err) self.name, err) Loading Loading @@ -122,6 +126,8 @@ class NetworkElement: try: try: self.replies[reply] = func(**kwargs) self.replies[reply] = func(**kwargs) # print(self.replies[reply].data_ele) # print(vars(self.replies[reply].data_ele)) except ncclient.NCClientError as err: except ncclient.NCClientError as err: LOGGER.warning( LOGGER.warning( "Failed: NETCONF <%s> on %s. Reason: %s", "Failed: NETCONF <%s> on %s. Reason: %s", Loading Loading @@ -244,6 +250,7 @@ class NetworkElementGroup(collections.UserDict): # TODO(Jean): Refactoring needed? # TODO(Jean): Refactoring needed? tables = list() tables = list() for name, state in self.items(): for name, state in self.items(): # print(state) tmp_tb = state.tables[table].copy() tmp_tb = state.tables[table].copy() tmp_tb["origin"] = name tmp_tb["origin"] = name tables.append(tmp_tb) tables.append(tmp_tb) Loading Loading @@ -276,6 +283,9 @@ class LnccCli(cmd2.Cmd): tables: Dict[str, pandas.DataFrame] = dict() tables: Dict[str, pandas.DataFrame] = dict() logging_config = None logging_config = None intro = ("Welcome to the Lightweight NETCONF Controller lncc.py\n" "Type 'help' for more information") def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) super().__init__(*args, **kwargs) logg = cmd2.Settable( logg = cmd2.Settable( Loading Loading @@ -526,27 +536,42 @@ class LnccCli(cmd2.Cmd): nargs="*", nargs="*", required=True) required=True) _nc_edit_parser.add_argument("--format") _nc_edit_parser.add_argument("--format") _nc_edit_parser.add_argument("--target") _nc_edit_parser.add_argument( _nc_edit_parser.add_argument("--default-operation") "--target", _nc_edit_parser.add_argument("--test-option") choices=DATASTORES, _nc_edit_parser.add_argument("--error-option") 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_edit_parser.set_defaults(operation="edit-config") _nc_copy_parser = _netconf_subparsers.add_parser("copy-config") _nc_copy_parser = _netconf_subparsers.add_parser("copy-config") _nc_copy_parser.add_argument("--target", required=True) _nc_copy_parser.add_argument("--target", required=True, choices=DATASTORES) _nc_copy_parser.add_argument("--source", required=True) _nc_copy_parser.add_argument("--source", required=True, choices=DATASTORES) _nc_copy_parser.set_defaults(operation="copy-config") _nc_copy_parser.set_defaults(operation="copy-config") _nc_delete_parser = _netconf_subparsers.add_parser("delete-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_delete_parser.set_defaults(operation="delete-config") _nc_lock_parser = _netconf_subparsers.add_parser("lock") _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_lock_parser.set_defaults(operation="lock") _nc_unlock_parser = _netconf_subparsers.add_parser("unlock") _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_unlock_parser.set_defaults(operation="unlock") _nc_close_parser = _netconf_subparsers.add_parser("close-session") _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_cancel_commit_parser.set_defaults(operation="cancel-commit") _nc_validate_parser = _netconf_subparsers.add_parser("validate") _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") _nc_validate_parser.set_defaults(operation="validate") @cmd2.with_argparser(netconf_parser) @cmd2.with_argparser(netconf_parser) Loading Loading @@ -634,8 +659,7 @@ class LnccCli(cmd2.Cmd): def table_concat(self, args: argparse.Namespace) -> None: def table_concat(self, args: argparse.Namespace) -> None: assert args.table assert args.table kwargs = {"table": args.table} self.nes.table_concat(table=args.table) self.nes.table_concat(**kwargs) self.pfeedback(f"Table '{args.table}' available for group") self.pfeedback(f"Table '{args.table}' available for group") def table_to_excel(self, args: argparse.Namespace) -> None: def table_to_excel(self, args: argparse.Namespace) -> None: Loading @@ -645,7 +669,7 @@ class LnccCli(cmd2.Cmd): with args.file: with args.file: self.nes.table_to_excel(args.file, args.tables) self.nes.table_to_excel(args.file, args.tables) self.pfeedback(f"Wrote Excel file {args.file.name}, " 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_parser = cmd2.Cmd2ArgumentParser() _table_subparsers = table_parser.add_subparsers(title="subcommands") _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") self.pfeedback(f"Sleeping for {args.seconds} seconds") time.sleep(args.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(): def main(): app = LnccCli(persistent_history_file=".lncc-history.json.xz", app = LnccCli(persistent_history_file=".lncc-history.json.xz", Loading Loading
lncc.py +97 −33 Original line number Original line Diff line number Diff line #!/usr/bin/env python3 #!/usr/bin/env python3 """Lightweight NETCONF Controlleur (LNCC) """Lightweight NETCONF Controller (LNCC) Emulates a real SDN Controlleur by pushing NETCONF operations, ... Emulates a real SDN Controlleur by pushing NETCONF operations, ... License License ======= ======= Not yet OpenSource licensed, it's still on Orange property as of May 6th 2022 Software Name : Lightweight NETCONF Controller (LNCC) TODO(Jean): Working with legal to make it OpenSource, probably BSD-3-Clause Version: 0.3.0 Software Name : Lightweight NETCONF Controlleur SPDX-FileCopyrightText: Copyright (c) 2022 Jean Rebiffé, Orange Innovation Networks 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. This software is distributed under the BSD 3-Clause "New" or "Revised" License, You shall not disclose such Confidential Information and shall use it only in the text of which is available at https://opensource.org/licenses/BSD-3-Clause accordance with the terms of the agreement you entered into. or see the "license.txt" file for more details. 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/) 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" __author__ = "Jean Rebiffe, Orange Innovation Networks, 2022" import argparse import argparse Loading Loading @@ -52,8 +50,11 @@ class TemplateError(LnccError): pass pass # TODO(Jean): Should be keep unsafe_hash=True? # https://datatracker.ietf.org/doc/html/rfc8342#section-5.1 @dataclass # (unsafe_hash=True) DATASTORES = {"running", "candidate", "startup", "intended"} @dataclass class NetworkElement: class NetworkElement: """Represent a single Network Element managable with NETCONF """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) LOGGER.debug("Start: NETCONF connect() to %s", self.name) try: try: self.conn = manager.connect_ssh(**self.params, self.conn = manager.connect_ssh(**self.params, device_params={ hostkey_verify=False) "name": "huaweiyang" # 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: except ncclient.NCClientError as err: LOGGER.warning("Failed: NETCONF connect() to %s: Reason: %s", LOGGER.warning("Failed: NETCONF connect() to %s: Reason: %s", self.name, err) self.name, err) Loading Loading @@ -122,6 +126,8 @@ class NetworkElement: try: try: self.replies[reply] = func(**kwargs) self.replies[reply] = func(**kwargs) # print(self.replies[reply].data_ele) # print(vars(self.replies[reply].data_ele)) except ncclient.NCClientError as err: except ncclient.NCClientError as err: LOGGER.warning( LOGGER.warning( "Failed: NETCONF <%s> on %s. Reason: %s", "Failed: NETCONF <%s> on %s. Reason: %s", Loading Loading @@ -244,6 +250,7 @@ class NetworkElementGroup(collections.UserDict): # TODO(Jean): Refactoring needed? # TODO(Jean): Refactoring needed? tables = list() tables = list() for name, state in self.items(): for name, state in self.items(): # print(state) tmp_tb = state.tables[table].copy() tmp_tb = state.tables[table].copy() tmp_tb["origin"] = name tmp_tb["origin"] = name tables.append(tmp_tb) tables.append(tmp_tb) Loading Loading @@ -276,6 +283,9 @@ class LnccCli(cmd2.Cmd): tables: Dict[str, pandas.DataFrame] = dict() tables: Dict[str, pandas.DataFrame] = dict() logging_config = None logging_config = None intro = ("Welcome to the Lightweight NETCONF Controller lncc.py\n" "Type 'help' for more information") def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) super().__init__(*args, **kwargs) logg = cmd2.Settable( logg = cmd2.Settable( Loading Loading @@ -526,27 +536,42 @@ class LnccCli(cmd2.Cmd): nargs="*", nargs="*", required=True) required=True) _nc_edit_parser.add_argument("--format") _nc_edit_parser.add_argument("--format") _nc_edit_parser.add_argument("--target") _nc_edit_parser.add_argument( _nc_edit_parser.add_argument("--default-operation") "--target", _nc_edit_parser.add_argument("--test-option") choices=DATASTORES, _nc_edit_parser.add_argument("--error-option") 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_edit_parser.set_defaults(operation="edit-config") _nc_copy_parser = _netconf_subparsers.add_parser("copy-config") _nc_copy_parser = _netconf_subparsers.add_parser("copy-config") _nc_copy_parser.add_argument("--target", required=True) _nc_copy_parser.add_argument("--target", required=True, choices=DATASTORES) _nc_copy_parser.add_argument("--source", required=True) _nc_copy_parser.add_argument("--source", required=True, choices=DATASTORES) _nc_copy_parser.set_defaults(operation="copy-config") _nc_copy_parser.set_defaults(operation="copy-config") _nc_delete_parser = _netconf_subparsers.add_parser("delete-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_delete_parser.set_defaults(operation="delete-config") _nc_lock_parser = _netconf_subparsers.add_parser("lock") _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_lock_parser.set_defaults(operation="lock") _nc_unlock_parser = _netconf_subparsers.add_parser("unlock") _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_unlock_parser.set_defaults(operation="unlock") _nc_close_parser = _netconf_subparsers.add_parser("close-session") _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_cancel_commit_parser.set_defaults(operation="cancel-commit") _nc_validate_parser = _netconf_subparsers.add_parser("validate") _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") _nc_validate_parser.set_defaults(operation="validate") @cmd2.with_argparser(netconf_parser) @cmd2.with_argparser(netconf_parser) Loading Loading @@ -634,8 +659,7 @@ class LnccCli(cmd2.Cmd): def table_concat(self, args: argparse.Namespace) -> None: def table_concat(self, args: argparse.Namespace) -> None: assert args.table assert args.table kwargs = {"table": args.table} self.nes.table_concat(table=args.table) self.nes.table_concat(**kwargs) self.pfeedback(f"Table '{args.table}' available for group") self.pfeedback(f"Table '{args.table}' available for group") def table_to_excel(self, args: argparse.Namespace) -> None: def table_to_excel(self, args: argparse.Namespace) -> None: Loading @@ -645,7 +669,7 @@ class LnccCli(cmd2.Cmd): with args.file: with args.file: self.nes.table_to_excel(args.file, args.tables) self.nes.table_to_excel(args.file, args.tables) self.pfeedback(f"Wrote Excel file {args.file.name}, " 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_parser = cmd2.Cmd2ArgumentParser() _table_subparsers = table_parser.add_subparsers(title="subcommands") _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") self.pfeedback(f"Sleeping for {args.seconds} seconds") time.sleep(args.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(): def main(): app = LnccCli(persistent_history_file=".lncc-history.json.xz", app = LnccCli(persistent_history_file=".lncc-history.json.xz", Loading