Today I want to talk about something that I have been enjoying and adopting more and more over the last 6 months, which is using the code as a form of documentation/source of truth for open-source projects/code.
What do I mean by this? By this, I simply mean,
using the code as a form of documentation, as a reference to help with troubleshooting, development, and learning.
The benefit of using the code as a source of truth is that:
- saves time from having to go through the documentation
- is always up to date
- you learn a few things along the way.
Good stuff. Let me show you a few examples...
VSCode
The easiest and first way you can use your code in this way is to use the IntelliSense feature of your code editor/IDE. In the case of VSCode you install the necessary extension for your language (Python extension here), and you are good to go. I use this to hover over objects to see what input arguments they take (example shown below).
Via Python
Another way you can use the code as a source of truth is to query the code directly within your language. Below are a few examples ...
First of all, we can show the Nornir inventory schema. Useful for seeing the different keys and value types permitted.
>>> from rich import print
>>> print(nr.inventory.schema())
{
'hosts': {
'$name': {
'name': 'str',
'connection_options': {
'$connection_type': {
'extras': {'$key': '$value'},
'hostname': 'str',
'port': 'int',
'username': 'str',
'password': 'str',
'platform': 'str'
}
},
'groups': ['$group_name'],
'data': {'$key': '$value'},
'hostname': 'str',
'port': 'int',
'username': 'str',
'password': 'str',
'platform': 'str'
}
},
'groups': {
'$group': {
'name': 'str',
'connection_options': {
'$connection_type': {
'extras': {'$key': '$value'},
'hostname': 'str',
'port': 'int',
'username': 'str',
'password': 'str',
'platform': 'str'
}
},
'groups': ['$group_name'],
'data': {'$key': '$value'},
'hostname': 'str',
'port': 'int',
'username': 'str',
'password': 'str',
'platform': 'str'
}
},
'defaults': {
'data': {'$key': '$value'},
'connection_options': {
'$connection_type': {
'extras': {'$key': '$value'},
'hostname': 'str',
'port': 'int',
'username': 'str',
'password': 'str',
'platform': 'str'
}
},
'hostname': 'str',
'port': 'int',
'username': 'str',
'password': 'str',
'platform': 'str'
}
}
If we now move over to Netmiko, we can use Python to see the support device types, by printing out the keys from CLASS_MAPPER_BASE
. Like so:
>>> from rich import print as rprint
>>>
>>> rprint(list(CLASS_MAPPER_BASE.keys()))
[
'a10',
'accedian',
'adtran_os',
'alcatel_aos',
'alcatel_sros',
'allied_telesis_awplus',
'apresia_aeos',
'arista_eos',
'aruba_os',
'aruba_osswitch',
'aruba_procurve',
...
Via Python + Rich Inspect
Another way in Python we can view our code is by using Rich Inspect. Rich is a tool to help prettify your terminal output. One module it provides is inspect
, which allows you to inspect an object in Python.
Here's an example where we use Rich Inspect to show us the available questions within Batfish.
# Install
$ pip install rich
>>> from rich import inspect
>>> inspect(bf.q, methods=True)
╭──────────────────────────────────────────────────────────────────────────── <class 'pybatfish.question.question.Questions'> ────────────────────────────────────────────────────────────────────────────╮
│ Class to hold and manage (e.g. load, list) Batfish questions. │
│ │
│ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │
│ │ <pybatfish.question.question.Questions object at 0x7f7102707c40> │ │
│ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ │
│ │
│ aaaAuthenticationLogin = def aaaAuthenticationLogin(...) Returns nodes that do not require authentication on all virtual terminal lines. │
│ bgpEdges = def bgpEdges(...) Returns BGP adjacencies. │
│ bgpPeerConfiguration = def bgpPeerConfiguration(...) Returns configuration settings for BGP peerings. │
│ bgpProcessConfiguration = def bgpProcessConfiguration(...) Returns configuration settings of BGP processes. │
│ bgpRib = def bgpRib(...) Returns routes in the BGP RIB. │
│ bgpSessionCompatibility = def bgpSessionCompatibility(...) Returns the compatibility of configured BGP sessions. │
│ bgpSessionStatus = def bgpSessionStatus(...) Returns the dynamic status of configured BGP sessions. │
│ bidirectionalReachability = def bidirectionalReachability(...) Searches for successfully delivered flows that can successfully receive a response. │
│ bidirectionalTraceroute = def bidirectionalTraceroute(...) Traces the path(s) for the specified flow, along with path(s) for reverse flows. │
│ compareFilters = def compareFilters(...) Compares filters with the same name in the current and reference snapshots. Returns pairs of lines, one from each filter, that match the same │
│ flow(s) but treat them differently (i.e. one permits and the other denies the flow). │
│ definedStructures = def definedStructures(...) Lists the structures defined in the network. │
│ detectLoops = def detectLoops(...) Detects forwarding loops. │
│ differentialReachability = def differentialReachability(...) Returns flows that are successful in one snapshot but not in another. │
│ edges = def edges(...) Returns different types of network adjacencies in a snapshot. │
│ eigrpEdges = def eigrpEdges(...) Returns EIGRP adjacencies. │
│ evpnL3VniProperties = def evpnL3VniProperties(...) Returns configuration settings of VXLANs. │
│ evpnRib = def evpnRib(...) Returns routes in the EVPN RIB. │
│ f5BigipVipConfiguration = def f5BigipVipConfiguration(...) Returns VIP configuration of F5 BIG-IP devices. │
│ fileParseStatus = def fileParseStatus(...) Displays file parse status. │
│ filterLineReachability = def filterLineReachability(...) Returns unreachable lines in filters (ACLs and firewall rules). │
│ filterTable = def filterTable(...) Returns subset of answer for a question. │
│ findMatchingFilterLines = def findMatchingFilterLines(...) Returns lines in filters (ACLs and firewall rules) that match any packet within the specified header constraints. │
│ initIssues = def initIssues(...) Returns issues encountered when processing the snapshot. │
│ interfaceMtu = def interfaceMtu(...) Finds interfaces where the configured MTU matches the specified comparator and mtuBytes. │
│ interfaceProperties = def interfaceProperties(...) Returns configuration settings of interfaces. │
│ ipOwners = def ipOwners(...) Returns where IP addresses are attached in the network. │
│ ipsecEdges = def ipsecEdges(...) Returns IPSec tunnels. │
│ ipsecSessionStatus = def ipsecSessionStatus(...) Returns the status of configured IPSec sessions. │
│ isisEdges = def isisEdges(...) Returns ISIS adjacencies. │
│ layer1Edges = def layer1Edges(...) Returns Layer 1 links. │
│ layer3Edges = def layer3Edges(...) Returns Layer 3 links. │
│ list = def list(tags=None): List available questions. │
│ list_tags = def list_tags(): Get the tags for available questions. │
│ load = def load(directory=None): Load questions from Batfish service or local directory. │
│ loopbackMultipathConsistency = def loopbackMultipathConsistency(...) Validates multipath consistency between all pairs of loopbacks. │
│ lpmRoutes = def lpmRoutes(...) Returns routes that are longest prefix match for a given IP address. │
│ mlagProperties = def mlagProperties(...) Returns MLAG configuration. │
│ multipathConsistency = def multipathConsistency(...) Validates multipath consistency. │
│ namedStructures = def namedStructures(...) Returns named structure definitions. │
│ nodeProperties = def nodeProperties(...) Returns configuration settings of nodes. │
│ ospfAreaConfiguration = def ospfAreaConfiguration(...) Returns configuration parameters of OSPF areas. │
│ ospfEdges = def ospfEdges(...) Returns OSPF adjacencies. │
│ ospfInterfaceConfiguration = def ospfInterfaceConfiguration(...) Returns OSPF configuration of interfaces. │
│ ospfProcessConfiguration = def ospfProcessConfiguration(...) Returns configuration parameters for OSPF routing processes. │
│ ospfSessionCompatibility = def ospfSessionCompatibility(...) Returns compatible OSPF sessions. │
│ parseWarning = def parseWarning(...) Returns warnings that occurred when parsing the snapshot. │
│ prefixTracer = def prefixTracer(...) Traces prefix propagation through the network. │
│ reachability = def reachability(...) Finds flows that match the specified path and header space conditions. │
│ referencedStructures = def referencedStructures(...) Lists the references in configuration files to vendor-specific structures. │
│ resolveFilterSpecifier = def resolveFilterSpecifier(...) Returns the set of filters corresponding to a filterSpec value. │
│ resolveInterfaceSpecifier = def resolveInterfaceSpecifier(...) Returns the set of interfaces corresponding to an interfaceSpec value. │
│ resolveIpsOfLocationSpecifier = def resolveIpsOfLocationSpecifier(...) Returns IPs that are auto-assigned to locations. │
│ resolveIpSpecifier = def resolveIpSpecifier(...) Returns the IP address space corresponding to an ipSpec value. │
│ resolveLocationSpecifier = def resolveLocationSpecifier(...) Returns the set of locations corresponding to a locationSpec value. │
│ resolveNodeSpecifier = def resolveNodeSpecifier(...) Returns the set of nodes corresponding to a nodeSpec value. │
│ routes = def routes(...) Returns routing tables. │
│ searchFilters = def searchFilters(...) Finds flows for which a filter takes a particular behavior. │
│ searchRoutePolicies = def searchRoutePolicies(...) Finds route announcements for which a route policy has a particular behavior. │
│ subnetMultipathConsistency = def subnetMultipathConsistency(...) Validates multipath consistency between all pairs of subnets. │
│ switchedVlanProperties = def switchedVlanProperties(...) Returns configuration settings of switched VLANs. │
│ testFilters = def testFilters(...) Returns how a flow is processed by a filter (ACLs, firewall rules). │
│ testRoutePolicies = def testRoutePolicies(...) Evaluates the processing of a route by a given policy. │
│ traceroute = def traceroute(...) Traces the path(s) for the specified flow. │
│ undefinedReferences = def undefinedReferences(...) Identifies undefined references in configuration. │
│ unusedStructures = def unusedStructures(...) Returns nodes with structures such as ACLs, routemaps, etc. that are defined but not used. │
│ viConversionStatus = def viConversionStatus(...) Displays vendor independent conversion status. │
│ viConversionWarning = def viConversionWarning(...) Returns Batfish warnings that occurred when converting to vendor independent model. │
│ viModel = def viModel(...) Lists configuration attributes of nodes and edges in the network. │
│ vrrpProperties = def vrrpProperties(...) Returns configuration settings of VRRP groups. │
│ vxlanEdges = def vxlanEdges(...) Returns VXLAN edges. │
│ vxlanVniProperties = def vxlanVniProperties(...) Returns configuration settings of VXLANs. │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
Great, right. It's worth noting by default Rich Inspect will only show the object attributes. Therefore to also show the methods, you add methods=True
, just as I previously showed.
Version Control
The final way to use the code as a source of truth is to use version control. By this, I mean viewing the code directly without going via Python, inspection libraries or IntelliSense features. This is useful as you don't have to have the code installed locally and can also look between releases and branches to see what has changed.
That covers some of the ways that I use the code as a source of truth, which I hope you may also find useful when working with open-source projects/code.