diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index dc940b9..18fbbf8 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -50,4 +50,3 @@ jobs: yamllint --version yamllint . shell: bash - diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5472377..b7e1ae5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,7 +3,7 @@ on: push: tags: - - '*' + - '*' jobs: publish: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4fd8d32..faa9b3a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -38,6 +38,22 @@ jobs: pip install -r requirements.txt shell: bash + - name: Testing DB migrations for errors + run: | + python3 manage.py makemigrations + python3 manage.py migrate + shell: bash + working-directory: ansible-webui/ + + - name: Testing DB migrations for warnings + run: | + m1=$(python3 manage.py makemigrations 2>&1) + if echo "$m1" | grep -q 'WARNING'; then exit 1;fi + m2=$(python3 manage.py migrate 2>&1) + if echo "$m2" | grep -q 'WARNING'; then exit 1;fi + shell: bash + working-directory: ansible-webui/ + - name: Running Tests run: python3 -m pytest shell: bash diff --git a/.gitignore b/.gitignore index 9082957..946a485 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ dist/ venv/ ansible_webui.egg-info/ -__pychache__.py \ No newline at end of file +__pychache__.py +**/aw.db \ No newline at end of file diff --git a/.pylintrc b/.pylintrc index abb5ddb..0be883c 100644 --- a/.pylintrc +++ b/.pylintrc @@ -52,7 +52,7 @@ ignore=CVS # ignore-list. The regex matches against paths and can be in Posix or Windows # format. Because '\\' represents the directory delimiter on Windows systems, # it can't be used as an escape character. -ignore-paths= +ignore-paths=venv/* # Files or directories matching the regular expression patterns are skipped. # The regex matches against base names, not paths. The default value ignores diff --git a/.yamllint b/.yamllint index 8c3ba93..7c0d54b 100644 --- a/.yamllint +++ b/.yamllint @@ -7,3 +7,6 @@ rules: allowed-values: ['true', 'false', 'yes', 'no'] line-length: max: 160 + +ignore: | + venv/* diff --git a/README.md b/README.md index eb79c04..2d4f521 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # Ansible WebUI +[![Documentation](https://readthedocs.org/projects/ansible-webui/badge/?version=latest)](https://ansible-webui.readthedocs.io/en/latest/?badge=latest) +[![Lint](https://github.com/ansibleguy/ansible-webui/actions/workflows/lint.yml/badge.svg)](https://github.com/ansibleguy/ansible-webui/actions/workflows/lint.yml) +[![Test](https://github.com/ansibleguy/ansible-webui/actions/workflows/test.yml/badge.svg)](https://github.com/ansibleguy/ansible-webui/actions/workflows/test.yml) + This project was inspired by [ansible-semaphore](https://github.com/ansible-semaphore/semaphore). The goal is to allow users to quickly install a WebUI for using Ansible locally. @@ -27,6 +31,12 @@ python3 -m ansible-webui ---- +## Usage + +[Documentation](http://ansible-webui.readthedocs.io/) + +---- + ## Contribute Feel free to contribute to this project using [pull-requests](https://github.com/ansibleguy/ansible-webui/pulls), [issues](https://github.com/ansibleguy/ansible-webui/issues) and [discussions](https://github.com/ansibleguy/ansible-webui/discussions)! diff --git a/ansible-webui/__init__.py b/ansible-webui/__init__.py index 8c84efa..88ac1d6 100644 --- a/ansible-webui/__init__.py +++ b/ansible-webui/__init__.py @@ -2,45 +2,6 @@ if __name__ == '__main__': - import signal - from platform import uname - from threading import Thread - from os import getpid, environ - from os import kill as os_kill - from time import sleep - - from gunicorn.arbiter import Arbiter - - from aw.config.main import init_globals - init_globals() - - from base.webserver import create_webserver - from base.scheduler import Scheduler - - if uname().system.lower() != 'linux': - raise SystemError('Currently only linux systems are supported!') - - scheduler = Scheduler() - schedulerThread = Thread(target=scheduler.start) - webserver = create_webserver() - - # override gunicorn signal handling to allow for graceful shutdown - def signal_exit(signum=None, stack=None): - scheduler.stop(signum) - os_kill(getpid(), signal.SIGQUIT) # trigger 'Arbiter.stop' - sleep(5) - exit(0) - - def signal_reload(signum=None, stack=None): - scheduler.reload(signum) - - Arbiter.SIGNALS.remove(signal.SIGHUP) - Arbiter.SIGNALS.remove(signal.SIGINT) - Arbiter.SIGNALS.remove(signal.SIGTERM) - - signal.signal(signal.SIGHUP, signal_reload) - signal.signal(signal.SIGINT, signal_exit) - signal.signal(signal.SIGTERM, signal_exit) - - schedulerThread.start() - webserver.run() + # pylint: disable=E0401 + from main import main + main() diff --git a/ansible-webui/aw/config/hardcoded.py b/ansible-webui/aw/config/hardcoded.py index 9aca8e7..2e76df6 100644 --- a/ansible-webui/aw/config/hardcoded.py +++ b/ansible-webui/aw/config/hardcoded.py @@ -8,3 +8,4 @@ ENV_KEY_DEV = 'AW_DEV' THREAD_JOIN_TIMEOUT = 3 RELOAD_INTERVAL = 10 +LOGIN_PATH = '/accounts/login/' diff --git a/ansible-webui/aw/model/base.py b/ansible-webui/aw/model/base.py index 6bdf26e..82e0839 100644 --- a/ansible-webui/aw/model/base.py +++ b/ansible-webui/aw/model/base.py @@ -12,5 +12,3 @@ class BareModel(models.Model): class Meta: abstract = True - - diff --git a/ansible-webui/aw/model/job.py b/ansible-webui/aw/model/job.py index d7e4730..62c0e45 100644 --- a/ansible-webui/aw/model/job.py +++ b/ansible-webui/aw/model/job.py @@ -1,5 +1,6 @@ from django.db import models from django.conf import settings +from django.contrib.auth.models import Group from crontab import CronTab @@ -13,14 +14,45 @@ class JobError(BareModel): logfile = models.FilePathField() +class JobPermission(BareModel): + field_list = ['name', 'permission', 'users', 'groups'] + + name = models.CharField(max_length=100) + permission = models.CharField( + max_length=50, + choices=[('all', 'Full'), ('read', 'Read'), ('write', 'Write'), ('execute', 'Execute')], + ) + users = models.ManyToManyField( + settings.AUTH_USER_MODEL, + through='JobPermissionMemberUser', + through_fields=('permission', 'user'), + ) + groups = models.ManyToManyField( + Group, + through='JobPermissionMemberGroup', + through_fields=('permission', 'group'), + ) + + +class JobPermissionMemberUser(BareModel): + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) + permission = models.ForeignKey(JobPermission, on_delete=models.CASCADE) + + +class JobPermissionMemberGroup(BareModel): + group = models.ForeignKey(Group, on_delete=models.CASCADE) + permission = models.ForeignKey(JobPermission, on_delete=models.CASCADE) + + class Job(BareModel): - field_list = ['inventory', 'playbook', 'schedule', 'name', 'job_id'] + field_list = ['job_id', 'inventory', 'playbook', 'schedule', 'name', 'permission'] + job_id = models.PositiveIntegerField(primary_key=True) inventory = models.CharField(max_length=150) playbook = models.CharField(max_length=150) schedule = models.CharField(max_length=50, validators=[CronTab]) name = models.CharField(max_length=100) - job_id = models.PositiveIntegerField(max_length=50) + permission = models.ForeignKey(JobPermission, on_delete=models.SET_NULL, null=True) class JobExecution(BareModel): @@ -31,7 +63,7 @@ class JobExecution(BareModel): ] user = models.ForeignKey( - settings.AUTH_USER_MODEL, on_delete=models.DO_NOTHING, blank=True, null=True, + settings.AUTH_USER_MODEL, on_delete=models.PROTECT, blank=True, null=True, related_name=f"jobexec_fk_user" ) job = models.ForeignKey(Job, on_delete=models.CASCADE, related_name=f"jobexec_fk_job") diff --git a/ansible-webui/aw/models.py b/ansible-webui/aw/models.py index dcffde6..8d6c6f5 100644 --- a/ansible-webui/aw/models.py +++ b/ansible-webui/aw/models.py @@ -1 +1 @@ -from aw.model.job import JobError, JobExecution, Job +from aw.model.job import JobError, JobExecution, Job, JobPermission, JobPermissionMemberGroup, JobPermissionMemberUser diff --git a/ansible-webui/aw/route.py b/ansible-webui/aw/route.py index c51eba1..f648f8e 100644 --- a/ansible-webui/aw/route.py +++ b/ansible-webui/aw/route.py @@ -1,6 +1,7 @@ from django.contrib.auth.decorators import login_required, user_passes_test from django.shortcuts import HttpResponse, redirect +from aw.config.hardcoded import LOGIN_PATH from aw.permission import authorized_to_access, authorized_to_exec, authorized_to_write @@ -10,32 +11,32 @@ def _deny(request) -> (bool, HttpResponse): @login_required -@user_passes_test(authorized_to_access, login_url='/') +@user_passes_test(authorized_to_access, login_url=LOGIN_PATH) def ui(request, **kwargs): bad, deny = _deny(request) if bad: return deny if request.method == 'POST': - return ui_write + return ui_write(request) if request.method == 'PUT': - return ui_write + return ui_exec(request) return HttpResponse(status=200, content=b"OK - read") @login_required -@user_passes_test(authorized_to_write, login_url='/') +@user_passes_test(authorized_to_write, login_url=LOGIN_PATH) def ui_write(request, **kwargs): return HttpResponse(status=200, content=b"OK - write") @login_required -@user_passes_test(authorized_to_exec, login_url='/') +@user_passes_test(authorized_to_exec, login_url=LOGIN_PATH) def ui_exec(request, **kwargs): return HttpResponse(status=200, content=b"OK - exec") def catchall(request, **kwargs): - return redirect('/accounts/login/') + return redirect(LOGIN_PATH) diff --git a/ansible-webui/aw/settings.py b/ansible-webui/aw/settings.py index 24d8c79..8a82844 100644 --- a/ansible-webui/aw/settings.py +++ b/ansible-webui/aw/settings.py @@ -3,12 +3,15 @@ from os import environ # Build paths inside the project like this: BASE_DIR / 'subdir'. -BASE_DIR = Path(__file__).resolve() +BASE_DIR = Path(__file__).resolve().parent.parent from aw.config.main import config +from aw.config.hardcoded import ENV_KEY_DEV, LOGIN_PATH +DEBUG = ENV_KEY_DEV in environ ALLOWED_HOSTS = ['*'] +DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' # Application definition INSTALLED_APPS = [ @@ -83,9 +86,9 @@ # Static files (CSS, JavaScript, Images) STATIC_URL = '/static/' -STATICFILES_DIRS = [os_path.join(BASE_DIR, 'static/')] +STATICFILES_DIRS = [BASE_DIR / 'aw' / 'static'] LOGIN_REDIRECT_URL = '/ui/' -LOGOUT_REDIRECT_URL = '/accounts/login/' +LOGOUT_REDIRECT_URL = LOGIN_PATH handler403 = 'aw.utils.handlers.handler403' handler500 = 'aw.utils.handlers.handler500' diff --git a/ansible-webui/aw/templates/error/js_disabled.html b/ansible-webui/aw/templates/error/js_disabled.html index 556ff99..0296896 100644 --- a/ansible-webui/aw/templates/error/js_disabled.html +++ b/ansible-webui/aw/templates/error/js_disabled.html @@ -1,5 +1,5 @@ \ No newline at end of file diff --git a/ansible-webui/aw/templates/head.html b/ansible-webui/aw/templates/head.html index a902b74..4b55d86 100644 --- a/ansible-webui/aw/templates/head.html +++ b/ansible-webui/aw/templates/head.html @@ -14,7 +14,7 @@ {% if request.user_agent.is_mobile %} {% endif %} - + {% load bootstrap5 %} {% bootstrap_javascript %} diff --git a/ansible-webui/aw/templates/registration/login.html b/ansible-webui/aw/templates/registration/login.html index dd1adf6..26a825e 100644 --- a/ansible-webui/aw/templates/registration/login.html +++ b/ansible-webui/aw/templates/registration/login.html @@ -21,10 +21,7 @@ {% endfor %} {% endif %}
- {% if user.is_authenticated %} - Your account doesn't have access to this page. To proceed, - please login with an account that has access. - {% else %} + {% if not user.is_authenticated %} Please login to see this page. {% endif %}
@@ -43,11 +40,5 @@
-
- This is the GrowAutomation Beta-System
- You can get access to this preview by supporting our cause:
- Sponsor -
- {% endif %} {% endblock %} \ No newline at end of file diff --git a/ansible-webui/aw/templatetags/util.py b/ansible-webui/aw/templatetags/util.py index dec0c15..8d8e54b 100644 --- a/ansible-webui/aw/templatetags/util.py +++ b/ansible-webui/aw/templatetags/util.py @@ -1,5 +1,31 @@ +from django import template + from aw.config.hardcoded import VERSION +register = template.Library() + + +@register.simple_tag def get_version() -> str: return VERSION + + +@register.simple_tag +def set_var(val): + return val + + +@register.filter +def get_full_uri(request): + return request.build_absolute_uri() + + +# @register.filter +# def format_ts(datetime_obj): +# return datetime.strftime(datetime_obj, config.DATETIME_TS_FORMAT) + + +# @register.simple_tag +# def random_gif() -> str: +# return f"img/500/{randint(1, 20)}.gif" diff --git a/ansible-webui/aw/utils/util_test.py b/ansible-webui/aw/utils/util_test.py new file mode 100644 index 0000000..29cf2e0 --- /dev/null +++ b/ansible-webui/aw/utils/util_test.py @@ -0,0 +1,4 @@ + + +def test_dummy(): + pass diff --git a/ansible-webui/main.py b/ansible-webui/main.py new file mode 100644 index 0000000..f882e28 --- /dev/null +++ b/ansible-webui/main.py @@ -0,0 +1,49 @@ +import signal +from platform import uname +from threading import Thread +from os import getpid +from os import kill as os_kill +from time import sleep +from sys import exit as sys_exit + +# pylint: disable=E0401,C0413 +from gunicorn.arbiter import Arbiter + +from aw.config.main import init_globals + +init_globals() + +from base.webserver import create_webserver +from base.scheduler import Scheduler + + +def main(): + if uname().system.lower() != 'linux': + raise SystemError('Currently only linux systems are supported!') + + scheduler = Scheduler() + scheduler_thread = Thread(target=scheduler.start) + webserver = create_webserver() + + # override gunicorn signal handling to allow for graceful shutdown + def signal_exit(signum=None, stack=None): + del stack + scheduler.stop(signum) + os_kill(getpid(), signal.SIGQUIT) # trigger 'Arbiter.stop' + sleep(5) + sys_exit(0) + + def signal_reload(signum=None, stack=None): + del stack + scheduler.reload(signum) + + Arbiter.SIGNALS.remove(signal.SIGHUP) + Arbiter.SIGNALS.remove(signal.SIGINT) + Arbiter.SIGNALS.remove(signal.SIGTERM) + + signal.signal(signal.SIGHUP, signal_reload) + signal.signal(signal.SIGINT, signal_exit) + signal.signal(signal.SIGTERM, signal_exit) + + scheduler_thread.start() + webserver.run() diff --git a/ansible-webui/manage.py b/ansible-webui/manage.py index 2846a5b..5081e8f 100644 --- a/ansible-webui/manage.py +++ b/ansible-webui/manage.py @@ -1,22 +1,28 @@ -#!/usr/bin/env python -"""Django's command-line utility for administrative tasks.""" -import os -import sys +#!/usr/bin/env python3 + +from os import environ +from sys import argv as sys_argv def main(): - """Run administrative tasks.""" - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'aw.settings') + environ.setdefault('DJANGO_SETTINGS_MODULE', 'aw.settings') + + # pylint: disable=E0401,C0415 + from aw.config.main import init_globals + init_globals() + try: # pylint: disable=C0415 from django.core.management import execute_from_command_line + except ImportError as exc: raise ImportError( "Couldn't import Django. Are you sure it's installed and " "available on your PYTHONPATH environment variable? Did you " "forget to activate a virtual environment?" ) from exc - execute_from_command_line(sys.argv) + + execute_from_command_line(sys_argv) if __name__ == '__main__': diff --git a/ansible-webui/route.py b/ansible-webui/route.py index e482386..feb1277 100644 --- a/ansible-webui/route.py +++ b/ansible-webui/route.py @@ -1,3 +1,4 @@ +# pylint: disable=E0401 from django.urls import path, re_path from django.conf.urls import include from django.contrib import admin diff --git a/docs/source/conf.py b/docs/source/conf.py index 5d3b2d6..1ed7f2e 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -5,7 +5,7 @@ project = 'Ansible WebUI' copyright = f'{datetime.now().year}, AnsibleGuy' author = 'AnsibleGuy' -extensions = ['sphinx_rtd_theme', 'myst_parser'] +extensions = ['sphinx_rtd_theme'] templates_path = ['_templates'] exclude_patterns = [] html_theme = 'sphinx_rtd_theme' diff --git a/scripts/migrate_db.sh b/scripts/migrate_db.sh new file mode 100644 index 0000000..fb026ab --- /dev/null +++ b/scripts/migrate_db.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +set -e + +cd "$(dirname "$0")/../ansible-webui/" + +echo '' +echo 'Creating migrations' +echo '' + +python3 manage.py makemigrations + +echo '' +echo 'Running migrations' +echo '' + +python3 manage.py migrate