diff options
author | Bradley A. Thornton <bthornto@thethorntons.net> | 2019-08-09 07:37:28 -0700 |
---|---|---|
committer | Bradley A. Thornton <bthornto@thethorntons.net> | 2019-08-09 07:37:28 -0700 |
commit | 8a50be4b9309387624e92a154366b34fa512cd8b (patch) | |
tree | 27c001d596821591836fe519b4d97384e434eb1a /plugins/module_utils | |
parent | 00ae0cf5ce60cf6149e67f5c83356a2c1877a35f (diff) | |
download | vyos.vyos-8a50be4b9309387624e92a154366b34fa512cd8b.tar.gz vyos.vyos-8a50be4b9309387624e92a154366b34fa512cd8b.zip |
add subdirs
Diffstat (limited to 'plugins/module_utils')
22 files changed, 1314 insertions, 0 deletions
diff --git a/plugins/module_utils/network/vyos/argspec/__init__.py b/plugins/module_utils/network/vyos/argspec/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/plugins/module_utils/network/vyos/argspec/__init__.py diff --git a/plugins/module_utils/network/vyos/argspec/facts/__init__.py b/plugins/module_utils/network/vyos/argspec/facts/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/plugins/module_utils/network/vyos/argspec/facts/__init__.py diff --git a/plugins/module_utils/network/vyos/argspec/facts/facts.py b/plugins/module_utils/network/vyos/argspec/facts/facts.py new file mode 100644 index 0000000..456c8bd --- /dev/null +++ b/plugins/module_utils/network/vyos/argspec/facts/facts.py @@ -0,0 +1,31 @@ +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +The arg spec for the vyos facts module. +""" + + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class FactsArgs(object): # pylint: disable=R0903 + """ The arg spec for the vyos facts module + """ + + def __init__(self, **kwargs): + pass + + choices = [ + 'all', + 'interfaces', + '!interfaces', + 'l3_interfaces', + '!l3_interfaces' + ] + + argument_spec = { + 'gather_subset': dict(default=['!config'], type='list'), + 'gather_network_resources': dict(choices=choices, type='list'), + } diff --git a/plugins/module_utils/network/vyos/argspec/interfaces/__init__.py b/plugins/module_utils/network/vyos/argspec/interfaces/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/plugins/module_utils/network/vyos/argspec/interfaces/__init__.py diff --git a/plugins/module_utils/network/vyos/argspec/interfaces/interfaces.py b/plugins/module_utils/network/vyos/argspec/interfaces/interfaces.py new file mode 100644 index 0000000..d6ab446 --- /dev/null +++ b/plugins/module_utils/network/vyos/argspec/interfaces/interfaces.py @@ -0,0 +1,67 @@ +# Copyright 2019 Red Hat +# 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 vyos_interfaces module +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class InterfacesArgs(object): # pylint: disable=R0903 + """The arg spec for the vyos_interfaces module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = \ + { + 'config': { + 'elements': 'dict', + 'options': { + 'description': {'type': 'str'}, + 'duplex': {'choices': ['full', 'half', 'auto']}, + 'enabled': {'default': True, 'type': 'bool'}, + 'mtu': {'type': 'int'}, + 'name': {'required': True, 'type': 'str'}, + 'speed': {'choices': ['auto', '10', '100', '1000', '2500', + '10000'], + 'type': 'str'}, + 'vifs': { + 'elements': 'dict', + 'options': { + 'vlan_id': {'type': 'int'}, + 'description': {'type': 'str'}, + 'enabled': {'default': True, 'type': 'bool'}, + 'mtu': {'type': 'int'} + }, + 'type': 'list' + }, + }, + 'type': 'list' + }, + 'state': {'choices': ['merged', 'replaced', + 'overridden', 'deleted'], + 'default': 'merged', + 'type': 'str'} + } # pylint: disable=C0301 diff --git a/plugins/module_utils/network/vyos/argspec/l3_interfaces/__init__.py b/plugins/module_utils/network/vyos/argspec/l3_interfaces/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/plugins/module_utils/network/vyos/argspec/l3_interfaces/__init__.py diff --git a/plugins/module_utils/network/vyos/argspec/l3_interfaces/l3_interfaces.py b/plugins/module_utils/network/vyos/argspec/l3_interfaces/l3_interfaces.py new file mode 100644 index 0000000..e5785a8 --- /dev/null +++ b/plugins/module_utils/network/vyos/argspec/l3_interfaces/l3_interfaces.py @@ -0,0 +1,101 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# 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 vyos_l3_interfaces module +""" + + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class L3_interfacesArgs(object): # pylint: disable=R0903 + """The arg spec for the vyos_l3_interfaces module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = { + 'config': { + 'elements': 'dict', + 'options': { + 'ipv4': { + 'elements': 'dict', + 'options': { + 'address': { + 'type': 'str' + } + }, + 'type': 'list' + }, + 'ipv6': { + 'elements': 'dict', + 'options': { + 'address': { + 'type': 'str' + } + }, + 'type': 'list' + }, + 'name': { + 'required': True, + 'type': 'str' + }, + 'vifs': { + 'elements': 'dict', + 'options': { + 'ipv4': { + 'elements': 'dict', + 'options': { + 'address': { + 'type': 'str' + } + }, + 'type': 'list' + }, + 'ipv6': { + 'elements': 'dict', + 'options': { + 'address': { + 'type': 'str' + } + }, + 'type': 'list' + }, + 'vlan_id': { + 'type': 'int' + } + }, + 'type': 'list' + } + }, + 'type': 'list' + }, + 'state': { + 'choices': ['merged', 'replaced', 'overridden', 'deleted'], + 'default': 'merged', + 'type': 'str' + } + } # pylint: disable=C0301 diff --git a/plugins/module_utils/network/vyos/config/__init__.py b/plugins/module_utils/network/vyos/config/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/plugins/module_utils/network/vyos/config/__init__.py diff --git a/plugins/module_utils/network/vyos/config/interfaces/__init__.py b/plugins/module_utils/network/vyos/config/interfaces/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/plugins/module_utils/network/vyos/config/interfaces/__init__.py diff --git a/plugins/module_utils/network/vyos/config/interfaces/interfaces.py b/plugins/module_utils/network/vyos/config/interfaces/interfaces.py new file mode 100644 index 0000000..b17971c --- /dev/null +++ b/plugins/module_utils/network/vyos/config/interfaces/interfaces.py @@ -0,0 +1,283 @@ +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +The vyos_interfaces 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 + + +from copy import deepcopy +from ansible.module_utils.network.common.cfg.base import ConfigBase +from ansible.module_utils.network.common.utils import to_list, dict_diff, remove_empties +from ansible.module_utils.six import iteritems +from ansible_collections.vyos.vyos.plugins.module_utils.network. \ + vyos.facts.facts import Facts + +from ansible_collections.vyos.vyos.plugins.module_utils.network. \ + vyos.utils.utils import search_obj_in_list, get_interface_type, dict_delete + + + +class Interfaces(ConfigBase): + """ + The vyos_interfaces class + """ + + gather_subset = [ + '!all', + '!min', + ] + + gather_network_resources = [ + 'interfaces' + ] + + def __init__(self, module): + super(Interfaces, self).__init__(module) + + def get_interfaces_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) + interfaces_facts = facts['ansible_network_resources'].get('interfaces') + if not interfaces_facts: + return [] + return interfaces_facts + + def execute_module(self): + """ Execute the module + :rtype: A dictionary + :returns: The result from module execution + """ + result = {'changed': False} + commands = list() + warnings = list() + + existing_interfaces_facts = self.get_interfaces_facts() + commands.extend(self.set_config(existing_interfaces_facts)) + if commands: + if self._module.check_mode: + resp = self._connection.edit_config(commands, commit=False) + else: + resp = self._connection.edit_config(commands) + result['changed'] = True + + result['commands'] = commands + + if self._module._diff: + result['diff'] = resp['diff'] if result['changed'] else None + + changed_interfaces_facts = self.get_interfaces_facts() + + result['before'] = existing_interfaces_facts + if result['changed']: + result['after'] = changed_interfaces_facts + + result['warnings'] = warnings + return result + + def set_config(self, existing_interfaces_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 + """ + want = self._module.params['config'] + have = existing_interfaces_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 + """ + commands = [] + state = self._module.params['state'] + if state == 'overridden': + commands.extend(self._state_overridden(want=want, have=have)) + + elif state == 'deleted': + if not want: + for intf in have: + commands.extend( + self._state_deleted( + {'name': intf['name']}, + intf + ) + ) + else: + for item in want: + obj_in_have = search_obj_in_list(item['name'], have) + commands.extend( + self._state_deleted( + item, obj_in_have + ) + ) + else: + for item in want: + name = item['name'] + obj_in_have = search_obj_in_list(name, have) + + if not obj_in_have: + obj_in_have = {'name': item['name']} + + elif state == 'merged': + commands.extend( + self._state_merged( + item, obj_in_have + ) + ) + + elif state == 'replaced': + commands.extend( + self._state_replaced( + item, obj_in_have + ) + ) + + return commands + + def _state_replaced(self, want, have): + """ The command generator when state is replaced + + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + commands = [] + if have: + commands.extend(self._state_deleted(want, have)) + + commands.extend(self._state_merged(want, have)) + + return commands + + def _state_overridden(self, want, have): + """ The command generator when state is overridden + + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + commands = [] + + for intf in have: + intf_in_want = search_obj_in_list(intf['name'], want) + if not intf_in_want: + commands.extend(self._state_deleted({'name': intf['name']}, intf)) + + for intf in want: + intf_in_have = search_obj_in_list(intf['name'], have) + commands.extend(self._state_replaced(intf, intf_in_have)) + + return commands + + def _state_merged(self, want, have): + """ The command generator when state is merged + + :rtype: A list + :returns: the commands necessary to merge the provided into + the current configuration + """ + commands = [] + want_copy = deepcopy(remove_empties(want)) + have_copy = deepcopy(have) + + want_vifs = want_copy.pop('vifs', []) + have_vifs = have_copy.pop('vifs', []) + + updates = dict_diff(have_copy, want_copy) + + if updates: + for key, value in iteritems(updates): + commands.append(self._compute_commands(key=key, value=value, interface=want_copy['name'])) + + if want_vifs: + for want_vif in want_vifs: + have_vif = search_obj_in_list(want_vif['vlan_id'], have_vifs, key='vlan_id') + if not have_vif: + have_vif = {'vlan_id': want_vif['vlan_id'], 'enabled': True} + + vif_updates = dict_diff(have_vif, want_vif) + if vif_updates: + for key, value in iteritems(vif_updates): + commands.append(self._compute_commands(key=key, value=value, interface=want_copy['name'], vif=want_vif['vlan_id'])) + + return commands + + def _state_deleted(self, 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 = [] + + want_copy = deepcopy(remove_empties(want)) + have_copy = deepcopy(have) + + want_vifs = want_copy.pop('vifs', []) + have_vifs = have_copy.pop('vifs', []) + + for key in dict_delete(have_copy, want_copy).keys(): + if key == 'enabled': + continue + commands.append(self._compute_commands(key=key, interface=want_copy['name'], remove=True)) + if have_copy['enabled'] is False: + commands.append(self._compute_commands(key='enabled', value=True, interface=want_copy['name'])) + + if have_vifs: + for have_vif in have_vifs: + want_vif = search_obj_in_list(have_vif['vlan_id'], want_vifs, key='vlan_id') + if not want_vif: + want_vif = {'vlan_id': have_vif['vlan_id'], 'enabled': True} + + for key in dict_delete(have_vif, want_vif).keys(): + if key == 'enabled': + continue + commands.append(self._compute_commands(key=key, interface=want_copy['name'], vif=want_vif['vlan_id'], remove=True)) + if have_vif['enabled'] is False: + commands.append(self._compute_commands(key='enabled', value=True, interface=want_copy['name'], vif=want_vif['vlan_id'])) + + return commands + + def _compute_commands(self, interface, key, vif=None, value=None, remove=False): + intf_context = 'interfaces {0} {1}'.format(get_interface_type(interface), interface) + set_cmd = 'set {0}'.format(intf_context) + del_cmd = 'delete {0}'.format(intf_context) + + if vif: + set_cmd = set_cmd + (' vif {0}'.format(vif)) + del_cmd = del_cmd + (' vif {0}'.format(vif)) + + if key == 'enabled': + if not value: + command = "{0} disable".format(set_cmd) + else: + command = "{0} disable".format(del_cmd) + else: + if not remove: + command = "{0} {1} '{2}'".format(set_cmd, key, value) + else: + command = "{0} {1}".format(del_cmd, key) + + return command diff --git a/plugins/module_utils/network/vyos/config/l3_interfaces/__init__.py b/plugins/module_utils/network/vyos/config/l3_interfaces/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/plugins/module_utils/network/vyos/config/l3_interfaces/__init__.py diff --git a/plugins/module_utils/network/vyos/config/l3_interfaces/l3_interfaces.py b/plugins/module_utils/network/vyos/config/l3_interfaces/l3_interfaces.py new file mode 100644 index 0000000..9027c84 --- /dev/null +++ b/plugins/module_utils/network/vyos/config/l3_interfaces/l3_interfaces.py @@ -0,0 +1,277 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +The vyos_l3_interfaces 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 + + +from copy import deepcopy +from ansible.module_utils.network.common.cfg.base import ConfigBase +from ansible.module_utils.network.common.utils import to_list, remove_empties +from ansible.module_utils.six import iteritems +from ansible_collections.vyos.vyos.plugins.module_utils.network. \ + vyos.facts.facts import Facts + +from ansible_collections.vyos.vyos.plugins.module_utils.network. \ + vyos.utils.utils import search_obj_in_list, get_interface_type, diff_list_of_dicts + + + +class L3_interfaces(ConfigBase): + """ + The vyos_l3_interfaces class + """ + + gather_subset = [ + '!all', + '!min', + ] + + gather_network_resources = [ + 'l3_interfaces', + ] + + def __init__(self, module): + super(L3_interfaces, self).__init__(module) + + def get_l3_interfaces_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) + l3_interfaces_facts = facts['ansible_network_resources'].get('l3_interfaces') + if not l3_interfaces_facts: + return [] + return l3_interfaces_facts + + def execute_module(self): + """ Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + result = {'changed': False} + warnings = list() + commands = list() + + existing_l3_interfaces_facts = self.get_l3_interfaces_facts() + commands.extend(self.set_config(existing_l3_interfaces_facts)) + if commands: + if self._module.check_mode: + resp = self._connection.edit_config(commands, commit=False) + else: + resp = self._connection.edit_config(commands) + result['changed'] = True + + result['commands'] = commands + + if self._module._diff: + result['diff'] = resp['diff'] if result['changed'] else None + + changed_l3_interfaces_facts = self.get_l3_interfaces_facts() + + result['before'] = existing_l3_interfaces_facts + if result['changed']: + result['after'] = changed_l3_interfaces_facts + + result['warnings'] = warnings + return result + + def set_config(self, existing_l3_interfaces_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 + """ + want = self._module.params['config'] + have = existing_l3_interfaces_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 + """ + commands = [] + state = self._module.params['state'] + if state == 'overridden': + commands.extend(self._state_overridden(want=want, have=have)) + + elif state == 'deleted': + if not want: + for intf in have: + commands.extend( + self._state_deleted( + {'name': intf['name']}, + intf + ) + ) + else: + for item in want: + obj_in_have = search_obj_in_list(item['name'], have) + commands.extend( + self._state_deleted( + item, obj_in_have + ) + ) + else: + for item in want: + name = item['name'] + obj_in_have = search_obj_in_list(name, have) + + if not obj_in_have: + obj_in_have = {'name': item['name']} + + if state == 'merged': + commands.extend( + self._state_merged( + item, obj_in_have + ) + ) + + elif state == 'replaced': + commands.extend( + self._state_replaced( + item, obj_in_have + ) + ) + + return commands + + def _state_replaced(self, want, have): + """ The command generator when state is replaced + + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + commands = [] + if have: + commands.extend(self._state_deleted(want, have)) + + commands.extend(self._state_merged(want, have)) + + return commands + + def _state_overridden(self, want, have): + """ The command generator when state is overridden + + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + commands = [] + + for intf in have: + intf_in_want = search_obj_in_list(intf['name'], want) + if not intf_in_want: + commands.extend(self._state_deleted({'name': intf['name']}, intf)) + + for intf in want: + intf_in_have = search_obj_in_list(intf['name'], have) + commands.extend(self._state_replaced(intf, intf_in_have)) + + return commands + + def _state_merged(self, want, have): + """ The command generator when state is merged + + :rtype: A list + :returns: the commands necessary to merge the provided into + the current configuration + """ + commands = [] + want_copy = deepcopy(remove_empties(want)) + have_copy = deepcopy(remove_empties(have)) + + want_vifs = want_copy.pop('vifs', []) + have_vifs = have_copy.pop('vifs', []) + + for update in self._get_updates(want_copy, have_copy): + for key, value in iteritems(update): + commands.append(self._compute_commands(key=key, value=value, interface=want_copy['name'])) + + if want_vifs: + for want_vif in want_vifs: + have_vif = search_obj_in_list(want_vif['vlan_id'], have_vifs, key='vlan_id') + if not have_vif: + have_vif = {} + + for update in self._get_updates(want_vif, have_vif): + for key, value in iteritems(update): + commands.append(self._compute_commands(key=key, value=value, interface=want_copy['name'], vif=want_vif['vlan_id'])) + + return commands + + def _state_deleted(self, 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 = [] + want_copy = deepcopy(remove_empties(want)) + have_copy = deepcopy(have) + + want_vifs = want_copy.pop('vifs', []) + have_vifs = have_copy.pop('vifs', []) + + for update in self._get_updates(have_copy, want_copy): + for key, value in iteritems(update): + commands.append(self._compute_commands(key=key, value=value, interface=want_copy['name'], remove=True)) + + if have_vifs: + for have_vif in have_vifs: + want_vif = search_obj_in_list(have_vif['vlan_id'], want_vifs, key='vlan_id') + if not want_vif: + want_vif = {'vlan_id': have_vif['vlan_id']} + + for update in self._get_updates(have_vif, want_vif): + for key, value in iteritems(update): + commands.append(self._compute_commands(key=key, interface=want_copy['name'], value=value, vif=want_vif['vlan_id'], remove=True)) + + return commands + + def _compute_commands(self, interface, key, vif=None, value=None, remove=False): + intf_context = 'interfaces {0} {1}'.format(get_interface_type(interface), interface) + set_cmd = 'set {0}'.format(intf_context) + del_cmd = 'delete {0}'.format(intf_context) + + if vif: + set_cmd = set_cmd + (' vif {0}'.format(vif)) + del_cmd = del_cmd + (' vif {0}'.format(vif)) + + if remove: + command = "{0} {1} '{2}'".format(del_cmd, key, value) + else: + command = "{0} {1} '{2}'".format(set_cmd, key, value) + + return command + + def _get_updates(self, want, have): + updates = [] + + updates = diff_list_of_dicts(want.get('ipv4', []), have.get('ipv4', [])) + updates.extend(diff_list_of_dicts(want.get('ipv6', []), have.get('ipv6', []))) + + return updates diff --git a/plugins/module_utils/network/vyos/facts/__init__.py b/plugins/module_utils/network/vyos/facts/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/plugins/module_utils/network/vyos/facts/__init__.py diff --git a/plugins/module_utils/network/vyos/facts/facts.py b/plugins/module_utils/network/vyos/facts/facts.py new file mode 100644 index 0000000..a065eaf --- /dev/null +++ b/plugins/module_utils/network/vyos/facts/facts.py @@ -0,0 +1,69 @@ +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +The facts class for vyos +this file validates each subset of facts and selectively +calls the appropriate facts gathering function +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +from ansible_collections.vyos.vyos.plugins.module_utils.network. \ + vyos.argspec.facts.facts import FactsArgs + +from ansible.module_utils.network.common.facts.facts import FactsBase +from ansible_collections.vyos.vyos.plugins.module_utils.network. \ + vyos.facts.interfaces.interfaces import InterfacesFacts + +from ansible_collections.vyos.vyos.plugins.module_utils.network. \ + vyos.facts.l3_interfaces.l3_interfaces import L3_interfacesFacts + +from ansible_collections.vyos.vyos.plugins.module_utils.network. \ + vyos.facts.legacy.base import Default, Neighbors, Config + +from ansible.module_utils. \ + network.vyos.vyos import run_commands, get_capabilities + + +FACT_LEGACY_SUBSETS = dict( + default=Default, + neighbors=Neighbors, + config=Config +) +FACT_RESOURCE_SUBSETS = dict( + interfaces=InterfacesFacts, + l3_interfaces=L3_interfacesFacts +) + + +class Facts(FactsBase): + """ The fact class for vyos + """ + + VALID_LEGACY_GATHER_SUBSETS = frozenset(FACT_LEGACY_SUBSETS.keys()) + VALID_RESOURCE_SUBSETS = frozenset(FACT_RESOURCE_SUBSETS.keys()) + + def __init__(self, module): + super(Facts, self).__init__(module) + + def get_facts(self, legacy_facts_type=None, resource_facts_type=None, data=None): + """ Collect the facts for vyos + + :param legacy_facts_type: List of legacy facts types + :param resource_facts_type: List of resource fact types + :param data: previously collected conf + :rtype: dict + :return: the facts gathered + """ + netres_choices = FactsArgs.argument_spec['gather_network_resources'].get('choices', []) + if self.VALID_RESOURCE_SUBSETS: + self.get_network_resources_facts(netres_choices, FACT_RESOURCE_SUBSETS, + resource_facts_type, data) + + if self.VALID_LEGACY_GATHER_SUBSETS: + self.get_network_legacy_facts(FACT_LEGACY_SUBSETS, legacy_facts_type) + + return self.ansible_facts, self._warnings diff --git a/plugins/module_utils/network/vyos/facts/interfaces/__init__.py b/plugins/module_utils/network/vyos/facts/interfaces/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/plugins/module_utils/network/vyos/facts/interfaces/__init__.py diff --git a/plugins/module_utils/network/vyos/facts/interfaces/interfaces.py b/plugins/module_utils/network/vyos/facts/interfaces/interfaces.py new file mode 100644 index 0000000..cc89e4f --- /dev/null +++ b/plugins/module_utils/network/vyos/facts/interfaces/interfaces.py @@ -0,0 +1,125 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +The vyos interfaces 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 + + +from re import findall, M +from copy import deepcopy +from ansible.module_utils.network.common import utils +from ansible_collections.vyos.vyos.plugins.module_utils.network. \ + vyos.argspec.interfaces.interfaces import InterfacesArgs + + + +class InterfacesFacts(object): + """ The vyos interfaces fact class + """ + + def __init__(self, module, subspec='config', options='options'): + self._module = module + self.argument_spec = InterfacesArgs.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 interfaces + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + :rtype: dictionary + :returns: facts + """ + if not data: + data = connection.get_config(flags=['| grep interfaces']) + + objs = [] + interface_names = findall(r'^set interfaces (?:ethernet|bonding|vti|loopback|vxlan) (?:\'*)(\S+)(?:\'*)', + data, M) + if interface_names: + for interface in set(interface_names): + intf_regex = r' %s .+$' % interface.strip("'") + cfg = findall(intf_regex, data, M) + obj = self.render_config(cfg) + obj['name'] = interface.strip("'") + if obj: + objs.append(obj) + facts = {} + if objs: + facts['interfaces'] = [] + params = utils.validate_config(self.argument_spec, {'config': objs}) + for cfg in params['config']: + facts['interfaces'].append(utils.remove_empties(cfg)) + + ansible_facts['ansible_network_resources'].update(facts) + return ansible_facts + + def render_config(self, conf): + """ + 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 + """ + vif_conf = '\n'.join(filter(lambda x: ('vif' in x), conf)) + eth_conf = '\n'.join(filter(lambda x: ('vif' not in x), conf)) + config = self.parse_attribs( + ['description', 'speed', 'mtu', 'duplex'], eth_conf) + config['vifs'] = self.parse_vifs(vif_conf) + + return utils.remove_empties(config) + + def parse_vifs(self, conf): + vif_names = findall(r'vif (?:\'*)(\d+)(?:\'*)', conf, M) + vifs_list = None + + if vif_names: + vifs_list = [] + for vif in set(vif_names): + vif_regex = r' %s .+$' % vif + cfg = '\n'.join(findall(vif_regex, conf, M)) + obj = self.parse_attribs(['description', 'mtu'], cfg) + obj['vlan_id'] = int(vif) + if obj: + vifs_list.append(obj) + vifs_list = sorted(vifs_list, key=lambda i: i['vlan_id']) + + return vifs_list + + def parse_attribs(self, attribs, conf): + config = {} + for item in attribs: + value = utils.parse_conf_arg(conf, item) + if value and item == 'mtu': + config[item] = int(value.strip("'")) + elif value: + config[item] = value.strip("'") + else: + config[item] = None + if 'disable' in conf: + config['enabled'] = False + else: + config['enabled'] = True + + return utils.remove_empties(config) diff --git a/plugins/module_utils/network/vyos/facts/l3_interfaces/__init__.py b/plugins/module_utils/network/vyos/facts/l3_interfaces/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/plugins/module_utils/network/vyos/facts/l3_interfaces/__init__.py diff --git a/plugins/module_utils/network/vyos/facts/l3_interfaces/l3_interfaces.py b/plugins/module_utils/network/vyos/facts/l3_interfaces/l3_interfaces.py new file mode 100644 index 0000000..61f635b --- /dev/null +++ b/plugins/module_utils/network/vyos/facts/l3_interfaces/l3_interfaces.py @@ -0,0 +1,132 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +The vyos l3_interfaces 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.six import iteritems +from ansible.module_utils.compat import ipaddress +from ansible_collections.vyos.vyos.plugins.module_utils.network. \ + vyos.argspec.l3_interfaces.l3_interfaces import L3_interfacesArgs + + + +class L3_interfacesFacts(object): + """ The vyos l3_interfaces fact class + """ + + def __init__(self, module, subspec='config', options='options'): + self._module = module + self.argument_spec = L3_interfacesArgs.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 l3_interfaces + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + :rtype: dictionary + :returns: facts + """ + if not data: + data = connection.get_config() + + # operate on a collection of resource x + objs = [] + interface_names = re.findall(r'set interfaces (?:ethernet|bonding|vti|vxlan) (?:\'*)(\S+)(?:\'*)', data, re.M) + if interface_names: + for interface in set(interface_names): + intf_regex = r' %s .+$' % interface + cfg = re.findall(intf_regex, data, re.M) + obj = self.render_config(cfg) + obj['name'] = interface.strip("'") + if obj: + objs.append(obj) + + ansible_facts['ansible_network_resources'].pop('l3_interfaces', None) + facts = {} + if objs: + facts['l3_interfaces'] = [] + params = utils.validate_config(self.argument_spec, {'config': objs}) + for cfg in params['config']: + facts['l3_interfaces'].append(utils.remove_empties(cfg)) + + ansible_facts['ansible_network_resources'].update(facts) + return ansible_facts + + def render_config(self, conf): + """ + 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 + """ + vif_conf = '\n'.join(filter(lambda x: ('vif' in x), conf)) + eth_conf = '\n'.join(filter(lambda x: ('vif' not in x), conf)) + config = self.parse_attribs(eth_conf) + config['vifs'] = self.parse_vifs(vif_conf) + + return utils.remove_empties(config) + + def parse_vifs(self, conf): + vif_names = re.findall(r'vif (\d+)', conf, re.M) + vifs_list = None + if vif_names: + vifs_list = [] + for vif in set(vif_names): + vif_regex = r' %s .+$' % vif + cfg = '\n'.join(re.findall(vif_regex, conf, re.M)) + obj = self.parse_attribs(cfg) + obj['vlan_id'] = vif + if obj: + vifs_list.append(obj) + + return vifs_list + + def parse_attribs(self, conf): + config = {} + ipaddrs = re.findall(r'address (\S+)', conf, re.M) + config['ipv4'] = [] + config['ipv6'] = [] + + for item in ipaddrs: + item = item.strip("'") + if item == 'dhcp': + config['ipv4'].append({'address': item}) + elif item == 'dhcpv6': + config['ipv6'].append({'address': item}) + else: + ip_version = ipaddress.ip_address(item.split("/")[0]).version + if ip_version == 4: + config['ipv4'].append({'address': item}) + else: + config['ipv6'].append({'address': item}) + + for key, value in iteritems(config): + if value == []: + config[key] = None + + return utils.remove_empties(config) diff --git a/plugins/module_utils/network/vyos/facts/legacy/__init__.py b/plugins/module_utils/network/vyos/facts/legacy/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/plugins/module_utils/network/vyos/facts/legacy/__init__.py diff --git a/plugins/module_utils/network/vyos/facts/legacy/base.py b/plugins/module_utils/network/vyos/facts/legacy/base.py new file mode 100644 index 0000000..cdf0cce --- /dev/null +++ b/plugins/module_utils/network/vyos/facts/legacy/base.py @@ -0,0 +1,161 @@ +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +The VyOS interfaces 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 platform +import re +from ansible.module_utils. \ + network.vyos.vyos import run_commands, get_capabilities + + +class LegacyFactsBase(object): + + COMMANDS = frozenset() + + def __init__(self, module): + self.module = module + self.facts = dict() + self.warnings = list() + self.responses = None + + def populate(self): + self.responses = run_commands(self.module, list(self.COMMANDS)) + + +class Default(LegacyFactsBase): + + COMMANDS = [ + 'show version', + ] + + def populate(self): + super(Default, self).populate() + data = self.responses[0] + self.facts['serialnum'] = self.parse_serialnum(data) + self.facts.update(self.platform_facts()) + + def parse_serialnum(self, data): + match = re.search(r'HW S/N:\s+(\S+)', data) + if match: + return match.group(1) + + def platform_facts(self): + platform_facts = {} + + resp = get_capabilities(self.module) + device_info = resp['device_info'] + + platform_facts['system'] = device_info['network_os'] + + for item in ('model', 'image', 'version', 'platform', 'hostname'): + val = device_info.get('network_os_%s' % item) + if val: + platform_facts[item] = val + + platform_facts['api'] = resp['network_api'] + platform_facts['python_version'] = platform.python_version() + + return platform_facts + + +class Config(LegacyFactsBase): + + COMMANDS = [ + 'show configuration commands', + 'show system commit', + ] + + def populate(self): + super(Config, self).populate() + + self.facts['config'] = self.responses + + commits = self.responses[1] + entries = list() + entry = None + + for line in commits.split('\n'): + match = re.match(r'(\d+)\s+(.+)by(.+)via(.+)', line) + if match: + if entry: + entries.append(entry) + + entry = dict(revision=match.group(1), + datetime=match.group(2), + by=str(match.group(3)).strip(), + via=str(match.group(4)).strip(), + comment=None) + else: + entry['comment'] = line.strip() + + self.facts['commits'] = entries + + +class Neighbors(LegacyFactsBase): + + COMMANDS = [ + 'show lldp neighbors', + 'show lldp neighbors detail', + ] + + def populate(self): + super(Neighbors, self).populate() + + all_neighbors = self.responses[0] + if 'LLDP not configured' not in all_neighbors: + neighbors = self.parse( + self.responses[1] + ) + self.facts['neighbors'] = self.parse_neighbors(neighbors) + + def parse(self, data): + parsed = list() + values = None + for line in data.split('\n'): + if not line: + continue + elif line[0] == ' ': + values += '\n%s' % line + elif line.startswith('Interface'): + if values: + parsed.append(values) + values = line + if values: + parsed.append(values) + return parsed + + def parse_neighbors(self, data): + facts = dict() + for item in data: + interface = self.parse_interface(item) + host = self.parse_host(item) + port = self.parse_port(item) + if interface not in facts: + facts[interface] = list() + facts[interface].append(dict(host=host, port=port)) + return facts + + def parse_interface(self, data): + match = re.search(r'^Interface:\s+(\S+),', data) + return match.group(1) + + def parse_host(self, data): + match = re.search(r'SysName:\s+(.+)$', data, re.M) + if match: + return match.group(1) + + def parse_port(self, data): + match = re.search(r'PortDescr:\s+(.+)$', data, re.M) + if match: + return match.group(1) diff --git a/plugins/module_utils/network/vyos/utils/__init__.py b/plugins/module_utils/network/vyos/utils/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/plugins/module_utils/network/vyos/utils/__init__.py diff --git a/plugins/module_utils/network/vyos/utils/utils.py b/plugins/module_utils/network/vyos/utils/utils.py new file mode 100644 index 0000000..f59c24f --- /dev/null +++ b/plugins/module_utils/network/vyos/utils/utils.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +# utils + + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +def search_obj_in_list(name, lst, key='name'): + for item in lst: + if item[key] == name: + return item + return None + + +def get_interface_type(interface): + """Gets the type of interface + """ + if interface.startswith('eth'): + return 'ethernet' + elif interface.startswith('bond'): + return 'bonding' + elif interface.startswith('vti'): + return 'vti' + elif interface.startswith('lo'): + return 'loopback' + + +def dict_delete(base, comparable): + """ + This function generates a dict containing key, value pairs for keys + that are present in the `base` dict but not present in the `comparable` + dict. + + :param base: dict object to base the diff on + :param comparable: dict object to compare against base + :returns: new dict object with key, value pairs that needs to be deleted. + + """ + to_delete = dict() + + for key in base: + if isinstance(base[key], dict): + sub_diff = dict_delete(base[key], comparable.get(key, {})) + if sub_diff: + to_delete[key] = sub_diff + else: + if key not in comparable: + to_delete[key] = base[key] + + return to_delete + + +def diff_list_of_dicts(want, have): + diff = [] + + set_w = set(tuple(d.items()) for d in want) + set_h = set(tuple(d.items()) for d in have) + difference = set_w.difference(set_h) + + for element in difference: + diff.append(dict((x, y) for x, y in element)) + + return diff |