diff --git a/.pylintrc b/.pylintrc index 665cb8aa..280b1ed0 100644 --- a/.pylintrc +++ b/.pylintrc @@ -32,7 +32,7 @@ unsafe-load-any-extension=no # A comma-separated list of package or module names from where C extensions may # be loaded. Extensions are loading into the active Python interpreter and may # run arbitrary code -extension-pkg-whitelist=vstutils.custom_model,vstutils.api.fields,vstutils.tools +extension-pkg-whitelist= # Allow optimization of some AST trees. This will activate a peephole AST # optimizer, which will apply various small optimizations. For instance, it can @@ -65,7 +65,7 @@ confidence= # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use"--disable=all --enable=classes # --disable=W" -disable=no-name-in-module,useless-super-delegation,len-as-condition,super-init-not-called,keyword-arg-before-vararg,no-else-return,no-self-argument,inconsistent-return-statements,unsubscriptable-object,too-many-branches,deprecated-lambda,old-style-class,no-init,expression-not-assigned,broad-except,logging-format-interpolation,model-no-explicit-unicode,too-many-ancestors,bad-continuation,bad-whitespace,redefined-builtin,missing-docstring,redefined-variable-type,no-self-use,line-too-long,suppressed-message,cmp-method,no-absolute-import,xrange-builtin,using-cmp-argument,basestring-builtin,backtick,unpacking-in-except,old-raise-syntax,getslice-method,long-builtin,print-statement,reduce-builtin,filter-builtin-not-iterating,import-star-module-level,unichr-builtin,dict-iter-method,range-builtin-not-iterating,file-builtin,old-division,standarderror-builtin,coerce-builtin,setslice-method,old-ne-operator,long-suffix,execfile-builtin,oct-method,metaclass-assignment,intern-builtin,apply-builtin,dict-view-method,raw_input-builtin,raising-string,coerce-method,unicode-builtin,next-method-called,hex-method,nonzero-method,round-builtin,cmp-builtin,reload-builtin,buffer-builtin,useless-suppression,zip-builtin-not-iterating,indexing-exception,map-builtin-not-iterating,delslice-method,old-octal-literal,input-builtin,parameter-unpacking,model-has-unicode,bare-except,too-few-public-methods,fixme,dangerous-default-value,attribute-defined-outside-init,pointless-string-statement,too-many-instance-attributes,arguments-differ,binary-op-exception,bad-classmethod-argument,locally-disabled,file-ignored,multiple-statements,superfluous-parens,bad-mcs-classmethod-argument,useless-object-inheritance +disable=unexpected-keyword-arg,no-name-in-module,useless-super-delegation,len-as-condition,super-init-not-called,keyword-arg-before-vararg,no-else-return,no-self-argument,inconsistent-return-statements,unsubscriptable-object,too-many-branches,deprecated-lambda,old-style-class,no-init,expression-not-assigned,broad-except,logging-format-interpolation,model-no-explicit-unicode,too-many-ancestors,bad-continuation,bad-whitespace,redefined-builtin,missing-docstring,redefined-variable-type,no-self-use,line-too-long,suppressed-message,cmp-method,no-absolute-import,xrange-builtin,using-cmp-argument,basestring-builtin,backtick,unpacking-in-except,old-raise-syntax,getslice-method,long-builtin,print-statement,reduce-builtin,filter-builtin-not-iterating,import-star-module-level,unichr-builtin,dict-iter-method,range-builtin-not-iterating,file-builtin,old-division,standarderror-builtin,coerce-builtin,setslice-method,old-ne-operator,long-suffix,execfile-builtin,oct-method,metaclass-assignment,intern-builtin,apply-builtin,dict-view-method,raw_input-builtin,raising-string,coerce-method,unicode-builtin,next-method-called,hex-method,nonzero-method,round-builtin,cmp-builtin,reload-builtin,buffer-builtin,useless-suppression,zip-builtin-not-iterating,indexing-exception,map-builtin-not-iterating,delslice-method,old-octal-literal,input-builtin,parameter-unpacking,model-has-unicode,bare-except,too-few-public-methods,fixme,dangerous-default-value,attribute-defined-outside-init,pointless-string-statement,too-many-instance-attributes,arguments-differ,binary-op-exception,bad-classmethod-argument,locally-disabled,file-ignored,multiple-statements,superfluous-parens,bad-mcs-classmethod-argument,useless-object-inheritance [REPORTS] @@ -277,13 +277,12 @@ ignore-mixin-members=yes # (useful for modules/projects where namespaces are manipulated during runtime # and thus existing member attributes cannot be deduced by static analysis. It # supports qualified module names, as well as Unix pattern matching. -ignored-modules=vstutils.tools,vstutils.custom_model,vstutils.urls,vstutils.api.base,vstutils.api.views,vstutils.gui.views,vstutils.api.permissions,vstutils.api.filters,vstutils.middleware,vstutils.environment,vstutils.utils,vstutils.exceptions,vstutils.models,vstutils.api.decorators,vstutils.api.swagger,vstutils.api.serializers, -# ignored-modules=vstutils +ignored-modules=vstutils.custom_model,vstutils.api.views, # List of class names for which member attributes should not be checked (useful # for classes with dynamically set attributes). This supports the use of # qualified names. -ignored-classes=optparse.Values,thread._local,_thread._local,vstutils.custom_model +ignored-classes=optparse.Values,thread._local,_thread._local, # List of members which are set dynamically and missed by pylint inference # system, and so shouldn't trigger E1101 when accessed. Python regular diff --git a/doc/config.rst b/doc/config.rst index 65311a81..164043a0 100644 --- a/doc/config.rst +++ b/doc/config.rst @@ -367,9 +367,9 @@ Installation of additional packages to Polemarch ------------------------------------------------ .. warning:: - .rpm or .dep installation methods are depracated. + .rpm or .deb installation methods are depracated. -If you want to install some additional package to Polemarch from .rpm or .dep, +If you want to install some additional package to Polemarch from .rpm or .deb, you should run next command: .. sourcecode:: bash diff --git a/polemarch/__init__.py b/polemarch/__init__.py index 81ad951e..2edf8bdc 100644 --- a/polemarch/__init__.py +++ b/polemarch/__init__.py @@ -31,6 +31,6 @@ "VST_ROOT_URLCONF": os.getenv("VST_ROOT_URLCONF", 'vstutils.urls'), } -__version__ = "1.4.4" +__version__ = "1.5.0" prepare_environment(**default_settings) diff --git a/polemarch/api/v2/filters.py b/polemarch/api/v2/filters.py index 9b6ed9a8..165c4be1 100644 --- a/polemarch/api/v2/filters.py +++ b/polemarch/api/v2/filters.py @@ -1,4 +1,7 @@ # pylint: disable=import-error +from functools import reduce +from operator import or_ +from django.db.models import Q from django_filters import (CharFilter, NumberFilter, IsoDateTimeFilter) from vstutils.api.filters import DefaultIDFilter, extra_filter, name_filter, filters from ...main import models @@ -15,6 +18,21 @@ def variables_filter(queryset, field, value): return queryset.var_filter(**kwargs) +def filter_name_endswith(queryset, field, value): + # pylint: disable=unused-argument + return queryset.filter( + reduce(or_, ( + Q(path__endswith='.{}'.format(v)) + for v in value.split(',') + )) + ) + + +def playbook_filter(queryset, field, value): + # pylint: disable=unused-argument + return queryset.filter(playbook__in=value.split(',')) + + class VariableFilter(DefaultIDFilter): key = CharFilter(method=name_filter, help_text=name_help.replace('name', 'key name')) value = CharFilter(method=name_filter, help_text='A value of instance.') @@ -54,11 +72,13 @@ class Meta: class ModuleFilter(filters.FilterSet): path__not = CharFilter(method=name_filter, help_text='Full path to module.') path = CharFilter(method=name_filter, help_text='Full path to module.') + name = CharFilter(method=filter_name_endswith, help_text='Name of module.') class Meta: model = models.Module fields = ( 'path', + 'name', ) @@ -115,12 +135,14 @@ class Meta: class TaskFilter(_BaseFilter): playbook__not = CharFilter(method=name_filter, help_text='Playbook filename.') playbook = CharFilter(method=name_filter, help_text='Playbook filename.') + pb_filter = CharFilter(method=playbook_filter, help_text='Playbook filename - filter for prefetch.') class Meta: model = models.Task fields = ('id', 'name', - 'playbook',) + 'playbook', + 'pb_filter',) class HistoryFilter(_BaseFilter): diff --git a/polemarch/api/v2/permissions.py b/polemarch/api/v2/permissions.py index 57e27656..09c63a43 100644 --- a/polemarch/api/v2/permissions.py +++ b/polemarch/api/v2/permissions.py @@ -8,6 +8,8 @@ def has_permission(self, request, view): def get_user_permission(self, request, view, obj): # nocv # pylint: disable=unused-argument + if hasattr(obj, 'owner') and obj.owner == request.user: + return True return False def has_object_permission(self, request, view, obj): diff --git a/polemarch/api/v2/serializers.py b/polemarch/api/v2/serializers.py index e93c6207..415c6e7d 100644 --- a/polemarch/api/v2/serializers.py +++ b/polemarch/api/v2/serializers.py @@ -171,14 +171,14 @@ class _WithPermissionsSerializer(_SignalSerializer): def is_valid(self, *args, **kwargs): result = super(_WithPermissionsSerializer, self).is_valid(*args, **kwargs) - if not hasattr(self, 'instance'): # nocv + if not hasattr(self, 'instance') or self.instance is None: # noce self.validated_data['owner'] = self.validated_data.get( 'owner', self.current_user() ) return result def current_user(self) -> User: - return self.context['request'].user # nocv + return self.context['request'].user # noce class UserSerializer(vst_serializers.UserSerializer): @@ -770,7 +770,7 @@ class Meta: 'owner',) -class ProjectCreateMasterSerializer(vst_serializers.VSTSerializer): +class ProjectCreateMasterSerializer(vst_serializers.VSTSerializer, _WithPermissionsSerializer): types = models.list_to_choices(models.Project.repo_handlers.keys()) auth_types = ['NONE', 'KEY', 'PASSWORD'] branch_auth_types = {t: "hidden" for t in models.Project.repo_handlers.keys()} @@ -884,7 +884,7 @@ def update(self, instance: models.ProjectTemplate, validated_data) -> models.Pro repo_auth=instance.repo_auth, auth_data=instance.auth_data or '', ) - serializer = ProjectCreateMasterSerializer(data=data) + serializer = ProjectCreateMasterSerializer(data=data, context=self.context) serializer.is_valid(raise_exception=True) serializer.save() return serializer.instance diff --git a/polemarch/api/v2/views.py b/polemarch/api/v2/views.py index cd1aa31c..805a4443 100644 --- a/polemarch/api/v2/views.py +++ b/polemarch/api/v2/views.py @@ -389,6 +389,7 @@ class GroupViewSet(_BaseGroupViewSet, _GroupMixin): __doc__ = _BaseGroupViewSet.__doc__ def nested_allow_check(self): + # pylint: disable=no-member exception = _BaseGroupViewSet.serializer_class_one.ValidationException if not self.nested_parent_object.children and self.nested_name == 'group': raise exception("Group is not children.") @@ -428,6 +429,7 @@ class InventoryViewSet(_GroupMixin): @deco.action(methods=["post"], detail=no) def import_inventory(self, request, **kwargs): + # pylint: disable=no-member serializer = self.get_serializer(data=request.data) serializer.is_valid(True) serializer.save() diff --git a/polemarch/main/models/__init__.py b/polemarch/main/models/__init__.py index 339f47df..2a02c8a3 100644 --- a/polemarch/main/models/__init__.py +++ b/polemarch/main/models/__init__.py @@ -7,7 +7,7 @@ from collections import OrderedDict import django_celery_beat from django_celery_beat.models import IntervalSchedule, CrontabSchedule -from django.db.models import signals, IntegerField +from django.db.models import signals, IntegerField, Q from django.dispatch import receiver from django.db.models.functions import Cast from django.core.validators import ValidationError @@ -230,6 +230,8 @@ def save_to_beat(instance: PeriodicTask, **kwargs) -> NoReturn: elif instance.type == "CRONTAB": cron_data = instance.crontab_kwargs schedule, _ = CrontabSchedule.objects.get_or_create(**cron_data) + schedule.timezone = settings.TIME_ZONE + schedule.save() manager.create(crontab=schedule, name=str(instance.id), task=task, @@ -356,3 +358,10 @@ def update_ptasks_with_templates(instance: Template, **kwargs) -> NoReturn: def cancel_task_on_delete_history(instance: History, **kwargs) -> NoReturn: exchange = KVExchanger(CmdExecutor.CANCEL_PREFIX + str(instance.id)) exchange.send(True, 60) if instance.working else None + + +@receiver(signals.post_migrate) +def update_crontab_timezone_ptasks(*args, **kwargs): + qs = CrontabSchedule.objects.exclude(timezone=settings.TIME_ZONE) + qs.filter(periodictask__name__startswith='polemarch').update(timezone=settings.TIME_ZONE) + qs.filter(periodictask__name__startswith='pmlib').update(timezone=settings.TIME_ZONE) diff --git a/polemarch/main/models/tasks.py b/polemarch/main/models/tasks.py index 4001fe92..f8fbc497 100644 --- a/polemarch/main/models/tasks.py +++ b/polemarch/main/models/tasks.py @@ -250,14 +250,17 @@ def inventory(self) -> InvOrString: @inventory.setter def inventory(self, inventory: InvOrString) -> NoReturn: - if isinstance(inventory, Inventory): - self._inventory = inventory # nocv - elif isinstance(inventory, (six.string_types, six.text_type, int)): + if isinstance(inventory, Inventory): # nocv + self._inventory = inventory + self.inventory_file = None + elif isinstance(inventory, (str, int)): try: self._inventory = self.project.inventories.get(pk=int(inventory)) + self.inventory_file = None except (ValueError, Inventory.DoesNotExist): self.project.check_path(inventory) self.inventory_file = inventory + self._inventory = None @property def crontab_kwargs(self) -> Dict: diff --git a/polemarch/main/models/utils.py b/polemarch/main/models/utils.py index 9f2533cf..a0506c94 100644 --- a/polemarch/main/models/utils.py +++ b/polemarch/main/models/utils.py @@ -13,7 +13,6 @@ from collections import namedtuple, OrderedDict from subprocess import Popen from functools import reduce -import six from django.utils import timezone from vstutils.utils import tmp_file, KVExchanger, raise_context from vstutils.tools import get_file_value @@ -109,7 +108,7 @@ def execute(self, cmd: Iterable[Text], cwd: Text): pm_ansible_path = ' '.join(self.pm_ansible()) new_cmd = list() for one_cmd in cmd: - if isinstance(one_cmd, six.string_types): + if isinstance(one_cmd, str): with raise_context(): one_cmd = one_cmd.decode('utf-8') new_cmd.append(one_cmd) @@ -145,7 +144,7 @@ def __init__(self, inventory: Union[Inventory, int, Text], cwd: Text = "/tmp", t self.tmpdir = tmpdir self._file = None self.is_file = True - if isinstance(inventory, (six.string_types, six.text_type)): + if isinstance(inventory, str): self.raw, self.keys = self.get_from_file(inventory) else: self.raw, self.keys = self.get_from_int(inventory) @@ -177,18 +176,20 @@ def file(self) -> Union[tmp_file, Text]: @property def file_name(self) -> Text: # pylint: disable=no-member - if isinstance(self.file, (six.string_types, six.text_type)): + if isinstance(self.file, str): return self.file return self.file.name def close(self) -> NoReturn: # pylint: disable=no-member map(lambda key_file: key_file.close(), self.keys) if self.keys else None - if not isinstance(self.file, (six.string_types, six.text_type)): + if not isinstance(self.file, str): self._file.close() def __init__(self, *args, **kwargs): self.args = args + if 'verbose' in kwargs: + kwargs['verbose'] = int(float(kwargs.get('verbose', 0))) self.kwargs = kwargs self.__will_raise_exception = False self.ref_type = self.ref_types[self.command_type] @@ -229,7 +230,7 @@ def __parse_key(self, key: Text, value: Text) -> Tuple[Text, List]: # pylint: disable=unused-argument, if re.match(r"[-]+BEGIN .+ KEY[-]+", value): # Add new line if not exists and generate tmpfile for private key value - value = value + '/n' if value[-1] != '/n' else value + value = value + '\n' if value[-1] != '\n' else value return self.__generate_arg_file(value) # Return path in project if it's path path = (Path(self.workdir)/Path(value).expanduser()).resolve() @@ -436,5 +437,5 @@ def __init__(self, target: Text, *pargs, **kwargs): super(AnsibleModule, self).__init__(*pargs, **kwargs) self.ansible_ref['module-name'] = {'type': 'string'} - def execute(self, group: Text, *args, **extra_args): + def execute(self, group: Text = 'all', *args, **extra_args): return super(AnsibleModule, self).execute(group, *args, **extra_args) diff --git a/polemarch/main/settings.py b/polemarch/main/settings.py index 7eebf4c7..94ecd1de 100644 --- a/polemarch/main/settings.py +++ b/polemarch/main/settings.py @@ -17,7 +17,7 @@ ] # Additional middleware and auth -MIDDLEWARE_CLASSES += [ +MIDDLEWARE += [ '{}.main.middleware.PolemarchHeadersMiddleware'.format(VST_PROJECT_LIB_NAME), ] @@ -146,52 +146,73 @@ # Polemarch handlers # Repos -class GitSectionConfig(SectionConfig): - section = 'git' - subsections = ['clone', 'fetch'] - section_defaults = { - 'fetch': { - "force": True, - } +class GitSection(BaseAppendSection): + pass + + +class GitFetchSection(GitSection): + types_map = { + 'all': cconfig.BoolType(), + 'append': cconfig.BoolType(), + 'multiple': cconfig.BoolType(), + 'unshallow': cconfig.BoolType(), + 'update-shallow': cconfig.BoolType(), + 'force': cconfig.BoolType(), + 'keep': cconfig.BoolType(), + 'prune': cconfig.BoolType(), + 'prune-tags': cconfig.BoolType(), + 'no-tags': cconfig.BoolType(), + 'tags': cconfig.BoolType(), + 'no-recurse-submodules': cconfig.BoolType(), + 'update-head-ok': cconfig.BoolType(), + 'quiet': cconfig.BoolType(), + 'verbose': cconfig.BoolType(), + 'ipv4': cconfig.BoolType(), + 'ipv6': cconfig.BoolType(), + 'depth': cconfig.IntType(), + 'deepen': cconfig.IntType(), + 'jobs': cconfig.IntType(), } + + def all(self): + data = super().all() + data['force'] = True + return data + + +class GitCloneSection(GitSection): types_map = { - 'fetch.all': SectionConfig.bool, - 'fetch.append': SectionConfig.bool, - 'fetch.multiple': SectionConfig.bool, - 'fetch.unshallow': SectionConfig.bool, - 'fetch.update-shallow': SectionConfig.bool, - 'fetch.force': SectionConfig.bool, - 'fetch.keep': SectionConfig.bool, - 'fetch.prune': SectionConfig.bool, - 'fetch.prune-tags': SectionConfig.bool, - 'fetch.no-tags': SectionConfig.bool, - 'fetch.tags': SectionConfig.bool, - 'fetch.no-recurse-submodules': SectionConfig.bool, - 'fetch.update-head-ok': SectionConfig.bool, - 'fetch.quiet': SectionConfig.bool, - 'fetch.verbose': SectionConfig.bool, - 'fetch.ipv4': SectionConfig.bool, - 'fetch.ipv6': SectionConfig.bool, - 'fetch.depth': SectionConfig.int, - 'fetch.deepen': SectionConfig.int, - 'fetch.jobs': SectionConfig.int, - 'clone.local': SectionConfig.bool, - 'clone.no-hardlinks': SectionConfig.bool, - 'clone.shared': SectionConfig.bool, - 'clone.dissociate': SectionConfig.bool, - 'clone.quiet': SectionConfig.bool, - 'clone.verbose': SectionConfig.bool, - 'clone.single-branch': SectionConfig.bool, - 'clone.no-single-branch': SectionConfig.bool, - 'clone.no-tags': SectionConfig.bool, - 'clone.shallow-submodules': SectionConfig.bool, - 'clone.no-shallow-submodules': SectionConfig.bool, - 'clone.depth': SectionConfig.int, - 'clone.jobs': SectionConfig.int, + 'local': cconfig.BoolType(), + 'no-hardlinks': cconfig.BoolType(), + 'shared': cconfig.BoolType(), + 'dissociate': cconfig.BoolType(), + 'quiet': cconfig.BoolType(), + 'verbose': cconfig.BoolType(), + 'single-branch': cconfig.BoolType(), + 'no-single-branch': cconfig.BoolType(), + 'no-tags': cconfig.BoolType(), + 'shallow-submodules': cconfig.BoolType(), + 'no-shallow-submodules': cconfig.BoolType(), + 'depth': cconfig.IntType(), + 'jobs': cconfig.IntType(), } -git = GitSectionConfig() +git_fetch = {} +git_clone = {} + +if TESTS_RUN: + config['git'] = dict(fetch=dict(), clone=dict()) + +if 'git' in config: + git = config['git'] + + if 'fetch' in git: + git_fetch = GitFetchSection('git.fetch', config, git['fetch']).all() + + if 'clone' in git: + git_clone = GitCloneSection('git.clone', config, git['clone']).all() + REPO_BACKENDS = { "MANUAL": { @@ -200,8 +221,8 @@ class GitSectionConfig(SectionConfig): "GIT": { "BACKEND": "{}.main.repo.Git".format(VST_PROJECT_LIB_NAME), "OPTIONS": { - "CLONE_KWARGS": git.get('CLONE', {}), - "FETCH_KWARGS": git.get('FETCH', {}), + "CLONE_KWARGS": git_clone, + "FETCH_KWARGS": git_fetch, "GIT_ENV": { "GLOBAL": { "GIT_SSL_NO_VERIFY": "true" @@ -259,10 +280,8 @@ class GitSectionConfig(SectionConfig): EXECUTOR = main.get("executor_path", fallback=__EXECUTOR_DEFAULT).strip().split(' ') SELFCARE = '/tmp/' -MANUAL_PROJECT_VARS = SectionConfig( - 'project_manual_vars', - dict(forks=4, timeout=30, fact_caching_timeout=3600, poll_interval=5) -).all() +MANUAL_PROJECT_VARS = config['project_manual_vars'].all() or \ + dict(forks=4, timeout=30, fact_caching_timeout=3600, poll_interval=5) PROJECT_REPOSYNC_WAIT_SECONDS = main.getseconds('repo_sync_on_run_timeout', fallback='1:00') PROJECT_CI_HANDLER_CLASS = "{}.main.ci.DefaultHandler".format(VST_PROJECT_LIB_NAME) @@ -312,7 +331,7 @@ class GitSectionConfig(SectionConfig): if "test" in sys.argv: REPO_BACKENDS['GIT']['OPTIONS']['CLONE_KWARGS']['local'] = True CLONE_RETRY = 0 - PROJECTS_DIR = '/tmp/polemarch_projects' + str(PY_VER) - HOOKS_DIR = '/tmp/polemarch_hooks' + str(PY_VER) + PROJECTS_DIR = '/tmp/polemarch_projects' + str(KWARGS['PY_VER']) + HOOKS_DIR = '/tmp/polemarch_hooks' + str(KWARGS['PY_VER']) os.makedirs(PROJECTS_DIR) if not os.path.exists(PROJECTS_DIR) else None os.makedirs(HOOKS_DIR) if not os.path.exists(HOOKS_DIR) else None diff --git a/polemarch/main/tests/executions.py b/polemarch/main/tests/executions.py index 8f4658b1..53b5e9d4 100644 --- a/polemarch/main/tests/executions.py +++ b/polemarch/main/tests/executions.py @@ -616,6 +616,9 @@ def playbook_tests(self, prj, playbook_count=1, execute=None, inventory="localho private_key=ssh_key_pattern ) bulk_data = self.project_bulk_sync_and_playbooks(prj['id']) + bulk_data += [ + self.get_mod_bulk('project', prj['id'], {}, 'playbook', 'get', filters='pb_filter='+_exec['playbook']), + ] bulk_data += [ self.get_mod_bulk('project', prj['id'], _exec, 'execute_playbook'), ] if execute else [] @@ -623,9 +626,11 @@ def playbook_tests(self, prj, playbook_count=1, execute=None, inventory="localho self.assertEqual(results[0]['status'], 200) self.assertEqual(results[1]['status'], 200) self.assertEqual(results[1]['data']['count'], playbook_count) + self.assertEqual(results[2]['data']['count'], 1) + self.assertEqual(results[2]['data']['results'][0]['playbook'], results[1]['data']['results'][0]['playbook']) if not execute: return - self.assertEqual(results[2]['status'], 201) + self.assertEqual(results[3]['status'], 201) def module_tests(self, prj): bulk_data = [ @@ -635,6 +640,9 @@ def module_tests(self, prj): self.get_mod_bulk( 'project', prj['id'], {}, 'module', 'get', filters='path=s3_website' ), + self.get_mod_bulk( + 'project', prj['id'], {}, 'module', 'get', filters='name=ping' + ), self.get_mod_bulk( 'project', prj['id'], {}, 'module/<1[data][results][0][id]>', 'get' ), @@ -645,7 +653,8 @@ def module_tests(self, prj): self.assertTrue(results[0]['data']['count'] > 1000) self.assertEqual(results[1]['data']['count'], 1) self.assertEqual(results[1]['data']['results'][0]['name'], 's3_website') - self.assertEqual(results[2]['data']['data']['module'], 's3_website') + self.assertEqual(results[2]['data']['results'][0]['name'], 'ping') + self.assertEqual(results[3]['data']['data']['module'], 's3_website') def get_complex_bulk(self, item, op='add', **kwargs): return self.get_bulk(item, kwargs, op) @@ -1602,3 +1611,113 @@ def test_project_ci(self): self.assertEqual(results[8]['status'], 201) self.assertEqual(results[9]['status'], 409) self.assertEqual(results[-1]['status'], 204) + + def test_periodic_task_edit(self): + results = self.make_bulk([ + # 0 + dict( + data_type=['project'], method='post', + data=dict(name='test_pt_errors', repo_type='MANUAL') + ), + # 1 + dict( + data_type=['inventory'], method='post', + data=dict(name='test_inv') + ), + # 2 + dict(data_type=['project', '<0[data][id]>', 'sync'], method='post'), + # 3 + dict( + data_type=['project', '<0[data][id]>', 'inventory'], method='post', + data=dict(id='<1[data][id]>') + ), + # 4 + dict( + data_type=['project', '<0[data][id]>', 'periodic_task'], method='post', + data=dict( + mode="shell", schedule="* * * * *", type="CRONTAB", + inventory='./localhost,', save_result=False, + kind="MODULE", name="test_pt", enabled=True + ) + ), + # 5 + dict(data_type=['project', '<0[data][id]>', 'periodic_task', '<4[data][id]>'], method='get'), + # 6 + dict( + data_type=['project', '<0[data][id]>', 'periodic_task', '<4[data][id]>', 'variables'], method='post', + data=dict(key='args', value='ls') + ), + # 7 + dict( + data_type=['project', '<0[data][id]>', 'periodic_task', '<4[data][id]>'], method='put', + data=dict( + mode="shell", schedule="* * * * *", type="CRONTAB", + inventory='<1[data][id]>', save_result=True, + kind="MODULE", name="test_pt", enabled=True + ) + ), + # 8 + dict(data_type=['project', '<0[data][id]>', 'periodic_task', '<4[data][id]>'], method='get'), + # 9 + dict( + data_type=['project', '<0[data][id]>', 'periodic_task', '<4[data][id]>'], method='put', + data=dict( + mode="shell", schedule="* * * * *", type="CRONTAB", + inventory='', save_result=True, + kind="MODULE", name="test_pt", enabled=True + ) + ), + # 10 + dict(data_type=['project', '<0[data][id]>', 'periodic_task', '<4[data][id]>'], method='get'), + # 11 + dict( + data_type=['project', '<0[data][id]>', 'periodic_task', '<4[data][id]>'], method='put', + data=dict( + mode="shell", schedule="* * * * *", type="CRONTAB", + inventory='./localhost, ', save_result=True, + kind="MODULE", name="test_pt", enabled=True + ) + ), + # 12 + dict(data_type=['project', '<0[data][id]>', 'periodic_task', '<4[data][id]>'], method='get'), + # 13 + dict( + data_type=['project', '<0[data][id]>', 'periodic_task', '<4[data][id]>', 'variables'], method='post', + data=dict(key='connection', value='local') + ), + # 14 + dict( + data_type=['project', '<0[data][id]>', 'periodic_task', '<4[data][id]>', 'variables'], method='post', + data=dict(key='verbose', value='4') + ), + # 15 + dict(data_type=['project', '<0[data][id]>', 'periodic_task', '<4[data][id]>', 'execute'], method='post'), + # 16 + dict(data_type=['project', '<0[data][id]>', 'history', '<15[data][history_id]>'], method='get'), + # 17 + dict(data_type=['project', '<0[data][id]>'], method='delete'), + ], 'put') + + for result in results: + self.assertIn(result['status'], [200, 201, 204]) + + # Grouped request indexes by response code + statuses = { + 200: (2, 5, 7, 8, 9, 10, 11, 12, 16,), + 201: (0, 1, 3, 4, 6, 13, 14, 15), + 204: (17,) + } + for status, indexes in statuses.items(): + for i in indexes: + self.assertEqual( + results[i]['status'], status, + msg='Bulk index: {}, Response status: `{}`!=`{}`, Response data:'.format( + i, results[i]['status'], status, results[i]['data'] + ) + ) + + self.assertEqual(results[5]['data']['inventory'], './localhost,') + self.assertEqual(results[8]['data']['inventory'], 1) + self.assertEqual(results[10]['data']['inventory'], '') + self.assertEqual(results[12]['data']['inventory'], './localhost, ') + self.assertEqual(results[-2]['data']['status'], 'OK') diff --git a/polemarch/main/tests/openapi.py b/polemarch/main/tests/openapi.py index 2f01d265..0d1e4d61 100644 --- a/polemarch/main/tests/openapi.py +++ b/polemarch/main/tests/openapi.py @@ -1872,6 +1872,7 @@ def check_path_playbook_list(self, schema, path, *args, **kwargs): params = [ dict(name='playbook', description=True, required=False, type='string'), dict(name='playbook__not', description=True, required=False, type='string'), + dict(name='pb_filter', description=True, required=False, type='string'), ] + self.pm_filters + self.pm_name_filter + self.default_filters responses = dict( description=True, @@ -1971,6 +1972,7 @@ def check_path_module_list(self, schema, path, *args, **kwargs): params = [ dict(name='path', description=True, required=False, type='string'), dict(name='path__not', description=True, required=False, type='string'), + dict(name='name', description=True, required=False, type='string'), ] + self.default_filters responses = dict( description=True, diff --git a/polemarch/static/css/polemarch-gui.css b/polemarch/static/css/polemarch-gui.css index 8d24e156..4301c824 100644 --- a/polemarch/static/css/polemarch-gui.css +++ b/polemarch/static/css/polemarch-gui.css @@ -385,4 +385,8 @@ body { .clear-btn-title { display: none; +} + +.details .form-group { + margin-bottom: 0; } \ No newline at end of file diff --git a/polemarch/static/js/pmFields.js b/polemarch/static/js/pmFields.js index 0cb94197..c120b069 100644 --- a/polemarch/static/js/pmFields.js +++ b/polemarch/static/js/pmFields.js @@ -104,10 +104,96 @@ guiFields.module_autocomplete = class ModuleAutocompleteField extends guiFields. */ guiFields.group_autocomplete = class GroupAutocompleteField extends guiFields.playbook_autocomplete {}; +/** + * Mixin for classes of fields, that depended on project field value. + * These fields should format queryset urls containing project id. + * Project id can be either in instance's data or in route's url params. + */ +const field_depended_on_project_mixin = (Class_name) => class extends Class_name { + /** + * Redefinition of 'formatQuerySetUrl' method of fk guiField. + */ + formatQuerySetUrl(url="", data={}, params={}) { /* jshint unused: false */ + if(url.indexOf('{') == -1) { + return url; + } + + let project = data.project || app.application.$route.params[path_pk_key]; + + if(project && typeof project == 'object' && project.value) { + project = project.value; + } + + return url.format({[path_pk_key]: project}); + } +}; + +/** + * History Mode guiField class. + */ +guiFields.history_mode = class HistoryModeField extends field_depended_on_project_mixin(guiFields.fk) { + /** + * Redefinition of 'getPrefetchValue' method of fk guiField. + */ + getPrefetchValue(data={}, prefetch_data={}) { + return { + value: prefetch_data[this.options.additionalProperties.value_field], + prefetch_value: data[this.options.name], + }; + } + /** + * Method returns string with value of instance's 'mode' field (playbook or module). + * @param data {object} Object with instance data. + * @return {string} + */ + getMode(data={}) { + return data.kind.toLowerCase(); + } + /** + * Method returns true, is instance's 'mode' field value equals to 'playbook', otherwise, returns false. + * @param data {object} Object with instance data. + * @return {boolean} + */ + isPlaybookMode(data={}) { + return this.getMode(data) === 'playbook'; + } + /** + * Redefinition of 'getPrefetchFilterName' method of fk guiField. + */ + getPrefetchFilterName(data={}) { + return this.isPlaybookMode(data) ? 'pb_filter' : 'name'; + } + /** + * Redefinition of 'isPrefetchDataForMe' method of fk guiField. + */ + isPrefetchDataForMe(data={}, prefetch_data={}) { + let field_name = this.isPlaybookMode(data) ? 'playbook' : 'name'; + + return data[this.options.name] == prefetch_data[field_name]; + } + /** + * Redefinition of 'getAppropriateQuerySet' method of fk guiField. + */ + getAppropriateQuerySet(data={}, querysets=null) { + let qs = querysets || this.options.additionalProperties.querysets; + + return qs.filter(item => item.url.indexOf(this.getMode(data)) !== -1)[0]; + } +}; + +/** + * One History Mode guiField class. + */ +guiFields.one_history_mode = class OneHistoryModeField extends guiFields.history_mode { + static get mixins() { + return super.mixins.concat(gui_fields_mixins.one_history_string, gui_fields_mixins.one_history_fk); + } +}; + /** * History Initiator guiField class. */ -guiFields.history_initiator = class HistoryInitiatorField extends guiFields.fk { +guiFields.history_initiator = class HistoryInitiatorField extends field_depended_on_project_mixin(guiFields.fk) { static get initiatorTypes() { return history_initiator_types; /* globals history_initiator_types */ } @@ -144,25 +230,6 @@ guiFields.history_initiator = class HistoryInitiatorField extends guiFields.fk { return selected; } - /** - * Redefinition of 'formatQuerySetUrl' method of fk guiField. - */ - formatQuerySetUrl(url="", data={}, params={}) { /* jshint unused: false */ - if(url.indexOf('{') == -1) { - return url; - } - - let project = data.project || app.application.$route.params[path_pk_key]; - - if(project && typeof project == 'object' && project.value) { - project = project.value; - } - - let f_obj = {}; - f_obj[path_pk_key] = project; - - return url.format(f_obj); - } }; /** @@ -295,7 +362,7 @@ guiFields.one_history_date_time = class OneHistoryDateTime extends guiFields.dat return; } - return moment(moment.tz(value, window.timeZone)).tz(moment.tz.guess()).format("YYYY-MM-DD HH:mm:ss"); + return moment(moment.tz(value, window.timeZone)).tz(moment.tz.guess()).format("YYYY-MM-DD HH:mm:ss"); } }; diff --git a/polemarch/static/js/pmHistory.js b/polemarch/static/js/pmHistory.js index 8bee5b3e..ca9d1807 100644 --- a/polemarch/static/js/pmHistory.js +++ b/polemarch/static/js/pmHistory.js @@ -26,6 +26,17 @@ const history_paths = [ '/project/{' + path_pk_key + '}/history/{history_id}/', ]; +/** + * Variable, that stores object with additional properties for history_mode field's options. + */ +const history_mode_additionalProperties = { + list_paths: [ + "/project/{" + path_pk_key + "}/playbook/", + "/project/{" + path_pk_key + "}/module/", + ], + value_field: 'id', +}; + /** * Function, that adds signal for some history model's fields. * @param {string} model @@ -49,24 +60,21 @@ function historyModelsFieldsHandler(model) { fields.executor.format = 'history_executor'; fields.executor.additionalProperties = { model: {$ref: "#/definitions/User"}, - // list_paths: ["/user/"], value_field: "id", view_field: "username", }; } - if(fields.project) { - fields.project.format = 'fk'; - fields.project.additionalProperties = { - model: {$ref: "#/definitions/Project"}, - value_field: "id", - view_field: "name", - }; - } - - if(fields.options) { - fields.options.hidden = true; - } + [ + 'options', + 'initiator_type', + 'kind', + 'project' + ].forEach(field => { + if(fields[field]) { + fields[field].hidden = true; + } + }); if(fields.initiator) { fields.initiator.format = 'history_initiator'; @@ -77,13 +85,15 @@ function historyModelsFieldsHandler(model) { }; } - if(fields.initiator_type) { - fields.initiator_type.hidden = true; - } if(fields.revision) { fields.revision.format = 'one_history_revision'; } + + if(fields.mode) { + fields.mode.format = 'history_mode'; + fields.mode.additionalProperties = {...history_mode_additionalProperties}; + } }); } @@ -92,8 +102,9 @@ function historyModelsFieldsHandler(model) { */ function OneHistory_kind_mode_callback(parent_values={}) { let obj = { - format: 'one_history_string', save_value: true, + format: 'one_history_mode', + additionalProperties: {...history_mode_additionalProperties}, }; if(parent_values.kind) { @@ -123,7 +134,6 @@ function OneHistoryFieldsHandler(model) { fields.executor.format = 'one_history_executor'; fields.initiator.format = 'one_history_initiator'; - fields.project.format = 'one_history_fk'; fields.inventory.format = 'one_history_fk'; fields.inventory.hidden = true; fields.execute_args.format = 'one_history_execute_args'; diff --git a/polemarch/static/js/pmPeriodicTasks.js b/polemarch/static/js/pmPeriodicTasks.js index 77fe16a5..a6b587ab 100644 --- a/polemarch/static/js/pmPeriodicTasks.js +++ b/polemarch/static/js/pmPeriodicTasks.js @@ -45,7 +45,7 @@ function OnePeriodictask_mode_callback(parent_values={}) { additionalProperties: { // there is no 'name' filters view_field: 'path', - value_field: 'path', + value_field: 'name', list_paths: ['/project/{' + path_pk_key + '}/module/'], }, required: true, diff --git a/polemarch/static/js/pmProjects.js b/polemarch/static/js/pmProjects.js index 46eec49a..6e98b547 100644 --- a/polemarch/static/js/pmProjects.js +++ b/polemarch/static/js/pmProjects.js @@ -223,7 +223,7 @@ const gui_project_page_additional = Vue.component('gui_project_page_additional', let buttons = {}; for(let key in data.playbooks) { - if(data.playbook.hasOwnProperty(key)) { + if(data.playbooks.hasOwnProperty(key)) { let val = data.playbooks[key]; buttons[key] = { @@ -434,4 +434,19 @@ tabSignal.connect("models[ProjectVariable].fields.beforeInit", (fields) => { if(fields.value && fields.value.additionalProperties) { fields.value.additionalProperties.callback = ProjectVariable_value_callback; } -}); \ No newline at end of file +}); + +/** + * Hides 'pb_filter' filter on the playbook list view. + */ +tabSignal.connect("views[/project/{" + path_pk_key + "}/playbook/].filters.beforeInit", filters => { + for(let key in filters) { + if(filters.hasOwnProperty(key)) { + let filter = filters[key]; + + if (filter.name == 'pb_filter') { + filter.hidden = true; + } + } + } +}); diff --git a/polemarch/static/js/pmTemplates.js b/polemarch/static/js/pmTemplates.js index 79bc4089..8012ce6c 100644 --- a/polemarch/static/js/pmTemplates.js +++ b/polemarch/static/js/pmTemplates.js @@ -765,7 +765,7 @@ const tmp_vars_list_mixin = { return qs.formQueryAndSend('patch', template_instance.data) .then(response => { /* jshint unused: false */ - return this.removeInstances_callback(selected); + return this.removeInstances_callback_custom(selected); }); }).catch(error => { debugger; @@ -784,7 +784,7 @@ const tmp_vars_list_mixin = { /** * Redefinition of 'removeInstances_callback' method of list view. */ - removeInstances_callback(selected) { + removeInstances_callback_custom(selected) { selected.forEach(item => { guiPopUp.success(pop_up_msg.instance.success.remove.format( [item, this.view.schema.name] diff --git a/polemarch/static/templates/pmHistory.html b/polemarch/static/templates/pmHistory.html index d5af4247..489348ca 100644 --- a/polemarch/static/templates/pmHistory.html +++ b/polemarch/static/templates/pmHistory.html @@ -8,13 +8,13 @@ -
+

- + Error {{ error.status }} @@ -32,18 +32,12 @@

-
-
-
-
-
-
-
- {{ error_data }} -
+
+
+

{{ error_data }}

-
+
@@ -55,7 +49,7 @@

- + {{ title | capitalize | split }} @@ -80,7 +74,7 @@

-
+
-
+
-
+
Execution output @@ -126,7 +120,7 @@

-
+
@@ -145,7 +139,7 @@

-
+
Details
-
+
diff --git a/polemarch/static/templates/pmProjects.html b/polemarch/static/templates/pmProjects.html index 46b4b01d..88a950ae 100644 --- a/polemarch/static/templates/pmProjects.html +++ b/polemarch/static/templates/pmProjects.html @@ -1,18 +1,16 @@