Skip to content

file-delete via juniper.device.rpc reports a failure path and UnboundLocalError #821

@andyjsharp

Description

@andyjsharp

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions