Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions docs/dev/validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,43 @@ ospf:
ipv6: bool
```

(validation-value-to-dict)=
### Dictionary Specified As a Single Value

Some _netlab_ attributes that are supposed to be dictionaries can accept non-dictionary values. You can use the **_value_to_dict** type definition key to specify a template dictionary that is used to create a replacement dictionary. The values of the replacement dictionary can be f-strings with `{value}` representing the original value.

```{warning}
The `{value}` string could be interpreted as a YAML dictionary and MUST therefore be quoted.
```

For example, to transform a BGP role specified as a string into a dictionary with the role name and `strict` flag, use this type definition:

```
attributes:
node:
role:
name:
Comment thread
ipspace marked this conversation as resolved.
type: str
valid_values: [ provider, customer, peer, rs-server, rs-client ]
strict: bool
_value_to_dict:
name: '{value}'
Comment thread
ipspace marked this conversation as resolved.
strict: False
```

You can often use the `_value_to_dict` functionality instead of `_alt_types` or custom data transformation. For example, the following data type definition for the BGP community attribute replaced a chunk of somewhat convoluted Python code:

```
attributes:
global:
community:
ibgp: { type: list, _subtype: bgp_community_type }
ebgp: { type: list, _subtype: bgp_community_type }
_value_to_dict:
ibgp: '{value}'
ebgp: '{value}'
```

(validation-ip-address)=
## IP Address Validation

Expand Down
39 changes: 37 additions & 2 deletions netsim/data/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@
# Data validation routines
#

import ast
import typing

from box import Box

from ..utils import log
from . import get_a_list, get_empty_box
from ..utils import log, strings
from . import get_a_list, get_box, get_empty_box

# We also need to import the whole data.types module to be able to do validation function lookup
from . import types as _tv
Expand Down Expand Up @@ -482,6 +483,23 @@ def check_valid_with(
category=log.IncorrectAttr,
module=module)

def transform_value_to_dict(data: typing.Any, mapping: typing.Any) -> typing.Any:
"""
Transform a non-dict value to a dictionary using 'mapping' template
"""
if isinstance(mapping,dict): # Recursively transform boxes and dicts
return get_box({ k: transform_value_to_dict(data,v) for k,v in mapping.items() })
if isinstance(mapping,list): # Recursively transform lists
return [ transform_value_to_dict(data,v) for v in mapping ]
if not isinstance(mapping,str) or '{' not in mapping:
return mapping # Return values that are not f-string verbatim

result = strings.eval_format(mapping,{ 'value': data })
try: # Evalute f-string and try evaluate its results
return ast.literal_eval(result) # as we could use f-string to generate a list or a computed int
except Exception:
return result # If the f-string result doesn't parse, return it as string
Comment thread
ipspace marked this conversation as resolved.

"""
validate_item -- validate a single item from an object:

Expand Down Expand Up @@ -550,6 +568,23 @@ def validate_item(
data = parent[key]
data_type = Box(data_type) # and fix datatype definition

# Another corner case: data type is a dictionary, we have a non-dict value, and the data type definition
# gives us a template to transform that value into a dictionary
#
if not isinstance(data,dict) and '_value_to_dict' in data_type and parent is not None:
try:
parent[key] = transform_value_to_dict(data,data_type._value_to_dict)
except Exception as ex:
Comment thread
ipspace marked this conversation as resolved.
log.error( # ... to log all dependency errors
f"Cannot transform value of attribute '{parent_path}.{key}' into a dictionary",
more_data=[str(ex)],
category=log.IncorrectValue,
module=module)
return False

data = parent[key]
data_type = Box(data_type)

alt_context = {} # Alt-type context passed to validation functions
if '_alt_types' in data_type: # Deal with alternate types first
alt_context = { '_alt_types': data_type._alt_types }
Expand Down
27 changes: 0 additions & 27 deletions netsim/modules/bgp.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,33 +61,6 @@ def check_bgp_parameters(node: Box, topology: Box) -> None:

must_be_asn(parent=node,key='bgp.as',path=f'nodes.{node.name}',module='bgp')

if "community" in node.bgp:
bgp_comm = node.bgp.community
if isinstance(bgp_comm,str):
node.bgp.community = { 'ibgp' : [ bgp_comm ], 'ebgp': [ bgp_comm ]}
elif isinstance(bgp_comm,list):
node.bgp.community = { 'ibgp' : bgp_comm, 'ebgp': bgp_comm }
elif not(isinstance(bgp_comm,dict)):
log.error(
f"bgp.community attribute in node {node.name} should be a string, a list, or a dictionary (found: {bgp_comm})",
log.IncorrectType,
'bgp')
return

for k in node.bgp.community.keys():
if not k in ['ibgp','ebgp']:
log.error(
text=f"Invalid BGP community setting in {k} node {node.name}",
category=log.IncorrectValue,
module='bgp')
else:
must_be_list(
parent=node.bgp.community,
path=f'nodes.{node.name}.bgp.community',
key=k,
valid_values=topology['defaults.attributes.global.bgp_community_type.valid_values'],
module='bgp')

def validate_bgp_sessions(node: Box, sessions: Box, attribute: str) -> bool:
OK = True
for k in list(sessions.keys()):
Expand Down
4 changes: 3 additions & 1 deletion netsim/modules/bgp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ attributes:
community:
ibgp: { type: list, _subtype: bgp_community_type }
ebgp: { type: list, _subtype: bgp_community_type }
_alt_types: [ str, BoxList ]
_value_to_dict:
ibgp: '{value}'
ebgp: '{value}'
replace_global_as: bool
confederation:
type: dict
Expand Down
Loading