From ff2aace569efb0f6a4334fc6830907254cb8aba9 Mon Sep 17 00:00:00 2001 From: jiangweidong Date: Thu, 3 Aug 2023 14:09:13 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20ssh=5Fping=E5=8F=8Acustom=5Fcommand?= =?UTF-8?q?=E6=94=AF=E6=8C=81sudo=E5=8F=8Asu=E5=88=87=E6=8D=A2=E7=94=A8?= =?UTF-8?q?=E6=88=B7=20(#11180)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../change_secret/custom/ssh/main.yml | 18 +++- .../verify_account/custom/ssh/main.yml | 6 ++ .../automations/ping/custom/ssh/main.yml | 6 ++ apps/ops/ansible/inventory.py | 11 +++ apps/ops/ansible/modules/custom_command.py | 9 +- .../ansible/modules_utils/custom_common.py | 99 +++++++++++++++---- 6 files changed, 119 insertions(+), 30 deletions(-) diff --git a/apps/accounts/automations/change_secret/custom/ssh/main.yml b/apps/accounts/automations/change_secret/custom/ssh/main.yml index 853a666d4..b35d2175a 100644 --- a/apps/accounts/automations/change_secret/custom/ssh/main.yml +++ b/apps/accounts/automations/change_secret/custom/ssh/main.yml @@ -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 diff --git a/apps/accounts/automations/verify_account/custom/ssh/main.yml b/apps/accounts/automations/verify_account/custom/ssh/main.yml index 29b1dc22b..4e35b9587 100644 --- a/apps/accounts/automations/verify_account/custom/ssh/main.yml +++ b/apps/accounts/automations/verify_account/custom/ssh/main.yml @@ -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) }}" diff --git a/apps/assets/automations/ping/custom/ssh/main.yml b/apps/assets/automations/ping/custom/ssh/main.yml index 9725f63d7..7820df8e7 100644 --- a/apps/assets/automations/ping/custom/ssh/main.yml +++ b/apps/assets/automations/ping/custom/ssh/main.yml @@ -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) }}" diff --git a/apps/ops/ansible/inventory.py b/apps/ops/ansible/inventory.py index 653c5f40d..3f3dc19b0 100644 --- a/apps/ops/ansible/inventory.py +++ b/apps/ops/ansible/inventory.py @@ -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' diff --git a/apps/ops/ansible/modules/custom_command.py b/apps/ops/ansible/modules/custom_command.py index e4f7cf11d..947da2d31 100644 --- a/apps/ops/ansible/modules/custom_command.py +++ b/apps/ops/ansible/modules/custom_command.py @@ -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'] diff --git a/apps/ops/ansible/modules_utils/custom_common.py b/apps/ops/ansible/modules_utils/custom_common.py index 5da1a725e..01546ff33 100644 --- a/apps/ops/ansible/modules_utils/custom_common.py +++ b/apps/ops/ansible/modules_utils/custom_common.py @@ -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