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 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
@@ -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

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