Loading lncc.py +92 −70 Original line number Original line Diff line number Diff line #!/usr/bin/env python3 #!/usr/bin/env python3 """Lightweight NETCONF Controller (LNCC) """Lightweight NETCONF Controller (LNCC) Emulates a real SDN Controlleur by pushing NETCONF operations, ... Emulates a real SDN Controller by pushing NETCONF operations, ... License License ======= ======= Loading @@ -17,41 +17,47 @@ the text of which is available at https://opensource.org/licenses/BSD-3-Clause or see the "license.txt" file for more details. or see the "license.txt" file for more details. Author: Jean Rebiffe, Orange Innovation Networks Author: Jean Rebiffe, Orange Innovation Networks Software description: Emulates a real SDN Controlleur by pushing NETCONF Software description: Emulates a real SDN Controller by pushing NETCONF operations, ... operations, ... """ """ __version__ = "0.3.0" __version__ = "0.3.1" __author__ = "Jean Rebiffe, Orange Innovation Networks, 2022" __author__ = "Jean Rebiffe, Orange Innovation Networks, 2022" import argparse import argparse import collections import logging import logging import logging.config import logging.config import sys import sys from collections import ChainMap, UserDict from dataclasses import dataclass, field from dataclasses import dataclass, field from typing import Dict, List, Optional, Set, Tuple, cast from typing import Any, Dict, List, Optional, Set, Tuple import cmd2 import ncclient import ncclient import pandas import pandas import yaml import yaml from ncclient import manager from ncclient import manager import cmd2 LOGGER = logging.getLogger("lncc") LOGGER = logging.getLogger("lncc") # https://datatracker.ietf.org/doc/html/rfc8342#section-5.1 DATASTORES = {"running", "candidate", "startup", "intended"} class LnccError(Exception): class LnccError(Exception): ... ... class TemplateError(LnccError): class TemplateError(LnccError): pass """Error when performing a XML templating. May appear on `NetworkElement.build_config()` for 2 reasons: # https://datatracker.ietf.org/doc/html/rfc8342#section-5.1 - the mapping parameters include inexistant mapping name DATASTORES = {"running", "candidate", "startup", "intended"} - the XML template contains a variable which doesnt appears in mappings Typically during <edit-config> operation command: `netconf edit-config` with `--template-config` and `--mapping` options. """ @dataclass @dataclass Loading @@ -69,25 +75,22 @@ class NetworkElement: name: str name: str params: Dict[str, str] = field(compare=False) params: Dict[str, str] = field(compare=False) conn: Optional[manager.Manager] = field(default=None, compare=False) conn: Optional[manager.Manager] = field(default=None, compare=False) replies: Dict[str, replies: Dict[str, ncclient.operations.retrieve.GetReply] = field( ncclient.operations.retrieve.GetReply] = field( default_factory=dict, compare=False) default_factory=dict, compare=False) tables: Dict[str, pandas.DataFrame] = field(default_factory=dict, tables: Dict[str, pandas.DataFrame] = field(default_factory=dict, compare=False) compare=False) configs: Dict[str, str] = field(default_factory=dict, configs: Dict[str, str] = field(default_factory=dict, compare=False) compare=False) mappings: Dict[str, Dict[str, str]] = field(default_factory=dict, mappings: Dict[str, Dict[str, str]] = field(default_factory=dict, compare=False) compare=False) def to_dict(self) -> dict: def to_dict(self) -> Dict[str, Any]: dic: dict = {"params": self.params} dic: Dict[str, Any] = {"params": self.params} if self.mappings: if self.mappings: dic["mappings"] = self.mappings dic["mappings"] = self.mappings return dic return dic def connect(self) -> manager.Manager: def connect(self) -> manager.Manager: """Initiate NETCONF connection""" """Initiate NETCONF connection""" # TODO(Jean): fix vendor-specific hook - needed on my lab 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, Loading @@ -106,7 +109,10 @@ class NetworkElement: return self.conn return self.conn def netconf_operation(self, operation: str, reply: str = None, **kwargs): def netconf_operation(self, operation: str, reply: Optional[str] = None, **kwargs) -> None: if not reply: if not reply: reply = operation reply = operation Loading Loading @@ -139,51 +145,66 @@ class NetworkElement: LOGGER.debug("Success: NETCONF <%s> on %s", operation, self.name) LOGGER.debug("Success: NETCONF <%s> on %s", operation, self.name) def build_config(self, config_template: str, mappings: Set[str], def build_config( group_mappings: dict) -> None: self, avails = self.mappings.copy() config_template: str, avails.update(self.mappings) mappings: Set[str], group_mappings: Dict[str, Dict[str, str]], ) -> None: assert isinstance(config_template, str) assert isinstance(config_template, str) unknwon = set(filter(lambda mapp: mapp not in avails, mappings)) # avails = self.mappings.copy() if unknwon: # avails.update(self.mappings) # unknwon = set(filter(lambda mapp: mapp not in avails, mappings)) # if unknwon: # raise KeyError(f"Unknwon mapping {unknwon} for NE {self.name} " # f"Available mappings: {set(avails)}") # usables: dict = dict(filter(lambda a: a[0] in mappings, # avails.items())) # mapping = dict(collections.ChainMap(*usables.values())) # TODO(jean): To use all mapping by default maps = ChainMap(self.mappings, group_mappings) if not mappings.issubset(maps): # TODO(Jean): build own Exception? # TODO(Jean): build own Exception? raise KeyError(f"Unknwon mapping {unknwon} for NE {self.name} " raise TemplateError(f"Unknwon mapping {mappings.difference(maps)} " f"Available mappings: {set(avails)}") f"for NE {self.name}. " f"Available mappings: {set(maps)}") usables: dict = dict(filter(lambda a: a[0] in mappings, usables = [maps[key] for key in mappings] avails.items())) fmtmap = ChainMap(*usables) mapping = dict(collections.ChainMap(*usables.values())) try: try: self.configs["default"] = config_template.format(**mapping) self.configs["default"] = config_template.format_map(fmtmap) except KeyError as err: except KeyError as err: raise TemplateError( raise TemplateError( f"Cannot build template, {err} missing") from err f"Cannot build template, {err} missing for NE {self.name}" ) from err def table_from_xml(self, def table_from_xml( self, table: str = "get", table: str = "get", reply: str = None, reply: Optional[str] = None, xsl_transform: str = None) -> None: xsl_transform: Optional[str] = None, ) -> None: """Build a panda DataFrame, based on previous XML ouput""" """Build a panda DataFrame, based on previous XML ouput""" if reply is None: if reply is None: reply = table reply = table # TODO(Jean): Fix mypy compains with pandas self.tables[table] = pandas.read_xml(self.replies[reply].data_xml, new_table: pandas.DataFrame = pandas.read_xml( stylesheet=xsl_transform) self.replies[reply].data_xml, stylesheet=xsl_transform) self.tables[table] = cast(pandas.DataFrame, new_table) class NetworkElementGroup(collections.UserDict): class NetworkElementGroup(UserDict): """Represent a Group of Network Element""" """Represent a Group of Network Element""" # TODO(Jean): Correct static dict in class declaration tables: Dict[str, pandas.DataFrame] = {} tables: Dict[str, pandas.DataFrame] = {} mappings: Dict[str, Dict[str, str]] = {} mappings: Dict[str, Dict[str, str]] = {} @staticmethod @staticmethod def from_dict(dic: dict) -> "NetworkElementGroup": def from_dict(dic: Dict[str, Any]) -> "NetworkElementGroup": """Build dict, that can be dumped in a file (YAML/JSON/...)""" """Build dict, that can be dumped in a file (YAML/JSON/...)""" assert "nes" in dic assert "nes" in dic Loading @@ -203,9 +224,9 @@ class NetworkElementGroup(collections.UserDict): return group return group def to_dict(self) -> dict: def to_dict(self) -> Dict[str, Dict[str, Any]]: """Build dict, that can be loaded from a file (YAML/JSON/...)""" """Build dict, that can be loaded from a file (YAML/JSON/...)""" dic: dict = {"nes": {}} dic: Dict[str, Dict[str, Any]] = {"nes": {}} if self.mappings: if self.mappings: dic["mappings"] = self.mappings dic["mappings"] = self.mappings for name, n_e in self.items(): for name, n_e in self.items(): Loading Loading @@ -239,10 +260,12 @@ class NetworkElementGroup(collections.UserDict): for n_e in self.values(): for n_e in self.values(): n_e.build_config(config_template, mappings, self.mappings) n_e.build_config(config_template, mappings, self.mappings) def table_from_xml(self, def table_from_xml( self, table: str = "get", table: str = "get", reply: str = None, reply: Optional[str] = None, xsl_transform: str = None) -> None: xsl_transform: Optional[str] = None, ) -> None: for n_e in self.data.values(): for n_e in self.data.values(): n_e.table_from_xml(table, reply, xsl_transform) n_e.table_from_xml(table, reply, xsl_transform) Loading @@ -250,7 +273,6 @@ class NetworkElementGroup(collections.UserDict): # TODO(Jean): Refactoring needed? # TODO(Jean): Refactoring needed? tables = [] tables = [] 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 @@ -261,7 +283,7 @@ class NetworkElementGroup(collections.UserDict): def table_to_excel(self, def table_to_excel(self, excel_writer: str = "get.xlsx", excel_writer: str = "get.xlsx", tables: List[str] = None): tables: Optional[List[str]] = None) -> None: if not tables: if not tables: tables = ["get"] tables = ["get"] Loading @@ -276,17 +298,18 @@ class LnccCli(cmd2.Cmd): The LNNC command-line interface is build around "cmd2" package The LNNC command-line interface is build around "cmd2" package >>> import sys >>> import sys >>> sys.exit(LnccCli().cmdloop()) >>> app = LnccCli() >>> sys.exit(app.cmdloop()) """ """ nes: NetworkElementGroup = NetworkElementGroup() nes: NetworkElementGroup = NetworkElementGroup() tables: Dict[str, pandas.DataFrame] = dict() tables: Dict[str, pandas.DataFrame] = {} logging_config = None logging_config = None intro = ("Welcome to the Lightweight NETCONF Controller lncc.py\n" intro = ("Welcome to the Lightweight NETCONF Controller lncc.py\n" "Type 'help' for more information") "Type 'help' for more information") def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) super().__init__(*args, **kwargs) logg = cmd2.Settable( logg = cmd2.Settable( name="logging_config", name="logging_config", Loading @@ -298,7 +321,7 @@ class LnccCli(cmd2.Cmd): self.add_settable(logg) self.add_settable(logg) @staticmethod @staticmethod def _onchange_logging(param_name, old, new): def _onchange_logging(param_name, old, new) -> None: del param_name, old # Unused del param_name, old # Unused # import io # import io # assert isinstance(new, io.TextIOWrapper) # assert isinstance(new, io.TextIOWrapper) Loading Loading @@ -332,7 +355,7 @@ class LnccCli(cmd2.Cmd): self.pfeedback(f"NE {n_e.name} added, current: {len(self.nes)} NEs") self.pfeedback(f"NE {n_e.name} added, current: {len(self.nes)} NEs") def ne_delete(self, args: argparse.Namespace): def ne_delete(self, args: argparse.Namespace) -> None: assert hasattr(args, "name") assert hasattr(args, "name") unknown = list(filter(lambda name: name not in self.nes, args.name)) unknown = list(filter(lambda name: name not in self.nes, args.name)) if unknown: if unknown: Loading @@ -356,7 +379,7 @@ class LnccCli(cmd2.Cmd): self.pfeedback(f"Deleted {len(args.name)} NEs: {args.name}, " self.pfeedback(f"Deleted {len(args.name)} NEs: {args.name}, " f"current: {len(self.nes)} NEs") f"current: {len(self.nes)} NEs") def ne_load(self, args: argparse.Namespace): def ne_load(self, args: argparse.Namespace) -> None: assert hasattr(args, "file") assert hasattr(args, "file") with args.file: with args.file: Loading @@ -374,7 +397,7 @@ class LnccCli(cmd2.Cmd): self.pfeedback("Loading complete: " self.pfeedback("Loading complete: " f"{len(news)} new NEs: {list(news.keys())}") f"{len(news)} new NEs: {list(news.keys())}") def ne_dump(self, args: argparse.Namespace): def ne_dump(self, args: argparse.Namespace) -> None: nes_dict = self.nes.to_dict() nes_dict = self.nes.to_dict() if args.file: if args.file: Loading @@ -383,7 +406,7 @@ class LnccCli(cmd2.Cmd): else: else: self.poutput(yaml.safe_dump(nes_dict)) self.poutput(yaml.safe_dump(nes_dict)) def ne_print(self, args: argparse.Namespace): def ne_print(self, args: argparse.Namespace) -> None: assert args.name assert args.name tables = list(self.nes[args.name].tables.keys()) tables = list(self.nes[args.name].tables.keys()) configs = list(self.nes[args.name].configs.keys()) configs = list(self.nes[args.name].configs.keys()) Loading Loading @@ -429,7 +452,7 @@ class LnccCli(cmd2.Cmd): @cmd2.with_argparser(ne_parser) @cmd2.with_argparser(ne_parser) @cmd2.with_category("Parameter management") @cmd2.with_category("Parameter management") def do_ne(self, args: argparse.Namespace): def do_ne(self, args: argparse.Namespace) -> None: if not hasattr(args, "func"): if not hasattr(args, "func"): self.do_help("netconf") self.do_help("netconf") return return Loading @@ -438,7 +461,7 @@ class LnccCli(cmd2.Cmd): # Mapping command and subcommands # Mapping command and subcommands def mapping_add(self, args: argparse.Namespace): def mapping_add(self, args: argparse.Namespace) -> None: assert hasattr(args, "name") assert hasattr(args, "name") assert hasattr(args, "mapping") assert hasattr(args, "mapping") assert hasattr(args, "ne") assert hasattr(args, "ne") Loading @@ -464,7 +487,7 @@ class LnccCli(cmd2.Cmd): else: else: self.nes.mappings[args.name] = maps self.nes.mappings[args.name] = maps def mapping_print(self, args: argparse.Namespace): def mapping_print(self, args: argparse.Namespace) -> None: del args # Unued del args # Unued self.poutput(f"Group mapping: {self.nes.mappings}") self.poutput(f"Group mapping: {self.nes.mappings}") for name, n_e in self.nes.items(): for name, n_e in self.nes.items(): Loading @@ -484,7 +507,7 @@ class LnccCli(cmd2.Cmd): @cmd2.with_argparser(mapping_parser) @cmd2.with_argparser(mapping_parser) @cmd2.with_category("Parameter management") @cmd2.with_category("Parameter management") def do_mapping(self, args: argparse.Namespace): def do_mapping(self, args: argparse.Namespace) -> None: if not hasattr(args, "func"): if not hasattr(args, "func"): self.do_help("mapping") self.do_help("mapping") return return Loading Loading @@ -522,6 +545,7 @@ class LnccCli(cmd2.Cmd): _nc_get_config_parser.add_argument("--filter-file", _nc_get_config_parser.add_argument("--filter-file", metavar="XML-FILE", metavar="XML-FILE", type=argparse.FileType("r")) type=argparse.FileType("r")) _nc_get_config_parser.add_argument("--source", required=True, choices=DATASTORES) _nc_get_config_parser.set_defaults(operation="get-config") _nc_get_config_parser.set_defaults(operation="get-config") _nc_edit_parser = _netconf_subparsers.add_parser("edit-config") _nc_edit_parser = _netconf_subparsers.add_parser("edit-config") Loading Loading @@ -612,7 +636,6 @@ class LnccCli(cmd2.Cmd): items3 = filter(lambda item: item[1] is not None, items2) items3 = filter(lambda item: item[1] is not None, items2) kwargs = dict(items3) kwargs = dict(items3) if "reply" not in kwargs: if "reply" not in kwargs: kwargs["reply"] = kwargs["operation"] kwargs["reply"] = kwargs["operation"] Loading Loading @@ -646,7 +669,6 @@ class LnccCli(cmd2.Cmd): kwargs = {"table": args.table} kwargs = {"table": args.table} # kwargs["reply"] = args.reply if args.reply else args.table kwargs["reply"] = args.reply or args.table kwargs["reply"] = args.reply or args.table if args.xsl_transform: if args.xsl_transform: Loading Loading @@ -753,7 +775,7 @@ class LnccCli(cmd2.Cmd): self.pfeedback(f"Displayed {kwargs['reply']}") self.pfeedback(f"Displayed {kwargs['reply']}") def main(): def main() -> None: app = LnccCli(persistent_history_file=".lncc-history.json.xz", app = LnccCli(persistent_history_file=".lncc-history.json.xz", startup_script=".lnccrc.txt") startup_script=".lnccrc.txt") sys.exit(app.cmdloop()) sys.exit(app.cmdloop()) Loading Loading
lncc.py +92 −70 Original line number Original line Diff line number Diff line #!/usr/bin/env python3 #!/usr/bin/env python3 """Lightweight NETCONF Controller (LNCC) """Lightweight NETCONF Controller (LNCC) Emulates a real SDN Controlleur by pushing NETCONF operations, ... Emulates a real SDN Controller by pushing NETCONF operations, ... License License ======= ======= Loading @@ -17,41 +17,47 @@ the text of which is available at https://opensource.org/licenses/BSD-3-Clause or see the "license.txt" file for more details. or see the "license.txt" file for more details. Author: Jean Rebiffe, Orange Innovation Networks Author: Jean Rebiffe, Orange Innovation Networks Software description: Emulates a real SDN Controlleur by pushing NETCONF Software description: Emulates a real SDN Controller by pushing NETCONF operations, ... operations, ... """ """ __version__ = "0.3.0" __version__ = "0.3.1" __author__ = "Jean Rebiffe, Orange Innovation Networks, 2022" __author__ = "Jean Rebiffe, Orange Innovation Networks, 2022" import argparse import argparse import collections import logging import logging import logging.config import logging.config import sys import sys from collections import ChainMap, UserDict from dataclasses import dataclass, field from dataclasses import dataclass, field from typing import Dict, List, Optional, Set, Tuple, cast from typing import Any, Dict, List, Optional, Set, Tuple import cmd2 import ncclient import ncclient import pandas import pandas import yaml import yaml from ncclient import manager from ncclient import manager import cmd2 LOGGER = logging.getLogger("lncc") LOGGER = logging.getLogger("lncc") # https://datatracker.ietf.org/doc/html/rfc8342#section-5.1 DATASTORES = {"running", "candidate", "startup", "intended"} class LnccError(Exception): class LnccError(Exception): ... ... class TemplateError(LnccError): class TemplateError(LnccError): pass """Error when performing a XML templating. May appear on `NetworkElement.build_config()` for 2 reasons: # https://datatracker.ietf.org/doc/html/rfc8342#section-5.1 - the mapping parameters include inexistant mapping name DATASTORES = {"running", "candidate", "startup", "intended"} - the XML template contains a variable which doesnt appears in mappings Typically during <edit-config> operation command: `netconf edit-config` with `--template-config` and `--mapping` options. """ @dataclass @dataclass Loading @@ -69,25 +75,22 @@ class NetworkElement: name: str name: str params: Dict[str, str] = field(compare=False) params: Dict[str, str] = field(compare=False) conn: Optional[manager.Manager] = field(default=None, compare=False) conn: Optional[manager.Manager] = field(default=None, compare=False) replies: Dict[str, replies: Dict[str, ncclient.operations.retrieve.GetReply] = field( ncclient.operations.retrieve.GetReply] = field( default_factory=dict, compare=False) default_factory=dict, compare=False) tables: Dict[str, pandas.DataFrame] = field(default_factory=dict, tables: Dict[str, pandas.DataFrame] = field(default_factory=dict, compare=False) compare=False) configs: Dict[str, str] = field(default_factory=dict, configs: Dict[str, str] = field(default_factory=dict, compare=False) compare=False) mappings: Dict[str, Dict[str, str]] = field(default_factory=dict, mappings: Dict[str, Dict[str, str]] = field(default_factory=dict, compare=False) compare=False) def to_dict(self) -> dict: def to_dict(self) -> Dict[str, Any]: dic: dict = {"params": self.params} dic: Dict[str, Any] = {"params": self.params} if self.mappings: if self.mappings: dic["mappings"] = self.mappings dic["mappings"] = self.mappings return dic return dic def connect(self) -> manager.Manager: def connect(self) -> manager.Manager: """Initiate NETCONF connection""" """Initiate NETCONF connection""" # TODO(Jean): fix vendor-specific hook - needed on my lab 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, Loading @@ -106,7 +109,10 @@ class NetworkElement: return self.conn return self.conn def netconf_operation(self, operation: str, reply: str = None, **kwargs): def netconf_operation(self, operation: str, reply: Optional[str] = None, **kwargs) -> None: if not reply: if not reply: reply = operation reply = operation Loading Loading @@ -139,51 +145,66 @@ class NetworkElement: LOGGER.debug("Success: NETCONF <%s> on %s", operation, self.name) LOGGER.debug("Success: NETCONF <%s> on %s", operation, self.name) def build_config(self, config_template: str, mappings: Set[str], def build_config( group_mappings: dict) -> None: self, avails = self.mappings.copy() config_template: str, avails.update(self.mappings) mappings: Set[str], group_mappings: Dict[str, Dict[str, str]], ) -> None: assert isinstance(config_template, str) assert isinstance(config_template, str) unknwon = set(filter(lambda mapp: mapp not in avails, mappings)) # avails = self.mappings.copy() if unknwon: # avails.update(self.mappings) # unknwon = set(filter(lambda mapp: mapp not in avails, mappings)) # if unknwon: # raise KeyError(f"Unknwon mapping {unknwon} for NE {self.name} " # f"Available mappings: {set(avails)}") # usables: dict = dict(filter(lambda a: a[0] in mappings, # avails.items())) # mapping = dict(collections.ChainMap(*usables.values())) # TODO(jean): To use all mapping by default maps = ChainMap(self.mappings, group_mappings) if not mappings.issubset(maps): # TODO(Jean): build own Exception? # TODO(Jean): build own Exception? raise KeyError(f"Unknwon mapping {unknwon} for NE {self.name} " raise TemplateError(f"Unknwon mapping {mappings.difference(maps)} " f"Available mappings: {set(avails)}") f"for NE {self.name}. " f"Available mappings: {set(maps)}") usables: dict = dict(filter(lambda a: a[0] in mappings, usables = [maps[key] for key in mappings] avails.items())) fmtmap = ChainMap(*usables) mapping = dict(collections.ChainMap(*usables.values())) try: try: self.configs["default"] = config_template.format(**mapping) self.configs["default"] = config_template.format_map(fmtmap) except KeyError as err: except KeyError as err: raise TemplateError( raise TemplateError( f"Cannot build template, {err} missing") from err f"Cannot build template, {err} missing for NE {self.name}" ) from err def table_from_xml(self, def table_from_xml( self, table: str = "get", table: str = "get", reply: str = None, reply: Optional[str] = None, xsl_transform: str = None) -> None: xsl_transform: Optional[str] = None, ) -> None: """Build a panda DataFrame, based on previous XML ouput""" """Build a panda DataFrame, based on previous XML ouput""" if reply is None: if reply is None: reply = table reply = table # TODO(Jean): Fix mypy compains with pandas self.tables[table] = pandas.read_xml(self.replies[reply].data_xml, new_table: pandas.DataFrame = pandas.read_xml( stylesheet=xsl_transform) self.replies[reply].data_xml, stylesheet=xsl_transform) self.tables[table] = cast(pandas.DataFrame, new_table) class NetworkElementGroup(collections.UserDict): class NetworkElementGroup(UserDict): """Represent a Group of Network Element""" """Represent a Group of Network Element""" # TODO(Jean): Correct static dict in class declaration tables: Dict[str, pandas.DataFrame] = {} tables: Dict[str, pandas.DataFrame] = {} mappings: Dict[str, Dict[str, str]] = {} mappings: Dict[str, Dict[str, str]] = {} @staticmethod @staticmethod def from_dict(dic: dict) -> "NetworkElementGroup": def from_dict(dic: Dict[str, Any]) -> "NetworkElementGroup": """Build dict, that can be dumped in a file (YAML/JSON/...)""" """Build dict, that can be dumped in a file (YAML/JSON/...)""" assert "nes" in dic assert "nes" in dic Loading @@ -203,9 +224,9 @@ class NetworkElementGroup(collections.UserDict): return group return group def to_dict(self) -> dict: def to_dict(self) -> Dict[str, Dict[str, Any]]: """Build dict, that can be loaded from a file (YAML/JSON/...)""" """Build dict, that can be loaded from a file (YAML/JSON/...)""" dic: dict = {"nes": {}} dic: Dict[str, Dict[str, Any]] = {"nes": {}} if self.mappings: if self.mappings: dic["mappings"] = self.mappings dic["mappings"] = self.mappings for name, n_e in self.items(): for name, n_e in self.items(): Loading Loading @@ -239,10 +260,12 @@ class NetworkElementGroup(collections.UserDict): for n_e in self.values(): for n_e in self.values(): n_e.build_config(config_template, mappings, self.mappings) n_e.build_config(config_template, mappings, self.mappings) def table_from_xml(self, def table_from_xml( self, table: str = "get", table: str = "get", reply: str = None, reply: Optional[str] = None, xsl_transform: str = None) -> None: xsl_transform: Optional[str] = None, ) -> None: for n_e in self.data.values(): for n_e in self.data.values(): n_e.table_from_xml(table, reply, xsl_transform) n_e.table_from_xml(table, reply, xsl_transform) Loading @@ -250,7 +273,6 @@ class NetworkElementGroup(collections.UserDict): # TODO(Jean): Refactoring needed? # TODO(Jean): Refactoring needed? tables = [] tables = [] 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 @@ -261,7 +283,7 @@ class NetworkElementGroup(collections.UserDict): def table_to_excel(self, def table_to_excel(self, excel_writer: str = "get.xlsx", excel_writer: str = "get.xlsx", tables: List[str] = None): tables: Optional[List[str]] = None) -> None: if not tables: if not tables: tables = ["get"] tables = ["get"] Loading @@ -276,17 +298,18 @@ class LnccCli(cmd2.Cmd): The LNNC command-line interface is build around "cmd2" package The LNNC command-line interface is build around "cmd2" package >>> import sys >>> import sys >>> sys.exit(LnccCli().cmdloop()) >>> app = LnccCli() >>> sys.exit(app.cmdloop()) """ """ nes: NetworkElementGroup = NetworkElementGroup() nes: NetworkElementGroup = NetworkElementGroup() tables: Dict[str, pandas.DataFrame] = dict() tables: Dict[str, pandas.DataFrame] = {} logging_config = None logging_config = None intro = ("Welcome to the Lightweight NETCONF Controller lncc.py\n" intro = ("Welcome to the Lightweight NETCONF Controller lncc.py\n" "Type 'help' for more information") "Type 'help' for more information") def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) super().__init__(*args, **kwargs) logg = cmd2.Settable( logg = cmd2.Settable( name="logging_config", name="logging_config", Loading @@ -298,7 +321,7 @@ class LnccCli(cmd2.Cmd): self.add_settable(logg) self.add_settable(logg) @staticmethod @staticmethod def _onchange_logging(param_name, old, new): def _onchange_logging(param_name, old, new) -> None: del param_name, old # Unused del param_name, old # Unused # import io # import io # assert isinstance(new, io.TextIOWrapper) # assert isinstance(new, io.TextIOWrapper) Loading Loading @@ -332,7 +355,7 @@ class LnccCli(cmd2.Cmd): self.pfeedback(f"NE {n_e.name} added, current: {len(self.nes)} NEs") self.pfeedback(f"NE {n_e.name} added, current: {len(self.nes)} NEs") def ne_delete(self, args: argparse.Namespace): def ne_delete(self, args: argparse.Namespace) -> None: assert hasattr(args, "name") assert hasattr(args, "name") unknown = list(filter(lambda name: name not in self.nes, args.name)) unknown = list(filter(lambda name: name not in self.nes, args.name)) if unknown: if unknown: Loading @@ -356,7 +379,7 @@ class LnccCli(cmd2.Cmd): self.pfeedback(f"Deleted {len(args.name)} NEs: {args.name}, " self.pfeedback(f"Deleted {len(args.name)} NEs: {args.name}, " f"current: {len(self.nes)} NEs") f"current: {len(self.nes)} NEs") def ne_load(self, args: argparse.Namespace): def ne_load(self, args: argparse.Namespace) -> None: assert hasattr(args, "file") assert hasattr(args, "file") with args.file: with args.file: Loading @@ -374,7 +397,7 @@ class LnccCli(cmd2.Cmd): self.pfeedback("Loading complete: " self.pfeedback("Loading complete: " f"{len(news)} new NEs: {list(news.keys())}") f"{len(news)} new NEs: {list(news.keys())}") def ne_dump(self, args: argparse.Namespace): def ne_dump(self, args: argparse.Namespace) -> None: nes_dict = self.nes.to_dict() nes_dict = self.nes.to_dict() if args.file: if args.file: Loading @@ -383,7 +406,7 @@ class LnccCli(cmd2.Cmd): else: else: self.poutput(yaml.safe_dump(nes_dict)) self.poutput(yaml.safe_dump(nes_dict)) def ne_print(self, args: argparse.Namespace): def ne_print(self, args: argparse.Namespace) -> None: assert args.name assert args.name tables = list(self.nes[args.name].tables.keys()) tables = list(self.nes[args.name].tables.keys()) configs = list(self.nes[args.name].configs.keys()) configs = list(self.nes[args.name].configs.keys()) Loading Loading @@ -429,7 +452,7 @@ class LnccCli(cmd2.Cmd): @cmd2.with_argparser(ne_parser) @cmd2.with_argparser(ne_parser) @cmd2.with_category("Parameter management") @cmd2.with_category("Parameter management") def do_ne(self, args: argparse.Namespace): def do_ne(self, args: argparse.Namespace) -> None: if not hasattr(args, "func"): if not hasattr(args, "func"): self.do_help("netconf") self.do_help("netconf") return return Loading @@ -438,7 +461,7 @@ class LnccCli(cmd2.Cmd): # Mapping command and subcommands # Mapping command and subcommands def mapping_add(self, args: argparse.Namespace): def mapping_add(self, args: argparse.Namespace) -> None: assert hasattr(args, "name") assert hasattr(args, "name") assert hasattr(args, "mapping") assert hasattr(args, "mapping") assert hasattr(args, "ne") assert hasattr(args, "ne") Loading @@ -464,7 +487,7 @@ class LnccCli(cmd2.Cmd): else: else: self.nes.mappings[args.name] = maps self.nes.mappings[args.name] = maps def mapping_print(self, args: argparse.Namespace): def mapping_print(self, args: argparse.Namespace) -> None: del args # Unued del args # Unued self.poutput(f"Group mapping: {self.nes.mappings}") self.poutput(f"Group mapping: {self.nes.mappings}") for name, n_e in self.nes.items(): for name, n_e in self.nes.items(): Loading @@ -484,7 +507,7 @@ class LnccCli(cmd2.Cmd): @cmd2.with_argparser(mapping_parser) @cmd2.with_argparser(mapping_parser) @cmd2.with_category("Parameter management") @cmd2.with_category("Parameter management") def do_mapping(self, args: argparse.Namespace): def do_mapping(self, args: argparse.Namespace) -> None: if not hasattr(args, "func"): if not hasattr(args, "func"): self.do_help("mapping") self.do_help("mapping") return return Loading Loading @@ -522,6 +545,7 @@ class LnccCli(cmd2.Cmd): _nc_get_config_parser.add_argument("--filter-file", _nc_get_config_parser.add_argument("--filter-file", metavar="XML-FILE", metavar="XML-FILE", type=argparse.FileType("r")) type=argparse.FileType("r")) _nc_get_config_parser.add_argument("--source", required=True, choices=DATASTORES) _nc_get_config_parser.set_defaults(operation="get-config") _nc_get_config_parser.set_defaults(operation="get-config") _nc_edit_parser = _netconf_subparsers.add_parser("edit-config") _nc_edit_parser = _netconf_subparsers.add_parser("edit-config") Loading Loading @@ -612,7 +636,6 @@ class LnccCli(cmd2.Cmd): items3 = filter(lambda item: item[1] is not None, items2) items3 = filter(lambda item: item[1] is not None, items2) kwargs = dict(items3) kwargs = dict(items3) if "reply" not in kwargs: if "reply" not in kwargs: kwargs["reply"] = kwargs["operation"] kwargs["reply"] = kwargs["operation"] Loading Loading @@ -646,7 +669,6 @@ class LnccCli(cmd2.Cmd): kwargs = {"table": args.table} kwargs = {"table": args.table} # kwargs["reply"] = args.reply if args.reply else args.table kwargs["reply"] = args.reply or args.table kwargs["reply"] = args.reply or args.table if args.xsl_transform: if args.xsl_transform: Loading Loading @@ -753,7 +775,7 @@ class LnccCli(cmd2.Cmd): self.pfeedback(f"Displayed {kwargs['reply']}") self.pfeedback(f"Displayed {kwargs['reply']}") def main(): def main() -> None: app = LnccCli(persistent_history_file=".lncc-history.json.xz", app = LnccCli(persistent_history_file=".lncc-history.json.xz", startup_script=".lnccrc.txt") startup_script=".lnccrc.txt") sys.exit(app.cmdloop()) sys.exit(app.cmdloop()) Loading