Skip to content

Commit 5c4c91b

Browse files
author
Shiva Shankar Vaddepally
committed
Fix for running cli commands through ansible using ssh connection.
Signed-off-by: Shiva Shankar Vaddepally <[email protected]>
1 parent 83af1b2 commit 5c4c91b

2 files changed

Lines changed: 183 additions & 9 deletions

File tree

README.md

Lines changed: 115 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,9 +132,122 @@ Refer to the [NetScaler ADM as an API proxy server](https://docs.netscaler.com/e
132132
Refer to the [sample_playboook](https://github.com/netscaler/ansible-collection-netscaleradc/tree/main/examples) and [playbook_anatomy.md](https://github.com/netscaler/ansible-collection-netscaleradc/blob/main/playbook_anatomy.md).
133133
134134
135-
### SSH_connections
135+
### SSH Connection Plugin
136136
137-
Refer to [SSH_connections examples](https://github.com/netscaler/ansible-collection-netscaleradc/tree/main/examples/ssh_connections) to know how `ansible.builtins.` plugins can be used to configure the NetScaler ADC.
137+
The collection provides an SSH connection plugin (`netscaler.adc.ssh_netscaler_adc`) that enables direct SSH connectivity to NetScaler ADC devices for executing shell commands and CLI operations.
138+
139+
#### Prerequisites
140+
141+
- SSH key-based authentication must be configured
142+
- SSH access to the NetScaler ADC device
143+
- Private SSH key file available on the control machine
144+
145+
#### Inventory Configuration
146+
147+
Configure your inventory file with the required SSH parameters:
148+
149+
**Example 1: Group-based inventory**
150+
```ini
151+
[demo_netscaler1]
152+
demo_netscalers
153+
154+
[demo_netscaler1:vars]
155+
ansible_host=10.10.10.10
156+
ansible_user=nsroot
157+
ansible_ssh_private_key_file=~/.ssh/id_rsa
158+
nitro_pass=YourPassword
159+
```
160+
161+
**Example 2: Inline inventory**
162+
```ini
163+
[demo_netscalers]
164+
netscaler_adc ansible_host=10.10.10.10 ansible_user=nsroot ansible_ssh_private_key_file=~/.ssh/id_rsa
165+
```
166+
167+
**Required inventory parameters:**
168+
- `ansible_host` - IP address or hostname of NetScaler ADC
169+
- `ansible_user` - SSH username (typically `nsroot`)
170+
- `ansible_ssh_private_key_file` - Path to SSH private key
171+
- `nitro_pass` - Password for nscli commands (required when using CLI commands)
172+
173+
#### Playbook Structure for Shell Commands
174+
175+
To execute FreeBSD shell commands on the NetScaler appliance:
176+
177+
```yaml
178+
---
179+
- name: Execute shell commands on NetScaler
180+
hosts: demo_netscalers
181+
connection: netscaler.adc.ssh_netscaler_adc
182+
remote_user: nsroot
183+
gather_facts: false
184+
185+
vars:
186+
ansible_python_interpreter: /var/python/bin/python
187+
188+
tasks:
189+
- name: List files in /var/tmp
190+
ansible.builtin.command: "ls -lhrt /var/tmp/"
191+
register: result
192+
changed_when: false
193+
194+
- name: Display output
195+
ansible.builtin.debug:
196+
msg: "{{ result.stdout_lines }}"
197+
```
198+
199+
**Required settings for shell access:**
200+
- `connection: netscaler.adc.ssh_netscaler_adc` - Use the SSH connection plugin
201+
- `ansible_python_interpreter: /var/python/bin/python` - Required for shell command execution
202+
- `gather_facts: false` - Fact gathering is not supported
203+
204+
#### Playbook Structure for NetScaler CLI Commands
205+
206+
To execute NetScaler CLI commands using nscli:
207+
208+
```yaml
209+
---
210+
- name: Execute NetScaler CLI commands
211+
hosts: demo_netscalers
212+
connection: netscaler.adc.ssh_netscaler_adc
213+
remote_user: nsroot
214+
gather_facts: false
215+
216+
vars:
217+
ansible_python_interpreter: /var/python/bin/python
218+
nscli_command: "show ns version"
219+
220+
tasks:
221+
- name: Run NetScaler CLI command
222+
ansible.builtin.command: "nscli -s -U :nsroot:{{ nitro_pass }} {{ nscli_command }}"
223+
register: cli_result
224+
changed_when: false
225+
226+
- name: Display CLI output
227+
ansible.builtin.debug:
228+
msg: "{{ cli_result.stdout_lines }}"
229+
```
230+
231+
**CLI command format:**
232+
- Use `nscli -s -U :nsroot:{{ nitro_pass }} <command>` to execute NetScaler CLI commands
233+
- The `-s` flag runs in non-interactive mode
234+
- The `-U :nsroot:{{ nitro_pass }}` provides authentication credentials
235+
- Replace `<command>` with any valid NetScaler CLI command (e.g., `show ns ip`, `show lb vserver`)
236+
237+
#### Running the Playbook
238+
239+
```bash
240+
ansible-playbook playbook.yaml -i inventory.ini
241+
```
242+
243+
Add `-vvvv` for verbose debugging output if needed:
244+
```bash
245+
ansible-playbook playbook.yaml -i inventory.ini -vvvv
246+
```
247+
248+
#### Additional Examples
249+
250+
For more complete examples and use cases, refer to the [SSH connections examples](https://github.com/netscaler/ansible-collection-netscaleradc/tree/main/examples/ssh_connections) directory.
138251

139252
### Authentication
140253

plugins/connection/ssh_netscaler_adc.py

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -391,25 +391,41 @@ def _return_tuple_manipulate(func):
391391
def wrapped(self, *args, **kwargs):
392392
return_tuple = func(self, *args, **kwargs)
393393
return_tuple = list(return_tuple)
394-
subst = ""
394+
395+
# Decode stdout
396+
stdout = codecs.decode(return_tuple[1], errors='ignore')
397+
398+
# Remove "Done" messages with surrounding newlines
395399
regex = r"(\r\n|\r|\n|)( Done)(\r\n|\r|\n)+"
396-
return_tuple[1] = re.sub(regex, subst, codecs.decode(return_tuple[1]), 0, re.UNICODE)
400+
stdout = re.sub(regex, "", stdout, 0, re.UNICODE)
401+
402+
# Remove Warning messages (multi-line)
403+
warning_regex = r"Warning: \[[\s\S]*?\]\s*"
404+
stdout = re.sub(warning_regex, "", stdout, 0, re.UNICODE)
397405

398-
# Ansible needs some data from return_tuple[1](or stdout). So, we are returning the same to ansible
406+
# Remove leading "]" characters that might remain
407+
stdout = re.sub(r"^\]\s*", "", stdout, re.MULTILINE)
408+
409+
# Remove extra blank lines
410+
stdout = re.sub(r"\n\s*\n", "\n", stdout)
411+
412+
# Try to extract JSON if present
399413
regex2 = r'{.*}'
400414
try:
401-
return_tuple[1] = re.findall(regex2, str(return_tuple[1]))[0]
402-
except IndexError:
415+
json_match = re.findall(regex2, stdout)
416+
if json_match:
417+
stdout = json_match[0]
418+
except (IndexError, AttributeError):
403419
pass
404-
# If no match, return the old `return_tuple[1]` (i.e., stdout)
420+
# If no match, use the cleaned stdout
405421

422+
return_tuple[1] = stdout.encode() if isinstance(return_tuple[1], bytes) else stdout
406423
return_tuple = tuple(return_tuple)
407424
return return_tuple
408425
return wrapped
409426

410427

411428
def _manipulate_cmd(func):
412-
413429
@wraps(func)
414430
def wrapped(self, cmd, *args, **kwargs):
415431
# Adding the 'shell' command for the citrix adc cli
@@ -455,3 +471,48 @@ def exec_command(self, cmd, in_data=None, sudoable=True):
455471
''' run a command on the remote host '''
456472

457473
return super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable)
474+
475+
@_return_tuple_manipulate
476+
def _bare_run(self, cmd, in_data, sudoable=True, checkrc=True):
477+
"""
478+
Wrapper around parent _bare_run to clean NetScaler output
479+
"""
480+
return super(Connection, self)._bare_run(cmd, in_data, sudoable=sudoable, checkrc=checkrc)
481+
482+
def put_file(self, in_path, out_path):
483+
"""
484+
Transfer a file from local to remote using piped method due to NetScaler limitations
485+
"""
486+
display.vvv(u"PUT {0} TO {1}".format(in_path, out_path), host=self.host)
487+
488+
# Force piped transfer for NetScaler
489+
# Save original transfer method
490+
original_method = self.get_option('ssh_transfer_method')
491+
492+
try:
493+
# Temporarily set to piped
494+
self._options['ssh_transfer_method'] = 'piped'
495+
return super(Connection, self).put_file(in_path, out_path)
496+
finally:
497+
# Restore original method
498+
if original_method:
499+
self._options['ssh_transfer_method'] = original_method
500+
501+
def fetch_file(self, in_path, out_path):
502+
"""
503+
Fetch a file from remote to local using piped method due to NetScaler limitations
504+
"""
505+
display.vvv(u"FETCH {0} TO {1}".format(in_path, out_path), host=self.host)
506+
507+
# Force piped transfer for NetScaler
508+
# Save original transfer method
509+
original_method = self.get_option('ssh_transfer_method')
510+
511+
try:
512+
# Temporarily set to piped
513+
self._options['ssh_transfer_method'] = 'piped'
514+
return super(Connection, self).fetch_file(in_path, out_path)
515+
finally:
516+
# Restore original method
517+
if original_method:
518+
self._options['ssh_transfer_method'] = original_method

0 commit comments

Comments
 (0)