Commit 69ae15a2 authored by Jean Rebiffe's avatar Jean Rebiffe
Browse files

Update lncc.py using feedback from Theodoros + update for Kawada request...

Update lncc.py using feedback from Theodoros + update for Kawada request (hostkey_verify=False) + Open Source license
parent 5a6a1492
Loading
Loading
Loading
Loading
+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
@@ -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


@@ -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)
@@ -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",
@@ -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)
@@ -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(
@@ -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")
@@ -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)
@@ -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:
@@ -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")
@@ -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",