diff options
Diffstat (limited to 'plugins/module_utils/network')
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 00000000..e69de29b --- /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 00000000..e69de29b --- /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 00000000..456c8bd1 --- /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 00000000..e69de29b --- /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 00000000..d6ab4465 --- /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 00000000..e69de29b --- /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 00000000..e5785a83 --- /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 00000000..e69de29b --- /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 00000000..e69de29b --- /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 00000000..b17971cc --- /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 00000000..e69de29b --- /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 00000000..9027c849 --- /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 00000000..e69de29b --- /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 00000000..a065eaf0 --- /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 00000000..e69de29b --- /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 00000000..cc89e4ff --- /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 00000000..e69de29b --- /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 00000000..61f635b5 --- /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 00000000..e69de29b --- /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 00000000..cdf0cceb --- /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 00000000..e69de29b --- /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 00000000..f59c24ff --- /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 | 
