diff --git a/apps/ops/ansible/inventory.py b/apps/ops/ansible/inventory.py index 1258742e0..c88c5c95f 100644 --- a/apps/ops/ansible/inventory.py +++ b/apps/ops/ansible/inventory.py @@ -10,7 +10,7 @@ __all__ = ['JMSInventory'] class JMSInventory: def __init__(self, assets, account_policy='privileged_first', - account_prefer='root,Administrator', host_callback=None): + account_prefer='root,Administrator', host_callback=None, unique_host_name=False): """ :param assets: :param account_prefer: account username name if not set use account_policy @@ -20,6 +20,8 @@ class JMSInventory: self.account_prefer = account_prefer self.account_policy = account_policy self.host_callback = host_callback + self.exclude_hosts = {} + self.unique_host_name = unique_host_name @staticmethod def clean_assets(assets): @@ -112,6 +114,9 @@ class JMSInventory: 'secret': account.secret, 'secret_type': account.secret_type } if account else None } + if self.unique_host_name: + host['name'] += '({})'.format(asset.id) + if host['jms_account'] and asset.platform.type == 'oracle': host['jms_account']['mode'] = 'sysdba' if account.privileged else None @@ -194,7 +199,7 @@ class JMSInventory: print(_("Skip hosts below:")) for i, host in enumerate(exclude_hosts, start=1): print("{}: [{}] \t{}".format(i, host['name'], host['error'])) - + self.exclude_hosts[host['name']] = host['error'] hosts = list(filter(lambda x: not x.get('error'), hosts)) data = {'all': {'hosts': {}}} for host in hosts: diff --git a/apps/ops/api/job.py b/apps/ops/api/job.py index ee518c008..bfaccbe39 100644 --- a/apps/ops/api/job.py +++ b/apps/ops/api/job.py @@ -1,12 +1,12 @@ from rest_framework.views import APIView - +from django.shortcuts import get_object_or_404 from rest_framework.response import Response from ops.api.base import SelfBulkModelViewSet from ops.models import Job, JobExecution from ops.serializers.job import JobSerializer, JobExecutionSerializer -__all__ = ['JobViewSet', 'JobExecutionViewSet', 'JobRunVariableHelpAPIView'] +__all__ = ['JobViewSet', 'JobExecutionViewSet', 'JobRunVariableHelpAPIView', 'JobAssetDetail'] from ops.tasks import run_ops_job_execution from ops.variables import JMS_JOB_VARIABLE_HELP @@ -73,3 +73,14 @@ class JobRunVariableHelpAPIView(APIView): def get(self, request, **kwargs): return Response(data=JMS_JOB_VARIABLE_HELP) + + +class JobAssetDetail(APIView): + rbac_perms = () + permission_classes = () + + def get(self, request, **kwargs): + execution_id = request.query_params.get('execution_id') + if execution_id: + execution = get_object_or_404(JobExecution, id=execution_id) + return Response(data=execution.assent_result_detail) diff --git a/apps/ops/models/job.py b/apps/ops/models/job.py index e96801535..75592d66f 100644 --- a/apps/ops/models/job.py +++ b/apps/ops/models/job.py @@ -88,7 +88,7 @@ class Job(JMSBaseModel, PeriodTaskModelMixin): @property def inventory(self): - return JMSInventory(self.assets.all(), self.runas_policy, self.runas) + return JMSInventory(self.assets.all(), self.runas_policy, self.runas, unique_host_name=True) def create_execution(self): return self.executions.create() @@ -110,6 +110,55 @@ class JobExecution(JMSBaseModel): 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 count(self): + if self.is_finished and not self.summary.get('error', None): + return { + "ok": len(self.summary['ok']), + "failed": len(self.summary['failures']) + len(self.summary['dark']), + "excludes": len(self.summary['excludes']), + "total": self.job.assets.count() + } + + @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.job.assets.all(): + asset_detail = { + "name": asset.name, + "status": "ok", + "tasks": [], + } + host_name = "{}({})".format(asset.name, asset.id) + if self.summary["excludes"].get(host_name, None): + asset_detail.update({"status": "excludes"}) + result["detail"].append(asset_detail) + break + if self.result["dark"].get(host_name, None): + asset_detail.update({"status": "failed"}) + for key, task in self.result["dark"][host_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(host_name, None): + asset_detail.update({"status": "failed"}) + for key, task in self.result["failures"][host_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(host_name, None): + for key, task in self.result["ok"][host_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 self.job.type @@ -124,6 +173,11 @@ class JobExecution(JMSBaseModel): def get_runner(self): inv = self.job.inventory inv.write_to_file(self.inventory_path) + if len(inv.exclude_hosts) > 0: + self.summary['excludes'] = inv.exclude_hosts + self.result['excludes'] = inv.exclude_hosts + self.save() + if isinstance(self.parameters, str): extra_vars = json.loads(self.parameters) else: @@ -191,7 +245,7 @@ class JobExecution(JMSBaseModel): def set_error(self, error): this = self.__class__.objects.get(id=self.id) # 重新获取一次,避免数据库超时连接超时 this.status = 'failed' - this.summary['error'] = str(error) + this.summary.update({'error': str(error)}) this.finish_task() def set_result(self, cb): @@ -200,8 +254,8 @@ class JobExecution(JMSBaseModel): } this = self.__class__.objects.get(id=self.id) this.status = status_mapper.get(cb.status, cb.status) - this.summary = cb.summary - this.result = cb.result + this.summary.update(cb.summary) + this.result.update(cb.result) this.finish_task() def finish_task(self): diff --git a/apps/ops/serializers/job.py b/apps/ops/serializers/job.py index 1ac944d42..d5999a8df 100644 --- a/apps/ops/serializers/job.py +++ b/apps/ops/serializers/job.py @@ -28,10 +28,13 @@ class JobSerializer(serializers.ModelSerializer, PeriodTaskSerializerMixin): class JobExecutionSerializer(serializers.ModelSerializer): creator = ReadableHiddenField(default=serializers.CurrentUserDefault()) job_type = serializers.ReadOnlyField(label=_("Job type")) + count = serializers.ReadOnlyField(label=_("Count")) class Meta: model = JobExecution - read_only_fields = ["id", "task_id", "timedelta", "time_cost", 'is_finished', 'date_start', 'date_created', + read_only_fields = ["id", "task_id", "timedelta", "count", "time_cost", 'is_finished', 'date_start', + 'date_finished', + 'date_created', 'is_success', 'task_id', 'short_id', 'job_type', 'creator'] fields = read_only_fields + [ "job", "parameters" diff --git a/apps/ops/urls/api_urls.py b/apps/ops/urls/api_urls.py index a0c2754cf..6fad435bf 100644 --- a/apps/ops/urls/api_urls.py +++ b/apps/ops/urls/api_urls.py @@ -24,7 +24,7 @@ router.register(r'task-executions', api.CeleryTaskExecutionViewSet, 'task-execut urlpatterns = [ path('variables/help/', api.JobRunVariableHelpAPIView.as_view(), name='variable-help'), - + path('job-execution/asset-detail/', api.JobAssetDetail.as_view(), name='asset-detail'), path('ansible/job-execution//log/', api.AnsibleTaskLogApi.as_view(), name='job-execution-log'), path('celery/task//task-execution//log/', api.CeleryTaskExecutionLogApi.as_view(),