Add vyos_interfaces resource module (#58589)

Signed-off-by: NilashishC <nilashishchakraborty8@gmail.com>
pull/59938/head
Nilashish Chakraborty 5 years ago committed by GitHub
parent 288d74ca48
commit 730fa78ca4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -64,6 +64,8 @@ The following modules will be removed in Ansible 2.13. Please update update your
* nxos_linkagg use :ref:`nxos_lag_interfaces <nxos_lag_interfaces_module>` instead.
* vyos_interface use :ref:`vyos_interfaces <vyos_interfaces_module>` instead.
Noteworthy module changes
-------------------------

@ -0,0 +1,29 @@
# 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'
]
argument_spec = {
'gather_subset': dict(default=['!config'], type='list'),
'gather_network_resources': dict(choices=choices, type='list'),
}

@ -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

@ -0,0 +1,280 @@
# 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.module_utils.network.vyos.facts.facts import Facts
from ansible.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

@ -0,0 +1,59 @@
# 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.module_utils.network.vyos.argspec.facts.facts import FactsArgs
from ansible.module_utils.network.common.facts.facts import FactsBase
from ansible.module_utils.network.vyos.facts.interfaces.interfaces import InterfacesFacts
from ansible.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,
)
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

@ -0,0 +1,123 @@
#
# -*- 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.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)

@ -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)

@ -0,0 +1,55 @@
# -*- 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

@ -20,7 +20,7 @@
#
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'status': ['deprecated'],
'supported_by': 'network'}
@ -33,6 +33,10 @@ short_description: Manage Interface on VyOS network devices
description:
- This module provides declarative management of Interfaces
on VyOS network devices.
deprecated:
removed_in: '2.13'
alternative: vyos_interfaces
why: Updated modules released with more functionality.
notes:
- Tested against VYOS 1.1.7
options:

@ -1,65 +1,90 @@
#!/usr/bin/python
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
#
# -*- 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 module file for vyos_facts
"""
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'status': [u'preview'],
'supported_by': 'network'}
DOCUMENTATION = """
---
module: vyos_facts
version_added: "2.2"
author: "Nathaniel Case (@Qalthos)"
short_description: Collect facts from remote devices running VyOS
version_added: 2.2
short_description: Get facts about vyos devices.
description:
- Collects a base set of device facts from a remote device that
is running VyOS. This module prepends all of the
base network fact keys with U(ansible_net_<fact>). The facts
module will always collect a base set of facts from the device
and can enable or disable collection of additional facts.
- Collects facts from network devices running the vyos operating
system. This module places the facts gathered in the fact tree keyed by the
respective resource name. The facts module will always collect a
base set of facts from the device and can enable or disable
collection of additional facts.
author:
- Nathaniel Case (@qalthos)
- Nilashish Chakraborty (@Nilashishc)
extends_documentation_fragment: vyos
notes:
- Tested against VYOS 1.1.7
- Tested against VyOS 1.1.8
options:
gather_subset:
description:
- When supplied, this argument will restrict the facts collected
to a given subset. Possible values for this argument include
all, default, config, and neighbors. Can specify a list of
values to include a larger subset. Values can also be used
all, default, config, and neighbors. Can specify a list of
values to include a larger subset. Values can also be used
with an initial C(M(!)) to specify that a specific subset should
not be collected.
required: false
default: "!config"
gather_network_resources:
description:
- When supplied, this argument will restrict the facts collected
to a given subset. Possible values for this argument include
all and the resources like interfaces.
Can specify a list of values to include a larger subset. Values
can also be used with an initial C(M(!)) to specify that a
specific subset should not be collected.
required: false
version_added: "2.9"
choices: ['all', 'interfaces', '!interfaces']
"""
EXAMPLES = """
- name: collect all facts from the device
vyos_facts:
# Gather all facts
- vyos_facts:
gather_subset: all
gather_network_resources: all
- name: collect only the config and default facts
vyos_facts:
# collect only the config and default facts
- vyos_facts:
gather_subset: config
- name: collect everything exception the config
vyos_facts:
# collect everything exception the config
- vyos_facts:
gather_subset: "!config"
# Collect only the interfaces facts
- vyos_facts:
gather_subset:
- '!all'
- '!min'
gather_network_resources:
- interfaces
# Do not collect interfaces facts
- vyos_facts:
gather_network_resources:
- "!interfaces"
# Collect interfaces and minimal default facts
- vyos_facts:
gather_subset: min
gather_network_resources: interfaces
"""
RETURN = """
@ -103,228 +128,38 @@ ansible_net_python_version:
description: The Python version Ansible controller is using
returned: always
type: str
ansible_net_gather_network_resources:
description: The list of fact resource subsets collected from the device
returned: always
type: list
"""
import platform
import re
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.six import iteritems
from ansible.module_utils.network.vyos.vyos import run_commands, get_capabilities
from ansible.module_utils.network.vyos.argspec.facts.facts import FactsArgs
from ansible.module_utils.network.vyos.facts.facts import Facts
from ansible.module_utils.network.vyos.vyos import vyos_argument_spec
class FactsBase(object):
COMMANDS = frozenset()
def __init__(self, module):
self.module = module
self.facts = dict()
self.responses = None
def populate(self):
self.responses = run_commands(self.module, list(self.COMMANDS))
class Default(FactsBase):
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(FactsBase):
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(FactsBase):
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)
FACT_SUBSETS = dict(
default=Default,
neighbors=Neighbors,
config=Config
)
VALID_SUBSETS = frozenset(FACT_SUBSETS.keys())
def main():
argument_spec = dict(
gather_subset=dict(default=['!config'], type='list')
)
"""
Main entry point for module execution
:returns: ansible_facts
"""
argument_spec = FactsArgs.argument_spec
argument_spec.update(vyos_argument_spec)
module = AnsibleModule(argument_spec=argument_spec,
supports_check_mode=True)
warnings = list()
gather_subset = module.params['gather_subset']
runable_subsets = set()
exclude_subsets = set()
for subset in gather_subset:
if subset == 'all':
runable_subsets.update(VALID_SUBSETS)
continue
if subset.startswith('!'):
subset = subset[1:]
if subset == 'all':
exclude_subsets.update(VALID_SUBSETS)
continue
exclude = True
else:
exclude = False
if subset not in VALID_SUBSETS:
module.fail_json(msg='Subset must be one of [%s], got %s' %
(', '.join(VALID_SUBSETS), subset))
if exclude:
exclude_subsets.add(subset)
else:
runable_subsets.add(subset)
if not runable_subsets:
runable_subsets.update(VALID_SUBSETS)
runable_subsets.difference_update(exclude_subsets)
runable_subsets.add('default')
facts = dict()
facts['gather_subset'] = list(runable_subsets)
instances = list()
for key in runable_subsets:
instances.append(FACT_SUBSETS[key](module))
warnings = ['default value for `gather_subset` '
'will be changed to `min` from `!config` v2.11 onwards']
for inst in instances:
inst.populate()
facts.update(inst.facts)
result = Facts(module).get_facts()
ansible_facts = dict()
for key, value in iteritems(facts):
key = 'ansible_net_%s' % key
ansible_facts[key] = value
ansible_facts, additional_warnings = result
warnings.extend(additional_warnings)
module.exit_json(ansible_facts=ansible_facts, warnings=warnings)

@ -0,0 +1,876 @@
#!/usr/bin/python
# -*- 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 module file for vyos_interfaces
"""
from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'network'
}
DOCUMENTATION = """
---
module: vyos_interfaces
version_added: 2.9
short_description: Manages interface attributes of VyOS network devices.
description:
- This module manages the interface attributes on VyOS network devices.
- This module supports managing base attributes of Ethernet, Bonding,
VXLAN, Loopback and Virtual Tunnel Interfaces.
author: Nilashish Chakraborty (@nilashishc)
options:
config:
description: The provided interfaces configuration.
type: list
suboptions:
name:
description:
- Full name of the interface, e.g. eth0, eth1, bond0, vti1, vxlan2.
type: str
required: True
description:
description:
- Interface description.
type: str
duplex:
description:
- Interface duplex mode.
- Applicable for Ethernet interfaces only.
choices: ['full', 'half', 'auto']
type: str
enabled:
default: True
description:
- Administrative state of the interface.
- Set the value to C(true) to administratively enable
the interface or C(false) to disable it.
type: bool
mtu:
description:
- MTU for a specific interface. Refer to vendor documentation for valid values.
- Applicable for Ethernet, Bonding, VXLAN and Virtual Tunnel interfaces.
type: int
speed:
description:
- Interface link speed.
- Applicable for Ethernet interfaces only.
type: str
choices: ['auto', '10', '100', '1000', '2500', '10000']
vifs:
description:
- Virtual sub-interfaces related configuration.
- 802.1Q VLAN interfaces are represented as virtual sub-interfaces in VyOS.
type: list
suboptions:
vlan_id:
description:
- Identifier for the virtual sub-interface.
type: int
description:
description:
- Virtual sub-interface description.
type: str
enabled:
description:
- Administrative state of the virtual sub-interface.
- Set the value to C(true) to administratively enable
the interface or C(false) to disable it.
type: bool
default: True
mtu:
description:
- MTU for the virtual sub-interface.
- Refer to vendor documentation for valid values.
type: int
state:
description:
- The state the configuration should be left in.
type: str
choices:
- merged
- replaced
- overridden
- deleted
default: merged
"""
EXAMPLES = """
# Using merged
#
# -------------
# Before state:
# -------------
#
# vyos@vyos:~$ show configuration commands | grep interfaces
# set interfaces ethernet eth0 address 'dhcp'
# set interfaces ethernet eth0 address 'dhcpv6'
# set interfaces ethernet eth0 duplex 'auto'
# set interfaces ethernet eth0 hw-id '08:00:27:30:f0:22'
# set interfaces ethernet eth0 smp-affinity 'auto'
# set interfaces ethernet eth0 speed 'auto'
# set interfaces ethernet eth1 hw-id '08:00:27:ea:0f:b9'
# set interfaces ethernet eth1 smp-affinity 'auto'
# set interfaces ethernet eth2 hw-id '08:00:27:c2:98:23'
# set interfaces ethernet eth2 smp-affinity 'auto'
# set interfaces ethernet eth3 hw-id '08:00:27:43:70:8c'
# set interfaces loopback lo
- name: Merge provided configuration with device configuration
vyos_interfaces:
config:
- name: eth2
description: 'Configured by Ansible'
enabled: True
vifs:
- vlan_id: 200
description: "VIF 200 - ETH2"
- name: eth3
description: 'Configured by Ansible'
mtu: 1500
- name: bond1
description: 'Bond - 1'
mtu: 1200
- name: vti2
description: 'VTI - 2'
enabled: false
state: merged
#
#
# -------------------------
# Module Execution Result
# -------------------------
#
# "before": [
# {
# "enabled": true,
# "name": "lo"
# },
# {
# "enabled": true,
# "name": "eth3"
# },
# {
# "enabled": true,
# "name": "eth2"
# },
# {
# "enabled": true,
# "name": "eth1"
# },
# {
# "duplex": "auto",
# "enabled": true,
# "name": "eth0",
# "speed": "auto"
# }
# ]
#
# "commands": [
# "set interfaces ethernet eth2 description 'Configured by Ansible'",
# "set interfaces ethernet eth2 vif 200",
# "set interfaces ethernet eth2 vif 200 description 'VIF 200 - ETH2'",
# "set interfaces ethernet eth3 description 'Configured by Ansible'",
# "set interfaces ethernet eth3 mtu '1500'",
# "set interfaces bonding bond1",
# "set interfaces bonding bond1 description 'Bond - 1'",
# "set interfaces bonding bond1 mtu '1200'",
# "set interfaces vti vti2",
# "set interfaces vti vti2 description 'VTI - 2'",
# "set interfaces vti vti2 disable"
# ]
#
# "after": [
# {
# "description": "Bond - 1",
# "enabled": true,
# "mtu": 1200,
# "name": "bond1"
# },
# {
# "enabled": true,
# "name": "lo"
# },
# {
# "description": "VTI - 2",
# "enabled": false,
# "name": "vti2"
# },
# {
# "description": "Configured by Ansible",
# "enabled": true,
# "mtu": 1500,
# "name": "eth3"
# },
# {
# "description": "Configured by Ansible",
# "enabled": true,
# "name": "eth2",
# "vifs": [
# {
# "description": "VIF 200 - ETH2",
# "enabled": true,
# "vlan_id": "200"
# }
# ]
# },
# {
# "enabled": true,
# "name": "eth1"
# },
# {
# "duplex": "auto",
# "enabled": true,
# "name": "eth0",
# "speed": "auto"
# }
# ]
#
#
# -------------
# After state:
# -------------
#
# vyos@vyos:~$ show configuration commands | grep interfaces
# set interfaces bonding bond1 description 'Bond - 1'
# set interfaces bonding bond1 mtu '1200'
# set interfaces ethernet eth0 address 'dhcp'
# set interfaces ethernet eth0 address 'dhcpv6'
# set interfaces ethernet eth0 duplex 'auto'
# set interfaces ethernet eth0 hw-id '08:00:27:30:f0:22'
# set interfaces ethernet eth0 smp-affinity 'auto'
# set interfaces ethernet eth0 speed 'auto'
# set interfaces ethernet eth1 hw-id '08:00:27:ea:0f:b9'
# set interfaces ethernet eth1 smp-affinity 'auto'
# set interfaces ethernet eth2 description 'Configured by Ansible'
# set interfaces ethernet eth2 hw-id '08:00:27:c2:98:23'
# set interfaces ethernet eth2 smp-affinity 'auto'
# set interfaces ethernet eth2 vif 200 description 'VIF 200 - ETH2'
# set interfaces ethernet eth3 description 'Configured by Ansible'
# set interfaces ethernet eth3 hw-id '08:00:27:43:70:8c'
# set interfaces ethernet eth3 mtu '1500'
# set interfaces loopback lo
# set interfaces vti vti2 description 'VTI - 2'
# set interfaces vti vti2 disable
#
# Using replaced
#
# -------------
# Before state:
# -------------
#
# vyos:~$ show configuration commands | grep eth
# set interfaces bonding bond1 description 'Bond - 1'
# set interfaces bonding bond1 mtu '1400'
# set interfaces ethernet eth0 address 'dhcp'
# set interfaces ethernet eth0 description 'Management Interface for the Appliance'
# set interfaces ethernet eth0 duplex 'auto'
# set interfaces ethernet eth0 hw-id '08:00:27:f3:6c:b5'
# set interfaces ethernet eth0 smp_affinity 'auto'
# set interfaces ethernet eth0 speed 'auto'
# set interfaces ethernet eth1 description 'Configured by Ansible Eng Team'
# set interfaces ethernet eth1 duplex 'full'
# set interfaces ethernet eth1 hw-id '08:00:27:ad:ef:65'
# set interfaces ethernet eth1 smp_affinity 'auto'
# set interfaces ethernet eth1 speed '100'
# set interfaces ethernet eth2 description 'Configured by Ansible'
# set interfaces ethernet eth2 duplex 'full'
# set interfaces ethernet eth2 hw-id '08:00:27:ab:4e:79'
# set interfaces ethernet eth2 mtu '500'
# set interfaces ethernet eth2 smp_affinity 'auto'
# set interfaces ethernet eth2 speed '100'
# set interfaces ethernet eth2 vif 200 description 'Configured by Ansible'
# set interfaces ethernet eth3 description 'Configured by Ansible'
# set interfaces ethernet eth3 duplex 'full'
# set interfaces ethernet eth3 hw-id '08:00:27:17:3c:85'
# set interfaces ethernet eth3 mtu '1500'
# set interfaces ethernet eth3 smp_affinity 'auto'
# set interfaces ethernet eth3 speed '100'
# set interfaces loopback lo
#
#
- name: Replace device configurations of listed interfaces with provided configurations
vyos_interfaces:
config:
- name: eth2
description: "Replaced by Ansible"
- name: eth3
description: "Replaced by Ansible"
- name: eth1
description: "Replaced by Ansible"
state: replaced
#
#
# -----------------------
# Module Execution Result
# -----------------------
#
# "before": [
# {
# "description": "Bond - 1",
# "enabled": true,
# "mtu": 1400,
# "name": "bond1"
# },
# {
# "enabled": true,
# "name": "lo"
# },
# {
# "description": "Configured by Ansible",
# "duplex": "full",
# "enabled": true,
# "mtu": 1500,
# "name": "eth3",
# "speed": "100"
# },
# {
# "description": "Configured by Ansible",
# "duplex": "full",
# "enabled": true,
# "mtu": 500,
# "name": "eth2",
# "speed": "100",
# "vifs": [
# {
# "description": "VIF 200 - ETH2",
# "enabled": true,
# "vlan_id": "200"
# }
# ]
# },
# {
# "description": "Configured by Ansible Eng Team",
# "duplex": "full",
# "enabled": true,
# "name": "eth1",
# "speed": "100"
# },
# {
# "description": "Management Interface for the Appliance",
# "duplex": "auto",
# "enabled": true,
# "name": "eth0",
# "speed": "auto"
# }
# ]
#
# "commands": [
# "delete interfaces ethernet eth2 speed",
# "delete interfaces ethernet eth2 duplex",
# "delete interfaces ethernet eth2 mtu",
# "delete interfaces ethernet eth2 vif 200 description",
# "set interfaces ethernet eth2 description 'Replaced by Ansible'",
# "delete interfaces ethernet eth3 speed",
# "delete interfaces ethernet eth3 duplex",
# "delete interfaces ethernet eth3 mtu",
# "set interfaces ethernet eth3 description 'Replaced by Ansible'",
# "delete interfaces ethernet eth1 speed",
# "delete interfaces ethernet eth1 duplex",
# "set interfaces ethernet eth1 description 'Replaced by Ansible'"
# ]
#
# "after": [
# {
# "description": "Bond - 1",
# "enabled": true,
# "mtu": 1400,
# "name": "bond1"
# },
# {
# "enabled": true,
# "name": "lo"
# },
# {
# "description": "Replaced by Ansible",
# "enabled": true,
# "name": "eth3"
# },
# {
# "description": "Replaced by Ansible",
# "enabled": true,
# "name": "eth2",
# "vifs": [
# {
# "enabled": true,
# "vlan_id": "200"
# }
# ]
# },
# {
# "description": "Replaced by Ansible",
# "enabled": true,
# "name": "eth1"
# },
# {
# "description": "Management Interface for the Appliance",
# "duplex": "auto",
# "enabled": true,
# "name": "eth0",
# "speed": "auto"
# }
# ]
#
#
# -------------
# After state:
# -------------
#
# vyos@vyos:~$ show configuration commands | grep interfaces
# set interfaces bonding bond1 description 'Bond - 1'
# set interfaces bonding bond1 mtu '1400'
# set interfaces ethernet eth0 address 'dhcp'
# set interfaces ethernet eth0 address 'dhcpv6'
# set interfaces ethernet eth0 description 'Management Interface for the Appliance'
# set interfaces ethernet eth0 duplex 'auto'
# set interfaces ethernet eth0 hw-id '08:00:27:30:f0:22'
# set interfaces ethernet eth0 smp-affinity 'auto'
# set interfaces ethernet eth0 speed 'auto'
# set interfaces ethernet eth1 description 'Replaced by Ansible'
# set interfaces ethernet eth1 hw-id '08:00:27:ea:0f:b9'
# set interfaces ethernet eth1 smp-affinity 'auto'
# set interfaces ethernet eth2 description 'Replaced by Ansible'
# set interfaces ethernet eth2 hw-id '08:00:27:c2:98:23'
# set interfaces ethernet eth2 smp-affinity 'auto'
# set interfaces ethernet eth2 vif 200
# set interfaces ethernet eth3 description 'Replaced by Ansible'
# set interfaces ethernet eth3 hw-id '08:00:27:43:70:8c'
# set interfaces loopback lo
#
#
# Using overridden
#
#
# --------------
# Before state
# --------------
#
# vyos@vyos:~$ show configuration commands | grep interfaces
# set interfaces ethernet eth0 address 'dhcp'
# set interfaces ethernet eth0 address 'dhcpv6'
# set interfaces ethernet eth0 description 'Ethernet Interface - 0'
# set interfaces ethernet eth0 duplex 'auto'
# set interfaces ethernet eth0 hw-id '08:00:27:30:f0:22'
# set interfaces ethernet eth0 mtu '1200'
# set interfaces ethernet eth0 smp-affinity 'auto'
# set interfaces ethernet eth0 speed 'auto'
# set interfaces ethernet eth1 description 'Configured by Ansible Eng Team'
# set interfaces ethernet eth1 hw-id '08:00:27:ea:0f:b9'
# set interfaces ethernet eth1 mtu '100'
# set interfaces ethernet eth1 smp-affinity 'auto'
# set interfaces ethernet eth1 vif 100 description 'VIF 100 - ETH1'
# set interfaces ethernet eth1 vif 100 disable
# set interfaces ethernet eth2 description 'Configured by Ansible Team (Admin Down)'
# set interfaces ethernet eth2 disable
# set interfaces ethernet eth2 hw-id '08:00:27:c2:98:23'
# set interfaces ethernet eth2 mtu '600'
# set interfaces ethernet eth2 smp-affinity 'auto'
# set interfaces ethernet eth3 description 'Configured by Ansible Network'
# set interfaces ethernet eth3 hw-id '08:00:27:43:70:8c'
# set interfaces loopback lo
# set interfaces vti vti1 description 'Virtual Tunnel Interface - 1'
# set interfaces vti vti1 mtu '68'
#
#
- name: Overrides all device configuration with provided configuration
vyos_interfaces:
config:
- name: eth0
description: Outbound Interface For The Appliance
speed: auto
duplex: auto
- name: eth2
speed: auto
duplex: auto
- name: eth3
mtu: 1200
state: overridden
#
#
# ------------------------
# Module Execution Result
# ------------------------
#
# "before": [
# {
# "enabled": true,
# "name": "lo"
# },
# {
# "description": "Virtual Tunnel Interface - 1",
# "enabled": true,
# "mtu": 68,
# "name": "vti1"
# },
# {
# "description": "Configured by Ansible Network",
# "enabled": true,
# "name": "eth3"
# },
# {
# "description": "Configured by Ansible Team (Admin Down)",
# "enabled": false,
# "mtu": 600,
# "name": "eth2"
# },
# {
# "description": "Configured by Ansible Eng Team",
# "enabled": true,
# "mtu": 100,
# "name": "eth1",
# "vifs": [
# {
# "description": "VIF 100 - ETH1",
# "enabled": false,
# "vlan_id": "100"
# }
# ]
# },
# {
# "description": "Ethernet Interface - 0",
# "duplex": "auto",
# "enabled": true,
# "mtu": 1200,
# "name": "eth0",
# "speed": "auto"
# }
# ]
#
# "commands": [
# "delete interfaces vti vti1 description",
# "delete interfaces vti vti1 mtu",
# "delete interfaces ethernet eth1 description",
# "delete interfaces ethernet eth1 mtu",
# "delete interfaces ethernet eth1 vif 100 description",
# "delete interfaces ethernet eth1 vif 100 disable",
# "delete interfaces ethernet eth0 mtu",
# "set interfaces ethernet eth0 description 'Outbound Interface For The Appliance'",
# "delete interfaces ethernet eth2 description",
# "delete interfaces ethernet eth2 mtu",
# "set interfaces ethernet eth2 duplex 'auto'",
# "delete interfaces ethernet eth2 disable",
# "set interfaces ethernet eth2 speed 'auto'",
# "delete interfaces ethernet eth3 description",
# "set interfaces ethernet eth3 mtu '1200'"
# ],
#
# "after": [
# {
# "enabled": true,
# "name": "lo"
# },
# {
# "enabled": true,
# "name": "vti1"
# },
# {
# "enabled": true,
# "mtu": 1200,
# "name": "eth3"
# },
# {
# "duplex": "auto",
# "enabled": true,
# "name": "eth2",
# "speed": "auto"
# },
# {
# "enabled": true,
# "name": "eth1",
# "vifs": [
# {
# "enabled": true,
# "vlan_id": "100"
# }
# ]
# },
# {
# "description": "Outbound Interface For The Appliance",
# "duplex": "auto",
# "enabled": true,
# "name": "eth0",
# "speed": "auto"
# }
# ]
#
#
# ------------
# After state
# ------------
#
# vyos@vyos:~$ show configuration commands | grep interfaces
# set interfaces ethernet eth0 address 'dhcp'
# set interfaces ethernet eth0 address 'dhcpv6'
# set interfaces ethernet eth0 description 'Outbound Interface For The Appliance'
# set interfaces ethernet eth0 duplex 'auto'
# set interfaces ethernet eth0 hw-id '08:00:27:30:f0:22'
# set interfaces ethernet eth0 smp-affinity 'auto'
# set interfaces ethernet eth0 speed 'auto'
# set interfaces ethernet eth1 hw-id '08:00:27:ea:0f:b9'
# set interfaces ethernet eth1 smp-affinity 'auto'
# set interfaces ethernet eth1 vif 100
# set interfaces ethernet eth2 duplex 'auto'
# set interfaces ethernet eth2 hw-id '08:00:27:c2:98:23'
# set interfaces ethernet eth2 smp-affinity 'auto'
# set interfaces ethernet eth2 speed 'auto'
# set interfaces ethernet eth3 hw-id '08:00:27:43:70:8c'
# set interfaces ethernet eth3 mtu '1200'
# set interfaces loopback lo
# set interfaces vti vti1
#
#
# Using deleted
#
#
# -------------
# Before state
# -------------
#
# vyos@vyos:~$ show configuration commands | grep interfaces
# set interfaces bonding bond0 mtu '1300'
# set interfaces bonding bond1 description 'LAG - 1'
# set interfaces ethernet eth0 address 'dhcp'
# set interfaces ethernet eth0 address 'dhcpv6'
# set interfaces ethernet eth0 description 'Outbound Interface for this appliance'
# set interfaces ethernet eth0 duplex 'auto'
# set interfaces ethernet eth0 hw-id '08:00:27:30:f0:22'
# set interfaces ethernet eth0 smp-affinity 'auto'
# set interfaces ethernet eth0 speed 'auto'
# set interfaces ethernet eth1 description 'Configured by Ansible Network'
# set interfaces ethernet eth1 duplex 'full'
# set interfaces ethernet eth1 hw-id '08:00:27:ea:0f:b9'
# set interfaces ethernet eth1 smp-affinity 'auto'
# set interfaces ethernet eth1 speed '100'
# set interfaces ethernet eth2 description 'Configured by Ansible'
# set interfaces ethernet eth2 disable
# set interfaces ethernet eth2 duplex 'full'
# set interfaces ethernet eth2 hw-id '08:00:27:c2:98:23'
# set interfaces ethernet eth2 mtu '600'
# set interfaces ethernet eth2 smp-affinity 'auto'
# set interfaces ethernet eth2 speed '100'
# set interfaces ethernet eth3 description 'Configured by Ansible Network'
# set interfaces ethernet eth3 duplex 'full'
# set interfaces ethernet eth3 hw-id '08:00:27:43:70:8c'
# set interfaces ethernet eth3 speed '100'
# set interfaces loopback lo
#
#
- name: Delete attributes of given interfaces (Note - This won't delete the interfaces themselves)
vyos_interfaces:
config:
- name: bond1
- name: eth1
- name: eth2
- name: eth3
state: deleted
#
#
# ------------------------
# Module Execution Results
# ------------------------
#
# "before": [
# {
# "enabled": true,
# "mtu": 1300,
# "name": "bond0"
# },
# {
# "description": "LAG - 1",
# "enabled": true,
# "name": "bond1"
# },
# {
# "enabled": true,
# "name": "lo"
# },
# {
# "description": "Configured by Ansible Network",
# "duplex": "full",
# "enabled": true,
# "name": "eth3",
# "speed": "100"
# },
# {
# "description": "Configured by Ansible",
# "duplex": "full",
# "enabled": false,
# "mtu": 600,
# "name": "eth2",
# "speed": "100"
# },
# {
# "description": "Configured by Ansible Network",
# "duplex": "full",
# "enabled": true,
# "name": "eth1",
# "speed": "100"
# },
# {
# "description": "Outbound Interface for this appliance",
# "duplex": "auto",
# "enabled": true,
# "name": "eth0",
# "speed": "auto"
# }
# ]
#
# "commands": [
# "delete interfaces bonding bond1 description",
# "delete interfaces ethernet eth1 speed",
# "delete interfaces ethernet eth1 duplex",
# "delete interfaces ethernet eth1 description",
# "delete interfaces ethernet eth2 speed",
# "delete interfaces ethernet eth2 disable",
# "delete interfaces ethernet eth2 duplex",
# "delete interfaces ethernet eth2 disable",
# "delete interfaces ethernet eth2 description",
# "delete interfaces ethernet eth2 disable",
# "delete interfaces ethernet eth2 mtu",
# "delete interfaces ethernet eth2 disable",
# "delete interfaces ethernet eth3 speed",
# "delete interfaces ethernet eth3 duplex",
# "delete interfaces ethernet eth3 description"
# ]
#
# "after": [
# {
# "enabled": true,
# "mtu": 1300,
# "name": "bond0"
# },
# {
# "enabled": true,
# "name": "bond1"
# },
# {
# "enabled": true,
# "name": "lo"
# },
# {
# "enabled": true,
# "name": "eth3"
# },
# {
# "enabled": true,
# "name": "eth2"
# },
# {
# "enabled": true,
# "name": "eth1"
# },
# {
# "description": "Outbound Interface for this appliance",
# "duplex": "auto",
# "enabled": true,
# "name": "eth0",
# "speed": "auto"
# }
# ]
#
#
# ------------
# After state
# ------------
#
# vyos@vyos:~$ show configuration commands | grep interfaces
# set interfaces bonding bond0 mtu '1300'
# set interfaces bonding bond1
# set interfaces ethernet eth0 address 'dhcp'
# set interfaces ethernet eth0 address 'dhcpv6'
# set interfaces ethernet eth0 description 'Outbound Interface for this appliance'
# set interfaces ethernet eth0 duplex 'auto'
# set interfaces ethernet eth0 hw-id '08:00:27:30:f0:22'
# set interfaces ethernet eth0 smp-affinity 'auto'
# set interfaces ethernet eth0 speed 'auto'
# set interfaces ethernet eth1 hw-id '08:00:27:ea:0f:b9'
# set interfaces ethernet eth1 smp-affinity 'auto'
# set interfaces ethernet eth2 hw-id '08:00:27:c2:98:23'
# set interfaces ethernet eth2 smp-affinity 'auto'
# set interfaces ethernet eth3 hw-id '08:00:27:43:70:8c'
# set interfaces loopback lo
#
#
"""
RETURN = """
before:
description: The configuration prior to the model invocation.
returned: always
sample: >
The configuration returned will always be in the same format
of the parameters above.
type: list
after:
description: The resulting configuration model invocation.
returned: when changed
sample: >
The configuration returned will always be in the same format
of the parameters above.
type: list
commands:
description: The set of commands pushed to the remote device.
returned: always
type: list
sample:
- 'set interfaces ethernet eth1 mtu 1200'
- 'set interfaces ethernet eth2 vif 100 description VIF 100'
"""
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.network.vyos.argspec.interfaces.interfaces import InterfacesArgs
from ansible.module_utils.network.vyos.config.interfaces.interfaces import Interfaces
def main():
"""
Main entry point for module execution
:returns: the result form module invocation
"""
module = AnsibleModule(argument_spec=InterfacesArgs.argument_spec,
supports_check_mode=True)
result = Interfaces(module).execute_module()
module.exit_json(**result)
if __name__ == '__main__':
main()

@ -0,0 +1,9 @@
---
- name: Ensure required interfaces are present in running-config
cli_config:
config: "{{ lines }}"
vars:
lines: |
set interfaces ethernet eth1
set interfaces ethernet eth2
set interfaces loopback lo

@ -0,0 +1,3 @@
---
testcase: "[^_].*"
test_items: []

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

@ -0,0 +1,19 @@
---
- name: Collect all cli test cases
find:
paths: "{{ role_path }}/tests/cli"
patterns: "{{ testcase }}.yaml"
use_regex: true
register: test_cases
delegate_to: localhost
- name: Set test_items
set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}"
- name: Run test case (connection=network_cli)
include: "{{ test_case_to_run }}"
vars:
ansible_connection: network_cli
with_items: "{{ test_items }}"
loop_control:
loop_var: test_case_to_run

@ -0,0 +1,2 @@
---
- { include: cli.yaml, tags: ['cli'] }

@ -0,0 +1,17 @@
---
- name: Setup
cli_config:
config: "{{ lines }}"
vars:
lines: |
set interfaces ethernet "{{ intf }}" description 'Configured by Ansible'
set interfaces ethernet "{{ intf }}" speed 'auto'
set interfaces ethernet "{{ intf }}" duplex 'auto'
set interfaces ethernet "{{ intf }}" mtu '1500'
set interfaces ethernet "{{ intf }}" vif 200
set interfaces ethernet "{{ intf }}" vif 200 description 'VIF - 200'
loop:
- eth1
- eth2
loop_control:
loop_var: intf

@ -0,0 +1,17 @@
---
- name: Remove Config
cli_config:
config: "{{ lines }}"
vars:
lines: |
delete interfaces ethernet "{{ intf }}" description
delete interfaces ethernet "{{ intf }}" speed
delete interfaces ethernet "{{ intf }}" duplex
delete interfaces ethernet "{{ intf }}" mtu
delete interfaces ethernet "{{ intf }}" disable
delete interfaces ethernet "{{ intf }}" vif
loop:
- eth1
- eth2
loop_control:
loop_var: intf

@ -0,0 +1,46 @@
---
- debug:
msg: "Start vyos_interfaces deleted integration tests ansible_connection={{ ansible_connection }}"
- include_tasks: _populate.yaml
- block:
- name: Delete attributes of given interfaces
vyos_interfaces: &deleted
config:
- name: eth1
- name: eth2
state: deleted
register: result
- name: Assert that the before dicts were correctly generated
assert:
that:
- "{{ populate | symmetric_difference(result['before']) |length == 0 }}"
- name: Assert that the correct set of commands were generated
assert:
that:
- "{{ deleted['commands'] | symmetric_difference(result['commands']) |length == 0 }}"
- name: Assert that the after dicts were correctly generated
assert:
that:
- "{{ deleted['after'] | symmetric_difference(result['after']) |length == 0 }}"
- name: Delete attributes of given interfaces (IDEMPOTENT)
vyos_interfaces: *deleted
register: result
- name: Assert that the previous task was idempotent
assert:
that:
- "result.changed == false"
- name: Assert that the before dicts were correctly generated
assert:
that:
- "{{ deleted['after'] | symmetric_difference(result['before']) |length == 0 }}"
always:
- include_tasks: _remove_config.yaml

@ -0,0 +1,59 @@
---
- debug:
msg: "START vyos_interfaces merged integration tests on connection={{ ansible_connection }}"
- include_tasks: _remove_config.yaml
- block:
- name: Merge the provided configuration with the exisiting running configuration
vyos_interfaces: &merged
config:
- name: eth1
description: "Configured by Ansible - Interface 1"
mtu: 1500
speed: auto
duplex: auto
vifs:
- vlan_id: 100
description: "Eth1 - VIF 100"
mtu: 400
- vlan_id: 101
description: "Eth1 - VIF 101"
- name: eth2
description: "Configured by Ansible - Interface 2 (ADMIN DOWN)"
mtu: 600
enabled: false
state: merged
register: result
- name: Assert that before dicts were correctly generated
assert:
that: "{{ merged['before'] | symmetric_difference(result['before']) |length == 0 }}"
- name: Assert that correct set of commands were generated
assert:
that:
- "{{ merged['commands'] | symmetric_difference(result['commands']) |length == 0 }}"
- name: Assert that after dicts was correctly generated
assert:
that:
- "{{ merged['after'] | symmetric_difference(result['after']) |length == 0 }}"
- name: Merge the provided configuration with the existing running configuration (IDEMPOTENT)
vyos_interfaces: *merged
register: result
- name: Assert that the previous task was idempotent
assert:
that:
- "result['changed'] == false"
- name: Assert that before dicts were correctly generated
assert:
that:
- "{{ merged['after'] | symmetric_difference(result['before']) |length == 0 }}"
always:
- include_tasks: _remove_config.yaml

@ -0,0 +1,52 @@
---
- debug:
msg: "START vyos_interfaces overridden integration tests on connection={{ ansible_connection }}"
- include_tasks: _remove_config.yaml
- include_tasks: _populate.yaml
- block:
- name: Overrides all device configuration with provided configuration
vyos_interfaces: &overridden
config:
- name: eth0
speed: "auto"
duplex: "auto"
- name: eth2
description: "Overridden by Ansible"
mtu: 1200
state: overridden
register: result
- name: Assert that before dicts were correctly generated
assert:
that:
- "{{ populate | symmetric_difference(result['before']) |length == 0 }}"
- name: Assert that correct commands were generated
assert:
that:
- "{{ overridden['commands'] | symmetric_difference(result['commands']) |length == 0 }}"
- name: Assert that after dicts were correctly generated
assert:
that:
- "{{ overridden['after'] | symmetric_difference(result['after']) |length == 0 }}"
- name: Overrides all device configuration with provided configurations (IDEMPOTENT)
vyos_interfaces: *overridden
register: result
- name: Assert that the previous task was idempotent
assert:
that:
- "result['changed'] == false"
- name: Assert that before dicts were correctly generated
assert:
that:
- "{{ overridden['after'] | symmetric_difference(result['before']) |length == 0 }}"
always:
- include_tasks: _remove_config.yaml

@ -0,0 +1,55 @@
---
- debug:
msg: "START vyos_interfaces replaced integration tests on connection={{ ansible_connection }}"
- include_tasks: _remove_config.yaml
- include_tasks: _populate.yaml
- block:
- name: Replace device configurations of listed interfaces with provided configurations
vyos_interfaces: &replaced
config:
- name: eth1
description: "Replaced by Ansible"
vifs:
- vlan_id: 100
description: "VIF 100 - Replaced by Ansible"
- name: eth2
mtu: 1400
description: "Replaced by Ansible"
state: replaced
register: result
- name: Assert that correct set of commands were generated
assert:
that:
- "{{ replaced['commands'] | symmetric_difference(result['commands']) |length == 0 }}"
- name: Assert that before dicts are correctly generated
assert:
that:
- "{{ populate | symmetric_difference(result['before']) |length == 0 }}"
- name: Assert that after dict is correctly generated
assert:
that:
- "{{ replaced['after'] | symmetric_difference(result['after']) |length == 0 }}"
- name: Replace device configurations of listed interfaces with provided configurarions (IDEMPOTENT)
vyos_interfaces: *replaced
register: result
- name: Assert that task was idempotent
assert:
that:
- "result['changed'] == false"
- name: Assert that before dict is correctly generated
assert:
that:
- "{{ replaced['after'] | symmetric_difference(result['before']) |length == 0 }}"
always:
- include_tasks: _remove_config.yaml

@ -0,0 +1,78 @@
---
- debug:
msg: "START vyos_interfaces round trip integration tests on connection={{ ansible_connection }}"
- include_tasks: _remove_config.yaml
- block:
- name: Apply the provided configuration (base config)
vyos_interfaces:
config:
- name: eth0
enabled: true
duplex: "auto"
speed: "auto"
- name: eth1
description: "Interface - 1"
mtu: 1500
vifs:
- vlan_id: 100
description: "Eth1 - VIF 100"
mtu: 200
- vlan_id: 101
enabled: false
- name: eth2
description: "Interface - 2"
enabled: true
mtu: 900
state: merged
register: base_config
- name: Gather interfaces facts
vyos_facts:
gather_subset:
- default
gather_network_resources:
- interfaces
- name: Apply the provided configuration (config to be reverted)
vyos_interfaces:
config:
- name: eth1
description: "Interface 1 - Description (WILL BE REVERTED)"
mtu: 1200
vifs:
- vlan_id: 100
description: "Eth1 - VIF 100 (WILL BE REVERTED)"
mtu: 400
- vlan_id: 101
description: "Eth1 - VIF 101 (WILL BE REMOVED)"
enabled: true
- name: eth2
description: "Interface 2 (ADMIN DOWN) (WILL BE REVERTED)"
mtu: 600
enabled: false
state: merged
register: result
- name: Assert that changes were applied
assert:
that: "{{ round_trip['after'] | symmetric_difference(result['after']) |length == 0 }}"
- name: Revert back to base config using facts round trip
vyos_interfaces:
config: "{{ ansible_facts['network_resources']['interfaces'] }}"
state: replaced
register: revert
- name: Assert that config was reverted
assert:
that: "{{ base_config['after'] | symmetric_difference(revert['after']) |length == 0 }}"
always:
- include_tasks: _remove_config.yaml

@ -0,0 +1,230 @@
---
merged:
before:
- name: "eth0"
enabled: true
speed: "auto"
duplex: "auto"
- name: "eth1"
enabled: true
- name: "eth2"
enabled: true
- name: "lo"
enabled: true
commands:
- "set interfaces ethernet eth1 description 'Configured by Ansible - Interface 1'"
- "set interfaces ethernet eth1 mtu '1500'"
- "set interfaces ethernet eth1 duplex 'auto'"
- "set interfaces ethernet eth1 speed 'auto'"
- "set interfaces ethernet eth1 vif 100 description 'Eth1 - VIF 100'"
- "set interfaces ethernet eth1 vif 100 mtu '400'"
- "set interfaces ethernet eth1 vif 101 description 'Eth1 - VIF 101'"
- "set interfaces ethernet eth2 description 'Configured by Ansible - Interface 2 (ADMIN DOWN)'"
- "set interfaces ethernet eth2 mtu '600'"
- "set interfaces ethernet eth2 disable"
after:
- name: "lo"
enabled: true
- name: "eth0"
enabled: true
duplex: "auto"
speed: "auto"
- name: "eth1"
description: "Configured by Ansible - Interface 1"
mtu: 1500
speed: "auto"
duplex: "auto"
enabled: true
vifs:
- vlan_id: 100
description: "Eth1 - VIF 100"
mtu: 400
enabled: true
- vlan_id: 101
description: "Eth1 - VIF 101"
enabled: true
- name: "eth2"
description: "Configured by Ansible - Interface 2 (ADMIN DOWN)"
mtu: 600
enabled: false
populate:
- name: "eth1"
enabled: true
speed: "auto"
duplex: "auto"
description: "Configured by Ansible"
mtu: 1500
vifs:
- vlan_id: 200
enabled: true
description: "VIF - 200"
- name: "eth2"
enabled: true
speed: "auto"
duplex: "auto"
description: "Configured by Ansible"
mtu: 1500
vifs:
- vlan_id: 200
enabled: true
description: "VIF - 200"
- name: "eth0"
enabled: true
duplex: "auto"
speed: "auto"
- name: "lo"
enabled: true
replaced:
commands:
- "delete interfaces ethernet eth1 mtu"
- "delete interfaces ethernet eth1 speed"
- "delete interfaces ethernet eth1 duplex"
- "delete interfaces ethernet eth1 vif 200 description"
- "set interfaces ethernet eth1 description 'Replaced by Ansible'"
- "set interfaces ethernet eth1 vif 100 description 'VIF 100 - Replaced by Ansible'"
- "delete interfaces ethernet eth2 speed"
- "delete interfaces ethernet eth2 duplex"
- "delete interfaces ethernet eth2 vif 200 description"
- "set interfaces ethernet eth2 description 'Replaced by Ansible'"
- "set interfaces ethernet eth2 mtu '1400'"
after:
- name: "lo"
enabled: true
- name: "eth1"
description: "Replaced by Ansible"
enabled: true
vifs:
- vlan_id: 100
enabled: true
description: "VIF 100 - Replaced by Ansible"
- vlan_id: 200
enabled: true
- name: "eth2"
mtu: 1400
description: "Replaced by Ansible"
enabled: true
vifs:
- vlan_id: 200
enabled: true
- name: "eth0"
enabled: true
duplex: "auto"
speed: "auto"
overridden:
commands:
- "delete interfaces ethernet eth1 description"
- "delete interfaces ethernet eth1 speed"
- "delete interfaces ethernet eth1 duplex"
- "delete interfaces ethernet eth1 mtu"
- "delete interfaces ethernet eth1 vif 200 description"
- "delete interfaces ethernet eth2 speed"
- "delete interfaces ethernet eth2 duplex"
- "delete interfaces ethernet eth2 vif 200 description"
- "set interfaces ethernet eth2 description 'Overridden by Ansible'"
- "set interfaces ethernet eth2 mtu '1200'"
after:
- name: "lo"
enabled: true
- name: "eth0"
enabled: true
speed: "auto"
duplex: "auto"
- name: "eth1"
enabled: true
vifs:
- vlan_id: 200
enabled: true
- name: "eth2"
enabled: true
description: "Overridden by Ansible"
mtu: 1200
vifs:
- vlan_id: 200
enabled: true
deleted:
commands:
- "delete interfaces ethernet eth1 description"
- "delete interfaces ethernet eth1 speed"
- "delete interfaces ethernet eth1 duplex"
- "delete interfaces ethernet eth1 mtu"
- "delete interfaces ethernet eth1 vif 200 description"
- "delete interfaces ethernet eth2 description"
- "delete interfaces ethernet eth2 speed"
- "delete interfaces ethernet eth2 duplex"
- "delete interfaces ethernet eth2 mtu"
- "delete interfaces ethernet eth2 vif 200 description"
after:
- name: "lo"
enabled: true
- name: "eth0"
enabled: true
speed: "auto"
duplex: "auto"
- name: "eth1"
enabled: true
vifs:
- vlan_id: 200
enabled: true
- name: "eth2"
enabled: true
vifs:
- vlan_id: 200
enabled: true
round_trip:
after:
- name: "lo"
enabled: true
- name: "eth0"
enabled: true
speed: "auto"
duplex: "auto"
- name: "eth1"
description: "Interface 1 - Description (WILL BE REVERTED)"
enabled: true
mtu: 1200
vifs:
- vlan_id: 100
description: "Eth1 - VIF 100 (WILL BE REVERTED)"
mtu: 400
enabled: true
- vlan_id: 101
description: "Eth1 - VIF 101 (WILL BE REMOVED)"
enabled: true
- name: "eth2"
description: "Interface 2 (ADMIN DOWN) (WILL BE REVERTED)"
mtu: 600
enabled: false

@ -5767,14 +5767,14 @@ lib/ansible/modules/network/vyos/vyos_facts.py future-import-boilerplate
lib/ansible/modules/network/vyos/vyos_facts.py metaclass-boilerplate
lib/ansible/modules/network/vyos/vyos_facts.py validate-modules:E324
lib/ansible/modules/network/vyos/vyos_facts.py validate-modules:E337
lib/ansible/modules/network/vyos/vyos_interface.py future-import-boilerplate
lib/ansible/modules/network/vyos/vyos_interface.py metaclass-boilerplate
lib/ansible/modules/network/vyos/vyos_interface.py validate-modules:E322
lib/ansible/modules/network/vyos/vyos_interface.py validate-modules:E324
lib/ansible/modules/network/vyos/vyos_interface.py validate-modules:E326
lib/ansible/modules/network/vyos/vyos_interface.py validate-modules:E337
lib/ansible/modules/network/vyos/vyos_interface.py validate-modules:E338
lib/ansible/modules/network/vyos/vyos_interface.py validate-modules:E340
lib/ansible/modules/network/vyos/_vyos_interface.py future-import-boilerplate
lib/ansible/modules/network/vyos/_vyos_interface.py metaclass-boilerplate
lib/ansible/modules/network/vyos/_vyos_interface.py validate-modules:E322
lib/ansible/modules/network/vyos/_vyos_interface.py validate-modules:E324
lib/ansible/modules/network/vyos/_vyos_interface.py validate-modules:E326
lib/ansible/modules/network/vyos/_vyos_interface.py validate-modules:E337
lib/ansible/modules/network/vyos/_vyos_interface.py validate-modules:E338
lib/ansible/modules/network/vyos/_vyos_interface.py validate-modules:E340
lib/ansible/modules/network/vyos/vyos_l3_interface.py future-import-boilerplate
lib/ansible/modules/network/vyos/vyos_l3_interface.py metaclass-boilerplate
lib/ansible/modules/network/vyos/vyos_l3_interface.py validate-modules:E322

@ -33,10 +33,13 @@ class TestVyosFactsModule(TestVyosModule):
def setUp(self):
super(TestVyosFactsModule, self).setUp()
self.mock_run_commands = patch('ansible.modules.network.vyos.vyos_facts.run_commands')
self.mock_run_commands = patch('ansible.module_utils.network.vyos.facts.legacy.base.run_commands')
self.run_commands = self.mock_run_commands.start()
self.mock_get_capabilities = patch('ansible.modules.network.vyos.vyos_facts.get_capabilities')
self.mock_get_resource_connection = patch('ansible.module_utils.network.common.facts.facts.get_resource_connection')
self.get_resource_connection = self.mock_get_resource_connection.start()
self.mock_get_capabilities = patch('ansible.module_utils.network.vyos.facts.legacy.base.get_capabilities')
self.get_capabilities = self.mock_get_capabilities.start()
self.get_capabilities.return_value = {
'device_info': {
@ -52,6 +55,7 @@ class TestVyosFactsModule(TestVyosModule):
super(TestVyosFactsModule, self).tearDown()
self.mock_run_commands.stop()
self.mock_get_capabilities.stop()
self.mock_get_resource_connection.stop()
def load_fixtures(self, commands=None):
def load_from_file(*args, **kwargs):
@ -74,7 +78,7 @@ class TestVyosFactsModule(TestVyosModule):
set_module_args(dict(gather_subset='default'))
result = self.execute_module()
facts = result.get('ansible_facts')
self.assertEqual(len(facts), 8)
self.assertEqual(len(facts), 10)
self.assertEqual(facts['ansible_net_hostname'].strip(), 'vyos01')
self.assertEqual(facts['ansible_net_version'], 'VyOS 1.1.7')
@ -82,7 +86,7 @@ class TestVyosFactsModule(TestVyosModule):
set_module_args(dict(gather_subset='!all'))
result = self.execute_module()
facts = result.get('ansible_facts')
self.assertEqual(len(facts), 8)
self.assertEqual(len(facts), 10)
self.assertEqual(facts['ansible_net_hostname'].strip(), 'vyos01')
self.assertEqual(facts['ansible_net_version'], 'VyOS 1.1.7')
@ -90,7 +94,7 @@ class TestVyosFactsModule(TestVyosModule):
set_module_args(dict(gather_subset=['!neighbors', '!config']))
result = self.execute_module()
facts = result.get('ansible_facts')
self.assertEqual(len(facts), 8)
self.assertEqual(len(facts), 10)
self.assertEqual(facts['ansible_net_hostname'].strip(), 'vyos01')
self.assertEqual(facts['ansible_net_version'], 'VyOS 1.1.7')

Loading…
Cancel
Save