mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-09-13 13:59:17 +00:00
feat: Supports running adhoc,playbook with variable (#14417)
* perf:Create a job that supports adding node parameters * feat: add variable model * feat: Modify Variable and AdHoc models, * feat: Parameters can be set when running job * feat: Supports setting variable type * feat: Supports running adhoc with parameters * feat: Supports running playbook with parameters * fix: Translate * feat: Support setting variables for scheduled tasks * perf: Translate --------- Co-authored-by: wangruidong <940853815@qq.com>
This commit is contained in:
@@ -78,7 +78,7 @@ class AdHocRunner:
|
||||
|
||||
|
||||
class PlaybookRunner:
|
||||
def __init__(self, inventory, playbook, project_dir='/tmp/', callback=None):
|
||||
def __init__(self, inventory, playbook, project_dir='/tmp/', callback=None, extra_vars=None, ):
|
||||
|
||||
self.id = uuid.uuid4()
|
||||
self.inventory = inventory
|
||||
@@ -89,6 +89,9 @@ class PlaybookRunner:
|
||||
self.cb = callback
|
||||
self.isolate = True
|
||||
self.envs = {}
|
||||
if extra_vars is None:
|
||||
extra_vars = {}
|
||||
self.extra_vars = extra_vars
|
||||
|
||||
def copy_playbook(self):
|
||||
entry = os.path.basename(self.playbook)
|
||||
@@ -119,6 +122,7 @@ class PlaybookRunner:
|
||||
status_handler=self.cb.status_handler,
|
||||
host_cwd=self.project_dir,
|
||||
envvars=self.envs,
|
||||
extravars=self.extra_vars,
|
||||
**kwargs
|
||||
)
|
||||
return self.cb
|
||||
|
@@ -4,3 +4,4 @@ from .adhoc import *
|
||||
from .celery import *
|
||||
from .job import *
|
||||
from .playbook import *
|
||||
from .variable import *
|
||||
|
@@ -22,6 +22,7 @@ from ops.models import Job, JobExecution
|
||||
from ops.serializers.job import (
|
||||
JobSerializer, JobExecutionSerializer, FileSerializer, JobTaskStopSerializer
|
||||
)
|
||||
from ops.utils import merge_nodes_and_assets
|
||||
|
||||
__all__ = [
|
||||
'JobViewSet', 'JobExecutionViewSet', 'JobRunVariableHelpAPIView', 'JobExecutionTaskDetail', 'UsernameHintsAPI'
|
||||
@@ -36,8 +37,6 @@ from accounts.models import Account
|
||||
from assets.const import Protocol
|
||||
from perms.const import ActionChoices
|
||||
from perms.utils.asset_perm import PermAssetDetailUtil
|
||||
from perms.models import PermNode
|
||||
from perms.utils import UserPermAssetUtil
|
||||
from jumpserver.settings import get_file_md5
|
||||
|
||||
|
||||
@@ -47,26 +46,12 @@ def set_task_to_serializer_data(serializer, task_id):
|
||||
setattr(serializer, "_data", data)
|
||||
|
||||
|
||||
def merge_nodes_and_assets(nodes, assets, user):
|
||||
if not nodes:
|
||||
return assets
|
||||
perm_util = UserPermAssetUtil(user=user)
|
||||
for node_id in nodes:
|
||||
if node_id == PermNode.FAVORITE_NODE_KEY:
|
||||
node_assets = perm_util.get_favorite_assets()
|
||||
elif node_id == PermNode.UNGROUPED_NODE_KEY:
|
||||
node_assets = perm_util.get_ungroup_assets()
|
||||
else:
|
||||
_, node_assets = perm_util.get_node_all_assets(node_id)
|
||||
assets.extend(node_assets.exclude(id__in=[asset.id for asset in assets]))
|
||||
return assets
|
||||
|
||||
|
||||
class JobViewSet(OrgBulkModelViewSet):
|
||||
serializer_class = JobSerializer
|
||||
filterset_fields = ('name', 'type')
|
||||
search_fields = ('name', 'comment')
|
||||
model = Job
|
||||
_parameters = None
|
||||
|
||||
def check_permissions(self, request):
|
||||
# job: upload_file
|
||||
@@ -106,10 +91,10 @@ class JobViewSet(OrgBulkModelViewSet):
|
||||
|
||||
def perform_create(self, serializer):
|
||||
run_after_save = serializer.validated_data.pop('run_after_save', False)
|
||||
node_ids = serializer.validated_data.pop('nodes', [])
|
||||
assets = serializer.validated_data.get('assets')
|
||||
assets = merge_nodes_and_assets(node_ids, assets, self.request.user)
|
||||
serializer.validated_data['assets'] = assets
|
||||
self._parameters = serializer.validated_data.pop('parameters', None)
|
||||
nodes = serializer.validated_data.pop('nodes', [])
|
||||
assets = serializer.validated_data.get('assets', [])
|
||||
assets = merge_nodes_and_assets(nodes, assets, self.request.user)
|
||||
if serializer.validated_data.get('type') == Types.upload_file:
|
||||
account_name = serializer.validated_data.get('runas')
|
||||
self.check_upload_permission(assets, account_name)
|
||||
@@ -126,6 +111,8 @@ class JobViewSet(OrgBulkModelViewSet):
|
||||
|
||||
def run_job(self, job, serializer):
|
||||
execution = job.create_execution()
|
||||
if self._parameters:
|
||||
execution.parameters = JobExecutionSerializer.validate_parameters(self._parameters)
|
||||
execution.creator = self.request.user
|
||||
execution.save()
|
||||
|
||||
@@ -300,7 +287,7 @@ class UsernameHintsAPI(APIView):
|
||||
permission_classes = [IsValidUser]
|
||||
|
||||
def post(self, request, **kwargs):
|
||||
node_ids = request.data.get('nodes', None)
|
||||
node_ids = request.data.get('nodes', [])
|
||||
asset_ids = request.data.get('assets', [])
|
||||
query = request.data.get('query', None)
|
||||
|
||||
|
25
apps/ops/api/variable.py
Normal file
25
apps/ops/api/variable.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
|
||||
from common.api.generic import JMSModelViewSet
|
||||
from common.const.http import OPTIONS, GET
|
||||
from common.permissions import IsValidUser
|
||||
from ..models import Variable
|
||||
from ..serializers import VariableSerializer, VariableFormDataSerializer
|
||||
|
||||
__all__ = [
|
||||
'VariableViewSet'
|
||||
]
|
||||
|
||||
|
||||
class VariableViewSet(JMSModelViewSet):
|
||||
queryset = Variable.objects.all()
|
||||
serializer_class = VariableSerializer
|
||||
http_method_names = ['options', 'get']
|
||||
|
||||
@action(methods=[GET], detail=False, serializer_class=VariableFormDataSerializer,
|
||||
permission_classes=[IsValidUser, ], url_path='form_data')
|
||||
def form_data(self, request, *args, **kwargs):
|
||||
# 只是为了动态返回serializer fields info
|
||||
return Response({})
|
@@ -85,3 +85,8 @@ COMMAND_EXECUTION_DISABLED = _('Command execution disabled')
|
||||
class Scope(models.TextChoices):
|
||||
public = 'public', pgettext_lazy("scope", 'Public')
|
||||
private = 'private', _('Private')
|
||||
|
||||
|
||||
class FieldType(models.TextChoices):
|
||||
text = 'text', _('Text')
|
||||
select = 'select', _('Select')
|
||||
|
28
apps/ops/migrations/0004_job_nodes_alter_job_assets.py
Normal file
28
apps/ops/migrations/0004_job_nodes_alter_job_assets.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 4.1.13 on 2024-10-21 08:02
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('assets', '0006_database_pg_ssl_mode'),
|
||||
('ops', '0003_alter_adhoc_unique_together_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='job',
|
||||
name='nodes',
|
||||
field=models.ManyToManyField(blank=True, to='assets.node', verbose_name='Node'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='job',
|
||||
name='assets',
|
||||
field=models.ManyToManyField(blank=True, to='assets.asset', verbose_name='Assets'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='job',
|
||||
unique_together={('name', 'org_id', 'creator', 'type')},
|
||||
),
|
||||
]
|
@@ -0,0 +1,53 @@
|
||||
# Generated by Django 4.1.13 on 2024-10-30 09:38
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('ops', '0004_job_nodes_alter_job_assets'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='historicaljob',
|
||||
name='periodic_variable',
|
||||
field=models.JSONField(default=dict, verbose_name='Periodic variable'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='job',
|
||||
name='periodic_variable',
|
||||
field=models.JSONField(default=dict, verbose_name='Periodic variable'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Variable',
|
||||
fields=[
|
||||
('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')),
|
||||
('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')),
|
||||
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
|
||||
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
|
||||
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=1024, null=True, verbose_name='Name')),
|
||||
('var_name', models.CharField(help_text="The variable name used in the script has a fixed prefix 'jms_' followed by the input variable name. For example, if the variable name is 'name,' the final generated environment variable will be 'jms_name'.", max_length=1024, null=True, verbose_name='Variable name')),
|
||||
('default_value', models.CharField(max_length=2048, null=True, verbose_name='Default Value')),
|
||||
('type', models.CharField(default='text', max_length=64, verbose_name='Variable type')),
|
||||
('tips', models.CharField(blank=True, default='', max_length=1024, null=True, verbose_name='Tips')),
|
||||
('required', models.BooleanField(default=False, verbose_name='Required')),
|
||||
('extra_args', models.JSONField(default=dict, verbose_name='ExtraVars')),
|
||||
('adhoc', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='variable', to='ops.adhoc', verbose_name='Adhoc')),
|
||||
('creator', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Creator')),
|
||||
('job', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='variable', to='ops.job', verbose_name='Job')),
|
||||
('playbook', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='variable', to='ops.playbook', verbose_name='Playbook')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Variable',
|
||||
'ordering': ['date_created'],
|
||||
},
|
||||
),
|
||||
]
|
@@ -5,3 +5,4 @@ from .adhoc import *
|
||||
from .celery import *
|
||||
from .playbook import *
|
||||
from .job import *
|
||||
from .variable import *
|
||||
|
@@ -29,6 +29,7 @@ from ops.ansible.exception import CommandInBlackListException
|
||||
from ops.mixin import PeriodTaskModelMixin
|
||||
from ops.variables import *
|
||||
from ops.const import Types, RunasPolicies, JobStatus, JobModules
|
||||
from ops.utils import merge_nodes_and_assets
|
||||
from orgs.mixins.models import JMSOrgBaseModel
|
||||
from perms.models import AssetPermission
|
||||
from perms.utils import UserPermAssetUtil
|
||||
@@ -50,11 +51,13 @@ def get_parent_keys(key, include_self=True):
|
||||
class JMSPermedInventory(JMSInventory):
|
||||
def __init__(self,
|
||||
assets,
|
||||
nodes,
|
||||
account_policy='privileged_first',
|
||||
account_prefer='root,Administrator',
|
||||
module=None,
|
||||
host_callback=None,
|
||||
user=None):
|
||||
assets = merge_nodes_and_assets(list(nodes), list(assets), user)
|
||||
super().__init__(assets, account_policy, account_prefer, host_callback, exclude_localhost=True)
|
||||
self.user = user
|
||||
self.module = module
|
||||
@@ -149,9 +152,11 @@ class Job(JMSOrgBaseModel, PeriodTaskModelMixin):
|
||||
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"))
|
||||
assets = models.ManyToManyField('assets.Asset', blank=True, verbose_name=_("Assets"))
|
||||
nodes = models.ManyToManyField('assets.Node', blank=True, verbose_name=_("Node"))
|
||||
use_parameter_define = models.BooleanField(default=False, verbose_name=(_('Use Parameter Define')))
|
||||
parameters_define = models.JSONField(default=dict, verbose_name=_('Parameters define'))
|
||||
periodic_variable = models.JSONField(default=dict, verbose_name=_('Periodic variable'))
|
||||
runas = models.CharField(max_length=128, default='root', verbose_name=_('Run as'))
|
||||
runas_policy = models.CharField(max_length=128, choices=RunasPolicies.choices, default=RunasPolicies.skip,
|
||||
verbose_name=_('Run as policy'))
|
||||
@@ -203,7 +208,7 @@ class Job(JMSOrgBaseModel, PeriodTaskModelMixin):
|
||||
|
||||
@property
|
||||
def inventory(self):
|
||||
return JMSPermedInventory(self.assets.all(),
|
||||
return JMSPermedInventory(self.assets.all(), self.nodes.all(),
|
||||
self.runas_policy, self.runas,
|
||||
user=self.creator, module=self.module)
|
||||
|
||||
@@ -220,7 +225,7 @@ class Job(JMSOrgBaseModel, PeriodTaskModelMixin):
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Job")
|
||||
unique_together = [('name', 'org_id', 'creator')]
|
||||
unique_together = [('name', 'org_id', 'creator', 'type')]
|
||||
ordering = ['date_created']
|
||||
|
||||
|
||||
@@ -328,7 +333,7 @@ class JobExecution(JMSOrgBaseModel):
|
||||
if isinstance(self.parameters, str):
|
||||
extra_vars = json.loads(self.parameters)
|
||||
else:
|
||||
extra_vars = {}
|
||||
extra_vars = self.parameters if self.parameters else {}
|
||||
static_variables = self.gather_static_variables()
|
||||
extra_vars.update(static_variables)
|
||||
|
||||
@@ -349,7 +354,8 @@ class JobExecution(JMSOrgBaseModel):
|
||||
runner = PlaybookRunner(
|
||||
self.inventory_path,
|
||||
self.current_job.playbook.entry,
|
||||
self.private_dir
|
||||
self.private_dir,
|
||||
extra_vars=extra_vars,
|
||||
)
|
||||
elif self.current_job.type == Types.upload_file:
|
||||
job_id = self.current_job.id
|
||||
|
@@ -23,8 +23,6 @@ dangerous_keywords = (
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
class Playbook(JMSBaseModel):
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
name = models.CharField(max_length=128, verbose_name=_('Name'), null=True)
|
||||
|
50
apps/ops/models/variable.py
Normal file
50
apps/ops/models/variable.py
Normal file
@@ -0,0 +1,50 @@
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from common.db.models import JMSBaseModel
|
||||
from ops.const import FieldType
|
||||
|
||||
|
||||
class Variable(JMSBaseModel):
|
||||
name = models.CharField(max_length=1024, verbose_name=_('Name'), null=True)
|
||||
var_name = models.CharField(
|
||||
max_length=1024, null=True, verbose_name=_('Variable name'),
|
||||
help_text=_("The variable name used in the script has a fixed prefix 'jms_' followed by the input variable "
|
||||
"name. For example, if the variable name is 'name,' the final generated environment variable will "
|
||||
"be 'jms_name'.")
|
||||
)
|
||||
default_value = models.CharField(max_length=2048, verbose_name=_('Default Value'), null=True)
|
||||
creator = models.ForeignKey('users.User', verbose_name=_("Creator"), on_delete=models.SET_NULL, null=True)
|
||||
type = models.CharField(max_length=64, default=FieldType.text, verbose_name=_('Variable type'))
|
||||
tips = models.CharField(max_length=1024, default='', verbose_name=_('Tips'), null=True, blank=True)
|
||||
required = models.BooleanField(default=False, verbose_name=_('Required'))
|
||||
extra_args = models.JSONField(default=dict, verbose_name=_('ExtraVars'))
|
||||
playbook = models.ForeignKey(
|
||||
'ops.Playbook', verbose_name=_("Playbook"), null=True, on_delete=models.CASCADE, related_name='variable'
|
||||
)
|
||||
adhoc = models.ForeignKey(
|
||||
'ops.AdHoc', verbose_name=_("Adhoc"), null=True, on_delete=models.CASCADE, related_name='variable'
|
||||
)
|
||||
job = models.ForeignKey('ops.Job', verbose_name=_("Job"), null=True, on_delete=models.CASCADE,
|
||||
related_name='variable')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def form_data(self):
|
||||
return {
|
||||
'var_name': self.var_name,
|
||||
'label': self.name,
|
||||
'help_text': self.tips,
|
||||
'read_only': False,
|
||||
'required': self.required,
|
||||
'type': self.type,
|
||||
'write_only': False,
|
||||
'default': self.default_value,
|
||||
'extra_args': self.extra_args,
|
||||
}
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Variable")
|
||||
ordering = ['date_created']
|
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from .celery import *
|
||||
from .variable import *
|
||||
from .adhoc import *
|
||||
|
@@ -3,17 +3,20 @@ from __future__ import unicode_literals
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.serializers.fields import ReadableHiddenField, LabeledChoiceField
|
||||
from common.serializers import WritableNestedModelSerializer
|
||||
from common.serializers.fields import ReadableHiddenField
|
||||
from common.serializers.mixin import CommonBulkModelSerializer
|
||||
from .mixin import ScopeSerializerMixin
|
||||
from ..const import Scope
|
||||
from ..models import AdHoc
|
||||
from ops.serializers import AdhocVariableSerializer
|
||||
|
||||
|
||||
class AdHocSerializer(ScopeSerializerMixin, CommonBulkModelSerializer):
|
||||
class AdHocSerializer(ScopeSerializerMixin, CommonBulkModelSerializer, WritableNestedModelSerializer):
|
||||
creator = ReadableHiddenField(default=serializers.CurrentUserDefault())
|
||||
variable = AdhocVariableSerializer(many=True, required=False, allow_null=True, label=_('Variable'))
|
||||
|
||||
class Meta:
|
||||
model = AdHoc
|
||||
read_only_field = ["id", "creator", "date_created", "date_updated", "created_by"]
|
||||
fields = read_only_field + ["id", "name", "scope", "module", "args", "comment"]
|
||||
fields_m2m = ['variable']
|
||||
fields = read_only_field + fields_m2m + ["id", "name", "scope", "module", "args", "comment"]
|
||||
|
@@ -3,20 +3,25 @@ import uuid
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from assets.models import Asset
|
||||
from common.serializers.fields import ReadableHiddenField
|
||||
from assets.models import Asset, Node
|
||||
from common.serializers import WritableNestedModelSerializer
|
||||
from common.serializers.fields import ReadableHiddenField, ObjectRelatedField
|
||||
from ops.mixin import PeriodTaskSerializerMixin
|
||||
from ops.models import Job, JobExecution
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from ops.serializers import JobVariableSerializer
|
||||
|
||||
|
||||
class JobSerializer(BulkOrgResourceModelSerializer, PeriodTaskSerializerMixin):
|
||||
class JobSerializer(BulkOrgResourceModelSerializer, PeriodTaskSerializerMixin, WritableNestedModelSerializer):
|
||||
creator = ReadableHiddenField(default=serializers.CurrentUserDefault())
|
||||
run_after_save = serializers.BooleanField(label=_("Execute after saving"), default=False, required=False)
|
||||
nodes = serializers.ListField(required=False, child=serializers.CharField())
|
||||
date_last_run = serializers.DateTimeField(label=_('Date last run'), read_only=True)
|
||||
name = serializers.CharField(label=_('Name'), max_length=128, allow_blank=True, required=False)
|
||||
assets = serializers.PrimaryKeyRelatedField(label=_('Assets'), queryset=Asset.objects, many=True, required=False)
|
||||
nodes = ObjectRelatedField(label=_('Nodes'), queryset=Node.objects, many=True, required=False)
|
||||
variable = JobVariableSerializer(many=True, required=False, allow_null=True, label=_('Variable'))
|
||||
parameters = serializers.JSONField(label=_('Parameters'), default={}, write_only=True, required=False,
|
||||
allow_null=True)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
instant = data.get('instant', False)
|
||||
@@ -39,6 +44,7 @@ class JobSerializer(BulkOrgResourceModelSerializer, PeriodTaskSerializerMixin):
|
||||
"id", "date_last_run", "date_created",
|
||||
"date_updated", "average_time_cost"
|
||||
]
|
||||
fields_m2m = ['variable']
|
||||
fields = read_only_fields + [
|
||||
"name", "instant", "type", "module",
|
||||
"args", "playbook", "assets",
|
||||
@@ -46,8 +52,8 @@ class JobSerializer(BulkOrgResourceModelSerializer, PeriodTaskSerializerMixin):
|
||||
"use_parameter_define", "parameters_define",
|
||||
"timeout", "chdir", "comment", "summary",
|
||||
"is_periodic", "interval", "crontab", "nodes",
|
||||
"run_after_save"
|
||||
]
|
||||
"run_after_save", "parameters", "periodic_variable"
|
||||
] + fields_m2m
|
||||
extra_kwargs = {
|
||||
'average_time_cost': {'label': _('Duration')},
|
||||
}
|
||||
@@ -97,3 +103,13 @@ class JobExecutionSerializer(BulkOrgResourceModelSerializer):
|
||||
if job_obj.creator != self.context['request'].user:
|
||||
raise serializers.ValidationError(_("You do not have permission for the current job."))
|
||||
return job_obj
|
||||
|
||||
@staticmethod
|
||||
def validate_parameters(parameters):
|
||||
prefix = "jms_"
|
||||
new_parameters = {}
|
||||
for key, value in parameters.items():
|
||||
if not key.startswith("jms_"):
|
||||
key = prefix + key
|
||||
new_parameters[key] = value
|
||||
return new_parameters
|
||||
|
@@ -1,11 +1,13 @@
|
||||
import os
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.serializers import WritableNestedModelSerializer
|
||||
from common.serializers.fields import ReadableHiddenField
|
||||
from common.serializers.mixin import CommonBulkModelSerializer
|
||||
from ops.models import Playbook
|
||||
from .mixin import ScopeSerializerMixin
|
||||
from ops.serializers.variable import PlaybookVariableSerializer
|
||||
|
||||
|
||||
def parse_playbook_name(path):
|
||||
@@ -13,10 +15,11 @@ def parse_playbook_name(path):
|
||||
return file_name.split(".")[-2]
|
||||
|
||||
|
||||
class PlaybookSerializer(ScopeSerializerMixin, CommonBulkModelSerializer):
|
||||
class PlaybookSerializer(ScopeSerializerMixin, CommonBulkModelSerializer, WritableNestedModelSerializer):
|
||||
creator = ReadableHiddenField(default=serializers.CurrentUserDefault())
|
||||
path = serializers.FileField(required=False)
|
||||
|
||||
variable = PlaybookVariableSerializer(many=True, required=False, allow_null=True, label=_('Variable'))
|
||||
|
||||
def to_internal_value(self, data):
|
||||
name = data.get('name', False)
|
||||
if not name and data.get('path'):
|
||||
@@ -26,7 +29,8 @@ class PlaybookSerializer(ScopeSerializerMixin, CommonBulkModelSerializer):
|
||||
class Meta:
|
||||
model = Playbook
|
||||
read_only_fields = ["id", "date_created", "date_updated", "created_by"]
|
||||
fields = read_only_fields + [
|
||||
fields_m2m = ['variable']
|
||||
fields = read_only_fields + fields_m2m + [
|
||||
"id", 'path', 'scope', "name", "comment", "creator",
|
||||
'create_method', 'vcs_url',
|
||||
]
|
||||
|
143
apps/ops/serializers/variable.py
Normal file
143
apps/ops/serializers/variable.py
Normal file
@@ -0,0 +1,143 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.serializers.fields import ReadableHiddenField, LabeledChoiceField, EncryptedField
|
||||
from common.serializers.mixin import CommonBulkModelSerializer
|
||||
from ops.const import FieldType
|
||||
from ops.models import Variable, AdHoc, Job, Playbook
|
||||
|
||||
__all__ = [
|
||||
'VariableSerializer', 'AdhocVariableSerializer', 'JobVariableSerializer', 'PlaybookVariableSerializer',
|
||||
'VariableFormDataSerializer'
|
||||
]
|
||||
|
||||
|
||||
class VariableSerializer(CommonBulkModelSerializer):
|
||||
creator = ReadableHiddenField(default=serializers.CurrentUserDefault())
|
||||
type = LabeledChoiceField(
|
||||
choices=FieldType.choices, default=FieldType.text, label=_("Variable Type")
|
||||
)
|
||||
extra_args = serializers.CharField(
|
||||
max_length=1024, label=_("ExtraVars"), required=False, allow_blank=True,
|
||||
help_text=_(
|
||||
"Each item is on a separate line, with each line separated by a colon. The part before the colon is the "
|
||||
"display content, and the part after the colon is the value.")
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Variable
|
||||
read_only_fields = ["id", "date_created", "date_updated", "created_by", "creator"]
|
||||
fields = read_only_fields + [
|
||||
"name", "var_name", "type", 'required', 'default_value', 'tips', 'adhoc', 'playbook', 'job', 'form_data',
|
||||
'extra_args'
|
||||
]
|
||||
|
||||
def validate(self, attrs):
|
||||
attrs = super().validate(attrs)
|
||||
type = attrs.get('type')
|
||||
attrs['extra_args'] = {}
|
||||
if type == FieldType.text:
|
||||
attrs['default_value'] = self.initial_data.get('text_default_value')
|
||||
elif type == FieldType.select:
|
||||
attrs['default_value'] = self.initial_data.get('select_default_value')
|
||||
options = self.initial_data.get('extra_args', '')
|
||||
attrs['extra_args'] = {"options": options}
|
||||
return attrs
|
||||
|
||||
def to_representation(self, instance):
|
||||
data = super().to_representation(instance)
|
||||
if instance.type == FieldType.select:
|
||||
data['extra_args'] = instance.extra_args.get('options', '')
|
||||
data['select_default_value'] = instance.default_value
|
||||
if instance.type == FieldType.text:
|
||||
data['text_default_value'] = instance.default_value
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def setup_eager_loading(cls, queryset):
|
||||
queryset = queryset.prefetch_related('adhoc', 'job', 'playbook')
|
||||
return queryset
|
||||
|
||||
|
||||
class AdhocVariableSerializer(VariableSerializer):
|
||||
adhoc = serializers.PrimaryKeyRelatedField(queryset=AdHoc.objects, required=False)
|
||||
|
||||
class Meta(VariableSerializer.Meta):
|
||||
fields = VariableSerializer.Meta.fields
|
||||
|
||||
|
||||
class JobVariableSerializer(VariableSerializer):
|
||||
job = serializers.PrimaryKeyRelatedField(queryset=Job.objects, required=False)
|
||||
|
||||
class Meta(VariableSerializer.Meta):
|
||||
fields = VariableSerializer.Meta.fields
|
||||
|
||||
|
||||
class PlaybookVariableSerializer(VariableSerializer):
|
||||
playbook = serializers.PrimaryKeyRelatedField(queryset=Playbook.objects, required=False)
|
||||
|
||||
class Meta(VariableSerializer.Meta):
|
||||
fields = VariableSerializer.Meta.fields
|
||||
|
||||
|
||||
def create_dynamic_text_choices(options):
|
||||
"""
|
||||
动态创建一个 TextChoices 子类。`options` 应该是一个列表,
|
||||
格式为 [(value1, display1), (value2, display2), ...]
|
||||
"""
|
||||
attrs = {
|
||||
key.upper(): value for value, key in options
|
||||
}
|
||||
attrs['choices'] = options
|
||||
return type('DynamicTextChoices', (models.TextChoices,), attrs)
|
||||
|
||||
|
||||
class VariableFormDataSerializer(serializers.Serializer):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
request = self.context.get('request')
|
||||
if not request:
|
||||
return
|
||||
params = request.query_params
|
||||
job = params.get('job')
|
||||
adhoc = params.get('adhoc')
|
||||
playbook = params.get('playbook')
|
||||
if job:
|
||||
variables = Variable.objects.filter(job=job).all()
|
||||
elif adhoc:
|
||||
variables = Variable.objects.filter(adhoc=adhoc).all()
|
||||
else:
|
||||
variables = Variable.objects.filter(playbook=playbook).all()
|
||||
dynamic_fields = [var.form_data for var in variables]
|
||||
|
||||
if dynamic_fields:
|
||||
for field in dynamic_fields:
|
||||
field_type = field['type']
|
||||
required = field['required']
|
||||
var_name = field["var_name"]
|
||||
label = field["label"]
|
||||
help_text = field['help_text']
|
||||
default = field['default']
|
||||
if field_type == FieldType.text:
|
||||
self.fields[var_name] = serializers.CharField(
|
||||
max_length=1024, label=label, help_text=help_text, required=required
|
||||
)
|
||||
elif field_type == FieldType.select:
|
||||
extra_args = field.get('extra_args', {})
|
||||
options = extra_args.get('options', '').splitlines()
|
||||
|
||||
DynamicFieldType = models.TextChoices(
|
||||
'DynamicFieldType',
|
||||
{
|
||||
option.split(':')[0]: option.split(':')[1] for option in
|
||||
options
|
||||
}
|
||||
)
|
||||
self.fields[var_name] = LabeledChoiceField(
|
||||
choices=DynamicFieldType.choices, required=required, label=label,
|
||||
help_text=help_text
|
||||
)
|
||||
if required and default is not None:
|
||||
self.fields[var_name].default = default
|
@@ -10,6 +10,7 @@ from django_celery_beat.models import PeriodicTask
|
||||
from common.const.crontab import CRONTAB_AT_AM_TWO
|
||||
from common.utils import get_logger, get_object_or_none, get_log_keep_day
|
||||
from ops.celery import app
|
||||
from ops.serializers.job import JobExecutionSerializer
|
||||
from orgs.utils import tmp_to_org, tmp_to_root_org
|
||||
from .celery.decorator import (
|
||||
register_as_period_task, after_app_ready_start
|
||||
@@ -64,6 +65,8 @@ def run_ops_job(job_id):
|
||||
with tmp_to_org(job.org):
|
||||
execution = job.create_execution()
|
||||
execution.creator = job.creator
|
||||
if job.periodic_variable:
|
||||
execution.parameters = JobExecutionSerializer.validate_parameters(job.periodic_variable)
|
||||
_run_ops_job_execution(execution)
|
||||
|
||||
|
||||
|
@@ -15,8 +15,8 @@ bulk_router = BulkRouter()
|
||||
bulk_router.register(r'adhocs', api.AdHocViewSet, 'adhoc')
|
||||
bulk_router.register(r'playbooks', api.PlaybookViewSet, 'playbook')
|
||||
bulk_router.register(r'jobs', api.JobViewSet, 'job')
|
||||
bulk_router.register(r'variable', api.VariableViewSet, 'variable')
|
||||
bulk_router.register(r'job-executions', api.JobExecutionViewSet, 'job-execution')
|
||||
|
||||
router.register(r'celery/period-tasks', api.CeleryPeriodTaskViewSet, 'celery-period-task')
|
||||
|
||||
router.register(r'tasks', api.CeleryTaskViewSet, 'task')
|
||||
|
@@ -5,6 +5,9 @@ from django.conf import settings
|
||||
|
||||
from common.utils import get_logger, make_dirs
|
||||
from jumpserver.const import PROJECT_DIR
|
||||
from perms.models import PermNode
|
||||
from perms.utils import UserPermAssetUtil
|
||||
from assets.models import Asset, Node
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
@@ -29,3 +32,19 @@ def get_ansible_log_verbosity(verbosity=0):
|
||||
return 1
|
||||
return verbosity
|
||||
|
||||
|
||||
def merge_nodes_and_assets(nodes, assets, user):
|
||||
if not nodes:
|
||||
return assets
|
||||
perm_util = UserPermAssetUtil(user=user)
|
||||
for node_id in nodes:
|
||||
if isinstance(node_id, Node):
|
||||
node_id = node_id.id
|
||||
if node_id == PermNode.FAVORITE_NODE_KEY:
|
||||
node_assets = perm_util.get_favorite_assets()
|
||||
elif node_id == PermNode.UNGROUPED_NODE_KEY:
|
||||
node_assets = perm_util.get_ungroup_assets()
|
||||
else:
|
||||
_, node_assets = perm_util.get_node_all_assets(node_id)
|
||||
assets.extend(node_assets.exclude(id__in=[asset.id for asset in assets]))
|
||||
return assets
|
||||
|
Reference in New Issue
Block a user