Skip to content

Commit a19821e

Browse files
committed
Enable libssh For Cisco IOS Devices
* The Cisco SSH server only supports the SCP protocol and subsequent attempts to interact with the device after a fetch or a put will fail. This patch addresses > * As I researched my own troubles in leveraging `libssh` on an older container image I wrote this patch. It needs a lot more work including at least: * Need to add check mode support * Need to finish the idempotency checking (namely the reporting back to Ansible) * Need to fix the `paramiko` support * Need to reinstate the check for only `network_cli` connection types * Was tested using 358553e9765e7873a9d3972dd877dfe415c1dc06 of libssh from https://gitlab.com/libssh/libssh-mirror/ * Was tested using a Cisco 2960X running 15.1
1 parent c07aaf0 commit a19821e

File tree

1 file changed

+83
-217
lines changed

1 file changed

+83
-217
lines changed

plugins/action/net_put.py

Lines changed: 83 additions & 217 deletions
Original file line numberDiff line numberDiff line change
@@ -7,249 +7,115 @@
77

88
__metaclass__ = type
99

10-
import hashlib
1110
import os
12-
import uuid
11+
import tempfile
1312

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
1813
from ansible.plugins.action import ActionBase
1914
from ansible.utils.display import Display
15+
from ansible.utils.hashing import checksum
2016

2117

2218
display = Display()
2319

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
2448

2549
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]
3150

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)
3355

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)
4361

4462
try:
45-
src = self._task.args["src"]
63+
self._src = self._task.args.get("src")
4664
except KeyError as exc:
4765
return {
4866
"failed": True,
4967
"msg": "missing required argument: %s" % exc,
5068
}
5169

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,
11689
)
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)
13491

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))
13893

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)
14494
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
15099
)
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)
214109

215110
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
222116
)
223-
)
117+
finally:
118+
self._connection._ssh_type_conn.reset()
119+
120+
return result
224121

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

Comments
 (0)