diff --git a/apps/ops/api/playbook.py b/apps/ops/api/playbook.py index aaafacd58..c51868c24 100644 --- a/apps/ops/api/playbook.py +++ b/apps/ops/api/playbook.py @@ -1,13 +1,19 @@ import os +import shutil import zipfile from django.conf import settings +from django.shortcuts import get_object_or_404 + from orgs.mixins.api import OrgBulkModelViewSet from ..exception import PlaybookNoValidEntry from ..models import Playbook from ..serializers.playbook import PlaybookSerializer -__all__ = ["PlaybookViewSet"] +__all__ = ["PlaybookViewSet", "PlaybookFileBrowserAPIView"] + +from rest_framework.views import APIView +from rest_framework.response import Response def unzip_playbook(src, dist): @@ -31,12 +37,175 @@ class PlaybookViewSet(OrgBulkModelViewSet): def perform_create(self, serializer): instance = serializer.save() - src_path = os.path.join(settings.MEDIA_ROOT, instance.path.name) - dest_path = os.path.join(settings.DATA_DIR, "ops", "playbook", instance.id.__str__()) - unzip_playbook(src_path, dest_path) - valid_entry = ('main.yml', 'main.yaml', 'main') - for f in os.listdir(dest_path): - if f in valid_entry: - return - os.remove(dest_path) - raise PlaybookNoValidEntry + if instance.create_method == 'blank': + dest_path = os.path.join(settings.DATA_DIR, "ops", "playbook", instance.id.__str__()) + os.makedirs(dest_path) + with open(os.path.join(dest_path, 'main.yml'), 'w') as f: + f.write('## write your playbook here') + + if instance.create_method == 'upload': + src_path = os.path.join(settings.MEDIA_ROOT, instance.path.name) + dest_path = os.path.join(settings.DATA_DIR, "ops", "playbook", instance.id.__str__()) + unzip_playbook(src_path, dest_path) + valid_entry = ('main.yml', 'main.yaml', 'main') + for f in os.listdir(dest_path): + if f in valid_entry: + return + os.remove(dest_path) + raise PlaybookNoValidEntry + + +class PlaybookFileBrowserAPIView(APIView): + rbac_perms = () + permission_classes = () + + def get(self, request, **kwargs): + playbook_id = kwargs.get('pk') + playbook = get_object_or_404(Playbook, id=playbook_id) + work_path = playbook.work_dir + file_key = request.query_params.get('key', '') + if file_key: + file_path = os.path.join(work_path, file_key) + with open(file_path, 'r') as f: + content = f.read() + return Response({'content': content}) + else: + expand_key = request.query_params.get('expand', '') + nodes = self.generate_tree(playbook, work_path, expand_key) + return Response(nodes) + + def post(self, request, **kwargs): + playbook_id = kwargs.get('pk') + playbook = get_object_or_404(Playbook, id=playbook_id) + work_path = playbook.work_dir + + parent_key = request.data.get('key', '') + if parent_key == 'root': + parent_key = '' + if os.path.dirname(parent_key) == 'root': + parent_key = os.path.basename(parent_key) + full_path = os.path.join(work_path, parent_key) + + is_directory = request.data.get('is_directory', False) + content = request.data.get('content', '') + + if is_directory: + new_file_path = os.path.join(full_path, 'new_dir') + i = 0 + while os.path.exists(new_file_path): + i += 1 + new_file_path = os.path.join(full_path, 'new_dir({})'.format(i)) + os.makedirs(new_file_path) + else: + new_file_path = os.path.join(full_path, 'new_file.yml') + i = 0 + while os.path.exists(new_file_path): + i += 1 + new_file_path = os.path.join(full_path, 'new_file({}).yml'.format(i)) + with open(new_file_path, 'w') as f: + f.write(content) + + relative_path = os.path.relpath(os.path.dirname(new_file_path), work_path) + new_node = { + "name": os.path.basename(new_file_path), + "title": os.path.basename(new_file_path), + "id": os.path.join(relative_path, os.path.basename(new_file_path)) + if not os.path.join(relative_path, os.path.basename(new_file_path)).startswith('.') + else os.path.basename(new_file_path), + "isParent": is_directory, + "pId": relative_path if not relative_path.startswith('.') else 'root', + "open": True, + } + if not is_directory: + new_node['iconSkin'] = 'file' + + return Response(new_node) + + def patch(self, request, **kwargs): + playbook_id = kwargs.get('pk') + playbook = get_object_or_404(Playbook, id=playbook_id) + work_path = playbook.work_dir + + file_key = request.data.get('key', '') + if os.path.dirname(file_key) == 'root': + file_key = os.path.basename(file_key) + + new_name = request.data.get('new_name', '') + content = request.data.get('content', '') + is_directory = request.data.get('is_directory', False) + + if not file_key or file_key == 'root': + return Response(status=400) + file_path = os.path.join(work_path, file_key) + + if new_name: + new_file_path = os.path.join(os.path.dirname(file_path), new_name) + os.rename(file_path, new_file_path) + file_path = new_file_path + + if not is_directory and content: + with open(file_path, 'w') as f: + f.write(content) + return Response({'msg': 'ok'}) + + def delete(self, request, **kwargs): + not_delete_allowed = ['root', 'main.yml'] + playbook_id = kwargs.get('pk') + playbook = get_object_or_404(Playbook, id=playbook_id) + work_path = playbook.work_dir + file_key = request.query_params.get('key', '') + if not file_key: + return Response(status=400) + if file_key in not_delete_allowed: + return Response(status=400) + file_path = os.path.join(work_path, file_key) + if os.path.isdir(file_path): + shutil.rmtree(file_path) + else: + os.remove(file_path) + return Response({'msg': 'ok'}) + + @staticmethod + def generate_tree(playbook, root_path, expand_key=None): + nodes = [{ + "name": playbook.name, + "title": playbook.name, + "id": 'root', + "isParent": True, + "open": True, + "pId": '', + "temp": False + }] + for path, dirs, files in os.walk(root_path): + dirs.sort() + files.sort() + + relative_path = os.path.relpath(path, root_path) + for d in dirs: + node = { + "name": d, + "title": d, + "id": os.path.join(relative_path, d) if not os.path.join(relative_path, d).startswith( + '.') else d, + "isParent": True, + "open": False, + "pId": relative_path if not relative_path.startswith('.') else 'root', + "temp": False + } + if expand_key == node['id']: + node['open'] = True + nodes.append(node) + for f in files: + node = { + "name": f, + "title": f, + "iconSkin": 'file', + "id": os.path.join(relative_path, f) if not os.path.join(relative_path, f).startswith( + '.') else f, + "isParent": False, + "open": False, + "pId": relative_path if not relative_path.startswith('.') else 'root', + "temp": False + } + nodes.append(node) + return nodes diff --git a/apps/ops/const.py b/apps/ops/const.py index 8838c31c5..fb394b6bf 100644 --- a/apps/ops/const.py +++ b/apps/ops/const.py @@ -29,6 +29,11 @@ DEFAULT_PASSWORD_RULES = { } +class CreateMethods(models.TextChoices): + blank = 'blank', _('Blank') + vcs = 'vcs', _('VCS') + + class Types(models.TextChoices): adhoc = 'adhoc', _('Adhoc') playbook = 'playbook', _('Playbook') diff --git a/apps/ops/migrations/0025_auto_20230117_1130.py b/apps/ops/migrations/0025_auto_20230117_1130.py new file mode 100644 index 000000000..b6d3881e7 --- /dev/null +++ b/apps/ops/migrations/0025_auto_20230117_1130.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.14 on 2023-01-17 03:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ops', '0024_alter_celerytask_date_last_publish'), + ] + + operations = [ + migrations.AddField( + model_name='playbook', + name='create_method', + field=models.CharField(choices=[('blank', 'Blank'), ('upload', 'Upload'), ('vcs', 'VCS')], default='blank', max_length=128, verbose_name='CreateMethod'), + ), + migrations.AddField( + model_name='playbook', + name='vcs_url', + field=models.CharField(blank=True, default='', max_length=1024, null=True, verbose_name='VCS URL'), + ), + ] diff --git a/apps/ops/models/playbook.py b/apps/ops/models/playbook.py index f92968762..0e649532e 100644 --- a/apps/ops/models/playbook.py +++ b/apps/ops/models/playbook.py @@ -5,6 +5,7 @@ from django.conf import settings from django.db import models from django.utils.translation import gettext_lazy as _ +from ops.const import CreateMethods from ops.exception import PlaybookNoValidEntry from orgs.mixins.models import JMSOrgBaseModel @@ -15,12 +16,20 @@ class Playbook(JMSOrgBaseModel): path = models.FileField(upload_to='playbooks/') creator = models.ForeignKey('users.User', verbose_name=_("Creator"), on_delete=models.SET_NULL, null=True) comment = models.CharField(max_length=1024, default='', verbose_name=_('Comment'), null=True, blank=True) + create_method = models.CharField(max_length=128, choices=CreateMethods.choices, default=CreateMethods.blank, + verbose_name=_('CreateMethod')) + vcs_url = models.CharField(max_length=1024, default='', verbose_name=_('VCS URL'), null=True, blank=True) @property def entry(self): - work_dir = os.path.join(settings.DATA_DIR, "ops", "playbook", self.id.__str__()) + work_dir = self.work_dir valid_entry = ('main.yml', 'main.yaml', 'main') for f in os.listdir(work_dir): if f in valid_entry: return os.path.join(work_dir, f) raise PlaybookNoValidEntry + + @property + def work_dir(self): + work_dir = os.path.join(settings.DATA_DIR, "ops", "playbook", self.id.__str__()) + return work_dir diff --git a/apps/ops/serializers/playbook.py b/apps/ops/serializers/playbook.py index a69bab3c9..3429766d5 100644 --- a/apps/ops/serializers/playbook.py +++ b/apps/ops/serializers/playbook.py @@ -27,5 +27,5 @@ class PlaybookSerializer(BulkOrgResourceModelSerializer): model = Playbook read_only_fields = ["id", "date_created", "date_updated"] fields = read_only_fields + [ - "id", 'path', "name", "comment", "creator", + "id", 'path', "name", "comment", "creator", 'create_method', 'vcs_url', ] diff --git a/apps/ops/urls/api_urls.py b/apps/ops/urls/api_urls.py index b0a4ccb14..e637d84fb 100644 --- a/apps/ops/urls/api_urls.py +++ b/apps/ops/urls/api_urls.py @@ -23,6 +23,7 @@ router.register(r'tasks', api.CeleryTaskViewSet, 'task') router.register(r'task-executions', api.CeleryTaskExecutionViewSet, 'task-executions') urlpatterns = [ + path('playbook//file/', api.PlaybookFileBrowserAPIView.as_view(), name='playbook-file'), path('variables/help/', api.JobRunVariableHelpAPIView.as_view(), name='variable-help'), path('job-execution/asset-detail/', api.JobAssetDetail.as_view(), name='asset-detail'), path('job-execution/task-detail//', api.JobExecutionTaskDetail.as_view(), name='task-detail'),