Files
jumpserver/apps/ops/models/job.py
fit2bot 56d533c802 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 19:02:09 +08:00

323 lines
12 KiB
Python

import datetime
import json
import logging
import os
import uuid
from celery import current_task
from django.conf import settings
from django.db import models
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
__all__ = ["Job", "JobExecution", "JobAuditLog"]
from simple_history.models import HistoricalRecords
from ops.ansible import JMSInventory, AdHocRunner, PlaybookRunner
from ops.mixin import PeriodTaskModelMixin
from ops.variables import *
from ops.const import Types, Modules, RunasPolicies, JobStatus
from orgs.mixins.models import JMSOrgBaseModel
class Job(JMSOrgBaseModel, PeriodTaskModelMixin):
name = models.CharField(max_length=128, null=True, verbose_name=_('Name'))
instant = models.BooleanField(default=False)
args = models.CharField(max_length=1024, default='', verbose_name=_('Args'), null=True, blank=True)
module = models.CharField(max_length=128, choices=Modules.choices, default=Modules.shell,
verbose_name=_('Module'), null=True)
chdir = models.CharField(default="", max_length=1024, verbose_name=_('Chdir'), null=True, blank=True)
timeout = models.IntegerField(default=-1, verbose_name=_('Timeout (Seconds)'))
playbook = models.ForeignKey('ops.Playbook', verbose_name=_("Playbook"), null=True, on_delete=models.SET_NULL)
type = models.CharField(max_length=128, choices=Types.choices, default=Types.adhoc, verbose_name=_("Type"))
creator = models.ForeignKey('users.User', verbose_name=_("Creator"), on_delete=models.SET_NULL, null=True)
assets = models.ManyToManyField('assets.Asset', verbose_name=_("Assets"))
runas = models.CharField(max_length=128, default='root', verbose_name=_('Runas'))
runas_policy = models.CharField(max_length=128, choices=RunasPolicies.choices, default=RunasPolicies.skip,
verbose_name=_('Runas policy'))
use_parameter_define = models.BooleanField(default=False, verbose_name=(_('Use Parameter Define')))
parameters_define = models.JSONField(default=dict, verbose_name=_('Parameters define'))
comment = models.CharField(max_length=1024, default='', verbose_name=_('Comment'), null=True, blank=True)
version = models.IntegerField(default=0)
history = HistoricalRecords()
def get_history(self, version):
return self.history.filter(version=version).first()
@property
def last_execution(self):
return self.executions.last()
@property
def date_last_run(self):
return self.last_execution.date_created if self.last_execution else None
@property
def summary(self):
summary = {
"total": 0,
"success": 0,
}
for execution in self.executions.all():
summary["total"] += 1
if execution.is_success:
summary["success"] += 1
return summary
@property
def average_time_cost(self):
total_cost = 0
finished_count = self.executions.filter(status__in=['success', 'failed']).count()
for execution in self.executions.filter(status__in=['success', 'failed']).all():
total_cost += execution.time_cost
return total_cost / finished_count if finished_count else 0
def get_register_task(self):
from ..tasks import run_ops_job_execution
name = "run_ops_job_period_{}".format(str(self.id)[:8])
task = run_ops_job_execution.name
args = (str(self.id),)
kwargs = {}
return name, task, args, kwargs
@property
def inventory(self):
return JMSInventory(self.assets.all(), self.runas_policy, self.runas)
def create_execution(self):
return self.executions.create(job_version=self.version)
class Meta:
verbose_name = _("Job")
ordering = ['date_created']
class JobExecution(JMSOrgBaseModel):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
task_id = models.UUIDField(null=True)
status = models.CharField(max_length=16, verbose_name=_('Status'), default=JobStatus.running)
job = models.ForeignKey(Job, on_delete=models.CASCADE, related_name='executions', null=True)
job_version = models.IntegerField(default=0)
parameters = models.JSONField(default=dict, verbose_name=_('Parameters'))
result = models.JSONField(blank=True, null=True, verbose_name=_('Result'))
summary = models.JSONField(default=dict, verbose_name=_('Summary'))
creator = models.ForeignKey('users.User', verbose_name=_("Creator"), on_delete=models.SET_NULL, null=True)
date_created = models.DateTimeField(auto_now_add=True, verbose_name=_('Date created'))
date_start = models.DateTimeField(null=True, verbose_name=_('Date start'), db_index=True)
date_finished = models.DateTimeField(null=True, verbose_name=_("Date finished"))
@property
def current_job(self):
if self.job.version != self.job_version:
return self.job.get_history(self.job_version)
return self.job
@property
def material(self):
if self.current_job.type == 'adhoc':
return "{}:{}".format(self.current_job.module, self.current_job.args)
if self.current_job.type == 'playbook':
return "{}:{}:{}".format(self.org.name, self.current_job.creator.name, self.current_job.playbook.name)
@property
def assent_result_detail(self):
if self.is_finished and not self.summary.get('error', None):
result = {
"summary": self.count,
"detail": [],
}
for asset in self.current_job.assets.all():
asset_detail = {
"name": asset.name,
"status": "ok",
"tasks": [],
}
if self.summary.get("excludes", None) and self.summary["excludes"].get(asset.name, None):
asset_detail.update({"status": "excludes"})
result["detail"].append(asset_detail)
break
if self.result["dark"].get(asset.name, None):
asset_detail.update({"status": "failed"})
for key, task in self.result["dark"][asset.name].items():
task_detail = {"name": key,
"output": "{}{}".format(task.get("stdout", ""), task.get("stderr", ""))}
asset_detail["tasks"].append(task_detail)
if self.result["failures"].get(asset.name, None):
asset_detail.update({"status": "failed"})
for key, task in self.result["failures"][asset.name].items():
task_detail = {"name": key,
"output": "{}{}".format(task.get("stdout", ""), task.get("stderr", ""))}
asset_detail["tasks"].append(task_detail)
if self.result["ok"].get(asset.name, None):
for key, task in self.result["ok"][asset.name].items():
task_detail = {"name": key,
"output": "{}{}".format(task.get("stdout", ""), task.get("stderr", ""))}
asset_detail["tasks"].append(task_detail)
result["detail"].append(asset_detail)
return result
@property
def job_type(self):
return Types[self.job.type].label
def compile_shell(self):
if self.current_job.type != 'adhoc':
return
module = self.current_job.module
# replace win_shell
if module == 'win_shell':
module = 'ansible.windows.win_shell'
if self.current_job.module in ['python']:
module = "shell"
shell = self.current_job.args
if self.current_job.chdir:
if module == self.current_job.module:
shell += " path={}".format(self.current_job.chdir)
else:
shell += " chdir={}".format(self.current_job.chdir)
if self.current_job.module in ['python']:
shell += " executable={}".format(self.current_job.module)
return module, shell
def get_runner(self):
inv = self.current_job.inventory
inv.write_to_file(self.inventory_path)
self.summary = self.result = {"excludes": {}}
if len(inv.exclude_hosts) > 0:
self.summary.update({"excludes": inv.exclude_hosts})
self.result.update({"excludes": inv.exclude_hosts})
self.save()
if isinstance(self.parameters, str):
extra_vars = json.loads(self.parameters)
else:
extra_vars = {}
static_variables = self.gather_static_variables()
extra_vars.update(static_variables)
if self.current_job.type == 'adhoc':
module, args = self.compile_shell()
runner = AdHocRunner(
self.inventory_path,
module,
timeout=self.current_job.timeout,
module_args=args,
pattern="all",
project_dir=self.private_dir,
extra_vars=extra_vars,
)
elif self.current_job.type == 'playbook':
runner = PlaybookRunner(
self.inventory_path, self.current_job.playbook.entry
)
else:
raise Exception("unsupported job type")
return runner
def gather_static_variables(self):
default = {
JMS_JOB_ID: str(self.current_job.id),
JMS_JOB_NAME: self.current_job.name,
}
if self.creator:
default.update({JMS_USERNAME: self.creator.username})
return default
@property
def short_id(self):
return str(self.id).split('-')[-1]
@property
def time_cost(self):
if self.is_finished:
return (self.date_finished - self.date_start).total_seconds()
return (timezone.now() - self.date_start).total_seconds()
@property
def timedelta(self):
if self.date_start and self.date_finished:
return self.date_finished - self.date_start
return None
@property
def is_finished(self):
return self.status in [JobStatus.success, JobStatus.failed, JobStatus.timeout]
@property
def is_success(self):
return self.status == JobStatus.success
@property
def inventory_path(self):
return os.path.join(self.private_dir, 'inventory', 'hosts')
@property
def private_dir(self):
uniq = self.date_created.strftime('%Y%m%d_%H%M%S') + '_' + self.short_id
job_name = self.current_job.name if self.current_job.name else 'instant'
return os.path.join(settings.ANSIBLE_DIR, job_name, uniq)
def set_error(self, error):
this = self.__class__.objects.get(id=self.id) # 重新获取一次,避免数据库超时连接超时
this.status = JobStatus.failed
this.summary.update({'error': str(error)})
this.finish_task()
def set_result(self, cb):
status_mapper = {
'successful': JobStatus.success,
}
this = self.__class__.objects.get(id=self.id)
this.status = status_mapper.get(cb.status, cb.status)
this.summary.update(cb.summary)
if this.result:
this.result.update(cb.result)
else:
this.result = cb.result
this.finish_task()
def finish_task(self):
self.date_finished = timezone.now()
self.save(update_fields=['result', 'status', 'summary', 'date_finished'])
def set_celery_id(self):
if not current_task:
return
task_id = current_task.request.root_id
self.task_id = task_id
def start(self, **kwargs):
self.date_start = timezone.now()
self.set_celery_id()
self.save()
runner = self.get_runner()
try:
cb = runner.run(**kwargs)
self.set_result(cb)
return cb
except Exception as e:
logging.error(e, exc_info=True)
self.set_error(e)
class Meta:
verbose_name = _("Job Execution")
ordering = ['-date_created']
class JobAuditLog(JobExecution):
@property
def creator_name(self):
return self.creator.name
class Meta:
proxy = True
verbose_name = _("Job audit log")