mirror of https://github.com/jumpserver/jumpserver
feat: Custom change password supports configuration of interactive items
parent
044fd238b8
commit
0032a44d2d
|
@ -20,6 +20,7 @@
|
||||||
become_private_key_path: "{{ jms_custom_become_private_key_path | default(None) }}"
|
become_private_key_path: "{{ jms_custom_become_private_key_path | default(None) }}"
|
||||||
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
||||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
||||||
|
recv_timeout: "{{ params.recv_timeout | default(30) }}"
|
||||||
register: ping_info
|
register: ping_info
|
||||||
delegate_to: localhost
|
delegate_to: localhost
|
||||||
|
|
||||||
|
@ -39,7 +40,9 @@
|
||||||
name: "{{ account.username }}"
|
name: "{{ account.username }}"
|
||||||
password: "{{ account.secret }}"
|
password: "{{ account.secret }}"
|
||||||
commands: "{{ params.commands }}"
|
commands: "{{ params.commands }}"
|
||||||
first_conn_delay_time: "{{ first_conn_delay_time | default(0.5) }}"
|
answers: "{{ params.answers }}"
|
||||||
|
recv_timeout: "{{ params.recv_timeout | default(5) }}"
|
||||||
|
prompt: "{{ params.prompt | default('.*') }}"
|
||||||
ignore_errors: true
|
ignore_errors: true
|
||||||
when: ping_info is succeeded
|
when: ping_info is succeeded
|
||||||
register: change_info
|
register: change_info
|
||||||
|
@ -58,4 +61,5 @@
|
||||||
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
|
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
|
||||||
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
||||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
||||||
|
recv_timeout: "{{ params.recv_timeout | default(30) }}"
|
||||||
delegate_to: localhost
|
delegate_to: localhost
|
||||||
|
|
|
@ -10,10 +10,25 @@ protocol: ssh
|
||||||
priority: 50
|
priority: 50
|
||||||
params:
|
params:
|
||||||
- name: commands
|
- name: commands
|
||||||
type: list
|
type: text
|
||||||
label: "{{ 'Params commands label' | trans }}"
|
label: "{{ 'Params commands label' | trans }}"
|
||||||
default: [ '' ]
|
default: ''
|
||||||
help_text: "{{ 'Params commands help text' | trans }}"
|
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: 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:
|
i18n:
|
||||||
SSH account change secret:
|
SSH account change secret:
|
||||||
|
@ -22,11 +37,81 @@ i18n:
|
||||||
en: 'Custom password change by SSH command line'
|
en: 'Custom password change by SSH command line'
|
||||||
|
|
||||||
Params commands help text:
|
Params commands help text:
|
||||||
zh: '自定义命令中如需包含账号的 账号、密码、SSH 连接的用户密码 字段,<br />请使用 {username}、{password}、{login_password}格式,执行任务时会进行替换 。<br />比如针对 Cisco 主机进行改密,一般需要配置五条命令:<br />1. enable<br />2. {login_password}<br />3. configure terminal<br />4. username {username} privilege 0 password {password} <br />5. end'
|
zh: |
|
||||||
ja: 'カスタム コマンドに SSH 接続用のアカウント番号、パスワード、ユーザー パスワード フィールドを含める必要がある場合は、<br />{ユーザー名}、{パスワード}、{login_password& を使用してください。 # 125; 形式。タスクの実行時に置き換えられます。 <br />たとえば、Cisco ホストのパスワードを変更するには、通常、次の 5 つのコマンドを設定する必要があります:<br />1.enable<br />2.{login_password}<br />3 .ターミナルの設定<br / >4. ユーザー名 {ユーザー名} 権限 0 パスワード {パスワード} <br />5. 終了'
|
请将命令中的指定位置改成特殊符号 <br />
|
||||||
en: 'If the custom command needs to include the account number, password, and user password field for SSH connection,<br />Please use {username}, {password}, {login_password&# 125; format, which will be replaced when executing the task. <br />For example, to change the password of a Cisco host, you generally need to configure five commands:<br />1. enable<br />2. {login_password}<br />3. configure terminal<br / >4. username {username} privilege 0 password {password} <br />5. end'
|
1. 改密账号 -> {username} <br />
|
||||||
|
2. 改密密码 -> {password} <br />
|
||||||
|
3. 登录用户密码 -> {login_password} <br />
|
||||||
|
多条命令使用换行分割,执行任务时系统会根据特殊符号替换真实数据。<br />
|
||||||
|
比如针对 Cisco 主机进行改密,一般需要配置五条命令:<br />
|
||||||
|
enable <br />
|
||||||
|
{login_password} <br />
|
||||||
|
configure terminal <br />
|
||||||
|
username {username} privilege 0 password {password} <br />
|
||||||
|
end <br />
|
||||||
|
ja: |
|
||||||
|
コマンド内の指定された位置を特殊記号に変更してください。<br />
|
||||||
|
新しいパスワード(アカウント変更) -> {username} <br />
|
||||||
|
新しいパスワード(パスワード変更) -> {password} <br />
|
||||||
|
ログインユーザーパスワード -> {login_password} <br />
|
||||||
|
複数のコマンドは改行で区切り、タスクを実行するときにシステムは特殊記号を使用して実際のデータを置き換えます。<br />
|
||||||
|
例えば、Cisco機器のパスワードを変更する場合、一般的には5つのコマンドを設定する必要があります:<br />
|
||||||
|
enable <br />
|
||||||
|
{login_password} <br />
|
||||||
|
configure terminal <br />
|
||||||
|
username {username} privilege 0 password {password} <br />
|
||||||
|
end <br />
|
||||||
|
en: |
|
||||||
|
Please change the specified positions in the command to special symbols. <br />
|
||||||
|
Change password account -> {username} <br />
|
||||||
|
Change password -> {password} <br />
|
||||||
|
Login user password -> {login_password} <br />
|
||||||
|
Multiple commands are separated by new lines, and when executing tasks, <br />
|
||||||
|
the system will replace the special symbols with real data. <br />
|
||||||
|
For example, to change the password for a Cisco device, you generally need to configure five commands: <br />
|
||||||
|
enable <br />
|
||||||
|
{login_password} <br />
|
||||||
|
configure terminal <br />
|
||||||
|
username {username} privilege 0 password {password} <br />
|
||||||
|
end <br />
|
||||||
|
|
||||||
Params commands label:
|
Params commands label:
|
||||||
zh: '自定义命令'
|
zh: '自定义命令'
|
||||||
ja: 'カスタムコマンド'
|
ja: 'カスタムコマンド'
|
||||||
en: 'Custom command'
|
en: 'Custom command'
|
||||||
|
|
||||||
|
Params recv_timeout label:
|
||||||
|
zh: '超时时间(秒)'
|
||||||
|
ja: 'タイムアウト(秒)'
|
||||||
|
en: 'Timeout (Seconds)'
|
||||||
|
|
||||||
|
Params recv_timeout help text:
|
||||||
|
zh: '等待命令结果返回的超时时间'
|
||||||
|
ja: 'コマンドの結果を待つタイムアウト時間'
|
||||||
|
en: 'The timeout for waiting for the command result to return'
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
|
@ -21,3 +21,4 @@
|
||||||
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
|
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
|
||||||
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
||||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
||||||
|
recv_timeout: "{{ params.recv_timeout | default(30) }}"
|
||||||
|
|
|
@ -21,4 +21,5 @@
|
||||||
become_private_key_path: "{{ jms_custom_become_private_key_path | default(None) }}"
|
become_private_key_path: "{{ jms_custom_become_private_key_path | default(None) }}"
|
||||||
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
||||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
||||||
|
recv_timeout: "{{ params.recv_timeout | default(30) }}"
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ class CeleryBaseService(BaseService):
|
||||||
print('\n- Start Celery as Distributed Task Queue: {}'.format(self.queue.capitalize()))
|
print('\n- Start Celery as Distributed Task Queue: {}'.format(self.queue.capitalize()))
|
||||||
ansible_config_path = os.path.join(settings.APPS_DIR, 'libs', 'ansible', 'ansible.cfg')
|
ansible_config_path = os.path.join(settings.APPS_DIR, 'libs', 'ansible', 'ansible.cfg')
|
||||||
ansible_modules_path = os.path.join(settings.APPS_DIR, 'libs', 'ansible', 'modules')
|
ansible_modules_path = os.path.join(settings.APPS_DIR, 'libs', 'ansible', 'modules')
|
||||||
os.environ.setdefault('LC_ALL', 'C.UTF-8')
|
os.environ.setdefault('LC_ALL', 'en_US.UTF-8')
|
||||||
os.environ.setdefault('PYTHONOPTIMIZE', '1')
|
os.environ.setdefault('PYTHONOPTIMIZE', '1')
|
||||||
os.environ.setdefault('ANSIBLE_FORCE_COLOR', 'True')
|
os.environ.setdefault('ANSIBLE_FORCE_COLOR', 'True')
|
||||||
os.environ.setdefault('ANSIBLE_CONFIG', ansible_config_path)
|
os.environ.setdefault('ANSIBLE_CONFIG', ansible_config_path)
|
||||||
|
|
|
@ -33,13 +33,8 @@ options:
|
||||||
commands:
|
commands:
|
||||||
description:
|
description:
|
||||||
- Custom change password commands.
|
- Custom change password commands.
|
||||||
type: list
|
type: str
|
||||||
required: true
|
required: true
|
||||||
first_conn_delay_time:
|
|
||||||
description:
|
|
||||||
- Delay for executing the command after SSH connection(unit: s)
|
|
||||||
type: float
|
|
||||||
required: false
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
EXAMPLES = '''
|
EXAMPLES = '''
|
||||||
|
@ -51,7 +46,7 @@ EXAMPLES = '''
|
||||||
login_password: "123456"
|
login_password: "123456"
|
||||||
name: "jms"
|
name: "jms"
|
||||||
password: "123456"
|
password: "123456"
|
||||||
commands: ['passwd {username}', '{password}', '{password}']
|
commands: 'passwd {username}\n{password}\n{password}']
|
||||||
'''
|
'''
|
||||||
|
|
||||||
RETURN = '''
|
RETURN = '''
|
||||||
|
@ -63,21 +58,23 @@ name:
|
||||||
|
|
||||||
from ansible.module_utils.basic import AnsibleModule
|
from ansible.module_utils.basic import AnsibleModule
|
||||||
|
|
||||||
from libs.ansible.modules_utils.custom_common import (
|
from libs.ansible.modules_utils.paramiko_client import (
|
||||||
SSHClient, common_argument_spec
|
SSHClient, common_argument_spec
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_commands(module):
|
def get_commands_and_answers(module) -> (list, list):
|
||||||
username = module.params['name']
|
username = module.params['name']
|
||||||
password = module.params['password']
|
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']
|
login_password = module.params['login_password']
|
||||||
for index, command in enumerate(commands):
|
commands = commands.format(
|
||||||
commands[index] = command.format(
|
username=username, password=password, login_password=login_password
|
||||||
username=username, password=password, login_password=login_password
|
)
|
||||||
)
|
|
||||||
return commands
|
return commands.split('\n'), answers.split('\n')
|
||||||
|
|
||||||
|
|
||||||
# =========================================
|
# =========================================
|
||||||
# Module execution.
|
# Module execution.
|
||||||
|
@ -89,21 +86,20 @@ def main():
|
||||||
argument_spec.update(
|
argument_spec.update(
|
||||||
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),
|
|
||||||
)
|
)
|
||||||
module = AnsibleModule(argument_spec=argument_spec)
|
module = AnsibleModule(argument_spec=argument_spec)
|
||||||
|
|
||||||
ssh_client = SSHClient(module)
|
commands, answers = get_commands_and_answers(module)
|
||||||
commands = get_commands(module)
|
|
||||||
if not commands:
|
if not commands:
|
||||||
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'
|
||||||
)
|
)
|
||||||
output, err_msg = ssh_client.execute(commands)
|
with SSHClient(module) as client:
|
||||||
if err_msg:
|
output, err_msg = client.execute(commands, answers)
|
||||||
module.fail_json(
|
if err_msg:
|
||||||
msg='There was a problem executing the command: %s' % err_msg
|
module.fail_json(
|
||||||
)
|
msg='There was a problem executing the command: %s' % err_msg
|
||||||
|
)
|
||||||
|
|
||||||
user = module.params['name']
|
user = module.params['name']
|
||||||
module.exit_json(changed=True, user=user)
|
module.exit_json(changed=True, user=user)
|
||||||
|
|
|
@ -34,7 +34,7 @@ is_available:
|
||||||
|
|
||||||
from ansible.module_utils.basic import AnsibleModule
|
from ansible.module_utils.basic import AnsibleModule
|
||||||
|
|
||||||
from libs.ansible.modules_utils.custom_common import (
|
from libs.ansible.modules_utils.paramiko_client import (
|
||||||
SSHClient, common_argument_spec
|
SSHClient, common_argument_spec
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -49,14 +49,11 @@ def main():
|
||||||
module = AnsibleModule(argument_spec=options, supports_check_mode=True,)
|
module = AnsibleModule(argument_spec=options, supports_check_mode=True,)
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
'changed': False, 'is_available': True
|
'changed': False, 'is_available': False
|
||||||
}
|
}
|
||||||
client = SSHClient(module)
|
with SSHClient(module) as client:
|
||||||
err = client.connect()
|
client.connect()
|
||||||
if err:
|
result['is_available'] = True
|
||||||
module.fail_json(msg='Unable to connect to asset: %s' % err)
|
|
||||||
result['is_available'] = False
|
|
||||||
|
|
||||||
return module.exit_json(**result)
|
return module.exit_json(**result)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,105 @@
|
||||||
import re
|
import re
|
||||||
|
import signal
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import paramiko
|
import paramiko
|
||||||
|
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
from sshtunnel import SSHTunnelForwarder
|
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),
|
||||||
|
prompt=dict(type='str', required=False, default='.*'),
|
||||||
|
answers=dict(type='str', required=False, default='.*'),
|
||||||
|
commands=dict(type='str', 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):
|
class OldSSHTransport(paramiko.transport.Transport):
|
||||||
_preferred_pubkeys = (
|
_preferred_pubkeys = (
|
||||||
"ssh-ed25519",
|
"ssh-ed25519",
|
||||||
|
@ -18,41 +113,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:
|
class SSHClient:
|
||||||
TIMEOUT = 20
|
|
||||||
SLEEP_INTERVAL = 2
|
|
||||||
COMPLETE_FLAG = 'complete'
|
|
||||||
|
|
||||||
def __init__(self, module):
|
def __init__(self, module):
|
||||||
self.module = module
|
self.module = module
|
||||||
self.channel = None
|
|
||||||
self.is_connect = False
|
|
||||||
self.gateway_server = None
|
self.gateway_server = None
|
||||||
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())
|
||||||
self.connect_params = self.get_connect_params()
|
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):
|
def get_connect_params(self):
|
||||||
params = {
|
params = {
|
||||||
|
@ -73,22 +151,7 @@ class SSHClient:
|
||||||
params['transport_factory'] = OldSSHTransport
|
params['transport_factory'] = OldSSHTransport
|
||||||
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):
|
|
||||||
# 正常命令切割后是[命令,用户名,交互前缀]
|
|
||||||
content_list = content.split() if len(content.split()) >= 3 else None
|
|
||||||
return content_list and user in content_list
|
|
||||||
|
|
||||||
def switch_user(self):
|
def switch_user(self):
|
||||||
self._get_channel()
|
|
||||||
if not self.module.params['become']:
|
if not self.module.params['become']:
|
||||||
return
|
return
|
||||||
method = self.module.params['become_method']
|
method = self.module.params['become_method']
|
||||||
|
@ -102,22 +165,72 @@ class SSHClient:
|
||||||
else:
|
else:
|
||||||
self.module.fail_json(msg='Become method %s not support' % method)
|
self.module.fail_json(msg='Become method %s not support' % method)
|
||||||
return
|
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):
|
__, e_msg = self.execute(
|
||||||
err_msg = ''
|
[f'{switch_method} {username}', password, 'whoami'],
|
||||||
else:
|
[become_prompt_re, DEFAULT_RE, username]
|
||||||
err_msg = su_output
|
)
|
||||||
return err_msg
|
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)
|
||||||
|
|
||||||
|
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):
|
def local_gateway_prepare(self):
|
||||||
gateway_args = self.module.params['gateway_args'] or ''
|
gateway_args = self.module.params['gateway_args'] or ''
|
||||||
|
@ -160,48 +273,15 @@ class SSHClient:
|
||||||
def after_runner_end(self):
|
def after_runner_end(self):
|
||||||
self.local_gateway_clean()
|
self.local_gateway_clean()
|
||||||
|
|
||||||
def connect(self):
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
try:
|
try:
|
||||||
self.before_runner_start()
|
|
||||||
self.client.connect(**self.connect_params)
|
|
||||||
self.is_connect = True
|
|
||||||
err_msg = self.switch_user()
|
|
||||||
self.after_runner_end()
|
self.after_runner_end()
|
||||||
except Exception as err:
|
if self.channel:
|
||||||
err_msg = str(err)
|
self.channel.close()
|
||||||
return err_msg
|
if self.client:
|
||||||
|
self.client.close()
|
||||||
def _get_recv(self, size=1024, encoding='utf-8'):
|
except Exception: # noqa
|
||||||
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:
|
|
||||||
pass
|
pass
|
Loading…
Reference in New Issue