From 217818f2b73b22edae6484e6b21cf97539ac4a92 Mon Sep 17 00:00:00 2001 From: jiangweidong <1053570670@qq.com> Date: Fri, 1 Nov 2024 08:32:33 +0800 Subject: [PATCH 1/3] feat: Custom change password supports configuration of interactive items --- .../change_secret/custom/ssh/main.yml | 7 +- .../change_secret/custom/ssh/manifest.yml | 110 ++++++- .../verify_account/custom/ssh/main.yml | 1 + .../automations/ping/custom/ssh/main.yml | 1 + apps/assets/const/device.py | 3 +- apps/libs/ansible/modules/custom_command.py | 44 ++- apps/libs/ansible/modules/ssh_ping.py | 13 +- .../{custom_common.py => remote_client.py} | 282 +++++++++++------- 8 files changed, 322 insertions(+), 139 deletions(-) rename apps/libs/ansible/modules_utils/{custom_common.py => remote_client.py} (50%) diff --git a/apps/accounts/automations/change_secret/custom/ssh/main.yml b/apps/accounts/automations/change_secret/custom/ssh/main.yml index 966454bfc..9d8d03e99 100644 --- a/apps/accounts/automations/change_secret/custom/ssh/main.yml +++ b/apps/accounts/automations/change_secret/custom/ssh/main.yml @@ -20,6 +20,7 @@ become_private_key_path: "{{ jms_custom_become_private_key_path | default(None) }}" old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}" gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}" + recv_timeout: "{{ params.recv_timeout | default(30) }}" register: ping_info delegate_to: localhost @@ -39,7 +40,10 @@ name: "{{ account.username }}" password: "{{ account.secret }}" commands: "{{ params.commands }}" - first_conn_delay_time: "{{ first_conn_delay_time | default(0.5) }}" + answers: "{{ params.answers }}" + recv_timeout: "{{ params.recv_timeout | default(30) }}" + delay_time: "{{ params.delay_time | default(2) }}" + prompt: "{{ params.prompt | default('.*') }}" ignore_errors: true when: ping_info is succeeded and check_conn_after_change register: change_info @@ -58,5 +62,6 @@ become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}" old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}" gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}" + recv_timeout: "{{ params.recv_timeout | default(30) }}" delegate_to: localhost when: check_conn_after_change \ No newline at end of file diff --git a/apps/accounts/automations/change_secret/custom/ssh/manifest.yml b/apps/accounts/automations/change_secret/custom/ssh/manifest.yml index 1b0a38a00..22fa3759c 100644 --- a/apps/accounts/automations/change_secret/custom/ssh/manifest.yml +++ b/apps/accounts/automations/change_secret/custom/ssh/manifest.yml @@ -10,10 +10,30 @@ protocol: ssh priority: 50 params: - name: commands - type: list + type: text label: "{{ 'Params commands label' | trans }}" - default: [ '' ] + default: '' help_text: "{{ 'Params commands help text' | trans }}" + - name: recv_timeout + type: int + label: "{{ 'Params recv_timeout label' | trans }}" + default: 30 + help_text: "{{ 'Params recv_timeout help text' | trans }}" + - name: delay_time + type: int + label: "{{ 'Params delay_time label' | trans }}" + default: 2 + help_text: "{{ 'Params delay_time help text' | trans }}" + - name: prompt + type: str + label: "{{ 'Params prompt label' | trans }}" + default: '.*' + help_text: "{{ 'Params prompt help text' | trans }}" + - name: answers + type: text + label: "{{ 'Params answer label' | trans }}" + default: '.*' + help_text: "{{ 'Params answer help text' | trans }}" i18n: SSH account change secret: @@ -22,11 +42,91 @@ i18n: en: 'Custom password change by SSH command line' Params commands help text: - zh: '自定义命令中如需包含账号的 账号、密码、SSH 连接的用户密码 字段,
请使用 {username}、{password}、{login_password}格式,执行任务时会进行替换 。
比如针对 Cisco 主机进行改密,一般需要配置五条命令:
1. enable
2. {login_password}
3. configure terminal
4. username {username} privilege 0 password {password}
5. end' - ja: 'カスタム コマンドに SSH 接続用のアカウント番号、パスワード、ユーザー パスワード フィールドを含める必要がある場合は、
{ユーザー名}、{パスワード}、{login_password& を使用してください。 # 125; 形式。タスクの実行時に置き換えられます。
たとえば、Cisco ホストのパスワードを変更するには、通常、次の 5 つのコマンドを設定する必要があります:
1.enable
2.{login_password}
3 .ターミナルの設定
4. ユーザー名 {ユーザー名} 権限 0 パスワード {パスワード}
5. 終了' - en: 'If the custom command needs to include the account number, password, and user password field for SSH connection,
Please use {username}, {password}, {login_password&# 125; format, which will be replaced when executing the task.
For example, to change the password of a Cisco host, you generally need to configure five commands:
1. enable
2. {login_password}
3. configure terminal
4. username {username} privilege 0 password {password}
5. end' + zh: | + 请将命令中的指定位置改成特殊符号
+ 1. 改密账号 -> {username}
+ 2. 改密密码 -> {password}
+ 3. 登录用户密码 -> {login_password}
+ 多条命令使用换行分割,执行任务时系统会根据特殊符号替换真实数据。
+ 比如针对 Cisco 主机进行改密,一般需要配置五条命令:
+ enable
+ {login_password}
+ configure terminal
+ username {username} privilege 0 password {password}
+ end
+ ja: | + コマンド内の指定された位置を特殊記号に変更してください。
+ 新しいパスワード(アカウント変更) -> {username}
+ 新しいパスワード(パスワード変更) -> {password}
+ ログインユーザーパスワード -> {login_password}
+ 複数のコマンドは改行で区切り、タスクを実行するときにシステムは特殊記号を使用して実際のデータを置き換えます。
+ 例えば、Cisco機器のパスワードを変更する場合、一般的には5つのコマンドを設定する必要があります:
+ enable
+ {login_password}
+ configure terminal
+ username {username} privilege 0 password {password}
+ end
+ en: | + Please change the specified positions in the command to special symbols.
+ Change password account -> {username}
+ Change password -> {password}
+ Login user password -> {login_password}
+ Multiple commands are separated by new lines, and when executing tasks,
+ the system will replace the special symbols with real data.
+ For example, to change the password for a Cisco device, you generally need to configure five commands:
+ enable
+ {login_password}
+ configure terminal
+ username {username} privilege 0 password {password}
+ end
Params commands label: zh: '自定义命令' ja: 'カスタムコマンド' en: 'Custom command' + + Params recv_timeout label: + zh: '超时时间' + ja: 'タイムアウト' + en: 'Timeout' + + Params recv_timeout help text: + zh: '等待命令结果返回的超时时间(秒)' + ja: 'コマンドの結果を待つタイムアウト時間(秒)' + en: 'The timeout for waiting for the command result to return (Seconds)' + + Params delay_time label: + zh: '延迟发送时间' + ja: '遅延送信時間' + en: 'Delayed send time' + + Params delay_time help text: + zh: '每条命令延迟发送的时间间隔(秒)' + ja: '各コマンド送信の遅延間隔(秒)' + en: 'Time interval for each command delay in sending (Seconds)' + + Params prompt label: + zh: '提示符' + ja: 'ヒント' + en: 'Prompt' + + Params prompt help text: + zh: '终端连接后显示的提示符信息(正则表达式)' + ja: 'ターミナル接続後に表示されるプロンプト情報(正規表現)' + en: 'Prompt information displayed after terminal connection (Regular expression)' + + Params answer label: + zh: '命令结果' + ja: 'コマンド結果' + en: 'Command result' + + Params answer help text: + zh: | + 根据结果匹配度决定是否执行下一条命令,输入框的内容和上方 “自定义命令” 内容按行一一对应(正则表达式) + ja: | + 結果の一致度に基づいて次のコマンドを実行するかどうかを決定します。 + 入力欄の内容は、上の「カスタムコマンド」の内容と行ごとに対応しています(せいきひょうげん) + en: | + Decide whether to execute the next command based on the result match. + The input content corresponds line by line with the content + of the `Custom command` above. (Regular expression) diff --git a/apps/accounts/automations/verify_account/custom/ssh/main.yml b/apps/accounts/automations/verify_account/custom/ssh/main.yml index 31178666f..831c1c783 100644 --- a/apps/accounts/automations/verify_account/custom/ssh/main.yml +++ b/apps/accounts/automations/verify_account/custom/ssh/main.yml @@ -21,3 +21,4 @@ become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}" old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}" gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}" + recv_timeout: "{{ params.recv_timeout | default(30) }}" diff --git a/apps/assets/automations/ping/custom/ssh/main.yml b/apps/assets/automations/ping/custom/ssh/main.yml index 89b92bcaa..de651cac3 100644 --- a/apps/assets/automations/ping/custom/ssh/main.yml +++ b/apps/assets/automations/ping/custom/ssh/main.yml @@ -21,4 +21,5 @@ become_private_key_path: "{{ jms_custom_become_private_key_path | default(None) }}" old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}" gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}" + recv_timeout: "{{ params.recv_timeout | default(30) }}" diff --git a/apps/assets/const/device.py b/apps/assets/const/device.py index e107d3e8c..8860dc3fb 100644 --- a/apps/assets/const/device.py +++ b/apps/assets/const/device.py @@ -39,8 +39,7 @@ class DeviceTypes(BaseType): '*': { 'ansible_enabled': True, 'ansible_config': { - 'ansible_connection': 'local', - 'first_conn_delay_time': 0.5, + 'ansible_connection': 'local' }, 'ping_enabled': True, 'gather_facts_enabled': False, diff --git a/apps/libs/ansible/modules/custom_command.py b/apps/libs/ansible/modules/custom_command.py index 55d0537ab..cb47e7ab0 100644 --- a/apps/libs/ansible/modules/custom_command.py +++ b/apps/libs/ansible/modules/custom_command.py @@ -33,13 +33,8 @@ options: commands: description: - Custom change password commands. - type: list + type: str required: true - first_conn_delay_time: - description: - - Delay for executing the command after SSH connection(unit: s) - type: float - required: false ''' EXAMPLES = ''' @@ -51,7 +46,7 @@ EXAMPLES = ''' login_password: "123456" name: "jms" password: "123456" - commands: ['passwd {username}', '{password}', '{password}'] + commands: 'passwd {username}\n{password}\n{password}'] ''' RETURN = ''' @@ -63,21 +58,25 @@ name: from ansible.module_utils.basic import AnsibleModule -from libs.ansible.modules_utils.custom_common import ( +from libs.ansible.modules_utils.remote_client import ( SSHClient, common_argument_spec ) -def get_commands(module): +def get_commands_and_answers(module) -> (list, list): username = module.params['name'] password = module.params['password'] - commands = module.params['commands'] or [] + commands = module.params['commands'] or '' + answers = module.params['answers'] or '' login_password = module.params['login_password'] - for index, command in enumerate(commands): - commands[index] = command.format( - username=username, password=password, login_password=login_password - ) - return commands + + if isinstance(commands, list): + commands = '\n'.join(commands) + commands = commands.format( + username=username, password=password, login_password=login_password + ) + return commands.split('\n'), answers.split('\n') + # ========================================= # Module execution. @@ -89,21 +88,20 @@ def main(): argument_spec.update( name=dict(required=True, aliases=['user']), password=dict(aliases=['pass'], no_log=True), - commands=dict(type='list', required=False), ) module = AnsibleModule(argument_spec=argument_spec) - ssh_client = SSHClient(module) - commands = get_commands(module) + commands, answers = get_commands_and_answers(module) if not commands: module.fail_json( msg='No command found, please go to the platform details to add' ) - output, err_msg = ssh_client.execute(commands) - if err_msg: - module.fail_json( - msg='There was a problem executing the command: %s' % err_msg - ) + with SSHClient(module) as client: + output, err_msg = client.execute(commands, answers) + if err_msg: + module.fail_json( + msg='There was a problem executing the command: %s' % err_msg + ) user = module.params['name'] module.exit_json(changed=True, user=user) diff --git a/apps/libs/ansible/modules/ssh_ping.py b/apps/libs/ansible/modules/ssh_ping.py index f5b08c63b..a35f177ac 100644 --- a/apps/libs/ansible/modules/ssh_ping.py +++ b/apps/libs/ansible/modules/ssh_ping.py @@ -34,7 +34,7 @@ is_available: from ansible.module_utils.basic import AnsibleModule -from libs.ansible.modules_utils.custom_common import ( +from libs.ansible.modules_utils.remote_client import ( SSHClient, common_argument_spec ) @@ -49,14 +49,11 @@ def main(): module = AnsibleModule(argument_spec=options, supports_check_mode=True,) result = { - 'changed': False, 'is_available': True + 'changed': False, 'is_available': False } - client = SSHClient(module) - err = client.connect() - if err: - module.fail_json(msg='Unable to connect to asset: %s' % err) - result['is_available'] = False - + with SSHClient(module) as client: + client.connect() + result['is_available'] = True return module.exit_json(**result) diff --git a/apps/libs/ansible/modules_utils/custom_common.py b/apps/libs/ansible/modules_utils/remote_client.py similarity index 50% rename from apps/libs/ansible/modules_utils/custom_common.py rename to apps/libs/ansible/modules_utils/remote_client.py index c70fba275..371e62881 100644 --- a/apps/libs/ansible/modules_utils/custom_common.py +++ b/apps/libs/ansible/modules_utils/remote_client.py @@ -1,10 +1,106 @@ import re +import signal import time import paramiko + +from functools import wraps + from sshtunnel import SSHTunnelForwarder +DEFAULT_RE = '.*' +SU_PROMPT_LOCALIZATIONS = [ + 'Password', + '암호', + 'パスワード', + 'Adgangskode', + 'Contraseña', + 'Contrasenya', + 'Hasło', + 'Heslo', + 'Jelszó', + 'Lösenord', + 'Mật khẩu', + 'Mot de passe', + 'Parola', + 'Parool', + 'Pasahitza', + 'Passord', + 'Passwort', + 'Salasana', + 'Sandi', + 'Senha', + 'Wachtwoord', + 'ססמה', + 'Лозинка', + 'Парола', + 'Пароль', + 'गुप्तशब्द', + 'शब्दकूट', + 'సంకేతపదము', + 'හස්පදය', + '密码', + '密碼', + '口令', + ] + + +def get_become_prompt_re(): + b_password_string = "|".join((r'(\w+\'s )?' + p) for p in SU_PROMPT_LOCALIZATIONS) + b_password_string = b_password_string + ' ?(:|:) ?' + return re.compile(b_password_string, flags=re.IGNORECASE) + + +become_prompt_re = get_become_prompt_re() + + +def common_argument_spec(): + options = dict( + login_host=dict(type='str', required=False, default='localhost'), + login_port=dict(type='int', required=False, default=22), + login_user=dict(type='str', required=False, default='root'), + 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), + gateway_args=dict(type='str', required=False, default=''), + recv_timeout=dict(type='int', required=False, default=30), + delay_time=dict(type='int', required=False, default=2), + prompt=dict(type='str', required=False, default='.*'), + answers=dict(type='str', required=False, default='.*'), + commands=dict(type='raw', required=False), + + 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), + + old_ssh_version=dict(type='bool', default=False, required=False), + ) + return options + + +def raise_timeout(name=''): + def decorate(func): + @wraps(func) + def wrapper(self, *args, **kwargs): + def handler(signum, frame): + raise TimeoutError(f'{name} timed out, wait {timeout}s') + + try: + timeout = getattr(self, 'timeout', 0) + if timeout > 0: + signal.signal(signal.SIGALRM, handler) + signal.alarm(timeout) + return func(self, *args, **kwargs) + except Exception as error: + signal.alarm(0) + raise error + return wrapper + return decorate + + class OldSSHTransport(paramiko.transport.Transport): _preferred_pubkeys = ( "ssh-ed25519", @@ -18,41 +114,24 @@ class OldSSHTransport(paramiko.transport.Transport): ) -def common_argument_spec(): - options = dict( - login_host=dict(type='str', required=False, default='localhost'), - login_port=dict(type='int', required=False, default=22), - login_user=dict(type='str', required=False, default='root'), - 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), - gateway_args=dict(type='str', required=False, default=''), - - 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), - - old_ssh_version=dict(type='bool', default=False, required=False), - ) - return options - - class SSHClient: - TIMEOUT = 20 - SLEEP_INTERVAL = 2 - COMPLETE_FLAG = 'complete' - def __init__(self, module): self.module = module - self.channel = None - self.is_connect = False self.gateway_server = None self.client = paramiko.SSHClient() self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) self.connect_params = self.get_connect_params() + self._channel = None + self.buffer_size = 1024 + self.connect_params = self.get_connect_params() + self.prompt = self.module.params['prompt'] + self.timeout = self.module.params['recv_timeout'] + + @property + def channel(self): + if self._channel is None: + self.connect() + return self._channel def get_connect_params(self): params = { @@ -73,22 +152,7 @@ class SSHClient: params['transport_factory'] = OldSSHTransport 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): - # 正常命令切割后是[命令,用户名,交互前缀] - content_list = content.split() if len(content.split()) >= 3 else None - return content_list and user in content_list - def switch_user(self): - self._get_channel() if not self.module.params['become']: return method = self.module.params['become_method'] @@ -102,22 +166,73 @@ class SSHClient: 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( - [f'whoami && echo "{self.COMPLETE_FLAG}"'], - validate_output=True - ) - if err_msg: - return err_msg - if self._is_match_user(username, i_output): - err_msg = '' - else: - err_msg = su_output - return err_msg + __, e_msg = self.execute( + [f'{switch_method} {username}', password, 'whoami'], + [become_prompt_re, DEFAULT_RE, username] + ) + if e_msg: + self.module.fail_json(msg='Become user %s failed.' % username) + + def connect(self): + self.before_runner_start() + try: + self.client.connect(**self.connect_params) + self._channel = self.client.invoke_shell() + self._get_match_recv() + self.switch_user() + except Exception as error: + self.module.fail_json(msg=str(error)) + + @staticmethod + def _fit_answers(commands, answers): + if answers is None or not isinstance(answers, list): + answers = [DEFAULT_RE] * len(commands) + elif len(answers) < len(commands): + answers += [DEFAULT_RE] * (len(commands) - len(answers)) + return answers + + @staticmethod + def __match(re_, content): + re_pattern = re_ + if isinstance(re_, str): + re_pattern = re.compile(re_, re.DOTALL | re.IGNORECASE) + elif not isinstance(re_pattern, re.Pattern): + raise ValueError(f'{re_} should be a regular expression') + return bool(re_pattern.search(content)) + + @raise_timeout('Recv message') + def _get_match_recv(self, answer_reg=DEFAULT_RE): + last_output, output = '', '' + while True: + if self.channel.recv_ready(): + recv = self.channel.recv(self.buffer_size).decode() + output += recv + if output and last_output != output: + fin_reg = self.prompt if answer_reg == DEFAULT_RE else answer_reg + if self.__match(fin_reg, output): + break + last_output = output + time.sleep(0.01) + return output + + @raise_timeout('Wait send message') + def _check_send(self): + while not self.channel.send_ready(): + time.sleep(0.01) + time.sleep(self.module.params['delay_time']) + + def execute(self, commands, answers=None): + all_output, error_msg = '', '' + try: + answers = self._fit_answers(commands, answers) + for index, command in enumerate(commands): + self._check_send() + self.channel.send(command + '\n') + all_output += f'{self._get_match_recv(answers[index])}\n' + except Exception as e: + error_msg = str(e) + return all_output, error_msg def local_gateway_prepare(self): gateway_args = self.module.params['gateway_args'] or '' @@ -160,48 +275,15 @@ class SSHClient: def after_runner_end(self): self.local_gateway_clean() - def connect(self): + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): try: - self.before_runner_start() - self.client.connect(**self.connect_params) - self.is_connect = True - err_msg = self.switch_user() self.after_runner_end() - 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, validate_output=False): - if not self.is_connect: - self.connect() - output, error_msg = '', '' - try: - for command in commands: - self.channel.send(command + '\n') - if not validate_output: - time.sleep(self.SLEEP_INTERVAL) - output += self._get_recv() - continue - start_time = time.time() - while self.COMPLETE_FLAG not in output: - if time.time() - start_time > self.TIMEOUT: - error_msg = output - print("切换用户操作超时,跳出循环。") - break - time.sleep(self.SLEEP_INTERVAL) - received_output = self._get_recv().replace(f'"{self.COMPLETE_FLAG}"', '') - output += received_output - except Exception as e: - error_msg = str(e) - return output, error_msg - - def __del__(self): - try: - self.channel.close() - self.client.close() - except Exception: + if self.channel: + self.channel.close() + if self.client: + self.client.close() + except Exception: # noqa pass From a9433bc48eeb57a5e8a2b987c8433b07a9d121a1 Mon Sep 17 00:00:00 2001 From: wangruidong <940853815@qq.com> Date: Sat, 8 Feb 2025 18:25:41 +0800 Subject: [PATCH 2/3] perf: Go and Python demo code --- apps/accounts/demos/go/README.zh-hans.md | 226 +++++++++---------- apps/accounts/demos/python/README.zh-hans.md | 125 +++------- 2 files changed, 144 insertions(+), 207 deletions(-) diff --git a/apps/accounts/demos/go/README.zh-hans.md b/apps/accounts/demos/go/README.zh-hans.md index 5eaef3574..3b4e49a43 100644 --- a/apps/accounts/demos/go/README.zh-hans.md +++ b/apps/accounts/demos/go/README.zh-hans.md @@ -1,133 +1,121 @@ -# JumpServer PAM 客户端 - -该包提供了一个 Go 客户端,用于与 JumpServer PAM API 交互,以检索各种资产的密码。它简化了发送请求和处理响应的过程。 - -## 功能 - -- 在发送请求之前验证参数。 -- 支持基于资产和账户的密码检索。 -- 使用 HMAC-SHA256 签名进行身份验证,方便与 JumpServer PAM API 集成。 - -## 使用说明 - -1. **下载 Go 代码文件**: - 将代码文件下载到您的项目目录中。 - -2. **导入包**: - 在您的 Go 文件中导入该包,您即可直接使用其功能。 - -## 需求 - -- `Go 1.16+` -- `github.com/google/uuid` -- `gopkg.in/twindagger/httpsig.v1` - -## 使用方法 - -### 初始化 - -要使用 JumpServer PAM 客户端,通过提供所需的 `endpoint`、`keyID` 和 `keySecret` 创建一个实例。 - ```go package main import ( + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "encoding/json" "fmt" - - "your_module_path/jms_pam" + "log" + "net/http" + "net/url" + "os" + "strings" + "time" ) -func main() { - client := jms_pam.NewJumpServerPAM( - "http://127.0.0.1", // 替换为您的 JumpServer 端点 - "your-key-id", // 替换为您的实际 Key ID - "your-key-secret", // 替换为您的实际 Key Secret - "", // 留空以使用默认的组织 ID +type APIClient struct { + Client *http.Client + APIURL string + KeyID string + KeySecret string + OrgID string +} + +func NewAPIClient() *APIClient { + return &APIClient{ + Client: &http.Client{}, + APIURL: getEnv("API_URL", "http://127.0.0.1:8080"), + KeyID: getEnv("API_KEY_ID", "72b0b0aa-ad82-4182-a631-ae4865e8ae0e"), + KeySecret: getEnv("API_KEY_SECRET", "6fuSO7P1m4cj8SSlgaYdblOjNAmnxDVD7tr8"), + OrgID: getEnv("ORG_ID", "00000000-0000-0000-0000-000000000002"), + } +} + +func getEnv(key, defaultValue string) string { + value := os.Getenv(key) + if value == "" { + return defaultValue + } + return value +} + +func (c *APIClient) GetAccountSecret(asset, account string) (map[string]interface{}, error) { + u, err := url.Parse(c.APIURL) + if err != nil { + return nil, fmt.Errorf("failed to parse API URL: %v", err) + } + u.Path = "/api/v1/accounts/integration-applications/account-secret/" + + q := u.Query() + q.Add("asset", asset) + q.Add("account", account) + u.RawQuery = q.Encode() + + req, err := http.NewRequest("GET", u.String(), nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %v", err) + } + + date := time.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05 GMT") + req.Header.Set("Accept", "application/json") + req.Header.Set("X-JMS-ORG", c.OrgID) + req.Header.Set("Date", date) + req.Header.Set("X-Source", "jms-pam") + + headersList := []string{"(request-target)", "accept", "date", "x-jms-org"} + var signatureParts []string + + for _, h := range headersList { + var value string + if h == "(request-target)" { + value = strings.ToLower(req.Method) + " " + req.URL.RequestURI() + } else { + canonicalKey := http.CanonicalHeaderKey(h) + value = req.Header.Get(canonicalKey) + } + signatureParts = append(signatureParts, fmt.Sprintf("%s: %s", h, value)) + } + + signatureString := strings.Join(signatureParts, "\n") + mac := hmac.New(sha256.New, []byte(c.KeySecret)) + mac.Write([]byte(signatureString)) + signatureB64 := base64.StdEncoding.EncodeToString(mac.Sum(nil)) + + headersJoined := strings.Join(headersList, " ") + authHeader := fmt.Sprintf( + `Signature keyId="%s",algorithm="hmac-sha256",headers="%s",signature="%s"`, + c.KeyID, + headersJoined, + signatureB64, ) + req.Header.Set("Authorization", authHeader) + + resp, err := c.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("request failed: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("API returned non-200 status: %d", resp.StatusCode) + } + + var result map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, fmt.Errorf("failed to decode response: %v", err) + } + + return result, nil } -``` - -### 创建密码请求 - -您可以通过指定资产或账户信息来创建请求。 - -```go -request, err := jms_pam.NewSecretRequest("Linux", "", "root", "") -if err != nil { - fmt.Println("创建请求时出错:", err) - return -} -``` - -### 发送请求 - -使用客户端的 `Send` 方法发送请求。 - -```go -secretObj, err := client.Send(request) -if err != nil { - fmt.Println("发送请求时出错:", err) - return -} -``` - -### 处理响应 - -检查密码是否成功检索,并相应地处理响应。 - -```go -if secretObj.Valid { - fmt.Println("密码:", secretObj.Secret) -} else { - fmt.Println("获取密码失败:", string(secretObj.Desc)) -} -``` - -### 完整示例 - -以下是如何使用该客户端的完整示例: - -```go -package main - -import ( - "fmt" - - "your_module_path/jms_pam" -) func main() { - client := jms_pam.NewJumpServerPAM( - "http://127.0.0.1", - "your-key-id", - "your-key-secret", - "", - ) - - request, err := jms_pam.NewSecretRequest("Linux", "", "root", "") + client := NewAPIClient() + result, err := client.GetAccountSecret("ubuntu_docker", "root") if err != nil { - fmt.Println("创建请求时出错:", err) - return - } - - secretObj, err := client.Send(request) - if err != nil { - fmt.Println("发送请求时出错:", err) - return - } - - if secretObj.Valid { - fmt.Println("密码:", secretObj.Secret) - } else { - fmt.Println("获取密码失败:", string(secretObj.Desc)) + log.Fatalf("Error: %v", err) } + fmt.Printf("Result: %+v\n", result) } -``` - -## 错误处理 - -该库会在创建 `SecretRequest` 时返回无效参数的错误。这包括对有效 UUID 的检查以及确保提供了必需的参数。 - -## 贡献 - -欢迎贡献!如有任何增强或错误修复,请提出问题或提交拉取请求。 +``` \ No newline at end of file diff --git a/apps/accounts/demos/python/README.zh-hans.md b/apps/accounts/demos/python/README.zh-hans.md index b7dcc5121..f2ec3182d 100644 --- a/apps/accounts/demos/python/README.zh-hans.md +++ b/apps/accounts/demos/python/README.zh-hans.md @@ -1,96 +1,45 @@ -# JumpServer PAM 客户端 - -该包提供了一个 Python 客户端,用于与 JumpServer PAM API 交互,以检索各种资产的密码。它简化了发送请求和处理响应的过程。 - -## 特性 - -- 在发送请求之前验证参数。 -- 支持基于资产和账户的密码检索。 -- 通过 HTTP 签名轻松集成 JumpServer PAM API。 - -## 安装 - -您可以通过 pip 安装该包: - -```bash -pip install jms_pam-0.0.1-py3-none-any.whl -``` - -## 需求 - -- `Python 3.6+` -- `requests` -- `httpsig` - -## 使用方法 - -### 初始化 - -要使用 JumpServer PAM 客户端,通过提供所需的 `endpoint`、`key_id` 和 `key_secret` 创建一个实例。 - ```python -from jms_pam import JumpServerPAM, SecretRequest +import requests +import os +from datetime import datetime +from httpsig.requests_auth import HTTPSignatureAuth -client = JumpServerPAM( - endpoint='http://127.0.0.1', - key_id='your-key-id', - key_secret='your-key-secret' -) -``` +API_URL = os.getenv("API_URL", "http://127.0.0.1:8080") +KEY_ID = os.getenv("API_KEY_ID", "72b0b0aa-ad82-4182-a631-ae4865e8ae0e") +KEY_SECRET = os.getenv("API_KEY_SECRET", "6fuSO7P1m4cj8SSlgaYdblOjNAmnxDVD7tr8") +ORG_ID = os.getenv("ORG_ID", "00000000-0000-0000-0000-000000000002") -### 创建密码请求 -您可以通过指定资产或账户信息来创建一个密码请求。 +class APIClient: + def __init__(self): + self.session = requests.Session() + self.auth = HTTPSignatureAuth( + key_id=KEY_ID, secret=KEY_SECRET, + algorithm='hmac-sha256', headers=['(request-target)', 'accept', 'date', 'x-jms-org'] + ) -```python -request = SecretRequest(asset='Linux', account='root') -``` + def get_account_secret(self, asset, account): + url = f"{API_URL}/api/v1/accounts/integration-applications/account-secret/" + headers = { + 'Accept': 'application/json', + 'X-JMS-ORG': ORG_ID, + 'Date': datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT'), + 'X-Source': 'jms-pam' + } + params = {"asset": asset, "account": account} -### 发送请求 + try: + response = self.session.get(url, auth=self.auth, headers=headers, params=params, timeout=10) + response.raise_for_status() + return response.json() + except requests.RequestException as e: + print(f"API 请求失败: {e}") + return None -使用客户端的 `send` 方法发送请求。 -```python -secret_obj = client.send(request) -``` - -### 处理响应 - -检查密码是否成功检索,并相应地处理响应。 - -```python -if secret_obj.valid: - print('密码: %s' % secret_obj.secret) -else: - print('获取密码失败: %s' % secret_obj.desc) -``` - -### 完整示例 - -以下是如何使用该客户端的完整示例: - -```python -from jms_pam import JumpServerPAM, SecretRequest - -client = JumpServerPAM( - endpoint='http://127.0.0.1', - key_id='your-key-id', - key_secret='your-key-secret' -) - -request = SecretRequest(asset='Linux', account='root') -secret_obj = client.send(request) - -if secret_obj.valid: - print('密码: %s' % secret_obj.secret) -else: - print('获取密码失败: %s' % secret_obj.desc) -``` - -## 错误处理 - -如果提供的参数不符合验证要求,库会引发 `RequestParamsError`。这包括对有效 UUID 的检查和参数之间的相互依赖性检查。 - -## 贡献 - -欢迎贡献!请打开一个问题或提交拉取请求,以进行任何增强或修复错误。 +# 示例调用 +if __name__ == "__main__": + client = APIClient() + result = client.get_account_secret(asset="ubuntu_docker", account="root") + print(result) +``` \ No newline at end of file From c7eb170942e9e73e4f76d4b9efe1a16b845f5c50 Mon Sep 17 00:00:00 2001 From: feng <1304903146@qq.com> Date: Mon, 10 Feb 2025 15:22:04 +0800 Subject: [PATCH 3/3] perf: Custom secret change --- apps/libs/ansible/modules/custom_command.py | 2 +- apps/libs/ansible/modules/mongodb_ping.py | 2 +- apps/libs/ansible/modules/rdp_ping.py | 92 +++++--- apps/libs/ansible/modules/ssh_ping.py | 48 +++-- .../ansible/modules_utils/remote_client.py | 200 ++++++++---------- 5 files changed, 189 insertions(+), 155 deletions(-) diff --git a/apps/libs/ansible/modules/custom_command.py b/apps/libs/ansible/modules/custom_command.py index cb47e7ab0..191e784a3 100644 --- a/apps/libs/ansible/modules/custom_command.py +++ b/apps/libs/ansible/modules/custom_command.py @@ -97,7 +97,7 @@ def main(): msg='No command found, please go to the platform details to add' ) with SSHClient(module) as client: - output, err_msg = client.execute(commands, answers) + __, err_msg = client.execute(commands, answers) if err_msg: module.fail_json( msg='There was a problem executing the command: %s' % err_msg diff --git a/apps/libs/ansible/modules/mongodb_ping.py b/apps/libs/ansible/modules/mongodb_ping.py index d018cd852..a4fb906d3 100644 --- a/apps/libs/ansible/modules/mongodb_ping.py +++ b/apps/libs/ansible/modules/mongodb_ping.py @@ -116,7 +116,7 @@ def main(): try: client.close() - except Exception: + except Exception: # noqa pass return module.exit_json(**result) diff --git a/apps/libs/ansible/modules/rdp_ping.py b/apps/libs/ansible/modules/rdp_ping.py index bfbac2211..a5c2c024e 100644 --- a/apps/libs/ansible/modules/rdp_ping.py +++ b/apps/libs/ansible/modules/rdp_ping.py @@ -7,14 +7,44 @@ __metaclass__ = type DOCUMENTATION = ''' --- module: custom_rdp_ping -short_description: Use rdp to probe whether an asset is connectable +short_description: Use RDP to probe whether an asset is connectable. description: - - Use rdp to probe whether an asset is connectable + - Use RDP to probe whether an asset is connectable. +options: + login_host: + description: Target host to connect. + type: str + required: False + default: localhost + login_port: + description: Target port to connect. + type: int + required: False + default: 22 + login_user: + description: Login user for the connection. + type: str + required: False + default: root + login_password: + description: Login password. + type: str + required: False + no_log: True + login_secret_type: + description: Authentication method. + type: str + required: False + default: password + gateway_args: + description: Arguments for setting up an SSH tunnel. + type: dict + required: False + default: null ''' EXAMPLES = ''' -- name: > - Ping asset server. +- name: Ping asset server using RDP. custom_rdp_ping: login_host: 127.0.0.1 login_port: 3389 @@ -24,12 +54,12 @@ EXAMPLES = ''' RETURN = ''' is_available: - description: Windows server availability. + description: Indicates if the Windows asset is available. returned: always type: bool sample: true conn_err_msg: - description: Connection error message. + description: Connection error message (if any). returned: always type: str sample: '' @@ -41,11 +71,6 @@ from sshtunnel import SSHTunnelForwarder from ansible.module_utils.basic import AnsibleModule -# ========================================= -# Module execution. -# - - def common_argument_spec(): options = dict( login_host=dict(type='str', required=False, default='localhost'), @@ -67,13 +92,12 @@ class RDPConnectionManager: self.result_queue = multiprocessing.Queue() def build_connection_details(self): - connection_details = { + return { 'hostname': self.params['login_host'], 'port': self.params['login_port'], - 'username': self.params['username'], - 'password': self.params['password'] + 'username': self.params['login_user'], + 'password': self.params['login_password'] } - return connection_details def setup_ssh_tunnel(self): gateway_args = self.params['gateway_args'] or {} @@ -90,8 +114,8 @@ class RDPConnectionManager: self.connection_details['port'] ) ) - tunnel.start() + self.connection_details['hostname'] = '127.0.0.1' self.connection_details['port'] = tunnel.local_bind_port self.ssh_tunnel = tunnel @@ -107,13 +131,23 @@ class RDPConnectionManager: self.close_ssh_tunnel() def check_rdp_connectivity(self): - connect_params = list(self.connection_details.values()) + ['', 0] - is_reachable = pyfreerdp.check_connectivity(*connect_params) + connect_params = [ + self.connection_details['hostname'], + self.connection_details['port'], + self.connection_details['username'], + self.connection_details['password'], + '', # extra parameter (if needed) + 0 # timeout (if needed) + ] + try: + is_reachable = pyfreerdp.check_connectivity(*connect_params) + except Exception as ex: + is_reachable = False self.result_queue.put(is_reachable) def attempt_connection(self): if self.params['login_secret_type'] != 'password': - error_message = f'unsupported authentication method: {self.params["login_secret_type"]}' + error_message = f"Unsupported authentication method: {self.params['login_secret_type']}" return False, error_message try: @@ -138,17 +172,21 @@ class RDPConnectionManager: def main(): argument_spec = common_argument_spec() module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) - result = {'changed': False} - module_params = module.params - rdp_manager = RDPConnectionManager(module_params) + rdp_manager = RDPConnectionManager(module.params) is_available, error_message = rdp_manager.attempt_connection() - result['is_available'] = is_available + + # Prepare the result structure. + result = { + 'changed': False, + 'is_available': is_available, + 'conn_err_msg': error_message + } if not is_available: - module.fail_json(msg=f'Unable to connect to asset: {error_message}') - - return module.exit_json(**result) + module.fail_json(msg=f"Unable to connect to asset: {error_message}", **result) + else: + module.exit_json(**result) if __name__ == '__main__': - main() \ No newline at end of file + main() diff --git a/apps/libs/ansible/modules/ssh_ping.py b/apps/libs/ansible/modules/ssh_ping.py index a35f177ac..e2180b1bb 100644 --- a/apps/libs/ansible/modules/ssh_ping.py +++ b/apps/libs/ansible/modules/ssh_ping.py @@ -4,18 +4,35 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type - DOCUMENTATION = ''' --- module: ssh_ping short_description: Use ssh to probe whether an asset is connectable description: - - Use ssh to probe whether an asset is connectable + - Use ssh to probe whether an asset is connectable. +options: + login_host: + description: The target host to connect. + type: str + required: True + login_port: + description: The port on the target host. + type: int + required: False + default: 22 + login_user: + description: The username for the SSH connection. + type: str + required: True + login_password: + description: The password for the SSH connection. + type: str + required: True + no_log: True ''' EXAMPLES = ''' -- name: > - Ping asset server. +- name: Ping asset server using SSH. ssh_ping: login_host: 127.0.0.1 login_port: 22 @@ -25,36 +42,27 @@ EXAMPLES = ''' RETURN = ''' is_available: - description: Ping server availability. + description: Indicate whether the target server is reachable via SSH. returned: always type: bool sample: true ''' - from ansible.module_utils.basic import AnsibleModule - -from libs.ansible.modules_utils.remote_client import ( - SSHClient, common_argument_spec -) - - -# ========================================= -# Module execution. -# +from libs.ansible.modules_utils.remote_client import SSHClient, common_argument_spec def main(): options = common_argument_spec() - module = AnsibleModule(argument_spec=options, supports_check_mode=True,) + module = AnsibleModule(argument_spec=options, supports_check_mode=True) + + result = {'changed': False, 'is_available': False} - result = { - 'changed': False, 'is_available': False - } with SSHClient(module) as client: client.connect() + result['is_available'] = True - return module.exit_json(**result) + module.exit_json(**result) if __name__ == '__main__': diff --git a/apps/libs/ansible/modules_utils/remote_client.py b/apps/libs/ansible/modules_utils/remote_client.py index 371e62881..4a7d7960c 100644 --- a/apps/libs/ansible/modules_utils/remote_client.py +++ b/apps/libs/ansible/modules_utils/remote_client.py @@ -1,55 +1,25 @@ import re import signal import time - -import paramiko - from functools import wraps +import paramiko from sshtunnel import SSHTunnelForwarder - DEFAULT_RE = '.*' SU_PROMPT_LOCALIZATIONS = [ - 'Password', - '암호', - 'パスワード', - 'Adgangskode', - 'Contraseña', - 'Contrasenya', - 'Hasło', - 'Heslo', - 'Jelszó', - 'Lösenord', - 'Mật khẩu', - 'Mot de passe', - 'Parola', - 'Parool', - 'Pasahitza', - 'Passord', - 'Passwort', - 'Salasana', - 'Sandi', - 'Senha', - 'Wachtwoord', - 'ססמה', - 'Лозинка', - 'Парола', - 'Пароль', - 'गुप्तशब्द', - 'शब्दकूट', - 'సంకేతపదము', - 'හස්පදය', - '密码', - '密碼', - '口令', - ] + 'Password', '암호', 'パスワード', 'Adgangskode', 'Contraseña', 'Contrasenya', + 'Hasło', 'Heslo', 'Jelszó', 'Lösenord', 'Mật khẩu', 'Mot de passe', + 'Parola', 'Parool', 'Pasahitza', 'Passord', 'Passwort', 'Salasana', + 'Sandi', 'Senha', 'Wachtwoord', 'ססמה', 'Лозинка', 'Парола', 'Пароль', + 'गुप्तशब्द', 'शब्दकूट', 'సంకేతపదము', 'හස්පදය', '密码', '密碼', '口令', +] def get_become_prompt_re(): - b_password_string = "|".join((r'(\w+\'s )?' + p) for p in SU_PROMPT_LOCALIZATIONS) - b_password_string = b_password_string + ' ?(:|:) ?' - return re.compile(b_password_string, flags=re.IGNORECASE) + pattern_segments = (r'(\w+\'s )?' + p for p in SU_PROMPT_LOCALIZATIONS) + prompt_pattern = "|".join(pattern_segments) + r' ?(:|:) ?' + return re.compile(prompt_pattern, flags=re.IGNORECASE) become_prompt_re = get_become_prompt_re() @@ -88,8 +58,8 @@ def raise_timeout(name=''): def handler(signum, frame): raise TimeoutError(f'{name} timed out, wait {timeout}s') + timeout = getattr(self, 'timeout', 0) try: - timeout = getattr(self, 'timeout', 0) if timeout > 0: signal.signal(signal.SIGALRM, handler) signal.alarm(timeout) @@ -97,7 +67,9 @@ def raise_timeout(name=''): except Exception as error: signal.alarm(0) raise error + return wrapper + return decorate @@ -122,8 +94,8 @@ class SSHClient: self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) self.connect_params = self.get_connect_params() self._channel = None + self.buffer_size = 1024 - self.connect_params = self.get_connect_params() self.prompt = self.module.params['prompt'] self.timeout = self.module.params['recv_timeout'] @@ -134,45 +106,54 @@ class SSHClient: return self._channel def get_connect_params(self): + p = self.module.params params = { - 'allow_agent': False, 'look_for_keys': False, - 'hostname': self.module.params['login_host'], - 'port': self.module.params['login_port'], - 'key_filename': self.module.params['login_private_key_path'] or None + 'allow_agent': False, + 'look_for_keys': False, + 'hostname': p['login_host'], + 'port': p['login_port'], + 'key_filename': p['login_private_key_path'] or None } - 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 + + if p['become']: + params['username'] = p['become_user'] + params['password'] = p['become_password'] + params['key_filename'] = p['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 - if self.module.params['old_ssh_version']: + params['username'] = p['login_user'] + params['password'] = p['login_password'] + params['key_filename'] = p['login_private_key_path'] or None + + if p['old_ssh_version']: params['transport_factory'] = OldSSHTransport + return params def switch_user(self): - if not self.module.params['become']: - return - 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) + p = self.module.params + if not p['become']: return - __, e_msg = self.execute( - [f'{switch_method} {username}', password, 'whoami'], + method = p['become_method'] + username = p['login_user'] + + if method == 'sudo': + switch_cmd = 'sudo su -' + pword = p['become_password'] + elif method == 'su': + switch_cmd = 'su -' + pword = p['login_password'] + else: + self.module.fail_json(msg=f'Become method {method} not supported.') + return + + # Expected to see a prompt, type the password, and check the username + output, error = self.execute( + [f'{switch_cmd} {username}', pword, 'whoami'], [become_prompt_re, DEFAULT_RE, username] ) - if e_msg: - self.module.fail_json(msg='Become user %s failed.' % username) + if error: + self.module.fail_json(msg=f'Failed to become user {username}. Output: {output}') def connect(self): self.before_runner_start() @@ -193,28 +174,32 @@ class SSHClient: return answers @staticmethod - def __match(re_, content): - re_pattern = re_ - if isinstance(re_, str): - re_pattern = re.compile(re_, re.DOTALL | re.IGNORECASE) - elif not isinstance(re_pattern, re.Pattern): - raise ValueError(f'{re_} should be a regular expression') - return bool(re_pattern.search(content)) + def __match(expression, content): + if isinstance(expression, str): + expression = re.compile(expression, re.DOTALL | re.IGNORECASE) + elif not isinstance(expression, re.Pattern): + raise ValueError(f'{expression} should be a regular expression') + + return bool(expression.search(content)) @raise_timeout('Recv message') def _get_match_recv(self, answer_reg=DEFAULT_RE): - last_output, output = '', '' + buffer_str = '' + prev_str = '' + + check_reg = self.prompt if answer_reg == DEFAULT_RE else answer_reg while True: if self.channel.recv_ready(): - recv = self.channel.recv(self.buffer_size).decode() - output += recv - if output and last_output != output: - fin_reg = self.prompt if answer_reg == DEFAULT_RE else answer_reg - if self.__match(fin_reg, output): + chunk = self.channel.recv(self.buffer_size).decode('utf-8', 'replace') + buffer_str += chunk + + if buffer_str and buffer_str != prev_str: + if self.__match(check_reg, buffer_str): break - last_output = output + prev_str = buffer_str time.sleep(0.01) - return output + + return buffer_str @raise_timeout('Wait send message') def _check_send(self): @@ -223,38 +208,44 @@ class SSHClient: time.sleep(self.module.params['delay_time']) def execute(self, commands, answers=None): - all_output, error_msg = '', '' + combined_output = '' + error_msg = '' + try: answers = self._fit_answers(commands, answers) - for index, command in enumerate(commands): + for cmd, ans_regex in zip(commands, answers): self._check_send() - self.channel.send(command + '\n') - all_output += f'{self._get_match_recv(answers[index])}\n' + self.channel.send(cmd + '\n') + combined_output += self._get_match_recv(ans_regex) + '\n' + except Exception as e: error_msg = str(e) - return all_output, error_msg + + return combined_output, error_msg def local_gateway_prepare(self): gateway_args = self.module.params['gateway_args'] or '' - pattern = r"(?:sshpass -p ([^ ]+))?\s*ssh -o Port=(\d+)\s+-o StrictHostKeyChecking=no\s+([\w@]+)@([" \ - r"\d.]+)\s+-W %h:%p -q(?: -i (.+))?'" + pattern = ( + r"(?:sshpass -p ([^ ]+))?\s*ssh -o Port=(\d+)\s+-o StrictHostKeyChecking=no\s+" + r"([\w@]+)@([\d.]+)\s+-W %h:%p -q(?: -i (.+))?'" + ) match = re.search(pattern, gateway_args) - if not match: return - password, port, username, address, private_key_path = match.groups() - password = password if password else None - private_key_path = private_key_path if private_key_path else None - remote_hostname = self.module.params['login_host'] - remote_port = self.module.params['login_port'] + password, port, username, remote_addr, key_path = match.groups() + password = password or None + key_path = key_path or None server = SSHTunnelForwarder( - (address, int(port)), + (remote_addr, int(port)), ssh_username=username, ssh_password=password, - ssh_pkey=private_key_path, - remote_bind_address=(remote_hostname, remote_port) + ssh_pkey=key_path, + remote_bind_address=( + self.module.params['login_host'], + self.module.params['login_port'] + ) ) server.start() @@ -263,11 +254,8 @@ class SSHClient: self.gateway_server = server def local_gateway_clean(self): - gateway_server = self.gateway_server - if not gateway_server: - return - - gateway_server.stop() + if self.gateway_server: + self.gateway_server.stop() def before_runner_start(self): self.local_gateway_prepare()