|
7 | 7 |
|
8 | 8 | __metaclass__ = type |
9 | 9 |
|
10 | | -import hashlib |
11 | 10 | import os |
12 | | -import uuid |
| 11 | +import tempfile |
13 | 12 |
|
14 | | -from ansible.errors import AnsibleError |
15 | | -from ansible.module_utils.common.text.converters import to_bytes, to_text |
16 | | -from ansible.module_utils.connection import Connection, ConnectionError |
17 | | -from ansible.module_utils.six.moves.urllib.parse import urlsplit |
18 | 13 | from ansible.plugins.action import ActionBase |
19 | 14 | from ansible.utils.display import Display |
| 15 | +from ansible.utils.hashing import checksum |
20 | 16 |
|
21 | 17 |
|
22 | 18 | display = Display() |
23 | 19 |
|
| 20 | +def tmp_file_factory(prefix): |
| 21 | + def outer(func): |
| 22 | + def inner(self, *args, **kwargs): |
| 23 | + try: |
| 24 | + sentinel = object() |
| 25 | + cached_fd, cached_fp = ( |
| 26 | + func.__globals__.get(f"{prefix}_fd", sentinel), |
| 27 | + func.__globals__.get(f"{prefix}_fp", sentinel), |
| 28 | + ) |
| 29 | + ( |
| 30 | + func.__globals__[f"{prefix}_fd"], |
| 31 | + func.__globals__[f"{prefix}_fp"], |
| 32 | + ) = tempfile.mkstemp(prefix="") |
| 33 | + return func(self, *args, **kwargs) |
| 34 | + finally: |
| 35 | + os.remove(func.__globals__[f"{prefix}_fp"]) |
| 36 | + |
| 37 | + if cached_fd is sentinel: |
| 38 | + del func.__globals__[f"{prefix}_fd"] |
| 39 | + else: |
| 40 | + func.__globals__[f"{prefix}_fd"] = cached_fd |
| 41 | + |
| 42 | + if cached_fp is sentinel: |
| 43 | + del func.__globals__[f"{prefix}_fp"] |
| 44 | + else: |
| 45 | + func.__globals__[f"{prefix}_fp"] = cached_fp |
| 46 | + return inner |
| 47 | + return outer |
24 | 48 |
|
25 | 49 | class ActionModule(ActionBase): |
26 | | - def run(self, tmp=None, task_vars=None): |
27 | | - changed = False |
28 | | - socket_path = None |
29 | | - network_os = self._get_network_os(task_vars).split(".")[-1] |
30 | | - persistent_connection = self._play_context.connection.split(".")[-1] |
31 | 50 |
|
32 | | - result = super(ActionModule, self).run(task_vars=task_vars) |
| 51 | + TRANSFERS_FILES = True |
| 52 | + |
| 53 | + def __init__(self, *args, **kwargs): |
| 54 | + super(ActionModule, self).__init__(*args, **kwargs) |
33 | 55 |
|
34 | | - if persistent_connection != "network_cli": |
35 | | - # It is supported only with network_cli |
36 | | - result["failed"] = True |
37 | | - result["msg"] = ( |
38 | | - "connection type %s is not valid for net_put module," |
39 | | - " please use fully qualified name of network_cli connection type" |
40 | | - % self._play_context.connection |
41 | | - ) |
42 | | - return result |
| 56 | + |
| 57 | + @tmp_file_factory("fetched") |
| 58 | + @tmp_file_factory("rendered") |
| 59 | + def run(self, tmp=None, task_vars=None): |
| 60 | + result = super(ActionModule, self).run(task_vars=task_vars) |
43 | 61 |
|
44 | 62 | try: |
45 | | - src = self._task.args["src"] |
| 63 | + self._src = self._task.args.get("src") |
46 | 64 | except KeyError as exc: |
47 | 65 | return { |
48 | 66 | "failed": True, |
49 | 67 | "msg": "missing required argument: %s" % exc, |
50 | 68 | } |
51 | 69 |
|
52 | | - src_file_path_name = src |
53 | | - |
54 | | - # Get destination file if specified |
55 | | - dest = self._task.args.get("dest") |
56 | | - |
57 | | - # Get proto |
58 | | - proto = self._task.args.get("protocol") |
59 | | - if proto is None: |
60 | | - proto = "scp" |
61 | | - |
62 | | - # Get mode if set |
63 | | - mode = self._task.args.get("mode") |
64 | | - if mode is None: |
65 | | - mode = "binary" |
66 | | - |
67 | | - if mode == "text": |
68 | | - try: |
69 | | - self._handle_src_option(convert_data=False) |
70 | | - except AnsibleError as exc: |
71 | | - return dict(failed=True, msg=to_text(exc)) |
72 | | - |
73 | | - # Now src has resolved file write to disk in current diectory for scp |
74 | | - src = self._task.args.get("src") |
75 | | - filename = str(uuid.uuid4()) |
76 | | - cwd = self._loader.get_basedir() |
77 | | - output_file = os.path.join(cwd, filename) |
78 | | - try: |
79 | | - with open(output_file, "wb") as f: |
80 | | - f.write(to_bytes(src, encoding="utf-8")) |
81 | | - except Exception: |
82 | | - os.remove(output_file) |
83 | | - raise |
84 | | - else: |
85 | | - try: |
86 | | - output_file = self._get_binary_src_file(src) |
87 | | - except ValueError as exc: |
88 | | - return dict(failed=True, msg=to_text(exc)) |
89 | | - |
90 | | - if socket_path is None: |
91 | | - socket_path = self._connection.socket_path |
92 | | - |
93 | | - conn = Connection(socket_path) |
94 | | - sock_timeout = conn.get_option("persistent_command_timeout") |
95 | | - |
96 | | - if dest is None: |
97 | | - dest = src_file_path_name |
98 | | - try: |
99 | | - changed = self._handle_existing_file(conn, output_file, dest, proto, sock_timeout) |
100 | | - if changed is False: |
101 | | - result["changed"] = changed |
102 | | - result["destination"] = dest |
103 | | - if mode == "text": |
104 | | - # Cleanup tmp file expanded wih ansible vars |
105 | | - os.remove(output_file) |
106 | | - return result |
107 | | - except Exception as exc: |
108 | | - result["msg"] = "Warning: %s idempotency check failed. Check dest" % exc |
109 | | - |
110 | | - try: |
111 | | - conn.copy_file( |
112 | | - source=output_file, |
113 | | - destination=dest, |
114 | | - proto=proto, |
115 | | - timeout=sock_timeout, |
| 70 | + self._check_destination = self._task.args.get("check_destination", True) |
| 71 | + self._decrypt = self._task.args.get("decrypt", True) |
| 72 | + self._dest = self._task.args.get("dest", self._src) |
| 73 | + self._mode = self._task.args.get("mode", "binary") |
| 74 | + self._protocol = self._task.args.get("protocol", "scp") |
| 75 | + |
| 76 | + self._src_real_file = self._loader.get_real_file(self._src, decrypt=self._decrypt) |
| 77 | + |
| 78 | + if self._mode == "binary": |
| 79 | + self._rendered_real_file = self._src_real_file |
| 80 | + elif self._mode == "text": |
| 81 | + self._rendered_real_file = rendered_fp |
| 82 | + template_result = self._execute_module( |
| 83 | + module_name="ansible.builtin.template", |
| 84 | + module_args={ |
| 85 | + "dest": self._rendered_real_file, |
| 86 | + "src": self._src_real_file |
| 87 | + }, |
| 88 | + task_vars=task_vars, |
116 | 89 | ) |
117 | | - except Exception as exc: |
118 | | - if to_text(exc) == "No response from server": |
119 | | - if network_os == "iosxr": |
120 | | - # IOSXR sometimes closes socket prematurely after completion |
121 | | - # of file transfer |
122 | | - result["msg"] = "Warning: iosxr scp server pre close issue. Please check dest" |
123 | | - else: |
124 | | - result["failed"] = True |
125 | | - result["msg"] = "Exception received: %s" % exc |
126 | | - |
127 | | - if mode == "text": |
128 | | - # Cleanup tmp file expanded wih ansible vars |
129 | | - os.remove(output_file) |
130 | | - |
131 | | - result["changed"] = changed |
132 | | - result["destination"] = dest |
133 | | - return result |
| 90 | + self._rendered_checksum = checksum(self._rendered_real_file) |
134 | 91 |
|
135 | | - def _handle_existing_file(self, conn, source, dest, proto, timeout): |
136 | | - """ |
137 | | - Determines whether the source and destination file match. |
| 92 | + display.vv("The rendered (if applicable) source file %s checksum is %s" % (self._rendered_real_file, self._rendered_checksum)) |
138 | 93 |
|
139 | | - :return: False if source and dest both exist and have matching sha1 sums, True otherwise. |
140 | | - """ |
141 | | - cwd = self._loader.get_basedir() |
142 | | - filename = str(uuid.uuid4()) |
143 | | - tmp_source_file = os.path.join(cwd, filename) |
144 | 94 | try: |
145 | | - conn.get_file( |
146 | | - source=dest, |
147 | | - destination=tmp_source_file, |
148 | | - proto=proto, |
149 | | - timeout=timeout, |
| 95 | + self._connection._ssh_type_conn.fetch_file( |
| 96 | + self._dest, |
| 97 | + fetched_fp, |
| 98 | + self._protocol |
150 | 99 | ) |
151 | | - except ConnectionError as exc: |
152 | | - error = to_text(exc) |
153 | | - if error.endswith("No such file or directory") or "File doesn't exist" in error: |
154 | | - if os.path.exists(tmp_source_file): |
155 | | - os.remove(tmp_source_file) |
156 | | - return True |
157 | | - try: |
158 | | - with open(source, "r") as f: |
159 | | - new_content = f.read() |
160 | | - with open(tmp_source_file, "r") as f: |
161 | | - old_content = f.read() |
162 | | - except (IOError, OSError): |
163 | | - os.remove(tmp_source_file) |
164 | | - raise |
165 | | - |
166 | | - sha1 = hashlib.sha1() |
167 | | - old_content_b = to_bytes(old_content, errors="surrogate_or_strict") |
168 | | - sha1.update(old_content_b) |
169 | | - checksum_old = sha1.digest() |
170 | | - |
171 | | - sha1 = hashlib.sha1() |
172 | | - new_content_b = to_bytes(new_content, errors="surrogate_or_strict") |
173 | | - sha1.update(new_content_b) |
174 | | - checksum_new = sha1.digest() |
175 | | - os.remove(tmp_source_file) |
176 | | - if checksum_old == checksum_new: |
177 | | - return False |
178 | | - return True |
179 | | - |
180 | | - def _get_binary_src_file(self, src): |
181 | | - working_path = self._get_working_path() |
182 | | - |
183 | | - if os.path.isabs(src) or urlsplit("src").scheme: |
184 | | - source = src |
185 | | - else: |
186 | | - source = self._loader.path_dwim_relative(working_path, "templates", src) |
187 | | - if not source: |
188 | | - source = self._loader.path_dwim_relative(working_path, src) |
189 | | - |
190 | | - if not os.path.exists(source): |
191 | | - raise ValueError("path specified in src not found") |
192 | | - |
193 | | - return source |
194 | | - |
195 | | - def _get_working_path(self): |
196 | | - cwd = self._loader.get_basedir() |
197 | | - if self._task._role is not None: |
198 | | - cwd = self._task._role._role_path |
199 | | - return cwd |
200 | | - |
201 | | - def _handle_src_option(self, convert_data=True): |
202 | | - src = self._task.args.get("src") |
203 | | - working_path = self._get_working_path() |
204 | | - |
205 | | - if os.path.isabs(src) or urlsplit("src").scheme: |
206 | | - source = src |
207 | | - else: |
208 | | - source = self._loader.path_dwim_relative(working_path, "templates", src) |
209 | | - if not source: |
210 | | - source = self._loader.path_dwim_relative(working_path, src) |
211 | | - |
212 | | - if not os.path.exists(source): |
213 | | - raise AnsibleError("path specified in src not found") |
| 100 | + except Exception as exc: |
| 101 | + if not ( |
| 102 | + "Error receiving information about file" in exc.message and |
| 103 | + "No such file or directory" in exc.message): |
| 104 | + raise exc |
| 105 | + display.vv("The file is not present on the remote device") |
| 106 | + finally: |
| 107 | + self._connection._ssh_type_conn.reset() |
| 108 | + self._dest_checksum = checksum(fetched_fp) |
214 | 109 |
|
215 | 110 | try: |
216 | | - with open(source, "r") as f: |
217 | | - template_data = to_text(f.read()) |
218 | | - except IOError as e: |
219 | | - raise AnsibleError( |
220 | | - "unable to load src file {0}, I/O error({1}): {2}".format( |
221 | | - source, e.errno, e.strerror |
| 111 | + if self._dest_checksum != self._rendered_checksum: |
| 112 | + self._connection._ssh_type_conn.put_file( |
| 113 | + self._loader.get_real_file(self._rendered_real_file), |
| 114 | + self._dest, |
| 115 | + self._protocol |
222 | 116 | ) |
223 | | - ) |
| 117 | + finally: |
| 118 | + self._connection._ssh_type_conn.reset() |
| 119 | + |
| 120 | + return result |
224 | 121 |
|
225 | | - # Create a template search path in the following order: |
226 | | - # [working_path, self_role_path, dependent_role_paths, dirname(source)] |
227 | | - searchpath = [working_path] |
228 | | - if self._task._role is not None: |
229 | | - searchpath.append(self._task._role._role_path) |
230 | | - if hasattr(self._task, "_block:"): |
231 | | - dep_chain = self._task._block.get_dep_chain() |
232 | | - if dep_chain is not None: |
233 | | - for role in dep_chain: |
234 | | - searchpath.append(role._role_path) |
235 | | - searchpath.append(os.path.dirname(source)) |
236 | | - self._templar.environment.loader.searchpath = searchpath |
237 | | - self._task.args["src"] = self._templar.template(template_data) |
238 | | - |
239 | | - def _get_network_os(self, task_vars): |
240 | | - if "network_os" in self._task.args and self._task.args["network_os"]: |
241 | | - display.vvvv("Getting network OS from task argument") |
242 | | - network_os = self._task.args["network_os"] |
243 | | - elif self._play_context.network_os: |
244 | | - display.vvvv("Getting network OS from inventory") |
245 | | - network_os = self._play_context.network_os |
246 | | - elif ( |
247 | | - "network_os" in task_vars.get("ansible_facts", {}) |
248 | | - and task_vars["ansible_facts"]["network_os"] |
249 | | - ): |
250 | | - display.vvvv("Getting network OS from fact") |
251 | | - network_os = task_vars["ansible_facts"]["network_os"] |
252 | | - else: |
253 | | - raise AnsibleError("ansible_network_os must be specified on this host") |
254 | | - |
255 | | - return network_os |
0 commit comments