Skip to content

Commit 464edf9

Browse files
authored
Implement "float" and "time" data types (ipspace#2620)
1 parent b8ecf15 commit 464edf9

File tree

8 files changed

+112
-18
lines changed

8 files changed

+112
-18
lines changed

docs/dev/validation.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ The dictionary-based approach allows in-depth validation of nested attributes, w
8383
(dev-valid-data-types)=
8484
## Valid Data Types
8585

86-
Validator recognizes standard Python data types (**str**, **int**, **bool**, **list** or **dict**) and the following networking-specific data types:
86+
Validator recognizes standard Python data types (**str**, **int**, **float**, **bool**, **list** or **dict**) and the following networking-specific data types:
8787

8888
| Data type. | Meaning |
8989
|----------------|---------|
@@ -102,6 +102,7 @@ Validator recognizes standard Python data types (**str**, **int**, **bool**, **l
102102
| **prefix_str** | An IPv4 or IPv6 prefix |
103103
| **rd** | Route distinguisher (ASN:ID or IP:ID) |
104104
| **r_proto** | Routing protocol identifier |
105+
| **time** | Time duration specified in seconds (`s`) or milliseconds (`ms`) |
105106

106107
The data type can be specified as a string (without additional parameters) or a dictionary with a **type** attribute (data type as a string) and other type-specific validation parameters.
107108

@@ -145,6 +146,8 @@ When an attribute has a data type defined with the **type** attribute, you can u
145146
| | **_subtype** -- validate values as belonging to the specified subtype |
146147
| | **_keytype** -- validate keys as belonging to the specified scalar type |
147148
| | **_list_to_dict** -- [value can be specified as a list](validation-list-to-dict) |
149+
| **float** | **min_value** -- minimum parameter value |
150+
| | **max_value** -- maximum parameter value |
148151
| **id** | **max_length** -- maximum identifier length |
149152
| **int** | **min_value** -- minimum parameter value |
150153
| | **max_value** -- maximum parameter value |

netsim/data/types.py

Lines changed: 59 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import functools
77
import ipaddress
88
import netaddr
9+
import datetime
910
import re
1011
import textwrap
1112

@@ -491,31 +492,34 @@ def must_be_id(value: typing.Any, max_length: typing.Union[int,str] = 16) -> dic
491492

492493
return { '_valid': True }
493494

494-
def check_int_type(
495+
def check_num_type(
495496
value: typing.Any,
496497
min_value: typing.Optional[int] = None, # Minimum value
497498
max_value: typing.Optional[int] = None, # Maximum value
498-
) -> dict:
499-
if not isinstance(value,int): # value must be an int
500-
return { '_type': 'an integer' }
499+
datatype: typing.Type = int,
500+
dataname: str = 'an integer') -> dict:
501+
502+
# Value must be an integer or the specified data type
503+
if not isinstance(value,(int,datatype)):
504+
return { '_type': dataname }
501505

502506
if isinstance(value,bool): # but not a bool
503-
return { '_value': 'a true integer (not a bool)' }
507+
return { '_value': 'a true number (not a bool)' }
504508

505509
if isinstance(max_value,str):
506510
max_value = resolve_const_value(max_value,None)
507511
if isinstance(min_value,str):
508512
min_value = resolve_const_value(min_value,None)
509513

510-
if isinstance(min_value,int) and isinstance(max_value,int):
514+
if isinstance(min_value,(int,datatype)) and isinstance(max_value,(int,datatype)):
511515
if value < min_value or value > max_value:
512-
return { '_value': f'an integer between {min_value} and {max_value}' }
513-
elif isinstance(min_value,int):
516+
return { '_value': f'{dataname} between {min_value} and {max_value}' }
517+
elif isinstance(min_value,(int,datatype)):
514518
if value < min_value:
515-
return { '_value': f'an integer larger or equal to {min_value}' }
516-
elif isinstance(max_value,int):
519+
return { '_value': f'{dataname} larger or equal to {min_value}' }
520+
elif isinstance(max_value,(int,datatype)):
517521
if value > max_value:
518-
return { '_value': f'an integer less than or equal to {max_value}' }
522+
return { '_value': f'{dataname} less than or equal to {max_value}' }
519523

520524
return { '_valid': True }
521525

@@ -536,7 +540,26 @@ def transform_to_int(value: typing.Any) -> int:
536540
except:
537541
pass
538542

539-
return(check_int_type(value,min_value,max_value))
543+
return(check_num_type(value,min_value,max_value))
544+
545+
@type_test()
546+
def must_be_float(
547+
value: typing.Any,
548+
min_value: typing.Optional[int] = None, # Minimum value
549+
max_value: typing.Optional[int] = None, # Maximum value
550+
) -> dict:
551+
552+
def transform_to_float(value: typing.Any) -> float:
553+
return float(value)
554+
555+
if isinstance(value,str): # Try to convert STR to INT
556+
try:
557+
transform_to_float(value)
558+
return { '_valid': True, '_transform': transform_to_float }
559+
except:
560+
pass
561+
562+
return(check_num_type(value,min_value,max_value,float,'a number'))
540563

541564
@type_test(false_value=False)
542565
def must_be_bool(value: typing.Any) -> dict:
@@ -564,6 +587,30 @@ def must_be_bool_false(value: typing.Any) -> dict:
564587

565588
return { '_valid': True } if value is False else { '_type': 'False' }
566589

590+
@type_test()
591+
def must_be_time(value: typing.Any) -> dict:
592+
593+
def transform_to_msec(duration: str) -> int:
594+
# Regex to capture number + unit (e.g. "200ms", "1s", "5m")
595+
match = re.fullmatch(r"(\d+(?:\.\d+)?)(ms|s)", duration.strip())
596+
if not match:
597+
raise ValueError(f"Invalid duration format: '{duration}'")
598+
599+
value, unit = match.groups()
600+
return int(value) if unit == 'ms' else int(1000 * float(value))
601+
602+
if isinstance(value,(int,float)) and not isinstance(value,bool):
603+
return { '_valid': True }
604+
605+
if isinstance(value,str): # Try to convert STR to delta time
606+
try:
607+
transform_to_msec(value)
608+
return { '_valid': True, '_transform': transform_to_msec }
609+
except Exception as ex:
610+
pass
611+
612+
return { '_type': 'a time interval'}
613+
567614
@type_test()
568615
def must_be_asn2(value: typing.Any) -> dict: # 2-octet ASN (in case we need it somewhere)
569616
err = 'an AS number (integer between 1 and 65535)'

netsim/extra/bgp.policy/plugin.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ def must_be_autobw(
2323
if value == 'auto':
2424
return { '_valid': True }
2525

26-
result = types.check_int_type(value,min_value,max_value)
26+
result = types.check_num_type(value,min_value,max_value)
2727
return expected_type if '_type' in result else result
2828

2929
types.register_type('autobw',must_be_autobw)

tests/errors/ia-bfd.log

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ IncorrectType in bfd: attribute 'bfd.min_tx' must be an integer, found BoxList
33
IncorrectType in bfd: attribute 'bfd.min_rx' must be an integer, found str
44
IncorrectValue in bfd: attribute 'nodes.r1.bfd.min_echo_rx' must be an integer larger or equal to 0
55
... use 'netlab show attributes --module bfd node' to display valid attributes
6-
IncorrectValue in bfd: attribute 'nodes.r1.bfd.multiplier' must be a true integer (not a bool)
6+
IncorrectValue in bfd: attribute 'nodes.r1.bfd.multiplier' must be a true number (not a bool)
77
IncorrectType in bfd: attribute 'nodes.r1.bfd.min_tx' must be an integer, found BoxList
88
IncorrectType in bfd: attribute 'nodes.r1.bfd.min_rx' must be an integer, found str
99
IncorrectType in bfd: attribute 'nodes.r2.bfd.min_tx' must be an integer, found NoneType

tests/errors/link-invalid-attr.log

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ IncorrectAttr in links: Invalid link attribute 'wtf' found in links[1]
33
IncorrectAttr in ospf: Invalid ospf link attribute 'wtf' found in links[1].ospf
44
... use 'netlab show attributes --module ospf link' to display valid attributes
55
IncorrectAttr in links: links[1] uses an attribute from module bgp which is not enabled in topology
6+
IncorrectType in links: attribute 'links[1].tc.x_float' must be a number, found str
7+
IncorrectValue in links: attribute 'links[1].tc.loss' must be a number between 0 and 100
8+
IncorrectType in links: attribute 'links[1].tc.delay' must be a time interval, found str
9+
IncorrectType in links: attribute 'links[1].tc.jitter' must be a time interval, found str
610
IncorrectAttr in links: Invalid interface attribute 'wtf' found in links[1].r1
711
... use 'netlab show attributes interface' to display valid attributes
812
IncorrectAttr in ospf: Invalid ospf interface attribute 'process' found in links[1].r1.ospf

tests/errors/link-invalid-attr.yml

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,18 @@
22
# Invalid link attribute
33
#
44
---
5-
module: [ ospf ]
6-
75
defaults:
8-
device: iosv
6+
device: none
7+
attributes:
8+
link:
9+
tc:
10+
delay: time
11+
jitter: time
12+
loss: { type: float, min_value: 0, max_value: 100 }
13+
x_float: float
14+
rate: { type: int, min_value: 1 }
15+
16+
module: [ ospf ]
917

1018
nodes:
1119
r1:
@@ -29,3 +37,8 @@ links:
2937
ospf.cost: 10
3038
ospf.wtf: 20
3139
bgp.advertise: True
40+
tc:
41+
x_float: really
42+
loss: 107.3
43+
delay: 2m
44+
jitter: 0,2ms

tests/topology/expected/link-bw.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ links:
2020
prefix:
2121
ipv4: 192.168.23.0/24
2222
ipv6: 2001:db8:cafe:2::/64
23+
tc:
24+
delay: 2
25+
jitter: 13
26+
loss: 0.3
27+
rate: 2000
2328
type: p2p
2429
name: input
2530
nodes:
@@ -43,6 +48,11 @@ nodes:
4348
ipv4: 192.168.23.2/24
4449
ipv6: 2001:db8:cafe:2::2/64
4550
node: e2
51+
tc:
52+
delay: 2
53+
jitter: 13
54+
loss: 0.3
55+
rate: 2000
4656
type: p2p
4757
loopback:
4858
ifindex: 0
@@ -77,6 +87,11 @@ nodes:
7787
ipv4: 192.168.23.1/24
7888
ipv6: 2001:db8:cafe:2::1/64
7989
node: e1
90+
tc:
91+
delay: 2
92+
jitter: 13
93+
loss: 0.3
94+
rate: 2000
8095
type: p2p
8196
loopback:
8297
ifindex: 0

tests/topology/input/link-bw.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
defaults:
22
device: iosv
33
inventory: dump
4+
attributes:
5+
link:
6+
tc:
7+
delay: time
8+
jitter: time
9+
loss: { type: float, min_value: 0, max_value: 100 }
10+
rate: { type: int, min_value: 1 }
411

512
nodes:
613
e1:
@@ -13,3 +20,8 @@ links:
1320
ipv4: 192.168.23.0/24
1421
ipv6: 2001:db8:cafe:2::/64
1522
bandwidth: 100000
23+
tc:
24+
delay: 2ms
25+
jitter: 0.013s
26+
loss: 0.3
27+
rate: 2000

0 commit comments

Comments
 (0)