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 gather_facts: no
vars: vars:
ansible_connection: local ansible_connection: local
ansible_become: false
tasks: tasks:
- name: Test privileged account - name: Test privileged account (paramiko)
ssh_ping: ssh_ping:
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
@ -12,9 +13,14 @@
login_password: "{{ jms_account.secret }}" login_password: "{{ jms_account.secret }}"
login_secret_type: "{{ jms_account.secret_type }}" login_secret_type: "{{ jms_account.secret_type }}"
login_private_key_path: "{{ jms_account.private_key_path }}" 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 register: ping_info
- name: Change asset password - name: Change asset password (paramiko)
custom_command: custom_command:
login_user: "{{ jms_account.username }}" login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}" login_password: "{{ jms_account.secret }}"
@ -22,6 +28,11 @@
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
login_secret_type: "{{ jms_account.secret_type }}" login_secret_type: "{{ jms_account.secret_type }}"
login_private_key_path: "{{ jms_account.private_key_path }}" 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 }}" name: "{{ account.username }}"
password: "{{ account.secret }}" password: "{{ account.secret }}"
commands: "{{ params.commands }}" commands: "{{ params.commands }}"
@ -30,9 +41,10 @@
when: ping_info is succeeded when: ping_info is succeeded
register: change_info register: change_info
- name: Verify password - name: Verify password (paramiko)
ssh_ping: ssh_ping:
login_user: "{{ account.username }}" login_user: "{{ account.username }}"
login_password: "{{ account.secret }}" login_password: "{{ account.secret }}"
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
become: false

View File

@ -2,6 +2,7 @@
gather_facts: no gather_facts: no
vars: vars:
ansible_connection: local ansible_connection: local
ansible_become: false
tasks: tasks:
- name: Verify account (paramiko) - name: Verify account (paramiko)
@ -12,3 +13,8 @@
login_password: "{{ account.secret }}" login_password: "{{ account.secret }}"
login_secret_type: "{{ account.secret_type }}" login_secret_type: "{{ account.secret_type }}"
login_private_key_path: "{{ account.private_key_path }}" 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 gather_facts: no
vars: vars:
ansible_connection: local ansible_connection: local
ansible_become: false
tasks: tasks:
- name: Test asset connection (paramiko) - name: Test asset connection (paramiko)
@ -12,3 +13,8 @@
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
login_secret_type: "{{ jms_account.secret_type }}" login_secret_type: "{{ jms_account.secret_type }}"
login_private_key_path: "{{ jms_account.private_key_path }}" 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 var['ansible_ssh_private_key_file'] = account.private_key_path
return var 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): def make_account_vars(self, host, asset, account, automation, protocol, platform, gateway):
from accounts.const import AutomationTypes from accounts.const import AutomationTypes
if not account: if not account:
@ -89,6 +99,7 @@ class JMSInventory:
su_from = account.su_from su_from = account.su_from
if platform.su_enabled and su_from: if platform.su_enabled and su_from:
host.update(self.make_account_ansible_vars(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' become_method = 'sudo' if platform.su_method != 'su' else 'su'
host['ansible_become'] = True host['ansible_become'] = True
host['ansible_become_method'] = 'sudo' host['ansible_become_method'] = 'sudo'

View File

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

View File

@ -1,7 +1,6 @@
import time import time
import paramiko import paramiko
from paramiko.ssh_exception import SSHException, NoValidConnectionsError
def common_argument_spec(): def common_argument_spec():
@ -12,6 +11,13 @@ def common_argument_spec():
login_password=dict(type='str', required=False, no_log=True), login_password=dict(type='str', required=False, no_log=True),
login_secret_type=dict(type='str', required=False, default='password'), login_secret_type=dict(type='str', required=False, default='password'),
login_private_key_path=dict(type='str', required=False, no_log=True), 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 return options
@ -19,6 +25,7 @@ def common_argument_spec():
class SSHClient: class SSHClient:
def __init__(self, module): def __init__(self, module):
self.module = module self.module = module
self.channel = None
self.is_connect = False self.is_connect = False
self.client = paramiko.SSHClient() self.client = paramiko.SSHClient()
self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
@ -28,40 +35,90 @@ class SSHClient:
'allow_agent': False, 'look_for_keys': False, 'allow_agent': False, 'look_for_keys': False,
'hostname': self.module.params['login_host'], 'hostname': self.module.params['login_host'],
'port': self.module.params['login_port'], '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 self.module.params['become']:
if secret_type == 'ssh_key': params['username'] = self.module.params['become_user']
params['key_filename'] = self.module.params['login_private_key_path'] params['password'] = self.module.params['become_password']
params['key_filename'] = self.module.params['become_private_key_path'] or None
else: else:
params['username'] = self.module.params['login_user']
params['password'] = self.module.params['login_password'] params['password'] = self.module.params['login_password']
params['key_filename'] = self.module.params['login_private_key_path'] or None
return params 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): def connect(self):
try: try:
self.client.connect(**self.get_connect_params()) self.client.connect(**self.get_connect_params())
except (SSHException, NoValidConnectionsError) as err:
err_msg = str(err)
else:
self.is_connect = True self.is_connect = True
err_msg = '' err_msg = self.switch_user()
except Exception as err:
err_msg = str(err)
return err_msg 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): def execute(self, commands):
if not self.is_connect: if not self.is_connect:
self.connect() self.connect()
output, error_msg = '', ''
channel = self.client.invoke_shell()
# 读取首次登陆终端返回的消息
channel.recv(2048)
# 网络设备一般登录有延迟,等终端有返回后再执行命令
delay_time = self.module.params['first_conn_delay_time']
time.sleep(delay_time)
err_msg = ''
try: try:
for command in commands: for command in commands:
channel.send(command + '\n') self.channel.send(command + '\n')
time.sleep(0.3) time.sleep(0.3)
except SSHException as e: output = self._get_recv()
err_msg = str(e) except Exception as e:
return err_msg error_msg = str(e)
return output, error_msg
def __del__(self):
try:
self.channel.close()
self.client.close()
except:
pass