From 3028f3562f3c1346f61ef2c209efcbdbdf9d8524 Mon Sep 17 00:00:00 2001 From: wangruidong <940853815@qq.com> Date: Tue, 16 Jun 2026 16:34:40 +0800 Subject: [PATCH] perf: add ANSIBLE_DOCKER_ENABLED configuration --- apps/jumpserver/conf.py | 1 + apps/jumpserver/settings/custom.py | 1 + apps/ops/ansible/docker.py | 76 ++++++++++++++++++++++++++++ apps/ops/ansible/exception.py | 6 ++- apps/ops/ansible/runner.py | 68 ++++++------------------- apps/settings/serializers/feature.py | 14 +++++ 6 files changed, 113 insertions(+), 53 deletions(-) create mode 100644 apps/ops/ansible/docker.py diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index c66cd8f6d..0a0b01908 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -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' ], diff --git a/apps/jumpserver/settings/custom.py b/apps/jumpserver/settings/custom.py index b84b6bd9f..fdd32e48d 100644 --- a/apps/jumpserver/settings/custom.py +++ b/apps/jumpserver/settings/custom.py @@ -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 diff --git a/apps/ops/ansible/docker.py b/apps/ops/ansible/docker.py new file mode 100644 index 000000000..1e4a7d43a --- /dev/null +++ b/apps/ops/ansible/docker.py @@ -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} + ) diff --git a/apps/ops/ansible/exception.py b/apps/ops/ansible/exception.py index dc5926f05..64cc34be1 100644 --- a/apps/ops/ansible/exception.py +++ b/apps/ops/ansible/exception.py @@ -1,5 +1,9 @@ -__all__ = ['CommandInBlackListException'] +__all__ = ['CommandInBlackListException', 'AnsibleDockerImageNotFound'] class CommandInBlackListException(Exception): pass + + +class AnsibleDockerImageNotFound(Exception): + pass diff --git a/apps/ops/ansible/runner.py b/apps/ops/ansible/runner.py index 4c037f31f..3d562fb04 100644 --- a/apps/ops/ansible/runner.py +++ b/apps/ops/ansible/runner.py @@ -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) diff --git a/apps/settings/serializers/feature.py b/apps/settings/serializers/feature.py index f1253af17..4a4080cf9 100644 --- a/apps/settings/serializers/feature.py +++ b/apps/settings/serializers/feature.py @@ -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'),