diff --git a/lib/ansible/module_utils/network/nxos/argspec/facts/facts.py b/lib/ansible/module_utils/network/nxos/argspec/facts/facts.py index 00d13771b52..10097a921c7 100644 --- a/lib/ansible/module_utils/network/nxos/argspec/facts/facts.py +++ b/lib/ansible/module_utils/network/nxos/argspec/facts/facts.py @@ -6,9 +6,11 @@ """ The arg spec for the nxos facts module. """ + CHOICES = [ 'all', 'lag_interfaces', + 'telemetry', ] diff --git a/lib/ansible/module_utils/network/nxos/argspec/telemetry/__init__.py b/lib/ansible/module_utils/network/nxos/argspec/telemetry/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/nxos/argspec/telemetry/telemetry.py b/lib/ansible/module_utils/network/nxos/argspec/telemetry/telemetry.py new file mode 100644 index 00000000000..fd9753e2451 --- /dev/null +++ b/lib/ansible/module_utils/network/nxos/argspec/telemetry/telemetry.py @@ -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 diff --git a/lib/ansible/module_utils/network/nxos/cmdref/__init__.py b/lib/ansible/module_utils/network/nxos/cmdref/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/nxos/cmdref/telemetry/__init__.py b/lib/ansible/module_utils/network/nxos/cmdref/telemetry/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/nxos/cmdref/telemetry/telemetry.py b/lib/ansible/module_utils/network/nxos/cmdref/telemetry/telemetry.py new file mode 100644 index 00000000000..10e44f4e4c1 --- /dev/null +++ b/lib/ansible/module_utils/network/nxos/cmdref/telemetry/telemetry.py @@ -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\\S+) (?P\\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\\S+) port (?P\\S+) protocol (?P\\S+) encoding (?P\\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\\S+)( depth (?P\\S+))?( query-condition (?P\\S+))?( filter-condition (?P\\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\\S+) sample-interval (?P\\S+)$ + setval: snsr-grp {id} sample-interval {sample_interval} + default: + id: ~ + sample_interval: ~ +''' diff --git a/lib/ansible/module_utils/network/nxos/config/telemetry/__init__.py b/lib/ansible/module_utils/network/nxos/config/telemetry/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/nxos/config/telemetry/telemetry.py b/lib/ansible/module_utils/network/nxos/config/telemetry/telemetry.py new file mode 100644 index 00000000000..538275115bd --- /dev/null +++ b/lib/ansible/module_utils/network/nxos/config/telemetry/telemetry.py @@ -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 is invalid for this module.') + if 'replaced' in state: + self._module.fail_json(msg='State: 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 ') + + 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 diff --git a/lib/ansible/module_utils/network/nxos/facts/facts.py b/lib/ansible/module_utils/network/nxos/facts/facts.py index 9db75c2f427..95c649fe42c 100644 --- a/lib/ansible/module_utils/network/nxos/facts/facts.py +++ b/lib/ansible/module_utils/network/nxos/facts/facts.py @@ -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.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.telemetry.telemetry import TelemetryFacts FACT_LEGACY_SUBSETS = dict( @@ -25,6 +26,7 @@ FACT_LEGACY_SUBSETS = dict( ) FACT_RESOURCE_SUBSETS = dict( lag_interfaces=Lag_interfacesFacts, + telemetry=TelemetryFacts, ) diff --git a/lib/ansible/module_utils/network/nxos/facts/telemetry/__init__.py b/lib/ansible/module_utils/network/nxos/facts/telemetry/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/nxos/facts/telemetry/telemetry.py b/lib/ansible/module_utils/network/nxos/facts/telemetry/telemetry.py new file mode 100644 index 00000000000..4cfa3b896c5 --- /dev/null +++ b/lib/ansible/module_utils/network/nxos/facts/telemetry/telemetry.py @@ -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) diff --git a/lib/ansible/module_utils/network/nxos/nxos.py b/lib/ansible/module_utils/network/nxos/nxos.py index 1f5d08bfcc9..d7885bcd3d4 100644 --- a/lib/ansible/module_utils/network/nxos/nxos.py +++ b/lib/ansible/module_utils/network/nxos/nxos.py @@ -32,6 +32,7 @@ import collections import json import re import sys +from copy import deepcopy from ansible.module_utils._text import to_text 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.common._collections_compat import Mapping 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.urls import fetch_url @@ -742,6 +744,9 @@ class NxosCmdRef: self._module = module self._check_imports() self._yaml_load(cmd_ref_str) + self.cache_existing = None + self.present_states = ['present', 'merged'] + self.absent_states = ['absent', 'deleted'] ref = self._ref # Create a list of supported commands based on ref keys @@ -932,31 +937,37 @@ class NxosCmdRef: # Last key in context is the 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. Store these states in each command's 'existing' key. """ ref = self._ref if ref.get('_cli_is_feature_disabled'): # 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']] return show_cmd = ref['_template']['get_command'] - # Add additional command context if needed. - for filter in ref['_context']: - show_cmd = show_cmd + " | section '{0}'".format(filter) + 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. + if ref['_context']: + output = CustomNetworkConfig(indent=2, contents=output) + output = output.get_section(ref['_context']) - output = self.execute_show_command(show_cmd, 'text') or [] if not output: # 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']] return # 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['_context'][-1] in output: ref['_context'][-1] = 'no ' + ref['_context'][-1] @@ -997,18 +1008,35 @@ class NxosCmdRef: """ ref = self._ref 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(): - if k in module.params and module.params[k] is not None: - playval = module.params[k] - # Normalize each value - 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'] = playval + for level in params.keys(): + if isinstance(params[level], dict): + params[level] = [params[level]] + for item in params[level]: + if k in item and item[k] is not None: + if not ref[k].get('playval'): + ref[k]['playval'] = {} + playval = item[k] + index = params[level].index(item) + # Normalize each value + 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): """Helper function to create list of commands to configure device @@ -1037,7 +1065,7 @@ class NxosCmdRef: else: raise ValueError("get_proposed: unknown 'kind' value specified for key '{0}'".format(k)) 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 # Commands may require parent commands for proper 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]] def compare(playval, existing): - if 'present' in ref['_state']: + if ref['_state'] in self.present_states: if existing is None: return False elif playval == existing: @@ -1070,7 +1098,7 @@ class NxosCmdRef: elif isinstance(existing, dict) and playval in existing.values(): 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()): existing = None if existing is None or playval not in existing.values(): @@ -1080,32 +1108,45 @@ class NxosCmdRef: # Compare against current state for k in play_keys: 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']) multiple = 'multiple' in ref[k].keys() # Multiple Instances: if isinstance(existing, dict) and multiple: 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 - dvalue = dict((k, v) for k, v in dvalue.items() if v != 'None') - if compare(playval, dvalue): - item_found = True - if item_found: + evalue = dict((k, v) for k, v in evalue.items() if v != 'None') + for pkey, pvalue in playval.items(): + if compare(pvalue, evalue): + if playval_copy.get(pkey): + del playval_copy[pkey] + if not playval_copy: continue # Single Instance: 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 + playval = playval_copy # Multiple Instances: if isinstance(existing, dict): 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: 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. # pylint: disable=unnecessary-lambda diff --git a/lib/ansible/module_utils/network/nxos/utils/telemetry/__init__.py b/lib/ansible/module_utils/network/nxos/utils/telemetry/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/nxos/utils/telemetry/telemetry.py b/lib/ansible/module_utils/network/nxos/utils/telemetry/telemetry.py new file mode 100644 index 00000000000..d0b563e7edd --- /dev/null +++ b/lib/ansible/module_utils/network/nxos/utils/telemetry/telemetry.py @@ -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 under 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 under 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 keys under are specified as follows:" + msg += " destination: {ip: , port: , protocol: , encoding: }}" + module.fail_json(msg=msg) + + if type == 'sensor_groups': + if not playvals.get('id'): + msg = "Invalid playbook value: {0}.".format(playvals) + msg += " Parameter under 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 under requires 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 diff --git a/lib/ansible/modules/network/nxos/nxos_facts.py b/lib/ansible/modules/network/nxos/nxos_facts.py index b5965c4248c..a4239d64e53 100644 --- a/lib/ansible/modules/network/nxos/nxos_facts.py +++ b/lib/ansible/modules/network/nxos/nxos_facts.py @@ -57,7 +57,7 @@ options: to a given subset. Possible values for this argument include all and the resources like interfaces, vlans etc. Can specify a list of values to include a larger subset. - choices: ['all', 'lag_interfaces'] + choices: ['all', 'lag_interfaces', 'telemetry'] required: false version_added: "2.9" """ diff --git a/lib/ansible/modules/network/nxos/nxos_telemetry.py b/lib/ansible/modules/network/nxos/nxos_telemetry.py new file mode 100644 index 00000000000..62e122d6e03 --- /dev/null +++ b/lib/ansible/modules/network/nxos/nxos_telemetry.py @@ -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() diff --git a/test/integration/targets/nxos_telemetry/defaults/main.yaml b/test/integration/targets/nxos_telemetry/defaults/main.yaml new file mode 100644 index 00000000000..5f709c5aac1 --- /dev/null +++ b/test/integration/targets/nxos_telemetry/defaults/main.yaml @@ -0,0 +1,2 @@ +--- +testcase: "*" diff --git a/test/integration/targets/nxos_telemetry/meta/main.yml b/test/integration/targets/nxos_telemetry/meta/main.yml new file mode 100644 index 00000000000..ae741cbdc71 --- /dev/null +++ b/test/integration/targets/nxos_telemetry/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - prepare_nxos_tests diff --git a/test/integration/targets/nxos_telemetry/tasks/cli.yaml b/test/integration/targets/nxos_telemetry/tasks/cli.yaml new file mode 100644 index 00000000000..9b62eaba65e --- /dev/null +++ b/test/integration/targets/nxos_telemetry/tasks/cli.yaml @@ -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 diff --git a/test/integration/targets/nxos_telemetry/tasks/main.yaml b/test/integration/targets/nxos_telemetry/tasks/main.yaml new file mode 100644 index 00000000000..c070015619c --- /dev/null +++ b/test/integration/targets/nxos_telemetry/tasks/main.yaml @@ -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 diff --git a/test/integration/targets/nxos_telemetry/tasks/nxapi.yaml b/test/integration/targets/nxos_telemetry/tasks/nxapi.yaml new file mode 100644 index 00000000000..cbf41b92947 --- /dev/null +++ b/test/integration/targets/nxos_telemetry/tasks/nxapi.yaml @@ -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 diff --git a/test/integration/targets/nxos_telemetry/tests/common/deleted.yaml b/test/integration/targets/nxos_telemetry/tests/common/deleted.yaml new file mode 100644 index 00000000000..b8a70d547cd --- /dev/null +++ b/test/integration/targets/nxos_telemetry/tests/common/deleted.yaml @@ -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" diff --git a/test/integration/targets/nxos_telemetry/tests/common/merged.yaml b/test/integration/targets/nxos_telemetry/tests/common/merged.yaml new file mode 100644 index 00000000000..39ed3a8ca25 --- /dev/null +++ b/test/integration/targets/nxos_telemetry/tests/common/merged.yaml @@ -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" diff --git a/test/units/modules/network/nxos/fixtures/nxos_telemetry/N9K.cfg b/test/units/modules/network/nxos/fixtures/nxos_telemetry/N9K.cfg new file mode 100644 index 00000000000..697c2c18bfe --- /dev/null +++ b/test/units/modules/network/nxos/fixtures/nxos_telemetry/N9K.cfg @@ -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 diff --git a/test/units/modules/network/nxos/nxos_module.py b/test/units/modules/network/nxos/nxos_module.py index 7a9871be8c5..47871986eeb 100644 --- a/test/units/modules/network/nxos/nxos_module.py +++ b/test/units/modules/network/nxos/nxos_module.py @@ -26,8 +26,8 @@ from units.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase from units.modules.utils import set_module_args as _set_module_args -def set_module_args(args): - if 'provider' not in args: +def set_module_args(args, ignore_provider=None): + if 'provider' not in args and not ignore_provider: args['provider'] = {'transport': args.get('transport') or 'cli'} return _set_module_args(args) diff --git a/test/units/modules/network/nxos/test_nxos_telemetry.py b/test/units/modules/network/nxos/test_nxos_telemetry.py new file mode 100644 index 00000000000..4fedd27a84e --- /dev/null +++ b/test/units/modules/network/nxos/test_nxos_telemetry.py @@ -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 . + +# 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 under 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 under 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 under 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 under requires 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 ' 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