diff --git a/clear/main.py b/clear/main.py index 4302ae00aa..bb7e475630 100755 --- a/clear/main.py +++ b/clear/main.py @@ -452,8 +452,7 @@ def translations(): # Load plugins and register them helper = util_base.UtilHelper() -for plugin in helper.load_plugins(plugins): - helper.register_plugin(plugin, cli) +helper.load_and_register_plugins(plugins, cli) if __name__ == '__main__': diff --git a/clear/plugins/auto/__init__.py b/clear/plugins/auto/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/config/main.py b/config/main.py index e9bab3172d..a728c7bd32 100644 --- a/config/main.py +++ b/config/main.py @@ -4562,8 +4562,7 @@ def delete(ctx): # Load plugins and register them helper = util_base.UtilHelper() -for plugin in helper.load_plugins(plugins): - helper.register_plugin(plugin, config) +helper.load_and_register_plugins(plugins, config) if __name__ == '__main__': diff --git a/config/plugins/auto/__init__.py b/config/plugins/auto/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/setup.py b/setup.py index 15f93b46f7..0240125036 100644 --- a/setup.py +++ b/setup.py @@ -23,8 +23,10 @@ 'acl_loader', 'clear', 'clear.plugins', + 'clear.plugins.auto', 'config', 'config.plugins', + 'config.plugins.auto', 'connect', 'consutil', 'counterpoll', @@ -46,6 +48,7 @@ 'show', 'show.interfaces', 'show.plugins', + 'show.plugins.auto', 'sonic_installer', 'sonic_installer.bootloader', 'sonic_package_manager', @@ -54,6 +57,7 @@ 'undebug', 'utilities_common', 'watchdogutil', + 'sonic_cli_gen', ], package_data={ 'show': ['aliases.ini'], @@ -157,6 +161,7 @@ 'spm = sonic_package_manager.main:cli', 'undebug = undebug.main:cli', 'watchdogutil = watchdogutil.main:watchdogutil', + 'sonic-cli-gen = sonic_cli_gen.main:cli', ] }, install_requires=[ diff --git a/show/main.py b/show/main.py index b0b2986a78..9afb0217e9 100755 --- a/show/main.py +++ b/show/main.py @@ -1473,8 +1473,7 @@ def ztp(status, verbose): # Load plugins and register them helper = util_base.UtilHelper() -for plugin in helper.load_plugins(plugins): - helper.register_plugin(plugin, cli) +helper.load_and_register_plugins(plugins, cli) if __name__ == '__main__': diff --git a/show/plugins/auto/__init__.py b/show/plugins/auto/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/sonic-utilities-data/bash_completion.d/sonic-cli-gen b/sonic-utilities-data/bash_completion.d/sonic-cli-gen new file mode 100644 index 0000000000..3327f9c513 --- /dev/null +++ b/sonic-utilities-data/bash_completion.d/sonic-cli-gen @@ -0,0 +1,8 @@ +_sonic_cli_gen_completion() { + COMPREPLY=( $( env COMP_WORDS="${COMP_WORDS[*]}" \ + COMP_CWORD=$COMP_CWORD \ + _SONIC_CLI_GEN_COMPLETE=complete $1 ) ) + return 0 +} + +complete -F _sonic_cli_gen_completion -o default sonic-cli-gen; diff --git a/sonic-utilities-data/debian/install b/sonic-utilities-data/debian/install index 82d087d54d..1f67b78c20 100644 --- a/sonic-utilities-data/debian/install +++ b/sonic-utilities-data/debian/install @@ -1,2 +1,3 @@ -bash_completion.d/ /etc/ -templates/*.j2 /usr/share/sonic/templates/ +bash_completion.d/ /etc/ +templates/*.j2 /usr/share/sonic/templates/ +templates/sonic-cli-gen/*.j2 /usr/share/sonic/templates/sonic-cli-gen/ diff --git a/sonic-utilities-data/templates/sonic-cli-gen/common.j2 b/sonic-utilities-data/templates/sonic-cli-gen/common.j2 new file mode 100644 index 0000000000..3b83ee5635 --- /dev/null +++ b/sonic-utilities-data/templates/sonic-cli-gen/common.j2 @@ -0,0 +1,3 @@ +{% macro cli_name(name) -%} +{{ name|lower|replace("_", "-") }} +{%- endmacro %} diff --git a/sonic-utilities-data/templates/sonic-cli-gen/config.py.j2 b/sonic-utilities-data/templates/sonic-cli-gen/config.py.j2 new file mode 100644 index 0000000000..7706ae3940 --- /dev/null +++ b/sonic-utilities-data/templates/sonic-cli-gen/config.py.j2 @@ -0,0 +1,570 @@ +{%- from "common.j2" import cli_name -%} +""" +Autogenerated config CLI plugin. +{% if source_template is defined %} +Source template: {{ source_template }} +{% endif %} +{% if source_yang_module is defined %} +Source YANG module: {{ source_yang_module }} +{% endif %} +""" + +import copy +import click +import utilities_common.cli as clicommon +import utilities_common.general as general +from config import config_mgmt + + +# Load sonic-cfggen from source since /usr/local/bin/sonic-cfggen does not have .py extension. +sonic_cfggen = general.load_module_from_source('sonic_cfggen', '/usr/local/bin/sonic-cfggen') + + +def exit_with_error(*args, **kwargs): + """ Print a message with click.secho and abort CLI. + + Args: + args: Positional arguments to pass to click.secho + kwargs: Keyword arguments to pass to click.secho + """ + + click.secho(*args, **kwargs) + raise click.Abort() + + +def validate_config_or_raise(cfg): + """ Validate config db data using ConfigMgmt. + + Args: + cfg (Dict): Config DB data to validate. + Raises: + Exception: when cfg does not satisfy YANG schema. + """ + + try: + cfg = sonic_cfggen.FormatConverter.to_serialized(copy.deepcopy(cfg)) + config_mgmt.ConfigMgmt().loadData(cfg) + except Exception as err: + raise Exception('Failed to validate configuration: {}'.format(err)) + + +def add_entry_validated(db, table, key, data): + """ Add new entry in table and validate configuration. + + Args: + db (swsscommon.ConfigDBConnector): Config DB connector obect. + table (str): Table name to add new entry to. + key (Union[str, Tuple]): Key name in the table. + data (Dict): Entry data. + Raises: + Exception: when cfg does not satisfy YANG schema. + """ + + cfg = db.get_config() + cfg.setdefault(table, {}) + if key in cfg[table]: + raise Exception(f"{key} already exists") + + cfg[table][key] = data + + validate_config_or_raise(cfg) + db.set_entry(table, key, data) + + +def update_entry_validated(db, table, key, data, create_if_not_exists=False): + """ Update entry in table and validate configuration. + If attribute value in data is None, the attribute is deleted. + + Args: + db (swsscommon.ConfigDBConnector): Config DB connector obect. + table (str): Table name to add new entry to. + key (Union[str, Tuple]): Key name in the table. + data (Dict): Entry data. + create_if_not_exists (bool): + In case entry does not exists already a new entry + is not created if this flag is set to False and + creates a new entry if flag is set to True. + Raises: + Exception: when cfg does not satisfy YANG schema. + """ + + cfg = db.get_config() + cfg.setdefault(table, {}) + + if not data: + raise Exception(f"No field/values to update {key}") + + if create_if_not_exists: + cfg[table].setdefault(key, {}) + + if key not in cfg[table]: + raise Exception(f"{key} does not exist") + + entry_changed = False + for attr, value in data.items(): + if value == cfg[table][key][attr]: + continue + entry_changed = True + if value is None: + cfg[table][key].pop(attr, None) + else: + cfg[table][key][attr] = value + + if not entry_changed: + return + + validate_config_or_raise(cfg) + db.set_entry(table, key, cfg[table][key]) + + +def del_entry_validated(db, table, key): + """ Delete entry in table and validate configuration. + + Args: + db (swsscommon.ConfigDBConnector): Config DB connector obect. + table (str): Table name to add new entry to. + key (Union[str, Tuple]): Key name in the table. + Raises: + Exception: when cfg does not satisfy YANG schema. + """ + + cfg = db.get_config() + cfg.setdefault(table, {}) + if key not in cfg[table]: + raise Exception(f"{key} does not exist") + + cfg[table].pop(key) + + validate_config_or_raise(cfg) + db.set_entry(table, key, None) + + +def add_list_entry_validated(db, table, key, attr, data): + """ Add new entry into list in table and validate configuration. + + Args: + db (swsscommon.ConfigDBConnector): Config DB connector obect. + table (str): Table name to add data to. + key (Union[str, Tuple]): Key name in the table. + attr (str): Attribute name which represents a list the data needs to be added to. + data (List): Data list to add to config DB. + Raises: + Exception: when cfg does not satisfy YANG schema. + """ + + cfg = db.get_config() + cfg.setdefault(table, {}) + if key not in cfg[table]: + raise Exception(f"{key} does not exist") + cfg[table][key].setdefault(attr, []) + for entry in data: + if entry in cfg[table][key][attr]: + raise Exception(f"{entry} already exists") + cfg[table][key][attr].append(entry) + + validate_config_or_raise(cfg) + db.set_entry(table, key, cfg[table][key]) + + +def del_list_entry_validated(db, table, key, attr, data): + """ Delete entry from list in table and validate configuration. + + Args: + db (swsscommon.ConfigDBConnector): Config DB connector obect. + table (str): Table name to remove data from. + key (Union[str, Tuple]): Key name in the table. + attr (str): Attribute name which represents a list the data needs to be removed from. + data (Dict): Data list to remove from config DB. + Raises: + Exception: when cfg does not satisfy YANG schema. + """ + + cfg = db.get_config() + cfg.setdefault(table, {}) + if key not in cfg[table]: + raise Exception(f"{key} does not exist") + cfg[table][key].setdefault(attr, []) + for entry in data: + if entry not in cfg[table][key][attr]: + raise Exception(f"{entry} does not exist") + cfg[table][key][attr].remove(entry) + if not cfg[table][key][attr]: + cfg[table][key].pop(attr) + + validate_config_or_raise(cfg) + db.set_entry(table, key, cfg[table][key]) + + +def clear_list_entry_validated(db, table, key, attr): + """ Clear list in object and validate configuration. + + Args: + db (swsscommon.ConfigDBConnector): Config DB connector obect. + table (str): Table name to remove the list attribute from. + key (Union[str, Tuple]): Key name in the table. + attr (str): Attribute name which represents a list that needs to be removed. + Raises: + Exception: when cfg does not satisfy YANG schema. + """ + + update_entry_validated(db, table, key, {attr: None}) + + +{# Generate click arguments macro +Jinja2 Call: + {{ gen_click_arguments([{"name": "leaf1", "is-leaf-list": False}, + {"name": "leaf2", "is-leaf-list": Talse}) }} +Result: +@click.argument( + "leaf1", + nargs=1, + required=True, +) +@click.argument( + "leaf2", + nargs=-1, + required=True, +) +#} +{%- macro gen_click_arguments(attrs) -%} +{%- for attr in attrs %} +@click.argument( + "{{ cli_name(attr.name) }}", + nargs={% if attr["is-leaf-list"] %}-1{% else %}1{% endif %}, + required=True, +) +{%- endfor %} +{%- endmacro %} + + +{# Generate click options macro +Jinja2 Call: + {{ gen_click_arguments([{"name": "leaf1", "is-mandatory": True, "description": "leaf1-desc"}, + {"name": "leaf2", "is-mandatory": False, "description": "leaf2-desc"}) }} +Result: +@click.option( + "--leaf1", + help="leaf1-desc [mandatory]", +) +@click.option( + "--leaf2", + help="leaf2-desc", +) +#} +{%- macro gen_click_options(attrs) -%} +{%- for attr in attrs %} +@click.option( + "--{{ cli_name(attr.name) }}", + help="{{ attr.description }}{% if attr['is-mandatory'] %}[mandatory]{% endif %}", +) +{%- endfor %} +{%- endmacro %} + +{# Generate valid python identifier from input names #} +{% macro pythonize(attrs) -%} +{{ attrs|map(attribute="name")|map("lower")|map("replace", "-", "_")|join(", ") }} +{%- endmacro %} + +{% macro gen_cfg_obj_list_update(group, table, object, attr) %} +{% set list_update_group = group + "_" + attr.name %} + +@{{ group }}.group(name="{{ cli_name(attr.name) }}", + cls=clicommon.AliasedGroup) +def {{ list_update_group }}(): + """ Add/Delete {{ attr.name }} in {{ table.name }} """ + + pass + +{# Add entries to list attribute config CLI generation +E.g: + @TABLE_object.command(name="add") + @click.argument("key1", nargs=1) + @click.argument("key2", nargs=1) + @click.argument("attribute", nargs=-1) + def TABLE_object_attribute_add(db, key1, key2, attribute): +#} +@{{ list_update_group }}.command(name="add") +{{ gen_click_arguments(object["keys"] + [attr]) }} +@clicommon.pass_db +def {{ list_update_group }}_add( + db, + {{ pythonize(object["keys"] + [attr]) }} +): + """ Add {{ attr.name }} in {{ table.name }} """ + + table = "{{ table.name }}" + key = {{ pythonize(object["keys"]) }} + attr = "{{ attr.name }}" + data = {{ pythonize([attr]) }} + + try: + add_list_entry_validated(db.cfgdb, table, key, attr, data) + except Exception as err: + exit_with_error(f"Error: {err}", fg="red") + + +{# Delete entries from list attribute config CLI generation +E.g: + @TABLE_object.command(name="delete") + @click.argument("key1", nargs=1) + @click.argument("key2", nargs=1) + @click.argument("attribute", nargs=-1) + def TABLE_object_attribute_delete(db, key1, key2, attribute): +#} +@{{ list_update_group }}.command(name="delete") +{{ gen_click_arguments(object["keys"] + [attr]) }} +@clicommon.pass_db +def {{ list_update_group }}_delete( + db, + {{ pythonize(object["keys"] + [attr]) }} +): + """ Delete {{ attr.name }} in {{ table.name }} """ + + table = "{{ table.name }}" + key = {{ pythonize(object["keys"]) }} + attr = "{{ attr.name }}" + data = {{ pythonize([attr]) }} + + try: + del_list_entry_validated(db.cfgdb, table, key, attr, data) + except Exception as err: + exit_with_error(f"Error: {err}", fg="red") + + +{# Clear entries from list attribute config CLI generation +E.g: + @TABLE_object.command(name="delete") + @click.argument("key1", nargs=1) + @click.argument("key2", nargs=1) + def TABLE_object_attribute_clear(db, key1, key2): +#} +@{{ list_update_group }}.command(name="clear") +{{ gen_click_arguments(object["keys"]) }} +@clicommon.pass_db +def {{ list_update_group }}_clear( + db, + {{ pythonize(object["keys"]) }} +): + """ Clear {{ attr.name }} in {{ table.name }} """ + + table = "{{ table.name }}" + key = {{ pythonize(object["keys"]) }} + attr = "{{ attr.name }}" + + try: + clear_list_entry_validated(db.cfgdb, table, key, attr) + except Exception as err: + exit_with_error(f"Error: {err}", fg="red") + +{% endmacro %} + + +{% macro gen_cfg_obj_list_update_all(group, table, object) %} +{% for attr in object.attrs %} +{% if attr["is-leaf-list"] %} +{{ gen_cfg_obj_list_update(group, table, object, attr) }} +{% endif %} +{% endfor %} +{% endmacro %} + + +{% macro gen_cfg_static_obj_attr(table, object, attr) %} +@{{ table.name }}_{{ object.name }}.command(name="{{ cli_name(attr.name) }}") +{{ gen_click_arguments([attr]) }} +@clicommon.pass_db +def {{ table.name }}_{{ object.name }}_{{ attr.name }}(db, {{ pythonize([attr]) }}): + """ {{ attr.description }} """ + + table = "{{ table.name }}" + key = "{{ object.name }}" + data = { + "{{ attr.name }}": {{ pythonize([attr]) }}, + } + try: + update_entry_validated(db.cfgdb, table, key, data, create_if_not_exists=True) + except Exception as err: + exit_with_error(f"Error: {err}", fg="red") +{% endmacro %} + + +{# Static objects config CLI generation +E.g: + @TABLE.group(name="object") + def TABLE_object(db): +#} +{% macro gen_cfg_static_obj(table, object) %} +@{{ table.name }}.group(name="{{ cli_name(object.name) }}", + cls=clicommon.AliasedGroup) +@clicommon.pass_db +def {{ table.name }}_{{ object.name }}(db): + """ {{ object.description }} """ + + pass + +{# Static objects attributes config CLI generation +E.g: + @TABLE_object.command(name="attribute") + def TABLE_object_attribute(db, attribute): +#} +{% for attr in object.attrs %} +{{ gen_cfg_static_obj_attr(table, object, attr) }} +{% endfor %} + +{{ gen_cfg_obj_list_update_all(table.name + "_" + object.name, table, object) }} +{% endmacro %} + +{# Dynamic objects config CLI generation #} + +{# Dynamic objects add command +E.g: + @TABLE.command(name="add") + @click.argument("key1") + @click.argument("key2") + @click.option("--attr1") + @click.option("--attr2") + @click.option("--attr3") + def TABLE_TABLE_LIST_add(db, key1, key2, attr1, attr2, attr3): +#} +{% macro gen_cfg_dyn_obj_add(group, table, object) %} +@{{ group }}.command(name="add") +{{ gen_click_arguments(object["keys"]) }} +{{ gen_click_options(object.attrs) }} +@clicommon.pass_db +def {{ group }}_add(db, {{ pythonize(object["keys"] + object.attrs) }}): + """ Add object in {{ table.name }}. """ + + table = "{{ table.name }}" + key = {{ pythonize(object["keys"]) }} + data = {} +{%- for attr in object.attrs %} + if {{ pythonize([attr]) }} is not None: +{%- if not attr["is-leaf-list"] %} + data["{{ attr.name }}"] = {{ pythonize([attr]) }} +{%- else %} + data["{{ attr.name }}"] = {{ pythonize([attr]) }}.split(",") +{%- endif %} +{%- endfor %} + + try: + add_entry_validated(db.cfgdb, table, key, data) + except Exception as err: + exit_with_error(f"Error: {err}", fg="red") +{% endmacro %} + +{# Dynamic objects update command +E.g: + @TABLE.command(name="update") + @click.argument("key1") + @click.argument("key2") + @click.option("--attr1") + @click.option("--attr2") + @click.option("--attr3") + def TABLE_TABLE_LIST_update(db, key1, key2, attr1, attr2, attr3): +#} +{% macro gen_cfg_dyn_obj_update(group, table, object) %} +@{{ group }}.command(name="update") +{{ gen_click_arguments(object["keys"]) }} +{{ gen_click_options(object.attrs) }} +@clicommon.pass_db +def {{ group }}_update(db, {{ pythonize(object["keys"] + object.attrs) }}): + """ Add object in {{ table.name }}. """ + + table = "{{ table.name }}" + key = {{ pythonize(object["keys"]) }} + data = {} +{%- for attr in object.attrs %} + if {{ pythonize([attr]) }} is not None: +{%- if not attr["is-leaf-list"] %} + data["{{ attr.name }}"] = {{ pythonize([attr]) }} +{%- else %} + data["{{ attr.name }}"] = {{ pythonize([attr]) }}.split(",") +{%- endif %} +{%- endfor %} + + try: + update_entry_validated(db.cfgdb, table, key, data) + except Exception as err: + exit_with_error(f"Error: {err}", fg="red") +{% endmacro %} + +{# Dynamic objects delete command +E.g: + @TABLE.command(name="delete") + @click.argument("key1") + @click.argument("key2") + def TABLE_TABLE_LIST_delete(db, key1, key2): +#} +{% macro gen_cfg_dyn_obj_delete(group, table, object) %} +@{{ group }}.command(name="delete") +{{ gen_click_arguments(object["keys"]) }} +@clicommon.pass_db +def {{ group }}_delete(db, {{ pythonize(object["keys"]) }}): + """ Delete object in {{ table.name }}. """ + + table = "{{ table.name }}" + key = {{ pythonize(object["keys"]) }} + try: + del_entry_validated(db.cfgdb, table, key) + except Exception as err: + exit_with_error(f"Error: {err}", fg="red") +{% endmacro %} + +{% macro gen_cfg_dyn_obj(table, object) %} +{# Generate another nested group in case table holds two types of objects #} +{% if table["dynamic-objects"]|length > 1 %} +{% set group = table.name + "_" + object.name %} +@{{ table.name }}.group(name="{{ cli_name(object.name) }}", + cls=clicommon.AliasedGroup) +def {{ group }}(): + """ {{ object.description }} """ + + pass +{% else %} +{% set group = table.name %} +{% endif %} + +{{ gen_cfg_dyn_obj_add(group, table, object) }} +{{ gen_cfg_dyn_obj_update(group, table, object) }} +{{ gen_cfg_dyn_obj_delete(group, table, object) }} +{{ gen_cfg_obj_list_update_all(group, table, object) }} +{% endmacro %} + + +{% for table in tables %} +@click.group(name="{{ cli_name(table.name) }}", + cls=clicommon.AliasedGroup) +def {{ table.name }}(): + """ {{ table.description }} """ + + pass + +{% if "static-objects" in table %} +{% for object in table["static-objects"] %} +{{ gen_cfg_static_obj(table, object) }} +{% endfor %} +{% endif %} + +{% if "dynamic-objects" in table %} +{% for object in table["dynamic-objects"] %} +{{ gen_cfg_dyn_obj(table, object) }} +{% endfor %} +{% endif %} + +{% endfor %} + +def register(cli): + """ Register new CLI nodes in root CLI. + + Args: + cli: Root CLI node. + Raises: + Exception: when root CLI already has a command + we are trying to register. + """ + +{%- for table in tables %} + cli_node = {{ table.name }} + if cli_node.name in cli.commands: + raise Exception(f"{cli_node.name} already exists in CLI") + cli.add_command({{ table.name }}) +{%- endfor %} diff --git a/sonic-utilities-data/templates/sonic-cli-gen/show.py.j2 b/sonic-utilities-data/templates/sonic-cli-gen/show.py.j2 new file mode 100644 index 0000000000..2a3d065fdf --- /dev/null +++ b/sonic-utilities-data/templates/sonic-cli-gen/show.py.j2 @@ -0,0 +1,254 @@ +{% from "common.j2" import cli_name -%} +""" +Auto-generated show CLI plugin. +{% if source_template is defined %} +Source template: {{ source_template }} +{% endif %} +{% if source_yang_module is defined %} +Source YANG module: {{ source_yang_module }} +{% endif %} +""" + +import click +import tabulate +import natsort +import utilities_common.cli as clicommon + + +{% macro column_name(name) -%} +{{ name|upper|replace("_", " ")|replace("-", " ") }} +{%- endmacro %} + + +def format_attr_value(entry, attr): + """ Helper that formats attribute to be presented in the table output. + + Args: + entry (Dict[str, str]): CONFIG DB entry configuration. + attr (Dict): Attribute metadata. + + Returns: + str: fomatted attribute value. + """ + + if attr["is-leaf-list"]: + return "\n".join(entry.get(attr["name"], [])) + return entry.get(attr["name"], "N/A") + + +def format_group_value(entry, attrs): + """ Helper that formats grouped attribute to be presented in the table output. + + Args: + entry (Dict[str, str]): CONFIG DB entry configuration. + attrs (List[Dict]): Attributes metadata that belongs to the same group. + + Returns: + str: fomatted group attributes. + """ + + data = [] + for attr in attrs: + if entry.get(attr["name"]): + data.append((attr["name"] + ":", format_attr_value(entry, attr))) + return tabulate.tabulate(data, tablefmt="plain") + + +{# Generates a python list that represents a row in the table view. +E.g: +Jinja2: +{{ + gen_row("entry", [ + {"name": "leaf1"}, + {"name": "leaf_1"}, + {"name": "leaf_2"}, + {"name": "leaf_3", "group": "group_0"} + ]) +}} +Result: +[ + format_attr_value( + entry, + {'name': 'leaf1'} + ), + format_attr_value( + entry, + {'name': 'leaf_1'} + ), + format_attr_value( + entry, + {'name': 'leaf_2'} + ), + format_group_value( + entry, + [{'name': 'leaf_3', 'group': 'group_0'}] + ), +] +#} +{% macro gen_row(entry, attrs) -%} +[ +{%- for attr in attrs|rejectattr("group", "defined") %} + format_attr_value( + {{ entry }}, + {{ attr }} + ), +{%- endfor %} +{%- for group, attrs in attrs|selectattr("group", "defined")|groupby("group") %} +{%- if group == "" %} +{%- for attr in attrs %} + format_attr_value( + {{ entry }}, + {{ attr }} + ), +{%- endfor %} +{%- else %} + format_group_value( + {{ entry }}, + {{ attrs }} + ), +{%- endif %} +{%- endfor %} +] +{% endmacro %} + +{# Generates a list that represents a header in table view. +E.g: +Jinja2: {{ + gen_header([ + {"name": "key"}, + {"name": "leaf_1"}, + {"name": "leaf_2"}, + {"name": "leaf_3", "group": "group_0"} + ]) + }} + +Result: +[ + "KEY", + "LEAF 1", + "LEAF 2", + "GROUP 0", +] + +#} +{% macro gen_header(attrs) -%} +[ +{% for attr in attrs|rejectattr("group", "defined") -%} + "{{ column_name(attr.name) }}", +{% endfor -%} +{% for group, attrs in attrs|selectattr("group", "defined")|groupby("group") -%} +{%- if group == "" %} +{% for attr in attrs -%} + "{{ column_name(attr.name) }}", +{% endfor -%} +{%- else %} + "{{ column_name(group) }}", +{%- endif %} +{% endfor -%} +] +{% endmacro %} + + +{% for table in tables %} +{% if "static-objects" in table %} +{# For static objects generate a command group called against table name. +E.g: +@click.group(name="table-name", + cls=clicommon.AliasedGroup) +def TABLE_NAME(): + """ TABLE DESCRIPTION """ + + pass +#} +@click.group(name="{{ cli_name(table.name) }}", + cls=clicommon.AliasedGroup) +def {{ table.name }}(): + """ {{ table.description }} """ + + pass + +{% for object in table["static-objects"] %} +{# For every object in static table generate a command +in the group to show individual object configuration. +CLI command is named against the object key in DB. +E.g: +@TABLE_NAME.command(name="object-name") +@clicommon.pass_db +def TABLE_NAME_object_name(db): + ... +#} +@{{ table.name }}.command(name="{{ cli_name(object.name) }}") +@clicommon.pass_db +def {{ table.name }}_{{ object.name }}(db): + """ {{ object.description }} """ + + header = {{ gen_header(object.attrs) }} + body = [] + + table = db.cfgdb.get_table("{{ table.name }}") + entry = table.get("{{ object.name }}", {}) + row = {{ gen_row("entry", object.attrs) }} + body.append(row) + click.echo(tabulate.tabulate(body, header)) + +{% endfor %} +{% elif "dynamic-objects" in table %} +{% if table["dynamic-objects"]|length > 1 %} +@click.group(name="{{ cli_name(table.name) }}", + cls=clicommon.AliasedGroup) +def {{ table.name }}(): + """ {{ table.description }} """ + + pass +{% endif %} +{% for object in table["dynamic-objects"] %} +{# Generate another nesting group in case table holds two types of objects #} +{% if table["dynamic-objects"]|length > 1 %} +{% set group = table.name %} +{% set name = object.name %} +{% else %} +{% set group = "click" %} +{% set name = table.name %} +{% endif %} + +{# Generate an implementation to display table. #} +@{{ group }}.group(name="{{ cli_name(name) }}", + cls=clicommon.AliasedGroup, + invoke_without_command=True) +@clicommon.pass_db +def {{ name }}(db): + """ {{ object.description }} [Callable command group] """ + + header = {{ gen_header(object["keys"] + object.attrs) }} + body = [] + + table = db.cfgdb.get_table("{{ table.name }}") + for key in natsort.natsorted(table): + entry = table[key] + if not isinstance(key, tuple): + key = (key,) + + row = [*key] + {{ gen_row("entry", object.attrs) }} + body.append(row) + + click.echo(tabulate.tabulate(body, header)) +{% endfor %} +{% endif %} +{% endfor %} + +def register(cli): + """ Register new CLI nodes in root CLI. + + Args: + cli (click.core.Command): Root CLI node. + Raises: + Exception: when root CLI already has a command + we are trying to register. + """ + +{%- for table in tables %} + cli_node = {{ table.name }} + if cli_node.name in cli.commands: + raise Exception(f"{cli_node.name} already exists in CLI") + cli.add_command({{ table.name }}) +{%- endfor %} diff --git a/sonic_cli_gen/__init__.py b/sonic_cli_gen/__init__.py new file mode 100644 index 0000000000..e7e775c0fb --- /dev/null +++ b/sonic_cli_gen/__init__.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python + +from sonic_cli_gen.generator import CliGenerator + +__all__ = ['CliGenerator'] + diff --git a/sonic_cli_gen/generator.py b/sonic_cli_gen/generator.py new file mode 100644 index 0000000000..9d5bac6008 --- /dev/null +++ b/sonic_cli_gen/generator.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python + +import os +import pkgutil +import jinja2 + +from sonic_cli_gen.yang_parser import YangParser + +templates_path_switch = '/usr/share/sonic/templates/sonic-cli-gen/' + + +class CliGenerator: + """ SONiC CLI generator. This class provides public API + for sonic-cli-gen python library. It can generate config, + show CLI plugins. + + Attributes: + logger: logger + """ + + def __init__(self, logger): + """ Initialize CliGenerator. """ + + self.logger = logger + + + def generate_cli_plugin( + self, + cli_group, + plugin_name, + config_db_path='configDB', + templates_path='/usr/share/sonic/templates/sonic-cli-gen/' + ): + """ Generate click CLI plugin and put it to: + /usr/local/lib//dist-packages//plugins/auto/ + """ + + parser = YangParser( + yang_model_name=plugin_name, + config_db_path=config_db_path, + allow_tbl_without_yang=True, + debug=False + ) + # yang_dict will be used as an input for templates located in + # /usr/share/sonic/templates/sonic-cli-gen/ + yang_dict = parser.parse_yang_model() + + loader = jinja2.FileSystemLoader(templates_path) + j2_env = jinja2.Environment(loader=loader) + try: + template = j2_env.get_template(cli_group + '.py.j2') + except jinja2.exceptions.TemplateNotFound: + self.logger.error(' Templates for auto-generation does NOT exist in folder {}'.format(templates_path)) + + plugin_path = get_cli_plugin_path(cli_group, plugin_name + '_yang.py') + + with open(plugin_path, 'w') as plugin_py: + plugin_py.write(template.render(yang_dict)) + self.logger.info(' Auto-generation successful! Location: {}'.format(plugin_path)) + + + def remove_cli_plugin(self, cli_group, plugin_name): + """ Remove CLI plugin from directory: + /usr/local/lib//dist-packages//plugins/auto/ + """ + + plugin_path = get_cli_plugin_path(cli_group, plugin_name + '_yang.py') + + if os.path.exists(plugin_path): + os.remove(plugin_path) + self.logger.info(' {} was removed.'.format(plugin_path)) + else: + self.logger.warning(' Path {} doest NOT exist!'.format(plugin_path)) + + +def get_cli_plugin_path(command, plugin_name): + pkg_loader = pkgutil.get_loader(f'{command}.plugins.auto') + if pkg_loader is None: + raise Exception(f'Failed to get plugins path for {command} CLI') + plugins_pkg_path = os.path.dirname(pkg_loader.path) + + return os.path.join(plugins_pkg_path, plugin_name) + diff --git a/sonic_cli_gen/main.py b/sonic_cli_gen/main.py new file mode 100644 index 0000000000..bfcd301aed --- /dev/null +++ b/sonic_cli_gen/main.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python + +import sys +import click +import logging +from sonic_cli_gen.generator import CliGenerator + +logger = logging.getLogger('sonic-cli-gen') +logging.basicConfig(stream=sys.stdout, level=logging.INFO) + + +@click.group() +@click.pass_context +def cli(ctx): + """ SONiC CLI Auto-generator tool.\r + Generate click CLI plugin for 'config' or 'show' CLI groups.\r + CLI plugin will be generated from the YANG model, which should be in:\r\n + /usr/local/yang-models/ \n + Generated CLI plugin will be placed in: \r\n + /usr/local/lib/python3.7/dist-packages//plugins/auto/ + """ + + context = { + 'gen': CliGenerator(logger) + } + ctx.obj = context + + +@cli.command() +@click.argument('cli_group', type=click.Choice(['config', 'show'])) +@click.argument('yang_model_name', type=click.STRING) +@click.pass_context +def generate(ctx, cli_group, yang_model_name): + """ Generate click CLI plugin. """ + + ctx.obj['gen'].generate_cli_plugin(cli_group, yang_model_name) + + +@cli.command() +@click.argument('cli_group', type=click.Choice(['config', 'show'])) +@click.argument('yang_model_name', type=click.STRING) +@click.pass_context +def remove(ctx, cli_group, yang_model_name): + """ Remove generated click CLI plugin from. """ + + ctx.obj['gen'].remove_cli_plugin(cli_group, yang_model_name) + + +if __name__ == '__main__': + cli() + diff --git a/sonic_cli_gen/yang_parser.py b/sonic_cli_gen/yang_parser.py new file mode 100644 index 0000000000..df0382536f --- /dev/null +++ b/sonic_cli_gen/yang_parser.py @@ -0,0 +1,679 @@ +#!/usr/bin/env python + +from collections import OrderedDict +from config.config_mgmt import ConfigMgmt +from typing import List, Dict + +yang_guidelines_link = 'https://github.com/Azure/SONiC/blob/master/doc/mgmt/SONiC_YANG_Model_Guidelines.md' + + +class YangParser: + """ YANG model parser + + Attributes: + yang_model_name: Name of the YANG model file + conf_mgmt: Instance of Config Mgmt class to + help parse YANG models + y_module: Reference to 'module' entity + from YANG model file + y_top_level_container: Reference to top level 'container' + entity from YANG model file + y_table_containers: Reference to 'container' entities + from YANG model file that represent Config DB tables + yang_2_dict: dictionary created from YANG model file that + represent Config DB schema. + + Below the 'yang_2_dict' obj in case if YANG model has a 'list' entity: + { + 'tables': [{ + 'name': 'value', + 'description': 'value', + 'dynamic-objects': [ + 'name': 'value', + 'description': 'value, + 'attrs': [ + { + 'name': 'value', + 'description': 'value', + 'is-leaf-list': False, + 'is-mandatory': False, + 'group': 'value' + } + ... + ], + 'keys': [ + { + 'name': 'ACL_TABLE_NAME', + 'description': 'value' + } + ... + ] + ], + }] + } + In case if YANG model does NOT have a 'list' entity, + it has the same structure as above, but 'dynamic-objects' + changed to 'static-objects' and have no 'keys' + """ + + def __init__(self, + yang_model_name, + config_db_path, + allow_tbl_without_yang, + debug): + self.yang_model_name = yang_model_name + self.conf_mgmt = None + self.y_module = None + self.y_top_level_container = None + self.y_table_containers = None + self.yang_2_dict = dict() + + try: + self.conf_mgmt = ConfigMgmt(config_db_path, + debug, + allow_tbl_without_yang) + except Exception as e: + raise Exception("Failed to load the {} class".format(str(e))) + + def _init_yang_module_and_containers(self): + """ Initialize inner class variables: + self.y_module + self.y_top_level_container + self.y_table_containers + + Raises: + Exception: if YANG model is invalid or NOT exist + """ + + self.y_module = self._find_yang_model_in_yjson_obj() + + if self.y_module is None: + raise Exception('The YANG model {} is NOT exist'.format(self.yang_model_name)) + + if self.y_module.get('container') is None: + raise Exception('The YANG model {} does NOT have\ + "top level container" element\ + Please follow the SONiC YANG model guidelines:\ + \n{}'.format(self.yang_model_name, yang_guidelines_link)) + self.y_top_level_container = self.y_module.get('container') + + if self.y_top_level_container.get('container') is None: + raise Exception('The YANG model {} does NOT have "container"\ + element after "top level container"\ + Please follow the SONiC YANG model guidelines:\ + \n{}'.format(self.yang_model_name, yang_guidelines_link)) + self.y_table_containers = self.y_top_level_container.get('container') + + def _find_yang_model_in_yjson_obj(self) -> OrderedDict: + """ Find provided YANG model inside the yJson object, + the yJson object contain all yang-models + parsed from directory - /usr/local/yang-models + + Returns: + reference to yang_model_name + """ + + for yang_model in self.conf_mgmt.sy.yJson: + if yang_model.get('module').get('@name') == self.yang_model_name: + return yang_model.get('module') + + def parse_yang_model(self) -> dict: + """ Parse provided YANG model and save + the output to self.yang_2_dict object + + Returns: + parsed YANG model in dictionary format + """ + + self._init_yang_module_and_containers() + self.yang_2_dict['tables'] = list() + + # determine how many (1 or more) containers a YANG model + # has after the 'top level' container + # 'table' container goes after the 'top level' container + self.yang_2_dict['tables'] += list_handler(self.y_table_containers, + lambda e: on_table_container(self.y_module, e, self.conf_mgmt)) + + return self.yang_2_dict + + +# ------------------------------HANDLERS-------------------------------- # + +def list_handler(y_entity, callback) -> List[Dict]: + """ Determine if the type of entity is a list, + if so - call the callback for every list element + """ + + if isinstance(y_entity, list): + return [callback(e) for e in y_entity] + else: + return [callback(y_entity)] + + +def on_table_container(y_module: OrderedDict, + tbl_container: OrderedDict, + conf_mgmt: ConfigMgmt) -> dict: + """ Parse 'table' container, + 'table' container goes after 'top level' container + + Args: + y_module: reference to 'module' + tbl_container: reference to 'table' container + conf_mgmt: reference to ConfigMgmt class instance, + it have yJson object which contain all parsed YANG models + Returns: + element for self.yang_2_dict['tables'] + """ + y2d_elem = { + 'name': tbl_container.get('@name'), + 'description': get_description(tbl_container) + } + + # determine if 'table container' has a 'list' entity + if tbl_container.get('list') is None: + y2d_elem['static-objects'] = list() + + # 'object' container goes after the 'table' container + # 'object' container have 2 types - list (like sonic-flex_counter.yang) + # and NOT list (like sonic-device_metadata.yang) + y2d_elem['static-objects'] += list_handler(tbl_container.get('container'), + lambda e: on_object_entity(y_module, e, conf_mgmt, is_list=False)) + else: + y2d_elem['dynamic-objects'] = list() + + # 'container' can have more than 1 'list' entity + y2d_elem['dynamic-objects'] += list_handler(tbl_container.get('list'), + lambda e: on_object_entity(y_module, e, conf_mgmt, is_list=True)) + + # move 'keys' elements from 'attrs' to 'keys' + change_dyn_obj_struct(y2d_elem['dynamic-objects']) + + return y2d_elem + + +def on_object_entity(y_module: OrderedDict, + y_entity: OrderedDict, + conf_mgmt: ConfigMgmt, + is_list: bool) -> dict: + """ Parse a 'object' entity, it could be a 'container' or a 'list' + 'Object' entity represent OBJECT in Config DB schema: + { + "TABLE": { + "OBJECT": { + "attr": "value" + } + } + } + + Args: + y_module: reference to 'module' + y_entity: reference to 'object' entity + conf_mgmt: reference to ConfigMgmt class instance, + it have yJson object which contain all parsed YANG models + is_list: boolean flag to determine if a 'list' was passed + Returns: + element for y2d_elem['static-objects'] OR y2d_elem['dynamic-objects'] + """ + + if y_entity is None: + return {} + + obj_elem = { + 'name': y_entity.get('@name'), + 'description': get_description(y_entity), + 'attrs': list() + } + + if is_list: + obj_elem['keys'] = get_list_keys(y_entity) + + attrs_list = list() + # grouping_name is empty because 'grouping' is not used so far + attrs_list.extend(get_leafs(y_entity, grouping_name='')) + attrs_list.extend(get_leaf_lists(y_entity, grouping_name='')) + attrs_list.extend(get_choices(y_module, y_entity, conf_mgmt, grouping_name='')) + attrs_list.extend(get_uses(y_module, y_entity, conf_mgmt)) + + obj_elem['attrs'] = attrs_list + + return obj_elem + + +def on_uses(y_module: OrderedDict, + y_uses, + conf_mgmt: ConfigMgmt) -> list: + """ Parse a YANG 'uses' entities + 'uses' referring to 'grouping' YANG entity + + Args: + y_module: reference to 'module' + y_uses: reference to 'uses' + conf_mgmt: reference to ConfigMgmt class instance, + it have yJson object which contain all parsed YANG model + Returns: + element for obj_elem['attrs'], 'attrs' contain a parsed 'leafs' + """ + + ret_attrs = list() + y_grouping = get_all_grouping(y_module, y_uses, conf_mgmt) + # trim prefixes in order to the next checks + trim_uses_prefixes(y_uses) + + # TODO: 'refine' support + for group in y_grouping: + if isinstance(y_uses, list): + for use in y_uses: + if group.get('@name') == use.get('@name'): + ret_attrs.extend(get_leafs(group, group.get('@name'))) + ret_attrs.extend(get_leaf_lists(group, group.get('@name'))) + ret_attrs.extend(get_choices(y_module, group, conf_mgmt, group.get('@name'))) + else: + if group.get('@name') == y_uses.get('@name'): + ret_attrs.extend(get_leafs(group, group.get('@name'))) + ret_attrs.extend(get_leaf_lists(group, group.get('@name'))) + ret_attrs.extend(get_choices(y_module, group, conf_mgmt, group.get('@name'))) + + return ret_attrs + + +def on_choices(y_module: OrderedDict, + y_choices, + conf_mgmt: ConfigMgmt, + grouping_name: str) -> list: + """ Parse a YANG 'choice' entities + + Args: + y_module: reference to 'module' + y_choices: reference to 'choice' element + conf_mgmt: reference to ConfigMgmt class instance, + it have yJson object which contain all parsed YANG model + grouping_name: if YANG entity contain 'uses', this arg represent 'grouping' name + Returns: + element for obj_elem['attrs'], 'attrs' contain a parsed 'leafs' + """ + + ret_attrs = list() + + # the YANG model can have multiple 'choice' entities + # inside a 'container' or 'list' + if isinstance(y_choices, list): + for choice in y_choices: + attrs = on_choice_cases(y_module, choice.get('case'), + conf_mgmt, grouping_name) + ret_attrs.extend(attrs) + else: + ret_attrs = on_choice_cases(y_module, y_choices.get('case'), + conf_mgmt, grouping_name) + + return ret_attrs + + +def on_choice_cases(y_module: OrderedDict, + y_cases, + conf_mgmt: ConfigMgmt, + grouping_name: str) -> list: + """ Parse a single YANG 'case' entity from the 'choice' entity. + The 'case' element can has inside - 'leaf', 'leaf-list', 'uses' + + Args: + y_module: reference to 'module' + y_cases: reference to 'case' + conf_mgmt: reference to ConfigMgmt class instance, + it have yJson object which contain all + parsed YANG model + grouping_name: if YANG entity contain 'uses', + this argument represent 'grouping' name + Returns: + element for the obj_elem['attrs'], the 'attrs' + contain a parsed 'leafs' + """ + + ret_attrs = list() + + if isinstance(y_cases, list): + for case in y_cases: + ret_attrs.extend(get_leafs(case, grouping_name)) + ret_attrs.extend(get_leaf_lists(case, grouping_name)) + ret_attrs.extend(get_uses(y_module, case, conf_mgmt)) + else: + ret_attrs.extend(get_leafs(y_cases, grouping_name)) + ret_attrs.extend(get_leaf_lists(y_cases, grouping_name)) + ret_attrs.extend(get_uses(y_module, y_cases, conf_mgmt)) + + return ret_attrs + + +def on_leafs(y_leafs, + grouping_name: str, + is_leaf_list: bool) -> list: + """ Parse all the 'leaf' or 'leaf-list' elements + + Args: + y_leafs: reference to all 'leaf' elements + grouping_name: if YANG entity contain 'uses', + this argument represent the 'grouping' name + is_leaf_list: boolean to determine if a 'leaf-list' + was passed as 'y_leafs' argument + Returns: + list of parsed 'leaf' elements + """ + + ret_attrs = list() + # The YANG 'container' entity may have only 1 'leaf' + # element OR a list of 'leaf' elements + ret_attrs += list_handler(y_leafs, lambda e: on_leaf(e, grouping_name, is_leaf_list)) + + return ret_attrs + + +def on_leaf(leaf: OrderedDict, + grouping_name: str, + is_leaf_list: bool) -> dict: + """ Parse a single 'leaf' element + + Args: + leaf: reference to a 'leaf' entity + grouping_name: if YANG entity contain 'uses', + this argument represent 'grouping' name + is_leaf_list: boolean to determine if 'leaf-list' + was passed in 'y_leafs' argument + Returns: + parsed 'leaf' element + """ + + attr = {'name': leaf.get('@name'), + 'description': get_description(leaf), + 'is-leaf-list': is_leaf_list, + 'is-mandatory': get_mandatory(leaf), + 'group': grouping_name} + + return attr + + +# ----------------------GETERS------------------------- # + +def get_mandatory(y_leaf: OrderedDict) -> bool: + """ Parse the 'mandatory' statement for a 'leaf' + + Args: + y_leaf: reference to a 'leaf' entity + Returns: + 'leaf' 'mandatory' value + """ + + if y_leaf.get('mandatory') is not None: + return True + + return False + + +def get_description(y_entity: OrderedDict) -> str: + """ Parse the 'description' entity from any YANG element + + Args: + y_entity: reference to YANG 'container' OR 'list' OR 'leaf' ... + Returns: + text of the 'description' + """ + + if y_entity.get('description') is not None: + return y_entity.get('description').get('text') + else: + return '' + + +def get_leafs(y_entity: OrderedDict, + grouping_name: str) -> list: + """ Check if the YANG entity have 'leafs', if so call handler + + Args: + y_entity: reference YANG 'container' or 'list' + or 'choice' or 'uses' + grouping_name: if YANG entity contain 'uses', + this argument represent 'grouping' name + Returns: + list of parsed 'leaf' elements + """ + + if y_entity.get('leaf') is not None: + return on_leafs(y_entity.get('leaf'), grouping_name, is_leaf_list=False) + + return [] + + +def get_leaf_lists(y_entity: OrderedDict, + grouping_name: str) -> list: + """ Check if the YANG entity have 'leaf-list', if so call handler + + Args: + y_entity: reference YANG 'container' or 'list' + or 'choice' or 'uses' + grouping_name: if YANG entity contain 'uses', + this argument represent 'grouping' name + Returns: + list of parsed 'leaf-list' elements + """ + + if y_entity.get('leaf-list') is not None: + return on_leafs(y_entity.get('leaf-list'), grouping_name, is_leaf_list=True) + + return [] + + +def get_choices(y_module: OrderedDict, + y_entity: OrderedDict, + conf_mgmt: ConfigMgmt, + grouping_name: str) -> list: + """ Check if the YANG entity have 'choice', if so call handler + + Args: + y_module: reference to 'module' + y_entity: reference YANG 'container' or 'list' + or 'choice' or 'uses' + conf_mgmt: reference to ConfigMgmt class instance, + it have yJson object which contain all parsed YANG model + grouping_name: if YANG entity contain 'uses', + this argument represent 'grouping' name + Returns: + list of parsed elements inside 'choice' + """ + + if y_entity.get('choice') is not None: + return on_choices(y_module, y_entity.get('choice'), conf_mgmt, grouping_name) + + return [] + + +def get_uses(y_module: OrderedDict, + y_entity: OrderedDict, + conf_mgmt: ConfigMgmt) -> list: + """ Check if the YANG entity have 'uses', if so call handler + + Args: + y_module: reference to 'module' + y_entity: reference YANG 'container' or 'list' + or 'choice' or 'uses' + conf_mgmt: reference to ConfigMgmt class instance, + it have yJson object which contain all parsed YANG model + Returns: + list of parsed elements inside 'grouping' + that referenced by 'uses' + """ + + if y_entity.get('uses') is not None: + return on_uses(y_module, y_entity.get('uses'), conf_mgmt) + + return [] + + +def get_all_grouping(y_module: OrderedDict, + y_uses: OrderedDict, + conf_mgmt: ConfigMgmt) -> list: + """ Get all the 'grouping' entities that was referenced + by 'uses' in current YANG model + + Args: + y_module: reference to 'module' + y_entity: reference to 'uses' + conf_mgmt: reference to ConfigMgmt class instance, + it have yJson object which contain all parsed YANG model + Returns: + list of 'grouping' elements + """ + + ret_grouping = list() + # prefix_list needed to find what YANG model was imported + prefix_list = get_import_prefixes(y_uses) + + # in case if 'grouping' located in the same YANG model + local_grouping = y_module.get('grouping') + if local_grouping is not None: + if isinstance(local_grouping, list): + ret_grouping.extend(local_grouping) + else: + ret_grouping.append(local_grouping) + + # if prefix_list is NOT empty it means that 'grouping' + # was imported from another YANG model + if prefix_list != []: + for prefix in prefix_list: + y_import = y_module.get('import') + if isinstance(y_import, list): + for _import in y_import: + if _import.get('prefix').get('@value') == prefix: + ret_grouping.extend(get_grouping_from_another_yang_model(_import.get('@module'), conf_mgmt)) + else: + if y_import.get('prefix').get('@value') == prefix: + ret_grouping.extend(get_grouping_from_another_yang_model(y_import.get('@module'), conf_mgmt)) + + return ret_grouping + + +def get_grouping_from_another_yang_model(yang_model_name: str, + conf_mgmt) -> list: + """ Get the YANG 'grouping' entity + + Args: + yang_model_name - YANG model to search + conf_mgmt - reference to ConfigMgmt class instance, + it have yJson object which contain all parsed YANG models + + Returns: + list of 'grouping' entities + """ + + ret_grouping = list() + + for yang_model in conf_mgmt.sy.yJson: + if (yang_model.get('module').get('@name') == yang_model_name): + grouping = yang_model.get('module').get('grouping') + if isinstance(grouping, list): + ret_grouping.extend(grouping) + else: + ret_grouping.append(grouping) + + return ret_grouping + + +def get_import_prefixes(y_uses: OrderedDict) -> list: + """ Parse 'import prefix' of YANG 'uses' entity + Example: + { + uses stypes:endpoint; + } + 'stypes' - prefix of imported YANG module. + 'endpoint' - YANG 'grouping' entity name + + Args: + y_uses: refrence to YANG 'uses' + Returns: + list of parsed prefixes + """ + + ret_prefixes = list() + + if isinstance(y_uses, list): + for use in y_uses: + prefix = use.get('@name').split(':')[0] + if prefix != use.get('@name'): + ret_prefixes.append(prefix) + else: + prefix = y_uses.get('@name').split(':')[0] + if prefix != y_uses.get('@name'): + ret_prefixes.append(prefix) + + return ret_prefixes + + +def trim_uses_prefixes(y_uses) -> list: + """ Trim prefixes from the 'uses' YANG entities. + If the YANG 'grouping' was imported from another + YANG file, it use the 'prefix' before the 'grouping' name: + { + uses sgrop:endpoint; + } + Where 'sgrop' = 'prefix'; 'endpoint' = 'grouping' name. + + Args: + y_uses: reference to 'uses' + + Returns: + list of 'uses' without 'prefixes' + """ + + prefixes = get_import_prefixes(y_uses) + + for prefix in prefixes: + if isinstance(y_uses, list): + for use in y_uses: + if prefix in use.get('@name'): + use['@name'] = use.get('@name').split(':')[1] + else: + if prefix in y_uses.get('@name'): + y_uses['@name'] = y_uses.get('@name').split(':')[1] + + +def get_list_keys(y_list: OrderedDict) -> list: + """ Parse YANG the 'key' entity. + If YANG model has a 'list' entity, inside the 'list' + there is 'key' entity. The 'key' - whitespace + separeted list of 'leafs' + + Args: + y_list: reference to the 'list' + Returns: + list of parsed keys + """ + + ret_list = list() + + keys = y_list.get('key').get('@value').split() + for k in keys: + key = {'name': k} + ret_list.append(key) + + return ret_list + + +def change_dyn_obj_struct(dynamic_objects: list): + """ Rearrange self.yang_2_dict['dynamic_objects'] structure. + If YANG model have a 'list' entity - inside the 'list' + it has 'key' entity. The 'key' entity it is whitespace + separeted list of 'leafs', those 'leafs' was parsed by + 'on_leaf()' function and placed under 'attrs' in + self.yang_2_dict['dynamic_objects'] need to move 'leafs' + from 'attrs' and put them into 'keys' section of + self.yang_2_dict['dynamic_objects'] + + Args: + dynamic_objects: reference to self.yang_2_dict['dynamic_objects'] + """ + + for obj in dynamic_objects: + for key in obj.get('keys'): + for attr in obj.get('attrs'): + if key.get('name') == attr.get('name'): + key['description'] = attr.get('description') + obj['attrs'].remove(attr) + break + diff --git a/tests/cli_autogen_input/autogen_test/show_cmd_output.py b/tests/cli_autogen_input/autogen_test/show_cmd_output.py new file mode 100644 index 0000000000..19c02c7783 --- /dev/null +++ b/tests/cli_autogen_input/autogen_test/show_cmd_output.py @@ -0,0 +1,81 @@ +""" +This module are holding correct output for the show command for cli_autogen_test.py +""" + + +show_device_metadata_localhost="""\ +HWSKU DEFAULT BGP STATUS DOCKER ROUTING CONFIG MODE HOSTNAME PLATFORM MAC DEFAULT PFCWD STATUS BGP ASN DEPLOYMENT ID TYPE BUFFER MODEL FRR MGMT FRAMEWORK CONFIG +----------- -------------------- ---------------------------- ---------- ---------------------- ----------------- ---------------------- --------- --------------- --------- -------------- --------------------------- +ACS-MSN2100 up N/A r-sonic-01 x86_64-mlnx_msn2100-r0 ff:ff:ff:ff:ff:00 disable N/A N/A ToRRouter traditional N/A +""" + + +show_device_metadata_localhost_changed_buffer_model="""\ +HWSKU DEFAULT BGP STATUS DOCKER ROUTING CONFIG MODE HOSTNAME PLATFORM MAC DEFAULT PFCWD STATUS BGP ASN DEPLOYMENT ID TYPE BUFFER MODEL FRR MGMT FRAMEWORK CONFIG +----------- -------------------- ---------------------------- ---------- ---------------------- ----------------- ---------------------- --------- --------------- --------- -------------- --------------------------- +ACS-MSN2100 up N/A r-sonic-01 x86_64-mlnx_msn2100-r0 ff:ff:ff:ff:ff:00 disable N/A N/A ToRRouter dynamic N/A +""" + + +show_device_neighbor="""\ +PEER NAME NAME MGMT ADDR LOCAL PORT PORT TYPE +----------- -------- ----------- ------------ ------ ------ +Ethernet0 Servers 10.217.0.1 Ethernet0 eth0 type +Ethernet4 Servers0 10.217.0.2 Ethernet4 eth1 type +""" + + +show_device_neighbor_added="""\ +PEER NAME NAME MGMT ADDR LOCAL PORT PORT TYPE +----------- -------- ----------- ------------ ------ ------ +Ethernet0 Servers 10.217.0.1 Ethernet0 eth0 type +Ethernet4 Servers0 10.217.0.2 Ethernet4 eth1 type +Ethernet8 Servers1 10.217.0.3 Ethernet8 eth2 type +""" + + +show_device_neighbor_deleted="""\ +PEER NAME NAME MGMT ADDR LOCAL PORT PORT TYPE +----------- -------- ----------- ------------ ------ ------ +Ethernet4 Servers0 10.217.0.2 Ethernet4 eth1 type +""" + + +show_device_neighbor_updated_mgmt_addr="""\ +PEER NAME NAME MGMT ADDR LOCAL PORT PORT TYPE +----------- -------- ----------- ------------ ------ ------ +Ethernet0 Servers 10.217.0.5 Ethernet0 eth0 type +Ethernet4 Servers0 10.217.0.2 Ethernet4 eth1 type +""" + + +show_device_neighbor_updated_name="""\ +PEER NAME NAME MGMT ADDR LOCAL PORT PORT TYPE +----------- -------- ----------- ------------ ------ ------ +Ethernet0 Servers1 10.217.0.1 Ethernet0 eth0 type +Ethernet4 Servers0 10.217.0.2 Ethernet4 eth1 type +""" + + +show_device_neighbor_updated_local_port="""\ +PEER NAME NAME MGMT ADDR LOCAL PORT PORT TYPE +----------- -------- ----------- ------------ ------ ------ +Ethernet0 Servers 10.217.0.1 Ethernet12 eth0 type +Ethernet4 Servers0 10.217.0.2 Ethernet4 eth1 type +""" + + +show_device_neighbor_updated_port="""\ +PEER NAME NAME MGMT ADDR LOCAL PORT PORT TYPE +----------- -------- ----------- ------------ ------ ------ +Ethernet0 Servers 10.217.0.1 Ethernet0 eth2 type +Ethernet4 Servers0 10.217.0.2 Ethernet4 eth1 type +""" + + +show_device_neighbor_updated_type="""\ +PEER NAME NAME MGMT ADDR LOCAL PORT PORT TYPE +----------- -------- ----------- ------------ ------ ------ +Ethernet0 Servers 10.217.0.1 Ethernet0 eth0 type2 +Ethernet4 Servers0 10.217.0.2 Ethernet4 eth1 type +""" diff --git a/tests/cli_autogen_input/autogen_test/sonic-device_metadata.yang b/tests/cli_autogen_input/autogen_test/sonic-device_metadata.yang new file mode 100644 index 0000000000..400cbf3bcd --- /dev/null +++ b/tests/cli_autogen_input/autogen_test/sonic-device_metadata.yang @@ -0,0 +1,123 @@ +module sonic-device_metadata { + + yang-version 1.1; + + namespace "http://github.com/Azure/sonic-device_metadata"; + prefix device_metadata; + + import ietf-yang-types { + prefix yang; + } + + import ietf-inet-types { + prefix inet; + } + + import sonic-types { + prefix stypes; + revision-date 2019-07-01; + } + + description "DEVICE_METADATA YANG Module for SONiC OS"; + + revision 2021-02-27 { + description "Added frr_mgmt_framework_config field to handle BGP + config DB schema events to configure FRR protocols."; + } + + revision 2020-04-10 { + description "First Revision"; + } + + container sonic-device_metadata { + + container DEVICE_METADATA { + + description "DEVICE_METADATA part of config_db.json"; + + container localhost{ + + leaf hwsku { + type stypes:hwsku; + } + + leaf default_bgp_status { + type enumeration { + enum up; + enum down; + } + default up; + } + + leaf docker_routing_config_mode { + type string { + pattern "unified|split|separated"; + } + default "unified"; + } + + leaf hostname { + type string { + length 1..255; + } + } + + leaf platform { + type string { + length 1..255; + } + } + + leaf mac { + type yang:mac-address; + } + + leaf default_pfcwd_status { + type enumeration { + enum disable; + enum enable; + } + default disable; + } + + leaf bgp_asn { + type inet:as-number; + } + + leaf deployment_id { + type uint32; + } + + leaf type { + type string { + length 1..255; + pattern "ToRRouter|LeafRouter|SpineChassisFrontendRouter|ChassisBackendRouter|ASIC"; + } + } + + leaf buffer_model { + description "This leaf is added for dynamic buffer calculation. + The dynamic model represents the model in which the buffer configurations, + like the headroom sizes and buffer pool sizes, are dynamically calculated based + on the ports' speed, cable length, and MTU. This model is used by Mellanox so far. + The traditional model represents the model in which all the buffer configurations + are statically configured in CONFIG_DB tables. This is the default model used by all other vendors"; + type string { + pattern "dynamic|traditional"; + } + } + + leaf frr_mgmt_framework_config { + type boolean; + description "FRR configurations are handled by sonic-frr-mgmt-framework module when set to true, + otherwise, sonic-bgpcfgd handles the FRR configurations based on the predefined templates."; + default "false"; + } + } + /* end of container localhost */ + } + /* end of container DEVICE_METADATA */ + } + /* end of top level container */ +} +/* end of module sonic-device_metadata */ diff --git a/tests/cli_autogen_input/autogen_test/sonic-device_neighbor.yang b/tests/cli_autogen_input/autogen_test/sonic-device_neighbor.yang new file mode 100644 index 0000000000..e1c745dd9a --- /dev/null +++ b/tests/cli_autogen_input/autogen_test/sonic-device_neighbor.yang @@ -0,0 +1,78 @@ +module sonic-device_neighbor { + + yang-version 1.1; + + namespace "http://github.com/Azure/sonic-device_neighbor"; + prefix device_neighbor; + + import ietf-inet-types { + prefix inet; + } + + import sonic-extension { + prefix ext; + revision-date 2019-07-01; + } + + import sonic-port { + prefix port; + revision-date 2019-07-01; + } + + description "DEVICE_NEIGHBOR YANG Module for SONiC OS"; + + revision 2020-04-10 { + description "First Revision"; + } + + container sonic-device_neighbor { + + container DEVICE_NEIGHBOR { + + description "DEVICE_NEIGHBOR part of config_db.json"; + + list DEVICE_NEIGHBOR_LIST { + + key "peer_name"; + + leaf peer_name { + type string { + length 1..255; + } + } + + leaf name { + type string { + length 1..255; + } + } + + leaf mgmt_addr { + type inet:ip-address; + } + + leaf local_port { + type leafref { + path /port:sonic-port/port:PORT/port:PORT_LIST/port:name; + } + } + + leaf port { + type string { + length 1..255; + } + } + + leaf type { + type string { + length 1..255; + } + } + } + /* end of list DEVICE_NEIGHBOR_LIST */ + } + /* end of container DEVICE_NEIGHBOR */ + } + /* end of top level container */ +} +/* end of module sonic-device_neighbor */ diff --git a/tests/cli_autogen_input/cli_autogen_common.py b/tests/cli_autogen_input/cli_autogen_common.py new file mode 100644 index 0000000000..141bceed9c --- /dev/null +++ b/tests/cli_autogen_input/cli_autogen_common.py @@ -0,0 +1,38 @@ +import os + +yang_models_path = '/usr/local/yang-models' + + +def move_yang_models(test_path, test_name, test_yang_models): + """ Move a test YANG models to known location """ + + for yang_model in test_yang_models: + src_path = os.path.join( + test_path, + 'cli_autogen_input', + test_name, + yang_model + ) + os.system('sudo cp {} {}'.format(src_path, yang_models_path)) + + +def remove_yang_models(test_yang_models): + """ Remove a test YANG models to known location """ + + for yang_model in test_yang_models: + yang_model_path = os.path.join(yang_models_path, yang_model) + os.system('sudo rm {}'.format(yang_model_path)) + + +def backup_yang_models(): + """ Make a copy of existing YANG models """ + + os.system('sudo cp -R {} {}'.format(yang_models_path, yang_models_path + '_backup')) + + +def restore_backup_yang_models(): + """ Restore existing YANG models from backup """ + + os.system('sudo cp {} {}'.format(yang_models_path + '_backup/*', yang_models_path)) + os.system('sudo rm -rf {}'.format(yang_models_path + '_backup')) + \ No newline at end of file diff --git a/tests/cli_autogen_input/config_db.json b/tests/cli_autogen_input/config_db.json new file mode 100644 index 0000000000..5d8c863cec --- /dev/null +++ b/tests/cli_autogen_input/config_db.json @@ -0,0 +1,65 @@ +{ + "DEVICE_METADATA|localhost": { + "buffer_model": "traditional", + "default_bgp_status": "up", + "default_pfcwd_status": "disable", + "hostname": "r-sonic-01", + "hwsku": "ACS-MSN2100", + "mac": "ff:ff:ff:ff:ff:00", + "platform": "x86_64-mlnx_msn2100-r0", + "type": "ToRRouter" + }, + "PORT|Ethernet0": { + "alias": "etp1", + "description": "etp1", + "index": "0", + "lanes": "0, 1, 2, 3", + "mtu": "9100", + "pfc_asym": "off", + "speed": "100000" + }, + "PORT|Ethernet4": { + "admin_status": "up", + "alias": "etp2", + "description": "Servers0:eth0", + "index": "1", + "lanes": "4, 5, 6, 7", + "mtu": "9100", + "pfc_asym": "off", + "speed": "100000" + }, + "PORT|Ethernet8": { + "admin_status": "up", + "alias": "etp3", + "description": "Servers0:eth2", + "index": "2", + "lanes": "8, 9, 10, 11", + "mtu": "9100", + "pfc_asym": "off", + "speed": "100000" + }, + "PORT|Ethernet12": { + "admin_status": "up", + "alias": "etp4", + "description": "Servers0:eth4", + "index": "3", + "lanes": "12, 13, 14, 15", + "mtu": "9100", + "pfc_asym": "off", + "speed": "100000" + }, + "DEVICE_NEIGHBOR|Ethernet0": { + "name": "Servers", + "port": "eth0", + "mgmt_addr": "10.217.0.1", + "local_port": "Ethernet0", + "type": "type" + }, + "DEVICE_NEIGHBOR|Ethernet4": { + "name": "Servers0", + "port": "eth1", + "mgmt_addr": "10.217.0.2", + "local_port": "Ethernet4", + "type": "type" + } +} \ No newline at end of file diff --git a/tests/cli_autogen_input/yang_parser_test/assert_dictionaries.py b/tests/cli_autogen_input/yang_parser_test/assert_dictionaries.py new file mode 100644 index 0000000000..bed2a4a06a --- /dev/null +++ b/tests/cli_autogen_input/yang_parser_test/assert_dictionaries.py @@ -0,0 +1,626 @@ +""" +Module holding correct dictionaries for test YANG models +""" + +one_table_container = { + "tables":[ + { + "description":"TABLE_1 description", + "name":"TABLE_1", + "static-objects":[ + { + } + ] + } + ] +} + +two_table_containers = { + "tables":[ + { + "description":"TABLE_1 description", + "name":"TABLE_1", + "static-objects":[ + { + + } + ] + }, + { + "description":"TABLE_2 description", + "name":"TABLE_2", + "static-objects":[ + { + + } + ] + } + ] +} + +one_object_container = { + "tables":[ + { + "description":"TABLE_1 description", + "name":"TABLE_1", + "static-objects":[ + { + "name":"OBJECT_1", + "description":"OBJECT_1 description", + "attrs":[ + ] + } + ] + } + ] +} + +two_object_containers = { + "tables":[ + { + "description":"FIRST_TABLE description", + "name":"TABLE_1", + "static-objects":[ + { + "name":"OBJECT_1", + "description":"OBJECT_1 description", + "attrs":[ + ] + }, + { + "name":"OBJECT_2", + "description":"OBJECT_2 description", + "attrs":[ + ] + } + ] + } + ] +} + +one_list = { + "tables":[ + { + "description":"TABLE_1 description", + "name":"TABLE_1", + "dynamic-objects":[ + { + "name":"TABLE_1_LIST", + "description":"TABLE_1_LIST description", + "keys":[ + { + "name": "key_name", + "description": "", + } + ], + "attrs":[ + ] + } + ] + } + ] +} + +two_lists = { + "tables":[ + { + "description":"TABLE_1 description", + "name":"TABLE_1", + "dynamic-objects":[ + { + "name":"TABLE_1_LIST_1", + "description":"TABLE_1_LIST_1 description", + "keys":[ + { + "name": "key_name1", + "description": "", + } + ], + "attrs":[ + ] + }, + { + "name":"TABLE_1_LIST_2", + "description":"TABLE_1_LIST_2 description", + "keys":[ + { + "name": "key_name2", + "description": "", + } + ], + "attrs":[ + ] + } + ] + } + ] +} + +static_object_complex_1 = { + "tables":[ + { + "description":"TABLE_1 description", + "name":"TABLE_1", + "static-objects":[ + { + "name":"OBJECT_1", + "description":"OBJECT_1 description", + "attrs":[ + { + "name":"OBJ_1_LEAF_1", + "description": "OBJ_1_LEAF_1 description", + "is-mandatory": False, + "is-leaf-list": False, + "group": '', + }, + { + "name":"OBJ_1_LEAF_LIST_1", + "description": "", + "is-mandatory": False, + "is-leaf-list": True, + "group": '', + }, + { + "name":"OBJ_1_CHOICE_1_LEAF_1", + "description": "", + "is-mandatory": False, + "is-leaf-list": False, + "group": '', + }, + { + "name":"OBJ_1_CHOICE_1_LEAF_2", + "description": "", + "is-mandatory": False, + "is-leaf-list": False, + "group": '', + } + ] + } + ] + } + ] +} + +static_object_complex_2 = { + "tables":[ + { + "description":"TABLE_1 description", + "name":"TABLE_1", + "static-objects":[ + { + "name":"OBJECT_1", + "description":"OBJECT_1 description", + "attrs":[ + { + "name":"OBJ_1_LEAF_1", + "description": "OBJ_1_LEAF_1 description", + "is-mandatory": False, + "is-leaf-list": False, + "group": '', + }, + { + "name":"OBJ_1_LEAF_2", + "description": "OBJ_1_LEAF_2 description", + "is-mandatory": False, + "is-leaf-list": False, + "group": '', + }, + { + "name":"OBJ_1_LEAF_LIST_1", + "description": "", + "is-mandatory": False, + "is-leaf-list": True, + "group": '', + }, + { + "name":"OBJ_1_LEAF_LIST_2", + "description": "", + "is-mandatory": False, + "is-leaf-list": True, + "group": '', + }, + { + "name":"OBJ_1_CHOICE_1_LEAF_1", + "description": "", + "is-mandatory": False, + "is-leaf-list": False, + "group": '', + }, + { + "name":"OBJ_1_CHOICE_1_LEAF_2", + "description": "", + "is-mandatory": False, + "is-leaf-list": False, + "group": '', + }, + { + "name":"OBJ_1_CHOICE_2_LEAF_1", + "description": "", + "is-mandatory": False, + "is-leaf-list": False, + "group": '', + }, + { + "name":"OBJ_1_CHOICE_2_LEAF_2", + "description": "", + "is-mandatory": False, + "is-leaf-list": False, + "group": '', + }, + ] + } + ] + } + ] +} + +dynamic_object_complex_1 = { + "tables":[ + { + "description":"TABLE_1 description", + "name":"TABLE_1", + "dynamic-objects":[ + { + "name":"OBJECT_1_LIST", + "description":"OBJECT_1_LIST description", + "attrs":[ + { + "name":"OBJ_1_LEAF_1", + "description": "OBJ_1_LEAF_1 description", + "is-mandatory": False, + "is-leaf-list": False, + "group": '', + }, + { + "name":"OBJ_1_LEAF_LIST_1", + "description": "", + "is-mandatory": False, + "is-leaf-list": True, + "group": '', + }, + { + "name":"OBJ_1_CHOICE_1_LEAF_1", + "description": "", + "is-mandatory": False, + "is-leaf-list": False, + "group": '', + }, + { + "name":"OBJ_1_CHOICE_1_LEAF_2", + "description": "", + "is-mandatory": False, + "is-leaf-list": False, + "group": '', + } + ], + "keys":[ + { + "name": "KEY_LEAF_1", + "description": "KEY_LEAF_1 description", + } + ] + } + ] + } + ] +} + +dynamic_object_complex_2 = { + "tables":[ + { + "description":"TABLE_1 description", + "name":"TABLE_1", + "dynamic-objects":[ + { + "name":"OBJECT_1_LIST", + "description":"OBJECT_1_LIST description", + "attrs":[ + { + "name":"OBJ_1_LEAF_1", + "description": "OBJ_1_LEAF_1 description", + "is-mandatory": False, + "is-leaf-list": False, + "group": '', + }, + { + "name":"OBJ_1_LEAF_2", + "description": "OBJ_1_LEAF_2 description", + "is-mandatory": False, + "is-leaf-list": False, + "group": '', + }, + { + "name":"OBJ_1_LEAF_LIST_1", + "description": "", + "is-mandatory": False, + "is-leaf-list": True, + "group": '', + }, + { + "name":"OBJ_1_LEAF_LIST_2", + "description": "", + "is-mandatory": False, + "is-leaf-list": True, + "group": '', + }, + { + "name":"OBJ_1_CHOICE_1_LEAF_1", + "description": "", + "is-mandatory": False, + "is-leaf-list": False, + "group": '', + }, + { + "name":"OBJ_1_CHOICE_1_LEAF_2", + "description": "", + "is-mandatory": False, + "is-leaf-list": False, + "group": '', + }, + { + "name":"OBJ_1_CHOICE_2_LEAF_1", + "description": "", + "is-mandatory": False, + "is-leaf-list": False, + "group": '', + }, + { + "name":"OBJ_1_CHOICE_2_LEAF_2", + "description": "", + "is-mandatory": False, + "is-leaf-list": False, + "group": '', + } + ], + "keys":[ + { + "name": "KEY_LEAF_1", + "description": "KEY_LEAF_1 description", + }, + { + "name": "KEY_LEAF_2", + "description": "KEY_LEAF_2 description", + } + ] + } + ] + } + ] +} + +choice_complex = { + "tables":[ + { + "description":"TABLE_1 description", + "name":"TABLE_1", + "static-objects":[ + { + "name":"OBJECT_1", + "description":"OBJECT_1 description", + "attrs":[ + { + "name":"LEAF_1", + "description": "", + "is-mandatory": False, + "is-leaf-list": False, + "group": '', + }, + { + "name":"LEAF_LIST_1", + "description": "", + "is-mandatory": False, + "is-leaf-list": True, + "group": '', + }, + { + "name":"GR_1_LEAF_1", + "description": "", + "is-mandatory": False, + "is-leaf-list": False, + "group": "GR_1", + }, + { + "name":"GR_1_LEAF_2", + "description": "", + "is-mandatory": False, + "is-leaf-list": False, + "group": 'GR_1', + }, + { + "name":"LEAF_2", + "description": "", + "is-mandatory": False, + "is-leaf-list": False, + "group": '', + }, + { + "name":"LEAF_3", + "description": "", + "is-mandatory": False, + "is-leaf-list": False, + "group": '', + }, + { + "name":"LEAF_LIST_2", + "description": "", + "is-mandatory": False, + "is-leaf-list": True, + "group": '', + }, + { + "name":"LEAF_LIST_3", + "description": "", + "is-mandatory": False, + "is-leaf-list": True, + "group": '', + }, + { + "name":"GR_5_LEAF_1", + "description": "", + "is-mandatory": False, + "is-leaf-list": False, + "group": 'GR_5', + }, + { + "name":"GR_5_LEAF_2", + "description": "", + "is-mandatory": False, + "is-leaf-list": False, + "group": 'GR_5', + }, + { + "name":"GR_2_LEAF_1", + "description": "", + "is-mandatory": False, + "is-leaf-list": False, + "group": 'GR_2', + }, + { + "name":"GR_2_LEAF_2", + "description": "", + "is-mandatory": False, + "is-leaf-list": False, + "group": 'GR_2', + }, + { + "name":"GR_3_LEAF_1", + "description": "", + "is-mandatory": False, + "is-leaf-list": False, + "group": 'GR_3', + }, + { + "name":"GR_3_LEAF_2", + "description": "", + "is-mandatory": False, + "is-leaf-list": False, + "group": 'GR_3', + }, + ] + } + ] + } + ] +} + +grouping_complex = { + "tables":[ + { + "description":"TABLE_1 description", + "name":"TABLE_1", + "static-objects":[ + { + "name":"OBJECT_1", + "description":"OBJECT_1 description", + "attrs":[ + { + "name":"GR_1_LEAF_1", + "description": "", + "is-mandatory": False, + "is-leaf-list": False, + "group": "GR_1", + }, + { + "name":"GR_1_LEAF_2", + "description": "", + "is-mandatory": False, + "is-leaf-list": False, + "group": 'GR_1', + }, + ] + }, + { + "name":"OBJECT_2", + "description":"OBJECT_2 description", + "attrs":[ + { + "name":"GR_5_LEAF_1", + "description": "", + "is-mandatory": False, + "is-leaf-list": False, + "group": "GR_5", + }, + { + "name":"GR_5_LEAF_LIST_1", + "description": "", + "is-mandatory": False, + "is-leaf-list": True, + "group": "GR_5", + }, + { + "name":"GR_6_LEAF_1", + "description": "", + "is-mandatory": False, + "is-leaf-list": False, + "group": "GR_6", + }, + { + "name":"GR_6_LEAF_2", + "description": "", + "is-mandatory": False, + "is-leaf-list": False, + "group": "GR_6", + }, + { + "name":"GR_6_CASE_1_LEAF_1", + "description": "", + "is-mandatory": False, + "is-leaf-list": False, + "group": "GR_6", + }, + { + "name":"GR_6_CASE_1_LEAF_LIST_1", + "description": "", + "is-mandatory": False, + "is-leaf-list": True, + "group": "GR_6", + }, + { + "name":"GR_6_CASE_2_LEAF_1", + "description": "", + "is-mandatory": False, + "is-leaf-list": False, + "group": "GR_6", + }, + { + "name":"GR_6_CASE_2_LEAF_2", + "description": "", + "is-mandatory": False, + "is-leaf-list": False, + "group": "GR_6", + }, + { + "name":"GR_6_CASE_2_LEAF_LIST_1", + "description": "", + "is-mandatory": False, + "is-leaf-list": True, + "group": "GR_6", + }, + { + "name":"GR_6_CASE_2_LEAF_LIST_2", + "description": "", + "is-mandatory": False, + "is-leaf-list": True, + "group": "GR_6", + }, + { + "name":"GR_4_LEAF_1", + "description": "", + "is-mandatory": False, + "is-leaf-list": False, + "group": "GR_4", + }, + { + "name":"GR_4_LEAF_2", + "description": "", + "is-mandatory": False, + "is-leaf-list": False, + "group": "GR_4", + }, + ] + } + ] + } + ] +} + diff --git a/tests/cli_autogen_input/yang_parser_test/sonic-1-list.yang b/tests/cli_autogen_input/yang_parser_test/sonic-1-list.yang new file mode 100644 index 0000000000..bc8603add4 --- /dev/null +++ b/tests/cli_autogen_input/yang_parser_test/sonic-1-list.yang @@ -0,0 +1,29 @@ +module sonic-1-list { + + yang-version 1.1; + + namespace "http://github.com/Azure/s-1-list"; + prefix s-1-list; + + container sonic-1-list { + /* sonic-1-list - top level container */ + + container TABLE_1 { + /* TABLE_1 - table container */ + + description "TABLE_1 description"; + + list TABLE_1_LIST { + /* TABLE_1 - object container */ + + description "TABLE_1_LIST description"; + + key "key_name"; + + leaf key_name { + type string; + } + } + } + } +} diff --git a/tests/cli_autogen_input/yang_parser_test/sonic-1-object-container.yang b/tests/cli_autogen_input/yang_parser_test/sonic-1-object-container.yang new file mode 100644 index 0000000000..8d19979157 --- /dev/null +++ b/tests/cli_autogen_input/yang_parser_test/sonic-1-object-container.yang @@ -0,0 +1,23 @@ +module sonic-1-object-container { + + yang-version 1.1; + + namespace "http://github.com/Azure/s-1-object"; + prefix s-1-object; + + container sonic-1-object-container { + /* sonic-1-object-container - top level container */ + + container TABLE_1 { + /* TABLE_1 - table container */ + + description "TABLE_1 description"; + + container OBJECT_1 { + /* OBJECT_1 - object container */ + + description "OBJECT_1 description"; + } + } + } +} diff --git a/tests/cli_autogen_input/yang_parser_test/sonic-1-table-container.yang b/tests/cli_autogen_input/yang_parser_test/sonic-1-table-container.yang new file mode 100644 index 0000000000..36b98415e5 --- /dev/null +++ b/tests/cli_autogen_input/yang_parser_test/sonic-1-table-container.yang @@ -0,0 +1,17 @@ +module sonic-1-table-container { + + yang-version 1.1; + + namespace "http://github.com/Azure/s-1-table"; + prefix s-1-table; + + container sonic-1-table-container { + /* sonic-1-table-container - top level container */ + + container TABLE_1 { + /* TABLE_1 - table container */ + + description "TABLE_1 description"; + } + } +} diff --git a/tests/cli_autogen_input/yang_parser_test/sonic-2-lists.yang b/tests/cli_autogen_input/yang_parser_test/sonic-2-lists.yang new file mode 100644 index 0000000000..fce9704f00 --- /dev/null +++ b/tests/cli_autogen_input/yang_parser_test/sonic-2-lists.yang @@ -0,0 +1,42 @@ +module sonic-2-lists { + + yang-version 1.1; + + namespace "http://github.com/Azure/s-2-lists"; + prefix s-2-lists; + + container sonic-2-lists { + /* sonic-2-lists - top level container */ + + container TABLE_1 { + /* TALBE_1 - table container */ + + + description "TABLE_1 description"; + + list TABLE_1_LIST_1 { + /* TALBE_1_LIST_1 - object container */ + + description "TABLE_1_LIST_1 description"; + + key "key_name1"; + + leaf key_name1 { + type string; + } + } + + list TABLE_1_LIST_2 { + /* TALBE_1_LIST_2 - object container */ + + description "TABLE_1_LIST_2 description"; + + key "key_name2"; + + leaf key_name2 { + type string; + } + } + } + } +} diff --git a/tests/cli_autogen_input/yang_parser_test/sonic-2-object-containers.yang b/tests/cli_autogen_input/yang_parser_test/sonic-2-object-containers.yang new file mode 100644 index 0000000000..e633b66246 --- /dev/null +++ b/tests/cli_autogen_input/yang_parser_test/sonic-2-object-containers.yang @@ -0,0 +1,29 @@ +module sonic-2-object-containers { + + yang-version 1.1; + + namespace "http://github.com/Azure/s-2-object"; + prefix s-2-object; + + container sonic-2-object-containers { + /* sonic-2-object-containers - top level container */ + + container TABLE_1 { + /* TABLE_1 - table container */ + + description "FIRST_TABLE description"; + + container OBJECT_1 { + /* OBJECT_1 - object container */ + + description "OBJECT_1 description"; + } + + container OBJECT_2 { + /* OBJECT_2 - object container */ + + description "OBJECT_2 description"; + } + } + } +} diff --git a/tests/cli_autogen_input/yang_parser_test/sonic-2-table-containers.yang b/tests/cli_autogen_input/yang_parser_test/sonic-2-table-containers.yang new file mode 100644 index 0000000000..f5284c67ee --- /dev/null +++ b/tests/cli_autogen_input/yang_parser_test/sonic-2-table-containers.yang @@ -0,0 +1,23 @@ +module sonic-2-table-containers { + + yang-version 1.1; + + namespace "http://github.com/Azure/s-2-table"; + prefix s-2-table; + + container sonic-2-table-containers { + /* sonic-2-table-containers - top level container */ + + container TABLE_1 { + /* TABLE_1 - table container */ + + description "TABLE_1 description"; + } + + container TABLE_2 { + /* TABLE_2 - table container */ + + description "TABLE_2 description"; + } + } +} diff --git a/tests/cli_autogen_input/yang_parser_test/sonic-choice-complex.yang b/tests/cli_autogen_input/yang_parser_test/sonic-choice-complex.yang new file mode 100644 index 0000000000..9d6e0de9ee --- /dev/null +++ b/tests/cli_autogen_input/yang_parser_test/sonic-choice-complex.yang @@ -0,0 +1,91 @@ +module sonic-choice-complex { + + yang-version 1.1; + + namespace "http://github.com/Azure/choice-complex"; + prefix choice-complex; + + import sonic-grouping-1 { + prefix sgroup1; + } + + import sonic-grouping-2 { + prefix sgroup2; + } + + grouping GR_5 { + leaf GR_5_LEAF_1 { + type string; + } + + leaf GR_5_LEAF_2 { + type string; + } + } + + grouping GR_6 { + leaf GR_6_LEAF_1 { + type string; + } + + leaf GR_6_LEAF_2 { + type string; + } + } + + container sonic-choice-complex { + /* sonic-choice-complex - top level container */ + + container TABLE_1 { + /* TABLE_1 - table container */ + + description "TABLE_1 description"; + + container OBJECT_1 { + /* OBJECT_1 - object container, it have + * 1 choice, which have 2 cases. + * first case have: 1 leaf, 1 leaf-list, 1 uses + * second case have: 2 leafs, 2 leaf-lists, 2 uses + */ + + description "OBJECT_1 description"; + + choice CHOICE_1 { + case CHOICE_1_CASE_1 { + leaf LEAF_1 { + type uint16; + } + + leaf-list LEAF_LIST_1 { + type string; + } + + uses sgroup1:GR_1; + } + + case CHOICE_1_CASE_2 { + leaf LEAF_2 { + type string; + } + + leaf LEAF_3 { + type string; + } + + leaf-list LEAF_LIST_2 { + type string; + } + + leaf-list LEAF_LIST_3 { + type string; + } + + uses GR_5; + uses sgroup1:GR_2; + uses sgroup2:GR_3; + } + } + } + } + } +} diff --git a/tests/cli_autogen_input/yang_parser_test/sonic-dynamic-object-complex-1.yang b/tests/cli_autogen_input/yang_parser_test/sonic-dynamic-object-complex-1.yang new file mode 100644 index 0000000000..383e94fb43 --- /dev/null +++ b/tests/cli_autogen_input/yang_parser_test/sonic-dynamic-object-complex-1.yang @@ -0,0 +1,57 @@ +module sonic-dynamic-object-complex-1 { + + yang-version 1.1; + + namespace "http://github.com/Azure/dynamic-complex-1"; + prefix dynamic-complex-1; + + container sonic-dynamic-object-complex-1 { + /* sonic-dynamic-object-complex-1 - top level container */ + + container TABLE_1 { + /* TABLE_1 - table container */ + + description "TABLE_1 description"; + + list OBJECT_1_LIST { + /* OBJECT_1_LIST - dynamic object container, it have: + * 1 key, + * 1 leaf, + * 1 leaf-list + * 1 choice + */ + + description "OBJECT_1_LIST description"; + + key "KEY_LEAF_1"; + + leaf KEY_LEAF_1 { + description "KEY_LEAF_1 description"; + type string; + } + + leaf OBJ_1_LEAF_1 { + description "OBJ_1_LEAF_1 description"; + type string; + } + + leaf-list OBJ_1_LEAF_LIST_1 { + type string; + } + + choice OBJ_1_CHOICE_1 { + case OBJ_1_CHOICE_1_CASE_1 { + leaf OBJ_1_CHOICE_1_LEAF_1 { + type uint16; + } + } + case OBJ_1_CHOICE_1_CASE_2 { + leaf OBJ_1_CHOICE_1_LEAF_2 { + type string; + } + } + } + } + } + } +} diff --git a/tests/cli_autogen_input/yang_parser_test/sonic-dynamic-object-complex-2.yang b/tests/cli_autogen_input/yang_parser_test/sonic-dynamic-object-complex-2.yang new file mode 100644 index 0000000000..a365b014ad --- /dev/null +++ b/tests/cli_autogen_input/yang_parser_test/sonic-dynamic-object-complex-2.yang @@ -0,0 +1,84 @@ +module sonic-dynamic-object-complex-2 { + + yang-version 1.1; + + namespace "http://github.com/Azure/dynamic-complex-2"; + prefix dynamic-complex-2; + + container sonic-dynamic-object-complex-2 { + /* sonic-dynamic-object-complex-2 - top level container */ + + container TABLE_1 { + /* TABLE_1 - table container */ + + description "TABLE_1 description"; + + list OBJECT_1_LIST { + /* OBJECT_1_LIST - dynamic object container, it have: + * 2 keys + * 2 leaf, + * 2 leaf-list + * 2 choice + */ + + description "OBJECT_1_LIST description"; + + key "KEY_LEAF_1 KEY_LEAF_2"; + + leaf KEY_LEAF_1 { + description "KEY_LEAF_1 description"; + type string; + } + + leaf KEY_LEAF_2 { + description "KEY_LEAF_2 description"; + type string; + } + + leaf OBJ_1_LEAF_1 { + description "OBJ_1_LEAF_1 description"; + type string; + } + + leaf OBJ_1_LEAF_2 { + description "OBJ_1_LEAF_2 description"; + type string; + } + + leaf-list OBJ_1_LEAF_LIST_1 { + type string; + } + + leaf-list OBJ_1_LEAF_LIST_2 { + type string; + } + + choice OBJ_1_CHOICE_1 { + case OBJ_1_CHOICE_1_CASE_1 { + leaf OBJ_1_CHOICE_1_LEAF_1 { + type uint16; + } + } + case OBJ_1_CHOICE_1_CASE_2 { + leaf OBJ_1_CHOICE_1_LEAF_2 { + type string; + } + } + } + + choice OBJ_1_CHOICE_2 { + case OBJ_1_CHOICE_2_CASE_1 { + leaf OBJ_1_CHOICE_2_LEAF_1 { + type uint16; + } + } + case OBJ_1_CHOICE_2_CASE_2 { + leaf OBJ_1_CHOICE_2_LEAF_2 { + type string; + } + } + } + } + } + } +} diff --git a/tests/cli_autogen_input/yang_parser_test/sonic-grouping-1.yang b/tests/cli_autogen_input/yang_parser_test/sonic-grouping-1.yang new file mode 100644 index 0000000000..bf0be792f5 --- /dev/null +++ b/tests/cli_autogen_input/yang_parser_test/sonic-grouping-1.yang @@ -0,0 +1,25 @@ +module sonic-grouping-1{ + + yang-version 1.1; + + namespace "http://github.com/Azure/s-grouping-1"; + prefix s-grouping-1; + + grouping GR_1 { + leaf GR_1_LEAF_1 { + type string; + } + leaf GR_1_LEAF_2 { + type string; + } + } + + grouping GR_2 { + leaf GR_2_LEAF_1 { + type string; + } + leaf GR_2_LEAF_2 { + type string; + } + } +} diff --git a/tests/cli_autogen_input/yang_parser_test/sonic-grouping-2.yang b/tests/cli_autogen_input/yang_parser_test/sonic-grouping-2.yang new file mode 100644 index 0000000000..58e9df6621 --- /dev/null +++ b/tests/cli_autogen_input/yang_parser_test/sonic-grouping-2.yang @@ -0,0 +1,25 @@ +module sonic-grouping-2 { + + yang-version 1.1; + + namespace "http://github.com/Azure/s-grouping-2"; + prefix s-grouping-2; + + grouping GR_3 { + leaf GR_3_LEAF_1 { + type string; + } + leaf GR_3_LEAF_2 { + type string; + } + } + + grouping GR_4 { + leaf GR_4_LEAF_1 { + type string; + } + leaf GR_4_LEAF_2 { + type string; + } + } +} diff --git a/tests/cli_autogen_input/yang_parser_test/sonic-grouping-complex.yang b/tests/cli_autogen_input/yang_parser_test/sonic-grouping-complex.yang new file mode 100644 index 0000000000..22956789b0 --- /dev/null +++ b/tests/cli_autogen_input/yang_parser_test/sonic-grouping-complex.yang @@ -0,0 +1,96 @@ +module sonic-grouping-complex { + + yang-version 1.1; + + namespace "http://github.com/Azure/grouping-complex"; + prefix grouping-complex; + + import sonic-grouping-1 { + prefix sgroup1; + } + + import sonic-grouping-2 { + prefix sgroup2; + } + + grouping GR_5 { + leaf GR_5_LEAF_1 { + type string; + } + + leaf-list GR_5_LEAF_LIST_1 { + type string; + } + } + + grouping GR_6 { + leaf GR_6_LEAF_1 { + type string; + } + + leaf GR_6_LEAF_2 { + type string; + } + + choice GR_6_CHOICE_1 { + case CHOICE_1_CASE_1 { + leaf GR_6_CASE_1_LEAF_1 { + type uint16; + } + + leaf-list GR_6_CASE_1_LEAF_LIST_1 { + type string; + } + } + + case CHOICE_1_CASE_2 { + leaf GR_6_CASE_2_LEAF_1 { + type uint16; + } + + leaf GR_6_CASE_2_LEAF_2 { + type uint16; + } + + leaf-list GR_6_CASE_2_LEAF_LIST_1 { + type string; + } + + leaf-list GR_6_CASE_2_LEAF_LIST_2 { + type string; + } + } + } + } + + container sonic-grouping-complex { + /* sonic-grouping-complex - top level container */ + + container TABLE_1 { + /* TABLE_1 - table container */ + + description "TABLE_1 description"; + + container OBJECT_1 { + /* OBJECT_1 - object container, it have + * 1 choice, which have 2 cases. + * first case have: 1 leaf, 1 leaf-list, 1 uses + * second case have: 2 leafs, 2 leaf-lists, 2 uses + */ + + description "OBJECT_1 description"; + + uses sgroup1:GR_1; + } + + container OBJECT_2 { + + description "OBJECT_2 description"; + + uses GR_5; + uses GR_6; + uses sgroup2:GR_4; + } + } + } +} diff --git a/tests/cli_autogen_input/yang_parser_test/sonic-static-object-complex-1.yang b/tests/cli_autogen_input/yang_parser_test/sonic-static-object-complex-1.yang new file mode 100644 index 0000000000..fa082d3b25 --- /dev/null +++ b/tests/cli_autogen_input/yang_parser_test/sonic-static-object-complex-1.yang @@ -0,0 +1,49 @@ +module sonic-static-object-complex-1 { + + yang-version 1.1; + + namespace "http://github.com/Azure/static-complex-1"; + prefix static-complex-1; + + container sonic-static-object-complex-1 { + /* sonic-static-object-complex-1 - top level container */ + + container TABLE_1 { + /* TABLE_1 - table container */ + + description "TABLE_1 description"; + + container OBJECT_1 { + /* OBJECT_1 - object container, it have: + * 1 leaf, + * 1 leaf-list + * 1 choice + */ + + description "OBJECT_1 description"; + + leaf OBJ_1_LEAF_1 { + description "OBJ_1_LEAF_1 description"; + type string; + } + + leaf-list OBJ_1_LEAF_LIST_1 { + type string; + } + + choice OBJ_1_CHOICE_1 { + case OBJ_1_CHOICE_1_CASE_1 { + leaf OBJ_1_CHOICE_1_LEAF_1 { + type uint16; + } + } + case OBJ_1_CHOICE_1_CASE_2 { + leaf OBJ_1_CHOICE_1_LEAF_2 { + type string; + } + } + } + } + } + } +} diff --git a/tests/cli_autogen_input/yang_parser_test/sonic-static-object-complex-2.yang b/tests/cli_autogen_input/yang_parser_test/sonic-static-object-complex-2.yang new file mode 100644 index 0000000000..4e53b2e1b1 --- /dev/null +++ b/tests/cli_autogen_input/yang_parser_test/sonic-static-object-complex-2.yang @@ -0,0 +1,71 @@ +module sonic-static-object-complex-2 { + + yang-version 1.1; + + namespace "http://github.com/Azure/static-complex-2"; + prefix static-complex-2; + + container sonic-static-object-complex-2 { + /* sonic-static-object-complex-2 - top level container */ + + container TABLE_1 { + /* TABLE_1 - table container */ + + description "TABLE_1 description"; + + container OBJECT_1 { + /* OBJECT_1 - object container, it have: + * 2 leafs, + * 2 leaf-lists, + * 2 choices + */ + + description "OBJECT_1 description"; + + leaf OBJ_1_LEAF_1 { + description "OBJ_1_LEAF_1 description"; + type string; + } + + leaf OBJ_1_LEAF_2 { + description "OBJ_1_LEAF_2 description"; + type string; + } + + leaf-list OBJ_1_LEAF_LIST_1 { + type string; + } + + leaf-list OBJ_1_LEAF_LIST_2 { + type string; + } + + choice OBJ_1_CHOICE_1 { + case OBJ_1_CHOICE_1_CASE_1 { + leaf OBJ_1_CHOICE_1_LEAF_1 { + type uint16; + } + } + case OBJ_1_CHOICE_1_CASE_2 { + leaf OBJ_1_CHOICE_1_LEAF_2 { + type string; + } + } + } + + choice OBJ_1_CHOICE_2 { + case OBJ_1_CHOICE_2_CASE_1 { + leaf OBJ_1_CHOICE_2_LEAF_1 { + type uint16; + } + } + case OBJ_1_CHOICE_2_CASE_2 { + leaf OBJ_1_CHOICE_2_LEAF_2 { + type string; + } + } + } + } + } + } +} diff --git a/tests/cli_autogen_test.py b/tests/cli_autogen_test.py new file mode 100644 index 0000000000..13407d1c13 --- /dev/null +++ b/tests/cli_autogen_test.py @@ -0,0 +1,243 @@ +import os +import logging +import pytest + +import show.plugins as show_plugins +import show.main as show_main +import config.plugins as config_plugins +import config.main as config_main +from .cli_autogen_input.autogen_test import show_cmd_output +from .cli_autogen_input.cli_autogen_common import backup_yang_models, restore_backup_yang_models, move_yang_models, remove_yang_models + +from utilities_common import util_base +from sonic_cli_gen.generator import CliGenerator +from .mock_tables import dbconnector +from utilities_common.db import Db +from click.testing import CliRunner + +logger = logging.getLogger(__name__) +gen = CliGenerator(logger) + +test_path = os.path.dirname(os.path.abspath(__file__)) +mock_db_path = os.path.join(test_path, 'cli_autogen_input', 'config_db') +config_db_path = os.path.join(test_path, 'cli_autogen_input', 'config_db.json') +templates_path = os.path.join(test_path, '../', 'sonic-utilities-data', 'templates', 'sonic-cli-gen') + +SUCCESS = 0 +ERROR = 1 +INVALID_VALUE = 'INVALID' + +test_yang_models = [ + 'sonic-device_metadata.yang', + 'sonic-device_neighbor.yang', +] + + +class TestCliAutogen: + @classmethod + def setup_class(cls): + logger.info('SETUP') + os.environ['UTILITIES_UNIT_TESTING'] = '2' + + backup_yang_models() + move_yang_models(test_path, 'autogen_test', test_yang_models) + + for yang_model in test_yang_models: + gen.generate_cli_plugin( + cli_group='show', + plugin_name=yang_model.split('.')[0], + config_db_path=config_db_path, + templates_path=templates_path + ) + gen.generate_cli_plugin( + cli_group='config', + plugin_name=yang_model.split('.')[0], + config_db_path=config_db_path, + templates_path=templates_path + ) + + helper = util_base.UtilHelper() + helper.load_and_register_plugins(show_plugins, show_main.cli) + helper.load_and_register_plugins(config_plugins, config_main.config) + + + @classmethod + def teardown_class(cls): + logger.info('TEARDOWN') + + for yang_model in test_yang_models: + gen.remove_cli_plugin('show', yang_model.split('.')[0]) + gen.remove_cli_plugin('config', yang_model.split('.')[0]) + + restore_backup_yang_models() + + dbconnector.dedicated_dbs['CONFIG_DB'] = None + + os.environ['UTILITIES_UNIT_TESTING'] = '0' + + + def test_show_device_metadata(self): + dbconnector.dedicated_dbs['CONFIG_DB'] = mock_db_path + db = Db() + runner = CliRunner() + + result = runner.invoke( + show_main.cli.commands['device-metadata'].commands['localhost'], [], obj=db + ) + + logger.debug("\n" + result.output) + logger.debug(result.exit_code) + assert result.exit_code == SUCCESS + assert result.output == show_cmd_output.show_device_metadata_localhost + + + def test_config_device_metadata(self): + dbconnector.dedicated_dbs['CONFIG_DB'] = mock_db_path + db = Db() + runner = CliRunner() + + result = runner.invoke( + config_main.config.commands['device-metadata'].commands['localhost'].commands['buffer-model'], ['dynamic'], obj=db + ) + + result = runner.invoke( + show_main.cli.commands['device-metadata'].commands['localhost'], [], obj=db + ) + + logger.debug("\n" + result.output) + logger.debug(result.exit_code) + assert result.exit_code == SUCCESS + assert result.output == show_cmd_output.show_device_metadata_localhost_changed_buffer_model + + + @pytest.mark.parametrize("parameter,value", [ + ('default-bgp-status', INVALID_VALUE), + ('docker-routing-config-mode', INVALID_VALUE), + ('mac', INVALID_VALUE), + ('default-pfcwd-status', INVALID_VALUE), + ('bgp-asn', INVALID_VALUE), + ('type', INVALID_VALUE), + ('buffer-model', INVALID_VALUE), + ('frr-mgmt-framework-config', INVALID_VALUE) + ]) + def test_config_device_metadata_invalid(self, parameter, value): + dbconnector.dedicated_dbs['CONFIG_DB'] = mock_db_path + db = Db() + runner = CliRunner() + + result = runner.invoke( + config_main.config.commands['device-metadata'].commands['localhost'].commands[parameter], [value], obj=db + ) + + logger.debug("\n" + result.output) + logger.debug(result.exit_code) + assert result.exit_code == ERROR + + + def test_show_device_neighbor(self): + dbconnector.dedicated_dbs['CONFIG_DB'] = mock_db_path + db = Db() + runner = CliRunner() + + result = runner.invoke( + show_main.cli.commands['device-neighbor'], [], obj=db + ) + + logger.debug("\n" + result.output) + logger.debug(result.exit_code) + assert show_cmd_output.show_device_neighbor + assert result.exit_code == SUCCESS + + + def test_config_device_neighbor_add(self): + dbconnector.dedicated_dbs['CONFIG_DB'] = mock_db_path + db = Db() + runner = CliRunner() + + result = runner.invoke( + config_main.config.commands['device-neighbor'].commands['add'], + ['Ethernet8', '--name', 'Servers1', '--mgmt-addr', '10.217.0.3', + '--local-port', 'Ethernet8', '--port', 'eth2', '--type', 'type'], + obj=db + ) + logger.debug("\n" + result.output) + logger.debug(result.exit_code) + + result = runner.invoke( + show_main.cli.commands['device-neighbor'], [], obj=db + ) + + logger.debug("\n" + result.output) + logger.debug(result.exit_code) + assert result.exit_code == SUCCESS + assert result.output == show_cmd_output.show_device_neighbor_added + + + def test_config_device_neighbor_delete(self): + dbconnector.dedicated_dbs['CONFIG_DB'] = mock_db_path + db = Db() + runner = CliRunner() + + result = runner.invoke( + config_main.config.commands['device-neighbor'].commands['delete'], + ['Ethernet0'], obj=db + ) + logger.debug("\n" + result.output) + logger.debug(result.exit_code) + + result = runner.invoke( + show_main.cli.commands['device-neighbor'], [], obj=db + ) + + logger.debug("\n" + result.output) + logger.debug(result.exit_code) + assert result.exit_code == SUCCESS + assert result.output == show_cmd_output.show_device_neighbor_deleted + + + @pytest.mark.parametrize("parameter,value,output", [ + ('--mgmt-addr', '10.217.0.5', show_cmd_output.show_device_neighbor_updated_mgmt_addr), + ('--name', 'Servers1', show_cmd_output.show_device_neighbor_updated_name), + ('--local-port', 'Ethernet12', show_cmd_output.show_device_neighbor_updated_local_port), + ('--port', 'eth2', show_cmd_output.show_device_neighbor_updated_port), + ('--type', 'type2', show_cmd_output.show_device_neighbor_updated_type), + ]) + def test_config_device_neighbor_update(self, parameter, value, output): + dbconnector.dedicated_dbs['CONFIG_DB'] = mock_db_path + db = Db() + runner = CliRunner() + + result = runner.invoke( + config_main.config.commands['device-neighbor'].commands['update'], + ['Ethernet0', parameter, value], obj=db + ) + logger.debug("\n" + result.output) + logger.debug(result.exit_code) + + result = runner.invoke( + show_main.cli.commands['device-neighbor'], [], obj=db + ) + + logger.debug("\n" + result.output) + logger.debug(result.exit_code) + assert result.exit_code == SUCCESS + assert result.output == output + + + @pytest.mark.parametrize("parameter,value", [ + ('--mgmt-addr', INVALID_VALUE), + ('--local-port', INVALID_VALUE) + ]) + def test_config_device_neighbor_update_invalid(self, parameter, value): + dbconnector.dedicated_dbs['CONFIG_DB'] = mock_db_path + db = Db() + runner = CliRunner() + + result = runner.invoke( + config_main.config.commands['device-neighbor'].commands['update'], + ['Ethernet0', parameter, value], obj=db + ) + logger.debug("\n" + result.output) + logger.debug(result.exit_code) + assert result.exit_code == ERROR + diff --git a/tests/cli_autogen_yang_parser_test.py b/tests/cli_autogen_yang_parser_test.py new file mode 100644 index 0000000000..ed82693e91 --- /dev/null +++ b/tests/cli_autogen_yang_parser_test.py @@ -0,0 +1,172 @@ +import os +import logging +import pprint + +from sonic_cli_gen.yang_parser import YangParser +from .cli_autogen_input.yang_parser_test import assert_dictionaries +from .cli_autogen_input.cli_autogen_common import move_yang_models, remove_yang_models + +logger = logging.getLogger(__name__) + +test_path = os.path.dirname(os.path.abspath(__file__)) + +test_yang_models = [ + 'sonic-1-table-container.yang', + 'sonic-2-table-containers.yang', + 'sonic-1-object-container.yang', + 'sonic-2-object-containers.yang', + 'sonic-1-list.yang', + 'sonic-2-lists.yang', + 'sonic-static-object-complex-1.yang', + 'sonic-static-object-complex-2.yang', + 'sonic-dynamic-object-complex-1.yang', + 'sonic-dynamic-object-complex-2.yang', + 'sonic-choice-complex.yang', + 'sonic-grouping-complex.yang', + 'sonic-grouping-1.yang', + 'sonic-grouping-2.yang', +] + + +class TestYangParser: + @classmethod + def setup_class(cls): + logger.info("SETUP") + os.environ['UTILITIES_UNIT_TESTING'] = "2" + move_yang_models(test_path, 'yang_parser_test', test_yang_models) + + @classmethod + def teardown_class(cls): + logger.info("TEARDOWN") + os.environ['UTILITIES_UNIT_TESTING'] = "0" + remove_yang_models(test_yang_models) + + def test_1_table_container(self): + """ Test for 1 'table' container + 'table' container represent TABLE in Config DB schema: + { + "TABLE": { + "OBJECT": { + "attr": "value" + ... + } + } + } + """ + + base_test('sonic-1-table-container', + assert_dictionaries.one_table_container) + + def test_2_table_containers(self): + """ Test for 2 'table' containers """ + + base_test('sonic-2-table-containers', + assert_dictionaries.two_table_containers) + + def test_1_object_container(self): + """ Test for 1 'object' container + 'object' container represent OBJECT in Config DB schema: + { + "TABLE": { + "OBJECT": { + "attr": "value" + ... + } + } + } + """ + + base_test('sonic-1-object-container', + assert_dictionaries.one_object_container) + + def test_2_object_containers(self): + """ Test for 2 'object' containers """ + + base_test('sonic-2-object-containers', + assert_dictionaries.two_object_containers) + + def test_1_list(self): + """ Test for 1 container that has inside + the YANG 'list' entity + """ + + base_test('sonic-1-list', assert_dictionaries.one_list) + + def test_2_lists(self): + """ Test for 2 containers that have inside + the YANG 'list' entity + """ + + base_test('sonic-2-lists', assert_dictionaries.two_lists) + + def test_static_object_complex_1(self): + """ Test for the object container with: + 1 leaf, 1 leaf-list, 1 choice. + """ + + base_test('sonic-static-object-complex-1', + assert_dictionaries.static_object_complex_1) + + def test_static_object_complex_2(self): + """ Test for object container with: + 2 leafs, 2 leaf-lists, 2 choices. + """ + + base_test('sonic-static-object-complex-2', + assert_dictionaries.static_object_complex_2) + + def test_dynamic_object_complex_1(self): + """ Test for object container with: + 1 key, 1 leaf, 1 leaf-list, 1 choice. + """ + + base_test('sonic-dynamic-object-complex-1', + assert_dictionaries.dynamic_object_complex_1) + + def test_dynamic_object_complex_2(self): + """ Test for object container with: + 2 keys, 2 leafs, 2 leaf-list, 2 choice. + """ + + base_test('sonic-dynamic-object-complex-2', + assert_dictionaries.dynamic_object_complex_2) + + def test_choice_complex(self): + """ Test for object container with the 'choice' + that have complex strucutre: + leafs, leaf-lists, multiple 'uses' from different files + """ + + base_test('sonic-choice-complex', + assert_dictionaries.choice_complex) + + def test_grouping_complex(self): + """ Test for object container with multitple 'uses' that using 'grouping' + from different files. The used 'grouping' have a complex structure: + leafs, leaf-lists, choices + """ + + base_test('sonic-grouping-complex', + assert_dictionaries.grouping_complex) + + +def base_test(yang_model_name, correct_dict): + """ General logic for each test case """ + + config_db_path = os.path.join(test_path, + 'mock_tables/config_db.json') + parser = YangParser(yang_model_name=yang_model_name, + config_db_path=config_db_path, + allow_tbl_without_yang=True, + debug=False) + yang_dict = parser.parse_yang_model() + pretty_log_debug(yang_dict) + assert yang_dict == correct_dict + + +def pretty_log_debug(dictionary): + """ Pretty print of parsed dictionary """ + + for line in pprint.pformat(dictionary).split('\n'): + logging.debug(line) + diff --git a/utilities_common/util_base.py b/utilities_common/util_base.py index ff5570735c..98fc230629 100644 --- a/utilities_common/util_base.py +++ b/utilities_common/util_base.py @@ -24,6 +24,7 @@ def iter_namespace(ns_pkg): for _, module_name, ispkg in iter_namespace(plugins_namespace): if ispkg: + yield from self.load_plugins(importlib.import_module(module_name)) continue log.log_debug('importing plugin: {}'.format(module_name)) try: @@ -82,3 +83,9 @@ def check_pddf_mode(self): return True else: return False + + def load_and_register_plugins(self, plugins, cli): + """ Load plugins and register them """ + + for plugin in self.load_plugins(plugins): + self.register_plugin(plugin, cli) \ No newline at end of file