diff --git a/lib/ansible/module_utils/network/fortios/argspec/__init__.py b/lib/ansible/module_utils/network/fortios/argspec/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/fortios/argspec/facts/__init__.py b/lib/ansible/module_utils/network/fortios/argspec/facts/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/fortios/argspec/facts/facts.py b/lib/ansible/module_utils/network/fortios/argspec/facts/facts.py new file mode 100644 index 00000000000..2f3e341810e --- /dev/null +++ b/lib/ansible/module_utils/network/fortios/argspec/facts/facts.py @@ -0,0 +1,45 @@ +from __future__ import (absolute_import, division, print_function) +# Copyright 2019 Fortinet, Inc. +# +# This program 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. +# +# This program 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 this program. If not, see . + +__metaclass__ = type + +""" +The arg spec for the fortios monitor module. +""" + + +class FactsArgs(object): + """ The arg spec for the fortios monitor module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = { + "host": {"required": False, "type": "str"}, + "username": {"required": False, "type": "str"}, + "password": {"required": False, "type": "str", "no_log": True}, + "vdom": {"required": False, "type": "str", "default": "root"}, + "https": {"required": False, "type": "bool", "default": True}, + "ssl_verify": {"required": False, "type": "bool", "default": False}, + "gather_subset": { + "required": True, "type": "list", "elements": "dict", + "options": { + "fact": {"required": True, "type": "str"}, + "filters": {"required": False, "type": "list", "elements": "dict"} + } + } + } diff --git a/lib/ansible/module_utils/network/fortios/argspec/system/__init__.py b/lib/ansible/module_utils/network/fortios/argspec/system/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/fortios/argspec/system/system.py b/lib/ansible/module_utils/network/fortios/argspec/system/system.py new file mode 100644 index 00000000000..76454f9d9ec --- /dev/null +++ b/lib/ansible/module_utils/network/fortios/argspec/system/system.py @@ -0,0 +1,28 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Fortinet, Inc. +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +The arg spec for the fortios_facts module +""" +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class SystemArgs(object): + """The arg spec for the fortios_facts module + """ + + FACT_SYSTEM_SUBSETS = frozenset([ + 'system_current-admins_select', + 'system_firmware_select', + 'system_fortimanager_status', + 'system_ha-checksums_select', + 'system_interface_select', + 'system_status_select', + 'system_time_select', + ]) + + def __init__(self, **kwargs): + pass diff --git a/lib/ansible/module_utils/network/fortios/facts/__init__.py b/lib/ansible/module_utils/network/fortios/facts/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/fortios/facts/facts.py b/lib/ansible/module_utils/network/fortios/facts/facts.py new file mode 100644 index 00000000000..a881b5aeda1 --- /dev/null +++ b/lib/ansible/module_utils/network/fortios/facts/facts.py @@ -0,0 +1,92 @@ +from __future__ import (absolute_import, division, print_function) +# Copyright 2019 Fortinet, Inc. +# +# This program 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. +# +# This program 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 this program. If not, see . + +__metaclass__ = type + +""" +The facts class for fortios +this file validates each subset of monitor and selectively +calls the appropriate facts gathering and monitoring function +""" + +from ansible.module_utils.network.fortios.argspec.facts.facts import FactsArgs +from ansible.module_utils.network.fortios.argspec.system.system import SystemArgs +from ansible.module_utils.network.common.facts.facts import FactsBase +from ansible.module_utils.network.fortios.facts.system.system import SystemFacts + + +class Facts(FactsBase): + """ The facts class for fortios + """ + + FACT_SUBSETS = { + "system": SystemFacts + } + + def __init__(self, module, fos=None, subset=None): + super(Facts, self).__init__(module) + self._fos = fos + self._subset = subset + + def gen_runable(self, subsets, valid_subsets): + """ Generate the runable subset + + :param module: The module instance + :param subsets: The provided subsets + :param valid_subsets: The valid subsets + :rtype: list + :returns: The runable subsets + """ + runable_subsets = [] + FACT_DETAIL_SUBSETS = [] + FACT_DETAIL_SUBSETS.extend(SystemArgs.FACT_SYSTEM_SUBSETS) + + for subset in subsets: + if subset['fact'] not in FACT_DETAIL_SUBSETS: + self._module.fail_json(msg='Subset must be one of [%s], got %s' % + (', '.join(sorted([item for item in FACT_DETAIL_SUBSETS])), subset['fact'])) + + for valid_subset in frozenset(self.FACT_SUBSETS.keys()): + if subset['fact'].startswith(valid_subset): + runable_subsets.append((subset, valid_subset)) + + return runable_subsets + + def get_network_legacy_facts(self, fact_legacy_obj_map, legacy_facts_type=None): + if not legacy_facts_type: + legacy_facts_type = self._gather_subset + + runable_subsets = self.gen_runable(legacy_facts_type, frozenset(fact_legacy_obj_map.keys())) + if runable_subsets: + self.ansible_facts['ansible_net_gather_subset'] = [] + + instances = list() + for (subset, valid_subset) in runable_subsets: + instances.append(fact_legacy_obj_map[valid_subset](self._module, self._fos, subset)) + + for inst in instances: + inst.populate_facts(self._connection, self.ansible_facts) + + def get_facts(self, facts_type=None, data=None): + """ Collect the facts for fortios + :param facts_type: List of facts types + :param data: previously collected conf + :rtype: dict + :return: the facts gathered + """ + self.get_network_legacy_facts(self.FACT_SUBSETS, facts_type) + + return self.ansible_facts, self._warnings diff --git a/lib/ansible/module_utils/network/fortios/facts/system/__init__.py b/lib/ansible/module_utils/network/fortios/facts/system/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/fortios/facts/system/system.py b/lib/ansible/module_utils/network/fortios/facts/system/system.py new file mode 100644 index 00000000000..5731a0985b1 --- /dev/null +++ b/lib/ansible/module_utils/network/fortios/facts/system/system.py @@ -0,0 +1,63 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Fortinet, Inc. +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +The fortios system facts class +It is in this file the runtime information is collected from the device +for a given resource, parsed, and the facts tree is populated +based on the configuration. +""" +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import re +from ansible.module_utils.network.common import utils +from ansible.module_utils.network.fortios.argspec.system.system import SystemArgs + + +class SystemFacts(object): + """ The fortios system facts class + """ + + def __init__(self, module, fos=None, subset=None, subspec='config', options='options'): + self._module = module + self._fos = fos + self._subset = subset + + def populate_facts(self, connection, ansible_facts, data=None): + """ Populate the facts for system + :param connection: the device connection + :param ansible_facts: Facts dictionary + :rtype: dictionary + :returns: facts + """ + ansible_facts['ansible_network_resources'].pop('system', None) + facts = {} + if self._subset['fact'].startswith(tuple(SystemArgs.FACT_SYSTEM_SUBSETS)): + gather_method = getattr(self, self._subset['fact'].replace('-', '_'), self.system_fact) + resp = gather_method() + facts.update({self._subset['fact']: resp}) + + ansible_facts['ansible_network_resources'].update(facts) + return ansible_facts + + def system_fact(self): + fos = self._fos + vdom = self._module.params['vdom'] + return fos.monitor('system', self._subset['fact'][len('system_'):].replace('_', '/'), vdom=vdom) + + def system_interface_select(self): + fos = self._fos + vdom = self._module.params['vdom'] + + query_string = '?vdom=' + vdom + system_interface_select_param = self._subset['filters'] + if system_interface_select_param: + for filter in system_interface_select_param: + for key, val in filter.items(): + if val: + query_string += '&' + str(key) + '=' + str(val) + + return fos.monitor('system', self._subset['fact'][len('system_'):].replace('_', '/') + query_string, vdom=None) diff --git a/lib/ansible/modules/network/fortios/fortios_facts.py b/lib/ansible/modules/network/fortios/fortios_facts.py new file mode 100644 index 00000000000..72527e82b08 --- /dev/null +++ b/lib/ansible/modules/network/fortios/fortios_facts.py @@ -0,0 +1,282 @@ +#!/usr/bin/python +from __future__ import (absolute_import, division, print_function) +# Copyright 2019 Fortinet, Inc. +# +# This program 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. +# +# This program 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 this program. If not, see . + +__metaclass__ = type + +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'metadata_version': '1.1'} + +DOCUMENTATION = ''' +--- +module: fortios_facts +version_added: "2.9" +short_description: Get facts about fortios devices. +description: + - Collects facts from network devices running the fortios operating + system. This module places the facts gathered in the fact tree keyed by the + respective resource name. This facts module will only collect those + facts which user specified in playbook. +author: + - Don Yao (@fortinetps) + - Miguel Angel Munoz (@mamunozgonzalez) + - Nicolas Thomas (@thomnico) +notes: + - Support both legacy mode (local_action) and httpapi + - Legacy mode run as a local_action in your playbook, requires fortiosapi library developed by Fortinet + - httpapi mode is the new recommend way for network modules +requirements: + - fortiosapi>=0.9.8 +options: + host: + description: + - FortiOS or FortiGate IP address. + type: str + required: false + username: + description: + - FortiOS or FortiGate username. + type: str + required: false + password: + description: + - FortiOS or FortiGate password. + type: str + default: "" + required: false + vdom: + description: + - Virtual domain, among those defined previously. A vdom is a + virtual instance of the FortiGate that can be configured and + used as a different unit. + type: str + default: root + required: false + https: + description: + - Indicates if the requests towards FortiGate must use HTTPS protocol. + type: bool + default: true + required: false + ssl_verify: + description: + - Ensures FortiGate certificate must be verified by a proper CA. + type: bool + default: false + required: false + gather_subset: + description: + - When supplied, this argument will restrict the facts collected + to a given subset. Possible values for this argument include + system_current-admins_select, system_firmware_select, + system_fortimanager_status, system_ha-checksums_select, + system_interface_select, system_status_select and system_time_select + type: list + elements: dict + required: true + suboptions: + fact: + description: + - Name of the facts to gather + type: str + required: true + filters: + description: + - Filters apply when gathering facts + type: list + elements: dict + required: false +''' + +EXAMPLES = ''' +- hosts: localhost + vars: + host: "192.168.122.40" + username: "admin" + password: "" + vdom: "root" + ssl_verify: "False" + + tasks: + - name: gather basic system status facts + fortios_facts: + host: "{{ host }}" + username: "{{ username }}" + password: "{{ password }}" + vdom: "{{ vdom }}" + gather_subset: + - fact: 'system_status_select' + + - name: gather all pysical interfaces status facts + fortios_facts: + host: "{{ host }}" + username: "{{ username }}" + password: "{{ password }}" + vdom: "{{ vdom }}" + gather_subset: + - fact: 'system_interface_select' + + - name: gather gather all pysical and vlan interfaces status facts + fortios_facts: + host: "{{ host }}" + username: "{{ username }}" + password: "{{ password }}" + vdom: "{{ vdom }}" + gather_subset: + - fact: 'system_interface_select' + filters: + - include_vlan: true + + - name: gather basic system info and physical interface port3 status facts + fortios_facts: + host: "{{ host }}" + username: "{{ username }}" + password: "{{ password }}" + vdom: "{{ vdom }}" + gather_subset: + - fact: 'system_status_select' + - fact: 'system_interface_select' + filters: + - interface_name: 'port3' +''' + +RETURN = ''' +build: + description: Build number of the fortigate image + returned: always + type: str + sample: '1547' +http_method: + description: Last method used to provision the content into FortiGate + returned: always + type: str + sample: 'GET' +name: + description: Name of the table used to fulfill the request + returned: always + type: str + sample: "firmware" +path: + description: Path of the table used to fulfill the request + returned: always + type: str + sample: "system" +revision: + description: Internal revision number + returned: always + type: str + sample: "17.0.2.10658" +serial: + description: Serial number of the unit + returned: always + type: str + sample: "FGVMEVYYQT3AB5352" +status: + description: Indication of the operation's result + returned: always + type: str + sample: "success" +vdom: + description: Virtual domain used + returned: always + type: str + sample: "root" +version: + description: Version of the FortiGate + returned: always + type: str + sample: "v5.6.3" +ansible_facts: + description: The list of fact subsets collected from the device + returned: always + type: dict + +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.connection import Connection +from ansible.module_utils.network.fortios.fortios import FortiOSHandler +from ansible.module_utils.network.fortimanager.common import FAIL_SOCKET_MSG +from ansible.module_utils.network.fortios.argspec.facts.facts import FactsArgs +from ansible.module_utils.network.fortios.facts.facts import Facts + + +def login(data, fos): + host = data['host'] + username = data['username'] + password = data['password'] + ssl_verify = data['ssl_verify'] + + fos.debug('on') + if 'https' in data and not data['https']: + fos.https('off') + else: + fos.https('on') + + fos.login(host, username, password, verify=ssl_verify) + + +def main(): + """ Main entry point for AnsibleModule + """ + argument_spec = FactsArgs.argument_spec + + module = AnsibleModule(argument_spec=argument_spec, + supports_check_mode=False) + + # legacy_mode refers to using fortiosapi instead of HTTPAPI + legacy_mode = 'host' in module.params and module.params['host'] is not None and \ + 'username' in module.params and module.params['username'] is not None and \ + 'password' in module.params and module.params['password'] is not None + + if not legacy_mode: + if module._socket_path: + warnings = [] + connection = Connection(module._socket_path) + module._connection = connection + fos = FortiOSHandler(connection) + + result = Facts(module, fos).get_facts() + + ansible_facts, additional_warnings = result + warnings.extend(additional_warnings) + + module.exit_json(ansible_facts=ansible_facts, warnings=warnings) + else: + module.fail_json(**FAIL_SOCKET_MSG) + else: + try: + from fortiosapi import FortiOSAPI + except ImportError: + module.fail_json(msg="fortiosapi module is required") + + warnings = [] + + fos = FortiOSAPI() + login(module.params, fos) + module._connection = fos + + result = Facts(module, fos).get_facts() + + ansible_facts, additional_warnings = result + warnings.extend(additional_warnings) + + module.exit_json(ansible_facts=ansible_facts, warnings=warnings) + + +if __name__ == '__main__': + main() diff --git a/test/units/modules/network/fortios/test_fortios_facts.py b/test/units/modules/network/fortios/test_fortios_facts.py new file mode 100644 index 00000000000..8b9a4145d72 --- /dev/null +++ b/test/units/modules/network/fortios/test_fortios_facts.py @@ -0,0 +1,103 @@ +# Copyright 2019 Fortinet, Inc. +# +# This program 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import os +import json +import pytest +from mock import ANY +from units.modules.utils import exit_json, fail_json +from units.compat import unittest +from units.compat.mock import patch +from ansible.module_utils import basic +from ansible.module_utils.network.fortios.fortios import FortiOSHandler + +try: + from ansible.module_utils.network.fortios.facts.facts import Facts + from ansible.modules.network.fortios import fortios_facts +except ImportError: + pytest.skip("Could not load required modules for testing", allow_module_level=True) + + +@pytest.fixture(autouse=True) +def connection_mock(mocker): + connection_class_mock = mocker.patch('ansible.modules.network.fortios.fortios_facts.Connection') + return connection_class_mock + + +fos_instance = FortiOSHandler(connection_mock) + + +def test_facts_get(mocker): + monitor_method_result = {'status': 'success', 'http_method': 'GET', 'http_status': 200} + monitor_method_mock = mocker.patch('ansible.module_utils.network.fortios.fortios.FortiOSHandler.monitor', return_value=monitor_method_result) + mock_module = patch.multiple(basic.AnsibleModule, exit_json=exit_json, fail_json=fail_json) + mock_module._connection = connection_mock + + # test case 01, args with single gather_subset + args = { + 'vdom': 'root', + 'gather_subset': [ + {'fact': 'system_status_select'}, + ] + } + mock_module.params = args + + response, ignore = Facts(mock_module, fos_instance).get_facts() + + monitor_method_mock.assert_called_with('system', 'status/select', vdom='root') + assert response['ansible_network_resources']['system_status_select']['status'] == 'success' + assert response['ansible_network_resources']['system_status_select']['http_status'] == 200 + + # test case 02, args with single gather_subset with filters + args = { + 'vdom': 'root', + 'gather_subset': [ + {'fact': 'system_interface_select', 'filters': [{'include_vlan': 'true'}, {'interface_name': 'port3'}]}, + ] + } + + mock_module.params = args + + response, ignore = Facts(mock_module, fos_instance).get_facts() + + monitor_method_mock.assert_called_with('system', 'interface/select?vdom=root&include_vlan=true&interface_name=port3', vdom=None) + assert response['ansible_network_resources']['system_interface_select']['status'] == 'success' + assert response['ansible_network_resources']['system_interface_select']['http_status'] == 200 + + # test case 03, args with multiple gather_subset + args = { + 'vdom': 'root', + 'gather_subset': [ + {'fact': 'system_current-admins_select'}, + {'fact': 'system_firmware_select'}, + {'fact': 'system_fortimanager_status'}, + {'fact': 'system_ha-checksums_select'}, + ] + } + + mock_module.params = args + + response, ignore = Facts(mock_module, fos_instance).get_facts() + + monitor_method_mock.assert_any_call('system', 'current-admins/select', vdom='root') + monitor_method_mock.assert_any_call('system', 'firmware/select', vdom='root') + monitor_method_mock.assert_any_call('system', 'fortimanager/status', vdom='root') + monitor_method_mock.assert_any_call('system', 'ha-checksums/select', vdom='root') + assert response['ansible_network_resources']['system_ha-checksums_select']['status'] == 'success' + assert response['ansible_network_resources']['system_ha-checksums_select']['http_status'] == 200