sc-napalm 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,30 @@
1
+ from .iosxr import SCIOSXR
2
+ from .junos import SCJunOS
3
+ from .nxos import SCNXOS
4
+ from .sros import SCNokiaSROSDriver
5
+ from .srl_ssh import NokiaSRLSSHDriver
6
+ from .srl import SCNokiaSRLDriver
7
+ from .eos import SCEOSDriver
8
+ from .nos import SCNOSDriver
9
+
10
+ PLATFORM_MAP = {
11
+ "iosxr": SCIOSXR,
12
+ "nxos": SCNXOS,
13
+ "junos": SCJunOS,
14
+ "sros": SCNokiaSROSDriver,
15
+ "srl_ssh": NokiaSRLSSHDriver,
16
+ "srl": SCNokiaSRLDriver,
17
+ "eos": SCEOSDriver,
18
+ "nos": SCNOSDriver,
19
+ }
20
+
21
+
22
+ def get_network_driver(platform: str):
23
+ """
24
+ Returns network driver based on platform string.
25
+ """
26
+ for valid_platform, driver in PLATFORM_MAP.items():
27
+ if valid_platform == platform:
28
+ return driver
29
+
30
+ raise NotImplementedError(f"Unsupported platform {platform}")
custom_napalm/base.py ADDED
@@ -0,0 +1,113 @@
1
+ from typing import Dict, List
2
+ import re
3
+ from napalm.base import models as napalm_models
4
+ from netmiko import ConnectHandler
5
+
6
+ from . import models as sc_models
7
+
8
+
9
+ class SCBaseDriver:
10
+ """
11
+ Base class that all children should inherit
12
+ """
13
+
14
+ # populate in child classes
15
+ INVENTORY_TO_TYPE = {}
16
+
17
+ def _get_inventory_type(self, name: str) -> str:
18
+ """
19
+ Maps the name of the inventory item to its type
20
+ """
21
+ for pattern, inv_type in self.INVENTORY_TO_TYPE.items():
22
+ if inv_type and inv_type not in sc_models.VALID_INVENTORY_TYPES.__args__:
23
+ raise TypeError(f"Invalid Inventory type {inv_type}")
24
+ if re.search(pattern, name):
25
+ return inv_type
26
+
27
+ raise ValueError(f"Unknown inventory item {name}")
28
+
29
+ def get_config(self) -> napalm_models.ConfigDict:
30
+ """napalm get config"""
31
+ raise NotImplementedError
32
+
33
+ def get_facts(self) -> napalm_models.FactsDict:
34
+ """napalm get facts"""
35
+ raise NotImplementedError
36
+
37
+ def get_optics(self) -> Dict[str, napalm_models.OpticsDict]:
38
+ """napalm get optics"""
39
+ raise NotImplementedError
40
+
41
+ def get_lldp_neighbors(self) -> Dict[str, List[napalm_models.LLDPNeighborDict]]:
42
+ """napalm get lldp neighbors"""
43
+ raise NotImplementedError
44
+
45
+ def get_inventory(self) -> List[sc_models.InventoryDict]:
46
+ """sc get inventory"""
47
+ raise NotImplementedError
48
+
49
+
50
+ class SCBaseNetconfDriver(SCBaseDriver):
51
+ """
52
+ Inclues some helper xml functions
53
+ """
54
+
55
+ netmiko_host_type = "linux"
56
+
57
+ def ssh_conn(self) -> ConnectHandler:
58
+ """
59
+ Ugly workaround for getting stuff via the cli over ssh.
60
+ Starts a netmiko ssh connection handler and returns it,
61
+ allowing you to interact with the CLI of the device.
62
+ """
63
+ optional_args = {}
64
+ if getattr(self, "optional_args", False):
65
+ optional_args = self.optional_args if self.optional_args else {}
66
+
67
+ args = {
68
+ "device_type": self.netmiko_host_type,
69
+ "host": self.hostname,
70
+ "username": self.username,
71
+ "password": self.password,
72
+ "ssh_config_file": optional_args.get("ssh_config", None),
73
+ }
74
+ return ConnectHandler(**args)
75
+
76
+ def _find_txt(self, xml_tree, path, default=None, namespaces=None):
77
+ """
78
+ Stolen from the napalm iosxr driver
79
+ Extract the text value from a leaf in an XML tree using XPath.
80
+
81
+ Will return a default value if leaf path not matched.
82
+ :param xml_tree:the XML Tree object. <type'lxml.etree._Element'>.
83
+ :param path: XPath to be applied in order to extract the desired data.
84
+ :param default: Value to be returned in case of a no match.
85
+ :param namespaces: namespace dictionary.
86
+ :return: a str value or None if leaf path not matched.
87
+ """
88
+ value = None
89
+ xpath_applied = xml_tree.xpath(path, namespaces=namespaces)
90
+
91
+ if xpath_applied:
92
+ if not len(xpath_applied[0]):
93
+ if xpath_applied[0].text is not None:
94
+ value = xpath_applied[0].text.strip()
95
+ else:
96
+ value = ""
97
+ else:
98
+ value = default
99
+
100
+ return value
101
+
102
+ # Helper xml methods that always pass in our namespaces by default
103
+ def _text(self, xml_tree, path, default=None):
104
+ return self._find_txt(xml_tree, path, default, namespaces=self.NS)
105
+
106
+ def _xpath(self, xml_tree, path):
107
+ return getattr(xml_tree, "xpath")(path, namespaces=self.NS)
108
+
109
+ def _find(self, xml_tree, element):
110
+ return getattr(xml_tree, "find")(element, namespaces=self.NS)
111
+
112
+ def _iterfind(self, xml_tree, element):
113
+ return getattr(xml_tree, "iterfind")(element, namespaces=self.NS)
@@ -0,0 +1 @@
1
+ from .eos import SCEOSDriver
@@ -0,0 +1,93 @@
1
+ from typing import List
2
+ from napalm.eos import EOSDriver
3
+ from ..base import SCBaseDriver
4
+ from ..models import InventoryDict
5
+
6
+
7
+ class SCEOSDriver(EOSDriver, SCBaseDriver):
8
+ ("fabric_module",)
9
+ ("fan",)
10
+ ("linecard",)
11
+ ("optic",)
12
+ ("psu",)
13
+ ("re",)
14
+ ("stack_cable",)
15
+ ("stack_member",)
16
+ ("uplink_module",)
17
+ ("aoc",)
18
+ ("dac",)
19
+
20
+ INVENTORY_TO_TYPE = {
21
+ r"Fabric": "fabric_module",
22
+ r"Linecard": "linecard",
23
+ r"Supervisor": "re",
24
+ }
25
+
26
+ def __init__(self, hostname, username, password, timeout=60, optional_args=None):
27
+ """
28
+ Forcing ssh transport since we don't enable the web interface
29
+ """
30
+ optional_args = optional_args if optional_args else {}
31
+ optional_args["transport"] = "ssh"
32
+
33
+ super().__init__(
34
+ hostname, username, password, timeout=timeout, optional_args=optional_args
35
+ )
36
+
37
+ def get_inventory(self) -> List[InventoryDict]:
38
+ inventory = self._run_commands(["show inventory"], encoding="json")
39
+
40
+ results = []
41
+
42
+ ### optics
43
+ for slot, optic in inventory[0]["xcvrSlots"].items():
44
+ if optic.get("modelName"):
45
+ results.append(
46
+ {
47
+ "type": "optic",
48
+ "subtype": optic["modelName"],
49
+ "name": f"Ethernet{slot}",
50
+ "part_number": optic["modelName"],
51
+ "serial_number": optic["serialNum"],
52
+ },
53
+ )
54
+
55
+ ### line cards
56
+ for slot, card in inventory[0]["cardSlots"].items():
57
+ if card.get("serialNum"):
58
+ results.append(
59
+ {
60
+ "type": self._get_inventory_type(slot),
61
+ "subtype": card["modelName"],
62
+ "name": f"Ethernet{slot}",
63
+ "part_number": card["modelName"],
64
+ "serial_number": card["serialNum"],
65
+ },
66
+ )
67
+
68
+ ### PSUs
69
+ for slot, psu in inventory[0]["powerSupplySlots"].items():
70
+ if psu.get("serialNum"):
71
+ results.append(
72
+ {
73
+ "type": "psu",
74
+ "subtype": None,
75
+ "name": f"PSU {slot}",
76
+ "part_number": psu["name"],
77
+ "serial_number": psu["serialNum"],
78
+ },
79
+ )
80
+
81
+ ### FANs
82
+ for slot, fan in inventory[0]["fanTraySlots"].items():
83
+ if fan.get("serialNum"):
84
+ results.append(
85
+ {
86
+ "type": "fan",
87
+ "subtype": None,
88
+ "name": f"FAN {slot}",
89
+ "part_number": fan["name"],
90
+ "serial_number": fan["serialNum"],
91
+ },
92
+ )
93
+ return results
custom_napalm/get.py ADDED
@@ -0,0 +1,83 @@
1
+ import argparse
2
+ from pprint import pprint
3
+
4
+ from napalm import get_network_driver
5
+
6
+ from custom_napalm import PLATFORM_MAP
7
+ from custom_napalm.base import SCBaseDriver
8
+ from custom_napalm.utils import configure_logging, LOG_LEVELS, get_from_args_or_env
9
+
10
+ # list of getters to run
11
+ GETTERS = [attr for attr in SCBaseDriver.__dict__ if attr.startswith("get")]
12
+
13
+ cred_args = {"sc_username": True, "sc_password": True}
14
+
15
+
16
+ def main():
17
+ parser = argparse.ArgumentParser(
18
+ description="""
19
+ Run a specific sc_napalm "getter" against a device.
20
+ """
21
+ )
22
+ parser.add_argument("device", help="device hostname or IP address")
23
+ parser.add_argument(
24
+ "sc_napalm_platform",
25
+ choices=PLATFORM_MAP,
26
+ help="The platform of this device",
27
+ )
28
+ parser.add_argument(
29
+ "cmd",
30
+ choices=GETTERS,
31
+ help="The getter command to run against this device",
32
+ )
33
+ parser.add_argument("--ssh-cfg", help="Use SSH config file to connect", type=str)
34
+ log_args = parser.add_mutually_exclusive_group()
35
+ log_args.add_argument(
36
+ "-l", "--log-level", help="Set log level for sc_napalm only", choices=LOG_LEVELS
37
+ )
38
+ log_args.add_argument(
39
+ "-L", "--LOG-LEVEL", help="set global log level", choices=LOG_LEVELS
40
+ )
41
+ parser.add_argument(
42
+ "--logfile",
43
+ type=str,
44
+ help="Save logging to a file (specified by name) instead of to stdout",
45
+ )
46
+
47
+ for cred_arg in cred_args:
48
+ parser.add_argument(f"--{cred_arg}", help="Specify credentials")
49
+ args = parser.parse_args()
50
+
51
+ log_level = args.log_level if args.log_level else args.LOG_LEVEL
52
+ if log_level:
53
+ configure_logging(
54
+ log_level,
55
+ log_globally=bool(args.LOG_LEVEL),
56
+ log_file=args.logfile,
57
+ log_to_console=not (bool(args.logfile)),
58
+ )
59
+
60
+ creds = {
61
+ cred: get_from_args_or_env(cred, args, required=reqd)
62
+ for cred, reqd in cred_args.items()
63
+ }
64
+ Driver = get_network_driver(args.sc_napalm_platform)
65
+
66
+ # setting up connection details
67
+ optional_args = {"look_for_keys": False}
68
+ if args.ssh_cfg:
69
+ optional_args = {"ssh_config_file": args.ssh_cfg}
70
+
71
+ with Driver(
72
+ args.device,
73
+ creds["sc_username"],
74
+ creds["sc_password"],
75
+ timeout=60,
76
+ optional_args=optional_args,
77
+ ) as conn:
78
+ result = getattr(conn, args.cmd)()
79
+ pprint(result)
80
+
81
+
82
+ if __name__ == "__main__":
83
+ main()
@@ -0,0 +1 @@
1
+ from .iosxr import SCIOSXR
@@ -0,0 +1,144 @@
1
+ NS = {
2
+ "int": "http://cisco.com/ns/yang/Cisco-IOS-XR-pfi-im-cmd-oper",
3
+ "int4": "http://cisco.com/ns/yang/Cisco-IOS-XR-ipv4-io-oper",
4
+ "int6": "http://cisco.com/ns/yang/Cisco-IOS-XR-ipv6-ma-oper",
5
+ "rib4": "http://cisco.com/ns/yang/Cisco-IOS-XR-ip-rib-ipv4-oper",
6
+ "rib6": "http://cisco.com/ns/yang/Cisco-IOS-XR-ip-rib-ipv6-oper",
7
+ "inv": "http://cisco.com/ns/yang/Cisco-IOS-XR-invmgr-oper",
8
+ }
9
+
10
+
11
+ IP_INT_RPC_REQ = """
12
+ <get xmlns="urn:ietf:params:xml:ns:netconf:base:1.1">
13
+ <filter>
14
+ <interfaces xmlns="http://cisco.com/ns/yang/Cisco-IOS-XR-pfi-im-cmd-oper">
15
+ <interfaces>
16
+ <interface/>
17
+ </interfaces>
18
+ </interfaces>
19
+ <ipv4-network xmlns="http://cisco.com/ns/yang/Cisco-IOS-XR-ipv4-io-oper">
20
+ <nodes>
21
+ <node>
22
+ <interface-data>
23
+ <vrfs>
24
+ <vrf>
25
+ <details>
26
+ <detail/>
27
+ </details>
28
+ </vrf>
29
+ </vrfs>
30
+ </interface-data>
31
+ </node>
32
+ </nodes>
33
+ </ipv4-network>
34
+ <ipv6-network xmlns="http://cisco.com/ns/yang/Cisco-IOS-XR-ipv6-ma-oper">
35
+ <nodes>
36
+ <node>
37
+ <interface-data>
38
+ <vrfs>
39
+ <vrf>
40
+ <global-details>
41
+ <global-detail/>
42
+ </global-details>
43
+ </vrf>
44
+ </vrfs>
45
+ </interface-data>
46
+ </node>
47
+ </nodes>
48
+ </ipv6-network>
49
+ </filter>
50
+ </get>"""
51
+
52
+ IP_ROUTE_RPC_REQ = """
53
+ <get xmlns="urn:ietf:params:xml:ns:netconf:base:1.1">
54
+ <filter>
55
+ <rib xmlns="http://cisco.com/ns/yang/Cisco-IOS-XR-ip-rib-ipv4-oper">
56
+ <vrfs>
57
+ <vrf>
58
+ <afs>
59
+ <af>
60
+ <af-name>IPv4</af-name>
61
+ <safs>
62
+ <saf>
63
+ <saf-name>Unicast</saf-name>
64
+ <ip-rib-route-table-names>
65
+ <ip-rib-route-table-name>
66
+ <route-table-name>default</route-table-name>
67
+ <routes>
68
+ <route>
69
+ <prefix-length/>
70
+ <protocol-name/>
71
+ <route-age/>
72
+ <route-path>
73
+ <active>true</active>
74
+ </route-path>
75
+ </route>
76
+ </routes>
77
+ </ip-rib-route-table-name>
78
+ </ip-rib-route-table-names>
79
+ </saf>
80
+ </safs>
81
+ </af>
82
+ </afs>
83
+ </vrf>
84
+ </vrfs>
85
+ </rib>
86
+
87
+ <ipv6-rib xmlns="http://cisco.com/ns/yang/Cisco-IOS-XR-ip-rib-ipv6-oper">
88
+ <vrfs>
89
+ <vrf>
90
+ <afs>
91
+ <af>
92
+ <af-name>IPv6</af-name>
93
+ <safs>
94
+ <saf>
95
+ <saf-name>Unicast</saf-name>
96
+ <ip-rib-route-table-names>
97
+ <ip-rib-route-table-name>
98
+ <route-table-name>default</route-table-name>
99
+ <routes>
100
+ <route>
101
+ <prefix-length/>
102
+ <protocol-name/>
103
+ <route-age/>
104
+ <route-path>
105
+ <active>true</active>
106
+ </route-path>
107
+ </route>
108
+ </routes>
109
+ </ip-rib-route-table-name>
110
+ </ip-rib-route-table-names>
111
+ </saf>
112
+ </safs>
113
+ </af>
114
+ </afs>
115
+ </vrf>
116
+ </vrfs>
117
+ </ipv6-rib>
118
+
119
+ </filter>
120
+ </get>
121
+ """
122
+
123
+ INV_RPC_REQ = """
124
+ <get xmlns="urn:ietf:params:xml:ns:netconf:base:1.1">
125
+ <filter>
126
+ <inventory xmlns="http://cisco.com/ns/yang/Cisco-IOS-XR-invmgr-oper">
127
+ <entities>
128
+ <entity>
129
+ <attributes>
130
+ <inv-basic-bag>
131
+ <description/>
132
+ <name/>
133
+ <serial-number/>
134
+ <manufacturer-name/>
135
+ <model-name/>
136
+ <is-field-replaceable-unit>true</is-field-replaceable-unit>
137
+ </inv-basic-bag>
138
+ </attributes>
139
+ </entity>
140
+ </entities>
141
+ </inventory>
142
+ </filter>
143
+ </get>
144
+ """
@@ -0,0 +1,55 @@
1
+ from typing import List
2
+ import logging
3
+
4
+ from lxml import etree
5
+
6
+ from napalm.iosxr_netconf import IOSXRNETCONFDriver
7
+ from ncclient.xml_ import to_ele
8
+
9
+ from ..base import SCBaseNetconfDriver
10
+ from ..models import InventoryDict
11
+
12
+ from .constants import INV_RPC_REQ
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class SCIOSXR(IOSXRNETCONFDriver, SCBaseNetconfDriver):
18
+ """
19
+ IOSXR Class
20
+ """
21
+
22
+ INVENTORY_TO_TYPE = {
23
+ r"PM\d+$": "psu",
24
+ r"FT\d+$": "fan",
25
+ r"FC\d+$": "fabric_module",
26
+ r"RP\d$": "re",
27
+ r"SC\d": None, # don't care about system controller
28
+ r"Rack 0": None, # this is the chassis, don't care about it
29
+ r"^(FourHundred|Hundred|Forty)GigE": "optic",
30
+ r"\d/\d": "linecard",
31
+ }
32
+
33
+ def get_inventory(self) -> List[InventoryDict]:
34
+ """
35
+ Gets inventory data
36
+ """
37
+ rpc_reply = self.device.dispatch(to_ele(INV_RPC_REQ)).xml
38
+ xml_result = etree.fromstring(rpc_reply)
39
+
40
+ output = []
41
+ for item in self._xpath(xml_result, "//inv:inv-basic-bag"):
42
+ name = self._text(item, "inv:name")
43
+ item_type = self._get_inventory_type(name)
44
+ if item_type:
45
+ output.append(
46
+ {
47
+ "type": item_type,
48
+ "name": name,
49
+ "part_number": self._text(item, "inv:model-name"),
50
+ "serial_number": self._text(item, "inv:serial-number"),
51
+ "parent": None,
52
+ }
53
+ )
54
+
55
+ return output
@@ -0,0 +1 @@
1
+ from .junos import SCJunOS