mirror of https://github.com/jumpserver/jumpserver
feat: 增加作业版本历史
parent
21d6243b61
commit
0c35205e31
|
@ -3,7 +3,6 @@ from django.shortcuts import get_object_or_404
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from ops.models import Job, JobExecution
|
from ops.models import Job, JobExecution
|
||||||
from ops.models.job import JobAuditLog
|
|
||||||
from ops.serializers.job import JobSerializer, JobExecutionSerializer
|
from ops.serializers.job import JobSerializer, JobExecutionSerializer
|
||||||
|
|
||||||
__all__ = ['JobViewSet', 'JobExecutionViewSet', 'JobRunVariableHelpAPIView', 'JobAssetDetail', ]
|
__all__ = ['JobViewSet', 'JobExecutionViewSet', 'JobRunVariableHelpAPIView', 'JobAssetDetail', ]
|
||||||
|
@ -58,6 +57,8 @@ class JobExecutionViewSet(OrgBulkModelViewSet):
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
instance = serializer.save()
|
instance = serializer.save()
|
||||||
|
instance.job_version = instance.job.version
|
||||||
|
instance.save()
|
||||||
task = run_ops_job_execution.delay(instance.id)
|
task = run_ops_job_execution.delay(instance.id)
|
||||||
set_task_to_serializer_data(serializer, task)
|
set_task_to_serializer_data(serializer, task)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
# Generated by Django 3.2.14 on 2022-12-20 11:41
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import simple_history.models
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('ops', '0029_auto_20221215_1712'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='JobAuditLog',
|
||||||
|
fields=[
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'proxy': True,
|
||||||
|
'indexes': [],
|
||||||
|
'constraints': [],
|
||||||
|
},
|
||||||
|
bases=('ops.jobexecution',),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='job',
|
||||||
|
name='version',
|
||||||
|
field=models.IntegerField(default=0),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='jobexecution',
|
||||||
|
name='job_version',
|
||||||
|
field=models.IntegerField(default=0),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='HistoricalJob',
|
||||||
|
fields=[
|
||||||
|
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
|
||||||
|
('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by')),
|
||||||
|
('date_created', models.DateTimeField(blank=True, editable=False, null=True, verbose_name='Date created')),
|
||||||
|
('date_updated', models.DateTimeField(blank=True, editable=False, verbose_name='Date updated')),
|
||||||
|
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
|
||||||
|
('is_periodic', models.BooleanField(default=False, verbose_name='Periodic perform')),
|
||||||
|
('interval', models.IntegerField(blank=True, default=24, null=True, verbose_name='Cycle perform')),
|
||||||
|
('crontab', models.CharField(blank=True, max_length=128, null=True, verbose_name='Regularly perform')),
|
||||||
|
('id', models.UUIDField(db_index=True, default=uuid.uuid4)),
|
||||||
|
('name', models.CharField(max_length=128, null=True, verbose_name='Name')),
|
||||||
|
('instant', models.BooleanField(default=False)),
|
||||||
|
('args', models.CharField(blank=True, default='', max_length=1024, null=True, verbose_name='Args')),
|
||||||
|
('module', models.CharField(choices=[('shell', 'Shell'), ('win_shell', 'Powershell')], default='shell', max_length=128, null=True, verbose_name='Module')),
|
||||||
|
('chdir', models.CharField(blank=True, default='', max_length=1024, null=True, verbose_name='Chdir')),
|
||||||
|
('timeout', models.IntegerField(default=60, verbose_name='Timeout (Seconds)')),
|
||||||
|
('type', models.CharField(choices=[('adhoc', 'Adhoc'), ('playbook', 'Playbook')], default='adhoc', max_length=128, verbose_name='Type')),
|
||||||
|
('runas', models.CharField(default='root', max_length=128, verbose_name='Runas')),
|
||||||
|
('runas_policy', models.CharField(choices=[('privileged_only', 'Privileged Only'), ('privileged_first', 'Privileged First'), ('skip', 'Skip')], default='skip', max_length=128, 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(blank=True, default='', max_length=1024, null=True, verbose_name='Comment')),
|
||||||
|
('version', models.IntegerField(default=0)),
|
||||||
|
('history_id', models.AutoField(primary_key=True, serialize=False)),
|
||||||
|
('history_date', models.DateTimeField(db_index=True)),
|
||||||
|
('history_change_reason', models.CharField(max_length=100, null=True)),
|
||||||
|
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
|
||||||
|
('creator', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='Creator')),
|
||||||
|
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
|
||||||
|
('playbook', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='ops.playbook', verbose_name='Playbook')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'historical job',
|
||||||
|
'verbose_name_plural': 'historical jobs',
|
||||||
|
'ordering': ('-history_date', '-history_id'),
|
||||||
|
'get_latest_by': ('history_date', 'history_id'),
|
||||||
|
},
|
||||||
|
bases=(simple_history.models.HistoricalChanges, models.Model),
|
||||||
|
),
|
||||||
|
]
|
|
@ -11,6 +11,8 @@ from celery import current_task
|
||||||
|
|
||||||
__all__ = ["Job", "JobExecution", "JobAuditLog"]
|
__all__ = ["Job", "JobExecution", "JobAuditLog"]
|
||||||
|
|
||||||
|
from simple_history.models import HistoricalRecords
|
||||||
|
|
||||||
from ops.ansible import JMSInventory, AdHocRunner, PlaybookRunner
|
from ops.ansible import JMSInventory, AdHocRunner, PlaybookRunner
|
||||||
from ops.mixin import PeriodTaskModelMixin
|
from ops.mixin import PeriodTaskModelMixin
|
||||||
from ops.variables import *
|
from ops.variables import *
|
||||||
|
@ -37,6 +39,11 @@ class Job(JMSOrgBaseModel, PeriodTaskModelMixin):
|
||||||
use_parameter_define = models.BooleanField(default=False, verbose_name=(_('Use Parameter Define')))
|
use_parameter_define = models.BooleanField(default=False, verbose_name=(_('Use Parameter Define')))
|
||||||
parameters_define = models.JSONField(default=dict, verbose_name=_('Parameters 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)
|
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
|
@property
|
||||||
def last_execution(self):
|
def last_execution(self):
|
||||||
|
@ -79,7 +86,7 @@ class Job(JMSOrgBaseModel, PeriodTaskModelMixin):
|
||||||
return JMSInventory(self.assets.all(), self.runas_policy, self.runas)
|
return JMSInventory(self.assets.all(), self.runas_policy, self.runas)
|
||||||
|
|
||||||
def create_execution(self):
|
def create_execution(self):
|
||||||
return self.executions.create()
|
return self.executions.create(job_version=self.version)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['date_created']
|
ordering = ['date_created']
|
||||||
|
@ -90,6 +97,7 @@ class JobExecution(JMSOrgBaseModel):
|
||||||
task_id = models.UUIDField(null=True)
|
task_id = models.UUIDField(null=True)
|
||||||
status = models.CharField(max_length=16, verbose_name=_('Status'), default=JobStatus.running)
|
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 = 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'))
|
parameters = models.JSONField(default=dict, verbose_name=_('Parameters'))
|
||||||
result = models.JSONField(blank=True, null=True, verbose_name=_('Result'))
|
result = models.JSONField(blank=True, null=True, verbose_name=_('Result'))
|
||||||
summary = models.JSONField(default=dict, verbose_name=_('Summary'))
|
summary = models.JSONField(default=dict, verbose_name=_('Summary'))
|
||||||
|
@ -98,12 +106,18 @@ class JobExecution(JMSOrgBaseModel):
|
||||||
date_start = models.DateTimeField(null=True, verbose_name=_('Date start'), db_index=True)
|
date_start = models.DateTimeField(null=True, verbose_name=_('Date start'), db_index=True)
|
||||||
date_finished = models.DateTimeField(null=True, verbose_name=_("Date finished"))
|
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
|
@property
|
||||||
def material(self):
|
def material(self):
|
||||||
if self.job.type == 'adhoc':
|
if self.current_job.type == 'adhoc':
|
||||||
return "{}:{}".format(self.job.module, self.job.args)
|
return "{}:{}".format(self.current_job.module, self.current_job.args)
|
||||||
if self.job.type == 'playbook':
|
if self.current_job.type == 'playbook':
|
||||||
return "{}:{}:{}".format(self.org.name, self.job.creator.name, self.job.playbook.name)
|
return "{}:{}:{}".format(self.org.name, self.current_job.creator.name, self.current_job.playbook.name)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def assent_result_detail(self):
|
def assent_result_detail(self):
|
||||||
|
@ -112,7 +126,7 @@ class JobExecution(JMSOrgBaseModel):
|
||||||
"summary": self.count,
|
"summary": self.count,
|
||||||
"detail": [],
|
"detail": [],
|
||||||
}
|
}
|
||||||
for asset in self.job.assets.all():
|
for asset in self.current_job.assets.all():
|
||||||
asset_detail = {
|
asset_detail = {
|
||||||
"name": asset.name,
|
"name": asset.name,
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
|
@ -145,17 +159,17 @@ class JobExecution(JMSOrgBaseModel):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def job_type(self):
|
def job_type(self):
|
||||||
return self.job.type
|
return self.current_job.type
|
||||||
|
|
||||||
def compile_shell(self):
|
def compile_shell(self):
|
||||||
if self.job.type != 'adhoc':
|
if self.current_job.type != 'adhoc':
|
||||||
return
|
return
|
||||||
result = "{}{}{} ".format('\'', self.job.args, '\'')
|
result = "{}{}{} ".format('\'', self.current_job.args, '\'')
|
||||||
result += "chdir={}".format(self.job.chdir)
|
result += "chdir={}".format(self.current_job.chdir)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def get_runner(self):
|
def get_runner(self):
|
||||||
inv = self.job.inventory
|
inv = self.current_job.inventory
|
||||||
inv.write_to_file(self.inventory_path)
|
inv.write_to_file(self.inventory_path)
|
||||||
self.summary = self.result = {"excludes": {}}
|
self.summary = self.result = {"excludes": {}}
|
||||||
if len(inv.exclude_hosts) > 0:
|
if len(inv.exclude_hosts) > 0:
|
||||||
|
@ -171,15 +185,15 @@ class JobExecution(JMSOrgBaseModel):
|
||||||
static_variables = self.gather_static_variables()
|
static_variables = self.gather_static_variables()
|
||||||
extra_vars.update(static_variables)
|
extra_vars.update(static_variables)
|
||||||
|
|
||||||
if self.job.type == 'adhoc':
|
if self.current_job.type == 'adhoc':
|
||||||
args = self.compile_shell()
|
args = self.compile_shell()
|
||||||
runner = AdHocRunner(
|
runner = AdHocRunner(
|
||||||
self.inventory_path, self.job.module, module_args=args,
|
self.inventory_path, self.current_job.module, module_args=args,
|
||||||
pattern="all", project_dir=self.private_dir, extra_vars=extra_vars,
|
pattern="all", project_dir=self.private_dir, extra_vars=extra_vars,
|
||||||
)
|
)
|
||||||
elif self.job.type == 'playbook':
|
elif self.current_job.type == 'playbook':
|
||||||
runner = PlaybookRunner(
|
runner = PlaybookRunner(
|
||||||
self.inventory_path, self.job.playbook.entry
|
self.inventory_path, self.current_job.playbook.entry
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise Exception("unsupported job type")
|
raise Exception("unsupported job type")
|
||||||
|
@ -187,8 +201,8 @@ class JobExecution(JMSOrgBaseModel):
|
||||||
|
|
||||||
def gather_static_variables(self):
|
def gather_static_variables(self):
|
||||||
default = {
|
default = {
|
||||||
JMS_JOB_ID: str(self.job.id),
|
JMS_JOB_ID: str(self.current_job.id),
|
||||||
JMS_JOB_NAME: self.job.name,
|
JMS_JOB_NAME: self.current_job.name,
|
||||||
}
|
}
|
||||||
if self.creator:
|
if self.creator:
|
||||||
default.update({JMS_USERNAME: self.creator.username})
|
default.update({JMS_USERNAME: self.creator.username})
|
||||||
|
@ -225,7 +239,7 @@ class JobExecution(JMSOrgBaseModel):
|
||||||
@property
|
@property
|
||||||
def private_dir(self):
|
def private_dir(self):
|
||||||
uniq = self.date_created.strftime('%Y%m%d_%H%M%S') + '_' + self.short_id
|
uniq = self.date_created.strftime('%Y%m%d_%H%M%S') + '_' + self.short_id
|
||||||
job_name = self.job.name if self.job.name else 'instant'
|
job_name = self.current_job.name if self.current_job.name else 'instant'
|
||||||
return os.path.join(settings.ANSIBLE_DIR, job_name, uniq)
|
return os.path.join(settings.ANSIBLE_DIR, job_name, uniq)
|
||||||
|
|
||||||
def set_error(self, error):
|
def set_error(self, error):
|
||||||
|
|
|
@ -3,6 +3,7 @@ from celery import signals
|
||||||
|
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
|
from django.db.models.signals import pre_save
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.db.utils import ProgrammingError
|
from django.db.utils import ProgrammingError
|
||||||
from django.utils import translation, timezone
|
from django.utils import translation, timezone
|
||||||
|
@ -12,7 +13,7 @@ from common.signals import django_ready
|
||||||
from common.db.utils import close_old_connections, get_logger
|
from common.db.utils import close_old_connections, get_logger
|
||||||
|
|
||||||
from .celery import app
|
from .celery import app
|
||||||
from .models import CeleryTaskExecution, CeleryTask
|
from .models import CeleryTaskExecution, CeleryTask, Job
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
@ -20,6 +21,12 @@ TASK_LANG_CACHE_KEY = 'TASK_LANG_{}'
|
||||||
TASK_LANG_CACHE_TTL = 1800
|
TASK_LANG_CACHE_TTL = 1800
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(pre_save, sender=Job)
|
||||||
|
def on_account_pre_create(sender, instance, **kwargs):
|
||||||
|
# 升级版本号
|
||||||
|
instance.version += 1
|
||||||
|
|
||||||
|
|
||||||
@receiver(django_ready)
|
@receiver(django_ready)
|
||||||
def sync_registered_tasks(*args, **kwargs):
|
def sync_registered_tasks(*args, **kwargs):
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
|
|
Loading…
Reference in New Issue