diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 0e9a840..b7e95a1 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -44,7 +44,7 @@ jobs: - name: Running PyLint run: | pylint --version - pylint --recursive=y . + pylint --recursive=y --load-plugins pylint_django --django-settings-module=aw.settings . - name: Running YamlLint run: | diff --git a/docs/source/usage/2_config.rst b/docs/source/usage/2_config.rst index 394a614..ab3a229 100644 --- a/docs/source/usage/2_config.rst +++ b/docs/source/usage/2_config.rst @@ -7,6 +7,8 @@ .. |cnf_admin| image:: ../_static/img/config_admin.png :class: wiki-img +.. |cnf_jobs| image:: ../_static/img/config_jobs.png + :class: wiki-img ========== 2 - Config diff --git a/docs/source/usage/api.rst b/docs/source/usage/api.rst index 676c0a4..266ca8a 100644 --- a/docs/source/usage/api.rst +++ b/docs/source/usage/api.rst @@ -14,7 +14,7 @@ API This project has a API first development approach! -To use the API you have to create an API key: `ui/settings/api_keys `_ +To use the API you have to create an API key. You can use the UI at :code:`Settings - API Keys` to do so. Examples diff --git a/docs/source/usage/permission.rst b/docs/source/usage/privileges.rst similarity index 76% rename from docs/source/usage/permission.rst rename to docs/source/usage/privileges.rst index 7c0cade..0555fbb 100644 --- a/docs/source/usage/permission.rst +++ b/docs/source/usage/privileges.rst @@ -13,9 +13,9 @@ .. |perm_overview| image:: ../_static/img/permission_overview.svg :class: wiki-img -=========== -Permissions -=========== +========== +Privileges +========== You can set job-permissions to limit user actions. @@ -24,20 +24,20 @@ Users & Groups The :code:`System - Admin - Users/Groups` admin-page allows you to create new users and manage group memberships. -To allow a user to create jobs and permissions you need to activate the :code:`Staff status`. +To allow a user to create jobs, permissions and global-credentials you need to activate the :code:`Staff status`. |perm_users_groups| ---- -Job permissions -*************** +Permissions +*********** -The UI at :code:`Settings - Permissions` allows you to create job permissions and link them to users and groups. +The UI at :code:`Settings - Permissions` allows you to create job & credential permissions and link them to users and groups. |perm_ui| -Each job can have multiple permissions linked to it. +Each job & credential can have multiple permissions linked to it. **Permission types:** diff --git a/requirements_lint.txt b/requirements_lint.txt index 1d5e9bf..bf33826 100644 --- a/requirements_lint.txt +++ b/requirements_lint.txt @@ -1,2 +1,3 @@ pylint +pylint-django yamllint diff --git a/scripts/lint.sh b/scripts/lint.sh index 0f0f636..0f3ac59 100644 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -8,7 +8,7 @@ echo '' echo 'LINTING Python' echo '' -pylint --recursive=y . +pylint --recursive=y --load-plugins pylint_django --django-settings-module=aw.settings . echo '' echo 'LINTING YAML' diff --git a/src/ansible-webui/aw/api_endpoints/base.py b/src/ansible-webui/aw/api_endpoints/base.py index 5ac92de..0ee91f1 100644 --- a/src/ansible-webui/aw/api_endpoints/base.py +++ b/src/ansible-webui/aw/api_endpoints/base.py @@ -1,12 +1,12 @@ from django.conf import settings from django.contrib.auth.models import AnonymousUser from django.core.exceptions import ObjectDoesNotExist -from django.contrib.auth.models import User, Group from rest_framework import serializers from rest_framework.permissions import IsAuthenticated from rest_framework_api_key.permissions import BaseHasAPIKey from aw.model.api import AwAPIKey +from aw.base import USERS, GROUPS class HasAwAPIKey(BaseHasAPIKey): @@ -17,7 +17,7 @@ class HasAwAPIKey(BaseHasAPIKey): # see: rest_framework_api_key.permissions.BaseHasAPIKey:get_from_header -def get_api_user(request) -> settings.AUTH_USER_MODEL: +def get_api_user(request) -> USERS: if isinstance(request.user, AnonymousUser): try: return AwAPIKey.objects.get_from_key( @@ -45,9 +45,9 @@ class GenericResponse(BaseResponse): class GroupSerializer(serializers.ModelSerializer): class Meta: - model = Group + model = GROUPS class UserSerializer(serializers.ModelSerializer): class Meta: - model = User + model = USERS diff --git a/src/ansible-webui/aw/api_endpoints/credentials.py b/src/ansible-webui/aw/api_endpoints/credentials.py index b1b68e1..8977797 100644 --- a/src/ansible-webui/aw/api_endpoints/credentials.py +++ b/src/ansible-webui/aw/api_endpoints/credentials.py @@ -1,4 +1,3 @@ -from django.conf import settings from django.core.exceptions import ObjectDoesNotExist from django.db.utils import IntegrityError from rest_framework.views import APIView @@ -11,6 +10,7 @@ from aw.api_endpoints.base import API_PERMISSION, get_api_user, GenericResponse, BaseResponse from aw.utils.permission import has_credentials_permission from aw.utils.util import is_null +from aw.base import USERS class JobGlobalCredentialsReadResponse(serializers.ModelSerializer): @@ -61,9 +61,8 @@ def are_global_credentials(request) -> bool: def _find_credentials( - credentials_id: int, are_global: bool, user: settings.AUTH_USER_MODEL + credentials_id: int, are_global: bool, user: USERS ) -> (BaseJobCredentials, None): - # pylint: disable=E1101 try: if are_global: return JobGlobalCredentials.objects.get(id=credentials_id) @@ -108,7 +107,6 @@ class APIJobCredentials(APIView): operation_id='credentials_list', ) def get(self, request): - # pylint: disable=E1101 user = get_api_user(request) credentials_global = [] credentials_global_raw = JobGlobalCredentials.objects.all() @@ -333,7 +331,6 @@ def put(self, request, credentials_id: int): status=400, ) - # pylint: disable=E1101 try: # not working with password properties: 'Job.objects.filter(id=job_id).update(**serializer.data)' for field, value in serializer.data.items(): diff --git a/src/ansible-webui/aw/api_endpoints/job.py b/src/ansible-webui/aw/api_endpoints/job.py index 12df0aa..fb135c5 100644 --- a/src/ansible-webui/aw/api_endpoints/job.py +++ b/src/ansible-webui/aw/api_endpoints/job.py @@ -10,12 +10,15 @@ from aw.model.job import Job, JobExecution from aw.model.job_permission import CHOICE_PERMISSION_READ, CHOICE_PERMISSION_EXECUTE, \ CHOICE_PERMISSION_WRITE, CHOICE_PERMISSION_FULL +from aw.model.job_credential import JobGlobalCredentials from aw.api_endpoints.base import API_PERMISSION, get_api_user, BaseResponse, GenericResponse from aw.api_endpoints.job_util import get_viewable_jobs_serialized, JobReadResponse, get_job_executions_serialized, \ JobExecutionReadResponse, get_viewable_jobs, get_job_execution_serialized -from aw.utils.permission import has_job_permission +from aw.utils.permission import has_job_permission, has_credentials_permission from aw.execute.queue import queue_add from aw.execute.util import update_execution_status, is_execution_status +from aw.utils.util import is_set +from aw.base import USERS class JobWriteRequest(serializers.ModelSerializer): @@ -23,13 +26,8 @@ class Meta: model = Job fields = Job.api_fields_write - # vault_pass = serializers.CharField(max_length=100, required=False, default=None) - # become_pass = serializers.CharField(max_length=100, required=False, default=None) - # connect_pass = serializers.CharField(max_length=100, required=False, default=None) - def _find_job(job_id: int) -> (Job, None): - # pylint: disable=E1101 try: return Job.objects.get(id=job_id) @@ -38,7 +36,6 @@ def _find_job(job_id: int) -> (Job, None): def _find_job_and_execution(job_id: int, exec_id: int) -> tuple[Job, (JobExecution, None)]: - # pylint: disable=E1101 job = _find_job(job_id) try: return job, JobExecution.objects.get(id=exec_id, job=job) @@ -68,6 +65,21 @@ def _want_job_executions(request) -> tuple: return False, max_count +def _has_credentials_permission(user: USERS, data: dict) -> bool: + if 'credentials_default' in data and is_set(data['credentials_default']): + try: + return has_credentials_permission( + user=user, + credentials=JobGlobalCredentials.objects.get(id=data['credentials_default']), + permission_needed=CHOICE_PERMISSION_READ, + ) + + except ObjectDoesNotExist: + pass + + return True + + class APIJob(APIView): http_method_names = ['post', 'get'] serializer_class = JobReadResponse @@ -118,7 +130,8 @@ def get(request): operation_id='job_create' ) def post(self, request): - if not get_api_user(request).is_staff: + user = get_api_user(request) + if not user.is_staff: return Response(data={'msg': 'Not privileged to create jobs'}, status=403) serializer = JobWriteRequest(data=request.data) @@ -129,6 +142,12 @@ def post(self, request): status=400, ) + if not _has_credentials_permission(user=user, data=serializer.validated_data): + return Response( + data={'msg': "Not privileged to use provided credentials"}, + status=403, + ) + try: serializer.save() @@ -240,17 +259,14 @@ def put(self, request, job_id: int): status=400, ) - # pylint: disable=E1101 - try: - # not working with password properties: 'Job.objects.filter(id=job_id).update(**serializer.data)' - for field, value in serializer.data.items(): - # if field in BaseJobCredentials.SECRET_ATTRS and \ - # (is_null(value) or value == BaseJobCredentials.SECRET_HIDDEN): - # value = getattr(job, field) - - setattr(job, field, value) + if not _has_credentials_permission(user=user, data=serializer.validated_data): + return Response( + data={'msg': "Not privileged to use provided credentials"}, + status=403, + ) - job.save() + try: + Job.objects.filter(id=job_id).update(**serializer.data) except IntegrityError as err: return Response( @@ -451,14 +467,13 @@ class APIJobExecution(APIView): ], ) def get(self, request): - # pylint: disable=E1101 jobs = get_viewable_jobs(get_api_user(request)) exec_count = _job_execution_count(request) if exec_count is None: exec_count = JOB_EXECUTION_LIMIT serialized = [] - for execution in JobExecution.objects.filter(job__in=jobs).order_by('updated')[:exec_count]: + for execution in JobExecution.objects.filter(job__in=jobs).order_by('-updated')[:exec_count]: serialized.append(get_job_execution_serialized(execution)) return Response(data=serialized, status=200) diff --git a/src/ansible-webui/aw/api_endpoints/job_util.py b/src/ansible-webui/aw/api_endpoints/job_util.py index 442e01b..21bed5d 100644 --- a/src/ansible-webui/aw/api_endpoints/job_util.py +++ b/src/ansible-webui/aw/api_endpoints/job_util.py @@ -1,10 +1,10 @@ -from django.conf import settings from rest_framework import serializers from aw.config.hardcoded import SHORT_TIME_FORMAT, JOB_EXECUTION_LIMIT from aw.model.job import Job, CHOICES_JOB_EXEC_STATUS, JobExecution from aw.utils.permission import get_viewable_jobs from aw.utils.util import datetime_from_db, get_next_cron_execution_str +from aw.base import USERS class JobReadResponse(serializers.ModelSerializer): @@ -36,7 +36,6 @@ class Meta: def get_job_execution_serialized(execution: JobExecution) -> dict: - # pylint: disable=E1101 serialized = { 'id': execution.id, 'job': execution.job.id, @@ -44,6 +43,7 @@ def get_job_execution_serialized(execution: JobExecution) -> dict: 'job_comment': execution.job.comment, 'user': execution.user.id if execution.user is not None else None, 'user_name': execution.user.username if execution.user is not None else 'Scheduled', + 'command': execution.command, 'status': execution.status, 'status_name': CHOICES_JOB_EXEC_STATUS[execution.status][1], 'time_start': datetime_from_db(execution.created).strftime(SHORT_TIME_FORMAT), @@ -67,7 +67,6 @@ def get_job_execution_serialized(execution: JobExecution) -> dict: def get_job_executions_serialized(job: Job, execution_count: int = JOB_EXECUTION_LIMIT) -> list[dict]: - # pylint: disable=E1101 serialized = [] for execution in JobExecution.objects.filter(job=job).order_by('-updated')[:execution_count]: serialized.append(get_job_execution_serialized(execution)) @@ -76,7 +75,7 @@ def get_job_executions_serialized(job: Job, execution_count: int = JOB_EXECUTION def get_viewable_jobs_serialized( - user: settings.AUTH_USER_MODEL, executions: bool = False, + user: USERS, executions: bool = False, execution_count: int = None ) -> list[dict]: serialized = [] diff --git a/src/ansible-webui/aw/api_endpoints/permission.py b/src/ansible-webui/aw/api_endpoints/permission.py index 6fa4577..af7a8e1 100644 --- a/src/ansible-webui/aw/api_endpoints/permission.py +++ b/src/ansible-webui/aw/api_endpoints/permission.py @@ -1,5 +1,3 @@ -from django.conf import settings -from django.contrib.auth.models import User, Group from django.core.exceptions import ObjectDoesNotExist from django.db.utils import IntegrityError from rest_framework.generics import GenericAPIView @@ -14,8 +12,7 @@ from aw.api_endpoints.base import API_PERMISSION, GenericResponse, get_api_user from aw.utils.permission import get_permission_name from aw.utils.util import is_set - -# pylint: disable=E1101 +from aw.base import USERS, GROUPS class PermissionReadResponse(serializers.ModelSerializer): @@ -48,14 +45,13 @@ class Meta: groups = serializers.MultipleChoiceField(allow_blank=True, choices=[]) def __init__(self, *args, **kwargs): - # pylint: disable=E1101 super().__init__(*args, **kwargs) self.fields['jobs'] = serializers.MultipleChoiceField(choices=[job.id for job in Job.objects.all()]) self.fields['credentials'] = serializers.MultipleChoiceField( choices=[creds.id for creds in JobGlobalCredentials.objects.all()] ) - self.fields['users'] = serializers.MultipleChoiceField(choices=[user.id for user in User.objects.all()]) - self.fields['groups'] = serializers.MultipleChoiceField(choices=[group.id for group in Group.objects.all()]) + self.fields['users'] = serializers.MultipleChoiceField(choices=[user.id for user in USERS.objects.all()]) + self.fields['groups'] = serializers.MultipleChoiceField(choices=[group.id for group in GROUPS.objects.all()]) @staticmethod def create_or_update(validated_data: dict, perm: JobPermission = None): @@ -98,7 +94,7 @@ def create_or_update(validated_data: dict, perm: JobPermission = None): users = [] for user_id in validated_data['users']: try: - users.append(User.objects.get(id=user_id)) + users.append(USERS.objects.get(id=user_id)) except ObjectDoesNotExist: continue @@ -109,7 +105,7 @@ def create_or_update(validated_data: dict, perm: JobPermission = None): groups = [] for group_id in validated_data['groups']: try: - groups.append(Group.objects.get(id=group_id)) + groups.append(GROUPS.objects.get(id=group_id)) except ObjectDoesNotExist: continue @@ -178,7 +174,7 @@ def build_permissions(perm_id_filter: int = None) -> (list, dict): return permissions -def has_permission_privileges(user: settings.AUTH_USER_MODEL) -> bool: +def has_permission_privileges(user: USERS) -> bool: # todo: create explicit privilege return user.is_staff diff --git a/src/ansible-webui/aw/base.py b/src/ansible-webui/aw/base.py new file mode 100644 index 0000000..6fda725 --- /dev/null +++ b/src/ansible-webui/aw/base.py @@ -0,0 +1,5 @@ +from django.conf import settings +from django.contrib.auth.models import Group + +USERS = settings.AUTH_USER_MODEL +GROUPS = Group diff --git a/src/ansible-webui/aw/execute/play.py b/src/ansible-webui/aw/execute/play.py index eda4f7c..a13a322 100644 --- a/src/ansible-webui/aw/execute/play.py +++ b/src/ansible-webui/aw/execute/play.py @@ -3,7 +3,7 @@ from ansible_runner import RunnerConfig, Runner from aw.config.main import config -from aw.model.job import Job, JobExecution +from aw.model.job import Job, JobExecution, JobExecutionResult from aw.execute.play_util import runner_cleanup, runner_prep, parse_run_result, failure, runner_logs, job_logs from aw.execute.util import get_path_run, is_execution_status from aw.utils.util import datetime_w_tz, is_null, timed_lru_cache # get_ansible_versions @@ -26,6 +26,9 @@ def ansible_playbook(job: Job, execution: (JobExecution, None)): if is_null(execution): execution = JobExecution(user=None, job=job, comment='Scheduled') + result = JobExecutionResult(time_start=time_start) + result.save() + log_files = job_logs(job=job, execution=execution) execution.log_stdout = log_files['stdout'] execution.log_stderr = log_files['stderr'] @@ -41,13 +44,16 @@ def _cancel_job() -> bool: runner_cfg = AwRunnerConfig(**opts) runner_logs(cfg=runner_cfg, log_files=log_files) runner_cfg.prepare() - log(msg=f"Running job '{job.name}': '{' '.join(runner_cfg.command)}'", level=5) + command = ' '.join(runner_cfg.command) + log(msg=f"Running job '{job.name}': '{command}'", level=5) + execution.command = command + execution.save() runner = Runner(config=runner_cfg, cancel_callback=_cancel_job) runner.run() parse_run_result( - time_start=time_start, + result=result, execution=execution, runner=runner, ) @@ -58,7 +64,7 @@ def _cancel_job() -> bool: except (OSError, AnsibleConfigError) as err: tb = traceback.format_exc(limit=1024) failure( - execution=execution, path_run=path_run, time_start=time_start, + execution=execution, path_run=path_run, result=result, error_s=str(err), error_m=tb, ) raise diff --git a/src/ansible-webui/aw/execute/play_util.py b/src/ansible-webui/aw/execute/play_util.py index ba65413..d6c6222 100644 --- a/src/ansible-webui/aw/execute/play_util.py +++ b/src/ansible-webui/aw/execute/play_util.py @@ -1,6 +1,5 @@ from pathlib import Path from shutil import rmtree -from datetime import datetime from re import sub as regex_replace from os import symlink from os import path as os_path @@ -8,7 +7,6 @@ from ansible_runner import Runner, RunnerConfig from django.core.exceptions import ObjectDoesNotExist -from django.conf import settings from aw.config.main import config, check_config_is_true from aw.config.hardcoded import FILE_TIME_FORMAT @@ -19,6 +17,7 @@ from aw.execute.util import update_execution_status, overwrite_and_delete_file, decode_job_env_vars, \ create_dirs, get_pwd_file, get_pwd_file_arg, write_pwd_file, is_execution_status from aw.utils.permission import has_credentials_permission, CHOICE_PERMISSION_READ +from aw.base import USERS # from aw.utils.debug import log_warn # see: https://ansible.readthedocs.io/projects/runner/en/latest/intro/ @@ -39,7 +38,7 @@ def _commandline_arguments_credentials(credentials: BaseJobCredentials, path_run return cmd_arguments -def _scheduled_or_has_credentials_access(user: settings.AUTH_USER_MODEL, credentials: BaseJobCredentials) -> bool: +def _scheduled_or_has_credentials_access(user: USERS, credentials: BaseJobCredentials) -> bool: if user is None: # scheduled execution return True @@ -70,7 +69,6 @@ def _get_credentials_to_use(job: Job, execution: JobExecution) -> (BaseJobCreden credentials = job.credentials_default elif job.credentials_needed and is_set(execution.user): - # pylint: disable=E1101 # try to get default user-credentials as a last-resort if the job needs some credentials try: credentials = JobUserCredentials.objects.filter(user=execution.user).first() @@ -78,6 +76,12 @@ def _get_credentials_to_use(job: Job, execution: JobExecution) -> (BaseJobCreden except ObjectDoesNotExist: pass + if job.credentials_needed and credentials is None: + raise AnsibleConfigError( + 'The job is set to require credentials - but none were provided or readable! ' + 'Make sure you have privileges for the configured credentials or create user-specific ones.' + ).with_traceback(None) from None + return credentials @@ -238,7 +242,7 @@ def job_logs(job: Job, execution: JobExecution) -> dict: } -def _run_stats(runner: Runner, job_result: JobExecutionResult) -> bool: +def _run_stats(runner: Runner, result: JobExecutionResult) -> bool: any_task_failed = False # https://stackoverflow.com/questions/70348314/get-python-ansible-runner-module-stdout-key-value for host in runner.stats['processed']: @@ -256,26 +260,23 @@ def _run_stats(runner: Runner, job_result: JobExecutionResult) -> bool: any_task_failed = True # todo: create errors - result_host.result = job_result + result_host.result = result result_host.save() return any_task_failed -def parse_run_result(execution: JobExecution, time_start: datetime, runner: Runner): - job_result = JobExecutionResult( - time_start=time_start, - time_fin=datetime_w_tz(), - failed=runner.errored, - ) - job_result.save() +def parse_run_result(execution: JobExecution, result: JobExecutionResult, runner: Runner): + result.time_fin = datetime_w_tz() + result.failed = runner.errored + result.save() any_task_failed = False if runner.stats is not None: - any_task_failed = _run_stats(runner=runner, job_result=job_result) + any_task_failed = _run_stats(runner=runner, result=result) - execution.result = job_result - if job_result.failed or any_task_failed: + execution.result = result + if result.failed or any_task_failed: update_execution_status(execution, status='Failed') else: @@ -288,7 +289,7 @@ def parse_run_result(execution: JobExecution, time_start: datetime, runner: Runn def failure( execution: JobExecution, path_run: Path, - time_start: datetime, error_s: str, error_m: str + result: JobExecutionResult, error_s: str, error_m: str ): update_execution_status(execution, status='Failed') job_error = JobError( @@ -296,13 +297,11 @@ def failure( med=error_m, ) job_error.save() - job_result = JobExecutionResult( - time_start=time_start, - time_fin=datetime_w_tz(), - failed=True, - error=job_error, - ) - job_result.save() - execution.result = job_result + result.time_fin = datetime_w_tz() + result.failed = True + result.error = job_error + result.save() + + execution.result = result execution.save() runner_cleanup(path_run) diff --git a/src/ansible-webui/aw/execute/queue.py b/src/ansible-webui/aw/execute/queue.py index 062e816..ac6e94c 100644 --- a/src/ansible-webui/aw/execute/queue.py +++ b/src/ansible-webui/aw/execute/queue.py @@ -1,11 +1,9 @@ -from django.conf import settings - from aw.model.job import Job, JobQueue from aw.utils.debug import log +from aw.base import USERS -def queue_get() -> (tuple[Job, settings.AUTH_USER_MODEL], None): - # pylint: disable=E1101 +def queue_get() -> (tuple[Job, USERS], None): next_queue_item = JobQueue.objects.order_by('-created').first() if next_queue_item is None: return None @@ -15,7 +13,7 @@ def queue_get() -> (tuple[Job, settings.AUTH_USER_MODEL], None): return job, user -def queue_add(job: Job, user: settings.AUTH_USER_MODEL): +def queue_add(job: Job, user: USERS): log(msg=f"Job '{job.name}' added to execution queue", level=4) queue_item = JobQueue(job=job, user=user) queue_item.save() diff --git a/src/ansible-webui/aw/execute/scheduler.py b/src/ansible-webui/aw/execute/scheduler.py index 1331170..84412dc 100644 --- a/src/ansible-webui/aw/execute/scheduler.py +++ b/src/ansible-webui/aw/execute/scheduler.py @@ -139,7 +139,6 @@ def _reload_action(self, added: list, removed: list, changed: list): self.status() def _reload_check(self) -> dict: - # pylint: disable=E1101 result = {'added': [], 'removed': [], 'changed': []} running = self.thread_manager.list() running_ids = [job.id for job in running] diff --git a/src/ansible-webui/aw/execute/threader.py b/src/ansible-webui/aw/execute/threader.py index 76afa8e..92c7f23 100644 --- a/src/ansible-webui/aw/execute/threader.py +++ b/src/ansible-webui/aw/execute/threader.py @@ -62,6 +62,8 @@ def run(self, error: bool = False) -> None: if self.config_invalid >= self.MAX_CONFIG_INVALID: self.next_execution_time = None + self.job.enabled = False + self.job.save() log(msg=f"Disabling job {self.log_name} because of invalid config! Please fix it", level=2) # exit loop because it will always fail; fixing the config will replace this threat instance return diff --git a/src/ansible-webui/aw/execute/util.py b/src/ansible-webui/aw/execute/util.py index 80f24ed..cc46166 100644 --- a/src/ansible-webui/aw/execute/util.py +++ b/src/ansible-webui/aw/execute/util.py @@ -50,7 +50,6 @@ def update_execution_status(execution: JobExecution, status: str): def is_execution_status(execution: JobExecution, status: str) -> bool: - # pylint: disable=E1101 is_status = JobExecution.objects.get(id=execution.id).status check_status = get_choice_key_by_value(choices=CHOICES_JOB_EXEC_STATUS, value=status) return is_status == check_status @@ -94,7 +93,7 @@ def get_pwd_file_arg(credentials: BaseJobCredentials, attr: str, path_run: (Path def write_pwd_file(credentials: BaseJobCredentials, attr: str, path_run: (Path, str)): - if is_null(getattr(credentials, attr)): + if credentials is None or is_null(getattr(credentials, attr)): return None return write_file_0600( diff --git a/src/ansible-webui/aw/model/api.py b/src/ansible-webui/aw/model/api.py index 0bd8e0e..651fac5 100644 --- a/src/ansible-webui/aw/model/api.py +++ b/src/ansible-webui/aw/model/api.py @@ -1,7 +1,8 @@ from django.db import models -from django.conf import settings from rest_framework_api_key.models import AbstractAPIKey +from aw.base import USERS + class AwAPIKey(AbstractAPIKey): - user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, editable=False) + user = models.ForeignKey(USERS, on_delete=models.CASCADE, editable=False) diff --git a/src/ansible-webui/aw/model/job.py b/src/ansible-webui/aw/model/job.py index 697c2c9..1ab7d10 100644 --- a/src/ansible-webui/aw/model/job.py +++ b/src/ansible-webui/aw/model/job.py @@ -1,12 +1,12 @@ from crontab import CronTab from django.db import models -from django.conf import settings from django.core.validators import ValidationError from django.utils import timezone from aw.model.base import BareModel, BaseModel, CHOICES_BOOL, DEFAULT_NONE from aw.config.hardcoded import SHORT_TIME_FORMAT from aw.model.job_credential import JobGlobalCredentials, JobUserCredentials +from aw.base import USERS class JobError(BareModel): @@ -50,7 +50,6 @@ class Meta: abstract = True def clean(self): - # pylint: disable=E1101 super().clean() for flag in self.BAD_ANSIBLE_FLAGS: @@ -171,7 +170,7 @@ class JobExecution(BaseJob): # NOTE: scheduled execution will have no user user = models.ForeignKey( - settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, + USERS, on_delete=models.SET_NULL, null=True, related_name='jobexec_fk_user', editable=False, ) job = models.ForeignKey(Job, on_delete=models.CASCADE, related_name='jobexec_fk_job') @@ -185,6 +184,7 @@ class JobExecution(BaseJob): ) log_stdout = models.CharField(max_length=300, **DEFAULT_NONE) log_stderr = models.CharField(max_length=300, **DEFAULT_NONE) + command = models.CharField(max_length=1000, **DEFAULT_NONE) credential_global = models.ForeignKey( JobGlobalCredentials, on_delete=models.SET_NULL, related_name='jobexec_fk_credglob', null=True, @@ -194,7 +194,6 @@ class JobExecution(BaseJob): ) def __str__(self) -> str: - # pylint: disable=E1101 status_name = CHOICES_JOB_EXEC_STATUS[int(self.status)][1] executor = 'scheduled' if self.user is not None: @@ -207,6 +206,6 @@ def __str__(self) -> str: class JobQueue(BareModel): job = models.ForeignKey(Job, on_delete=models.CASCADE, related_name='jobqueue_fk_job') user = models.ForeignKey( - settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, + USERS, on_delete=models.SET_NULL, null=True, related_name='jobqueue_fk_user', ) diff --git a/src/ansible-webui/aw/model/job_credential.py b/src/ansible-webui/aw/model/job_credential.py index 9f96aba..d643718 100644 --- a/src/ansible-webui/aw/model/job_credential.py +++ b/src/ansible-webui/aw/model/job_credential.py @@ -1,9 +1,9 @@ from django.db import models -from django.conf import settings from aw.model.base import BaseModel, DEFAULT_NONE from aw.utils.util import is_null, is_set from aw.utils.crypto import decrypt, encrypt +from aw.base import USERS class BaseJobCredentials(BaseModel): @@ -147,10 +147,9 @@ class JobUserCredentials(BaseJobCredentials): api_fields_write.append('user') form_fields = JobGlobalCredentials.api_fields_write.copy() - user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) + user = models.ForeignKey(USERS, on_delete=models.CASCADE) def __str__(self) -> str: - # pylint: disable=E1101 return f"Credentials '{self.name}' of user '{self.user.username}'{self._get_set_creds_str()}" class Meta: diff --git a/src/ansible-webui/aw/model/job_permission.py b/src/ansible-webui/aw/model/job_permission.py index 0c5fb21..4fb2a3f 100644 --- a/src/ansible-webui/aw/model/job_permission.py +++ b/src/ansible-webui/aw/model/job_permission.py @@ -1,11 +1,10 @@ from django.db import models -from django.conf import settings -from django.contrib.auth.models import Group from aw.model.base import BareModel, BaseModel from aw.utils.util import get_choice_by_value from aw.model.job import Job from aw.model.job_credential import JobGlobalCredentials +from aw.base import USERS, GROUPS CHOICE_PERMISSION_READ = 5 CHOICE_PERMISSION_EXECUTE = 10 @@ -29,12 +28,12 @@ class JobPermission(BaseModel): name = models.CharField(max_length=100) permission = models.PositiveSmallIntegerField(choices=CHOICES_PERMISSION, default=0) users = models.ManyToManyField( - settings.AUTH_USER_MODEL, + USERS, through='JobPermissionMemberUser', through_fields=('permission', 'user'), ) groups = models.ManyToManyField( - Group, + GROUPS, through='JobPermissionMemberGroup', through_fields=('permission', 'group'), ) @@ -86,11 +85,10 @@ class Meta: class JobPermissionMemberUser(BareModel): - user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) + user = models.ForeignKey(USERS, on_delete=models.CASCADE) permission = models.ForeignKey(JobPermission, on_delete=models.CASCADE) def __str__(self) -> str: - # pylint: disable=E1101 return f"Permission '{self.permission.name}' member user '{self.user.username}'" class Meta: @@ -100,7 +98,7 @@ class Meta: class JobPermissionMemberGroup(BareModel): - group = models.ForeignKey(Group, on_delete=models.CASCADE) + group = models.ForeignKey(GROUPS, on_delete=models.CASCADE) permission = models.ForeignKey(JobPermission, on_delete=models.CASCADE) def __str__(self) -> str: diff --git a/src/ansible-webui/aw/settings.py b/src/ansible-webui/aw/settings.py index 5fc67c2..97cffc6 100644 --- a/src/ansible-webui/aw/settings.py +++ b/src/ansible-webui/aw/settings.py @@ -1,6 +1,15 @@ from pathlib import Path -from aw.config.main import config, VERSION +try: + from aw.config.main import config, VERSION + +except ImportError: + # pylint-django + from aw.config.main import init_globals + init_globals() + from aw.config.main import config, VERSION + + from aw.config.hardcoded import LOGIN_PATH, PORT_WEB from aw.utils.deployment import deployment_dev, deployment_prod from aw.config.environment import get_aw_env_var diff --git a/src/ansible-webui/aw/static/css/aw.css b/src/ansible-webui/aw/static/css/aw.css index 20891c7..9e2e9a4 100644 --- a/src/ansible-webui/aw/static/css/aw.css +++ b/src/ansible-webui/aw/static/css/aw.css @@ -514,6 +514,8 @@ code { border-radius: 0.3em; padding: 4px 5px 6px; white-space: nowrap; + word-break: break-word; + white-space: pre-wrap; } div .code { @@ -522,9 +524,10 @@ div .code { background-color: #404040; color: var(--awLight); letter-spacing: normal; - word-break: break-all; + word-break: break-word; white-space: pre-wrap; padding: 1% 1.5%; + max-width: 100%; } td div hr { diff --git a/src/ansible-webui/aw/static/js/aw.js b/src/ansible-webui/aw/static/js/aw.js index bfd30c8..9a89ae7 100644 --- a/src/ansible-webui/aw/static/js/aw.js +++ b/src/ansible-webui/aw/static/js/aw.js @@ -220,7 +220,7 @@ function fetchApiTableDataPlaceholder(dataTable, placeholderId) { // for example with second (hidden) child-row - see: 'job-manage' // for example with two tables - see: 'job-credentials' -function fetchApiTableData(apiEndpoint, updateFunction, secondRow = false, placeholderFunction = null, targetTable = null, dataSubKey = null) { +function fetchApiTableData(apiEndpoint, updateFunction, secondRow = false, placeholderFunction = null, targetTable = null, dataSubKey = null, reverseData = false) { // NOTE: data needs to be list of dict and include an 'id' attribute if (targetTable == null) { targetTable = "aw-api-data-table"; @@ -234,6 +234,9 @@ function fetchApiTableData(apiEndpoint, updateFunction, secondRow = false, place if (dataSubKey != null) { var data = data[dataSubKey]; } + if (reverseData) { + var data = data.reverse(); + } existingEntryIds = []; // for each existing entry for (i = 0, len = data.length; i < len; i++) { diff --git a/src/ansible-webui/aw/static/js/jobs/logs.js b/src/ansible-webui/aw/static/js/jobs/logs.js index e05b463..66f8911 100644 --- a/src/ansible-webui/aw/static/js/jobs/logs.js +++ b/src/ansible-webui/aw/static/js/jobs/logs.js @@ -77,6 +77,17 @@ function updateApiTableDataJobLogs(row, row2, entry) { let row2Col = row2.insertCell(0); row2Col.setAttribute("colspan", "100%"); row2Col.innerHTML = logsTemplates; + if (entry.command != null) { + let logsContainer = document.getElementById("aw-execution-logs-" + entry.id); + logsContainer.innerHTML = "Running command:
" + entry.command + "
" + logsContainer.innerHTML; + } + if (entry.error_s != null) { + let errorContainer = document.getElementById("aw-execution-errors-" + entry.id); + errorContainer.innerHTML += ('

Error:
' + entry.error_s + ''); + if (entry.error_m != null) { + errorContainer.innerHTML += ('
Error full:
' + entry.error_m + '
'); + } + } } $( document ).ready(function() { @@ -86,6 +97,6 @@ $( document ).ready(function() { setInterval('addLogLines($this)', (DATA_REFRESH_SEC * 1000)); }); apiEndpoint = "/api/job_exec"; - fetchApiTableData(apiEndpoint, updateApiTableDataJobLogs, true); - setInterval('fetchApiTableData(apiEndpoint, updateApiTableDataJobLogs, true)', (DATA_REFRESH_SEC * 1000)); + fetchApiTableData(apiEndpoint, updateApiTableDataJobLogs, true, null, null, null, true); + setInterval('fetchApiTableData(apiEndpoint, updateApiTableDataJobLogs, true, null, null, null, true)', (DATA_REFRESH_SEC * 1000)); }); diff --git a/src/ansible-webui/aw/static/js/jobs/manage.js b/src/ansible-webui/aw/static/js/jobs/manage.js index e8456fc..a354a36 100644 --- a/src/ansible-webui/aw/static/js/jobs/manage.js +++ b/src/ansible-webui/aw/static/js/jobs/manage.js @@ -50,17 +50,14 @@ function updateApiTableDataJob(row, row2, entry) { let execs = '
'; for (i = 0, len = entry.executions.length; i < len; i++) { exec = entry.executions[i]; - execs += ('
Start time: ' + exec.time_start) - execs += ('
Finish time: ' + exec.time_fin) - execs += ('
Executed by: ' + exec.user_name) - execs += ('
Status: ' + exec.status_name + '') - execs += ('
Logs: Output, ') - execs += ('Error') + execs += ('
Start time: ' + exec.time_start); + execs += ('
Finish time: ' + exec.time_fin); + execs += ('
Executed by: ' + exec.user_name); + execs += ('
Status: ' + exec.status_name + ''); + execs += ('
Logs: Output, '); + execs += ('Error'); if (exec.error_s != null) { - execs += ('

Error: ' + exec.error_s + '') - if (exec.error_m != null) { - execs += ('
Error full:
' + exec.error_m + '
') - } + execs += ('

Error: ' + exec.error_s + ''); } } execs += '
'; diff --git a/src/ansible-webui/aw/templates/jobs/logs.html b/src/ansible-webui/aw/templates/jobs/logs.html index 99b1341..8dcd6c6 100644 --- a/src/ansible-webui/aw/templates/jobs/logs.html +++ b/src/ansible-webui/aw/templates/jobs/logs.html @@ -33,6 +33,7 @@ Error
+