mirror of
https://github.com/jumpserver/jumpserver.git
synced 2026-07-02 07:01:30 +00:00
perf: add ANSIBLE_DOCKER_ENABLED configuration
This commit is contained in:
@@ -589,6 +589,7 @@ class Config(dict):
|
||||
'SECURITY_MFA_AUTH_ENABLED_FOR_THIRD_PARTY': True,
|
||||
'SECURITY_MFA_BY_EMAIL': False,
|
||||
'SECURITY_COMMAND_EXECUTION': False,
|
||||
'ANSIBLE_DOCKER_ENABLED': True,
|
||||
'SECURITY_COMMAND_BLACKLIST': [
|
||||
'reboot', 'shutdown', 'poweroff', 'halt', 'dd', 'half', 'top'
|
||||
],
|
||||
|
||||
@@ -43,6 +43,7 @@ SECURITY_MFA_BY_EMAIL = CONFIG.SECURITY_MFA_BY_EMAIL
|
||||
SECURITY_MAX_IDLE_TIME = CONFIG.SECURITY_MAX_IDLE_TIME # Unit: minute
|
||||
SECURITY_MAX_SESSION_TIME = CONFIG.SECURITY_MAX_SESSION_TIME # Unit: hour
|
||||
SECURITY_COMMAND_EXECUTION = CONFIG.SECURITY_COMMAND_EXECUTION
|
||||
ANSIBLE_DOCKER_ENABLED = CONFIG.ANSIBLE_DOCKER_ENABLED
|
||||
SECURITY_COMMAND_BLACKLIST = CONFIG.SECURITY_COMMAND_BLACKLIST
|
||||
SECURITY_PASSWORD_EXPIRATION_TIME_ADMIN = CONFIG.SECURITY_PASSWORD_EXPIRATION_TIME_ADMIN # Unit: day
|
||||
SECURITY_PASSWORD_EXPIRATION_TIME = CONFIG.SECURITY_PASSWORD_EXPIRATION_TIME # Unit: day
|
||||
|
||||
76
apps/ops/ansible/docker.py
Normal file
76
apps/ops/ansible/docker.py
Normal file
@@ -0,0 +1,76 @@
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from common.utils.safe import safe_run_cmd
|
||||
from .exception import AnsibleDockerImageNotFound
|
||||
|
||||
ANSIBLE_EE_IMAGE = 'jumpserver/ansible-executor:latest'
|
||||
ANSIBLE_EE_PYTHON_INTERPRETER = '/usr/bin/python3.11'
|
||||
|
||||
__all__ = [
|
||||
'ANSIBLE_EE_IMAGE',
|
||||
'ANSIBLE_EE_PYTHON_INTERPRETER',
|
||||
'use_ansible_docker_isolation',
|
||||
'docker_extravars',
|
||||
'docker_isolation_kwargs',
|
||||
'prepare_isolated_ansible_cfg',
|
||||
'stage_inventory_for_docker',
|
||||
'ensure_ansible_docker_image',
|
||||
]
|
||||
|
||||
|
||||
def use_ansible_docker_isolation():
|
||||
return settings.ANSIBLE_DOCKER_ENABLED
|
||||
|
||||
|
||||
def docker_extravars(extra_vars):
|
||||
extravars = dict(extra_vars or {})
|
||||
if use_ansible_docker_isolation():
|
||||
extravars.setdefault('local_python_interpreter', ANSIBLE_EE_PYTHON_INTERPRETER)
|
||||
return extravars
|
||||
|
||||
|
||||
def docker_isolation_kwargs():
|
||||
return {
|
||||
'process_isolation': True,
|
||||
'process_isolation_executable': 'docker',
|
||||
'container_image': ANSIBLE_EE_IMAGE,
|
||||
'container_options': ['--network=jms_net'],
|
||||
}
|
||||
|
||||
|
||||
def prepare_isolated_ansible_cfg(project_dir):
|
||||
if not use_ansible_docker_isolation():
|
||||
return
|
||||
src = os.path.join(settings.APPS_DIR, 'libs', 'ansible', 'ansible.cfg')
|
||||
dst = os.path.join(project_dir, 'ansible.cfg')
|
||||
shutil.copyfile(src, dst)
|
||||
|
||||
|
||||
def stage_inventory_for_docker(project_dir, inventory_path):
|
||||
if not use_ansible_docker_isolation():
|
||||
return inventory_path
|
||||
standard_dir = os.path.join(project_dir, 'inventory')
|
||||
standard_path = os.path.join(standard_dir, 'hosts')
|
||||
if os.path.realpath(inventory_path) == os.path.realpath(standard_path):
|
||||
return standard_path
|
||||
os.makedirs(standard_dir, mode=0o700, exist_ok=True)
|
||||
shutil.copy2(inventory_path, standard_path)
|
||||
return standard_path
|
||||
|
||||
|
||||
def ensure_ansible_docker_image():
|
||||
if not use_ansible_docker_isolation():
|
||||
return
|
||||
result = safe_run_cmd(['docker', 'image', 'inspect', ANSIBLE_EE_IMAGE])
|
||||
if not result or result.returncode != 0:
|
||||
raise AnsibleDockerImageNotFound(
|
||||
_('Ansible Docker image "%(image)s" not found. '
|
||||
'You can disable this option in System Settings - Feature Settings - Job Center - '
|
||||
'Ansible Docker isolation to run locally. '
|
||||
'Please run: docker pull %(image)s')
|
||||
% {'image': ANSIBLE_EE_IMAGE}
|
||||
)
|
||||
@@ -1,5 +1,9 @@
|
||||
__all__ = ['CommandInBlackListException']
|
||||
__all__ = ['CommandInBlackListException', 'AnsibleDockerImageNotFound']
|
||||
|
||||
|
||||
class CommandInBlackListException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class AnsibleDockerImageNotFound(Exception):
|
||||
pass
|
||||
|
||||
@@ -6,65 +6,28 @@ from django.conf import settings
|
||||
from django.utils._os import safe_join
|
||||
|
||||
from common.utils import is_macos
|
||||
|
||||
from ..utils import get_ansible_log_verbosity
|
||||
from .callback import DefaultCallback
|
||||
from .docker import (
|
||||
docker_extravars,
|
||||
docker_isolation_kwargs,
|
||||
ensure_ansible_docker_image,
|
||||
prepare_isolated_ansible_cfg,
|
||||
stage_inventory_for_docker,
|
||||
use_ansible_docker_isolation,
|
||||
)
|
||||
from .exception import CommandInBlackListException
|
||||
from .interface import interface
|
||||
from ..utils import get_ansible_log_verbosity
|
||||
|
||||
__all__ = ['AdHocRunner', 'PlaybookRunner', 'SuperPlaybookRunner', 'UploadFileRunner']
|
||||
|
||||
ANSIBLE_EE_IMAGE = 'jumpserver/ansible-executor:latest'
|
||||
ANSIBLE_EE_PYTHON_INTERPRETER = '/usr/bin/python3.11'
|
||||
|
||||
|
||||
def docker_extravars(extra_vars):
|
||||
extravars = dict(extra_vars or {})
|
||||
if use_ansible_docker_isolation():
|
||||
extravars.setdefault('local_python_interpreter', ANSIBLE_EE_PYTHON_INTERPRETER)
|
||||
return extravars
|
||||
|
||||
|
||||
def use_ansible_docker_isolation():
|
||||
return True
|
||||
|
||||
|
||||
def docker_isolation_kwargs():
|
||||
return {
|
||||
'process_isolation': True,
|
||||
'process_isolation_executable': 'docker',
|
||||
'container_image': ANSIBLE_EE_IMAGE,
|
||||
'container_options': ['--network=jms_net'],
|
||||
}
|
||||
|
||||
|
||||
def prepare_isolated_ansible_cfg(project_dir):
|
||||
"""Copy ansible.cfg into job dir so the EE container picks up SSH settings."""
|
||||
if not use_ansible_docker_isolation():
|
||||
return
|
||||
src = os.path.join(settings.APPS_DIR, 'libs', 'ansible', 'ansible.cfg')
|
||||
dst = os.path.join(project_dir, 'ansible.cfg')
|
||||
shutil.copyfile(src, dst)
|
||||
|
||||
|
||||
def stage_inventory_for_docker(project_dir, inventory_path):
|
||||
"""Stage custom inventory into private_data_dir/inventory/hosts for Docker EE."""
|
||||
if not use_ansible_docker_isolation():
|
||||
return inventory_path
|
||||
standard_dir = os.path.join(project_dir, 'inventory')
|
||||
standard_path = os.path.join(standard_dir, 'hosts')
|
||||
if os.path.realpath(inventory_path) == os.path.realpath(standard_path):
|
||||
return standard_path
|
||||
os.makedirs(standard_dir, mode=0o700, exist_ok=True)
|
||||
shutil.copy2(inventory_path, standard_path)
|
||||
return standard_path
|
||||
|
||||
|
||||
class AdHocRunner:
|
||||
cmd_modules_choices = ('shell', 'raw', 'command', 'script', 'win_shell')
|
||||
need_local_connection_modules_choices = ("mysql", "postgresql", "sqlserver", "huawei")
|
||||
|
||||
def __init__(self, inventory, job_module, module, module_args='', pattern='*', project_dir='/tmp/',
|
||||
extra_vars=None,
|
||||
def __init__(self, inventory, job_module, module, module_args='', pattern='*', project_dir='/tmp/', extra_vars=None,
|
||||
dry_run=False, timeout=-1):
|
||||
if extra_vars is None:
|
||||
extra_vars = {}
|
||||
@@ -100,7 +63,7 @@ class AdHocRunner:
|
||||
verbosity = get_ansible_log_verbosity(verbosity)
|
||||
|
||||
if not os.path.exists(self.project_dir):
|
||||
os.mkdir(self.project_dir, 0o755)
|
||||
os.makedirs(self.project_dir, 0o755, exist_ok=True)
|
||||
private_env = safe_join(self.project_dir, 'env')
|
||||
if os.path.exists(private_env):
|
||||
shutil.rmtree(private_env)
|
||||
@@ -123,7 +86,7 @@ class AdHocRunner:
|
||||
}
|
||||
if use_ansible_docker_isolation():
|
||||
run_kwargs.update(docker_isolation_kwargs())
|
||||
|
||||
ensure_ansible_docker_image()
|
||||
interface.run(**run_kwargs)
|
||||
return self.cb
|
||||
|
||||
@@ -172,6 +135,7 @@ class PlaybookRunner:
|
||||
kwargs = dict(kwargs)
|
||||
if use_ansible_docker_isolation():
|
||||
kwargs.update(docker_isolation_kwargs())
|
||||
ensure_ansible_docker_image()
|
||||
elif self.isolate and not is_macos():
|
||||
kwargs['process_isolation'] = True
|
||||
kwargs['process_isolation_executable'] = 'bwrap'
|
||||
@@ -223,7 +187,7 @@ class UploadFileRunner:
|
||||
|
||||
def run(self, verbosity=0, **kwargs):
|
||||
if not os.path.exists(self.project_dir):
|
||||
os.makedirs(self.project_dir, mode=0o755)
|
||||
os.makedirs(self.project_dir, mode=0o755, exist_ok=True)
|
||||
|
||||
prepare_isolated_ansible_cfg(self.project_dir)
|
||||
src_path = self.stage_upload_files()
|
||||
@@ -243,7 +207,7 @@ class UploadFileRunner:
|
||||
}
|
||||
if use_ansible_docker_isolation():
|
||||
run_kwargs.update(docker_isolation_kwargs())
|
||||
|
||||
ensure_ansible_docker_image()
|
||||
interface.run(**run_kwargs)
|
||||
try:
|
||||
shutil.rmtree(self.share_src_dir)
|
||||
|
||||
@@ -6,6 +6,7 @@ from rest_framework import serializers
|
||||
|
||||
from common.serializers.fields import EncryptedField
|
||||
from common.utils import date_expired_default
|
||||
from ops.ansible.docker import ANSIBLE_EE_IMAGE
|
||||
|
||||
__all__ = [
|
||||
'AnnouncementSettingSerializer', 'OpsSettingSerializer', 'VaultSettingSerializer',
|
||||
@@ -17,6 +18,14 @@ from settings.const import (
|
||||
ChatAITypeChoices, GPTModelChoices, DeepSeekModelChoices, ChatAIMethodChoices
|
||||
)
|
||||
|
||||
ANSIBLE_DOCKER_HELP_TEXT = _(
|
||||
'Run Ansible jobs in Docker execution environment (%(image)s). '
|
||||
'You can disable this option in System Settings - Feature Settings - Job Center - '
|
||||
'Ansible Docker isolation to run locally. '
|
||||
'If the image is missing, pull it on the ansible worker: '
|
||||
'docker pull %(image)s'
|
||||
) % {'image': ANSIBLE_EE_IMAGE}
|
||||
|
||||
|
||||
class AnnouncementSerializer(serializers.Serializer):
|
||||
ID = serializers.CharField(required=False, allow_blank=True, allow_null=True)
|
||||
@@ -204,6 +213,11 @@ class OpsSettingSerializer(serializers.Serializer):
|
||||
required=False, label=_('Adhoc command'),
|
||||
help_text=_('Allow users to execute batch commands in the Workbench - Job Center - Adhoc')
|
||||
)
|
||||
ANSIBLE_DOCKER_ENABLED = serializers.BooleanField(
|
||||
required=False,
|
||||
label=_('Ansible Docker isolation'),
|
||||
help_text=ANSIBLE_DOCKER_HELP_TEXT,
|
||||
)
|
||||
SECURITY_COMMAND_BLACKLIST = serializers.ListField(
|
||||
child=serializers.CharField(max_length=1024),
|
||||
label=_('Command blacklist'),
|
||||
|
||||
Reference in New Issue
Block a user