1010__metaclass__ = type
1111
1212import os
13+ import stat
1314import platform
15+ import subprocess
1416import sys
17+
1518from ansible .module_utils .basic import AnsibleModule
1619
1720
21+ # trace_realpath() and _join_tracepath() adapated from stdlib posixpath.py
22+ # https://github.com/python/cpython/blob/v3.12.6/Lib/posixpath.py#L423-L492
23+ # Copyright (c) 2001 - 2023 Python Software Foundation
24+ # Copyright (c) 2024 Alex Willmer <[email protected] > 25+ # License: Python Software Foundation License Version 2
26+
27+ def trace_realpath (filename , strict = False ):
28+ """
29+ Return the canonical path of the specified filename, and a trace of
30+ the route taken, eliminating any symbolic links encountered in the path.
31+ """
32+ path , trace , ok = _join_tracepath (filename [:0 ], filename , strict , seen = {}, trace = [])
33+ return os .path .abspath (path ), trace
34+
35+
36+ def _join_tracepath (path , rest , strict , seen , trace ):
37+ """
38+ Join two paths, normalizing and eliminating any symbolic links encountered
39+ in the second path.
40+ """
41+ trace .append (rest )
42+ if isinstance (path , bytes ):
43+ sep = b'/'
44+ curdir = b'.'
45+ pardir = b'..'
46+ else :
47+ sep = '/'
48+ curdir = '.'
49+ pardir = '..'
50+
51+ if os .path .isabs (rest ):
52+ rest = rest [1 :]
53+ path = sep
54+
55+ while rest :
56+ name , _ , rest = rest .partition (sep )
57+ if not name or name == curdir :
58+ # current dir
59+ continue
60+ if name == pardir :
61+ # parent dir
62+ if path :
63+ path , name = os .path .split (path )
64+ if name == pardir :
65+ path = os .path .join (path , pardir , pardir )
66+ else :
67+ path = pardir
68+ continue
69+ newpath = os .path .join (path , name )
70+ try :
71+ st = os .lstat (newpath )
72+ except OSError :
73+ if strict :
74+ raise
75+ is_link = False
76+ else :
77+ is_link = stat .S_ISLNK (st .st_mode )
78+ if not is_link :
79+ path = newpath
80+ continue
81+ # Resolve the symbolic link
82+ if newpath in seen :
83+ # Already seen this path
84+ path = seen [newpath ]
85+ if path is not None :
86+ # use cached value
87+ continue
88+ # The symlink is not resolved, so we must have a symlink loop.
89+ if strict :
90+ # Raise OSError(errno.ELOOP)
91+ os .stat (newpath )
92+ else :
93+ # Return already resolved part + rest of the path unchanged.
94+ return os .path .join (newpath , rest ), trace , False
95+ seen [newpath ] = None # not resolved symlink
96+ path , trace , ok = _join_tracepath (path , os .readlink (newpath ), strict , seen , trace )
97+ if not ok :
98+ return os .path .join (path , rest ), False
99+ seen [newpath ] = path # resolved symlink
100+
101+ return path , trace , True
102+
103+
18104def main ():
19105 module = AnsibleModule (argument_spec = dict (
20106 facts_copy = dict (type = dict , default = {}),
@@ -33,7 +119,18 @@ def main():
33119 sys .executable = "/usr/bin/python"
34120
35121 facts_copy = module .params ['facts_copy' ]
122+
36123 discovered_interpreter_python = facts_copy ['discovered_interpreter_python' ]
124+ d_i_p_realpath , d_i_p_trace = trace_realpath (discovered_interpreter_python )
125+ d_i_p_proc = subprocess .Popen (
126+ [discovered_interpreter_python , '-c' , 'import sys; print(sys.executable)' ],
127+ stdout = subprocess .PIPE , stderr = subprocess .PIPE ,
128+
129+ )
130+ d_i_p_stdout , d_i_p_stderr = d_i_p_proc .communicate ()
131+
132+ sys_exec_realpath , sys_exec_trace = trace_realpath (sys .executable )
133+
37134 result = {
38135 'changed' : False ,
39136 'ansible_facts' : module .params ['facts_to_override' ],
@@ -43,7 +140,17 @@ def main():
43140 ),
44141 'discovered_python' : {
45142 'as_seen' : discovered_interpreter_python ,
46- 'resolved' : os .path .realpath (discovered_interpreter_python ),
143+ 'resolved' : d_i_p_realpath ,
144+ 'trace' : [os .path .abspath (p ) for p in d_i_p_trace ],
145+ 'sys' : {
146+ 'executable' : {
147+ 'as_seen' : d_i_p_stdout .decode ('ascii' ).rstrip ('\n ' ),
148+ 'proc' : {
149+ 'stderr' : d_i_p_stderr .decode ('ascii' ),
150+ 'returncode' : d_i_p_proc .returncode ,
151+ },
152+ },
153+ },
47154 },
48155 'running_python' : {
49156 'platform' : {
@@ -54,7 +161,8 @@ def main():
54161 'sys' : {
55162 'executable' : {
56163 'as_seen' : sys .executable ,
57- 'resolved' : os .path .realpath (sys .executable ),
164+ 'resolved' : sys_exec_realpath ,
165+ 'trace' : [os .path .abspath (p ) for p in sys_exec_trace ],
58166 },
59167 'platform' : sys .platform ,
60168 'version_info' : {
0 commit comments