Skip to content

proxy_command support#819

Open
chidanandpujar wants to merge 2 commits into
Juniper:masterfrom
chidanandpujar:proxysupport
Open

proxy_command support#819
chidanandpujar wants to merge 2 commits into
Juniper:masterfrom
chidanandpujar:proxysupport

Conversation

@chidanandpujar
Copy link
Copy Markdown
Collaborator

Added support for proxy_command='-o ProxyCommand="ssh -W %h:%p -q regress@bastian"'

Copy link
Copy Markdown
Collaborator

@dineshbaburam91 dineshbaburam91 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please update all possible functional tests and add the functional/unit test cases

@chidanandpujar
Copy link
Copy Markdown
Collaborator Author

chidanandpujar commented May 18, 2026

Please update all possible functional tests and add the functional/unit test cases

functional test case is added

PyEZ logs

username@masterhost:~/junos_eznc_proxy$ cat test.py 
from jnpr.junos import Device

dev = Device(
    host="xx.xx.xx.xx",
    user="username",
    port=22,
    ssh_private_key_file="~/.ssh/id_rsa",
    proxy_command="ssh -W %h:%p -q username@xx.xx.xx.xx",
)
dev.open()
print(dev.facts)
dev.close()

username@masterhost:~/junos_eznc_proxy$ python3 test.py 
{'2RE': True, 'HOME': '/var/home/username', 'RE0': {'mastership_state': 'master', 'status': 'OK', 'model': 'RE-VMX', 'last_reboot_reason': 'Router rebooted after a normal shutdown.', 'up_time': '30 days, 20 hours, 46 minutes, 55 seconds'}, 'RE1': {'mastership_state': 'backup', 'status': 'OK', 'model': 'RE-VMX', 'last_reboot_reason': 'Router rebooted after a normal shutdown.', 'up_time': '47 days, 20 hours, 50 minutes, 37 seconds'}, 'RE_hw_mi': False, 'current_re': ['re0', 'master', 'node', 'fwdd', 'member', 'pfem'], 'domain': 'englab.juniper.net', 'fqdn': xyz', 'hostname': 'evoeventtesta', 'hostname_info': {'re0': 'evoeventtesta', 're1': 'evoeventtesta11'}, 'ifd_style': 'CLASSIC', 'junos_info': {'re0': {'text': '18.4R3-S11', 'object': junos.version_info(major=(18, 4), type=R, minor=3-S11, build=3-S11)}, 're1': {'text': '18.4R3-S11', 'object': junos.version_info(major=(18, 4), type=R, minor=3-S11, build=3-S11)}}, 'master': 'RE0', 'model': 'MX960', 'model_info': {'re0': 'MX960', 're1': 'MX960'}, 'personality': 'MX', 're_info': {'default': {'0': {'mastership_state': 'master', 'status': 'OK', 'model': 'RE-VMX', 'last_reboot_reason': 'Router rebooted after a normal shutdown.'}, '1': {'mastership_state': 'backup', 'status': 'OK', 'model': 'RE-VMX', 'last_reboot_reason': 'Router rebooted after a normal shutdown.'}, 'default': {'mastership_state': 'master', 'status': 'OK', 'model': 'RE-VMX', 'last_reboot_reason': 'Router rebooted after a normal shutdown.'}}}, 're_master': {'default': '0'}, 'serialnumber': 'VMXeaac', 'srx_cluster': None, 'srx_cluster_id': None, 'srx_cluster_redundancy_group': None, 'switch_style': 'BRIDGE_DOMAIN', 'vc_capable': False, 'vc_fabric': None, 'vc_master': None, 'vc_mode': None, 'version': '18.4R3-S11', 'version_RE0': '18.4R3-S11', 'version_RE1': '18.4R3-S11', 'version_info': junos.version_info(major=(18, 4), type=R, minor=3-S11, build=3-S11), 'virtual': True, 'vmhost': False, 'vmhost_info': {}}
                             

username@masterhost:~/junos_eznc_proxy$ cat test_neg.py 
from jnpr.junos import Device

dev = Device(
    host="xx.xx.xx.xx",
    user="username",
    port=22,
    ssh_private_key_file="~/.ssh/id_rsa",
    #proxy_command="ssh -W %h:%p -q username@xx.xx.xx.xx",
    ssh_config="/dev/null",
)
dev.open()
print(dev.facts)
dev.close()

username@masterhost:~/junos_eznc_proxy$ python3 test_neg.py 
Traceback (most recent call last):
  File "/home/username/.local/lib/python3.12/site-packages/jnpr/junos/device.py", line 1421, in open
    self._conn = netconf_ssh.connect(
                 ^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/ncclient/manager.py", line 242, in connect
    return connect_ssh(*args, **kwds)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/ncclient/manager.py", line 155, in connect_ssh
    session.connect(*args, **kwds)
  File "/usr/local/lib/python3.12/site-packages/ncclient/transport/ssh.py", line 282, in connect
    raise SSHError("Could not open socket to %s:%s" % (host, port))
ncclient.transport.errors.SSHError: Could not open socket to xx.xx.xx.xx:22

Ansible logs:

xyz@masterhost:~/ansible_collection_proxy/ansible-junos-stdlib/tests$ cat inventory_proxy
[junos_devices]
evoeventtesta ansible_host=xyz

[junos_devices:vars]
ansible_connection=local
ansible_user=xyz
ansible_private_key_file=~/.ssh/id_rsa
ansible_port=22

[all:vars]
ansible_python_interpreter=/usr/bin/python3

xyz@masterhost:~/ansible_collection_proxy/ansible-junos-stdlib/tests$ ansible-playbook -i inventory_proxy pb.juniper_junos_proxy_command.yml 

PLAY [Test proxy_command via juniper.device — positive] ******************************************************************************

TASK [Get facts via bastion using proxy_command] *************************************************************************************
[WARNING]: Deprecation warnings can be disabled by setting `deprecation_warnings=False` in ansible.cfg.
[DEPRECATION WARNING]: Importing 'to_bytes' from 'ansible.module_utils._text' is deprecated. This feature will be removed from ansible-core version 2.24. Use ansible.module_utils.common.text.converters instead.
ok: [evoeventtesta]

TASK [Print hostname fact] ***********************************************************************************************************
ok: [evoeventtesta] => {
    "msg": "Connected to: evoeventtesta"
}

TASK [Assert connection succeeded] ***************************************************************************************************
ok: [evoeventtesta] => {
    "changed": false,
    "msg": "PASS: Connected via bastion proxy_command"
}

PLAY [Test proxy_command via juniper.device — negative] ******************************************************************************

TASK [Attempt facts without proxy (expect failure)] **********************************************************************************
[ERROR]: Task failed: Module failed: Unable to make a PyEZ connection: ConnectRefusedError(xyz)
Origin: /home/xyz/ansible_collection_proxy/ansible-junos-stdlib/tests/pb.juniper_junos_proxy_command.yml:36:7

34
35   tasks:
36     - name: Attempt facts without proxy (expect failure)
         ^ column 7

fatal: [evoeventtesta]: FAILED! => {"changed": false, "msg": "Unable to make a PyEZ connection: ConnectRefusedError(xyz)"}
...ignoring

TASK [Assert connection was refused (direct access blocked)] *************************************************************************
ok: [evoeventtesta] => {
    "changed": false,
    "msg": "PASS: Direct access correctly blocked — proxy_command required"
}

PLAY RECAP ***************************************************************************************************************************
evoeventtesta              : ok=5    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=1   


xyz@masterhost:~/ansible_collection_proxy/ansible-junos-stdlib/tests$ cat inventory_proxy
[junos_devices]
evoeventtesta ansible_host=xyz

[junos_devices:vars]
ansible_connection=juniper.device.pyez
ansible_user=xyz
ansible_private_key_file=~/.ssh/id_rsa
ansible_port=22

[all:vars]
ansible_python_interpreter=/usr/bin/python3

xyz@masterhost:~/ansible_collection_proxy/ansible-junos-stdlib/tests$ ansible-playbook -i inventory_proxy pb.juniper_junos_proxy_command.yml 

PLAY [Test proxy_command via juniper.device — positive] ******************************************************************************

TASK [Get facts via bastion using proxy_command] *************************************************************************************
[WARNING]: Deprecation warnings can be disabled by setting `deprecation_warnings=False` in ansible.cfg.
[DEPRECATION WARNING]: Importing 'to_bytes' from 'ansible.module_utils._text' is deprecated. This feature will be removed from ansible-core version 2.24. Use ansible.module_utils.common.text.converters instead.
ok: [evoeventtesta]

TASK [Print hostname fact] ***********************************************************************************************************
ok: [evoeventtesta] => {
    "msg": "Connected to: evoeventtesta"
}

TASK [Assert connection succeeded] ***************************************************************************************************
ok: [evoeventtesta] => {
    "changed": false,
    "msg": "PASS: Connected via bastion proxy_command"
}

PLAY [Test proxy_command via juniper.device — negative] ******************************************************************************

TASK [Attempt facts without proxy (expect failure)] **********************************************************************************
[ERROR]: Task failed: Module failed: Unable to make a PyEZ connection: ConnectRefusedError(xyz)
Origin: /home/xyz/ansible_collection_proxy/ansible-junos-stdlib/tests/pb.juniper_junos_proxy_command.yml:36:7

34
35   tasks:
36     - name: Attempt facts without proxy (expect failure)
         ^ column 7

fatal: [evoeventtesta]: FAILED! => {"changed": false, "msg": "Task failed: Module failed: Unable to make a PyEZ connection: ConnectRefusedError(xyz)"}
...ignoring

TASK [Assert connection was refused (direct access blocked)] *************************************************************************
ok: [evoeventtesta] => {
    "changed": false,
    "msg": "PASS: Direct access correctly blocked — proxy_command required"
}

PLAY RECAP ***************************************************************************************************************************
evoeventtesta              : ok=5    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=1   


regress@masterhost:~/ansible_collection_proxy/ansible-junos-stdlib/tests$ cat inventory_proxy
[junos_devices]
evoeventtesta ansible_host=xx.xx.xx.xx

[junos_devices:vars]
#ansible_connection=juniper.device.pyez
ansible_connection=local
ansible_user=regress
ansible_private_key_file=~/.ssh/id_rsa
ansible_port=22

[all:vars]
ansible_python_interpreter=/usr/bin/python3


username@masterhost:~/ansible_collection_proxy/ansible-junos-stdlib/tests$ cat pb.juniper_junos_proxy_command.yml 
---
# Play 1: Positive — connect via bastion using proxy_command
- name: Test proxy_command via juniper.device — positive
  hosts: all
  gather_facts: false

  vars:
    proxy_command: '-o ProxyCommand="ssh -W %h:%p -q username@xx.xx.xx.xx"'

  tasks:
    - name: Get facts via bastion using proxy_command
      juniper.device.facts:
      register: result

    - name: Print hostname fact
      ansible.builtin.debug:
        msg: "Connected to: {{ result.facts.hostname }}"

    - name: Assert connection succeeded
      ansible.builtin.assert:
        that:
          - result.failed == false
          - result.facts.hostname is defined
        success_msg: "PASS: Connected via bastion proxy_command"
        fail_msg: "FAIL: Could not connect via bastion"

# Play 2: Negative — direct access must be blocked when proxy_command is unset
- name: Test proxy_command via juniper.device — negative
  hosts: all
  gather_facts: false

  vars:
    proxy_command: ""    # override inventory value — disables proxy

  tasks:
    - name: Attempt facts without proxy (expect failure)
      juniper.device.facts:
      register: result
      ignore_errors: true

    - name: Assert connection was refused (direct access blocked)
      ansible.builtin.assert:
        that:
          - result.failed == true
          - "'ConnectRefusedError' in result.msg or 'ConnectError' in result.msg or 'Unable to make' in result.msg"
        success_msg: "PASS: Direct access correctly blocked — proxy_command required"
        fail_msg: "FAIL: Direct access succeeded but should have been blocked"
username@masterhost:~/ansible_collection_proxy/ansible-junos-stdlib/tests$ ansible-playbook -i inventory_proxy_both pb.juniper_junos_proxy_command.yml  -v
Using /home/username/ansible_collection_proxy/ansible-junos-stdlib/tests/ansible.cfg as config file

PLAY [Test proxy_command via juniper.device — positive] ******************************************************************************

TASK [Get facts via bastion using proxy_command] *************************************************************************************
[WARNING]: Deprecation warnings can be disabled by setting `deprecation_warnings=False` in ansible.cfg.
[DEPRECATION WARNING]: Importing 'to_bytes' from 'ansible.module_utils._text' is deprecated. This feature will be removed from ansible-core version 2.24. Use ansible.module_utils.common.text.converters instead.
ok: [local_connection_testcases] => {"ansible_facts": {"junos": {"HOME": "/var/home/username", "RE0": {"last_reboot_reason": "Router rebooted after a normal shutdown.", "mastership_state": "master", "model": "RE-VMX", "status": "OK", "up_time": "30 days, 20 hours, 58 minutes, 51 seconds"}, "RE1": {"last_reboot_reason": "Router rebooted after a normal shutdown.", "mastership_state": "backup", "model": "RE-VMX", "status": "OK", "up_time": "47 days, 21 hours, 2 minutes, 40 seconds"}, "RE_hw_mi": false, "current_re": ["re0", "master", "node", "fwdd", "member", "pfem"], "domain": "englab.juniper.net", "fqdn": "evoeventtesta.englab.juniper.net", "has_2RE": true, "hostname": "evoeventtesta", "hostname_info": {"re0": "evoeventtesta", "re1": "evoeventtesta11"}, "ifd_style": "CLASSIC", "junos_info": {"re0": {"object": {"as_tuple": [18, 4, "R", "3-S11", "3-S11"], "build": "3-S11", "major": [18, 4], "minor": "3-S11", "type": "R", "v_dict": {"build": "3-S11", "major": [18, 4], "minor": "3-S11", "type": "R"}}, "text": "18.4R3-S11"}, "re1": {"object": {"as_tuple": [18, 4, "R", "3-S11", "3-S11"], "build": "3-S11", "major": [18, 4], "minor": "3-S11", "type": "R", "v_dict": {"build": "3-S11", "major": [18, 4], "minor": "3-S11", "type": "R"}}, "text": "18.4R3-S11"}}, "master": "RE0", "master_state": true, "model": "MX960", "model_info": {"re0": "MX960", "re1": "MX960"}, "personality": "MX", "re_info": {"default": {"0": {"last_reboot_reason": "Router rebooted after a normal shutdown.", "mastership_state": "master", "model": "RE-VMX", "status": "OK"}, "1": {"last_reboot_reason": "Router rebooted after a normal shutdown.", "mastership_state": "backup", "model": "RE-VMX", "status": "OK"}, "default": {"last_reboot_reason": "Router rebooted after a normal shutdown.", "mastership_state": "master", "model": "RE-VMX", "status": "OK"}}}, "re_master": {"default": "0"}, "re_name": "re0", "serialnumber": "VMXeaac", "srx_cluster": null, "srx_cluster_id": null, "srx_cluster_redundancy_group": null, "switch_style": "BRIDGE_DOMAIN", "vc_capable": false, "vc_fabric": null, "vc_master": null, "vc_mode": null, "version": "18.4R3-S11", "version_RE0": "18.4R3-S11", "version_RE1": "18.4R3-S11", "version_info": {"as_tuple": [18, 4, "R", "3-S11", "3-S11"], "build": "3-S11", "major": [18, 4], "minor": "3-S11", "type": "R", "v_dict": {"build": "3-S11", "major": [18, 4], "minor": "3-S11", "type": "R"}}, "virtual": true, "vmhost": false, "vmhost_info": {}}}, "changed": false, "facts": {"HOME": "/var/home/username", "RE0": {"last_reboot_reason": "Router rebooted after a normal shutdown.", "mastership_state": "master", "model": "RE-VMX", "status": "OK", "up_time": "30 days, 20 hours, 58 minutes, 51 seconds"}, "RE1": {"last_reboot_reason": "Router rebooted after a normal shutdown.", "mastership_state": "backup", "model": "RE-VMX", "status": "OK", "up_time": "47 days, 21 hours, 2 minutes, 40 seconds"}, "RE_hw_mi": false, "current_re": ["re0", "master", "node", "fwdd", "member", "pfem"], "domain": "englab.juniper.net", "fqdn": "evoeventtesta.englab.juniper.net", "has_2RE": true, "hostname": "evoeventtesta", "hostname_info": {"re0": "evoeventtesta", "re1": "evoeventtesta11"}, "ifd_style": "CLASSIC", "junos_info": {"re0": {"object": {"as_tuple": [18, 4, "R", "3-S11", "3-S11"], "build": "3-S11", "major": [18, 4], "minor": "3-S11", "type": "R", "v_dict": {"build": "3-S11", "major": [18, 4], "minor": "3-S11", "type": "R"}}, "text": "18.4R3-S11"}, "re1": {"object": {"as_tuple": [18, 4, "R", "3-S11", "3-S11"], "build": "3-S11", "major": [18, 4], "minor": "3-S11", "type": "R", "v_dict": {"build": "3-S11", "major": [18, 4], "minor": "3-S11", "type": "R"}}, "text": "18.4R3-S11"}}, "master": "RE0", "master_state": true, "model": "MX960", "model_info": {"re0": "MX960", "re1": "MX960"}, "personality": "MX", "re_info": {"default": {"0": {"last_reboot_reason": "Router rebooted after a normal shutdown.", "mastership_state": "master", "model": "RE-VMX", "status": "OK"}, "1": {"last_reboot_reason": "Router rebooted after a normal shutdown.", "mastership_state": "backup", "model": "RE-VMX", "status": "OK"}, "default": {"last_reboot_reason": "Router rebooted after a normal shutdown.", "mastership_state": "master", "model": "RE-VMX", "status": "OK"}}}, "re_master": {"default": "0"}, "re_name": "re0", "serialnumber": "VMXeaac", "srx_cluster": null, "srx_cluster_id": null, "srx_cluster_redundancy_group": null, "switch_style": "BRIDGE_DOMAIN", "vc_capable": false, "vc_fabric": null, "vc_master": null, "vc_mode": null, "version": "18.4R3-S11", "version_RE0": "18.4R3-S11", "version_RE1": "18.4R3-S11", "version_info": {"as_tuple": [18, 4, "R", "3-S11", "3-S11"], "build": "3-S11", "major": [18, 4], "minor": "3-S11", "type": "R", "v_dict": {"build": "3-S11", "major": [18, 4], "minor": "3-S11", "type": "R"}}, "virtual": true, "vmhost": false, "vmhost_info": {}}}

TASK [Print hostname fact] ***********************************************************************************************************
ok: [local_connection_testcases] => {
    "msg": "Connected to: evoeventtesta"
}

TASK [Assert connection succeeded] ***************************************************************************************************
ok: [local_connection_testcases] => {
    "changed": false,
    "msg": "PASS: Connected via bastion proxy_command"
}

PLAY [Test proxy_command via juniper.device — negative] ******************************************************************************

TASK [Attempt facts without proxy (expect failure)] **********************************************************************************
[ERROR]: Task failed: Module failed: Unable to make a PyEZ connection: ConnectRefusedError(xx.xx.xx.xx)
Origin: /home/username/ansible_collection_proxy/ansible-junos-stdlib/tests/pb.juniper_junos_proxy_command.yml:36:7

34
35   tasks:
36     - name: Attempt facts without proxy (expect failure)
         ^ column 7

fatal: [local_connection_testcases]: FAILED! => {"changed": false, "msg": "Unable to make a PyEZ connection: ConnectRefusedError(xx.xx.xx.xx)"}
...ignoring

TASK [Assert connection was refused (direct access blocked)] *************************************************************************
ok: [local_connection_testcases] => {
    "changed": false,
    "msg": "PASS: Direct access correctly blocked — proxy_command required"
}

PLAY RECAP ***************************************************************************************************************************
local_connection_testcases : ok=5    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=1   




Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants