mirror of
https://github.com/freeipa/ansible-freeipa.git
synced 2026-07-31 03:44:41 +00:00
Created FreeIPABaseModule class to facilitate creation of new modules
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Authors:
|
# Authors:
|
||||||
|
# Sergio Oliveira Campos <seocam@redhat.com>
|
||||||
# Thomas Woerner <twoerner@redhat.com>
|
# Thomas Woerner <twoerner@redhat.com>
|
||||||
#
|
#
|
||||||
# Copyright (C) 2019 Red Hat
|
# Copyright (C) 2019 Red Hat
|
||||||
@@ -27,10 +28,12 @@ import tempfile
|
|||||||
import shutil
|
import shutil
|
||||||
import gssapi
|
import gssapi
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from pprint import pformat
|
||||||
from ipalib import api
|
from ipalib import api
|
||||||
from ipalib import errors as ipalib_errors
|
from ipalib import errors as ipalib_errors # noqa
|
||||||
from ipalib.config import Env
|
from ipalib.config import Env
|
||||||
from ipalib.constants import DEFAULT_CONFIG, LDAP_GENERALIZED_TIME_FORMAT
|
from ipalib.constants import DEFAULT_CONFIG, LDAP_GENERALIZED_TIME_FORMAT
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from ipalib.install.kinit import kinit_password, kinit_keytab
|
from ipalib.install.kinit import kinit_password, kinit_keytab
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@@ -38,7 +41,9 @@ except ImportError:
|
|||||||
from ipapython.ipautil import run
|
from ipapython.ipautil import run
|
||||||
from ipaplatform.paths import paths
|
from ipaplatform.paths import paths
|
||||||
from ipalib.krb_utils import get_credentials_if_valid
|
from ipalib.krb_utils import get_credentials_if_valid
|
||||||
|
from ansible.module_utils.basic import AnsibleModule
|
||||||
from ansible.module_utils._text import to_text
|
from ansible.module_utils._text import to_text
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from ipalib.x509 import Encoding
|
from ipalib.x509 import Encoding
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@@ -52,7 +57,7 @@ if six.PY3:
|
|||||||
unicode = str
|
unicode = str
|
||||||
|
|
||||||
|
|
||||||
def valid_creds(module, principal):
|
def valid_creds(module, principal): # noqa
|
||||||
"""
|
"""
|
||||||
Get valid credintials matching the princial, try GSSAPI first
|
Get valid credintials matching the princial, try GSSAPI first
|
||||||
"""
|
"""
|
||||||
@@ -205,9 +210,24 @@ def date_format(value):
|
|||||||
raise ValueError("Invalid date '%s'" % value)
|
raise ValueError("Invalid date '%s'" % value)
|
||||||
|
|
||||||
|
|
||||||
def compare_args_ipa(module, args, ipa):
|
def compare_args_ipa(module, args, ipa): # noqa
|
||||||
|
"""Compare IPA obj attrs with the command args.
|
||||||
|
|
||||||
|
This function compares IPA objects attributes with the args the
|
||||||
|
module is intending to use to call a command. This is useful to know
|
||||||
|
if call to IPA server will be needed or not.
|
||||||
|
In other to compare we have to prepare the perform slight changes in
|
||||||
|
data formats.
|
||||||
|
|
||||||
|
Returns True if they are the same and False otherwise.
|
||||||
|
"""
|
||||||
|
base_debug_msg = "Ansible arguments and IPA commands differed. "
|
||||||
|
|
||||||
for key in args.keys():
|
for key in args.keys():
|
||||||
if key not in ipa:
|
if key not in ipa:
|
||||||
|
module.debug(
|
||||||
|
base_debug_msg + "Command key not present in IPA: %s" % key
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
arg = args[key]
|
arg = args[key]
|
||||||
@@ -220,25 +240,35 @@ def compare_args_ipa(module, args, ipa):
|
|||||||
if isinstance(ipa_arg, list):
|
if isinstance(ipa_arg, list):
|
||||||
if not isinstance(arg, list):
|
if not isinstance(arg, list):
|
||||||
arg = [arg]
|
arg = [arg]
|
||||||
|
if len(ipa_arg) != len(arg):
|
||||||
|
module.debug(
|
||||||
|
base_debug_msg
|
||||||
|
+ "List length doesn't match for key %s: %d %d"
|
||||||
|
% (key, len(arg), len(ipa_arg),)
|
||||||
|
)
|
||||||
|
return False
|
||||||
if isinstance(ipa_arg[0], str) and isinstance(arg[0], int):
|
if isinstance(ipa_arg[0], str) and isinstance(arg[0], int):
|
||||||
arg = [to_text(_arg) for _arg in arg]
|
arg = [to_text(_arg) for _arg in arg]
|
||||||
if isinstance(ipa_arg[0], unicode) and isinstance(arg[0], int):
|
if isinstance(ipa_arg[0], unicode) and isinstance(arg[0], int):
|
||||||
arg = [to_text(_arg) for _arg in arg]
|
arg = [to_text(_arg) for _arg in arg]
|
||||||
# module.warn("%s <=> %s" % (repr(arg), repr(ipa_arg)))
|
|
||||||
try:
|
try:
|
||||||
arg_set = set(arg)
|
arg_set = set(arg)
|
||||||
ipa_arg_set = set(ipa_arg)
|
ipa_arg_set = set(ipa_arg)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
if arg != ipa_arg:
|
if arg != ipa_arg:
|
||||||
# module.warn("%s != %s" % (repr(arg), repr(ipa_arg)))
|
module.debug(
|
||||||
|
base_debug_msg
|
||||||
|
+ "Different values: %s %s" % (arg, ipa_arg)
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
if arg_set != ipa_arg_set:
|
if arg_set != ipa_arg_set:
|
||||||
# module.warn("%s != %s" % (repr(arg), repr(ipa_arg)))
|
module.debug(
|
||||||
|
base_debug_msg
|
||||||
|
+ "Different set content: %s %s"
|
||||||
|
% (arg_set, ipa_arg_set,)
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# module.warn("%s == %s" % (repr(arg), repr(ipa_arg)))
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@@ -289,6 +319,16 @@ def encode_certificate(cert):
|
|||||||
return encoded
|
return encoded
|
||||||
|
|
||||||
|
|
||||||
|
def is_valid_port(port):
|
||||||
|
if not isinstance(port, int):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if 1 <= port <= 65535:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def is_ipv4_addr(ipaddr):
|
def is_ipv4_addr(ipaddr):
|
||||||
"""
|
"""
|
||||||
Test if figen IP address is a valid IPv4 address
|
Test if figen IP address is a valid IPv4 address
|
||||||
@@ -309,3 +349,294 @@ def is_ipv6_addr(ipaddr):
|
|||||||
except socket.error:
|
except socket.error:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class AnsibleFreeIPAParams(dict):
|
||||||
|
def __init__(self, ansible_module):
|
||||||
|
self.update(ansible_module.params)
|
||||||
|
self.ansible_module = ansible_module
|
||||||
|
|
||||||
|
@property
|
||||||
|
def names(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
param = self.get(name)
|
||||||
|
if param is not None:
|
||||||
|
return _afm_convert(param)
|
||||||
|
|
||||||
|
|
||||||
|
class FreeIPABaseModule(AnsibleModule):
|
||||||
|
"""
|
||||||
|
Base class for FreeIPA Ansible modules.
|
||||||
|
|
||||||
|
Provides methods useful methods to be used by our modules.
|
||||||
|
|
||||||
|
This class should be overriten and instantiated for the module.
|
||||||
|
A basic implementation of an Ansible FreeIPA module expects its
|
||||||
|
class to:
|
||||||
|
|
||||||
|
1. Define a class attribute ``ipa_param_mapping``
|
||||||
|
2. Implement the method ``define_ipa_commands()``
|
||||||
|
3. Implement the method ``check_ipa_params()`` (optional)
|
||||||
|
|
||||||
|
After instantiating the class the method ``ipa_run()`` should be called.
|
||||||
|
|
||||||
|
Example (ansible-freeipa/plugins/modules/ipasomemodule.py):
|
||||||
|
|
||||||
|
class SomeIPAModule(FreeIPABaseModule):
|
||||||
|
ipa_param_mapping = {
|
||||||
|
"arg_to_be_passed_to_ipa_command": "module_param",
|
||||||
|
"another_arg": "get_another_module_param",
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_another_module_param(self):
|
||||||
|
another_module_param = self.ipa_params.another_module_param
|
||||||
|
# Validate or modify another_module_param
|
||||||
|
# ...
|
||||||
|
return another_module_param
|
||||||
|
|
||||||
|
def check_ipa_params(self):
|
||||||
|
# Validate your params here
|
||||||
|
# Example:
|
||||||
|
if not self.ipa_params.module_param in VALID_OPTIONS:
|
||||||
|
self.fail_json(msg="Invalid value for argument module_param")
|
||||||
|
|
||||||
|
def define_ipa_commands(self):
|
||||||
|
args = self.get_ipa_command_args()
|
||||||
|
|
||||||
|
self.add_ipa_command(
|
||||||
|
"some_ipa_command",
|
||||||
|
name="obj-name",
|
||||||
|
args=args,
|
||||||
|
)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ipa_module = SomeIPAModule(argument_spec=dict(
|
||||||
|
module_param=dict(
|
||||||
|
type="str",
|
||||||
|
default=None,
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
another_module_param=dict(
|
||||||
|
type="str",
|
||||||
|
default=None,
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
))
|
||||||
|
ipa_module.ipa_run()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
ipa_param_mapping = None
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(FreeIPABaseModule, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
# Attributes to store kerberos credentials (if needed)
|
||||||
|
self.ccache_dir = None
|
||||||
|
self.ccache_name = None
|
||||||
|
|
||||||
|
# Status of an execution. Will be changed to True
|
||||||
|
# if something is actually peformed.
|
||||||
|
self.changed = False
|
||||||
|
|
||||||
|
# Status of the connection with the IPA server.
|
||||||
|
# We need to know if the connection was actually stablished
|
||||||
|
# before we start sending commands.
|
||||||
|
self.ipa_connected = False
|
||||||
|
|
||||||
|
# Commands to be executed
|
||||||
|
self.ipa_commands = []
|
||||||
|
|
||||||
|
# Module exit arguments.
|
||||||
|
self.exit_args = {}
|
||||||
|
|
||||||
|
# Wrapper around the AnsibleModule.params.
|
||||||
|
# Return the actual params but performing transformations
|
||||||
|
# when needed.
|
||||||
|
self.ipa_params = AnsibleFreeIPAParams(self)
|
||||||
|
|
||||||
|
def get_ipa_command_args(self):
|
||||||
|
"""
|
||||||
|
Return a dict to be passed to an IPA command.
|
||||||
|
|
||||||
|
The keys of ``ipa_param_mapping`` are also the keys of the return dict.
|
||||||
|
|
||||||
|
The values of ``ipa_param_mapping`` needs to be either:
|
||||||
|
* A str with the name of a defined method; or
|
||||||
|
* A key of ``AnsibleModule.param``.
|
||||||
|
|
||||||
|
In case of a method the return of the method will be set as value
|
||||||
|
for the return dict.
|
||||||
|
|
||||||
|
In case of a AnsibleModule.param the value of the param will be
|
||||||
|
set in the return dict. In addition to that boolean values will be
|
||||||
|
automaticaly converted to uppercase strings (as required by FreeIPA
|
||||||
|
server).
|
||||||
|
|
||||||
|
"""
|
||||||
|
args = {}
|
||||||
|
for ipa_param_name, param_name in self.ipa_param_mapping.items():
|
||||||
|
|
||||||
|
# Check if param_name is actually a param
|
||||||
|
if param_name in self.ipa_params:
|
||||||
|
value = self.ipa_params.get(param_name)
|
||||||
|
if isinstance(value, bool):
|
||||||
|
value = "TRUE" if value else "FALSE"
|
||||||
|
|
||||||
|
# Since param wasn't a param check if it's a method name
|
||||||
|
elif hasattr(self, param_name):
|
||||||
|
method = getattr(self, param_name)
|
||||||
|
if callable(method):
|
||||||
|
value = method()
|
||||||
|
|
||||||
|
# We don't have a way to guess the value so fail.
|
||||||
|
else:
|
||||||
|
self.fail_json(
|
||||||
|
msg=(
|
||||||
|
"Couldn't get a value for '%s'. Option '%s' is not "
|
||||||
|
"a module argument neither a defined method."
|
||||||
|
)
|
||||||
|
% (ipa_param_name, param_name)
|
||||||
|
)
|
||||||
|
|
||||||
|
if value is not None:
|
||||||
|
args[ipa_param_name] = value
|
||||||
|
|
||||||
|
return args
|
||||||
|
|
||||||
|
def check_ipa_params(self):
|
||||||
|
"""Validate ipa_params before command is called."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def define_ipa_commands(self):
|
||||||
|
"""Define commands that will be run in IPA server."""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def api_command(self, command, name=None, args=None):
|
||||||
|
"""Execute a single command in IPA server."""
|
||||||
|
if args is None:
|
||||||
|
args = {}
|
||||||
|
|
||||||
|
if name is None:
|
||||||
|
return api_command_no_name(self, command, args)
|
||||||
|
|
||||||
|
return api_command(self, command, name, args)
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
"""
|
||||||
|
Connect to IPA server.
|
||||||
|
|
||||||
|
Check the there are working Kerberos credentials to connect to
|
||||||
|
IPA server. If there are not we perform a temporary kinit
|
||||||
|
that will be terminated when exiting the context.
|
||||||
|
|
||||||
|
If the connection fails ``ipa_connected`` attribute will be set
|
||||||
|
to False.
|
||||||
|
"""
|
||||||
|
principal = self.ipa_params.ipaadmin_principal
|
||||||
|
password = self.ipa_params.ipaadmin_password
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not valid_creds(self, principal):
|
||||||
|
self.ccache_dir, self.ccache_name = temp_kinit(
|
||||||
|
principal, password,
|
||||||
|
)
|
||||||
|
|
||||||
|
api_connect()
|
||||||
|
|
||||||
|
except Exception as excpt:
|
||||||
|
self.fail_json(msg=str(excpt))
|
||||||
|
else:
|
||||||
|
self.ipa_connected = True
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
"""
|
||||||
|
Terminate a connection with the IPA server.
|
||||||
|
|
||||||
|
Deal with exceptions, destroy temporary kinit credentials and
|
||||||
|
exit the module with proper arguments.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if exc_val:
|
||||||
|
self.fail_json(msg=str(exc_val))
|
||||||
|
|
||||||
|
# TODO: shouldn't we also disconnect from api backend?
|
||||||
|
temp_kdestroy(self.ccache_dir, self.ccache_name)
|
||||||
|
|
||||||
|
self.exit_json(changed=self.changed, user=self.exit_args)
|
||||||
|
|
||||||
|
def get_command_errors(self, command, result):
|
||||||
|
"""Look for erros into command results."""
|
||||||
|
# Get all errors
|
||||||
|
# All "already a member" and "not a member" failures in the
|
||||||
|
# result are ignored. All others are reported.
|
||||||
|
errors = []
|
||||||
|
for item in result.get("failed", tuple()):
|
||||||
|
failed_item = result["failed"][item]
|
||||||
|
for member_type in failed_item:
|
||||||
|
for member, failure in failed_item[member_type]:
|
||||||
|
if (
|
||||||
|
"already a member" in failure
|
||||||
|
or "not a member" in failure
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
errors.append(
|
||||||
|
"%s: %s %s: %s"
|
||||||
|
% (command, member_type, member, failure)
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(errors) > 0:
|
||||||
|
self.fail_json(", ".join("errors"))
|
||||||
|
|
||||||
|
def add_ipa_command(self, command, name=None, args=None):
|
||||||
|
"""Add a command to the list of commands to be executed."""
|
||||||
|
self.ipa_commands.append((name, command, args or {}))
|
||||||
|
|
||||||
|
def _run_ipa_commands(self):
|
||||||
|
"""Execute commands in self.ipa_commands."""
|
||||||
|
result = None
|
||||||
|
|
||||||
|
for name, command, args in self.ipa_commands:
|
||||||
|
try:
|
||||||
|
result = self.api_command(command, name, args)
|
||||||
|
except Exception as excpt:
|
||||||
|
self.fail_json(msg="%s: %s: %s" % (command, name, str(excpt)))
|
||||||
|
else:
|
||||||
|
if "completed" in result:
|
||||||
|
if result["completed"] > 0:
|
||||||
|
self.changed = True
|
||||||
|
else:
|
||||||
|
self.changed = True
|
||||||
|
|
||||||
|
self.get_command_errors(command, result)
|
||||||
|
|
||||||
|
def require_ipa_attrs_change(self, command_args, ipa_attrs):
|
||||||
|
"""
|
||||||
|
Compare given args with current object attributes.
|
||||||
|
|
||||||
|
Returns True in case current IPA object attributes differ from
|
||||||
|
args passed to the module.
|
||||||
|
"""
|
||||||
|
equal = compare_args_ipa(self, command_args, ipa_attrs)
|
||||||
|
return not equal
|
||||||
|
|
||||||
|
def pdebug(self, value):
|
||||||
|
"""Debug with pretty formatting."""
|
||||||
|
self.debug(pformat(value))
|
||||||
|
|
||||||
|
def ipa_run(self):
|
||||||
|
"""Execute module actions."""
|
||||||
|
with self:
|
||||||
|
if not self.ipa_connected:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.check_ipa_params()
|
||||||
|
self.define_ipa_commands()
|
||||||
|
self._run_ipa_commands()
|
||||||
|
|||||||
Reference in New Issue
Block a user