feat: Custom change password supports configuration of interactive items

pull/14387/head
jiangweidong 3 weeks ago
parent 372196ca37
commit 7b3cfe0f8c

@ -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
register: change_info
@ -58,4 +62,5 @@
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

@ -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 连接的用户密码 字段,<br />请使用 &#123;username&#125;、&#123;password&#125;、&#123;login_password&#125;格式,执行任务时会进行替换 。<br />比如针对 Cisco 主机进行改密,一般需要配置五条命令:<br />1. enable<br />2. &#123;login_password&#125;<br />3. configure terminal<br />4. username &#123;username&#125; privilege 0 password &#123;password&#125; <br />5. end'
ja: 'カスタム コマンドに SSH 接続用のアカウント番号、パスワード、ユーザー パスワード フィールドを含める必要がある場合は、<br />&#123;ユーザー名&#125;、&#123;パスワード&#125;、&#123;login_password& を使用してください。 # 125; 形式。タスクの実行時に置き換えられます。 <br />たとえば、Cisco ホストのパスワードを変更するには、通常、次の 5 つのコマンドを設定する必要があります:<br />1.enable<br />2.&#123;login_password&#125;<br />3 .ターミナルの設定<br / >4. ユーザー名 &#123;ユーザー名&#125; 権限 0 パスワード &#123;パスワード&#125; <br />5. 終了'
en: 'If the custom command needs to include the account number, password, and user password field for SSH connection,<br />Please use &#123;username&#125;, &#123;password&#125;, &#123;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. &#123;login_password&#125;<br />3. configure terminal<br / >4. username &#123;username&#125; privilege 0 password &#123;password&#125; <br />5. end'
zh: |
请将命令中的指定位置改成特殊符号 <br />
1. 改密账号 -> {username} <br />
2. 改密密码 -> {password} <br />
3. 登录用户密码 -> {login_password} <br />
<strong>多条命令使用换行分割,</strong>执行任务时系统会根据特殊符号替换真实数据。<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 />
<strong>複数のコマンドは改行で区切り、</strong>タスクを実行するときにシステムは特殊記号を使用して実際のデータを置き換えます。<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 />
<strong>Multiple commands are separated by new lines,</strong> 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:
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)

@ -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) }}"

@ -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) }}"

@ -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,

@ -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)

@ -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)

@ -1,21 +1,58 @@
import re
import signal
import time
import paramiko
from functools import wraps
from sshtunnel import SSHTunnelForwarder
class OldSSHTransport(paramiko.transport.Transport):
_preferred_pubkeys = (
"ssh-ed25519",
"ecdsa-sha2-nistp256",
"ecdsa-sha2-nistp384",
"ecdsa-sha2-nistp521",
"ssh-rsa",
"rsa-sha2-256",
"rsa-sha2-512",
"ssh-dss",
)
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():
@ -26,8 +63,12 @@ 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),
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),
@ -40,19 +81,57 @@ def common_argument_spec():
return options
class SSHClient:
TIMEOUT = 20
SLEEP_INTERVAL = 2
COMPLETE_FLAG = 'complete'
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",
"ecdsa-sha2-nistp256",
"ecdsa-sha2-nistp384",
"ecdsa-sha2-nistp521",
"ssh-rsa",
"rsa-sha2-256",
"rsa-sha2-512",
"ssh-dss",
)
class SSHClient:
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
__, e_msg = self.execute(
[f'{switch_method} {username}', password, 'whoami'],
[become_prompt_re, DEFAULT_RE, username]
)
if err_msg:
return err_msg
if e_msg:
self.module.fail_json(msg='Become user %s failed.' % username)
if self._is_match_user(username, i_output):
err_msg = ''
else:
err_msg = su_output
return err_msg
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):
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 __enter__(self):
return self
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):
def __exit__(self, exc_type, exc_val, exc_tb):
try:
self.channel.close()
self.client.close()
except Exception:
self.after_runner_end()
if self.channel:
self.channel.close()
if self.client:
self.client.close()
except Exception: # noqa
pass
Loading…
Cancel
Save