2023-04-14 22:58:51 +00:00
import pathlib
2023-04-23 07:13:07 +00:00
from copy import deepcopy
from typing import Any , Callable , ClassVar , Dict , NoReturn , Optional , Type , TypeVar , Union , overload
2023-04-14 22:58:51 +00:00
import ansible . module_utils . basic as basic
2023-04-20 20:09:58 +00:00
2023-04-15 22:10:13 +00:00
try :
2023-04-20 22:12:47 +00:00
from ansible_collections . sebastian . systemd . plugins . module_utils . generic import Types , _sdict
2023-04-15 22:10:13 +00:00
except ImportError :
2023-04-20 22:12:47 +00:00
from plugins . module_utils . generic import Types , _sdict
2023-04-14 22:58:51 +00:00
2023-04-23 07:17:41 +00:00
2023-04-14 22:58:51 +00:00
__all__ = (
" AnsibleModule " ,
" SystemdUnitModule " ,
2023-04-23 07:17:41 +00:00
" installable " ,
" SystemdReloadMixin " ,
2023-04-14 22:58:51 +00:00
)
T = TypeVar ( " T " )
2023-04-23 07:17:41 +00:00
def docify ( input : Union [ dict , _sdict ] ) - > dict :
options = dict ( )
for name , help in input . items ( ) :
options [ name ] = dict ( type = help [ " type " ] )
if hasattr ( help , " _help " ) and help . _help is not None :
options [ name ] [ " description " ] = help . _help
if " description " in help and isinstance ( help [ " description " ] , str ) :
options [ name ] [ " description " ] = options [ name ] [ " description " ] . split ( " \n " )
if " required " in help and help [ " required " ] :
options [ name ] [ " required " ] = True
else :
options [ name ] [ " required " ] = False
if help [ " type " ] == " list " :
options [ name ] [ " elements " ] = help [ " elements " ]
if not options [ name ] [ " required " ] :
options [ name ] [ " default " ] = [ ]
if " default " in help :
options [ name ] [ " default " ] = help [ " default " ]
if " options " in help :
options [ name ] [ " options " ] = docify ( help [ " options " ] )
if " choices " in help :
options [ name ] [ " choices " ] = tuple ( help [ " choices " ] )
return options
2023-04-14 22:58:51 +00:00
class AnsibleModule ( object ) :
2023-04-15 07:40:27 +00:00
""" Simple wrapper for the basic.AnsibleModule """
2023-04-14 22:58:51 +00:00
2023-04-15 07:40:27 +00:00
#: name of the module. This is required for the generation of the Ansible documentation
2023-04-14 22:58:51 +00:00
name : ClassVar [ str ]
2023-04-15 07:40:27 +00:00
#: The AnsibleModule for this Module
2023-04-14 22:58:51 +00:00
module : basic . AnsibleModule
2023-04-15 07:40:27 +00:00
#: The result of this Module call. It always contains the changed key, so in any case an Module can report if it changed anything
2023-04-14 22:58:51 +00:00
result : dict
2023-04-15 07:40:27 +00:00
#: the specification of the arguments. Subclasses that are usable Modules must set this value.
2023-04-14 22:58:51 +00:00
module_spec : ClassVar [ dict ]
2023-04-15 07:40:27 +00:00
#: This is set by classes that define common things for their subclasses, like behaviour of the run and check methods. This is used by `SystemdUnitModule`
2023-04-14 22:58:51 +00:00
_common_args = dict ( )
@property
def params ( self ) - > Dict [ str , Any ] :
2023-04-15 07:40:27 +00:00
""" params is an wrapper for the module.params """
2023-04-14 22:58:51 +00:00
return self . module . params # type: ignore
def __init__ ( self ) :
self . result = dict ( changed = False )
specs = dict ( )
2023-04-23 07:13:07 +00:00
specs . update ( deepcopy ( self . _common_args ) )
modspec = self . module_spec
2023-04-20 22:13:38 +00:00
if " argument_spec " in modspec and " argument_spec " in self . _common_args :
specs [ " argument_spec " ] . update ( modspec [ " argument_spec " ] )
del modspec [ " argument_spec " ]
specs . update ( modspec )
2023-04-14 22:58:51 +00:00
self . module = basic . AnsibleModule ( * * specs )
self . tmpdir = pathlib . Path ( self . module . tmpdir )
def set ( self , key : str , value ) :
2023-04-15 07:40:27 +00:00
""" sets an value for the result """
2023-04-14 22:58:51 +00:00
self . result [ key ] = value
2023-04-15 07:40:27 +00:00
@overload
def diff ( self , diff : Dict [ str , str ] ) :
pass
2023-04-23 07:15:37 +00:00
@overload
2023-04-14 22:58:51 +00:00
def diff (
2023-04-20 20:09:58 +00:00
self ,
2023-04-15 07:40:27 +00:00
before : Optional [ str ] = None ,
after : Optional [ str ] = None ,
2023-04-14 22:58:51 +00:00
before_header : Optional [ str ] = None ,
after_header : Optional [ str ] = None ,
2023-04-23 07:15:37 +00:00
) :
pass
def diff ( # type: ignore
self ,
diff = None ,
* ,
before = None ,
after = None ,
before_header = None ,
after_header = None ,
2023-04-14 22:58:51 +00:00
) :
2023-04-15 07:40:27 +00:00
""" adds the special return value " diff " . This allows Modules to present the changes of files to the caller. it takes care of the special semantics of the return value """
2023-04-14 22:58:51 +00:00
if " diff " not in self . result :
self . result [ " diff " ] = list ( )
2023-04-15 07:40:27 +00:00
if diff is not None and not any ( ( before is not None , after is not None ) ) :
pass
elif all ( ( before is not None , after is not None , diff is None ) ) :
diff = dict (
before = before ,
after = after ,
)
if before_header is not None :
diff [ " before_header " ] = before_header
if after_header is not None :
diff [ " after_header " ] = after_header
else :
2023-04-20 22:15:03 +00:00
raise TypeError ( " only diff or before and after can be set, not both of them " )
2023-04-14 22:58:51 +00:00
self . result [ " diff " ] . append ( diff )
def get ( self , key : str , default : T = None ) - > T :
2023-04-15 07:40:27 +00:00
""" returns an Parameter of the Module. """
2023-04-14 22:58:51 +00:00
if self . params [ key ] is None and default is not None :
return default
2023-04-23 07:16:48 +00:00
elif self . params [ key ] is None and default is None :
raise KeyError ( )
2023-04-14 22:58:51 +00:00
return self . params [ key ]
2023-04-16 09:26:47 +00:00
def changed_get ( self ) :
""" value that shows if changes were detected/made """
2023-04-14 22:58:51 +00:00
return self . result [ " changed " ]
def changed_set ( self , value ) :
self . result [ " changed " ] = not not value
2023-04-16 09:26:47 +00:00
changed = property ( changed_get , changed_set , None , changed_get . __doc__ )
2023-04-14 22:58:51 +00:00
def prepare ( self ) :
raise NotImplementedError ( )
def check ( self ) :
raise NotImplementedError ( )
def run ( self ) :
raise NotImplementedError ( )
2023-04-15 07:40:27 +00:00
def __call__ ( self ) - > NoReturn :
""" This calls the module. first prepare is called and then check or run, depending on the check mode.
2023-04-20 20:09:58 +00:00
If an exception is raised this is catched and the module automatically fails with an traceback
"""
2023-04-14 22:58:51 +00:00
self . prepare ( )
try :
if self . module . check_mode :
self . check ( )
else :
self . run ( )
except Exception as exc :
import traceback
self . module . fail_json (
" " . join ( traceback . format_exception ( type ( exc ) , exc , exc . __traceback__ ) ) ,
* * self . result ,
)
self . module . exit_json ( * * self . result )
@classmethod
def doc ( cls ) - > str :
2023-04-15 07:40:27 +00:00
""" this returns the documentation string of the module. If the help arguments of an Types method was given, it adds this as an helptext of this parameter """
2023-04-14 22:58:51 +00:00
try :
import yaml
except ImportError :
return " --- \n "
doc = cls . __doc__
if doc is None :
doc = " "
help : _sdict
2023-04-20 22:14:19 +00:00
specs = dict ( )
if " argument_spec " in cls . _common_args :
specs . update ( cls . _common_args [ " argument_spec " ] )
if " argument_spec " in cls . module_spec :
specs . update ( cls . module_spec [ " argument_spec " ] )
2023-04-23 07:17:41 +00:00
options = docify ( specs )
2023-04-14 22:58:51 +00:00
docu = doc . split ( " \n " )
return str (
yaml . safe_dump (
dict (
module = cls . name ,
short_description = docu [ 0 ] ,
description = docu ,
options = options ,
) ,
stream = None ,
explicit_start = " --- " ,
)
)
class SystemdUnitModule ( AnsibleModule ) :
2023-04-15 07:40:27 +00:00
#: path of the unitfile managed by this module
2023-04-14 22:58:51 +00:00
unitfile : pathlib . Path
2023-04-15 07:40:27 +00:00
#: subclasses of this always support the file common args and the check mode
2023-04-14 22:58:51 +00:00
_common_args = dict (
supports_check_mode = True ,
add_file_common_args = True ,
2023-04-20 22:12:47 +00:00
argument_spec = dict (
description = Types . str ( help = " An description for programs that access systemd " ) ,
documentation = Types . list ( str , help = " Paths where documentation can be found " ) ,
requires = Types . list (
str ,
help = " list of units that this unit requires. If it fails or can ' t be started this unit fails. without before/after this is started at the same time " ,
) ,
wants = Types . list ( str , help = " list of units that this unit wants. If it fails or can ' t be started it does not affect this unit " ) ,
partof = Types . list (
str ,
help = " list of units that this unit is part of. \n If the restart this unit does it too, but if this restarts it does not affect the other units. " ,
) ,
before = Types . list ( str , help = " list of units that this unit needs to be started before this unit. " ) ,
after = Types . list ( str , help = " list of units that this unit wants to be started after this unit " ) ,
) ,
2023-04-14 22:58:51 +00:00
)
2023-04-15 07:40:27 +00:00
#: if defined it will be called after run has changed the unitfile
2023-04-14 22:58:51 +00:00
post : Optional [ Callable [ [ ] , None ] ]
def unit ( self ) - > str :
raise NotImplementedError ( )
2023-04-20 22:12:47 +00:00
def header ( self ) - > str :
header = " [Unit] \n "
if self . get ( " description " , False ) :
header + = " Description= {} \n " . format ( self . get ( " description " ) )
if self . get ( " documentation " , False ) :
header + = " Documentation= {} \n " . format ( " " . join ( self . get ( " documentation " ) ) )
if self . get ( " requires " , False ) :
header + = " Requires= {} \n " . format ( " " . join ( self . get ( " requires " ) ) )
if self . get ( " wants " , False ) :
header + = " Wants= {} \n " . format ( " " . join ( self . get ( " wants " ) ) )
if self . get ( " partof " , False ) :
header + = " PartOf= {} \n " . format ( " " . join ( self . get ( " partof " ) ) )
if self . get ( " before " , False ) :
header + = " Before= {} \n " . format ( " " . join ( self . get ( " before " ) ) )
if self . get ( " after " , False ) :
header + = " After= {} \n " . format ( " " . join ( self . get ( " after " ) ) )
return header
2023-04-14 22:58:51 +00:00
def unitfile_gen ( self ) :
path = self . tmpdir / " newunit "
with open ( path , " w " ) as unit :
unit . write ( self . unit ( ) )
self . module . set_owner_if_different ( path . as_posix ( ) , " root " , False )
self . module . set_group_if_different ( path . as_posix ( ) , " root " , False )
self . module . set_mode_if_different ( path . as_posix ( ) , " 0644 " , False )
if self . unitfile . exists ( ) :
diff = dict ( )
2023-04-15 07:40:27 +00:00
self . changed = self . changed | self . module . set_owner_if_different (
2023-04-14 22:58:51 +00:00
self . unitfile . as_posix ( ) ,
" root " ,
self . result [ " changed " ] ,
diff ,
)
2023-04-15 07:40:27 +00:00
self . diff ( diff )
diff = dict ( )
self . changed = self . changed | self . module . set_group_if_different (
2023-04-14 22:58:51 +00:00
self . unitfile . as_posix ( ) ,
" root " ,
self . result [ " changed " ] ,
diff ,
)
2023-04-15 07:40:27 +00:00
self . diff ( diff )
diff = dict ( )
self . changed = self . changed | self . module . set_mode_if_different (
2023-04-14 22:58:51 +00:00
self . unitfile . as_posix ( ) ,
" 0644 " ,
self . result [ " changed " ] ,
diff ,
)
2023-04-15 07:40:27 +00:00
self . diff ( diff )
2023-04-14 22:58:51 +00:00
def check ( self ) :
self . unitfile_gen ( )
if not self . unitfile . exists ( ) :
2023-04-20 22:15:03 +00:00
self . diff ( before = " " , after = self . unit ( ) , before_header = self . unitfile . as_posix ( ) )
2023-04-15 07:40:27 +00:00
self . changed = True
2023-04-14 22:58:51 +00:00
else :
2023-04-20 22:15:03 +00:00
if self . module . sha256 ( self . unitfile . as_posix ( ) ) != self . module . sha256 ( ( self . tmpdir / " newunit " ) . as_posix ( ) ) :
2023-04-15 07:40:27 +00:00
self . changed = True
2023-04-14 22:58:51 +00:00
self . diff (
before = self . unitfile . read_text ( ) ,
after = self . unit ( ) ,
before_header = self . unitfile . as_posix ( ) ,
)
def run ( self ) :
2023-04-15 07:40:27 +00:00
self . check ( )
if not self . changed :
2023-04-14 22:58:51 +00:00
return
self . module . atomic_move (
src = ( self . tmpdir / " newunit " ) . as_posix ( ) ,
dest = self . unitfile . as_posix ( ) ,
)
if hasattr ( self , " post " ) and self . post is not None :
self . post ( )
2023-04-20 22:12:47 +00:00
_INSTALL_MAPPING = dict (
required_by = " RequiredBy " ,
wanted_by = " WantedBy " ,
)
def installable ( _class : Type [ SystemdUnitModule ] ) :
""" adds the required arguments to the spec and adds the install method for the unit method """
arguments = dict (
required_by = Types . list ( elements = str , help = " systemd units that require this mount " ) ,
wanted_by = Types . list (
elements = str ,
help = " systemd units that want the mount, but not explicitly require it. Commonly used for target if not service explicitly require it. " ,
) ,
)
_class . module_spec [ " argument_spec " ] . update ( arguments )
def install ( self : SystemdUnitModule ) - > str :
output = " [Install] \n "
for argument , key in _INSTALL_MAPPING . items ( ) :
if self . get ( argument , False ) :
for unit in self . get ( argument ) :
output + = " {} = {} \n " . format ( key , unit )
return output
_class . install = install
return _class