NX-OS Telemetry Resource Module (#59126)

* Combined telemetry module commit

* Minor fixes

* Add back whitespace

* Add telemetry subscription support and simplify

* Remove comment line

* Make ansibot happy

* Create common build_args method

* More ansibot fixes

* Refactored integration tests, remove old files

* Add subscription tests

* Add integration tests

* Update module docs

* Test updates

* Address review comments

* Comment should be one line, not two

* Address Trishna comments

* State deleted should purge all config

* Remove misleading comment

* Doc fixes

* Fix source int bug and remove local debug msg

* Add additional integration test checks
pull/60055/head
Mike Wiebe 5 years ago committed by Trishna Guha
parent 178db5f3ed
commit 9610f2b8ac

@ -6,9 +6,11 @@
""" """
The arg spec for the nxos facts module. The arg spec for the nxos facts module.
""" """
CHOICES = [ CHOICES = [
'all', 'all',
'lag_interfaces', 'lag_interfaces',
'telemetry',
] ]

@ -0,0 +1,90 @@
#
# -*- coding: utf-8 -*-
# Copyright 2019 Cisco and/or its affiliates.
# GNU General Public License v3.0+
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
#############################################
# WARNING #
#############################################
#
# This file is auto generated by the resource
# module builder playbook.
#
# Do not edit this file manually.
#
# Changes to this file will be over written
# by the resource module builder.
#
# Changes should be made in the model used to
# generate this file or in the resource module
# builder template.
#
#############################################
"""
The arg spec for the nxos_telemetry module
"""
from __future__ import absolute_import, division, print_function
__metaclass__ = type
class TelemetryArgs(object): # pylint: disable=R0903
"""The arg spec for the nxos_telemetry module
"""
def __init__(self, **kwargs):
pass
argument_spec = {
'config': {
'options': {
'certificate': {
'options': {
'hostname': {'type': 'str'},
'key': {'type': 'str'}},
'type': 'dict'},
'compression': {'choices': ['gzip'], 'type': 'str'},
'source_interface': {'type': 'str'},
'vrf': {'type': 'str'},
'destination_groups': {
'options': {
'destination': {
'options': {
'encoding': {'choices': ['GPB', 'JSON'],
'type': 'str'},
'ip': {'type': 'str'},
'port': {'type': 'int'},
'protocol': {'choices': ['HTTP', 'TCP', 'UDP', 'gRPC'],
'type': 'str'}},
'type': 'dict'},
'id': {'type': 'int'}},
'type': 'list'},
'sensor_groups': {
'options': {
'data_source': {'choices': ['NX-API', 'DME', 'YANG'],
'type': 'str'},
'id': {'type': 'int'},
'path': {
'options': {
'depth': {'type': 'str'},
'filter_condition': {'type': 'str'},
'name': {'type': 'str'},
'query_condition': {'type': 'str'}},
'type': 'dict'}},
'type': 'list'},
'subscriptions': {
'options': {
'destination_group': {'type': 'int'},
'id': {'type': 'int'},
'sensor_group': {
'options': {
'id': {'type': 'int'},
'sample_interval': {'type': 'int'}},
'type': 'dict'}},
'type': 'list'}},
'type': 'dict'},
'state': {
'choices': ['merged', 'replaced', 'deleted'],
'default': 'merged',
'type': 'str'}} # pylint: disable=C0301

@ -0,0 +1,145 @@
#
# -*- coding: utf-8 -*-
# Copyright 2019 Cisco and/or its affiliates.
# GNU General Public License v3.0+
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
#
# Telemetry Command Reference File
from __future__ import absolute_import, division, print_function
__metaclass__ = type
TMS_GLOBAL = '''
# The cmd_ref is a yaml formatted list of module commands.
# A leading underscore denotes a non-command variable; e.g. _template.
# TMS does not have convenient global json data so this cmd_ref uses raw cli configs.
---
_template: # _template holds common settings for all commands
# Enable feature telemetry if disabled
feature: telemetry
# Common get syntax for TMS commands
get_command: show run telemetry all
# Parent configuration for TMS commands
context:
- telemetry
certificate:
_exclude: ['N3K', 'N5K', 'N6k', 'N7k']
kind: dict
getval: certificate (?P<key>\\S+) (?P<hostname>\\S+)$
setval: certificate {key} {hostname}
default:
key: ~
hostname: ~
compression:
_exclude: ['N3K', 'N5K', 'N6k', 'N7k']
kind: str
getval: use-compression (\\S+)$
setval: 'use-compression {0}'
default: ~
context: &dpcontext
- telemetry
- destination-profile
source_interface:
_exclude: ['N3K', 'N5K', 'N6k', 'N7k']
kind: str
getval: source-interface (\\S+)$
setval: 'source-interface {0}'
default: ~
context: *dpcontext
vrf:
_exclude: ['N3K', 'N5K', 'N6k', 'N7k']
kind: str
getval: use-vrf (\\S+)$
setval: 'use-vrf {0}'
default: ~
context: *dpcontext
'''
TMS_DESTGROUP = '''
# The cmd_ref is a yaml formatted list of module commands.
# A leading underscore denotes a non-command variable; e.g. _template.
# TBD: Use Structured Where Possible
---
_template: # _template holds common settings for all commands
# Enable feature telemetry if disabled
feature: telemetry
# Common get syntax for TMS commands
get_command: show run telemetry all
# Parent configuration for TMS commands
context:
- telemetry
destination:
_exclude: ['N3K', 'N5K', 'N6k', 'N7k']
multiple: true
kind: dict
getval: ip address (?P<ip>\\S+) port (?P<port>\\S+) protocol (?P<protocol>\\S+) encoding (?P<encoding>\\S+)
setval: ip address {ip} port {port} protocol {protocol} encoding {encoding}
default:
ip: ~
port: ~
protocol: ~
encoding: ~
'''
TMS_SENSORGROUP = '''
# The cmd_ref is a yaml formatted list of module commands.
# A leading underscore denotes a non-command variable; e.g. _template.
# TBD: Use Structured Where Possible
---
_template: # _template holds common settings for all commands
# Enable feature telemetry if disabled
feature: telemetry
# Common get syntax for TMS commands
get_command: show run telemetry all
# Parent configuration for TMS commands
context:
- telemetry
data_source:
_exclude: ['N3K', 'N5K', 'N6k', 'N7k']
kind: str
getval: data-source (\\S+)$
setval: 'data-source {0}'
default: ~
path:
_exclude: ['N3K', 'N5K', 'N6k', 'N7k']
multiple: true
kind: dict
getval: path (?P<name>\\S+)( depth (?P<depth>\\S+))?( query-condition (?P<query_condition>\\S+))?( filter-condition (?P<filter_condition>\\S+))?$
setval: path {name} depth {depth} query-condition {query_condition} filter-condition {filter_condition}
default:
name: ~
depth: ~
query_condition: ~
filter_condition: ~
'''
TMS_SUBSCRIPTION = '''
# The cmd_ref is a yaml formatted list of module commands.
# A leading underscore denotes a non-command variable; e.g. _template.
# TBD: Use Structured Where Possible
---
_template: # _template holds common settings for all commands
# Enable feature telemetry if disabled
feature: telemetry
# Common get syntax for TMS commands
get_command: show run telemetry all
# Parent configuration for TMS commands
context:
- telemetry
destination_group:
_exclude: ['N3K', 'N5K', 'N6k', 'N7k']
multiple: true
kind: int
getval: dst-grp (\\S+)$
setval: 'dst-grp {0}'
default: ~
sensor_group:
_exclude: ['N3K', 'N5K', 'N6k', 'N7k']
multiple: true
kind: dict
getval: snsr-grp (?P<id>\\S+) sample-interval (?P<sample_interval>\\S+)$
setval: snsr-grp {id} sample-interval {sample_interval}
default:
id: ~
sample_interval: ~
'''

@ -0,0 +1,286 @@
#
# -*- coding: utf-8 -*-
# Copyright 2019 Cisco and/or its affiliates.
# GNU General Public License v3.0+
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
"""
The nxos_telemetry class
It is in this file where the current configuration (as dict)
is compared to the provided configuration (as dict) and the command set
necessary to bring the current configuration to it's desired end-state is
created
"""
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import re
from copy import deepcopy
from ansible.module_utils.network.common.cfg.base import ConfigBase
from ansible.module_utils.network.common.utils import to_list
from ansible.module_utils.network.nxos.facts.facts import Facts
from ansible.module_utils.network.nxos.cmdref.telemetry.telemetry import TMS_GLOBAL, TMS_DESTGROUP, TMS_SENSORGROUP, TMS_SUBSCRIPTION
from ansible.module_utils.network.nxos.utils.telemetry.telemetry import normalize_data, remove_duplicate_context
from ansible.module_utils.network.nxos.utils.telemetry.telemetry import valiate_input, get_setval_path
from ansible.module_utils.network.nxos.utils.telemetry.telemetry import get_module_params_subsection
from ansible.module_utils.network.nxos.utils.utils import normalize_interface
from ansible.module_utils.network.nxos.nxos import NxosCmdRef
class Telemetry(ConfigBase):
"""
The nxos_telemetry class
"""
gather_subset = [
'!all',
'!min',
]
gather_network_resources = [
'telemetry',
]
def __init__(self, module):
super(Telemetry, self).__init__(module)
def get_telemetry_facts(self):
""" Get the 'facts' (the current configuration)
:rtype: A dictionary
:returns: The current configuration as a dictionary
"""
facts, _warnings = Facts(self._module).get_facts(self.gather_subset, self.gather_network_resources)
telemetry_facts = facts['ansible_network_resources'].get('telemetry')
if not telemetry_facts:
return {}
return telemetry_facts
def edit_config(self, commands):
return self._connection.edit_config(commands)
def execute_module(self):
""" Execute the module
:rtype: A dictionary
:returns: The result from module execution
"""
result = {'changed': False}
commands = list()
warnings = list()
state = self._module.params['state']
if 'overridden' in state:
self._module.fail_json(msg='State <overridden> is invalid for this module.')
if 'replaced' in state:
self._module.fail_json(msg='State: <replaced> not yet supported')
# When state is 'deleted', the module_params should not contain data
# under the 'config' key
if 'deleted' in state and self._module.params.get('config'):
self._module.fail_json(msg='Remove config key from playbook when state is <deleted>')
if self._module.params['config'] is None:
self._module.params['config'] = {}
# Normalize interface name.
int = self._module.params['config'].get('source_interface')
if int:
self._module.params['config']['source_interface'] = normalize_interface(int)
existing_telemetry_facts = self.get_telemetry_facts()
commands.extend(self.set_config(existing_telemetry_facts))
if commands:
if not self._module.check_mode:
self.edit_config(commands)
# TODO: edit_config is only available for network_cli. Once we
# add support for httpapi, we will need to switch to load_config
# or add support to httpapi for edit_config
#
# self._connection.load_config(commands)
result['changed'] = True
result['commands'] = commands
changed_telemetry_facts = self.get_telemetry_facts()
result['before'] = existing_telemetry_facts
if result['changed']:
result['after'] = changed_telemetry_facts
result['warnings'] = warnings
return result
def set_config(self, existing_tms_global_facts):
""" Collect the configuration from the args passed to the module,
collect the current configuration (as a dict from facts)
:rtype: A list
:returns: the commands necessary to migrate the current configuration
to the desired configuration
"""
config = self._module.params['config']
want = dict((k, v) for k, v in config.items() if v is not None)
have = existing_tms_global_facts
resp = self.set_state(want, have)
return to_list(resp)
def set_state(self, want, have):
""" Select the appropriate function based on the state provided
:param want: the desired configuration as a dictionary
:param have: the current configuration as a dictionary
:rtype: A list
:returns: the commands necessary to migrate the current configuration
to the desired configuration
"""
state = self._module.params['state']
# The deleted case is very simple since we purge all telemetry config
# and does not require any processing using NxosCmdRef objects.
if state == 'deleted':
return self._state_deleted(want, have)
# Save off module params
ALL_MP = self._module.params['config']
cmd_ref = {}
cmd_ref['TMS_GLOBAL'] = {}
cmd_ref['TMS_DESTGROUP'] = {}
cmd_ref['TMS_SENSORGROUP'] = {}
cmd_ref['TMS_SUBSCRIPTION'] = {}
# Get Telemetry Global Data
cmd_ref['TMS_GLOBAL']['ref'] = []
self._module.params['config'] = get_module_params_subsection(ALL_MP, 'TMS_GLOBAL')
cmd_ref['TMS_GLOBAL']['ref'].append(NxosCmdRef(self._module, TMS_GLOBAL))
ref = cmd_ref['TMS_GLOBAL']['ref'][0]
ref.set_context()
ref.get_existing()
ref.get_playvals()
device_cache = ref.cache_existing
if device_cache is None:
device_cache_lines = []
else:
device_cache_lines = device_cache.split("\n")
# Get Telemetry Destination Group Data
if want.get('destination_groups'):
td = {'name': 'destination_groups', 'type': 'TMS_DESTGROUP',
'obj': TMS_DESTGROUP, 'cmd': 'destination-group {0}'}
cmd_ref[td['type']]['ref'] = []
saved_ids = []
for playvals in want[td['name']]:
valiate_input(playvals, td['name'], self._module)
if playvals['id'] in saved_ids:
continue
saved_ids.append(playvals['id'])
resource_key = td['cmd'].format(playvals['id'])
# Only build the NxosCmdRef object for the destination group module parameters.
self._module.params['config'] = get_module_params_subsection(ALL_MP, td['type'], playvals['id'])
cmd_ref[td['type']]['ref'].append(NxosCmdRef(self._module, td['obj']))
ref = cmd_ref[td['type']]['ref'][-1]
ref.set_context([resource_key])
ref.get_existing(device_cache)
ref.get_playvals()
normalize_data(ref)
# Get Telemetry Sensor Group Data
if want.get('sensor_groups'):
td = {'name': 'sensor_groups', 'type': 'TMS_SENSORGROUP',
'obj': TMS_SENSORGROUP, 'cmd': 'sensor-group {0}'}
cmd_ref[td['type']]['ref'] = []
saved_ids = []
for playvals in want[td['name']]:
valiate_input(playvals, td['name'], self._module)
if playvals['id'] in saved_ids:
continue
saved_ids.append(playvals['id'])
resource_key = td['cmd'].format(playvals['id'])
# Only build the NxosCmdRef object for the sensor group module parameters.
self._module.params['config'] = get_module_params_subsection(ALL_MP, td['type'], playvals['id'])
cmd_ref[td['type']]['ref'].append(NxosCmdRef(self._module, td['obj']))
ref = cmd_ref[td['type']]['ref'][-1]
ref.set_context([resource_key])
if get_setval_path(self._module):
# Sensor group path setting can contain optional values.
# Call get_setval_path helper function to process any
# optional setval keys.
ref._ref['path']['setval'] = get_setval_path(self._module)
ref.get_existing(device_cache)
ref.get_playvals()
# Get Telemetry Subscription Data
if want.get('subscriptions'):
td = {'name': 'subscriptions', 'type': 'TMS_SUBSCRIPTION',
'obj': TMS_SUBSCRIPTION, 'cmd': 'subscription {0}'}
cmd_ref[td['type']]['ref'] = []
saved_ids = []
for playvals in want[td['name']]:
valiate_input(playvals, td['name'], self._module)
if playvals['id'] in saved_ids:
continue
saved_ids.append(playvals['id'])
resource_key = td['cmd'].format(playvals['id'])
# Only build the NxosCmdRef object for the subscription module parameters.
self._module.params['config'] = get_module_params_subsection(ALL_MP, td['type'], playvals['id'])
cmd_ref[td['type']]['ref'].append(NxosCmdRef(self._module, td['obj']))
ref = cmd_ref[td['type']]['ref'][-1]
ref.set_context([resource_key])
ref.get_existing(device_cache)
ref.get_playvals()
if state == 'overridden':
if want == have:
return []
commands = self._state_overridden(cmd_ref, want, have)
elif state == 'merged':
if want == have:
return []
commands = self._state_merged(cmd_ref)
elif state == 'replaced':
if want == have:
return []
commands = self._state_replaced(cmd_ref)
return commands
@staticmethod
def _state_replaced(cmd_ref):
""" The command generator when state is replaced
:rtype: A list
:returns: the commands necessary to migrate the current configuration
to the desired configuration
"""
commands = []
return commands
@staticmethod
def _state_merged(cmd_ref):
""" The command generator when state is merged
:rtype: A list
:returns: the commands necessary to merge the provided into
the current configuration
"""
commands = cmd_ref['TMS_GLOBAL']['ref'][0].get_proposed()
if cmd_ref['TMS_DESTGROUP'].get('ref'):
for cr in cmd_ref['TMS_DESTGROUP']['ref']:
commands.extend(cr.get_proposed())
if cmd_ref['TMS_SENSORGROUP'].get('ref'):
for cr in cmd_ref['TMS_SENSORGROUP']['ref']:
commands.extend(cr.get_proposed())
if cmd_ref['TMS_SUBSCRIPTION'].get('ref'):
for cr in cmd_ref['TMS_SUBSCRIPTION']['ref']:
commands.extend(cr.get_proposed())
return remove_duplicate_context(commands)
@staticmethod
def _state_deleted(want, have):
""" The command generator when state is deleted
:rtype: A list
:returns: the commands necessary to remove the current configuration
of the provided objects
"""
commands = []
if want != have:
commands = ['no telemetry']
return commands

@ -13,6 +13,7 @@ from ansible.module_utils.network.nxos.argspec.facts.facts import FactsArgs
from ansible.module_utils.network.common.facts.facts import FactsBase from ansible.module_utils.network.common.facts.facts import FactsBase
from ansible.module_utils.network.nxos.facts.legacy.base import Default, Legacy, Hardware, Config, Interfaces, Features from ansible.module_utils.network.nxos.facts.legacy.base import Default, Legacy, Hardware, Config, Interfaces, Features
from ansible.module_utils.network.nxos.facts.lag_interfaces.lag_interfaces import Lag_interfacesFacts from ansible.module_utils.network.nxos.facts.lag_interfaces.lag_interfaces import Lag_interfacesFacts
from ansible.module_utils.network.nxos.facts.telemetry.telemetry import TelemetryFacts
FACT_LEGACY_SUBSETS = dict( FACT_LEGACY_SUBSETS = dict(
@ -25,6 +26,7 @@ FACT_LEGACY_SUBSETS = dict(
) )
FACT_RESOURCE_SUBSETS = dict( FACT_RESOURCE_SUBSETS = dict(
lag_interfaces=Lag_interfacesFacts, lag_interfaces=Lag_interfacesFacts,
telemetry=TelemetryFacts,
) )

@ -0,0 +1,163 @@
#
# -*- coding: utf-8 -*-
# Copyright 2019 Cisco and/or its affiliates.
# GNU General Public License v3.0+
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
"""
The nxos telemetry fact class
It is in this file the configuration is collected from the device
for a given resource, parsed, and the facts tree is populated
based on the configuration.
"""
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import re
from copy import deepcopy
from ansible.module_utils.network.common import utils
from ansible.module_utils.network.nxos.argspec.telemetry.telemetry import TelemetryArgs
from ansible.module_utils.network.nxos.cmdref.telemetry.telemetry import TMS_GLOBAL, TMS_DESTGROUP, TMS_SENSORGROUP, TMS_SUBSCRIPTION
from ansible.module_utils.network.nxos.utils.telemetry.telemetry import get_instance_data, cr_key_lookup
from ansible.module_utils.network.nxos.utils.telemetry.telemetry import normalize_data
from ansible.module_utils.network.nxos.nxos import NxosCmdRef, normalize_interface
class TelemetryFacts(object):
""" The nxos telemetry fact class
"""
def __init__(self, module, subspec='config', options='options'):
self._module = module
self.argument_spec = TelemetryArgs.argument_spec
spec = deepcopy(self.argument_spec)
if subspec:
if options:
facts_argument_spec = spec[subspec][options]
else:
facts_argument_spec = spec[subspec]
else:
facts_argument_spec = spec
self.generated_spec = utils.generate_dict(facts_argument_spec)
def populate_facts(self, connection, ansible_facts, data=None):
""" Populate the facts for telemetry
:param connection: the device connection
:param ansible_facts: Facts dictionary
:param data: previously collected conf
:rtype: dictionary
:returns: facts
"""
if connection: # just for linting purposes, remove
pass
cmd_ref = {}
cmd_ref['TMS_GLOBAL'] = {}
cmd_ref['TMS_DESTGROUP'] = {}
cmd_ref['TMS_SENSORGROUP'] = {}
cmd_ref['TMS_SUBSCRIPTION'] = {}
# For fact gathering, module state should be 'present' when using
# NxosCmdRef to query state
if self._module.params.get('state'):
saved_module_state = self._module.params['state']
self._module.params['state'] = 'present'
# Get Telemetry Global Data
cmd_ref['TMS_GLOBAL']['ref'] = []
cmd_ref['TMS_GLOBAL']['ref'].append(NxosCmdRef(self._module, TMS_GLOBAL))
ref = cmd_ref['TMS_GLOBAL']['ref'][0]
ref.set_context()
ref.get_existing()
device_cache = ref.cache_existing
if device_cache is None:
device_cache_lines = []
else:
device_cache_lines = device_cache.split("\n")
# Get Telemetry Destination Group Data
cmd_ref['TMS_DESTGROUP']['ref'] = []
for line in device_cache_lines:
if re.search(r'destination-group', line):
resource_key = line.strip()
cmd_ref['TMS_DESTGROUP']['ref'].append(NxosCmdRef(self._module, TMS_DESTGROUP))
ref = cmd_ref['TMS_DESTGROUP']['ref'][-1]
ref.set_context([resource_key])
ref.get_existing(device_cache)
normalize_data(ref)
# Get Telemetry Sensorgroup Group Data
cmd_ref['TMS_SENSORGROUP']['ref'] = []
for line in device_cache_lines:
if re.search(r'sensor-group', line):
resource_key = line.strip()
cmd_ref['TMS_SENSORGROUP']['ref'].append(NxosCmdRef(self._module, TMS_SENSORGROUP))
ref = cmd_ref['TMS_SENSORGROUP']['ref'][-1]
ref.set_context([resource_key])
ref.get_existing(device_cache)
# Get Telemetry Subscription Data
cmd_ref['TMS_SUBSCRIPTION']['ref'] = []
for line in device_cache_lines:
if re.search(r'subscription', line):
resource_key = line.strip()
cmd_ref['TMS_SUBSCRIPTION']['ref'].append(NxosCmdRef(self._module, TMS_SUBSCRIPTION))
ref = cmd_ref['TMS_SUBSCRIPTION']['ref'][-1]
ref.set_context([resource_key])
ref.get_existing(device_cache)
objs = []
objs = self.render_config(self.generated_spec, cmd_ref)
facts = {'telemetry': {}}
if objs:
# params = utils.validate_config(self.argument_spec, {'config': objs})
facts['telemetry'] = objs
ansible_facts['ansible_network_resources'].update(facts)
if self._module.params.get('state'):
self._module.params['state'] = saved_module_state
return ansible_facts
def render_config(self, spec, cmd_ref):
"""
Render config as dictionary structure and delete keys
from spec for null values
:param spec: The facts tree, generated from the argspec
:param conf: The configuration
:rtype: dictionary
:returns: The generated config
"""
config = deepcopy(spec)
config['destination_groups'] = []
config['sensor_groups'] = []
config['subscriptions'] = []
managed_objects = ['TMS_GLOBAL', 'TMS_DESTGROUP', 'TMS_SENSORGROUP', 'TMS_SUBSCRIPTION']
# Walk the argspec and cmd_ref objects and build out config dict.
for key in config.keys():
for mo in managed_objects:
for cr in cmd_ref[mo]['ref']:
cr_keys = cr_key_lookup(key, mo)
for cr_key in cr_keys:
if cr._ref.get(cr_key) and cr._ref[cr_key].get('existing'):
if isinstance(config[key], dict):
for k in config[key].keys():
for existing_key in cr._ref[cr_key]['existing'].keys():
config[key][k] = cr._ref[cr_key]['existing'][existing_key][k]
continue
if isinstance(config[key], list):
for existing_key in cr._ref[cr_key]['existing'].keys():
data = get_instance_data(key, cr_key, cr, existing_key)
config[key].append(data)
continue
for existing_key in cr._ref[cr_key]['existing'].keys():
config[key] = cr._ref[cr_key]['existing'][existing_key]
elif cr._ref.get(cr_key):
data = get_instance_data(key, cr_key, cr, None)
if isinstance(config[key], list) and data not in config[key]:
config[key].append(data)
return utils.remove_empties(config)

@ -32,6 +32,7 @@ import collections
import json import json
import re import re
import sys import sys
from copy import deepcopy
from ansible.module_utils._text import to_text from ansible.module_utils._text import to_text
from ansible.module_utils.basic import env_fallback from ansible.module_utils.basic import env_fallback
@ -39,6 +40,7 @@ from ansible.module_utils.network.common.utils import to_list, ComplexList
from ansible.module_utils.connection import Connection, ConnectionError from ansible.module_utils.connection import Connection, ConnectionError
from ansible.module_utils.common._collections_compat import Mapping from ansible.module_utils.common._collections_compat import Mapping
from ansible.module_utils.network.common.config import NetworkConfig, dumps from ansible.module_utils.network.common.config import NetworkConfig, dumps
from ansible.module_utils.network.common.config import CustomNetworkConfig
from ansible.module_utils.six import iteritems, string_types, PY2, PY3 from ansible.module_utils.six import iteritems, string_types, PY2, PY3
from ansible.module_utils.urls import fetch_url from ansible.module_utils.urls import fetch_url
@ -742,6 +744,9 @@ class NxosCmdRef:
self._module = module self._module = module
self._check_imports() self._check_imports()
self._yaml_load(cmd_ref_str) self._yaml_load(cmd_ref_str)
self.cache_existing = None
self.present_states = ['present', 'merged']
self.absent_states = ['absent', 'deleted']
ref = self._ref ref = self._ref
# Create a list of supported commands based on ref keys # Create a list of supported commands based on ref keys
@ -932,31 +937,37 @@ class NxosCmdRef:
# Last key in context is the resource key # Last key in context is the resource key
ref['_resource_key'] = context[-1] if context else ref['_resource_key'] ref['_resource_key'] = context[-1] if context else ref['_resource_key']
def get_existing(self): def get_existing(self, cache_output=None):
"""Update ref with existing command states from the device. """Update ref with existing command states from the device.
Store these states in each command's 'existing' key. Store these states in each command's 'existing' key.
""" """
ref = self._ref ref = self._ref
if ref.get('_cli_is_feature_disabled'): if ref.get('_cli_is_feature_disabled'):
# Add context to proposed if state is present # Add context to proposed if state is present
if 'present' in ref['_state']: if ref['_state'] in self.present_states:
[ref['_proposed'].append(ctx) for ctx in ref['_context']] [ref['_proposed'].append(ctx) for ctx in ref['_context']]
return return
show_cmd = ref['_template']['get_command'] show_cmd = ref['_template']['get_command']
if cache_output:
output = cache_output
else:
output = self.execute_show_command(show_cmd, 'text') or []
self.cache_existing = output
# Add additional command context if needed. # Add additional command context if needed.
for filter in ref['_context']: if ref['_context']:
show_cmd = show_cmd + " | section '{0}'".format(filter) output = CustomNetworkConfig(indent=2, contents=output)
output = output.get_section(ref['_context'])
output = self.execute_show_command(show_cmd, 'text') or []
if not output: if not output:
# Add context to proposed if state is present # Add context to proposed if state is present
if 'present' in ref['_state']: if ref['_state'] in self.present_states:
[ref['_proposed'].append(ctx) for ctx in ref['_context']] [ref['_proposed'].append(ctx) for ctx in ref['_context']]
return return
# We need to remove the last item in context for state absent case. # We need to remove the last item in context for state absent case.
if 'absent' in ref['_state'] and ref['_context']: if ref['_state'] in self.absent_states and ref['_context']:
if ref['_resource_key'] and ref['_resource_key'] == ref['_context'][-1]: if ref['_resource_key'] and ref['_resource_key'] == ref['_context'][-1]:
if ref['_context'][-1] in output: if ref['_context'][-1] in output:
ref['_context'][-1] = 'no ' + ref['_context'][-1] ref['_context'][-1] = 'no ' + ref['_context'][-1]
@ -997,18 +1008,35 @@ class NxosCmdRef:
""" """
ref = self._ref ref = self._ref
module = self._module module = self._module
params = {}
if module.params.get('config'):
# Resource module builder packs playvals under 'config' key
param_data = module.params.get('config')
params['global'] = param_data
for key in param_data.keys():
if isinstance(param_data[key], list):
params[key] = param_data[key]
else:
params['global'] = module.params
for k in ref.keys(): for k in ref.keys():
if k in module.params and module.params[k] is not None: for level in params.keys():
playval = module.params[k] if isinstance(params[level], dict):
# Normalize each value params[level] = [params[level]]
if 'int' == ref[k]['kind']: for item in params[level]:
playval = int(playval) if k in item and item[k] is not None:
elif 'list' == ref[k]['kind']: if not ref[k].get('playval'):
playval = [str(i) for i in playval] ref[k]['playval'] = {}
elif 'dict' == ref[k]['kind']: playval = item[k]
for key, v in playval.items(): index = params[level].index(item)
playval[key] = str(v) # Normalize each value
ref[k]['playval'] = playval if 'int' == ref[k]['kind']:
playval = int(playval)
elif 'list' == ref[k]['kind']:
playval = [str(i) for i in playval]
elif 'dict' == ref[k]['kind']:
for key, v in playval.items():
playval[key] = str(v)
ref[k]['playval'][index] = playval
def build_cmd_set(self, playval, existing, k): def build_cmd_set(self, playval, existing, k):
"""Helper function to create list of commands to configure device """Helper function to create list of commands to configure device
@ -1037,7 +1065,7 @@ class NxosCmdRef:
else: else:
raise ValueError("get_proposed: unknown 'kind' value specified for key '{0}'".format(k)) raise ValueError("get_proposed: unknown 'kind' value specified for key '{0}'".format(k))
if cmd: if cmd:
if 'absent' == ref['_state'] and not re.search(r'^no', cmd): if ref['_state'] in self.absent_states and not re.search(r'^no', cmd):
cmd = 'no ' + cmd cmd = 'no ' + cmd
# Commands may require parent commands for proper context. # Commands may require parent commands for proper context.
# Global _template context is replaced by parameter context # Global _template context is replaced by parameter context
@ -1062,7 +1090,7 @@ class NxosCmdRef:
play_keys = [k for k in ref['commands'] if 'playval' in ref[k]] play_keys = [k for k in ref['commands'] if 'playval' in ref[k]]
def compare(playval, existing): def compare(playval, existing):
if 'present' in ref['_state']: if ref['_state'] in self.present_states:
if existing is None: if existing is None:
return False return False
elif playval == existing: elif playval == existing:
@ -1070,7 +1098,7 @@ class NxosCmdRef:
elif isinstance(existing, dict) and playval in existing.values(): elif isinstance(existing, dict) and playval in existing.values():
return True return True
if 'absent' in ref['_state']: if ref['_state'] in self.absent_states:
if isinstance(existing, dict) and all(x is None for x in existing.values()): if isinstance(existing, dict) and all(x is None for x in existing.values()):
existing = None existing = None
if existing is None or playval not in existing.values(): if existing is None or playval not in existing.values():
@ -1080,32 +1108,45 @@ class NxosCmdRef:
# Compare against current state # Compare against current state
for k in play_keys: for k in play_keys:
playval = ref[k]['playval'] playval = ref[k]['playval']
# Create playval copy to avoid RuntimeError
# dictionary changed size during iteration error
playval_copy = deepcopy(playval)
existing = ref[k].get('existing', ref[k]['default']) existing = ref[k].get('existing', ref[k]['default'])
multiple = 'multiple' in ref[k].keys() multiple = 'multiple' in ref[k].keys()
# Multiple Instances: # Multiple Instances:
if isinstance(existing, dict) and multiple: if isinstance(existing, dict) and multiple:
item_found = False item_found = False
for dkey, dvalue in existing.items():
if isinstance(dvalue, dict): for ekey, evalue in existing.items():
if isinstance(evalue, dict):
# Remove values set to string 'None' from dvalue # Remove values set to string 'None' from dvalue
dvalue = dict((k, v) for k, v in dvalue.items() if v != 'None') evalue = dict((k, v) for k, v in evalue.items() if v != 'None')
if compare(playval, dvalue): for pkey, pvalue in playval.items():
item_found = True if compare(pvalue, evalue):
if item_found: if playval_copy.get(pkey):
del playval_copy[pkey]
if not playval_copy:
continue continue
# Single Instance: # Single Instance:
else: else:
if compare(playval, existing): for pkey, pval in playval.items():
if compare(pval, existing):
if playval_copy.get(pkey):
del playval_copy[pkey]
if not playval_copy:
continue continue
playval = playval_copy
# Multiple Instances: # Multiple Instances:
if isinstance(existing, dict): if isinstance(existing, dict):
for dkey, dvalue in existing.items(): for dkey, dvalue in existing.items():
self.build_cmd_set(playval, dvalue, k) for pval in playval.values():
self.build_cmd_set(pval, dvalue, k)
# Single Instance: # Single Instance:
else: else:
self.build_cmd_set(playval, existing, k) for pval in playval.values():
self.build_cmd_set(pval, existing, k)
# Remove any duplicate commands before returning. # Remove any duplicate commands before returning.
# pylint: disable=unnecessary-lambda # pylint: disable=unnecessary-lambda

@ -0,0 +1,181 @@
#
# -*- coding: utf-8 -*-
# Copyright 2019 Cisco and/or its affiliates.
# GNU General Public License v3.0+
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
"""
The nxos telemetry utility library
"""
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import re
def get_module_params_subsection(module_params, tms_config, resource_key=None):
"""
Helper method to get a specific module_params subsection
"""
mp = {}
if tms_config == 'TMS_GLOBAL':
relevant_keys = ['certificate',
'compression',
'source_interface',
'vrf']
for key in relevant_keys:
mp[key] = module_params[key]
if tms_config == 'TMS_DESTGROUP':
mp['destination_groups'] = []
for destgrp in module_params['destination_groups']:
if destgrp['id'] == resource_key:
mp['destination_groups'].append(destgrp)
if tms_config == 'TMS_SENSORGROUP':
mp['sensor_groups'] = []
for sensor in module_params['sensor_groups']:
if sensor['id'] == resource_key:
mp['sensor_groups'].append(sensor)
if tms_config == 'TMS_SUBSCRIPTION':
mp['subscriptions'] = []
for sensor in module_params['subscriptions']:
if sensor['id'] == resource_key:
mp['subscriptions'].append(sensor)
return mp
def valiate_input(playvals, type, module):
"""
Helper method to validate playbook values for destination groups
"""
if type == 'destination_groups':
if not playvals.get('id'):
msg = "Invalid playbook value: {0}.".format(playvals)
msg += " Parameter <id> under <destination_groups> is required"
module.fail_json(msg=msg)
if playvals.get('destination') and not isinstance(playvals['destination'], dict):
msg = "Invalid playbook value: {0}.".format(playvals)
msg += " Parameter <destination> under <destination_groups> must be a dict"
module.fail_json(msg=msg)
if not playvals.get('destination') and len(playvals) > 1:
msg = "Invalid playbook value: {0}.".format(playvals)
msg += " Playbook entry contains unrecongnized parameters."
msg += " Make sure <destination> keys under <destination_groups> are specified as follows:"
msg += " destination: {ip: <ip>, port: <port>, protocol: <prot>, encoding: <enc>}}"
module.fail_json(msg=msg)
if type == 'sensor_groups':
if not playvals.get('id'):
msg = "Invalid playbook value: {0}.".format(playvals)
msg += " Parameter <id> under <sensor_groups> is required"
module.fail_json(msg=msg)
if playvals.get('path') and 'name' not in playvals['path'].keys():
msg = "Invalid playbook value: {0}.".format(playvals)
msg += " Parameter <path> under <sensor_groups> requires <name> key"
module.fail_json(msg=msg)
def get_instance_data(key, cr_key, cr, existing_key):
"""
Helper method to get instance data used to populate list structure in config
fact dictionary
"""
data = {}
if existing_key is None:
instance = None
else:
instance = cr._ref[cr_key]['existing'][existing_key]
patterns = {
'destination_groups': r"destination-group (\d+)",
'sensor_groups': r"sensor-group (\d+)",
'subscriptions': r"subscription (\d+)",
}
if key in patterns.keys():
m = re.search(patterns[key], cr._ref['_resource_key'])
instance_key = m.group(1)
data = {'id': instance_key, cr_key: instance}
# Remove None values
data = dict((k, v) for k, v in data.items() if v is not None)
return data
def cr_key_lookup(key, mo):
"""
Helper method to get instance key value for Managed Object (mo)
"""
cr_keys = [key]
if key == 'destination_groups' and mo == 'TMS_DESTGROUP':
cr_keys = ['destination']
elif key == 'sensor_groups' and mo == 'TMS_SENSORGROUP':
cr_keys = ['data_source', 'path']
elif key == 'subscriptions' and mo == 'TMS_SUBSCRIPTION':
cr_keys = ['destination_group', 'sensor_group']
return cr_keys
def normalize_data(cmd_ref):
''' Normalize playbook values and get_exisiting data '''
playval = cmd_ref._ref.get('destination').get('playval')
existing = cmd_ref._ref.get('destination').get('existing')
dest_props = ['protocol', 'encoding']
if playval:
for prop in dest_props:
for key in playval.keys():
playval[key][prop] = playval[key][prop].lower()
if existing:
for key in existing.keys():
for prop in dest_props:
existing[key][prop] = existing[key][prop].lower()
def remove_duplicate_context(cmds):
''' Helper method to remove duplicate telemetry context commands '''
if not cmds:
return cmds
feature_indices = [i for i, x in enumerate(cmds) if x == "feature telemetry"]
telemetry_indices = [i for i, x in enumerate(cmds) if x == "telemetry"]
if len(feature_indices) == 1 and len(telemetry_indices) == 1:
return cmds
if len(feature_indices) == 1 and not telemetry_indices:
return cmds
if len(telemetry_indices) == 1 and not feature_indices:
return cmds
if feature_indices and feature_indices[-1] > 1:
cmds.pop(feature_indices[-1])
return remove_duplicate_context(cmds)
if telemetry_indices and telemetry_indices[-1] > 1:
cmds.pop(telemetry_indices[-1])
return remove_duplicate_context(cmds)
def get_setval_path(module):
''' Build setval for path parameter based on playbook inputs
Full Command:
- path {name} depth {depth} query-condition {query_condition} filter-condition {filter_condition}
Required:
- path {name}
Optional:
- depth {depth}
- query-condition {query_condition},
- filter-condition {filter_condition}
'''
path = module.params['config']['sensor_groups'][0].get('path')
if path is None:
return path
setval = 'path {name}'
if 'depth' in path.keys():
setval = setval + ' depth {depth}'
if 'query_condition' in path.keys():
setval = setval + ' query-condition {query_condition}'
if 'filter_condition' in path.keys():
setval = setval + ' filter-condition {filter_condition}'
return setval

@ -57,7 +57,7 @@ options:
to a given subset. Possible values for this argument include to a given subset. Possible values for this argument include
all and the resources like interfaces, vlans etc. all and the resources like interfaces, vlans etc.
Can specify a list of values to include a larger subset. Can specify a list of values to include a larger subset.
choices: ['all', 'lag_interfaces'] choices: ['all', 'lag_interfaces', 'telemetry']
required: false required: false
version_added: "2.9" version_added: "2.9"
""" """

@ -0,0 +1,333 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright 2019 Cisco and/or its affiliates.
# GNU General Public License v3.0+
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
#############################################
# WARNING #
#############################################
#
# This file is auto generated by the resource
# module builder playbook.
#
# Do not edit this file manually.
#
# Changes to this file will be over written
# by the resource module builder.
#
# Changes should be made in the model used to
# generate this file or in the resource module
# builder template.
#
#############################################
"""
The module file for nxos_telemetry
"""
from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'network'
}
DOCUMENTATION = """
---
module: nxos_telemetry
version_added: 2.9
short_description: 'Telemetry Monitoring Service (TMS) configuration'
description: 'Manages Telemetry Monitoring Service (TMS) configuration'
author: Mike Wiebe (@mikewiebe)
notes:
- 'Supported on N9k Version 7.0(3)I7(5) and later.'
options:
config:
description: The provided configuration
type: dict
suboptions:
certificate:
type: dict
description:
- Certificate SSL/TLS and hostname values.
- Value must be a dict defining values for keys (key and hostname).
suboptions:
key:
description:
- Certificate key
type: str
hostname:
description:
- Certificate hostname
type: str
compression:
type: str
description:
- Destination profile compression method.
choices:
- gzip
source_interface:
type: str
description:
- Destination profile source interface.
- Valid value is a str representing the source interface name.
vrf:
type: str
description:
- Destination profile vrf.
- Valid value is a str representing the vrf name.
destination_groups:
type: list
description:
- List of telemetry destination groups.
suboptions:
id:
type: int
description:
- Destination group identifier.
- Value must be a int representing the destination group identifier.
destination:
type: dict
description:
- Group destination ipv4, port, protocol and encoding values.
- Value must be a dict defining values for keys (ip, port, protocol, encoding).
suboptions:
ip:
type: str
description:
- Destination group IP address.
port:
type: int
description:
- Destination group port number.
protocol:
type: str
description:
- Destination group protocol.
choices:
- HTTP
- TCP
- UDP
- gRPC
encoding:
type: str
description:
- Destination group encoding.
choices:
- GPB
- JSON
sensor_groups:
type: list
description:
- List of telemetry sensor groups.
suboptions:
id:
type: int
description:
- Sensor group identifier.
- Value must be a int representing the sensor group identifier.
data_source:
type: str
description:
- Telemetry data source.
choices:
- NX-API
- DME
- YANG
path:
type: dict
description:
- Telemetry sensor path.
- Value must be a dict defining values for keys (name, depth, filter_condition, query_condition).
- Mandatory Keys (name)
- Optional Keys (depth, filter_condition, query_condition)
suboptions:
name:
type: str
description:
- Sensor group path name.
depth:
type: str
description:
- Sensor group depth.
filter_condition:
type: str
description:
- Sensor group filter condition.
query_condition:
type: str
description:
- Sensor group query condition.
subscriptions:
type: list
description:
- List of telemetry subscriptions.
suboptions:
id:
type: int
description:
- Subscription identifier.
- Value must be a int representing the subscription identifier.
destination_group:
type: int
description:
- Associated destination group.
sensor_group:
type: dict
description:
- Associated sensor group.
- Value must be a dict defining values for keys (id, sample_interval).
suboptions:
id:
type: int
description:
- Associated sensor group id.
sample_interval:
type: int
description:
- Associated sensor group id sample interval.
state:
description:
- Final configuration state
type: str
choices:
- merged
- replaced
- deleted
default: merged
"""
EXAMPLES = """
# Using deleted
# This action will delete all telemetry configuration on the device
- name: Delete Telemetry Configuration
nxos_telemetry:
state: deleted
# Using merged
# This action will merge telemetry configuration defined in the playbook with
# telemetry configuration that is already on the device.
- name: Merge Telemetry Configuration
nxos_telemetry:
config:
certificate:
key: /bootflash/server.key
hostname: localhost
compression: gzip
source_interface: Ethernet1/1
vrf: management
destination_groups:
- id: 2
destination:
ip: 192.168.0.2
port: 50001
protocol: gPRC
encoding: GPB
- id: 55
destination:
ip: 192.168.0.55
port: 60001
protocol: gPRC
encoding: GPB
sensor_groups:
- id: 1
data_source: NX-API
path:
name: '"show lldp neighbors detail"'
depth: 0
- id: 55
data_source: DME
path:
name: 'sys/ch'
depth: unbounded
filter_condition: 'ne(eqptFt.operSt,"ok")'
subscriptions:
- id: 5
destination_group: 55
sensor_group:
id: 1
sample_interval: 1000
- id: 6
destination_group: 2
sensor_group:
id: 55
sample_interval: 2000
state: merged
# Using replaced
# This action will replace telemetry configuration on the device with the
# telmetry configuration defined in the playbook.
- name: Override Telemetry Configuration
nxos_telemetry:
config:
certificate:
key: /bootflash/server.key
hostname: localhost
compression: gzip
source_interface: Ethernet1/1
vrf: management
destination_groups:
- id: 2
destination:
ip: 192.168.0.2
port: 50001
protocol: gPRC
encoding: GPB
subscriptions:
- id: 5
destination_group: 55
state: replaced
"""
RETURN = """
before:
description: The configuration prior to the model invocation.
returned: always
type: dict
sample: >
The configuration returned will always be in the same format
of the parameters above.
after:
description: The resulting configuration model invocation.
returned: when changed
type: dict
sample: >
The configuration returned will always be in the same format
of the parameters above.
commands:
description: The set of commands pushed to the remote device.
returned: always
type: list
sample: ['command 1', 'command 2', 'command 3']
"""
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.network.nxos.argspec.telemetry.telemetry import TelemetryArgs
from ansible.module_utils.network.nxos.config.telemetry.telemetry import Telemetry
def main():
"""
Main entry point for module execution
:returns: the result form module invocation
"""
module = AnsibleModule(argument_spec=TelemetryArgs.argument_spec,
supports_check_mode=True)
result = Telemetry(module).execute_module()
module.exit_json(**result)
if __name__ == '__main__':
main()

@ -0,0 +1,2 @@
dependencies:
- prepare_nxos_tests

@ -0,0 +1,27 @@
---
- name: collect common test cases
find:
paths: "{{ role_path }}/tests/common"
patterns: "{{ testcase }}.yaml"
connection: local
register: test_cases
- name: collect cli test cases
find:
paths: "{{ role_path }}/tests/cli"
patterns: "{{ testcase }}.yaml"
connection: local
register: cli_cases
- set_fact:
test_cases:
files: "{{ test_cases.files }} + {{ cli_cases.files }}"
- name: set test_items
set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}"
- name: run test cases (connection=network_cli)
include: "{{ test_case_to_run }} ansible_connection=network_cli connection={{ cli }}"
with_items: "{{ test_items }}"
loop_control:
loop_var: test_case_to_run

@ -0,0 +1,18 @@
---
- set_fact: run_test="true"
# Telemetry module only supported on N9k
- set_fact: run_test="false"
when: platform is not search("N9K")
# Telemetry module not supported on versions earlier then 7.0(3)I7(x)
- set_fact: run_test="false"
when: imagetag is search("I2|I3|I4|I5|I6")
- include: cli.yaml
tags: 'cli'
when: run_test
# Uncomment below when nxapi is supported for resource module builder modules
# - include: nxapi.yaml
# tags: 'nxapi'
# when: run_test

@ -0,0 +1,27 @@
---
- name: collect common test cases
find:
paths: "{{ role_path }}/tests/common"
patterns: "{{ testcase }}.yaml"
connection: local
register: test_cases
- name: collect nxapi test cases
find:
paths: "{{ role_path }}/tests/nxapi"
patterns: "{{ testcase }}.yaml"
connection: local
register: nxapi_cases
- set_fact:
test_cases:
files: "{{ test_cases.files }} + {{ nxapi_cases.files }}"
- name: set test_items
set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}"
- name: run test cases (connection=httpapi)
include: "{{ test_case_to_run }} ansible_connection=httpapi connection={{ nxapi }}"
with_items: "{{ test_items }}"
loop_control:
loop_var: test_case_to_run

@ -0,0 +1,93 @@
---
- debug: msg="START connection={{ ansible_connection }} nxos_telemetry deleted sanity test"
- set_fact: source_interface="Loopback55"
when: imagetag and (major_version is version_compare('9.1', 'ge'))
- set_fact: before_keys_length=6
- set_fact: before_keys_length=7
when: imagetag and (major_version is version_compare('9.1', 'ge'))
- name: Setup
nxos_feature: &setup_teardown
feature: telemetry
state: disabled
ignore_errors: yes
- name: Setup - Configure Telemetry
nxos_telemetry:
state: 'merged'
config:
certificate:
key: /bootflash/server.key
hostname: localhost
compression: gzip
source_interface: "{{source_interface|default(omit)}}"
vrf: management
destination_groups:
- id: 2
destination:
ip: 192.168.0.1
port: 50001
protocol: grpc
encoding: gpb
- { id: 2, destination: {ip: 192.168.0.2, port: 60001, protocol: gRPC, encoding: GPB}}
- { id: 10, destination: {ip: 192.168.0.1, port: 50001, protocol: Grpc, encoding: gPB}}
- { id: 10, destination: {ip: 192.168.0.2, port: 60001, protocol: gRPC, encoding: gpb}}
sensor_groups:
- { id: 8, data_source: NX-API, path: {name: sys/bgp, depth: 0, query_condition: foo, filter_condition: foo}}
- { id: 2, data_source: NX-API, path: {name: sys/bgp/inst, depth: unbounded, query_condition: foo, filter_condition: foo}}
- { id: 55, data_source: DME, path: {name: 'sys/bgp/inst/dom-default/peer-[10.10.10.11]/ent-[10.10.10.11]', depth: 0, query_condition: foo, filter_condition: foo}}
- { id: 55, data_source: DME, path: {name: sys/ospf, depth: 0, query_condition: foo, filter_condition: 'or(eq(ethpmPhysIf.operSt,"down"),eq(ethpmPhysIf.operSt,"up"))'}}
subscriptions:
- { id: 44, destination_group: 10, sensor_group: {id: 8, sample_interval: 2000}}
- { id: 44, destination_group: 2, sensor_group: {id: 2, sample_interval: 2000}}
- { id: 55, destination_group: 10, sensor_group: {id: 55, sample_interval: 2000}}
- block:
- name: Gather Telemetry Facts Before Changes
nxos_facts: &facts
gather_subset:
- '!all'
- '!min'
gather_network_resources:
- telemetry
- name: Telemetry - deleted
nxos_telemetry: &deleted
state: 'deleted'
register: result
# result.before|dict2items|length checks the number of dictionary keys.
- assert:
that:
- "result.changed == true"
- "'no telemetry' in result.commands"
- "result.before|dict2items|length == {{ before_keys_length }}"
- assert:
that:
- "(ansible_facts.network_resources.telemetry|dict2items)|symmetric_difference(result.before|dict2items)|length == 0"
- name: Gather Telemetry Facts After Changes
nxos_facts: *facts
- assert:
that:
- "(ansible_facts.network_resources.telemetry|dict2items)|symmetric_difference(result.after|dict2items)|length == 0"
- name: Telemetry - deleted - idempotence
nxos_telemetry: *deleted
register: result
- assert:
that:
- "result.changed == false"
- "result.commands|length == 0"
always:
- name: Teardown
nxos_feature: *setup_teardown
ignore_errors: yes
- debug: msg="END connection={{ ansible_connection }} nxos_telemetry deleted sanity test"

@ -0,0 +1,184 @@
---
- debug: msg="START connection={{ ansible_connection }} nxos_telemetry merged sanity test"
- set_fact: source_interface="Loopback55"
when: imagetag and (major_version is version_compare('9.1', 'ge'))
- set_fact: command_list_length=30
- set_fact: command_list_length=31
when: imagetag and (major_version is version_compare('9.1', 'ge'))
- name: Setup
nxos_feature: &setup_teardown
feature: telemetry
state: disabled
ignore_errors: yes
- block:
- name: Gather Telemetry Facts Before Changes
nxos_facts: &facts
gather_subset:
- '!all'
- '!min'
gather_network_resources:
- telemetry
- name: Telemetry - merged
nxos_telemetry: &merged
state: 'merged'
config:
certificate:
key: /bootflash/server.key
hostname: localhost
compression: gzip
source_interface: "{{source_interface|default(omit)}}"
vrf: management
destination_groups:
- id: 2
destination:
ip: 192.168.0.1
port: 50001
protocol: grpc
encoding: gpb
- { id: 2, destination: {ip: 192.168.0.2, port: 60001, protocol: gRPC, encoding: GPB}}
- { id: 10, destination: {ip: 192.168.0.1, port: 50001, protocol: Grpc, encoding: gPB}}
- { id: 10, destination: {ip: 192.168.0.2, port: 60001, protocol: gRPC, encoding: gpb}}
sensor_groups:
- { id: 8, data_source: NX-API, path: {name: sys/bgp, depth: 0, query_condition: foo, filter_condition: foo}}
- { id: 2, data_source: NX-API, path: {name: sys/bgp/inst, depth: unbounded, query_condition: foo, filter_condition: foo}}
- { id: 55, data_source: DME, path: {name: 'sys/bgp/inst/dom-default/peer-[10.10.10.11]/ent-[10.10.10.11]', depth: 0, query_condition: foo, filter_condition: foo}}
- { id: 55, data_source: DME, path: {name: sys/ospf, depth: 0, query_condition: foo, filter_condition: 'or(eq(ethpmPhysIf.operSt,"down"),eq(ethpmPhysIf.operSt,"up"))'}}
subscriptions:
- { id: 44, destination_group: 10, sensor_group: {id: 8, sample_interval: 2000}}
- { id: 44, destination_group: 2, sensor_group: {id: 2, sample_interval: 2000}}
- { id: 55, destination_group: 10, sensor_group: {id: 55, sample_interval: 2000}}
register: result
- assert:
that:
- "result.changed == true"
- "result.before|length == 0"
- "'feature telemetry' in result.commands"
- "'telemetry' in result.commands"
- "'certificate /bootflash/server.key localhost' in result.commands"
- "'destination-profile' in result.commands"
- "'use-compression gzip' in result.commands"
- "'use-vrf management' in result.commands"
- "'destination-group 2' in result.commands"
- "'ip address 192.168.0.1 port 50001 protocol grpc encoding gpb' in result.commands"
- "'ip address 192.168.0.2 port 60001 protocol grpc encoding gpb' in result.commands"
- "'destination-group 10' in result.commands"
- "'ip address 192.168.0.1 port 50001 protocol grpc encoding gpb' in result.commands"
- "'ip address 192.168.0.2 port 60001 protocol grpc encoding gpb' in result.commands"
- "'sensor-group 8' in result.commands"
- "'data-source NX-API' in result.commands"
- "'path sys/bgp depth 0 query-condition foo filter-condition foo' in result.commands"
- "'sensor-group 2' in result.commands"
- "'data-source NX-API' in result.commands"
- "'path sys/bgp/inst depth unbounded query-condition foo filter-condition foo' in result.commands"
- "'sensor-group 55' in result.commands"
- "'data-source DME' in result.commands"
- "'path sys/bgp/inst/dom-default/peer-[10.10.10.11]/ent-[10.10.10.11] depth 0 query-condition foo filter-condition foo' in result.commands"
- "'path sys/ospf depth 0 query-condition foo filter-condition or(eq(ethpmPhysIf.operSt,\"down\"),eq(ethpmPhysIf.operSt,\"up\"))' in result.commands"
- "'subscription 44' in result.commands"
- "'dst-grp 10' in result.commands"
- "'dst-grp 2' in result.commands"
- "'snsr-grp 8 sample-interval 2000' in result.commands"
- "'snsr-grp 2 sample-interval 2000' in result.commands"
- "'subscription 55' in result.commands"
- "'dst-grp 10' in result.commands"
- "'snsr-grp 55 sample-interval 2000' in result.commands"
- "result.commands|length == {{ command_list_length }}"
# Source interface may or may not be included based on the image version.
- assert:
that:
- "'source-interface loopback55' in result.commands"
when: imagetag and (major_version is version_compare('9.1', 'ge'))
- assert:
that:
- "(ansible_facts.network_resources.telemetry|dict2items)|symmetric_difference(result.before|dict2items)|length == 0"
- name: Gather Telemetry Facts After Changes
nxos_facts: *facts
- assert:
that:
- "(ansible_facts.network_resources.telemetry|dict2items)|symmetric_difference(result.after|dict2items)|length == 0"
- name: Telemetry - merged - idempotence
nxos_telemetry: *merged
register: result
- assert:
that:
- "result.changed == false"
- "result.commands|length == 0"
- name: Telemetry - change values
nxos_telemetry: &merged_change
state: 'merged'
config:
certificate:
key: /bootflash/local_server.key
hostname: localhost
compression: gzip
source_interface: "{{source_interface|default(omit)}}"
vrf: management
destination_groups:
- id: 2
destination:
ip: 192.168.0.1
port: 50001
protocol: grpc
encoding: gpb
- { id: 2, destination: {ip: 192.168.0.2, port: 60001, protocol: gRPC, encoding: GPB}}
- { id: 10, destination: {ip: 192.168.0.1, port: 50001, protocol: Grpc, encoding: gPB}}
- { id: 10, destination: {ip: 192.168.0.2, port: 60001, protocol: gRPC, encoding: gpb}}
sensor_groups:
- { id: 8, data_source: NX-API, path: {name: sys/bgp, depth: 0, query_condition: foo, filter_condition: foo}}
- { id: 2, data_source: NX-API, path: {name: sys/bgp/inst, depth: unbounded, query_condition: foo, filter_condition: foo}}
- { id: 55, data_source: DME, path: {name: 'sys/bgp/inst/dom-default/peer-[10.10.10.11]/ent-[10.10.10.11]', depth: 0, query_condition: foo, filter_condition: foo}}
- { id: 55, data_source: DME, path: {name: sys/ospf, depth: 0, query_condition: foo, filter_condition: 'or(eq(ethpmPhysIf.operSt,"down"),eq(ethpmPhysIf.operSt,"up"))'}}
subscriptions:
- { id: 44, destination_group: 10, sensor_group: {id: 8, sample_interval: 1000}}
- { id: 44, destination_group: 2, sensor_group: {id: 2, sample_interval: 2000}}
- { id: 55, destination_group: 10, sensor_group: {id: 55, sample_interval: 2000}}
register: result
# The step above should result in only the following changes:
# "commands": [
# "telemetry",
# "certificate /bootflash/local_server.key localhost",
# "subscription 44",
# "snsr-grp 8 sample-interval 1000"
# ],
- set_fact:
test_list:
- "telemetry"
- "certificate /bootflash/local_server.key localhost"
- "subscription 44"
- "snsr-grp 8 sample-interval 1000"
- assert:
that:
- "result.changed == true"
- "test_list|symmetric_difference(result.commands)|length == 0"
- name: Telemetry - change values - idempotent
nxos_telemetry: *merged_change
register: result
- assert:
that:
- "result.changed == false"
- "result.commands|length == 0"
always:
- name: Teardown
nxos_feature: *setup_teardown
ignore_errors: yes
- debug: msg="END connection={{ ansible_connection }} nxos_telemetry merged sanity test"

@ -0,0 +1,43 @@
feature telemetry
telemetry
certificate /bootflash/server.key localhost
destination-profile
use-vrf management
use-compression gzip
source-interface loopback55
destination-group 2
ip address 192.168.0.1 port 50001 protocol gRPC encoding GPB
ip address 192.168.0.2 port 60001 protocol gRPC encoding GPB
destination-group 10
ip address 192.168.0.1 port 50001 protocol gRPC encoding GPB
ip address 192.168.0.2 port 60001 protocol gRPC encoding GPB
sensor-group 2
data-source DME
path boo depth 0
path sys/ospf depth 0 query-condition qc filter-condition fc
path interfaces depth 0
path sys/bgp
path sys/bgp/inst depth 0 query-condition foo filter-condition foo
path sys/bgp/inst/dom-default/peer-[10.10.10.11]/ent-[10.10.10.11]
path sys/bgp/inst/dom-default/peer-[20.20.20.11]/ent-[20.20.20.11]
path too depth 0 filter-condition foo
sensor-group 55
sensor-group 56
data-source DME
path environment
path interface
path resources
path vxlan
subscription 3
subscription 4
dst-grp 2
snsr-grp 2 sample-interval 1000
subscription 5
dst-grp 2
snsr-grp 2 sample-interval 1000
subscription 6
dst-grp 10
subscription 7
dst-grp 10
snsr-grp 2 sample-interval 1000

@ -26,8 +26,8 @@ from units.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase
from units.modules.utils import set_module_args as _set_module_args from units.modules.utils import set_module_args as _set_module_args
def set_module_args(args): def set_module_args(args, ignore_provider=None):
if 'provider' not in args: if 'provider' not in args and not ignore_provider:
args['provider'] = {'transport': args.get('transport') or 'cli'} args['provider'] = {'transport': args.get('transport') or 'cli'}
return _set_module_args(args) return _set_module_args(args)

@ -0,0 +1,964 @@
# (c) 2019 Red Hat Inc.
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
from units.compat.mock import patch
from units.modules.utils import AnsibleFailJson
from ansible.modules.network.nxos import nxos_telemetry
from ansible.module_utils.network.nxos.nxos import NxosCmdRef
from ansible.module_utils.network.nxos.config.telemetry.telemetry import Telemetry
from .nxos_module import TestNxosModule, load_fixture, set_module_args
# TBD: These imports / import checks are only needed as a workaround for
# shippable, which fails this test due to import yaml & import ordereddict.
import pytest
from ansible.module_utils.network.nxos.nxos import nxosCmdRef_import_check
msg = nxosCmdRef_import_check()
ignore_provider_arg = True
@pytest.mark.skipif(len(msg), reason=msg)
class TestNxosTelemetryModule(TestNxosModule):
module = nxos_telemetry
def setUp(self):
super(TestNxosTelemetryModule, self).setUp()
self.mock_FACT_LEGACY_SUBSETS = patch('ansible.module_utils.network.nxos.facts.facts.FACT_LEGACY_SUBSETS')
self.FACT_LEGACY_SUBSETS = self.mock_FACT_LEGACY_SUBSETS.start()
self.mock_get_resource_connection_config = patch('ansible.module_utils.network.common.cfg.base.get_resource_connection')
self.get_resource_connection_config = self.mock_get_resource_connection_config.start()
self.mock_get_resource_connection_facts = patch('ansible.module_utils.network.common.facts.facts.get_resource_connection')
self.get_resource_connection_facts = self.mock_get_resource_connection_facts.start()
self.mock_edit_config = patch('ansible.module_utils.network.nxos.config.telemetry.telemetry.Telemetry.edit_config')
self.edit_config = self.mock_edit_config.start()
self.mock_execute_show_command = patch('ansible.module_utils.network.nxos.nxos.NxosCmdRef.execute_show_command')
self.execute_show_command = self.mock_execute_show_command.start()
self.mock_get_platform_shortname = patch('ansible.module_utils.network.nxos.nxos.NxosCmdRef.get_platform_shortname')
self.get_platform_shortname = self.mock_get_platform_shortname.start()
def tearDown(self):
super(TestNxosTelemetryModule, self).tearDown()
self.mock_FACT_LEGACY_SUBSETS.stop()
self.mock_get_resource_connection_config.stop()
self.mock_get_resource_connection_facts.stop()
self.mock_edit_config.stop()
self.mock_execute_show_command.stop()
self.get_platform_shortname.stop()
def load_fixtures(self, commands=None, device=''):
self.mock_FACT_LEGACY_SUBSETS.return_value = dict()
self.get_resource_connection_config.return_value = 'Connection'
self.get_resource_connection_facts.return_value = 'Connection'
self.edit_config.return_value = None
# ---------------------------
# Telemetry Global Test Cases
# ---------------------------
def test_tms_global_merged_n9k(self):
# Assumes feature telemetry is disabled
# TMS global config is not present.
self.execute_show_command.return_value = None
self.get_platform_shortname.return_value = 'N9K'
set_module_args(dict(
config=dict(
certificate={'key': '/bootflash/sample.key', 'hostname': 'server.example.com'},
compression='gzip',
source_interface='Ethernet2/1',
vrf='blue',
)
), ignore_provider_arg)
self.execute_module(changed=True, commands=[
'feature telemetry',
'telemetry',
'certificate /bootflash/sample.key server.example.com',
'destination-profile',
'use-compression gzip',
'source-interface Ethernet2/1',
'use-vrf blue'
])
def test_tms_global_checkmode_n9k(self):
# Assumes feature telemetry is disabled
# TMS global config is not present.
self.execute_show_command.return_value = None
self.get_platform_shortname.return_value = 'N9K'
set_module_args(dict(
_ansible_check_mode=True,
config=dict(
certificate={'key': '/bootflash/sample.key', 'hostname': 'server.example.com'},
compression='gzip',
source_interface='Ethernet2/1',
vrf='blue',
)
), ignore_provider_arg)
self.execute_module(changed=True, commands=[
'feature telemetry',
'telemetry',
'certificate /bootflash/sample.key server.example.com',
'destination-profile',
'use-compression gzip',
'source-interface Ethernet2/1',
'use-vrf blue'
])
def test_tms_global_merged2_n9k(self):
# Assumes feature telemetry is disabled
# TMS global config is not present.
# Configure only vrf
self.execute_show_command.return_value = None
self.get_platform_shortname.return_value = 'N9K'
set_module_args(dict(
config=dict(
vrf='blue',
)
), ignore_provider_arg)
self.execute_module(changed=True, commands=[
'feature telemetry',
'telemetry',
'destination-profile',
'use-vrf blue'
])
def test_tms_global_idempotent_n9k(self):
# Assumes feature telemetry is enabled
# TMS global config is present.
self.execute_show_command.return_value = load_fixture('nxos_telemetry', 'N9K.cfg')
self.get_platform_shortname.return_value = 'N9K'
set_module_args(dict(
config=dict(
certificate={'key': '/bootflash/server.key', 'hostname': 'localhost'},
compression='gzip',
source_interface='loopback55',
vrf='management',
)
), ignore_provider_arg)
self.execute_module(changed=False)
def test_tms_global_change_cert_n9k(self):
# Assumes feature telemetry is enabled
# TMS global config is present
# Change certificate
self.execute_show_command.return_value = load_fixture('nxos_telemetry', 'N9K.cfg')
self.get_platform_shortname.return_value = 'N9K'
set_module_args(dict(
config=dict(
certificate={'key': '/bootflash/server.key', 'hostname': 'my_host'},
compression='gzip',
source_interface='loopback55',
vrf='management',
)
), ignore_provider_arg)
self.execute_module(changed=True, commands=[
'telemetry',
'certificate /bootflash/server.key my_host'
])
def test_tms_global_change_interface_n9k(self):
# Assumes feature telemetry is enabled
# TMS global config is present
# Change interface
self.execute_show_command.return_value = load_fixture('nxos_telemetry', 'N9K.cfg')
self.get_platform_shortname.return_value = 'N9K'
set_module_args(dict(
config=dict(
certificate={'key': '/bootflash/server.key', 'hostname': 'localhost'},
compression='gzip',
source_interface='Ethernet8/1',
vrf='management',
)
), ignore_provider_arg)
self.execute_module(changed=True, commands=[
'telemetry',
'destination-profile',
'source-interface Ethernet8/1'
])
def test_tms_global_change_several_n9k(self):
# Assumes feature telemetry is enabled
# TMS global config is present
# Change source_interface, vrf and cert
self.execute_show_command.return_value = load_fixture('nxos_telemetry', 'N9K.cfg')
self.get_platform_shortname.return_value = 'N9K'
set_module_args(dict(
config=dict(
certificate={'key': '/bootflash/server_5.key', 'hostname': 'my_host'},
compression='gzip',
source_interface='Ethernet8/1',
vrf='blue',
)
), ignore_provider_arg)
self.execute_module(changed=True, commands=[
'telemetry',
'certificate /bootflash/server_5.key my_host',
'destination-profile',
'source-interface Ethernet8/1',
'use-vrf blue',
])
# ------------------------------
# Telemetry DestGroup Test Cases
# ------------------------------
def test_tms_destgroup_input_validation_1(self):
# Mandatory parameter 'id' missing.
self.execute_show_command.return_value = None
self.get_platform_shortname.return_value = 'N9K'
args = build_args([
{'destination': {'ip': '192.168.1.1', 'port': '5001', 'protocol': 'GRPC', 'encoding': 'GPB'}}
], 'destination_groups')
set_module_args(args, ignore_provider_arg)
with pytest.raises(AnsibleFailJson) as errinfo:
self.execute_module()
testdata = errinfo.value.args[0]
assert 'Parameter <id> under <destination_groups> is required' in str(testdata['msg'])
assert testdata['failed']
def test_tms_destgroup_input_validation_2(self):
# Parameter 'destination' is not a dict.
self.execute_show_command.return_value = None
self.get_platform_shortname.return_value = 'N9K'
args = build_args([
{'id': '88',
'destination': '192.168.1.1',
}
], 'destination_groups')
set_module_args(args, ignore_provider_arg)
with pytest.raises(AnsibleFailJson) as errinfo:
self.execute_module()
testdata = errinfo.value.args[0]
assert "Parameter <destination> under <destination_groups> must be a dict" in str(testdata['msg'])
assert testdata['failed']
def test_tms_destgroup_input_validation_3(self):
# Parameter 'destination' is not a dict.
self.execute_show_command.return_value = None
self.get_platform_shortname.return_value = 'N9K'
args = build_args([
{'id': '88',
'ip': '192.168.1.1',
'port': '5001'
}
], 'destination_groups')
set_module_args(args, ignore_provider_arg)
with pytest.raises(AnsibleFailJson) as errinfo:
self.execute_module()
testdata = errinfo.value.args[0]
assert 'Playbook entry contains unrecongnized parameters' in str(testdata['msg'])
assert testdata['failed']
def test_tms_destgroup_merged_n9k(self):
# Assumes feature telemetry is enabled
# TMS destgroup config is not present.
self.execute_show_command.return_value = None
self.get_platform_shortname.return_value = 'N9K'
args = build_args([
{'id': '88',
'destination': {'ip': '192.168.1.1', 'port': '5001', 'protocol': 'GRPC', 'encoding': 'GPB'},
},
{'id': '88',
'destination': {'ip': '192.168.1.2', 'port': '6001', 'protocol': 'GRPC', 'encoding': 'GPB'},
},
{'id': '99',
'destination': {'ip': '192.168.1.2', 'port': '6001', 'protocol': 'GRPC', 'encoding': 'GPB'},
},
{'id': '99',
'destination': {'ip': '192.168.1.1', 'port': '5001', 'protocol': 'GRPC', 'encoding': 'GPB'},
},
], 'destination_groups')
set_module_args(args, ignore_provider_arg)
self.execute_module(changed=True, commands=[
'feature telemetry',
'telemetry',
'destination-group 88',
'ip address 192.168.1.1 port 5001 protocol grpc encoding gpb',
'ip address 192.168.1.2 port 6001 protocol grpc encoding gpb',
'destination-group 99',
'ip address 192.168.1.2 port 6001 protocol grpc encoding gpb',
'ip address 192.168.1.1 port 5001 protocol grpc encoding gpb',
])
def test_tms_destgroup_checkmode_n9k(self):
# Assumes feature telemetry is enabled
# TMS destgroup config is not present.
self.execute_show_command.return_value = None
self.get_platform_shortname.return_value = 'N9K'
args = build_args([
{'id': '88',
'destination': {'ip': '192.168.1.1', 'port': '5001', 'protocol': 'GRPC', 'encoding': 'GPB'},
}
], 'destination_groups', state='merged', check_mode=True)
set_module_args(args, ignore_provider_arg)
self.execute_module(changed=True, commands=[
'feature telemetry',
'telemetry',
'destination-group 88',
'ip address 192.168.1.1 port 5001 protocol grpc encoding gpb'
])
def test_tms_destgroup_merged2_n9k(self):
# Assumes feature telemetry is enabled
# TMS destgroup config is not present.
# Configure only identifier
self.execute_show_command.return_value = None
self.get_platform_shortname.return_value = 'N9K'
args = build_args([
{'id': '88'}
], 'destination_groups')
set_module_args(args, ignore_provider_arg)
self.execute_module(changed=True, commands=[
'feature telemetry',
'telemetry',
'destination-group 88',
])
def test_tms_destgroup_idempotent_n9k(self):
# Assumes feature telemetry is enabled
# TMS destgroup config is not present.
# Configure only identifier
self.execute_show_command.return_value = load_fixture('nxos_telemetry', 'N9K.cfg')
self.get_platform_shortname.return_value = 'N9K'
args = build_args([
{'id': '2',
'destination': {'ip': '192.168.0.2', 'port': '60001', 'protocol': 'grpc', 'encoding': 'gpb'},
}
], 'destination_groups')
set_module_args(args, ignore_provider_arg)
self.execute_module(changed=False)
def test_tms_destgroup_idempotent2_n9k(self):
# Assumes feature telemetry is enabled
# TMS destgroup config is not present.
# Configure only identifier
self.execute_show_command.return_value = load_fixture('nxos_telemetry', 'N9K.cfg')
self.get_platform_shortname.return_value = 'N9K'
args = build_args([
{'id': '2'}
], 'destination_groups')
set_module_args(args, ignore_provider_arg)
self.execute_module(changed=False)
def test_tms_destgroup_merged_aggregate_idempotent_n9k(self):
# Assumes feature telemetry is enabled
# TMS destgroup config is present.
self.execute_show_command.return_value = load_fixture('nxos_telemetry', 'N9K.cfg')
self.get_platform_shortname.return_value = 'N9K'
args = build_args([
{'id': '2',
'destination': {'ip': '192.168.0.1', 'port': '50001', 'protocol': 'gRPC', 'encoding': 'gpb'}
},
{'id': '10',
'destination': {'ip': '192.168.0.1', 'port': '50001', 'protocol': 'gRPC', 'encoding': 'gpb'}
}
], 'destination_groups')
set_module_args(args, ignore_provider_arg)
self.execute_module(changed=False)
def test_tms_destgroup_change_n9k(self):
# TMS destgroup config is not present.
# Change protocol and encoding for dest group 2
self.execute_show_command.return_value = load_fixture('nxos_telemetry', 'N9K.cfg')
self.get_platform_shortname.return_value = 'N9K'
args = build_args([
{'id': '2',
'destination': {'ip': '192.168.0.1', 'port': '50001', 'protocol': 'http', 'encoding': 'JSON'}
},
{'id': '10',
'destination': {'ip': '192.168.0.1', 'port': '50001', 'protocol': 'gRPC', 'encoding': 'gpb'}
}
], 'destination_groups')
set_module_args(args, ignore_provider_arg)
self.execute_module(changed=True, commands=[
'telemetry', 'destination-group 2',
'ip address 192.168.0.1 port 50001 protocol http encoding json'
])
def test_tms_destgroup_add_n9k(self):
# TMS destgroup config is not present.
# Add destinations to destgroup 10
# Add new destgroup 55 and 56
self.execute_show_command.return_value = load_fixture('nxos_telemetry', 'N9K.cfg')
self.get_platform_shortname.return_value = 'N9K'
args = build_args([
{'id': '10',
'destination': {'ip': '192.168.0.1', 'port': '50001', 'protocol': 'gRPC', 'encoding': 'gpb'}
},
{'id': '10',
'destination': {'ip': '192.168.0.10', 'port': '50001', 'protocol': 'gRPC', 'encoding': 'gpb'}
},
{'id': '55',
'destination': {'ip': '192.168.0.2', 'port': '50001', 'protocol': 'gRPC', 'encoding': 'gpb'}
},
{'id': '56'},
], 'destination_groups')
set_module_args(args, ignore_provider_arg)
self.execute_module(changed=True, commands=[
'telemetry',
'destination-group 10',
'ip address 192.168.0.10 port 50001 protocol grpc encoding gpb',
'destination-group 55',
'ip address 192.168.0.2 port 50001 protocol grpc encoding gpb',
'destination-group 56'
])
# --------------------------------
# Telemetry SensorGroup Test Cases
# --------------------------------
def test_tms_sensorgroup_merged_n9k(self):
# Assumes feature telemetry is enabled
# TMS sensorgroup config is not present.
self.execute_show_command.return_value = None
self.get_platform_shortname.return_value = 'N9K'
td55_name = 'sys/bgp/inst/dom-default/peer-[10.10.10.11]/ent-[10.10.10.11]'
td55_fc = 'or(eq(ethpmPhysIf.operSt,"down"),eq(ethpmPhysIf.operSt,"up"))'
args = build_args([
{'id': '2',
'data_source': 'NX-API',
'path': {'name': 'sys/bgp', 'depth': 0, 'query_condition': 'foo', 'filter_condition': 'foo'},
},
{'id': '2',
'data_source': 'NX-API',
'path': {'name': 'sys/bgp/inst', 'depth': 'unbounded', 'query_condition': 'foo', 'filter_condition': 'foo'},
},
{'id': '55',
'data_source': 'DME',
'path': {'name': td55_name, 'depth': 0, 'query_condition': 'foo', 'filter_condition': 'foo'},
},
{'id': '55',
'data_source': 'DME',
'path': {'name': 'sys/ospf', 'depth': 0, 'query_condition': 'foo', 'filter_condition': td55_fc},
},
], 'sensor_groups')
set_module_args(args, ignore_provider_arg)
self.execute_module(changed=True, commands=[
'feature telemetry',
'telemetry',
'sensor-group 2',
'data-source NX-API',
'path sys/bgp depth 0 query-condition foo filter-condition foo',
'path sys/bgp/inst depth unbounded query-condition foo filter-condition foo',
'sensor-group 55',
'data-source DME',
'path sys/bgp/inst/dom-default/peer-[10.10.10.11]/ent-[10.10.10.11] depth 0 query-condition foo filter-condition foo',
'path sys/ospf depth 0 query-condition foo filter-condition or(eq(ethpmPhysIf.operSt,"down"),eq(ethpmPhysIf.operSt,"up"))',
])
def test_tms_sensorgroup_input_validation_1(self):
# Mandatory parameter 'id' missing.
self.execute_show_command.return_value = None
self.get_platform_shortname.return_value = 'N9K'
args = build_args([
{'data_source': 'DME',
'path': {'name': 'sys/bgp', 'depth': 0, 'query_condition': 'query_condition_xyz', 'filter_condition': 'filter_condition_xyz'},
},
], 'sensor_groups')
set_module_args(args, ignore_provider_arg)
with pytest.raises(AnsibleFailJson) as errinfo:
self.execute_module()
testdata = errinfo.value.args[0]
assert 'Parameter <id> under <sensor_groups> is required' in str(testdata['msg'])
assert testdata['failed']
def test_tms_sensorgroup_input_validation_2(self):
# Path present but mandatory 'name' key is not
self.execute_show_command.return_value = None
self.get_platform_shortname.return_value = 'N9K'
args = build_args([
{'id': '77',
'data_source': 'DME',
'path': {'depth': 0, 'query_condition': 'query_condition_xyz', 'filter_condition': 'filter_condition_xyz'},
},
], 'sensor_groups')
set_module_args(args, ignore_provider_arg)
with pytest.raises(AnsibleFailJson) as errinfo:
self.execute_module()
testdata = errinfo.value.args[0]
assert 'Parameter <path> under <sensor_groups> requires <name> key' in str(testdata['msg'])
assert testdata['failed']
def test_tms_sensorgroup_resource_key_n9k(self):
# TMS sensorgroup config is not present.
self.execute_show_command.return_value = None
self.get_platform_shortname.return_value = 'N9K'
args = build_args([
{'id': '77'}
], 'sensor_groups')
set_module_args(args, ignore_provider_arg)
self.execute_module(changed=True, commands=[
'feature telemetry',
'telemetry',
'sensor-group 77',
])
def test_tms_sensorgroup_merged_variable_args1_n9k(self):
# TMS sensorgroup config is not present.
# Only path key name provided
self.execute_show_command.return_value = None
self.get_platform_shortname.return_value = 'N9K'
args = build_args([
{'id': '77',
'data_source': 'DME',
'path': {'name': 'sys/bgp'},
},
], 'sensor_groups')
set_module_args(args, ignore_provider_arg)
self.execute_module(changed=True, commands=[
'feature telemetry',
'telemetry',
'sensor-group 77',
'data-source DME',
'path sys/bgp',
])
def test_tms_sensorgroup_merged_variable_args2_n9k(self):
# TMS sensorgroup config is not present.
# Only path keys name and depth provided
self.execute_show_command.return_value = None
self.get_platform_shortname.return_value = 'N9K'
args = build_args([
{'id': '77',
'data_source': 'DME',
'path': {'name': 'sys/bgp', 'depth': 0},
},
], 'sensor_groups')
set_module_args(args, ignore_provider_arg)
self.execute_module(changed=True, commands=[
'feature telemetry',
'telemetry',
'sensor-group 77',
'data-source DME',
'path sys/bgp depth 0',
])
def test_tms_sensorgroup_merged_variable_args3_n9k(self):
# TMS sensorgroup config is not present.
# Only path keys name, depth and query_condition provided
self.execute_show_command.return_value = None
self.get_platform_shortname.return_value = 'N9K'
args = build_args([
{'id': '77',
'data_source': 'DME',
'path': {'name': 'sys/bgp', 'depth': 0, 'query_condition': 'query_condition_xyz'},
},
], 'sensor_groups')
set_module_args(args, ignore_provider_arg)
self.execute_module(changed=True, commands=[
'feature telemetry',
'telemetry',
'sensor-group 77',
'data-source DME',
'path sys/bgp depth 0 query-condition query_condition_xyz',
])
def test_tms_sensorgroup_merged_variable_args4_n9k(self):
# TMS sensorgroup config is not present.
# Only path keys name, depth and filter_condition provided
self.execute_show_command.return_value = None
self.get_platform_shortname.return_value = 'N9K'
args = build_args([
{'id': '77',
'data_source': 'DME',
'path': {'name': 'sys/bgp', 'depth': 0, 'filter_condition': 'filter_condition_xyz'},
},
], 'sensor_groups')
set_module_args(args, ignore_provider_arg)
self.execute_module(changed=True, commands=[
'feature telemetry',
'telemetry',
'sensor-group 77',
'data-source DME',
'path sys/bgp depth 0 filter-condition filter_condition_xyz',
])
def test_tms_sensorgroup_merged_idempotent_n9k(self):
# Assumes feature telemetry is enabled
# TMS sensorgroup config is not present.
self.execute_show_command.return_value = load_fixture('nxos_telemetry', 'N9K.cfg')
self.get_platform_shortname.return_value = 'N9K'
args = build_args([
{'id': '2',
'data_source': 'DME',
'path': {'name': 'sys/ospf', 'depth': 0, 'query_condition': 'qc', 'filter_condition': 'fc'},
},
], 'sensor_groups')
set_module_args(args, ignore_provider_arg)
self.execute_module(changed=False)
def test_tms_sensorgroup_vxlan_idempotent_n9k(self):
# TMS sensorgroup config present.
self.execute_show_command.return_value = load_fixture('nxos_telemetry', 'N9K.cfg')
self.get_platform_shortname.return_value = 'N9K'
args = build_args([
{'id': '56',
'data_source': 'DME',
'path': {'name': 'vxlan'},
},
], 'sensor_groups')
set_module_args(args, ignore_provider_arg)
self.execute_module(changed=False)
def test_tms_sensorgroup_idempotent_variable1_n9k(self):
# TMS sensorgroup config is present with path key name.
self.execute_show_command.return_value = load_fixture('nxos_telemetry', 'N9K.cfg')
self.get_platform_shortname.return_value = 'N9K'
args = build_args([
{'id': '2',
'data_source': 'DME',
'path': {'name': 'sys/bgp/inst/dom-default/peer-[10.10.10.11]/ent-[10.10.10.11]'},
},
], 'sensor_groups')
set_module_args(args, ignore_provider_arg)
self.execute_module(changed=False)
def test_tms_sensorgroup_idempotent_variable2_n9k(self):
# TMS sensorgroup config is present with path key name and depth.
self.execute_show_command.return_value = load_fixture('nxos_telemetry', 'N9K.cfg')
self.get_platform_shortname.return_value = 'N9K'
args = build_args([
{'id': '2',
'data_source': 'DME',
'path': {'name': 'boo', 'depth': 0},
},
], 'sensor_groups')
set_module_args(args, ignore_provider_arg)
self.execute_module(changed=False)
def test_tms_sensorgroup_idempotent_resource_key_n9k(self):
# TMS sensorgroup config is present resource key only.
self.execute_show_command.return_value = load_fixture('nxos_telemetry', 'N9K.cfg')
self.get_platform_shortname.return_value = 'N9K'
args = build_args([
{'id': '55'}
], 'sensor_groups')
set_module_args(args, ignore_provider_arg)
self.execute_module(changed=False)
def test_tms_sensorgroup_present_path_environment_n9k(self):
# TMS sensorgroup config is not present.
# Path name 'environment' test
self.execute_show_command.return_value = None
self.get_platform_shortname.return_value = 'N9K'
args = build_args([
{'id': '77',
'data_source': 'YANG',
'path': {'name': 'environment'},
},
], 'sensor_groups')
set_module_args(args, ignore_provider_arg)
self.execute_module(changed=True, commands=[
'feature telemetry',
'telemetry',
'sensor-group 77',
'data-source YANG',
'path environment',
])
def test_tms_sensorgroup_present_path_interface_n9k(self):
# TMS sensorgroup config is not present.
# Path name 'interface' test
self.execute_show_command.return_value = None
self.get_platform_shortname.return_value = 'N9K'
args = build_args([
{'id': '77',
'data_source': 'NATIVE',
'path': {'name': 'interface'},
},
], 'sensor_groups')
set_module_args(args, ignore_provider_arg)
self.execute_module(changed=True, commands=[
'feature telemetry',
'telemetry',
'sensor-group 77',
'data-source NATIVE',
'path interface',
])
def test_tms_sensorgroup_present_path_interface_n9k(self):
# TMS sensorgroup config is not present.
# Path name 'resources' test
self.execute_show_command.return_value = None
self.get_platform_shortname.return_value = 'N9K'
args = build_args([
{'id': '77',
'data_source': 'NX-API',
'path': {'name': 'resources'},
},
], 'sensor_groups')
set_module_args(args, ignore_provider_arg)
self.execute_module(changed=True, commands=[
'feature telemetry',
'telemetry',
'sensor-group 77',
'data-source NX-API',
'path resources',
])
# ---------------------------------
# Telemetry Subscription Test Cases
# ---------------------------------
def test_tms_subscription_merged_n9k(self):
# TMS subscription config is not present.
self.execute_show_command.return_value = None
self.get_platform_shortname.return_value = 'N9K'
args = build_args([
{'id': 5,
'destination_group': 55,
'sensor_group': {'id': 1, 'sample_interval': 1000},
},
{'id': 88,
'destination_group': 3,
'sensor_group': {'id': 4, 'sample_interval': 2000},
},
], 'subscriptions')
set_module_args(args, ignore_provider_arg)
self.execute_module(changed=True, commands=[
'feature telemetry',
'telemetry',
'subscription 5',
'dst-grp 55',
'snsr-grp 1 sample-interval 1000',
'subscription 88',
'dst-grp 3',
'snsr-grp 4 sample-interval 2000'
])
def test_tms_subscription_merged_idempotent_n9k(self):
# TMS subscription config is not present.
self.execute_show_command.return_value = load_fixture('nxos_telemetry', 'N9K.cfg')
self.get_platform_shortname.return_value = 'N9K'
args = build_args([
{'id': 3,
},
{'id': 7,
'destination_group': 10,
'sensor_group': {'id': 2, 'sample_interval': 1000},
},
{'id': 5,
'destination_group': 2,
'sensor_group': {'id': 2, 'sample_interval': 1000},
},
], 'subscriptions')
set_module_args(args, ignore_provider_arg)
self.execute_module(changed=False)
def test_tms_subscription_merged_change1_n9k(self):
# TMS subscription config present.
# Change sample interval for sensor group 2
self.execute_show_command.return_value = load_fixture('nxos_telemetry', 'N9K.cfg')
self.get_platform_shortname.return_value = 'N9K'
args = build_args([
{'id': 3,
},
{'id': 7,
'destination_group': 10,
'sensor_group': {'id': 2, 'sample_interval': 3000},
},
{'id': 5,
'destination_group': 2,
'sensor_group': {'id': 2, 'sample_interval': 1000},
},
], 'subscriptions')
set_module_args(args, ignore_provider_arg)
self.execute_module(changed=True, commands=[
'telemetry',
'subscription 7',
'snsr-grp 2 sample-interval 3000'
])
def test_tms_subscription_add_n9k(self):
# TMS subscription config present.
# Add new destination_group and sensor_group to subscription 5
self.execute_show_command.return_value = load_fixture('nxos_telemetry', 'N9K.cfg')
self.get_platform_shortname.return_value = 'N9K'
args = build_args([
{'id': 3,
},
{'id': 7,
'destination_group': 10,
'sensor_group': {'id': 2, 'sample_interval': 1000},
},
{'id': 5,
'destination_group': 2,
'sensor_group': {'id': 2, 'sample_interval': 1000},
},
{'id': 5,
'destination_group': 7,
'sensor_group': {'id': 2, 'sample_interval': 1000},
},
{'id': 5,
'destination_group': 8,
'sensor_group': {'id': 9, 'sample_interval': 1000},
},
{'id': 5,
'destination_group': 9,
'sensor_group': {'id': 10, 'sample_interval': 1000},
},
], 'subscriptions')
set_module_args(args, ignore_provider_arg)
self.execute_module(changed=True, commands=[
'telemetry',
'subscription 5',
'dst-grp 7',
'dst-grp 8',
'dst-grp 9',
'snsr-grp 9 sample-interval 1000',
'snsr-grp 10 sample-interval 1000'
])
def test_telemetry_full_n9k(self):
# Assumes feature telemetry is disabled
# TMS global config is not present.
self.execute_show_command.return_value = None
self.get_platform_shortname.return_value = 'N9K'
set_module_args({
'state': 'merged',
'config': {
'certificate': {'key': '/bootflash/sample.key', 'hostname': 'server.example.com'},
'compression': 'gzip',
'source_interface': 'Ethernet2/1',
'vrf': 'blue',
'destination_groups': [
{'id': '88',
'destination': {'ip': '192.168.1.1', 'port': '5001', 'protocol': 'GRPC', 'encoding': 'GPB'},
},
{'id': '88',
'destination': {'ip': '192.168.1.2', 'port': '6001', 'protocol': 'GRPC', 'encoding': 'GPB'},
},
{'id': '99',
'destination': {'ip': '192.168.1.2', 'port': '6001', 'protocol': 'GRPC', 'encoding': 'GPB'},
},
{'id': '99',
'destination': {'ip': '192.168.1.1', 'port': '5001', 'protocol': 'GRPC', 'encoding': 'GPB'},
},
],
'sensor_groups': [
{'id': '77',
'data_source': 'DME',
'path': {'name': 'sys/bgp', 'depth': 0, 'query_condition': 'query_condition_xyz', 'filter_condition': 'filter_condition_xyz'},
},
{'id': '99',
'data_source': 'DME',
'path': {'name': 'sys/bgp', 'depth': 0, 'query_condition': 'query_condition_xyz', 'filter_condition': 'filter_condition_xyz'},
},
],
'subscriptions': [
{'id': 5,
'destination_group': 55,
'sensor_group': {'id': 1, 'sample_interval': 1000},
},
{'id': 88,
'destination_group': 3,
'sensor_group': {'id': 4, 'sample_interval': 2000},
},
],
}
}, ignore_provider_arg)
self.execute_module(changed=True, commands=[
'feature telemetry',
'telemetry',
'certificate /bootflash/sample.key server.example.com',
'destination-profile',
'use-compression gzip',
'source-interface Ethernet2/1',
'use-vrf blue',
'destination-group 88',
'ip address 192.168.1.1 port 5001 protocol grpc encoding gpb',
'ip address 192.168.1.2 port 6001 protocol grpc encoding gpb',
'destination-group 99',
'ip address 192.168.1.2 port 6001 protocol grpc encoding gpb',
'ip address 192.168.1.1 port 5001 protocol grpc encoding gpb',
'sensor-group 77',
'data-source DME',
'path sys/bgp depth 0 query-condition query_condition_xyz filter-condition filter_condition_xyz',
'sensor-group 99',
'data-source DME',
'path sys/bgp depth 0 query-condition query_condition_xyz filter-condition filter_condition_xyz',
'subscription 5',
'dst-grp 55',
'snsr-grp 1 sample-interval 1000',
'subscription 88',
'dst-grp 3',
'snsr-grp 4 sample-interval 2000'
])
def test_telemetry_deleted_input_validation_n9k(self):
# State is 'deleted' and 'config' key present.
self.execute_show_command.return_value = load_fixture('nxos_telemetry', 'N9K.cfg')
self.get_platform_shortname.return_value = 'N9K'
set_module_args(dict(
state='deleted',
config=dict(
certificate={'key': '/bootflash/server.key', 'hostname': 'localhost'},
compression='gzip',
source_interface='loopback55',
vrf='management',
)
), ignore_provider_arg)
with pytest.raises(AnsibleFailJson) as errinfo:
self.execute_module()
testdata = errinfo.value.args[0]
assert 'Remove config key from playbook when state is <deleted>' in str(testdata['msg'])
assert testdata['failed']
def test_telemetry_deleted_n9k(self):
# Assumes feature telemetry is enabled
# TMS global config is present.
# Make absent with all playbook keys provided
self.execute_show_command.return_value = load_fixture('nxos_telemetry', 'N9K.cfg')
self.get_platform_shortname.return_value = 'N9K'
set_module_args(dict(
state='deleted',
), ignore_provider_arg)
self.execute_module(changed=True, commands=['no telemetry'])
def test_telemetry_deleted_idempotent_n9k(self):
# Assumes feature telemetry is enabled
# TMS global config is present.
# Make absent with all playbook keys provided
self.execute_show_command.return_value = None
self.get_platform_shortname.return_value = 'N9K'
set_module_args(dict(
state='deleted',
), ignore_provider_arg)
self.execute_module(changed=False)
def build_args(data, type, state=None, check_mode=None):
if state is None:
state = 'merged'
if check_mode is None:
check_mode = False
args = {
'state': state,
'_ansible_check_mode': check_mode,
'config': {
type: data
}
}
return args
Loading…
Cancel
Save