perf: ansible runner in isolated mode (#13434)

perf: use new ansible runner

perf: change lock

Co-authored-by: ibuler <ibuler@qq.com>
pull/13474/head
fit2bot 2024-06-24 10:21:31 +08:00 committed by GitHub
parent 9be77cf58f
commit 165d030c8e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 92 additions and 68 deletions

View File

@ -94,6 +94,7 @@ ARG TOOLS=" \
sshpass \ sshpass \
telnet \ telnet \
vim \ vim \
bubblewrap \
wget" wget"
ARG APT_MIRROR=http://mirrors.ustc.edu.cn ARG APT_MIRROR=http://mirrors.ustc.edu.cn

View File

@ -2,16 +2,18 @@ import os
from collections import defaultdict from collections import defaultdict
from functools import reduce from functools import reduce
from django.conf import settings
class DefaultCallback: class DefaultCallback:
STATUS_MAPPER = { STATUS_MAPPER = {
'successful': 'success', "successful": "success",
'failure': 'failed', "failure": "failed",
'failed': 'failed', "failed": "failed",
'running': 'running', "running": "running",
'pending': 'pending', "pending": "pending",
'timeout': 'timeout', "timeout": "timeout",
'unknown': 'unknown' "unknown": "unknown",
} }
def __init__(self): def __init__(self):
@ -28,7 +30,7 @@ class DefaultCallback:
dark={}, dark={},
skipped=[], skipped=[],
) )
self.status = 'running' self.status = "running"
self.finished = False self.finished = False
self.local_pid = 0 self.local_pid = 0
self.private_data_dir = None self.private_data_dir = None
@ -42,61 +44,63 @@ class DefaultCallback:
return results return results
def is_success(self): def is_success(self):
return self.status != 'success' return self.status != "success"
def event_handler(self, data, **kwargs): def event_handler(self, data, **kwargs):
event = data.get('event', None) event = data.get("event", None)
if not event: if not event:
return return
pid = data.get('pid', None) pid = data.get("pid", None)
if pid: if pid:
self.write_pid(pid) self.write_pid(pid)
event_data = data.get('event_data', {}) event_data = data.get("event_data", {})
host = event_data.get('remote_addr', '') host = event_data.get("remote_addr", "")
task = event_data.get('task', '') task = event_data.get("task", "")
res = event_data.get('res', {}) res = event_data.get("res", {})
handler = getattr(self, event, self.on_any) handler = getattr(self, event, self.on_any)
handler(event_data, host=host, task=task, res=res) handler(event_data, host=host, task=task, res=res)
def runner_on_ok(self, event_data, host=None, task=None, res=None): def runner_on_ok(self, event_data, host=None, task=None, res=None):
detail = { detail = {
'action': event_data.get('task_action', ''), "action": event_data.get("task_action", ""),
'res': res, "res": res,
'rc': res.get('rc', 0), "rc": res.get("rc", 0),
'stdout': res.get('stdout', ''), "stdout": res.get("stdout", ""),
} }
self.result['ok'][host][task] = detail self.result["ok"][host][task] = detail
def runner_on_skipped(self, event_data, host=None, task=None, **kwargs): def runner_on_skipped(self, event_data, host=None, task=None, **kwargs):
detail = { detail = {
'action': event_data.get('task_action', ''), "action": event_data.get("task_action", ""),
'res': {}, "res": {},
'rc': 0, "rc": 0,
} }
self.result['skipped'][host][task] = detail self.result["skipped"][host][task] = detail
def runner_on_failed(self, event_data, host=None, task=None, res=None, **kwargs): def runner_on_failed(self, event_data, host=None, task=None, res=None, **kwargs):
detail = { detail = {
'action': event_data.get('task_action', ''), "action": event_data.get("task_action", ""),
'res': res, "res": res,
'rc': res.get('rc', 0), "rc": res.get("rc", 0),
'stdout': res.get('stdout', ''), "stdout": res.get("stdout", ""),
'stderr': ';'.join([res.get('stderr', ''), res.get('msg', '')]).strip(';') "stderr": ";".join([res.get("stderr", ""), res.get("msg", "")]).strip(";"),
} }
ignore_errors = event_data.get('ignore_errors', False) ignore_errors = event_data.get("ignore_errors", False)
error_key = 'ignored' if ignore_errors else 'failures' error_key = "ignored" if ignore_errors else "failures"
self.result[error_key][host][task] = detail self.result[error_key][host][task] = detail
def runner_on_unreachable(self, event_data, host=None, task=None, res=None, **kwargs): def runner_on_unreachable(
self, event_data, host=None, task=None, res=None, **kwargs
):
detail = { detail = {
'action': event_data.get('task_action', ''), "action": event_data.get("task_action", ""),
'res': res, "res": res,
'rc': 255, "rc": 255,
'stderr': ';'.join([res.get('stderr', ''), res.get('msg', '')]).strip(';') "stderr": ";".join([res.get("stderr", ""), res.get("msg", "")]).strip(";"),
} }
self.result['dark'][host][task] = detail self.result["dark"][host][task] = detail
def runner_on_start(self, event_data, **kwargs): def runner_on_start(self, event_data, **kwargs):
pass pass
@ -117,21 +121,26 @@ class DefaultCallback:
pass pass
def playbook_on_stats(self, event_data, **kwargs): def playbook_on_stats(self, event_data, **kwargs):
error_func = lambda err, task_detail: err + f"{task_detail[0]}: {task_detail[1]['stderr']};" error_func = (
for tp in ['dark', 'failures']: lambda err, task_detail: err
+ f"{task_detail[0]}: {task_detail[1]['stderr']};"
)
for tp in ["dark", "failures"]:
for host, tasks in self.result[tp].items(): for host, tasks in self.result[tp].items():
error = reduce(error_func, tasks.items(), '').strip(';') error = reduce(error_func, tasks.items(), "").strip(";")
self.summary[tp][host] = error self.summary[tp][host] = error
failures = list(self.result['failures'].keys()) failures = list(self.result["failures"].keys())
dark_or_failures = list(self.result['dark'].keys()) + failures dark_or_failures = list(self.result["dark"].keys()) + failures
for host, tasks in self.result.get('ignored', {}).items(): for host, tasks in self.result.get("ignored", {}).items():
ignore_errors = reduce(error_func, tasks.items(), '').strip(';') ignore_errors = reduce(error_func, tasks.items(), "").strip(";")
if host in failures: if host in failures:
self.summary['failures'][host] += ignore_errors self.summary["failures"][host] += ignore_errors
self.summary['ok'] = list(set(self.result['ok'].keys()) - set(dark_or_failures)) self.summary["ok"] = list(set(self.result["ok"].keys()) - set(dark_or_failures))
self.summary['skipped'] = list(set(self.result['skipped'].keys()) - set(dark_or_failures)) self.summary["skipped"] = list(
set(self.result["skipped"].keys()) - set(dark_or_failures)
)
def playbook_on_include(self, event_data, **kwargs): def playbook_on_include(self, event_data, **kwargs):
pass pass
@ -151,6 +160,13 @@ class DefaultCallback:
def playbook_on_no_hosts_remaining(self, event_data, **kwargs): def playbook_on_no_hosts_remaining(self, event_data, **kwargs):
pass pass
def playbook_on_start(self, event_data, **kwargs):
if settings.DEBUG_DEV:
print("DEBUG: delete inventory: ", os.path.join(self.private_data_dir, 'inventory'))
inventory_path = os.path.join(self.private_data_dir, 'inventory', 'hosts')
if os.path.exists(inventory_path):
os.remove(inventory_path)
def warning(self, event_data, **kwargs): def warning(self, event_data, **kwargs):
pass pass
@ -158,11 +174,11 @@ class DefaultCallback:
pass pass
def status_handler(self, data, **kwargs): def status_handler(self, data, **kwargs):
status = data.get('status', '') status = data.get("status", "")
self.status = self.STATUS_MAPPER.get(status, 'unknown') self.status = self.STATUS_MAPPER.get(status, "unknown")
self.private_data_dir = data.get("private_data_dir", None) self.private_data_dir = data.get("private_data_dir", None)
def write_pid(self, pid): def write_pid(self, pid):
pid_filepath = os.path.join(self.private_data_dir, 'local.pid') pid_filepath = os.path.join(self.private_data_dir, "local.pid")
with open(pid_filepath, 'w') as f: with open(pid_filepath, "w") as f:
f.write(str(pid)) f.write(str(pid))

View File

@ -4,9 +4,10 @@ import uuid
from django.conf import settings from django.conf import settings
from django.utils._os import safe_join from django.utils._os import safe_join
from .interface import interface
from .callback import DefaultCallback from .callback import DefaultCallback
from .exception import CommandInBlackListException from .exception import CommandInBlackListException
from .interface import interface
from ..utils import get_ansible_log_verbosity from ..utils import get_ansible_log_verbosity
__all__ = ['AdHocRunner', 'PlaybookRunner', 'SuperPlaybookRunner', 'UploadFileRunner'] __all__ = ['AdHocRunner', 'PlaybookRunner', 'SuperPlaybookRunner', 'UploadFileRunner']
@ -44,7 +45,7 @@ class AdHocRunner:
def set_local_connection(self): def set_local_connection(self):
if self.job_module in self.need_local_connection_modules_choices: if self.job_module in self.need_local_connection_modules_choices:
self.envs.update({"LOCAL_CONNECTION_ENABLED": "1"}) self.envs.update({"ANSIBLE_SUPER_MODE": "1"})
def run(self, verbosity=0, **kwargs): def run(self, verbosity=0, **kwargs):
self.check_module() self.check_module()
@ -84,6 +85,7 @@ class PlaybookRunner:
if not callback: if not callback:
callback = DefaultCallback() callback = DefaultCallback()
self.cb = callback self.cb = callback
self.isolate = True
self.envs = {} self.envs = {}
def copy_playbook(self): def copy_playbook(self):
@ -101,6 +103,11 @@ class PlaybookRunner:
if os.path.exists(private_env): if os.path.exists(private_env):
shutil.rmtree(private_env) shutil.rmtree(private_env)
kwargs = dict(kwargs)
if self.isolate:
kwargs['process_isolation'] = True
kwargs['process_isolation_executable'] = 'bwrap'
interface.run( interface.run(
private_data_dir=self.project_dir, private_data_dir=self.project_dir,
inventory=self.inventory, inventory=self.inventory,
@ -118,7 +125,8 @@ class PlaybookRunner:
class SuperPlaybookRunner(PlaybookRunner): class SuperPlaybookRunner(PlaybookRunner):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.envs = {"LOCAL_CONNECTION_ENABLED": "1"} self.envs = {"ANSIBLE_SUPER_MODE": "1"}
self.isolate = False
class UploadFileRunner: class UploadFileRunner:

19
poetry.lock generated
View File

@ -480,7 +480,7 @@ description = "Radically simple IT automation"
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.9"
files = [ files = [
{file = "v2.14.1.4.zip", hash = "sha256:1805e06391223ac6198229a18f501f0e001e66c3b334cb7c5061e0ac810297d6"}, {file = "v2.14.1.5.zip", hash = "sha256:3f05961f2902bc5228ebd44875aadc18cff02f3190791f214a40b2adb07735e1"},
] ]
[package.dependencies] [package.dependencies]
@ -492,17 +492,16 @@ resolvelib = ">=0.5.3,<0.9.0"
[package.source] [package.source]
type = "url" type = "url"
url = "https://github.com/jumpserver/ansible/archive/refs/tags/v2.14.1.4.zip" url = "https://github.com/jumpserver/ansible/archive/refs/tags/v2.14.1.5.zip"
[[package]] [[package]]
name = "ansible-runner" name = "ansible-runner"
version = "2.3.3" version = "2.4.0.2"
description = "\"Consistent Ansible Python API and CLI with container and process isolation runtime capabilities\"" description = "\"Consistent Ansible Python API and CLI with container and process isolation runtime capabilities\""
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.9"
files = [ files = [
{file = "ansible-runner-2.3.3.tar.gz", hash = "sha256:38ff635e4b94791de2956c81e265836ec4965b30e9ee35d72fcf3271dc46b98b"}, {file = "2.4.0.2.zip", hash = "sha256:e8c995ab977a8cb354b64ac84c238699dbfcd28f7157bd169572a05d2edffe6c"},
{file = "ansible_runner-2.3.3-py3-none-any.whl", hash = "sha256:c57ae0d096760d66b2897b0f9009856c7b83fd5428dcb831f470cba348346396"},
] ]
[package.dependencies] [package.dependencies]
@ -510,12 +509,10 @@ packaging = "*"
pexpect = ">=4.5" pexpect = ">=4.5"
python-daemon = "*" python-daemon = "*"
pyyaml = "*" pyyaml = "*"
six = "*"
[package.source] [package.source]
type = "legacy" type = "url"
url = "https://pypi.tuna.tsinghua.edu.cn/simple" url = "https://github.com/jumpserver-dev/ansible-runner/archive/refs/tags/2.4.0.2.zip"
reference = "tsinghua"
[[package]] [[package]]
name = "anyio" name = "anyio"
@ -2877,6 +2874,7 @@ googleapis-common-protos = ">=1.56.2,<2.0.dev0"
grpcio = {version = ">=1.49.1,<2.0dev", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""} grpcio = {version = ">=1.49.1,<2.0dev", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}
grpcio-status = {version = ">=1.49.1,<2.0.dev0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""} grpcio-status = {version = ">=1.49.1,<2.0.dev0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}
proto-plus = ">=1.22.3,<2.0.0dev" proto-plus = ">=1.22.3,<2.0.0dev"
protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<5.0.0.dev0" protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<5.0.0.dev0"
requests = ">=2.18.0,<3.0.0.dev0" requests = ">=2.18.0,<3.0.0.dev0"
@ -8023,3 +8021,4 @@ reference = "tsinghua"
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.11" python-versions = "^3.11"
content-hash = "de1b3ef35e2119f1ea8a2a2aee7a9586d055ed04837bb9f6f84083aa24c85b85" content-hash = "de1b3ef35e2119f1ea8a2a2aee7a9586d055ed04837bb9f6f84083aa24c85b85"

View File

@ -11,9 +11,9 @@ python = "^3.11"
cython = "3.0.0" cython = "3.0.0"
aiofiles = "23.1.0" aiofiles = "23.1.0"
amqp = "5.1.1" amqp = "5.1.1"
ansible-core = { url = "https://github.com/jumpserver/ansible/archive/refs/tags/v2.14.1.4.zip" } ansible-core = { url = "https://github.com/jumpserver-dev/ansible/archive/refs/tags/v2.14.1.6.zip" }
ansible = "7.1.0" ansible = "7.1.0"
ansible-runner = "2.3.3" ansible-runner = { url = "https://github.com/jumpserver-dev/ansible-runner/archive/refs/tags/2.4.0.1.zip" }
asn1crypto = "1.5.1" asn1crypto = "1.5.1"
bcrypt = "4.0.1" bcrypt = "4.0.1"
billiard = "4.1.0" billiard = "4.1.0"

View File

@ -3,10 +3,10 @@
import argparse import argparse
import logging import logging
import shutil
import subprocess
import os import os
import shutil
import signal import signal
import subprocess
import tempfile import tempfile
from apps.libs.process.ssh import kill_ansible_ssh_process from apps.libs.process.ssh import kill_ansible_ssh_process