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' msg='No command found, please go to the platform details to add'
) )
with SSHClient(module) as client: with SSHClient(module) as client:
output, err_msg = client.execute(commands, answers) __, err_msg = client.execute(commands, answers)
if err_msg: if err_msg:
module.fail_json( module.fail_json(
msg='There was a problem executing the command: %s' % err_msg msg='There was a problem executing the command: %s' % err_msg

View File

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

View File

@ -7,14 +7,44 @@ __metaclass__ = type
DOCUMENTATION = ''' DOCUMENTATION = '''
--- ---
module: custom_rdp_ping 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: 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 = ''' EXAMPLES = '''
- name: > - name: Ping asset server using RDP.
Ping asset server.
custom_rdp_ping: custom_rdp_ping:
login_host: 127.0.0.1 login_host: 127.0.0.1
login_port: 3389 login_port: 3389
@ -24,12 +54,12 @@ EXAMPLES = '''
RETURN = ''' RETURN = '''
is_available: is_available:
description: Windows server availability. description: Indicates if the Windows asset is available.
returned: always returned: always
type: bool type: bool
sample: true sample: true
conn_err_msg: conn_err_msg:
description: Connection error message. description: Connection error message (if any).
returned: always returned: always
type: str type: str
sample: '' sample: ''
@ -41,11 +71,6 @@ from sshtunnel import SSHTunnelForwarder
from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import AnsibleModule
# =========================================
# Module execution.
#
def common_argument_spec(): def common_argument_spec():
options = dict( options = dict(
login_host=dict(type='str', required=False, default='localhost'), login_host=dict(type='str', required=False, default='localhost'),
@ -67,13 +92,12 @@ class RDPConnectionManager:
self.result_queue = multiprocessing.Queue() self.result_queue = multiprocessing.Queue()
def build_connection_details(self): def build_connection_details(self):
connection_details = { return {
'hostname': self.params['login_host'], 'hostname': self.params['login_host'],
'port': self.params['login_port'], 'port': self.params['login_port'],
'username': self.params['username'], 'username': self.params['login_user'],
'password': self.params['password'] 'password': self.params['login_password']
} }
return connection_details
def setup_ssh_tunnel(self): def setup_ssh_tunnel(self):
gateway_args = self.params['gateway_args'] or {} gateway_args = self.params['gateway_args'] or {}
@ -90,8 +114,8 @@ class RDPConnectionManager:
self.connection_details['port'] self.connection_details['port']
) )
) )
tunnel.start() tunnel.start()
self.connection_details['hostname'] = '127.0.0.1' self.connection_details['hostname'] = '127.0.0.1'
self.connection_details['port'] = tunnel.local_bind_port self.connection_details['port'] = tunnel.local_bind_port
self.ssh_tunnel = tunnel self.ssh_tunnel = tunnel
@ -107,13 +131,23 @@ class RDPConnectionManager:
self.close_ssh_tunnel() self.close_ssh_tunnel()
def check_rdp_connectivity(self): def check_rdp_connectivity(self):
connect_params = list(self.connection_details.values()) + ['', 0] connect_params = [
is_reachable = pyfreerdp.check_connectivity(*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) self.result_queue.put(is_reachable)
def attempt_connection(self): def attempt_connection(self):
if self.params['login_secret_type'] != 'password': 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 return False, error_message
try: try:
@ -138,17 +172,21 @@ class RDPConnectionManager:
def main(): def main():
argument_spec = common_argument_spec() argument_spec = common_argument_spec()
module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
result = {'changed': False} rdp_manager = RDPConnectionManager(module.params)
module_params = module.params
rdp_manager = RDPConnectionManager(module_params)
is_available, error_message = rdp_manager.attempt_connection() 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: if not is_available:
module.fail_json(msg=f'Unable to connect to asset: {error_message}') module.fail_json(msg=f"Unable to connect to asset: {error_message}", **result)
else:
return module.exit_json(**result) module.exit_json(**result)
if __name__ == '__main__': if __name__ == '__main__':
main() main()

View File

@ -4,18 +4,35 @@ from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
DOCUMENTATION = ''' DOCUMENTATION = '''
--- ---
module: ssh_ping module: ssh_ping
short_description: Use ssh to probe whether an asset is connectable short_description: Use ssh to probe whether an asset is connectable
description: 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 = ''' EXAMPLES = '''
- name: > - name: Ping asset server using SSH.
Ping asset server.
ssh_ping: ssh_ping:
login_host: 127.0.0.1 login_host: 127.0.0.1
login_port: 22 login_port: 22
@ -25,36 +42,27 @@ EXAMPLES = '''
RETURN = ''' RETURN = '''
is_available: is_available:
description: Ping server availability. description: Indicate whether the target server is reachable via SSH.
returned: always returned: always
type: bool type: bool
sample: true sample: true
''' '''
from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import AnsibleModule
from libs.ansible.modules_utils.remote_client import SSHClient, common_argument_spec
from libs.ansible.modules_utils.remote_client import (
SSHClient, common_argument_spec
)
# =========================================
# Module execution.
#
def main(): def main():
options = common_argument_spec() 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: with SSHClient(module) as client:
client.connect() client.connect()
result['is_available'] = True result['is_available'] = True
return module.exit_json(**result) module.exit_json(**result)
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -1,55 +1,25 @@
import re import re
import signal import signal
import time import time
import paramiko
from functools import wraps from functools import wraps
import paramiko
from sshtunnel import SSHTunnelForwarder from sshtunnel import SSHTunnelForwarder
DEFAULT_RE = '.*' DEFAULT_RE = '.*'
SU_PROMPT_LOCALIZATIONS = [ SU_PROMPT_LOCALIZATIONS = [
'Password', 'Password', '암호', 'パスワード', 'Adgangskode', 'Contraseña', 'Contrasenya',
'암호', 'Hasło', 'Heslo', 'Jelszó', 'Lösenord', 'Mật khẩu', 'Mot de passe',
'パスワード', 'Parola', 'Parool', 'Pasahitza', 'Passord', 'Passwort', 'Salasana',
'Adgangskode', 'Sandi', 'Senha', 'Wachtwoord', 'ססמה', 'Лозинка', 'Парола', 'Пароль',
'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(): def get_become_prompt_re():
b_password_string = "|".join((r'(\w+\'s )?' + p) for p in SU_PROMPT_LOCALIZATIONS) pattern_segments = (r'(\w+\'s )?' + p for p in SU_PROMPT_LOCALIZATIONS)
b_password_string = b_password_string + ' ?(:|) ?' prompt_pattern = "|".join(pattern_segments) + r' ?(:|) ?'
return re.compile(b_password_string, flags=re.IGNORECASE) return re.compile(prompt_pattern, flags=re.IGNORECASE)
become_prompt_re = get_become_prompt_re() become_prompt_re = get_become_prompt_re()
@ -88,8 +58,8 @@ def raise_timeout(name=''):
def handler(signum, frame): def handler(signum, frame):
raise TimeoutError(f'{name} timed out, wait {timeout}s') raise TimeoutError(f'{name} timed out, wait {timeout}s')
timeout = getattr(self, 'timeout', 0)
try: try:
timeout = getattr(self, 'timeout', 0)
if timeout > 0: if timeout > 0:
signal.signal(signal.SIGALRM, handler) signal.signal(signal.SIGALRM, handler)
signal.alarm(timeout) signal.alarm(timeout)
@ -97,7 +67,9 @@ def raise_timeout(name=''):
except Exception as error: except Exception as error:
signal.alarm(0) signal.alarm(0)
raise error raise error
return wrapper return wrapper
return decorate return decorate
@ -122,8 +94,8 @@ class 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._channel = None
self.buffer_size = 1024 self.buffer_size = 1024
self.connect_params = self.get_connect_params()
self.prompt = self.module.params['prompt'] self.prompt = self.module.params['prompt']
self.timeout = self.module.params['recv_timeout'] self.timeout = self.module.params['recv_timeout']
@ -134,45 +106,54 @@ class SSHClient:
return self._channel return self._channel
def get_connect_params(self): def get_connect_params(self):
p = self.module.params
params = { params = {
'allow_agent': False, 'look_for_keys': False, 'allow_agent': False,
'hostname': self.module.params['login_host'], 'look_for_keys': False,
'port': self.module.params['login_port'], 'hostname': p['login_host'],
'key_filename': self.module.params['login_private_key_path'] or None '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'] if p['become']:
params['password'] = self.module.params['become_password'] params['username'] = p['become_user']
params['key_filename'] = self.module.params['become_private_key_path'] or None params['password'] = p['become_password']
params['key_filename'] = p['become_private_key_path'] or None
else: else:
params['username'] = self.module.params['login_user'] params['username'] = p['login_user']
params['password'] = self.module.params['login_password'] params['password'] = p['login_password']
params['key_filename'] = self.module.params['login_private_key_path'] or None params['key_filename'] = p['login_private_key_path'] or None
if self.module.params['old_ssh_version']:
if p['old_ssh_version']:
params['transport_factory'] = OldSSHTransport params['transport_factory'] = OldSSHTransport
return params return params
def switch_user(self): def switch_user(self):
if not self.module.params['become']: p = self.module.params
return if not p['become']:
method = self.module.params['become_method']
username = self.module.params['login_user']
if method == 'sudo':
switch_method = 'sudo su -'
password = self.module.params['become_password']
elif method == 'su':
switch_method = 'su -'
password = self.module.params['login_password']
else:
self.module.fail_json(msg='Become method %s not support' % method)
return return
__, e_msg = self.execute( method = p['become_method']
[f'{switch_method} {username}', password, 'whoami'], 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] [become_prompt_re, DEFAULT_RE, username]
) )
if e_msg: if error:
self.module.fail_json(msg='Become user %s failed.' % username) self.module.fail_json(msg=f'Failed to become user {username}. Output: {output}')
def connect(self): def connect(self):
self.before_runner_start() self.before_runner_start()
@ -193,28 +174,32 @@ class SSHClient:
return answers return answers
@staticmethod @staticmethod
def __match(re_, content): def __match(expression, content):
re_pattern = re_ if isinstance(expression, str):
if isinstance(re_, str): expression = re.compile(expression, re.DOTALL | re.IGNORECASE)
re_pattern = re.compile(re_, re.DOTALL | re.IGNORECASE) elif not isinstance(expression, re.Pattern):
elif not isinstance(re_pattern, re.Pattern): raise ValueError(f'{expression} should be a regular expression')
raise ValueError(f'{re_} should be a regular expression')
return bool(re_pattern.search(content)) return bool(expression.search(content))
@raise_timeout('Recv message') @raise_timeout('Recv message')
def _get_match_recv(self, answer_reg=DEFAULT_RE): 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: while True:
if self.channel.recv_ready(): if self.channel.recv_ready():
recv = self.channel.recv(self.buffer_size).decode() chunk = self.channel.recv(self.buffer_size).decode('utf-8', 'replace')
output += recv buffer_str += chunk
if output and last_output != output:
fin_reg = self.prompt if answer_reg == DEFAULT_RE else answer_reg if buffer_str and buffer_str != prev_str:
if self.__match(fin_reg, output): if self.__match(check_reg, buffer_str):
break break
last_output = output prev_str = buffer_str
time.sleep(0.01) time.sleep(0.01)
return output
return buffer_str
@raise_timeout('Wait send message') @raise_timeout('Wait send message')
def _check_send(self): def _check_send(self):
@ -223,38 +208,44 @@ class SSHClient:
time.sleep(self.module.params['delay_time']) time.sleep(self.module.params['delay_time'])
def execute(self, commands, answers=None): def execute(self, commands, answers=None):
all_output, error_msg = '', '' combined_output = ''
error_msg = ''
try: try:
answers = self._fit_answers(commands, answers) answers = self._fit_answers(commands, answers)
for index, command in enumerate(commands): for cmd, ans_regex in zip(commands, answers):
self._check_send() self._check_send()
self.channel.send(command + '\n') self.channel.send(cmd + '\n')
all_output += f'{self._get_match_recv(answers[index])}\n' combined_output += self._get_match_recv(ans_regex) + '\n'
except Exception as e: except Exception as e:
error_msg = str(e) error_msg = str(e)
return all_output, error_msg
return combined_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 ''
pattern = r"(?:sshpass -p ([^ ]+))?\s*ssh -o Port=(\d+)\s+-o StrictHostKeyChecking=no\s+([\w@]+)@([" \ pattern = (
r"\d.]+)\s+-W %h:%p -q(?: -i (.+))?'" 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) match = re.search(pattern, gateway_args)
if not match: if not match:
return return
password, port, username, address, private_key_path = match.groups() password, port, username, remote_addr, key_path = match.groups()
password = password if password else None password = password or None
private_key_path = private_key_path if private_key_path else None key_path = key_path or None
remote_hostname = self.module.params['login_host']
remote_port = self.module.params['login_port']
server = SSHTunnelForwarder( server = SSHTunnelForwarder(
(address, int(port)), (remote_addr, int(port)),
ssh_username=username, ssh_username=username,
ssh_password=password, ssh_password=password,
ssh_pkey=private_key_path, ssh_pkey=key_path,
remote_bind_address=(remote_hostname, remote_port) remote_bind_address=(
self.module.params['login_host'],
self.module.params['login_port']
)
) )
server.start() server.start()
@ -263,11 +254,8 @@ class SSHClient:
self.gateway_server = server self.gateway_server = server
def local_gateway_clean(self): def local_gateway_clean(self):
gateway_server = self.gateway_server if self.gateway_server:
if not gateway_server: self.gateway_server.stop()
return
gateway_server.stop()
def before_runner_start(self): def before_runner_start(self):
self.local_gateway_prepare() self.local_gateway_prepare()