├── .coveragerc ├── .dockerignore ├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── question-or-other-issue.md ├── dependabot.yml └── workflows │ ├── codeql-analysis.yml │ ├── publish-docker.yml │ ├── publish-to-pypi.yml │ ├── security_scanning.yml │ └── tests.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── Dockerfile ├── Gemfile ├── HISTORY.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── RELEASE.md ├── Rakefile ├── docs ├── Debian-Jessie.md ├── Deployment-setups.md └── EL7.md ├── poetry.lock ├── puppetboard-s2i-template.yaml ├── puppetboard ├── __init__.py ├── app.py ├── core.py ├── default_settings.py ├── docker_settings.py ├── errors.py ├── forms.py ├── schedulers │ └── classes.py ├── static │ ├── css │ │ ├── fonts.css │ │ ├── puppetboard.css │ │ └── radiator.css │ ├── custom-natural-time-delta │ │ └── natural-time-delta.js │ ├── custom-natural │ │ └── natural.js │ ├── favicon.ico │ ├── favicon.svg │ ├── fonts │ │ ├── cousine │ │ │ ├── d6lIkaiiRdih4SpP_SAvzAbt.woff2 │ │ │ ├── d6lIkaiiRdih4SpP_SQvzA.woff2 │ │ │ ├── d6lIkaiiRdih4SpP_SYvzAbt.woff2 │ │ │ ├── d6lIkaiiRdih4SpP_ScvzAbt.woff2 │ │ │ ├── d6lIkaiiRdih4SpP_SgvzAbt.woff2 │ │ │ ├── d6lIkaiiRdih4SpP_SkvzAbt.woff2 │ │ │ ├── d6lIkaiiRdih4SpP_SovzAbt.woff2 │ │ │ └── d6lIkaiiRdih4SpP_SsvzAbt.woff2 │ │ ├── lato │ │ │ ├── S6u8w4BMUTPHjxsAUi-qJCY.woff2 │ │ │ ├── S6u8w4BMUTPHjxsAXC-q.woff2 │ │ │ ├── S6u9w4BMUTPHh6UVSwaPGR_p.woff2 │ │ │ ├── S6u9w4BMUTPHh6UVSwiPGQ.woff2 │ │ │ ├── S6u_w4BMUTPHjxsI5wq_FQft1dw.woff2 │ │ │ ├── S6u_w4BMUTPHjxsI5wq_Gwft.woff2 │ │ │ ├── S6uyw4BMUTPHjx4wXg.woff2 │ │ │ └── S6uyw4BMUTPHjxAwXjeu.woff2 │ │ └── open-sans │ │ │ ├── memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4gaVI.woff2 │ │ │ ├── memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4iaVIGxA.woff2 │ │ │ ├── memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4jaVIGxA.woff2 │ │ │ ├── memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4kaVIGxA.woff2 │ │ │ ├── memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4saVIGxA.woff2 │ │ │ ├── memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4taVIGxA.woff2 │ │ │ ├── memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4uaVIGxA.woff2 │ │ │ └── memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4vaVIGxA.woff2 │ ├── js │ │ ├── dailychart.js │ │ ├── pretty.js │ │ ├── radiator.js │ │ ├── scroll.top.js │ │ └── utils.js │ └── libs │ │ ├── billboard.js │ │ ├── billboard.min.css │ │ └── billboard.pkgd.min.js │ │ ├── datatables.net-buttons-se │ │ ├── buttons.semanticui.min.css │ │ └── buttons.semanticui.min.js │ │ ├── datatables.net-buttons │ │ ├── buttons.colVis.min.js │ │ ├── buttons.html5.min.js │ │ └── dataTables.buttons.min.js │ │ ├── datatables.net-se │ │ ├── dataTables.semanticui.min.css │ │ └── dataTables.semanticui.min.js │ │ ├── datatables.net │ │ └── jquery.dataTables.min.js │ │ ├── fomantic-ui │ │ ├── semantic.min.css │ │ ├── semantic.min.js │ │ └── themes │ │ │ └── default │ │ │ └── assets │ │ │ ├── fonts │ │ │ ├── brand-icons.eot │ │ │ ├── brand-icons.svg │ │ │ ├── brand-icons.ttf │ │ │ ├── brand-icons.woff │ │ │ ├── brand-icons.woff2 │ │ │ ├── icons.eot │ │ │ ├── icons.svg │ │ │ ├── icons.ttf │ │ │ ├── icons.woff │ │ │ ├── icons.woff2 │ │ │ ├── outline-icons.eot │ │ │ ├── outline-icons.svg │ │ │ ├── outline-icons.ttf │ │ │ ├── outline-icons.woff │ │ │ └── outline-icons.woff2 │ │ │ └── images │ │ │ └── flags.png │ │ ├── jquery │ │ └── jquery.min.js │ │ ├── jszip │ │ └── jszip.min.js │ │ └── moment.js │ │ └── moment-with-locales.min.js ├── templates │ ├── 400.html │ ├── 403.html │ ├── 404.html │ ├── 412.html │ ├── 500.html │ ├── _facts_sorter.html │ ├── _macros.html │ ├── _static_offline.html │ ├── _static_online.html │ ├── catalog.html │ ├── catalog_compare.html │ ├── catalogs.html │ ├── catalogs.json.tpl │ ├── class.html │ ├── class_resource.json.tpl │ ├── classes.html │ ├── classes.json.tpl │ ├── fact.html │ ├── facts.html │ ├── failures.html │ ├── index.html │ ├── inventory.html │ ├── inventory.json.tpl │ ├── layout.html │ ├── metric.html │ ├── metrics.html │ ├── node.html │ ├── nodes.html │ ├── query.html │ ├── radiator.html │ ├── report.html │ ├── reports.html │ └── reports.json.tpl ├── utils.py ├── version.py └── views │ ├── catalogs.py │ ├── classes.py │ ├── dailychart.py │ ├── facts.py │ ├── failures.py │ ├── index.py │ ├── inventory.py │ ├── metrics.py │ ├── nodes.py │ ├── query.py │ ├── radiator.py │ └── reports.py ├── pylintrc ├── pyproject.toml ├── pytest.ini ├── requirements-docker.txt ├── requirements-test.txt ├── requirements.txt ├── screenshots ├── class.png ├── classes.png ├── fact.png ├── fact_value.png ├── facts.png ├── failures.png ├── inventory.png ├── metric.png ├── metrics.png ├── node.png ├── nodes.png ├── overview.png ├── query_result_json.png ├── query_result_table.png ├── radiator.png └── report.png ├── settings.py.sample ├── setup.cfg ├── setup.py ├── test ├── __init__.py ├── conftest.py ├── data │ └── test_json_report_ok ├── test_app.py ├── test_app_error.py ├── test_core.py ├── test_docker_settings.py ├── test_form.py ├── test_utils.py └── views │ ├── __init__.py │ ├── test_catalogs.py │ ├── test_classes.py │ ├── test_dailychart.py │ ├── test_facts.py │ ├── test_index.py │ ├── test_inventory.py │ ├── test_metrics.py │ ├── test_nodes.py │ ├── test_query.py │ ├── test_radiator.py │ └── test_reports.py └── wsgi.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | exclude_lines = 3 | pragma: notest 4 | 5 | [run] 6 | source = 7 | puppetboard 8 | 9 | omit = 10 | # don't check the test themselves, obviously 11 | test/* 12 | 13 | # no point in checking these 14 | setup.py 15 | wsgi.py 16 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # roughly the same as in .gitignore 2 | .idea 3 | .mypy_cache 4 | .pytest_cache 5 | build 6 | dist 7 | puppetboard.egg-info 8 | venv 9 | 10 | # git-related files 11 | .git 12 | 13 | # we don't need these in the release too 14 | .github 15 | docs 16 | screenshots 17 | test 18 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | # Use 4 spaces for the Python files 13 | [*.py] 14 | indent_size = 4 15 | max_line_length = 80 16 | 17 | # The JSON files contain newlines inconsistently 18 | [*.json] 19 | insert_final_newline = ignore 20 | 21 | # Minified JavaScript files shouldn't be changed 22 | [**.min.js] 23 | indent_style = ignore 24 | insert_final_newline = ignore 25 | 26 | [*.md] 27 | trim_trailing_whitespace = false 28 | 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | 12 | A clear and concise description of what the bug is. 13 | 14 | **Puppetboard version** 15 | 16 | F.e. 4.0.1 17 | 18 | **Environment and installation method** 19 | 20 | F.e.: 21 | * Centos 7 and Python 3.8 from Software Collections, 22 | * installed with the Forge module v9.0.0 and using the PyPI package (default). 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | A clear and concise description of what you want to happen. 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question-or-other-issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question or other issue 3 | about: Non-bug and non-feature requests go here :) 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | Write whatever you like :) 11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "pip" 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "13:00" 8 | open-pull-requests-limit: 10 9 | groups: 10 | python: 11 | update-types: 12 | - "major" 13 | - "patch" 14 | - "minor" 15 | patterns: 16 | - "*" 17 | 18 | # Maintain dependencies for GitHub Actions 19 | - package-ecosystem: github-actions 20 | directory: "/" 21 | schedule: 22 | interval: daily 23 | time: "13:00" 24 | open-pull-requests-limit: 10 25 | 26 | - package-ecosystem: "docker" 27 | directory: "/" 28 | schedule: 29 | interval: "daily" 30 | time: "13:00" 31 | open-pull-requests-limit: 10 32 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "master" ] 17 | pull_request: 18 | branches: [ "master" ] 19 | schedule: 20 | - cron: '35 0 * * 5' 21 | 22 | jobs: 23 | analyze: 24 | name: Analyze 25 | # Runner size impacts CodeQL analysis time. To learn more, please see: 26 | # - https://gh.io/recommended-hardware-resources-for-running-codeql 27 | # - https://gh.io/supported-runners-and-hardware-resources 28 | # - https://gh.io/using-larger-runners 29 | # Consider using larger runners for possible analysis time improvements. 30 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} 31 | timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} 32 | permissions: 33 | # required for all workflows 34 | security-events: write 35 | 36 | # only required for workflows in private repositories 37 | actions: read 38 | contents: read 39 | 40 | strategy: 41 | fail-fast: false 42 | matrix: 43 | language: [ 'javascript-typescript', 'python', 'ruby' ] 44 | # CodeQL supports [ 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' ] 45 | # Use only 'java-kotlin' to analyze code written in Java, Kotlin or both 46 | # Use only 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both 47 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 48 | 49 | steps: 50 | - name: Checkout repository 51 | uses: actions/checkout@v4 52 | 53 | # Initializes the CodeQL tools for scanning. 54 | - name: Initialize CodeQL 55 | uses: github/codeql-action/init@v3 56 | with: 57 | languages: ${{ matrix.language }} 58 | # If you wish to specify custom queries, you can do so here or in a config file. 59 | # By default, queries listed here will override any specified in a config file. 60 | # Prefix the list here with "+" to use these queries and those in the config file. 61 | 62 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 63 | # queries: security-extended,security-and-quality 64 | 65 | 66 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). 67 | # If this step fails, then you should remove it and run the build manually (see below) 68 | - name: Autobuild 69 | uses: github/codeql-action/autobuild@v3 70 | 71 | # ℹ️ Command-line programs to run using the OS shell. 72 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 73 | 74 | # If the Autobuild fails above, remove it and uncomment the following three lines. 75 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 76 | 77 | # - run: | 78 | # echo "Run, Build Application using script" 79 | # ./location_of_script_within_repo/buildscript.sh 80 | 81 | - name: Perform CodeQL Analysis 82 | uses: github/codeql-action/analyze@v3 83 | with: 84 | category: "/language:${{matrix.language}}" 85 | -------------------------------------------------------------------------------- /.github/workflows/publish-docker.yml: -------------------------------------------------------------------------------- 1 | name: publish 🐳 Docker image 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | build-and-push-container: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: read 13 | packages: write 14 | steps: 15 | 16 | - name: cut v from tag 17 | env: 18 | TAG: ${{ github.ref_name }} 19 | id: split 20 | run: echo "tag=${TAG:1}" >> $GITHUB_OUTPUT 21 | 22 | - uses: voxpupuli/gha-build-and-publish-a-container@v2 23 | with: 24 | registry_password: ${{ secrets.GITHUB_TOKEN }} 25 | build_arch: linux/amd64,linux/arm64 26 | docker_username: voxpupulibot 27 | docker_password: ${{ secrets.DOCKERHUB_BOT_PASSWORD}} 28 | tags: | 29 | ghcr.io/${{ github.repository }}:${{ steps.split.outputs.tag }} 30 | ghcr.io/${{ github.repository }}:latest 31 | docker.io/${{ github.repository }}:${{ steps.split.outputs.tag }} 32 | docker.io/${{ github.repository }}:latest 33 | 34 | - name: Update Docker Hub Description 35 | uses: peter-evans/dockerhub-description@v4 36 | with: 37 | username: voxpupulibot 38 | password: ${{ secrets.DOCKERHUB_BOT_PASSWORD }} 39 | -------------------------------------------------------------------------------- /.github/workflows/publish-to-pypi.yml: -------------------------------------------------------------------------------- 1 | name: publish 🐍 egg to PyPI 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | build-n-publish: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Set up Python 3.12 16 | uses: actions/setup-python@v5 17 | with: 18 | python-version: 3.12 19 | - name: Install dependencies 20 | run: | 21 | sudo apt install python3-poetry 22 | - name: Build 23 | run: | 24 | poetry install --with=test 25 | poetry build 26 | - name: Publish distribution 📦 to PyPI 27 | uses: pypa/gh-action-pypi-publish@v1.12.4 28 | with: 29 | password: ${{ secrets.PYPI_API_TOKEN_PUPPETBOARD }} 30 | # repository_url: https://test.pypi.org/legacy/ 31 | - name: Create release in GitHub 32 | id: create_release 33 | uses: actions/create-release@v1 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token 36 | with: 37 | tag_name: ${{ github.ref }} 38 | release_name: ${{ github.ref }} 39 | draft: false 40 | prerelease: false 41 | -------------------------------------------------------------------------------- /.github/workflows/security_scanning.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Security Scanning 🕵️ 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | - master 9 | pull_request: 10 | branches: 11 | - main 12 | - master 13 | 14 | jobs: 15 | scan_ci_container: 16 | name: 'Scan CI container' 17 | runs-on: ubuntu-latest 18 | permissions: 19 | actions: read 20 | contents: read 21 | security-events: write 22 | steps: 23 | - name: Checkout repository 24 | uses: actions/checkout@v4 25 | 26 | - name: Build CI container 27 | uses: docker/build-push-action@v6 28 | with: 29 | tags: 'ci/puppetboard:${{ github.sha }}' 30 | push: false 31 | 32 | - name: Scan image with Anchore Grype 33 | uses: anchore/scan-action@v6 34 | id: scan 35 | with: 36 | image: 'ci/puppetboard:${{ github.sha }}' 37 | fail-build: false 38 | 39 | - name: Inspect action SARIF report 40 | run: jq . ${{ steps.scan.outputs.sarif }} 41 | 42 | - name: Upload Anchore scan SARIF report 43 | uses: github/codeql-action/upload-sarif@v3 44 | with: 45 | sarif_file: ${{ steps.scan.outputs.sarif }} 46 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests (unit) 2 | 3 | on: 4 | pull_request: {} 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | test: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | max-parallel: 5 16 | matrix: 17 | python-version: [3.9, '3.10', 3.11, 3.12, 3.13] 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | - name: Set up Python ${{ matrix.python-version }} 22 | uses: actions/setup-python@v5 23 | with: 24 | python-version: ${{ matrix.python-version }} 25 | - name: Install dependencies 26 | run: | 27 | sudo apt install python3-poetry 28 | poetry install --with=test 29 | poetry run mypy --install-types --non-interactive puppetboard/ test/ 30 | - name: Test 31 | run: | 32 | poetry run pytest --cov=. --cov-report=xml --strict-markers --mypy -v puppetboard test 33 | poetry run pylint --errors-only puppetboard test 34 | - name: Upload coverage to Codecov 35 | uses: codecov/codecov-action@v5.4.2 36 | with: 37 | token: ${{ secrets.CODECOV_TOKEN }} 38 | fail_ci_if_error: true 39 | - name: Build 40 | run: | 41 | poetry build -v 42 | 43 | build_docker_image: 44 | name: 'Test building a container' 45 | runs-on: ubuntu-latest 46 | permissions: 47 | contents: read 48 | steps: 49 | - name: Checkout repository 50 | uses: actions/checkout@v4 51 | - name: Build Docker image 52 | uses: docker/build-push-action@v6 53 | with: 54 | context: . 55 | push: false 56 | 57 | security-tests: 58 | runs-on: ubuntu-latest 59 | steps: 60 | - uses: actions/checkout@v4 61 | - name: Set up Python 3.13 62 | uses: actions/setup-python@v5 63 | with: 64 | python-version: 3.13 65 | - name: Run bandit 66 | run: | 67 | sudo apt install python3-poetry 68 | poetry install --with=test 69 | poetry run bandit -r puppetboard 70 | 71 | tests: 72 | needs: 73 | - test 74 | - build_docker_image 75 | - security-tests 76 | runs-on: ubuntu-latest 77 | name: Test suite 78 | steps: 79 | - run: echo Test suite completed 80 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # Editor tmp files 4 | .*.sw* 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Packages 10 | *.egg 11 | *.egg-info 12 | dist 13 | build 14 | .eggs 15 | eggs 16 | parts 17 | bin 18 | var 19 | sdist 20 | develop-eggs 21 | .installed.cfg 22 | lib 23 | lib64 24 | 25 | # Installer logs 26 | pip-log.txt 27 | 28 | # Unit test / coverage reports 29 | .cache 30 | .coverage 31 | coverage.xml 32 | .tox 33 | .mypy_cache 34 | .pytest_cache 35 | 36 | # Translations 37 | *.mo 38 | 39 | # Mr Developer 40 | .mr.developer.cfg 41 | .project 42 | .pydevproject 43 | 44 | # PuppetDB Settings 45 | /settings.py 46 | 47 | # Virtual Environment 48 | venv 49 | 50 | # IDE files 51 | .idea 52 | 53 | Gemfile.lock 54 | .bundle 55 | .vendor 56 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/gdubicki/pre-commit-pngquant 5 | rev: 9fad42f9df2ec8306c73d3e7c0628893ead0ebeb 6 | hooks: 7 | - id: pngquant 8 | args: [--speed=1] 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.13-alpine 2 | 3 | LABEL org.label-schema.maintainer="Voxpupuli Team " \ 4 | org.label-schema.vendor="Voxpupuli" \ 5 | org.label-schema.url="https://github.com/voxpupuli/puppetboard" \ 6 | org.label-schema.license="Apache-2.0" \ 7 | org.label-schema.vcs-url="https://github.com/voxpupuli/puppetboard" \ 8 | org.label-schema.schema-version="1.0" \ 9 | org.label-schema.dockerfile="/Dockerfile" 10 | 11 | ENV PUPPETBOARD_PORT 80 12 | ENV PUPPETBOARD_HOST 0.0.0.0 13 | ENV PUPPETBOARD_STATUS_ENDPOINT /status 14 | ENV PUPPETBOARD_SETTINGS docker_settings.py 15 | EXPOSE 80 16 | 17 | HEALTHCHECK --interval=1m --timeout=5s --start-period=10s CMD python3 -c "import requests; import sys; rc = 0 if requests.get('http://localhost:${PUPPETBOARD_PORT}${PUPPETBOARD_URL_PREFIX:-}${PUPPETBOARD_STATUS_ENDPOINT}').ok else 255; sys.exit(rc)" 18 | 19 | RUN apk add --no-cache gcc libmemcached-dev libc-dev zlib-dev 20 | RUN mkdir -p /usr/src/app/ 21 | WORKDIR /usr/src/app/ 22 | COPY . /usr/src/app 23 | RUN pip install --no-cache-dir -r requirements-docker.txt . 24 | 25 | COPY Dockerfile / 26 | 27 | RUN addgroup -S appgroup && adduser -S appuser -G appgroup 28 | USER appuser 29 | 30 | CMD gunicorn -b ${PUPPETBOARD_HOST}:${PUPPETBOARD_PORT} --preload --workers="${PUPPETBOARD_WORKERS:-1}" -e SCRIPT_NAME="${PUPPETBOARD_URL_PREFIX:-}" --access-logfile=- puppetboard.app:app 31 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source ENV['GEM_SOURCE'] || 'https://rubygems.org' 4 | 5 | group :release, optional: true do 6 | gem 'faraday-retry', '~> 2.1', require: false 7 | gem 'github_changelog_generator', '~> 1.16.4', require: false 8 | end 9 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include CHANGELOG.md 3 | include LICENSE 4 | recursive-include puppetboard/static * 5 | recursive-include puppetboard/templates * 6 | include requirements*.txt 7 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # How to release 2 | 3 | ## On a fork do 4 | 5 | ```shell 6 | git switch -c release-x.y.z 7 | ``` 8 | 9 | Edit [pyproject.toml](pyproject.toml) and set future version. With poetry locally, you can also do `poetry version ` to automatically update. 10 | 11 | ```shell 12 | bundle config set --local path .vendor 13 | bundle config set --local with 'release' 14 | bundle install 15 | 16 | CHANGELOG_GITHUB_TOKEN="token_MC_token-face" bundle exec rake changelog 17 | git add -A 18 | git commit -m 'Release X.Y.Z' 19 | git push origin releae-x.y.z 20 | ``` 21 | 22 | ## as a maintainer on upstream do 23 | 24 | ```shell 25 | git switch master 26 | git pull 27 | git tag $version 28 | git push --tags 29 | ``` 30 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | begin 2 | require 'github_changelog_generator/task' 3 | rescue LoadError 4 | # github_changelog_generator is an optional group 5 | else 6 | GitHubChangelogGenerator::RakeTask.new :changelog do |config| 7 | version = File.readlines('pyproject.toml').find{ |l| l.match(/version/) }.split(' ').last.gsub('"', '') 8 | config.future_release = "v#{version}" if /^\d+\.\d+.\d+$/.match?(version) 9 | config.header = "# Changelog\n\nAll notable changes to this project will be documented in this file." 10 | config.exclude_labels = %w[duplicate question invalid wontfix wont-fix skip-changelog github_actions] 11 | config.user = 'voxpupuli' 12 | config.project = 'puppetboard' 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /docs/Debian-Jessie.md: -------------------------------------------------------------------------------- 1 | # Install Using debian jessie 2 | 3 | ``` 4 | $ apt-get install python-pip git 5 | 6 | $ mkdir /opt/voxpupuli-puppetboard/ 7 | $ cd /opt/voxpupuli-puppetboard/ 8 | $ git clone https://github.com/voxpupuli/puppetboard 9 | $ cd /opt/voxpupuli-puppetboard/puppetboard 10 | $ pip install puppetboard 11 | 12 | ``` 13 | 14 | * /etc/apache2/sites-available/voxpupuli-puppetboard.conf 15 | 16 | ``` 17 | 18 | ServerName puppetboard.my.domain 19 | WSGIDaemonProcess puppetboard user=www-data group=www-data threads=5 python-path=/usr/local/lib/python2.7/dist-packages/puppetboard:python-home=/opt/voxpupuli-puppetboard/puppetboard:/opt/voxpupuli-puppetboard/puppetboard/puppetboard:/usr/local/lib/python2.7/dist-packages/puppetboard/static 20 | WSGIScriptAlias / /opt/voxpupuli-puppetboard/puppetboard/wsgi.py 21 | ErrorLog /var/log/apache2/puppetboard.error.log 22 | CustomLog /var/log/apache2/puppetboard.access.log combined 23 | 24 | 25 | 26 | Order deny,allow 27 | Allow from all 28 | Require all granted 29 | 30 | 31 | 32 | Alias /static /usr/local/lib/python2.7/dist-packages/puppetboard/static 33 | 34 | Satisfy Any 35 | Allow from all 36 | Require all granted 37 | 38 | 39 | 40 | WSGIProcessGroup puppetboard 41 | WSGIApplicationGroup %{GLOBAL} 42 | Order deny,allow 43 | Allow from all 44 | Require all granted 45 | 46 | 47 | ``` 48 | 49 | ``` 50 | $ a2ensite voxpupuli-puppetboard.conf 51 | ``` 52 | 53 | * /opt/voxpupuli-puppetboard/puppetboard/wsgi.py 54 | ``` 55 | from __future__ import absolute_import 56 | import os 57 | 58 | import sys 59 | sys.path.append('/opt/voxpupuli-puppetboard/puppetboard') 60 | 61 | from puppetboard.app import app as application 62 | ``` 63 | -------------------------------------------------------------------------------- /docs/EL7.md: -------------------------------------------------------------------------------- 1 | # Steps to get this working with EL7 2 | 3 | ## Modules 4 | 5 | You will likely need to track down more module dependencies, though this is what 6 | I used. 7 | 8 | `Puppetfile` 9 | 10 | ```ruby 11 | mode 'apache', 12 | :git => 'https://github.com/puppetlabs/puppetlabs-apache.git', 13 | :ref => '3.4.0' 14 | 15 | mod 'apt', 16 | :git => 'https://github.com/puppetlabs/puppetlabs-apt.git', 17 | :ref => '6.2.1' 18 | 19 | mod 'concat', 20 | :git => 'https://github.com/puppetlabs/puppetlabs-concat.git', 21 | :ref => '5.1.0' 22 | 23 | mod 'epel', 24 | :git => 'https://github.com/stahnma/puppet-module-epel.git', 25 | :ref => '1.3.1' 26 | 27 | mod 'firewall', 28 | :git => 'https://github.com/puppetlabs/puppetlabs-firewall.git', 29 | :ref => '1.14.0' 30 | 31 | mod 'inifile', 32 | :git => 'https://github.com/puppetlabs/puppetlabs-inifile.git', 33 | :ref => '2.4.0' 34 | 35 | mod 'postgresql', 36 | :git => 'https://github.com/puppetlabs/puppet-postgresql.git', 37 | :ref => '5.11.0' 38 | 39 | mod 'puppetdb', 40 | :git => 'https://github.com/puppetlabs/puppetlabs-puppetdb.git', 41 | :ref => '7.1.0' 42 | 43 | mod 'puppetboard', 44 | :git => 'https://github.com/voxpupuli/puppet-puppetboard.git', 45 | :ref => 'v5.0.0' 46 | 47 | mod 'python', 48 | :git => 'https://github.com/voxpupuli/puppet-python.git', 49 | :ref => 'v2.2.2' 50 | 51 | mod 'selinux', 52 | :git => 'https://github.com/ghoneycutt/puppet-module-selinux.git', 53 | :ref => 'v2.2.0' 54 | 55 | mod 'stdlib', 56 | :git => 'https://github.com/puppetlabs/puppetlabs-stdlib.git', 57 | :ref => '5.1.0' 58 | 59 | mod 'translate', 60 | :git => 'https://github.com/puppetlabs/puppetlabs-translate.git', 61 | :ref => '1.2.0' 62 | ``` 63 | 64 | ## Manifests 65 | 66 | ### Role 67 | 68 | `role/manifests/puppetboard.pp` 69 | 70 | ```puppet 71 | # @summary Role for puppetboard 72 | # 73 | class role::puppetboard { 74 | 75 | include ::profile::puppetboard 76 | } 77 | ``` 78 | 79 | ### Profile 80 | 81 | `profile/manifests/puppetboard.pp` 82 | 83 | ```puppet 84 | # Class: profile::puppetboard 85 | # 86 | # Puppetboard is a WebUI to inspect PuppetDB 87 | # 88 | class profile::puppetboard { 89 | 90 | include ::apache 91 | 92 | $puppetboard_certname = $trusted['certname'] 93 | $ssl_dir = '/etc/httpd/ssl' 94 | 95 | file { $ssl_dir: 96 | ensure => 'directory', 97 | owner => 'root', 98 | group => 'root', 99 | mode => '0755', 100 | } 101 | 102 | file { "${ssl_dir}/certs": 103 | ensure => 'directory', 104 | owner => 'root', 105 | group => 'root', 106 | mode => '0755', 107 | } 108 | 109 | file { "${ssl_dir}/private_keys": 110 | ensure => 'directory', 111 | owner => 'root', 112 | group => 'root', 113 | mode => '0750', 114 | } 115 | 116 | file { "${ssl_dir}/certs/ca.pem": 117 | ensure => 'file', 118 | owner => 'root', 119 | group => 'root', 120 | mode => '0644', 121 | source => "${::settings::ssldir}/certs/ca.pem", 122 | before => Class['::puppetboard'], 123 | } 124 | 125 | file { "${ssl_dir}/certs/${puppetboard_certname}.pem": 126 | ensure => 'file', 127 | owner => 'root', 128 | group => 'root', 129 | mode => '0644', 130 | source => "${::settings::ssldir}/certs/${puppetboard_certname}.pem", 131 | before => Class['::puppetboard'], 132 | } 133 | 134 | file { "${ssl_dir}/private_keys/${puppetboard_certname}.pem": 135 | ensure => 'file', 136 | owner => 'root', 137 | group => 'root', 138 | mode => '0644', 139 | source => "${::settings::ssldir}/private_keys/${puppetboard_certname}.pem", 140 | before => Class['::puppetboard'], 141 | } 142 | 143 | class { '::puppetboard': 144 | groups => 'root', 145 | manage_git => true, 146 | manage_virtualenv => true, 147 | manage_selinux => false, 148 | puppetdb_host => 'puppetdb.example.com', 149 | puppetdb_port => 8081, 150 | puppetdb_key => "${ssl_dir}/private_keys/${puppetboard_certname}.pem", 151 | puppetdb_ssl_verify => "${ssl_dir}/certs/ca.pem", 152 | puppetdb_cert => "${ssl_dir}/certs/${puppetboard_certname}.pem", 153 | reports_count => 40, 154 | } 155 | 156 | class { '::apache::mod::wsgi': 157 | wsgi_socket_prefix => '/var/run/wsgi', 158 | } 159 | 160 | class { '::puppetboard::apache::vhost': 161 | vhost_name => 'puppetboard.example.com', 162 | port => 80, 163 | } 164 | } 165 | ``` 166 | 167 | ## Hiera data 168 | 169 | ```yaml 170 | --- 171 | 172 | # Personal preference, you don't *need* this. 173 | puppetboard::enable_catalog: true 174 | 175 | python::dev: true 176 | 177 | selinux::mode: permissive 178 | ``` 179 | 180 | I put this in `data/role/puppetboard.yaml` with `hiera.yaml` like the 181 | following, though you could put this under certname or wherever you see 182 | fit. 183 | 184 | ```yaml 185 | --- 186 | version: 5 187 | defaults: 188 | # The default value for "datadir" is "data" under the same directory as the hiera.yaml 189 | # file (this file) 190 | # When specifying a datadir, make sure the directory exists. 191 | # See https://puppet.com/docs/puppet/latest/environments_about.html for further details on environments. 192 | # datadir: data 193 | # data_hash: yaml_data 194 | hierarchy: 195 | - name: "Per-node data (yaml version)" 196 | path: "nodes/%{::trusted.certname}.yaml" 197 | - name: "Role data" 198 | paths: 199 | - "role/%{facts.role}.yaml" 200 | - name: "Other YAML hierarchy levels" 201 | paths: 202 | - "common.yaml" 203 | ``` 204 | -------------------------------------------------------------------------------- /puppetboard/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Puppetboard 3 | # 4 | -------------------------------------------------------------------------------- /puppetboard/app.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import unicode_literals 3 | 4 | import logging 5 | import os 6 | import sys 7 | from datetime import datetime 8 | 9 | from flask import render_template, Response 10 | 11 | # these imports are required by Flask - DO NOT remove them although they look unused 12 | # noinspection PyUnresolvedReferences 13 | import puppetboard.views.catalogs # noqa: F401 14 | # noinspection PyUnresolvedReferences 15 | import puppetboard.views.classes # noqa: F401 16 | # noinspection PyUnresolvedReferences 17 | import puppetboard.views.dailychart # noqa: F401 18 | # noinspection PyUnresolvedReferences 19 | import puppetboard.views.facts # noqa: F401 20 | # noinspection PyUnresolvedReferences 21 | import puppetboard.views.index # noqa: F401 22 | # noinspection PyUnresolvedReferences 23 | import puppetboard.views.inventory # noqa: F401 24 | # noinspection PyUnresolvedReferences 25 | import puppetboard.views.metrics # noqa: F401 26 | # noinspection PyUnresolvedReferences 27 | import puppetboard.views.nodes # noqa: F401 28 | # noinspection PyUnresolvedReferences 29 | import puppetboard.views.query # noqa: F401 30 | # noinspection PyUnresolvedReferences 31 | import puppetboard.views.radiator # noqa: F401 32 | # noinspection PyUnresolvedReferences 33 | import puppetboard.views.reports # noqa: F401 34 | # noinspection PyUnresolvedReferences 35 | import puppetboard.views.failures # noqa: F401 36 | import puppetboard.errors # noqa: F401 37 | 38 | from puppetboard.core import get_app, get_puppetdb, get_scheduler 39 | from puppetboard.version import __version__ 40 | from puppetboard.utils import is_a_test, check_db_version, check_secret_key 41 | 42 | app = get_app() 43 | puppetdb = get_puppetdb() 44 | get_scheduler() 45 | running_as = os.path.basename(sys.argv[0]) 46 | if not is_a_test(): 47 | check_db_version(puppetdb) 48 | check_secret_key(app.config.get('SECRET_KEY')) 49 | 50 | logging.basicConfig(level=app.config['LOGLEVEL'].upper()) 51 | log = logging.getLogger(__name__) 52 | 53 | menu_entries = [ 54 | ('index', 'Overview'), 55 | ('failures', 'Failures'), 56 | ('nodes', 'Nodes'), 57 | ('facts', 'Facts'), 58 | ('reports', 'Reports'), 59 | ('metrics', 'Metrics'), 60 | ('inventory', 'Inventory'), 61 | ('catalogs', 'Catalogs'), 62 | ('classes', 'Classes'), 63 | ('radiator', 'Radiator'), 64 | ('query', 'Query'), 65 | ] 66 | 67 | if not app.config.get('ENABLE_QUERY'): 68 | menu_entries.remove(('query', 'Query')) 69 | 70 | if not app.config.get('ENABLE_CATALOG'): 71 | menu_entries.remove(('catalogs', 'Catalogs')) 72 | 73 | if not app.config.get('ENABLE_CLASS'): 74 | menu_entries.remove(('classes', 'Classes')) 75 | 76 | app.jinja_env.globals.update(menu_entries=menu_entries) 77 | 78 | 79 | @app.context_processor 80 | def utility_processor(): 81 | def now(format='%m/%d/%Y %H:%M:%S'): 82 | """returns the formated datetime""" 83 | return datetime.now().strftime(format) 84 | 85 | def version(): 86 | return __version__ 87 | 88 | def fact_os_detection(os_facts): 89 | os_name = "" 90 | os_family = os_facts['family'] 91 | 92 | try: 93 | if os_family == "windows": 94 | os_name = os_facts["windows"]["product_name"] 95 | elif os_family == "Darwin": 96 | os_name = os_facts["macosx"]["product"] 97 | else: 98 | os_name = os_facts["distro"]["description"] 99 | except KeyError: 100 | pass 101 | 102 | return os_name 103 | 104 | return dict( 105 | now=now, 106 | version=version, 107 | fact_os_detection=fact_os_detection, 108 | ) 109 | 110 | 111 | @app.route('/offline/') 112 | def offline_static(filename): 113 | mimetype = 'text/html' 114 | if filename.endswith('.css'): 115 | mimetype = 'text/css' 116 | elif filename.endswith('.js'): 117 | mimetype = 'text/javascript' 118 | 119 | return Response(response=render_template('static/%s' % filename), 120 | status=200, mimetype=mimetype) 121 | 122 | 123 | @app.route('/status') 124 | def health_status(): 125 | return 'OK' 126 | -------------------------------------------------------------------------------- /puppetboard/default_settings.py: -------------------------------------------------------------------------------- 1 | import secrets 2 | 3 | PUPPETDB_HOST = 'localhost' 4 | PUPPETDB_PORT = 8080 5 | PUPPETDB_PROTO = None 6 | PUPPETDB_SSL_VERIFY = True 7 | PUPPETDB_KEY = None 8 | PUPPETDB_CERT = None 9 | PUPPETDB_TIMEOUT = 20 10 | DEFAULT_ENVIRONMENT = 'production' 11 | # this empty string has to be changed, we validate it with check_secret_key() 12 | SECRET_KEY = '' # nosec 13 | UNRESPONSIVE_HOURS = 2 14 | ENABLE_QUERY = True 15 | # Uncomment to restrict the enabled PuppetDB endpoints in the query page. 16 | # ENABLED_QUERY_ENDPOINTS = ['facts', 'nodes'] 17 | LOCALISE_TIMESTAMP = True 18 | LOGLEVEL = 'info' 19 | NORMAL_TABLE_COUNT = 100 20 | LITTLE_TABLE_COUNT = 10 21 | TABLE_COUNT_SELECTOR = [10, 20, 50, 100, 500] 22 | DISPLAYED_METRICS = ['resources.total', 23 | 'events.failure', 24 | 'events.success', 25 | 'resources.skipped', 26 | 'events.noop'] 27 | OFFLINE_MODE = False 28 | ENABLE_CATALOG = False 29 | OVERVIEW_FILTER = None 30 | PAGE_TITLE = "Puppetboard" 31 | GRAPH_TYPE = 'pie' 32 | GRAPH_FACTS = ['architecture', 33 | 'clientversion', 34 | 'domain', 35 | 'lsbcodename', 36 | 'lsbdistcodename', 37 | 'lsbdistid', 38 | 'lsbdistrelease', 39 | 'lsbmajdistrelease', 40 | 'netmask', 41 | 'osfamily', 42 | 'puppetversion', 43 | 'processorcount'] 44 | INVENTORY_FACTS = [('Hostname', 'trusted'), 45 | ('IP Address', 'ipaddress'), 46 | ('OS', 'os'), 47 | ('Architecture', 'hardwaremodel'), 48 | ('Kernel Version', 'kernelrelease'), 49 | ('Puppet Version', 'puppetversion'), ] 50 | 51 | INVENTORY_FACT_TEMPLATES = { 52 | 'trusted': ( 53 | """""" 54 | """{{value.hostname}}""" 55 | """""" 56 | ), 57 | 'os': "{{ fact_os_detection(value) }}", 58 | } 59 | REFRESH_RATE = 30 60 | DAILY_REPORTS_CHART_ENABLED = True 61 | DAILY_REPORTS_CHART_DAYS = 8 62 | WITH_EVENT_NUMBERS = True 63 | SHOW_ERROR_AS = 'friendly' # or 'raw' 64 | CODE_PREFIX_TO_REMOVE = '/etc/puppetlabs/code/environments(/.*?/modules)?' 65 | FAVORITE_ENVS = [ 66 | 'production', 67 | 'staging', 68 | 'qa', 69 | 'test', 70 | 'dev', 71 | ] 72 | 73 | ENABLE_CLASS = False 74 | # mapping between the status of the events (from PuppetDB) and the columns of the table to display 75 | CLASS_EVENTS_STATUS_COLUMNS = [ 76 | # ('skipped', 'Skipped'), 77 | ('failure', 'Failure'), 78 | ('success', 'Success'), 79 | ('noop', 'Noop'), 80 | ] 81 | # Type of caching object to use when `SCHEDULER_ENABLED` is set to `True`. 82 | # If more than one worker, use a shared backend (e.g. `MemcachedCache`) 83 | # to allow the sharing of the cache between the processes. 84 | CACHE_TYPE = 'SimpleCache' 85 | # Cache litefime in second 86 | CACHE_DEFAULT_TIMEOUT = 3600 87 | 88 | # List of scheduled jobs to trigger 89 | # * `id`: job's ID 90 | # * `func`: full path of the function to execute 91 | # * `trigger`: should be 'interval' if you want to run the job at regular intervals 92 | # * `seconds`: number of seconds between 2 triggered jobs 93 | SCHEDULER_JOBS = [{ 94 | 'id': 'do_build_async_cache_1', 95 | 'func': 'puppetboard.schedulers.classes:build_async_cache', 96 | 'trigger': 'interval', 97 | 'seconds': 300, 98 | }] 99 | SCHEDULER_ENABLED = False 100 | SCHEDULER_LOCK_BIND_PORT = 49100 101 | -------------------------------------------------------------------------------- /puppetboard/errors.py: -------------------------------------------------------------------------------- 1 | from flask import render_template 2 | from werkzeug.exceptions import InternalServerError 3 | 4 | from puppetboard.core import environments, get_app 5 | 6 | app = get_app() 7 | 8 | 9 | @app.errorhandler(400) 10 | def bad_request(e): 11 | envs = environments() 12 | return render_template('400.html', envs=envs), 400 13 | 14 | 15 | @app.errorhandler(403) 16 | def forbidden(e): 17 | envs = environments() 18 | return render_template('403.html', envs=envs), 403 19 | 20 | 21 | @app.errorhandler(404) 22 | def not_found(e): 23 | envs = environments() 24 | return render_template('404.html', envs=envs), 404 25 | 26 | 27 | @app.errorhandler(412) 28 | def precond_failed(e): 29 | """We're slightly abusing 412 to handle missing features 30 | depending on the API version.""" 31 | envs = environments() 32 | return render_template('412.html', envs=envs), 412 33 | 34 | 35 | @app.errorhandler(500) 36 | def server_error(e): 37 | envs = {} 38 | try: 39 | envs = environments() 40 | except InternalServerError: 41 | pass 42 | return render_template('500.html', envs=envs), 500 43 | -------------------------------------------------------------------------------- /puppetboard/forms.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | 3 | from flask_wtf import FlaskForm 4 | from wtforms import (BooleanField, SelectField, TextAreaField, validators) 5 | 6 | from puppetboard.core import get_app 7 | 8 | app = get_app() 9 | QUERY_ENDPOINTS = OrderedDict([ 10 | # PuppetDB API endpoint, Form name 11 | ('pql', 'PQL'), 12 | ('nodes', 'Nodes'), 13 | ('resources', 'Resources'), 14 | ('facts', 'Facts'), 15 | ('factsets', 'Fact Sets'), 16 | ('fact-paths', 'Fact Paths'), 17 | ('fact-contents', 'Fact Contents'), 18 | ('reports', 'Reports'), 19 | ('events', 'Events'), 20 | ('catalogs', 'Catalogs'), 21 | ('edges', 'Edges'), 22 | ('environments', 'Environments'), 23 | ]) 24 | ENABLED_QUERY_ENDPOINTS = app.config.get( 25 | 'ENABLED_QUERY_ENDPOINTS', list(QUERY_ENDPOINTS.keys())) 26 | 27 | 28 | class QueryForm(FlaskForm): 29 | """The form used to allow freeform queries to be executed against 30 | PuppetDB.""" 31 | query = TextAreaField('Query', [validators.DataRequired( 32 | message='A query is required.')]) 33 | endpoints = SelectField('API endpoint', choices=[ 34 | (key, value) for key, value in QUERY_ENDPOINTS.items() 35 | if key in ENABLED_QUERY_ENDPOINTS], default='pql') 36 | rawjson = BooleanField('Raw JSON') 37 | -------------------------------------------------------------------------------- /puppetboard/schedulers/classes.py: -------------------------------------------------------------------------------- 1 | from pypuppetdb.QueryBuilder import (AndOperator, InOperator, FromOperator, 2 | EqualsOperator, NullOperator, OrOperator, 3 | ExtractOperator, LessEqualOperator, SubqueryOperator) 4 | 5 | from puppetboard.core import get_app, get_cache, get_puppetdb, environments, stream_template, REPORTS_COLUMNS 6 | from puppetboard.utils import (yield_or_stop, check_env, get_or_abort) 7 | from puppetboard.views.classes import (get_status_from_events) 8 | 9 | app = get_app() 10 | cache = get_cache() 11 | puppetdb = get_puppetdb() 12 | 13 | events_status_columns = ['skipped','failure','success','noop'] 14 | 15 | 16 | def build_async_cache(): 17 | """Scheduled job triggered at regular interval in order to pre-compute the 18 | results to display in the classes view. 19 | The result contains the events associated with the last reports and is 20 | stored in the cache. 21 | """ 22 | columns = [col for col in app.config['CLASS_EVENTS_STATUS_COLUMNS'] if col[0] in events_status_columns] 23 | 24 | envs = puppetdb.environments() 25 | for env in puppetdb.environments(): 26 | env = env['name'] 27 | query = AndOperator() 28 | query.add(EqualsOperator("environment", env)) 29 | # get events from last report for each active node 30 | query_in = InOperator('hash') 31 | query_ex = ExtractOperator() 32 | query_ex.add_field('latest_report_hash') 33 | query_from = FromOperator('nodes') 34 | query_null = NullOperator('deactivated', True) 35 | query_ex.add_query(query_null) 36 | query_from.add_query(query_ex) 37 | query_in.add_query(query_from) 38 | reportlist = puppetdb.reports(query=query_in) 39 | 40 | new_cache = {} 41 | for report in yield_or_stop(reportlist): 42 | report_hash = report.hash_ 43 | for event in yield_or_stop(report.events()): 44 | containing_class = event.item['class'] 45 | status = event.status 46 | new_cache[containing_class] = new_cache.get(containing_class, {}) 47 | new_cache[containing_class][report_hash] = new_cache[containing_class].get(report_hash, { 48 | 'node_name': report.node, 49 | 'node_status': report.status, 50 | 'class_status': 'skipped', 51 | 'report_hash': report_hash, 52 | 'nb_events_per_status': {col[0]: 0 for col in columns}, 53 | }) 54 | if status in new_cache[containing_class][report_hash]['nb_events_per_status']: 55 | new_cache[containing_class][report_hash]['nb_events_per_status'][status] += 1 56 | for class_name in new_cache: 57 | for report_hash, report in new_cache[class_name].items(): 58 | status = get_status_from_events(report['nb_events_per_status']) 59 | new_cache[class_name][report_hash]['class_status'] = get_status_from_events(report['nb_events_per_status']) 60 | 61 | cache.set(f'classes_resource_{env}', new_cache) 62 | -------------------------------------------------------------------------------- /puppetboard/static/css/radiator.css: -------------------------------------------------------------------------------- 1 | html,body,div,span,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,code,del,dfn,em,img,q,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td {margin:0;padding:0;border:0;font-weight:inherit;font-style:inherit;font-size:100%;font-family:inherit;vertical-align:baseline;} 2 | body{line-height:20px;} 3 | table{border-collapse:separate;border-spacing:0;} 4 | caption,th,td {text-align:left;font-weight:normal;} 5 | table,td,th {vertical-align:middle;} 6 | blockquote:before,blockquote:after,q:before,q:after {content:"";} 7 | blockquote,q {quotes:"" "";} 8 | a img {border:none;} 9 | ul {list-style-position:inside;} 10 | body.radiator_controller {font-size:1000%;line-height:1;font-family:"Helvetica Neue",Arial,Helvetica,sans-serif;background-color:#000;} 11 | body.radiator_controller table.node_summary {padding:20px;position:absolute;height:100%;width:100%;} 12 | body.radiator_controller table.node_summary .count_column {min-width:2em;width:2em;} 13 | body.radiator_controller table.node_summary tr:last-child td {border-bottom:none;} 14 | body.radiator_controller table.node_summary tr:last-child td .label {border-left:0px #000 solid;} 15 | body.radiator_controller table.node_summary tr.unreported .percent {background-color:#3D96AE;border-radius:0 3px 3px 0;} 16 | body.radiator_controller table.node_summary tr.unreported .label,body.radiator_controller table.node_summary tr.unreported .count {color:#3D96AE;} 17 | body.radiator_controller table.node_summary tr.unreported .label {border-left:1px #333 dashed;} 18 | body.radiator_controller table.node_summary tr.failed .percent {background-color:#cc2211;border-radius:0 3px 3px 0;} 19 | body.radiator_controller table.node_summary tr.failed .label,body.radiator_controller table.node_summary tr.failed .count {color:#cc2211;} 20 | body.radiator_controller table.node_summary tr.failed .label {border-left:1px #333 dashed;} 21 | body.radiator_controller table.node_summary tr.noop .percent {background-color:#DB843D;border-radius:0 3px 3px 0;} 22 | body.radiator_controller table.node_summary tr.noop .label,body.radiator_controller table.node_summary tr.noop .count {color:#DB843D;} 23 | body.radiator_controller table.node_summary tr.noop .label {border-left:1px #333 dashed;} 24 | body.radiator_controller table.node_summary tr.changed .percent {background-color:#4572A7;border-radius:0 3px 3px 0;} 25 | body.radiator_controller table.node_summary tr.changed .label,body.radiator_controller table.node_summary tr.changed .count {color:#4572A7;} 26 | body.radiator_controller table.node_summary tr.changed .label {border-left:1px #333 dashed;} 27 | body.radiator_controller table.node_summary tr.unchanged .percent {background-color:#89A54E;border-radius:0 3px 3px 0;} 28 | body.radiator_controller table.node_summary tr.unchanged .label,body.radiator_controller table.node_summary tr.unchanged .count {color:#89A54E;} 29 | body.radiator_controller table.node_summary tr.unchanged .label {border-left:1px #333 dashed;} 30 | body.radiator_controller table.node_summary tr.total {color:#fff;background-color:#181818;} 31 | body.radiator_controller table.node_summary tr.total .percent {background-color:white;border-radius:0 3px 3px 0;} 32 | body.radiator_controller table.node_summary tr.total .label,body.radiator_controller table.node_summary tr.total .count {color:white;} 33 | body.radiator_controller table.node_summary tr.total .label {border-left:1px #333 dashed;} 34 | body.radiator_controller table.node_summary tr.total .percent {display:none;} 35 | body.radiator_controller table.node_summary tr.total td {border-top:1px solid #fff;} 36 | body.radiator_controller table.node_summary tr td {color:#ccc;font-weight:normal;position:relative;border-bottom:1px solid #333;vertical-align:baseline;} 37 | body.radiator_controller table.node_summary tr td div {position:relative;height:100%;} 38 | body.radiator_controller table.node_summary tr td .percent {color:#000;position:absolute;top:0;left:0;height:100%;overflow:hidden;} 39 | body.radiator_controller table.node_summary tr td .percent span {margin-left:0.1em;} 40 | body.radiator_controller table.node_summary tr td .label {position:relative;height:100%;} 41 | body.radiator_controller table.node_summary tr td .label span {margin-left:0.1em;} 42 | body.radiator_controller table.node_summary tr td .count {text-align:right;width:100%;display:inline-block;font-weight:bold;margin-top:-0.12em;} 43 | -------------------------------------------------------------------------------- /puppetboard/static/custom-natural-time-delta/natural-time-delta.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Shodhan Save on Jan 23, 2018. 3 | * Updated @ Jan 25, 2018 4 | * Modified for the Puppetboard by Greg Dubicki. 5 | */ 6 | 7 | /** 8 | * This plug-in allows sorting of human-readable time delta, viz., 9 | * "1 week", "2 weeks 3 days", "4 weeks 5 days 6 hours", "1:24 hours" etc. 10 | * 11 | * Currently this plugin supports time range from microseconds to decades. 12 | * 13 | * The plugin also takes care of singular and plural values like week(s) 14 | * 15 | * @name Natural Time Delta 16 | * @summary Sort human-readable time delta 17 | * 18 | * @example 19 | * $("#example").DataTable({ 20 | * columnDefs: [ 21 | * { "type": "natural-time-delta", "targets": 2 } 22 | * ] 23 | * }); 24 | */ 25 | 26 | jQuery.extend(jQuery.fn.dataTableExt.oSort,{ 27 | "natural-time-delta-pre" : function(data){ 28 | // get the non-formatted value from title 29 | data = data.match(/title="(.*?)"/)[1].toLowerCase(); 30 | 31 | var total_duration = 0; 32 | var pattern = /(\d+\s*decades?\s*)?(\d+\s*years?\s*)?(\d+\s*months?\s*)?(\d+\s*weeks?\s*)?(\d+\s*days?\s*)?(\d+:?\d*?\s*hours?\s*)?(\d+\s*minutes?\s*)?(\d+\s*seconds?\s*)?(\d+\s*milliseconds?\s*)?(\d+\s*microseconds?\s*)?/i 33 | var get_duration = function (el, unit_name, duration_in_seconds) { 34 | if (el === undefined) { 35 | return 0; 36 | } 37 | 38 | var split_by = unit_name[0] 39 | var no_of_units = el.split(split_by)[0].trim() 40 | 41 | if ((unit_name === "hour") && (no_of_units.split(':').length === 2)) { 42 | // this is hour with minutes looking like this: "1:26 hours" 43 | var hours = parseFloat(no_of_units.split(':')[0]); 44 | var minutes = parseFloat(no_of_units.split(':')[1]); 45 | return (hours * 60 * 60) + (minutes * 60); 46 | } else { 47 | return parseFloat(no_of_units) * duration_in_seconds; 48 | } 49 | }; 50 | 51 | var matches = data.match(pattern); 52 | matches.reverse(); 53 | 54 | var time_elements = [ 55 | {"unit_name": "microsecond", "duration_in_seconds": 1 / 1000 / 1000}, 56 | {"unit_name": "millisecond", "duration_in_seconds": 1 / 1000}, 57 | {"unit_name": "second", "duration_in_seconds": 1}, 58 | {"unit_name": "minute", "duration_in_seconds": 1 * 60}, 59 | {"unit_name": "hour", "duration_in_seconds": 1 * 60 * 60}, 60 | {"unit_name": "day", "duration_in_seconds": 1 * 60 * 60 * 24}, 61 | {"unit_name": "week", "duration_in_seconds": 1 * 60 * 60 * 24 * 7}, 62 | {"unit_name": "month", "duration_in_seconds": 1 * 60 * 60 * 24 * 7 * 30}, 63 | {"unit_name": "year", "duration_in_seconds": 1 * 60 * 60 * 24 * 7 * 30 * 12}, 64 | {"unit_name": "decade", "duration_in_seconds": 1 * 60 * 60 * 24 * 7 * 30 * 12 * 10}, 65 | ]; 66 | 67 | time_elements.forEach(function (el, i) { 68 | var duration = get_duration(matches[i], el["unit_name"], el["duration_in_seconds"]); 69 | total_duration += duration; 70 | }); 71 | 72 | return total_duration || -1; 73 | }, 74 | 75 | "natural-time-delta-asc" : function (a, b) { 76 | return ((a < b) ? -1 : ((a > b) ? 1 : 0)); 77 | }, 78 | 79 | "natural-time-delta-desc" : function (a, b) { 80 | return ((a < b) ? 1 : ((a > b) ? -1 : 0)); 81 | } 82 | }); 83 | -------------------------------------------------------------------------------- /puppetboard/static/custom-natural/natural.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Allan Jardine on Feb 17, 2023. 3 | * Updated @ Feb 22, 2024 4 | * Modified for the Puppetboard by ArthurWuTW. 5 | */ 6 | 7 | /** 8 | * Data can often be a complicated mix of numbers and letters (file names 9 | * are a common example) and sorting them in a natural manner is quite a 10 | * difficult problem. 11 | * 12 | * Fortunately the Javascript `localeCompare` method is now widely supported 13 | * and provides a natural sorting method we can use with DataTables. 14 | * 15 | * @name Natural sorting 16 | * @summary Sort data with a mix of numbers and letters _naturally_. 17 | * 18 | * @example 19 | * // Natural sorting 20 | * new DataTable('#myTable', 21 | * columnDefs: [ 22 | * { type: 'natural', target: 0 } 23 | * ] 24 | * } ); 25 | * 26 | */ 27 | 28 | 29 | jQuery.extend(jQuery.fn.dataTableExt.oSort,{ 30 | 31 | "natural-asc" : function (a, b) { 32 | return a.localeCompare(b, navigator.languages[0] || navigator.language, { 33 | numeric: true, 34 | ignorePunctuation: true, 35 | }); 36 | }, 37 | 38 | "natural-desc" : function (a, b) { 39 | return (a.localeCompare(b, navigator.languages[0] || navigator.language, { numeric: true, ignorePunctuation: true }) * 40 | -1); 41 | } 42 | }); 43 | -------------------------------------------------------------------------------- /puppetboard/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxpupuli/puppetboard/6b6e8dfbbd9b35ce540598f6dde4811e544db446/puppetboard/static/favicon.ico -------------------------------------------------------------------------------- /puppetboard/static/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 40 | -------------------------------------------------------------------------------- /puppetboard/static/fonts/cousine/d6lIkaiiRdih4SpP_SAvzAbt.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxpupuli/puppetboard/6b6e8dfbbd9b35ce540598f6dde4811e544db446/puppetboard/static/fonts/cousine/d6lIkaiiRdih4SpP_SAvzAbt.woff2 -------------------------------------------------------------------------------- /puppetboard/static/fonts/cousine/d6lIkaiiRdih4SpP_SQvzA.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxpupuli/puppetboard/6b6e8dfbbd9b35ce540598f6dde4811e544db446/puppetboard/static/fonts/cousine/d6lIkaiiRdih4SpP_SQvzA.woff2 -------------------------------------------------------------------------------- /puppetboard/static/fonts/cousine/d6lIkaiiRdih4SpP_SYvzAbt.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxpupuli/puppetboard/6b6e8dfbbd9b35ce540598f6dde4811e544db446/puppetboard/static/fonts/cousine/d6lIkaiiRdih4SpP_SYvzAbt.woff2 -------------------------------------------------------------------------------- /puppetboard/static/fonts/cousine/d6lIkaiiRdih4SpP_ScvzAbt.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxpupuli/puppetboard/6b6e8dfbbd9b35ce540598f6dde4811e544db446/puppetboard/static/fonts/cousine/d6lIkaiiRdih4SpP_ScvzAbt.woff2 -------------------------------------------------------------------------------- /puppetboard/static/fonts/cousine/d6lIkaiiRdih4SpP_SgvzAbt.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxpupuli/puppetboard/6b6e8dfbbd9b35ce540598f6dde4811e544db446/puppetboard/static/fonts/cousine/d6lIkaiiRdih4SpP_SgvzAbt.woff2 -------------------------------------------------------------------------------- /puppetboard/static/fonts/cousine/d6lIkaiiRdih4SpP_SkvzAbt.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxpupuli/puppetboard/6b6e8dfbbd9b35ce540598f6dde4811e544db446/puppetboard/static/fonts/cousine/d6lIkaiiRdih4SpP_SkvzAbt.woff2 -------------------------------------------------------------------------------- /puppetboard/static/fonts/cousine/d6lIkaiiRdih4SpP_SovzAbt.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxpupuli/puppetboard/6b6e8dfbbd9b35ce540598f6dde4811e544db446/puppetboard/static/fonts/cousine/d6lIkaiiRdih4SpP_SovzAbt.woff2 -------------------------------------------------------------------------------- /puppetboard/static/fonts/cousine/d6lIkaiiRdih4SpP_SsvzAbt.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxpupuli/puppetboard/6b6e8dfbbd9b35ce540598f6dde4811e544db446/puppetboard/static/fonts/cousine/d6lIkaiiRdih4SpP_SsvzAbt.woff2 -------------------------------------------------------------------------------- /puppetboard/static/fonts/lato/S6u8w4BMUTPHjxsAUi-qJCY.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxpupuli/puppetboard/6b6e8dfbbd9b35ce540598f6dde4811e544db446/puppetboard/static/fonts/lato/S6u8w4BMUTPHjxsAUi-qJCY.woff2 -------------------------------------------------------------------------------- /puppetboard/static/fonts/lato/S6u8w4BMUTPHjxsAXC-q.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxpupuli/puppetboard/6b6e8dfbbd9b35ce540598f6dde4811e544db446/puppetboard/static/fonts/lato/S6u8w4BMUTPHjxsAXC-q.woff2 -------------------------------------------------------------------------------- /puppetboard/static/fonts/lato/S6u9w4BMUTPHh6UVSwaPGR_p.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxpupuli/puppetboard/6b6e8dfbbd9b35ce540598f6dde4811e544db446/puppetboard/static/fonts/lato/S6u9w4BMUTPHh6UVSwaPGR_p.woff2 -------------------------------------------------------------------------------- /puppetboard/static/fonts/lato/S6u9w4BMUTPHh6UVSwiPGQ.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxpupuli/puppetboard/6b6e8dfbbd9b35ce540598f6dde4811e544db446/puppetboard/static/fonts/lato/S6u9w4BMUTPHh6UVSwiPGQ.woff2 -------------------------------------------------------------------------------- /puppetboard/static/fonts/lato/S6u_w4BMUTPHjxsI5wq_FQft1dw.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxpupuli/puppetboard/6b6e8dfbbd9b35ce540598f6dde4811e544db446/puppetboard/static/fonts/lato/S6u_w4BMUTPHjxsI5wq_FQft1dw.woff2 -------------------------------------------------------------------------------- /puppetboard/static/fonts/lato/S6u_w4BMUTPHjxsI5wq_Gwft.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxpupuli/puppetboard/6b6e8dfbbd9b35ce540598f6dde4811e544db446/puppetboard/static/fonts/lato/S6u_w4BMUTPHjxsI5wq_Gwft.woff2 -------------------------------------------------------------------------------- /puppetboard/static/fonts/lato/S6uyw4BMUTPHjx4wXg.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxpupuli/puppetboard/6b6e8dfbbd9b35ce540598f6dde4811e544db446/puppetboard/static/fonts/lato/S6uyw4BMUTPHjx4wXg.woff2 -------------------------------------------------------------------------------- /puppetboard/static/fonts/lato/S6uyw4BMUTPHjxAwXjeu.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxpupuli/puppetboard/6b6e8dfbbd9b35ce540598f6dde4811e544db446/puppetboard/static/fonts/lato/S6uyw4BMUTPHjxAwXjeu.woff2 -------------------------------------------------------------------------------- /puppetboard/static/fonts/open-sans/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4gaVI.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxpupuli/puppetboard/6b6e8dfbbd9b35ce540598f6dde4811e544db446/puppetboard/static/fonts/open-sans/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4gaVI.woff2 -------------------------------------------------------------------------------- /puppetboard/static/fonts/open-sans/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4iaVIGxA.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxpupuli/puppetboard/6b6e8dfbbd9b35ce540598f6dde4811e544db446/puppetboard/static/fonts/open-sans/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4iaVIGxA.woff2 -------------------------------------------------------------------------------- /puppetboard/static/fonts/open-sans/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4jaVIGxA.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxpupuli/puppetboard/6b6e8dfbbd9b35ce540598f6dde4811e544db446/puppetboard/static/fonts/open-sans/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4jaVIGxA.woff2 -------------------------------------------------------------------------------- /puppetboard/static/fonts/open-sans/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4kaVIGxA.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxpupuli/puppetboard/6b6e8dfbbd9b35ce540598f6dde4811e544db446/puppetboard/static/fonts/open-sans/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4kaVIGxA.woff2 -------------------------------------------------------------------------------- /puppetboard/static/fonts/open-sans/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4saVIGxA.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxpupuli/puppetboard/6b6e8dfbbd9b35ce540598f6dde4811e544db446/puppetboard/static/fonts/open-sans/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4saVIGxA.woff2 -------------------------------------------------------------------------------- /puppetboard/static/fonts/open-sans/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4taVIGxA.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxpupuli/puppetboard/6b6e8dfbbd9b35ce540598f6dde4811e544db446/puppetboard/static/fonts/open-sans/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4taVIGxA.woff2 -------------------------------------------------------------------------------- /puppetboard/static/fonts/open-sans/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4uaVIGxA.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxpupuli/puppetboard/6b6e8dfbbd9b35ce540598f6dde4811e544db446/puppetboard/static/fonts/open-sans/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4uaVIGxA.woff2 -------------------------------------------------------------------------------- /puppetboard/static/fonts/open-sans/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4vaVIGxA.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxpupuli/puppetboard/6b6e8dfbbd9b35ce540598f6dde4811e544db446/puppetboard/static/fonts/open-sans/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4vaVIGxA.woff2 -------------------------------------------------------------------------------- /puppetboard/static/js/dailychart.js: -------------------------------------------------------------------------------- 1 | jQuery(function ($) { 2 | let url = "daily_reports_chart.json"; 3 | let certname = $("#dailyReportsChart").data("certname"); 4 | let days = parseInt($("#dailyReportsChart").data("days")); 5 | let defaultJSON = [] 6 | 7 | for (let index = days-1; index >= 0; index--) { 8 | defaultJSON.push({ 9 | day: moment().startOf('day').subtract(index, 'days').format('YYYY-MM-DD'), 10 | unchanged: 0, 11 | changed: 0, 12 | failed: 0 13 | }) 14 | } 15 | 16 | let chart = bb.generate({ 17 | bindto: "#dailyReportsChart", 18 | data: { 19 | type: "bar", 20 | json: defaultJSON, 21 | keys: { 22 | x: "day", 23 | value: ["failed", "changed", "unchanged"], 24 | }, 25 | groups: [["failed", "changed", "unchanged"]], 26 | colors: { 27 | // Must match CSS colors 28 | failed: "#AA4643", 29 | changed: "#4572A7", 30 | unchanged: "#89A54E", 31 | }, 32 | }, 33 | size: { 34 | height: 160, 35 | }, 36 | axis: { 37 | x: { 38 | type: "category", 39 | }, 40 | }, 41 | }); 42 | 43 | if (typeof certname !== typeof undefined && certname !== false) { 44 | // truncate /node/certname from URL, to determine path to json 45 | url = 46 | window.location.href.replace(/\/node\/[^/]+$/, "") + 47 | "/daily_reports_chart.json?certname=" + 48 | certname 49 | } 50 | 51 | $.getJSON(url, function(data) { 52 | chart.load({json: data.result}) 53 | }); 54 | }) 55 | -------------------------------------------------------------------------------- /puppetboard/static/js/pretty.js: -------------------------------------------------------------------------------- 1 | function pretty_print(to_show) { 2 | 3 | if (Object.prototype.toString.call(to_show) === "[object String]") { 4 | 5 | // Print plain string as-is to avoid making it surrounded with "" 6 | to_show = '' + to_show + ''; 7 | 8 | } else { 9 | 10 | // Pretty-print the JSON, with syntax highlight 11 | // Based on https://stackoverflow.com/a/7220510/2693875 12 | 13 | let is_complex = false; 14 | 15 | to_show = JSON.stringify(to_show, null, 4); // spacing level = 4 16 | to_show = to_show.replace(/&/g, '&').replace(//g, '>'); 17 | to_show = to_show.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) { 18 | let cls = 'number'; 19 | if (/^"/.test(match)) { 20 | if (/:$/.test(match)) { 21 | is_complex = true 22 | cls = 'key'; 23 | } else { 24 | cls = 'string'; 25 | } 26 | } else if (/true|false/.test(match)) { 27 | cls = 'boolean'; 28 | } else if (/null/.test(match)) { 29 | cls = 'null'; 30 | } 31 | return '' + match + ''; 32 | }); 33 | 34 | if (is_complex) { 35 | // Add pre tag for indentation to be visible 36 | to_show = '
' + to_show + '
' 37 | } 38 | 39 | } 40 | 41 | return to_show 42 | } 43 | -------------------------------------------------------------------------------- /puppetboard/static/js/radiator.js: -------------------------------------------------------------------------------- 1 | function resizeMe() { 2 | var preferredHeight = 944; 3 | var displayHeight = $(window).height(); 4 | var percentageHeight = displayHeight / preferredHeight; 5 | 6 | var preferredWidth = 1100; 7 | var displayWidth = $(window).width(); 8 | var percentageWidth = displayWidth / preferredWidth; 9 | 10 | var newFontSize; 11 | if (percentageHeight < percentageWidth) { 12 | newFontSize = Math.floor("960" * percentageHeight) - 30; 13 | } else { 14 | newFontSize = Math.floor("960" * percentageWidth) - 30; 15 | } 16 | $("body").css("font-size", newFontSize + "%") 17 | } 18 | 19 | $(document).ready(function() { 20 | $(window).on('resize', resizeMe).trigger('resize'); 21 | }) 22 | -------------------------------------------------------------------------------- /puppetboard/static/js/scroll.top.js: -------------------------------------------------------------------------------- 1 | jQuery(function($) { 2 | $(document).scroll(function() { 3 | if ( $(window).scrollTop() > 100 ) { 4 | $('#scroll-btn-top').addClass('show'); 5 | } else { 6 | $('#scroll-btn-top').removeClass('show'); 7 | } 8 | }); 9 | 10 | $('#scroll-btn-top').click(function() { 11 | $('html, body').animate( { scrollTop: 0 }, 500 ); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /puppetboard/static/js/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Simple debounce function 3 | */ 4 | debounce = function(cb, delay = 250) { 5 | let timeout 6 | 7 | return (...args) => { 8 | clearTimeout(timeout) 9 | timeout = setTimeout(() => { 10 | cb(...args) 11 | }, delay) 12 | } 13 | } 14 | 15 | /** 16 | * Transform date string with tz to shorter string. 17 | * If localisation enabled in config, date is timezoned with browser 18 | * If localisation disabled in config, date is UTC 19 | */ 20 | $.fn.transformDatetime = function (format = "MMM DD YYYY - HH:mm:ss") { 21 | this.each(function () { 22 | let $el = $(this) 23 | let localise = $el.data("localise") 24 | let dt = moment($el.text()) 25 | 26 | if (!dt.isValid()) return 27 | 28 | if (localise) { 29 | $el.text(dt.format("MMM DD YYYY - HH:mm:ss")) 30 | } else { 31 | $el.text(dt.utc().format("MMM DD YYYY - HH:mm:ss")) 32 | } 33 | }) 34 | return this 35 | } 36 | 37 | 38 | /** 39 | * Enable filtering on fact list 40 | * 41 | * If text is entered as input, it will use that text to create a regexp 42 | * that will match each fact name. 43 | * If a match is found, make sure the fact name is visible. 44 | * If no match is found, make sure the fact name is hidden. 45 | * Then, we display all segments and hide all those that do not have 46 | * at least a visible fact name. 47 | */ 48 | $.fn.factList = function () { 49 | this.each(function() { 50 | const $el = $(this) 51 | const $input = $el.find("input[name=filter]") 52 | const $segments = $el.find(".segment") 53 | const $facts = $el.find(".segment li") 54 | 55 | $input.on("input", debounce(function (e) { 56 | const text = $input.val() 57 | 58 | if (!text) { 59 | $segments.show() 60 | $facts.show() 61 | return 62 | } 63 | 64 | const pattern = new RegExp(text, "i") 65 | 66 | $facts.each(function () { 67 | const $fact = $(this) 68 | 69 | if (pattern.test($fact.text())) { 70 | $fact.show() 71 | } else { 72 | $fact.hide() 73 | } 74 | }) 75 | $segments.show() 76 | $segments.not(':has(li:visible)').hide() 77 | }, 250)) 78 | }) 79 | 80 | return this 81 | } 82 | 83 | $(document).ready(function () { 84 | $("[data-localise]").transformDatetime() 85 | 86 | let dataTable = $("#main-table").DataTable() 87 | $("#main-table-search").on("input", function () { 88 | dataTable.search(this.value).draw() 89 | }) 90 | 91 | $('#fact-list').factList() 92 | }) 93 | -------------------------------------------------------------------------------- /puppetboard/static/libs/billboard.js/billboard.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2017 ~ present NAVER Corp. 3 | * billboard.js project is licensed under the MIT license 4 | * 5 | * billboard.js, JavaScript chart library 6 | * https://naver.github.io/billboard.js/ 7 | * 8 | * @version 3.5.1 9 | */.bb svg{-webkit-tap-highlight-color:rgba(0,0,0,0);font:10px sans-serif}.bb line,.bb path{fill:none;stroke:#000}.bb .bb-button,.bb text{-webkit-user-select:none;-moz-user-select:none;user-select:none}.bb-bars path,.bb-event-rect,.bb-legend-item-tile,.bb-xgrid-focus,.bb-ygrid,.bb-ygrid-focus{shape-rendering:crispEdges}.bb-chart-arc .bb-gauge-value{fill:#000}.bb-chart-arc path{stroke:#fff}.bb-chart-arc rect{stroke:#fff;stroke-width:1}.bb-chart-arc text{fill:#fff;font-size:13px}.bb-axis{shape-rendering:crispEdges}.bb-grid{pointer-events:none}.bb-grid line{stroke:#aaa}.bb-grid text{fill:#aaa}.bb-xgrid,.bb-ygrid{stroke-dasharray:3 3}.bb-text.bb-empty{fill:grey;font-size:2em}.bb-line{stroke-width:1px}.bb-circle._expanded_{stroke-width:1px;stroke:#fff}.bb-selected-circle{fill:#fff;stroke-width:2px}.bb-bar{stroke-width:0}.bb-bar._expanded_{fill-opacity:.75}.bb-candlestick{stroke-width:1px}.bb-candlestick._expanded_{fill-opacity:.75}.bb-circles.bb-focused,.bb-target.bb-focused{opacity:1}.bb-circles.bb-focused path.bb-line,.bb-circles.bb-focused path.bb-step,.bb-target.bb-focused path.bb-line,.bb-target.bb-focused path.bb-step{stroke-width:2px}.bb-circles.bb-defocused,.bb-target.bb-defocused{opacity:.3!important}.bb-circles.bb-defocused .text-overlapping,.bb-target.bb-defocused .text-overlapping{opacity:.05!important}.bb-region{fill:#4682b4}.bb-brush .extent,.bb-region,.bb-zoom-brush{fill-opacity:.1}.bb-legend-item{font-size:12px;user-select:none}.bb-legend-item-hidden{opacity:.15}.bb-legend-background{fill:#fff;stroke:#d3d3d3;stroke-width:1;opacity:.75}.bb-title{font:14px sans-serif}.bb-tooltip-container{user-select:none;z-index:10}.bb-tooltip{background-color:#fff;border-collapse:collapse;border-spacing:0;-webkit-box-shadow:7px 7px 12px -9px #777;-moz-box-shadow:7px 7px 12px -9px #777;box-shadow:7px 7px 12px -9px #777;empty-cells:show;opacity:.9}.bb-tooltip tr{border:1px solid #ccc}.bb-tooltip th{background-color:#aaa;color:#fff;font-size:14px;padding:2px 5px;text-align:left}.bb-tooltip td{background-color:#fff;border-left:1px dotted #999;font-size:13px;padding:3px 6px}.bb-tooltip td>span,.bb-tooltip td>svg{display:inline-block;height:10px;margin-right:6px;width:10px}.bb-tooltip.value{text-align:right}.bb-area{stroke-width:0;opacity:.2}.bb-chart-arcs-title{dominant-baseline:middle;font-size:1.3em}text.bb-chart-arcs-gauge-title{dominant-baseline:middle;font-size:2.7em}.bb-chart-arcs .bb-chart-arcs-background{fill:#e0e0e0;stroke:#fff}.bb-chart-arcs .bb-chart-arcs-gauge-unit{fill:#000;font-size:16px}.bb-chart-arcs .bb-chart-arcs-gauge-max,.bb-chart-arcs .bb-chart-arcs-gauge-min{fill:#777}.bb-chart-arcs .bb-levels circle{fill:none;stroke:#848282;stroke-width:.5px}.bb-chart-arcs .bb-levels text{fill:#848282}.bb-chart-radars .bb-levels polygon{fill:none;stroke:#848282;stroke-width:.5px}.bb-chart-radars .bb-levels text{fill:#848282}.bb-chart-radars .bb-axis line{stroke:#848282;stroke-width:.5px}.bb-chart-radars .bb-axis text{cursor:default;font-size:1.15em}.bb-chart-radars .bb-shapes polygon{fill-opacity:.2;stroke-width:1px}.bb-button{position:absolute;right:10px;top:10px}.bb-button .bb-zoom-reset{background-color:#fff;border:1px solid #ccc;border-radius:5px;cursor:pointer;font-size:11px;padding:5px} -------------------------------------------------------------------------------- /puppetboard/static/libs/datatables.net-buttons-se/buttons.semanticui.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | Bootstrap integration for DataTables' Buttons 3 | ©2016 SpryMedia Ltd - datatables.net/license 4 | */ 5 | (function(b){"function"===typeof define&&define.amd?define(["jquery","datatables.net-se","datatables.net-buttons"],function(a){return b(a,window,document)}):"object"===typeof exports?module.exports=function(a,c){a||(a=window);c&&c.fn.dataTable||(c=require("datatables.net-se")(a,c).$);c.fn.dataTable.Buttons||require("datatables.net-buttons")(a,c);return b(c,a,a.document)}:b(jQuery,window,document)})(function(b,a,c,e){a=b.fn.dataTable;b.extend(!0,a.Buttons.defaults,{dom:{container:{className:"dt-buttons ui basic buttons"}, 6 | button:{tag:"button",className:"dt-button ui button",spacerClass:"dt-button ui button"},collection:{tag:"div",className:"ui basic vertical buttons",closeButton:!1},splitWrapper:{tag:"div",className:"dt-btn-split-wrapper buttons",closeButton:!1},splitDropdown:{tag:"button",text:"▼",className:"ui floating button dt-btn-split-drop dropdown icon",closeButton:!1},splitDropdownButton:{tag:"button",className:"dt-btn-split-drop-button ui button",closeButton:!1}}});b(c).on("buttons-popover.dt",function(){var d= 7 | !1;b(".dtsp-panesContainer").each(function(){b(this).is("button")||(d=!0)});d&&b(".dtsp-panesContainer").removeClass("vertical buttons")});return a.Buttons}); 8 | -------------------------------------------------------------------------------- /puppetboard/static/libs/datatables.net-buttons/buttons.colVis.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | Column visibility buttons for Buttons and DataTables. 3 | 2016 SpryMedia Ltd - datatables.net/license 4 | */ 5 | (function(h){"function"===typeof define&&define.amd?define(["jquery","datatables.net","datatables.net-buttons"],function(e){return h(e,window,document)}):"object"===typeof exports?module.exports=function(e,g){e||(e=window);g&&g.fn.dataTable||(g=require("datatables.net")(e,g).$);g.fn.dataTable.Buttons||require("datatables.net-buttons")(e,g);return h(g,e,e.document)}:h(jQuery,window,document)})(function(h,e,g,l){e=h.fn.dataTable;h.extend(e.ext.buttons,{colvis:function(b,a){var c=null,d={extend:"collection", 6 | init:function(f,k){c=k},text:function(f){return f.i18n("buttons.colvis","Column visibility")},className:"buttons-colvis",closeButton:!1,buttons:[{extend:"columnsToggle",columns:a.columns,columnText:a.columnText}]};b.on("column-reorder.dt"+a.namespace,function(f,k,m){b.button(null,b.button(null,c).node()).collectionRebuild([{extend:"columnsToggle",columns:a.columns,columnText:a.columnText}])});return d},columnsToggle:function(b,a){return b.columns(a.columns).indexes().map(function(c){return{extend:"columnToggle", 7 | columns:c,columnText:a.columnText}}).toArray()},columnToggle:function(b,a){return{extend:"columnVisibility",columns:a.columns,columnText:a.columnText}},columnsVisibility:function(b,a){return b.columns(a.columns).indexes().map(function(c){return{extend:"columnVisibility",columns:c,visibility:a.visibility,columnText:a.columnText}}).toArray()},columnVisibility:{columns:l,text:function(b,a,c){return c._columnText(b,c)},className:"buttons-columnVisibility",action:function(b,a,c,d){b=a.columns(d.columns); 8 | a=b.visible();b.visible(d.visibility!==l?d.visibility:!(a.length&&a[0]))},init:function(b,a,c){var d=this;a.attr("data-cv-idx",c.columns);b.on("column-visibility.dt"+c.namespace,function(f,k){k.bDestroying||k.nTable!=b.settings()[0].nTable||d.active(b.column(c.columns).visible())}).on("column-reorder.dt"+c.namespace,function(f,k,m){c.destroying||1!==b.columns(c.columns).count()||(d.text(c._columnText(b,c)),d.active(b.column(c.columns).visible()))});this.active(b.column(c.columns).visible())},destroy:function(b, 9 | a,c){b.off("column-visibility.dt"+c.namespace).off("column-reorder.dt"+c.namespace)},_columnText:function(b,a){var c=b.column(a.columns).index(),d=b.settings()[0].aoColumns[c].sTitle;d||(d=b.column(c).header().innerHTML);d=d.replace(/\n/g," ").replace(//gi," ").replace(//g,"").replace(//g,"").replace(/<.*?>/g,"").replace(/^\s+|\s+$/g,"");return a.columnText?a.columnText(b,c,d):d}},colvisRestore:{className:"buttons-colvisRestore",text:function(b){return b.i18n("buttons.colvisRestore", 10 | "Restore visibility")},init:function(b,a,c){c._visOriginal=b.columns().indexes().map(function(d){return b.column(d).visible()}).toArray()},action:function(b,a,c,d){a.columns().every(function(f){f=a.colReorder&&a.colReorder.transpose?a.colReorder.transpose(f,"toOriginal"):f;this.visible(d._visOriginal[f])})}},colvisGroup:{className:"buttons-colvisGroup",action:function(b,a,c,d){a.columns(d.show).visible(!0,!1);a.columns(d.hide).visible(!1,!1);a.columns.adjust()},show:[],hide:[]}});return e.Buttons}); 11 | -------------------------------------------------------------------------------- /puppetboard/static/libs/datatables.net-se/dataTables.semanticui.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | DataTables Bootstrap 3 integration 3 | ©2011-2015 SpryMedia Ltd - datatables.net/license 4 | */ 5 | var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.findInternal=function(a,b,c){a instanceof String&&(a=String(a));for(var e=a.length,d=0;d<'right aligned eight wide column'f>><'row dt-table'<'sixteen wide column'tr>><'row'<'seven wide column'i><'right aligned nine wide column'p>>>", 12 | renderer:"semanticUI"});a.extend(d.ext.classes,{sWrapper:"dataTables_wrapper dt-semanticUI",sFilter:"dataTables_filter ui input",sProcessing:"dataTables_processing ui segment",sPageButton:"paginate_button item"});d.ext.renderer.pageButton.semanticUI=function(f,l,B,C,m,u){var v=new d.Api(f),D=f.oClasses,n=f.oLanguage.oPaginate,E=f.oLanguage.oAria.paginate||{},h,k,w=0,z=function(q,x){var y,F=function(p){p.preventDefault();a(p.currentTarget).hasClass("disabled")||v.page()==p.data.action||v.page(p.data.action).draw("page")}; 13 | var r=0;for(y=x.length;r",{"class":D.sPageButton+" "+k,id:0===B&&"string"=== 14 | typeof g?f.sTableId+"_"+g:null,href:"#","aria-controls":f.sTableId,"aria-label":E[g],"data-dt-idx":w,tabindex:f.iTabIndex}).html(h).appendTo(q),f.oApi._fnBindAction(t,{action:g},F),w++)}}};try{var A=a(l).find(c.activeElement).data("dt-idx")}catch(q){}z(a(l).empty().html('