feat: 支持 ansible receptor private 方式认证, 支持运行完成工作空间清理

pull/12996/head
Aaron3S 2024-04-11 17:37:46 +08:00 committed by Bryan
parent fa5d9d3df4
commit 574639d5e1
5 changed files with 73 additions and 22 deletions

View File

@ -137,16 +137,13 @@ class BaseAccount(VaultModelMixin, JMSOrgBaseModel):
else: else:
return None return None
@property def get_private_key_path(self, path):
def private_key_path(self):
if self.secret_type != SecretType.SSH_KEY \ if self.secret_type != SecretType.SSH_KEY \
or not self.secret \ or not self.secret \
or not self.private_key: or not self.private_key:
return None return None
project_dir = settings.PROJECT_DIR
tmp_dir = os.path.join(project_dir, 'tmp')
key_name = '.' + md5(self.private_key.encode('utf-8')).hexdigest() key_name = '.' + md5(self.private_key.encode('utf-8')).hexdigest()
key_path = os.path.join(tmp_dir, key_name) key_path = os.path.join(path, key_name)
if not os.path.exists(key_path): if not os.path.exists(key_path):
# https://github.com/ansible/ansible-runner/issues/544 # https://github.com/ansible/ansible-runner/issues/544
# ssh requires OpenSSH format keys to have a full ending newline. # ssh requires OpenSSH format keys to have a full ending newline.
@ -158,6 +155,12 @@ class BaseAccount(VaultModelMixin, JMSOrgBaseModel):
os.chmod(key_path, 0o400) os.chmod(key_path, 0o400)
return key_path return key_path
@property
def private_key_path(self):
project_dir = settings.PROJECT_DIR
tmp_dir = os.path.join(project_dir, 'tmp')
return self.get_private_key_path(tmp_dir)
def get_private_key(self): def get_private_key(self):
if not self.private_key: if not self.private_key:
return None return None

View File

@ -73,3 +73,7 @@ class Gateway(Host):
def private_key_path(self): def private_key_path(self):
account = self.select_account account = self.select_account
return account.private_key_path if account else None return account.private_key_path if account else None
def get_private_key_path(self, path):
account = self.select_account
return account.get_private_key_path(path) if account else None

View File

@ -0,0 +1,36 @@
import os
import shutil
from functools import wraps
from settings.api import settings
def cleanup_post_run(func):
def get_instance(*args):
if not len(args) > 0:
return
return args[0]
@wraps(func)
def wrapper(*args, **kwargs):
instance = get_instance(*args)
if not instance or not issubclass(type(instance), WorkPostRunCleaner):
raise NotImplementedError("you should extend 'WorkPostRunCleaner'")
try:
return func(*args, **kwargs)
finally:
instance.clean_post_run()
return wrapper
class WorkPostRunCleaner:
@property
def clean_dir(self):
raise NotImplemented
def clean_post_run(self):
if settings.DEBUG_DEV:
return
if self.clean_dir and os.path.exists(self.clean_dir):
shutil.rmtree(self.clean_dir)

View File

@ -45,7 +45,7 @@ class JMSInventory:
return groups return groups
@staticmethod @staticmethod
def make_proxy_command(gateway): def make_proxy_command(gateway, path_dir):
proxy_command_list = [ proxy_command_list = [
"ssh", "-o", "Port={}".format(gateway.port), "ssh", "-o", "Port={}".format(gateway.port),
"-o", "StrictHostKeyChecking=no", "-o", "StrictHostKeyChecking=no",
@ -58,7 +58,7 @@ class JMSInventory:
0, "sshpass -p {}".format(gateway.password) 0, "sshpass -p {}".format(gateway.password)
) )
if gateway.private_key: if gateway.private_key:
proxy_command_list.append("-i {}".format(gateway.private_key_path)) proxy_command_list.append("-i {}".format(gateway.get_private_key_path(path_dir)))
proxy_command = "-o ProxyCommand='{}'".format( proxy_command = "-o ProxyCommand='{}'".format(
" ".join(proxy_command_list) " ".join(proxy_command_list)
@ -66,7 +66,7 @@ class JMSInventory:
return {"ansible_ssh_common_args": proxy_command} return {"ansible_ssh_common_args": proxy_command}
@staticmethod @staticmethod
def make_account_ansible_vars(account): def make_account_ansible_vars(account, path_dir):
var = { var = {
'ansible_user': account.username, 'ansible_user': account.username,
} }
@ -76,18 +76,18 @@ class JMSInventory:
if account.secret_type == 'password': if account.secret_type == 'password':
var['ansible_password'] = account.escape_jinja2_syntax(account.secret) var['ansible_password'] = account.escape_jinja2_syntax(account.secret)
elif account.secret_type == 'ssh_key': elif account.secret_type == 'ssh_key':
var['ansible_ssh_private_key_file'] = account.private_key_path var['ansible_ssh_private_key_file'] = account.get_private_key_path(path_dir)
return var return var
@staticmethod @staticmethod
def make_custom_become_ansible_vars(account, su_from_auth): def make_custom_become_ansible_vars(account, su_from_auth, path_dir):
su_method = su_from_auth['ansible_become_method'] su_method = su_from_auth['ansible_become_method']
var = { var = {
'custom_become': True, 'custom_become': True,
'custom_become_method': su_method, 'custom_become_method': su_method,
'custom_become_user': account.su_from.username, 'custom_become_user': account.su_from.username,
'custom_become_password': account.escape_jinja2_syntax(account.su_from.secret), 'custom_become_password': account.escape_jinja2_syntax(account.su_from.secret),
'custom_become_private_key_path': account.su_from.private_key_path 'custom_become_private_key_path': account.su_from.get_private_key_path(path_dir)
} }
return var return var
@ -100,7 +100,7 @@ class JMSInventory:
setting = getattr(p, 'setting') setting = getattr(p, 'setting')
host['old_ssh_version'] = setting.get('old_ssh_version', False) host['old_ssh_version'] = setting.get('old_ssh_version', False)
def make_account_vars(self, host, asset, account, automation, protocol, platform, gateway): def make_account_vars(self, host, asset, account, automation, protocol, platform, gateway, path_dir):
from accounts.const import AutomationTypes from accounts.const import AutomationTypes
if not account: if not account:
host['error'] = _("No account available") host['error'] = _("No account available")
@ -114,15 +114,15 @@ class JMSInventory:
if platform.su_enabled and su_from: if platform.su_enabled and su_from:
su_from_auth = account.get_ansible_become_auth() su_from_auth = account.get_ansible_become_auth()
host.update(su_from_auth) host.update(su_from_auth)
host.update(self.make_custom_become_ansible_vars(account, su_from_auth)) host.update(self.make_custom_become_ansible_vars(account, su_from_auth, path_dir))
elif platform.su_enabled and not su_from and \ elif platform.su_enabled and not su_from and \
self.task_type in (AutomationTypes.change_secret, AutomationTypes.push_account): self.task_type in (AutomationTypes.change_secret, AutomationTypes.push_account):
host.update(self.make_account_ansible_vars(account)) host.update(self.make_account_ansible_vars(account, path_dir))
host['ansible_become'] = True host['ansible_become'] = True
host['ansible_become_user'] = 'root' host['ansible_become_user'] = 'root'
host['ansible_become_password'] = account.escape_jinja2_syntax(account.secret) host['ansible_become_password'] = account.escape_jinja2_syntax(account.secret)
else: else:
host.update(self.make_account_ansible_vars(account)) host.update(self.make_account_ansible_vars(account, path_dir))
if platform.name == 'Huawei': if platform.name == 'Huawei':
host['ansible_connection'] = 'network_cli' host['ansible_connection'] = 'network_cli'
@ -134,11 +134,11 @@ class JMSInventory:
host['gateway'] = { host['gateway'] = {
'address': gateway.address, 'port': gateway.port, 'address': gateway.address, 'port': gateway.port,
'username': gateway.username, 'secret': gateway.password, 'username': gateway.username, 'secret': gateway.password,
'private_key_path': gateway.private_key_path 'private_key_path': gateway.get_private_key_path(path_dir)
} }
host['jms_asset']['port'] = port host['jms_asset']['port'] = port
else: else:
ansible_ssh_common_args = self.make_proxy_command(gateway) ansible_ssh_common_args = self.make_proxy_command(gateway, path_dir)
host['jms_asset'].update(ansible_ssh_common_args) host['jms_asset'].update(ansible_ssh_common_args)
host.update(ansible_ssh_common_args) host.update(ansible_ssh_common_args)
@ -168,7 +168,7 @@ class JMSInventory:
ansible_config['ansible_winrm_connection_timeout'] = 120 ansible_config['ansible_winrm_connection_timeout'] = 120
return ansible_config return ansible_config
def asset_to_host(self, asset, account, automation, protocols, platform): def asset_to_host(self, asset, account, automation, protocols, platform, path_dir):
try: try:
ansible_config = dict(automation.ansible_config) ansible_config = dict(automation.ansible_config)
except (AttributeError, TypeError): except (AttributeError, TypeError):
@ -191,7 +191,7 @@ class JMSInventory:
'jms_account': { 'jms_account': {
'id': str(account.id), 'username': account.username, 'id': str(account.id), 'username': account.username,
'secret': account.escape_jinja2_syntax(account.secret), 'secret': account.escape_jinja2_syntax(account.secret),
'secret_type': account.secret_type, 'private_key_path': account.private_key_path 'secret_type': account.secret_type, 'private_key_path': account.get_private_key_path(path_dir)
} if account else None } if account else None
} }
@ -210,7 +210,7 @@ class JMSInventory:
gateway = asset.domain.select_gateway() gateway = asset.domain.select_gateway()
self.make_account_vars( self.make_account_vars(
host, asset, account, automation, protocol, platform, gateway host, asset, account, automation, protocol, platform, gateway, path_dir
) )
return host return host
@ -274,7 +274,7 @@ class JMSInventory:
for asset in assets: for asset in assets:
protocols = self.set_platform_protocol_setting_to_asset(asset, platform_protocols) protocols = self.set_platform_protocol_setting_to_asset(asset, platform_protocols)
account = self.select_account(asset) account = self.select_account(asset)
host = self.asset_to_host(asset, account, automation, protocols, platform) host = self.asset_to_host(asset, account, automation, protocols, platform, path_dir)
if not automation.ansible_enabled: if not automation.ansible_enabled:
host['error'] = _('Ansible disabled') host['error'] = _('Ansible disabled')

View File

@ -2,11 +2,14 @@ import concurrent.futures
import os import os
import queue import queue
import socket import socket
from django.conf import settings from django.conf import settings
import ansible_runner import ansible_runner
from django.utils.functional import LazyObject from django.utils.functional import LazyObject
from receptorctl import ReceptorControl from receptorctl import ReceptorControl
from ops.ansible.cleaner import WorkPostRunCleaner, cleanup_post_run
class WarpedReceptorctl(LazyObject): class WarpedReceptorctl(LazyObject):
def _setup(self): def _setup(self):
@ -33,7 +36,7 @@ def run(**kwargs):
return receptor_runner.run() return receptor_runner.run()
class AnsibleReceptorRunner: class AnsibleReceptorRunner(WorkPostRunCleaner):
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.runner_params = kwargs self.runner_params = kwargs
self.unit_id = None self.unit_id = None
@ -46,6 +49,11 @@ class AnsibleReceptorRunner:
f.write(self.unit_id) f.write(self.unit_id)
f.flush() f.flush()
@property
def clean_dir(self):
return self.runner_params.get("private_data_dir", None)
@cleanup_post_run
def run(self): def run(self):
input, output = socket.socketpair() input, output = socket.socketpair()