Add api_facts module (#88)

* Add API docs fragment.

* Improve documentation.

* Move API code to api module_utils.

* Improve docs.

* Add api_facts module.

Does not yet support 'config'. I'm not sure whether that's actually
possible with the API.

* Convert subnet to integer if possible.

* Cleanup.

* Linting and fix tests.

* Remove things that make no sense.

* Simplify code.

* Add basic tests.

* Lint.
This commit is contained in:
Felix Fontein 2022-05-12 16:17:43 +02:00 committed by GitHub
parent a90c696589
commit 3d80ccec5f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 1618 additions and 277 deletions

View file

@ -0,0 +1,72 @@
# -*- coding: utf-8 -*-
# Copyright: (c) 2020, Nikolay Dachev <nikolay@dachev.info>
# GNU General Public License v3.0+ https://www.gnu.org/licenses/gpl-3.0.txt
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
class ModuleDocFragment(object):
DOCUMENTATION = r'''
options:
hostname:
description:
- RouterOS hostname API.
required: true
type: str
username:
description:
- RouterOS login user.
required: true
type: str
password:
description:
- RouterOS user password.
required: true
type: str
tls:
description:
- If is set TLS will be used for RouterOS API connection.
required: false
type: bool
default: false
aliases:
- ssl
port:
description:
- RouterOS api port. If I(tls) is set, port will apply to TLS/SSL connection.
- Defaults are C(8728) for the HTTP API, and C(8729) for the HTTPS API.
type: int
validate_certs:
description:
- Set to C(false) to skip validation of TLS certificates.
- See also I(validate_cert_hostname). Only used when I(tls=true).
- B(Note:) instead of simply deactivating certificate validations to "make things work",
please consider creating your own CA certificate and using it to sign certificates used
for your router. You can tell the module about your CA certificate with the I(ca_path)
option.
type: bool
default: true
version_added: 1.2.0
validate_cert_hostname:
description:
- Set to C(true) to validate hostnames in certificates.
- See also I(validate_certs). Only used when I(tls=true) and I(validate_certs=true).
type: bool
default: false
version_added: 1.2.0
ca_path:
description:
- PEM formatted file that contains a CA certificate to be used for certificate validation.
- See also I(validate_cert_hostname). Only used when I(tls=true) and I(validate_certs=true).
type: path
version_added: 1.2.0
requirements:
- librouteros
- Python >= 3.6 (for librouteros)
seealso:
- ref: ansible_collections.community.routeros.docsite.api-guide
description: How to connect to RouterOS devices with the RouterOS API
'''

108
plugins/module_utils/api.py Normal file
View file

@ -0,0 +1,108 @@
# -*- coding: utf-8 -*-
# Copyright: (c) 2022, Felix Fontein (@felixfontein) <felix@fontein.de>
# Copyright: (c) 2020, Nikolay Dachev <nikolay@dachev.info>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
from ansible.module_utils.basic import missing_required_lib
from ansible.module_utils.common.text.converters import to_native
import re
import ssl
import traceback
LIB_IMP_ERR = None
try:
from librouteros import connect
from librouteros.exceptions import LibRouterosError
HAS_LIB = True
except Exception as e:
HAS_LIB = False
LIB_IMP_ERR = traceback.format_exc()
def check_has_library(module):
if not HAS_LIB:
module.fail_json(
msg=missing_required_lib('librouteros'),
exception=LIB_IMP_ERR,
)
def api_argument_spec():
return dict(
username=dict(type='str', required=True),
password=dict(type='str', required=True, no_log=True),
hostname=dict(type='str', required=True),
port=dict(type='int'),
tls=dict(type='bool', default=False, aliases=['ssl']),
validate_certs=dict(type='bool', default=True),
validate_cert_hostname=dict(type='bool', default=False),
ca_path=dict(type='path'),
)
def _ros_api_connect(module, username, password, host, port, use_tls, validate_certs, validate_cert_hostname, ca_path):
'''Connect to RouterOS API.'''
if not port:
if use_tls:
port = 8729
else:
port = 8728
try:
if use_tls:
ctx = ssl.create_default_context(cafile=ca_path)
wrap_context = ctx.wrap_socket
if not validate_certs:
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
elif not validate_cert_hostname:
ctx.check_hostname = False
else:
# Since librouteros doesn't pass server_hostname,
# we have to do this ourselves:
def wrap_context(*args, **kwargs):
kwargs.pop('server_hostname', None)
return ctx.wrap_socket(*args, server_hostname=host, **kwargs)
api = connect(
username=username,
password=password,
host=host,
ssl_wrapper=wrap_context,
port=port,
)
else:
api = connect(
username=username,
password=password,
host=host,
port=port,
)
except Exception as e:
connection = {
'username': username,
'hostname': host,
'port': port,
'ssl': use_tls,
'status': 'Error while connecting: %s' % to_native(e),
}
module.fail_json(msg=connection['status'], connection=connection)
return api
def create_api(module):
return _ros_api_connect(
module,
module.params['username'],
module.params['password'],
module.params['hostname'],
module.params['port'],
module.params['tls'],
module.params['validate_certs'],
module.params['validate_cert_hostname'],
module.params['ca_path'],
)

View file

@ -13,43 +13,14 @@ module: api
author: "Nikolay Dachev (@NikolayDachev)"
short_description: Ansible module for RouterOS API
description:
- Ansible module for RouterOS API with python librouteros.
- This module can add, remove, update, query and execute arbitrary command in routeros via API.
- Ansible module for RouterOS API with the Python C(librouteros) library.
- This module can add, remove, update, query and execute arbitrary command in RouterOS via API.
notes:
- I(add), I(remove), I(update), I(cmd) and I(query) are mutually exclusive.
- I(check_mode) is not supported.
requirements:
- librouteros
- Python >= 3.6 (for librouteros)
extends_documentation_fragment:
- community.routeros.api
options:
hostname:
description:
- RouterOS hostname API.
required: true
type: str
username:
description:
- RouterOS login user.
required: true
type: str
password:
description:
- RouterOS user password.
required: true
type: str
tls:
description:
- If is set TLS will be used for RouterOS API connection.
required: false
type: bool
default: false
aliases:
- ssl
port:
description:
- RouterOS api port. If I(tls) is set, port will apply to TLS/SSL connection.
- Defaults are C(8728) for the HTTP API, and C(8729) for the HTTPS API.
type: int
path:
description:
- Main path for all other arguments.
@ -95,33 +66,7 @@ options:
- Example path C(system script) and cmd C(run .id=*03) is equivalent in RouterOS CLI C(/system script run number=0).
- Example path C(ip address) and cmd C(print) is equivalent in RouterOS CLI C(/ip address print).
type: str
validate_certs:
description:
- Set to C(false) to skip validation of TLS certificates.
- See also I(validate_cert_hostname). Only used when I(tls=true).
- B(Note:) instead of simply deactivating certificate validations to "make things work",
please consider creating your own CA certificate and using it to sign certificates used
for your router. You can tell the module about your CA certificate with the I(ca_path)
option.
type: bool
default: true
version_added: 1.2.0
validate_cert_hostname:
description:
- Set to C(true) to validate hostnames in certificates.
- See also I(validate_certs). Only used when I(tls=true) and I(validate_certs=true).
type: bool
default: false
version_added: 1.2.0
ca_path:
description:
- PEM formatted file that contains a CA certificate to be used for certificate validation.
- See also I(validate_cert_hostname). Only used when I(tls=true) and I(validate_certs=true).
type: path
version_added: 1.2.0
seealso:
- ref: ansible_collections.community.routeros.docsite.api-guide
description: How to connect to RouterOS devices with the RouterOS API
- ref: ansible_collections.community.routeros.docsite.quoting
description: How to quote and unquote commands and arguments
'''
@ -275,58 +220,44 @@ from ansible_collections.community.routeros.plugins.module_utils.quoting import
split_routeros_command,
)
from ansible_collections.community.routeros.plugins.module_utils.api import (
api_argument_spec,
check_has_library,
create_api,
)
import re
import ssl
import traceback
LIB_IMP_ERR = None
try:
from librouteros import connect
from librouteros.exceptions import LibRouterosError
from librouteros.query import Key
HAS_LIB = True
except Exception as e:
HAS_LIB = False
LIB_IMP_ERR = traceback.format_exc()
except Exception:
# Handled in api module_utils
pass
class ROS_api_module:
def __init__(self):
module_args = dict(
username=dict(type='str', required=True),
password=dict(type='str', required=True, no_log=True),
hostname=dict(type='str', required=True),
port=dict(type='int'),
tls=dict(type='bool', default=False, aliases=['ssl']),
path=dict(type='str', required=True),
add=dict(type='str'),
remove=dict(type='str'),
update=dict(type='str'),
cmd=dict(type='str'),
query=dict(type='str'),
validate_certs=dict(type='bool', default=True),
validate_cert_hostname=dict(type='bool', default=False),
ca_path=dict(type='path'),
)
module_args.update(api_argument_spec())
self.module = AnsibleModule(argument_spec=module_args,
supports_check_mode=False,
mutually_exclusive=(('add', 'remove', 'update',
'cmd', 'query'),),)
if not HAS_LIB:
self.module.fail_json(msg=missing_required_lib("librouteros"),
exception=LIB_IMP_ERR)
check_has_library(self.module)
self.api = self.ros_api_connect(self.module.params['username'],
self.module.params['password'],
self.module.params['hostname'],
self.module.params['port'],
self.module.params['tls'],
self.module.params['validate_certs'],
self.module.params['validate_cert_hostname'],
self.module.params['ca_path'],
)
self.api = create_api(self.module)
self.path = self.module.params['path'].split()
self.add = self.module.params['add']
@ -497,49 +428,6 @@ class ROS_api_module:
self.result['message'].append("%s" % e)
self.return_result(False, False)
def ros_api_connect(self, username, password, host, port, use_tls, validate_certs, validate_cert_hostname, ca_path):
# connect to routeros api
conn_status = {"connection": {"username": username,
"hostname": host,
"port": port,
"ssl": use_tls,
"status": "Connected"}}
try:
if use_tls:
if not port:
port = 8729
conn_status["connection"]["port"] = port
ctx = ssl.create_default_context(cafile=ca_path)
wrap_context = ctx.wrap_socket
if not validate_certs:
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
elif not validate_cert_hostname:
ctx.check_hostname = False
else:
# Since librouteros doesn't pass server_hostname,
# we have to do this ourselves:
def wrap_context(*args, **kwargs):
kwargs.pop('server_hostname', None)
return ctx.wrap_socket(*args, server_hostname=host, **kwargs)
api = connect(username=username,
password=password,
host=host,
ssl_wrapper=wrap_context,
port=port)
else:
if not port:
port = 8728
conn_status["connection"]["port"] = port
api = connect(username=username,
password=password,
host=host,
port=port)
except Exception as e:
conn_status["connection"]["status"] = "error: %s" % e
self.module.fail_json(msg=to_native([conn_status]))
return api
def main():

View file

@ -0,0 +1,483 @@
#!/usr/bin/python
# Copyright: (c) 2022, Felix Fontein <felix@fontein.de>
# Copyright: (c) 2020, Nikolay Dachev <nikolay@dachev.info>
# Copyright: (c) 2018, Egor Zaitsev (@heuels)
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = '''
---
module: api_facts
author:
- "Egor Zaitsev (@heuels)"
- "Nikolay Dachev (@NikolayDachev)"
- "Felix Fontein (@felixfontein)"
version_added: 2.1.0
short_description: Collect facts from remote devices running MikroTik RouterOS using the API
description:
- Collects a base set of device facts from a remote device that
is running RouterOS. This module prepends all of the
base network fact keys with C(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.
- As opposed to the M(community.routeros.facts) module, it uses the
RouterOS API, similar to the M(community.routeros.api) module.
extends_documentation_fragment:
- community.routeros.api
options:
gather_subset:
description:
- When supplied, this argument will restrict the facts collected
to a given subset. Possible values for this argument include
C(all), C(hardware), C(interfaces), and C(routing).
- Can specify a list of values to include a larger subset.
Values can also be used with an initial C(!) to specify that a
specific subset should not be collected.
required: false
default: 'all'
type: list
elements: str
seealso:
- module: community.routeros.facts
- module: community.routeros.api
'''
EXAMPLES = """
- name: Collect all facts from the device
community.routeros.api_facts:
hostname: 192.168.88.1
username: admin
password: password
gather_subset: all
- name: Do not collect hardware facts
community.routeros.api_facts:
hostname: 192.168.88.1
username: admin
password: password
gather_subset:
- "!hardware"
"""
RETURN = """
ansible_facts:
description: "Dictionary of IP geolocation facts for a host's IP address."
returned: always
type: dict
contains:
ansible_net_gather_subset:
description: The list of fact subsets collected from the device.
returned: always
type: list
# default
ansible_net_model:
description: The model name returned from the device.
returned: I(gather_subset) contains C(default)
type: str
ansible_net_serialnum:
description: The serial number of the remote device.
returned: I(gather_subset) contains C(default)
type: str
ansible_net_version:
description: The operating system version running on the remote device.
returned: I(gather_subset) contains C(default)
type: str
ansible_net_hostname:
description: The configured hostname of the device.
returned: I(gather_subset) contains C(default)
type: str
ansible_net_arch:
description: The CPU architecture of the device.
returned: I(gather_subset) contains C(default)
type: str
ansible_net_uptime:
description: The uptime of the device.
returned: I(gather_subset) contains C(default)
type: str
ansible_net_cpu_load:
description: Current CPU load.
returned: I(gather_subset) contains C(default)
type: str
# hardware
ansible_net_spacefree_mb:
description: The available disk space on the remote device in MiB.
returned: I(gather_subset) contains C(hardware)
type: dict
ansible_net_spacetotal_mb:
description: The total disk space on the remote device in MiB.
returned: I(gather_subset) contains C(hardware)
type: dict
ansible_net_memfree_mb:
description: The available free memory on the remote device in MiB.
returned: I(gather_subset) contains C(hardware)
type: int
ansible_net_memtotal_mb:
description: The total memory on the remote device in MiB.
returned: I(gather_subset) contains C(hardware)
type: int
# interfaces
ansible_net_all_ipv4_addresses:
description: All IPv4 addresses configured on the device.
returned: I(gather_subset) contains C(interfaces)
type: list
ansible_net_all_ipv6_addresses:
description: All IPv6 addresses configured on the device.
returned: I(gather_subset) contains C(interfaces)
type: list
ansible_net_interfaces:
description: A hash of all interfaces running on the system.
returned: I(gather_subset) contains C(interfaces)
type: dict
ansible_net_neighbors:
description: The list of neighbors from the remote device.
returned: I(gather_subset) contains C(interfaces)
type: dict
# routing
ansible_net_bgp_peer:
description: A dictionary with BGP peer information.
returned: I(gather_subset) contains C(routing)
type: dict
ansible_net_bgp_vpnv4_route:
description: A dictionary with BGP vpnv4 route information.
returned: I(gather_subset) contains C(routing)
type: dict
ansible_net_bgp_instance:
description: A dictionary with BGP instance information.
returned: I(gather_subset) contains C(routing)
type: dict
ansible_net_route:
description: A dictionary for routes in all routing tables.
returned: I(gather_subset) contains C(routing)
type: dict
ansible_net_ospf_instance:
description: A dictionary with OSPF instances.
returned: I(gather_subset) contains C(routing)
type: dict
ansible_net_ospf_neighbor:
description: A dictionary with OSPF neighbors.
returned: I(gather_subset) contains C(routing)
type: dict
"""
import re
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.six import iteritems
from ansible.module_utils.common.text.converters import to_native
from ansible_collections.community.routeros.plugins.module_utils.api import (
api_argument_spec,
check_has_library,
create_api,
)
try:
from librouteros.exceptions import LibRouterosError
except Exception:
# Handled in api module_utils
pass
class FactsBase(object):
COMMANDS = []
def __init__(self, module, api):
self.module = module
self.api = api
self.facts = {}
self.responses = None
def populate(self):
self.responses = []
for path in self.COMMANDS:
self.responses.append(self.query_path(path))
def query_path(self, path):
api_path = self.api.path()
for part in path:
api_path = api_path.join(part)
try:
return list(api_path)
except LibRouterosError as e:
self.module.warn('Error while querying path {path}: {error}'.format(
path=' '.join(path),
error=to_native(e),
))
return []
class Default(FactsBase):
COMMANDS = [
['system', 'identity'],
['system', 'resource'],
['system', 'routerboard'],
]
def populate(self):
super(Default, self).populate()
data = self.responses[0]
if data:
self.facts['hostname'] = data[0].get('name')
data = self.responses[1]
if data:
self.facts['version'] = data[0].get('version')
self.facts['arch'] = data[0].get('architecture-name')
self.facts['uptime'] = data[0].get('uptime')
self.facts['cpu_load'] = data[0].get('cpu-load')
data = self.responses[2]
if data:
self.facts['model'] = data[0].get('model')
self.facts['serialnum'] = data[0].get('serial-number')
class Hardware(FactsBase):
COMMANDS = [
['system', 'resource'],
]
def populate(self):
super(Hardware, self).populate()
data = self.responses[0]
if data:
self.parse_filesystem_info(data[0])
self.parse_memory_info(data[0])
def parse_filesystem_info(self, data):
self.facts['spacefree_mb'] = self.to_megabytes(data.get('free-hdd-space'))
self.facts['spacetotal_mb'] = self.to_megabytes(data.get('total-hdd-space'))
def parse_memory_info(self, data):
self.facts['memfree_mb'] = self.to_megabytes(data.get('free-memory'))
self.facts['memtotal_mb'] = self.to_megabytes(data.get('total-memory'))
def to_megabytes(self, value):
if value is None:
return None
return float(value) / 1024 / 1024
class Interfaces(FactsBase):
COMMANDS = [
['interface'],
['ip', 'address'],
['ipv6', 'address'],
['ip', 'neighbor'],
]
def populate(self):
super(Interfaces, self).populate()
self.facts['interfaces'] = {}
self.facts['all_ipv4_addresses'] = []
self.facts['all_ipv6_addresses'] = []
self.facts['neighbors'] = []
data = self.responses[0]
if data:
interfaces = self.parse_interfaces(data)
self.populate_interfaces(interfaces)
data = self.responses[1]
if data:
data = self.parse_detail(data)
self.populate_addresses(data, 'ipv4')
data = self.responses[2]
if data:
data = self.parse_detail(data)
self.populate_addresses(data, 'ipv6')
data = self.responses[3]
if data:
self.facts['neighbors'] = list(self.parse_detail(data))
def populate_interfaces(self, data):
for key, value in iteritems(data):
self.facts['interfaces'][key] = value
def populate_addresses(self, data, family):
for value in data:
key = value['interface']
if family not in self.facts['interfaces'][key]:
self.facts['interfaces'][key][family] = []
addr, subnet = value['address'].split('/')
subnet = subnet.strip()
# Try to convert subnet to an integer
try:
subnet = int(subnet)
except Exception:
pass
ip = dict(address=addr.strip(), subnet=subnet)
self.add_ip_address(addr.strip(), family)
self.facts['interfaces'][key][family].append(ip)
def add_ip_address(self, address, family):
if family == 'ipv4':
self.facts['all_ipv4_addresses'].append(address)
else:
self.facts['all_ipv6_addresses'].append(address)
def parse_interfaces(self, data):
facts = {}
for entry in data:
if 'name' not in entry:
continue
entry.pop('.id', None)
facts[entry['name']] = entry
return facts
def parse_detail(self, data):
for entry in data:
if 'interface' not in entry:
continue
entry.pop('.id', None)
yield entry
class Routing(FactsBase):
COMMANDS = [
['routing', 'bgp', 'peer'],
['routing', 'bgp', 'vpnv4-route'],
['routing', 'bgp', 'instance'],
['ip', 'route'],
['routing', 'ospf', 'instance'],
['routing', 'ospf', 'neighbor'],
]
def populate(self):
super(Routing, self).populate()
self.facts['bgp_peer'] = {}
self.facts['bgp_vpnv4_route'] = {}
self.facts['bgp_instance'] = {}
self.facts['route'] = {}
self.facts['ospf_instance'] = {}
self.facts['ospf_neighbor'] = {}
data = self.responses[0]
if data:
peer = self.parse(data, 'name')
self.populate_result('bgp_peer', peer)
data = self.responses[1]
if data:
vpnv4 = self.parse(data, 'interface')
self.populate_result('bgp_vpnv4_route', vpnv4)
data = self.responses[2]
if data:
instance = self.parse(data, 'name')
self.populate_result('bgp_instance', instance)
data = self.responses[3]
if data:
route = self.parse(data, 'routing-mark', fallback='main')
self.populate_result('route', route)
data = self.responses[4]
if data:
instance = self.parse(data, 'name')
self.populate_result('ospf_instance', instance)
data = self.responses[5]
if data:
instance = self.parse(data, 'instance')
self.populate_result('ospf_neighbor', instance)
def parse(self, data, key, fallback=None):
facts = {}
for line in data:
name = line.get(key) or fallback
line.pop('.id', None)
facts[name] = line
return facts
def populate_result(self, name, data):
for key, value in iteritems(data):
self.facts[name][key] = value
FACT_SUBSETS = dict(
default=Default,
hardware=Hardware,
interfaces=Interfaces,
routing=Routing,
)
VALID_SUBSETS = frozenset(FACT_SUBSETS.keys())
warnings = []
def main():
argument_spec = dict(
gather_subset=dict(
default=['all'],
type='list',
elements='str',
)
)
argument_spec.update(api_argument_spec())
module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
check_has_library(module)
api = create_api(module)
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='Bad subset: %s' % 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 = {}
facts['gather_subset'] = sorted(runable_subsets)
instances = []
for key in runable_subsets:
instances.append(FACT_SUBSETS[key](module, api))
for inst in instances:
inst.populate()
facts.update(inst.facts)
ansible_facts = {}
for key, value in iteritems(facts):
key = 'ansible_net_%s' % key
ansible_facts[key] = value
module.exit_json(ansible_facts=ansible_facts, warnings=warnings)
if __name__ == '__main__':
main()

View file

@ -21,10 +21,10 @@ options:
description:
- When supplied, this argument will restrict the facts collected
to a given subset. Possible values for this argument include
C(all), C(hardware), C(config), and C(interfaces). Can specify a list of
values to include a larger subset. Values can also be used
with an initial C(!) to specify that a specific subset should
not be collected.
C(all), C(hardware), C(config), C(interfaces), and C(routing).
- Can specify a list of values to include a larger subset.
Values can also be used with an initial C(!) to specify that a
specific subset should not be collected.
required: false
default: '!config'
type: list
@ -52,67 +52,67 @@ EXAMPLES = """
RETURN = """
ansible_facts:
description: "Dictionary of ip geolocation facts for a host's IP address"
description: "Dictionary of IP geolocation facts for a host's IP address."
returned: always
type: dict
contains:
ansible_net_gather_subset:
description: The list of fact subsets collected from the device
description: The list of fact subsets collected from the device.
returned: always
type: list
# default
ansible_net_model:
description: The model name returned from the device
returned: always
description: The model name returned from the device.
returned: I(gather_subset) contains C(default)
type: str
ansible_net_serialnum:
description: The serial number of the remote device
returned: always
description: The serial number of the remote device.
returned: I(gather_subset) contains C(default)
type: str
ansible_net_version:
description: The operating system version running on the remote device
returned: always
description: The operating system version running on the remote device.
returned: I(gather_subset) contains C(default)
type: str
ansible_net_hostname:
description: The configured hostname of the device
returned: always
description: The configured hostname of the device.
returned: I(gather_subset) contains C(default)
type: str
ansible_net_arch:
description: The CPU architecture of the device
returned: always
description: The CPU architecture of the device.
returned: I(gather_subset) contains C(default)
type: str
ansible_net_uptime:
description: The uptime of the device
returned: always
description: The uptime of the device.
returned: I(gather_subset) contains C(default)
type: str
ansible_net_cpu_load:
description: Current CPU load
returned: always
description: Current CPU load.
returned: I(gather_subset) contains C(default)
type: str
# hardware
ansible_net_spacefree_mb:
description: The available disk space on the remote device in MiB
returned: when hardware is configured
description: The available disk space on the remote device in MiB.
returned: I(gather_subset) contains C(hardware)
type: dict
ansible_net_spacetotal_mb:
description: The total disk space on the remote device in MiB
returned: when hardware is configured
description: The total disk space on the remote device in MiB.
returned: I(gather_subset) contains C(hardware)
type: dict
ansible_net_memfree_mb:
description: The available free memory on the remote device in MiB
returned: when hardware is configured
description: The available free memory on the remote device in MiB.
returned: I(gather_subset) contains C(hardware)
type: int
ansible_net_memtotal_mb:
description: The total memory on the remote device in MiB
returned: when hardware is configured
description: The total memory on the remote device in MiB.
returned: I(gather_subset) contains C(hardware)
type: int
# config
ansible_net_config:
description: The current active config from the device
returned: when config is configured
description: The current active config from the device.
returned: I(gather_subset) contains C(config)
type: str
ansible_net_config_nonverbose:
@ -121,52 +121,52 @@ ansible_facts:
- This value is idempotent in the sense that if the facts module is run twice and the device's config
was not changed between the runs, the value is identical. This is achieved by running C(/export)
and stripping the timestamp from the comment in the first line.
returned: when config is configured
returned: I(gather_subset) contains C(config)
type: str
version_added: 1.2.0
# interfaces
ansible_net_all_ipv4_addresses:
description: All IPv4 addresses configured on the device
returned: when interfaces is configured
description: All IPv4 addresses configured on the device.
returned: I(gather_subset) contains C(interfaces)
type: list
ansible_net_all_ipv6_addresses:
description: All IPv6 addresses configured on the device
returned: when interfaces is configured
description: All IPv6 addresses configured on the device.
returned: I(gather_subset) contains C(interfaces)
type: list
ansible_net_interfaces:
description: A hash of all interfaces running on the system
returned: when interfaces is configured
description: A hash of all interfaces running on the system.
returned: I(gather_subset) contains C(interfaces)
type: dict
ansible_net_neighbors:
description: The list of neighbors from the remote device
returned: when interfaces is configured
description: The list of neighbors from the remote device.
returned: I(gather_subset) contains C(interfaces)
type: dict
# routing
ansible_net_bgp_peer:
description: The dict bgp peer
returned: peer information
description: A dictionary with BGP peer information.
returned: I(gather_subset) contains C(routing)
type: dict
ansible_net_bgp_vpnv4_route:
description: The dict bgp vpnv4 route
returned: vpnv4 route information
description: A dictionary with BGP vpnv4 route information.
returned: I(gather_subset) contains C(routing)
type: dict
ansible_net_bgp_instance:
description: The dict bgp instance
returned: bgp instance information
description: A dictionary with BGP instance information.
returned: I(gather_subset) contains C(routing)
type: dict
ansible_net_route:
description: The dict routes in all routing table
returned: routes information in all routing table
description: A dictionary for routes in all routing tables.
returned: I(gather_subset) contains C(routing)
type: dict
ansible_net_ospf_instance:
description: The dict ospf instance
returned: ospf instance information
description: A dictionary with OSPF instances.
returned: I(gather_subset) contains C(routing)
type: dict
ansible_net_ospf_neighbor:
description: The dict ospf neighbor
returned: ospf neighbor information
description: A dictionary with OSPF neighbors.
returned: I(gather_subset) contains C(routing)
type: dict
"""
import re

View file

@ -12,7 +12,7 @@
assert:
that:
- result is failed
- "'error: [Errno 111] Connection refused' in result.msg"
- "'Error while connecting: [Errno 111] Connection refused' == result.msg"
- name: Run command module
community.routeros.command:

View file

@ -0,0 +1,115 @@
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
class FakeLibRouterosError(Exception):
def __init__(self, message):
self.message = message
super(FakeLibRouterosError, self).__init__(self.message)
class TrapError(FakeLibRouterosError):
def __init__(self, message="failure: already have interface with such name"):
super(TrapError, self).__init__(message)
# fixtures
class fake_ros_api(object):
def __init__(self, api, path):
pass
def path(self, api, path):
fake_bridge = [{".id": "*DC", "name": "b2", "mtu": "auto", "actual-mtu": 1500,
"l2mtu": 65535, "arp": "enabled", "arp-timeout": "auto",
"mac-address": "3A:C1:90:D6:E8:44", "protocol-mode": "rstp",
"fast-forward": "true", "igmp-snooping": "false",
"auto-mac": "true", "ageing-time": "5m", "priority":
"0x8000", "max-message-age": "20s", "forward-delay": "15s",
"transmit-hold-count": 6, "vlan-filtering": "false",
"dhcp-snooping": "false", "running": "true", "disabled": "false"}]
return fake_bridge
def arbitrary(self, api, path):
def retr(self, *args, **kwargs):
if 'name' not in kwargs.keys():
raise TrapError(message="no such command")
dummy_test_string = '/interface/bridge add name=unit_test_brige_arbitrary'
result = "/%s/%s add name=%s" % (path[0], path[1], kwargs['name'])
return [result]
return retr
def add(self, name):
if name == "unit_test_brige_exist":
raise TrapError
return '*A1'
def remove(self, id):
if id != "*A1":
raise TrapError(message="no such item (4)")
return '*A1'
def update(self, **kwargs):
if kwargs['.id'] != "*A1" or 'name' not in kwargs.keys():
raise TrapError(message="no such item (4)")
return ["updated: {'.id': '%s' % kwargs['.id'], 'name': '%s' % kwargs['name']}"]
def select(self, *args):
dummy_bridge = [{".id": "*A1", "name": "dummy_bridge_A1"},
{".id": "*A2", "name": "dummy_bridge_A2"},
{".id": "*A3", "name": "dummy_bridge_A3"}]
result = []
for dummy in dummy_bridge:
found = {}
for search in args:
if search in dummy.keys():
found[search] = dummy[search]
else:
continue
if len(found.keys()) == 2:
result.append(found)
if result:
return result
else:
return ["no results for 'interface bridge 'query' %s" % ' '.join(args)]
def select_where(self, api, path):
api_path = Where()
return api_path
class Where(object):
def __init__(self):
pass
def select(self, *args):
return self
def where(self, *args):
return ["*A1"]
class Key(object):
def __init__(self, name):
self.name = name
self.str_return()
def str_return(self):
return str(self.name)

View file

@ -21,107 +21,11 @@ import json
import pytest
from ansible_collections.community.routeros.tests.unit.compat.mock import patch, MagicMock
from ansible_collections.community.routeros.tests.unit.plugins.modules.fake_api import FakeLibRouterosError, Key, fake_ros_api
from ansible_collections.community.routeros.tests.unit.plugins.modules.utils import set_module_args, AnsibleExitJson, AnsibleFailJson, ModuleTestCase
from ansible_collections.community.routeros.plugins.modules import api
class FakeLibRouterosError(Exception):
def __init__(self, message):
self.message = message
super(FakeLibRouterosError, self).__init__(self.message)
class TrapError(FakeLibRouterosError):
def __init__(self, message="failure: already have interface with such name"):
super(TrapError, self).__init__(message)
# fixtures
class fake_ros_api(object):
def __init__(self, api, path):
pass
def path(self, api, path):
fake_bridge = [{".id": "*DC", "name": "b2", "mtu": "auto", "actual-mtu": 1500,
"l2mtu": 65535, "arp": "enabled", "arp-timeout": "auto",
"mac-address": "3A:C1:90:D6:E8:44", "protocol-mode": "rstp",
"fast-forward": "true", "igmp-snooping": "false",
"auto-mac": "true", "ageing-time": "5m", "priority":
"0x8000", "max-message-age": "20s", "forward-delay": "15s",
"transmit-hold-count": 6, "vlan-filtering": "false",
"dhcp-snooping": "false", "running": "true", "disabled": "false"}]
return fake_bridge
def arbitrary(self, api, path):
def retr(self, *args, **kwargs):
if 'name' not in kwargs.keys():
raise TrapError(message="no such command")
dummy_test_string = '/interface/bridge add name=unit_test_brige_arbitrary'
result = "/%s/%s add name=%s" % (path[0], path[1], kwargs['name'])
return [result]
return retr
def add(self, name):
if name == "unit_test_brige_exist":
raise TrapError
return '*A1'
def remove(self, id):
if id != "*A1":
raise TrapError(message="no such item (4)")
return '*A1'
def update(self, **kwargs):
if kwargs['.id'] != "*A1" or 'name' not in kwargs.keys():
raise TrapError(message="no such item (4)")
return ["updated: {'.id': '%s' % kwargs['.id'], 'name': '%s' % kwargs['name']}"]
def select(self, *args):
dummy_bridge = [{".id": "*A1", "name": "dummy_bridge_A1"},
{".id": "*A2", "name": "dummy_bridge_A2"},
{".id": "*A3", "name": "dummy_bridge_A3"}]
result = []
for dummy in dummy_bridge:
found = {}
for search in args:
if search in dummy.keys():
found[search] = dummy[search]
else:
continue
if len(found.keys()) == 2:
result.append(found)
if result:
return result
else:
return ["no results for 'interface bridge 'query' %s" % ' '.join(args)]
def select_where(self, api, path):
api_path = Where()
return api_path
class Where(object):
def __init__(self):
pass
def select(self, *args):
return self
def where(self, *args):
return ["*A1"]
class Key(object):
def __init__(self, name):
self.name = name
self.str_return()
def str_return(self):
return str(self.name)
class TestRouterosApiModule(ModuleTestCase):
def setUp(self):
@ -130,12 +34,17 @@ class TestRouterosApiModule(ModuleTestCase):
self.module = api
self.module.LibRouterosError = FakeLibRouterosError
self.module.connect = MagicMock(new=fake_ros_api)
self.patch_create_api = patch('ansible_collections.community.routeros.plugins.modules.api.create_api', MagicMock(new=fake_ros_api))
self.patch_create_api.start()
self.module.Key = MagicMock(new=Key)
self.config_module_args = {"username": "admin",
"password": "pаss",
"hostname": "127.0.0.1",
"path": "interface bridge"}
def tearDown(self):
self.patch_create_api.stop()
def test_module_fail_when_required_args_missing(self):
with self.assertRaises(AnsibleFailJson) as exc:
set_module_args({})

View file

@ -0,0 +1,766 @@
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import json
import pytest
from ansible_collections.community.routeros.tests.unit.compat.mock import patch, MagicMock
from ansible_collections.community.routeros.tests.unit.plugins.modules.fake_api import FakeLibRouterosError, Key, fake_ros_api
from ansible_collections.community.routeros.tests.unit.plugins.modules.utils import set_module_args, AnsibleExitJson, AnsibleFailJson, ModuleTestCase
from ansible_collections.community.routeros.plugins.modules import api_facts
API_RESPONSES = {
('interface', ): [
{
'.id': '*1',
'name': 'first-ether',
'default-name': 'ether1',
'type': 'ether',
'mtu': 1500,
'actual-mtu': 1500,
'l2mtu': 1598,
'max-l2mtu': 4074,
'mac-address': '00:11:22:33:44:55',
'last-link-up-time': 'apr/22/2022 07:54:55',
'link-downs': 0,
'rx-byte': 1234,
'tx-byte': 1234,
'rx-packet': 1234,
'tx-packet': 1234,
'rx-drop': 1234,
'tx-drop': 1234,
'tx-queue-drop': 1234,
'rx-error': 1234,
'tx-error': 1234,
'fp-rx-byte': 1234,
'fp-tx-byte': 1234,
'fp-rx-packet': 1234,
'fp-tx-packet': 1234,
'running': True,
'disabled': False,
},
{
'.id': '*2',
'name': 'second-ether',
'default-name': 'ether2',
'type': 'ether',
'mtu': 1500,
'actual-mtu': 1500,
'l2mtu': 1598,
'max-l2mtu': 4074,
'mac-address': '00:11:22:33:44:66',
'last-link-up-time': 'apr/22/2022 07:54:55',
'link-downs': 0,
'rx-byte': 1234,
'tx-byte': 1234,
'rx-packet': 1234,
'tx-packet': 1234,
'rx-drop': 1234,
'tx-drop': 1234,
'tx-queue-drop': 1234,
'rx-error': 1234,
'tx-error': 1234,
'fp-rx-byte': 1234,
'fp-tx-byte': 1234,
'fp-rx-packet': 1234,
'fp-tx-packet': 1234,
'running': True,
'slave': True,
'disabled': False,
},
{
'.id': '*3',
'name': 'third-ether',
'default-name': 'ether3',
'type': 'ether',
'mtu': 1500,
'actual-mtu': 1500,
'l2mtu': 1598,
'max-l2mtu': 4074,
'mac-address': '00:11:22:33:44:77',
'last-link-up-time': 'apr/22/2022 07:54:55',
'link-downs': 0,
'rx-byte': 1234,
'tx-byte': 1234,
'rx-packet': 1234,
'tx-packet': 1234,
'rx-drop': 1234,
'tx-drop': 1234,
'tx-queue-drop': 1234,
'rx-error': 1234,
'tx-error': 1234,
'fp-rx-byte': 1234,
'fp-tx-byte': 1234,
'fp-rx-packet': 1234,
'fp-tx-packet': 1234,
'running': True,
'slave': True,
'disabled': False,
},
{
'.id': '*4',
'name': 'fourth-ether',
'default-name': 'ether4',
'type': 'ether',
'mtu': 1500,
'actual-mtu': 1500,
'l2mtu': 1598,
'max-l2mtu': 4074,
'mac-address': '00:11:22:33:44:88',
'last-link-down-time': 'apr/23/2022 08:22:50',
'last-link-up-time': 'apr/23/2022 08:22:52',
'link-downs': 2,
'rx-byte': 1234,
'tx-byte': 1234,
'rx-packet': 1234,
'tx-packet': 1234,
'rx-drop': 1234,
'tx-drop': 1234,
'tx-queue-drop': 1234,
'rx-error': 1234,
'tx-error': 1234,
'fp-rx-byte': 1234,
'fp-tx-byte': 1234,
'fp-rx-packet': 1234,
'fp-tx-packet': 1234,
'running': True,
'disabled': False,
},
{
'.id': '*5',
'name': 'fifth-ether',
'default-name': 'ether5',
'type': 'ether',
'mtu': 1500,
'actual-mtu': 1500,
'l2mtu': 1598,
'max-l2mtu': 4074,
'mac-address': '00:11:22:33:44:99',
'last-link-down-time': 'may/02/2022 18:12:32',
'last-link-up-time': 'may/02/2022 18:08:01',
'link-downs': 14,
'rx-byte': 1234,
'tx-byte': 1234,
'rx-packet': 1234,
'tx-packet': 1234,
'rx-drop': 1234,
'tx-drop': 1234,
'tx-queue-drop': 1234,
'rx-error': 1234,
'tx-error': 1234,
'fp-rx-byte': 1234,
'fp-tx-byte': 1234,
'fp-rx-packet': 1234,
'fp-tx-packet': 1234,
'running': False,
'slave': True,
'disabled': False,
},
{
'.id': '*7',
'name': 'my-bridge',
'type': 'bridge',
'mtu': 'auto',
'actual-mtu': 1500,
'l2mtu': 1598,
'mac-address': '00:11:22:33:44:66',
'last-link-up-time': 'apr/22/2022 07:54:48',
'link-downs': 0,
'rx-byte': 1234,
'tx-byte': 1234,
'rx-packet': 1234,
'tx-packet': 1234,
'rx-drop': 1234,
'tx-drop': 1234,
'tx-queue-drop': 1234,
'rx-error': 1234,
'tx-error': 1234,
'fp-rx-byte': 1234,
'fp-tx-byte': 1234,
'fp-rx-packet': 1234,
'fp-tx-packet': 1234,
'running': True,
'disabled': False,
},
],
('ip', 'address', ): [
{
'.id': '*1',
'address': '192.168.1.1/24',
'network': '192.168.1.0',
'interface': 'my-bridge',
'actual-interface': 'my-bridge',
'invalid': False,
'dynamic': False,
'disabled': False,
'comment': 'Wohnung',
},
{
'.id': '*5',
'address': '192.168.2.1/24',
'network': '192.168.2.0',
'interface': 'fourth-ether',
'actual-interface': 'fourth-ether',
'invalid': False,
'dynamic': False,
'disabled': False,
'comment': 'VoIP',
},
{
'.id': '*6',
'address': '1.2.3.4/21',
'network': '84.73.216.0',
'interface': 'first-ether',
'actual-interface': 'first-ether',
'invalid': False,
'dynamic': True,
'disabled': False,
},
],
('ipv6', 'address', ): [
{
'.id': '*1',
'address': 'fe80::1:2:3/64',
'from-pool': '',
'interface': 'my-bridge',
'actual-interface': 'my-bridge',
'eui-64': False,
'advertise': False,
'no-dad': False,
'invalid': False,
'dynamic': True,
'link-local': True,
'disabled': False,
},
{
'.id': '*2',
'address': 'fe80::1:2:4/64',
'from-pool': '',
'interface': 'fourth-ether',
'actual-interface': 'fourth-ether',
'eui-64': False,
'advertise': False,
'no-dad': False,
'invalid': False,
'dynamic': True,
'link-local': True,
'disabled': False,
},
{
'.id': '*3',
'address': 'fe80::1:2:5/64',
'from-pool': '',
'interface': 'first-ether',
'actual-interface': 'first-ether',
'eui-64': False,
'advertise': False,
'no-dad': False,
'invalid': False,
'dynamic': True,
'link-local': True,
'disabled': False,
},
],
('ip', 'neighbor', ): [],
('system', 'identity', ): [
{
'name': 'MikroTik',
},
],
('system', 'resource', ): [
{
'uptime': '2w3d4h5m6s',
'version': '6.49.6 (stable)',
'build-time': 'Apr/07/2022 17:53:31',
'free-memory': 12345678,
'total-memory': 23456789,
'cpu': 'MIPS 24Kc V7.4',
'cpu-count': 1,
'cpu-frequency': 400,
'cpu-load': 48,
'free-hdd-space': 123456789,
'total-hdd-space': 234567890,
'write-sect-since-reboot': 1234,
'write-sect-total': 12345,
'bad-blocks': 0,
'architecture-name': 'mipsbe',
'board-name': 'RB750GL',
'platform': 'MikroTik',
},
],
('system', 'routerboard', ): [
{
'routerboard': True,
'model': '750GL',
'serial-number': '0123456789AB',
'firmware-type': 'ar7240',
'factory-firmware': '3.09',
'current-firmware': '6.49.6',
'upgrade-firmware': '6.49.6',
},
],
('routing', 'bgp', 'peer', ): [],
('routing', 'bgp', 'vpnv4-route', ): [],
('routing', 'bgp', 'instance', ): [
{
'.id': '*0',
'name': 'default',
'as': 65530,
'router-id': '0.0.0.0',
'redistribute-connected': False,
'redistribute-static': False,
'redistribute-rip': False,
'redistribute-ospf': False,
'redistribute-other-bgp': False,
'out-filter': '',
'client-to-client-reflection': True,
'ignore-as-path-len': False,
'routing-table': '',
'default': True,
'disabled': False,
},
],
('ip', 'route', ): [
{
'.id': '*30000001',
'dst-address': '0.0.0.0/0',
'gateway': '1.2.3.0',
'gateway-status': '1.2.3.0 reachable via first-ether',
'distance': 1,
'scope': 30,
'target-scope': 10,
'vrf-interface': 'first-ether',
'active': True,
'dynamic': True,
'static': True,
'disabled': False,
},
{
'.id': '*40162F13',
'dst-address': '84.73.216.0/21',
'pref-src': '1.2.3.4',
'gateway': 'first-ether',
'gateway-status': 'first-ether reachable',
'distance': 0,
'scope': 10,
'active': True,
'dynamic': True,
'connect': True,
'disabled': False,
},
{
'.id': '*4016AA23',
'dst-address': '192.168.2.0/24',
'pref-src': '192.168.2.1',
'gateway': 'fourth-ether',
'gateway-status': 'fourth-ether reachable',
'distance': 0,
'scope': 10,
'active': True,
'dynamic': True,
'connect': True,
'disabled': False,
},
{
'.id': '*40168E05',
'dst-address': '192.168.1.0/24',
'pref-src': '192.168.1.1',
'gateway': 'my-bridge',
'gateway-status': 'my-bridge reachable',
'distance': 0,
'scope': 10,
'active': True,
'dynamic': True,
'connect': True,
'disabled': False,
},
],
('routing', 'ospf', 'instance', ): [
{
'.id': '*0',
'name': 'default',
'router-id': '0.0.0.0',
'distribute-default': 'never',
'redistribute-connected': False,
'redistribute-static': False,
'redistribute-rip': False,
'redistribute-bgp': False,
'redistribute-other-ospf': False,
'metric-default': 1,
'metric-connected': 20,
'metric-static': 20,
'metric-rip': 20,
'metric-bgp': 'auto',
'metric-other-ospf': 'auto',
'in-filter': 'ospf-in',
'out-filter': 'ospf-out',
'state': 'down',
'default': True,
'disabled': False,
},
],
('routing', 'ospf', 'neighbor', ): [],
}
class TestRouterosApiFactsModule(ModuleTestCase):
def setUp(self):
super(TestRouterosApiFactsModule, self).setUp()
librouteros = pytest.importorskip('librouteros')
self.module = api_facts
self.module.LibRouterosError = FakeLibRouterosError
self.module.connect = MagicMock(new=fake_ros_api)
self.patch_create_api = patch('ansible_collections.community.routeros.plugins.modules.api_facts.create_api', MagicMock(new=fake_ros_api))
self.patch_create_api.start()
self.patch_query_path = patch('ansible_collections.community.routeros.plugins.modules.api_facts.FactsBase.query_path', self.query_path)
self.patch_query_path.start()
self.module.Key = MagicMock(new=Key)
self.config_module_args = {
'username': 'admin',
'password': 'pаss',
'hostname': '127.0.0.1',
}
def tearDown(self):
self.patch_query_path.stop()
self.patch_create_api.stop()
def query_path(self, path):
response = API_RESPONSES.get(tuple(path))
if response is None:
raise Exception('Unexpected command: %s' % repr(path))
return response
def test_module_fail_when_required_args_missing(self):
with self.assertRaises(AnsibleFailJson) as exc:
set_module_args({})
self.module.main()
result = exc.exception.args[0]
self.assertEqual(result['failed'], True)
def test_module_fail_when_invalid_gather_subset(self):
with self.assertRaises(AnsibleFailJson) as exc:
module_args = self.config_module_args.copy()
module_args['gather_subset'] = ['!foobar']
set_module_args(module_args)
self.module.main()
result = exc.exception.args[0]
self.assertEqual(result['failed'], True)
self.assertEqual(result['msg'], 'Bad subset: foobar')
def test_full_run(self):
with self.assertRaises(AnsibleExitJson) as exc:
set_module_args(self.config_module_args.copy())
self.module.main()
result = exc.exception.args[0]
self.assertEqual(result['changed'], False)
self.assertEqual(result['ansible_facts']['ansible_net_all_ipv4_addresses'], [
'192.168.1.1',
'192.168.2.1',
'1.2.3.4',
])
self.assertEqual(result['ansible_facts']['ansible_net_all_ipv6_addresses'], [
'fe80::1:2:3',
'fe80::1:2:4',
'fe80::1:2:5',
])
self.assertEqual(result['ansible_facts']['ansible_net_arch'], 'mipsbe')
self.assertEqual(result['ansible_facts']['ansible_net_bgp_instance'], {
'default': {
'as': 65530,
'client-to-client-reflection': True,
'default': True,
'disabled': False,
'ignore-as-path-len': False,
'name': 'default',
'out-filter': '',
'redistribute-connected': False,
'redistribute-ospf': False,
'redistribute-other-bgp': False,
'redistribute-rip': False,
'redistribute-static': False,
'router-id': '0.0.0.0',
'routing-table': ''
},
})
self.assertEqual(result['ansible_facts']['ansible_net_bgp_peer'], {})
self.assertEqual(result['ansible_facts']['ansible_net_bgp_vpnv4_route'], {})
self.assertEqual(result['ansible_facts']['ansible_net_cpu_load'], 48)
self.assertEqual(result['ansible_facts']['ansible_net_gather_subset'], [
'default',
'hardware',
'interfaces',
'routing',
])
self.assertEqual(result['ansible_facts']['ansible_net_hostname'], 'MikroTik')
self.assertEqual(result['ansible_facts']['ansible_net_interfaces'], {
'my-bridge': {
'actual-mtu': 1500,
'disabled': False,
'fp-rx-byte': 1234,
'fp-rx-packet': 1234,
'fp-tx-byte': 1234,
'fp-tx-packet': 1234,
'ipv4': [
{
'address': '192.168.1.1',
'subnet': 24
}
],
'ipv6': [
{
'address': 'fe80::1:2:3',
'subnet': 64
}
],
'l2mtu': 1598,
'last-link-up-time': 'apr/22/2022 07:54:48',
'link-downs': 0,
'mac-address': '00:11:22:33:44:66',
'mtu': 'auto',
'name': 'my-bridge',
'running': True,
'rx-byte': 1234,
'rx-drop': 1234,
'rx-error': 1234,
'rx-packet': 1234,
'tx-byte': 1234,
'tx-drop': 1234,
'tx-error': 1234,
'tx-packet': 1234,
'tx-queue-drop': 1234,
'type': 'bridge'
},
'first-ether': {
'actual-mtu': 1500,
'default-name': 'ether1',
'disabled': False,
'fp-rx-byte': 1234,
'fp-rx-packet': 1234,
'fp-tx-byte': 1234,
'fp-tx-packet': 1234,
'ipv4': [
{
'address': '1.2.3.4',
'subnet': 21
}
],
'ipv6': [
{
'address': 'fe80::1:2:5',
'subnet': 64
}
],
'l2mtu': 1598,
'last-link-up-time': 'apr/22/2022 07:54:55',
'link-downs': 0,
'mac-address': '00:11:22:33:44:55',
'max-l2mtu': 4074,
'mtu': 1500,
'name': 'first-ether',
'running': True,
'rx-byte': 1234,
'rx-drop': 1234,
'rx-error': 1234,
'rx-packet': 1234,
'tx-byte': 1234,
'tx-drop': 1234,
'tx-error': 1234,
'tx-packet': 1234,
'tx-queue-drop': 1234,
'type': 'ether'
},
'second-ether': {
'actual-mtu': 1500,
'default-name': 'ether2',
'disabled': False,
'fp-rx-byte': 1234,
'fp-rx-packet': 1234,
'fp-tx-byte': 1234,
'fp-tx-packet': 1234,
'l2mtu': 1598,
'last-link-up-time': 'apr/22/2022 07:54:55',
'link-downs': 0,
'mac-address': '00:11:22:33:44:66',
'max-l2mtu': 4074,
'mtu': 1500,
'name': 'second-ether',
'running': True,
'rx-byte': 1234,
'rx-drop': 1234,
'rx-error': 1234,
'rx-packet': 1234,
'slave': True,
'tx-byte': 1234,
'tx-drop': 1234,
'tx-error': 1234,
'tx-packet': 1234,
'tx-queue-drop': 1234,
'type': 'ether'
},
'third-ether': {
'actual-mtu': 1500,
'default-name': 'ether3',
'disabled': False,
'fp-rx-byte': 1234,
'fp-rx-packet': 1234,
'fp-tx-byte': 1234,
'fp-tx-packet': 1234,
'l2mtu': 1598,
'last-link-up-time': 'apr/22/2022 07:54:55',
'link-downs': 0,
'mac-address': '00:11:22:33:44:77',
'max-l2mtu': 4074,
'mtu': 1500,
'name': 'third-ether',
'running': True,
'rx-byte': 1234,
'rx-drop': 1234,
'rx-error': 1234,
'rx-packet': 1234,
'slave': True,
'tx-byte': 1234,
'tx-drop': 1234,
'tx-error': 1234,
'tx-packet': 1234,
'tx-queue-drop': 1234,
'type': 'ether'
},
'fourth-ether': {
'actual-mtu': 1500,
'default-name': 'ether4',
'disabled': False,
'fp-rx-byte': 1234,
'fp-rx-packet': 1234,
'fp-tx-byte': 1234,
'fp-tx-packet': 1234,
'ipv4': [
{
'address': '192.168.2.1',
'subnet': 24
}
],
'ipv6': [
{
'address': 'fe80::1:2:4',
'subnet': 64
}
],
'l2mtu': 1598,
'last-link-down-time': 'apr/23/2022 08:22:50',
'last-link-up-time': 'apr/23/2022 08:22:52',
'link-downs': 2,
'mac-address': '00:11:22:33:44:88',
'max-l2mtu': 4074,
'mtu': 1500,
'name': 'fourth-ether',
'running': True,
'rx-byte': 1234,
'rx-drop': 1234,
'rx-error': 1234,
'rx-packet': 1234,
'tx-byte': 1234,
'tx-drop': 1234,
'tx-error': 1234,
'tx-packet': 1234,
'tx-queue-drop': 1234,
'type': 'ether'
},
'fifth-ether': {
'actual-mtu': 1500,
'default-name': 'ether5',
'disabled': False,
'fp-rx-byte': 1234,
'fp-rx-packet': 1234,
'fp-tx-byte': 1234,
'fp-tx-packet': 1234,
'l2mtu': 1598,
'last-link-down-time': 'may/02/2022 18:12:32',
'last-link-up-time': 'may/02/2022 18:08:01',
'link-downs': 14,
'mac-address': '00:11:22:33:44:99',
'max-l2mtu': 4074,
'mtu': 1500,
'name': 'fifth-ether',
'running': False,
'rx-byte': 1234,
'rx-drop': 1234,
'rx-error': 1234,
'rx-packet': 1234,
'slave': True,
'tx-byte': 1234,
'tx-drop': 1234,
'tx-error': 1234,
'tx-packet': 1234,
'tx-queue-drop': 1234,
'type': 'ether'
}
})
self.assertEqual(result['ansible_facts']['ansible_net_memfree_mb'], 12345678 / 1048576.0)
self.assertEqual(result['ansible_facts']['ansible_net_memtotal_mb'], 23456789 / 1048576.0)
self.assertEqual(result['ansible_facts']['ansible_net_model'], '750GL')
self.assertEqual(result['ansible_facts']['ansible_net_neighbors'], [])
self.assertEqual(result['ansible_facts']['ansible_net_ospf_instance'], {
'default': {
'default': True,
'disabled': False,
'distribute-default': 'never',
'in-filter': 'ospf-in',
'metric-bgp': 'auto',
'metric-connected': 20,
'metric-default': 1,
'metric-other-ospf': 'auto',
'metric-rip': 20,
'metric-static': 20,
'name': 'default',
'out-filter': 'ospf-out',
'redistribute-bgp': False,
'redistribute-connected': False,
'redistribute-other-ospf': False,
'redistribute-rip': False,
'redistribute-static': False,
'router-id': '0.0.0.0',
'state': 'down'
}
})
self.assertEqual(result['ansible_facts']['ansible_net_ospf_neighbor'], {})
self.assertEqual(result['ansible_facts']['ansible_net_route'], {
'main': {
'active': True,
'connect': True,
'disabled': False,
'distance': 0,
'dst-address': '192.168.1.0/24',
'dynamic': True,
'gateway': 'my-bridge',
'gateway-status': 'my-bridge reachable',
'pref-src': '192.168.1.1',
'scope': 10
}
})
self.assertEqual(result['ansible_facts']['ansible_net_serialnum'], '0123456789AB')
self.assertEqual(result['ansible_facts']['ansible_net_spacefree_mb'], 123456789 / 1048576.0)
self.assertEqual(result['ansible_facts']['ansible_net_spacetotal_mb'], 234567890 / 1048576.0)
self.assertEqual(result['ansible_facts']['ansible_net_uptime'], '2w3d4h5m6s')
self.assertEqual(result['ansible_facts']['ansible_net_version'], '6.49.6 (stable)')