├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── features.yml │ └── problem.yml └── workflows │ ├── codeql.yml │ ├── lint.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .pylintrc ├── .yamllint ├── CHANGELOG.md ├── CONTRIBUTE.md ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── SECURITY.md ├── docker ├── .gitignore ├── Dockerfile_dev ├── Dockerfile_production ├── Dockerfile_production_aws ├── Dockerfile_production_unprivileged ├── Dockerfile_unstable ├── Dockerfile_unstable_unprivileged ├── GNUmakefile ├── entrypoint_dev.sh ├── entrypoint_production.sh ├── entrypoint_unstable.sh └── requirements.sh ├── docs ├── .readthedocs.yaml ├── html.sh ├── html_infra.sh ├── meta │ ├── robots.txt │ └── sitemap.xml ├── requirements.txt ├── source │ ├── _include │ │ ├── head.rst │ │ └── warn_develop.rst │ ├── _static │ │ ├── css │ │ │ └── main.css │ │ └── img │ │ │ ├── alert_email.png │ │ │ ├── api_docs.png │ │ │ ├── credentials_job.png │ │ │ ├── credentials_permission.png │ │ │ ├── credentials_ui.png │ │ │ ├── job_execution.png │ │ │ ├── job_prompts_1.png │ │ │ ├── job_prompts_2.png │ │ │ ├── logo.svg │ │ │ ├── permission_overview.svg │ │ │ ├── permission_ui.png │ │ │ ├── permission_users_groups.png │ │ │ ├── repo_ui.png │ │ │ └── troubleshoot_system_overview.svg │ ├── conf.py │ ├── index.rst │ └── usage │ │ ├── 1_intro.rst │ │ ├── 2_install.rst │ │ ├── 3_run.rst │ │ ├── 4_config.rst │ │ ├── alerts.rst │ │ ├── api.rst │ │ ├── authentication.rst │ │ ├── backup.rst │ │ ├── credentials.rst │ │ ├── development.rst │ │ ├── docker.rst │ │ ├── integrations.rst │ │ ├── jobs.rst │ │ ├── privileges.rst │ │ ├── repositories.rst │ │ ├── security.rst │ │ └── troubleshooting.rst └── venv.sh ├── examples ├── nginx.conf ├── saml_google_workspace.yml └── systemd_service.conf ├── pyproject.toml ├── requirements.txt ├── requirements_build.txt ├── requirements_lint.txt ├── requirements_test.txt ├── scripts ├── build.sh ├── docker_build.sh ├── docker_release.sh ├── kill_ps.sh ├── lint.sh ├── migrate_db.sh ├── run_dev.sh ├── run_pip_build.sh ├── run_shared.sh ├── run_staging.sh ├── test.sh └── update_version.sh ├── src └── ansibleguy-webui │ ├── __init__.py │ ├── __main__.py │ ├── aw │ ├── __init__.py │ ├── admin.py │ ├── api.py │ ├── api_endpoints │ │ ├── __init__.py │ │ ├── alert.py │ │ ├── base.py │ │ ├── credentials.py │ │ ├── filesystem.py │ │ ├── job.py │ │ ├── job_util.py │ │ ├── key.py │ │ ├── permission.py │ │ ├── repository.py │ │ └── system.py │ ├── apps.py │ ├── base.py │ ├── config │ │ ├── __init__.py │ │ ├── defaults.py │ │ ├── environment.py │ │ ├── form_metadata.py │ │ ├── hardcoded.py │ │ ├── main.py │ │ └── navigation.py │ ├── db_sqlite_patched │ │ ├── NOTE │ │ ├── __init__.py │ │ ├── _functions.py │ │ ├── base.py │ │ ├── client.py │ │ ├── creation.py │ │ ├── features.py │ │ ├── introspection.py │ │ ├── operations.py │ │ └── schema.py │ ├── execute │ │ ├── __init__.py │ │ ├── alert.py │ │ ├── alert_plugin │ │ │ ├── plugin_email.py │ │ │ └── plugin_wrapper.py │ │ ├── play.py │ │ ├── play_credentials.py │ │ ├── play_util.py │ │ ├── queue.py │ │ ├── repository.py │ │ ├── scheduler.py │ │ ├── threader.py │ │ └── util.py │ ├── main.py │ ├── migrations │ │ ├── 0001_v0_0_12.py │ │ ├── 0002_v0_0_13.py │ │ ├── 0003_v0_0_14.py │ │ ├── 0004_v0_0_15.py │ │ ├── 0005_v0_0_18.py │ │ ├── 0006_v0_0_19.py │ │ ├── 0007_v0_0_21.py │ │ ├── 0008_v0_0_22.py │ │ ├── 0009_v0_0_24.py │ │ ├── 0010_v0_0_25.py │ │ └── __init__.py │ ├── model │ │ ├── __init__.py │ │ ├── alert.py │ │ ├── api.py │ │ ├── base.py │ │ ├── job.py │ │ ├── job_credential.py │ │ ├── permission.py │ │ ├── repository.py │ │ └── system.py │ ├── settings.py │ ├── static │ │ ├── css │ │ │ ├── aw.css │ │ │ └── aw_mobile.css │ │ ├── img │ │ │ └── logo.svg │ │ ├── js │ │ │ ├── aw.js │ │ │ ├── aw_nav.js │ │ │ ├── jobs │ │ │ │ ├── credentials.js │ │ │ │ ├── edit.js │ │ │ │ ├── logs.js │ │ │ │ ├── manage.js │ │ │ │ └── repository.js │ │ │ ├── login.js │ │ │ └── settings │ │ │ │ ├── alert.js │ │ │ │ ├── api_key.js │ │ │ │ ├── environment.js │ │ │ │ └── permission.js │ │ └── vendor │ │ │ ├── css │ │ │ └── bootstrap.min.css │ │ │ ├── js │ │ │ ├── bootstrap.min.js │ │ │ ├── jquery.min.js │ │ │ └── popper.min.js │ │ │ └── versions.txt │ ├── templates │ │ ├── body.html │ │ ├── button │ │ │ ├── autoRefresh.html │ │ │ ├── icon │ │ │ │ ├── add.html │ │ │ │ ├── add_dir.html │ │ │ │ ├── add_git.html │ │ │ │ ├── collapse.html │ │ │ │ ├── copy.html │ │ │ │ ├── delete.html │ │ │ │ ├── download.html │ │ │ │ ├── edit.html │ │ │ │ ├── expand.html │ │ │ │ ├── return.html │ │ │ │ ├── run.html │ │ │ │ ├── sort.html │ │ │ │ ├── stop.html │ │ │ │ ├── toggle_off.html │ │ │ │ └── toggle_on.html │ │ │ └── refresh.html │ │ ├── django_saml2_auth │ │ │ ├── denied.html │ │ │ ├── error.html │ │ │ └── signout.html │ │ ├── email │ │ │ ├── alert.html │ │ │ └── alert.txt │ │ ├── error │ │ │ ├── 403.html │ │ │ ├── 500.html │ │ │ └── js_disabled.html │ │ ├── fallback.html │ │ ├── forms │ │ │ ├── base.html │ │ │ ├── job.html │ │ │ ├── snippet.html │ │ │ └── snippet_test.html │ │ ├── head.html │ │ ├── jobs │ │ │ ├── credentials.html │ │ │ ├── credentials_edit.html │ │ │ ├── edit.html │ │ │ ├── logs.html │ │ │ ├── manage.html │ │ │ ├── repository.html │ │ │ └── repository_edit.html │ │ ├── nav.html │ │ ├── registration │ │ │ ├── login.html │ │ │ ├── password_change_done.html │ │ │ ├── password_change_form.html │ │ │ ├── remember_user.html │ │ │ └── saml.html │ │ ├── rest_framework │ │ │ └── api.html │ │ ├── settings │ │ │ ├── alert.html │ │ │ ├── alert_edit.html │ │ │ ├── api_key.html │ │ │ ├── permission.html │ │ │ └── permission_edit.html │ │ └── system │ │ │ ├── config.html │ │ │ └── environment.html │ ├── templatetags │ │ ├── __init__.py │ │ ├── form_util.py │ │ └── util.py │ ├── urls.py │ ├── utils │ │ ├── __init__.py │ │ ├── crypto.py │ │ ├── debug.py │ │ ├── deployment.py │ │ ├── handlers.py │ │ ├── http.py │ │ ├── permission.py │ │ ├── subps.py │ │ ├── util.py │ │ ├── util_no_config.py │ │ ├── util_test.py │ │ └── version.py │ └── views │ │ ├── __init__.py │ │ ├── base.py │ │ ├── forms │ │ ├── auth.py │ │ ├── job.py │ │ ├── settings.py │ │ └── system.py │ │ ├── job.py │ │ ├── main.py │ │ ├── settings.py │ │ ├── system.py │ │ └── validation.py │ ├── cli.py │ ├── cli_init.py │ ├── db.py │ ├── handle_signals.py │ ├── main.py │ ├── manage.py │ ├── web_serve_static.py │ └── webserver.py └── test ├── ansible.cfg ├── demo ├── play1.yml ├── reset.sh └── web-service.yml ├── integration ├── api │ └── main.py ├── auth │ ├── saml.py │ └── saml.yml ├── config.yml └── webui │ └── main.py ├── inv └── hosts.yml ├── play1.yml └── roles └── test1 ├── defaults └── main.yml └── tasks └── main.yml /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | --- 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/features.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: Feature request 4 | description: Suggest an idea for this project 5 | title: "Feature: " 6 | labels: ['enhancement', 'triage'] 7 | 8 | body: 9 | - type: markdown 10 | attributes: 11 | value: | 12 | Please make sure to go through these steps **before opening an issue**: 13 | 14 | - [ ] Read the documentation to make sure the feature does not yet exist: 15 | [Docs](https://webui.ansibleguy.net/) 16 | 17 | - [ ] Make sure the feature is in-scope. We cannot extend the basic functionality of Ansible itself: 18 | [Ansible Docs](https://docs.ansible.com/ansible/2.8/user_guide/playbooks_best_practices.html#directory-layout) 19 | 20 | The Ansible-WebUI project tries to keep its codebase as simple & small as possible. 21 | Be aware we will not implement all 'nice-to-have' or 'fancy' features. 22 | 23 | - type: dropdown 24 | id: scope 25 | attributes: 26 | label: Scope 27 | description: What version of our software are you running? 28 | options: 29 | - Unknown 30 | - Frontend (User Interface) 31 | - Ansible (Job Execution) 32 | - Backend (API) 33 | - Service (Job Scheduling, Job Preparation) 34 | - Database 35 | default: 0 36 | validations: 37 | required: true 38 | 39 | - type: textarea 40 | id: description 41 | attributes: 42 | label: Description 43 | description: | 44 | A clear and concise description of: 45 | 46 | * what you want to happen 47 | * what you are missing 48 | * why that would be beneficial 49 | validations: 50 | required: true 51 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/problem.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: Problem 4 | description: You have encountered problems when using the modules 5 | title: "Problem: " 6 | labels: ['problem', 'triage'] 7 | 8 | body: 9 | - type: markdown 10 | attributes: 11 | value: | 12 | Please make sure to go through these steps **before opening an issue**: 13 | 14 | - [ ] Read the documentation: [Docs](https://webui.ansibleguy.net/) 15 | 16 | - [ ] Read the troubleshooting info: [Troubleshooting](https://webui.ansibleguy.net/usage/troubleshooting.html) 17 | 18 | - [ ] Check if there are existing [issues](https://github.com/ansibleguy/webui/issues) 19 | or [discussions](https://github.com/ansibleguy/webui/discussions) regarding your topic 20 | 21 | - type: textarea 22 | id: versions 23 | attributes: 24 | label: Versions 25 | description: | 26 | Provide your system versions. 27 | You can find it at `System - Environment` 28 | Click the copy-button below the `System version` table and paste them here. 29 | validations: 30 | required: true 31 | 32 | - type: dropdown 33 | id: scope 34 | attributes: 35 | label: Scope 36 | description: What version of our software are you running? 37 | options: 38 | - Unknown 39 | - Frontend (User Interface) 40 | - Ansible (Job Execution) 41 | - Backend (API) 42 | - Service (Job Scheduling, Job Preparation) 43 | - Database 44 | default: 0 45 | validations: 46 | required: true 47 | 48 | - type: textarea 49 | id: problem 50 | attributes: 51 | label: Issue 52 | description: | 53 | Describe the problem you encountered and tell us what you would have expected to happen 54 | validations: 55 | required: true 56 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: 'CodeQL' 4 | 5 | on: 6 | push: 7 | tags: 8 | - '*' 9 | # takes too long to run on every push.. 10 | # push: 11 | # branches: ['latest'] 12 | # paths: 13 | # - '**.py' 14 | # - '.github/workflows/codeql.yml' 15 | # pull_request: 16 | # branches: ['latest'] 17 | # paths: 18 | # - '**.py' 19 | # - '.github/workflows/codeql.yml' 20 | 21 | jobs: 22 | analyze: 23 | name: Analyze 24 | runs-on: 'ubuntu-latest' 25 | timeout-minutes: 180 26 | permissions: 27 | security-events: write 28 | actions: read 29 | contents: read 30 | 31 | steps: 32 | - name: Checkout repository 33 | uses: actions/checkout@v4 34 | 35 | - name: Initialize CodeQL 36 | uses: github/codeql-action/init@v3 37 | with: 38 | languages: 'python' 39 | 40 | - name: Autobuild 41 | uses: github/codeql-action/autobuild@v3 42 | 43 | - name: Perform CodeQL Analysis 44 | uses: github/codeql-action/analyze@v3 45 | with: 46 | category: '/language:python' 47 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: Lint 4 | 5 | on: 6 | push: 7 | branches: [latest] 8 | paths: 9 | - '**.py' 10 | - '**.yml' 11 | - '.github/workflows/lint.yml' 12 | - 'requirements_lint.txt' 13 | pull_request: 14 | branches: [latest] 15 | paths: 16 | - '**.py' 17 | - '**.yml' 18 | - '.github/workflows/lint.yml' 19 | - 'requirements_lint.txt' 20 | jobs: 21 | build: 22 | runs-on: ubuntu-latest 23 | timeout-minutes: 2 24 | defaults: 25 | run: 26 | shell: bash 27 | 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v3 31 | with: 32 | ref: ${{ github.ref }} 33 | 34 | - name: Install python 35 | uses: actions/setup-python@v4 36 | with: 37 | python-version: '3.11' 38 | 39 | - name: Install dependencies 40 | run: | 41 | pip install -r requirements_lint.txt >/dev/null 42 | pip install -r requirements.txt >/dev/null 43 | 44 | - name: Running PyLint 45 | run: | 46 | export DJANGO_SETTINGS_MODULE='aw.settings' 47 | export AW_INIT=1 48 | pylint --version 49 | pylint --rcfile .pylintrc --recursive=y --load-plugins pylint_django --django-settings-module=aw.settings . 50 | 51 | - name: Running YamlLint 52 | run: | 53 | yamllint --version 54 | yamllint . 55 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: Release on PyPI 4 | 5 | on: 6 | push: 7 | tags: 8 | - '*' 9 | 10 | jobs: 11 | publish: 12 | name: Upload release to PyPI 13 | runs-on: ubuntu-latest 14 | timeout-minutes: 3 15 | defaults: 16 | run: 17 | shell: bash 18 | 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v3 22 | with: 23 | ref: ${{ github.ref }} 24 | 25 | - name: Install python 26 | uses: actions/setup-python@v4 27 | with: 28 | python-version: '3.11' 29 | 30 | - name: Install dependencies 31 | run: | 32 | pip install -r requirements_build.txt >/dev/null 33 | pip install -r requirements.txt >/dev/null 34 | 35 | # NOTE: timeout for running the app includes db migrations 36 | - name: Testing to build Ansible-WebUI with PIP 37 | run: | 38 | cd /tmp 39 | echo 'CREATING TMP VENV' 40 | tmp_venv="/tmp/aw-venv/$(date +%s)" 41 | python3 -m virtualenv "$tmp_venv" >/dev/null 42 | source "${tmp_venv}/bin/activate" 43 | 44 | echo 'INSTALLING MODULE' 45 | python3 -m pip install -e "$GITHUB_WORKSPACE" >/dev/null 46 | 47 | set +e 48 | echo 'RUNNING APP' 49 | export AW_DB="/tmp/$(date +%s).aw.db" 50 | timeout 20 python3 -m ansibleguy-webui 51 | ec="$?" 52 | 53 | echo 'CLEANUP' 54 | deactivate 55 | rm -rf "$tmp_venv" 56 | 57 | if [[ "$ec" != "124" ]] 58 | then 59 | exit 1 60 | fi 61 | 62 | - name: Extract tag name 63 | id: version 64 | run: echo ::set-output name=TAG_NAME::$(echo $GITHUB_REF | cut -d / -f 3 | cut -c 1-) 65 | 66 | - name: Building 67 | run: | 68 | git reset --hard 69 | git ls-files . --exclude-standard --others | grep 'migrations' | xargs --no-run-if-empty rm 70 | 71 | echo "${{ steps.version.outputs.TAG_NAME }}" > VERSION 72 | python3 -m build 73 | 74 | - name: Publish to PyPI 75 | uses: pypa/gh-action-pypi-publish@release/v1 76 | with: 77 | password: ${{ secrets.PYPI_API_TOKEN }} 78 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | dist/ 3 | venv/ 4 | ansible_webui.egg-info/ 5 | __pychache__.py 6 | **/aw*.db 7 | **/aw*.db.*bak* 8 | **/aw*.db-shm 9 | **/aw*.db-wal 10 | VERSION 11 | docs/build/ -------------------------------------------------------------------------------- /.yamllint: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | extends: default 4 | 5 | rules: 6 | truthy: 7 | allowed-values: ['true', 'false', 'yes', 'no'] 8 | line-length: 9 | max: 160 10 | 11 | ignore: | 12 | venv/* 13 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include src/ansibleguy-webui/* 2 | include requirements.txt -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | This project is currently still in early development. 4 | 5 | If you find any security issue - we are happy to fix them. 6 | 7 | ## Supported Versions 8 | 9 | 10 | | Version | Supported | 11 | | ------- | ------------------ | 12 | | 0.0.x | :white_check_mark: | 13 | 14 | ## Reporting a Vulnerability 15 | 16 | You can currently report security vulnerabilities [as issues](https://github.com/ansibleguy/webui/issues/new/choose) 17 | 18 | We will try to get back to you as soon as possible. 19 | -------------------------------------------------------------------------------- /docker/.gitignore: -------------------------------------------------------------------------------- 1 | aw_secret 2 | storage 3 | ./var 4 | -------------------------------------------------------------------------------- /docker/Dockerfile_dev: -------------------------------------------------------------------------------- 1 | FROM python:3.11-alpine 2 | 3 | # BUILD: docker build -f Dockerfile_dev -t ansible0guy/webui:dev --no-cache . 4 | # RUN: docker run -it --name ansible-webui-dev --publish 127.0.0.1:8000:8000 --volume /tmp/awtest:/data --volume $(pwd):/aw ansible-webui:dev 5 | 6 | RUN apk add --no-cache openssh-client sshpass xmlsec && \ 7 | pip install --no-cache-dir --upgrade pip 2>/dev/null && \ 8 | mkdir -p /data/log 9 | 10 | ENV AW_ENV=dev \ 11 | AW_VERSION=dev \ 12 | AW_DOCKER=1 \ 13 | PYTHONUNBUFFERED=1 \ 14 | AW_DB=/data/aw.db \ 15 | AW_PATH_LOG=/data/log \ 16 | AW_PATH_PLAY=/aw/test 17 | WORKDIR /aw/test 18 | EXPOSE 8000 19 | 20 | COPY --chmod=0755 entrypoint_dev.sh /entrypoint.sh 21 | COPY --chmod=0755 requirements.sh /entrypoint_requirements.sh 22 | ENTRYPOINT ["/entrypoint.sh"] 23 | -------------------------------------------------------------------------------- /docker/Dockerfile_production: -------------------------------------------------------------------------------- 1 | FROM python:3.11-alpine 2 | 3 | ARG AW_VERSION 4 | 5 | # BUILD CMD: docker build -f Dockerfile_production -t ansible-webui: --build-arg "AW_VERSION=" --no-cache . 6 | # RUN: docker run -d --name ansible-webui --publish 127.0.0.1:8000:8000 --volume $(pwd)/ansible/data:/data --volume $(pwd)/ansible/play:/play ansible-webui: 7 | 8 | RUN apk add --no-cache git git-lfs openssh-client sshpass xmlsec && \ 9 | pip install --no-cache-dir --upgrade pip 2>/dev/null && \ 10 | pip install --no-cache-dir "git+https://github.com/ansibleguy/webui.git@${AW_VERSION}" && \ 11 | mkdir -p /play /data/log 12 | 13 | ENV AW_VERSION=${AW_VERSION} \ 14 | AW_DOCKER=1 \ 15 | PYTHONUNBUFFERED=1 \ 16 | AW_DB=/data/aw.db \ 17 | AW_PATH_LOG=/data/log \ 18 | AW_PATH_PLAY=/play 19 | WORKDIR /play 20 | EXPOSE 8000 21 | 22 | COPY --chmod=0755 entrypoint_production.sh /entrypoint.sh 23 | COPY --chmod=0755 requirements.sh /entrypoint_requirements.sh 24 | ENTRYPOINT ["/entrypoint.sh"] 25 | -------------------------------------------------------------------------------- /docker/Dockerfile_production_aws: -------------------------------------------------------------------------------- 1 | ARG AW_VERSION=0.0 2 | 3 | # BUILD: docker build -f Dockerfile_production_aws -t "ansible0guy/webui-aws:" --build-arg "AW_VERSION=" --no-cache --progress=plain . 4 | 5 | # references: 6 | # https://github.com/ansibleguy/webui/discussions/5 7 | # https://github.com/aws/session-manager-plugin/issues/12#issuecomment-972880203 8 | # https://github.com/aws/session-manager-plugin/blob/mainline/Dockerfile 9 | # https://github.com/aws/session-manager-plugin/blob/mainline/makefile 10 | 11 | FROM public.ecr.aws/docker/library/golang:1.17-alpine AS ssm-builder 12 | # https://hub.docker.com/_/golang/tags?page=1&name=1.15 13 | 14 | ARG SSM_VERSION=1.2.694.0 15 | # ssm version: https://github.com/aws/session-manager-plugin/releases 16 | 17 | RUN apk add --no-cache make git gcc libc-dev curl bash zip && \ 18 | curl -sLO https://github.com/aws/session-manager-plugin/archive/${SSM_VERSION}.tar.gz && \ 19 | mkdir -p /go/src/github.com && \ 20 | tar xzf ${SSM_VERSION}.tar.gz && \ 21 | mv session-manager-plugin-${SSM_VERSION} /go/src/github.com/session-manager-plugin && \ 22 | echo -n ${SSM_VERSION} > /go/src/github.com/session-manager-plugin/VERSION && \ 23 | cd /go/src/github.com/session-manager-plugin && \ 24 | make pre-build && \ 25 | make pre-release && \ 26 | make build-linux-amd64 && \ 27 | make prepack-linux-amd64 28 | 29 | # debugging - check if executables exist and are working: 30 | # /go/src/github.com/session-manager-plugin/bin/linux_amd64_plugin/session-manager-plugin --version && 31 | # /go/src/github.com/session-manager-plugin/bin/linux_amd64/ssmcli help 32 | 33 | FROM ansible0guy/webui-unprivileged:${AW_VERSION} 34 | ARG AW_USER=aw 35 | 36 | USER root 37 | RUN apk add py3-boto3 aws-cli 38 | COPY --from=ssm-builder /go/src/github.com/session-manager-plugin/bin/linux_amd64_plugin/session-manager-plugin \ 39 | /go/src/github.com/session-manager-plugin/bin/linux_amd64/ssmcli \ 40 | /usr/bin/ 41 | USER ${AW_USER} 42 | -------------------------------------------------------------------------------- /docker/Dockerfile_production_unprivileged: -------------------------------------------------------------------------------- 1 | FROM python:3.11-alpine 2 | 3 | # BUILD CMD: docker build -f Dockerfile_production -t ansible-webui: --build-arg "AW_VERSION=" --no-cache . 4 | # RUN: 5 | # sudo useradd ansible-webui --shell /usr/sbin/nologin --uid 8785 --user-group 6 | # mkdir ${YOUR_DATA_DIR} && chown -R ansible-webui ${YOUR_DATA_DIR} 7 | # docker run -d --name ansible-webui --publish 127.0.0.1:8000:8000 --volume ${YOUR_DATA_DIR}:/data --volume $(pwd)/ansible/play:/play ansible-webui: 8 | 9 | ARG AW_VERSION 10 | ARG AW_UID=8785 11 | ARG AW_USER=aw 12 | 13 | RUN apk add --no-cache git git-lfs openssh-client sshpass xmlsec && \ 14 | pip install --no-cache-dir --upgrade pip 2>/dev/null && \ 15 | pip install --no-cache-dir "git+https://github.com/ansibleguy/webui.git@${AW_VERSION}" && \ 16 | adduser --uid ${AW_UID} --home /home/ansible-webui --shell /usr/sbin/nologin --disabled-password ${AW_USER} ${AW_USER} && \ 17 | mkdir -p /play /data/log && \ 18 | chown -R ${AW_USER}:${AW_USER} /data /play 19 | 20 | 21 | ENV AW_VERSION=${AW_VERSION} \ 22 | AW_DOCKER=1 \ 23 | PYTHONUNBUFFERED=1 \ 24 | AW_DB=/data/aw.db \ 25 | AW_PATH_LOG=/data/log \ 26 | AW_PATH_PLAY=/play 27 | WORKDIR /play 28 | USER ${AW_USER} 29 | EXPOSE 8000 30 | 31 | COPY --chmod=0755 entrypoint_production.sh /entrypoint.sh 32 | COPY --chmod=0755 requirements.sh /entrypoint_requirements.sh 33 | ENTRYPOINT ["/entrypoint.sh"] 34 | -------------------------------------------------------------------------------- /docker/Dockerfile_unstable: -------------------------------------------------------------------------------- 1 | FROM python:3.11-alpine 2 | 3 | # BUILD: docker build -f Dockerfile_unstable -t ansible0guy/webui:unstable --no-cache . 4 | # RUN: docker run -it --name ansible-webui-dev --publish 127.0.0.1:8000:8000 --volume /tmp/awtest:/data ansible0guy/webui:unstable 5 | 6 | RUN apk add --no-cache git git-lfs openssh-client sshpass xmlsec && \ 7 | pip install --no-cache-dir --upgrade pip 2>/dev/null && \ 8 | mkdir -p /play /data/log 9 | 10 | ENV AW_ENV=dev \ 11 | AW_VERSION=latest \ 12 | AW_DOCKER=1 \ 13 | PYTHONUNBUFFERED=1 \ 14 | AW_DB=/data/aw.db \ 15 | AW_PATH_LOG=/data/log \ 16 | AW_PATH_PLAY=/play 17 | WORKDIR /play 18 | EXPOSE 8000 19 | 20 | COPY --chmod=0755 entrypoint_unstable.sh /entrypoint.sh 21 | COPY --chmod=0755 requirements.sh /entrypoint_requirements.sh 22 | ENTRYPOINT ["/entrypoint.sh"] 23 | -------------------------------------------------------------------------------- /docker/Dockerfile_unstable_unprivileged: -------------------------------------------------------------------------------- 1 | FROM python:3.11-alpine 2 | 3 | # BUILD: docker build -f Dockerfile_unstable_unprivileged -t ansible0guy/webui-unprivileged:unstable --no-cache . 4 | # RUN: docker run -it --name ansible-webui-unstable --publish 127.0.0.1:8000:8000 ansible0guy/webui-unprivileged:unstable 5 | # RUN WITH PERSISTENT DB: 6 | # sudo useradd ansible-webui --shell /usr/sbin/nologin --uid 8785 --user-group 7 | # mkdir ${YOUR_DATA_DIR} && chown -R ansible-webui ${YOUR_DATA_DIR} 8 | # docker run -it --name ansible-webui-unstable --publish 127.0.0.1:8000:8000 --volume ${YOUR_DATA_DIR}:/data ansible-webui:unstable 9 | 10 | ARG AW_UID=8785 11 | ARG AW_USER=aw 12 | 13 | RUN apk add --no-cache git git-lfs openssh-client sshpass xmlsec && \ 14 | pip install --no-cache-dir --upgrade pip 2>/dev/null && \ 15 | adduser --uid ${AW_UID} --home /home/ansible-webui --shell /usr/sbin/nologin --disabled-password ${AW_USER} ${AW_USER} && \ 16 | mkdir -p /play /data/log && \ 17 | chown -R ${AW_USER}:${AW_USER} /data /play 18 | 19 | ENV AW_ENV=dev \ 20 | AW_VERSION=latest \ 21 | AW_DOCKER=1 \ 22 | PYTHONUNBUFFERED=1 \ 23 | AW_DB=/data/aw.db \ 24 | AW_PATH_LOG=/data/log \ 25 | AW_PATH_PLAY=/play 26 | WORKDIR /play 27 | USER ${AW_USER} 28 | EXPOSE 8000 29 | 30 | COPY --chmod=0755 entrypoint_unstable.sh /entrypoint.sh 31 | COPY --chmod=0755 requirements.sh /entrypoint_requirements.sh 32 | ENTRYPOINT ["/entrypoint.sh"] 33 | -------------------------------------------------------------------------------- /docker/entrypoint_dev.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if ! [ -d '/aw' ] 4 | then 5 | echo 'YOU HAVE TO MOUNT THE APP SOURCES AT /aw' 6 | exit 1 7 | fi 8 | 9 | echo 'INSTALLING/UPGRADING REQUIREMENTS..' 10 | pip install --upgrade -r /aw/requirements.txt --root-user-action=ignore --no-warn-script-location >/dev/null 11 | 12 | . /entrypoint_requirements.sh 13 | 14 | python3 /aw/src/ansibleguy-webui/ 15 | -------------------------------------------------------------------------------- /docker/entrypoint_production.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | . /entrypoint_requirements.sh 4 | 5 | python3 -m ansibleguy-webui 6 | -------------------------------------------------------------------------------- /docker/entrypoint_unstable.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | . /entrypoint_requirements.sh 4 | 5 | echo 'INSTALLING/UPGRADING latest..' 6 | pip install --no-warn-script-location --upgrade --force-reinstall --no-cache-dir --root-user-action=ignore --no-warn-script-location "git+https://github.com/ansibleguy/webui.git@latest" >/dev/null 7 | 8 | python3 -m ansibleguy-webui 9 | -------------------------------------------------------------------------------- /docker/requirements.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo 'INSTALLING/UPGRADING DEFAULT ANSIBLE DEPENDENCIES..' 4 | pip install --upgrade jmespath netaddr passlib pywinrm requests cryptography --root-user-action=ignore --no-warn-script-location >/dev/null 5 | 6 | if [ -f '/play/requirements.txt' ] 7 | then 8 | echo 'INSTALLING/UPGRADING PYTHON MODULES..' 9 | pip install --upgrade -r '/play/requirements.txt' --root-user-action=ignore --no-warn-script-location >/dev/null 10 | fi 11 | 12 | if [ -f '/play/requirements.yml' ] 13 | then 14 | echo 'INSTALLING/UPGRADING ANSIBLE-COLLECTIONS..' 15 | ansible-galaxy collection install --upgrade -r /play/requirements.yml >/dev/null 16 | echo 'INSTALLING ANSIBLE-ROLES..' 17 | ansible-galaxy role install --force -r /play/requirements.yml >/dev/null 18 | fi 19 | 20 | if [ -f '/play/requirements_collections.yml' ] 21 | then 22 | echo 'INSTALLING/UPGRADING ANSIBLE-COLLECTIONS..' 23 | ansible-galaxy collection install --upgrade -r /play/requirements_collections.yml >/dev/null 24 | fi 25 | 26 | if [ -f '/play/collections/requirements.yml' ] 27 | then 28 | echo 'INSTALLING/UPGRADING ANSIBLE-COLLECTIONS..' 29 | ansible-galaxy collection install --upgrade -r /play/collections/requirements.yml >/dev/null 30 | fi 31 | 32 | if [ -f '/play/requirements_roles.yml' ] 33 | then 34 | echo 'INSTALLING ANSIBLE-ROLES..' 35 | ansible-galaxy role install --force -r /play/requirements_roles.yml >/dev/null 36 | fi 37 | 38 | if [ -f '/play/roles/requirements.yml' ] 39 | then 40 | echo 'INSTALLING ANSIBLE-ROLES..' 41 | ansible-galaxy role install --force -r /play/roles/requirements.yml >/dev/null 42 | fi 43 | -------------------------------------------------------------------------------- /docs/.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | version: 2 6 | 7 | build: 8 | os: 'ubuntu-22.04' 9 | tools: 10 | python: "3.11" 11 | 12 | sphinx: 13 | configuration: 'docs/source/conf.py' 14 | 15 | # Optionally build your docs in additional formats such as PDF and ePub 16 | formats: 17 | - 'pdf' 18 | - 'epub' 19 | 20 | python: 21 | install: 22 | - requirements: 'docs/requirements.txt' 23 | -------------------------------------------------------------------------------- /docs/html.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | cd "$(dirname "$0")" 6 | 7 | rm -rf build 8 | mkdir build 9 | 10 | sphinx-build -b html source/ build/ 11 | -------------------------------------------------------------------------------- /docs/html_infra.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -z "$1" ] 4 | then 5 | DEST_DIR='build' 6 | else 7 | DEST_DIR="$1" 8 | fi 9 | 10 | set -euo pipefail 11 | 12 | function log() { 13 | msg="$1" 14 | echo '' 15 | echo "### ${msg} ###" 16 | echo '' 17 | } 18 | 19 | cd "$(dirname "$0")" 20 | 21 | SRC_DIR="$(pwd)" 22 | 23 | TS="$(date +%s)" 24 | TMP_DIR="/tmp/${TS}" 25 | mkdir -p "${TMP_DIR}" 26 | 27 | VENV_BIN='/tmp/.ag-docs-venv/bin/activate' 28 | if [ -f "$VENV_BIN" ] 29 | then 30 | source "$VENV_BIN" 31 | fi 32 | 33 | log 'BUILDING DOCS' 34 | export PYTHONWARNINGS='ignore' 35 | sphinx-build -b html source/ "${TMP_DIR}/" >/dev/null 36 | 37 | log 'PATCHING METADATA' 38 | cp "${SRC_DIR}/meta/"* "${TMP_DIR}/" 39 | 40 | HTML_META_SRC="" 41 | HTML_META="${HTML_META_SRC}" 42 | HTML_META="${HTML_META}" 43 | HTML_META_EN="${HTML_META}" # 44 | # HTML_LOGO_LINK_SRC='href=".*Go to homepage"' 45 | # HTML_LOGO_LINK_EN='href="https://www.o-x-l.com" class="oxl-nav-logo" title="OXL IT Services Website"' 46 | HTML_TITLE_BAD_EN='AnsibleGuy WebUI documentation' 47 | HTML_TITLE_OK='Simple Ansible WebUI' 48 | HTML_LANG_NONE=' 2 | 3 | https://webui.ansibleguy.net/ 4 | 5 | https://webui.ansibleguy.net/usage/1_intro.html 6 | https://webui.ansibleguy.net/usage/2_install.html 7 | https://webui.ansibleguy.net/usage/3_run.html 8 | https://webui.ansibleguy.net/usage/4_config.html 9 | 10 | https://webui.ansibleguy.net/usage/alerts.html 11 | https://webui.ansibleguy.net/usage/api.html 12 | https://webui.ansibleguy.net/usage/authentication.html 13 | https://webui.ansibleguy.net/usage/backup.html 14 | https://webui.ansibleguy.net/usage/credentials.html 15 | https://webui.ansibleguy.net/usage/development.html 16 | https://webui.ansibleguy.net/usage/docker.html 17 | https://webui.ansibleguy.net/usage/integrations.html 18 | https://webui.ansibleguy.net/usage/jobs.html 19 | https://webui.ansibleguy.net/usage/privileges.html 20 | https://webui.ansibleguy.net/usage/repositories.html 21 | https://webui.ansibleguy.net/usage/security.html 22 | https://webui.ansibleguy.net/usage/troubleshooting.html 23 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx 2 | piccolo_theme -------------------------------------------------------------------------------- /docs/source/_include/head.rst: -------------------------------------------------------------------------------- 1 | .. tip:: 2 | Check out `the repository on GitHub `_ 3 | 4 | Check out the demo at: `demo.webui.ansibleguy.net `_ | 5 | Login: User :code:`demo`, Password :code:`Ansible1337` 6 | 7 | .. warning:: 8 | **DISCLAIMER**: This is an **unofficial community project**! Do not confuse it with the vanilla `Ansible `_ product! 9 | -------------------------------------------------------------------------------- /docs/source/_include/warn_develop.rst: -------------------------------------------------------------------------------- 1 | .. warning:: 2 | This project still in early development! **DO NOT USE IN PRODUCTION!** 3 | -------------------------------------------------------------------------------- /docs/source/_static/css/main.css: -------------------------------------------------------------------------------- 1 | .wiki-img { 2 | width: 90%; 3 | margin: 5px 0px; 4 | border-radius: 5px; 5 | border-style: solid; 6 | border-color: #343a40; 7 | border-width: 3px; 8 | } 9 | 10 | .wiki-img-sm { 11 | width: 60%; 12 | margin: 5px 0px; 13 | border-radius: 5px; 14 | border-style: solid; 15 | border-color: #343a40; 16 | border-width: 3px; 17 | } 18 | -------------------------------------------------------------------------------- /docs/source/_static/img/alert_email.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansibleguy/webui/933b57a51f90c42a7679868e3b33ccc73f5def05/docs/source/_static/img/alert_email.png -------------------------------------------------------------------------------- /docs/source/_static/img/api_docs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansibleguy/webui/933b57a51f90c42a7679868e3b33ccc73f5def05/docs/source/_static/img/api_docs.png -------------------------------------------------------------------------------- /docs/source/_static/img/credentials_job.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansibleguy/webui/933b57a51f90c42a7679868e3b33ccc73f5def05/docs/source/_static/img/credentials_job.png -------------------------------------------------------------------------------- /docs/source/_static/img/credentials_permission.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansibleguy/webui/933b57a51f90c42a7679868e3b33ccc73f5def05/docs/source/_static/img/credentials_permission.png -------------------------------------------------------------------------------- /docs/source/_static/img/credentials_ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansibleguy/webui/933b57a51f90c42a7679868e3b33ccc73f5def05/docs/source/_static/img/credentials_ui.png -------------------------------------------------------------------------------- /docs/source/_static/img/job_execution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansibleguy/webui/933b57a51f90c42a7679868e3b33ccc73f5def05/docs/source/_static/img/job_execution.png -------------------------------------------------------------------------------- /docs/source/_static/img/job_prompts_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansibleguy/webui/933b57a51f90c42a7679868e3b33ccc73f5def05/docs/source/_static/img/job_prompts_1.png -------------------------------------------------------------------------------- /docs/source/_static/img/job_prompts_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansibleguy/webui/933b57a51f90c42a7679868e3b33ccc73f5def05/docs/source/_static/img/job_prompts_2.png -------------------------------------------------------------------------------- /docs/source/_static/img/permission_ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansibleguy/webui/933b57a51f90c42a7679868e3b33ccc73f5def05/docs/source/_static/img/permission_ui.png -------------------------------------------------------------------------------- /docs/source/_static/img/permission_users_groups.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansibleguy/webui/933b57a51f90c42a7679868e3b33ccc73f5def05/docs/source/_static/img/permission_users_groups.png -------------------------------------------------------------------------------- /docs/source/_static/img/repo_ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansibleguy/webui/933b57a51f90c42a7679868e3b33ccc73f5def05/docs/source/_static/img/repo_ui.png -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | # pylint: disable=W0622 4 | project = 'AnsibleGuy WebUI' 5 | copyright = f'{datetime.now().year}, AnsibleGuy' 6 | author = 'AnsibleGuy' 7 | extensions = ['piccolo_theme'] 8 | templates_path = ['_templates'] 9 | exclude_patterns = [] 10 | html_theme = 'piccolo_theme' 11 | html_static_path = ['_static'] 12 | html_logo = '_static/img/logo.svg' 13 | html_favicon = '_static/img/logo.svg' 14 | html_js_files = ['https://files.oxl.at/js/feedback.js'] 15 | html_css_files = ['css/main.css', 'https://files.oxl.at/css/feedback.css'] 16 | master_doc = 'index' 17 | display_version = True 18 | sticky_navigation = True 19 | source_suffix = { 20 | '.rst': 'restructuredtext', 21 | '.md': 'markdown', 22 | } 23 | html_theme_options = { 24 | 'banner_text': 'Repository on GitHub | ' 25 | 'Report Errors | ' 26 | 'Get Support' 27 | } 28 | html_short_title = 'Ansible WebUI' 29 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | ===== 2 | Index 3 | ===== 4 | 5 | .. include:: _include/head.rst 6 | 7 | .. toctree:: 8 | :caption: Usage 9 | :glob: 10 | :maxdepth: 1 11 | 12 | usage/* 13 | -------------------------------------------------------------------------------- /docs/source/usage/1_intro.rst: -------------------------------------------------------------------------------- 1 | .. _usage_intro: 2 | 3 | .. include:: ../_include/head.rst 4 | 5 | ========= 6 | 1 - Intro 7 | ========= 8 | 9 | Comparison 10 | ********** 11 | 12 | There are multiple Ansible WebUI products - how do they compare to this product? 13 | 14 | * `Ansible AWX `_ / `Ansible Automation Platform `_ 15 | 16 | If you want an enterprise-grade solution - you might want to use these official products. 17 | 18 | They have many neat features and are designed to run in containerized & scalable environments. 19 | 20 | The actual enterprise solution named 'Ansible Automation Platform' can be pretty expensive. 21 | 22 | 23 | * `Semaphore UI `_ 24 | 25 | Semaphore is a pretty lightweight WebUI for Ansible. 26 | 27 | It is a single binary and built from Golang (backend) and Node.js/Vue.js (frontend). 28 | 29 | Ansible job execution is done using `custom implementation `_. 30 | 31 | The project is `managed by a single maintainer and has some issues `_. It seems to develop in the direction of large-scale containerized deployments. 32 | 33 | The 'Ansible-WebUI' project was inspired by Semaphore. 34 | 35 | 36 | * **This project** 37 | 38 | It is built to be lightweight. 39 | 40 | As Ansible already requires Python3 - I chose it as primary language. 41 | 42 | The backend stack is built of `gunicorn `_ and the frontend consists of Django templates and vanilla JS/jQuery. 43 | 44 | Ansible job execution is done using the official `ansible-runner `_ library! 45 | 46 | Target users are small to medium businesses and Ansible users which just want a UI to run their playbooks. 47 | -------------------------------------------------------------------------------- /docs/source/usage/2_install.rst: -------------------------------------------------------------------------------- 1 | .. _usage_install: 2 | 3 | .. include:: ../_include/head.rst 4 | 5 | ================ 6 | 2 - Installation 7 | ================ 8 | 9 | Ansible 10 | ******* 11 | 12 | See `the documentation `_ on how to install Ansible. 13 | 14 | **Make sure to read** the `Ansible best-practices `_ on how to use Ansible! 15 | 16 | ---- 17 | 18 | Demo 19 | **** 20 | 21 | Check out the demo at: `demo.webui.ansibleguy.net `_ 22 | 23 | Login: User :code:`demo`, Password :code:`Ansible1337` 24 | 25 | ---- 26 | 27 | Install 28 | ******* 29 | 30 | Requires Python >=3.10 31 | 32 | .. code-block:: bash 33 | 34 | python3 -m pip install ansibleguy-webui 35 | 36 | **Using docker**: 37 | 38 | .. code-block:: bash 39 | 40 | docker image pull ansible0guy/webui:latest 41 | 42 | 43 | Start 44 | ***** 45 | 46 | **TLDR**: 47 | 48 | .. code-block:: bash 49 | 50 | cd $PLAYBOOK_DIR 51 | python3 -m ansibleguy-webui 52 | 53 | 54 | 55 | **Using docker**: 56 | 57 | .. code-block:: bash 58 | 59 | docker run -d --name ansible-webui --publish 127.0.0.1:8000:8000 ansible0guy/webui:latest 60 | 61 | 62 | **Details**: 63 | 64 | See: :ref:`Usage - Run ` 65 | 66 | 67 | Now you can open the Ansible-WebUI in your browser: `http://localhost:8000 `_ 68 | 69 | ---- 70 | 71 | Proxy 72 | ***** 73 | 74 | You can find a nginx config example here: `Nginx config example `_ 75 | 76 | ---- 77 | 78 | Ansible Role 79 | ************ 80 | 81 | You can find an Ansible Role to install the app on Debian here: `ansibleguy.sw_ansible_webui `_ 82 | 83 | ---- 84 | 85 | Service 86 | ******* 87 | 88 | You might want to create a service-user: 89 | 90 | .. code-block:: bash 91 | 92 | sudo useradd ansible-webui --shell /usr/sbin/nologin --create-home --home-dir /home/ansible-webui 93 | 94 | 95 | You can find a service config example here: `Systemd config example `_ 96 | 97 | Enabling & starting the service: 98 | 99 | .. code-block:: bash 100 | 101 | systemctl enable ansible-webui.service 102 | systemctl start ansible-webui.service 103 | 104 | For production usage you should use a proxy like nginx in from of the Ansible-WebUI webservice! 105 | -------------------------------------------------------------------------------- /docs/source/usage/3_run.rst: -------------------------------------------------------------------------------- 1 | .. _usage_run: 2 | 3 | .. include:: ../_include/head.rst 4 | 5 | ======= 6 | 3 - Run 7 | ======= 8 | 9 | Getting Started 10 | *************** 11 | 12 | You may want to: 13 | 14 | * Set the :code:`AW_SECRET` environmental variable with a length of at least 30 characters! 15 | * Provide a Playbook base-directory - either: 16 | 17 | * Change into the target directory before executing :code:`python3 -m ansibleguy-webui` 18 | * Create :ref:`a Repository ` 19 | * Set the :code:`AW_PATH_PLAY` to your Playbook base-directory (env-var or via WebUI) 20 | 21 | 22 | See: :ref:`Usage - Config ` for more details 23 | 24 | ---- 25 | 26 | Run Locally (PIP) 27 | ***************** 28 | 29 | .. code-block:: bash 30 | 31 | # foreground 32 | python3 -m ansibleguy-webui 33 | 34 | # or background 35 | python3 -m ansibleguy-webui > /tmp/aw.log 2> /tmp/aw.err.log & 36 | 37 | # at the first startup you will see the auto-generated credentials: 38 | 39 | AnsibleGuy-WebUI Version 0.0.12 40 | [2024-02-25 20:59:47 +0100] [5302] [INFO] Using DB: 41 | [2024-02-25 20:59:47 +0100] [5302] [WARN] Initializing database .. 42 | [2024-02-25 20:59:50 +0100] [5302] [WARN] No admin was found in the database! 43 | [2024-02-25 20:59:50 +0100] [5302] [WARN] Generated user: 'ansible' 44 | [2024-02-25 20:59:50 +0100] [5302] [WARN] Generated pwd: '' 45 | [2024-02-25 20:59:50 +0100] [5302] [WARN] Make sure to change the password! 46 | [2024-02-25 20:59:50 +0100] [5302] [INFO] Listening on http://127.0.0.1:8000 47 | [2024-02-25 20:59:50 +0100] [5302] [WARN] Starting.. 48 | [2024-02-25 20:59:50 +0100] [5302] [INFO] Starting job-threads 49 | 50 | ---- 51 | 52 | Run Dockerized 53 | ************** 54 | 55 | .. code-block:: bash 56 | 57 | docker run -d --name ansible-webui --publish 127.0.0.1:8000:8000 ansible0guy/webui:latest 58 | 59 | # or with persistent data (volumes: /data = storage for logs & DB, /play = ansible playbook base-directory) 60 | docker run -d --name ansible-webui --publish 127.0.0.1:8000:8000 --volume $(pwd)/ansible/data:/data --volume $(pwd)/ansible/play:/play ansible0guy/webui:latest 61 | 62 | # find initial password 63 | docker logs ansible-webui 64 | -------------------------------------------------------------------------------- /docs/source/usage/api.rst: -------------------------------------------------------------------------------- 1 | .. _usage_api: 2 | 3 | .. include:: ../_include/head.rst 4 | 5 | .. |api_docs| image:: ../_static/img/api_docs.png 6 | :class: wiki-img 7 | 8 | === 9 | API 10 | === 11 | 12 | This project has a API first development approach! 13 | 14 | To use the API you have to create an API key. You can use the UI at :code:`Settings - API Keys` to do so. 15 | 16 | You can also create API keys using the CLI: :code:`python3 -m ansibleguy-webui.cli -a api-key.create -p ` 17 | 18 | Examples 19 | ******** 20 | 21 | Requests must have the API key set in the :code:`X-Api-Key` header. 22 | 23 | .. code-block:: bash 24 | 25 | # list own api keys 26 | curl -X 'GET' 'http://localhost:8000/api/key' -H 'accept: application/json' -H "X-Api-Key: " 27 | > {"tokens":["ansible-2024-01-20-16-50-51","ansible-2024-01-20-16-10-42"]} 28 | 29 | # list jobs 30 | curl -X 'GET' 'http://localhost:8000/api/job' -H 'accept: application/json' -H "X-Api-Key: " 31 | > [{"id":34,"name":"Deploy App","inventory":"inventories/dev/hosts.yml","playbook":"app.yml","schedule":"22 14 * * 4,5","limit":"dev1,dev3","verbosity":0,"comment":"Deploy my app to the first two development servers","environment_vars":"MY_APP_ENV=DEV,TZ=UTC"}] 32 | 33 | # execute job 34 | curl -X 'POST' 'http://localhost:8000/api/job/34' -H 'accept: application/json' -H "X-Api-Key: " 35 | > {"msg":"Job 'Deploy App' execution queued"} 36 | 37 | API Docs 38 | ******** 39 | 40 | You can see the available API-endpoints in the built-in API-docs at :code:`System - API Docs` (*swagger*) 41 | 42 | |api_docs| 43 | -------------------------------------------------------------------------------- /docs/source/usage/backup.rst: -------------------------------------------------------------------------------- 1 | .. _usage_backup: 2 | 3 | .. include:: ../_include/head.rst 4 | 5 | 6 | ====== 7 | Backup 8 | ====== 9 | 10 | The only data to back-up is: 11 | 12 | * Your encryption key 13 | 14 | * The database - placed at :code:`${HOME}/.config/ansible-webui/aw.db` or as configured 15 | 16 | * The logs - placed at :code:`${HOME}/.local/share/ansible-webui/` or as configured 17 | -------------------------------------------------------------------------------- /docs/source/usage/credentials.rst: -------------------------------------------------------------------------------- 1 | .. _usage_credentials: 2 | 3 | .. include:: ../_include/head.rst 4 | 5 | .. |creds_ui| image:: ../_static/img/credentials_ui.png 6 | :class: wiki-img 7 | 8 | .. |creds_job| image:: ../_static/img/credentials_job.png 9 | :class: wiki-img 10 | 11 | .. |creds_perm| image:: ../_static/img/credentials_permission.png 12 | :class: wiki-img 13 | 14 | =========== 15 | Credentials 16 | =========== 17 | 18 | You can define :code:`global` and :code:`user` credentials. 19 | 20 | The saved credential secrets are never returned to the user/Web-UI! They are saved encrypted to the database! 21 | 22 | The UI at :code:`Jobs - Credentials` allows you to manage them. 23 | 24 | |creds_ui| 25 | 26 | Global Credentials 27 | ****************** 28 | 29 | Global credentials can be used for scheduled job executions. 30 | 31 | Users that are members of the :code:`AW Credentials Managers` group are able to create and manage global credentials. 32 | 33 | Access to global credentials can be controlled using :ref:`permissions `. 34 | 35 | |creds_perm| 36 | 37 | ---- 38 | 39 | User Credentials 40 | **************** 41 | 42 | User credential can only be used and accessed by the user that created them. 43 | 44 | Jobs that are executed by an user will use: (*if the job is set to need credentials*) 45 | 46 | * the user-credentials matching the jobs :code:`credential category` 47 | 48 | * or the first user-credentials found as a fallback in case no other credentials were provided/configured 49 | 50 | ---- 51 | 52 | Jobs 53 | **** 54 | 55 | You can define if a job needs credentials to run in its settings: 56 | 57 | |creds_job| 58 | -------------------------------------------------------------------------------- /docs/source/usage/development.rst: -------------------------------------------------------------------------------- 1 | .. _usage_development: 2 | 3 | .. include:: ../_include/head.rst 4 | 5 | =========== 6 | Development 7 | =========== 8 | 9 | Feel free to contribute to this project using `pull-requests `_, `issues `_ and `discussions `_! 10 | 11 | Testers are also very welcome! Please `give feedback `_ 12 | 13 | For further details - see: `Contribute `_ 14 | 15 | Read into the :ref:`Troubleshooting Guide ` to get some insight on how the stack works. 16 | 17 | 18 | ---- 19 | 20 | Install Unstable Version 21 | ************************ 22 | 23 | **WARNING**: If you run non-release versions you will have to save your :code:`src/ansibleguy-webui/aw/migrations/*` else your database upgrades might fail. Can be ignored if you do not care about losing the Ansible-WebUI config. 24 | 25 | .. code-block:: bash 26 | 27 | # download 28 | git clone https://github.com/ansibleguy/webui 29 | 30 | # install dependencies (venv recommended) 31 | cd webui 32 | python3 -m pip install --upgrade requirements.txt 33 | bash scripts/update_version.sh 34 | 35 | # run 36 | python3 src/ansibleguy-webui/ 37 | 38 | 39 | **Using docker**: 40 | 41 | .. code-block:: bash 42 | 43 | docker image pull ansible0guy/webui:unstable 44 | docker run -it --name ansible-webui-dev --publish 127.0.0.1:8000:8000 --volume /tmp/awdata:/data ansible0guy/webui:unstable 45 | # to safe db-migrations use: 46 | # --volume /var/local/ansible-webui/migrations/:/usr/local/lib/python3.10/site-packages/ansible-webui/aw/migrations 47 | -------------------------------------------------------------------------------- /docs/source/usage/docker.rst: -------------------------------------------------------------------------------- 1 | .. _usage_docker: 2 | 3 | .. include:: ../_include/head.rst 4 | 5 | ====== 6 | Docker 7 | ====== 8 | 9 | You can find the dockerfiles and scripts used to build the images `in the Repository `_ 10 | 11 | Ansible Requirements 12 | ******************** 13 | 14 | Our `docker image ansible0guy/webui `_ enables you to install Ansible dependencies on container startup. 15 | 16 | Files inside the container: 17 | 18 | * Python3 Modules: :code:`/play/requirements.txt` 19 | * `Ansible Roles & Collections `_: :code:`/play/requirements.yml` 20 | 21 | * Only Ansible Roles: :code:`/play/requirements_roles.yml` or :code:`/play/roles/requirements.yml` 22 | * Only Ansible Collections: :code:`/play/requirements_collections.yml` or :code:`/play/collections/requirements.yml` 23 | 24 | ---- 25 | 26 | Unprivileged 27 | ************ 28 | 29 | There are images for running Ansible-WebUI as unprivileged user :code:`aw` with UID/GID :code:`8785` inside the container: 30 | 31 | * Latest: :code:`ansible0guy/webui-unprivileged:latest` 32 | 33 | * Unstable: :code:`ansible0guy/webui-unprivileged:unstable` 34 | 35 | ---- 36 | 37 | Persistent Data 38 | *************** 39 | 40 | It might make sense for you to mount these paths in the container: 41 | 42 | * :code:`/data` (:code:`AW_DB` & :code:`AW_PATH_LOG` env-vars) - for database & execution-logs 43 | * :code:`/play` (:code:`AW_PATH_PLAY` env-var) - for static Ansible playbook base-directory 44 | 45 | If you are running an :code:`unprivileged` image - you will have to allow the service-user to write to the directories. The UID needs to match! 46 | 47 | Basic example: 48 | 49 | .. code-block:: bash 50 | 51 | # add matching service-user on the host system 52 | sudo useradd ansible-webui --shell /usr/sbin/nologin --uid 8785 --user-group 53 | chown ansible-webui:ansible-webui ${YOUR_DATA_DIR} 54 | 55 | ---- 56 | 57 | AWS CLI Support 58 | *************** 59 | 60 | There is also an image that has `AWS-CLI support `_ pre-enabled: :code:`ansible0guy/webui-aws:latest` (needed for :code:`community.aws.*` modules) 61 | 62 | Its base-image is :code:`ansible0guy/webui-unprivileged:latest` 63 | 64 | ---- 65 | 66 | Custom build 67 | ************ 68 | 69 | If you want to build a custom docker image - make sure to set those environmental variables: 70 | 71 | :code:`AW_VERSION=X.X.X AW_DOCKER=1 PYTHONUNBUFFERED=1` 72 | -------------------------------------------------------------------------------- /docs/source/usage/integrations.rst: -------------------------------------------------------------------------------- 1 | .. _usage_integrations: 2 | 3 | .. include:: ../_include/head.rst 4 | 5 | ============ 6 | Integrations 7 | ============ 8 | 9 | 10 | ARA Records Ansible 11 | ******************* 12 | 13 | ARA can be used to gather detailed statistics of Ansible executions. 14 | 15 | To enable AW to send data to an ARA server - you need to: 16 | 17 | * Install the :code:`ara` Python3 module on your controller system 18 | * Configure the server at :code:`System - Config` 19 | 20 | Quote: ara provides Ansible reporting by recording ansible and ansible-playbook commands regardless of how and where they run. 21 | 22 | `Documentation `_ | `Repository `_ 23 | 24 | ---- 25 | 26 | Identity Providers using SAML SSO 27 | ********************************* 28 | 29 | Easily integrate with SAML2 SSO identity providers like Okta, Azure AD and others. 30 | 31 | For configuration - see: :ref:`Usage - Authentication ` 32 | -------------------------------------------------------------------------------- /docs/source/usage/jobs.rst: -------------------------------------------------------------------------------- 1 | .. _usage_jobs: 2 | 3 | .. include:: ../_include/head.rst 4 | 5 | .. |job_exec| image:: ../_static/img/job_execution.png 6 | :class: wiki-img 7 | 8 | .. |job_prompts1| image:: ../_static/img/job_prompts_1.png 9 | :class: wiki-img 10 | 11 | .. |job_prompts2| image:: ../_static/img/job_prompts_2.png 12 | :class: wiki-img 13 | 14 | 15 | ==== 16 | Jobs 17 | ==== 18 | 19 | You can use the UI at :code:`Jobs - Manage` to create and execute jobs. 20 | 21 | ---- 22 | 23 | Create 24 | ****** 25 | 26 | To get an overview - Check out the demo at: `demo.webui.ansibleguy.net `_ | Login: User :code:`demo`, Password :code:`Ansible1337` 27 | 28 | The job creation form will help you by browsing for playbooks and inventories. For this to work correctly - you should first select the repository to use (*if any is in use*). 29 | 30 | You can optionally define a :code:`schedule` in `Cron-format `_ to automatically execute the job. Schedule jobs depend on :ref:`Global Credentials ` (*if any are needed*). 31 | 32 | :code:`Credential categories` can be defined if you want to use user-specific credentials to manage your systems. The credentials of the executing user will be dynamically matched if the job is set to :code:`Needs credentials`. 33 | 34 | For transparency - the full command that is executed is added on the logs-view. 35 | 36 | ---- 37 | 38 | Execute 39 | ******* 40 | 41 | You have two options to execute a job: 42 | 43 | * **Quick execution** - run job as configured without overrides 44 | 45 | * **Custom execution** - run job with execution-specific overrides 46 | 47 | The fields available as overrides can be configured in the job settings! 48 | 49 | |job_prompts1| 50 | 51 | |job_prompts2| 52 | 53 | These will be shown in the job overview: 54 | 55 | |job_exec| 56 | -------------------------------------------------------------------------------- /docs/source/usage/privileges.rst: -------------------------------------------------------------------------------- 1 | .. _usage_permission: 2 | 3 | .. include:: ../_include/head.rst 4 | 5 | .. |perm_users_groups| image:: ../_static/img/permission_users_groups.png 6 | :class: wiki-img 7 | 8 | .. |perm_ui| image:: ../_static/img/permission_ui.png 9 | :class: wiki-img 10 | 11 | .. |perm_overview| image:: ../_static/img/permission_overview.svg 12 | :class: wiki-img 13 | 14 | ========== 15 | Privileges 16 | ========== 17 | 18 | You can set permissions to limit user actions. 19 | 20 | Users & Groups 21 | ************** 22 | 23 | The :code:`System - Admin - Users/Groups` admin-page allows you to create new users and manage group memberships. 24 | 25 | Users can change their own password at :code:`System - Password` 26 | 27 | The :code:`Superuser` flag can be used to grant all privileges to a user. 28 | 29 | |perm_users_groups| 30 | 31 | ---- 32 | 33 | Managers 34 | ******** 35 | 36 | To allow a users to perform management actions - add them to the corresponding system-group. 37 | 38 | Available ones are: 39 | 40 | * :code:`AW Job Managers` - create new jobs, view and update all existing ones 41 | 42 | * :code:`AW Permission Managers` - create, update and delete permissions 43 | 44 | * :code:`AW Repository Managers` - create new repositories, view and update all existing ones 45 | 46 | * :code:`AW Credentials Managers` - create new global credentials, view and update all existing ones 47 | 48 | * :code:`AW System Managers` - configure system settings 49 | 50 | ---- 51 | 52 | Permissions 53 | *********** 54 | 55 | The UI at :code:`Settings - Permissions` allows you to create job, credential & repository permissions and link them to users and groups. 56 | 57 | |perm_ui| 58 | 59 | Each job, credential & repository can have multiple permissions linked to it. 60 | 61 | **Permission types:** 62 | 63 | * **Read** - only allow user to read job and job-logs 64 | * **Execute** - allow user to start & stop the job + 'Read' 65 | * **Write** - allow user to modify the job + 'Execute' 66 | * **Full** - allow user to delete the job + 'Write' 67 | 68 | |perm_overview| 69 | -------------------------------------------------------------------------------- /docs/source/usage/repositories.rst: -------------------------------------------------------------------------------- 1 | .. _usage_repositories: 2 | 3 | .. include:: ../_include/head.rst 4 | 5 | .. |repo_ui| image:: ../_static/img/repo_ui.png 6 | :class: wiki-img 7 | 8 | 9 | ============ 10 | Repositories 11 | ============ 12 | 13 | By default the static Repository set by :code:`AW_PATH_PLAY` is used. 14 | 15 | You are able to create multiple Repositories that act as Ansible-Playbook base-directories. 16 | 17 | |repo_ui| 18 | 19 | ---- 20 | 21 | Static 22 | ****** 23 | 24 | Absolute path to an existing local static directory that contains your `playbook directory structure `_. 25 | 26 | ---- 27 | 28 | Git 29 | *** 30 | 31 | Git repositories are also supported. 32 | 33 | They can either be updated at execution or completely re-created (*isolated*). 34 | 35 | The timeout for any single git-command is 5min. 36 | 37 | ---- 38 | 39 | Override commands 40 | ================= 41 | 42 | If you have some special environment or want to tweak the way your repository is cloned - you can override the default git-commands! 43 | 44 | Default commands: 45 | 46 | **Create** 47 | 48 | .. code-block:: bash 49 | 50 | git clone --branch ${BRANCH} (--depth ${DEPTH}) ${ORIGIN} 51 | # if LFS is enabled 52 | git lfs fetch 53 | git lfs checkout 54 | 55 | **Update** 56 | 57 | .. code-block:: bash 58 | 59 | git reset --hard 60 | git pull (--depth ${DEPTH}) 61 | # if LFS is enabled 62 | git lfs fetch 63 | git lfs checkout 64 | 65 | ---- 66 | 67 | Hook commands 68 | ============= 69 | 70 | You are able to run some hook-commands before and after updating the repository. 71 | 72 | If you want to run multiple ones - they need to be comma-separated. 73 | 74 | These hooks will not be processed if you override the actual create/update command. 75 | 76 | The cleanup-hook can be used to commit files that were created by the job-execution. 77 | 78 | **Note**: For security reasons (XSS) these characters are currently not allowed: :code:`< >` 79 | 80 | ---- 81 | 82 | Clone via SSH 83 | ============= 84 | 85 | You can specify which :code:`known_hosts` file AW should use using the :ref:`System config `! 86 | 87 | You are able to append the port to the origin string like so: :code:`git@git.intern -p1337` 88 | 89 | The SSH-key configured in the linked credentials will be used. 90 | 91 | ---- 92 | 93 | Example GitHub Private-Repository 94 | ================================= 95 | 96 | 1. Create global Credentials that use your Access-Token as :code:`connect password` 97 | 2. Create the Git-Repository and link the Credentials 98 | -------------------------------------------------------------------------------- /docs/venv.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | cd "$(dirname "$0")" 6 | 7 | VENV_PATH='/tmp/.ag-docs-venv' 8 | 9 | python3 -m virtualenv "$VENV_PATH" 10 | source "${VENV_PATH}/bin/activate" 11 | 12 | pip install -r requirements.txt >/dev/null -------------------------------------------------------------------------------- /examples/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | listen [::]:80; 4 | server_name ${YOUR-HOSTNAMES}; 5 | 6 | if ($request_method !~ ^(GET|POST|PUT|DELETE)$ ) { 7 | return 405; 8 | } 9 | 10 | # if using letsencrypt certbot 11 | location /.well-known/acme-challenge/ { 12 | allow all; 13 | default_type "text/plain"; 14 | try_files $uri =404; 15 | } 16 | 17 | location / { 18 | return 301 https://$host$request_uri; 19 | } 20 | } 21 | 22 | server { 23 | listen 443 ssl http2; 24 | listen [::]:443 ssl http2; 25 | server_name ${YOUR-HOSTNAMES}; 26 | 27 | ssl_certificate_key '${PATH-TO-YOUR-UNENCRYPTED-KEY}'; 28 | ssl_certificate '${PATH-TO-YOUR-CHAIN-CERT}'; # should use the certificate chain => top is server cert; bottom root cert 29 | ssl_stapling on; 30 | ssl_stapling_verify on; 31 | 32 | if ($request_method !~ ^(GET|POST|PUT|DELETE)$ ) { 33 | return 405; 34 | } 35 | 36 | autoindex off; 37 | server_tokens off; 38 | proxy_pass_request_headers on; 39 | proxy_connect_timeout 150; 40 | proxy_send_timeout 100; 41 | proxy_read_timeout 100; 42 | proxy_buffers 4 32k; 43 | client_max_body_size 50m; 44 | client_body_buffer_size 128k; 45 | client_header_buffer_size 2k; 46 | client_header_timeout 5s; 47 | large_client_header_buffers 3 1k; 48 | ssl_session_cache shared:SSL:10m; 49 | ssl_session_timeout 10m; 50 | client_body_timeout 5s; 51 | ssl_protocols TLSv1.2 TLSv1.3; 52 | ssl_prefer_server_ciphers on; 53 | ssl_ciphers EECDH+ECDSA+AESGCM:EECDH+aRSA+AESGCM:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!SRP:!DSS:!RC4; 54 | 55 | add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; 56 | add_header Referrer-Policy same-origin; 57 | add_header X-Frame-Options SAMEORIGIN; 58 | add_header X-Content-Type-Options nosniff; 59 | add_header X-XSS-Protection "1; mode=block"; 60 | add_header Set-Cookie "Path=/;HttpOnly;Secure;SameSite=none"; 61 | 62 | location ~ ^/static/admin/ { 63 | root /django/contrib/admin/; 64 | try_files $uri =404; 65 | } 66 | location ~ ^/static/fontawesomefree/ { 67 | root /site-packages/fontawesomefree/; 68 | try_files $uri =404; 69 | } 70 | location ~ ^/static/ { 71 | root /ansibleguy-webui/aw/; // p.e. /home/ansible-webui/venv/lib/python3.11/site-packages/ansibleguy-webui/aw/ 72 | try_files $uri =404; 73 | } 74 | 75 | location / { 76 | proxy_pass http://127.0.0.1:8000; 77 | proxy_set_header Host $http_host; 78 | proxy_set_header X-Forwarded-Host $host; 79 | proxy_set_header X-Forwarded-Server $host; 80 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 81 | proxy_set_header X-Forwarded-Proto $scheme; 82 | proxy_set_header X-Real-IP $remote_addr; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /examples/saml_google_workspace.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | # ENVIRONMENTAL VARIABLES 4 | # AW_CONFIG=/etc/ansible-webui/config.yml (this config file) 5 | 6 | # GOOGLE ADMIN SETTINGS (https://admin.google.com/ac/apps/saml/) 7 | # Provider details: 8 | # ACS URL: https:///a/saml/acs/ 9 | # Entity ID: https:///a/saml/acs/ 10 | # Signed response: yes 11 | # Name ID format: UNSPECIFIED 12 | # Name ID: Primary email 13 | # Attribute mapping: 14 | # Primary email => email 15 | # First name => first_name 16 | # Last name => last_name 17 | # Groups => groups 18 | 19 | AUTH: 'saml' 20 | SAML: 21 | # replace with your domain 22 | ASSERTION_URL: 'https://' 23 | ENTITY_ID: 'https:///a/saml/acs/' 24 | DEFAULT_NEXT_URL: 'https://' 25 | 26 | METADATA_LOCAL_FILE_PATH: '/etc/ansible-webui/GoogleIDPMetadata.xml' 27 | CERT_FILE: '/etc/ansible-webui/Google__SAML2_0.pem' 28 | WANT_ASSERTIONS_SIGNED: false 29 | TOKEN_REQUIRED: false 30 | 31 | CREATE_USER: true 32 | NEW_USER_PROFILE: 33 | USER_GROUPS: [] 34 | ACTIVE_STATUS: true 35 | STAFF_STATUS: true 36 | SUPERUSER_STATUS: false 37 | 38 | ATTRIBUTES_MAP: 39 | email: 'email' 40 | username: 'email' 41 | first_name: 'first_name' 42 | last_name: 'last_name' 43 | groups: 'groups' 44 | 45 | GROUPS_MAP: # map IDP groups to django groups 46 | 'IDP GROUP': 'AW Job Managers' 47 | -------------------------------------------------------------------------------- /examples/systemd_service.conf: -------------------------------------------------------------------------------- 1 | # /etc/systemd/system/ansible-webui.service 2 | 3 | # when using with a virtual-environment 4 | [Unit] 5 | Description=AnsibleGuy WebUI Service 6 | Documentation=https://webui.ansibleguy.net/ 7 | Documentation=https://github.com/ansibleguy/webui 8 | 9 | [Service] 10 | Type=simple 11 | EnvironmentFile=/etc/ansible-webui/env.txt 12 | Environment=LANG="C.UTF-8" 13 | Environment=LC_ALL="C.UTF-8" 14 | Environment=PYTHONUNBUFFERED="1" 15 | 16 | ExecStart=/bin/bash -c 'source /home/ansible-webui/venv/bin/activate \ 17 | && python3 -m ansibleguy-webui' 18 | 19 | User=ansible-webui 20 | Group=ansible-webui 21 | Restart=on-failure 22 | RestartSec=5s 23 | 24 | StandardOutput=journal 25 | StandardError=journal 26 | SyslogIdentifier=ansible-webui 27 | 28 | [Install] 29 | WantedBy=multi-user.target 30 | 31 | # without a venv 32 | [Unit] 33 | Description=AnsibleGuy WebUI Service 34 | Documentation=https://webui.ansibleguy.net/ 35 | Documentation=https://github.com/ansibleguy/webui 36 | 37 | [Service] 38 | Type=simple 39 | EnvironmentFile=/etc/ansible-webui/env.txt 40 | Environment=LANG="en_US.UTF-8" 41 | Environment=LC_ALL="en_US.UTF-8" 42 | Environment=PYTHONUNBUFFERED="1" 43 | 44 | ExecStart=/usr/bin/python3 -m ansibleguy-webui 45 | ExecReload=/usr/bin/kill -s HUP $MAINPID 46 | 47 | User=ansible-webui 48 | Group=ansible-webui 49 | Restart=on-failure 50 | RestartSec=5s 51 | 52 | StandardOutput=journal 53 | StandardError=journal 54 | SyslogIdentifier=ansible-webui 55 | 56 | [Install] 57 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools >= 61.0"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "ansibleguy-webui" 7 | authors = [ 8 | {name = "AnsibleGuy", email = "contact@ansibleguy.net"}, 9 | ] 10 | description = "Basic WebUI for using Ansible" 11 | readme = "README.md" 12 | requires-python = ">=3.10" # django 5.0 13 | keywords = ["ansible", "webui", "automation", "iac"] 14 | license = {file = "LICENSE.txt"} 15 | classifiers = [ 16 | 'Programming Language :: Python :: 3', 17 | 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', 18 | 'Framework :: Django', 19 | ] 20 | dynamic = ["dependencies", "version"] 21 | 22 | [project.urls] 23 | Homepage = "https://github.com/ansibleguy/webui" 24 | Documentation = "https://webui.ansibleguy.net/" 25 | Repository = "https://github.com/ansibleguy/webui.git" 26 | Issues = "https://github.com/ansibleguy/issues" 27 | 28 | [tool.setuptools.dynamic] 29 | dependencies = {file = ["requirements.txt"]} 30 | version = {file = ["VERSION"]} 31 | 32 | [tool.setuptools.packages.find] 33 | where = ["src"] 34 | 35 | [tool.setuptools.package-data] 36 | "*" = ["*.html", "*.js", "*.css", "*.svg", "*.txt"] 37 | 38 | # [project.scripts] 39 | # ansibleguy-webui = "ansibleguy-webui.__main__" 40 | # ansibleguy-webui-db = "ansibleguy-webui.manage" 41 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # webui 2 | gunicorn 3 | Django==5.0.* 4 | pytz 5 | pyyaml 6 | django-auto-logout 7 | pycryptodome 8 | 9 | ## auth 10 | grafana-django-saml2-auth 11 | 12 | ## api 13 | djangorestframework==3.* 14 | djangorestframework-api-key==2.* 15 | drf-spectacular 16 | 17 | ## styles 18 | fontawesomefree 19 | 20 | # scheduling 21 | crontab 22 | 23 | # ansible 24 | ansible-core 25 | # ansible-runner 26 | ansibleguy-runner==2.4.0.post5 27 | 28 | # config 29 | PyYAML 30 | 31 | # email 32 | premailer 33 | -------------------------------------------------------------------------------- /requirements_build.txt: -------------------------------------------------------------------------------- 1 | build 2 | virtualenv 3 | -------------------------------------------------------------------------------- /requirements_lint.txt: -------------------------------------------------------------------------------- 1 | pylint 2 | pylint-django 3 | yamllint 4 | -------------------------------------------------------------------------------- /requirements_test.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | selenium 3 | blinker<1.8.0 4 | selenium-wire 5 | chromedriver-autoinstaller 6 | requests 7 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | cd "$(dirname "$0")/.." 6 | rm -rf dist/* 7 | 8 | # bash scripts/update_version.sh 9 | python3 -m pip install -r ./requirements_build.txt >/dev/null 10 | python3 -m build 11 | # python3 -m twine upload --repository pypi dist/* 12 | -------------------------------------------------------------------------------- /scripts/docker_build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | if [ -z "$1" ] 6 | then 7 | echo 'YOU NEED TO SUPPLY A VERSION!' 8 | exit 1 9 | fi 10 | 11 | set -u 12 | 13 | VERSION="$1" 14 | 15 | cd "$(dirname "$0")/../docker" 16 | 17 | IMAGE_REPO="ansible0guy/webui" 18 | IMAGE_REPO_UNPRIV="${IMAGE_REPO}-unprivileged" 19 | IMAGE_REPO_AWS="${IMAGE_REPO}-aws" 20 | 21 | # todo: allow for multi-platform builds 22 | # RELEASE_ARCHS="linux/arm/v7,linux/arm64/v8,linux/amd64" 23 | 24 | image="${IMAGE_REPO}:${VERSION}" 25 | image_latest="${IMAGE_REPO}:latest" 26 | 27 | image_unpriv="${IMAGE_REPO_UNPRIV}:${VERSION}" 28 | image_unpriv_latest="${IMAGE_REPO_UNPRIV}:latest" 29 | 30 | image_aws="${IMAGE_REPO_AWS}:${VERSION}" 31 | image_aws_latest="${IMAGE_REPO_AWS}:latest" 32 | 33 | container="ansible-webui-${VERSION}" 34 | 35 | read -r -p "Build version ${VERSION} as latest? [y/N] " -n 1 36 | 37 | function cleanup_container() { 38 | if docker ps -a | grep -q "$container" 39 | then 40 | docker stop "$container" 41 | docker rm "$container" 42 | fi 43 | } 44 | 45 | echo '' 46 | echo "### CLEANUP ###" 47 | cleanup_container 48 | 49 | if docker image ls | grep "$IMAGE_REPO" | grep -q "$VERSION" 50 | then 51 | docker image rm "$image" || true 52 | docker image rm "$image_unpriv" || true 53 | docker image rm "$image_aws" || true 54 | fi 55 | 56 | if [[ "$REPLY" =~ ^[Yy]$ ]] 57 | then 58 | if docker image ls | grep "$IMAGE_REPO" | grep -q 'latest' 59 | then 60 | docker image rm "$image_latest" || true 61 | docker image rm "$image_unpriv_latest" || true 62 | docker image rm "$image_aws_latest" || true 63 | fi 64 | fi 65 | 66 | echo '' 67 | echo "### BUILDING IMAGE ${image} ###" 68 | docker build -f Dockerfile_production -t "$image" --network host --build-arg "AW_VERSION=${VERSION}" --no-cache . 69 | 70 | if [[ "$REPLY" =~ ^[Yy]$ ]] 71 | then 72 | docker build -f Dockerfile_production -t "$image_latest" --network host --build-arg "AW_VERSION=${VERSION}" . 73 | fi 74 | 75 | echo '' 76 | echo "### BUILDING IMAGE ${image_unpriv} ###" 77 | docker build -f Dockerfile_production_unprivileged -t "$image_unpriv" --network host --build-arg "AW_VERSION=${VERSION}" --no-cache . 78 | 79 | if [[ "$REPLY" =~ ^[Yy]$ ]] 80 | then 81 | docker build -f Dockerfile_production_unprivileged -t "$image_unpriv_latest" --network host --build-arg "AW_VERSION=${VERSION}" . 82 | fi 83 | 84 | echo '' 85 | echo "### BUILDING IMAGE ${image_aws} ###" 86 | docker build -f Dockerfile_production_aws -t "$image_aws" --network host --build-arg "AW_VERSION=${VERSION}" --no-cache --progress=plain . 87 | 88 | if [[ "$REPLY" =~ ^[Yy]$ ]] 89 | then 90 | docker build -f Dockerfile_production_aws -t "$image_aws_latest" --network host --build-arg "AW_VERSION=${VERSION}" . 91 | fi 92 | -------------------------------------------------------------------------------- /scripts/docker_release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | if [ -z "$1" ] 6 | then 7 | echo 'YOU NEED TO SUPPLY A VERSION!' 8 | exit 1 9 | fi 10 | 11 | set -u 12 | 13 | VERSION="$1" 14 | IMAGE_REPO="ansible0guy/webui" 15 | IMAGE_REPO_UNPRIV="${IMAGE_REPO}-unprivileged" 16 | IMAGE_REPO_AWS="${IMAGE_REPO}-aws" 17 | 18 | image="${IMAGE_REPO}:${VERSION}" 19 | image_latest="${IMAGE_REPO}:latest" 20 | 21 | image_unpriv="${IMAGE_REPO_UNPRIV}:${VERSION}" 22 | image_unpriv_latest="${IMAGE_REPO_UNPRIV}:latest" 23 | 24 | image_aws="${IMAGE_REPO_AWS}:${VERSION}" 25 | image_aws_latest="${IMAGE_REPO_AWS}:latest" 26 | 27 | if ! docker image ls | grep "$IMAGE_REPO" | grep -q "$VERSION" 28 | then 29 | echo "Image not found: ${image}" 30 | exit 1 31 | fi 32 | 33 | echo '' 34 | echo "### RELEASING IMAGES WITH TAG ${VERSION} ###" 35 | docker push "$image" 36 | docker push "$image_unpriv" 37 | docker push "$image_aws" 38 | 39 | echo '' 40 | read -r -p "Release version ${VERSION} as latest? [y/N] " -n 1 41 | if [[ "$REPLY" =~ ^[Yy]$ ]] 42 | then 43 | if ! docker image ls | grep "$IMAGE_REPO" | grep -q 'latest' 44 | then 45 | echo "Image not found: ${image_latest}" 46 | exit 1 47 | fi 48 | 49 | echo '' 50 | echo "### RELEASING IMAGES WITH TAG latest ###" 51 | docker push "$image_latest" 52 | docker push "$image_unpriv_latest" 53 | docker push "$image_aws_latest" 54 | fi 55 | -------------------------------------------------------------------------------- /scripts/kill_ps.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # in case some process got stuck because of a bug 4 | pkill -f -9 'src/ansibleguy-webui' 5 | -------------------------------------------------------------------------------- /scripts/lint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | cd "$(dirname "$0")/.." 6 | 7 | export AW_INIT=1 8 | 9 | echo '' 10 | echo 'LINTING Python' 11 | echo '' 12 | 13 | export DJANGO_SETTINGS_MODULE='aw.settings' 14 | pylint --rcfile .pylintrc --recursive=y --load-plugins pylint_django --django-settings-module=aw.settings . 15 | 16 | echo '' 17 | echo 'LINTING YAML' 18 | echo '' 19 | 20 | yamllint . 21 | 22 | -------------------------------------------------------------------------------- /scripts/migrate_db.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | CLEAN=0 6 | if [ -n "$1" ] && [[ "$1" == "clean" ]] 7 | then 8 | CLEAN=1 9 | fi 10 | 11 | cd "$(dirname "$0")/.." 12 | 13 | echo '' 14 | echo 'Removing temporary migrations' 15 | echo '' 16 | 17 | if [[ "$CLEAN" == "1" ]] 18 | then 19 | git ls-files . --exclude-standard --others | grep 'migrations' | xargs --no-run-if-empty rm 20 | fi 21 | 22 | cd "$(pwd)/src/ansibleguy-webui/" 23 | 24 | echo '' 25 | echo 'Creating migrations' 26 | echo '' 27 | 28 | python3 manage.py makemigrations 29 | python3 manage.py makemigrations aw 30 | 31 | echo '' 32 | echo 'Running migrations' 33 | echo '' 34 | 35 | python3 manage.py migrate 36 | -------------------------------------------------------------------------------- /scripts/run_dev.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | export TEST_QUIET=0 6 | if [[ -n "$1" ]] && [[ "$1" == "q" ]] 7 | then 8 | export TEST_QUIET=1 9 | fi 10 | 11 | cd "$(dirname "$0")" 12 | export AW_DEV=1 13 | export AW_ENV='dev' 14 | export AW_SECRET='asdfThisIsSuperSecret!12345678' # keep sessions on auto-reload 15 | source ./run_shared.sh 16 | -------------------------------------------------------------------------------- /scripts/run_pip_build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | cd "$(dirname "$0")/.." 6 | path_repo="$(pwd)" 7 | tmp_venv="/tmp/aw-venv/$(date +%s)" 8 | 9 | echo '' 10 | echo 'Installing requirements' 11 | echo '' 12 | 13 | python3 -m pip install -r ./requirements_build.txt >/dev/null 14 | 15 | echo '' 16 | echo "Creating virtualenv ${tmp_venv}" 17 | echo '' 18 | 19 | python3 -m virtualenv "$tmp_venv" >/dev/null 20 | source "${tmp_venv}/bin/activate" 21 | export AW_DB="${tmp_venv}/aw.dev.db" 22 | 23 | echo '' 24 | echo 'Building & Installing Module using PIP' 25 | echo '' 26 | 27 | bash ./scripts/update_version.sh 28 | 29 | python3 -m pip install -e "$path_repo" >/dev/null 30 | 31 | echo '' 32 | echo 'Starting app' 33 | echo '' 34 | 35 | cd /tmp 36 | python3 -m ansibleguy-webui 37 | 38 | echo '' 39 | echo "Removing virtualenv ${tmp_venv}" 40 | echo '' 41 | 42 | deactivate 43 | rm -rf "$tmp_venv" 44 | -------------------------------------------------------------------------------- /scripts/run_shared.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | function log() { 6 | echo '' 7 | echo "### $1 ###" 8 | echo '' 9 | } 10 | 11 | cd "$(pwd)/.." 12 | # git pull 13 | TEST_DB="$(pwd)/src/ansibleguy-webui/aw.${AW_ENV}.db" 14 | TEST_MIGRATE='' 15 | 16 | if [ -f "$TEST_DB" ] && [[ "$TEST_QUIET" != "1" ]] 17 | then 18 | # shellcheck disable=SC2162 19 | read -p "Delete existing ${AW_ENV} DB? (yes/NO) " del_dev_db 20 | 21 | if [[ "$del_dev_db" == 'y' ]] || [[ "$del_dev_db" == 'yes' ]] 22 | then 23 | echo "Removing ${AW_ENV} DB.." 24 | rm "$TEST_DB" 25 | TEST_MIGRATE='clean' 26 | fi 27 | elif ! [ -f "$TEST_DB" ] 28 | then 29 | echo "Creating DB ${TEST_DB}" 30 | fi 31 | 32 | export AW_DB="$TEST_DB" 33 | export DJANGO_SUPERUSER_USERNAME='ansible' 34 | export DJANGO_SUPERUSER_PASSWORD='automateMe' 35 | export DJANGO_SUPERUSER_EMAIL='ansible@localhost' 36 | 37 | log 'SETTING VERSION' 38 | bash ./scripts/update_version.sh 39 | version="$(cat './VERSION')" 40 | export AW_VERSION="$version" 41 | path_play="$(pwd)/test" 42 | export AW_PATH_PLAY="$path_play" 43 | 44 | if [[ "$TEST_QUIET" != "1" ]] 45 | then 46 | log 'INSTALLING REQUIREMENTS' 47 | python3 -m pip install --upgrade -r ./requirements.txt >/dev/null 48 | 49 | log 'INITIALIZING DATABASE SCHEMA' 50 | bash ./scripts/migrate_db.sh "$TEST_MIGRATE" 51 | 52 | log 'CREATING USERS' 53 | python3 ./src/ansibleguy-webui/manage.py createsuperuser --noinput || true 54 | fi 55 | 56 | log 'STARTING APP' 57 | python3 ./src/ansibleguy-webui 58 | -------------------------------------------------------------------------------- /scripts/run_staging.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | export TEST_QUIET=0 6 | if [[ -n "$1" ]] && [[ "$1" == "q" ]] 7 | then 8 | export TEST_QUIET=1 9 | fi 10 | 11 | cd "$(dirname "$0")" 12 | export AW_ENV='staging' 13 | source ./run_shared.sh 14 | -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | cd "$(dirname "$0")/.." 6 | 7 | echo '' 8 | echo 'UNIT TESTS' 9 | echo '' 10 | 11 | python3 -m pytest 12 | 13 | function failure() { 14 | echo '' 15 | echo '### FAILED ###' 16 | echo '' 17 | pkill -f ansibleguy-webui 18 | exit 1 19 | } 20 | 21 | echo '' 22 | echo 'INTEGRATION TESTS WEB-UI' 23 | echo '' 24 | 25 | if pgrep -f 'ansibleguy-webui' 26 | then 27 | echo 'An instance of Ansible-WebUI is already running! Stop it first (pkill -f ansibleguy-webui)' 28 | exit 1 29 | fi 30 | 31 | 32 | echo 'Starting AnsibleGuy-WebUI..' 33 | export AW_ENV='dev' 34 | # shellcheck disable=SC2155 35 | export AW_DB="/tmp/$(date +%s).aw.db" 36 | # shellcheck disable=SC2155 37 | export AW_PATH_PLAY="$(pwd)/test" 38 | export AW_ADMIN='tester' 39 | export AW_ADMIN_PWD='someSecret!Pwd' 40 | python3 src/ansibleguy-webui/ >/dev/null 2>/dev/null & 41 | echo '' 42 | sleep 5 43 | 44 | set +e 45 | if ! python3 test/integration/webui/main.py 46 | then 47 | failure 48 | fi 49 | 50 | sleep 1 51 | 52 | echo '' 53 | echo 'INTEGRATION TESTS API' 54 | echo '' 55 | 56 | echo 'Create API key' 57 | api_key="$(python3 src/ansibleguy-webui/cli.py -a api-key.create -p "$AW_ADMIN" | grep 'Key=' | cut -d '=' -f2)" 58 | export AW_API_KEY="$api_key" 59 | sleep 1 60 | 61 | if ! python3 test/integration/api/main.py 62 | then 63 | failure 64 | fi 65 | 66 | sleep 1 67 | pkill -f 'ansibleguy-webui' 68 | 69 | echo '' 70 | echo 'INTEGRATION TESTS SAML' 71 | echo '' 72 | 73 | sleep 5 74 | 75 | echo 'Starting AnsibleGuy-WebUI with SAML enabled..' 76 | # shellcheck disable=SC2155 77 | export AW_DB="/tmp/$(date +%s).aw.db" 78 | # shellcheck disable=SC2155 79 | export AW_CONFIG="$(pwd)/test/integration/auth/saml.yml" 80 | python3 src/ansibleguy-webui/ >/dev/null 2>/dev/null & 81 | echo '' 82 | sleep 5 83 | 84 | set +e 85 | if ! python3 test/integration/auth/saml.py 86 | then 87 | failure 88 | fi 89 | 90 | sleep 1 91 | export AW_CONFIG='' 92 | pkill -f 'ansibleguy-webui' 93 | 94 | echo '' 95 | echo 'TESTING TO CLI TOOLS' 96 | echo '' 97 | 98 | REPO_BASE="$(pwd)" 99 | cd /tmp 100 | export AW_DB="${REPO_BASE}/src/ansibleguy-webui/aw.dev.db" 101 | python3 "${REPO_BASE}/src/ansibleguy-webui/cli.py" --version 102 | python3 "${REPO_BASE}/src/ansibleguy-webui/manage.py" 103 | cd "$REPO_BASE" 104 | 105 | echo '' 106 | echo 'TESTING TO INITIALIZE AW-DB' 107 | echo '' 108 | 109 | # shellcheck disable=SC2155 110 | TMP_DIR="/tmp/aw_$(date +%s)" 111 | mkdir -p "$TMP_DIR" 112 | cp -r ./* "$TMP_DIR" 113 | cd "$TMP_DIR" 114 | rm -rf ./src/ansibleguy-webui/aw/migrations/* 115 | export AW_DB="${TMP_DIR}/aw.db" 116 | timeout 10 python3 src/ansibleguy-webui 117 | ec="$?" 118 | if [[ "$ec" != "124" ]] 119 | then 120 | exit 1 121 | fi 122 | 123 | echo '' 124 | echo '### FINISHED ###' 125 | echo '' 126 | -------------------------------------------------------------------------------- /scripts/update_version.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | cd "$(dirname "$0")/.." 6 | 7 | git pull || true 8 | last_tag="$(git describe --tags --abbrev=0)" 9 | echo "${last_tag}.dev" > "$(dirname "$0")/../VERSION" 10 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansibleguy/webui/933b57a51f90c42a7679868e3b33ccc73f5def05/src/ansibleguy-webui/__init__.py -------------------------------------------------------------------------------- /src/ansibleguy-webui/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | if __name__ == '__main__': 4 | # pylint: disable=E0401 5 | from sys import argv as sys_argv 6 | from sys import exit as sys_exit 7 | from sys import path as sys_path 8 | from os import path as os_path 9 | from os import environ 10 | 11 | try: 12 | from main import main 13 | 14 | except ModuleNotFoundError: 15 | sys_path.append(os_path.dirname(os_path.abspath(__file__))) 16 | from main import main 17 | 18 | if len(sys_argv) > 1: 19 | if sys_argv[1] in ['--version', '-v']: 20 | from cli_init import init_cli 21 | from cli import _print_version 22 | init_cli() 23 | _print_version() 24 | sys_exit(0) 25 | 26 | elif sys_argv[1] in ['--config', '-c']: 27 | from aw.config.hardcoded import ENV_KEY_CONFIG 28 | environ[ENV_KEY_CONFIG] = sys_argv[2] 29 | 30 | from aw.config.main import VERSION 31 | print(f'AnsibleGuy-WebUI Version {VERSION}') 32 | main() 33 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansibleguy/webui/933b57a51f90c42a7679868e3b33ccc73f5def05/src/ansibleguy-webui/aw/__init__.py -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.auth.admin import UserAdmin as BaseUserAdmin 3 | from rest_framework_api_key.admin import APIKey 4 | 5 | from aw.base import USERS 6 | from aw.model.api import AwAPIKey 7 | from aw.model.job import Job, JobExecution, JobExecutionResult, JobError, JobExecutionResultHost 8 | from aw.model.permission import JobPermission, JobPermissionMemberUser, JobPermissionMemberGroup, \ 9 | JobPermissionMapping 10 | from aw.model.job_credential import JobGlobalCredentials, JobUserCredentials 11 | from aw.model.repository import Repository 12 | from aw.model.system import SystemConfig, UserExtended 13 | from aw.model.alert import AlertUser, AlertGroup, AlertGlobal, AlertPlugin 14 | 15 | 16 | class UserExtendedInline(admin.StackedInline): 17 | model = UserExtended 18 | can_delete = False 19 | 20 | 21 | class UserAdmin(BaseUserAdmin): 22 | inlines = [UserExtendedInline] 23 | 24 | 25 | admin.site.unregister(APIKey) 26 | admin.site.unregister(USERS) 27 | admin.site.register(USERS, UserAdmin) 28 | 29 | admin.site.register(Job) 30 | admin.site.register(JobExecution) 31 | admin.site.register(JobPermission) 32 | admin.site.register(JobPermissionMemberUser) 33 | admin.site.register(JobPermissionMemberGroup) 34 | admin.site.register(JobPermissionMapping) 35 | admin.site.register(JobExecutionResult) 36 | admin.site.register(JobExecutionResultHost) 37 | admin.site.register(JobError) 38 | admin.site.register(JobGlobalCredentials) 39 | admin.site.register(JobUserCredentials) 40 | admin.site.register(AwAPIKey) 41 | admin.site.register(Repository) 42 | admin.site.register(SystemConfig) 43 | admin.site.register(AlertUser) 44 | admin.site.register(AlertGroup) 45 | admin.site.register(AlertGlobal) 46 | admin.site.register(AlertPlugin) 47 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/api.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView 3 | 4 | from aw.api_endpoints.key import APIKey, APIKeyItem 5 | from aw.api_endpoints.job import APIJob, APIJobItem, APIJobExecutionItem, APIJobExecutionLogs, \ 6 | APIJobExecutionLogFile, APIJobExecution 7 | from aw.api_endpoints.permission import APIPermission, APIPermissionItem 8 | from aw.api_endpoints.credentials import APIJobCredentials, APIJobCredentialsItem 9 | from aw.api_endpoints.filesystem import APIFsBrowse, APIFsExists 10 | from aw.api_endpoints.system import APISystemConfig 11 | from aw.api_endpoints.repository import APIRepository, APIRepositoryItem, APIRepositoryLogFile 12 | from aw.api_endpoints.alert import APIAlertPlugin, APIAlertPluginItem, APIAlertUser, APIAlertUserItem, \ 13 | APIAlertGlobal, APIAlertGlobalItem, APIAlertGroup, APIAlertGroupItem 14 | # from aw.api_endpoints.base import not_implemented 15 | 16 | urlpatterns_api = [ 17 | path('api/key/', APIKeyItem.as_view()), 18 | path('api/key', APIKey.as_view()), 19 | path('api/job///log/', APIJobExecutionLogs.as_view()), 20 | path('api/job///log', APIJobExecutionLogFile.as_view()), 21 | path('api/job//', APIJobExecutionItem.as_view()), 22 | path('api/job/', APIJobItem.as_view()), 23 | path('api/job_exec', APIJobExecution.as_view()), 24 | path('api/job', APIJob.as_view()), 25 | path('api/permission/', APIPermissionItem.as_view()), 26 | path('api/permission', APIPermission.as_view()), 27 | path('api/credentials/', APIJobCredentialsItem.as_view()), 28 | path('api/credentials', APIJobCredentials.as_view()), 29 | path('api/repository/log/', APIRepositoryLogFile.as_view()), 30 | path('api/repository/', APIRepositoryItem.as_view()), 31 | path('api/repository', APIRepository.as_view()), 32 | path('api/alert/plugin/', APIAlertPluginItem.as_view()), 33 | path('api/alert/plugin', APIAlertPlugin.as_view()), 34 | path('api/alert/global/', APIAlertGlobalItem.as_view()), 35 | path('api/alert/global', APIAlertGlobal.as_view()), 36 | path('api/alert/group/', APIAlertGroupItem.as_view()), 37 | path('api/alert/group', APIAlertGroup.as_view()), 38 | path('api/alert/user/', APIAlertUserItem.as_view()), 39 | path('api/alert/user', APIAlertUser.as_view()), 40 | path('api/config', APISystemConfig.as_view()), 41 | path('api/fs/browse/', APIFsBrowse.as_view()), 42 | path('api/fs/exists', APIFsExists.as_view()), 43 | path('api/_schema/', SpectacularAPIView.as_view(), name='_schema'), 44 | path('api/_docs', SpectacularSwaggerView.as_view(url_name='_schema'), name='swagger-ui'), 45 | ] 46 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/api_endpoints/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansibleguy/webui/933b57a51f90c42a7679868e3b33ccc73f5def05/src/ansibleguy-webui/aw/api_endpoints/__init__.py -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/api_endpoints/key.py: -------------------------------------------------------------------------------- 1 | from hashlib import md5 2 | 3 | from django.core.exceptions import ObjectDoesNotExist 4 | from rest_framework.views import APIView 5 | from rest_framework import serializers 6 | from rest_framework.response import Response 7 | from drf_spectacular.utils import extend_schema, OpenApiResponse 8 | 9 | from aw.utils.util import datetime_w_tz 10 | from aw.config.hardcoded import KEY_TIME_FORMAT 11 | from aw.model.api import AwAPIKey 12 | from aw.api_endpoints.base import API_PERMISSION, get_api_user, BaseResponse, GenericResponse 13 | 14 | 15 | class KeyReadResponse(BaseResponse): 16 | token = serializers.CharField() 17 | id = serializers.CharField() 18 | 19 | 20 | class KeyWriteResponse(BaseResponse): 21 | token = serializers.CharField() 22 | secret = serializers.CharField() 23 | 24 | 25 | class APIKey(APIView): 26 | http_method_names = ['post', 'get'] 27 | serializer_class = KeyReadResponse 28 | permission_classes = API_PERMISSION 29 | 30 | @staticmethod 31 | @extend_schema( 32 | request=None, 33 | responses={200: KeyReadResponse}, 34 | summary='Return a list of all existing API keys of the current user.', 35 | ) 36 | def get(request): 37 | tokens = [] 38 | for key in AwAPIKey.objects.filter(user=get_api_user(request)): 39 | tokens.append({'token': key.name, 'id': md5(key.name.encode('utf-8')).hexdigest()}) 40 | 41 | return Response(tokens) 42 | 43 | @extend_schema( 44 | request=None, 45 | responses={200: OpenApiResponse(KeyWriteResponse, description='Returns generated API token & key')}, 46 | summary='Create a new API key.', 47 | ) 48 | def post(self, request): 49 | self.serializer_class = KeyWriteResponse 50 | user = get_api_user(request) 51 | token = f'{user}-{datetime_w_tz().strftime(KEY_TIME_FORMAT)}' 52 | _, key = AwAPIKey.objects.create_key(name=token, user=user) 53 | return Response({'token': token, 'key': key}) 54 | 55 | 56 | class APIKeyItem(APIView): 57 | http_method_names = ['delete'] 58 | serializer_class = GenericResponse 59 | permission_classes = API_PERMISSION 60 | 61 | @extend_schema( 62 | request=None, 63 | responses={ 64 | 200: OpenApiResponse(response=GenericResponse, description='API key deleted'), 65 | 404: OpenApiResponse(response=GenericResponse, description='API key does not exist'), 66 | }, 67 | summary='Delete one of the existing API keys of the current user.', 68 | ) 69 | def delete(self, request, token: str): 70 | try: 71 | result = AwAPIKey.objects.get(user=get_api_user(request), name=token) 72 | 73 | if result is not None: 74 | result.delete() 75 | return Response(data={'msg': 'API key deleted'}, status=200) 76 | 77 | except ObjectDoesNotExist: 78 | pass 79 | 80 | return Response(data={'msg': 'API key not found'}, status=404) 81 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | from django.dispatch import receiver 3 | from django.db.backends.signals import connection_created 4 | 5 | 6 | class AwConfig(AppConfig): 7 | name = 'aw' 8 | verbose_name = 'Ansible-WebUI' 9 | 10 | 11 | # configuring sqlite at application startup/connection initialization 12 | @receiver(connection_created) 13 | def configure_sqlite(connection, **kwargs): 14 | if connection.vendor == 'sqlite': 15 | with connection.cursor() as cursor: 16 | # https://www.sqlite.org/pragma.html#pragma_journal_mode 17 | cursor.execute('PRAGMA journal_mode = WAL;') 18 | # https://www.sqlite.org/pragma.html#pragma_busy_timeout 19 | cursor.execute('PRAGMA busy_timeout = 5000;') 20 | # https://www.sqlite.org/pragma.html#pragma_synchronous 21 | cursor.execute('PRAGMA synchronous = NORMAL;') 22 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/base.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from django.contrib.auth.models import Group 3 | 4 | USERS = get_user_model() 5 | GROUPS = Group 6 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansibleguy/webui/933b57a51f90c42a7679868e3b33ccc73f5def05/src/ansibleguy-webui/aw/config/__init__.py -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/config/defaults.py: -------------------------------------------------------------------------------- 1 | from os import environ, getcwd 2 | from pathlib import Path 3 | from secrets import choice as random_choice 4 | from string import digits, ascii_letters, punctuation 5 | from datetime import datetime 6 | 7 | 8 | def inside_docker() -> bool: 9 | return 'AW_DOCKER' in environ and environ['AW_DOCKER'] == '1' 10 | 11 | 12 | def _get_existing_ansible_config_file() -> (str, None): 13 | # https://docs.ansible.com/ansible/latest/reference_appendices/config.html#the-configuration-file 14 | 15 | for file in [ 16 | getcwd() + '/ansible.cfg', 17 | environ['HOME'] + '/ansible.cfg', 18 | environ['HOME'] + '/.ansible.cfg', 19 | '/etc/ansible/ansible.cfg', 20 | ]: 21 | if Path(file).is_file(): 22 | return file 23 | 24 | return None 25 | 26 | 27 | def _get_defaults_docker(var: str) -> any: 28 | if not inside_docker(): 29 | return None 30 | 31 | return { 32 | 'path_ssh_known_hosts': f'{getcwd()}/known_hosts', 33 | }[var] 34 | 35 | 36 | # need to be referenced multiple times without import dependencies 37 | CONFIG_DEFAULTS = { 38 | 'port': 8000, 39 | 'address': '127.0.0.1', 40 | 'run_timeout': 3600, 41 | 'path_run': '/tmp/ansible-webui', 42 | 'path_play': getcwd(), 43 | 'path_log': f"{environ['HOME']}/.local/share/ansible-webui", 44 | 'path_template': None, # only for custom overrides 45 | 'db': f"{environ['HOME']}/.config/ansible-webui", 46 | 'timezone': datetime.now().astimezone().tzname(), 47 | 'secret': ''.join(random_choice(ascii_letters + digits + punctuation) for _ in range(50)), 48 | 'session_timeout': 12 * 60 * 60, # 12h 49 | 'path_ansible_config': _get_existing_ansible_config_file(), 50 | 'path_ssh_known_hosts': _get_defaults_docker('path_ssh_known_hosts'), 51 | 'debug': False, 52 | 'logo_url': 'img/logo.svg', 53 | 'ssl_file_key': None, 54 | 'ssl_file_crt': None, 55 | 'auth_mode': 'local', 56 | 'saml_config': None, 57 | 'jwt_algo': 'HS256', 58 | 'jwt_secret': ''.join(random_choice(ascii_letters + digits + punctuation) for _ in range(30)), 59 | } 60 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/config/environment.py: -------------------------------------------------------------------------------- 1 | from os import environ 2 | from functools import cache 3 | 4 | from aw.config.defaults import CONFIG_DEFAULTS 5 | from aw.config.hardcoded import ENV_KEY_CONFIG, ENV_KEY_SAML 6 | 7 | AW_ENV_VARS = { 8 | 'hostnames': ['AW_HOSTNAMES'], 9 | 'port': ['AW_PORT'], 10 | 'proxy': ['AW_PROXY'], 11 | 'address': ['AW_LISTEN', 'AW_LISTEN_ADDRESS'], 12 | 'timezone': ['AW_TIMEZONE'], 13 | 'secret': ['AW_SECRET'], 14 | 'path_run': ['AW_PATH_RUN'], 15 | 'path_play': ['AW_PATH_PLAY', 'ANSIBLE_PLAYBOOK_DIR'], 16 | 'version': ['AW_VERSION'], 17 | 'deployment': ['AW_ENV'], 18 | 'serve_static': ['AW_SERVE_STATIC'], 19 | 'init_admin': ['AW_ADMIN'], 20 | 'init_admin_pwd': ['AW_ADMIN_PWD'], 21 | 'db': ['AW_DB'], 22 | 'db_migrate': ['AW_DB_MIGRATE'], 23 | 'run_timeout': ['AW_RUN_TIMEOUT'], 24 | 'path_ansible_config': ['ANSIBLE_CONFIG'], 25 | 'path_log': ['AW_PATH_LOG'], 26 | 'session_timeout': ['AW_SESSION_TIMEOUT'], 27 | 'path_ssh_known_hosts': ['AW_SSH_KNOWN_HOSTS'], 28 | 'ssl_file_crt': ['AW_SSL_CERT'], 29 | 'ssl_file_key': ['AW_SSL_KEY'], 30 | 'debug': ['AW_DEBUG'], 31 | 'auth_mode': ['AW_AUTH'], 32 | 'saml_config': [ENV_KEY_SAML], 33 | } 34 | AW_ENV_VARS_SECRET = ['secret', 'init_admin', 'init_admin_pwd', 'saml_config'] 35 | 36 | AW_ENV_VARS_REV = {} 37 | for key_config, keys_env in AW_ENV_VARS.items(): 38 | for key_env in keys_env: 39 | AW_ENV_VARS_REV[key_env] = key_config 40 | 41 | 42 | def get_aw_env_var(var: str) -> (str, None): 43 | if var in AW_ENV_VARS: 44 | for key in AW_ENV_VARS[var]: 45 | if key in environ: 46 | return environ[key] 47 | 48 | return None 49 | 50 | 51 | @cache 52 | def get_aw_env_var_or_default(var: str) -> (str, list, None): 53 | val = get_aw_env_var(var) 54 | if val is None: 55 | val = CONFIG_DEFAULTS.get(var, None) 56 | 57 | return val 58 | 59 | 60 | def check_aw_env_var_is_set(var: str) -> bool: 61 | return get_aw_env_var(var) is not None 62 | 63 | 64 | # only use on edge-cases; as.config.main.Config.is_true is preferred 65 | def check_aw_env_var_true(var: str, fallback: bool = False) -> bool: 66 | val = get_aw_env_var_or_default(var) 67 | if val is None: 68 | return fallback 69 | 70 | return str(val).lower() in ['1', 'true', 'y', 'yes'] 71 | 72 | 73 | def auth_mode_saml() -> bool: 74 | return get_aw_env_var_or_default('auth_mode').lower() == 'saml' and \ 75 | ENV_KEY_SAML in environ and \ 76 | (ENV_KEY_CONFIG in environ and environ[ENV_KEY_CONFIG] != '0') 77 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/config/hardcoded.py: -------------------------------------------------------------------------------- 1 | # todo: some of these settings could be moved to the system-config later on 2 | 3 | THREAD_JOIN_TIMEOUT = 3 4 | INTERVAL_RELOAD = 10 # start/stop threads for configured jobs 5 | INTERVAL_CHECK = 5 # check for queued jobs 6 | LOGIN_PATH = '/a/login/' 7 | LOGOUT_PATH = '/o/' 8 | LOG_TIME_FORMAT = '%Y-%m-%d %H:%M:%S %z' 9 | SHORT_TIME_FORMAT = '%Y-%m-%d %H:%M:%S' 10 | FILE_TIME_FORMAT = '%Y-%m-%d_%H-%M-%S' 11 | KEY_TIME_FORMAT = '%Y-%m-%d-%H-%M-%S' 12 | MIN_SECRET_LEN = 30 13 | JOB_EXECUTION_LIMIT = 20 14 | GRP_MANAGER = { 15 | 'job': 'AW Job Managers', 16 | 'permission': 'AW Permission Managers', 17 | 'repository': 'AW Repository Managers', 18 | 'credentials': 'AW Credentials Managers', 19 | 'alert': 'AW Alert Managers', 20 | 'system': 'AW System Managers', 21 | } 22 | REPO_CLONE_TIMEOUT = 300 23 | ENV_KEY_CONFIG = 'AW_CONFIG' 24 | ENV_KEY_SAML = 'AW_SAML' 25 | SECRET_HIDDEN = '⬤' * 15 26 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/config/main.py: -------------------------------------------------------------------------------- 1 | from os import environ 2 | from importlib.metadata import version, PackageNotFoundError 3 | from sys import stderr 4 | 5 | from pytz import all_timezones, timezone, BaseTzInfo 6 | from django.db.utils import IntegrityError, OperationalError 7 | from django.core.exceptions import ImproperlyConfigured, AppRegistryNotReady 8 | 9 | from aw.config.environment import get_aw_env_var, get_aw_env_var_or_default 10 | from aw.utils.util_no_config import set_timezone, is_set 11 | from aw.config.defaults import CONFIG_DEFAULTS 12 | 13 | 14 | def __get_module_version() -> str: 15 | env_version = get_aw_env_var_or_default('version') 16 | if env_version is not None: 17 | return env_version 18 | 19 | try: 20 | return version('ansibleguy-webui') 21 | 22 | except PackageNotFoundError: 23 | # NOTE: not able to use aw.utils.debug.log_warn because of circular dependency 24 | stderr.write('\x1b[1;33mWARNING: Module version could not be determined!\x1b[0m\n') 25 | return '0.0.0' 26 | 27 | 28 | VERSION = __get_module_version() 29 | 30 | 31 | class Config: 32 | def __init__(self): 33 | set_timezone(self.get('timezone')) 34 | 35 | @staticmethod 36 | def _from_env_or_db(setting: str) -> any: 37 | env_var_value = get_aw_env_var(setting) 38 | if is_set(env_var_value): 39 | return env_var_value 40 | 41 | try: 42 | if 'AW_INIT' in environ and environ['AW_INIT'] == '1': 43 | # do not try to use ORM before django-init 44 | raise AppRegistryNotReady 45 | 46 | # pylint: disable=C0415 47 | from aw.model.system import get_config_from_db 48 | value = getattr(get_config_from_db(), str(setting)) 49 | if value is not None: 50 | return value 51 | 52 | except (IntegrityError, OperationalError, ImproperlyConfigured, AppRegistryNotReady, ImportError, 53 | AttributeError): 54 | # if database not initialized or migrations missing; or env-only var 55 | pass 56 | 57 | if setting not in CONFIG_DEFAULTS: 58 | return None 59 | 60 | return CONFIG_DEFAULTS[setting] 61 | 62 | def get(self, setting: str) -> any: 63 | return self._from_env_or_db(setting) 64 | 65 | def __getitem__(self, setting): 66 | return self._from_env_or_db(setting) 67 | 68 | @property 69 | def timezone_str(self) -> str: 70 | tz_str = self.get('timezone') 71 | 72 | if tz_str not in all_timezones: 73 | return 'UTC' 74 | 75 | return tz_str 76 | 77 | @property 78 | def timezone(self) -> BaseTzInfo: 79 | return timezone(self.timezone_str) 80 | 81 | def is_true(self, setting: str, fallback: bool = False) -> bool: 82 | val = self.get(setting) 83 | if val is None: 84 | return fallback 85 | 86 | if isinstance(val, bool): 87 | return val 88 | 89 | return str(val).lower() in ['1', 'true', 'y', 'yes'] 90 | 91 | 92 | def init_config(): 93 | environ.setdefault('DJANGO_SETTINGS_MODULE', 'aw.settings') 94 | environ['PYTHONIOENCODING'] = 'utf8' 95 | environ['PYTHONUNBUFFERED'] = '1' 96 | environ['ANSIBLE_FORCE_COLOR'] = '1' 97 | 98 | # pylint: disable=W0601 99 | global config 100 | config = Config() 101 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/config/navigation.py: -------------------------------------------------------------------------------- 1 | from aw.config.hardcoded import LOGOUT_PATH 2 | 3 | NAVIGATION = { 4 | 'left': { 5 | # 'Dashboard': '/ui/', 6 | 'Jobs': { 7 | 'Manage': '/ui/jobs/manage', 8 | 'Logs': '/ui/jobs/log', 9 | 'Credentials': '/ui/jobs/credentials', 10 | 'Repositories': '/ui/jobs/repository', 11 | }, 12 | 'Settings': { 13 | 'API Keys': '/ui/settings/api_keys', 14 | 'Permissions': '/ui/settings/permissions', 15 | 'Alerts': '/ui/settings/alerts', 16 | }, 17 | 'System': { 18 | 'Admin': '/ui/system/admin/', 19 | 'API Docs': '/ui/system/api_docs', 20 | 'Config': '/ui/system/config', 21 | 'Environment': '/ui/system/environment', 22 | 'Password': '/a/password_change/', 23 | }, 24 | }, 25 | 'right': { 26 | 'GH': { 27 | 'element': '', 28 | 'url': 'https://github.com/ansibleguy/webui', 29 | 'login': False, 30 | }, 31 | 'DON': { 32 | 'element': '', 33 | 'url': 'https://ko-fi.com/ansible0guy', 34 | 'login': False, 35 | }, 36 | 'BUG': { 37 | 'element': '', 38 | 'url': 'https://github.com/ansibleguy/webui/issues', 39 | 'login': False, 40 | }, 41 | 'DOC': { 42 | 'element': '', 43 | 'url': 'https://webui.ansibleguy.net/', 44 | 'login': False, 45 | }, 46 | 'LO': { 47 | 'element': '', 49 | 'url': LOGOUT_PATH, 50 | 'method': 'post', 51 | 'login': True, 52 | }, 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/db_sqlite_patched/NOTE: -------------------------------------------------------------------------------- 1 | NOTE: quick-patch for https://github.com/django/django/commit/a0204ac183ad6bca71707676d994d5888cf966aa 2 | 3 | todo: remove once feature is available natively in django 5.1 4 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/db_sqlite_patched/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansibleguy/webui/933b57a51f90c42a7679868e3b33ccc73f5def05/src/ansibleguy-webui/aw/db_sqlite_patched/__init__.py -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/db_sqlite_patched/_functions.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=W0401,W0614 2 | from django.db.backends.sqlite3._functions import * 3 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/db_sqlite_patched/base.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=W0401,W0614,E0102 2 | from django.db.backends.sqlite3.base import * 3 | 4 | 5 | # NOTE: quick-patch for https://github.com/django/django/commit/a0204ac183ad6bca71707676d994d5888cf966aa 6 | # todo: remove once feature is available natively in django 5.1 7 | 8 | class DatabaseWrapper(DatabaseWrapper): 9 | def _start_transaction_under_autocommit(self): 10 | self.cursor().execute('BEGIN IMMEDIATE') 11 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/db_sqlite_patched/client.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=W0401,W0614 2 | from django.db.backends.sqlite3.client import * 3 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/db_sqlite_patched/creation.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=W0401,W0614 2 | from django.db.backends.sqlite3.creation import * 3 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/db_sqlite_patched/features.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=W0401,W0614 2 | from django.db.backends.sqlite3.features import * 3 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/db_sqlite_patched/introspection.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=W0401,W0614 2 | from django.db.backends.sqlite3.introspection import * 3 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/db_sqlite_patched/operations.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=W0401,W0614 2 | from django.db.backends.sqlite3.operations import * 3 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/db_sqlite_patched/schema.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=W0401,W0614 2 | from django.db.backends.sqlite3.schema import * 3 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/execute/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansibleguy/webui/933b57a51f90c42a7679868e3b33ccc73f5def05/src/ansibleguy-webui/aw/execute/__init__.py -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/execute/play.py: -------------------------------------------------------------------------------- 1 | import traceback 2 | 3 | from ansibleguy_runner import RunnerConfig, Runner 4 | 5 | from aw.config.main import config 6 | from aw.model.job import Job, JobExecution, JobExecutionResult 7 | from aw.execute.play_util import runner_cleanup, runner_prep, parse_run_result, failure, runner_logs 8 | from aw.execute.util import get_path_run, is_execution_status, job_logs 9 | from aw.execute.repository import ExecuteRepository 10 | from aw.execute.alert import Alert 11 | from aw.utils.util import datetime_w_tz, is_null, timed_lru_cache # get_ansible_versions 12 | from aw.utils.handlers import AnsibleConfigError, AnsibleRepositoryError 13 | from aw.utils.debug import log 14 | 15 | 16 | class AwRunnerConfig(RunnerConfig): 17 | def __init__(self, **kwargs): 18 | super().__init__(**kwargs, timeout=config['run_timeout'], quiet=True) 19 | 20 | 21 | def ansible_playbook(job: Job, execution: (JobExecution, None)): 22 | time_start = datetime_w_tz() 23 | path_run = get_path_run() 24 | if is_null(execution): 25 | execution = JobExecution(user=None, job=job, comment='Scheduled') 26 | 27 | result = JobExecutionResult(time_start=time_start) 28 | result.save() 29 | execution.result = result 30 | execution.save() 31 | 32 | log_files = job_logs(job=job, execution=execution) 33 | 34 | @timed_lru_cache(seconds=1) # check actual status every N seconds; lower DB queries 35 | def _cancel_job() -> bool: 36 | return is_execution_status(execution, 'Stopping') 37 | 38 | exec_repo = ExecuteRepository(repository=job.repository, execution=execution, path_run=path_run) 39 | try: 40 | exec_repo.create_or_update_repository() 41 | project_dir = exec_repo.get_project_dir() 42 | opts = runner_prep(job=job, execution=execution, path_run=path_run, project_dir=project_dir) 43 | execution.save() 44 | 45 | runner_cfg = AwRunnerConfig(**opts) 46 | runner_logs(cfg=runner_cfg, log_files=log_files) 47 | runner_cfg.prepare() 48 | command = ' '.join(runner_cfg.command) 49 | log(msg=f"Running job '{job.name}': '{command}'", level=5) 50 | execution.command = command[command.find('ansible-playbook'):] 51 | execution.save() 52 | 53 | runner = Runner(config=runner_cfg, cancel_callback=_cancel_job) 54 | runner.run() 55 | 56 | parse_run_result( 57 | result=result, 58 | execution=execution, 59 | runner=runner, 60 | ) 61 | del runner 62 | 63 | runner_cleanup(execution=execution, path_run=path_run, exec_repo=exec_repo) 64 | Alert(job=job, execution=execution).go() 65 | 66 | except ( 67 | AnsibleConfigError, AnsibleRepositoryError, 68 | OSError, ValueError, AttributeError, IndexError, KeyError, 69 | ) as err: 70 | tb = traceback.format_exc(limit=1024) 71 | failure( 72 | execution=execution, exec_repo=exec_repo, path_run=path_run, result=result, 73 | error_s=str(err), error_m=tb, 74 | ) 75 | Alert(job=job, execution=execution).go() 76 | raise 77 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/execute/queue.py: -------------------------------------------------------------------------------- 1 | from aw.model.job import JobExecution, JobQueue 2 | from aw.utils.debug import log 3 | 4 | 5 | def queue_get() -> (JobExecution, None): 6 | next_queue_item = JobQueue.objects.order_by('-created').first() 7 | if next_queue_item is None: 8 | return None 9 | 10 | execution = next_queue_item.execution 11 | next_queue_item.delete() 12 | return execution 13 | 14 | 15 | def queue_add(execution): 16 | log(msg=f"Job '{execution.job.name} {execution.id}' added to execution queue", level=4) 17 | JobQueue(execution=execution).save() 18 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/main.py: -------------------------------------------------------------------------------- 1 | from django.core.wsgi import get_wsgi_application 2 | 3 | app = get_wsgi_application() 4 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/migrations/0002_v0_0_13.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.2 on 2024-02-28 18:37 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("aw", "0001_v0_0_12"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="systemconfig", 14 | name="ara_server", 15 | field=models.CharField(blank=True, default=None, max_length=300, null=True), 16 | ), 17 | migrations.AddField( 18 | model_name="systemconfig", 19 | name="global_environment_vars", 20 | field=models.CharField( 21 | blank=True, default=None, max_length=1000, null=True 22 | ), 23 | ), 24 | migrations.AlterField( 25 | model_name="job", 26 | name="credentials_needed", 27 | field=models.BooleanField( 28 | choices=[(True, "Yes"), (False, "No")], default=False 29 | ), 30 | ), 31 | migrations.AlterField( 32 | model_name="job", 33 | name="inventory_file", 34 | field=models.CharField(blank=True, default=None, max_length=300, null=True), 35 | ), 36 | migrations.AlterField( 37 | model_name="jobglobalcredentials", 38 | name="become_user", 39 | field=models.CharField( 40 | blank=True, default="root", max_length=100, null=True 41 | ), 42 | ), 43 | migrations.AlterField( 44 | model_name="jobusercredentials", 45 | name="become_user", 46 | field=models.CharField( 47 | blank=True, default="root", max_length=100, null=True 48 | ), 49 | ), 50 | ] 51 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/migrations/0003_v0_0_14.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.2 on 2024-03-03 08:45 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("aw", "0002_v0_0_13"), 9 | ] 10 | 11 | operations = [ 12 | migrations.CreateModel( 13 | name="SchemaMetadata", 14 | fields=[ 15 | ( 16 | "id", 17 | models.AutoField( 18 | auto_created=True, 19 | primary_key=True, 20 | serialize=False, 21 | verbose_name="ID", 22 | ), 23 | ), 24 | ("created", models.DateTimeField(auto_now_add=True)), 25 | ("updated", models.DateTimeField(auto_now=True)), 26 | ("schema_version", models.CharField(max_length=50)), 27 | ( 28 | "schema_version_prev", 29 | models.CharField( 30 | blank=True, default=None, max_length=50, null=True 31 | ), 32 | ), 33 | ], 34 | options={ 35 | "abstract": False, 36 | }, 37 | ), 38 | ] 39 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/migrations/0004_v0_0_15.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.2 on 2024-03-03 09:27 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("aw", "0003_v0_0_14"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="systemconfig", 14 | name="timezone", 15 | field=models.CharField(default="UTC", max_length=300), 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/migrations/0005_v0_0_18.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.3 on 2024-03-15 18:44 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("aw", "0004_v0_0_15"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="jobexecution", 14 | name="command", 15 | field=models.CharField( 16 | blank=True, default=None, max_length=2000, null=True 17 | ), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/migrations/0007_v0_0_21.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.6 on 2024-05-20 16:26 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ("aw", "0006_v0_0_19"), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name="jobqueue", 15 | name="job", 16 | ), 17 | migrations.RemoveField( 18 | model_name="jobqueue", 19 | name="user", 20 | ), 21 | migrations.AddField( 22 | model_name="job", 23 | name="execution_prompts_optional", 24 | field=models.CharField( 25 | blank=True, default=None, max_length=2000, null=True 26 | ), 27 | ), 28 | migrations.AddField( 29 | model_name="job", 30 | name="execution_prompts_required", 31 | field=models.CharField( 32 | blank=True, default=None, max_length=2000, null=True 33 | ), 34 | ), 35 | migrations.AddField( 36 | model_name="jobqueue", 37 | name="execution", 38 | field=models.ForeignKey( 39 | blank=True, 40 | default=None, 41 | null=True, 42 | on_delete=django.db.models.deletion.CASCADE, 43 | related_name="jobqueue_fk_jobexec", 44 | to="aw.jobexecution", 45 | ), 46 | ), 47 | ] 48 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/migrations/0008_v0_0_22.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.7 on 2024-07-13 22:47 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("aw", "0007_v0_0_21"), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name="job", 15 | name="execution_prompts_optional", 16 | ), 17 | migrations.RemoveField( 18 | model_name="job", 19 | name="execution_prompts_required", 20 | ), 21 | migrations.AddField( 22 | model_name="job", 23 | name="execution_prompts", 24 | field=models.CharField( 25 | blank=True, default=None, max_length=5000, null=True 26 | ), 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/migrations/0009_v0_0_24.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.9 on 2024-09-17 17:32 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("aw", "0008_v0_0_22"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="repository", 15 | name="git_hook_cleanup", 16 | field=models.CharField( 17 | blank=True, default=None, max_length=1000, null=True 18 | ), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/migrations/0010_v0_0_25.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.10 on 2025-01-21 20:22 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('aw', '0009_v0_0_24'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='repository', 15 | name='git_timeout', 16 | field=models.PositiveSmallIntegerField(default=30), 17 | ), 18 | migrations.AlterField( 19 | model_name='job', 20 | name='cmd_args', 21 | field=models.CharField(blank=True, default=None, max_length=1000, null=True), 22 | ), 23 | migrations.AlterField( 24 | model_name='job', 25 | name='comment', 26 | field=models.CharField(blank=True, default=None, max_length=300, null=True), 27 | ), 28 | migrations.AlterField( 29 | model_name='job', 30 | name='playbook_file', 31 | field=models.CharField(max_length=150), 32 | ), 33 | migrations.AlterField( 34 | model_name='job', 35 | name='tags', 36 | field=models.CharField(blank=True, default=None, max_length=500, null=True), 37 | ), 38 | migrations.AlterField( 39 | model_name='job', 40 | name='tags_skip', 41 | field=models.CharField(blank=True, default=None, max_length=500, null=True), 42 | ), 43 | migrations.AlterField( 44 | model_name='jobexecution', 45 | name='cmd_args', 46 | field=models.CharField(blank=True, default=None, max_length=1000, null=True), 47 | ), 48 | migrations.AlterField( 49 | model_name='jobexecution', 50 | name='comment', 51 | field=models.CharField(blank=True, default=None, max_length=300, null=True), 52 | ), 53 | migrations.AlterField( 54 | model_name='jobexecution', 55 | name='status', 56 | field=models.PositiveSmallIntegerField(choices=[(0, 'Waiting'), (1, 'Starting'), (2, 'Running'), (3, 'Failed'), (4, 'Finished'), (5, 'Stopping'), (6, 'Stopped'), (7, 'Retry')], default=0), 57 | ), 58 | migrations.AlterField( 59 | model_name='jobexecution', 60 | name='tags', 61 | field=models.CharField(blank=True, default=None, max_length=500, null=True), 62 | ), 63 | migrations.AlterField( 64 | model_name='jobexecution', 65 | name='tags_skip', 66 | field=models.CharField(blank=True, default=None, max_length=500, null=True), 67 | ), 68 | migrations.AlterField( 69 | model_name='repository', 70 | name='status', 71 | field=models.PositiveSmallIntegerField(choices=[(0, 'Waiting'), (1, 'Starting'), (2, 'Running'), (3, 'Failed'), (4, 'Finished'), (5, 'Stopping'), (6, 'Stopped'), (7, 'Retry')], default=0), 72 | ), 73 | ] 74 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansibleguy/webui/933b57a51f90c42a7679868e3b33ccc73f5def05/src/ansibleguy-webui/aw/migrations/__init__.py -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/model/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansibleguy/webui/933b57a51f90c42a7679868e3b33ccc73f5def05/src/ansibleguy-webui/aw/model/__init__.py -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/model/api.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from rest_framework_api_key.models import AbstractAPIKey 3 | 4 | from aw.base import USERS 5 | 6 | 7 | class AwAPIKey(AbstractAPIKey): 8 | user = models.ForeignKey(USERS, on_delete=models.CASCADE, editable=False) 9 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/model/base.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | CHOICES_BOOL = ( 4 | (True, 'Yes'), 5 | (False, 'No') 6 | ) 7 | DEFAULT_NONE = {'null': True, 'default': None, 'blank': True} 8 | JOB_EXEC_STATUS_WAIT = 0 9 | JOB_EXEC_STATUS_START = 1 10 | JOB_EXEC_STATUS_RUN = 2 11 | JOB_EXEC_STATUS_FAILED = 3 12 | JOB_EXEC_STATUS_SUCCESS = 4 13 | JOB_EXEC_STATUS_STOPPING = 5 14 | JOB_EXEC_STATUS_RETRY = 7 15 | CHOICES_JOB_EXEC_STATUS = [ 16 | (JOB_EXEC_STATUS_WAIT, 'Waiting'), 17 | (JOB_EXEC_STATUS_START, 'Starting'), 18 | (JOB_EXEC_STATUS_RUN, 'Running'), 19 | (JOB_EXEC_STATUS_FAILED, 'Failed'), 20 | (JOB_EXEC_STATUS_SUCCESS, 'Finished'), 21 | (JOB_EXEC_STATUS_STOPPING, 'Stopping'), 22 | (6, 'Stopped'), 23 | (JOB_EXEC_STATUS_RETRY, 'Retry'), 24 | ] 25 | JOB_EXEC_STATI_ACTIVE = [ 26 | JOB_EXEC_STATUS_WAIT, 27 | JOB_EXEC_STATUS_START, 28 | JOB_EXEC_STATUS_RUN, 29 | JOB_EXEC_STATUS_STOPPING, 30 | JOB_EXEC_STATUS_RETRY, 31 | ] 32 | 33 | 34 | class BareModel(models.Model): 35 | created = models.DateTimeField(auto_now_add=True) 36 | 37 | class Meta: 38 | abstract = True 39 | 40 | 41 | class BaseModel(BareModel): 42 | updated = models.DateTimeField(auto_now=True) 43 | 44 | class Meta: 45 | abstract = True 46 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/static/css/aw_mobile.css: -------------------------------------------------------------------------------- 1 | @media only screen and (max-width: 1300px) { /* tablets */ 2 | .aw-text-responsive, .aw-text-responsive .btn { 3 | font-size: 0.9rem; 4 | } 5 | 6 | .aw-form-inline-right { 7 | float: none; 8 | } 9 | 10 | form .form-control, .aw-fs-choices { 11 | width: 45vw; 12 | } 13 | 14 | .aw-responsive-lg { 15 | display: none; 16 | } 17 | } 18 | 19 | @media only screen and (max-width: 991px) { 20 | 21 | 22 | } 23 | 24 | @media only screen and (max-width: 767px) { /* tablets */ 25 | .aw-text-responsive, .aw-text-responsive .btn { 26 | font-size: 0.9rem; 27 | } 28 | 29 | .aw-nav-main { 30 | font-weight: bold; 31 | text-transform: uppercase; 32 | } 33 | 34 | .aw-nav-left, .aw-nav-dd1, .aw-nav-dd2, .aw-nav-main-a1, .aw-nav-main-a2, .aw-nav-main-a3 { 35 | align-items: center !important; 36 | text-align: center !important; 37 | } 38 | 39 | .aw-nav-right-li { 40 | padding-left: 8px; 41 | padding-right: 8px; 42 | } 43 | 44 | .aw-nav-right { 45 | flex-direction: row !important; 46 | align-items: center; 47 | justify-content: center; 48 | border-top: 2px solid rgba(255, 255, 255, 0.6); 49 | margin-top: 10px; 50 | } 51 | 52 | form .form-control, .aw-fs-choices { 53 | width: 60vw; 54 | } 55 | 56 | .aw-responsive-lg { 57 | display: none; 58 | } 59 | 60 | .aw-responsive-med { 61 | display: none; 62 | } 63 | 64 | .aw-execution-logs { 65 | font-size: small; 66 | } 67 | } 68 | 69 | @media only screen and (max-width: 500px) { /* mobile devices */ 70 | .aw-text-responsive, .aw-text-responsive .btn { 71 | font-size: 0.8rem; 72 | } 73 | 74 | .aw-file-content-line { 75 | text-indent: -5vw; 76 | padding-left: 5vw; 77 | } 78 | 79 | form .form-control, .aw-login, .aw-login-fields, .aw-fs-choices { 80 | width: 90vw; 81 | } 82 | 83 | .aw-btn-action-icon { 84 | font-size: 20px !important; 85 | } 86 | 87 | .aw-btn-action { 88 | padding: 1px 1px !important; 89 | } 90 | 91 | .aw-responsive-lg { 92 | display: none; 93 | } 94 | 95 | .aw-responsive-med { 96 | display: none; 97 | } 98 | } 99 | 100 | @include media-breakpoint-between(xs,sm){ 101 | .btn { 102 | @include button-size($btn-padding-y-sm, $btn-padding-x-sm, $font-size-sm, $btn-border-radius-sm); 103 | } 104 | 105 | .aw-responsive-lg { 106 | display: none; 107 | } 108 | 109 | .aw-responsive-med { 110 | display: none; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/static/js/aw_nav.js: -------------------------------------------------------------------------------- 1 | $( document ).ready(function() { 2 | // submenu 3 | $('.dropdown-menu a.dropdown-toggle').on('click', function(e) { 4 | if (!$(this).next().hasClass('show')) { 5 | $(this).parents('.dropdown-menu').first().find('.show').removeClass("show"); 6 | } 7 | var $subMenu = $(this).next(".dropdown-menu"); 8 | $subMenu.toggleClass('show'); 9 | 10 | $(this).parents('li.nav-item.dropdown.show').on('hidden.bs.dropdown', function(e) { 11 | $('.dropdown-submenu .show').removeClass("show"); 12 | }); 13 | 14 | return false; 15 | }); 16 | 17 | // light/dark scheme toggle script 18 | const colorSchemeButton = document.getElementById('aw-switch-colorScheme'); 19 | const colorScheme = document.querySelector('meta[name="color-scheme"]'); 20 | const colorSchemeVar = 'color-scheme'; 21 | const colorSchemeLight = 'light'; 22 | const colorSchemeDark = 'dark'; 23 | const colorSchemeDefault = 'none'; 24 | 25 | function getColorSchema(preference) { 26 | if (preference !== colorSchemeDefault) { 27 | return preference; 28 | } else if (matchMedia('(prefers-color-scheme: light)').matches) { 29 | return colorSchemeLight; 30 | } else { 31 | return colorSchemeDark; 32 | } 33 | } 34 | 35 | function setColorSchema(mode) { 36 | document.body.className = mode; 37 | colorScheme.content = mode; 38 | localStorage.setItem(colorSchemeVar, mode); 39 | } 40 | 41 | function switchColorScheme(mode) { 42 | if (mode === colorSchemeLight) { 43 | return colorSchemeDark; 44 | } else { 45 | return colorSchemeLight; 46 | } 47 | } 48 | 49 | let userPreference = localStorage.getItem(colorSchemeVar) || colorSchemeDefault; 50 | setColorSchema(getColorSchema(userPreference)); 51 | 52 | if (colorSchemeButton != null) { 53 | colorSchemeButton.onclick = function() { 54 | userPreference = switchColorScheme(userPreference); 55 | setColorSchema(userPreference); 56 | }; 57 | } 58 | }); 59 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/static/js/jobs/credentials.js: -------------------------------------------------------------------------------- 1 | function updateApiTableDataCreds(row, entry) { 2 | row.innerHTML = document.getElementById(ELEM_ID_TMPL_ROW).innerHTML; 3 | row.cells[0].innerText = entry.name; 4 | 5 | let users = []; 6 | if (is_set(entry.connect_user)) { 7 | users.push('Connect User: ' + entry.connect_user); 8 | } 9 | if (is_set(entry.become_user)) { 10 | users.push('Become User: ' + entry.become_user); 11 | } 12 | if (users.length == 0) { 13 | row.cells[1].innerText = '-'; 14 | } else { 15 | row.cells[1].innerHTML = users.join('
'); 16 | } 17 | 18 | let vaults = []; 19 | if (is_set(entry.vault_file)) { 20 | vaults.push('Vault-File: ' + entry.vault_file); 21 | } 22 | if (is_set(entry.vault_id)) { 23 | vaults.push(('Vault-ID: ' + entry.vault_id)); 24 | } 25 | if (vaults.length == 0) { 26 | row.cells[2].innerText = '-'; 27 | } else { 28 | row.cells[2].innerHTML = vaults.join('
'); 29 | } 30 | 31 | secrets = []; 32 | if (entry.ssh_key_is_set) { 33 | secrets.push('SSH private key'); 34 | } 35 | if (entry.connect_pass_is_set) { 36 | secrets.push('Connect password'); 37 | } 38 | if (entry.become_pass_is_set) { 39 | secrets.push('Become password'); 40 | } 41 | if (entry.vault_pass_is_set) { 42 | secrets.push('Vault password'); 43 | } 44 | if (secrets.length == 0) { 45 | row.cells[3].innerText = '-'; 46 | } else { 47 | row.cells[3].innerHTML = secrets.join('
'); 48 | } 49 | } 50 | 51 | function updateApiTableDataUserCreds(row, entry) { 52 | updateApiTableDataCreds(row, entry); 53 | let actionsTemplate = document.getElementById("aw-api-data1-tmpl-actions").innerHTML; 54 | row.cells[4].innerHTML = actionsTemplate.replaceAll('${ID}', entry.id); 55 | } 56 | 57 | function updateApiTableDataGlobalCreds(row, entry) { 58 | updateApiTableDataCreds(row, entry); 59 | let actionsTemplate = document.getElementById("aw-api-data2-tmpl-actions").innerHTML; 60 | row.cells[4].innerHTML = actionsTemplate.replaceAll('${ID}', entry.id); 61 | } 62 | 63 | function updateUserCreds() { 64 | let apiEndpoint = "/api/credentials?global=false"; 65 | let targetTable = "aw-api-data1-table"; 66 | let dataSubkey = "user"; 67 | fetchApiTableData(apiEndpoint, updateApiTableDataUserCreds, false, null, targetTable, dataSubkey); 68 | } 69 | 70 | function updateGlobalCreds() { 71 | let apiEndpoint = "/api/credentials?global=true"; 72 | let targetTable = "aw-api-data2-table"; 73 | let dataSubkey = "shared"; 74 | fetchApiTableData(apiEndpoint, updateApiTableDataGlobalCreds, false, null, targetTable, dataSubkey); 75 | } 76 | 77 | $( document ).ready(function() { 78 | updateGlobalCreds(); 79 | setInterval('updateGlobalCreds()', (DATA_REFRESH_SEC * 1000)); 80 | 81 | updateUserCreds(); 82 | setInterval('updateUserCreds()', (DATA_REFRESH_SEC * 1000)); 83 | }); 84 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/static/js/jobs/repository.js: -------------------------------------------------------------------------------- 1 | function updateApiTableDataRepository(row, entry) { 2 | row.innerHTML = document.getElementById(ELEM_ID_TMPL_ROW).innerHTML; 3 | row.cells[0].innerText = entry.name; 4 | row.cells[1].innerText = entry.rtype_name; 5 | let actionsTemplate = document.getElementById(ELEM_ID_TMPL_ACTIONS).innerHTML; 6 | actionsTemplate = actionsTemplate.replaceAll('${ID}', entry.id); 7 | row.cells[4].innerHTML = actionsTemplate.replaceAll('${RTYPE}', entry.rtype_name.toLowerCase()); 8 | 9 | if (entry.rtype_name == "Git") { 10 | row.cells[2].innerHTML = entry.git_origin + ':' + entry.git_branch; 11 | } else { 12 | row.cells[2].innerText = entry.static_path; 13 | } 14 | if (entry.rtype_name == "Static") { 15 | row.cells[3].innerText = '-'; 16 | let actionButtonUpdate = row.cells[4].getElementsByClassName("aw-repo-update")[0]; 17 | actionButtonUpdate.setAttribute("hidden", "hidden"); 18 | } else if (is_set(entry.status_name) && !entry.git_isolate) { 19 | if (is_set(entry.time_update)) { 20 | row.cells[3].innerHTML += 'Updated: ' + entry.time_update + '
'; 21 | } 22 | row.cells[3].innerHTML += 'Status: ' + 23 | entry.status_name + ''; 24 | let statusTemplate = document.getElementById("aw-api-data-tmpl-status").innerHTML; 25 | 26 | statusTemplate = statusTemplate.replaceAll('${LOG_STDOUT}', entry.log_stdout); 27 | statusTemplate = statusTemplate.replaceAll('${LOG_STDOUT_URL}', entry.log_stdout_url); 28 | 29 | if (is_set(entry.log_stderr)) { 30 | statusTemplate = statusTemplate.replaceAll('${LOG_STDERR}', entry.log_stderr); 31 | statusTemplate = statusTemplate.replaceAll('${LOG_STDERR_URL}', entry.log_stderr_url); 32 | } else { 33 | statusTemplate = statusTemplate.replaceAll('${LOG_STDERR}', TITLE_NULL); 34 | statusTemplate = statusTemplate.replaceAll('${LOG_STDERR_URL}', LINK_NULL); 35 | } 36 | 37 | row.cells[3].innerHTML += '
' + statusTemplate; 38 | } 39 | 40 | } 41 | 42 | $( document ).ready(function() { 43 | apiEndpoint = "/api/repository"; 44 | fetchApiTableData(apiEndpoint, updateApiTableDataRepository); 45 | setInterval('fetchApiTableData(apiEndpoint, updateApiTableDataRepository)', (DATA_REFRESH_SEC * 1000)); 46 | }); 47 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/static/js/login.js: -------------------------------------------------------------------------------- 1 | const ELEM_ID_USER = "id_username"; 2 | const ELEM_ID_REMEMBER = "aw-login-remember"; 3 | 4 | function loadUsername() { 5 | if (localStorage.login_remember == 1) { 6 | let usernameField = document.getElementById(ELEM_ID_USER); 7 | if (is_set(localStorage.login_username)) { 8 | usernameField.value = localStorage.login_username; 9 | } 10 | } 11 | } 12 | 13 | function saveUserName() { 14 | if (localStorage.login_remember == 1) { 15 | let usernameField = document.getElementById(ELEM_ID_USER); 16 | if (is_set(usernameField.value)) { 17 | localStorage.login_username = usernameField.value; 18 | } 19 | } 20 | } 21 | 22 | function forgetUserName() { 23 | localStorage.login_remember = 0; 24 | localStorage.login_username = ""; 25 | } 26 | 27 | function handleRememberUsername(checkbox) { 28 | if (checkbox.checked == true) { 29 | localStorage.login_remember = 1; 30 | saveUserName(); 31 | } else { 32 | forgetUserName(); 33 | } 34 | } 35 | 36 | $( document ).ready(function() { 37 | $(".aw-login").on("click", "#" + ELEM_ID_REMEMBER, function(){ 38 | saveUserName(); 39 | }); 40 | $(".aw-login").on("click", "#" + ELEM_ID_REMEMBER, function(){ 41 | if (this.checked == true) { 42 | localStorage.login_remember = 1; 43 | saveUserName(); 44 | } else { 45 | forgetUserName(); 46 | } 47 | }); 48 | $(".aw-login").on("submit", ".aw-login-form", function(){ 49 | saveUserName(); 50 | }); 51 | if (localStorage.login_remember == 1) { 52 | document.getElementById(ELEM_ID_REMEMBER).setAttribute("checked", "checked"); 53 | loadUsername(); 54 | } 55 | }); 56 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/static/js/settings/api_key.js: -------------------------------------------------------------------------------- 1 | function updateApiTableDataKey(row, entry) { 2 | row.insertCell(0).innerText = entry.token; 3 | actionsTemplate = document.getElementById(ELEM_ID_TMPL_ACTIONS).innerHTML; 4 | row.insertCell(1).innerHTML = actionsTemplate.replaceAll('${TOKEN}', entry.token); 5 | } 6 | 7 | $( document ).ready(function() { 8 | apiEndpoint = "/api/key"; 9 | $(".aw-api-key-add").click(function(){ 10 | $.post(apiEndpoint, function(data, status){ 11 | prompt("Your new API key:\n\nToken: " + data.token + "\nKey:", data.key); 12 | }); 13 | }); 14 | fetchApiTableData(apiEndpoint, updateApiTableDataKey); 15 | setInterval('fetchApiTableData(apiEndpoint, updateApiTableDataKey)', (DATA_REFRESH_SEC * 1000)); 16 | }); 17 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/static/js/settings/environment.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansibleguy/webui/933b57a51f90c42a7679868e3b33ccc73f5def05/src/ansibleguy-webui/aw/static/js/settings/environment.js -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/static/js/settings/permission.js: -------------------------------------------------------------------------------- 1 | function updateApiTableDataPermission(row, entry) { 2 | row.innerHTML = document.getElementById(ELEM_ID_TMPL_ROW).innerHTML; 3 | row.cells[0].innerText = entry.name; 4 | row.cells[1].innerText = entry.permission_name; 5 | if (entry.jobs_all) { 6 | row.cells[2].innerText = 'All'; 7 | } else if (entry.jobs_name.length == 0) { 8 | row.cells[2].innerText = '-'; 9 | } else { 10 | row.cells[2].innerText = entry.jobs_name.join(', '); 11 | } 12 | if (entry.credentials_all) { 13 | row.cells[3].innerText = 'All'; 14 | } else if (entry.credentials_name.length == 0) { 15 | row.cells[3].innerText = '-'; 16 | } else { 17 | row.cells[3].innerText = entry.credentials_name.join(', '); 18 | } 19 | if (entry.repositories_all) { 20 | row.cells[4].innerText = 'All'; 21 | } else if (entry.repositories_name.length == 0) { 22 | row.cells[4].innerText = '-'; 23 | } else { 24 | row.cells[4].innerText = entry.repositories_name.join(', '); 25 | } 26 | if (entry.users_name.length == 0) { 27 | row.cells[5].innerText = '-'; 28 | } else { 29 | row.cells[5].innerText = entry.users_name.join(', '); 30 | } 31 | if (entry.groups_name.length == 0) { 32 | row.cells[6].innerText = '-'; 33 | } else { 34 | row.cells[6].innerText = entry.groups_name.join(', '); 35 | } 36 | 37 | actionsTemplate = document.getElementById(ELEM_ID_TMPL_ACTIONS).innerHTML; 38 | row.cells[7].innerHTML = actionsTemplate.replaceAll('${ID}', entry.id); 39 | } 40 | 41 | $( document ).ready(function() { 42 | apiEndpoint = "/api/permission"; 43 | fetchApiTableData(apiEndpoint, updateApiTableDataPermission); 44 | setInterval('fetchApiTableData(apiEndpoint, updateApiTableDataPermission)', (DATA_REFRESH_SEC * 1000)); 45 | }); 46 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/static/vendor/versions.txt: -------------------------------------------------------------------------------- 1 | js/popper.min.js 2.11.8 https://cdnjs.com/libraries/popper.js/2.11.8 2 | css/bootstrap.min.css 5.3.3 https://cdnjs.com/libraries/bootstrap/5.3.3 3 | js/bootstrap.min.js 5.3.3 https://cdnjs.com/libraries/bootstrap/5.3.3 4 | js/jquery.min.js 3.7.1 https://cdnjs.com/libraries/jquery/3.7.1 5 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/templates/body.html: -------------------------------------------------------------------------------- 1 | 2 | {% load util %} 3 | {% load static %} 4 | 5 | 6 | {% include "./head.html" %} 7 | 8 | {% block extrahead %} 9 | {% endblock %} 10 | 11 | 12 | 13 | {% include "./nav.html" %} 14 | {% include "./error/js_disabled.html" %} 15 |
16 |
17 |
{{ request.GET|get_value:"error"|ignore_none }}
18 |
19 | {% block content %} 20 | {% endblock %} 21 | {% if show_update_time %} 22 |
23 | {% endif %} 24 | 25 |
26 |
27 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/templates/button/autoRefresh.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/templates/button/icon/add.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/templates/button/icon/add_dir.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/templates/button/icon/add_git.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/templates/button/icon/collapse.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/templates/button/icon/copy.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/templates/button/icon/delete.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/templates/button/icon/download.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/templates/button/icon/edit.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/templates/button/icon/expand.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/templates/button/icon/return.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/templates/button/icon/run.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/templates/button/icon/sort.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/templates/button/icon/stop.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/templates/button/icon/toggle_off.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/templates/button/icon/toggle_on.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/templates/button/refresh.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/templates/django_saml2_auth/denied.html: -------------------------------------------------------------------------------- 1 | {% extends "../body.html" %} 2 | {% block content %} 3 |

Sorry, you are not allowed to access this app

4 |

To report a problem with your access please contact your system administrator

5 | 6 | 7 | 8 | {% endblock %} -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/templates/django_saml2_auth/error.html: -------------------------------------------------------------------------------- 1 | {% extends "../body.html" %} 2 | {% load util %} 3 | {% block content %} 4 |

Sorry, you are not allowed to access this app

5 |

To report a problem with your access please contact your system administrator

6 | {% if error_code %}

Error code: {{ error_code }}{{ error_code|saml_error_by_code }}

{% endif %} 7 | {% if reason %}

Reason: {{ reason }}

{% endif %} 8 | 9 | 10 | 11 | {% endblock %} -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/templates/django_saml2_auth/signout.html: -------------------------------------------------------------------------------- 1 | {% extends "../body.html" %} 2 | {% block content %} 3 |

You have signed out successfully.

4 |

If you want to login again or switch to another account, please do so in SSO.

5 | 6 | 7 | 8 | {% endblock %} -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/templates/email/alert.txt: -------------------------------------------------------------------------------- 1 | {% load util %} 2 | Job: {{ execution.job.name }} 3 | Status: {{ execution.status_name }} 4 | 5 | Executed by: {{ execution.user_name }} 6 | Start time: {{ execution.time_created_str }} 7 | {% if execution.result is not none %}Finish time: {{ execution.result.time_fin_str }} 8 | Duration: {{ execution.result.time_duration_str }} 9 | {% if execution.result.error is not none %} 10 | Short error message: '{{ execution.result.error.short }}' 11 | Long error message: '{{ execution.result.error.med }}'{% endif %} 12 | {% if error_msgs|get_value:'text'|exists %} 13 | Execution errors: 14 | {% for msg in error_msgs|get_value:'text' %}{{ msg }} 15 | {% endfor %}{% endif %} 16 | {% endif %} 17 | {% for log_attr in execution.log_file_fields %}{% set_var execution|get_value:log_attr as log_file %}{% set_var log_attr|concat:'_url' as log_url %}{% if log_file|file_exists %} 18 | {{ log_attr|whitespace_char:'_'|capitalize }}: {{ web_addr }}{{ execution|get_value:log_url }}{% endif %}{% endfor %} 19 | {% if stats|exists %} 20 | 21 | Statistics: 22 | {% for host, host_stats in stats.items %} 23 | Host: {{ host }} 24 | Stats: {% for k, v in host_stats.items %}{{ k }}: {{ v }}{% if not forloop.last %}, {% endif %}{% endfor %}{% endfor %} 25 | {% endif %} -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/templates/error/403.html: -------------------------------------------------------------------------------- 1 | {% extends "../body.html" %} 2 | {% block content %} 3 | {% endblock %} 4 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/templates/error/500.html: -------------------------------------------------------------------------------- 1 | {% extends "../body.html" %} 2 | {% block content %} 3 | {% endblock %} 4 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/templates/error/js_disabled.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/templates/fallback.html: -------------------------------------------------------------------------------- 1 | {% extends "./body.html" %} 2 | {% block content %} 3 | {{ content | safe }} 4 | {% endblock %} -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/templates/forms/base.html: -------------------------------------------------------------------------------- 1 |
2 | {% csrf_token %} 3 | 4 | {{ form }} 5 |
6 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/templates/forms/snippet.html: -------------------------------------------------------------------------------- 1 | {% load util %} 2 | {% load form_util %} 3 |
4 | {{ form.non_field_errors }} 5 |
6 | 7 | {% for bf in form.visible_fields %} 8 |
9 | 10 |
11 | {% if bf|form_field_is_dropdown %} 12 | {{ bf|get_form_field_select:existing|safe }} 13 | {% else %} 14 | {{ bf|get_form_field_input:existing|safe }} 15 | {% endif %} 16 |
17 | {% if bf.help_text|exists %} 18 |
19 | Info: {{ bf.help_text|safe }} 20 |
21 | {% endif %} 22 |
23 | {# for debugging #} 24 | {# bf.field|to_dict #}{# existing; todo: fix select existing values in dropdown #} 25 | {# existing #} 26 | {{ bf.errors }} 27 |
28 |
29 | {% endfor %} 30 | 31 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/templates/forms/snippet_test.html: -------------------------------------------------------------------------------- 1 | {% load util %} 2 | {% load form_util %} 3 | {{ form.non_field_errors }} 4 | {% for bf in form %} 5 |
6 | {{ bf.errors }} 7 | {{ bf.label_tag }} {{ field }} 8 | {{ bf.field|to_dict }} 9 | {% if bf|form_field_is_dropdown %} 10 |
11 | {{ bf|get_form_field_select|safe }} 12 |
13 | {% endif %} 14 |
15 | {% endfor %} 16 | {{ form }} -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/templates/head.html: -------------------------------------------------------------------------------- 1 | {% load util %} 2 | {% load static %} 3 | Ansible WebUI 4 | 5 | 6 | 7 | 8 | 9 | 10 | {% if script_unsafe_inline is none %} 11 | 12 | {% else %} 13 | 14 | {% endif %} 15 | {% comment %} 16 | CSP NOTES: 17 | fontawesome will fail if style-src is restricted 18 | script-src unsafe-eval is for the setInterval calls.. 19 | script_unsafe_inline is for template-generated inline-scripts (only on job-edit view) 20 | {% endcomment %} 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/templates/jobs/credentials.html: -------------------------------------------------------------------------------- 1 | {% extends "../body.html" %} 2 | {% load util %} 3 | {% load static %} 4 | {% block content %} 5 | 6 |
7 |

User Credentials

8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
NameUsersVaultSecretsActions
18 | 19 | 22 | 23 |
33 | 43 |
44 |

Global Credentials

45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 62 | 63 |
NameUsersVaultSecretsActions
55 | {% include "../button/refresh.html" %} 56 | 57 | 60 | 61 |
64 | 74 |
75 | {% endblock %} -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/templates/jobs/credentials_edit.html: -------------------------------------------------------------------------------- 1 | {% extends "../body.html" %} 2 | {% load static %} 3 | {% load util %} 4 | {% block content %} 5 | {% if 'global' in request.GET and request.GET|get_value:"global" == 'false' %} 6 |

User Credentials

7 | {% else %} 8 |

Global Credentials

9 | {% endif %} 10 | 11 | 12 | 15 | 16 | 17 | {% include "../forms/base.html" %} 18 | 19 | 20 | 23 | 24 | 25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/templates/jobs/repository.html: -------------------------------------------------------------------------------- 1 | {% extends "../body.html" %} 2 | {% load util %} 3 | {% load static %} 4 | {% block content %} 5 | 6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
Name{% include "../button/icon/sort.html" %}Type{% include "../button/icon/sort.html" %}Path/Origin{% include "../button/icon/sort.html" %}Status{% include "../button/icon/sort.html" %}Actions
17 | {% include "../button/refresh.html" %} 18 | 19 | 22 | 23 | 24 | 27 | 28 |
38 | 56 | 61 |
62 | {% endblock %} -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/templates/jobs/repository_edit.html: -------------------------------------------------------------------------------- 1 | {% extends "../body.html" %} 2 | {% load static %} 3 | {% load util %} 4 | {% block content %} 5 | 6 | 9 | 10 | 11 |
12 | {% csrf_token %} 13 | 14 | 15 | {{ form }} 16 |
17 | 18 | 19 | 20 | 23 | 24 | 25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/templates/registration/login.html: -------------------------------------------------------------------------------- 1 | {% extends "../body.html" %} 2 | {% load util %} 3 | {% load static %} 4 | {% block content %} 5 | 6 | {% if ''|auth_sso and user.is_authenticated %} 7 | 8 | 9 | 10 | {% else %} 11 |
12 | LOGO 13 |
14 | 57 | {% endif %} 58 | {% endblock %} -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/templates/registration/password_change_done.html: -------------------------------------------------------------------------------- 1 | {% extends "../body.html" %} 2 | {% load util %} 3 | {% load static %} 4 | {% block content %} 5 | 22 | {% endblock %} -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/templates/registration/password_change_form.html: -------------------------------------------------------------------------------- 1 | {% extends "../body.html" %} 2 | {% load util %} 3 | {% load static %} 4 | {% block content %} 5 | 61 | {% endblock %} -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/templates/registration/remember_user.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 6 |
-------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/templates/registration/saml.html: -------------------------------------------------------------------------------- 1 | {% extends "../body.html" %} 2 | {% load util %} 3 | {% load static %} 4 | {% block content %} 5 | 6 | {% if user.is_authenticated %} 7 | 8 | 9 | 10 | {% else %} 11 |
12 | LOGO 13 |
14 | 50 | {% endif %} 51 | {% endblock %} -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/templates/rest_framework/api.html: -------------------------------------------------------------------------------- 1 | {% extends "rest_framework/base.html" %} 2 | {# https://github.com/encode/django-rest-framework/blob/master/rest_framework/templates/rest_framework/base.html #} 3 | {% load static %} 4 | {% block title %} 5 | Ansible-WebUI API 6 | {% endblock %} 7 | {% block meta %} 8 | 9 | 10 | 11 | 12 | {% endblock %} 13 | {% block branding %} 14 | 15 | Ansible-WebUI 16 | 17 | {% endblock %} -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/templates/settings/alert_edit.html: -------------------------------------------------------------------------------- 1 | {% extends "../body.html" %} 2 | {% load static %} 3 | {% load util %} 4 | {% block content %} 5 | 6 | 9 | 10 | 11 | {% include "../forms/base.html" %} 12 | 13 | 14 | 17 | 18 | 19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/templates/settings/api_key.html: -------------------------------------------------------------------------------- 1 | {% extends "../body.html" %} 2 | {% load static %} 3 | {% block content %} 4 | 5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 | 18 | 19 |
API TokenActions
13 | {% include "../button/refresh.html" %} 14 | 17 |
20 | 25 |
26 | {% endblock %} -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/templates/settings/permission.html: -------------------------------------------------------------------------------- 1 | {% extends "../body.html" %} 2 | {% load util %} 3 | {% load static %} 4 | {% block content %} 5 | 6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 |
NamePermissionJobsCredentialsRepositoriesUsersGroupsActions
20 | {% include "../button/refresh.html" %} 21 | 22 | 25 | 26 |
39 | 49 |
50 | {% endblock %} -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/templates/settings/permission_edit.html: -------------------------------------------------------------------------------- 1 | {% extends "../body.html" %} 2 | {% load static %} 3 | {% load util %} 4 | {% block content %} 5 | 6 | 9 | 10 | 11 | {% include "../forms/base.html" %} 12 | 13 | 14 | 17 | 18 | 19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/templates/system/config.html: -------------------------------------------------------------------------------- 1 | {% extends "../body.html" %} 2 | {% load static %} 3 | {% load util %} 4 | {% block content %} 5 | {% include "../forms/base.html" %} 6 |


7 | 27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansibleguy/webui/933b57a51f90c42a7679868e3b33ccc73f5def05/src/ansibleguy-webui/aw/templatetags/__init__.py -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/urls.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=E0401 2 | from django.urls import path, re_path 3 | from django.conf.urls import include 4 | from django.contrib import admin 5 | from django.contrib.auth.views import LoginView, PasswordChangeView 6 | 7 | from web_serve_static import urlpatterns_static 8 | from aw.api import urlpatterns_api 9 | from aw.views.main import urlpatterns_ui, catchall, logout 10 | from aw.config.environment import check_aw_env_var_true, auth_mode_saml 11 | from aw.utils.deployment import deployment_dev 12 | from aw.views.forms.auth import saml_sp_initiated_login, saml_sp_initiated_login_init 13 | 14 | urlpatterns = [] 15 | 16 | if deployment_dev() or check_aw_env_var_true(var='serve_static', fallback=True): 17 | urlpatterns += urlpatterns_static 18 | 19 | urlpatterns += urlpatterns_api 20 | 21 | # AUTH 22 | if auth_mode_saml(): 23 | urlpatterns += [ 24 | path('a/saml/init/', saml_sp_initiated_login_init), 25 | path('a/saml/', include('django_saml2_auth.urls')), 26 | # user views 27 | path('a/login/', saml_sp_initiated_login, name='login_sso'), 28 | path('a/login/fallback/', LoginView.as_view(), name='login'), 29 | ] 30 | 31 | else: 32 | urlpatterns += [ 33 | path('a/login/', LoginView.as_view(), name='login'), 34 | ] 35 | 36 | urlpatterns += [ 37 | path('a/password_change/', PasswordChangeView.as_view()), 38 | path('_admin/', admin.site.urls), 39 | path('o/', logout), 40 | ] 41 | 42 | # UI 43 | urlpatterns += urlpatterns_ui 44 | urlpatterns += [ 45 | # fallback 46 | re_path(r'^', catchall), 47 | ] 48 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansibleguy/webui/933b57a51f90c42a7679868e3b33ccc73f5def05/src/ansibleguy-webui/aw/utils/__init__.py -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/utils/crypto.py: -------------------------------------------------------------------------------- 1 | from base64 import b64encode, b64decode 2 | from hashlib import sha256 3 | 4 | from Crypto.Cipher import AES 5 | from Crypto.Random import get_random_bytes 6 | from Crypto.Util.Padding import pad, unpad 7 | 8 | from aw.config.main import config 9 | from aw.utils.util import is_null 10 | from aw.utils.debug import log_warn, log 11 | 12 | __KEY = sha256(config['secret'].encode('utf-8')).digest() 13 | 14 | 15 | def encrypt(plaintext: str) -> str: 16 | if is_null(plaintext): 17 | return '' 18 | 19 | try: 20 | return _encrypt(plaintext.encode('utf-8')).decode('utf-8') 21 | 22 | except ValueError as err: 23 | log_warn("Unable to encrypt data!") 24 | log(msg=f"Got error encrypting plaintext: '{err}'", level=6) 25 | return '' 26 | 27 | 28 | def _encrypt(plaintext: bytes) -> bytes: 29 | iv = get_random_bytes(AES.block_size) 30 | cipher = AES.new(__KEY, AES.MODE_CBC, iv) 31 | ciphertext = iv + cipher.encrypt( 32 | plaintext=pad( 33 | data_to_pad=plaintext, 34 | block_size=AES.block_size, 35 | style='pkcs7', 36 | ), 37 | ) 38 | return b64encode(ciphertext) 39 | 40 | 41 | def decrypt(ciphertext: str) -> str: 42 | if is_null(ciphertext): 43 | return '' 44 | 45 | try: 46 | return _decrypt(ciphertext.encode('utf-8')).decode('utf-8') 47 | 48 | except ValueError as err: 49 | log_warn("Unable to decrypt secret! Maybe the key 'AW_SECRET' changed?") 50 | log(msg=f"Got error decrypting ciphertext: '{err}'", level=6) 51 | return '' 52 | 53 | 54 | def _decrypt(ciphertext: bytes) -> bytes: 55 | ciphertext = b64decode(ciphertext) 56 | cipher = AES.new(__KEY, AES.MODE_CBC, ciphertext[:AES.block_size]) 57 | return unpad( 58 | padded_data=cipher.decrypt(ciphertext[AES.block_size:]), 59 | block_size=AES.block_size, 60 | style='pkcs7', 61 | ) 62 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/utils/debug.py: -------------------------------------------------------------------------------- 1 | from os import getpid 2 | from sys import stderr, stdout 3 | from inspect import stack as inspect_stack 4 | from inspect import getfile as inspect_getfile 5 | 6 | from aw.config.main import config 7 | from aw.utils.util import datetime_w_tz 8 | from aw.utils.deployment import deployment_dev, deployment_staging 9 | from aw.config.hardcoded import LOG_TIME_FORMAT 10 | 11 | PID = getpid() 12 | 13 | LEVEL_NAME_MAPPING = { 14 | 1: 'FATAL', 15 | 2: 'ERROR', 16 | 3: 'WARN', 17 | 4: 'INFO', 18 | 5: 'INFO', 19 | 6: 'DEBUG', 20 | 7: 'DEBUG', 21 | } 22 | 23 | 24 | def _log_prefix() -> str: 25 | # time format adapted to the one used by gunicorn 26 | # todo: update gunicorn log format (gunicorn.glogging.CONFIG_DEFAULTS) 27 | return f'[{datetime_w_tz().strftime(LOG_TIME_FORMAT)}] [{PID}]' 28 | 29 | 30 | def log(msg: str, level: int = 3): 31 | debug = deployment_dev() or config['debug'] 32 | prefix_caller = '' 33 | 34 | if level > 5 and not debug: 35 | return 36 | 37 | if debug: 38 | caller = inspect_getfile(inspect_stack()[1][0]).rsplit('/', 1)[1].rsplit('.', 1)[0] 39 | prefix_caller = f'[{caller}] ' 40 | 41 | print(f"{_log_prefix()} [{LEVEL_NAME_MAPPING[level]}] {prefix_caller}{msg}") 42 | 43 | 44 | def log_warn(msg: str, _stderr: bool = False): 45 | if _stderr: 46 | stderr.write(f'\x1b[1;33m{_log_prefix()} [{LEVEL_NAME_MAPPING[3]}] {msg}\x1b[0m\n') 47 | 48 | else: 49 | stdout.write(f'\x1b[1;33m{_log_prefix()} [{LEVEL_NAME_MAPPING[3]}] {msg}\x1b[0m\n') 50 | 51 | 52 | def log_error(msg: str): 53 | stderr.write(f'\033[01;{_log_prefix()} [{LEVEL_NAME_MAPPING[2]}] {msg}\x1b[0m\n') 54 | 55 | 56 | def warn_if_development(): 57 | if deployment_dev(): 58 | log_warn('Development mode!') 59 | 60 | elif deployment_staging(): 61 | log_warn('Staging mode!') 62 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/utils/deployment.py: -------------------------------------------------------------------------------- 1 | from aw.config.environment import get_aw_env_var 2 | from aw.config.defaults import inside_docker 3 | from aw.config.main import VERSION 4 | 5 | 6 | def deployment_dev() -> bool: 7 | return get_aw_env_var('deployment') == 'dev' 8 | 9 | 10 | def deployment_staging() -> bool: 11 | return get_aw_env_var('deployment') == 'staging' 12 | 13 | 14 | def deployment_prod() -> bool: 15 | return not deployment_dev() and not deployment_staging() 16 | 17 | 18 | def deployment_docker() -> bool: 19 | return inside_docker() 20 | 21 | 22 | def is_release_version() -> bool: 23 | return VERSION not in ['dev', 'staging', 'latest', '0.0.0'] and \ 24 | VERSION.find('dev') == -1 and VERSION.find('staging') == -1 and \ 25 | VERSION.find('latest') == -1 26 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/utils/handlers.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | from aw.utils.debug import log 4 | 5 | 6 | class AnsibleConfigError(Exception): 7 | pass 8 | 9 | 10 | class AnsibleRepositoryError(Exception): 11 | pass 12 | 13 | 14 | def handler_log(request, msg: str, status: int): 15 | log(f"{request.build_absolute_uri()} - Got error {status} - {msg}") 16 | 17 | 18 | def handler404(request, msg: str): 19 | handler_log(request=request, msg=msg, status=404) 20 | return render(request, 'error/404.html', context={'request': request, 'error_msg': msg}) 21 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/utils/http.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | 3 | from django.shortcuts import HttpResponse, redirect 4 | 5 | from aw.config.hardcoded import LOGIN_PATH 6 | 7 | 8 | def deny_request(request) -> (bool, HttpResponse): 9 | if request.method not in ['GET', 'POST', 'PUT']: 10 | return True, HttpResponse(status=405) 11 | 12 | return False, None 13 | 14 | 15 | def ui_endpoint_wrapper_auth(func) -> Callable: 16 | def wrapper(request, *args, **kwargs): 17 | del args 18 | del kwargs 19 | 20 | bad, deny = deny_request(request) 21 | if bad: 22 | return deny 23 | 24 | return func(request) 25 | 26 | return wrapper 27 | 28 | 29 | def ui_endpoint_wrapper(func) -> Callable: 30 | def wrapper(request, *args, **kwargs): 31 | del args 32 | del kwargs 33 | 34 | bad, deny = deny_request(request) 35 | if bad: 36 | return deny 37 | 38 | if not request.user.is_authenticated: 39 | return redirect(LOGIN_PATH) 40 | 41 | return func(request) 42 | 43 | return wrapper 44 | 45 | 46 | def ui_endpoint_wrapper_kwargs(func) -> Callable: 47 | def wrapper(request, *args, **kwargs): 48 | del args 49 | 50 | bad, deny = deny_request(request) 51 | if bad: 52 | return deny 53 | 54 | if not request.user.is_authenticated: 55 | return redirect(LOGIN_PATH) 56 | 57 | return func(request, **kwargs) 58 | 59 | return wrapper 60 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/utils/subps.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | from pathlib import Path 3 | from os import environ 4 | from functools import cache 5 | 6 | from aw.settings import BASE_DIR 7 | from aw.utils.debug import log 8 | from aw.config.environment import AW_ENV_VARS_SECRET, AW_ENV_VARS 9 | 10 | 11 | # pylint: disable=R0914 12 | def process( 13 | cmd: (str, list), timeout_sec: int = None, shell: bool = False, 14 | cwd: Path = BASE_DIR, env: dict = None, 15 | ) -> dict: 16 | cmd_str = cmd 17 | if isinstance(cmd, list): 18 | cmd_str = ' '.join(cmd) 19 | 20 | log(msg=f"Executing command: '{cmd_str}'", level=6) 21 | 22 | # merge provided env with current env and hide secrets 23 | env_full = environ.copy() 24 | if env is not None: 25 | env_full = {**env_full, **env} 26 | 27 | for secret_var in AW_ENV_VARS_SECRET: 28 | for secret_env_var in AW_ENV_VARS[secret_var]: 29 | if secret_env_var in env_full: 30 | env_full.pop(secret_env_var) 31 | 32 | try: 33 | with subprocess.Popen( 34 | cmd, 35 | shell=shell, 36 | stdout=subprocess.PIPE, 37 | stderr=subprocess.PIPE, 38 | cwd=cwd, 39 | env=env_full, 40 | ) as p: 41 | b_stdout, b_stderr = p.communicate(timeout=timeout_sec) 42 | stdout, stderr, rc = b_stdout.decode('utf-8').strip(), b_stderr.decode('utf-8').strip(), p.returncode 43 | 44 | except (subprocess.TimeoutExpired, subprocess.SubprocessError, subprocess.CalledProcessError, 45 | OSError, IOError) as error: 46 | stdout, stderr, rc = None, str(error), 1 47 | 48 | return { 49 | 'stdout': stdout, 50 | 'stderr': stderr, 51 | 'rc': rc, 52 | } 53 | 54 | 55 | @cache 56 | def process_cache( 57 | cmd: str, timeout_sec: int = None, shell: bool = False, 58 | cwd: Path = BASE_DIR, env: dict = None, 59 | ) -> dict: 60 | # read-only commands which results can be cached 61 | return process(cmd=cmd.split(' '), timeout_sec=timeout_sec, shell=shell, cwd=cwd, env=env) 62 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/utils/util_no_config.py: -------------------------------------------------------------------------------- 1 | from os import environ 2 | from time import tzset 3 | 4 | 5 | def set_timezone(timezone: str): 6 | environ['TZ'] = timezone 7 | environ.setdefault('TZ', timezone) 8 | tzset() 9 | 10 | 11 | def is_null(data) -> bool: 12 | if data is None: 13 | return True 14 | 15 | return str(data).strip() == '' 16 | 17 | 18 | def is_set(data) -> bool: 19 | return not is_null(data) 20 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/utils/util_test.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | def test_dummy(): 4 | pass 5 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansibleguy/webui/933b57a51f90c42a7679868e3b33ccc73f5def05/src/ansibleguy-webui/aw/views/__init__.py -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/views/base.py: -------------------------------------------------------------------------------- 1 | from aw.model.job import Job 2 | from aw.model.job_credential import JobGlobalCredentials 3 | from aw.base import USERS, GROUPS 4 | from aw.model.repository import Repository 5 | 6 | 7 | def choices_job() -> list[tuple]: 8 | # todo: only show jobs the user is privileged to view => get_viewable_jobs(user) 9 | return [(job.id, job.name) for job in Job.objects.all()] 10 | 11 | 12 | def choices_global_credentials() -> list[tuple]: 13 | # todo: only show credentials the user is privileged to view => get_viewable_credentials(user) 14 | return [(credentials.id, credentials.name) for credentials in JobGlobalCredentials.objects.all()] 15 | 16 | 17 | def choices_repositories() -> list[tuple]: 18 | # todo: only show credentials the user is privileged to view => get_viewable_credentials(user) 19 | return [(repo.id, repo.name) for repo in Repository.objects.all()] 20 | 21 | 22 | def choices_user() -> list[tuple]: 23 | return [(user.id, user.username) for user in USERS.objects.all()] 24 | 25 | 26 | def choices_group() -> list[tuple]: 27 | return [(group.id, group.name) for group in GROUPS.objects.all()] 28 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/views/forms/auth.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import urljoin 2 | 3 | from django.shortcuts import redirect, render, HttpResponse 4 | from django_saml2_auth.user import create_jwt_token 5 | 6 | from aw.utils.http import ui_endpoint_wrapper_auth 7 | from aw.settings import SAML2_AUTH, LOGIN_PATH, LOGIN_REDIRECT_URL 8 | 9 | 10 | # SP-initiated SAML SSO; see: https://github.com/grafana/django-saml2-auth/issues/105 11 | @ui_endpoint_wrapper_auth 12 | def saml_sp_initiated_login(request) -> HttpResponse: 13 | if request.user.is_authenticated: 14 | return redirect(LOGIN_REDIRECT_URL) 15 | 16 | return render(request, status=200, template_name='registration/saml.html') 17 | 18 | 19 | @ui_endpoint_wrapper_auth 20 | def saml_sp_initiated_login_init(request) -> HttpResponse: 21 | if request.user.is_authenticated: 22 | return redirect(LOGIN_REDIRECT_URL) 23 | 24 | if request.method != 'POST' or 'username' not in request.POST: 25 | return redirect(f"{LOGIN_PATH}?error=Required 'username' was not provided!") 26 | 27 | token = create_jwt_token(request.POST['username']) 28 | assertion_url = SAML2_AUTH['ASSERTION_URL'] 29 | sso_init_url = urljoin(assertion_url, f'a/saml/sp/?token={token}') 30 | return redirect(sso_init_url) 31 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/views/forms/system.py: -------------------------------------------------------------------------------- 1 | from pytz import all_timezones 2 | from django import forms 3 | from django.shortcuts import HttpResponse 4 | from django.shortcuts import render 5 | from django.contrib.auth.decorators import login_required 6 | 7 | from aw.config.main import config 8 | from aw.config.defaults import CONFIG_DEFAULTS 9 | from aw.utils.http import ui_endpoint_wrapper 10 | from aw.config.form_metadata import FORM_LABEL, FORM_HELP 11 | from aw.config.environment import AW_ENV_VARS, AW_ENV_VARS_SECRET 12 | from aw.model.system import SystemConfig, get_config_from_db 13 | from aw.utils.deployment import deployment_dev 14 | from aw.model.base import CHOICES_BOOL 15 | 16 | 17 | class SystemConfigForm(forms.ModelForm): 18 | class Meta: 19 | model = SystemConfig 20 | fields = SystemConfig.form_fields 21 | field_order = SystemConfig.form_fields 22 | labels = FORM_LABEL['system']['config'] 23 | help_texts = FORM_HELP['system']['config'] 24 | 25 | path_run = forms.CharField( 26 | max_length=500, initial=CONFIG_DEFAULTS['path_run'], required=True, 27 | label=Meta.labels['path_run'], 28 | ) 29 | path_play = forms.CharField( 30 | max_length=500, initial=CONFIG_DEFAULTS['path_play'], required=True, 31 | label=Meta.labels['path_play'], 32 | ) 33 | path_log = forms.CharField( 34 | max_length=500, initial=CONFIG_DEFAULTS['path_log'], required=True, 35 | label=Meta.labels['path_log'], 36 | ) 37 | path_ansible_config = forms.CharField( 38 | max_length=500, initial=CONFIG_DEFAULTS['path_ansible_config'], required=False, 39 | label=Meta.labels['path_ansible_config'], 40 | ) 41 | path_ssh_known_hosts = forms.CharField( 42 | max_length=500, initial=CONFIG_DEFAULTS['path_ssh_known_hosts'], required=False, 43 | label=Meta.labels['path_ssh_known_hosts'], 44 | ) 45 | timezone = forms.ChoiceField( 46 | required=False, 47 | widget=forms.Select, 48 | choices=[(tz, tz) for tz in sorted(all_timezones)], 49 | label=Meta.labels['timezone'], 50 | ) 51 | debug = forms.ChoiceField( 52 | initial=CONFIG_DEFAULTS['debug'] or deployment_dev(), choices=CHOICES_BOOL, 53 | ) 54 | mail_pass = forms.CharField( 55 | max_length=100, required=False, label=Meta.labels['mail_pass'], 56 | ) 57 | 58 | 59 | @login_required 60 | @ui_endpoint_wrapper 61 | def system_config(request) -> HttpResponse: 62 | config_form = SystemConfigForm() 63 | form_method = 'put' 64 | form_api = 'config' 65 | 66 | existing = {key: config[key] for key in SystemConfig.form_fields} 67 | existing['_enc_mail_pass'] = get_config_from_db()._enc_mail_pass 68 | config_form_html = config_form.render( 69 | template_name='forms/snippet.html', 70 | context={'form': config_form, 'existing': existing}, 71 | ) 72 | return render( 73 | request, status=200, template_name='system/config.html', 74 | context={ 75 | 'form': config_form_html, 'form_api': form_api, 'form_method': form_method, 76 | 'env_vars': AW_ENV_VARS, 'env_labels': FORM_LABEL['system']['config'], 77 | 'env_vars_secret': AW_ENV_VARS_SECRET, 78 | 'env_vars_config': {key: config[key] for key in AW_ENV_VARS}, 79 | } 80 | ) 81 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/views/main.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.decorators import login_required 2 | from django.shortcuts import redirect, render 3 | from django.contrib.auth.views import logout_then_login 4 | from django.shortcuts import HttpResponse 5 | from django.urls import path, re_path 6 | 7 | from aw.config.hardcoded import LOGIN_PATH 8 | from aw.settings import LOGIN_REDIRECT_URL 9 | from aw.utils.http import ui_endpoint_wrapper 10 | from aw.views.settings import urlpatterns_settings 11 | from aw.views.job import urlpatterns_jobs 12 | from aw.views.system import urlpatterns_system 13 | 14 | 15 | def _local_iframe(_path: str, title: str) -> str: 16 | return f'' 17 | 18 | 19 | @login_required 20 | @ui_endpoint_wrapper 21 | def admin(request) -> HttpResponse: 22 | return render(request, status=200, template_name='fallback.html', context={ 23 | 'content': _local_iframe('/_admin/', title='Admin') 24 | }) 25 | 26 | 27 | @login_required 28 | @ui_endpoint_wrapper 29 | def api_docs(request) -> HttpResponse: 30 | return render(request, status=200, template_name='fallback.html', context={ 31 | 'content': _local_iframe('/api/_docs', title='API Docs') 32 | }) 33 | 34 | 35 | @login_required 36 | @ui_endpoint_wrapper 37 | def not_implemented(request) -> HttpResponse: 38 | return render(request, status=404, template_name='fallback.html', context={'content': 'Not yet implemented'}) 39 | 40 | 41 | @ui_endpoint_wrapper 42 | def catchall(request) -> HttpResponse: 43 | if request.user.is_authenticated: 44 | return redirect(LOGIN_REDIRECT_URL) 45 | 46 | return redirect(LOGIN_PATH) # will be done by endpoint_wrapper 47 | 48 | 49 | @login_required 50 | @ui_endpoint_wrapper 51 | def logout(request) -> HttpResponse: 52 | return logout_then_login(request) 53 | 54 | 55 | urlpatterns_ui = [ 56 | path('ui/system/admin/', admin), 57 | path('ui/system/api_docs', api_docs), 58 | ] 59 | urlpatterns_ui += urlpatterns_jobs 60 | urlpatterns_ui += urlpatterns_settings 61 | urlpatterns_ui += urlpatterns_system 62 | urlpatterns_ui += [ 63 | path('ui/', not_implemented), 64 | re_path(r'^ui/*', not_implemented), 65 | ] 66 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/views/settings.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from django.shortcuts import HttpResponse, render 3 | from django.contrib.auth.decorators import login_required 4 | 5 | from aw.utils.http import ui_endpoint_wrapper 6 | from aw.views.forms.settings import setting_permission_edit, setting_alert_plugin_edit, setting_alert_user_edit, \ 7 | setting_alert_global_edit, setting_alert_group_edit 8 | 9 | 10 | @login_required 11 | @ui_endpoint_wrapper 12 | def setting_api_key(request) -> HttpResponse: 13 | return render(request, status=200, template_name='settings/api_key.html', context={'show_update_time': True}) 14 | 15 | 16 | @login_required 17 | @ui_endpoint_wrapper 18 | def setting_permission(request) -> HttpResponse: 19 | return render(request, status=200, template_name='settings/permission.html', context={'show_update_time': True}) 20 | 21 | 22 | @login_required 23 | @ui_endpoint_wrapper 24 | def setting_alert(request) -> HttpResponse: 25 | return render(request, status=200, template_name='settings/alert.html', context={'show_update_time': True}) 26 | 27 | 28 | urlpatterns_settings = [ 29 | path('ui/settings/api_keys', setting_api_key), 30 | path('ui/settings/permissions/', setting_permission_edit), 31 | path('ui/settings/permissions', setting_permission), 32 | path('ui/settings/alerts/plugin/', setting_alert_plugin_edit), 33 | path('ui/settings/alerts/user/', setting_alert_user_edit), 34 | path('ui/settings/alerts/group/', setting_alert_group_edit), 35 | path('ui/settings/alerts/global/', setting_alert_global_edit), 36 | path('ui/settings/alerts', setting_alert), 37 | ] 38 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/aw/views/validation.py: -------------------------------------------------------------------------------- 1 | AW_VALIDATIONS = { 2 | 'file_system_browse': ['inventory_file', 'playbook_file'], 3 | 'file_system_exists': [ 4 | 'vault_file', 'static_path', 'path_run', 'path_play', 'path_log', 'path_ansible_config', 5 | 'path_ssh_known_hosts', 6 | ], 7 | } 8 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from sys import exit as sys_exit 4 | from sys import argv as sys_argv 5 | from sys import path as sys_path 6 | from os import path as os_path 7 | from os import geteuid, stat, listdir 8 | from argparse import ArgumentParser 9 | from json import dumps as json_dumps 10 | from pathlib import Path 11 | 12 | # pylint: disable=C0415 13 | 14 | 15 | def _api_key(username: str): 16 | # python3 -m ansibleguy-webui.cli api-key.create 17 | from aw.base import USERS 18 | from aw.model.api import AwAPIKey 19 | from aw.utils.util import datetime_w_tz 20 | from aw.config.hardcoded import KEY_TIME_FORMAT 21 | 22 | user = USERS.objects.get(username=username) 23 | token = f'{user}-{datetime_w_tz().strftime(KEY_TIME_FORMAT)}' 24 | _, key = AwAPIKey.objects.create_key(name=token, user=user) 25 | print(f'API Key created:\nToken={token}\nKey={key}') 26 | sys_exit(0) 27 | 28 | 29 | def _print_version(): 30 | # python3 -m ansibleguy-webui.cli --version 31 | from aw.utils.version import get_version, get_system_versions 32 | 33 | print(f'Version: {get_version()}\n{json_dumps(get_system_versions(), indent=4)}') 34 | sys_exit(0) 35 | 36 | 37 | def _list_migrations(module_base: str): 38 | print('Migrations') 39 | migrations = listdir(Path(module_base) / 'aw' / 'migrations') 40 | migrations.sort() 41 | for mig in migrations: 42 | if mig.find('_v') != -1: 43 | version = mig.split('_', 1)[1][1:-3].replace('_', '.') 44 | print(f" Version: {version}: {mig}") 45 | 46 | sys_exit(0) 47 | 48 | 49 | def main(): 50 | this_file = os_path.abspath(__file__) 51 | file_owner_uid = stat(this_file).st_uid 52 | if geteuid() not in [0, file_owner_uid]: 53 | print('ERROR: Only root and the code-owner are permitted to run this script!') 54 | sys_exit(1) 55 | 56 | module_base = os_path.dirname(this_file) 57 | # pylint: disable=E0401,C0415 58 | try: 59 | from cli_init import init_cli 60 | 61 | except ModuleNotFoundError: 62 | sys_path.append(module_base) 63 | from cli_init import init_cli 64 | 65 | init_cli() 66 | 67 | if len(sys_argv) > 1: 68 | if sys_argv[1] in ['-v', '--version']: 69 | _print_version() 70 | 71 | parser = ArgumentParser() 72 | parser.add_argument( 73 | '-a', '--action', type=str, required=True, 74 | choices=['api-key.create', 'migrations.list'], 75 | ) 76 | parser.add_argument('-p', '--parameter', type=str, required=False) 77 | args = parser.parse_args() 78 | 79 | if args.action == 'api-key.create' and args.parameter is not None: 80 | _api_key(username=args.parameter) 81 | 82 | if args.action == 'migrations.list': 83 | _list_migrations(module_base) 84 | 85 | 86 | if __name__ == '__main__': 87 | main() 88 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/cli_init.py: -------------------------------------------------------------------------------- 1 | from os import environ 2 | from os import path as os_path 3 | from sys import path as sys_path 4 | from importlib.metadata import version, PackageNotFoundError 5 | 6 | from django import setup as django_setup 7 | 8 | 9 | def init_cli(): 10 | environ.setdefault('AW_INIT', '1') 11 | # workaround for CI 12 | if 'AW_VERSION' not in environ: 13 | try: 14 | environ['AW_VERSION'] = version('ansible-webui') 15 | 16 | except PackageNotFoundError: 17 | environ['AW_VERSION'] = '0.0.0' 18 | 19 | # pylint: disable=E0401,C0415 20 | try: 21 | from aw.config.main import init_config 22 | 23 | except ModuleNotFoundError: 24 | sys_path.append(os_path.dirname(os_path.abspath(__file__))) 25 | from aw.config.main import init_config 26 | 27 | init_config() 28 | from aw.utils.debug import warn_if_development 29 | warn_if_development() 30 | 31 | django_setup() 32 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/handle_signals.py: -------------------------------------------------------------------------------- 1 | import signal 2 | from os import environ 3 | from os import kill as os_kill 4 | from sys import exit as sys_exit 5 | from time import sleep 6 | 7 | from gunicorn.arbiter import Arbiter 8 | from django.db.utils import IntegrityError, OperationalError 9 | from django.core.exceptions import ImproperlyConfigured, AppRegistryNotReady 10 | 11 | from aw.utils.debug import log 12 | 13 | 14 | def handle_signals(scheduler): 15 | # override gunicorn signal handling to allow for graceful shutdown 16 | Arbiter.SIGNALS.remove(signal.SIGHUP) 17 | Arbiter.SIGNALS.remove(signal.SIGINT) 18 | Arbiter.SIGNALS.remove(signal.SIGTERM) 19 | 20 | def signal_exit(signum=None, stack=None): 21 | del signum, stack 22 | scheduler.stop() 23 | 24 | log('Stopping webserver..') 25 | os_kill(int(environ['MAINPID']), signal.SIGQUIT) # trigger 'Arbiter.stop' 26 | sleep(3) 27 | 28 | try: 29 | # pylint: disable=C0415 30 | log('Closing database..') 31 | from django.db import connections 32 | connections.close_all() 33 | 34 | except (IntegrityError, OperationalError, ImproperlyConfigured, AppRegistryNotReady, ImportError): 35 | pass 36 | 37 | log('Gracefully stopped - Goodbye!') 38 | sys_exit(0) 39 | 40 | def signal_reload(signum=None, stack=None): 41 | del stack 42 | scheduler.reload(signum) 43 | 44 | signal.signal(signal.SIGHUP, signal_reload) 45 | signal.signal(signal.SIGINT, signal_exit) 46 | signal.signal(signal.SIGTERM, signal_exit) 47 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from sys import argv as sys_argv 4 | from sys import path as sys_path 5 | from os import path as os_path 6 | 7 | # pylint: disable=C0415 8 | 9 | 10 | def main(): 11 | # pylint: disable=E0401,C0415 12 | try: 13 | from cli_init import init_cli 14 | 15 | except ModuleNotFoundError: 16 | sys_path.append(os_path.dirname(os_path.abspath(__file__))) 17 | from cli_init import init_cli 18 | 19 | init_cli() 20 | from django.core.management import execute_from_command_line 21 | execute_from_command_line(sys_argv) 22 | 23 | 24 | if __name__ == '__main__': 25 | main() 26 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/web_serve_static.py: -------------------------------------------------------------------------------- 1 | """ 2 | used to serve static files if no proxy is in use 3 | source: django.contrib.staticfiles.views(serve) 4 | 5 | can be switched off by setting the environmental variable 'AW_STATIC' 6 | """ 7 | from posixpath import normpath 8 | from os import path as os_path 9 | from re import escape as regex_escape 10 | 11 | from django.contrib.staticfiles import finders 12 | from django.http import Http404 13 | from django.views import static 14 | from django.conf import settings 15 | from django.urls import re_path 16 | 17 | 18 | def serve(request, path, **kwargs): 19 | normalized_path = normpath(path).lstrip('/') 20 | absolute_path = finders.find(normalized_path) 21 | 22 | if not absolute_path: 23 | if path.endswith("/") or path == "": 24 | raise Http404('Directory indexes are not allowed here.') 25 | raise Http404(f"'{path}' could not be found") 26 | 27 | document_root, path = os_path.split(absolute_path) 28 | return static.serve(request, path, document_root=document_root, **kwargs) 29 | 30 | 31 | # pylint: disable=C0209 32 | urlpatterns_static = [re_path( 33 | r"^%s(?P.*)$" % regex_escape(settings.STATIC_URL.lstrip('/')), serve, 34 | )] 35 | -------------------------------------------------------------------------------- /src/ansibleguy-webui/webserver.py: -------------------------------------------------------------------------------- 1 | from multiprocessing import cpu_count 2 | from string import ascii_letters 3 | from random import choice as random_choice 4 | from pathlib import Path 5 | from ssl import PROTOCOL_TLSv1_2 6 | 7 | import gunicorn 8 | from gunicorn.app.wsgiapp import WSGIApplication 9 | 10 | from aw.utils.deployment import deployment_dev, deployment_docker 11 | from aw.utils.debug import log, warn_if_development 12 | from aw.config.environment import get_aw_env_var_or_default 13 | 14 | PORT_WEB = get_aw_env_var_or_default('port') 15 | LISTEN_ADDRESS = get_aw_env_var_or_default('address') 16 | 17 | # https://docs.gunicorn.org/en/stable/settings.html 18 | OPTIONS_DEV = { 19 | 'reload': True, 20 | 'loglevel': 'info', 21 | 'workers': 2, 22 | } 23 | OPTIONS_PROD = { 24 | 'bind': f'{LISTEN_ADDRESS}:{PORT_WEB}', 25 | 'reload': False, 26 | 'loglevel': 'warning', 27 | } 28 | 29 | if deployment_docker(): 30 | OPTIONS_PROD['bind'] = f'0.0.0.0:{PORT_WEB}' 31 | 32 | 33 | class StandaloneApplication(WSGIApplication): 34 | def __init__(self, app_uri, options=None): 35 | self.options = options or {} 36 | self.app_uri = app_uri 37 | super().__init__() 38 | 39 | def load_config(self): 40 | config = { 41 | key: value 42 | for key, value in self.options.items() 43 | if key in self.cfg.settings and value is not None 44 | } 45 | for key, value in config.items(): 46 | self.cfg.set(key.lower(), value) 47 | 48 | 49 | def init_webserver(): 50 | gunicorn.SERVER = ''.join(random_choice(ascii_letters) for _ in range(10)) 51 | opts = { 52 | 'workers': (cpu_count() * 2) + 1, 53 | **OPTIONS_PROD 54 | } 55 | if deployment_dev(): 56 | warn_if_development() 57 | opts = {**opts, **OPTIONS_DEV} 58 | 59 | scheme = 'http' 60 | ssl_cert = get_aw_env_var_or_default('ssl_file_crt') 61 | ssl_key = get_aw_env_var_or_default('ssl_file_key') 62 | if ssl_cert is not None and ssl_key is not None: 63 | if not Path(ssl_cert).is_file() or not Path(ssl_key).is_file(): 64 | log( 65 | msg=f"Either SSL certificate or SSL key is not readable: {ssl_cert}, {ssl_key}", 66 | level=1, 67 | ) 68 | 69 | else: 70 | opts = { 71 | **opts, 72 | 'keyfile': ssl_key, 73 | 'certfile': ssl_cert, 74 | 'ssl_version': PROTOCOL_TLSv1_2, 75 | 'do_handshake_on_connect': True, 76 | } 77 | scheme = 'https' 78 | 79 | log(msg=f"Listening on {scheme}://{opts['bind']}", level=5) 80 | 81 | StandaloneApplication( 82 | app_uri="aw.main:app", 83 | options=opts 84 | ).run() 85 | -------------------------------------------------------------------------------- /test/ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | localhost_warning=False 3 | -------------------------------------------------------------------------------- /test/demo/play1.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - hosts: localhost 4 | become: false 5 | gather_facts: false 6 | tasks: 7 | - name: Listing files in directory 8 | ansible.builtin.command: 'ls -l /play' 9 | register: job1 10 | 11 | - name: Show response 12 | ansible.builtin.debug: 13 | var: job1.stdout_lines 14 | 15 | - name: Add config file 16 | ansible.builtin.copy: 17 | content: | 18 | # ansible_managed 19 | # this is some config.. 20 | key={{ job_value | default('no') }} 21 | dest: '/tmp/awtest.txt' 22 | mode: 0640 23 | 24 | - name: Wait some time 25 | ansible.builtin.pause: 26 | seconds: 30 27 | 28 | - name: Remove file 29 | ansible.builtin.file: 30 | path: '/tmp/awtest.txt' 31 | state: absent 32 | 33 | 34 | - name: Output some information 35 | ansible.builtin.debug: 36 | msg: 'Ansible-WebUI DEMO' 37 | -------------------------------------------------------------------------------- /test/demo/reset.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | AW_ADMIN='USER' 6 | AW_ADMIN_PWD='PWD' 7 | DIR_DATA='/var/local/ansible-webui/' 8 | DIR_PLAY="${DIR_DATA}play/" 9 | DIR_LOG="${DIR_DATA}log/" 10 | AW_USER='ansible-webui' 11 | IMAGE='ansible0guy/webui-unprivileged:latest' 12 | 13 | # useradd $AW_USER --shell /usr/sbin/nologin --uid 8785 --user-group 14 | 15 | echo '### REMOVING EXISTING ###' 16 | if docker ps -a | grep -q ansible-webui 17 | then 18 | docker stop ansible-webui 19 | docker rm ansible-webui 20 | fi 21 | 22 | echo '### CLEANUP ###' 23 | if [ -f /var/local/ansible-webui/aw.db ] 24 | then 25 | BAK_DIR="/var/local/ansible-webui/$(date +%s)" 26 | mkdir "$BAK_DIR" 27 | mv /var/local/ansible-webui/aw.db "${BAK_DIR}/aw.db" 28 | fi 29 | 30 | cp "${DIR_DATA}/aw.db.bak" "${DIR_DATA}/aw.db" 31 | chown "$AW_USER" "$DIR_DATA" "${DIR_DATA}/aw.db" 32 | chown -R "$AW_USER" "$DIR_LOG" 33 | chown -R root:"$AW_USER" "$DIR_PLAY" 34 | 35 | # rm -f /var/local/ansible-webui/log/* 36 | 37 | echo '### UPDATING ###' 38 | docker pull "$IMAGE" 39 | 40 | echo '### STARTING ###' 41 | docker run -d --restart unless-stopped --name ansible-webui --publish 8000:8000 --volume "$DIR_DATA":/data --volume "$DIR_PLAY":/play --env AW_ADMIN_PWD="$AW_ADMIN_PWD" --env AW_ADMIN="$AW_ADMIN" --env AW_HOSTNAMES=demo.webui.ansibleguy.net --env AW_PROXY=1 "$IMAGE" 42 | -------------------------------------------------------------------------------- /test/demo/web-service.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - hosts: localhost 4 | become: false 5 | gather_facts: false 6 | tasks: 7 | - name: Listing files in directory 8 | ansible.builtin.command: 'ls -l /play' 9 | register: job1 10 | 11 | - name: Show response 12 | ansible.builtin.debug: 13 | var: job1.stdout_lines 14 | 15 | - name: Add config file 16 | ansible.builtin.copy: 17 | content: | 18 | # ansible_managed 19 | # this is some config.. 20 | key={{ job_value | default('no') }} 21 | dest: '/tmp/awtest.txt' 22 | mode: 0640 23 | 24 | - name: Wait some time 25 | ansible.builtin.pause: 26 | seconds: 5 27 | 28 | - name: Remove file 29 | ansible.builtin.file: 30 | path: '/tmp/awtest.txt' 31 | state: absent 32 | 33 | - name: "Showing randomness ({{ item }}/10)" 34 | ansible.builtin.pause: 35 | prompt: "{{ lookup('password', '/dev/null chars=ascii_letters,digit length=20') }}" 36 | seconds: 1 37 | loop: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 38 | 39 | - name: Output some information 40 | ansible.builtin.debug: 41 | msg: 'Ansible-WebUI DEMO' 42 | -------------------------------------------------------------------------------- /test/integration/auth/saml.py: -------------------------------------------------------------------------------- 1 | from os import environ 2 | from time import sleep 3 | 4 | from seleniumwire import webdriver 5 | from selenium.webdriver.common.by import By 6 | from selenium.webdriver.common.keys import Keys 7 | import chromedriver_autoinstaller 8 | 9 | # pylint: disable=R0801 10 | 11 | BASE_URL = 'http://127.0.0.1:8000' 12 | options = webdriver.ChromeOptions() 13 | options.add_argument('--headless') 14 | options.add_argument('--disable-extensions') 15 | options.add_argument('--remote-debugging-port=9222') 16 | chromedriver_autoinstaller.install() 17 | DRIVER = webdriver.Chrome(options=options) 18 | 19 | 20 | def _response_code(url: str) -> (int, None): 21 | for request in DRIVER.requests: 22 | if request.response and request.url == url: 23 | return request.response.status_code 24 | 25 | return None 26 | 27 | 28 | def _response_ok(url: str) -> bool: 29 | return _response_code(url) in [200, 302] 30 | 31 | 32 | def login_fallback(user: str, pwd: str): 33 | print('TESTING FALLBACK-LOGIN') 34 | login_url = f'{BASE_URL}/a/login/fallback/' 35 | DRIVER.get(login_url) 36 | DRIVER.find_element(By.ID, 'id_username').send_keys(user) 37 | DRIVER.find_element(By.ID, 'id_password').send_keys(pwd) 38 | DRIVER.find_element(By.ID, 'id_password').send_keys(Keys.RETURN) 39 | assert _response_ok(login_url) 40 | 41 | login_redirect = f'{BASE_URL}/ui/jobs/manage' 42 | assert DRIVER.current_url == login_redirect 43 | assert _response_ok(login_redirect) 44 | 45 | 46 | def test_get_locations(locations: list): 47 | for location in locations: 48 | print(f'TESTING GET {location}') 49 | url = f'{BASE_URL}/{location}' 50 | sleep(0.1) 51 | DRIVER.get(url) 52 | assert _response_ok(url) 53 | 54 | 55 | def test_auth_pages(): 56 | test_get_locations([ 57 | 'a/login/', 'a/login/fallback/', 58 | ]) 59 | 60 | 61 | def test_fallback_main_pages(): 62 | # not all.. but some to make sure the fallback-auth is working 63 | test_get_locations([ 64 | 'ui/jobs/manage', 'ui/jobs/log', 'ui/system/config', 'a/password_change/', 65 | ]) 66 | 67 | 68 | def main(): 69 | try: 70 | test_auth_pages() 71 | login_fallback(user=environ['AW_ADMIN'], pwd=environ['AW_ADMIN_PWD']) 72 | test_fallback_main_pages() 73 | 74 | finally: 75 | DRIVER.quit() 76 | 77 | 78 | if __name__ == '__main__': 79 | main() 80 | -------------------------------------------------------------------------------- /test/integration/auth/saml.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | # test it using: https://mocksaml.com/; SP-initiated not testable 4 | 5 | # RUN: 6 | # AW_AUTH=saml AW_SAML_CONFIG=test/integration/auth/saml.yml bash scripts/run_dev.sh q 7 | 8 | AUTH: 'saml' 9 | SAML: 10 | METADATA_AUTO_CONF_URL: 'https://mocksaml.com/api/saml/metadata' 11 | ASSERTION_URL: 'http://localhost:8000' 12 | ENTITY_ID: 'http://localhost:8000/a/saml/acs/' 13 | DEFAULT_NEXT_URL: 'http://localhost:8000/' 14 | 15 | CREATE_USER: true 16 | NEW_USER_PROFILE: 17 | USER_GROUPS: [] 18 | ACTIVE_STATUS: true 19 | STAFF_STATUS: true 20 | SUPERUSER_STATUS: false 21 | 22 | ATTRIBUTES_MAP: 23 | email: 'email' 24 | username: 'email' 25 | token: 'id' 26 | first_name: 'firstName' 27 | last_name: 'lastName' 28 | 29 | DEBUG: true 30 | LOGGING: 31 | version: 1 32 | formatters: 33 | simple: 34 | format: '[%(asctime)s] [%(levelname)s] [%(name)s.%(funcName)s] %(message)s' 35 | handlers: 36 | stdout: 37 | class: 'logging.StreamHandler' 38 | stream: 'ext://sys.stdout' 39 | level: 'DEBUG' 40 | formatter: 'simple' 41 | loggers: 42 | saml2: 43 | level: 'DEBUG' 44 | root: 45 | level: 'DEBUG' 46 | handlers: ['stdout'] 47 | -------------------------------------------------------------------------------- /test/integration/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | admin: 'test' 4 | admin_pwd: 'myPwd' 5 | secret: 'adslfksldjflsmelnfslefnlsneflksneflksneflksneklf' 6 | 7 | run_timeout: 600 8 | path_run: '/tmp/test1' 9 | db: '/tmp/test1.aw.db' 10 | -------------------------------------------------------------------------------- /test/integration/webui/main.py: -------------------------------------------------------------------------------- 1 | from os import environ 2 | from time import sleep 3 | 4 | from seleniumwire import webdriver 5 | from selenium.webdriver.common.by import By 6 | from selenium.webdriver.common.keys import Keys 7 | import chromedriver_autoinstaller 8 | 9 | # pylint: disable=R0801 10 | 11 | BASE_URL = 'http://127.0.0.1:8000' 12 | options = webdriver.ChromeOptions() 13 | options.add_argument('--headless') 14 | options.add_argument('--disable-extensions') 15 | options.add_argument('--remote-debugging-port=9222') 16 | chromedriver_autoinstaller.install() 17 | DRIVER = webdriver.Chrome(options=options) 18 | 19 | 20 | def _response_code(url: str) -> (int, None): 21 | for request in DRIVER.requests: 22 | if request.response and request.url == url: 23 | return request.response.status_code 24 | 25 | return None 26 | 27 | 28 | def _response_ok(url: str) -> bool: 29 | return _response_code(url) in [200, 302] 30 | 31 | 32 | def login(user: str, pwd: str): 33 | print('TESTING LOGIN') 34 | login_url = f'{BASE_URL}/a/login/' 35 | DRIVER.get(login_url) 36 | DRIVER.find_element(By.ID, 'id_username').send_keys(user) 37 | DRIVER.find_element(By.ID, 'id_password').send_keys(pwd) 38 | DRIVER.find_element(By.ID, 'id_password').send_keys(Keys.RETURN) 39 | assert _response_ok(login_url) 40 | 41 | login_redirect = f'{BASE_URL}/ui/jobs/manage' 42 | assert DRIVER.current_url == login_redirect 43 | assert _response_ok(login_redirect) 44 | 45 | 46 | def test_get_locations(locations: list): 47 | for location in locations: 48 | print(f'TESTING GET {location}') 49 | url = f'{BASE_URL}/{location}' 50 | sleep(0.1) 51 | DRIVER.get(url) 52 | assert _response_ok(url) 53 | 54 | 55 | def test_main_pages(): 56 | test_get_locations([ 57 | 'ui/jobs/manage', 'ui/jobs/log', 'ui/jobs/credentials', 'ui/jobs/repository', 58 | 'ui/settings/api_keys', 'ui/settings/permissions', 59 | 'ui/system/admin/', 'ui/system/api_docs', 'ui/system/environment', 'ui/system/config', 60 | 'a/password_change/', 'ui/settings/alerts', 61 | ]) 62 | 63 | 64 | def test_actions_views(): 65 | test_get_locations([ 66 | 'ui/jobs/manage/job', 67 | 'ui/jobs/credentials/0?global=false', 'ui/jobs/credentials/0?global=true', 68 | 'ui/settings/permissions/0', 'ui/jobs/repository/git/0', 'ui/jobs/repository/static/0', 69 | 'ui/settings/alerts/global/0', 'ui/settings/alerts/group/0', 'ui/settings/alerts/user/0', 70 | 'ui/settings/alerts/plugin/0', 71 | ]) 72 | 73 | 74 | def main(): 75 | try: 76 | login(user=environ['AW_ADMIN'], pwd=environ['AW_ADMIN_PWD']) 77 | test_main_pages() 78 | test_actions_views() 79 | # todo: add action post variants 80 | 81 | finally: 82 | DRIVER.quit() 83 | 84 | 85 | if __name__ == '__main__': 86 | main() 87 | -------------------------------------------------------------------------------- /test/inv/hosts.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | all: 4 | hosts: 5 | -------------------------------------------------------------------------------- /test/play1.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - hosts: localhost 4 | become: false 5 | gather_facts: false 6 | 7 | vars: 8 | test: 'run1' 9 | 10 | roles: 11 | - test1 12 | -------------------------------------------------------------------------------- /test/roles/test1/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | test1_var1: 'NOT SET' 4 | test1_var2: 5 | -------------------------------------------------------------------------------- /test/roles/test1/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: TEST 1 | Basic 4 | ansible.builtin.debug: 5 | msg: "TEST 1: '{{ test1_var1 }}'" 6 | when: test == 'run1' 7 | 8 | - name: TEST 1 | Checking environmental variable 9 | ansible.builtin.assert: 10 | that: 11 | - lookup('ansible.builtin.env', 'TEST1_VAR2') == 'run2' 12 | when: test == 'run2' 13 | 14 | - name: TEST 1 | Sleeping 15 | ansible.builtin.pause: 16 | seconds: 60 17 | when: test == 'run3' 18 | 19 | - name: TEST 1 | Fail 20 | ansible.builtin.fail: 21 | msg: 'Just failing' 22 | when: test == 'run4' 23 | --------------------------------------------------------------------------------