jumpserver/apps/assets/automations/base/manager.py

526 lines
18 KiB
Python
Raw Normal View History

2023-10-25 02:03:33 +00:00
import hashlib
import json
2022-10-09 12:54:11 +00:00
import os
2022-10-28 10:28:41 +00:00
import shutil
2024-11-18 11:06:04 +00:00
import time
from collections import defaultdict
2022-10-28 10:28:41 +00:00
from socket import gethostname
2022-10-09 12:54:11 +00:00
import yaml
2022-10-09 12:54:11 +00:00
from django.conf import settings
2024-11-18 11:06:04 +00:00
from django.template.loader import render_to_string
2022-10-09 12:54:11 +00:00
from django.utils import timezone
2022-10-12 10:08:57 +00:00
from django.utils.translation import gettext as _
2024-11-18 11:06:04 +00:00
from premailer import transform
from sshtunnel import SSHTunnelForwarder
2022-10-09 12:54:11 +00:00
2022-10-12 10:08:57 +00:00
from assets.automations.methods import platform_automation_methods
2024-11-18 11:06:04 +00:00
from common.db.utils import safe_db_connection
from common.tasks import send_mail_async
from common.utils import get_logger, lazyproperty, is_openssh_format_key, ssh_pubkey_gen
from ops.ansible import JMSInventory, DefaultCallback, SuperPlaybookRunner
from ops.ansible.interface import interface
2022-10-12 10:08:57 +00:00
logger = get_logger(__name__)
class SSHTunnelManager:
def __init__(self, *args, **kwargs):
self.gateway_servers = dict()
@staticmethod
def file_to_json(path):
2024-11-20 11:12:28 +00:00
with open(path, "r") as f:
d = json.load(f)
return d
@staticmethod
def json_to_file(path, data):
2024-11-20 11:12:28 +00:00
with open(path, "w") as f:
json.dump(data, f, indent=4, sort_keys=True)
def local_gateway_prepare(self, runner):
info = self.file_to_json(runner.inventory)
servers, not_valid = [], []
2024-11-20 11:12:28 +00:00
for k, host in info["all"]["hosts"].items():
jms_asset, jms_gateway = host.get("jms_asset"), host.get("jms_gateway")
if not jms_gateway:
continue
try:
server = SSHTunnelForwarder(
2024-11-20 11:12:28 +00:00
(jms_gateway["address"], jms_gateway["port"]),
ssh_username=jms_gateway["username"],
ssh_password=jms_gateway["secret"],
ssh_pkey=jms_gateway["private_key_path"],
remote_bind_address=(jms_asset["address"], jms_asset["port"]),
)
server.start()
except Exception as e:
2024-11-20 11:12:28 +00:00
err_msg = "Gateway is not active: %s" % jms_asset.get("name", "")
print(f"\033[31m {err_msg} 原因: {e} \033[0m\n")
not_valid.append(k)
else:
local_bind_port = server.local_bind_port
2024-11-20 11:12:28 +00:00
host["ansible_host"] = jms_asset["address"] = host["login_host"] = (
interface.get_gateway_proxy_host()
)
host["ansible_port"] = jms_asset["port"] = host["login_port"] = (
local_bind_port
)
servers.append(server)
# 网域不可连接的,就不继续执行此资源的后续任务了
for a in set(not_valid):
2024-11-20 11:12:28 +00:00
info["all"]["hosts"].pop(a)
self.json_to_file(runner.inventory, info)
self.gateway_servers[runner.id] = servers
def local_gateway_clean(self, runner):
servers = self.gateway_servers.get(runner.id, [])
for s in servers:
try:
s.stop()
except Exception:
pass
2022-10-12 10:08:57 +00:00
class PlaybookCallback(DefaultCallback):
def playbook_on_stats(self, event_data, **kwargs):
super().playbook_on_stats(event_data, **kwargs)
2022-10-09 12:54:11 +00:00
2024-11-18 11:06:04 +00:00
class BaseManager:
def __init__(self, execution):
self.execution = execution
self.time_start = time.time()
self.summary = defaultdict(int)
self.result = defaultdict(list)
self.duration = 0
def get_assets_group_by_platform(self):
return self.execution.all_assets_group_by_platform()
2024-11-19 10:05:59 +00:00
def pre_run(self):
2024-11-18 11:06:04 +00:00
self.execution.date_start = timezone.now()
2024-11-20 11:12:28 +00:00
self.execution.save(update_fields=["date_start"])
2024-11-18 11:06:04 +00:00
def update_execution(self):
self.duration = int(time.time() - self.time_start)
self.execution.date_finished = timezone.now()
self.execution.duration = self.duration
self.execution.summary = self.summary
self.execution.result = self.result
2024-11-20 11:12:28 +00:00
self.execution.status = "success"
2024-11-18 11:06:04 +00:00
with safe_db_connection():
2024-11-19 10:05:59 +00:00
self.execution.save()
2024-11-18 11:06:04 +00:00
def print_summary(self):
2024-11-22 06:30:45 +00:00
content = "\nSummery: \n"
for k, v in self.summary.items():
content += f"\t - {k}: {v}\n"
content += "\t - Using: {}s\n".format(self.duration)
print(content)
2024-11-18 11:06:04 +00:00
2024-11-19 10:05:59 +00:00
def get_report_template(self):
2024-11-18 11:06:04 +00:00
raise NotImplementedError
2024-11-19 10:05:59 +00:00
def get_report_subject(self):
2024-11-20 11:12:28 +00:00
return f"Automation {self.execution.id} finished"
2024-11-19 10:05:59 +00:00
def get_report_context(self):
return {
2024-11-20 11:12:28 +00:00
"execution": self.execution,
"summary": self.execution.summary,
"result": self.execution.result,
2024-11-18 11:06:04 +00:00
}
2024-11-19 10:05:59 +00:00
def send_report_if_need(self):
recipients = self.execution.recipients
if not recipients:
return
2024-11-22 06:30:45 +00:00
print("Send report to: ", ",".join(recipients))
2024-11-19 10:05:59 +00:00
report = self.gen_report()
report = transform(report)
subject = self.get_report_subject()
emails = [r.email for r in recipients if r.email]
send_mail_async(subject, report, emails, html_message=report)
def gen_report(self):
template_path = self.get_report_template()
context = self.get_report_context()
2024-11-18 11:06:04 +00:00
data = render_to_string(template_path, context)
return data
2024-11-19 10:05:59 +00:00
def post_run(self):
2024-11-18 11:06:04 +00:00
self.update_execution()
self.print_summary()
self.send_report_if_need()
def run(self, *args, **kwargs):
2024-11-19 10:05:59 +00:00
self.pre_run()
2024-11-18 11:06:04 +00:00
self.do_run(*args, **kwargs)
2024-11-19 10:05:59 +00:00
self.post_run()
2024-11-18 11:06:04 +00:00
def do_run(self, *args, **kwargs):
raise NotImplementedError
@staticmethod
def json_dumps(data):
return json.dumps(data, indent=4, sort_keys=True)
2024-11-22 06:30:45 +00:00
class PlaybookPrepareMixin:
2024-11-20 11:12:28 +00:00
bulk_size = 100
ansible_account_policy = "privileged_first"
ansible_account_prefer = "root,Administrator"
2024-11-22 06:30:45 +00:00
summary: dict
result: dict
params: dict
execution = None
2024-11-20 11:12:28 +00:00
2024-11-22 06:30:45 +00:00
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
2024-11-20 11:12:28 +00:00
# example: {'gather_fact_windows': {'id': 'gather_fact_windows', 'name': '', 'method': 'gather_fact', ...} }
2022-10-12 10:08:57 +00:00
self.method_id_meta_mapper = {
2024-11-20 11:12:28 +00:00
method["id"]: method
v3.0.0-rc1 (#9322) * perf:automation * pref: 修改账号推送 * perf: 修改 assets * perf: 修改 accounts * feat: 优化代码 * fix: 修复 ObjectRelatedField 获取 value attr 时先判断是否有 attr 属性 * perf: 增加翻译 * feat: 增加部分翻译 * feat: 去除无用列 * perf: ticket remove app * fix: 修复创建账号备份任务失败的问题 * perf: 添加 accounts app * perf: ticket type serializer (#9252) Co-authored-by: feng <1304903146@qq.com> * perf: ticket * perf: 修改 accounts api * perf: 优化 AssetPermissionSerializer fields 顺序 * perf: 修改 accounts * feat: 限制常用用户名api返回长度 * feat: 限制常用用户名api返回长度 * perf: 修改 LoginAssetACL 序列类,增加 users_username_group, accounts_username_group... 字段 * perf: 修改 CommandFilterACLSerializer 增加 command_groups_amount 字段 * perf: 修改rbac API啥的 (#9254) * perf: migrate * perf: 修改 AssetPermedSerializer domain 字段类型 * perf: 放开push account 权限位 * perf: 修改 accounts * perf: 修改 LoginACLSerializer 字段类型 * pref: 修改数据库 migrations * perf: filter asset systemuser * perf: 修改 SessionSerializer 字段类型 * pref: 修改 applet host * perf: 修改 SessionCommandSerializer 字段类型 * perf: 修改 accounts import * perf: 修改 celery datetime * perf: 修改 asset serializer * pref: 修改 labeled field * feat: 修改翻译 * perf: 修改 JobSerializer 字段类型 * feat: 支持使用 ws 发送终断任务 * perf: add AccessTokenAuthentication * perf: 修改 BaseStorageSerializer 字段类型 * perf: 修改 AppletHostSerializer 字段类型 * perf: signal event * perf: asset types automations (#9259) Co-authored-by: feng <1304903146@qq.com> * perf: 修改下载 rdp 文件时返回的 address 地址信息为空的问题 * perf: 修改 AssetSerializer.accounts.secret 为 write_only; 修改 DomainWithGatewaySerializer.gateways 返回 account 信息及 secret 字段; * perf: automation 干库 (#9260) Co-authored-by: feng <1304903146@qq.com> * perf: account push api * feat: 修改迁移文件 * feat: 删除无用代码 * feat: 优化部分资源无操作日志 * perf: 修改 account * perf: perm tree * perf: asset serializers retrieve * perf: 格式化代码 * perf: AutomationExecution (#9268) Co-authored-by: feng <1304903146@qq.com> * perf: AssetDetailSerializer 和 Asset Model 添加 specific_info 字段; * perf: 修改账号推送 * feat: handle ws heartbeat status * perf: k8s tree (#9269) Co-authored-by: feng <1304903146@qq.com> * perf: 修改账号推送 * perf: 修改 asset detail serializer * fix: 修复 windows 不能运行 powershell 命令的问题 * feat: 支持按照资源时间线查看操作活动 * feat: 翻译 * feat: 优化操作日志 * perf: asset clone * fix: 错误的修改改回去 * perf: create asset account * feat: 增加task 刷新续传功能 * fix: applet host deloypment filter host * perf: 修改了 common 结构,和 push accounts * perf: 整理 common 结构 * perf: 修改 const import * perf: 修改 allow bulk destroy * fix: applet host search fileds * perf: applet bulk delete * fix: applet list 404 * perf: 修改 common view * feat: 增加一些翻译, 修复 playbook 上传的错误 * fix: 修改错别字 * perf: 修改 applets status * perf: 修改网关 api * perf: automateion (#9281) Co-authored-by: feng <1304903146@qq.com> Co-authored-by: feng626 <57284900+feng626@users.noreply.github.com> * perf: 失效 connect methods 当 applet 删除 或者 host 删除 * perf: 网关账号的密码类型改成 LabelField * perf: chrome applet script * perf: verify code ttl (#9282) Co-authored-by: feng <1304903146@qq.com> * perf: database ping * perf: ws * perf: 修改网关创建 * perf: account task org (#9285) Co-authored-by: feng <1304903146@qq.com> * perf: asset test api * perf: port 添加 account * pref: 修改 db mapper permission * fix: db port mapper list api * perf: account change secret (#9286) Co-authored-by: feng <1304903146@qq.com> * perf: 修改 setup_eager_loading * perf: SecretStrategy * feat: 修改 ConnectionToken Create API 支持校验 ACL 逻辑 * feat: 修改 ConnectionToken Create API 支持校验 ACL 逻辑 * feat: 修改 ConnectionToken Create API 支持校验 ACL 逻辑 * pref: web database 信号转发 * perf: account push automation * perf: push filter account * perf: 修改 publish 版本 * perf: 修改网关 * fix: 修改资产 Specific 信息中 JSONField 字段返回 json.loads 对象 * feat: 远程应用内置Navicat Premium 16 * feat: 更新下载链接 * feat: 整理代码格式 * perf: 修改 terminal point * perf: update chrome applet script * fix: 资产 specific 获取 JSONField 时, 判断值的类型不为 list, dict * perf: domain (#9292) Co-authored-by: feng <1304903146@qq.com> * perf: 优化 endpoint 监听端口,仅 oracle 动态 * perf: 修改翻译 * perf: 修改文案 * perf: 修改缺失的翻译 * perf: 修改 endpoint help text * feat: 还原格式 * feat: 去掉基类 * feat: 增加特权账号字段 * perf: decode content * fix: check pid * perf: 修改 smart endpoint * perf: 修改 endpoint mysql default port * feat: 优化 * perf: 修改 endpoint mysql default port * perf: gateway test (#9295) Co-authored-by: feng <1304903146@qq.com> * perf: migrate * perf: 修改 endpoint mysql default port * fix: 修复获取任务执行结果死循环 * feat: 作业审计日志增加字段 * fix: add on_transaction_commit task post save * perf: gateway (#9297) Co-authored-by: feng <1304903146@qq.com> * feat: 过滤 jumpserver 自动产生的用户 * fix: 修复ops节点选择的问题 * fix: 修改 统一 connection-token 和 command 的 review API 返回数据 from_ticket_info * perf: change secret (#9298) Co-authored-by: feng <1304903146@qq.com> * perf: 修改 db port manager * perf: 修改 db port manager * perf: add celery log mark * perf: remove debug log data * fix: navicat use manual type * fix: remove navicate download url * perf: push_account_enabled (#9301) Co-authored-by: feng <1304903146@qq.com> * fix: 修改navicat启动程序MD5值 * perf: push account (#9303) Co-authored-by: feng <1304903146@qq.com> * feat: Redis/MongoDB 支持SSL * fix: 修改授权规则过滤字段 node_name,node_id; 修复获取授权节点下的资产为空的问题; * perf: push account button (#9305) Co-authored-by: feng <1304903146@qq.com> * perf: account push * fix: 修复获取 /user//assets/tree/ 返回用户授权的所有资产 * perf: asset ping (#9307) Co-authored-by: feng <1304903146@qq.com> * perf: asset enabled_info * perf: 优化activity记录都保存至operatelog中 * feat: 远程应用navicat支持试用版连接 * perf: 优化迁移文件 * perf: 修改资产列表 API category type 字段 choices 根据 category 进行返回 * fix * perf: 修改账号列表 API 解决根据 node_id asset_id 搜索账号列表无效的问题 * fix: navicat dba账号登录 * perf: 优化navicat连接 * perf: 修改账号列表 Model Manager 继承自 OrgManager,解决组织过滤问题 * perf: 修改账号列表 Filter 支持根据 platform,category,type 字段搜索 * perf: change secret email (#9312) Co-authored-by: feng <1304903146@qq.com> * feat: 保证认证信息一定清理 * perf: add mariadb * perf: 修改资产类型树数量统计资产或账号 * perf: applet chrome quit * perf: 优化关闭欢迎页面 * fix * perf: executed amount * perf: 修改 built-in applet installation * perf: 修改资产列表增加标签搜索 * perf: 修改资产列表增加标签搜索 * perf: account task automation (#9319) Co-authored-by: feng <1304903146@qq.com> * perf: account trigger * perf: 修改系统设置文案:批量命令执行 -> 作业中心 * perf: 优化migrate (#9320) Co-authored-by: feng <1304903146@qq.com> * perf: 修改资产节点树 API,支持搜索资产、节点 * perf: audit dashboard (#9321) Co-authored-by: feng <1304903146@qq.com> * fix: 修改 has_perm 权限判断兼容 list 和 str 类型 * perf: 修改一些换行 * perf: 修改 ansible config * fix: oracle依赖文件地址错误 (#9324) * perf: ansible mudules * perf: 修改 runner host cwd Co-authored-by: ibuler <ibuler@qq.com> Co-authored-by: Aaron3S <chenyang@fit2cloud.com> Co-authored-by: Bai <baijiangjie@gmail.com> Co-authored-by: feng <1304903146@qq.com> Co-authored-by: feng626 <57284900+feng626@users.noreply.github.com> Co-authored-by: Eric <xplzv@126.com> Co-authored-by: jiangweidong <weidong.jiang@fit2cloud.com> Co-authored-by: jiangweidong <80373698+Hi-JWD@users.noreply.github.com>
2023-01-16 11:02:09 +00:00
for method in self.platform_automation_methods
2024-11-20 11:12:28 +00:00
if method["method"] == self.__class__.method_type()
2022-10-12 10:08:57 +00:00
}
# 根据执行方式就行分组, 不同资产的改密、推送等操作可能会使用不同的执行方式
# 然后根据执行方式分组, 再根据 bulk_size 分组, 生成不同的 playbook
self.playbooks = []
2024-11-22 06:30:45 +00:00
@classmethod
def method_type(cls):
raise NotImplementedError
def get_params(self, automation, method_type):
2024-11-20 11:12:28 +00:00
method_attr = "{}_method".format(method_type)
method_params = "{}_params".format(method_type)
method_id = getattr(automation, method_attr)
automation_params = getattr(automation, method_params)
2024-11-20 11:12:28 +00:00
serializer = self.method_id_meta_mapper[method_id]["params_serializer"]
if serializer is None:
return {}
data = self.params.get(method_id)
if not data:
2023-07-05 06:28:26 +00:00
data = automation_params.get(method_id, {})
params = serializer(data).data
return params
2022-10-12 10:08:57 +00:00
v3.0.0-rc1 (#9322) * perf:automation * pref: 修改账号推送 * perf: 修改 assets * perf: 修改 accounts * feat: 优化代码 * fix: 修复 ObjectRelatedField 获取 value attr 时先判断是否有 attr 属性 * perf: 增加翻译 * feat: 增加部分翻译 * feat: 去除无用列 * perf: ticket remove app * fix: 修复创建账号备份任务失败的问题 * perf: 添加 accounts app * perf: ticket type serializer (#9252) Co-authored-by: feng <1304903146@qq.com> * perf: ticket * perf: 修改 accounts api * perf: 优化 AssetPermissionSerializer fields 顺序 * perf: 修改 accounts * feat: 限制常用用户名api返回长度 * feat: 限制常用用户名api返回长度 * perf: 修改 LoginAssetACL 序列类,增加 users_username_group, accounts_username_group... 字段 * perf: 修改 CommandFilterACLSerializer 增加 command_groups_amount 字段 * perf: 修改rbac API啥的 (#9254) * perf: migrate * perf: 修改 AssetPermedSerializer domain 字段类型 * perf: 放开push account 权限位 * perf: 修改 accounts * perf: 修改 LoginACLSerializer 字段类型 * pref: 修改数据库 migrations * perf: filter asset systemuser * perf: 修改 SessionSerializer 字段类型 * pref: 修改 applet host * perf: 修改 SessionCommandSerializer 字段类型 * perf: 修改 accounts import * perf: 修改 celery datetime * perf: 修改 asset serializer * pref: 修改 labeled field * feat: 修改翻译 * perf: 修改 JobSerializer 字段类型 * feat: 支持使用 ws 发送终断任务 * perf: add AccessTokenAuthentication * perf: 修改 BaseStorageSerializer 字段类型 * perf: 修改 AppletHostSerializer 字段类型 * perf: signal event * perf: asset types automations (#9259) Co-authored-by: feng <1304903146@qq.com> * perf: 修改下载 rdp 文件时返回的 address 地址信息为空的问题 * perf: 修改 AssetSerializer.accounts.secret 为 write_only; 修改 DomainWithGatewaySerializer.gateways 返回 account 信息及 secret 字段; * perf: automation 干库 (#9260) Co-authored-by: feng <1304903146@qq.com> * perf: account push api * feat: 修改迁移文件 * feat: 删除无用代码 * feat: 优化部分资源无操作日志 * perf: 修改 account * perf: perm tree * perf: asset serializers retrieve * perf: 格式化代码 * perf: AutomationExecution (#9268) Co-authored-by: feng <1304903146@qq.com> * perf: AssetDetailSerializer 和 Asset Model 添加 specific_info 字段; * perf: 修改账号推送 * feat: handle ws heartbeat status * perf: k8s tree (#9269) Co-authored-by: feng <1304903146@qq.com> * perf: 修改账号推送 * perf: 修改 asset detail serializer * fix: 修复 windows 不能运行 powershell 命令的问题 * feat: 支持按照资源时间线查看操作活动 * feat: 翻译 * feat: 优化操作日志 * perf: asset clone * fix: 错误的修改改回去 * perf: create asset account * feat: 增加task 刷新续传功能 * fix: applet host deloypment filter host * perf: 修改了 common 结构,和 push accounts * perf: 整理 common 结构 * perf: 修改 const import * perf: 修改 allow bulk destroy * fix: applet host search fileds * perf: applet bulk delete * fix: applet list 404 * perf: 修改 common view * feat: 增加一些翻译, 修复 playbook 上传的错误 * fix: 修改错别字 * perf: 修改 applets status * perf: 修改网关 api * perf: automateion (#9281) Co-authored-by: feng <1304903146@qq.com> Co-authored-by: feng626 <57284900+feng626@users.noreply.github.com> * perf: 失效 connect methods 当 applet 删除 或者 host 删除 * perf: 网关账号的密码类型改成 LabelField * perf: chrome applet script * perf: verify code ttl (#9282) Co-authored-by: feng <1304903146@qq.com> * perf: database ping * perf: ws * perf: 修改网关创建 * perf: account task org (#9285) Co-authored-by: feng <1304903146@qq.com> * perf: asset test api * perf: port 添加 account * pref: 修改 db mapper permission * fix: db port mapper list api * perf: account change secret (#9286) Co-authored-by: feng <1304903146@qq.com> * perf: 修改 setup_eager_loading * perf: SecretStrategy * feat: 修改 ConnectionToken Create API 支持校验 ACL 逻辑 * feat: 修改 ConnectionToken Create API 支持校验 ACL 逻辑 * feat: 修改 ConnectionToken Create API 支持校验 ACL 逻辑 * pref: web database 信号转发 * perf: account push automation * perf: push filter account * perf: 修改 publish 版本 * perf: 修改网关 * fix: 修改资产 Specific 信息中 JSONField 字段返回 json.loads 对象 * feat: 远程应用内置Navicat Premium 16 * feat: 更新下载链接 * feat: 整理代码格式 * perf: 修改 terminal point * perf: update chrome applet script * fix: 资产 specific 获取 JSONField 时, 判断值的类型不为 list, dict * perf: domain (#9292) Co-authored-by: feng <1304903146@qq.com> * perf: 优化 endpoint 监听端口,仅 oracle 动态 * perf: 修改翻译 * perf: 修改文案 * perf: 修改缺失的翻译 * perf: 修改 endpoint help text * feat: 还原格式 * feat: 去掉基类 * feat: 增加特权账号字段 * perf: decode content * fix: check pid * perf: 修改 smart endpoint * perf: 修改 endpoint mysql default port * feat: 优化 * perf: 修改 endpoint mysql default port * perf: gateway test (#9295) Co-authored-by: feng <1304903146@qq.com> * perf: migrate * perf: 修改 endpoint mysql default port * fix: 修复获取任务执行结果死循环 * feat: 作业审计日志增加字段 * fix: add on_transaction_commit task post save * perf: gateway (#9297) Co-authored-by: feng <1304903146@qq.com> * feat: 过滤 jumpserver 自动产生的用户 * fix: 修复ops节点选择的问题 * fix: 修改 统一 connection-token 和 command 的 review API 返回数据 from_ticket_info * perf: change secret (#9298) Co-authored-by: feng <1304903146@qq.com> * perf: 修改 db port manager * perf: 修改 db port manager * perf: add celery log mark * perf: remove debug log data * fix: navicat use manual type * fix: remove navicate download url * perf: push_account_enabled (#9301) Co-authored-by: feng <1304903146@qq.com> * fix: 修改navicat启动程序MD5值 * perf: push account (#9303) Co-authored-by: feng <1304903146@qq.com> * feat: Redis/MongoDB 支持SSL * fix: 修改授权规则过滤字段 node_name,node_id; 修复获取授权节点下的资产为空的问题; * perf: push account button (#9305) Co-authored-by: feng <1304903146@qq.com> * perf: account push * fix: 修复获取 /user//assets/tree/ 返回用户授权的所有资产 * perf: asset ping (#9307) Co-authored-by: feng <1304903146@qq.com> * perf: asset enabled_info * perf: 优化activity记录都保存至operatelog中 * feat: 远程应用navicat支持试用版连接 * perf: 优化迁移文件 * perf: 修改资产列表 API category type 字段 choices 根据 category 进行返回 * fix * perf: 修改账号列表 API 解决根据 node_id asset_id 搜索账号列表无效的问题 * fix: navicat dba账号登录 * perf: 优化navicat连接 * perf: 修改账号列表 Model Manager 继承自 OrgManager,解决组织过滤问题 * perf: 修改账号列表 Filter 支持根据 platform,category,type 字段搜索 * perf: change secret email (#9312) Co-authored-by: feng <1304903146@qq.com> * feat: 保证认证信息一定清理 * perf: add mariadb * perf: 修改资产类型树数量统计资产或账号 * perf: applet chrome quit * perf: 优化关闭欢迎页面 * fix * perf: executed amount * perf: 修改 built-in applet installation * perf: 修改资产列表增加标签搜索 * perf: 修改资产列表增加标签搜索 * perf: account task automation (#9319) Co-authored-by: feng <1304903146@qq.com> * perf: account trigger * perf: 修改系统设置文案:批量命令执行 -> 作业中心 * perf: 优化migrate (#9320) Co-authored-by: feng <1304903146@qq.com> * perf: 修改资产节点树 API,支持搜索资产、节点 * perf: audit dashboard (#9321) Co-authored-by: feng <1304903146@qq.com> * fix: 修改 has_perm 权限判断兼容 list 和 str 类型 * perf: 修改一些换行 * perf: 修改 ansible config * fix: oracle依赖文件地址错误 (#9324) * perf: ansible mudules * perf: 修改 runner host cwd Co-authored-by: ibuler <ibuler@qq.com> Co-authored-by: Aaron3S <chenyang@fit2cloud.com> Co-authored-by: Bai <baijiangjie@gmail.com> Co-authored-by: feng <1304903146@qq.com> Co-authored-by: feng626 <57284900+feng626@users.noreply.github.com> Co-authored-by: Eric <xplzv@126.com> Co-authored-by: jiangweidong <weidong.jiang@fit2cloud.com> Co-authored-by: jiangweidong <80373698+Hi-JWD@users.noreply.github.com>
2023-01-16 11:02:09 +00:00
@property
def platform_automation_methods(self):
return platform_automation_methods
2023-02-22 07:13:51 +00:00
def prepare_runtime_dir(self):
2022-10-09 12:54:11 +00:00
ansible_dir = settings.ANSIBLE_DIR
2024-11-20 11:12:28 +00:00
task_name = self.execution.snapshot["name"]
dir_name = "{}_{}".format(task_name.replace(" ", "_"), self.execution.id)
2022-10-09 12:54:11 +00:00
path = os.path.join(
2024-11-20 11:12:28 +00:00
ansible_dir,
"automations",
self.execution.snapshot["type"],
dir_name,
timezone.now().strftime("%Y%m%d_%H%M%S"),
2022-10-09 12:54:11 +00:00
)
2022-10-13 12:28:18 +00:00
if not os.path.exists(path):
os.makedirs(path, exist_ok=True, mode=0o755)
2023-02-22 07:13:51 +00:00
return path
2024-11-22 06:30:45 +00:00
def host_callback(self, host, automation=None, **kwargs):
method_type = self.__class__.method_type()
host = self.convert_cert_to_file(host, kwargs.get("path_dir"))
host["params"] = self.get_params(automation, method_type)
return host
2022-10-09 12:54:11 +00:00
@staticmethod
def write_cert_to_file(filename, content):
2024-11-20 11:12:28 +00:00
with open(filename, "w") as f:
f.write(content)
return filename
def convert_cert_to_file(self, host, path_dir):
if not path_dir:
return host
2024-11-20 11:12:28 +00:00
specific = host.get("jms_asset", {}).get("secret_info", {})
cert_fields = ("ca_cert", "client_key", "client_cert")
filtered = list(filter(lambda x: specific.get(x), cert_fields))
if not filtered:
return host
2024-11-20 11:12:28 +00:00
cert_dir = os.path.join(path_dir, "certs")
if not os.path.exists(cert_dir):
os.makedirs(cert_dir, 0o700, True)
for f in filtered:
2024-11-20 11:12:28 +00:00
result = self.write_cert_to_file(os.path.join(cert_dir, f), specific.get(f))
host["jms_asset"]["secret_info"][f] = result
return host
2022-10-28 10:28:41 +00:00
@staticmethod
def generate_public_key(private_key):
return ssh_pubkey_gen(private_key=private_key, hostname=gethostname())
@staticmethod
def generate_private_key_path(secret, path_dir):
2024-11-20 11:12:28 +00:00
key_name = "." + hashlib.md5(secret.encode("utf-8")).hexdigest()
2022-10-28 10:28:41 +00:00
key_path = os.path.join(path_dir, key_name)
2022-11-01 03:52:51 +00:00
2022-10-28 10:28:41 +00:00
if not os.path.exists(key_path):
# https://github.com/ansible/ansible-runner/issues/544
# ssh requires OpenSSH format keys to have a full ending newline.
# It does not require this for old-style PEM keys.
2024-11-20 11:12:28 +00:00
with open(key_path, "w") as f:
f.write(secret)
2024-11-20 11:12:28 +00:00
if is_openssh_format_key(secret.encode("utf-8")):
f.write("\n")
2022-10-28 10:28:41 +00:00
os.chmod(key_path, 0o400)
return key_path
def generate_inventory(self, platformed_assets, inventory_path, protocol):
2022-10-09 12:54:11 +00:00
inventory = JMSInventory(
2022-10-13 12:28:18 +00:00
assets=platformed_assets,
account_prefer=self.ansible_account_prefer,
2022-10-09 12:54:11 +00:00
account_policy=self.ansible_account_policy,
2022-10-28 10:28:41 +00:00
host_callback=self.host_callback,
task_type=self.__class__.method_type(),
protocol=protocol,
2022-10-09 12:54:11 +00:00
)
2022-10-13 12:28:18 +00:00
inventory.write_to_file(inventory_path)
2024-11-22 06:30:45 +00:00
@lazyproperty
def runtime_dir(self):
path = self.prepare_runtime_dir()
if settings.DEBUG_DEV:
msg = "Ansible runtime dir: {}".format(path)
print(msg)
return path
@staticmethod
def generate_playbook(method, sub_playbook_dir):
2024-11-20 11:12:28 +00:00
method_playbook_dir_path = method["dir"]
sub_playbook_path = os.path.join(sub_playbook_dir, "project", "main.yml")
2022-10-13 12:28:18 +00:00
shutil.copytree(method_playbook_dir_path, os.path.dirname(sub_playbook_path))
2024-11-20 11:12:28 +00:00
with open(sub_playbook_path, "r") as f:
2022-10-13 12:28:18 +00:00
plays = yaml.safe_load(f)
for play in plays:
2024-11-20 11:12:28 +00:00
play["hosts"] = "all"
2022-10-13 12:28:18 +00:00
2024-11-20 11:12:28 +00:00
with open(sub_playbook_path, "w") as f:
2022-10-13 12:28:18 +00:00
yaml.safe_dump(plays, f)
return sub_playbook_path
2022-10-12 10:08:57 +00:00
2024-11-20 11:12:28 +00:00
def check_automation_enabled(self, platform, assets):
if not platform.automation or not platform.automation.ansible_enabled:
print(_(" - Platform {} ansible disabled").format(platform.name))
self.on_assets_not_ansible_enabled(assets)
automation = platform.automation
method_type = self.__class__.method_type()
enabled_attr = "{}_enabled".format(method_type)
method_attr = "{}_method".format(method_type)
method_enabled = (
automation
and getattr(automation, enabled_attr)
and getattr(automation, method_attr)
and getattr(automation, method_attr) in self.method_id_meta_mapper
)
if not method_enabled:
self.on_assets_not_method_enabled(assets, method_type)
2024-11-22 06:30:45 +00:00
return False
return True
def on_assets_not_ansible_enabled(self, assets):
self.summary["error_assets"] += len(assets)
self.result["error_assets"].extend([str(asset) for asset in assets])
for asset in assets:
print("\t{}".format(asset))
def on_assets_not_method_enabled(self, assets, method_type):
self.summary["error_assets"] += len(assets)
self.result["error_assets"].extend([str(asset) for asset in assets])
for asset in assets:
print("\t{}".format(asset))
def on_playbook_not_found(self, assets):
print("Playbook generate failed")
class BasePlaybookManager(PlaybookPrepareMixin, BaseManager):
bulk_size = 100
ansible_account_policy = "privileged_first"
ansible_account_prefer = "root,Administrator"
def __init__(self, execution):
super().__init__(execution)
self.params = execution.snapshot.get("params", {})
def get_assets_group_by_platform(self):
return self.execution.all_assets_group_by_platform()
@classmethod
def method_type(cls):
raise NotImplementedError
def get_runners_by_platform(self, platform, _assets, _index):
sub_dir = "{}_{}".format(platform.name, _index)
playbook_dir = os.path.join(self.runtime_dir, sub_dir)
inventory_path = os.path.join(self.runtime_dir, sub_dir, "hosts.json")
method_id = getattr(
platform.automation,
"{}_method".format(self.__class__.method_type()),
)
method = self.method_id_meta_mapper.get(method_id)
protocol = method.get("protocol")
self.generate_inventory(_assets, inventory_path, protocol)
playbook_path = self.generate_playbook(method, playbook_dir)
if not playbook_path:
self.on_playbook_not_found(_assets)
return None, None
runner = SuperPlaybookRunner(
inventory_path,
playbook_path,
self.runtime_dir,
callback=PlaybookCallback(),
)
return runner, inventory_path
2024-11-20 11:12:28 +00:00
2022-10-12 10:08:57 +00:00
def get_runners(self):
assets_group_by_platform = self.get_assets_group_by_platform()
2023-02-22 03:18:42 +00:00
if settings.DEBUG_DEV:
2024-11-20 11:12:28 +00:00
msg = "Assets group by platform: {}".format(dict(assets_group_by_platform))
2023-02-22 07:13:51 +00:00
print(msg)
2024-11-18 11:06:04 +00:00
2022-10-12 10:08:57 +00:00
runners = []
for platform, assets in assets_group_by_platform.items():
2024-11-22 06:30:45 +00:00
self.summary["total_assets"] += len(assets)
if not assets:
2024-11-22 06:30:45 +00:00
print("No assets for platform: {}".format(platform.name))
continue
2024-11-20 11:12:28 +00:00
if not self.check_automation_enabled(platform, assets):
2024-11-22 06:30:45 +00:00
print("Platform {} ansible disabled".format(platform.name))
continue
2022-10-13 12:28:18 +00:00
2024-11-20 11:12:28 +00:00
# 避免一个任务太大,分批执行
assets_bulked = [
assets[i : i + self.bulk_size]
for i in range(0, len(assets), self.bulk_size)
]
2022-10-13 12:28:18 +00:00
for i, _assets in enumerate(assets_bulked, start=1):
2024-11-22 06:30:45 +00:00
runner, inventory_path = self.get_runners_by_platform(
platform, _assets, i
2022-10-13 12:28:18 +00:00
)
2024-11-22 06:30:45 +00:00
if not runner or not inventory_path:
continue
2024-11-20 11:12:28 +00:00
with open(inventory_path, "r") as f:
inventory_data = json.load(f)
2024-11-20 11:12:28 +00:00
if not inventory_data["all"].get("hosts"):
continue
2024-11-22 06:30:45 +00:00
runners.append(
(
runner,
{
"assets": _assets,
"inventory": inventory_path,
"platform": platform,
},
)
)
2022-10-12 10:08:57 +00:00
return runners
2022-10-14 08:33:24 +00:00
def on_host_success(self, host, result):
2024-11-22 06:30:45 +00:00
self.summary["ok_assets"] += 1
self.result["ok_assets"].append(host)
2022-10-14 08:33:24 +00:00
def on_host_error(self, host, error, result):
2024-11-22 06:30:45 +00:00
self.summary["fail_assets"] += 1
self.result["fail_assets"].append((host, str(error)))
print(f"\033[31m {host} error: {error} \033[0m\n")
2022-10-14 08:33:24 +00:00
2024-11-19 10:05:59 +00:00
def _on_host_success(self, host, result, hosts):
2024-11-20 11:12:28 +00:00
self.on_host_success(host, result.get("ok", ""))
2024-11-18 11:06:04 +00:00
2024-11-19 10:05:59 +00:00
def _on_host_error(self, host, result, hosts):
2024-11-20 11:12:28 +00:00
error = hosts.get(host, "")
detail = result.get("failures", "") or result.get("dark", "")
2024-11-18 11:06:04 +00:00
self.on_host_error(host, error, detail)
2022-10-13 12:28:18 +00:00
def on_runner_success(self, runner, cb):
2022-10-14 08:33:24 +00:00
summary = cb.summary
for state, hosts in summary.items():
2024-11-19 10:05:59 +00:00
# 错误行为为host 是 dict ok 时是 list
2024-11-20 11:12:28 +00:00
if state == "ok":
2024-11-18 11:06:04 +00:00
handler = self._on_host_success
2024-11-20 11:12:28 +00:00
elif state == "skipped":
2024-11-18 11:06:04 +00:00
continue
else:
handler = self._on_host_error
2022-10-14 08:33:24 +00:00
for host in hosts:
2022-10-17 03:22:21 +00:00
result = cb.host_results.get(host)
2024-11-19 10:05:59 +00:00
handler(host, result, hosts)
2022-10-09 12:54:11 +00:00
2024-11-22 06:30:45 +00:00
def on_runner_failed(self, runner, e, assets=None, **kwargs):
self.summary["fail_assets"] += len(assets)
self.result["fail_assets"].extend(
[(str(asset), str("e")[:10]) for asset in assets]
)
2022-10-12 10:08:57 +00:00
print("Runner failed: {} {}".format(e, self))
2022-10-09 12:54:11 +00:00
2023-02-22 07:13:51 +00:00
def delete_runtime_dir(self):
if settings.DEBUG_DEV:
return
shutil.rmtree(self.runtime_dir, ignore_errors=True)
2024-11-18 11:06:04 +00:00
def do_run(self, *args, **kwargs):
2024-05-22 10:18:56 +00:00
print(_(">>> Task preparation phase"), end="\n")
2022-10-12 10:08:57 +00:00
runners = self.get_runners()
if len(runners) > 1:
2024-11-20 11:12:28 +00:00
print(
_(">>> Executing tasks in batches, total {runner_count}").format(
runner_count=len(runners)
)
)
elif len(runners) == 1:
2024-05-22 10:18:56 +00:00
print(_(">>> Start executing tasks"))
else:
2024-05-22 10:18:56 +00:00
print(_(">>> No tasks need to be executed"), end="\n")
2022-10-12 10:08:57 +00:00
2024-11-22 06:30:45 +00:00
for i, runner_info in enumerate(runners, start=1):
2022-10-12 10:08:57 +00:00
if len(runners) > 1:
2024-05-22 10:18:56 +00:00
print(_(">>> Begin executing batch {index} of tasks").format(index=i))
2024-11-22 06:30:45 +00:00
runner, info = runner_info
ssh_tunnel = SSHTunnelManager()
ssh_tunnel.local_gateway_prepare(runner)
2024-11-18 11:06:04 +00:00
2022-10-12 10:08:57 +00:00
try:
kwargs.update({"clean_workspace": False})
2022-10-12 10:08:57 +00:00
cb = runner.run(**kwargs)
2022-10-13 12:28:18 +00:00
self.on_runner_success(runner, cb)
2022-10-12 10:08:57 +00:00
except Exception as e:
2024-11-22 06:30:45 +00:00
self.on_runner_failed(runner, e, **runner_info)
finally:
ssh_tunnel.local_gateway_clean(runner)
2024-11-20 11:12:28 +00:00
print("\n")