Repository: ansible/pylibssh (language: python)

Issue:
Unable to transfer files using SCP to a Cisco IOS device
<!--- Verify first that your issue is not already reported on GitHub -->
<!--- Also test if the latest release and devel branch are affected too -->
<!--- Complete *all* sections as described, this form is processed automatically -->

##### SUMMARY
When attempting to transfer a file using SCP to an IOS device, you get a "No such file or directory" error. If you switch back to Paramiko it works fine.

##### ISSUE TYPE
- Bug Report

##### PYLISSH and LIBSSH VERSION
<!--- Paste verbatim output between quotes -->
```
libssh_0.9.3
ansible_pylibssh-0.4.0
```

##### OS / ENVIRONMENT
<!--- Provide all relevant information below, e.g. target OS versions, network device firmware, etc. -->
This is running from a custom EE based off of `quay.io/ansible/awx-ee:0.6.0`. The only additions are some extra packages for netbox, scp, and pylibssh.

The target is a Cisco 3750xp running IOS 15.2(4)E8. I have run this with ssh1 and ssh2 enabled on the switch, and both ended with the same result.

##### STEPS TO REPRODUCE
<!--- Describe exactly how to reproduce the problem, using a minimal test-case -->
Running this task
<!--- Paste example playbooks or commands between quotes below -->
```
- name: transfering upgrade image to switch...
  block:
    - ansible.netcommon.net_put:
        src: "../files/c3750e-universalk9-mz.152-4.E10.bin"
        dest: "flash:c3750e-universalk9-mz.152-4.E10.bin"
  when: ansible_network_os == 'cisco.ios.ios'
```

<!--- HINT: You can paste gist.github.com links for larger files -->

##### EXPECTED RESULTS
<!--- Describe what you expected to happen when running the steps above -->
This is actually the output when you add the var `ansible_network_cli_ssh_type: paramiko`, nothing else was changed between this run and the failed one using libssh.

SSH Logs from the switch: https://gist.github.com/vrelk/da99ec3107e18d7239f0286ee582e2c9 (start/end trimmed)

```
TASK [upgrade : ansible.netcommon.net_put] *************************************
task path: /runner/project/roles/upgrade/tasks/xfer.yml:3
redirecting (type: connection) ansible.builtin.network_cli to ansible.netcommon.network_cli
redirecting (type: become) ansible.builtin.enable to ansible.netcommon.enable
<10.255.79.10> attempting to start connection
<10.255.79.10> using connection plugin ansible.netcommon.network_cli
Found ansible-connection at path /usr/local/bin/ansible-connection
<10.255.79.10> found existing local domain socket, using it!
<10.255.79.10> updating play_context for connection
<10.255.79.10> 
<10.255.79.10> local domain socket path is /home/runner/.ansible/pc/11ab5503fd
Getting network OS from inventory
changed: [3750xp-stack] => {
    "changed": true,
    "destination": "flash:c3750e-universalk9-mz.152-4.E10.bin"
}
```

##### ACTUAL RESULTS
<!--- Describe what actually happened. If possible run with extra verbosity (-vvvv) -->
This is the debug output on the switch that corresponds to this task (with ssh and scp debugging enabled): https://gist.github.com/vrelk/23fa0dfd09b8f27f79118684bac75d08
<!--- Paste verbatim command output between quotes -->
```
TASK [upgrade : ansible.netcommon.net_put] *************************************
task path: /runner/project/roles/upgrade/tasks/xfer.yml:3
redirecting (type: connection) ansible.builtin.network_cli to ansible.netcommon.network_cli
redirecting (type: become) ansible.builtin.enable to ansible.netcommon.enable
<10.255.79.10> attempting to start connection
<10.255.79.10> using connection plugin ansible.netcommon.network_cli
Found ansible-connection at path /usr/local/bin/ansible-connection
<10.255.79.10> found existing local domain socket, using it!
<10.255.79.10> updating play_context for connection
<10.255.79.10> 
<10.255.79.10> local domain socket path is /home/runner/.ansible/pc/11ab5503fd
Getting network OS from inventory
fatal: [3750xp-stack]: FAILED! => {
    "changed": false,
    "destination": "flash:c3750e-universalk9-mz.152-4.E10.bin",
    "msg": "Exception received: Error transferring file to flash:c3750e-universalk9-mz.152-4.E10.bin: Initializing SCP session of remote file [flash:c3750e-universalk9-mz.152-4.E10.bin] for write failed with error [b'Socket error: No such file or directory']"
}
```



Discussion / hints:
I am having a very similar issue copying files to IOS devices with net_put. It works with paramiko but not with libssh. This issue may be related to https://github.com/ansible/pylibssh/issues/500

Here is my playbook:

- hosts: rtr01
  tasks:
      - name: PUSH TEST FILE TO DEVICE
      ansible.netcommon.net_put:
        src: /home/software/cisco/<ios>.tgz
        dest: flash:/<ios>.tgz
      vars:
         ansible_connection: network_cli 
         ansible_network_cli_ssh_type: paramiko 

It works when I specify "ansible_network_cli_ssh_type: paramiko" variable, but without specifying paramiko and using libssh it fails with this error:
"Exception received unable to connect to socket /home/<user>/.ansible/pc/17de54cbc6. See the socket path issue category in Network Debug and Troubleshooting guide."


Fix patch:
diff --git a/docs/changelog-fragments/208.bugfix.rst b/docs/changelog-fragments/208.bugfix.rst
new file mode 120000
index 000000000..aad01ce8b
--- /dev/null
+++ b/docs/changelog-fragments/208.bugfix.rst
@@ -0,0 +1,1 @@
+620.bugfix.rst
\ No newline at end of file
diff --git a/docs/changelog-fragments/325.bugfix.rst b/docs/changelog-fragments/325.bugfix.rst
new file mode 120000
index 000000000..aad01ce8b
--- /dev/null
+++ b/docs/changelog-fragments/325.bugfix.rst
@@ -0,0 +1,1 @@
+620.bugfix.rst
\ No newline at end of file
diff --git a/docs/changelog-fragments/620.bugfix.rst b/docs/changelog-fragments/620.bugfix.rst
new file mode 100644
index 000000000..d4db77c88
--- /dev/null
+++ b/docs/changelog-fragments/620.bugfix.rst
@@ -0,0 +1,1 @@
+Downloading non-existent remote files via SCP no longer crashes the program -- by :user:`Jakuje`.
diff --git a/src/pylibsshext/scp.pyx b/src/pylibsshext/scp.pyx
index 46004f959..c3f9933f5 100644
--- a/src/pylibsshext/scp.pyx
+++ b/src/pylibsshext/scp.pyx
@@ -96,7 +96,7 @@ cdef class SCP:
         :param local_file: The path on the local host where the file should be placed
         :type local_file: str
         """
-        cdef char *read_buffer
+        cdef char *read_buffer = NULL
 
         remote_file_b = remote_file
         if isinstance(remote_file_b, unicode):


Test patch:
diff --git a/tests/unit/scp_test.py b/tests/unit/scp_test.py
index 4b210cb14..07072124e 100644
--- a/tests/unit/scp_test.py
+++ b/tests/unit/scp_test.py
@@ -2,10 +2,13 @@
 
 """Tests suite for scp."""
 
+import os
 import uuid
 
 import pytest
 
+from pylibsshext.errors import LibsshSCPException
+
 
 @pytest.fixture
 def ssh_scp(ssh_client_session):
@@ -57,3 +60,18 @@ def test_get(dst_path, src_path, ssh_scp, transmit_payload):
     """Check that SCP file download works."""
     ssh_scp.get(str(src_path), str(dst_path))
     assert dst_path.read_bytes() == transmit_payload
+
+
+@pytest.fixture
+def path_to_non_existent_src_file(tmp_path):
+    """Return a remote path that does not exist."""
+    path = tmp_path / 'non-existing.txt'
+    assert not path.exists()
+    return path
+
+
+def test_copy_from_non_existent_remote_path(path_to_non_existent_src_file, ssh_scp):
+    """Check that SCP file download raises exception if the remote file is missing."""
+    error_msg = '^Error receiving information about file:'
+    with pytest.raises(LibsshSCPException, match=error_msg):
+        ssh_scp.get(str(path_to_non_existent_src_file), os.devnull)