diff --git a/docs/topology/validate.md b/docs/topology/validate.md index 3e9db17a48..87f1e8c440 100644 --- a/docs/topology/validate.md +++ b/docs/topology/validate.md @@ -50,6 +50,7 @@ The **config** parameter can be a string (the template to deploy) or a dictionar **Notes:** * Every test entry should have **show**, **exec**, **config**, **suzieq**, **ansible** or **wait** parameter. +* The device types specified in **show**, **exec**, or **valid** dictionaries can be *parent devices* (for example, `ios` to cover all Cisco IOS platforms). * A test entry with just the **wait** parameter is valid and can be used to delay the test procedure. * Test entries with **show** parameter must have **valid** expression. * Test entries with **valid** expression must have **show**, **exec**, **suzieq**, or **ansible** parameter. diff --git a/netsim/cli/validate/tests.py b/netsim/cli/validate/tests.py index 00d68ac00a..84b133a6d6 100644 --- a/netsim/cli/validate/tests.py +++ b/netsim/cli/validate/tests.py @@ -124,7 +124,8 @@ def execute_node_validation( node = topology.nodes[n_name] result = data.get_empty_box() - action = utils.find_test_action(v_entry,node) # Find the action to show/execute/wait + # Find the action to show/execute/wait + action = utils.find_test_action(v_entry,node,topology) if action == 'wait': # Test with pure 'wait' return (True,True) # is assumed to be successful diff --git a/netsim/cli/validate/utils.py b/netsim/cli/validate/utils.py index aeff75d2b1..4e4421028c 100644 --- a/netsim/cli/validate/utils.py +++ b/netsim/cli/validate/utils.py @@ -7,26 +7,46 @@ from box import Box, BoxList +from ...augment import devices as a_devices from ...utils import log, templates from . import plugin, report -''' -Get generic or per-device action from a validation entry -* If the validation entry is a string, use that -* If the validation entry is a dictionary, use device-specific item -* If the result of the above is not a string we have a failure, get out -* If the resulting string contains '{{' run it through Jinja2 engine +def get_device_inheritance_list(node: Box, topology: Box) -> list: + ''' + Get the device inheritance list for the current node, starting with the + device type and ending with the most generic parent. If the device has no + 'parent' attribute, the list has one element (the device type) + ''' + dev_data = a_devices.get_consolidated_device_data(node,topology.defaults) + dev_search: list = dev_data.get('_parents',[]) + [ node.device ] + return list(reversed(dev_search)) -If we try to get the string to pass to the device from a plugin, then any -plugin evaluation errors indicate something is badly broken, so we log the -error with as much data as feasible... and if the end-user ever sees that -error message, the author of the validation plugin did a lousy job. -''' def get_entry_value(v_entry: Box, action: str, node: Box, topology: Box) -> typing.Any: - n_device = node.device + ''' + Get generic or per-device action from a validation entry + + * If the validation entry is a string, use that + * If the validation entry is a dictionary, use device-specific item + * If the result of the above is not a string we have a failure, get out + * If the resulting string contains '{{' run it through Jinja2 engine + + If we try to get the string to pass to the device from a plugin, then any + plugin evaluation errors indicate something is badly broken, so we log the + error with as much data as feasible... and if the end-user ever sees that + error message, the author of the validation plugin did a lousy job. + ''' if action in v_entry: - value = v_entry[action][n_device] if isinstance(v_entry[action],dict) else v_entry[action] + value = None + if not isinstance(v_entry[action],dict): + value = v_entry[action] + else: + for device in get_device_inheritance_list(node,topology): + if device in v_entry[action]: + if log.VERBOSE and device != node.device: + log.info(f'Using {device} {action} definition for node {node.name}/{node.device}') + value = v_entry[action][device] + break elif 'plugin' in v_entry: try: value = plugin.exec_plugin_function(action,v_entry,node) @@ -81,7 +101,7 @@ def get_exec_list(v_entry: Box, action: str, node: Box, topology: Box) -> list: * If there's no plugin, but we have 'wait' action, return 'wait' * If everything fails, return None (nothing usable for the current node) ''' -def find_test_action(v_entry: Box, node: Box) -> typing.Optional[str]: +def find_test_action(v_entry: Box, node: Box, topology: Box) -> typing.Optional[str]: action_kw_found = False for kw in ('show','exec','config','suzieq','ansible'): if kw not in v_entry: @@ -90,8 +110,11 @@ def find_test_action(v_entry: Box, node: Box) -> typing.Optional[str]: action_kw_found = True if kw in ['suzieq','config'] or isinstance(v_entry[kw],(str,int)): return kw - if node.device in v_entry[kw]: - return kw + + # Iterate device options from most-specific (device) to most generic (top parent) + for device in get_device_inheritance_list(node,topology): + if device in v_entry[kw]: + return kw if 'plugin' in v_entry: return plugin.find_plugin_action(v_entry,node) diff --git a/netsim/defaults/attributes.yml b/netsim/defaults/attributes.yml index 5d18f48f59..2fb615e788 100644 --- a/netsim/defaults/attributes.yml +++ b/netsim/defaults/attributes.yml @@ -232,7 +232,7 @@ _v_option: expression to evaluate. type: dict _alt_types: [ str ] - _keytype: device + _keytype: { type: device, include_templates: true } _r_import: _description: | diff --git a/tests/integration/services/01-dns-client.yml b/tests/integration/services/01-dns-client.yml index fdadeaa632..fab27a1698 100644 --- a/tests/integration/services/01-dns-client.yml +++ b/tests/integration/services/01-dns-client.yml @@ -32,14 +32,8 @@ validate: wait_msg: Waiting for interfaces/DNS to wake up exec: eos: ping t1 repeat 3 - iol: ping t1 - ioll2: ping t1 - iosv: ping t1 - iosvl2: ping t1 - csr: ping t1 - cat8000v: ping t1 + ios: ping t1 linux: ping -c 3 t1 - bird: ping -c 3 t1 frr: ping -c 3 t1 valid: >- "172.16.0.4" in stdout