umnet-ncs-instances 0.0.22
Python object representing NSO service or device instances.
Requires Python: >=3.6

## Problem Statement
UMnet's goal is to represent our entire network configuration in NSO.
As of Nov 2021, our current network configuration lives in rancid backups,
TopologyWalker files, Netinfo, Equipdb, and a few other places.
We need some way to get this data from all these sources and use it to
populate NSO.

After NSO is populated, we will still need ways to create data objects in python
that are structured appropriately for NSO to ingest (for example, in Netsplash).


## Solution Details
This package contains a set of classes that represent NSO objects designed by UMnet
(services, actions, etc). When you import these classes into your python code, you
can work with them similar to how you can work with NSO's high level python API.
When you're done you can generate XML using the gen_xml() method.

- `types.py` has the basic classes that emulate NSO's [maagic python api](https://developer.cisco.com/docs/nso/api/#!ncs-maagic/module-ncs-maagic).
- `instances.py` has instance classes that represent specific UMnet NCS instances.

When an `NCSInstance` object is initialized in code, its structure is set via the
initializer. `__setattr__` has been overloaded for all of the object types such
that you can't change the structure of the object or the types post-initialization.
What you can do is:
- Set a `Leaf` value (value must match the type specificed during initialization)
- Append a value to a `LeafList` (same type restriction applies)
- Set the choice for a `Choice` object (via the "choose" method)
- Add an entry to a `List` as you would to a dict.

You can also read these values after setting them in your code for validation purposes. Just
like with the maagic API you can read a `Leaf` directly. A `LeafList` behaves like a python list,
and a `List` behaves like a dict.

# An Example
Here is how a device instance is represented in `instances.py`.

```
class Device(NCSInstance):

    _path = "/config/devices/device"

    _nsmap = {
            "config":"http://tail-f.com/ns/config/1.0",
            "devices":"http://tail-f.com/ns/ncs",
            }

    _xml_munge = {
        r'<ned-id>(.+)</ned-id>':'<ned-id xmlns:\g<1>="http://tail-f.com/ns/ned-id/\g<1>">\g<1></ned-id>'
    }

    def __init__(self):

        self.name = Leaf(str)
        self.address = Leaf(str)
        self.authgroup = Leaf(str, value="default")
        self.state = Container()
        self.state.admin_state = Leaf(str, value="unlocked")
        self.device_type = Choice(['cli','netconf'])
        self.device_type.cli = Container()
        self.device_type.netconf = Container()
        self.device_type.cli.ned_id = Leaf(str)
        self.device_type.netconf.ned_id = Leaf(str)
        self.in_band_mgmt = Container()
        self.in_band_mgmt.set_ns("http://umnet.umich.edu/umnetcommon")
        self.in_band_mgmt.ip_address = Leaf(str)
        self.in_band_mgmt.interface = Leaf(str)
```
Here is some basic python code that creates a new device.
```
from umnet_ncs_instances import Device

device = Device()
device.name = "d-ARBL-1"
device.addresss = "198.108.10.4"
device.in_band_mgmt.ip_address = "198.108.10.4"
device.in_band_mgmt.interface = "Loopback0"
device.device_type.choose("cli")
device.device_type.cli.ned_id = "cisco-ios-cli-6.73"
print(device.gen_xml())
```

When you run the code above, here is what you'll get:
```
(venv) bash-3.2$ python test.py
<config xmlns="http://tail-f.com/ns/config/1.0">
  <devices xmlns="http://tail-f.com/ns/ncs">
    <device>
      <name>d-ARBL-1</name>
      <address>198.108.10.4</address>
      <authgroup>default</authgroup>
      <state>
        <admin-state>unlocked</admin-state>
      </state>
      <cli>
        <ned-id xmlns:cisco-ios-cli-6.73="http://tail-f.com/ns/ned-id/cisco-ios-cli-6.73">cisco-ios-cli-6.73</ned-id>
      </cli>
      <in-band-mgmt xmlns="http://umnet.umich.edu/umnetcommon">
        <ip-address>198.108.10.4</ip-address>
        <interface>Loopback0</interface>
      </in-band-mgmt>
    </device>
  </devices>
</config>
```

# Why do it this way?
There are many ways to accomplish this task, almost all of which wouldn't require you to
manually re-define yang objects as python classes as we're doing here. Our goal with this repo is to
create objects representing NSO instances that:
1. Can be used in code anywhere, without requiring access to an instance of NSO running UMnet packages.
2. Are easy to understand/manipulate and provide a similar experience as working with service objects
generated by NSO's high level python API.
3. Don't require compiling yang files.
4. Generate xml that can be used in an RPC call or manually ingested by NSO.