PY-SIL py-sil-app

py-sil-app

The py-sil-app program provides a complete functioning application for implementing YANG object callback functions in a subsystem. It provides complete functionality for the PY-SIL Subsystem. The netconfd-pro program must be built with the make flags WITH_PY_SIL and WITH_YCONTROL.

Note

This program is a complete application. The PY-SIL functionality will only be supported if this subsystem application is used correctly.

SYNOPSIS

py-sil-app [--pymodpath=string] [--address=string] [--port=num]
       [--subsys=<subsys-id>] [--library=name] [--retry-limit=num]  [--library=name]
       [<logging-parameters>]

USAGE

The py-sil-app program does not use a YANG-based configuration file.

If py-sil-app is called without any parameters, it will attempt to connect to the server with default settings (e.g. subsys-id=subsys1).

py-sil-app supports the following CLI parameters:

  • --address=string

    The --address parameter is optional. It represents the IP address of the main server that will be used for the session. The default is localhost.

  • --library=string

    The --library parameter keyword is optional. It specifies a PY-SIL library (module or bundle) that should be selected by this subsystem. If any --library parameters are present, then only those PY-SIL libraries will be loaded by this subsystem, if the main server indicates that the module or bundle is loaded. This option can be used multiple times, once for each selected module or bundle.

    If no --library parameters are present then the py-sil-app will attempt to load all PY-SIL modules and bundles reported by the server.

    If the PY-SIL library is not found then it will be skipped by the subsystem.

    Example:
    
    --library=foo  (selects libfoo_sa.so)
    
  • --log-level=enum

    The --log-level, --log, and other logging parameters are also supported.

  • --port=uint16

    The --port parameter is optional. It represents the TCP port number of the main server that will be used for the session. The default is the local system, determined by the local netconf-subsystem-pro.

  • --pymodpath=string Specifies the PY-SIL libraries search path to use while searching for PY-SIL libraries.

  • --retry-limit=num

    The --retry-limit parameter will cause the subsystem to terminate after the specified number of re-connect attempts. The default value is 5000 re-connect attempts. The parameter is a uint16 number between 1 and 65535.

  • --subsys-id=string

    The --subsys-id parameter keyword is optional. It specifies the subsystem identifier to use when registering with the netconfd-pro server. The default is 'subsys1' if no value is specified.

Starting PY-SIL Subsystems with py-sil-app

If the server is built using the WITH_PY_SIL=1 and WITH_YCONTROL=1 or EVERYTHING=1 make flag then it will listen for "sil-sa" service connections from PY-SIL subsystems.

The py-sil-app program can be used to support the PY-SIL libraries on a subsystem.

A subsystem can run on the same system as netconfd-pro or a different system.

A subsystem can be started before or after the main server. Connect and re-connect functionality is built into the program.

The PY-SIL libraries loaded depends on 2 factors:

  • The <module> and <bundle> parameters included in the <register-response> message from the server. The py-sil-app program will attempt to find the PY-SIL libraries for these modules and bundles.

  • By default the program will look in the ./ and ~/pysil directories. If a library is not found, then the module will be skipped. The PY-SIL libraries supported on a subsystem can be controlled by limiting which PY-SIL libraries are present.

  • Optional you can provide --pymodpath to specify search path to use while searching for PY-SIL libraries.

Example: Start the server with a non-default socket:

# server started
> netconfd-pro --socket-type=tcp --socket-address=192.168.0.45 --socket-port=8090

# py-sil-app started
> py-sil-app --subsys-id=sub1 --address=192.168.0.45 --port=8090

Example 2: Start 2 subsystems on the same host:

# server started
> netconfd-pro --module=mod1 --module=mod2

# py-sil-app started
> py-sil-app --subsys-id=sub1 --library=mod1
> py-sil-app --subsys-id=sub2 --library=mod2

PY-SIL py-sil-app CLI Reference

py-sil-app CLI Reference

PY-SIL Application Example

The following file can be found in the /usr/share/yumapro/py-sil-app/py-sil-app.py directory.

#!/usr/bin/env python3

"""
----------------------------------------------------------------------------
Copyright (c) 2024, YumaWorks, Inc., All Rights Reserved.

Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied.  See the License for the
specific language governing permissions and limitations
under the License.
----------------------------------------------------------------------------
"""

import os
import sys
import time
import pycontrol
import pysil

from typing import *

from pysilcommon import types as pysiltypes
from pysilcommon.addons import set_py_sil_modpath, set_log_level, enable_log_colors
from pysilcommon.addons import log as addon_log


STATUS_T = pysiltypes.STATUS_T_


def log(msg: str, log_level="INFO"):
    """Logging function"""
    addon_log(f"PY-SIL-APP: {msg}", log_level)


def get_argv_param(
    argv: list,
    name: str,
    err_empty: bool = False,
    error_not_present: bool = False,
    remove: bool = False,
) -> (int, str):
    """
    search parameters in argv list

    argv - list of cli parameters
    name - name of parameter to search
    err_empty - returns error if parameter exists but empty
    error_not_present - returns error if parameter doesn't exist
    remove - remove from argv

    * RETURNS:
    *   (status code, value)
    """
    full_name = f"--{name}"
    l = len(full_name)
    val = None
    for i, v in enumerate(argv):
        if v.startswith(full_name):
            if l == len(v):
                # found, no value means bool True
                val = True
            elif v[l] == "=":
                # found, value after "="
                val = v[(l + 1) :]
            else:
                # diffenet parameter with the same start letters
                continue
            if remove:
                # remove from argv
                del argv[i]
            break

    if (error_not_present and val is None) or (err_empty and val == ""):
        return (STATUS_T.ERR_NCX_INVALID_VALUE, val)
    return (STATUS_T.NO_ERR, val)


def print_usage():
    """
    * FUNCTION print_usage
    *
    * Print the program usage text
    """
    print("\nUsage:")
    print(
        "\n   py-sil-app [--pymodpath=string] [<logging-parameters>] "
        "[--subsys-id=string] [--library=name] [--retry-limit=num]"
    )
    print(
        "\nPy-sil-app is a sample YumaPro subsystem program to demonstrate how "
        "the PY-SIL and YControl libraries can be used within an application."
        "\nRefer to the online YumaPro Developer Manual for full details on PY-SIL development:"
        "\n\nhttps://docs.yumaworks.com/en/latest/dev/index.html."
    )
    print("\nExample:")
    print("\n   py-sil-app --pymodpath=/home/user/pysil --library=sample")
    exit()


def get_cli_parms(argv: list) -> (pysiltypes.status_t, dict):
    """
    /********************************************************************
    * FUNCTION get_cli_parms
    *
    * Check the CLI parameters for the getconfig commands
    *
    * RETURNS:

    *   status code
    * dict with parameters:
                char **address,
                uint16 *port,
                boolean *if_notif,
                uint16 *retry_limit
    *********************************************************************/
    """
    params = {
        "address": None,
        "port": 0,
        "if_notif": False,
        "retry_limit": 0,
        "log_level": 3,  # info
    }

    ncx_invalid_value_response = (STATUS_T.ERR_NCX_INVALID_VALUE, params)

    ad = "--address="
    po = "--port="
    li = "--library="
    if_notif = "--with-notif"
    rl = "--retry-limit="
    i = 1
    for a in argv:
        if a.startswith(ad):
            params["address"] = a[len(ad) :]
            if not params["address"]:
                return ncx_invalid_value_response

        elif a.startswith(if_notif):
            params["if_notif"] = True
        elif a.startswith(po):
            try:
                params["port"] = int(a[len(po) :])
                if not (0 < params["port"] <= 65535):
                    return ncx_invalid_value_response
            except:
                return ncx_invalid_value_response

        elif a.startswith(li):
            library = a[len(li) :]
            if not library:
                return ncx_invalid_value_response

            res = pysil.sil_sa_add_library_parm(library)
            if res != STATUS_T.NO_ERR:
                return ncx_invalid_value_response
            if "library" not in params:
                params["library"] = []
            params["library"].append(library)

        elif a.startswith(rl):
            try:
                params["retry_limit"] = int(a[len(rl) :])
                if not (0 < params["retry_limit"] <= 65535):
                    return ncx_invalid_value_response
            except:
                return ncx_invalid_value_response

        # else ignore because probably an NCX CLI parm

    return (STATUS_T.NO_ERR, params)


# end get_cli_parms

if __name__ == "__main__":
    argv = sys.argv

    # get log_level param
    res, log_level = get_argv_param(argv, name="log-level")
    # set log level value
    if log_level:
        set_log_level(log_level)

    # Redirect stdout to a file if --log= param provided to app
    res, log_file_name = get_argv_param(argv, name="log", remove=True)
    res, log_append = get_argv_param(argv, name="log-append", remove=True)
    if log_file_name:
        mode = "a" if log_append else "w"
        log_file = open(log_file_name, mode)
        sys.stdout.flush()
        os.dup2(log_file.fileno(), sys.stdout.fileno())
    else:
        # allow colors for console only
        enable_log_colors()

    try:
        # log(f"START PY-SIL")
        ycontrol_done = False

        # set PY_SIL_MODPATH from input parameters
        res, pymodpath = get_argv_param(
            argv, name="pymodpath", err_empty=True, error_not_present=False, remove=True
        )
        if res != STATUS_T.NO_ERR:
            print_usage()

        if pymodpath:
            set_py_sil_modpath(pymodpath)

        """
        need to check for the subsys-id parm before
        the system is initialized
        """
        res, subsys = get_argv_param(argv, name="subsys-id", err_empty=True)
        if res != STATUS_T.NO_ERR:
            print_usage()

        # 1) setup yumapro messaging service profile
        if res == STATUS_T.NO_ERR:
            if subsys is None:
                subsys = "subsys1"
            log(f"subsys={subsys}")
            # cut parameters - remove name of python file
            # argv = sys.argv[1:]
            log(f"Pass parameters to ycontrol_init: {argv}")

            log("pycontrol.ycontrol_init")

            pycontrol.ycontrol_init(len(argv), argv, subsys)
            log(f"ycontrol_init res={res}")
            ycontrol_done = True

        # 2) register services with the control layer
        if res == STATUS_T.NO_ERR:
            log("pysil.sil_sa_register_service")
            res = pysil.sil_sa_register_service()
            log(f"sil_sa_register_service res={res}")

        # get the CLI parameters after the system is initialized
        # so library parameter handled correctly
        if res == STATUS_T.NO_ERR:
            res, params = get_cli_parms(argv)
            if res != STATUS_T.NO_ERR:
                print_usage()

        # set the retry limit if provided
        if res == STATUS_T.NO_ERR and params["retry_limit"] > 0:
            log("pycontrol.ycontrol_set_retry_limit")
            pycontrol.ycontrol_set_retry_limit(params["retry_limit"])

        # 3) do 2nd stage init of the control manager (connect to server)
        if res == STATUS_T.NO_ERR:
            if params["address"]:
                if params["port"] == 0:
                    params["port"] = 2023
                # res = ycontrol_init2_ha("server1", address, port)
                res = pycontrol.ycontrol_init2_ha(
                    "server1", params["address"], params["port"]
                )
            else:
                log("pycontrol.ycontrol_init2")
                res = pycontrol.ycontrol_init2()

        sleep_val = 0.01  # 1/100 sec
        done = False

        # 4) call ycontrol_check_io periodically from the main program control loop
        log("wait cycle")
        while not done and res == STATUS_T.NO_ERR:
            res = pycontrol.ycontrol_check_io()

            if pycontrol.ycontrol_shutdown_now():
                # YControl has received a <shutdown-event>
                # from the server subsystem is no longer active
                # could ignore or shut down YControl IO loop
                log(f"YControl has received a <shutdown-event>")
                done = True

            # Using sleep to represent other program work; remove for real
            if not done and res == STATUS_T.NO_ERR:
                time.sleep(sleep_val)

        log("Done, result: %s" % res)

    except Exception as e:
        print(f"An error occurred: {e}")

# Close the log file when done
if log_file_name:
    log_file.close()