PY-SIL Tutorial

Tutorial Introduction

Welcome to this comprehensive tutorial on creating a PY-SIL code with GET2 callbacks. In this guide, we will walk through the process of building a simple PY-SIL subsystem that efficiently retrieves data from a YAML file containing addresses and seamlessly returns it to the server.

A PY-SIL GET2 Callback function is usually mapped to a single data node. The server will issue “get” and “getnext” requests via this API, and the callback code will return value nodes for the data it is managing. These data nodes are not maintained in the server Data Tree. They are not cached in the server.

Create a New Folder for the Project

Start by creating a new Python project and setting up the necessary file structure. Create a new folder for your project by using the mkdir (make directory) command. Replace project-folder-name with your desired folder name and desired location. $HOME here is just an example location where the PY-SIL will be located:

mkdir $HOME/pysil-tutorial

Navigate into your new project folder by typing:

cd $HOME/pysil-tutorial

Virtual environment (optional)

You can optionally use a tool like virtualenv to create a virtual environment for your project.

> # Create a virtual environment
> python -m venv venv
>
> # Activate the virtual environment
> source venv/bin/activate

Project Setup

Tutorial will use following YANG module 'address_get_2'. Create it in your project folder with content like provided below.

module address_get_2 {
  namespace "urn:address_get_2";
  prefix "address_get_2";
  revision "2024-01-29";

  container addresses {
    config false;
    list address {
      key "last-name first-name";
      leaf last-name {
        type string;
        description
          "Last name of the person who is associated with this address";
      }
      leaf first-name {
        type string;
        description
          "First name of the person who is associated with this address";
      }
      leaf street {
        type string;
        description "street address";
      }
      leaf city {
        type string;
        description "City address";
      }
      leaf zipcode {
        type string { length "5 | 10"; }
        description "zipcode";
      }
      list phone {
        key phone-type;
        leaf phone-type {
          type enumeration {
            enum cell;
            enum home;
            enum work;
          }
        }
        leaf phone-number {
          mandatory true;
          type string;
        }
      }  // list phone
    } // list address
  } // container addresses

} // module address

Project Dependencies

To continue you should have PY-SIL Development Environment installed.

PY-SIL Code Generation Example

To generate PY-SIL code stub use following command:

pyang -f py-sil address_get_2.yang

By default it will generate following files in your project folder.

Following file structure will be generated

address_get_2/
    |-- u_address_get_2.py
    |-- y_address_get_2.py

For the full code generation options see PY-SIL Code Generation

Implement Callbacks

Next step we should implement actual code in the auto-generated callback functions in the u_address_get_2.py

get_address - callback function

Here is example of final get_address function that contains actual code with detailed comments below.

def get_address(self, silcb: SILCbGet):
    """
    Get database object callback
    Path: list /address_get_2:addresses/address

    Parameters
    ----------
        silcb: SILCbGet
            Control block

    Returns
    -------
        STATUS_T
            status of the callback.

    Additional
    ----------
        silcb.local_keys: dict with local keys
            silcb.local_keys["last-name"]: str
                Local key leaf 'last-name' in list 'address' Path: /address_get_2:addresses/address/last-name
            silcb.local_keys["first-name"]: str
                Local key leaf 'first-name' in list 'address' Path: /address_get_2:addresses/address/first-name
        silcb.local_keys_present: list of present and valid local keys
        silcb.local_keys_fixed: list of local keys fixed in a getnext request
    """
    self.log(
        f"Enter 'get_address' {self.name}@{self.revision} ancestor_keys={silcb.ancestor_keys} local_keys={silcb.local_keys}"
    )

    # Step 1: Load data from YAML file
    test_data = load_test_data()

    # Step 2: apply filters by first and last name if needed
    if "last-name" in silcb.local_keys_fixed:
        test_data = [
            r for r in test_data if r.get("last-name") == silcb.local_keys["last-name"]
        ]
    if "first-name" in silcb.local_keys_fixed:
        test_data = [
            r
            for r in test_data
            if r.get("first-name") == silcb.local_keys["first-name"]
        ]

    test_data_len = len(test_data)
    # print("FILTERED DATA: %s" % test_data_len)

    # Step 3: Set i index to 0 if mode is GETCB_GET_VALUE next code in function will return current row
    i = 0

    # Step 4: If the callback mode is GETCB_GET_VALUE, no additional action is taken, as the next code will return the current row.
    if silcb.cbmode == GETCB_MODE_T.GETCB_GET_VALUE:
        pass

    # Step 5: Find current row if callback mode is GETCB_GETNEXT_VALUE
    if silcb.cbmode == GETCB_MODE_T.GETCB_GETNEXT_VALUE:
        # find current row first
        while test_data_len > i:
            # print(test_data[i].get("last-name"))
            # print(test_data[i].get("first-name"))
            if (
                test_data[i].get("last-name") == silcb.local_keys["last-name"]
                and test_data[i].get("first-name") == silcb.local_keys["first-name"]
            ):
                i += 1
                break
            i += 1

    # Step 6: If the row doesn't exist, exit with an error
    if test_data_len <= i:
        return STATUS_T.ERR_NCX_NO_INSTANCE

    curr = test_data[i]

    # Step 7: Create an python object
    return_data = {
        "last-name": curr.get("last-name"),
        "first-name": curr.get("first-name"),
        "street": curr.get("street"),
        "city": curr.get("city"),
        "zipcode": curr.get("zipcode"),
    }

    # Step 8: Save return data and mark if more data exists
    res = silcb.add_return_data(data=return_data, has_more=(test_data_len > i + 1))

    # Step 9: Return status
    return res
  • Step 1 Load Data from YAML File: Loads test data from a YAML file using the load_test_data function.

  • Step 2 Filter Data by Last and First Name: Applies filters to the test data based on the last and first names if they are fixed.

  • Step 3 Set i index to 0 if mode is GETCB_GET_VALUE next code in function will return current row

  • Step 4 If the callback mode is GETCB_GET_VALUE, no additional action is taken, as the next code will return the current row.

  • Step 5 Find current row if callback mode is GETCB_GETNEXT_VALUE

  • Step 6 If the row doesn't exist, exit with an error

  • Step 7 Create an python object

  • Step 8 Save return data and mark if more data exists

  • Step 9 Return status

get_phone - callback function Here is example of final get_phone function that contains actual code with detailed comments.

def get_phone(self, silcb: SILCbGet):
    """
    Get database object callback
    Path: list /address_get_2:addresses/address/phone

    Parameters
    ----------
        silcb: SILCbGet
            Control block

    Returns
    -------
        STATUS_T
            status of the callback.

    Additional
    ----------
        silcb.ancestor_keys: dict with ancestor keys
            silcb.ancestor_keys["address__last-name"]: str
                Ancestor key leaf 'last-name' in list 'address' Path: /address_get_2:addresses/address/last-name
            silcb.ancestor_keys["address__first-name"]: str
                Ancestor key leaf 'first-name' in list 'address' Path: /address_get_2:addresses/address/first-name
        silcb.local_keys: dict with local keys
            silcb.local_keys["phone-type"]: str
                Local key leaf 'phone-type' in list 'phone' Path: /address_get_2:addresses/address/phone/phone-type
        silcb.local_keys_present: list of present and valid local keys
        silcb.local_keys_fixed: list of local keys fixed in a getnext request
    """
    self.log(
        f"Enter 'get_phone' {self.name}@{self.revision} ancestor_keys={silcb.ancestor_keys} local_keys={silcb.local_keys}"
    )

    # load data from YAML file
    test_data = load_test_data()

    # find specific address
    address = None
    for r in test_data:
        if (
            r.get("last-name") == silcb.ancestor_keys["last-name"]
            and r.get("first-name") == silcb.ancestor_keys["first-name"]
        ):
            address = r
            break

    if not address:
        return STATUS_T.ERR_NCX_NO_INSTANCE

    # now test_data is data from specific address
    test_data = address.get("phone", [])

    # apply filters by phone-type
    if "phone-type" in silcb.local_keys_fixed:
        test_data = [
            r
            for r in test_data
            if r.get("phone-type") == silcb.local_keys["phone-type"]
        ]

    test_data_len = len(test_data)
    print("FILTERED DATA: %s" % test_data_len)

    # by default - first row
    i = 0
    if silcb.cbmode == GETCB_MODE_T.GETCB_GET_VALUE:
        pass
    if silcb.cbmode == GETCB_MODE_T.GETCB_GETNEXT_VALUE:
        # find currecnt row first
        while test_data_len > i:
            print(test_data[i].get("phone-type"))
            if test_data[i].get("phone-type") == silcb.local_keys["phone-type"]:
                i += 1
                break
            i += 1

    # if the row doesn't exists - exit with error
    if test_data_len <= i:
        return STATUS_T.ERR_NCX_NO_INSTANCE

    curr = test_data[i]

    # return data
    phone = {
        "phone-type": curr.get("phone-type"),
        "phone-number": curr.get("phone-number"),
    }

    silcb.add_return_data(data=phone, has_more=(test_data_len > i + 1))
    return STATUS_T.NO_ERR

load_test_data Here is example of load_test_data() function. It should be inserted into u_address_get_2.py file and it's not auto-generated. This function reads the content of the 'tutorial_addressbook.yaml' file, which is expected to be located in the same directory as this script. It uses the 'yaml' library to parse the YAML content and returns it in the form of a Python dictionary.

Note

This is an example file, and it is optional step. The callbacks can use hard wired values for testing purposes.

import yaml

def load_test_data():
    from pathlib import Path

    with open("%s/tutorial_addressbook.yaml" % Path(__file__).parent.resolve(), "r") as f:
        data = yaml.safe_load(f)
    return data

tutorial_addressbook.yaml

YAML file with actual data for retrieval. Need to be located in the same folder as your project:

- first-name: John
  last-name: Doe
  street: 123 Main St
  city: Anytown
  zipcode: 12345
  phone:
    - phone-type: cell
      phone-number: 555-1234
    - phone-type: home
      phone-number: 535-5644
    - phone-type: work
      phone-number: 555-5678

- first-name: Jane
  last-name: Smith
  street: 456 Oak Ave
  city: Somewhere
  zipcode: 67890
  phone:
    - phone-type: cell
      phone-number: 555-4321
    - phone-type: home
      phone-number: 555-8765

- first-name: Michael
  last-name: Johnson
  street: 789 Pine Ln
  city: Cityville
  zipcode: 45678
  phone:
    - phone-type: cell
      phone-number: 555-1111
    - phone-type: home
      phone-number: 555-2222
    - phone-type: work
      phone-number: 555-3333

- first-name: Emily
  last-name: Williams
  street: 101 Elm St
  city: Townsville
  zipcode: 98765
  phone:
    - phone-type: cell
      phone-number: 555-3333
    - phone-type: work
      phone-number: 555-4444

- first-name: Robert
  last-name: Brown
  street: 202 Maple Blvd
  city: Villagetown
  zipcode: 54321
  phone:
    - phone-type: cell
      phone-number: 555-5555
    - phone-type: work
      phone-number: 555-6666

- first-name: Bob
  last-name: Smith
  street: 456 Oak Ave
  city: Somewhere
  zipcode: 67890

- first-name: Keks
  last-name: Smith
  street: 456 Oak Ave
  city: Somewhere
  zipcode: 67890
  phone:
    - phone-type: cell
      phone-number: 555-4321
    - phone-type: work
      phone-number: 555-8765
    - phone-type: home
      phone-number: 555-9999

- first-name: Sarah
  last-name: Johnson
  street: 321 Pine Ln
  city: Cityville
  zipcode: 45678
  phone:
    - phone-type: cell
      phone-number: 555-7777
    - phone-type: work
      phone-number: 555-8888

- first-name: Alex
  last-name: White
  street: 505 Cedar St
  city: Villagetown
  zipcode: 54321
  phone:
    - phone-type: work
      phone-number: 555-0000
    - phone-type: home
      phone-number: 555-1111

Final project folder structure

pysil-tutorial/
  |-- address_get_2/
  |    |-- u_address_get_2.py
  |    |-- y_address_get_2.py
  |    |-- tutorial_addressbook.yaml
  |-- address_get_2.yang

Test environment

Follow next steps to create a test environment for the PY-SIL code. Use dedicated terminal windows for each process.

Server startup

> netconfd-pro --log-level=debug4 --module=address_get_2
--access-control=off --modpath=$HOME/pysil-tutorial

This command launches netconfd-pro with the following configuration:

  • --log-level=debug4: Sets the logging level to debug4, providing detailed debug information. Adjust the log level based on your debugging needs.

  • --module=address_get_2: Specifies the YANG module named address_get_2 for NETCONF operations.

  • --access-control=off: Disables access control, which means that there are no restrictions on who can access the NETCONF server. Intended solely for illustrative purposes.

  • --modpath=[path to your YANG modules folder]: Specifies folder where address_get_2.yang is located.

Python SIL-SA app startup

> py-sil-app.py --log-level=debug3 --pymodpath=$HOME/pysil-tutorial
  • --pymodpath=[path to your PY-SIL project folder]: Specifies folder where your PY-SIL libraries are located. In this tutorial that would be pysil-tutorial/ directory. Note, the path is not address_get_2/ but the parent directory pysil-tutorial/ where all the PY-SIL libraries are located.

Note

If the --pymodpath CLI parameter is not provided, the default search path for PY-SIL code is $HOME/pysil directory. If it is not found in this default location the application will attempt to locate PY-SIL in the current directory.

yangcli-pro startup

> yangcli-pro --server=localhost --user=[your user] --password=[your user password]

Launch yangcli-pro to connect to a NETCONF server. This command initiates a connection to a NETCONF server with the specified parameters:

  • --server=localhost: Specifies that the NETCONF server is located on the local machine. Adjust the localhost part if the server is on a different host.

  • --user=[your user]: Replace [your user] with the actual username you want to use for authentication.

  • --password=[your user password]: Replace [your user password] with the actual password corresponding to the specified username.

Make sure to replace the placeholders with your actual credentials.

Example Usage

You could try following commands using yangcli-pro:

Get all data

> sget /addresses

Server should reply with the following:

rpc-reply {
data {
    addresses {
    address  Doe John {
        last-name Doe
        first-name John
        street '123 Main St'
        city Anytown
        phone  cell {
        phone-type cell
        phone-number 555-1234
        }
    }
    address  Smith Jane {
        last-name Smith
        first-name Jane
        street '456 Oak Ave'
        city Somewhere
        phone  cell {
        phone-type cell
        phone-number 555-4321
        }
    }
    address  Johnson Michael {
        last-name Johnson
        first-name Michael
        street '789 Pine Ln'
        city Cityville
        phone  cell {
        phone-type cell
        phone-number 555-1111
        }
    }
    address  Williams Emily {
        last-name Williams
        first-name Emily
        street '101 Elm St'
        city Townsville
        phone  cell {
        phone-type cell
        phone-number 555-3333
        }
    }
    address  Brown Robert {
        last-name Brown
        first-name Robert
        street '202 Maple Blvd'
        city Villagetown
        phone  cell {
        phone-type cell
        phone-number 555-5555
        }
    }
    address  Smith Bob {
        last-name Smith
        first-name Bob
        street '456 Oak Ave'
        city Somewhere
    }
    address  Smith Keks {
        last-name Smith
        first-name Keks
        street '456 Oak Ave'
        city Somewhere
        phone  cell {
        phone-type cell
        phone-number 555-4321
        }
    }
    address  Johnson Sarah {
        last-name Johnson
        first-name Sarah
        street '321 Pine Ln'
        city Cityville
        phone  cell {
        phone-type cell
        phone-number 555-7777
        }
    }
    address  White Alex {
        last-name White
        first-name Alex
        street '505 Cedar St'
        city Villagetown
        phone  work {
        phone-type work
        phone-number 555-0000
        }
        phone  home {
        phone-type home
        phone-number 555-1111
        }
    }
    }
}
}

Filter by key

> sget /addresses/address[last-name="Smith"]

Server should reply with the following:

rpc-reply {
    data {
        addresses {
        address  Smith Jane {
            last-name Smith
            first-name Jane
            street '456 Oak Ave'
            city Somewhere
            phone  cell {
            phone-type cell
            phone-number 555-4321
            }
        }
        address  Smith Bob {
            last-name Smith
            first-name Bob
            street '456 Oak Ave'
            city Somewhere
        }
        address  Smith Keks {
            last-name Smith
            first-name Keks
            street '456 Oak Ave'
            city Somewhere
            phone  cell {
            phone-type cell
            phone-number 555-4321
            }
        }
        }
    }
}

Get exact entry

> sget /addresses/address[last-name="Smith"][first-name="Keks"]

Server should reply with exact entry selected by two keys:

rpc-reply {
    data {
        addresses {
        address  Smith Keks {
            last-name Smith
            first-name Keks
            street '456 Oak Ave'
            city Somewhere
            phone  cell {
            phone-type cell
            phone-number 555-4321
            }
        }
        }
    }
}