feat: ssh_ping及custom_command支持sudo及su切换用户 (#11180)

pull/11190/head
jiangweidong 2023-08-03 14:09:13 +08:00 committed by GitHub
parent 8cfec07faa
commit ff2aace569
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 119 additions and 30 deletions

View File

@ -2,9 +2,10 @@
gather_facts: no
vars:
ansible_connection: local
ansible_become: false
tasks:
- name: Test privileged account
- name: Test privileged account (paramiko)
ssh_ping:
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
@ -12,9 +13,14 @@
login_password: "{{ jms_account.secret }}"
login_secret_type: "{{ jms_account.secret_type }}"
login_private_key_path: "{{ jms_account.private_key_path }}"
become: "{{ custom_become | default(False) }}"
become_method: "{{ custom_become_method | default('su') }}"
become_user: "{{ custom_become_user | default('') }}"
become_password: "{{ custom_become_password | default('') }}"
become_private_key_path: "{{ custom_become_private_key_path | default(None) }}"
register: ping_info
- name: Change asset password
- name: Change asset password (paramiko)
custom_command:
login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}"
@ -22,6 +28,11 @@
login_port: "{{ jms_asset.port }}"
login_secret_type: "{{ jms_account.secret_type }}"
login_private_key_path: "{{ jms_account.private_key_path }}"
become: "{{ custom_become | default(False) }}"
become_method: "{{ custom_become_method | default('su') }}"
become_user: "{{ custom_become_user | default('') }}"
become_password: "{{ custom_become_password | default('') }}"
become_private_key_path: "{{ custom_become_private_key_path | default(None) }}"
name: "{{ account.username }}"
password: "{{ account.secret }}"
commands: "{{ params.commands }}"
@ -30,9 +41,10 @@
when: ping_info is succeeded
register: change_info
- name: Verify password
- name: Verify password (paramiko)
ssh_ping:
login_user: "{{ account.username }}"
login_password: "{{ account.secret }}"
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
become: false

View File

@ -2,6 +2,7 @@
gather_facts: no
vars:
ansible_connection: local
ansible_become: false
tasks:
- name: Verify account (paramiko)
@ -12,3 +13,8 @@
login_password: "{{ account.secret }}"
login_secret_type: "{{ account.secret_type }}"
login_private_key_path: "{{ account.private_key_path }}"
become: "{{ custom_become | default(False) }}"
become_method: "{{ custom_become_method | default('su') }}"
become_user: "{{ custom_become_user | default('') }}"
become_password: "{{ custom_become_password | default('') }}"
become_private_key_path: "{{ custom_become_private_key_path | default(None) }}"

View File

@ -2,6 +2,7 @@
gather_facts: no
vars:
ansible_connection: local
ansible_become: false
tasks:
- name: Test asset connection (paramiko)
@ -12,3 +13,8 @@
login_port: "{{ jms_asset.port }}"
login_secret_type: "{{ jms_account.secret_type }}"
login_private_key_path: "{{ jms_account.private_key_path }}"
become: "{{ custom_become | default(False) }}"
become_method: "{{ custom_become_method | default('su') }}"
become_user: "{{ custom_become_user | default('') }}"
become_password: "{{ custom_become_password | default('') }}"
become_private_key_path: "{{ custom_become_private_key_path | default(None) }}"

View File

@ -76,6 +76,16 @@ class JMSInventory:
var['ansible_ssh_private_key_file'] = account.private_key_path
return var
@staticmethod
def make_custom_become_ansible_vars(account, platform):
var = {
'custom_become': True, 'custom_become_method': platform.su_method,
'custom_become_user': account.su_from.username,
'custom_become_password': account.su_from.secret,
'custom_become_private_key_path': account.su_from.private_key_path
}
return var
def make_account_vars(self, host, asset, account, automation, protocol, platform, gateway):
from accounts.const import AutomationTypes
if not account:
@ -89,6 +99,7 @@ class JMSInventory:
su_from = account.su_from
if platform.su_enabled and su_from:
host.update(self.make_account_ansible_vars(su_from))
host.update(self.make_custom_become_ansible_vars(account, platform))
become_method = 'sudo' if platform.su_method != 'su' else 'su'
host['ansible_become'] = True
host['ansible_become_method'] = 'sudo'

View File

@ -90,9 +90,6 @@ def main():
name=dict(required=True, aliases=['user']),
password=dict(aliases=['pass'], no_log=True),
commands=dict(type='list', required=False),
first_conn_delay_time=dict(
type='float', required=False, default=0.5
),
)
module = AnsibleModule(argument_spec=argument_spec)
@ -102,10 +99,10 @@ def main():
module.fail_json(
msg='No command found, please go to the platform details to add'
)
err = ssh_client.execute(commands)
if err:
output, err_msg = ssh_client.execute(commands)
if err_msg:
module.fail_json(
msg='There was a problem executing the command: %s' % err
msg='There was a problem executing the command: %s' % err_msg
)
user = module.params['name']

View File

@ -1,7 +1,6 @@
import time
import paramiko
from paramiko.ssh_exception import SSHException, NoValidConnectionsError
def common_argument_spec():
@ -12,6 +11,13 @@ def common_argument_spec():
login_password=dict(type='str', required=False, no_log=True),
login_secret_type=dict(type='str', required=False, default='password'),
login_private_key_path=dict(type='str', required=False, no_log=True),
first_conn_delay_time=dict(type='float', required=False, default=0.5),
become=dict(type='bool', default=False, required=False),
become_method=dict(type='str', required=False),
become_user=dict(type='str', required=False),
become_password=dict(type='str', required=False, no_log=True),
become_private_key_path=dict(type='str', required=False, no_log=True),
)
return options
@ -19,6 +25,7 @@ def common_argument_spec():
class SSHClient:
def __init__(self, module):
self.module = module
self.channel = None
self.is_connect = False
self.client = paramiko.SSHClient()
self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
@ -28,40 +35,90 @@ class SSHClient:
'allow_agent': False, 'look_for_keys': False,
'hostname': self.module.params['login_host'],
'port': self.module.params['login_port'],
'username': self.module.params['login_user'],
'key_filename': self.module.params['login_private_key_path'] or None
}
secret_type = self.module.params['login_secret_type']
if secret_type == 'ssh_key':
params['key_filename'] = self.module.params['login_private_key_path']
if self.module.params['become']:
params['username'] = self.module.params['become_user']
params['password'] = self.module.params['become_password']
params['key_filename'] = self.module.params['become_private_key_path'] or None
else:
params['username'] = self.module.params['login_user']
params['password'] = self.module.params['login_password']
params['key_filename'] = self.module.params['login_private_key_path'] or None
return params
def _get_channel(self):
self.channel = self.client.invoke_shell()
# 读取首次登陆终端返回的消息
self.channel.recv(2048)
# 网络设备一般登录有延迟,等终端有返回后再执行命令
delay_time = self.module.params['first_conn_delay_time']
time.sleep(delay_time)
@staticmethod
def _is_match_user(user, content):
# 正常命令切割后是[命令,用户名,交互前缀]
remote_user = content.split()[1] if len(content.split()) >= 3 else None
return remote_user and remote_user == user
def switch_user(self):
self._get_channel()
if not self.module.params['become']:
return None
method = self.module.params['become_method']
username = self.module.params['login_user']
if method == 'sudo':
switch_method = 'sudo su -'
password = self.module.params['become_password']
elif method == 'su':
switch_method = 'su -'
password = self.module.params['login_password']
else:
self.module.fail_json(msg='Become method %s not support' % method)
return
commands = [f'{switch_method} {username}', password]
su_output, err_msg = self.execute(commands)
if err_msg:
return err_msg
i_output, err_msg = self.execute(['whoami'])
if err_msg:
return err_msg
if self._is_match_user(username, i_output):
err_msg = ''
else:
err_msg = su_output
return err_msg
def connect(self):
try:
self.client.connect(**self.get_connect_params())
except (SSHException, NoValidConnectionsError) as err:
err_msg = str(err)
else:
self.is_connect = True
err_msg = ''
err_msg = self.switch_user()
except Exception as err:
err_msg = str(err)
return err_msg
def _get_recv(self, size=1024, encoding='utf-8'):
output = self.channel.recv(size).decode(encoding)
return output
def execute(self, commands):
if not self.is_connect:
self.connect()
channel = self.client.invoke_shell()
# 读取首次登陆终端返回的消息
channel.recv(2048)
# 网络设备一般登录有延迟,等终端有返回后再执行命令
delay_time = self.module.params['first_conn_delay_time']
time.sleep(delay_time)
err_msg = ''
output, error_msg = '', ''
try:
for command in commands:
channel.send(command + '\n')
self.channel.send(command + '\n')
time.sleep(0.3)
except SSHException as e:
err_msg = str(e)
return err_msg
output = self._get_recv()
except Exception as e:
error_msg = str(e)
return output, error_msg
def __del__(self):
try:
self.channel.close()
self.client.close()
except:
pass