Skip to content
Snippets Groups Projects
Unverified Commit e76047ed authored by Sergio Oliveira Campos's avatar Sergio Oliveira Campos
Browse files

Created FreeIPABaseModule class to facilitate creation of new modules

parent b211b50b
No related branches found
No related tags found
No related merge requests found
...@@ -2,6 +2,7 @@ ...@@ -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 ...@@ -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: ...@@ -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: ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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()
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment