mirror of https://github.com/jumpserver/jumpserver
feat: 支持 ansible receptor private 方式认证, 支持运行完成工作空间清理
parent
fa5d9d3df4
commit
574639d5e1
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
|
@ -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')
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue