Useful Libraries
This section contains many useful apis to help with the development of Genie Triggers or pyATS testscripts.
Dq
Dictionary query allow to question Python dictionary in a very intuitive syntax.
Find all the bgp neighbors which are Established
.
# Find all the bgp neighbors which are Established
>>> output = device.parse('show bgp neighbors')
>>> output.q.contains('Established').get_values('neighbor')
# effectively, device.parse() API returns a modified parsed dictionary
# enabling you to make quick accesses to the Dq object without having to be
# explicit. This is equivalent to:
>>> from genie.utils import Dq
>>> output = device.parse('show bgp neighbors')
>>> Dq(output).contains('Established').get_values('neighbor')
['10.2.2.2']
This navigate the dictionary nested structure, find all the keys and value
which contains the established
, return a list of neighbor.
Find all the routes which are connected
.
>>> output = device.parse('show ip route')
# Find all the routes which are Connected
output.q.contains('connected').get_values('routes')
['10.0.1.0/24', '10.1.1.1/32', '10.11.11.11/32', '172.16.21.0/24']
Find all the ospf
routes.
# Find all the routes for Ospf
output.q.contains('ospf').get_values('routes')
['10.0.2.0/24', '10.2.2.2/32']
Typically to do the same, you would need for loops, if statement, and so on. Dq simplify the whole process!
Last but not least Dq supports regex (regular expression).
# Check if the module in line card #4 contains a status
# and its value is ok or active
output.q.contains('lc').contains('4').contains_key_value('status', 'ok|active', value_regex=True)
{'lc': {'4': {'NX-OSv Ethernet Module': {'status': 'ok'}}}})
Dq support multiple Api, where each chain with each other. Dq builds its own
datastructure, which is a ListDict, you can reconstruct it as a real
dictionary with the matching requirements with the api reconstruct
.
Dq supports the following chain’d action.
contains
Filters down the dictionary and only keep the paths which contains the provided value. It will not keep anything else.
>>> output = dev.parse('show module')
>>> output.q.contains('lc')
>>> output.q.contains('lc').contains('4')
To make contains work with a regex input all you have to do is to set the regex
variable to True
within the contains api arguments, as shown in examples below
>>> output = dev.parse('show module')
>>> output.q.contains('[1,2]', regex=True)
>>> output.q.contains('.*ware', regex=True)
To do a case insensitive comparison in contains you have to set both regex and ignore_case variables to True
.
>>> output = dev.parse('show module')
# Ignores case sensitive for the value Model
>>> output.q.contains('Model', regex=True, ignore_case=True)
By giving level
, grab information from upper/lower level. In case of level=-1
, it means information from 1 above level will be collected.
>>> output = dev.parse('show module')
>>> output.q.contains('.*ware', regex=True, level=-1)
not_contains
Only keep the paths which does not contains the provided value. Very useful to remove unwanted path, and have a dictionary which only have the desired keys/paths.
>>> output = dev.parse('show module')
# Remove all linecard information from the parsed output
>>> output.q.not_contains('lc')
# Remove all linecard number 4 information from the parsed output
>>> output.q.contains('lc').not_contains('4')
# Remove all linecard number 4information from the parsed output and save as
# a new dictionary
>>> new_dict = output.q.contains('lc').not_contains('4').reconstruct()
Again, you can exclude the unwanted paths, with entering a regular expression input.
>>> output = dev.parse('show module')
# Remove all the keywrods that has address or number in them
>>> output.q.not_contains('.*(address|number).*', regex=True)
# Remove all the linecards and router processor that has the id 1 or 4
# As well as remove all the keywords that ends with phrase ware.
>>> output.q.not_contains('1|4', regex=True).not_contains('.*ware', regex=True)
As same as contains
, level
, ignore_case
argument can be passed to not_contains
.
Note
level
argument is supported only for contains
and not_contains
get_values
Return a list of the values of the key.
>>> dev.parse('show module').q.contains('ok').get_values('lc')
['2', '3', '4']
get_values
is very powerful, as it allows to collect all the values of a
specific key. It also supports the nested index.
>>> dev.parse('show interface').q.get_values('[0]')
['mgmt0', 'Ethernet2/1', 'Ethernet2/2', 'Ethernet2/3', 'Ethernet2/4', 'Ethernet2/5']
Only one value can be collected by using index
and it returns without list.
And slicing in index
is also possible. Slicing is exact same with what we can
do with list in Python.
>>> dev.parse('show interface').q.get_values('[0]', 0)
'mgmt0'
>>> dev.parse('show interface').q.get_values('[0]', '[0:2]')
['mgmt0', 'Ethernet2/1']
get_value does not return a Dq object, considered a “Final” api.
contains_key_value
Similar to contains
except instead of only the expected value the parent key is also
provided. contains_key_value
accept two arguments. One is the parent key, and
the key. Both must be following each other. The difference with contains
is
that the value can be anywhere in the nested dictionary.
It is very useful for common value, which can be present at multiple location in the dictionary.
>>> output = dev.parse('show module')
# Filter down on the first rp.
>>> output.q.contains_key_value('rp', '1')
>>> dev.parse('show interface').q.contains_key_value('enabled', True).get_values('[0]')
['mgmt0', 'Ethernet2/1', 'Ethernet2/2', 'loopback0', 'loopback1']
To input regular expression values, if looking for keys with a regex pattern you need to set
key_regex
to True. For applying regex pattern on values, you need to set value_regex
variable
to True. Examples below elaborate this functionality
>>> output = dev.parse('show module')
# If only searching for a value with regex
>>> output.q.contains_key_value('model', 'N7K.*', value_regex=True)
# If only searching for a key with regex
>>> output.q.contains_key_value('[1,2,3]', 'NX-OSv Ethernet Module', key_regex=True)
# If searching for both key and value using regex
>>> output.q.contains_key_value('slot/world_wide_name|mac.*|model', '[a-zA-Z0-9\-\s]+', key_regex=True, value_regex=True)
Similar to contains
here also you can do case insensitive comparison.
- If the key
has to be case insensitive then you have to set key_regex
and ignore_case_key
as True.
- If the value
has to be case insensitive then you have to set value_regex
and ignore_case_value
as True.
>>> output = dev.parse('show module')
# Ignores case sensitive for key CHECKSUM and value 0x1abc
>>> output.q.contains_key_value('CHECKSUM', '0x1abc', key_regex=True, ignore_case_key=True,\
value_regex=True, ignore_case_value=True)
not_contains_key_value
The opposite of contains_key_value. Only keep the path which does not contains the provided value.
>>> output = dev.parse('show module')
# Filter down on all the other module than rp 1
>>> output.q.not_contains_key_value('rp', '1')
Similar rules for regex is applied as what was already explained for contains_key_value api.
>>> output = dev.parse('show module')
# if applying regex only for value set value_regex=True
>>> output.q.not_contains_key_value('lc', '(3|4)', value_regex=True)
This one also supports both ignore_case_key
and ignore_case_value
which was already explained for contains_key_value api.
value_operator
Filter down based on the value of a certain key with {==, !=, >=, <=, >, <}
# Get all path which has crc_error greater than 100
>>> output = dev.parse('show interfaces')
>>> output.q.value_operator('in_crc_errors', '>', 100).get_values('[0]')
[]
sum_value_operator
Filter down based on the value of a certain key and sum up the values and evaluate with {==, !=, >=, <=, >, <} against the total value. Comparing to value_operator, this allows you to sum up the values from structure data and create new value as total. This operator helps you to reduce steps to calculate the values in your python code. For example, below snipped code gathers all ‘in_rate’ from ‘show interfaces’ and you will be able to check how much incoming rate has on the device instead of checking per interface.
# sum up all path which has in_rate and check if the total value is greater than 100
# and then get the total value via get_value()
>>> output = dev.parse('show interfaces')
>>> output.q.sum_value_operator('in_rate', '>', 100).get_values('[0]')
145000.0
count
Count how many element match the requirement.
>>> output = dev.parse('show interfaces')
# Count how many interfaces which has in_crc_error greater than 100
>>> output.q.value_operator('in_crc_errors', '>', 100).count()
0
Does not return a Dq object, considered a “Final” api.
raw
Straight dictionary access.
>>> mod.raw('[slot][rp][1][NX-OSv Supervisor Module][model]')
'N7K-SUP1'
Does not return a Dq object, considered a “Final” api.
reconstruct
Rebuilds a dictionary from a Dq object once filtered down.
>>> output = dev.parse('show interfaces')
# Count how many interfaces which has in_crc_error greater than 100
>>> new_dict = output.q.value_operator('in_crc_errors', '>', 100).reconstruct()
Variable new_dict
is now a dictionary which contains all the interfaces which
have an in_crc_error greater than 100.
Example
Here is an examples on how to use it
Get ntp associated server
# Get testbed file
>>> from genie.testbed import load
>>> tb = load('testbed.yaml')
>>> dev = tb.devices['nx-osv-1']
>>> dev.connect()
>>> parsed_output = dev.parse('show ntp associations')
>>> parsed_output.q.contains('mode').get_values('peer')
['192.168.1.10']
Get all interfaces which have in_crc_errors
>>> new_dict = output.q.value_operator('in_crc_errors', '>', 100).get_values('[0]')
['Ethernet2/1']
It is very easy to verify any keys like this with Dq.
query_validator
Dq accepts query strings (Method str_to_dq_query
) and can be verified with query_validator.
If it is valid it will return True, otherwise False. It is a staticmethod, hence it can be used without instantiate Dq.
Example
>>> from genie.utils import Dq
>>> s = "contains_key_value('a', 'b')"
>>> Dq.query_validator(s)
True
>>> s = "dont_exists('a', 'b')"
>>> Dq.query_validator(s)
False
str_to_dq_query
This function accepts a string and convert it to the proper DQ query, create a Dq object, create the query and call the functions, and returns the output. It is a staticmethod, hence it can be used without instantiate Dq.
Example
# mod is a valid dictionary object
>>> Dq.str_to_dq_query(mod, "contains('rp')")
{'rp': {'1': {'NX-OSv Supervisor Module': {'ports': '0', 'model': 'N7K-SUP1', 'status': 'active', 'software': '7.3(0)D1(1)',
'hardware': '0.0', 'slot/world_wide_name': '--', 'mac_address': '5e-00-40-01-00-00 to 5e-00-40-01-07-ff',
'serial_number': 'TM40010000B'}}}}
# mod is a valid dictionary object
>>> Dq.str_to_dq_query(mod, "contains('lc').not_contains('2').get_values('slot/world_wide_name')")
['--', 1, 2, 3]
Timeout
In any kind of automation, there is a need of polling. Try to do a check/action, verify if expected result is there, otherwise, sleep for a defined time and repeat up to a defined maximum time.
Class Timeout
was made to do this.
from genie.utils.timeout import Timeout
# Try up to 60 seconds, and between interval wait 10 seconds, display timeout logs
timeout = Timeout(max_time = 60, interval = 10, disable_log = False)
while timeout.iterate():
ret = do_something(**kwargs)
if ret is None:
return
# Didn't get expected result, keep trying
timeout.sleep()
When the maximum time is reached, an TimeoutError
is raised. Timeout can be
used to time limit a trigger, to time limit a specific action.
This api is very useful for any kind of polling.
TempResult
TempResult
stores a temporary result. This is useful to keep in memory the
result, without committing it to the testcase yet. TempResult
follows the
pyATS Rollup concept. Once ready, the result can
be applied on the container, either to a section or to a step.
# Starts with no result, let's assume we are within a step container is a
# step, however it also work at a section level.
from genie.utils.timeout import TempResult
with steps.start('the first step') as step:
temp = TempResult(container=step)
# Set result to be passed. If we did temp.result() the step result would be
# passed
temp.passed('Some pass message')
# Set result to be errored. If we did temp.result() the step result would be
# errored
temp.errored('Some error message')
# Errored + passed = Error, so temp result is still errored
temp.passed('Some pass message')
# The step has final result of errored.
temp.result()
Note
container can also be a section, in this case just pass container=self
Diff
Genie comes with a very useful dict
and object
diff tool. Provided two
dictionaries, or object, it will go through every branch and compare all the
keys. At the end, it provides a Linux look-a-like diff.
>>> from genie.utils.diff import Diff
>>> a = {'a':5, 'b':7, 'c':{'ca':8, 'cb':9}}
>>> b = {'a':5, 'f':7, 'c':{'ca':8, 'cb':9}}
>>> dd = Diff(a,b)
>>> dd.findDiff()
>>> print(dd)
+f: 7
-b: 7
It also supports an exclude key, for the keys that shouldn’t be compared.
>>> from genie.utils.diff import Diff
>>> a = {'a':1, 'b':2, 'c':{'ca':9}}
>>> c = {'a':2, 'c':3, 'd':7, 'c':{'ca':{'d':9}}}
>>> dd = Diff(a, c, exclude=['d'])
>>> dd.findDiff()
>>> print(dd)
-b: 2
+a: 2
-a: 1
c:
+ ca:
+ d: 9
- ca: 9
You can also only see which one were added
>>> dd = Diff(a, c, mode='add')
>>> dd.findDiff()
>>> print(dd)
+d: 7
Or Removed
>>> dd = Diff(a, c, mode='remove')
>>> dd.findDiff()
>>> print(dd)
-b: 2
Or modified, which mean it existed, but the value was modified
>>> dd = Diff(a, c, mode='modified')
>>> dd.findDiff()
>>> print(dd)
+a: 2
-a: 1
c:
+ ca:
+ d: 9
- ca: 9
If you need a string representation of added items without diff labeling, you can do
>>> a = {'a': 1, 'w': 5, 'p': {'q': {'a': 6}}}
>>> c = {'b': 2, 'c': {'d': {'e': {'f': 2, 'g': 5}}}}
>>> dd = Diff(a, c)
>>> dd.findDiff()
>>> print(dd.diff_string('+'))
b 2
c
d
e
f 2
g 5
Similarly, you can get a string for the removed items
>>> dd = Diff(a, c)
>>> dd.findDiff()
>>> print(dd.diff_string('-'))
a 1
p
q
a 6
w 5
To print unchanged entries in a list or tuple, you can specify the verbose
option like so
>>> a = { 'key': {'value': [1, 2, 3, 4]}}
>>> b = { 'key': {'value': [1, 3, 3, 4]}}
>>> dd = Diff(a, b, verbose=True)
>>> dd.findDiff()
>>> print(dd)
key:
value:
index[0]: 1
- index[1]: 2
+ index[1]: 3
index[2]: 3
index[3]: 4
Diff is used everywhere within Genie, for the PTS
comparison, ops comparison,
within Triggers. It is another important key component of Genie.
The same can be done with objects. Currently it supports Genie.conf
and
Genie.ops
objects.
Config
Config
object store device show-running
outputs into structure data. This
structure data can then be used to compare show-running over time.
from genie.utils.config import Config
cfg = '''\
service unsupported-transceiver
hostname PE1
clock timezone PDT -7
exception pakmem on
exception sparse off
exception kdebugger enable
logging buffered 120000000
telnet vrf default ipv4 server max-servers 10
cdp
line template vty
timestamp disable
exec-timeout 0 0'''
config = Config(cfg)
config.tree()
>>> pprint.pprint(config.config)
{'cdp': {},
'clock timezone PDT -7': {},
'exception kdebugger enable': {},
'exception pakmem on': {},
'exception sparse off': {},
'hostname PE1': {},
'line template vty': {' exec-timeout 0 0': {}, ' timestamp disable': {}},
'logging buffered 120000000': {},
'service unsupported-transceiver': {},
'telnet vrf default ipv4 server max-servers 10': {}}
This dictionary can then be used with the diff tool to compare two configuration.
Find
The Find
api is the a very important of Genie. It’s such a great tool, that
it was provided to pyATS. Read on it on the pyATS website.