From cb4153c28572f7a27d38dda12e30f45195809f7c Mon Sep 17 00:00:00 2001 From: Matthew Krupcale Date: Sun, 4 Dec 2016 05:43:35 -0500 Subject: [PATCH] FreeIPA module polymorphic restructuring, Python 3 support, and small fixes. (#18542) * Moved JSON-RPC client IPAClient class to ansible.module_utils.ipa, which is extended by all ipa modules * IPAClient: Changed to 2-clause BSD license * IPAClient (lines 37-39): Added some additional imports for use with Python 3 * IPAClient (line 41): Explicitly extend Python base object * IPAClient (line 57): Properly URL quoted the username/password form data as per https://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.1 * IPAClient (line 62): Data should be bytes or bytearray in Python 3 (still str in Python 2) * IPAClient (line 65): Print error message, not returned body * IPAClient (line 70): getheader() is not present in Python 3 version of HTTPMessage; get() is present in both Python 2/3 * IPAClient (line 88): Convert form data to bytes for Python 3 again * IPAClient (line 91): Print error message, not returned body * IPAClient (line 96-104): json.loads() requires a string; HTTPResponse.read() returns bytes in Python 3 and str in Python 2, so decode the bytes/string using the HTTPResponse returned charset (default to 'latin-1') * Add author/copyright notice --- lib/ansible/module_utils/ipa.py | 119 ++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 lib/ansible/module_utils/ipa.py diff --git a/lib/ansible/module_utils/ipa.py b/lib/ansible/module_utils/ipa.py new file mode 100644 index 00000000000..ccf580c564e --- /dev/null +++ b/lib/ansible/module_utils/ipa.py @@ -0,0 +1,119 @@ +# -*- coding: utf-8 -*- +# 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) 2016 Thomas Krahn (@Nosmoht) +# All rights reserved. +# +# 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. + +try: + import json +except ImportError: + import simplejson as json + +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.urls import fetch_url +from ansible.module_utils.six.moves.urllib.parse import quote +from ansible.module_utils.six import PY3 +from ansible.module_utils._text import to_bytes, to_text + +class IPAClient(object): + def __init__(self, module, host, port, protocol): + self.host = host + self.port = port + self.protocol = protocol + self.module = module + self.headers = None + + def get_base_url(self): + return '%s://%s/ipa' % (self.protocol, self.host) + + def get_json_url(self): + return '%s/session/json' % self.get_base_url() + + def login(self, username, password): + url = '%s/session/login_password' % self.get_base_url() + data = 'user=%s&password=%s' % (quote(username, safe=''), quote(password, safe='')) + headers = {'referer': self.get_base_url(), + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'text/plain'} + try: + resp, info = fetch_url(module=self.module, url=url, data=to_bytes(data), headers=headers) + status_code = info['status'] + if status_code not in [200, 201, 204]: + self._fail('login', info['msg']) + + self.headers = {'referer': self.get_base_url(), + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Cookie': resp.info().get('Set-Cookie')} + except Exception: + e = get_exception() + self._fail('login', str(e)) + + def _fail(self, msg, e): + if 'message' in e: + err_string = e.get('message') + else: + err_string = e + self.module.fail_json(msg='%s: %s' % (msg, err_string)) + + def _post_json(self, method, name, item=None): + if item is None: + item = {} + url = '%s/session/json' % self.get_base_url() + data = {'method': method, 'params': [[name], item]} + try: + resp, info = fetch_url(module=self.module, url=url, data=to_bytes(json.dumps(data)), headers=self.headers) + status_code = info['status'] + if status_code not in [200, 201, 204]: + self._fail(method, info['msg']) + except Exception: + e = get_exception() + self._fail('post %s' % method, str(e)) + + if PY3: + charset = resp.headers.get_content_charset('latin-1') + else: + response_charset = resp.headers.getparam('charset') + if response_charset: + charset = response_charset + else: + charset = 'latin-1' + resp = json.loads(to_text(resp.read(), encoding=charset), encoding=charset) + err = resp.get('error') + if err is not None: + self._fail('repsonse %s' % method, err) + + if 'result' in resp: + result = resp.get('result') + if 'result' in result: + result = result.get('result') + if isinstance(result, list): + if len(result) > 0: + return result[0] + else: + return {} + return result + return None