Summary
When invoking file-delete via juniper.device.rpc, the module reports a failure path (No start of json char found and UnboundLocalError) even though the file is actually deleted on the device.
-
juniper.device.rpc should treat successful file-delete responses as success.
-
No module-level UnboundLocalError should be emitted.
-
No generic wrapper message (No start of json char found) should be produced for this successful operation.
-
The RPC task returns data indicating module failure with:
No start of json char found
UnboundLocalError: cannot access local variable 'resp' ...
-
Verification confirms the target file is deleted successfully.
Environment
- juniper.device==2.0.1
- ansible==11.1.0
- ansible-core==2.18.1
- junos-eznc==2.7.6
- SRX320 running 23.4R2-S5.5
Output Excerpt
TASK [PUT file to device] ... changed: [test-device-1]
TASK [DELETE file from device (RPC file-delete)] ... ok: [test-device-1]
TASK [Verify file no longer exists] ... ok: [test-device-1]
"msg": {
"contains_json_start_error": true,
"contains_unbound_local_error": true,
"delete_msg": "MODULE FAILURE: No start of json char found\nSee stdout/stderr for the exact error",
"delete_exception": "... UnboundLocalError: cannot access local variable 'resp' where it is not associated with a value",
"delete_module_stderr": "... UnboundLocalError: cannot access local variable 'resp' where it is not associated with a value",
"remote_path": "/var/tmp/jmuse-dev-bug-rpc-<timestamp>.txt",
"verify_output": "/var/tmp/jmuse-dev-bug-rpc-<timestamp>.txt: No such file or directory"
}
Playbook
An example playbook that creates a test file locally, transfers to target device and then removes the file, capturing output.
- name: simple file put and delete via RPC
hosts: "{{ target_device | default('targets') }}"
gather_facts: false
connection: juniper.device.pyez
collections:
- juniper.device
vars:
jmuse_remote_dir: "/var/tmp"
tasks:
- name: Generate test identifier once for this run
ansible.builtin.set_fact:
jmuse_test_id: "{{ lookup('pipe', 'date +%Y%m%dT%H%M%S') }}"
changed_when: false
- name: Resolve stable test paths for this run
ansible.builtin.set_fact:
jmuse_remote_filename: "jmuse-dev-bug-rpc-{{ jmuse_test_id }}.txt"
jmuse_remote_path: "{{ jmuse_remote_dir }}/jmuse-dev-bug-rpc-{{ jmuse_test_id }}.txt"
jmuse_local_payload_path: "/tmp/jmuse-dev-bug-rpc-{{ jmuse_test_id }}-{{ inventory_hostname }}.txt"
changed_when: false
- name: Create local payload file on controller
ansible.builtin.copy:
dest: "{{ jmuse_local_payload_path }}"
mode: "0600"
content: |
JMUSE dev-bug RPC repro baseline
host={{ inventory_hostname }}
created_utc={{ lookup('pipe', 'date -u +%Y-%m-%dT%H:%M:%SZ') }}
delegate_to: localhost
changed_when: false
- name: PUT file to device
juniper.device.file_copy:
action: put
local_dir: "{{ jmuse_local_payload_path | dirname }}"
file: "{{ jmuse_local_payload_path | basename }}"
transfer_filename: "{{ jmuse_remote_filename }}"
remote_dir: "{{ jmuse_remote_dir }}"
checksum: true
register: jmuse_put_result
- name: DELETE file from device (RPC file-delete)
juniper.device.rpc:
rpc: file-delete
kwargs:
path: "{{ jmuse_remote_path }}"
register: jmuse_delete_result
failed_when: false
changed_when: false
- name: Verify file no longer exists
juniper.device.command:
commands:
- "file list {{ jmuse_remote_path }}"
formats:
- text
register: jmuse_verify_result
failed_when: false
changed_when: false
- name: Show RPC delete summary
ansible.builtin.debug:
msg:
put_failed: "{{ jmuse_put_result.failed | default(false) }}"
delete_failed: "{{ jmuse_delete_result.failed | default(false) }}"
delete_msg: "{{ jmuse_delete_result.msg | default('') }}"
delete_exception: "{{ jmuse_delete_result.exception | default('') }}"
delete_module_stderr: "{{ jmuse_delete_result.module_stderr | default('') }}"
contains_unbound_local_error: "{{ 'UnboundLocalError' in ((jmuse_delete_result.module_stderr | default('')) + (jmuse_delete_result.exception | default(''))) }}"
contains_json_start_error: "{{ 'No start of json char found' in ((jmuse_delete_result.module_stderr | default('')) + (jmuse_delete_result.msg | default('')) + (jmuse_delete_result.exception | default(''))) }}"
remote_path: "{{ jmuse_remote_path }}"
verify_output: "{{ jmuse_verify_result.stdout | default(jmuse_verify_result.msg | default('')) }}"
- name: Clean up local payload file
ansible.builtin.file:
path: "{{ jmuse_local_payload_path }}"
state: absent
delegate_to: localhost
changed_when: false
Analysis
juniper.device.plugins.modules.rpc (rpc.py) The module reports an internal failure path while the underlying device operation succeeds. This creates false-negative automation signals and forces downstream users to add defensive error-ignore logic.
What is happening:
rpc.py:560 catches generic Exception from get_rpc.
It only re-raises when exception text contains RpcError or ConnectError.
For other exceptions, it falls through without setting resp.
Later rpc.py:594 uses resp and triggers UnboundLocalError.
Summary
When invoking file-delete via juniper.device.rpc, the module reports a failure path (No start of json char found and UnboundLocalError) even though the file is actually deleted on the device.
juniper.device.rpc should treat successful file-delete responses as success.
No module-level UnboundLocalError should be emitted.
No generic wrapper message (No start of json char found) should be produced for this successful operation.
The RPC task returns data indicating module failure with:
No start of json char foundUnboundLocalError: cannot access local variable 'resp' ...Verification confirms the target file is deleted successfully.
Environment
Output Excerpt
Playbook
An example playbook that creates a test file locally, transfers to target device and then removes the file, capturing output.
Analysis
juniper.device.plugins.modules.rpc (rpc.py) The module reports an internal failure path while the underlying device operation succeeds. This creates false-negative automation signals and forces downstream users to add defensive error-ignore logic.
What is happening:
rpc.py:560 catches generic Exception from get_rpc.
It only re-raises when exception text contains RpcError or ConnectError.
For other exceptions, it falls through without setting resp.
Later rpc.py:594 uses resp and triggers UnboundLocalError.