mirror of
				https://github.com/ansible-collections/community.general.git
				synced 2025-10-25 13:34:01 -07:00 
			
		
		
		
	
		
			Some checks are pending
		
		
	
	EOL CI / EOL Sanity (Ⓐ2.17) (push) Waiting to run
				
			EOL CI / EOL Units (Ⓐ2.17+py3.10) (push) Waiting to run
				
			EOL CI / EOL Units (Ⓐ2.17+py3.12) (push) Waiting to run
				
			EOL CI / EOL Units (Ⓐ2.17+py3.7) (push) Waiting to run
				
			EOL CI / EOL I (Ⓐ2.17+alpine319+py:azp/posix/1/) (push) Waiting to run
				
			EOL CI / EOL I (Ⓐ2.17+alpine319+py:azp/posix/2/) (push) Waiting to run
				
			EOL CI / EOL I (Ⓐ2.17+alpine319+py:azp/posix/3/) (push) Waiting to run
				
			EOL CI / EOL I (Ⓐ2.17+fedora39+py:azp/posix/1/) (push) Waiting to run
				
			EOL CI / EOL I (Ⓐ2.17+fedora39+py:azp/posix/2/) (push) Waiting to run
				
			EOL CI / EOL I (Ⓐ2.17+fedora39+py:azp/posix/3/) (push) Waiting to run
				
			EOL CI / EOL I (Ⓐ2.17+ubuntu2004+py:azp/posix/1/) (push) Waiting to run
				
			EOL CI / EOL I (Ⓐ2.17+ubuntu2004+py:azp/posix/2/) (push) Waiting to run
				
			EOL CI / EOL I (Ⓐ2.17+ubuntu2004+py:azp/posix/3/) (push) Waiting to run
				
			nox / Run extra sanity tests (push) Waiting to run
				
			* use f-strings in module utils * Apply suggestions from code review Co-authored-by: Felix Fontein <felix@fontein.de> * remove unused imports --------- Co-authored-by: Felix Fontein <felix@fontein.de>
		
			
				
	
	
		
			205 lines
		
	
	
	
		
			8 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			205 lines
		
	
	
	
		
			8 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| # 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)
 | |
| #
 | |
| # Simplified BSD License (see LICENSES/BSD-2-Clause.txt or https://opensource.org/licenses/BSD-2-Clause)
 | |
| # SPDX-License-Identifier: BSD-2-Clause
 | |
| 
 | |
| from __future__ import annotations
 | |
| 
 | |
| import json
 | |
| import os
 | |
| import socket
 | |
| import uuid
 | |
| 
 | |
| import re
 | |
| from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
 | |
| from ansible.module_utils.urls import fetch_url, HAS_GSSAPI
 | |
| from ansible.module_utils.basic import env_fallback, AnsibleFallbackNotFound
 | |
| from urllib.parse import quote
 | |
| 
 | |
| 
 | |
| def _env_then_dns_fallback(*args, **kwargs):
 | |
|     ''' Load value from environment or DNS in that order'''
 | |
|     try:
 | |
|         result = env_fallback(*args, **kwargs)
 | |
|         if result == '':
 | |
|             raise AnsibleFallbackNotFound
 | |
|         return result
 | |
|     except AnsibleFallbackNotFound:
 | |
|         # If no host was given, we try to guess it from IPA.
 | |
|         # The ipa-ca entry is a standard entry that IPA will have set for
 | |
|         # the CA.
 | |
|         try:
 | |
|             return socket.gethostbyaddr(socket.gethostbyname('ipa-ca'))[0]
 | |
|         except Exception:
 | |
|             raise AnsibleFallbackNotFound
 | |
| 
 | |
| 
 | |
| 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
 | |
|         self.timeout = module.params.get('ipa_timeout')
 | |
|         self.use_gssapi = False
 | |
| 
 | |
|     def get_base_url(self):
 | |
|         return f'{self.protocol}://{self.host}/ipa'
 | |
| 
 | |
|     def get_json_url(self):
 | |
|         return f'{self.get_base_url()}/session/json'
 | |
| 
 | |
|     def login(self, username, password):
 | |
|         if 'KRB5CCNAME' in os.environ and HAS_GSSAPI:
 | |
|             self.use_gssapi = True
 | |
|         elif 'KRB5_CLIENT_KTNAME' in os.environ and HAS_GSSAPI:
 | |
|             ccache = f"MEMORY:{uuid.uuid4()!s}"
 | |
|             os.environ['KRB5CCNAME'] = ccache
 | |
|             self.use_gssapi = True
 | |
|         else:
 | |
|             if not password:
 | |
|                 if 'KRB5CCNAME' in os.environ or 'KRB5_CLIENT_KTNAME' in os.environ:
 | |
|                     self.module.warn("In order to use GSSAPI, you need to install 'urllib_gssapi'")
 | |
|                 self._fail('login', 'Password is required if not using '
 | |
|                            'GSSAPI. To use GSSAPI, please set the '
 | |
|                            'KRB5_CLIENT_KTNAME or KRB5CCNAME (or both) '
 | |
|                            ' environment variables.')
 | |
|             url = f'{self.get_base_url()}/session/login_password'
 | |
|             data = f"user={quote(username, safe='')}&password={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, timeout=self.timeout)
 | |
|                 status_code = info['status']
 | |
|                 if status_code not in [200, 201, 204]:
 | |
|                     self._fail('login', info['msg'])
 | |
| 
 | |
|                 self.headers = {'Cookie': info.get('set-cookie')}
 | |
|             except Exception as e:
 | |
|                 self._fail('login', to_native(e))
 | |
|         if not self.headers:
 | |
|             self.headers = dict()
 | |
|         self.headers.update({
 | |
|             'referer': self.get_base_url(),
 | |
|             'Content-Type': 'application/json',
 | |
|             'Accept': 'application/json'})
 | |
| 
 | |
|     def _fail(self, msg, e):
 | |
|         if 'message' in e:
 | |
|             err_string = e.get('message')
 | |
|         else:
 | |
|             err_string = e
 | |
|         self.module.fail_json(msg=f'{msg}: {err_string}')
 | |
| 
 | |
|     def get_ipa_version(self):
 | |
|         response = self.ping()['summary']
 | |
|         ipa_ver_regex = re.compile(r'IPA server version (\d+\.\d+\.\d+).*')
 | |
|         version_match = ipa_ver_regex.match(response)
 | |
|         ipa_version = None
 | |
|         if version_match:
 | |
|             ipa_version = version_match.groups()[0]
 | |
|         return ipa_version
 | |
| 
 | |
|     def ping(self):
 | |
|         return self._post_json(method='ping', name=None)
 | |
| 
 | |
|     def _post_json(self, method, name, item=None):
 | |
|         if item is None:
 | |
|             item = {}
 | |
|         url = f'{self.get_base_url()}/session/json'
 | |
|         data = dict(method=method)
 | |
| 
 | |
|         # TODO: We should probably handle this a little better.
 | |
|         if method in ('ping', 'config_show', 'otpconfig_show'):
 | |
|             data['params'] = [[], {}]
 | |
|         elif method in ('config_mod', 'otpconfig_mod'):
 | |
|             data['params'] = [[], item]
 | |
|         else:
 | |
|             data['params'] = [[name], item]
 | |
| 
 | |
|         try:
 | |
|             resp, info = fetch_url(module=self.module, url=url, data=to_bytes(json.dumps(data)),
 | |
|                                    headers=self.headers, timeout=self.timeout, use_gssapi=self.use_gssapi)
 | |
|             status_code = info['status']
 | |
|             if status_code not in [200, 201, 204]:
 | |
|                 self._fail(method, info['msg'])
 | |
|         except Exception as e:
 | |
|             self._fail(f'post {method}', to_native(e))
 | |
| 
 | |
|         charset = resp.headers.get_content_charset('latin-1')
 | |
|         resp = json.loads(to_text(resp.read(), encoding=charset))
 | |
|         err = resp.get('error')
 | |
|         if err is not None:
 | |
|             self._fail(f'response {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
 | |
| 
 | |
|     def get_diff(self, ipa_data, module_data):
 | |
|         result = []
 | |
|         for key in module_data.keys():
 | |
|             mod_value = module_data.get(key, None)
 | |
|             if isinstance(mod_value, list):
 | |
|                 default = []
 | |
|             else:
 | |
|                 default = None
 | |
|             ipa_value = ipa_data.get(key, default)
 | |
|             if isinstance(ipa_value, list) and not isinstance(mod_value, list):
 | |
|                 mod_value = [mod_value]
 | |
|             if isinstance(ipa_value, list) and isinstance(mod_value, list):
 | |
|                 mod_value = sorted(mod_value)
 | |
|                 ipa_value = sorted(ipa_value)
 | |
|             if mod_value != ipa_value:
 | |
|                 result.append(key)
 | |
|         return result
 | |
| 
 | |
|     def modify_if_diff(self, name, ipa_list, module_list, add_method, remove_method, item=None, append=None):
 | |
|         changed = False
 | |
|         diff = list(set(ipa_list) - set(module_list))
 | |
|         if append is not True and len(diff) > 0:
 | |
|             changed = True
 | |
|             if not self.module.check_mode:
 | |
|                 if item:
 | |
|                     remove_method(name=name, item={item: diff})
 | |
|                 else:
 | |
|                     remove_method(name=name, item=diff)
 | |
| 
 | |
|         diff = list(set(module_list) - set(ipa_list))
 | |
|         if len(diff) > 0:
 | |
|             changed = True
 | |
|             if not self.module.check_mode:
 | |
|                 if item:
 | |
|                     add_method(name=name, item={item: diff})
 | |
|                 else:
 | |
|                     add_method(name=name, item=diff)
 | |
| 
 | |
|         return changed
 | |
| 
 | |
| 
 | |
| def ipa_argument_spec():
 | |
|     return dict(
 | |
|         ipa_prot=dict(type='str', default='https', choices=['http', 'https'], fallback=(env_fallback, ['IPA_PROT'])),
 | |
|         ipa_host=dict(type='str', default='ipa.example.com', fallback=(_env_then_dns_fallback, ['IPA_HOST'])),
 | |
|         ipa_port=dict(type='int', default=443, fallback=(env_fallback, ['IPA_PORT'])),
 | |
|         ipa_user=dict(type='str', default='admin', fallback=(env_fallback, ['IPA_USER'])),
 | |
|         ipa_pass=dict(type='str', no_log=True, fallback=(env_fallback, ['IPA_PASS'])),
 | |
|         ipa_timeout=dict(type='int', default=10, fallback=(env_fallback, ['IPA_TIMEOUT'])),
 | |
|         validate_certs=dict(type='bool', default=True),
 | |
|     )
 |