mirror of https://github.com/jumpserver/jumpserver
				
				
				
			feat: ssh_ping及custom_command支持sudo及su切换用户 (#11180)
							parent
							
								
									8cfec07faa
								
							
						
					
					
						commit
						ff2aace569
					
				| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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) }}"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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) }}"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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']
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue