diff --git a/apps/ops/api/job.py b/apps/ops/api/job.py index a49b1af90..9b36a5aba 100644 --- a/apps/ops/api/job.py +++ b/apps/ops/api/job.py @@ -3,7 +3,6 @@ from django.shortcuts import get_object_or_404 from rest_framework.response import Response from ops.models import Job, JobExecution -from ops.models.job import JobAuditLog from ops.serializers.job import JobSerializer, JobExecutionSerializer __all__ = ['JobViewSet', 'JobExecutionViewSet', 'JobRunVariableHelpAPIView', 'JobAssetDetail', ] @@ -58,6 +57,8 @@ class JobExecutionViewSet(OrgBulkModelViewSet): def perform_create(self, serializer): instance = serializer.save() + instance.job_version = instance.job.version + instance.save() task = run_ops_job_execution.delay(instance.id) set_task_to_serializer_data(serializer, task) diff --git a/apps/ops/migrations/0030_auto_20221220_1941.py b/apps/ops/migrations/0030_auto_20221220_1941.py new file mode 100644 index 000000000..cebbd8aa1 --- /dev/null +++ b/apps/ops/migrations/0030_auto_20221220_1941.py @@ -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), + ), + ] diff --git a/apps/ops/models/job.py b/apps/ops/models/job.py index 4f003895e..bdd00382f 100644 --- a/apps/ops/models/job.py +++ b/apps/ops/models/job.py @@ -11,6 +11,8 @@ from celery import current_task __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 * @@ -37,6 +39,11 @@ class Job(JMSOrgBaseModel, PeriodTaskModelMixin): 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): @@ -79,7 +86,7 @@ class Job(JMSOrgBaseModel, PeriodTaskModelMixin): return JMSInventory(self.assets.all(), self.runas_policy, self.runas) def create_execution(self): - return self.executions.create() + return self.executions.create(job_version=self.version) class Meta: ordering = ['date_created'] @@ -90,6 +97,7 @@ class JobExecution(JMSOrgBaseModel): 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')) @@ -98,12 +106,18 @@ class JobExecution(JMSOrgBaseModel): 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.job.type == 'adhoc': - return "{}:{}".format(self.job.module, self.job.args) - if self.job.type == 'playbook': - return "{}:{}:{}".format(self.org.name, self.job.creator.name, self.job.playbook.name) + 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): @@ -112,7 +126,7 @@ class JobExecution(JMSOrgBaseModel): "summary": self.count, "detail": [], } - for asset in self.job.assets.all(): + for asset in self.current_job.assets.all(): asset_detail = { "name": asset.name, "status": "ok", @@ -145,17 +159,17 @@ class JobExecution(JMSOrgBaseModel): @property def job_type(self): - return self.job.type + return self.current_job.type def compile_shell(self): - if self.job.type != 'adhoc': + if self.current_job.type != 'adhoc': return - result = "{}{}{} ".format('\'', self.job.args, '\'') - result += "chdir={}".format(self.job.chdir) + result = "{}{}{} ".format('\'', self.current_job.args, '\'') + result += "chdir={}".format(self.current_job.chdir) return result def get_runner(self): - inv = self.job.inventory + inv = self.current_job.inventory inv.write_to_file(self.inventory_path) self.summary = self.result = {"excludes": {}} if len(inv.exclude_hosts) > 0: @@ -171,15 +185,15 @@ class JobExecution(JMSOrgBaseModel): static_variables = self.gather_static_variables() extra_vars.update(static_variables) - if self.job.type == 'adhoc': + if self.current_job.type == 'adhoc': args = self.compile_shell() 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, ) - elif self.job.type == 'playbook': + elif self.current_job.type == 'playbook': runner = PlaybookRunner( - self.inventory_path, self.job.playbook.entry + self.inventory_path, self.current_job.playbook.entry ) else: raise Exception("unsupported job type") @@ -187,8 +201,8 @@ class JobExecution(JMSOrgBaseModel): def gather_static_variables(self): default = { - JMS_JOB_ID: str(self.job.id), - JMS_JOB_NAME: self.job.name, + 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}) @@ -225,7 +239,7 @@ class JobExecution(JMSOrgBaseModel): @property def private_dir(self): 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) def set_error(self, error): diff --git a/apps/ops/signal_handlers.py b/apps/ops/signal_handlers.py index 965bd494c..b1d2c39f3 100644 --- a/apps/ops/signal_handlers.py +++ b/apps/ops/signal_handlers.py @@ -3,6 +3,7 @@ from celery import signals from django.db import transaction from django.core.cache import cache +from django.db.models.signals import pre_save from django.dispatch import receiver from django.db.utils import ProgrammingError 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 .celery import app -from .models import CeleryTaskExecution, CeleryTask +from .models import CeleryTaskExecution, CeleryTask, Job logger = get_logger(__name__) @@ -20,6 +21,12 @@ TASK_LANG_CACHE_KEY = 'TASK_LANG_{}' TASK_LANG_CACHE_TTL = 1800 +@receiver(pre_save, sender=Job) +def on_account_pre_create(sender, instance, **kwargs): + # 升级版本号 + instance.version += 1 + + @receiver(django_ready) def sync_registered_tasks(*args, **kwargs): with transaction.atomic():