mirror of
https://github.com/freeipa/ansible-freeipa.git
synced 2026-03-26 21:33:05 +00:00
Created FreeIPABaseModule class to facilitate creation of new modules
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Authors:
|
||||
# Sergio Oliveira Campos <seocam@redhat.com>
|
||||
# Thomas Woerner <twoerner@redhat.com>
|
||||
#
|
||||
# Copyright (C) 2019 Red Hat
|
||||
@@ -27,10 +28,12 @@ import tempfile
|
||||
import shutil
|
||||
import gssapi
|
||||
from datetime import datetime
|
||||
from pprint import pformat
|
||||
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.constants import DEFAULT_CONFIG, LDAP_GENERALIZED_TIME_FORMAT
|
||||
|
||||
try:
|
||||
from ipalib.install.kinit import kinit_password, kinit_keytab
|
||||
except ImportError:
|
||||
@@ -38,7 +41,9 @@ except ImportError:
|
||||
from ipapython.ipautil import run
|
||||
from ipaplatform.paths import paths
|
||||
from ipalib.krb_utils import get_credentials_if_valid
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils._text import to_text
|
||||
|
||||
try:
|
||||
from ipalib.x509 import Encoding
|
||||
except ImportError:
|
||||
@@ -52,7 +57,7 @@ if six.PY3:
|
||||
unicode = str
|
||||
|
||||
|
||||
def valid_creds(module, principal):
|
||||
def valid_creds(module, principal): # noqa
|
||||
"""
|
||||
Get valid credintials matching the princial, try GSSAPI first
|
||||
"""
|
||||
@@ -205,9 +210,24 @@ def date_format(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():
|
||||
if key not in ipa:
|
||||
module.debug(
|
||||
base_debug_msg + "Command key not present in IPA: %s" % key
|
||||
)
|
||||
return False
|
||||
else:
|
||||
arg = args[key]
|
||||
@@ -220,25 +240,35 @@ def compare_args_ipa(module, args, ipa):
|
||||
if isinstance(ipa_arg, list):
|
||||
if not isinstance(arg, list):
|
||||
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):
|
||||
arg = [to_text(_arg) for _arg in arg]
|
||||
if isinstance(ipa_arg[0], unicode) and isinstance(arg[0], int):
|
||||
arg = [to_text(_arg) for _arg in arg]
|
||||
# module.warn("%s <=> %s" % (repr(arg), repr(ipa_arg)))
|
||||
try:
|
||||
arg_set = set(arg)
|
||||
ipa_arg_set = set(ipa_arg)
|
||||
except TypeError:
|
||||
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
|
||||
else:
|
||||
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
|
||||
|
||||
# module.warn("%s == %s" % (repr(arg), repr(ipa_arg)))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -289,6 +319,16 @@ def encode_certificate(cert):
|
||||
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):
|
||||
"""
|
||||
Test if figen IP address is a valid IPv4 address
|
||||
@@ -309,3 +349,294 @@ def is_ipv6_addr(ipaddr):
|
||||
except socket.error:
|
||||
return False
|
||||
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