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 %}