Skip to content

Commit

Permalink
integrated basic django/scheduler system
Browse files Browse the repository at this point in the history
  • Loading branch information
ansibleguy committed Jan 14, 2024
1 parent 5c76814 commit 050a3d3
Show file tree
Hide file tree
Showing 35 changed files with 1,284 additions and 79 deletions.
4 changes: 3 additions & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ jobs:
python-version: '3.10'

- name: Install dependencies
run: pip install -r requirements_lint.txt
run: |
pip install -r requirements_lint.txt
pip install -r requirements.txt
shell: bash

- name: Running PyLint
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ jobs:
- name: Update version in setup.py
run: >-
sed -i 's/version=.*/version="${{ steps.version.outputs.TAG_NAME }}",/g' setup.py
sed -i 's/VERSION =.*/VERSION = "${{ steps.version.outputs.TAG_NAME }}"/g' ansible-webui/aw/config/hardcoded.py
- name: Building
run: python3 -m build
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ jobs:
python-version: '3.10'

- name: Install dependencies
run: pip install -r requirements_test.txt
run: |
pip install -r requirements_test.txt
pip install -r requirements.txt
shell: bash

- name: Running Tests
Expand Down
25 changes: 25 additions & 0 deletions CONTRIBUTE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Contribute

You can run the service in its development mode:

```bash
python3 -m pip install -r ${REPO}/requirements.txt

export AW_DEV=1

python3 -m ansible-webui
# OR
cd ${REPO}/ansible-webui/
python3 __init__.py
```

Run tests and lint:

```bash
python3 -m pip install -r ${REPO}/requirements.txt
python3 -m pip install -r ${REPO}/requirements_lint.txt
python3 -m pip install -r ${REPO}/requirements_test.txt

bash ${REPO}/scripts/lint.sh
bash ${REPO}/scripts/test.sh
```
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ python3 -m ansible-webui

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)!

See also: [Contributing](https://github.com/ansibleguy/ansible-webui/blob/latest/CONTRIBUTE.md)


----

Expand All @@ -56,7 +58,7 @@ Feel free to contribute to this project using [pull-requests](https://github.com

- [ ] Ad-Hoc execution

- [ ] Scheduled execution
- [ ] Scheduled execution (Cron-Format)

- [ ] Job Logging

Expand Down
60 changes: 35 additions & 25 deletions ansible-webui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,45 @@


if __name__ == '__main__':
from multiprocessing import cpu_count
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.app.wsgiapp import WSGIApplication
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)

class StandaloneApplication(WSGIApplication):
def __init__(self, app_uri, options=None):
self.options = options or {}
self.app_uri = app_uri
super().__init__()

def load_config(self):
config = {
key: value
for key, value in self.options.items()
if key in self.cfg.settings and value is not None
}
for key, value in config.items():
self.cfg.set(key.lower(), value)

# https://docs.gunicorn.org/en/stable/settings.html
options = {
'bind': '127.0.0.1:8000',
'workers': (cpu_count() * 2) + 1,
'reload': False,
'loglevel': 'warning',
}
StandaloneApplication("aw.main:app", options).run()
schedulerThread.start()
webserver.run()
10 changes: 10 additions & 0 deletions ansible-webui/aw/config/hardcoded.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
PERMISSIONS = dict(
access='AW_ACCESS',
write='AW_WRITE',
exec='AW_EXEC',
)

VERSION = "0.0.1"
ENV_KEY_DEV = 'AW_DEV'
THREAD_JOIN_TIMEOUT = 3
RELOAD_INTERVAL = 10
30 changes: 30 additions & 0 deletions ansible-webui/aw/config/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from os import environ
from secrets import choice as random_choice
from string import digits, ascii_letters, punctuation
from datetime import datetime
from pytz import all_timezones


ENVIRON_FALLBACK = {
'timezone': {'keys': ['AW_TIMEZONE', 'TZ'], 'fallback': datetime.now().astimezone().tzname()},
'_secret': {
'keys': ['AW_TIMEZONE'],
'fallback': ''.join(random_choice(ascii_letters + digits + punctuation) for i in range(50))
},
}


def init_globals():
global config
config = {}

for cnf_key, values in ENVIRON_FALLBACK.items():
for env_key in values['keys']:
if env_key in environ:
config[cnf_key] = environ[env_key]

if cnf_key not in config:
config[cnf_key] = values['fallback']

if config['timezone'] not in all_timezones:
config['timezone'] = 'GMT'
12 changes: 12 additions & 0 deletions ansible-webui/aw/execute/play.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# https://docs.ansible.com/ansible/latest/dev_guide/developing_api.html
# https://github.com/ansible/ansible/blob/devel/lib/ansible/cli/playbook.py
# https://github.com/ansible/ansible/blob/devel/lib/ansible/cli/__init__.py
# https://github.com/ansible/ansible/blob/devel/lib/ansible/executor/playbook_executor.py

# from ansible.executor.playbook_executor import PlaybookExecutor

from aw.config.main import config


def ansible_playbook(job):
pass
17 changes: 4 additions & 13 deletions ansible-webui/aw/main.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,9 @@
"""
WSGI config for base project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/
"""

import os
from os import environ

from django.core.wsgi import get_wsgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'aw.settings')
os.environ['PYTHONIOENCODING'] = 'utf8'
os.environ['PYTHONUNBUFFERED'] = '1'
environ.setdefault('DJANGO_SETTINGS_MODULE', 'aw.settings')
environ['PYTHONIOENCODING'] = 'utf8'
environ['PYTHONUNBUFFERED'] = '1'

app = get_wsgi_application()
16 changes: 16 additions & 0 deletions ansible-webui/aw/model/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from django.db import models

BOOLEAN_CHOICES = (
(True, 'Yes'),
(False, 'No')
)


class BareModel(models.Model):
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)

class Meta:
abstract = True


47 changes: 47 additions & 0 deletions ansible-webui/aw/model/job.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from django.db import models
from django.conf import settings

from crontab import CronTab

from aw.model.base import BareModel


class JobError(BareModel):
field_list = ['short', 'logfile']

short = models.TextField(max_length=1024)
logfile = models.FilePathField()


class Job(BareModel):
field_list = ['inventory', 'playbook', 'schedule', 'name', 'job_id']

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)


class JobExecution(BareModel):
field_list = [
'user', 'start', 'fin', 'error',
'result_ok', 'result_changed', 'result_unreachable', 'result_failed', 'result_skipped',
'result_rescued', 'result_ignored',
]

user = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.DO_NOTHING, blank=True, null=True,
related_name=f"jobexec_fk_user"
)
job = models.ForeignKey(Job, on_delete=models.CASCADE, related_name=f"jobexec_fk_job")
start = models.DateTimeField(auto_now_add=True)
fin = models.DateTimeField(blank=True, null=True, default=None)
error = models.ForeignKey(JobError, on_delete=models.CASCADE, related_name=f"jobexec_fk_error")
result_ok = models.PositiveSmallIntegerField(default=0)
result_changed = models.PositiveSmallIntegerField(default=0)
result_unreachable = models.PositiveSmallIntegerField(default=0)
result_failed = models.PositiveSmallIntegerField(default=0)
result_skipped = models.PositiveSmallIntegerField(default=0)
result_rescued = models.PositiveSmallIntegerField(default=0)
result_ignored = models.PositiveSmallIntegerField(default=0)
1 change: 1 addition & 0 deletions ansible-webui/aw/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from aw.model.job import JobError, JobExecution, Job
20 changes: 20 additions & 0 deletions ansible-webui/aw/permission.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from aw.config.hardcoded import PERMISSIONS


def _group_check(user, permission: str) -> bool:
if user and user.groups.filter(name=PERMISSIONS[permission]).exists():
return True

return False


def authorized_to_access(user) -> bool:
return _group_check(user, 'access')


def authorized_to_exec(user) -> bool:
return _group_check(user, 'exec')


def authorized_to_write(user) -> bool:
return _group_check(user, 'write')
41 changes: 41 additions & 0 deletions ansible-webui/aw/route.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from django.contrib.auth.decorators import login_required, user_passes_test
from django.shortcuts import HttpResponse, redirect

from aw.permission import authorized_to_access, authorized_to_exec, authorized_to_write


def _deny(request) -> (bool, HttpResponse):
if request.method not in ['GET', 'POST', 'PUT']:
return HttpResponse(status=405)


@login_required
@user_passes_test(authorized_to_access, login_url='/')
def ui(request, **kwargs):
bad, deny = _deny(request)
if bad:
return deny

if request.method == 'POST':
return ui_write

if request.method == 'PUT':
return ui_write

return HttpResponse(status=200, content=b"OK - read")


@login_required
@user_passes_test(authorized_to_write, login_url='/')
def ui_write(request, **kwargs):
return HttpResponse(status=200, content=b"OK - write")


@login_required
@user_passes_test(authorized_to_exec, login_url='/')
def ui_exec(request, **kwargs):
return HttpResponse(status=200, content=b"OK - exec")


def catchall(request, **kwargs):
return redirect('/accounts/login/')
Loading

0 comments on commit 050a3d3

Please sign in to comment.