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]
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()
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')
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()