perf: Custom secret change

pull/14871/head
feng 2025-02-10 15:22:04 +08:00 committed by feng626
parent a9433bc48e
commit c7eb170942
5 changed files with 189 additions and 155 deletions

View File

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

View File

@ -116,7 +116,7 @@ def main():
try:
client.close()
except Exception:
except Exception: # noqa
pass
return module.exit_json(**result)

View File

@ -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]
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,16 +172,20 @@ 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__':

View File

@ -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__':

View File

@ -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')
try:
timeout = getattr(self, 'timeout', 0)
try:
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()