1# -*- coding: utf-8 -*- 2# This code is part of Ansible, but is an independent component. 3# This particular file snippet, and this file snippet only, is BSD licensed. 4# Modules you write using this snippet, which is embedded dynamically by Ansible 5# still belong to the author of the module, and may assign their own license 6# to the complete work. 7# 8# Copyright (c) 2016 Thomas Krahn (@Nosmoht) 9# 10# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause) 11 12from __future__ import (absolute_import, division, print_function) 13__metaclass__ = type 14 15import json 16import os 17import socket 18import uuid 19 20import re 21from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text 22from ansible.module_utils.six import PY3 23from ansible.module_utils.six.moves.urllib.parse import quote 24from ansible.module_utils.urls import fetch_url, HAS_GSSAPI 25from ansible.module_utils.basic import env_fallback, AnsibleFallbackNotFound 26 27 28def _env_then_dns_fallback(*args, **kwargs): 29 ''' Load value from environment or DNS in that order''' 30 try: 31 result = env_fallback(*args, **kwargs) 32 if result == '': 33 raise AnsibleFallbackNotFound 34 except AnsibleFallbackNotFound: 35 # If no host was given, we try to guess it from IPA. 36 # The ipa-ca entry is a standard entry that IPA will have set for 37 # the CA. 38 try: 39 return socket.gethostbyaddr(socket.gethostbyname('ipa-ca'))[0] 40 except Exception: 41 raise AnsibleFallbackNotFound 42 43 44class IPAClient(object): 45 def __init__(self, module, host, port, protocol): 46 self.host = host 47 self.port = port 48 self.protocol = protocol 49 self.module = module 50 self.headers = None 51 self.timeout = module.params.get('ipa_timeout') 52 self.use_gssapi = False 53 54 def get_base_url(self): 55 return '%s://%s/ipa' % (self.protocol, self.host) 56 57 def get_json_url(self): 58 return '%s/session/json' % self.get_base_url() 59 60 def login(self, username, password): 61 if 'KRB5CCNAME' in os.environ and HAS_GSSAPI: 62 self.use_gssapi = True 63 elif 'KRB5_CLIENT_KTNAME' in os.environ and HAS_GSSAPI: 64 ccache = "MEMORY:" + str(uuid.uuid4()) 65 os.environ['KRB5CCNAME'] = ccache 66 self.use_gssapi = True 67 else: 68 if not password: 69 if 'KRB5CCNAME' in os.environ or 'KRB5_CLIENT_KTNAME' in os.environ: 70 self.module.warn("In order to use GSSAPI, you need to install 'urllib_gssapi'") 71 self._fail('login', 'Password is required if not using ' 72 'GSSAPI. To use GSSAPI, please set the ' 73 'KRB5_CLIENT_KTNAME or KRB5CCNAME (or both) ' 74 ' environment variables.') 75 url = '%s/session/login_password' % self.get_base_url() 76 data = 'user=%s&password=%s' % (quote(username, safe=''), quote(password, safe='')) 77 headers = {'referer': self.get_base_url(), 78 'Content-Type': 'application/x-www-form-urlencoded', 79 'Accept': 'text/plain'} 80 try: 81 resp, info = fetch_url(module=self.module, url=url, data=to_bytes(data), headers=headers, timeout=self.timeout) 82 status_code = info['status'] 83 if status_code not in [200, 201, 204]: 84 self._fail('login', info['msg']) 85 86 self.headers = {'Cookie': info.get('set-cookie')} 87 except Exception as e: 88 self._fail('login', to_native(e)) 89 if not self.headers: 90 self.headers = dict() 91 self.headers.update({ 92 'referer': self.get_base_url(), 93 'Content-Type': 'application/json', 94 'Accept': 'application/json'}) 95 96 def _fail(self, msg, e): 97 if 'message' in e: 98 err_string = e.get('message') 99 else: 100 err_string = e 101 self.module.fail_json(msg='%s: %s' % (msg, err_string)) 102 103 def get_ipa_version(self): 104 response = self.ping()['summary'] 105 ipa_ver_regex = re.compile(r'IPA server version (\d\.\d\.\d).*') 106 version_match = ipa_ver_regex.match(response) 107 ipa_version = None 108 if version_match: 109 ipa_version = version_match.groups()[0] 110 return ipa_version 111 112 def ping(self): 113 return self._post_json(method='ping', name=None) 114 115 def _post_json(self, method, name, item=None): 116 if item is None: 117 item = {} 118 url = '%s/session/json' % self.get_base_url() 119 data = dict(method=method) 120 121 # TODO: We should probably handle this a little better. 122 if method in ('ping', 'config_show', 'otpconfig_show'): 123 data['params'] = [[], {}] 124 elif method in ('config_mod', 'otpconfig_mod'): 125 data['params'] = [[], item] 126 else: 127 data['params'] = [[name], item] 128 129 try: 130 resp, info = fetch_url(module=self.module, url=url, data=to_bytes(json.dumps(data)), 131 headers=self.headers, timeout=self.timeout, use_gssapi=self.use_gssapi) 132 status_code = info['status'] 133 if status_code not in [200, 201, 204]: 134 self._fail(method, info['msg']) 135 except Exception as e: 136 self._fail('post %s' % method, to_native(e)) 137 138 if PY3: 139 charset = resp.headers.get_content_charset('latin-1') 140 else: 141 response_charset = resp.headers.getparam('charset') 142 if response_charset: 143 charset = response_charset 144 else: 145 charset = 'latin-1' 146 resp = json.loads(to_text(resp.read(), encoding=charset)) 147 err = resp.get('error') 148 if err is not None: 149 self._fail('response %s' % method, err) 150 151 if 'result' in resp: 152 result = resp.get('result') 153 if 'result' in result: 154 result = result.get('result') 155 if isinstance(result, list): 156 if len(result) > 0: 157 return result[0] 158 else: 159 return {} 160 return result 161 return None 162 163 def get_diff(self, ipa_data, module_data): 164 result = [] 165 for key in module_data.keys(): 166 mod_value = module_data.get(key, None) 167 if isinstance(mod_value, list): 168 default = [] 169 else: 170 default = None 171 ipa_value = ipa_data.get(key, default) 172 if isinstance(ipa_value, list) and not isinstance(mod_value, list): 173 mod_value = [mod_value] 174 if isinstance(ipa_value, list) and isinstance(mod_value, list): 175 mod_value = sorted(mod_value) 176 ipa_value = sorted(ipa_value) 177 if mod_value != ipa_value: 178 result.append(key) 179 return result 180 181 def modify_if_diff(self, name, ipa_list, module_list, add_method, remove_method, item=None): 182 changed = False 183 diff = list(set(ipa_list) - set(module_list)) 184 if len(diff) > 0: 185 changed = True 186 if not self.module.check_mode: 187 if item: 188 remove_method(name=name, item={item: diff}) 189 else: 190 remove_method(name=name, item=diff) 191 192 diff = list(set(module_list) - set(ipa_list)) 193 if len(diff) > 0: 194 changed = True 195 if not self.module.check_mode: 196 if item: 197 add_method(name=name, item={item: diff}) 198 else: 199 add_method(name=name, item=diff) 200 201 return changed 202 203 204def ipa_argument_spec(): 205 return dict( 206 ipa_prot=dict(type='str', default='https', choices=['http', 'https'], fallback=(env_fallback, ['IPA_PROT'])), 207 ipa_host=dict(type='str', default='ipa.example.com', fallback=(_env_then_dns_fallback, ['IPA_HOST'])), 208 ipa_port=dict(type='int', default=443, fallback=(env_fallback, ['IPA_PORT'])), 209 ipa_user=dict(type='str', default='admin', fallback=(env_fallback, ['IPA_USER'])), 210 ipa_pass=dict(type='str', no_log=True, fallback=(env_fallback, ['IPA_PASS'])), 211 ipa_timeout=dict(type='int', default=10, fallback=(env_fallback, ['IPA_TIMEOUT'])), 212 validate_certs=dict(type='bool', default=True), 213 ) 214