diff --git a/.github/BOTMETA.yml b/.github/BOTMETA.yml index 621cf47315..298597fa20 100644 --- a/.github/BOTMETA.yml +++ b/.github/BOTMETA.yml @@ -353,6 +353,11 @@ files: $modules/utilities/logic/pause.py: samdoran $modules/utilities/logic/wait_for.py: gregswift $modules/web_infrastructure/ansible_tower/: $team_tower + $modules/web_infrastructure/sophos_utm/: + maintainers: $team_e-spirit + keywords: + - utm + - sophos $modules/web_infrastructure/django_manage.py: scottanderson42 $modules/web_infrastructure/htpasswd.py: $team_ansible $modules/web_infrastructure/jboss: $team_jboss @@ -604,6 +609,8 @@ files: maintainers: $team_vultr labels: - cloud + $module_utils/utm_utils.py: + maintainers: $team_e-spirit $module_utils/vmware: support: core maintainers: $team_vmware @@ -1126,6 +1133,7 @@ macros: team_scaleway: sieben hekonsek Spredzy abarbare anthony25 pilou- team_tower: ghjm jlaska matburt wwitzel3 simfarm ryanpetrello rooftopcellist AlanCoding team_ucs: dsoper2 johnamcdonough SDBrett vallard vvb dagwieers + team_e-spirit: MatrixCrawler getjack team_vmware: Akasurde dav1x warthog9 team_vultr: resmo Spredzy team_windows: dagwieers jborean93 jhawkesworth nitzmahone diff --git a/docs/docsite/rst/dev_guide/developing_module_utilities.rst b/docs/docsite/rst/dev_guide/developing_module_utilities.rst index 96fc58e818..379370670e 100644 --- a/docs/docsite/rst/dev_guide/developing_module_utilities.rst +++ b/docs/docsite/rst/dev_guide/developing_module_utilities.rst @@ -75,5 +75,6 @@ The following is a list of ``module_utils`` files and a general description. The - six/__init__.py - Bundled copy of the `Six Python library `_ to aid in writing code compatible with both Python 2 and Python 3. - splitter.py - String splitting and manipulation utilities for working with Jinja2 templates - urls.py - Utilities for working with http and https requests +- utm_utils.py - Contains base class for creating new Sophos UTM Modules and helper functions for handling the rest interface of Sophos UTM - vca.py - Contains utilities for modules that work with VMware vCloud Air - vmware.py - Contains utilities for modules that work with VMware vSphere VMs diff --git a/lib/ansible/module_utils/utm_utils.py b/lib/ansible/module_utils/utm_utils.py new file mode 100644 index 0000000000..30b362aa84 --- /dev/null +++ b/lib/ansible/module_utils/utm_utils.py @@ -0,0 +1,218 @@ +# This code is part of Ansible, but is an independent component. +# This particular file snippet, and this file snippet only, is BSD licensed. +# Modules you write using this snippet, which is embedded dynamically by Ansible +# still belong to the author of the module, and may assign their own license +# to the complete work. +# +# Copyright: (c) 2018, Johannes Brunswicker +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +import json + +from ansible.module_utils._text import to_native +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.urls import fetch_url + + +class UTMModuleConfigurationError(Exception): + + def __init__(self, msg, **args): + super(UTMModuleConfigurationError, self).__init__(self, msg) + self.msg = msg + self.module_fail_args = args + + def do_fail(self, module): + module.fail_json(msg=self.msg, other=self.module_fail_args) + + +class UTMModule(AnsibleModule): + """ + This is a helper class to construct any UTM Module. This will automatically add the utm host, port, token, + protocol, validate_certs and state field to the module. If you want to implement your own sophos utm module + just initialize this UTMModule class and define the Payload fields that are needed for your module. + See the other modules like utm_aaa_group for example. + """ + + def __init__(self, argument_spec, bypass_checks=False, no_log=False, check_invalid_arguments=None, + mutually_exclusive=None, required_together=None, required_one_of=None, add_file_common_args=False, + supports_check_mode=False, required_if=None): + default_specs = dict( + utm_host=dict(type='str', required=True), + utm_port=dict(type='int', default=4444), + utm_token=dict(type='str', required=True, no_log=True), + utm_protocol=dict(type='str', required=False, default="https", choices=["https", "http"]), + validate_certs=dict(type='bool', required=False, default=True), + state=dict(default='present', choices=['present', 'absent', 'info']) + ) + super(UTMModule, self).__init__(self._merge_specs(default_specs, argument_spec), bypass_checks, no_log, + check_invalid_arguments, mutually_exclusive, required_together, required_one_of, + add_file_common_args, supports_check_mode, required_if) + + def _merge_specs(self, default_specs, custom_specs): + result = default_specs.copy() + result.update(custom_specs) + return result + + +class UTM: + + def __init__(self, module, endpoint, change_relevant_keys, info_only=False): + """ + Initialize UTM Class + :param module: The Ansible module + :param endpoint: The corresponding endpoint to the module + :param change_relevant_keys: The keys of the object to check for changes + :param info_only: When implementing an info module, set this to true. Will allow access to the info method only + """ + self.info_only = info_only + self.module = module + self.request_url = module.params.get('utm_protocol') + "://" + module.params.get('utm_host') + ":" + to_native( + module.params.get('utm_port')) + "/api/objects/" + endpoint + "/" + + """ + The change_relevant_keys will be checked for changes to determine whether the object needs to be updated + """ + self.change_relevant_keys = change_relevant_keys + self.module.params['url_username'] = 'token' + self.module.params['url_password'] = module.params.get('utm_token') + if all(elem in self.change_relevant_keys for elem in module.params.keys()): + raise UTMModuleConfigurationError( + "The keys " + to_native( + self.change_relevant_keys) + " to check are not in the modules keys:\n" + to_native( + module.params.keys())) + + def execute(self): + try: + if not self.info_only: + if self.module.params.get('state') == 'present': + self._add() + elif self.module.params.get('state') == 'absent': + self._remove() + else: + self._info() + except Exception as e: + self.module.fail_json(msg=to_native(e)) + + def _info(self): + """ + returns the info for an object in utm + """ + info, result = self._lookup_entry(self.module, self.request_url) + if info["status"] >= 400: + self.module.fail_json(result=json.loads(info)) + else: + if result is None: + self.module.exit_json(changed=False) + else: + self.module.exit_json(result=result, changed=False) + + def _add(self): + """ + adds or updates a host object on utm + """ + is_changed = False + info, result = self._lookup_entry(self.module, self.request_url) + if info["status"] >= 400: + self.module.fail_json(result=json.loads(info)) + else: + data_as_json_string = self.module.jsonify(self.module.params) + if result is None: + response, info = fetch_url(self.module, self.request_url, method="POST", + headers={"Accept": "application/json", "Content-type": "application/json"}, + data=data_as_json_string) + if info["status"] >= 400: + self.module.fail_json(msg=json.loads(info["body"])) + is_changed = True + result = self._clean_result(json.loads(response.read())) + else: + if self._is_object_changed(self.change_relevant_keys, self.module, result): + response, info = fetch_url(self.module, self.request_url + result['_ref'], method="PUT", + headers={"Accept": "application/json", + "Content-type": "application/json"}, + data=data_as_json_string) + if info['status'] >= 400: + self.module.fail_json(msg=json.loads(info["body"])) + is_changed = True + result = self._clean_result(json.loads(response.read())) + self.module.exit_json(result=result, changed=is_changed) + + def _remove(self): + """ + removes an object from utm + """ + is_changed = False + info, result = self._lookup_entry(self.module, self.request_url) + if result is not None: + response, info = fetch_url(self.module, self.request_url + result['_ref'], method="DELETE", + headers={"Accept": "application/json", "X-Restd-Err-Ack": "all"}, + data=self.module.jsonify(self.module.params)) + if info["status"] >= 400: + self.module.fail_json(msg=json.loads(info["body"])) + else: + is_changed = True + self.module.exit_json(changed=is_changed) + + def _lookup_entry(self, module, request_url): + """ + Lookup for existing entry + :param module: + :param request_url: + :return: + """ + response, info = fetch_url(module, request_url, method="GET", headers={"Accept": "application/json"}) + result = None + if response is not None: + results = json.loads(response.read()) + result = next(iter(filter(lambda d: d['name'] == module.params.get('name'), results)), None) + return info, result + + def _clean_result(self, result): + """ + Will clean the result from irrelevant fields + :param result: The result from the query + :return: The modified result + """ + del result['utm_host'] + del result['utm_port'] + del result['utm_token'] + del result['utm_protocol'] + del result['validate_certs'] + del result['url_username'] + del result['url_password'] + del result['state'] + return result + + def _is_object_changed(self, keys, module, result): + """ + Check if my object is changed + :param keys: The keys that will determine if an object is changed + :param module: The module + :param result: The result from the query + :return: + """ + for key in keys: + if module.params.get(key) != result[key]: + return True + return False diff --git a/lib/ansible/modules/web_infrastructure/sophos_utm/__init__.py b/lib/ansible/modules/web_infrastructure/sophos_utm/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lib/ansible/modules/web_infrastructure/sophos_utm/utm_dns_host.py b/lib/ansible/modules/web_infrastructure/sophos_utm/utm_dns_host.py new file mode 100644 index 0000000000..f5d479df99 --- /dev/null +++ b/lib/ansible/modules/web_infrastructure/sophos_utm/utm_dns_host.py @@ -0,0 +1,156 @@ +#!/usr/bin/python + +# Copyright: (c) 2018, Johannes Brunswicker +# 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 + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community' +} + +DOCUMENTATION = """ +--- +module: utm_dns_host + +author: + - Johannes Brunswicker (@MatrixCrawler) + +short_description: create, update or destroy dns entry in Sophos UTM + +description: + - Create, update or destroy a dns entry in SOPHOS UTM. + - This module needs to have the REST Ability of the UTM to be activated. + +version_added: "2.8" + +options: + name: + description: + - The name of the object. Will be used to identify the entry + required: true + address: + description: + - The IPV4 Address of the entry. Can be left empty for automatic resolving. + default: 0.0.0.0 + address6: + description: + - The IPV6 Address of the entry. Can be left empty for automatic resolving. + default: "::" + comment: + description: + - An optional comment to add to the dns host object + hostname: + description: + - The hostname for the dns host object + interface: + description: + - The reference name of the interface to use. If not provided the default interface will be used + resolved: + description: + - whether the hostname's ipv4 address is already resolved or not + default: False + type: bool + resolved6: + description: + - whether the hostname's ipv6 address is already resolved or not + default: False + type: bool + timeout: + description: + - the timeout for the utm to resolve the ip address for the hostname again + default: 0 + +extends_documentation_fragment: + - utm +""" + +EXAMPLES = """ +- name: Create UTM dns host entry + utm_dns_host: + utm_host: sophos.host.name + utm_token: abcdefghijklmno1234 + name: TestDNSEntry + hostname: testentry.some.tld + state: present + +- name: Remove UTM dns host entry + utm_dns_host: + utm_host: sophos.host.name + utm_token: abcdefghijklmno1234 + name: TestDNSEntry + state: absent +""" + +RETURN = """ +result: + description: The utm object that was created + returned: success + type: complex + contains: + _ref: + description: The reference name of the object + type: string + _locked: + description: Whether or not the object is currently locked + type: boolean + name: + description: The name of the object + type: string + address: + description: The ipv4 address of the object + type: string + address6: + description: The ipv6 adress of the object + type: string + comment: + description: The comment string + type: string + hostname: + description: The hostname of the object + type: string + interface: + description: The reference name of the interface the object is associated with + type: string + resolved: + description: Whether the ipv4 address is resolved or not + type: boolean + resolved6: + description: Whether the ipv6 address is resolved or not + type: boolean + timeout: + description: The timeout until a new resolving will be attempted + type: int +""" + +from ansible.module_utils.utm_utils import UTM, UTMModule +from ansible.module_utils._text import to_native + + +def main(): + endpoint = "network/dns_host" + key_to_check_for_changes = ["comment", "hostname", "interface"] + module = UTMModule( + argument_spec=dict( + name=dict(type='str', required=True), + address=dict(type='str', required=False, default='0.0.0.0'), + address6=dict(type='str', required=False, default='::'), + comment=dict(type='str', required=False, default=""), + hostname=dict(type='str', required=False), + interface=dict(type='str', required=False, default=""), + resolved=dict(type='bool', required=False, default=False), + resolved6=dict(type='bool', required=False, default=False), + timeout=dict(type='int', required=False, default=0), + ) + ) + try: + UTM(module, endpoint, key_to_check_for_changes).execute() + except Exception as e: + module.fail_json(msg=to_native(e)) + + +if __name__ == '__main__': + main() diff --git a/lib/ansible/utils/module_docs_fragments/utm.py b/lib/ansible/utils/module_docs_fragments/utm.py new file mode 100644 index 0000000000..980d3ae76c --- /dev/null +++ b/lib/ansible/utils/module_docs_fragments/utm.py @@ -0,0 +1,44 @@ +# Copyright: (c) 2018, Johannes Brunswicker +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + + +class ModuleDocFragment(object): + DOCUMENTATION = """ +options: + utm_host: + description: + - The REST Endpoint of the Sophos UTM. + required: true + utm_port: + description: + - The port of the REST interface. + default: 4444 + utm_token: + description: + - "The token used to identify at the REST-API. See U(https://www.sophos.com/en-us/medialibrary/\ + PDFs/documentation/UTMonAWS/Sophos-UTM-RESTful-API.pdf?la=en), Chapter 2.4.2." + required: true + utm_protocol: + description: + - The protocol of the REST Endpoint. + choices: + - https + - http + default: https + validate_certs: + description: + - Whether the REST interface's ssl certificate should be verified or not. + default: True + type: bool + state: + description: + - The desired state of the object. + - C(present) will create or update an object + - C(absent) will delete an object if it was present + - C(info) will return the object details + choices: + - present + - absent + - info + default: present +"""