├── .dockerignore ├── .flake8 ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── question.md └── workflows │ ├── docker.yml │ └── main.yml ├── .gitignore ├── .readthedocs.yaml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── GeoHealthCheck ├── __init__.py ├── app.py ├── check.py ├── config_main.py ├── enums.py ├── factory.py ├── geocoder.py ├── healthcheck.py ├── init.py ├── manage.py ├── migrations │ ├── README.md │ ├── __init__.py │ ├── alembic.ini │ ├── alembic_helpers.py │ ├── env.py │ ├── script.py.mako │ └── versions │ │ ├── 2638c2a40625_.py │ │ ├── 3381760e2a8c_.py │ │ ├── 34531bfd7cab_.py │ │ ├── 496427d03f87_.py │ │ ├── 90e1c865a561_.py │ │ ├── 933717a14052_.py │ │ ├── 992013af402f_.py │ │ ├── __init__.py │ │ ├── bb91fb332c36_.py │ │ └── f72ff1ac3967_.py ├── models.py ├── notifications.py ├── plugin.py ├── plugins │ ├── __init__.py │ ├── check │ │ ├── __init__.py │ │ └── checks.py │ ├── geocode │ │ ├── fixedlocation.py │ │ └── webgeocoder.py │ ├── probe │ │ ├── __init__.py │ │ ├── esrifs.py │ │ ├── ghcreport.py │ │ ├── http.py │ │ ├── mapbox.py │ │ ├── ogc3dtiles.py │ │ ├── ogcfeat.py │ │ ├── owsgetcaps.py │ │ ├── sta.py │ │ ├── tms.py │ │ ├── wcs.py │ │ ├── wfs.py │ │ ├── wms.py │ │ ├── wmsdrilldown.py │ │ └── wmts.py │ ├── resourceauth │ │ ├── __init__.py │ │ └── resourceauths.py │ └── user │ │ ├── README.md │ │ └── __init__.py ├── probe.py ├── resourceauth.py ├── result.py ├── scheduler.py ├── static │ ├── lib │ │ ├── jqueryui │ │ │ ├── jquery-ui.css │ │ │ ├── jquery-ui.js │ │ │ ├── jquery-ui.min.css │ │ │ ├── jquery-ui.min.js │ │ │ ├── jquery-ui.structure.css │ │ │ ├── jquery-ui.structure.min.css │ │ │ ├── jquery-ui.theme.css │ │ │ └── jquery-ui.theme.min.css │ │ └── jspark │ │ │ └── jspark.js │ └── site │ │ ├── css │ │ └── dashboard.css │ │ └── js │ │ ├── resources_list.js │ │ └── runs_chart.js ├── templates │ ├── add.html │ ├── edit_resource.html │ ├── home.html │ ├── includes │ │ ├── check_edit_form.html │ │ ├── check_info.html │ │ ├── overall_status.html │ │ ├── probe_edit_form.html │ │ ├── probe_info.html │ │ ├── resources_list.html │ │ └── runs.html │ ├── layout.html │ ├── login.html │ ├── notification_email.txt │ ├── opensearch_description.xml │ ├── register.html │ ├── reset_password_email.txt │ ├── reset_password_form.html │ ├── reset_password_request.html │ ├── resource.html │ ├── resources.html │ ├── runs.html │ └── status_report_email.txt ├── translations │ ├── de │ │ └── LC_MESSAGES │ │ │ └── messages.po │ ├── en │ │ └── LC_MESSAGES │ │ │ └── messages.po │ ├── es_BO │ │ └── LC_MESSAGES │ │ │ └── messages.po │ ├── fr │ │ └── LC_MESSAGES │ │ │ └── messages.po │ ├── hr_HR │ │ └── LC_MESSAGES │ │ │ └── messages.po │ ├── nl_NL │ │ └── LC_MESSAGES │ │ │ └── messages.po │ └── pt_BR │ │ └── LC_MESSAGES │ │ └── messages.po ├── util.py └── views.py ├── LICENSE ├── README.md ├── SECURITY.md ├── VERSION ├── babel.cfg ├── docker ├── README.md ├── compose │ ├── README.md │ ├── docker-compose.postgis.yml │ ├── docker-compose.yml │ ├── ghc-postgis.env │ └── ghc.env ├── config_site.py ├── docker-clean.sh ├── install-docker-ubuntu.sh ├── plugins │ ├── README.md │ └── user │ │ ├── __init.py__ │ │ └── mywmsprobe.py └── scripts │ ├── configure.sh │ ├── cron-jobs-daily.sh │ ├── cron-jobs-hourly.sh │ ├── install.sh │ ├── requirements.txt │ ├── run-runner.sh │ ├── run-tests.sh │ ├── run-web.sh │ └── set-timezone.sh ├── docs ├── Makefile ├── _static │ ├── architecture.odg │ ├── datamodel.png │ ├── ghc-parts.jpg │ ├── logo_medium.png │ ├── notifications_config.png │ └── userguide │ │ ├── add-resource-1-s.png │ │ ├── add-resource-1.png │ │ ├── add-resource-2-s.png │ │ ├── add-resource-2.png │ │ ├── dashboard-home-s.png │ │ ├── dashboard-home.png │ │ ├── edit-resource-1-s.png │ │ ├── edit-resource-1.png │ │ ├── edit-resource-2-s.png │ │ ├── edit-resource-2.png │ │ ├── edit-resource-3-s.png │ │ ├── edit-resource-3.png │ │ ├── email-notification-fail.png │ │ ├── email-notification-ok.png │ │ ├── register-s.png │ │ ├── register.png │ │ ├── wms-resource-history-detail-s.png │ │ ├── wms-resource-history-detail.png │ │ ├── wms-resource-history-s.png │ │ ├── wms-resource-history.png │ │ ├── wms-resource-s.png │ │ ├── wms-resource.png │ │ ├── wms-resources-s.png │ │ └── wms-resources.png ├── admin.rst ├── architecture.rst ├── conf.py ├── config.rst ├── contact.rst ├── index.rst ├── install.rst ├── license.rst ├── make.bat ├── plugins.rst ├── requirements.txt └── userguide.rst ├── jobs.cron ├── pavement.py ├── requirements-dev.txt ├── requirements.txt └── tests ├── data ├── README.md ├── fixtures.json ├── minimal.json └── resources.json ├── run_tests.py ├── test_plugins.py └── test_resources.py /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 79 3 | exclude = .git,.cache,docs,docker,build,dist,GeoHealthCheck/migrations 4 | # max-complexity = 38 5 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: ["https://www.paypal.me/nlopen"] 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "(replace with your title)" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is (one or two lines). 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior, e.g.: 15 | 16 | 1. Go to '...' 17 | 2. Click on '....' 18 | 3. Scroll down to '....' 19 | 4. Seeing error 20 | 21 | **Expected Behavior** 22 | A clear and concise description of what you expected to happen. 23 | 24 | **Screenshots or Logfiles** 25 | If applicable, add screenshots or logfile texts, to help explain your problem. 26 | 27 | **Context (please complete the following information):** 28 | - OS: [e.g. Mac OSX version] 29 | - Browser [e.g. chrome, safari] 30 | - Browser Version [e.g. 22] 31 | - Python Version [e.g. 2.7] 32 | - GeoHealthCheck Version [e.g. 0.7.0] 33 | 34 | If running with Docker: 35 | - Docker installed version 36 | - GeoHealthCheck Docker Image version 37 | 38 | **Additional context** 39 | Add any other context about the problem here. 40 | -------------------------------------------------------------------------------- /.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 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: I have a question 4 | title: "(Put question subject here)" 5 | labels: question 6 | assignees: '' 7 | 8 | --- 9 | 10 | If you have a specific question, for example on installation guidance or Plugin development, you can pose this below. It is probably easier/quicker to pose your question on the GeoHealthCheck Gitter Channel: https://gitter.im/geopython/GeoHealthCheck, an interactive chat-channel where GHC developers and users gather. 11 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | # Triggers a Docker workflow on push events and PRs but 2 | # pushes to DockerHub only for push on the master branch. 3 | # Runs GHC unit tests before DockerHub push. 4 | # 5 | # Author: Just van den Broecke - 2021 6 | # 7 | name: Docker Build ⚓ 8 | 9 | on: 10 | push: 11 | branches: 12 | - master 13 | paths-ignore: 14 | - '**.md' 15 | 16 | pull_request: 17 | paths-ignore: 18 | - '**.md' 19 | 20 | jobs: 21 | # Single job now to build Docker Image, run GHC unit tests, and push to DockerHub 22 | build_test_push: 23 | 24 | name: Build, Test and Push Docker Image to DockerHub 25 | 26 | runs-on: ubuntu-latest 27 | 28 | # v2 https://github.com/docker/build-push-action/blob/master/UPGRADE.md 29 | steps: 30 | - name: Checkout ✅ 31 | uses: actions/checkout@v2 32 | 33 | - name: Prepare 📦 34 | id: prep 35 | run: | 36 | DOCKER_IMAGE=geopython/geohealthcheck 37 | VERSION=latest 38 | if [[ $GITHUB_REF == refs/tags/* ]]; then 39 | VERSION=${GITHUB_REF#refs/tags/} 40 | elif [[ $GITHUB_REF == refs/heads/* ]]; then 41 | VERSION=$(echo ${GITHUB_REF#refs/heads/} | sed -r 's#/+#-#g') 42 | elif [[ $GITHUB_REF == refs/pull/* ]]; then 43 | VERSION=pr-${{ github.event.number }} 44 | fi 45 | if [[ $VERSION == master ]]; then 46 | VERSION=latest 47 | fi 48 | TAGS="${DOCKER_IMAGE}:${VERSION}" 49 | echo ::set-output name=image::${DOCKER_IMAGE} 50 | echo ::set-output name=version::${VERSION} 51 | echo ::set-output name=tags::${TAGS} 52 | echo ::set-output name=created::$(date -u +'%Y-%m-%dT%H:%M:%SZ') 53 | 54 | - name: Show Image Settings 📦 55 | run: echo "IMAGE=${{ steps.prep.outputs.image }} VERSION=${{ steps.prep.outputs.version }} TAGS=${{ steps.prep.outputs.tags }}" 56 | 57 | - name: Set up Docker Buildx 📦 58 | uses: docker/setup-buildx-action@v1 59 | 60 | - name: Login to DockerHub 📦 61 | if: github.event_name != 'pull_request' 62 | uses: docker/login-action@v1 63 | with: 64 | username: ${{ secrets.DOCKER_USERNAME }} 65 | password: ${{ secrets.DOCKER_PASSWORD }} 66 | 67 | - name: Docker Build only - retain local Image 📦 68 | uses: docker/build-push-action@v2 69 | with: 70 | context: . 71 | load: true 72 | push: false 73 | tags: ${{ steps.prep.outputs.tags }} 74 | labels: | 75 | org.opencontainers.image.source=${{ github.event.repository.html_url }} 76 | org.opencontainers.image.created=${{ steps.prep.outputs.created }} 77 | org.opencontainers.image.revision=${{ github.sha }} 78 | 79 | - name: GHC Unit Tests with Docker Image ⚙️ 80 | run: docker run --entrypoint "/run-tests.sh" ${{ steps.prep.outputs.image }}:${{ steps.prep.outputs.version }} 81 | 82 | - name: Push to Docker repo (on GH Push only) ☁️ 83 | if: ${{ github.event_name == 'push' }} 84 | run: docker push ${{ steps.prep.outputs.image }}:${{ steps.prep.outputs.version }} 85 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # Main GHC CI workflow. Inspired by 2 | # https://github.com/geopython/pycsw/blob/master/.github/workflows/main.yml 3 | # 4 | # Author: Just van den Broecke - 2021 5 | # 6 | name: Main GHC CI ⚙️ 7 | 8 | on: [ push, pull_request ] 9 | 10 | jobs: 11 | main: 12 | runs-on: ubuntu-20.04 13 | strategy: 14 | matrix: 15 | include: 16 | - python-version: 3.7 17 | # - python-version: 3.8 18 | steps: 19 | - name: Checkout ✅ 20 | uses: actions/checkout@v2 21 | 22 | - name: Setup Python ${{ matrix.python-version }} 🐍 23 | uses: actions/setup-python@v2 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | 27 | - name: Install Requirements 📦 28 | run: | 29 | python -m pip install --upgrade pip 30 | pip install wheel 31 | pip install -r requirements.txt 32 | pip install -r requirements-dev.txt 33 | 34 | - name: Setup GHC App and init DB 🗃️ 35 | run: | 36 | paver setup 37 | echo -e "admin\ntest\ntest\nyou@example.com\nyou@example.com" | python GeoHealthCheck/models.py create 38 | 39 | - name: Flake8 - Verify Coding Conventions ⚙️ 40 | run: flake8 41 | 42 | - name: Load Fixtures Test Data ⚙️ 43 | run: python GeoHealthCheck/models.py load tests/data/fixtures.json y 44 | 45 | - name: Run Probes ⚙️ 46 | run: python GeoHealthCheck/healthcheck.py 47 | 48 | - name: Run Unit Tests ⚙️ 49 | run: python tests/run_tests.py 50 | 51 | - name: Build Docs 📖 52 | run: cd docs && make html 53 | 54 | - name: Cleanup 💯 55 | run: python GeoHealthCheck/models.py drop 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | venv/ 12 | bin/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # Installer logs 27 | pip-log.txt 28 | pip-delete-this-directory.txt 29 | 30 | # Unit test / coverage reports 31 | htmlcov/ 32 | .tox/ 33 | .coverage 34 | .cache 35 | nosetests.xml 36 | coverage.xml 37 | 38 | # Translations 39 | *.mo 40 | 41 | # Mr Developer 42 | .mr.developer.cfg 43 | .project 44 | .pydevproject 45 | 46 | # Rope 47 | .ropeproject 48 | 49 | # Django stuff: 50 | *.log 51 | *.pot 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # Flask 57 | instance 58 | tmp/ 59 | GeoHealthCheck/static/docs 60 | GeoHealthCheck/static/lib 61 | GeoHealthCheck.wsgi 62 | GeoHealthCheck.conf 63 | 64 | # Data 65 | GeoHealthCheck/data.db 66 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-22.04 11 | tools: 12 | python: "3.11" 13 | 14 | # Build documentation in the docs/ directory with Sphinx 15 | sphinx: 16 | configuration: docs/conf.py 17 | 18 | # We recommend specifying your dependencies to enable reproducible builds: 19 | # https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 20 | python: 21 | install: 22 | - requirements: docs/requirements.txt 23 | 24 | formats: 25 | - pdf 26 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at coc@osgeo.org. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to GeoHealthCheck 2 | 3 | We welcome contributions to GeoHealthCheck, in the form of issues, bug fixes, or 4 | suggestions for enhancements. This document sets out our guidelines and best 5 | practices for such contributions. 6 | 7 | It's based on the [Contributing to Open Source Projects 8 | Guide](https://contribution-guide-org.readthedocs.io/). 9 | 10 | GeoHealthCheck has the following modes of contribution: 11 | 12 | - GitHub Commit Access 13 | - GitHub Pull Requests 14 | 15 | ## Code of Conduct 16 | 17 | Contributors to this project are expected to act respectfully toward others in accordance with the [OSGeo Code of Conduct](https://www.osgeo.org/code_of_conduct). 18 | 19 | ## Submitting Bugs 20 | 21 | ### Due Diligence 22 | 23 | Before submitting a bug, please do the following: 24 | 25 | * Perform __basic troubleshooting__ steps: 26 | 27 | * __Make sure you're on the latest version.__ If you're not on the most 28 | recent version, your problem may have been solved already! Upgrading is 29 | always the best first step. 30 | * [__Search the issue 31 | tracker__](https://github.com/geopython/GeoHealthCheck/issues) 32 | to make sure it's not a known issue. 33 | 34 | ### What to put in your bug report 35 | 36 | Make sure your report gets the attention it deserves: bug reports with missing 37 | information may be ignored or punted back to you, delaying a fix. The below 38 | constitutes a bare minimum; more info is almost always better: 39 | 40 | * __What version of Python are you using?__ For example, are you using Python 41 | 2.7, Python 3.7, PyPy 2.0? 42 | * __What operating system are you using?__ Windows (7, 8, 10, 32-bit, 64-bit), 43 | Mac OS X, (10.7.4, 10.9.0), GNU/Linux (which distribution, which version?) 44 | Again, more detail is better. 45 | * __Which version or versions of the software are you using?__ Ideally, you've 46 | followed the advice above and are on the latest version, but please confirm 47 | this. 48 | * __How can the we recreate your problem?__ Imagine that we have never used 49 | GeoHealthCheck before and have downloaded it for the first time. Exactly what steps 50 | do we need to take to reproduce your problem? 51 | 52 | ## Contributions and Licensing 53 | 54 | ### Contributor License Agreement 55 | 56 | Your contribution will be under our [license](https://github.com/geopython/GeoHealthCheck/blob/master/LICENSE) as per [GitHub's terms of service](https://help.github.com/articles/github-terms-of-service/#6-contributions-under-repository-license). 57 | 58 | ### GitHub Commit Access 59 | 60 | * Proposals to provide developers with GitHub commit access shall be emailed to the GeoHealthCheck [Gitter channel](https://gitter.im/geopython/GeoHealthCheck). Proposals shall be approved by the GeoHealthCheck development team. Committers shall be added by the project admin. 61 | * Removal of commit access shall be handled in the same manner. 62 | 63 | ### GitHub Pull Requests 64 | 65 | * Pull requests may include copyright in the source code header by the contributor if the contribution is significant or the contributor wants to claim copyright on their contribution. 66 | * All contributors shall be listed at https://github.com/geopython/GeoHealthCheck/graphs/contributors 67 | * Unclaimed copyright, by default, is assigned to the main copyright holders as specified in https://github.com/geopython/GeoHealthCheck/blob/master/LICENSE 68 | 69 | ### Version Control Branching 70 | 71 | * Always __make a new branch__ for your work, no matter how small. This makes 72 | it easy for others to take just that one set of changes from your repository, 73 | in case you have multiple unrelated changes floating around. 74 | 75 | * __Don't submit unrelated changes in the same branch/pull request!__ If it 76 | is not possible to review your changes quickly and easily, we may reject 77 | your request. 78 | 79 | * __Base your new branch off of the appropriate branch__ on the main repository: 80 | 81 | * In general the released version of GeoHealthCheck is based on the ``master`` 82 | (default) branch whereas development work is done under other non-default 83 | branches. Unless you are sure that your issue affects a non-default 84 | branch, __base your branch off the ``master`` one__. 85 | 86 | * Note that depending on how long it takes for the dev team to merge your 87 | patch, the copy of ``master`` you worked off of may get out of date! 88 | * If you find yourself 'bumping' a pull request that's been sidelined for a 89 | while, __make sure you rebase or merge to latest ``master``__ to ensure a 90 | speedier resolution. 91 | 92 | ### Code Formatting 93 | 94 | * __Please follow the coding conventions and style used in GeoHealthCheck.__ 95 | * GeoHealthCheck endeavours to follow the 96 | [PEP-8](http://www.python.org/dev/peps/pep-0008/) guidelines. 97 | 98 | ## Suggesting Enhancements 99 | 100 | We welcome suggestions for enhancements, but reserve the right to reject them 101 | if they do not follow future plans for GeoHealthCheck. 102 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:jammy 2 | 3 | # Credits to yjacolin for providing first versions 4 | LABEL original_developer="yjacolin " \ 5 | maintainer="Just van den Broecke " 6 | 7 | # These are default values, 8 | # Override when running container via docker(-compose) 9 | 10 | # ARGS 11 | ARG TZ="Etc/UTC" 12 | ARG LANG="en_US.UTF-8" 13 | ARG ADD_DEB_PACKAGES="" 14 | 15 | # General ENV settings 16 | ENV LC_ALL="en_US.UTF-8" \ 17 | LANG="en_US.UTF-8" \ 18 | LANGUAGE="en_US.UTF-8" \ 19 | \ 20 | \ 21 | DEB_PACKAGES="locales gunicorn postgresql-client python3-gunicorn python3-gevent python3-psycopg2 python3-lxml python3-pyproj" \ 22 | DEB_BUILD_DEPS="make python3-pip" \ 23 | # GHC ENV settings\ 24 | ADMIN_NAME=admin \ 25 | ADMIN_PWD=admin \ 26 | ADMIN_EMAIL=admin.istrator@mydomain.com \ 27 | SQLALCHEMY_DATABASE_URI='sqlite:////GeoHealthCheck/DB/data.db' \ 28 | SQLALCHEMY_ENGINE_OPTION_PRE_PING=False \ 29 | SECRET_KEY='d544ccc37dc3ad214c09b1b7faaa64c60351d5c8bb48b342' \ 30 | GHC_PROBE_HTTP_TIMEOUT_SECS=30 \ 31 | GHC_MINIMAL_RUN_FREQUENCY_MINS=10 \ 32 | GHC_RETENTION_DAYS=30 \ 33 | GHC_SELF_REGISTER=False \ 34 | GHC_NOTIFICATIONS=False \ 35 | GHC_NOTIFICATIONS_VERBOSITY=True \ 36 | GHC_WWW_LINK_EXCEPTION_CHECK=False \ 37 | GHC_LARGE_XML=False \ 38 | GHC_ADMIN_EMAIL='you@example.com' \ 39 | GHC_RUNNER_IN_WEBAPP=False \ 40 | GHC_REQUIRE_WEBAPP_AUTH=False \ 41 | GHC_BASIC_AUTH_DISABLED=False \ 42 | GHC_VERIFY_SSL=True \ 43 | GHC_LOG_LEVEL=30 \ 44 | GHC_LOG_FORMAT='%(asctime)s - %(name)s - %(levelname)s - %(message)s' \ 45 | GHC_NOTIFICATIONS_EMAIL='you2@example.com,them@example.com' \ 46 | GHC_SITE_TITLE='GeoHealthCheck' \ 47 | GHC_SITE_URL='http://localhost' \ 48 | GHC_SMTP_SERVER=None \ 49 | GHC_SMTP_PORT=None \ 50 | GHC_SMTP_TLS=False \ 51 | GHC_SMTP_SSL=False \ 52 | GHC_SMTP_USERNAME=None \ 53 | GHC_SMTP_PASSWORD=None \ 54 | GHC_GEOIP_URL='http://ip-api.com/json/{hostname}' \ 55 | GHC_GEOIP_LATFIELD='lat' \ 56 | GHC_GEOIP_LONFIELD='lon' \ 57 | GHC_METADATA_CACHE_SECS=900 \ 58 | \ 59 | # WSGI server settings, assumed is gunicorn \ 60 | HOST=0.0.0.0 \ 61 | PORT=80 \ 62 | WSGI_WORKERS=4 \ 63 | WSGI_WORKER_TIMEOUT=6000 \ 64 | WSGI_WORKER_CLASS='gevent' \ 65 | \ 66 | # GHC Core Plugins modules and/or classes, seldom needed to set: \ 67 | # if not specified here or in Container environment \ 68 | # all GHC built-in Plugins will be active. \ 69 | #ENV GHC_PLUGINS 'GeoHealthCheck.plugins.probe.owsgetcaps,\ 70 | # GeoHealthCheck.plugins.probe.wms, ...., ...\ 71 | # GeoHealthCheck.plugins.check.checks' \ 72 | \ 73 | # GHC User Plugins, best be overridden via Container environment \ 74 | GHC_USER_PLUGINS='' 75 | 76 | # Install operating system dependencies 77 | RUN \ 78 | apt-get update \ 79 | && apt-get --no-install-recommends install -y ${DEB_PACKAGES} ${DEB_BUILD_DEPS} ${ADD_DEB_PACKAGES} \ 80 | && localedef -i en_US -c -f UTF-8 -A /usr/share/locale/locale.alias en_US.UTF-8 \ 81 | && echo "For ${TZ} date=$(date)" && echo "Locale=$(locale)" 82 | 83 | # Add standard files and Add/override Plugins 84 | # Alternative Entrypoints to run GHC jobs 85 | # Override default Entrypoint with these on Containers 86 | COPY docker/scripts/*.sh docker/config_site.py docker/plugins / 87 | 88 | # Add Source Code 89 | COPY . /GeoHealthCheck 90 | 91 | # Install 92 | RUN \ 93 | chmod a+x /*.sh && ./install.sh \ 94 | # Cleanup TODO: remove unused Locales and TZs 95 | && apt-get remove --purge -y ${DEB_BUILD_DEPS} \ 96 | && apt-get clean \ 97 | && apt autoremove -y \ 98 | && rm -rf /var/lib/apt/lists/* 99 | 100 | 101 | # For SQLite 102 | VOLUME ["/GeoHealthCheck/DB/"] 103 | 104 | EXPOSE ${PORT} 105 | 106 | ENTRYPOINT /run-web.sh 107 | -------------------------------------------------------------------------------- /GeoHealthCheck/__init__.py: -------------------------------------------------------------------------------- 1 | # ================================================================= 2 | # 3 | # Authors: Tom Kralidis 4 | # 5 | # Copyright (c) 2014 Tom Kralidis 6 | # 7 | # Permission is hereby granted, free of charge, to any person 8 | # obtaining a copy of this software and associated documentation 9 | # files (the "Software"), to deal in the Software without 10 | # restriction, including without limitation the rights to use, 11 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the 13 | # Software is furnished to do so, subject to the following 14 | # conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be 17 | # included in all copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 21 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 23 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 24 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 26 | # OTHER DEALINGS IN THE SOFTWARE. 27 | # 28 | # ================================================================= 29 | 30 | from util import read 31 | 32 | 33 | def get_package_version(file_): 34 | """get version from top-level package init""" 35 | return read(file_) 36 | 37 | 38 | __version__ = get_package_version('../VERSION') 39 | -------------------------------------------------------------------------------- /GeoHealthCheck/check.py: -------------------------------------------------------------------------------- 1 | from plugin import Plugin 2 | from result import CheckResult 3 | 4 | 5 | class Check(Plugin): 6 | """ 7 | Base class for specific Plugin implementations to perform 8 | a check on results from a Probe. 9 | """ 10 | 11 | def __init__(self): 12 | Plugin.__init__(self) 13 | self.probe = None 14 | 15 | # Lifecycle 16 | def init(self, probe, check_vars): 17 | """ 18 | Initialize Checker with parent Probe and parameters dict. 19 | :return: 20 | """ 21 | 22 | self.probe = probe 23 | self.check_vars = check_vars 24 | self._parameters = check_vars.parameters 25 | self._result = CheckResult(self, check_vars) 26 | self._result.start() 27 | 28 | # Lifecycle 29 | def set_result(self, success, message): 30 | self._result.set(success, message) 31 | self._result.stop() 32 | 33 | # Lifecycle 34 | def perform(self): 35 | """ 36 | Perform this Check's specific check. TODO: return Result object. 37 | :return: 38 | """ 39 | pass 40 | -------------------------------------------------------------------------------- /GeoHealthCheck/enums.py: -------------------------------------------------------------------------------- 1 | # ================================================================= 2 | # 3 | # Authors: Tom Kralidis 4 | # 5 | # Copyright (c) 2014 Tom Kralidis 6 | # 7 | # Permission is hereby granted, free of charge, to any person 8 | # obtaining a copy of this software and associated documentation 9 | # files (the "Software"), to deal in the Software without 10 | # restriction, including without limitation the rights to use, 11 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the 13 | # Software is furnished to do so, subject to the following 14 | # conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be 17 | # included in all copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 21 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 23 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 24 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 26 | # OTHER DEALINGS IN THE SOFTWARE. 27 | # 28 | # ================================================================= 29 | 30 | RESOURCE_TYPES = { 31 | 'OGC:WMS': { 32 | 'label': 'Web Map Service (WMS)', 33 | 'versions': ['1.3.0'], 34 | 'capabilities': 'service=WMS&version=1.3.0&request=GetCapabilities' 35 | }, 36 | 'OGC:WMTS': { 37 | 'label': 'Web Map Tile Service (WMTS)', 38 | 'versions': ['1.0.0'], 39 | 'capabilities': 'service=WMTS&version=1.0.0&request=GetCapabilities' 40 | }, 41 | 'OSGeo:TMS': { 42 | 'label': 'Tile Map Service (TMS)', 43 | 'versions': ['1.0.0'], 44 | }, 45 | 'OGC:WFS': { 46 | 'label': 'Web Feature Service (WFS)', 47 | 'versions': ['1.1.0'], 48 | 'capabilities': 'service=WFS&version=1.1.0&request=GetCapabilities' 49 | }, 50 | 'OGC:WCS': { 51 | 'label': 'Web Coverage Service (WCS)', 52 | 'versions': ['1.1.0'], 53 | 'capabilities': 'service=WCS&version=1.1.0&request=GetCapabilities' 54 | }, 55 | 'OGC:WPS': { 56 | 'label': 'Web Processing Service (WPS)', 57 | 'versions': ['1.0.0'], 58 | 'capabilities': 'service=WPS&version=1.0.0&request=GetCapabilities' 59 | }, 60 | 'OGC:CSW': { 61 | 'label': 'Catalogue Service (CSW)', 62 | 'versions': ['2.0.2'], 63 | 'capabilities': 'service=CSW&version=2.0.2&request=GetCapabilities' 64 | }, 65 | 'OGC:SOS': { 66 | 'label': 'Sensor Observation Service (SOS)', 67 | 'versions': ['1.0.0'], 68 | 'capabilities': 'service=SOS&version=1.0.0&request=GetCapabilities' 69 | }, 70 | 'OGC:STA': { 71 | 'label': 'SensorThings API (STA)', 72 | 'versions': ['1.0'] 73 | }, 74 | 'OGCFeat': { 75 | 'label': 'OGC API Features (OAFeat)' 76 | }, 77 | 'OGC:3DTiles': { 78 | 'label': 'OGC 3D Tiles (OGC3D)' 79 | }, 80 | 'ESRI:FS': { 81 | 'label': 'ESRI ArcGIS FeatureServer (FS)' 82 | }, 83 | 'Mapbox:TileJSON': { 84 | 'label': 'Mapbox TileJSON Service (TileJSON)' 85 | }, 86 | 'urn:geoss:waf': { 87 | 'label': 'Web Accessible Folder (WAF)' 88 | }, 89 | 'WWW:LINK': { 90 | 'label': 'Web Address (URL)' 91 | }, 92 | 'FTP': { 93 | 'label': 'File Transfer Protocol (FTP)' 94 | }, 95 | 'OSGeo:GeoNode': { 96 | 'label': 'GeoNode instance' 97 | }, 98 | 'GHC:Report': { 99 | 'label': 'GeoHealthCheck Reporter (GHC-R)' 100 | }, 101 | } 102 | -------------------------------------------------------------------------------- /GeoHealthCheck/factory.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | LOGGER = logging.getLogger(__name__) 4 | 5 | 6 | class Factory: 7 | """ 8 | Object, Function class Factory (Pattern). 9 | Based on: http://stackoverflow.com/questions/2226330/ 10 | instantiate-a-python-class-from-a-name 11 | Also contains introspection util functions. 12 | """ 13 | 14 | @staticmethod 15 | def create_obj(class_string): 16 | 17 | # Get value for 'class' property 18 | try: 19 | if not class_string: 20 | raise ValueError('Class name not provided') 21 | 22 | # class object from module.class name 23 | class_obj = Factory.create_class(class_string) 24 | 25 | # class instance from class object with constructor args 26 | return class_obj() 27 | except Exception as e: 28 | LOGGER.error("cannot create object instance from class '%s' e=%s" % 29 | (class_string, str(e))) 30 | raise e 31 | 32 | @staticmethod 33 | def create_class(class_string): 34 | """Returns class instance specified by a string. 35 | 36 | Args: 37 | class_string: The string representing a class. 38 | 39 | Raises: 40 | ValueError if module part of the class is not specified. 41 | """ 42 | class_obj = None 43 | try: 44 | module_name, dot, class_name = class_string.rpartition('.') 45 | if module_name == '': 46 | raise ValueError('Class name must contain module part.') 47 | class_obj = getattr( 48 | __import__(module_name, globals(), locals(), 49 | [class_name]), class_name) 50 | except Exception as e: 51 | LOGGER.error("cannot create class '%s'" % class_string) 52 | raise e 53 | 54 | return class_obj 55 | 56 | @staticmethod 57 | def create_module(module_string): 58 | """Returns module instance specified by a string. 59 | 60 | Args: 61 | module_string: The string representing a module. 62 | 63 | Raises: 64 | ValueError if module can not be imported from string. 65 | """ 66 | module_obj = None 67 | try: 68 | module_obj = __import__(module_string, globals(), 69 | locals(), fromlist=['']) 70 | except Exception as e: 71 | LOGGER.error("cannot create module from '%s'" % module_string) 72 | raise e 73 | 74 | return module_obj 75 | 76 | @staticmethod 77 | def create_function(function_string): 78 | # Creating a global function instance is the same as a class instance 79 | return Factory.create_class(function_string) 80 | 81 | @staticmethod 82 | def get_class_vars(clazz, candidates=[]): 83 | """ 84 | Class method to get all (uppercase) class variables of a class 85 | as a dict 86 | """ 87 | import inspect 88 | 89 | if type(clazz) is str: 90 | clazz = Factory.create_class(clazz) 91 | 92 | members = inspect.getmembers(clazz) 93 | # return members 94 | vars = dict() 95 | for member in members: 96 | key, value = member 97 | if key in candidates: 98 | vars[key] = value 99 | continue 100 | 101 | if not key.startswith('__') and key.isupper() \ 102 | and not inspect.isclass(value) \ 103 | and not inspect.isfunction(value) \ 104 | and not inspect.isbuiltin(value) \ 105 | and not inspect.ismethod(value): 106 | vars[key] = value 107 | 108 | return vars 109 | 110 | @staticmethod 111 | def get_class_for_method(method): 112 | method_name = method.__name__ 113 | classes = [method.im_class] 114 | while classes: 115 | c = classes.pop() 116 | if method_name in c.__dict__: 117 | return c 118 | else: 119 | classes = list(c.__bases__) + classes 120 | return None 121 | 122 | @staticmethod 123 | def full_class_name_for_obj(o): 124 | module = o.__class__.__module__ 125 | if module is None or module == str.__class__.__module__: 126 | return o.__class__.__name__ 127 | return module + '.' + o.__class__.__name__ 128 | -------------------------------------------------------------------------------- /GeoHealthCheck/geocoder.py: -------------------------------------------------------------------------------- 1 | # ================================================================= 2 | # 3 | # Authors: Rob van Loon 4 | # 5 | # Copyright (c) 2021 Rob van Loon 6 | # 7 | # Permission is hereby granted, free of charge, to any person 8 | # obtaining a copy of this software and associated documentation 9 | # files (the "Software"), to deal in the Software without 10 | # restriction, including without limitation the rights to use, 11 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the 13 | # Software is furnished to do so, subject to the following 14 | # conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be 17 | # included in all copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 21 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 23 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 24 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 26 | # OTHER DEALINGS IN THE SOFTWARE. 27 | # 28 | # ================================================================= 29 | 30 | from plugin import Plugin 31 | import logging 32 | 33 | 34 | LOGGER = logging.getLogger(__name__) 35 | 36 | 37 | class Geocoder(Plugin): 38 | """ 39 | Base class for specific Geocode plugins to locate servers by their hostname 40 | """ 41 | 42 | def __init__(self): 43 | super().__init__() 44 | 45 | def init(self, geocoder_vars): 46 | self._geocoder_vars = geocoder_vars 47 | 48 | def locate(self, hostname): 49 | """ 50 | Perform a locate on the host. 51 | 52 | :param hostname string: the hostname of the server for which we want 53 | the coords. 54 | 55 | TODO: return result as tuple with location in lat-lon. Example: 56 | `(52.4, 21.0)` 57 | """ 58 | pass 59 | 60 | def log(self, text): 61 | LOGGER.info(text) 62 | -------------------------------------------------------------------------------- /GeoHealthCheck/init.py: -------------------------------------------------------------------------------- 1 | # ================================================================= 2 | # 3 | # Authors: Tom Kralidis 4 | # 5 | # Copyright (c) 2014 Tom Kralidis 6 | # 7 | # Permission is hereby granted, free of charge, to any person 8 | # obtaining a copy of this software and associated documentation 9 | # files (the "Software"), to deal in the Software without 10 | # restriction, including without limitation the rights to use, 11 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the 13 | # Software is furnished to do so, subject to the following 14 | # conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be 17 | # included in all copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 21 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 23 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 24 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 26 | # OTHER DEALINGS IN THE SOFTWARE. 27 | # 28 | # ================================================================= 29 | 30 | import os 31 | import sys 32 | import logging 33 | from flask import Flask 34 | from flask_sqlalchemy import SQLAlchemy 35 | from flask_babel import Babel 36 | 37 | LOGGER = logging.getLogger(__name__) 38 | 39 | 40 | def to_list(obj): 41 | obj_type = type(obj) 42 | if obj_type is str: 43 | return obj.replace(' ', '').split(',') 44 | elif obj_type is list: 45 | return obj 46 | elif obj_type is set: 47 | return list(obj) 48 | else: 49 | raise TypeError('unknown type for Plugin: %s' + str(obj_type)) 50 | 51 | 52 | class App: 53 | """ 54 | Singleton: sole static instance of Flask App 55 | """ 56 | app_instance = None 57 | db_instance = None 58 | babel_instance = None 59 | plugins_instance = None 60 | home_dir = None 61 | count = 0 62 | 63 | @staticmethod 64 | def init(): 65 | # Do init once 66 | app = Flask(__name__) 67 | 68 | app.url_map.strict_slashes = False 69 | 70 | # Read and override configs 71 | app.config.from_pyfile('config_main.py') 72 | # config_site.py not present in doc-build: silently fail 73 | sphinx_build = os.environ.get('SPHINX_BUILD', '0') 74 | app.config.from_pyfile('../instance/config_site.py', 75 | silent=sphinx_build == '1') 76 | app.config.from_envvar('GHC_SETTINGS', silent=True) 77 | 78 | # Global Logging config 79 | logging.basicConfig(level=int(app.config['GHC_LOG_LEVEL']), 80 | format=app.config['GHC_LOG_FORMAT']) 81 | 82 | app.config['GHC_SITE_URL'] = \ 83 | app.config['GHC_SITE_URL'].rstrip('/') 84 | 85 | app.secret_key = app.config['SECRET_KEY'] 86 | SQLALCHEMY_ENGINE_OPTIONS = { 87 | 'pool_pre_ping': app.config[ 88 | 'SQLALCHEMY_ENGINE_OPTION_PRE_PING' 89 | ] 90 | } 91 | App.db_instance = SQLAlchemy(app, 92 | engine_options=SQLALCHEMY_ENGINE_OPTIONS) 93 | App.babel_instance = Babel(app) 94 | 95 | # Plugins (via Docker ENV) must be list, but may have been 96 | # specified as comma-separated string, or older set notation 97 | app.config['GHC_PLUGINS'] = to_list(app.config['GHC_PLUGINS']) 98 | app.config['GHC_USER_PLUGINS'] = \ 99 | to_list(app.config['GHC_USER_PLUGINS']) 100 | 101 | # Concatenate core- and user-Plugins 102 | App.plugins_instance = \ 103 | app.config['GHC_PLUGINS'] + app.config['GHC_USER_PLUGINS'] 104 | 105 | # Needed to find Plugins 106 | home_dir = os.path.dirname(os.path.abspath(__file__)) 107 | App.home_dir = sys.path.append('%s/..' % home_dir) 108 | 109 | # Finally assign app-instance 110 | App.app_instance = app 111 | App.count += 1 112 | LOGGER.info("created GHC App instance #%d" % App.count) 113 | 114 | @staticmethod 115 | def get_app(): 116 | return App.app_instance 117 | 118 | @staticmethod 119 | def get_babel(): 120 | return App.babel_instance 121 | 122 | @staticmethod 123 | def get_config(): 124 | return App.app_instance.config 125 | 126 | @staticmethod 127 | def get_db(): 128 | return App.db_instance 129 | 130 | @staticmethod 131 | def get_home_dir(): 132 | return App.home_dir 133 | 134 | @staticmethod 135 | def get_plugins(): 136 | return App.plugins_instance 137 | 138 | 139 | App.init() 140 | -------------------------------------------------------------------------------- /GeoHealthCheck/manage.py: -------------------------------------------------------------------------------- 1 | # Flask script mainly for managing the DB 2 | # Credits: https://github.com/miguelgrinberg 3 | # See https://flask-migrate.readthedocs.io/en/latest/ 4 | # and https://blog.miguelgrinberg.com/post/ 5 | # flask-migrate-alembic-database-migration-wrapper-for-flask/page/3 6 | # 7 | # Usage: 8 | # 9 | # $ python manage.py --help 10 | # usage: manage.py [-h] {shell,db,runserver} ... 11 | # 12 | # positional arguments: 13 | # {shell,db,runserver} 14 | # shell Runs a Python shell inside Flask application context. 15 | # db Perform database migrations 16 | # runserver Runs the Flask development server i.e. app.run() 17 | # 18 | # optional arguments: 19 | # -h, --help show this help message and exit 20 | # 21 | # For DB management: 22 | # $ python manage.py db --help 23 | # usage: Perform database migrations 24 | # 25 | # positional arguments: 26 | # {upgrade,migrate,current,stamp,init,downgrade,history,revision} 27 | # upgrade Upgrade to a later version 28 | # migrate Alias for 'revision --autogenerate' 29 | # current Display the current revision for each database. 30 | # stamp 'stamp' the revision table with the given revision; 31 | # dont run any migrations 32 | # init Generates a new migration 33 | # downgrade Revert to a previous version 34 | # history List changeset scripts in chronological order. 35 | # revision Create a new revision file. 36 | # 37 | # optional arguments: 38 | # -h, --help show this help message and exit 39 | 40 | from flask_script import Manager 41 | from flask_migrate import Migrate, MigrateCommand 42 | from init import App 43 | 44 | DB = App.get_db() 45 | APP = App.get_app() 46 | 47 | migrate = Migrate(APP, DB) 48 | 49 | manager = Manager(APP) 50 | manager.add_command('db', MigrateCommand) 51 | 52 | if __name__ == '__main__': 53 | manager.run() 54 | -------------------------------------------------------------------------------- /GeoHealthCheck/migrations/README.md: -------------------------------------------------------------------------------- 1 | # Database upgrade support 2 | 3 | This dir contains various files for developing database upgrades. 4 | Upgrades are supported using Alembic via Flask-Migrate 5 | and Flask-Script. 6 | Users should be able to upgrade existing installs via: 7 | 8 | # In top dir of installation 9 | paver upgrade 10 | 11 | The `versions` dir contains the various upgrades. These were 12 | initially created using the Alembic `autogenerate` facility 13 | and modified by hand to adapt to various local circumstances, 14 | mainly to check if particular tables/columns already exist (as 15 | Alembic was introduced later in the project) 16 | 17 | The script [manage.py](../manage.py) contains a command processor 18 | for various DB management tasks related to migrations and upgrading. 19 | 20 | Whenever a change in the database schema or table content 21 | conventions has changed a new migration should be created via the command. 22 | 23 | python manage.py db migrate 24 | 25 | Where `migrate` is an alias for `revision --autogenerate`. 26 | Alternatively if the autogeneration does not work, create an empty migration: 27 | 28 | python manage.py db revision 29 | 30 | In both cases this will create a new revision and a `_.py` file 31 | under `versions/` to upgrade 32 | to that revision. After this command that `.py` file should be inspected 33 | and modified where needed. In particular Postgres installations using the 34 | `public` schema should comment out any management of the `spatial_ref_sys` table. 35 | The helper scripts in [alembic_helpers.py](alembic_helpers.py) can be handy 36 | to check various DB metadata. 37 | 38 | Subsequently the upgrade can be performed using: 39 | 40 | python manage.py db upgrade 41 | # or the equivalent (for users) 42 | paver upgrade 43 | 44 | ## Revisions 45 | 46 | See the corresponding .py files under `versions`. 47 | 48 | ### Initial 49 | 50 | Initial GHC "version 0" had three tables: 51 | 52 | * `user`, `resource` and `run` 53 | 54 | ### 496427d03f87 - Introduce Tags 55 | 56 | Changes: 57 | 58 | * create two new tables: `tag` and `resource_tags`. 59 | 60 | ### 992013af402f - Introduce Probes and Checks 61 | 62 | Changes: 63 | 64 | * create two new tables: `probe_vars` and `check_vars` 65 | * add column `report` to `run` table 66 | 67 | ### 2638c2a40625 - Add resource.active column 68 | 69 | Changes: 70 | 71 | * add column `active` to `resource` table 72 | -------------------------------------------------------------------------------- /GeoHealthCheck/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopython/GeoHealthCheck/48fb6493694981689a811204968c9c5ee50ef5b5/GeoHealthCheck/migrations/__init__.py -------------------------------------------------------------------------------- /GeoHealthCheck/migrations/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # template used to generate migration files 5 | # file_template = %%(rev)s_%%(slug)s 6 | 7 | # set to 'true' to run the environment during 8 | # the 'revision' command, regardless of autogenerate 9 | # revision_environment = false 10 | 11 | 12 | # Logging configuration 13 | [loggers] 14 | keys = root,sqlalchemy,alembic 15 | 16 | [handlers] 17 | keys = console 18 | 19 | [formatters] 20 | keys = generic 21 | 22 | [logger_root] 23 | level = WARN 24 | handlers = console 25 | qualname = 26 | 27 | [logger_sqlalchemy] 28 | level = WARN 29 | handlers = 30 | qualname = sqlalchemy.engine 31 | 32 | [logger_alembic] 33 | level = INFO 34 | handlers = 35 | qualname = alembic 36 | 37 | [handler_console] 38 | class = StreamHandler 39 | args = (sys.stderr,) 40 | level = NOTSET 41 | formatter = generic 42 | 43 | [formatter_generic] 44 | format = %(levelname)-5.5s [%(name)s] %(message)s 45 | datefmt = %H:%M:%S 46 | -------------------------------------------------------------------------------- /GeoHealthCheck/migrations/alembic_helpers.py: -------------------------------------------------------------------------------- 1 | # From: http://www.derstappen-it.de/tech-blog/sqlalchemie-alembic-check-if-table-has-column 2 | # Extended with get_table_names() and tables_exist() by JvdB 3 | # 4 | from alembic import op 5 | from sqlalchemy import engine_from_config 6 | from sqlalchemy.engine import reflection 7 | 8 | 9 | # Get the SQLAlchemy Inspector from Engine 10 | def get_inspector(): 11 | config = op.get_context().config 12 | engine = engine_from_config( 13 | config.get_section(config.config_ini_section), prefix='sqlalchemy.') 14 | return reflection.Inspector.from_engine(engine) 15 | 16 | 17 | # Get list of column names from table 18 | def get_column_names(table): 19 | # get_columns: list of dicts with (column) 'name' entries 20 | columns = get_inspector().get_columns(table) 21 | return [column['name'] for column in columns] 22 | 23 | 24 | # Check if table has column 25 | def table_has_column(table, column_name): 26 | return column_name in get_column_names(table) 27 | 28 | 29 | # Get list of table names from database 30 | def get_table_names(): 31 | return get_inspector().get_table_names() 32 | 33 | 34 | # Check if list of table names in database 35 | def tables_exist(tables): 36 | table_names = get_table_names() 37 | for table in tables: 38 | if table not in table_names: 39 | return False 40 | 41 | return True 42 | 43 | 44 | # Get list of index names from table 45 | def get_index_names(table): 46 | # get_indexes: list of dicts with (index) 'name' entries 47 | indexes = get_inspector().get_indexes(table) 48 | return [index['name'] for index in indexes] 49 | 50 | 51 | # Check if table has named index 52 | def table_has_index(table, index_name): 53 | return index_name in get_index_names(table) 54 | 55 | 56 | # Create index on a table if not exists 57 | def create_index(index_name, table, columns, unique=False): 58 | if table_has_index(table, index_name): 59 | return 60 | 61 | # Index does not exist: create 62 | op.create_index(op.f(index_name), table, columns, unique=unique) 63 | -------------------------------------------------------------------------------- /GeoHealthCheck/migrations/env.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | from alembic import context 3 | from sqlalchemy import engine_from_config, pool 4 | from logging.config import fileConfig 5 | import logging 6 | 7 | # this is the Alembic Config object, which provides 8 | # access to the values within the .ini file in use. 9 | config = context.config 10 | 11 | # Interpret the config file for Python logging. 12 | # This line sets up loggers basically. 13 | fileConfig(config.config_file_name) 14 | logger = logging.getLogger('alembic.env') 15 | 16 | # add your model's MetaData object here 17 | # for 'autogenerate' support 18 | # from myapp import mymodel 19 | # target_metadata = mymodel.Base.metadata 20 | from flask import current_app 21 | config.set_main_option('sqlalchemy.url', 22 | current_app.config.get('SQLALCHEMY_DATABASE_URI')) 23 | target_metadata = current_app.extensions['migrate'].db.metadata 24 | 25 | # other values from the config, defined by the needs of env.py, 26 | # can be acquired: 27 | # my_important_option = config.get_main_option("my_important_option") 28 | # ... etc. 29 | 30 | 31 | def run_migrations_offline(): 32 | """Run migrations in 'offline' mode. 33 | 34 | This configures the context with just a URL 35 | and not an Engine, though an Engine is acceptable 36 | here as well. By skipping the Engine creation 37 | we don't even need a DBAPI to be available. 38 | 39 | Calls to context.execute() here emit the given string to the 40 | script output. 41 | 42 | """ 43 | url = config.get_main_option("sqlalchemy.url") 44 | context.configure(url=url) 45 | 46 | with context.begin_transaction(): 47 | context.run_migrations() 48 | 49 | 50 | def run_migrations_online(): 51 | """Run migrations in 'online' mode. 52 | 53 | In this scenario we need to create an Engine 54 | and associate a connection with the context. 55 | 56 | """ 57 | 58 | # this callback is used to prevent an auto-migration from being generated 59 | # when there are no changes to the schema 60 | # reference: http://alembic.readthedocs.org/en/latest/cookbook.html 61 | def process_revision_directives(context, revision, directives): 62 | if getattr(config.cmd_opts, 'autogenerate', False): 63 | script = directives[0] 64 | if script.upgrade_ops.is_empty(): 65 | directives[:] = [] 66 | logger.info('No changes in schema detected.') 67 | 68 | engine = engine_from_config(config.get_section(config.config_ini_section), 69 | prefix='sqlalchemy.', 70 | poolclass=pool.NullPool) 71 | 72 | connection = engine.connect() 73 | context.configure(connection=connection, 74 | target_metadata=target_metadata, 75 | process_revision_directives=process_revision_directives, 76 | **current_app.extensions['migrate'].configure_args) 77 | 78 | try: 79 | with context.begin_transaction(): 80 | context.run_migrations() 81 | finally: 82 | connection.close() 83 | 84 | if context.is_offline_mode(): 85 | run_migrations_offline() 86 | else: 87 | run_migrations_online() 88 | -------------------------------------------------------------------------------- /GeoHealthCheck/migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /GeoHealthCheck/migrations/versions/2638c2a40625_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 2638c2a40625 4 | Revises: 992013af402f 5 | Create Date: 2017-09-08 10:48:19.596099 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | import imp 11 | import os 12 | from GeoHealthCheck.migrations import alembic_helpers 13 | 14 | # revision identifiers, used by Alembic. 15 | revision = '2638c2a40625' 16 | down_revision = '992013af402f' 17 | branch_labels = None 18 | depends_on = None 19 | 20 | 21 | def upgrade(): 22 | if not alembic_helpers.table_has_column('resource', 'active'): 23 | print('Column active not present in resource table, will create') 24 | op.add_column(u'resource', sa.Column('active', sa.Boolean(), 25 | nullable=False, default=1, server_default='True')) 26 | else: 27 | print('Column active already present in resource table') 28 | 29 | 30 | def downgrade(): 31 | print('Dropping Column active from resource table') 32 | op.drop_column(u'resource', 'active') 33 | -------------------------------------------------------------------------------- /GeoHealthCheck/migrations/versions/3381760e2a8c_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 3381760e2a8c 4 | Revises: f72ff1ac3967 5 | Create Date: 2021-08-20 09:53:01.704105 6 | 7 | """ 8 | from alembic import op 9 | from GeoHealthCheck.migrations import alembic_helpers 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '3381760e2a8c' 14 | down_revision = 'f72ff1ac3967' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # Only create indexes if not-existing 21 | print('Create indexes if not-existing...') 22 | alembic_helpers.create_index('ix_check_vars_probe_vars_identifier', 'check_vars', ['probe_vars_identifier'], unique=False) 23 | alembic_helpers.create_index('ix_probe_vars_resource_identifier', 'probe_vars', ['resource_identifier'], unique=False) 24 | alembic_helpers.create_index('ix_resource_owner_identifier', 'resource', ['owner_identifier'], unique=False) 25 | alembic_helpers.create_index('ix_resource_tags_resource_identifier', 'resource_tags', ['resource_identifier'], unique=False) 26 | alembic_helpers.create_index('ix_resource_tags_tag_id', 'resource_tags', ['tag_id'], unique=False) 27 | alembic_helpers.create_index('ix_run_resource_identifier', 'run', ['resource_identifier'], unique=False) 28 | 29 | 30 | def downgrade(): 31 | # ### commands auto generated by Alembic - please adjust! ### 32 | op.drop_index(op.f('ix_run_resource_identifier'), table_name='run') 33 | op.drop_index(op.f('ix_resource_tags_tag_id'), table_name='resource_tags') 34 | op.drop_index(op.f('ix_resource_tags_resource_identifier'), table_name='resource_tags') 35 | op.drop_index(op.f('ix_resource_owner_identifier'), table_name='resource') 36 | op.drop_index(op.f('ix_probe_vars_resource_identifier'), table_name='probe_vars') 37 | op.drop_index(op.f('ix_check_vars_probe_vars_identifier'), table_name='check_vars') 38 | # ### end Alembic commands ### 39 | -------------------------------------------------------------------------------- /GeoHealthCheck/migrations/versions/34531bfd7cab_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 34531bfd7cab 4 | Revises: bb91fb332c36 5 | Create Date: 2018-03-19 16:59:42.235474 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from GeoHealthCheck.migrations import alembic_helpers 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '34531bfd7cab' 14 | down_revision = 'bb91fb332c36' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | if not alembic_helpers.table_has_column('resource', 'run_frequency'): 21 | print('Column run_frequency not present in resource table, will create') 22 | op.add_column(u'resource', sa.Column('run_frequency', sa.Integer(), 23 | nullable=False, default=60, server_default='60')) 24 | else: 25 | print('Column run_frequency already present in resource table') 26 | 27 | if not alembic_helpers.tables_exist(['resource_lock']): 28 | print('Table for Resource locking not present, will create') 29 | # ### commands auto generated by Alembic - please adjust! ### 30 | op.create_table('resource_lock', 31 | sa.Column('identifier', sa.Integer(), nullable=False, unique=True), 32 | sa.Column('resource_identifier', sa.Integer(), nullable=False, unique=True), 33 | sa.Column('owner', sa.Text, nullable=False, default='NOT SET', server_default='NOT SET'), 34 | sa.Column('start_time', sa.DateTime, nullable=False), 35 | sa.Column('end_time', sa.DateTime, nullable=False), 36 | sa.ForeignKeyConstraint(['resource_identifier'], ['resource.identifier'], ), 37 | sa.PrimaryKeyConstraint('identifier') 38 | ) 39 | # ### end Alembic commands ### 40 | else: 41 | print('Table for Resource-locking already present, will not create') 42 | 43 | 44 | def downgrade(): 45 | print('Dropping Column run_frequency from resource table') 46 | op.drop_column(u'resource', 'run_frequency') 47 | op.drop_table('resource_lock') 48 | -------------------------------------------------------------------------------- /GeoHealthCheck/migrations/versions/496427d03f87_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 496427d03f87 4 | Revises: 5 | Create Date: 2017-04-24 17:47:44.802571 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | import imp 11 | import os 12 | from GeoHealthCheck.migrations import alembic_helpers 13 | 14 | # revision identifiers, used by Alembic. 15 | revision = '496427d03f87' 16 | down_revision = None 17 | branch_labels = None 18 | depends_on = None 19 | 20 | 21 | def upgrade(): 22 | # ### commands auto generated by Alembic - please adjust! ### 23 | print('Before: DB tables: %s' % str(alembic_helpers.get_table_names())) 24 | if not alembic_helpers.tables_exist(['tag', 'resource_tags']): 25 | print('Tables for Tag-support not present, will create them') 26 | op.create_table('tag', 27 | sa.Column('id', sa.Integer(), nullable=False), 28 | sa.Column('name', sa.String(length=100), nullable=False), 29 | sa.PrimaryKeyConstraint('id'), 30 | sa.UniqueConstraint('name') 31 | ) 32 | op.create_table('resource_tags', 33 | sa.Column('identifier', sa.Integer(), nullable=False), 34 | sa.Column('tag_id', sa.Integer(), nullable=True), 35 | sa.Column('resource_identifier', sa.Integer(), nullable=True), 36 | sa.ForeignKeyConstraint(['resource_identifier'], ['resource.identifier'], ), 37 | sa.ForeignKeyConstraint(['tag_id'], ['tag.id'], ), 38 | sa.PrimaryKeyConstraint('identifier') 39 | ) 40 | else: 41 | print('Tables for tag-support already present, will not create them') 42 | 43 | # op.drop_table('spatial_ref_sys') 44 | # ### end Alembic commands ### 45 | 46 | def downgrade(): 47 | # ### commands auto generated by Alembic - please adjust! ### 48 | # op.create_table('spatial_ref_sys', 49 | # sa.Column('srid', sa.INTEGER(), autoincrement=False, nullable=False), 50 | # sa.Column('auth_name', sa.VARCHAR(length=256), autoincrement=False, nullable=True), 51 | # sa.Column('auth_srid', sa.INTEGER(), autoincrement=False, nullable=True), 52 | # sa.Column('srtext', sa.VARCHAR(length=2048), autoincrement=False, nullable=True), 53 | # sa.Column('proj4text', sa.VARCHAR(length=2048), autoincrement=False, nullable=True), 54 | # sa.CheckConstraint(u'(srid > 0) AND (srid <= 998999)', name=u'spatial_ref_sys_srid_check'), 55 | # sa.PrimaryKeyConstraint('srid', name=u'spatial_ref_sys_pkey') 56 | # ) 57 | op.drop_table('resource_tags') 58 | op.drop_table('tag') 59 | # ### end Alembic commands ### 60 | -------------------------------------------------------------------------------- /GeoHealthCheck/migrations/versions/90e1c865a561_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 90e1c865a561 4 | Revises: 34531bfd7cab 5 | Create Date: 2018-11-15 21:51:51.569697 6 | 7 | """ 8 | from GeoHealthCheck.init import App 9 | from GeoHealthCheck.models import User 10 | DB = App.get_db() 11 | 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = '90e1c865a561' 15 | down_revision = '34531bfd7cab' 16 | branch_labels = None 17 | depends_on = None 18 | 19 | 20 | def upgrade(): 21 | users = User.query.all() 22 | for user in users: 23 | # May happen when adding users before upgrade 24 | if len(user.password) > 80: 25 | print('NOT Encrypting for: %s' % user.username) 26 | continue 27 | print('Encrypting for user: %s' % user.username) 28 | user.set_password(user.password) 29 | DB.session.commit() 30 | 31 | 32 | def downgrade(): 33 | print('Sorry. we cannot downgrade from encrypted passwords for obvious reasons.') 34 | -------------------------------------------------------------------------------- /GeoHealthCheck/migrations/versions/933717a14052_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 933717a14052 4 | Revises: 3381760e2a8c 5 | Create Date: 2022-08-10 15:26:38.960066 6 | 7 | DB Upgrade for name changes OGC API Features components. 8 | Finalizes name-changes in PR 9 | https://github.com/geopython/GeoHealthCheck/pull/431 10 | 11 | Basically this SQL: 12 | UPDATE resource SET resource_type = 'OGCFeat' WHERE resource_type = 'OGC:WFS3'; 13 | UPDATE probe_vars SET probe_class = 'GeoHealthCheck.plugins.probe.ogcfeat.OGCFeatCaps' WHERE probe_class = 'GeoHealthCheck.plugins.probe.wfs3.WFS3Caps'; 14 | UPDATE probe_vars SET probe_class = 'GeoHealthCheck.plugins.probe.ogcfeat.OGCFeatDrilldown' WHERE probe_class = 'GeoHealthCheck.plugins.probe.wfs3.WFS3Drilldown'; 15 | UPDATE probe_vars SET probe_class = 'GeoHealthCheck.plugins.probe.ogcfeat.OGCFeatOpenAPIValidator' WHERE probe_class = 'GeoHealthCheck.plugins.probe.wfs3.WFS3OpenAPIValidator'; 16 | 17 | """ 18 | from alembic import op 19 | from sqlalchemy.sql import table, column 20 | from sqlalchemy import String 21 | 22 | # revision identifiers, used by Alembic. 23 | revision = '933717a14052' 24 | down_revision = '3381760e2a8c' 25 | branch_labels = None 26 | depends_on = None 27 | 28 | 29 | def upgrade(): 30 | print('Update (upgrade) records refering to OGC:WFS3 and WFS3 names to OGCFeat') 31 | 32 | resource = table('resource', 33 | column('resource_type', String) 34 | ) 35 | 36 | op.execute( 37 | resource.update(). 38 | where(resource.c.resource_type == op.inline_literal('OGC:WFS3')). 39 | values({'resource_type': op.inline_literal('OGCFeat')}) 40 | ) 41 | 42 | probe_vars = table('probe_vars', 43 | column('probe_class', String) 44 | ) 45 | 46 | op.execute( 47 | probe_vars.update(). 48 | where(probe_vars.c.probe_class == op.inline_literal('GeoHealthCheck.plugins.probe.wfs3.WFS3Caps')). 49 | values({'probe_class': op.inline_literal('GeoHealthCheck.plugins.probe.ogcfeat.OGCFeatCaps')}) 50 | ) 51 | 52 | op.execute( 53 | probe_vars.update(). 54 | where(probe_vars.c.probe_class == op.inline_literal('GeoHealthCheck.plugins.probe.wfs3.WFS3Drilldown')). 55 | values({'probe_class': op.inline_literal('GeoHealthCheck.plugins.probe.ogcfeat.OGCFeatDrilldown')}) 56 | ) 57 | 58 | op.execute( 59 | probe_vars.update(). 60 | where(probe_vars.c.probe_class == op.inline_literal('GeoHealthCheck.plugins.probe.wfs3.WFS3OpenAPIValidator')). 61 | values({'probe_class': op.inline_literal('GeoHealthCheck.plugins.probe.ogcfeat.OGCFeatOpenAPIValidator')}) 62 | ) 63 | 64 | 65 | def downgrade(): 66 | # ### commands auto generated by Alembic - please adjust! ### 67 | print('Update (downgrade) records refering to OGCFeat to OGC:WFS3 and WFS3') 68 | resource = table('resource', 69 | column('resource_type', String) 70 | ) 71 | 72 | op.execute( 73 | resource.update(). 74 | where(resource.c.resource_type == op.inline_literal('OGCFeat')). 75 | values({'resource_type': op.inline_literal('OGC:WFS3')}) 76 | ) 77 | 78 | probe_vars = table('probe_vars', 79 | column('probe_class', String) 80 | ) 81 | 82 | op.execute( 83 | probe_vars.update(). 84 | where(probe_vars.c.probe_class == op.inline_literal('GeoHealthCheck.plugins.probe.ogcfeat.OGCFeatCaps')). 85 | values({'probe_class': op.inline_literal('GeoHealthCheck.plugins.probe.wfs3.WFS3Caps')}) 86 | ) 87 | 88 | op.execute( 89 | probe_vars.update(). 90 | where(probe_vars.c.probe_class == op.inline_literal('GeoHealthCheck.plugins.probe.ogcfeat.OGCFeatDrilldown')). 91 | values({'probe_class': op.inline_literal('GeoHealthCheck.plugins.probe.wfs3.WFS3Drilldown')}) 92 | ) 93 | 94 | op.execute( 95 | probe_vars.update(). 96 | where(probe_vars.c.probe_class == op.inline_literal('GeoHealthCheck.plugins.probe.ogcfeat.OGCFeatOpenAPIValidator')). 97 | values({'probe_class': op.inline_literal('GeoHealthCheck.plugins.probe.wfs3.WFS3OpenAPIValidator')}) 98 | ) 99 | # ### end Alembic commands ### 100 | -------------------------------------------------------------------------------- /GeoHealthCheck/migrations/versions/992013af402f_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 992013af402f 4 | Revises: 496427d03f87 5 | Create Date: 2017-04-24 17:51:15.481079 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | import imp 11 | import os 12 | from GeoHealthCheck.migrations import alembic_helpers 13 | 14 | # revision identifiers, used by Alembic. 15 | revision = '992013af402f' 16 | down_revision = '496427d03f87' 17 | branch_labels = None 18 | depends_on = None 19 | 20 | 21 | def upgrade(): 22 | 23 | if not alembic_helpers.tables_exist(['probe_vars', 'check_vars']): 24 | print('Tables for Probe-support not present, will create them') 25 | # ### commands auto generated by Alembic - please adjust! ### 26 | op.create_table('probe_vars', 27 | sa.Column('identifier', sa.Integer(), nullable=False), 28 | sa.Column('resource_identifier', sa.Integer(), nullable=True), 29 | sa.Column('probe_class', sa.Text(), nullable=False), 30 | sa.Column('parameters', sa.Text(), nullable=True), 31 | sa.ForeignKeyConstraint(['resource_identifier'], ['resource.identifier'], ), 32 | sa.PrimaryKeyConstraint('identifier') 33 | ) 34 | op.create_table('check_vars', 35 | sa.Column('identifier', sa.Integer(), nullable=False), 36 | sa.Column('probe_vars_identifier', sa.Integer(), nullable=True), 37 | sa.Column('check_class', sa.Text(), nullable=False), 38 | sa.Column('parameters', sa.Text(), nullable=True), 39 | sa.ForeignKeyConstraint(['probe_vars_identifier'], ['probe_vars.identifier'], ), 40 | sa.PrimaryKeyConstraint('identifier') 41 | ) 42 | # ### end Alembic commands ### 43 | else: 44 | print('Tables for Probe-support already present, will not create them') 45 | 46 | if not alembic_helpers.table_has_column('run', 'report'): 47 | print('Column report not present in run table, will create') 48 | op.add_column(u'run', sa.Column('report', sa.Text(), nullable=True)) 49 | else: 50 | print('Column report already present in run table') 51 | 52 | 53 | def downgrade(): 54 | # ### commands auto generated by Alembic - please adjust! ### 55 | op.drop_column(u'run', 'report') 56 | op.drop_table('check_vars') 57 | op.drop_table('probe_vars') 58 | # ### end Alembic commands ### 59 | -------------------------------------------------------------------------------- /GeoHealthCheck/migrations/versions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopython/GeoHealthCheck/48fb6493694981689a811204968c9c5ee50ef5b5/GeoHealthCheck/migrations/versions/__init__.py -------------------------------------------------------------------------------- /GeoHealthCheck/migrations/versions/bb91fb332c36_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: bb91fb332c36 4 | Revises: f2bac0a1cdaa 5 | Create Date: 2018-02-13 17:08:09.313113 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from GeoHealthCheck.migrations import alembic_helpers 11 | 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = 'bb91fb332c36' 15 | down_revision = '2638c2a40625' 16 | branch_labels = None 17 | depends_on = None 18 | 19 | 20 | def upgrade(): 21 | if not alembic_helpers.tables_exist(['recipient']): 22 | print('creating Recipient table(s)') 23 | 24 | # ### commands auto generated by Alembic - please adjust! ### 25 | op.create_table('recipient', 26 | sa.Column('id', sa.Integer(), nullable=False), 27 | sa.Column('channel', sa.Enum('email', 'webhook', name='recipient_channel_types'), nullable=False), 28 | sa.Column('location', sa.Text(), nullable=False), 29 | sa.PrimaryKeyConstraint('id') 30 | ) 31 | 32 | op.create_table('resourcenotification', 33 | sa.Column('resource_id', sa.Integer(), nullable=False), 34 | sa.Column('recipient_id', sa.Integer(), nullable=False), 35 | sa.ForeignKeyConstraint(['resource_id'], ['resource.identifier'], ), 36 | sa.ForeignKeyConstraint(['recipient_id'], ['recipient.id'], ), 37 | sa.PrimaryKeyConstraint('resource_id', 'recipient_id') 38 | ) 39 | # ### end Alembic commands ### 40 | else: 41 | print('Tables for recipient support already present, will not create them') 42 | 43 | 44 | def downgrade(): 45 | op.drop_table('resourcenotification') 46 | op.drop_table('recipient') 47 | -------------------------------------------------------------------------------- /GeoHealthCheck/migrations/versions/f72ff1ac3967_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: f72ff1ac3967 4 | Revises: 90e1c865a561 5 | Create Date: 2019-06-24 09:33:20.664465 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from GeoHealthCheck.migrations import alembic_helpers 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'f72ff1ac3967' 14 | down_revision = '90e1c865a561' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | 20 | def upgrade(): 21 | if not alembic_helpers.table_has_column('resource', 'auth'): 22 | print('Column auth not present in resource table, will create') 23 | op.add_column(u'resource', sa.Column('auth', sa.Text(), 24 | nullable=True, default=None, server_default=None)) 25 | else: 26 | print('Column auth already present in resource table') 27 | 28 | 29 | def downgrade(): 30 | print('Dropping Column auth from resource table') 31 | op.drop_column(u'resource', 'auth') 32 | -------------------------------------------------------------------------------- /GeoHealthCheck/plugins/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopython/GeoHealthCheck/48fb6493694981689a811204968c9c5ee50ef5b5/GeoHealthCheck/plugins/__init__.py -------------------------------------------------------------------------------- /GeoHealthCheck/plugins/check/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopython/GeoHealthCheck/48fb6493694981689a811204968c9c5ee50ef5b5/GeoHealthCheck/plugins/check/__init__.py -------------------------------------------------------------------------------- /GeoHealthCheck/plugins/geocode/fixedlocation.py: -------------------------------------------------------------------------------- 1 | # ================================================================= 2 | # 3 | # Authors: Rob van Loon 4 | # 5 | # Copyright (c) 2021 Rob van Loon 6 | # 7 | # Permission is hereby granted, free of charge, to any person 8 | # obtaining a copy of this software and associated documentation 9 | # files (the "Software"), to deal in the Software without 10 | # restriction, including without limitation the rights to use, 11 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the 13 | # Software is furnished to do so, subject to the following 14 | # conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be 17 | # included in all copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 21 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 23 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 24 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 26 | # OTHER DEALINGS IN THE SOFTWARE. 27 | # 28 | # ================================================================= 29 | 30 | from GeoHealthCheck.geocoder import Geocoder 31 | 32 | 33 | class FixedLocation(Geocoder): 34 | """ 35 | Spoof getting a geolocation for a server by provinding a fixed lat, lon 36 | result. The lat, lon can be specified in the initialisation parameters. 37 | When omitted: default to 0, 0. 38 | """ 39 | 40 | NAME = 'Fixed geolocation' 41 | 42 | DESCRIPTION = 'Geolocator service returning a fixed position (so ' \ 43 | 'actually no real geolocation).' 44 | 45 | LATITUDE = 0 46 | """ 47 | Parameter with the default latitude position. This is overruled when the 48 | latitude option is provided in the init step. 49 | """ 50 | 51 | LONGITUDE = 0 52 | """ 53 | Parameter with the default longitude position. This is overruled when the 54 | longitude option is provided in the init step. 55 | """ 56 | 57 | def __init__(self): 58 | super().__init__() 59 | self._lat = self.LATITUDE 60 | self._lon = self.LONGITUDE 61 | 62 | def init(self, geocode_vars={}): 63 | """ 64 | Initialise the geocoder service with an optional dictionary. 65 | 66 | When the dictionary contains the element `lat` and/or `lon`, then these 67 | values are used to position the server. 68 | """ 69 | super().init(geocode_vars) 70 | self._lat = geocode_vars.get('lat', self.LATITUDE) 71 | self._lon = geocode_vars.get('lon', self.LONGITUDE) 72 | 73 | def locate(self, _=None): 74 | """ 75 | Perform a geocoding to locate a server. In this case it will render a 76 | fixed position, so provinding the adress of the server is optional. 77 | """ 78 | return (self._lat, self._lon) 79 | -------------------------------------------------------------------------------- /GeoHealthCheck/plugins/probe/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopython/GeoHealthCheck/48fb6493694981689a811204968c9c5ee50ef5b5/GeoHealthCheck/plugins/probe/__init__.py -------------------------------------------------------------------------------- /GeoHealthCheck/plugins/probe/ghcreport.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from email.mime.text import MIMEText 3 | from email.utils import formataddr 4 | 5 | from flask_babel import gettext 6 | 7 | from GeoHealthCheck.init import App 8 | from GeoHealthCheck.probe import Probe 9 | from GeoHealthCheck.result import Result 10 | from GeoHealthCheck.util import render_template2, send_email 11 | 12 | LOGGER = logging.getLogger(__name__) 13 | 14 | 15 | class GHCEmailReporter(Probe): 16 | """ 17 | Probe for GeoHealthCheck endpoint recurring status Reporter. 18 | When invoked it will get the overall status of the GHC Endpoint and email 19 | a summary, with links to more detailed reports. 20 | """ 21 | 22 | NAME = 'GHC Email Reporter' 23 | 24 | DESCRIPTION = 'Fetches Resources status summary ' \ 25 | 'from GHC endpoint and reports by email' 26 | 27 | RESOURCE_TYPE = 'GHC:Report' 28 | 29 | REQUEST_METHOD = 'GET' 30 | 31 | PARAM_DEFS = { 32 | 'email': { 33 | 'type': 'string', 34 | 'description': 'A comma-separated list of email addresses \ 35 | to send status report to', 36 | 'default': None, 37 | 'required': True 38 | } 39 | } 40 | 41 | """Param defs""" 42 | 43 | def __init__(self): 44 | Probe.__init__(self) 45 | 46 | def perform_request(self): 47 | """ 48 | Perform the reporting. 49 | """ 50 | 51 | # Be sure to use bare root URL http://.../FeatureServer 52 | ghc_url = self._resource.url.split('?')[0] 53 | 54 | # Assemble request templates with root FS URL 55 | 56 | summary_url = ghc_url + '/api/v1.0/summary/' 57 | 58 | # 1. Get the summary (JSON) report from GHC endpoint 59 | result = Result(True, 'Get GHC Report') 60 | result.start() 61 | try: 62 | response = self.perform_get_request(summary_url) 63 | status = response.status_code 64 | overall_status = status / 100 65 | if overall_status in [4, 5]: 66 | raise Exception('HTTP Error status=%d reason=%s' 67 | % (status, response.reason)) 68 | 69 | summary_report = response.json() 70 | except Exception as err: 71 | msg = 'Cannot get summary from %s err=%s' % \ 72 | (summary_url, str(err)) 73 | result.set(False, msg) 74 | result.stop() 75 | self.result.add_result(result) 76 | return 77 | 78 | # ASSERTION - summary report fetch ok 79 | 80 | # 2. Do email reporting with summary report 81 | result = Result(True, 'Send Email') 82 | result.start() 83 | 84 | try: 85 | config = App.get_config() 86 | 87 | # Create message body with report 88 | template_vars = { 89 | 'summary': summary_report, 90 | 'config': config 91 | } 92 | 93 | msg_body = render_template2( 94 | 'status_report_email.txt', template_vars) 95 | 96 | resource = self._resource 97 | to_addrs = self._parameters.get('email', None) 98 | if to_addrs is None: 99 | raise Exception( 100 | 'No emails set for GHCEmailReporter in resource=%s' % 101 | resource.identifier) 102 | 103 | to_addrs = to_addrs.replace(' ', '') 104 | if len(to_addrs) == 0: 105 | raise Exception( 106 | 'No emails set for GHCEmailReporter in resource=%s' % 107 | resource.identifier) 108 | 109 | to_addrs = to_addrs.split(',') 110 | msg = MIMEText(msg_body, 'plain', 'utf-8') 111 | msg['From'] = formataddr((config['GHC_SITE_TITLE'], 112 | config['GHC_ADMIN_EMAIL'])) 113 | msg['To'] = ', '.join(to_addrs) 114 | msg['Subject'] = '[%s] %s' % (config['GHC_SITE_TITLE'], 115 | gettext('Status summary')) 116 | 117 | from_addr = '%s <%s>' % (config['GHC_SITE_TITLE'], 118 | config['GHC_ADMIN_EMAIL']) 119 | 120 | msg_text = msg.as_string() 121 | send_email(config['GHC_SMTP'], from_addr, to_addrs, msg_text) 122 | except Exception as err: 123 | msg = 'Cannot send email. Contact admin: ' 124 | LOGGER.warning(msg + ' err=' + str(err)) 125 | result.set(False, 'Cannot send email: %s' % str(err)) 126 | 127 | result.stop() 128 | 129 | # Add to overall Probe result 130 | self.result.add_result(result) 131 | -------------------------------------------------------------------------------- /GeoHealthCheck/plugins/probe/http.py: -------------------------------------------------------------------------------- 1 | from GeoHealthCheck.probe import Probe 2 | 3 | 4 | class HttpGet(Probe): 5 | """ 6 | Do HTTP GET Request, to poll/ping any Resource bare url. 7 | """ 8 | 9 | NAME = 'HTTP GET Resource URL' 10 | DESCRIPTION = 'Simple HTTP GET on Resource URL' 11 | RESOURCE_TYPE = '*:*' 12 | REQUEST_METHOD = 'GET' 13 | 14 | CHECKS_AVAIL = { 15 | 'GeoHealthCheck.plugins.check.checks.HttpStatusNoError': { 16 | 'default': True 17 | }, 18 | 'GeoHealthCheck.plugins.check.checks.ContainsStrings': {}, 19 | 'GeoHealthCheck.plugins.check.checks.NotContainsStrings': {}, 20 | 'GeoHealthCheck.plugins.check.checks.HttpHasContentType': {} 21 | } 22 | """Checks avail""" 23 | 24 | 25 | class HttpGetQuery(HttpGet): 26 | """ 27 | Do HTTP GET Request, to poll/ping any Resource bare url with query string. 28 | """ 29 | 30 | NAME = 'HTTP GET Resource URL with query' 31 | DESCRIPTION = """ 32 | HTTP GET Resource URL with 33 | ?query string to be user-supplied (without ?) 34 | """ 35 | REQUEST_TEMPLATE = '?{query}' 36 | 37 | PARAM_DEFS = { 38 | 'query': { 39 | 'type': 'string', 40 | 'description': 'The query string to add to request (without ?)', 41 | 'default': None, 42 | 'required': True 43 | } 44 | } 45 | """Param defs""" 46 | 47 | 48 | class HttpPost(HttpGet): 49 | """ 50 | Do HTTP POST Request, to send POST request to 51 | Resource bare url with POST body. 52 | """ 53 | 54 | NAME = 'HTTP POST Resource URL with body' 55 | DESCRIPTION = """ 56 | HTTP POST to Resource URL with body 57 | content(-type) to be user-supplied 58 | """ 59 | 60 | REQUEST_METHOD = 'POST' 61 | REQUEST_HEADERS = {'content-type': '{post_content_type}'} 62 | REQUEST_TEMPLATE = '{body}' 63 | 64 | PARAM_DEFS = { 65 | 'body': { 66 | 'type': 'string', 67 | 'description': 'The post body to send', 68 | 'default': None, 69 | 'required': True 70 | }, 71 | 'content_type': { 72 | 'type': 'string', 73 | 'description': 'The post content type to send', 74 | 'default': 'text/xml;charset=UTF-8', 75 | 'required': True 76 | } 77 | } 78 | """Param defs""" 79 | 80 | def get_request_headers(self): 81 | """ 82 | Overridden from Probe: construct request_headers 83 | via parameter substitution from content_type Parameter. 84 | """ 85 | 86 | # content_type = 87 | # {'post_content_type': self._parameters['content_type']} 88 | # request_headers = 89 | # self.REQUEST_HEADERS['content-type'].format(**content_type) 90 | # Hmm seems simpler 91 | headers = Probe.get_request_headers(self) 92 | headers.update( 93 | {'Content-Type': self._parameters['content_type']}) 94 | 95 | return headers 96 | -------------------------------------------------------------------------------- /GeoHealthCheck/plugins/probe/mapbox.py: -------------------------------------------------------------------------------- 1 | from GeoHealthCheck.probe import Probe 2 | import math 3 | from pyproj import CRS, Transformer 4 | 5 | 6 | class TileJSON(Probe): 7 | """ 8 | TileJSON 9 | """ 10 | 11 | NAME = 'TileJSON' 12 | DESCRIPTION = 'Request Mapbox TileJSON Service and ' + \ 13 | 'request each zoom level at center coordinates' 14 | RESOURCE_TYPE = 'Mapbox:TileJSON' 15 | REQUEST_METHOD = 'GET' 16 | 17 | CHECKS_AVAIL = { 18 | 'GeoHealthCheck.plugins.check.checks.HttpStatusNoError': { 19 | 'default': True 20 | }, 21 | } 22 | """Checks avail""" 23 | 24 | PARAM_DEFS = { 25 | 'lat_4326': { 26 | 'type': 'float', 27 | 'description': 'latitude in EPSG:4326', 28 | 'required': False 29 | }, 30 | 'lon_4326': { 31 | 'type': 'float', 32 | 'description': 'longitude in EPSG:4326', 33 | 'required': False 34 | }, 35 | } 36 | 37 | def perform_request(self): 38 | url_base = self._resource.url 39 | 40 | # Remove trailing '/' if present 41 | if url_base.endswith('/'): 42 | url_base = url_base[0:-2] 43 | 44 | # Add .json to url if not present yet 45 | if url_base.endswith('.json'): 46 | json_url = url_base 47 | else: 48 | json_url = url_base + '.json' 49 | 50 | self.log('Requesting: %s url=%s' % (self.REQUEST_METHOD, json_url)) 51 | self.response = Probe.perform_get_request(self, json_url) 52 | self.run_checks() 53 | 54 | tile_info = self.response.json() 55 | 56 | lat, lon = self.get_latlon(tile_info) 57 | if not lat or not lon: 58 | # If none of the above are present, raise error 59 | err_message = 'No center coordinates given in tile.json.' + \ 60 | 'Please add lat/lon as probe parameters.' 61 | self.result.set(False, err_message) 62 | return 63 | 64 | # Convert bound coordinates to WebMercator 65 | transformer = Transformer.from_crs(CRS('EPSG:4326'), 66 | CRS('EPSG:3857'), 67 | always_xy=False) 68 | wm_coords = transformer.transform(lat, lon) 69 | 70 | # Circumference (2 * pi * Semi-major Axis) 71 | circ = 2 * math.pi * 6378137.0 72 | 73 | # For calculating the relative tile index for zoom levels 74 | x_rel = (circ / 2 + wm_coords[0]) / circ 75 | y_rel = (circ / 2 - wm_coords[1]) / circ 76 | 77 | for tile_url in tile_info['tiles']: 78 | zoom_list = range(tile_info.get('minzoom', 0), 79 | tile_info.get('maxzoom', 22) + 1) 80 | 81 | for zoom in zoom_list: 82 | tile_count = 2 ** zoom 83 | zxy = { 84 | 'z': zoom, 85 | 'x': int(x_rel * tile_count), 86 | 'y': int(y_rel * tile_count), 87 | } 88 | 89 | # Determine the tile URL. 90 | zoom_url = tile_url.format(**zxy) 91 | 92 | self.log('Requesting zoom %s: url=%s' % (zoom, zoom_url)) 93 | 94 | self.response = Probe.perform_get_request(self, zoom_url) 95 | self.run_checks() 96 | 97 | def get_latlon(self, tile_info): 98 | if ('lat_4326' in self._parameters and 99 | 'lon_4326' in self._parameters): 100 | if (self._parameters['lat_4326'] and 101 | self._parameters['lon_4326']): 102 | lat = self._parameters['lat_4326'] 103 | lon = self._parameters['lon_4326'] 104 | return lat, lon 105 | 106 | # If there are no user input parameters, take center from json 107 | if 'center' in tile_info: 108 | lat, lon = tile_info['center'][1], tile_info['center'][0] 109 | return lat, lon 110 | 111 | # If there is no center attribute in json, take bounds from json 112 | if 'bounds' in tile_info: 113 | lat = (tile_info['bounds'][1] + tile_info['bounds'][3]) / 2 114 | lon = (tile_info['bounds'][0] + tile_info['bounds'][2]) / 2 115 | return lat, lon 116 | 117 | return False, False 118 | -------------------------------------------------------------------------------- /GeoHealthCheck/plugins/probe/ogc3dtiles.py: -------------------------------------------------------------------------------- 1 | from GeoHealthCheck.probe import Probe 2 | import requests 3 | 4 | 5 | class OGC3DTiles(Probe): 6 | """ 7 | OGC3DTiles 8 | """ 9 | 10 | NAME = 'GET Tileset.json and tile data' 11 | DESCRIPTION = 'Request tileset.json, ' + \ 12 | 'and recursively find and request tile data url' 13 | RESOURCE_TYPE = 'OGC:3DTiles' 14 | REQUEST_METHOD = 'GET' 15 | 16 | CHECKS_AVAIL = { 17 | 'GeoHealthCheck.plugins.check.checks.HttpStatusNoError': { 18 | 'default': True 19 | }, 20 | } 21 | """Checks avail""" 22 | 23 | def perform_request(self): 24 | url_base = self._resource.url 25 | 26 | # Remove trailing '/' if present 27 | if url_base.endswith('/'): 28 | url_base = url_base[:-1] 29 | elif url_base.endswith('/tileset.json'): 30 | url_base = url_base.split('/tileset.json')[0] 31 | 32 | # Request tileset.json 33 | try: 34 | tile_url = url_base + '/tileset.json' 35 | self.log('Requesting: %s url=%s' % (self.REQUEST_METHOD, tile_url)) 36 | self.response = Probe.perform_get_request(self, tile_url) 37 | self.run_checks() 38 | except requests.exceptions.RequestException as e: 39 | msg = "Request Err: Error requesting tileset.json %s %s" \ 40 | % (e.__class__.__name__, str(e)) 41 | self.result.set(False, msg) 42 | # If error occurs during request of tileset.json, no use going on 43 | return 44 | 45 | # Get data url from tileset.json and request tile data 46 | try: 47 | tile_root = self.response.json()['root'] 48 | data_uri = self.get_3d_tileset_content_uri(tile_root) 49 | data_url = url_base + '/' + data_uri 50 | self.log('Requesting: %s url=%s' % (self.REQUEST_METHOD, data_url)) 51 | self.response = Probe.perform_get_request(self, data_url) 52 | self.run_checks() 53 | except requests.exceptions.RequestException as e: 54 | msg = "Request Err: Error requesting tile data %s %s" \ 55 | % (e.__class__.__name__, str(e)) 56 | self.result.set(False, msg) 57 | 58 | def get_3d_tileset_content_uri(self, tile): 59 | # Use recursion to find tile data url 60 | for child in tile['children']: 61 | if 'content' in child: 62 | return child['content']['uri'] 63 | 64 | result = self.get_3d_tileset_content_uri(child) 65 | 66 | return result 67 | -------------------------------------------------------------------------------- /GeoHealthCheck/plugins/probe/sta.py: -------------------------------------------------------------------------------- 1 | from GeoHealthCheck.probe import Probe 2 | 3 | 4 | class StaCaps(Probe): 5 | """Probe for SensorThings API main endpoint url""" 6 | 7 | NAME = 'STA Capabilities' 8 | DESCRIPTION = 'Perform STA Capabilities Operation and check validity' 9 | RESOURCE_TYPE = 'OGC:STA' 10 | 11 | REQUEST_METHOD = 'GET' 12 | 13 | def __init__(self): 14 | Probe.__init__(self) 15 | 16 | CHECKS_AVAIL = { 17 | 'GeoHealthCheck.plugins.check.checks.HttpStatusNoError': { 18 | 'default': True 19 | }, 20 | 'GeoHealthCheck.plugins.check.checks.JsonParse': { 21 | 'default': True 22 | }, 23 | 'GeoHealthCheck.plugins.check.checks.ContainsStrings': { 24 | 'default': True, 25 | 'set_params': { 26 | 'strings': { 27 | 'name': 'Must contain STA Entity names', 28 | 'value': ['Things', 'Datastreams', 'Observations', 29 | 'FeaturesOfInterest', 'Locations'] 30 | } 31 | } 32 | }, 33 | } 34 | """ 35 | Checks avail for all specific Caps checks. 36 | Optionally override Check.PARAM_DEFS using set_params 37 | e.g. with specific `value` or even `name`. 38 | """ 39 | 40 | 41 | class StaGetEntities(Probe): 42 | """Fetch STA entities of type and check result""" 43 | 44 | NAME = 'STA GetEntities' 45 | DESCRIPTION = 'Fetch all STA Entities of given type' 46 | RESOURCE_TYPE = 'OGC:STA' 47 | 48 | REQUEST_METHOD = 'GET' 49 | 50 | # e.g. http://52.26.56.239:8080/OGCSensorThings/v1.0/Things 51 | REQUEST_TEMPLATE = '/{entities}' 52 | 53 | def __init__(self): 54 | Probe.__init__(self) 55 | 56 | PARAM_DEFS = { 57 | 'entities': { 58 | 'type': 'string', 59 | 'description': 'The STA Entity collection type', 60 | 'default': 'Things', 61 | 'required': True, 62 | 'range': ['Things', 'DataStreams', 'Observations', 63 | 'Locations', 'Sensors', 'FeaturesOfInterest', 64 | 'ObservedProperties', 'HistoricalLocations'] 65 | } 66 | } 67 | """Param defs""" 68 | 69 | CHECKS_AVAIL = { 70 | 'GeoHealthCheck.plugins.check.checks.HttpStatusNoError': { 71 | 'default': True 72 | }, 73 | 'GeoHealthCheck.plugins.check.checks.JsonParse': { 74 | 'default': True 75 | } 76 | } 77 | """Check for STA Get entity Collection""" 78 | -------------------------------------------------------------------------------- /GeoHealthCheck/plugins/probe/wcs.py: -------------------------------------------------------------------------------- 1 | from GeoHealthCheck.probe import Probe 2 | from owslib.wcs import WebCoverageService 3 | 4 | 5 | class WcsGetCoverage(Probe): 6 | """ 7 | Get WCS coverage image using the OGC WCS GetCoverage v2.0.1 Operation. 8 | """ 9 | 10 | NAME = 'WCS GetCoverage v2.0.1' 11 | DESCRIPTION = """ 12 | Do WCS GetCoverage v2.0.1 request with user-specified parameters 13 | for single Layer. 14 | """ 15 | RESOURCE_TYPE = 'OGC:WCS' 16 | 17 | REQUEST_METHOD = 'GET' 18 | REQUEST_TEMPLATE = '?SERVICE=WCS&VERSION=2.0.1' + \ 19 | '&REQUEST=GetCoverage&COVERAGEID={layers}' + \ 20 | '&FORMAT={format}' + \ 21 | '&SUBSET=x({subset[0]},{subset[2]})' + \ 22 | '&SUBSET=y({subset[1]},{subset[3]})' + \ 23 | '&SUBSETTINGCRS={subsetting_crs}' + \ 24 | '&WIDTH={width}&HEIGHT={height}' 25 | 26 | PARAM_DEFS = { 27 | 'layers': { 28 | 'type': 'stringlist', 29 | 'description': 'The WCS Layer ID, select one', 30 | 'default': [], 31 | 'required': True, 32 | 'range': None 33 | }, 34 | 'format': { 35 | 'type': 'string', 36 | 'description': 'Image outputformat', 37 | 'default': [], 38 | 'required': True, 39 | 'range': None 40 | }, 41 | 'subset': { 42 | 'type': 'bbox', 43 | 'description': 'The WCS subset of x and y axis', 44 | 'default': ['-180', '-90', '180', '90'], 45 | 'required': True, 46 | 'range': None 47 | }, 48 | 'subsetting_crs': { 49 | 'type': 'string', 50 | 'description': 'The crs of SUBSET and also OUTPUTCRS', 51 | 'default': '', 52 | 'required': True, 53 | 'range': None 54 | }, 55 | 'width': { 56 | 'type': 'string', 57 | 'description': 'The image width', 58 | 'default': '10', 59 | 'required': True 60 | }, 61 | 'height': { 62 | 'type': 'string', 63 | 'description': 'The image height', 64 | 'default': '10', 65 | 'required': True 66 | } 67 | } 68 | """Param defs""" 69 | 70 | CHECKS_AVAIL = { 71 | 'GeoHealthCheck.plugins.check.checks.HttpStatusNoError': { 72 | 'default': True 73 | }, 74 | 'GeoHealthCheck.plugins.check.checks.NotContainsOwsException': { 75 | 'default': True 76 | }, 77 | 'GeoHealthCheck.plugins.check.checks.HttpHasImageContentType': { 78 | 'default': True 79 | } 80 | } 81 | """ 82 | Checks for WCS GetCoverage Response available. 83 | Optionally override Check PARAM_DEFS using set_params 84 | e.g. with specific `value` or even `name`. 85 | """ 86 | def __init__(self): 87 | Probe.__init__(self) 88 | self.layer_count = 0 89 | 90 | def get_metadata(self, resource, version='2.0.1'): 91 | """ 92 | Get metadata, specific per Resource type. 93 | :param resource: 94 | :param version: 95 | :return: Metadata object 96 | """ 97 | return WebCoverageService(resource.url, version=version) 98 | 99 | # Overridden: expand param-ranges from WCS metadata 100 | def expand_params(self, resource): 101 | 102 | # Use WCS Capabilities doc to get metadata for 103 | # PARAM_DEFS ranges/defaults 104 | try: 105 | wcs = self.get_metadata_cached(resource, version='2.0.1') 106 | layers = wcs.contents 107 | self.layer_count = len(layers) 108 | 109 | # Layers to select 110 | self.PARAM_DEFS['layers']['range'] = list(layers.keys()) 111 | 112 | # Take random layer to determine generic attrs 113 | for layer_name in layers: 114 | layer_entry = layers[layer_name] 115 | break 116 | 117 | # Image Format 118 | self.PARAM_DEFS['format']['range'] = layer_entry.supportedFormats 119 | 120 | # SRS 121 | bbox = layer_entry.boundingboxes 122 | subsetting_crs = bbox[0]['nativeSrs'] 123 | self.PARAM_DEFS['subsetting_crs']['default'] = subsetting_crs 124 | 125 | # BBOX 126 | self.log('bbox: %s' % str(bbox[0]['bbox'])) 127 | self.PARAM_DEFS['subset']['default'] = bbox[0]['bbox'] 128 | 129 | except Exception as err: 130 | raise err 131 | -------------------------------------------------------------------------------- /GeoHealthCheck/plugins/probe/wmsdrilldown.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from GeoHealthCheck.probe import Probe 4 | from GeoHealthCheck.result import Result 5 | from owslib.wms import WebMapService 6 | 7 | 8 | class WmsDrilldown(Probe): 9 | """ 10 | Probe for WMS endpoint "drilldown": starting 11 | with GetCapabilities doc: get Layers and do 12 | GetMap on them etc. Using OWSLib.WebMapService. 13 | 14 | TODO: needs finalization. 15 | """ 16 | 17 | NAME = 'WMS Drilldown' 18 | DESCRIPTION = 'Traverses a WMS endpoint by drilling down from Capabilities' 19 | RESOURCE_TYPE = 'OGC:WMS' 20 | 21 | REQUEST_METHOD = 'GET' 22 | 23 | PARAM_DEFS = { 24 | 'drilldown_level': { 25 | 'type': 'string', 26 | 'description': 'How heavy the drilldown should be.', 27 | 'default': 'minor', 28 | 'required': True, 29 | 'range': ['minor', 'moderate', 'full'] 30 | } 31 | } 32 | """Param defs""" 33 | 34 | def __init__(self): 35 | Probe.__init__(self) 36 | 37 | def perform_request(self): 38 | """ 39 | Perform the drilldown. 40 | See https://github.com/geopython/OWSLib/blob/ 41 | master/tests/doctests/wms_GeoServerCapabilities.txt 42 | """ 43 | wms = None 44 | 45 | # 1. Test capabilities doc, parses 46 | result = Result(True, 'Test Capabilities') 47 | result.start() 48 | try: 49 | wms = WebMapService(self._resource.url, 50 | headers=self.get_request_headers()) 51 | title = wms.identification.title 52 | self.log('response: title=%s' % title) 53 | except Exception as err: 54 | result.set(False, str(err)) 55 | 56 | result.stop() 57 | self.result.add_result(result) 58 | 59 | # 2. Test layers 60 | # TODO: use parameters to work on less/more drilling 61 | # "full" could be all layers. 62 | result = Result(True, 'Test Layers') 63 | result.start() 64 | try: 65 | # Pick a random layer 66 | layer_name = random.sample(wms.contents.keys(), 1)[0] 67 | layer = wms[layer_name] 68 | 69 | # TODO Only use EPSG:4326, later random CRS 70 | if 'EPSG:4326' in layer.crsOptions \ 71 | and layer.boundingBoxWGS84: 72 | 73 | # Search GetMap operation 74 | get_map_oper = None 75 | for oper in wms.operations: 76 | if oper.name == 'GetMap': 77 | get_map_oper = oper 78 | break 79 | 80 | format = None 81 | for format in get_map_oper.formatOptions: 82 | if format.startswith('image/'): 83 | break 84 | 85 | # format = random.sample(get_map_oper.formatOptions, 1)[0] 86 | 87 | self.log('testing layer: %s' % layer_name) 88 | layer_bbox = layer.boundingBoxWGS84 89 | wms.getmap(layers=[layer_name], 90 | styles=[''], 91 | srs='EPSG:4326', 92 | bbox=(layer_bbox[0], 93 | layer_bbox[1], 94 | layer_bbox[2], 95 | layer_bbox[3]), 96 | size=(256, 256), 97 | format=format, 98 | transparent=False) 99 | 100 | self.log('WMS GetMap: format=%s' % format) 101 | # Etc, to be finalized 102 | 103 | except Exception as err: 104 | result.set(False, str(err)) 105 | 106 | result.stop() 107 | 108 | # Add to overall Probe result 109 | self.result.add_result(result) 110 | -------------------------------------------------------------------------------- /GeoHealthCheck/plugins/resourceauth/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopython/GeoHealthCheck/48fb6493694981689a811204968c9c5ee50ef5b5/GeoHealthCheck/plugins/resourceauth/__init__.py -------------------------------------------------------------------------------- /GeoHealthCheck/plugins/resourceauth/resourceauths.py: -------------------------------------------------------------------------------- 1 | import base64 2 | 3 | from GeoHealthCheck.resourceauth import ResourceAuth 4 | 5 | 6 | class NoAuth(ResourceAuth): 7 | """ 8 | Checks if header exists and has given header value. 9 | See http://docs.python-requests.org/en/master/user/quickstart 10 | """ 11 | 12 | NAME = 'None' 13 | DESCRIPTION = 'Default class for no auth' 14 | 15 | PARAM_DEFS = {} 16 | """Param defs""" 17 | 18 | def __init__(self): 19 | ResourceAuth.__init__(self) 20 | 21 | def verify(self): 22 | return False 23 | 24 | def encode(self): 25 | return None 26 | 27 | 28 | class BasicAuth(ResourceAuth): 29 | """ 30 | Basic authentication. 31 | """ 32 | 33 | NAME = 'Basic' 34 | DESCRIPTION = 'Default class for no auth' 35 | 36 | PARAM_DEFS = { 37 | 'username': { 38 | 'type': 'string', 39 | 'description': 'Username', 40 | 'default': None, 41 | 'required': True, 42 | 'range': None 43 | }, 44 | 'password': { 45 | 'type': 'password', 46 | 'description': 'Password', 47 | 'default': None, 48 | 'required': True, 49 | 'range': None 50 | } 51 | } 52 | """Param defs""" 53 | 54 | def __init__(self): 55 | ResourceAuth.__init__(self) 56 | 57 | def verify(self): 58 | if self.auth_dict is None: 59 | return False 60 | 61 | if 'data' not in self.auth_dict: 62 | return False 63 | 64 | auth_data = self.auth_dict['data'] 65 | 66 | if auth_data.get('username', None) is None: 67 | return False 68 | 69 | if len(auth_data.get('username', '')) == 0: 70 | return False 71 | 72 | if auth_data.get('password', None) is None: 73 | return False 74 | 75 | if len(auth_data.get('password', '')) == 0: 76 | return False 77 | 78 | return True 79 | 80 | def encode_auth_header_val(self): 81 | """ 82 | Get encoded authorization header value from config data. 83 | Authorization scheme-specific. :: 84 | 85 | { 86 | 'type': 'Basic', 87 | 'data': { 88 | 'username': 'the_user', 89 | 'password': 'the_password' 90 | } 91 | } 92 | 93 | :return: None or http Basic auth header value 94 | """ 95 | 96 | # Has auth, encode as HTTP header value 97 | # Basic auth: 98 | # http://mozgovipc.blogspot.nl/2012/06/ 99 | # python-http-basic-authentication-with.html 100 | # base64 encode username and password 101 | # write the Authorization header 102 | # like: 'Basic base64encode(username + ':' + password) 103 | auth_creds = self.auth_dict['data'] 104 | auth_val = base64.encodebytes( 105 | '{}:{}'.format(auth_creds['username'], auth_creds['password']). 106 | encode()) 107 | auth_val = 'Basic {}'.format(auth_val.decode()) 108 | return auth_val 109 | 110 | 111 | class BearerTokenAuth(ResourceAuth): 112 | """ 113 | Bearer token auth 114 | """ 115 | 116 | NAME = 'Bearer Token' 117 | DESCRIPTION = 'Bearer token auth' 118 | 119 | PARAM_DEFS = { 120 | 'token': { 121 | 'type': 'password', 122 | 'description': 'Token string', 123 | 'default': None, 124 | 'required': True, 125 | 'range': None 126 | } 127 | } 128 | """Param defs""" 129 | 130 | def __init__(self): 131 | ResourceAuth.__init__(self) 132 | 133 | def verify(self): 134 | if self.auth_dict is None: 135 | return False 136 | 137 | if 'data' not in self.auth_dict: 138 | return False 139 | 140 | auth_data = self.auth_dict['data'] 141 | 142 | if auth_data.get('token', None) is None: 143 | return False 144 | 145 | if len(auth_data.get('token', '')) == 0: 146 | return False 147 | 148 | return True 149 | 150 | def encode_auth_header_val(self): 151 | """ 152 | Get encoded authorization header value from config data. 153 | Authorization scheme-specific. :: 154 | 155 | { 156 | 'type': 'Bearer Token', 157 | 'data': { 158 | 'token': 'the_token' 159 | } 160 | } 161 | 162 | :return: None or http auth header value 163 | """ 164 | 165 | # Bearer Type, see eg. https://tools.ietf.org/html/rfc6750 166 | return "Bearer %s" % self.auth_dict['data']['token'] 167 | -------------------------------------------------------------------------------- /GeoHealthCheck/plugins/user/README.md: -------------------------------------------------------------------------------- 1 | # User Plugins 2 | 3 | GHC User Plugins can add new Probes and Checks or extend existing ones. 4 | 5 | ## In Regular GHC App 6 | 7 | There's always two steps required to get your Plugins available in the GHC 8 | App: 9 | 10 | - add their class or module names to available Plugins config in your `site_config.py` 11 | - add their location to the `PYTHONPATH` 12 | 13 | Easiest is to directly add your Plugins in the directory `GeoHealthCheck/plugins/user` under 14 | your GHC install dir. In that case step 2 is not required, but you should name the Plugins 15 | `GeoHealthCheck.plugins.user..[yourclass]` within `site_config.py` 16 | such that they can be found in the app's `PYTHONPATH`. 17 | 18 | ## Via Docker 19 | 20 | When using Docker, Plugins need to be available under `/plugins` within the 21 | GHC Docker Image or Container. When the Container starts it will copy all content under 22 | `/userplugins` to the internal dir `/GeoHealthCheck/GeoHealthCheck/plugins/user`. 23 | 24 | Plugins can be added to your app as follows: 25 | 26 | - add to Docker Image at build-time 27 | - add to Docker Container at run-time, e.g. via Docker Compose 28 | 29 | See [Docker Readme at GHC GitHub](https://github.com/geopython/GeoHealthCheck/blob/master/docker/README.md) 30 | for more info. 31 | -------------------------------------------------------------------------------- /GeoHealthCheck/plugins/user/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopython/GeoHealthCheck/48fb6493694981689a811204968c9c5ee50ef5b5/GeoHealthCheck/plugins/user/__init__.py -------------------------------------------------------------------------------- /GeoHealthCheck/resourceauth.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | from plugin import Plugin 4 | from factory import Factory 5 | from util import encode, decode 6 | from init import App 7 | APP = App.get_app() 8 | LOGGER = logging.getLogger(__name__) 9 | 10 | 11 | class ResourceAuth(Plugin): 12 | """ 13 | Base class for specific Plugin implementations to perform 14 | authentication on a Resource. Subclasses provide specific 15 | auth methods like Basic Auth, Bearer Token etc. 16 | """ 17 | 18 | def __init__(self): 19 | Plugin.__init__(self) 20 | self.resource = None 21 | self.auth_dict = None 22 | 23 | # Lifecycle 24 | def init(self, auth_dict=None): 25 | """ 26 | Initialize ResourceAuth with related Resource and auth dict. 27 | :return: 28 | """ 29 | self.auth_dict = auth_dict 30 | 31 | @staticmethod 32 | def create(auth_dict): 33 | auth_type = auth_dict['type'] 34 | auth_obj_def = ResourceAuth.get_auth_defs()[auth_type] 35 | auth_obj = Factory.create_obj( 36 | Factory.full_class_name_for_obj(auth_obj_def)) 37 | auth_obj.init(auth_dict) 38 | return auth_obj 39 | 40 | @staticmethod 41 | def get_auth_defs(): 42 | """ 43 | Get available ResourceAuth definitions. 44 | :return: dict keyed by NAME with object instance values 45 | """ 46 | auth_classes = Plugin.get_plugins( 47 | baseclass='GeoHealthCheck.resourceauth.ResourceAuth') 48 | result = {} 49 | for auth_class in auth_classes: 50 | auth_obj = Factory.create_obj(auth_class) 51 | result[auth_obj.NAME] = auth_obj 52 | 53 | return result 54 | 55 | def verify(self): 56 | return False 57 | 58 | def encode(self): 59 | """ 60 | Encode/encrypt auth dict structure. 61 | :return: encoded string 62 | """ 63 | if not self.verify(): 64 | return None 65 | 66 | try: 67 | s = json.dumps(self.auth_dict) 68 | return encode(APP.config['SECRET_KEY'], s) 69 | except Exception as err: 70 | LOGGER.error('Error encoding auth: %s' % str(err)) 71 | raise err 72 | 73 | @staticmethod 74 | def decode(encoded): 75 | """ 76 | Decode/decrypt encrypted string into auth dict. 77 | :return: encoded auth dict 78 | """ 79 | if encoded is None: 80 | return None 81 | 82 | try: 83 | s = decode(APP.config['SECRET_KEY'], encoded) 84 | return json.loads(s) 85 | except Exception as err: 86 | LOGGER.error('Error decoding auth: %s' % str(err)) 87 | raise err 88 | 89 | def add_auth_header(self, headers_dict): 90 | auth_header = self.get_auth_header() 91 | if auth_header: 92 | headers_dict.update(auth_header) 93 | return headers_dict 94 | 95 | def get_auth_header(self): 96 | """ 97 | Get encoded authorization header value from config data. 98 | Authorization scheme-specific. 99 | :return: None or dict with http auth header 100 | """ 101 | if not self.verify(): 102 | return None 103 | 104 | auth_val = self.encode_auth_header_val() 105 | if not auth_val: 106 | return None 107 | 108 | return {'Authorization': auth_val.replace('\n', '')} 109 | 110 | def encode_auth_header_val(self): 111 | return None 112 | -------------------------------------------------------------------------------- /GeoHealthCheck/result.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | 4 | class Result(object): 5 | """ 6 | Base class for results for Resource or Probe. 7 | """ 8 | 9 | def __init__(self, success=True, message='OK'): 10 | self.success = success 11 | self.message = message 12 | self.start_time = None 13 | self.end_time = None 14 | self.response_time_secs = -1 15 | self.response_time_str = -1 16 | self.results = [] 17 | self.results_failed = [] 18 | 19 | def add_result(self, result): 20 | self.results.append(result) 21 | if not result.success: 22 | self.success = False 23 | self.results_failed.append(result) 24 | # First failed result is usually main failure reason 25 | self.message = self.results_failed[0].message 26 | 27 | def get_report(self): 28 | return { 29 | 'success': self.success, 30 | 'message': self.message, 31 | 'response_time': self.response_time_str 32 | } 33 | 34 | def set(self, success, message): 35 | self.success = success 36 | self.message = message 37 | 38 | def start(self): 39 | self.start_time = datetime.datetime.utcnow() 40 | 41 | def stop(self): 42 | self.end_time = datetime.datetime.utcnow() 43 | 44 | delta = self.end_time - self.start_time 45 | self.response_time_secs = delta.seconds 46 | self.response_time_str = '%s.%s' % (delta.seconds, delta.microseconds) 47 | 48 | def __str__(self): 49 | if self.message: 50 | self.message = self.message 51 | return "success=%s msg=%s response_time=%s" % \ 52 | (self.success, self.message, self.response_time_str) 53 | 54 | 55 | class ResourceResult(Result): 56 | """ 57 | Holds result data from a single Resource: one Resource, N Probe(Results). 58 | Provides Run data. 59 | """ 60 | REPORT_VERSION = '1' 61 | 62 | def __init__(self, resource): 63 | Result.__init__(self) 64 | self.resource = resource 65 | 66 | def get_report(self): 67 | report = { 68 | 'report_version': ResourceResult.REPORT_VERSION, 69 | 'resource_id': self.resource.identifier, 70 | 'resource_type': self.resource.resource_type, 71 | 'resource_title': self.resource.title, 72 | 'url': self.resource.url, 73 | 'success': self.success, 74 | 'message': self.message, 75 | 'start_time': self.start_time.strftime( 76 | '%Y-%m-%dT%H:%M:%SZ'), 77 | 'end_time': self.end_time.strftime( 78 | '%Y-%m-%dT%H:%M:%SZ'), 79 | 'response_time': self.response_time_str, 80 | 'probes': [] 81 | } 82 | 83 | for probe_result in self.results: 84 | probe_report = probe_result.get_report() 85 | report['probes'].append(probe_report) 86 | 87 | return report 88 | 89 | 90 | class ProbeResult(Result): 91 | """ 92 | Holds result data from a single Probe: one Probe, N Checks. 93 | 94 | """ 95 | 96 | def __init__(self, probe, probe_vars): 97 | Result.__init__(self) 98 | self.probe = probe 99 | self.probe_vars = probe_vars 100 | 101 | def get_report(self): 102 | report = { 103 | 'probe_id': self.probe_vars.identifier, 104 | 'class': self.probe_vars.probe_class, 105 | 'name': getattr(self.probe, 'NAME', None), 106 | 'success': self.success, 107 | 'message': self.message, 108 | 'response_time': self.response_time_str, 109 | 'checks': [] 110 | } 111 | 112 | for check_result in self.results: 113 | check_report = check_result.get_report() 114 | report['checks'].append(check_report) 115 | 116 | return report 117 | 118 | 119 | class CheckResult(Result): 120 | """ 121 | Holds result data from a single Check. 122 | """ 123 | 124 | def __init__(self, check, check_vars, success=True, message="OK"): 125 | Result.__init__(self, success, message) 126 | self.check = check 127 | self.check_vars = check_vars 128 | self.parameters = check_vars.parameters 129 | 130 | def get_report(self): 131 | report = { 132 | 'check_id': self.check_vars.identifier, 133 | 'class': self.check_vars.check_class, 134 | 'name': getattr(self.check, 'NAME', None), 135 | 'success': self.success, 136 | 'message': self.message, 137 | 'response_time': self.response_time_str 138 | } 139 | 140 | return report 141 | 142 | 143 | # Util to quickly add Results and open new one. 144 | def push_result(obj, result, val, msg, new_result_name): 145 | result.set(val, msg) 146 | result.stop() 147 | obj.result.add_result(result) 148 | result = Result(True, new_result_name) 149 | result.start() 150 | return result 151 | -------------------------------------------------------------------------------- /GeoHealthCheck/static/lib/jqueryui/jquery-ui.css: -------------------------------------------------------------------------------- 1 | /*! jQuery UI - v1.12.1 - 2018-02-14 2 | * http://jqueryui.com 3 | * Includes: core.css, autocomplete.css, menu.css 4 | * Copyright jQuery Foundation and other contributors; Licensed MIT */ 5 | 6 | /* Layout helpers 7 | ----------------------------------*/ 8 | .ui-helper-hidden { 9 | display: none; 10 | } 11 | .ui-helper-hidden-accessible { 12 | border: 0; 13 | clip: rect(0 0 0 0); 14 | height: 1px; 15 | margin: -1px; 16 | overflow: hidden; 17 | padding: 0; 18 | position: absolute; 19 | width: 1px; 20 | } 21 | .ui-helper-reset { 22 | margin: 0; 23 | padding: 0; 24 | border: 0; 25 | outline: 0; 26 | line-height: 1.3; 27 | text-decoration: none; 28 | font-size: 100%; 29 | list-style: none; 30 | } 31 | .ui-helper-clearfix:before, 32 | .ui-helper-clearfix:after { 33 | content: ""; 34 | display: table; 35 | border-collapse: collapse; 36 | } 37 | .ui-helper-clearfix:after { 38 | clear: both; 39 | } 40 | .ui-helper-zfix { 41 | width: 100%; 42 | height: 100%; 43 | top: 0; 44 | left: 0; 45 | position: absolute; 46 | opacity: 0; 47 | filter:Alpha(Opacity=0); /* support: IE8 */ 48 | } 49 | 50 | .ui-front { 51 | z-index: 100; 52 | } 53 | 54 | 55 | /* Interaction Cues 56 | ----------------------------------*/ 57 | .ui-state-disabled { 58 | cursor: default !important; 59 | pointer-events: none; 60 | } 61 | 62 | 63 | /* Icons 64 | ----------------------------------*/ 65 | .ui-icon { 66 | display: inline-block; 67 | vertical-align: middle; 68 | margin-top: -.25em; 69 | position: relative; 70 | text-indent: -99999px; 71 | overflow: hidden; 72 | background-repeat: no-repeat; 73 | } 74 | 75 | .ui-widget-icon-block { 76 | left: 50%; 77 | margin-left: -8px; 78 | display: block; 79 | } 80 | 81 | /* Misc visuals 82 | ----------------------------------*/ 83 | 84 | /* Overlays */ 85 | .ui-widget-overlay { 86 | position: fixed; 87 | top: 0; 88 | left: 0; 89 | width: 100%; 90 | height: 100%; 91 | } 92 | .ui-autocomplete { 93 | position: absolute; 94 | top: 0; 95 | left: 0; 96 | cursor: default; 97 | } 98 | .ui-menu { 99 | list-style: none; 100 | padding: 0; 101 | margin: 0; 102 | display: block; 103 | outline: 0; 104 | } 105 | .ui-menu .ui-menu { 106 | position: absolute; 107 | } 108 | .ui-menu .ui-menu-item { 109 | margin: 0; 110 | cursor: pointer; 111 | /* support: IE10, see #8844 */ 112 | list-style-image: url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"); 113 | } 114 | .ui-menu .ui-menu-item-wrapper { 115 | position: relative; 116 | padding: 3px 1em 3px .4em; 117 | } 118 | .ui-menu .ui-menu-divider { 119 | margin: 5px 0; 120 | height: 0; 121 | font-size: 0; 122 | line-height: 0; 123 | border-width: 1px 0 0 0; 124 | } 125 | .ui-menu .ui-state-focus, 126 | .ui-menu .ui-state-active { 127 | margin: -1px; 128 | } 129 | 130 | /* icon support */ 131 | .ui-menu-icons { 132 | position: relative; 133 | } 134 | .ui-menu-icons .ui-menu-item-wrapper { 135 | padding-left: 2em; 136 | } 137 | 138 | /* left-aligned */ 139 | .ui-menu .ui-icon { 140 | position: absolute; 141 | top: 0; 142 | bottom: 0; 143 | left: .2em; 144 | margin: auto 0; 145 | } 146 | 147 | /* right-aligned */ 148 | .ui-menu .ui-menu-icon { 149 | left: auto; 150 | right: 0; 151 | } 152 | -------------------------------------------------------------------------------- /GeoHealthCheck/static/lib/jqueryui/jquery-ui.structure.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery UI CSS Framework 1.12.1 3 | * http://jqueryui.com 4 | * 5 | * Copyright jQuery Foundation and other contributors 6 | * Released under the MIT license. 7 | * http://jquery.org/license 8 | * 9 | * http://api.jqueryui.com/category/theming/ 10 | */ 11 | /* Layout helpers 12 | ----------------------------------*/ 13 | .ui-helper-hidden { 14 | display: none; 15 | } 16 | .ui-helper-hidden-accessible { 17 | border: 0; 18 | clip: rect(0 0 0 0); 19 | height: 1px; 20 | margin: -1px; 21 | overflow: hidden; 22 | padding: 0; 23 | position: absolute; 24 | width: 1px; 25 | } 26 | .ui-helper-reset { 27 | margin: 0; 28 | padding: 0; 29 | border: 0; 30 | outline: 0; 31 | line-height: 1.3; 32 | text-decoration: none; 33 | font-size: 100%; 34 | list-style: none; 35 | } 36 | .ui-helper-clearfix:before, 37 | .ui-helper-clearfix:after { 38 | content: ""; 39 | display: table; 40 | border-collapse: collapse; 41 | } 42 | .ui-helper-clearfix:after { 43 | clear: both; 44 | } 45 | .ui-helper-zfix { 46 | width: 100%; 47 | height: 100%; 48 | top: 0; 49 | left: 0; 50 | position: absolute; 51 | opacity: 0; 52 | filter:Alpha(Opacity=0); /* support: IE8 */ 53 | } 54 | 55 | .ui-front { 56 | z-index: 100; 57 | } 58 | 59 | 60 | /* Interaction Cues 61 | ----------------------------------*/ 62 | .ui-state-disabled { 63 | cursor: default !important; 64 | pointer-events: none; 65 | } 66 | 67 | 68 | /* Icons 69 | ----------------------------------*/ 70 | .ui-icon { 71 | display: inline-block; 72 | vertical-align: middle; 73 | margin-top: -.25em; 74 | position: relative; 75 | text-indent: -99999px; 76 | overflow: hidden; 77 | background-repeat: no-repeat; 78 | } 79 | 80 | .ui-widget-icon-block { 81 | left: 50%; 82 | margin-left: -8px; 83 | display: block; 84 | } 85 | 86 | /* Misc visuals 87 | ----------------------------------*/ 88 | 89 | /* Overlays */ 90 | .ui-widget-overlay { 91 | position: fixed; 92 | top: 0; 93 | left: 0; 94 | width: 100%; 95 | height: 100%; 96 | } 97 | .ui-autocomplete { 98 | position: absolute; 99 | top: 0; 100 | left: 0; 101 | cursor: default; 102 | } 103 | .ui-menu { 104 | list-style: none; 105 | padding: 0; 106 | margin: 0; 107 | display: block; 108 | outline: 0; 109 | } 110 | .ui-menu .ui-menu { 111 | position: absolute; 112 | } 113 | .ui-menu .ui-menu-item { 114 | margin: 0; 115 | cursor: pointer; 116 | /* support: IE10, see #8844 */ 117 | list-style-image: url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"); 118 | } 119 | .ui-menu .ui-menu-item-wrapper { 120 | position: relative; 121 | padding: 3px 1em 3px .4em; 122 | } 123 | .ui-menu .ui-menu-divider { 124 | margin: 5px 0; 125 | height: 0; 126 | font-size: 0; 127 | line-height: 0; 128 | border-width: 1px 0 0 0; 129 | } 130 | .ui-menu .ui-state-focus, 131 | .ui-menu .ui-state-active { 132 | margin: -1px; 133 | } 134 | 135 | /* icon support */ 136 | .ui-menu-icons { 137 | position: relative; 138 | } 139 | .ui-menu-icons .ui-menu-item-wrapper { 140 | padding-left: 2em; 141 | } 142 | 143 | /* left-aligned */ 144 | .ui-menu .ui-icon { 145 | position: absolute; 146 | top: 0; 147 | bottom: 0; 148 | left: .2em; 149 | margin: auto 0; 150 | } 151 | 152 | /* right-aligned */ 153 | .ui-menu .ui-menu-icon { 154 | left: auto; 155 | right: 0; 156 | } 157 | -------------------------------------------------------------------------------- /GeoHealthCheck/static/lib/jqueryui/jquery-ui.structure.min.css: -------------------------------------------------------------------------------- 1 | /*! jQuery UI - v1.12.1 - 2018-02-14 2 | * http://jqueryui.com 3 | * Copyright jQuery Foundation and other contributors; Licensed MIT */ 4 | 5 | .ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important;pointer-events:none}.ui-icon{display:inline-block;vertical-align:middle;margin-top:-.25em;position:relative;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-icon-block{left:50%;margin-left:-8px;display:block}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-autocomplete{position:absolute;top:0;left:0;cursor:default}.ui-menu{list-style:none;padding:0;margin:0;display:block;outline:0}.ui-menu .ui-menu{position:absolute}.ui-menu .ui-menu-item{margin:0;cursor:pointer;list-style-image:url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")}.ui-menu .ui-menu-item-wrapper{position:relative;padding:3px 1em 3px .4em}.ui-menu .ui-menu-divider{margin:5px 0;height:0;font-size:0;line-height:0;border-width:1px 0 0 0}.ui-menu .ui-state-focus,.ui-menu .ui-state-active{margin:-1px}.ui-menu-icons{position:relative}.ui-menu-icons .ui-menu-item-wrapper{padding-left:2em}.ui-menu .ui-icon{position:absolute;top:0;bottom:0;left:.2em;margin:auto 0}.ui-menu .ui-menu-icon{left:auto;right:0} -------------------------------------------------------------------------------- /GeoHealthCheck/static/lib/jspark/jspark.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Javascript Sparklines Library 3 | * Written By John Resig 4 | * http://ejohn.org/projects/jspark/ 5 | * 6 | * This work is tri-licensed under the MPL, GPL, and LGPL: 7 | * http://www.mozilla.org/MPL/ 8 | * 9 | * To use, place your data points within your HTML, like so: 10 | * 10,8,20,5... 11 | * 12 | * in your CSS you might want to have the rule: 13 | * .sparkline { display: none } 14 | * so that non-compatible browsers don't see a huge pile of numbers. 15 | * 16 | * Finally, include this library in your header, like so: 17 | * 18 | */ 19 | 20 | addEvent( window, "load", function() { 21 | var a = document.getElementsByTagName("*") || document.all; 22 | 23 | for ( var i = 0; i < a.length; i++ ) 24 | if ( has( a[i].className, "sparkline" ) ) 25 | sparkline( a[i] ); 26 | } ); 27 | 28 | function has(s,c) { 29 | var r = new RegExp("(^| )" + c + "\W*"); 30 | return ( r.test(s) ? true : false ); 31 | } 32 | 33 | function addEvent( obj, type, fn ) { 34 | if ( obj.attachEvent ) { 35 | obj['e'+type+fn] = fn; 36 | obj[type+fn] = function(){obj['e'+type+fn]( window.event );} 37 | obj.attachEvent( 'on'+type, obj[type+fn] ); 38 | } else 39 | obj.addEventListener( type, fn, false ); 40 | } 41 | 42 | function removeEvent( obj, type, fn ) { 43 | if ( obj.detachEvent ) { 44 | obj.detachEvent( 'on'+type, obj[type+fn] ); 45 | obj[type+fn] = null; 46 | } else 47 | obj.removeEventListener( type, fn, false ); 48 | } 49 | 50 | 51 | function sparkline(o) { 52 | var p = o.innerHTML.split(','); 53 | while ( o.childNodes.length > 0 ) 54 | o.removeChild( o.firstChild ); 55 | 56 | var nw = "auto"; 57 | var nh = "auto"; 58 | if ( window.getComputedStyle ) { 59 | nw = window.getComputedStyle( o, null ).width; 60 | nh = window.getComputedStyle( o, null ).height; 61 | } 62 | 63 | if ( nw != "auto" ) nw = nw.substr( 0, nw.length - 2 ); 64 | if ( nh != "auto" ) nh = nh.substr( 0, nh.length - 2 ); 65 | 66 | var f = 2; 67 | var w = ( nw == "auto" || nw == 0 ? p.length * f : nw - 0 ); 68 | var h = ( nh == "auto" || nh == 0 ? "1em" : nh ); 69 | 70 | var co = document.createElement("canvas"); 71 | 72 | if ( co.getContext ) o.style.display = 'inline'; 73 | else return false; 74 | 75 | co.style.height = h; 76 | co.style.width = w; 77 | co.width = w; 78 | o.appendChild( co ); 79 | 80 | var h = co.offsetHeight; 81 | co.height = h; 82 | 83 | var min = 9999; 84 | var max = -1; 85 | 86 | for ( var i = 0; i < p.length; i++ ) { 87 | p[i] = p[i] - 0; 88 | if ( p[i] < min ) min = p[i]; 89 | if ( p[i] > max ) max = p[i]; 90 | } 91 | 92 | if ( co.getContext ) { 93 | var c = co.getContext("2d"); 94 | c.strokeStyle = "red"; 95 | c.lineWidth = 1.0; 96 | c.beginPath(); 97 | 98 | for ( var i = 0; i < p.length; i++ ) { 99 | if ( i == 0 ) 100 | c.moveTo( (w / p.length) * i, h - (((p[i] - min) / (max - min)) * h) ); 101 | c.lineTo( (w / p.length) * i, h - (((p[i] - min) / (max - min)) * h) ); 102 | } 103 | 104 | c.stroke(); 105 | o.style.display = 'inline'; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /GeoHealthCheck/static/site/css/dashboard.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Base structure 3 | */ 4 | 5 | /* Move down content because we have a fixed navbar that is 50px tall */ 6 | body { 7 | padding-top: 50px; 8 | } 9 | 10 | 11 | /* 12 | * Global add-ons 13 | */ 14 | 15 | .sub-header { 16 | padding-bottom: 10px; 17 | border-bottom: 1px solid #eee; 18 | } 19 | 20 | /* 21 | * Top navigation 22 | * Hide default border to remove 1px line. 23 | */ 24 | .navbar-fixed-top { 25 | border: 0; 26 | } 27 | 28 | /* 29 | * Sidebar 30 | */ 31 | 32 | /* Hide for mobile, show later */ 33 | .sidebar { 34 | display: none; 35 | } 36 | @media (min-width: 768px) { 37 | .sidebar { 38 | position: fixed; 39 | top: 51px; 40 | bottom: 0; 41 | left: 0; 42 | z-index: 1000; 43 | display: block; 44 | padding: 20px; 45 | overflow-x: hidden; 46 | overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */ 47 | background-color: #f5f5f5; 48 | border-right: 1px solid #eee; 49 | } 50 | } 51 | 52 | /* Sidebar navigation */ 53 | .nav-sidebar { 54 | margin-right: -21px; /* 20px padding + 1px border */ 55 | margin-bottom: 20px; 56 | margin-left: -20px; 57 | } 58 | .nav-sidebar > li > a { 59 | padding-right: 20px; 60 | padding-left: 20px; 61 | } 62 | .nav-sidebar > .active > a, 63 | .nav-sidebar > .active > a:hover, 64 | .nav-sidebar > .active > a:focus { 65 | color: #fff; 66 | background-color: #428bca; 67 | } 68 | 69 | 70 | /* 71 | * Main content 72 | */ 73 | 74 | .main { 75 | padding: 20px; 76 | } 77 | @media (min-width: 768px) { 78 | .main { 79 | padding-right: 40px; 80 | padding-left: 40px; 81 | } 82 | } 83 | .main .page-header { 84 | margin-top: 0; 85 | } 86 | 87 | 88 | /* 89 | * Placeholder dashboard ideas 90 | */ 91 | 92 | .placeholders { 93 | margin-bottom: 30px; 94 | text-align: center; 95 | } 96 | .placeholders h4 { 97 | margin-bottom: 0; 98 | } 99 | .placeholder { 100 | margin-bottom: 20px; 101 | } 102 | .placeholder img { 103 | display: inline-block; 104 | border-radius: 50%; 105 | } 106 | 107 | footer { 108 | position: fixed; 109 | height: 20px; 110 | bottom: 0; 111 | width: 100%; 112 | background-color: black; 113 | color: #9d9d9d; 114 | } 115 | 116 | .totals-chart { 117 | height: 110px; 118 | } 119 | 120 | .resource-map { 121 | height: 200px; 122 | } 123 | 124 | #all-resources-map { 125 | height: 135px; 126 | } 127 | 128 | .run-chart { 129 | height: 250px; 130 | } 131 | 132 | .btn.nohover:hover { 133 | cursor:default !important; 134 | } 135 | 136 | .input-small { 137 | width: 20%; 138 | } 139 | 140 | .col-md-2.sidebar { 141 | width: 16.6667% !important; 142 | } 143 | 144 | input:read-only { 145 | background-color: #eeeeee !important; 146 | } 147 | 148 | .probe-entry { 149 | border: 1px solid #aaa; 150 | background-color: #eee !important; 151 | } 152 | 153 | table tbody th { 154 | width: 25%!important; 155 | } 156 | table tbody th + td { 157 | width: 85%!important; 158 | } 159 | 160 | table input[type=text], table select { 161 | width: 100%; 162 | } 163 | 164 | span.glyphicon.add { 165 | color: #3a3; 166 | } 167 | span.glyphicon.remove { 168 | color: #a33; 169 | } 170 | -------------------------------------------------------------------------------- /GeoHealthCheck/static/site/js/resources_list.js: -------------------------------------------------------------------------------- 1 | $('#resources-table').dataTable({ 2 | 'paging': false, 3 | 'sDom': ''//false // no search box 4 | }); 5 | // Morris.Donut({ 6 | // element: 'totals-chart', 7 | // colors: ['#5CB85C', '#D9534F'], 8 | // data: [ 9 | // {label: 'Working', value: {{ response['success']['number']|safe }} }, 10 | // {label: 'Broken', value: {{ response['fail']['number']|safe }} }, 11 | // ], 12 | // resize: true 13 | // }); 14 | 15 | 16 | // rudimentary table filter 17 | $('#filter').keyup(function () { 18 | //var selector = '.searchable tr'; 19 | var selector = 'td.facet-name'; 20 | var term = $(this).val(); 21 | var tokens = []; 22 | var td_text = null; 23 | var facet = null; 24 | var num_results = 0; 25 | 26 | if (term.match('^site:|title:|type:|url:')) { 27 | if (term.match('^title:')) { 28 | selector = 'td.facet-name'; 29 | } 30 | else if (term.match('^type:')) { 31 | selector = 'td.facet-type'; 32 | } 33 | else if (term.match('^url:|site:')) { 34 | selector = 'a.facet-url'; 35 | } 36 | tokens = term.split(':'); 37 | facet = tokens[0]; 38 | term = tokens[1]; 39 | } 40 | 41 | var rex = new RegExp(term, 'i'); 42 | $('.searchable tr').hide(); // hide all rows 43 | 44 | $(selector).each(function() { 45 | if (facet === 'url') { 46 | td_text = $(this).attr('title'); 47 | } 48 | if (facet === 'site') { 49 | td_text = $(this).attr('title').split('/')[2]; 50 | } 51 | else { 52 | td_text = $(this).text(); 53 | } 54 | if (rex.test(td_text)) { 55 | $(this).closest('tr').show(); 56 | num_results += 1; 57 | } 58 | }); 59 | $('#resources-table-num-results').html(num_results + ' result' + (num_results === 1 ? '' : 's')); 60 | }); 61 | $('select').select2({disabled: true, tags: true}); 62 | -------------------------------------------------------------------------------- /GeoHealthCheck/static/site/js/runs_chart.js: -------------------------------------------------------------------------------- 1 | // Setup Chart 2 | 3 | function prepData(rawData, hoverTemplate) { 4 | var xField = 'datetime'; 5 | var yField = 'value'; 6 | var idField = 'id'; 7 | var successField = 'success'; 8 | var x = []; 9 | var y = []; 10 | var ids = []; 11 | var markerColors = []; 12 | rawData.forEach(function (datum, i) { 13 | 14 | x.push(new Date(datum[xField])); 15 | y.push(datum[yField]); 16 | ids.push(datum[idField]); 17 | if (datum[successField] === 1) { 18 | markerColors.push('#5CB85C'); 19 | } else { 20 | markerColors.push('#D9534F'); 21 | } 22 | }); 23 | 24 | return [{ 25 | name: '', 26 | type: 'scatter', 27 | mode: 'lines+markers', 28 | hovertemplate: hoverTemplate, 29 | hoverlabel: { 30 | bgcolor: '#EEEEEE' 31 | }, 32 | line: { 33 | color: '#0000CC', 34 | width: 1 35 | }, 36 | 37 | marker: { 38 | color: markerColors, 39 | size: 8, 40 | line: { 41 | color: '#111111', 42 | width: 1 43 | } 44 | }, 45 | x: x, 46 | y: y, 47 | ids: ids 48 | }]; 49 | } 50 | 51 | function showRunDetails(runURL) { 52 | $.ajax({ 53 | type: "GET", 54 | url: runURL, 55 | contentType: "application/json; charset=utf-8", 56 | dataType: "json", 57 | success: function (data) { 58 | // Format JSON: http://jsfiddle.net/K83cK 59 | var runData = data.runs[0]; 60 | $('#run-chart-hover-date').text(runData.checked_datetime); 61 | $('#run-chart-hover-resptime').text(runData.response_time.toFixed(2) + ' s'); 62 | $('#run-chart-hover-msg').text(runData.message); 63 | $('#run-open').removeClass('disabled').attr("href", runURL + '.html'); 64 | }, 65 | error: function (errMsg) { 66 | $('#run-chart-hover').text("Error: " + errMsg); 67 | } 68 | }); 69 | } 70 | 71 | function drawChart(elementId, runData, resourceURL, hoverTemplate) { 72 | var runChart = document.getElementById(elementId); 73 | if (!runChart) { 74 | return; 75 | } 76 | 77 | var selectorOptions = { 78 | buttons: [ 79 | { 80 | step: 'hour', 81 | stepmode: 'backward', 82 | count: 1, 83 | label: '1h' 84 | }, 85 | { 86 | step: 'hour', 87 | stepmode: 'backward', 88 | count: 6, 89 | label: '6h' 90 | }, 91 | { 92 | step: 'hour', 93 | stepmode: 'backward', 94 | count: 24, 95 | label: '24h' 96 | }, 97 | { 98 | step: 'week', 99 | stepmode: 'backward', 100 | count: 1, 101 | label: '1w' 102 | }, 103 | { 104 | step: 'month', 105 | stepmode: 'backward', 106 | count: 1, 107 | label: '1m' 108 | }, 109 | { 110 | step: 'all', 111 | }], 112 | }; 113 | 114 | var data = prepData(runData, hoverTemplate); 115 | 116 | var layout = { 117 | title: 'Probe Runs', 118 | hovermode: 'closest', 119 | paper_bgcolor: '#EEEEEE', 120 | xaxis: { 121 | type: 'date', 122 | rangeselector: selectorOptions, 123 | rangeslider: { 124 | bgcolor: '#DDDDDD' 125 | }, 126 | title: { 127 | text: 'Date' 128 | } 129 | }, 130 | yaxis: { 131 | type: 'linear', 132 | fixedrange: true, 133 | title: { 134 | text: 'Duration (secs)' 135 | } 136 | } 137 | }; 138 | 139 | var options = { 140 | scrollZoom: true, // lets us scroll to zoom in and out - works 141 | showLink: false, // removes the link to edit on plotly - works 142 | // Names: https://github.com/plotly/plotly.js/blob/master/src/components/modebar/buttons.js 143 | modeBarButtonsToRemove: ['lasso2d', 'zoom2d', 'pan', 'pan2d', 'autoScale2d', 'sendDataToCloud', 'hoverCompareCartesian', 'hoverClosestCartesian', 'toggleSpikelines', 'select2d'], 144 | // modeBarButtonsToAdd: ['lasso2d'], 145 | displayLogo: false, // this one also seems to not work 146 | displayModeBar: true, //this one does work 147 | }; 148 | 149 | function waitForPlotly() { 150 | if (window.Plotly) { 151 | Plotly.plot(runChart, data, layout, options); 152 | 153 | runChart.on('plotly_hover', function (data) { 154 | showRunDetails(resourceURL + '/' + data.points[0].id); 155 | }); 156 | 157 | runChart.on('plotly_click', function (data) { 158 | showRunDetails(resourceURL + '/' + data.points[0].id); 159 | }); 160 | } 161 | else { 162 | if (console) { 163 | console.log('Wait for Plotly...'); 164 | } 165 | window.setTimeout("waitForPlotly();", 100); 166 | } 167 | } 168 | 169 | // Start drawing when Plotly completely ready 170 | waitForPlotly(); 171 | } -------------------------------------------------------------------------------- /GeoHealthCheck/templates/add.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |
4 |
5 |

{{ _('Add Resource') }}

6 |
7 | 18 |
19 | 20 |
21 | 22 |

{{ _('Enter the URL without any query parameters') }}:
{{ _('good') }}: http://host/wms
{{ _('bad') }}:http://host/wms?service=WMS&version=1.1.1&request=GetCapabilities

23 |
24 |
25 | 26 |
27 |
28 | 29 |
30 |
31 |
32 | {% endblock %} 33 | 34 | {% block extrafoot %} 35 | 43 | {% endblock %} 44 | -------------------------------------------------------------------------------- /GeoHealthCheck/templates/home.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block extrahead %} 4 | 5 | 6 | {% endblock %} 7 | 8 | {% block body %} 9 |
10 | {% include 'includes/overall_status.html' %} 11 | {% endblock %} 12 | 13 | {% block extrabody %} 14 |

15 | {{ _('Select a link on the left to view Resources.') }} 16 |

17 |

{{ _('Failing Resources') }}

18 | {% set resources = response['failed_resources'] %} 19 |
20 | {% if resources|length > 0 %} 21 | {% include 'includes/resources_list.html' %} 22 | {% else %} 23 | {{ _('None') }}! 24 | {% endif %} 25 |
26 | 27 |
28 | {% endblock %} 29 | 30 | {% block extrafoot %} 31 | 32 | 33 | 34 | {% endblock %} 35 | -------------------------------------------------------------------------------- /GeoHealthCheck/templates/includes/check_edit_form.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 81 | 82 |
4 | 5 | 6 | 10 | 15 | 16 | {% if check.parameters %} 17 | 18 | 76 | 77 | 78 | {% endif %} 79 |
7 | Check: {{ check_info.NAME }} 8 |
Check: {{ check_info.DESCRIPTION }} 9 |
11 | 14 |
19 | 20 | 21 | 73 | 74 |
22 | Check Parameters 23 | 24 | {% for param in check.parameters %} 25 | 26 | 27 | 69 | 70 | {% endfor %} 71 |
{{ param }} 28 | {% set check_param_val = check.parameters[param] %} 29 | {% set check_param_type = check_info.PARAM_DEFS[param]['type'] %} 30 | {% set check_param_range = check_info.PARAM_DEFS[param]['range'] %} 31 | {% set check_param_default = check_info.PARAM_DEFS[param]['default'] %} 32 | 33 | {% if check_param_type == 'bbox' or check_param_type == 'stringlist' %} 34 | {% set check_param_type = 'list' %} 35 | {% set check_param_val = check_param_val|join(',') %} 36 | {% endif %} 37 | 38 | {% if check_param_range %} 39 | 57 | 58 | {% else %} 59 | 60 | 67 | {% endif %} 68 |
72 |
75 |
 
80 |
83 | -------------------------------------------------------------------------------- /GeoHealthCheck/templates/includes/check_info.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 56 | 57 |
4 | Check: {{ check_info.NAME }} - by {{ check_info.AUTHOR }} 5 |
{{ check_info.DESCRIPTION }} 6 | 7 | 8 | 11 | 12 | 13 | 53 | 54 |
9 | Check-class: {{ check_class }} 10 |
14 | Check-Parameters 15 | {% if not check_info.PARAM_DEFS %} 16 |
This Check has no parameters. 17 | {% else %} 18 | 19 | {% for param, param_def in check_info.PARAM_DEFS.items() %} 20 | 21 | 22 | 48 | 49 | {% endfor %} 50 |
{{ param }} 23 | {{ param_def.description }} 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 |
type{{ param_def.type }}
required{{ param_def.required }}
range{{ param_def.range }}
default{{ param_def.default }}
(fixed) value{{ param_def.value }}
46 | 47 |
51 | {% endif %} 52 |
55 |
58 | -------------------------------------------------------------------------------- /GeoHealthCheck/templates/includes/overall_status.html: -------------------------------------------------------------------------------- 1 |

{{ _('Dashboard') }}

2 | {% if response['first_run'].checked_datetime and response['last_run'].checked_datetime %} 3 | 4 | {% endif %} 5 |
6 |
7 |
8 |
9 |
10 |
11 | 12 |
13 |
14 |
{{ response['success']['percentage'] }}%
15 |
16 |
17 |
18 | 19 | 23 | 24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | 32 |
33 |
34 |
{{ response['reliability'] }}%
35 |
36 |
37 |
38 | 39 | 43 | 44 |
45 |
46 | {% if response['fail']['number'] > 0 %} 47 |
48 |
49 |
50 |
51 |
52 | 53 |
54 |
55 |
{{ response['fail']['percentage'] }}%
56 |
57 |
58 |
59 | 60 | 64 | 65 |
66 |
67 | {% endif %} 68 |
69 |
70 | -------------------------------------------------------------------------------- /GeoHealthCheck/templates/includes/probe_info.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 90 | 98 | 99 |
4 | {{ probe_avail.NAME }} - by {{ probe_avail.AUTHOR }} 5 |
{{ probe_avail.DESCRIPTION }} 6 | 88 | 89 |
91 | 94 | 97 |
100 | -------------------------------------------------------------------------------- /GeoHealthCheck/templates/includes/resources_list.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | {% for resource in resources %} 12 | 13 | 14 | 27 | {% if resource.last_run %} 28 | 36 | {% endif %} 37 | 42 | {% else %} 43 | 44 | 45 | {% endif %} 46 | 47 | {% endfor %} 48 | 49 |
{{ _('Type') }}{{ _('Name') }}{{ _('Status') }}{{ _('Reliability') }}
{{ resource_types[resource.resource_type]['label'] }} 15 | {{ resource.title }} 16 |
17 | {% if resource.tags %} 18 | 25 | {% endif %} 26 |
29 | 30 | {% if resource.last_run.success %} 31 | 32 | {% else %} 33 | 34 | 35 | 38 | 39 | {{ resource.reliability|round2 }}% 40 | 41 | No runs yet 
50 | -------------------------------------------------------------------------------- /GeoHealthCheck/templates/includes/runs.html: -------------------------------------------------------------------------------- 1 | {% for run in runs %} 2 |

Run id={{ run.id }}

3 | 4 | 5 | 6 | 7 | 8 |
Time: {{ run.checked_datetime }}Success: {{ run.success }}Total Response Time: {{ run.response_time }}
9 | 10 |

Probe Results

11 | {% for probe in run.report.probes %} 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {% if probe.checks|length < 1 %} 23 | 24 | 25 | 26 | {% endif %} 27 | {% if probe.checks|length > 0 %} 28 | 29 | 44 | 45 | {% endif %} 46 |
Probe{{ probe.name }}
Success{{ probe.success }}
Response Time{{ probe.response_time }}
Message{{ probe.message|e }}
Checks 30 | {% for check in probe.checks %} 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
Check{{ check.name }}
Success{{ check.success }}
Message{{ check.message|e }}
42 | {% endfor %} 43 |
47 | 48 | {% endfor %} 49 |
50 | {% endfor %} 51 | -------------------------------------------------------------------------------- /GeoHealthCheck/templates/login.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |
4 |
5 |

{{ _('Login') }}

6 |
7 | 8 | 9 |
10 |
11 | 12 |
13 | {% if not config['GHC_REQUIRE_WEBAPP_AUTH'] %} 14 | 17 | {% endif %} 18 | 21 | {% if config['GHC_REQUIRE_WEBAPP_AUTH'] %} 22 |
23 | {{ _('This app requires an authenticated user') }} 24 |
25 | {% endif %} 26 |
27 |
28 | {% endblock %} 29 | -------------------------------------------------------------------------------- /GeoHealthCheck/templates/notification_email.txt: -------------------------------------------------------------------------------- 1 | {{ result }}: {{ resource.title }} 2 | 3 | 4 | {{ _('Hi: this is an automated message from the') }} {{ config.GHC_SITE_TITLE }} {{ _('service') }}. 5 | 6 | {{ _('Resource') }}: {{ resource.title }} 7 | {{ _('Resource type') }}: {{ resource.resource_type }} 8 | {{ _('Resource URL') }}: {{ resource.url }} 9 | 10 | {{ _('Details') }}: 11 | 12 | {{ _('Date') }}: {{ run.checked_datetime.strftime('%Y-%m-%dT%H:%M:%SZ') }} 13 | 14 | {{ _('Message') }}: {{ run.message }} 15 | 16 | {{ _('Details') }}: {{ config.GHC_SITE_URL }}/resource/{{resource.identifier}} 17 | 18 | {{ config.GHC_SITE_TITLE }} 19 | {{ config.GHC_SITE_URL }} 20 | -------------------------------------------------------------------------------- /GeoHealthCheck/templates/opensearch_description.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ config['GHC_SITE_TITLE'] }} 4 | {{ config['GHC_SITE_TITLE'] }} 5 | {{ config['GHC_SITE_TITLE'] }} 6 | {{ config['GHC_SITE_TITLE'] }} 7 | UTF-8 8 | 9 | 10 | 11 | {{ _('Powered by') }} GeoHealthCheck http://geopython.github.io/GeoHealthCheck 12 | 13 | -------------------------------------------------------------------------------- /GeoHealthCheck/templates/register.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |
4 | {% if config['GHC_SELF_REGISTER'] %} 5 |
6 |

{{ _('Register') }}

7 |
8 | 9 | 10 | 11 |
12 |
13 | 14 |
15 |
16 | {% else %} 17 |

{{ errmsg }}

18 | {% endif %} 19 |
20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /GeoHealthCheck/templates/reset_password_email.txt: -------------------------------------------------------------------------------- 1 | {{ _('Hi: this is an automated message from the') }} {{ config.GHC_SITE_TITLE }} {{ _('service') }}. 2 | 3 | {{ _('You have requested to reset your password') }}. 4 | {{ _('Follow the link below to create a new password') }}. 5 | {{ _('Ignore this email if this was not initiated by you') }}. 6 | 7 | {{ reset_url }} 8 | 9 | {{ _('Username') }}: {{ username }} 10 | 11 | {{ _('Greetings from') }} 12 | 13 | {{ config.GHC_SITE_TITLE }} 14 | -------------------------------------------------------------------------------- /GeoHealthCheck/templates/reset_password_form.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |
4 |
5 |

{{ _('Enter new password') }}

6 |
7 | 8 |
9 |
10 | 11 |
12 |
13 |
14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /GeoHealthCheck/templates/reset_password_request.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |
4 |
5 |

{{ _('Reset Password') }}

6 |
7 | 8 |
9 |
10 | 11 |
12 |
13 |
14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /GeoHealthCheck/templates/resources.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block extrahead %} 4 | 5 | 6 | {% endblock %} 7 | 8 | {% block body %} 9 |
10 | {% include 'includes/overall_status.html' %} 11 | {% endblock %} 12 | 13 | {% block extrabody %} 14 | 15 |

{{ _('Resources') }} JSON CSV

16 | 17 |
18 | (foo, site:.org, title:foo, type:wms, url:example.org) 19 |
20 | {{ response['total'] }} {{ _('results') }} 21 | 22 |
23 | {% set resources = response['resources'] %} 24 | {% include 'includes/resources_list.html' %} 25 |
26 |
27 | {% endblock %} 28 | 29 | {% block extrafoot %} 30 | 31 | 32 | 48 | {% endblock %} 49 | -------------------------------------------------------------------------------- /GeoHealthCheck/templates/runs.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block extrahead %} 4 | {% endblock %} 5 | 6 | {% block body %} 7 |
8 | {% include 'includes/runs.html' %} 9 |
10 | {% endblock %} 11 | 12 | {% block extrafoot %} 13 | 14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /GeoHealthCheck/templates/status_report_email.txt: -------------------------------------------------------------------------------- 1 | {{ _('Hi: this is a status report sent by the') }} "{{ config.GHC_SITE_TITLE }}" {{ _('service') }} 2 | {{ _('at') }} {{ config.GHC_SITE_URL }} {{ _('for the GeoHealthCheck endpoint at') }} {{ summary.site_url }} 3 | 4 | == {{ _('Monitoring Period') }} == 5 | {{ _('From') }}: {{ summary.first_run.checked_datetime }} 6 | {{ _('To') }}: {{ summary.last_run.checked_datetime }} 7 | 8 | == {{ _('Status') }} == 9 | {{ _('Resources') }}: {{ summary.total }} 10 | {{ _('Operational') }}: {{ summary.success.number }} 11 | {{ _('Failing') }}: {{ summary.fail.number }} 12 | {{ _('Reliability') }}: {{ summary.reliability }}% 13 | 14 | == {{ _('Failing') }} {{ _('Resources') }} == 15 | {% for resource in summary.failed_resources %} 16 | [{{resource.identifier}}] - "{{resource.title}}" 17 | {{ _('Reliability') }}: {{ resource.reliability }}% 18 | {{ _('URL') }}: {{ resource.url }} 19 | {{ _('Owner') }}: {{ resource.owner }} 20 | {{ _('Details') }}: {{ summary.site_url }}/resource/{{resource.identifier}} 21 | {% endfor %} 22 | {% if summary.failed_resources|length == 0 %}{{ _('None') }}!{% endif %} 23 | == {{ _('Links') }} == 24 | {{ _('Home') }}: {{ summary.site_url }} 25 | {{ _('Summary') }} JSON: {{ summary.site_url }}/api/v1.0/summary 26 | {{ _('Summary') }} TXT: {{ summary.site_url }}/api/v1.0/summary.txt 27 | {{ _('Status') }} CSV: {{ summary.site_url }}/csv 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2015 Tom Kralidis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CI Build](https://github.com/geopython/GeoHealthCheck/actions/workflows/main.yml/badge.svg)](https://github.com/geopython/GeoHealthCheck/actions/workflows/main.yml) 2 | [![Docker Build](https://github.com/geopython/GeoHealthCheck/actions/workflows/docker.yml/badge.svg)](https://github.com/geopython/GeoHealthCheck/actions/workflows/docker.yml) 3 | [![Full Documentation](https://img.shields.io/badge/ReadTheDocs-online-green.svg)](https://docs.geohealthcheck.org) 4 | [![Gitter](https://img.shields.io/gitter/room/geopython/GeoHealthCheck.svg?style=flat-square)](https://gitter.im/geopython/GeoHealthCheck) 5 | 6 | GeoHealthCheck 7 | ============== 8 | 9 | GeoHealthCheck (GHC) is a Service Status and QoS Checker for OGC Web Services and web APIs in general. 10 | See also the [full GHC documentation](http://docs.geohealthcheck.org/). 11 | 12 | Easiest is [to run GHC using Docker](https://github.com/geopython/GeoHealthCheck/blob/master/docker/README.md). 13 | Below a quick overview of a manual install on Unix-based systems like Apple MacOS and Linux. 14 | 15 | ```bash 16 | virtualenv GeoHealthCheck && cd $_ 17 | . bin/activate 18 | git clone https://github.com/geopython/GeoHealthCheck.git 19 | cd GeoHealthCheck 20 | pip install Paver 21 | # setup installation 22 | paver setup 23 | # generate secret key 24 | paver create_secret_key 25 | # setup local configuration (overrides GeoHealthCheck/config_main.py) 26 | vi instance/config_site.py 27 | # edit at least secret key: 28 | # - SECRET_KEY # copy/paste result string from paver create_secret_key 29 | 30 | # Optional: edit other settings or leave defaults 31 | # - SQLALCHEMY_DATABASE_URI 32 | # - GHC_RETENTION_DAYS 33 | # - GHC_SELF_REGISTER 34 | # - GHC_RUNNER_IN_WEBAPP 35 | # - GHC_ADMIN_EMAIL 36 | # - GHC_SITE_TITLE 37 | # - GHC_MAP (or use default settings) 38 | 39 | # setup database and superuser account interactively 40 | paver create 41 | 42 | # start webserver with healthcheck runner daemon inside 43 | # (default is 0.0.0.0:8000) 44 | python GeoHealthCheck/app.py 45 | # or start webserver on another port 46 | python GeoHealthCheck/app.py 0.0.0.0:8881 47 | # or start webserver on another IP 48 | python GeoHealthCheck/app.py 192.168.0.105:8001 49 | 50 | # OR start webserver and separate runner daemon (scheduler) process 51 | vi instance/config_site.py 52 | # GHC_RUNNER_IN_WEBAPP = False 53 | python GeoHealthCheck/scheduler.py & 54 | python GeoHealthCheck/app.py 55 | 56 | # next: use a real webserver or preferably Docker for production 57 | 58 | # other commands 59 | # 60 | # drop database 61 | python GeoHealthCheck/models.py drop 62 | 63 | # load data in database (WARN: deletes existing data!) 64 | # See example data .json files in tests/data 65 | python GeoHealthCheck/models.py load <.json data file> [y/n] 66 | 67 | ``` 68 | 69 | More in the [full GHC documentation](http://docs.geohealthcheck.org/). 70 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # GeoHealthCheck Security Policy 2 | 3 | ## Reporting 4 | 5 | Security/vulnerability reports **should not** be submitted through GitHub issues or public discussions, but instead please send your report 6 | to **geopython-security nospam @ lists.osgeo.org** - (remove the blanks and 'nospam'). 7 | 8 | Please follow the [contributor guidelines](https://github.com/geopython/GeoHealthCheck/blob/master/CONTRIBUTING.md#submitting-bugs) when submitting a vulnerability report. 9 | 10 | ## Supported Versions 11 | 12 | The GeoHealthCheck development team will release patches for security vulnerabilities for the following versions: 13 | 14 | | Version | Supported | 15 | | ------- | ------------------ | 16 | | 0.8.x | :white_check_mark: | 17 | | 0.7.x | :white_check_mark: | 18 | | < 7.x | :x: | 19 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.9.0 2 | -------------------------------------------------------------------------------- /babel.cfg: -------------------------------------------------------------------------------- 1 | [python: **.py] 2 | [jinja2: **/templates/**] 3 | extensions=jinja2.ext.autoescape,jinja2.ext.with_ 4 | -------------------------------------------------------------------------------- /docker/compose/README.md: -------------------------------------------------------------------------------- 1 | # GHC with Docker Compose 2 | 3 | [Docker](https://www.docker.com/) is the fastest/recommended way to get GHC up and running. 4 | [Docker Compose](https://docs.docker.com/compose/) is *a tool for defining and running multi-container Docker applications.* 5 | 6 | Within this directory are *examples* for running GHC using Docker compose. 7 | You should copy and adapt these for your own deployment. 8 | -------------------------------------------------------------------------------- /docker/compose/docker-compose.postgis.yml: -------------------------------------------------------------------------------- 1 | # GHC Docker setup with Postgres as backend DB. 2 | # 3 | # To run: 4 | # sudo docker-compose -f docker-compose.postgis.yml up [-d] 5 | # 6 | version: "3" 7 | 8 | services: 9 | ghc_web: 10 | image: geopython/geohealthcheck:latest 11 | 12 | container_name: ghc_web 13 | 14 | restart: unless-stopped 15 | 16 | env_file: 17 | - ghc.env 18 | - ghc-postgis.env 19 | 20 | links: 21 | - postgis_ghc 22 | 23 | depends_on: 24 | - postgis_ghc 25 | 26 | ports: 27 | - 8083:80 28 | 29 | # volumes: 30 | # Optional Plugins, using Path on the host, relative to this Compose file 31 | # To activate: 2 steps for runner and GHC webapp: 32 | # - configure in ghc.env 33 | # - mount these as docker volume on host 34 | # See https://docs.docker.com/compose/compose-file/#volumes 35 | # - ./../GeoHealthCheck/plugins:/plugins:ro 36 | 37 | ghc_runner: 38 | image: geopython/geohealthcheck:latest 39 | 40 | container_name: ghc_runner 41 | 42 | restart: unless-stopped 43 | 44 | env_file: 45 | - ghc.env 46 | - ghc-postgis.env 47 | 48 | links: 49 | - postgis_ghc 50 | 51 | depends_on: 52 | - postgis_ghc 53 | 54 | entrypoint: 55 | - /run-runner.sh 56 | 57 | # volumes: 58 | # Optional Plugins, using Path on the host, relative to this Compose file 59 | # To activate 2 steps: 60 | # - configure in ghc.env 61 | # - mount these as docker volume on host 62 | # See https://docs.docker.com/compose/compose-file/#volumes 63 | # - ./../GeoHealthCheck/plugins:/plugins:ro 64 | 65 | postgis_ghc: 66 | image: mdillon/postgis:10-alpine 67 | 68 | container_name: postgis_ghc 69 | 70 | restart: unless-stopped 71 | 72 | env_file: 73 | - ghc-postgis.env 74 | 75 | volumes: 76 | - ghc_pgdb:/var/lib/postgresql/data 77 | 78 | # If you ever need to expose PG ports on host externally 79 | # ports: 80 | # - 5432:5432 81 | 82 | expose: 83 | - 5432 84 | 85 | # docker-compose v2+ needs separate volumes section 86 | volumes: 87 | ghc_pgdb: 88 | -------------------------------------------------------------------------------- /docker/compose/docker-compose.yml: -------------------------------------------------------------------------------- 1 | # Default Docker Compose config: 2 | # runs GHC Web App (ghc_web) and GHC Daemon Runner (ghc_runner) Docker containers 3 | # with SQLite backend. ghc_runner will run using the schedule 4 | # configured in the GHC Web App. 5 | # 6 | # Plugins (optional) need to be configured for both Docker containers. 7 | version: "3" 8 | 9 | services: 10 | ghc_web: 11 | image: geopython/geohealthcheck:latest 12 | 13 | container_name: ghc_web 14 | 15 | restart: unless-stopped 16 | 17 | ports: 18 | - 8083:80 19 | 20 | env_file: 21 | - ghc.env 22 | 23 | volumes: 24 | - ghc_sqlitedb:/GeoHealthCheck/DB 25 | # Optional Plugins, using Path on the host, relative to this Compose file 26 | # To activate: 2 steps for runner and GHC webapp: 27 | # - configure in ghc.env 28 | # - mount these as docker volume on host 29 | # See https://docs.docker.com/compose/compose-file/#volumes 30 | # - ./../GeoHealthCheck/plugins:/plugins:ro 31 | 32 | ghc_runner: 33 | image: geopython/geohealthcheck:latest 34 | 35 | container_name: ghc_runner 36 | 37 | restart: unless-stopped 38 | 39 | env_file: 40 | - ghc.env 41 | 42 | entrypoint: 43 | - /run-runner.sh 44 | 45 | volumes: 46 | - ghc_sqlitedb:/GeoHealthCheck/DB 47 | # Optional Plugins, using Path on the host, relative to this Compose file 48 | # To activate 2 steps: 49 | # - configure in ghc.env 50 | # - mount these as docker volume on host 51 | # See https://docs.docker.com/compose/compose-file/#volumes 52 | # - ./../GeoHealthCheck/plugins:/plugins:ro 53 | 54 | # docker-compose v2+ needs separate volumes section 55 | volumes: 56 | ghc_sqlitedb: 57 | -------------------------------------------------------------------------------- /docker/compose/ghc-postgis.env: -------------------------------------------------------------------------------- 1 | # We mainly need to override the default (sqlite) DB URI 2 | SQLALCHEMY_DATABASE_URI=postgresql://ghc:ghc@postgis_ghc:5432/ghc 3 | 4 | # Postgres Docker container settings 5 | POSTGRES_USER=ghc 6 | POSTGRES_PASSWORD=ghc 7 | POSTGRES_DB=ghc 8 | -------------------------------------------------------------------------------- /docker/compose/ghc.env: -------------------------------------------------------------------------------- 1 | SQLALCHEMY_DATABASE_URI=sqlite:////GeoHealthCheck/DB/data.db 2 | 3 | # Core variables settings, change at will. 4 | GHC_RUNNER_IN_WEBAPP=False 5 | GHC_NOTIFICATIONS=True 6 | GHC_NOTIFICATIONS_VERBOSITY=True 7 | GHC_ADMIN_EMAIL=us@gmail.com 8 | GHC_NOTIFICATIONS_EMAIL=us@gmail.com,them@domain.com 9 | GHC_SMTP_SERVER=smtp.gmail.com 10 | GHC_SMTP_PORT=587 11 | GHC_SMTP_TLS=True 12 | GHC_SMTP_SSL=False 13 | GHC_SMTP_USERNAME=us@gmail.com 14 | GHC_SMTP_PASSWORD=the_passw 15 | GHC_LOG_LEVEL=20 16 | 17 | # GHC_USER_PLUGINS=GeoHealthCheck.plugins.user.mywmsprobe,GeoHealthCheck.plugins.user.mywmsprobe2 18 | 19 | # Optionally set container Timezone 20 | # CONTAINER_TIMEZONE=Europe/Amsterdam 21 | 22 | # Optionally: set language 23 | # LC_ALL=nl_NL.UTF-8 24 | # LANG=nl_NL.UTF-8 25 | # LANGUAGE=nl_NL.UTF-8 26 | -------------------------------------------------------------------------------- /docker/config_site.py: -------------------------------------------------------------------------------- 1 | # ================================================================= 2 | # 3 | # Authors: Tom Kralidis 4 | # 5 | # Copyright (c) 2014 Tom Kralidis 6 | # 7 | # Permission is hereby granted, free of charge, to any person 8 | # obtaining a copy of this software and associated documentation 9 | # files (the "Software"), to deal in the Software without 10 | # restriction, including without limitation the rights to use, 11 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the 13 | # Software is furnished to do so, subject to the following 14 | # conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be 17 | # included in all copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 21 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 23 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 24 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 26 | # OTHER DEALINGS IN THE SOFTWARE. 27 | # 28 | # ================================================================= 29 | import os 30 | 31 | 32 | def str2bool(v): 33 | return v.lower() in ("yes", "true", "t", "1") 34 | 35 | 36 | def str2None(v): 37 | return None if v == 'None' else v 38 | 39 | 40 | DEBUG = False 41 | SQLALCHEMY_ECHO = False 42 | 43 | # Use Env vars via os.environ() from Dockerfile: 44 | # makes it easier to override via Docker "environment" 45 | # settings on running GHC Containers. 46 | SQLALCHEMY_DATABASE_URI = os.environ['SQLALCHEMY_DATABASE_URI'] 47 | # When True enables 'pre_ping' (optionally reconnect to DB) for the SQLALCHEMY create_engine options 48 | SQLALCHEMY_ENGINE_OPTION_PRE_PING = os.environ['SQLALCHEMY_ENGINE_OPTION_PRE_PING'] 49 | # Replace None with 'your secret key string' in quotes 50 | SECRET_KEY = os.environ['SECRET_KEY'] 51 | 52 | GHC_RETENTION_DAYS = int(os.environ['GHC_RETENTION_DAYS']) 53 | GHC_PROBE_HTTP_TIMEOUT_SECS = int(os.environ['GHC_PROBE_HTTP_TIMEOUT_SECS']) 54 | GHC_MINIMAL_RUN_FREQUENCY_MINS = int(os.environ['GHC_MINIMAL_RUN_FREQUENCY_MINS']) 55 | GHC_SELF_REGISTER = str2bool(os.environ['GHC_SELF_REGISTER']) 56 | GHC_NOTIFICATIONS = str2bool(os.environ['GHC_NOTIFICATIONS']) 57 | GHC_NOTIFICATIONS_VERBOSITY = str2bool(os.environ['GHC_NOTIFICATIONS_VERBOSITY']) 58 | GHC_WWW_LINK_EXCEPTION_CHECK = str2bool(os.environ['GHC_WWW_LINK_EXCEPTION_CHECK']) 59 | GHC_LARGE_XML = str2bool(os.environ['GHC_LARGE_XML']) 60 | GHC_NOTIFICATIONS_EMAIL = os.environ['GHC_NOTIFICATIONS_EMAIL'] 61 | GHC_ADMIN_EMAIL = os.environ['GHC_ADMIN_EMAIL'] 62 | GHC_SITE_TITLE = os.environ['GHC_SITE_TITLE'] 63 | GHC_SITE_URL = os.environ['GHC_SITE_URL'] 64 | GHC_RUNNER_IN_WEBAPP = str2bool(os.environ['GHC_RUNNER_IN_WEBAPP']) 65 | GHC_REQUIRE_WEBAPP_AUTH = str2bool(os.environ['GHC_REQUIRE_WEBAPP_AUTH']) 66 | GHC_BASIC_AUTH_DISABLED = str2bool(os.environ['GHC_BASIC_AUTH_DISABLED']) 67 | GHC_VERIFY_SSL = str2bool(os.environ['GHC_VERIFY_SSL']) 68 | GHC_LOG_LEVEL = int(os.environ['GHC_LOG_LEVEL']) 69 | GHC_LOG_FORMAT = os.environ['GHC_LOG_FORMAT'] 70 | 71 | # Optional ENV set for GHC_PLUGINS (internal/core Plugins) 72 | # if not set default from config_main.py applies 73 | if os.environ.get('GHC_PLUGINS'): 74 | GHC_PLUGINS = os.environ['GHC_PLUGINS'] 75 | 76 | # Optional ENV set for GHC_USER_PLUGINS 77 | # if not set none applies 78 | if os.environ.get('GHC_USER_PLUGINS'): 79 | GHC_USER_PLUGINS = os.environ['GHC_USER_PLUGINS'] 80 | 81 | GHC_SMTP = { 82 | 'server': os.environ['GHC_SMTP_SERVER'], 83 | 'port': os.environ['GHC_SMTP_PORT'], 84 | 'tls': str2bool(os.environ['GHC_SMTP_TLS']), 85 | 'ssl': str2bool(os.environ['GHC_SMTP_SSL']), 86 | 'username': str2None(os.environ.get('GHC_SMTP_USERNAME')), 87 | 'password': str2None(os.environ.get('GHC_SMTP_PASSWORD')) 88 | } 89 | 90 | # TODO: provide for GHC Plugins 91 | 92 | GHC_RELIABILITY_MATRIX = { 93 | 'red': { 94 | 'min': 0, 95 | 'max': 49 96 | }, 97 | 'orange': { 98 | 'min': 50, 99 | 'max': 79 100 | }, 101 | 'green': { 102 | 'min': 80, 103 | 'max': 100 104 | } 105 | } 106 | 107 | GHC_MAP = { 108 | 'url': 'https://tile.osm.org/{z}/{x}/{y}.png', 109 | 'centre_lat': 42.3626, 110 | 'centre_long': -71.0843, 111 | 'maxzoom': 18, 112 | 'subdomains': 1234, 113 | } 114 | 115 | GEOIP = { 116 | 'plugin': 'GeoHealthCheck.plugins.geocode.webgeocoder.HttpGetGeocoder', 117 | 'parameters': { 118 | 'geocoder_url': os.environ['GHC_GEOIP_URL'], 119 | 'lat_field': os.environ['GHC_GEOIP_LATFIELD'], 120 | 'lon_field': os.environ['GHC_GEOIP_LONFIELD'] 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /docker/docker-clean.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker rm $(docker ps -a -q) 3 | docker rmi $(docker images -f dangling=true -q) 4 | -------------------------------------------------------------------------------- /docker/install-docker-ubuntu.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # This prepares an empty Ubuntu system for running Docker. 4 | # 5 | # Just van den Broecke - 2017 6 | # DEPRECATED - 2021 update: there are much quicker ways 7 | # See https://docs.docker.com/engine/install/ubuntu/ 8 | # 9 | # Below was based on 10 | # https://docs.docker.com/engine/installation/linux/ubuntu/ 11 | # as on may 26 2017. 12 | # Run as root or prepend all commands with "sudo"! 13 | # 14 | 15 | # Optional, comment out for your locale 16 | # set time right and configure timezone and locale 17 | # echo "Europe/Amsterdam" > /etc/timezone 18 | # dpkg-reconfigure -f noninteractive tzdata 19 | 20 | # Bring system uptodate 21 | apt-get update 22 | apt-get -y upgrade 23 | 24 | # Install packages to allow apt to use a repository over HTTPS 25 | apt-get install -y software-properties-common apt-transport-https ca-certificates curl 26 | 27 | # Add keys and extra repos 28 | curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - 29 | 30 | # Verify key 31 | apt-key fingerprint 0EBFCD88 32 | 33 | # Add Docker repo to deb config 34 | add-apt-repository \ 35 | "deb [arch=amd64] https://download.docker.com/linux/ubuntu \ 36 | $(lsb_release -cs) \ 37 | stable" 38 | 39 | # Bring packages uptodate 40 | apt-get update 41 | 42 | # The linux-image-extra package allows you use the aufs storage driver. 43 | # at popup keep locally installed config option 44 | # apt-get install -y linux-image-extra-$(uname -r) 45 | apt-get install -y linux-image-extra-$(uname -r) linux-image-extra-virtual 46 | 47 | # https://askubuntu.com/questions/98416/error-kernel-headers-not-found-but-they-are-in-place 48 | apt-get install -y build-essential linux-headers-`uname -r` dkms 49 | 50 | # Install Docker CE 51 | apt-get install docker-ce 52 | 53 | # If you are installing on Ubuntu 14.04 or 12.04, apparmor is required. 54 | # You can install it using (usually already installed) 55 | # apt-get install -y apparmor 56 | 57 | # Start the docker daemon. Usually already running 58 | # service docker start 59 | 60 | # Docker compose 61 | export dockerComposeVersion="1.20.1" 62 | curl -L https://github.com/docker/compose/releases/download/${dockerComposeVersion}/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose 63 | chmod +x /usr/local/bin/docker-compose 64 | -------------------------------------------------------------------------------- /docker/plugins/README.md: -------------------------------------------------------------------------------- 1 | # User Plugins 2 | 3 | GHC User Plugins can add new Probes and Checks or extend existing ones. 4 | 5 | ## Via Docker 6 | 7 | When using Docker, Plugins need to be available under `/GeoHealthCheck/GeoHealthCheck/plugins` within the 8 | GHC Docker Image or Container. 9 | 10 | You can choose to add your Plugins to the GHC Docker Image at build-time 11 | or to the GHC Docker Container at run-time. The latter method is preferred as you can use the standard 12 | GHC Docker Image from Docker Hub. In both cases your 13 | Plugin-modules and classes need to be configured in the GHC `GHC_USER_PLUGINS` Environment variable. 14 | 15 | ## At Image build-time 16 | 17 | The following steps: 18 | 19 | - place your Plugins within the sub-directory `user` 20 | - do regular Docker build `docker build -t geopython/geohealthcheck .` 21 | 22 | During the build Docker will `ADD` (copy) this dir to `/plugins` within the GHC Docker Image. 23 | The [install.sh](../install.sh) script will then move `/plugins` 24 | to the app-dir `/GeoHealthCheck/GeoHealthCheck/plugins`. 25 | 26 | ## At Container run-time (preferred) 27 | 28 | The following steps: 29 | 30 | - place your Plugins within this directory (or any other dir on your host) 31 | - make a Docker Volume mapping from this dir to the Container internal dir `/plugins` 32 | - specify your Plugins via Container Environment as `GHC_USER_PLUGINS: (comma-separated string of modules and/or classes)` 33 | - within `GHC_USER_PLUGINS` the Python package `GeoHealthCheck.plugins` is needed as prefix 34 | 35 | Example via [docker-compose.yml](../compose/docker-compose.yml): 36 | 37 | ``` 38 | services: 39 | geohealthcheck: 40 | image: geopython/geohealthcheck:latest 41 | ports: 42 | - 8083:80 43 | # Override settings to enable email notifications 44 | environment: 45 | GHC_USER_PLUGINS: 'GeoHealthCheck.plugins.user.myplugins' 46 | . 47 | . 48 | volumes: 49 | - ghc_sqlitedb:/GeoHealthCheck/DB 50 | - Path on the host, relative to the Compose file 51 | - ./../plugins:/plugins:ro 52 | ``` 53 | 54 | Or if you run the Image via `docker run` : 55 | 56 | 57 | ``` 58 | docker run -d --name GeoHealthCheck -p 8083:80 \ 59 | -v ghc_sqlitedb:/GeoHealthCheck/DB \ 60 | -v ./plugins:/plugins:ro \ 61 | -e 'GHC_USER_PLUGINS=GeoHealthCheck.plugins.user.myplugins' 62 | geopython/geohealthcheck:latest 63 | ``` 64 | 65 | When the Container starts it will copy all content under 66 | `/plugins` to the internal dir `/GeoHealthCheck/GeoHealthCheck/plugins`. 67 | 68 | -------------------------------------------------------------------------------- /docker/plugins/user/__init.py__: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopython/GeoHealthCheck/48fb6493694981689a811204968c9c5ee50ef5b5/docker/plugins/user/__init.py__ -------------------------------------------------------------------------------- /docker/plugins/user/mywmsprobe.py: -------------------------------------------------------------------------------- 1 | from GeoHealthCheck.probe import Probe 2 | from GeoHealthCheck.result import Result 3 | from owslib.wms import WebMapService 4 | 5 | 6 | class MyWMSProbe(Probe): 7 | """ 8 | Example Probe for WMS Probe user plugin. This is a free-form Probe 9 | that overrides perform_request with custom checks. 10 | 11 | To configure a probe, use Docker Container ENV 12 | GHC_USER_PLUGINS='GeoHealthCheck.plugins.user.mywmsprobe,...'. 13 | Note that GeoHealthCheck.plugins package prefix is required as 14 | Plugins are placed in GHC app tree there. 15 | """ 16 | 17 | NAME = 'MyWMSProbe' 18 | DESCRIPTION = 'Example User Probe, gets WMS Capabilities' 19 | RESOURCE_TYPE = 'OGC:WMS' 20 | 21 | REQUEST_METHOD = 'GET' 22 | 23 | PARAM_DEFS = { 24 | 'probing_level': { 25 | 'type': 'string', 26 | 'description': 'How heavy the Probe should be.', 27 | 'default': 'minor', 28 | 'required': True, 29 | 'range': ['minor', 'moderate', 'full'] 30 | } 31 | } 32 | """Param defs""" 33 | 34 | def __init__(self): 35 | Probe.__init__(self) 36 | 37 | def perform_request(self): 38 | """ 39 | Perform the request. 40 | See https://github.com/geopython/OWSLib/blob/ 41 | master/tests/doctests/wms_GeoServerCapabilities.txt 42 | """ 43 | 44 | # Test capabilities doc 45 | result = Result(True, 'Test Capabilities') 46 | result.start() 47 | try: 48 | wms = WebMapService(self._resource.url) 49 | title = wms.identification.title 50 | self.log('response: title=%s' % title) 51 | except Exception as err: 52 | result.set(False, str(err)) 53 | 54 | # Do more rigorous stuff here below 55 | result.stop() 56 | 57 | self.result.add_result(result) 58 | -------------------------------------------------------------------------------- /docker/scripts/configure.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Sets up various stuff in Docker Container: database and Plugins 3 | 4 | echo "START /configure.sh" 5 | 6 | # Make sure PYTHONPATH includes GeoHealthCheck 7 | export PYTHONPATH=/GeoHealthCheck/GeoHealthCheck:$PYTHONPATH 8 | 9 | # Determine database type from DB URI 10 | DB_TYPE=$(echo ${SQLALCHEMY_DATABASE_URI} | cut -f1 -d:) 11 | echo "Using DB_TYPE=${DB_TYPE}" 12 | 13 | # Create DB shorthand 14 | function create_db() { 15 | pushd /GeoHealthCheck/ || exit 1 16 | paver create -u ${ADMIN_NAME} -p ${ADMIN_PWD} -e ${ADMIN_EMAIL} 17 | popd || exit 1 18 | } 19 | 20 | # Init actions per DB type 21 | case ${DB_TYPE} in 22 | 23 | sqlite) 24 | if [ ! -f /GeoHealthCheck/DB/data.db ] 25 | then 26 | echo "Creating SQLite DB tables..." 27 | create_db 28 | else 29 | echo "NOT creating SQLite DB tables..." 30 | fi 31 | ;; 32 | 33 | postgresql) 34 | # format: postgresql://user:pw@host:5432/db 35 | # Bit tricky, may use awk, but cut out DB elements from URI 36 | DB_NAME=$(echo ${SQLALCHEMY_DATABASE_URI} | cut -f4 -d/) 37 | DB_PASSWD_HOST=$(echo ${SQLALCHEMY_DATABASE_URI} | cut -f3 -d:) 38 | DB_HOST=$(echo ${DB_PASSWD_HOST} | cut -f2 -d@) 39 | DB_PASSWD=$(echo ${DB_PASSWD_HOST} | cut -f1 -d@) 40 | DB_USER_SLASH=$(echo ${SQLALCHEMY_DATABASE_URI} | cut -f2 -d:) 41 | DB_USER=$(echo ${DB_USER_SLASH} | cut -f3 -d/) 42 | export PGPASSWORD=${DB_PASSWD} 43 | 44 | # We need to wait until PG Container available 45 | echo "Check if Postgres is avail/ready..." 46 | until pg_isready -h "${DB_HOST}"; do 47 | echo "Exit code=$? - Postgres not ready - sleeping" 48 | sleep 1 49 | done 50 | 51 | # Check if we need to create DB tables 52 | echo "Postgres is up - check if DB populated" 53 | if ! psql -h "${DB_HOST}" -U "${DB_USER}" -c 'SELECT COUNT(*) FROM resource' ${DB_NAME} 54 | then 55 | echo "Creating Postgres DB tables..." 56 | create_db 57 | else 58 | echo "Postgres DB already populated" 59 | fi 60 | 61 | ;; 62 | *) 63 | echo "Unknown database type ${DB_TYPE}, exiting" 64 | exit -1 65 | ;; 66 | esac 67 | 68 | # Copy possible mounted Plugins into app tree 69 | if [ -d /plugins ] 70 | then 71 | cp -ar /plugins/* /GeoHealthCheck/GeoHealthCheck/plugins/ 72 | fi 73 | 74 | echo "END /configure.sh" 75 | -------------------------------------------------------------------------------- /docker/scripts/cron-jobs-daily.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | python3 /GeoHealthCheck/GeoHealthCheck/models.py flush 4 | 5 | -------------------------------------------------------------------------------- /docker/scripts/cron-jobs-hourly.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | # Copy possible mounted Plugins into app tree 5 | if [ -d /plugins ] 6 | then 7 | cp -ar /plugins/* /GeoHealthCheck/GeoHealthCheck/plugins/ 8 | fi 9 | 10 | python /GeoHealthCheck/GeoHealthCheck/healthcheck.py 11 | -------------------------------------------------------------------------------- /docker/scripts/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # GHC Source was added in Dockerfile, install 4 | # NB we use gunicorn/eventlet async workers as some Probes may take a long time 5 | # e.g. fetching Metadata (Caps) and testing all layers 6 | # Install Python packages for installation and setup 7 | 8 | pushd /GeoHealthCheck || exit 1 9 | 10 | # Docker-specific deps 11 | pip install -r docker/scripts/requirements.txt 12 | 13 | # Sets up GHC itself 14 | paver setup 15 | mv /config_site.py /GeoHealthCheck/instance/config_site.py 16 | 17 | # Copy possible Plugins into app tree 18 | if [ -d /plugins ] 19 | then 20 | # Copy possible Plugins into app tree 21 | echo "Installing Plugins..." 22 | cp -ar /plugins/* GeoHealthCheck/plugins/ 23 | 24 | # Remove to allow later Volume mount of /plugins 25 | rm -rf /plugins 26 | fi 27 | 28 | popd || exit 1 29 | -------------------------------------------------------------------------------- /docker/scripts/requirements.txt: -------------------------------------------------------------------------------- 1 | Paver==1.3.4 2 | -------------------------------------------------------------------------------- /docker/scripts/run-runner.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "START /run-runner.sh" 4 | 5 | # Set the timezone. 6 | # /set-timezone.sh 7 | 8 | # Configure: DB and plugins. 9 | /configure.sh 10 | 11 | # Make sure PYTHONPATH includes GeoHealthCheck 12 | export PYTHONPATH=/GeoHealthCheck/GeoHealthCheck:$PYTHONPATH 13 | 14 | cd /GeoHealthCheck 15 | paver runner_daemon 16 | 17 | echo "END /run-runner.sh" 18 | -------------------------------------------------------------------------------- /docker/scripts/run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Run unit tests in Docker Image 3 | # 4 | # Just van den Broecke - 2021 5 | # Usage: 6 | # docker run --entrypoint "/run-tests.sh" geopython/geohealthcheck:latest 7 | # 8 | echo "START /run-tests.sh" 9 | 10 | # Set the timezone. 11 | # /set-timezone.sh 12 | 13 | # Configure: DB and plugins. 14 | /configure.sh 15 | 16 | # Make sure PYTHONPATH includes GeoHealthCheck 17 | export PYTHONPATH=/GeoHealthCheck/GeoHealthCheck:$PYTHONPATH 18 | 19 | cd /GeoHealthCheck 20 | paver run_tests 21 | 22 | echo "END /run-tests.sh" 23 | -------------------------------------------------------------------------------- /docker/scripts/run-web.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Runs the GHC app with gunicorn 4 | 5 | echo "START /run-web.sh" 6 | 7 | # Set the timezone. 8 | # /set-timezone.sh 9 | 10 | # Configure: DB and plugins. 11 | /configure.sh 12 | 13 | # Make sure PYTHONPATH includes GeoHealthCheck 14 | export PYTHONPATH=/GeoHealthCheck/GeoHealthCheck:$PYTHONPATH 15 | 16 | cd /GeoHealthCheck 17 | 18 | paver upgrade 19 | 20 | # SCRIPT_NAME should not have value '/' 21 | [ "${SCRIPT_NAME}" = '/' ] && export SCRIPT_NAME="" && echo "make SCRIPT_NAME empty from /" 22 | 23 | echo "Running GHC WSGI on ${HOST}:${PORT} with ${WSGI_WORKERS} workers and SCRIPT_NAME=${SCRIPT_NAME}" 24 | exec gunicorn --workers ${WSGI_WORKERS} \ 25 | --worker-class=${WSGI_WORKER_CLASS} \ 26 | --timeout ${WSGI_WORKER_TIMEOUT} \ 27 | --name="Gunicorn_GHC" \ 28 | --bind ${HOST}:${PORT} \ 29 | GeoHealthCheck.app:APP 30 | 31 | # Built-in Flask server, deprecated 32 | # python /GeoHealthCheck/GeoHealthCheck/app.py ${HOST}:${PORT} 33 | 34 | echo "END /run-web.sh" 35 | -------------------------------------------------------------------------------- /docker/scripts/set-timezone.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Set timezone and time right in container. 4 | # See: https://www.ivankrizsan.se/2015/10/31/time-in-docker-containers/ (Alpine) 5 | 6 | # Set the timezone. Base image does not contain 7 | # the setup-timezone script, so an alternate way is used. 8 | if [ "$CONTAINER_TIMEZONE" = "" ]; 9 | then 10 | CONTAINER_TIMEZONE="Europe/London" 11 | else 12 | echo "Container timezone not modified" 13 | fi 14 | 15 | cp /usr/share/zoneinfo/${CONTAINER_TIMEZONE} /etc/localtime && \ 16 | echo "${CONTAINER_TIMEZONE}" > /etc/timezone && \ 17 | echo "Container timezone set to: $CONTAINER_TIMEZONE" 18 | 19 | # Force immediate synchronisation of the time and start the time-synchronization service. 20 | # In order to be able to use ntpd in the container, it must be run with the SYS_TIME capability. 21 | # In addition you may want to add the SYS_NICE capability, 22 | # in order for ntpd to be able to modify its priority. 23 | # ntpd -s 24 | -------------------------------------------------------------------------------- /docs/_static/architecture.odg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopython/GeoHealthCheck/48fb6493694981689a811204968c9c5ee50ef5b5/docs/_static/architecture.odg -------------------------------------------------------------------------------- /docs/_static/datamodel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopython/GeoHealthCheck/48fb6493694981689a811204968c9c5ee50ef5b5/docs/_static/datamodel.png -------------------------------------------------------------------------------- /docs/_static/ghc-parts.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopython/GeoHealthCheck/48fb6493694981689a811204968c9c5ee50ef5b5/docs/_static/ghc-parts.jpg -------------------------------------------------------------------------------- /docs/_static/logo_medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopython/GeoHealthCheck/48fb6493694981689a811204968c9c5ee50ef5b5/docs/_static/logo_medium.png -------------------------------------------------------------------------------- /docs/_static/notifications_config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopython/GeoHealthCheck/48fb6493694981689a811204968c9c5ee50ef5b5/docs/_static/notifications_config.png -------------------------------------------------------------------------------- /docs/_static/userguide/add-resource-1-s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopython/GeoHealthCheck/48fb6493694981689a811204968c9c5ee50ef5b5/docs/_static/userguide/add-resource-1-s.png -------------------------------------------------------------------------------- /docs/_static/userguide/add-resource-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopython/GeoHealthCheck/48fb6493694981689a811204968c9c5ee50ef5b5/docs/_static/userguide/add-resource-1.png -------------------------------------------------------------------------------- /docs/_static/userguide/add-resource-2-s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopython/GeoHealthCheck/48fb6493694981689a811204968c9c5ee50ef5b5/docs/_static/userguide/add-resource-2-s.png -------------------------------------------------------------------------------- /docs/_static/userguide/add-resource-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopython/GeoHealthCheck/48fb6493694981689a811204968c9c5ee50ef5b5/docs/_static/userguide/add-resource-2.png -------------------------------------------------------------------------------- /docs/_static/userguide/dashboard-home-s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopython/GeoHealthCheck/48fb6493694981689a811204968c9c5ee50ef5b5/docs/_static/userguide/dashboard-home-s.png -------------------------------------------------------------------------------- /docs/_static/userguide/dashboard-home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopython/GeoHealthCheck/48fb6493694981689a811204968c9c5ee50ef5b5/docs/_static/userguide/dashboard-home.png -------------------------------------------------------------------------------- /docs/_static/userguide/edit-resource-1-s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopython/GeoHealthCheck/48fb6493694981689a811204968c9c5ee50ef5b5/docs/_static/userguide/edit-resource-1-s.png -------------------------------------------------------------------------------- /docs/_static/userguide/edit-resource-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopython/GeoHealthCheck/48fb6493694981689a811204968c9c5ee50ef5b5/docs/_static/userguide/edit-resource-1.png -------------------------------------------------------------------------------- /docs/_static/userguide/edit-resource-2-s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopython/GeoHealthCheck/48fb6493694981689a811204968c9c5ee50ef5b5/docs/_static/userguide/edit-resource-2-s.png -------------------------------------------------------------------------------- /docs/_static/userguide/edit-resource-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopython/GeoHealthCheck/48fb6493694981689a811204968c9c5ee50ef5b5/docs/_static/userguide/edit-resource-2.png -------------------------------------------------------------------------------- /docs/_static/userguide/edit-resource-3-s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopython/GeoHealthCheck/48fb6493694981689a811204968c9c5ee50ef5b5/docs/_static/userguide/edit-resource-3-s.png -------------------------------------------------------------------------------- /docs/_static/userguide/edit-resource-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopython/GeoHealthCheck/48fb6493694981689a811204968c9c5ee50ef5b5/docs/_static/userguide/edit-resource-3.png -------------------------------------------------------------------------------- /docs/_static/userguide/email-notification-fail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopython/GeoHealthCheck/48fb6493694981689a811204968c9c5ee50ef5b5/docs/_static/userguide/email-notification-fail.png -------------------------------------------------------------------------------- /docs/_static/userguide/email-notification-ok.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopython/GeoHealthCheck/48fb6493694981689a811204968c9c5ee50ef5b5/docs/_static/userguide/email-notification-ok.png -------------------------------------------------------------------------------- /docs/_static/userguide/register-s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopython/GeoHealthCheck/48fb6493694981689a811204968c9c5ee50ef5b5/docs/_static/userguide/register-s.png -------------------------------------------------------------------------------- /docs/_static/userguide/register.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopython/GeoHealthCheck/48fb6493694981689a811204968c9c5ee50ef5b5/docs/_static/userguide/register.png -------------------------------------------------------------------------------- /docs/_static/userguide/wms-resource-history-detail-s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopython/GeoHealthCheck/48fb6493694981689a811204968c9c5ee50ef5b5/docs/_static/userguide/wms-resource-history-detail-s.png -------------------------------------------------------------------------------- /docs/_static/userguide/wms-resource-history-detail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopython/GeoHealthCheck/48fb6493694981689a811204968c9c5ee50ef5b5/docs/_static/userguide/wms-resource-history-detail.png -------------------------------------------------------------------------------- /docs/_static/userguide/wms-resource-history-s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopython/GeoHealthCheck/48fb6493694981689a811204968c9c5ee50ef5b5/docs/_static/userguide/wms-resource-history-s.png -------------------------------------------------------------------------------- /docs/_static/userguide/wms-resource-history.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopython/GeoHealthCheck/48fb6493694981689a811204968c9c5ee50ef5b5/docs/_static/userguide/wms-resource-history.png -------------------------------------------------------------------------------- /docs/_static/userguide/wms-resource-s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopython/GeoHealthCheck/48fb6493694981689a811204968c9c5ee50ef5b5/docs/_static/userguide/wms-resource-s.png -------------------------------------------------------------------------------- /docs/_static/userguide/wms-resource.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopython/GeoHealthCheck/48fb6493694981689a811204968c9c5ee50ef5b5/docs/_static/userguide/wms-resource.png -------------------------------------------------------------------------------- /docs/_static/userguide/wms-resources-s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopython/GeoHealthCheck/48fb6493694981689a811204968c9c5ee50ef5b5/docs/_static/userguide/wms-resources-s.png -------------------------------------------------------------------------------- /docs/_static/userguide/wms-resources.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geopython/GeoHealthCheck/48fb6493694981689a811204968c9c5ee50ef5b5/docs/_static/userguide/wms-resources.png -------------------------------------------------------------------------------- /docs/admin.rst: -------------------------------------------------------------------------------- 1 | .. _admin: 2 | 3 | Administration 4 | ============== 5 | 6 | This chapter describes maintenance tasks for the administrator of a GHC instance. 7 | There is a separate :ref:`userguide` that provides guidance to the end-user to 8 | configure the actual Resource healthchecks. 9 | 10 | Each of the sections below is geared at a specific administrative task area. 11 | 12 | Database 13 | -------- 14 | 15 | For database administration the following commands are available. 16 | 17 | create db 18 | ......... 19 | 20 | To create the database execute the following: 21 | 22 | Open a command line, (if needed activate your virtualenv), and do :: 23 | 24 | python GeoHealthCheck/models.py create 25 | 26 | drop db 27 | ....... 28 | 29 | To delete the database execute the following, however you will loose all your information. So please ensure backup if needed: 30 | 31 | Open a command line, (if needed activate your virtualenv), and do :: 32 | 33 | python GeoHealthCheck/models.py drop 34 | 35 | Note: you need to create a Database again before you can start GHC again. 36 | 37 | load data 38 | ......... 39 | 40 | To load a JSON data file, do (WARN: deletes existing data!) :: 41 | 42 | python GeoHealthCheck/models.py load [y/n] 43 | 44 | Hint: see `tests/data` for example JSON data files. 45 | 46 | export data 47 | ........... 48 | 49 | Exporting database-data to a .json file with or without Runs is still to be done. 50 | 51 | Exporting Resource and Run data from a running GHC instance can be effected via 52 | a REST API, for example: 53 | 54 | * all Resources: https://demo.geohealthcheck.org/json (or `as CSV `_) 55 | * one Resource: https://demo.geohealthcheck.org/resource/1/json (or `CSV `_) 56 | * all history (Runs) of one Resource: https://demo.geohealthcheck.org/resource/1/history/json (or `in csv `_) 57 | 58 | NB for detailed reporting data only JSON is supported. 59 | 60 | .. _admin_user_mgt: 61 | 62 | User Management 63 | --------------- 64 | 65 | During initial setup, a single `admin` user is created interactively. 66 | 67 | Via the **GHC_SELF_REGISTER** config setting, you allow/disallow registrations from users on the webapp (UI). 68 | 69 | Passwords 70 | ......... 71 | 72 | Passwords are stored encrypted. Even the same password-string will have different "hashes". 73 | There is no way that GHC can decrypt a stored password. This can become a challenge in cases where 74 | a password is forgotten and somehow the email-based reset is not available nor working. 75 | In that case, password-hashes can be created from the command-line using the Python library `passlib `_ 76 | within an interactive Python-shell as follows: :: 77 | 78 | $ pip install passlib 79 | # or in Debian/Ubuntu: apt-get install python-passlib 80 | 81 | python 82 | >>> from passlib.hash import pbkdf2_sha256 83 | >>> 84 | >>> hash = pbkdf2_sha256.hash("mynewpassword") 85 | >>> print(hash) 86 | '$pbkdf2-sha256$29000$da51rlVKKWVsLSWEsBYCoA$2/shIdqAxGJkDq6TTeIOgQKbtYAOPSi5EA3TDij1L6Y' 87 | >>> pbkdf2_sha256.verify("mynewpassword", hash) 88 | True 89 | 90 | Or more compact within the root dir of your GHC installation: :: 91 | 92 | >>> from GeoHealthCheck.util import create_hash 93 | >>> create_hash('mynewpassword') 94 | '$pbkdf2-sha256$29000$8X4PAUAIAcC4V2rNea9Vqg$XnMx1SfEiBzBAMOQOOC7uxCcyzVuKaHENLj3IfXvfu0' 95 | 96 | Or even more compact within the root dir of your GHC installation via Paver: :: 97 | 98 | $ paver create_hash -p mypass 99 | ---> pavement.create_hash 100 | Copy/paste the entire token below for example to set password 101 | $pbkdf2-sha256$29000$FkJoTYnxPqc0pjQG4HxP6Q$C3SZb8jqtM7zKS1DSLcouc/CL9XMI9cL5xT6DRTOEd4 102 | 103 | Then copy-paste the hash-string into the `password`-field of the User-record in the User-table. For example in SQL something like: :: 104 | 105 | $ sqlite3 data.db 106 | # or psql equivalent for Postgres 107 | 108 | sqlite> UPDATE user SET password = '' WHERE username == 'myusername'; 109 | 110 | Build Documentation 111 | ------------------- 112 | 113 | Open a command line, (if needed activate your virtualenv) and move into the directory ``GeoHealthCheck/doc/``. 114 | In there, type ``make html`` plus ENTER and the documentation should be built locally. 115 | -------------------------------------------------------------------------------- /docs/contact.rst: -------------------------------------------------------------------------------- 1 | .. _contact: 2 | 3 | Contact 4 | ======= 5 | 6 | The website `geohealthcheck.org `_ is the main entry point. 7 | 8 | All development is done via GitHub: see https://github.com/geopython/geohealthcheck. 9 | 10 | Links 11 | ----- 12 | 13 | - website: http://geohealthcheck.org 14 | - GitHub: https://github.com/geopython/geohealthcheck 15 | - Demo: https://demo.geohealthcheck.org 16 | - Presentation: http://geohealthcheck.org/presentation 17 | - Gitter Chat: https://gitter.im/geopython/GeoHealthCheck 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | GeoHealthCheck 2 | ============== 3 | 4 | .. image:: _static/logo_medium.png 5 | 6 | Overview 7 | -------- 8 | 9 | GeoHealthCheck (GHC) is a Python application to support monitoring OGC services uptime, 10 | availability and Quality of Service (QoS). 11 | 12 | GHC can be used to monitor overall health of OGC services (OWS) like WMS, WFS, WCS, WMTS, SOS, CSW 13 | and more, plus some recent OGC APIs like SensorThings API and WFS v3 (OGC Features API). 14 | But also standard web REST APIs and ordinary URLs can be monitored. 15 | 16 | Features 17 | -------- 18 | 19 | - lightweight (Python with Flask) 20 | - easy setup 21 | - support for numerous OGC resources 22 | - flexible and customizable: look and feel, scoring matrix 23 | - user management 24 | - database agnostic: any SQLAlchemy supported backend 25 | - database upgrades: using Alembic with Flask-Migrate 26 | - extensible healthchecks via Plugins 27 | - per-resource scheduling and notifications 28 | - per-resource HTTP-authentication like Basic, Token (optional) 29 | - regular status summary report via email (optional) 30 | 31 | Links 32 | ----- 33 | 34 | - website: http://geohealthcheck.org 35 | - GitHub: https://github.com/geopython/geohealthcheck 36 | - Demo: https://demo.geohealthcheck.org (official demo, master branch) 37 | - Presentation: http://geohealthcheck.org/presentation 38 | - Gitter Chat: https://gitter.im/geopython/GeoHealthCheck 39 | 40 | This document applies to GHC version |release| and was generated on |today|. 41 | The latest version is always available at http://docs.geohealthcheck.org. 42 | 43 | Contents: 44 | 45 | .. toctree:: 46 | :numbered: 47 | :maxdepth: 2 48 | 49 | install.rst 50 | config.rst 51 | admin.rst 52 | userguide.rst 53 | architecture.rst 54 | plugins.rst 55 | license.rst 56 | contact.rst 57 | 58 | Indices and tables 59 | ================== 60 | 61 | * :ref:`genindex` 62 | * :ref:`modindex` 63 | * :ref:`search` 64 | -------------------------------------------------------------------------------- /docs/license.rst: -------------------------------------------------------------------------------- 1 | .. _license: 2 | 3 | License 4 | ======= 5 | 6 | .. include:: ../LICENSE 7 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | # for ReadTheDocs 2 | sphinx-rtd-theme==1.3.0 3 | sphinx-autoapi 4 | -------------------------------------------------------------------------------- /jobs.cron: -------------------------------------------------------------------------------- 1 | @hourly /virtualenv/bin/python /path/to/GeoHealthCheck/GeoHealthCheck/healthcheck.py run 2 | @daily /virtualenv/bin/python /path/to/GeoHealthCheck/GeoHealthCheck/models.py flush 3 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | flake8==5.0.4 2 | Paver==1.3.4 3 | pylint==2.13.9 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Jinja2==3.0.3 2 | markupsafe==2.0.1 3 | Flask==1.1.1 4 | Flask-Babel==0.12.2 5 | Flask-Login==0.4.1 6 | Flask-Migrate==2.5.2 7 | Flask-Script==2.0.6 8 | SQLAlchemy==1.3.8 9 | Flask-SQLAlchemy==2.4.0 10 | itsdangerous==1.1.0 11 | pyproj >=2.6.1 12 | lxml >= 4.8.0, <= 4.9.2 13 | OWSLib==0.20.0 14 | jsonschema==3.0.2 # downgrade from 3.2.0 on sept 29, 2020, issue 331, consider better fix 15 | openapi-spec-validator==0.2.8 16 | Sphinx==5.3.0 17 | sphinx-rtd-theme==1.3.0 18 | sphinx-autoapi 19 | requests>=2.23.0 20 | WTForms==2.2.1 21 | APScheduler==3.6.1 22 | passlib==1.7.1 23 | Werkzeug==0.16.1 24 | tzlocal<3.0 # Fix based on https://github.com/Yelp/elastalert/issues/2968 25 | -------------------------------------------------------------------------------- /tests/data/README.md: -------------------------------------------------------------------------------- 1 | # Test Data 2 | 3 | The test data in this directory consists of configurations, settings and 4 | publically available OGC Web Services to facilitate testing. 5 | -------------------------------------------------------------------------------- /tests/data/minimal.json: -------------------------------------------------------------------------------- 1 | { 2 | "users": { 3 | "admin": { 4 | "username": "admin", 5 | "password": "admin", 6 | "email": "foo@example.com", 7 | "role": "admin" 8 | } 9 | }, 10 | "tags": { 11 | }, 12 | "resources": { 13 | "PDOK BAG WMS": { 14 | "owner": "admin", 15 | "resource_type": "OGC:WMS", 16 | "active": true, 17 | "title": "PDOK BAG Web Map Service", 18 | "url": "https://service.pdok.nl/lv/bag/wms/v2_0", 19 | "tags": [] 20 | } 21 | }, 22 | "probe_vars": { 23 | "PDOK BAG WMS - GetCaps": { 24 | "resource": "PDOK BAG WMS", 25 | "probe_class": "GeoHealthCheck.plugins.probe.owsgetcaps.WmsGetCaps", 26 | "parameters": { 27 | "service": "WMS", 28 | "version": "1.0.0" 29 | } 30 | }, 31 | "PDOK BAG WMS - GetMap Single": { 32 | "resource": "PDOK BAG WMS", 33 | "probe_class": "GeoHealthCheck.plugins.probe.wms.WmsGetMapV1", 34 | "parameters": { 35 | } 36 | } 37 | }, 38 | "check_vars": { 39 | "PDOK BAG WMS - GetCaps - XML Parse": { 40 | "probe_vars": "PDOK BAG WMS - GetCaps", 41 | "check_class": "GeoHealthCheck.plugins.check.checks.XmlParse", 42 | "parameters": {} 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/run_tests.py: -------------------------------------------------------------------------------- 1 | # ================================================================= 2 | # 3 | # Authors: Tom Kralidis , 4 | # Just van den Broecke 5 | # 6 | # Copyright (c) 2014 Tom Kralidis 7 | # 8 | # Permission is hereby granted, free of charge, to any person 9 | # obtaining a copy of this software and associated documentation 10 | # files (the "Software"), to deal in the Software without 11 | # restriction, including without limitation the rights to use, 12 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | # copies of the Software, and to permit persons to whom the 14 | # Software is furnished to do so, subject to the following 15 | # conditions: 16 | # 17 | # The above copyright notice and this permission notice shall be 18 | # included in all copies or substantial portions of the Software. 19 | # 20 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 21 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 22 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 23 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 24 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 25 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 26 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 27 | # OTHER DEALINGS IN THE SOFTWARE. 28 | # 29 | # ================================================================= 30 | 31 | import unittest 32 | import sys 33 | import os 34 | 35 | TEST_DIR = os.path.dirname(os.path.abspath(__file__)) 36 | GHC_DIR = TEST_DIR[:-5] + 'GeoHealthCheck' 37 | 38 | # Needed to find classes and plugins 39 | sys.path.append(GHC_DIR) 40 | 41 | # Shorthand to run all test scripts in tests dir. 42 | # TODO use nose or more intelligent test_*.py discovery 43 | if __name__ == '__main__': 44 | unittest.main(module='test_plugins', exit=False) 45 | unittest.main(module='test_resources') 46 | --------------------------------------------------------------------------------