2023-08-08 09:26:29 +00:00
|
|
|
import re
|
2023-04-14 10:31:09 +00:00
|
|
|
import time
|
|
|
|
|
|
|
|
import paramiko
|
2023-08-08 09:26:29 +00:00
|
|
|
from sshtunnel import SSHTunnelForwarder
|
2023-04-14 10:31:09 +00:00
|
|
|
|
2024-01-13 11:40:46 +00:00
|
|
|
|
2024-04-01 09:28:18 +00:00
|
|
|
class OldSSHTransport(paramiko.transport.Transport):
|
2024-01-13 11:40:46 +00:00
|
|
|
_preferred_pubkeys = (
|
|
|
|
"ssh-ed25519",
|
|
|
|
"ecdsa-sha2-nistp256",
|
|
|
|
"ecdsa-sha2-nistp384",
|
|
|
|
"ecdsa-sha2-nistp521",
|
|
|
|
"ssh-rsa",
|
|
|
|
"rsa-sha2-256",
|
|
|
|
"rsa-sha2-512",
|
|
|
|
"ssh-dss",
|
|
|
|
)
|
|
|
|
|
2023-04-14 10:31:09 +00:00
|
|
|
|
2023-06-07 09:28:35 +00:00
|
|
|
def common_argument_spec():
|
2023-04-14 10:31:09 +00:00
|
|
|
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),
|
2023-08-03 06:09:13 +00:00
|
|
|
first_conn_delay_time=dict(type='float', required=False, default=0.5),
|
2023-08-08 09:26:29 +00:00
|
|
|
gateway_args=dict(type='str', required=False, default=''),
|
2023-08-03 06:09:13 +00:00
|
|
|
|
|
|
|
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),
|
2024-04-01 09:28:18 +00:00
|
|
|
|
|
|
|
old_ssh_version=dict(type='bool', default=False, required=False),
|
2023-04-14 10:31:09 +00:00
|
|
|
)
|
|
|
|
return options
|
|
|
|
|
|
|
|
|
|
|
|
class SSHClient:
|
2023-12-21 11:49:24 +00:00
|
|
|
TIMEOUT = 20
|
2023-11-14 09:03:20 +00:00
|
|
|
SLEEP_INTERVAL = 2
|
2023-10-25 08:21:45 +00:00
|
|
|
COMPLETE_FLAG = 'complete'
|
|
|
|
|
2023-04-14 10:31:09 +00:00
|
|
|
def __init__(self, module):
|
|
|
|
self.module = module
|
2023-08-03 06:09:13 +00:00
|
|
|
self.channel = None
|
2023-04-14 10:31:09 +00:00
|
|
|
self.is_connect = False
|
2023-08-08 09:26:29 +00:00
|
|
|
self.gateway_server = None
|
2023-04-14 10:31:09 +00:00
|
|
|
self.client = paramiko.SSHClient()
|
|
|
|
self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
2023-08-08 09:26:29 +00:00
|
|
|
self.connect_params = self.get_connect_params()
|
2023-04-14 10:31:09 +00:00
|
|
|
|
|
|
|
def get_connect_params(self):
|
|
|
|
params = {
|
|
|
|
'allow_agent': False, 'look_for_keys': False,
|
|
|
|
'hostname': self.module.params['login_host'],
|
|
|
|
'port': self.module.params['login_port'],
|
2023-12-28 09:31:44 +00:00
|
|
|
'key_filename': self.module.params['login_private_key_path'] or None
|
2023-04-14 10:31:09 +00:00
|
|
|
}
|
2023-08-03 06:09:13 +00:00
|
|
|
if self.module.params['become']:
|
|
|
|
params['username'] = self.module.params['become_user']
|
|
|
|
params['password'] = self.module.params['become_password']
|
2023-12-28 09:31:44 +00:00
|
|
|
params['key_filename'] = self.module.params['become_private_key_path'] or None
|
2023-04-14 10:31:09 +00:00
|
|
|
else:
|
2023-08-03 06:09:13 +00:00
|
|
|
params['username'] = self.module.params['login_user']
|
2023-04-14 10:31:09 +00:00
|
|
|
params['password'] = self.module.params['login_password']
|
2023-12-28 09:31:44 +00:00
|
|
|
params['key_filename'] = self.module.params['login_private_key_path'] or None
|
2024-04-01 09:28:18 +00:00
|
|
|
if self.module.params['old_ssh_version']:
|
|
|
|
params['transport_factory'] = OldSSHTransport
|
2023-04-14 10:31:09 +00:00
|
|
|
return params
|
|
|
|
|
2023-08-03 06:09:13 +00:00
|
|
|
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):
|
|
|
|
# 正常命令切割后是[命令,用户名,交互前缀]
|
2023-10-09 06:35:21 +00:00
|
|
|
content_list = content.split() if len(content.split()) >= 3 else None
|
|
|
|
return content_list and user in content_list
|
2023-08-03 06:09:13 +00:00
|
|
|
|
|
|
|
def switch_user(self):
|
|
|
|
self._get_channel()
|
|
|
|
if not self.module.params['become']:
|
2023-10-09 06:35:21 +00:00
|
|
|
return
|
2023-08-03 06:09:13 +00:00
|
|
|
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
|
|
|
|
commands = [f'{switch_method} {username}', password]
|
|
|
|
su_output, err_msg = self.execute(commands)
|
|
|
|
if err_msg:
|
|
|
|
return err_msg
|
2023-10-25 08:21:45 +00:00
|
|
|
i_output, err_msg = self.execute(
|
|
|
|
[f'whoami && echo "{self.COMPLETE_FLAG}"'],
|
|
|
|
validate_output=True
|
|
|
|
)
|
2023-08-03 06:09:13 +00:00
|
|
|
if err_msg:
|
|
|
|
return err_msg
|
|
|
|
|
|
|
|
if self._is_match_user(username, i_output):
|
|
|
|
err_msg = ''
|
|
|
|
else:
|
|
|
|
err_msg = su_output
|
|
|
|
return err_msg
|
|
|
|
|
2023-08-08 09:26:29 +00:00
|
|
|
def local_gateway_prepare(self):
|
|
|
|
gateway_args = self.module.params['gateway_args'] or ''
|
2024-07-19 02:37:42 +00:00
|
|
|
pattern = r"(?:sshpass -p ([^ ]+))?\s*ssh -o Port=(\d+)\s+-o StrictHostKeyChecking=no\s+([\w@]+)@([" \
|
2023-08-08 09:26:29 +00:00
|
|
|
r"\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']
|
|
|
|
|
|
|
|
server = SSHTunnelForwarder(
|
|
|
|
(address, int(port)),
|
|
|
|
ssh_username=username,
|
|
|
|
ssh_password=password,
|
|
|
|
ssh_pkey=private_key_path,
|
|
|
|
remote_bind_address=(remote_hostname, remote_port)
|
|
|
|
)
|
|
|
|
|
|
|
|
server.start()
|
|
|
|
self.connect_params['hostname'] = '127.0.0.1'
|
|
|
|
self.connect_params['port'] = server.local_bind_port
|
|
|
|
self.gateway_server = server
|
|
|
|
|
|
|
|
def local_gateway_clean(self):
|
|
|
|
gateway_server = self.gateway_server
|
|
|
|
if not gateway_server:
|
|
|
|
return
|
|
|
|
try:
|
|
|
|
gateway_server.stop()
|
|
|
|
except Exception:
|
|
|
|
pass
|
|
|
|
|
|
|
|
def before_runner_start(self):
|
|
|
|
self.local_gateway_prepare()
|
|
|
|
|
|
|
|
def after_runner_end(self):
|
|
|
|
self.local_gateway_clean()
|
|
|
|
|
2023-04-14 10:31:09 +00:00
|
|
|
def connect(self):
|
|
|
|
try:
|
2023-08-08 09:26:29 +00:00
|
|
|
self.before_runner_start()
|
|
|
|
self.client.connect(**self.connect_params)
|
2023-04-14 10:31:09 +00:00
|
|
|
self.is_connect = True
|
2023-08-03 06:09:13 +00:00
|
|
|
err_msg = self.switch_user()
|
2023-08-08 09:26:29 +00:00
|
|
|
self.after_runner_end()
|
2023-08-03 06:09:13 +00:00
|
|
|
except Exception as err:
|
|
|
|
err_msg = str(err)
|
2023-04-14 10:31:09 +00:00
|
|
|
return err_msg
|
|
|
|
|
2023-08-03 06:09:13 +00:00
|
|
|
def _get_recv(self, size=1024, encoding='utf-8'):
|
|
|
|
output = self.channel.recv(size).decode(encoding)
|
|
|
|
return output
|
|
|
|
|
2023-10-25 08:21:45 +00:00
|
|
|
def execute(self, commands, validate_output=False):
|
2023-04-14 10:31:09 +00:00
|
|
|
if not self.is_connect:
|
|
|
|
self.connect()
|
2023-08-03 06:09:13 +00:00
|
|
|
output, error_msg = '', ''
|
2023-04-14 10:31:09 +00:00
|
|
|
try:
|
|
|
|
for command in commands:
|
2023-08-03 06:09:13 +00:00
|
|
|
self.channel.send(command + '\n')
|
2023-10-25 08:21:45 +00:00
|
|
|
if not validate_output:
|
|
|
|
time.sleep(self.SLEEP_INTERVAL)
|
|
|
|
output += self._get_recv()
|
|
|
|
continue
|
2023-12-21 11:49:24 +00:00
|
|
|
start_time = time.time()
|
2023-10-25 08:21:45 +00:00
|
|
|
while self.COMPLETE_FLAG not in output:
|
2023-12-21 11:49:24 +00:00
|
|
|
if time.time() - start_time > self.TIMEOUT:
|
|
|
|
error_msg = output
|
|
|
|
print("切换用户操作超时,跳出循环。")
|
|
|
|
break
|
2023-10-25 08:21:45 +00:00
|
|
|
time.sleep(self.SLEEP_INTERVAL)
|
|
|
|
received_output = self._get_recv().replace(f'"{self.COMPLETE_FLAG}"', '')
|
|
|
|
output += received_output
|
2023-08-03 06:09:13 +00:00
|
|
|
except Exception as e:
|
|
|
|
error_msg = str(e)
|
|
|
|
return output, error_msg
|
|
|
|
|
|
|
|
def __del__(self):
|
|
|
|
try:
|
|
|
|
self.channel.close()
|
|
|
|
self.client.close()
|
2023-10-09 06:35:21 +00:00
|
|
|
except Exception:
|
2023-08-03 06:09:13 +00:00
|
|
|
pass
|