From b566f81a6b7ff82a197477bdf3e650f22c639514 Mon Sep 17 00:00:00 2001 From: Robert Kaussow Date: Wed, 9 Jun 2021 20:44:10 +0200 Subject: [PATCH] inital commit --- .chglog/CHANGELOG.tpl.md | 27 + .chglog/config.yml | 25 + .dictionary | 3 + .drone.jsonnet | 493 ++++++++++++++++ .drone.yml | 635 +++++++++++++++++++++ .github/settings.yml | 56 ++ .gitignore | 111 ++++ .markdownlint.yml | 6 + .prettierignore | 2 + CONTRIBUTING.md | 31 ++ LICENSE | 21 + Makefile | 20 + README.md | 24 + docker/Dockerfile.amd64 | 23 + docker/Dockerfile.arm | 23 + docker/Dockerfile.arm64 | 23 + docker/manifest-quay.tmpl | 24 + docker/manifest.tmpl | 24 + poetry.lock | 1022 ++++++++++++++++++++++++++++++++++ prometheuspvesd/__init__.py | 3 + prometheuspvesd/cli.py | 111 ++++ prometheuspvesd/config.py | 247 ++++++++ prometheuspvesd/discovery.py | 206 +++++++ prometheuspvesd/exception.py | 17 + prometheuspvesd/model.py | 48 ++ prometheuspvesd/utils.py | 242 ++++++++ pyproject.toml | 95 ++++ renovate.json | 4 + setup.cfg | 19 + 29 files changed, 3585 insertions(+) create mode 100755 .chglog/CHANGELOG.tpl.md create mode 100755 .chglog/config.yml create mode 100644 .dictionary create mode 100644 .drone.jsonnet create mode 100644 .drone.yml create mode 100644 .github/settings.yml create mode 100644 .gitignore create mode 100644 .markdownlint.yml create mode 100644 .prettierignore create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 docker/Dockerfile.amd64 create mode 100644 docker/Dockerfile.arm create mode 100644 docker/Dockerfile.arm64 create mode 100644 docker/manifest-quay.tmpl create mode 100644 docker/manifest.tmpl create mode 100644 poetry.lock create mode 100644 prometheuspvesd/__init__.py create mode 100644 prometheuspvesd/cli.py create mode 100644 prometheuspvesd/config.py create mode 100644 prometheuspvesd/discovery.py create mode 100644 prometheuspvesd/exception.py create mode 100644 prometheuspvesd/model.py create mode 100644 prometheuspvesd/utils.py create mode 100644 pyproject.toml create mode 100644 renovate.json create mode 100644 setup.cfg diff --git a/.chglog/CHANGELOG.tpl.md b/.chglog/CHANGELOG.tpl.md new file mode 100755 index 0000000..3f7457d --- /dev/null +++ b/.chglog/CHANGELOG.tpl.md @@ -0,0 +1,27 @@ +# Changelog + +{{ range .Versions -}} +## {{ if .Tag.Previous }}[{{ .Tag.Name }}]({{ $.Info.RepositoryURL }}/compare/{{ .Tag.Previous.Name }}...{{ .Tag.Name }}){{ else }}{{ .Tag.Name }}{{ end }} ({{ datetime "2006-01-02" .Tag.Date }}) + +{{ range .CommitGroups -}} +### {{ .Title }} + +{{ $subjects := list }} +{{ range .Commits -}} +{{ if not (has .Subject $subjects) -}} +- {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }} +{{ $subjects = append $subjects .Subject -}} +{{ end }} +{{- end }} +{{- end -}} + +{{- if .NoteGroups -}} +{{ range .NoteGroups -}} +### {{ .Title }} + +{{ range .Notes }} +{{ .Body }} +{{ end }} +{{ end -}} +{{ end -}} +{{ end -}} diff --git a/.chglog/config.yml b/.chglog/config.yml new file mode 100755 index 0000000..8cc2a67 --- /dev/null +++ b/.chglog/config.yml @@ -0,0 +1,25 @@ +style: github +template: CHANGELOG.tpl.md +info: + title: CHANGELOG + repository_url: https://github.com/thegeeklab/prometheus-pve-sd +options: + commit_groups: + title_maps: + feat: Features + fix: Bug Fixes + perf: Performance Improvements + refactor: Code Refactoring + chore: Others + test: Testing + ci: CI Pipeline + docs: Documentation + header: + pattern: "^(\\w*)(?:\\(([\\w\\$\\.\\-\\*\\s]*)\\))?\\:\\s(.*)$" + pattern_maps: + - Type + - Scope + - Subject + notes: + keywords: + - BREAKING CHANGE diff --git a/.dictionary b/.dictionary new file mode 100644 index 0000000..e767b3a --- /dev/null +++ b/.dictionary @@ -0,0 +1,3 @@ +Kaussow +PyPI +xoxys diff --git a/.drone.jsonnet b/.drone.jsonnet new file mode 100644 index 0000000..b5c4a74 --- /dev/null +++ b/.drone.jsonnet @@ -0,0 +1,493 @@ +local PythonVersion(pyversion='3.6') = { + name: 'python' + std.strReplace(pyversion, '.', '') + '-pytest', + image: 'python:' + pyversion, + environment: { + PY_COLORS: 1, + }, + commands: [ + 'pip install poetry poetry-dynamic-versioning -qq', + 'poetry config experimental.new-installer false', + 'poetry install', + 'poetry version', + 'poetry run prometheus-pve-sd --help', + ], + depends_on: [ + 'fetch', + ], +}; + +local PipelineLint = { + kind: 'pipeline', + name: 'lint', + platform: { + os: 'linux', + arch: 'amd64', + }, + steps: [ + { + name: 'yapf', + image: 'python:3.9', + environment: { + PY_COLORS: 1, + }, + commands: [ + 'git fetch -tq', + 'pip install poetry poetry-dynamic-versioning -qq', + 'poetry config experimental.new-installer false', + 'poetry install', + 'poetry run yapf -dr ./prometheuspvesd', + ], + }, + { + name: 'flake8', + image: 'python:3.9', + environment: { + PY_COLORS: 1, + }, + commands: [ + 'git fetch -tq', + 'pip install poetry poetry-dynamic-versioning -qq', + 'poetry config experimental.new-installer false', + 'poetry install', + 'poetry run flake8 ./prometheuspvesd', + ], + }, + ], + trigger: { + ref: ['refs/heads/main', 'refs/tags/**', 'refs/pull/**'], + }, +}; + +local PipelineTest = { + kind: 'pipeline', + name: 'test', + platform: { + os: 'linux', + arch: 'amd64', + }, + steps: [ + { + name: 'fetch', + image: 'python:3.9', + commands: [ + 'git fetch -tq', + ], + }, + PythonVersion(pyversion='3.6'), + PythonVersion(pyversion='3.7'), + PythonVersion(pyversion='3.8'), + PythonVersion(pyversion='3.9'), + ], + depends_on: [ + 'lint', + ], + trigger: { + ref: ['refs/heads/main', 'refs/tags/**', 'refs/pull/**'], + }, +}; + +local PipelineSecurity = { + kind: 'pipeline', + name: 'security', + platform: { + os: 'linux', + arch: 'amd64', + }, + steps: [ + { + name: 'bandit', + image: 'python:3.9', + environment: { + PY_COLORS: 1, + }, + commands: [ + 'git fetch -tq', + 'pip install poetry poetry-dynamic-versioning -qq', + 'poetry config experimental.new-installer false', + 'poetry install', + 'poetry run bandit -r ./prometheuspvesd -x ./prometheuspvesd/test', + ], + }, + ], + depends_on: [ + 'test', + ], + trigger: { + ref: ['refs/heads/main', 'refs/tags/**', 'refs/pull/**'], + }, +}; + +local PipelineBuildPackage = { + kind: 'pipeline', + name: 'build-package', + platform: { + os: 'linux', + arch: 'amd64', + }, + steps: [ + { + name: 'build', + image: 'python:3.9', + commands: [ + 'git fetch -tq', + 'pip install poetry poetry-dynamic-versioning -qq', + 'poetry build', + ], + }, + { + name: 'checksum', + image: 'alpine', + commands: [ + 'cd dist/ && sha256sum * > ../sha256sum.txt', + ], + }, + { + name: 'changelog-generate', + image: 'thegeeklab/git-chglog', + commands: [ + 'git fetch -tq', + 'git-chglog --no-color --no-emoji -o CHANGELOG.md ${DRONE_TAG:---next-tag unreleased unreleased}', + ], + }, + { + name: 'changelog-format', + image: 'thegeeklab/alpine-tools', + commands: [ + 'prettier CHANGELOG.md', + 'prettier -w CHANGELOG.md', + ], + }, + { + name: 'publish-github', + image: 'plugins/github-release', + settings: { + overwrite: true, + api_key: { from_secret: 'github_token' }, + files: ['dist/*', 'sha256sum.txt'], + title: '${DRONE_TAG}', + note: 'CHANGELOG.md', + }, + when: { + ref: ['refs/tags/**'], + }, + }, + { + name: 'publish-pypi', + image: 'python:3.9', + commands: [ + 'git fetch -tq', + 'pip install poetry poetry-dynamic-versioning -qq', + 'poetry publish -n', + ], + environment: { + POETRY_HTTP_BASIC_PYPI_USERNAME: { from_secret: 'pypi_username' }, + POETRY_HTTP_BASIC_PYPI_PASSWORD: { from_secret: 'pypi_password' }, + }, + when: { + ref: ['refs/tags/**'], + }, + }, + ], + depends_on: [ + 'security', + ], + trigger: { + ref: ['refs/heads/main', 'refs/tags/**', 'refs/pull/**'], + }, +}; + +local PipelineBuildContainer(arch='amd64') = { + local build = if arch == 'arm' then [{ + name: 'build', + image: 'python:3.9-alpine', + commands: [ + 'apk add -Uq --no-cache build-base libressl-dev libffi-dev musl-dev python3-dev git cargo', + 'git fetch -tq', + 'pip install poetry poetry-dynamic-versioning -qq', + 'poetry build', + ], + environment: { + CARGO_NET_GIT_FETCH_WITH_CLI: true, + }, + }] else [{ + name: 'build', + image: 'python:3.9', + commands: [ + 'git fetch -tq', + 'pip install poetry poetry-dynamic-versioning -qq', + 'poetry build', + ], + }], + + kind: 'pipeline', + name: 'build-container-' + arch, + platform: { + os: 'linux', + arch: arch, + }, + steps: build + [ + { + name: 'dryrun', + image: 'thegeeklab/drone-docker:19', + settings: { + dry_run: true, + dockerfile: 'docker/Dockerfile.' + arch, + repo: 'thegeeklab/${DRONE_REPO_NAME}', + username: { from_secret: 'docker_username' }, + password: { from_secret: 'docker_password' }, + }, + depends_on: ['build'], + when: { + ref: ['refs/pull/**'], + }, + }, + { + name: 'publish-dockerhub', + image: 'thegeeklab/drone-docker:19', + settings: { + auto_tag: true, + auto_tag_suffix: arch, + dockerfile: 'docker/Dockerfile.' + arch, + repo: 'thegeeklab/${DRONE_REPO_NAME}', + username: { from_secret: 'docker_username' }, + password: { from_secret: 'docker_password' }, + }, + when: { + ref: ['refs/heads/main', 'refs/tags/**'], + }, + depends_on: ['dryrun'], + }, + { + name: 'publish-quay', + image: 'thegeeklab/drone-docker:19', + settings: { + auto_tag: true, + auto_tag_suffix: arch, + dockerfile: 'docker/Dockerfile.' + arch, + registry: 'quay.io', + repo: 'quay.io/thegeeklab/${DRONE_REPO_NAME}', + username: { from_secret: 'quay_username' }, + password: { from_secret: 'quay_password' }, + }, + when: { + ref: ['refs/heads/main', 'refs/tags/**'], + }, + depends_on: ['dryrun'], + }, + ], + depends_on: [ + 'security', + ], + trigger: { + ref: ['refs/heads/main', 'refs/tags/**', 'refs/pull/**'], + }, +}; + +local PipelineDocs = { + kind: 'pipeline', + name: 'docs', + platform: { + os: 'linux', + arch: 'amd64', + }, + concurrency: { + limit: 1, + }, + steps: [ + { + name: 'assets', + image: 'thegeeklab/alpine-tools', + commands: [ + 'make doc', + ], + }, + { + name: 'markdownlint', + image: 'thegeeklab/markdownlint-cli', + commands: [ + "markdownlint 'docs/content/**/*.md' 'README.md' 'CONTRIBUTING.md'", + ], + }, + { + name: 'spellcheck', + image: 'node:lts-alpine', + commands: [ + 'npm install -g spellchecker-cli', + "spellchecker --files 'docs/content/**/*.md' 'README.md' -d .dictionary -p spell indefinite-article syntax-urls --no-suggestions", + ], + environment: { + FORCE_COLOR: true, + NPM_CONFIG_LOGLEVEL: 'error', + }, + }, + { + name: 'testbuild', + image: 'thegeeklab/hugo:0.83.1', + commands: [ + 'hugo -s docs/ -b http://localhost/', + ], + }, + { + name: 'link-validation', + image: 'thegeeklab/link-validator', + commands: [ + 'link-validator -ro', + ], + environment: { + LINK_VALIDATOR_BASE_DIR: 'docs/public', + }, + }, + { + name: 'build', + image: 'thegeeklab/hugo:0.83.1', + commands: [ + 'hugo -s docs/', + ], + }, + { + name: 'beautify', + image: 'node:lts-alpine', + commands: [ + 'npm install -g js-beautify', + "html-beautify -r -f 'docs/public/**/*.html'", + ], + environment: { + FORCE_COLOR: true, + NPM_CONFIG_LOGLEVEL: 'error', + }, + }, + { + name: 'publish', + image: 'plugins/s3-sync', + settings: { + access_key: { from_secret: 's3_access_key' }, + bucket: 'geekdocs', + delete: true, + endpoint: 'https://sp.rknet.org', + path_style: true, + secret_key: { from_secret: 's3_secret_access_key' }, + source: 'docs/public/', + strip_prefix: 'docs/public/', + target: '/${DRONE_REPO_NAME}', + }, + when: { + ref: ['refs/heads/main', 'refs/tags/**'], + }, + }, + ], + depends_on: [ + 'build-package', + 'build-container-amd64', + 'build-container-arm64', + 'build-container-arm', + ], + trigger: { + ref: ['refs/heads/main', 'refs/tags/**', 'refs/pull/**'], + }, +}; + +local PipelineNotifications = { + kind: 'pipeline', + name: 'notifications', + platform: { + os: 'linux', + arch: 'amd64', + }, + steps: [ + { + image: 'plugins/manifest', + name: 'manifest-dockerhub', + settings: { + ignore_missing: true, + auto_tag: true, + username: { from_secret: 'docker_username' }, + password: { from_secret: 'docker_password' }, + spec: 'docker/manifest.tmpl', + }, + when: { + status: ['success'], + }, + }, + { + image: 'plugins/manifest', + name: 'manifest-quay', + settings: { + ignore_missing: true, + auto_tag: true, + username: { from_secret: 'quay_username' }, + password: { from_secret: 'quay_password' }, + spec: 'docker/manifest-quay.tmpl', + }, + when: { + status: ['success'], + }, + }, + { + name: 'pushrm-dockerhub', + pull: 'always', + image: 'chko/docker-pushrm:1', + environment: { + DOCKER_PASS: { + from_secret: 'docker_password', + }, + DOCKER_USER: { + from_secret: 'docker_username', + }, + PUSHRM_FILE: 'README.md', + PUSHRM_SHORT: 'Prometheus Service Discovery for Proxmox VE', + PUSHRM_TARGET: 'thegeeklab/${DRONE_REPO_NAME}', + }, + when: { + status: ['success'], + }, + }, + { + name: 'pushrm-quay', + pull: 'always', + image: 'chko/docker-pushrm:1', + environment: { + APIKEY__QUAY_IO: { + from_secret: 'quay_token', + }, + PUSHRM_FILE: 'README.md', + PUSHRM_TARGET: 'quay.io/thegeeklab/${DRONE_REPO_NAME}', + }, + when: { + status: ['success'], + }, + }, + { + name: 'matrix', + image: 'plugins/matrix', + settings: { + homeserver: { from_secret: 'matrix_homeserver' }, + roomid: { from_secret: 'matrix_roomid' }, + template: 'Status: **{{ build.status }}**
Build: [{{ repo.Owner }}/{{ repo.Name }}]({{ build.link }}) ({{ build.branch }}) by {{ build.author }}
Message: {{ build.message }}', + username: { from_secret: 'matrix_username' }, + password: { from_secret: 'matrix_password' }, + }, + when: { + status: ['success', 'failure'], + }, + }, + ], + depends_on: [ + 'docs', + ], + trigger: { + ref: ['refs/heads/main', 'refs/tags/**'], + status: ['success', 'failure'], + }, +}; + +[ + PipelineLint, + PipelineTest, + PipelineSecurity, + PipelineBuildPackage, + PipelineBuildContainer(arch='amd64'), + PipelineBuildContainer(arch='arm64'), + PipelineBuildContainer(arch='arm'), + PipelineDocs, + PipelineNotifications, +] diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..81dfc22 --- /dev/null +++ b/.drone.yml @@ -0,0 +1,635 @@ +--- +kind: pipeline +name: lint + +platform: + os: linux + arch: amd64 + +steps: +- name: yapf + image: python:3.9 + commands: + - git fetch -tq + - pip install poetry poetry-dynamic-versioning -qq + - poetry config experimental.new-installer false + - poetry install + - poetry run yapf -dr ./prometheuspvesd + environment: + PY_COLORS: 1 + +- name: flake8 + image: python:3.9 + commands: + - git fetch -tq + - pip install poetry poetry-dynamic-versioning -qq + - poetry config experimental.new-installer false + - poetry install + - poetry run flake8 ./prometheuspvesd + environment: + PY_COLORS: 1 + +trigger: + ref: + - refs/heads/main + - refs/tags/** + - refs/pull/** + +--- +kind: pipeline +name: test + +platform: + os: linux + arch: amd64 + +steps: +- name: fetch + image: python:3.9 + commands: + - git fetch -tq + +- name: python36-pytest + image: python:3.6 + commands: + - pip install poetry poetry-dynamic-versioning -qq + - poetry config experimental.new-installer false + - poetry install + - poetry version + - poetry run prometheus-pve-sd --help + environment: + PY_COLORS: 1 + depends_on: + - fetch + +- name: python37-pytest + image: python:3.7 + commands: + - pip install poetry poetry-dynamic-versioning -qq + - poetry config experimental.new-installer false + - poetry install + - poetry version + - poetry run prometheus-pve-sd --help + environment: + PY_COLORS: 1 + depends_on: + - fetch + +- name: python38-pytest + image: python:3.8 + commands: + - pip install poetry poetry-dynamic-versioning -qq + - poetry config experimental.new-installer false + - poetry install + - poetry version + - poetry run prometheus-pve-sd --help + environment: + PY_COLORS: 1 + depends_on: + - fetch + +- name: python39-pytest + image: python:3.9 + commands: + - pip install poetry poetry-dynamic-versioning -qq + - poetry config experimental.new-installer false + - poetry install + - poetry version + - poetry run prometheus-pve-sd --help + environment: + PY_COLORS: 1 + depends_on: + - fetch + +trigger: + ref: + - refs/heads/main + - refs/tags/** + - refs/pull/** + +depends_on: +- lint + +--- +kind: pipeline +name: security + +platform: + os: linux + arch: amd64 + +steps: +- name: bandit + image: python:3.9 + commands: + - git fetch -tq + - pip install poetry poetry-dynamic-versioning -qq + - poetry config experimental.new-installer false + - poetry install + - poetry run bandit -r ./prometheuspvesd -x ./prometheuspvesd/test + environment: + PY_COLORS: 1 + +trigger: + ref: + - refs/heads/main + - refs/tags/** + - refs/pull/** + +depends_on: +- test + +--- +kind: pipeline +name: build-package + +platform: + os: linux + arch: amd64 + +steps: +- name: build + image: python:3.9 + commands: + - git fetch -tq + - pip install poetry poetry-dynamic-versioning -qq + - poetry build + +- name: checksum + image: alpine + commands: + - cd dist/ && sha256sum * > ../sha256sum.txt + +- name: changelog-generate + image: thegeeklab/git-chglog + commands: + - git fetch -tq + - git-chglog --no-color --no-emoji -o CHANGELOG.md ${DRONE_TAG:---next-tag unreleased unreleased} + +- name: changelog-format + image: thegeeklab/alpine-tools + commands: + - prettier CHANGELOG.md + - prettier -w CHANGELOG.md + +- name: publish-github + image: plugins/github-release + settings: + api_key: + from_secret: github_token + files: + - dist/* + - sha256sum.txt + note: CHANGELOG.md + overwrite: true + title: ${DRONE_TAG} + when: + ref: + - refs/tags/** + +- name: publish-pypi + image: python:3.9 + commands: + - git fetch -tq + - pip install poetry poetry-dynamic-versioning -qq + - poetry publish -n + environment: + POETRY_HTTP_BASIC_PYPI_PASSWORD: + from_secret: pypi_password + POETRY_HTTP_BASIC_PYPI_USERNAME: + from_secret: pypi_username + when: + ref: + - refs/tags/** + +trigger: + ref: + - refs/heads/main + - refs/tags/** + - refs/pull/** + +depends_on: +- security + +--- +kind: pipeline +name: build-container-amd64 + +platform: + os: linux + arch: amd64 + +steps: +- name: build + image: python:3.9 + commands: + - git fetch -tq + - pip install poetry poetry-dynamic-versioning -qq + - poetry build + +- name: dryrun + image: thegeeklab/drone-docker:19 + settings: + dockerfile: docker/Dockerfile.amd64 + dry_run: true + password: + from_secret: docker_password + repo: thegeeklab/${DRONE_REPO_NAME} + username: + from_secret: docker_username + when: + ref: + - refs/pull/** + depends_on: + - build + +- name: publish-dockerhub + image: thegeeklab/drone-docker:19 + settings: + auto_tag: true + auto_tag_suffix: amd64 + dockerfile: docker/Dockerfile.amd64 + password: + from_secret: docker_password + repo: thegeeklab/${DRONE_REPO_NAME} + username: + from_secret: docker_username + when: + ref: + - refs/heads/main + - refs/tags/** + depends_on: + - dryrun + +- name: publish-quay + image: thegeeklab/drone-docker:19 + settings: + auto_tag: true + auto_tag_suffix: amd64 + dockerfile: docker/Dockerfile.amd64 + password: + from_secret: quay_password + registry: quay.io + repo: quay.io/thegeeklab/${DRONE_REPO_NAME} + username: + from_secret: quay_username + when: + ref: + - refs/heads/main + - refs/tags/** + depends_on: + - dryrun + +trigger: + ref: + - refs/heads/main + - refs/tags/** + - refs/pull/** + +depends_on: +- security + +--- +kind: pipeline +name: build-container-arm64 + +platform: + os: linux + arch: arm64 + +steps: +- name: build + image: python:3.9 + commands: + - git fetch -tq + - pip install poetry poetry-dynamic-versioning -qq + - poetry build + +- name: dryrun + image: thegeeklab/drone-docker:19 + settings: + dockerfile: docker/Dockerfile.arm64 + dry_run: true + password: + from_secret: docker_password + repo: thegeeklab/${DRONE_REPO_NAME} + username: + from_secret: docker_username + when: + ref: + - refs/pull/** + depends_on: + - build + +- name: publish-dockerhub + image: thegeeklab/drone-docker:19 + settings: + auto_tag: true + auto_tag_suffix: arm64 + dockerfile: docker/Dockerfile.arm64 + password: + from_secret: docker_password + repo: thegeeklab/${DRONE_REPO_NAME} + username: + from_secret: docker_username + when: + ref: + - refs/heads/main + - refs/tags/** + depends_on: + - dryrun + +- name: publish-quay + image: thegeeklab/drone-docker:19 + settings: + auto_tag: true + auto_tag_suffix: arm64 + dockerfile: docker/Dockerfile.arm64 + password: + from_secret: quay_password + registry: quay.io + repo: quay.io/thegeeklab/${DRONE_REPO_NAME} + username: + from_secret: quay_username + when: + ref: + - refs/heads/main + - refs/tags/** + depends_on: + - dryrun + +trigger: + ref: + - refs/heads/main + - refs/tags/** + - refs/pull/** + +depends_on: +- security + +--- +kind: pipeline +name: build-container-arm + +platform: + os: linux + arch: arm + +steps: +- name: build + image: python:3.9-alpine + commands: + - apk add -Uq --no-cache build-base libressl-dev libffi-dev musl-dev python3-dev git cargo + - git fetch -tq + - pip install poetry poetry-dynamic-versioning -qq + - poetry build + environment: + CARGO_NET_GIT_FETCH_WITH_CLI: true + +- name: dryrun + image: thegeeklab/drone-docker:19 + settings: + dockerfile: docker/Dockerfile.arm + dry_run: true + password: + from_secret: docker_password + repo: thegeeklab/${DRONE_REPO_NAME} + username: + from_secret: docker_username + when: + ref: + - refs/pull/** + depends_on: + - build + +- name: publish-dockerhub + image: thegeeklab/drone-docker:19 + settings: + auto_tag: true + auto_tag_suffix: arm + dockerfile: docker/Dockerfile.arm + password: + from_secret: docker_password + repo: thegeeklab/${DRONE_REPO_NAME} + username: + from_secret: docker_username + when: + ref: + - refs/heads/main + - refs/tags/** + depends_on: + - dryrun + +- name: publish-quay + image: thegeeklab/drone-docker:19 + settings: + auto_tag: true + auto_tag_suffix: arm + dockerfile: docker/Dockerfile.arm + password: + from_secret: quay_password + registry: quay.io + repo: quay.io/thegeeklab/${DRONE_REPO_NAME} + username: + from_secret: quay_username + when: + ref: + - refs/heads/main + - refs/tags/** + depends_on: + - dryrun + +trigger: + ref: + - refs/heads/main + - refs/tags/** + - refs/pull/** + +depends_on: +- security + +--- +kind: pipeline +name: docs + +platform: + os: linux + arch: amd64 + +concurrency: + limit: 1 + +steps: +- name: assets + image: thegeeklab/alpine-tools + commands: + - make doc + +- name: markdownlint + image: thegeeklab/markdownlint-cli + commands: + - markdownlint 'docs/content/**/*.md' 'README.md' 'CONTRIBUTING.md' + +- name: spellcheck + image: node:lts-alpine + commands: + - npm install -g spellchecker-cli + - spellchecker --files 'docs/content/**/*.md' 'README.md' -d .dictionary -p spell indefinite-article syntax-urls --no-suggestions + environment: + FORCE_COLOR: true + NPM_CONFIG_LOGLEVEL: error + +- name: testbuild + image: thegeeklab/hugo:0.83.1 + commands: + - hugo -s docs/ -b http://localhost/ + +- name: link-validation + image: thegeeklab/link-validator + commands: + - link-validator -ro + environment: + LINK_VALIDATOR_BASE_DIR: docs/public + +- name: build + image: thegeeklab/hugo:0.83.1 + commands: + - hugo -s docs/ + +- name: beautify + image: node:lts-alpine + commands: + - npm install -g js-beautify + - html-beautify -r -f 'docs/public/**/*.html' + environment: + FORCE_COLOR: true + NPM_CONFIG_LOGLEVEL: error + +- name: publish + image: plugins/s3-sync + settings: + access_key: + from_secret: s3_access_key + bucket: geekdocs + delete: true + endpoint: https://sp.rknet.org + path_style: true + secret_key: + from_secret: s3_secret_access_key + source: docs/public/ + strip_prefix: docs/public/ + target: /${DRONE_REPO_NAME} + when: + ref: + - refs/heads/main + - refs/tags/** + +trigger: + ref: + - refs/heads/main + - refs/tags/** + - refs/pull/** + +depends_on: +- build-package +- build-container-amd64 +- build-container-arm64 +- build-container-arm + +--- +kind: pipeline +name: notifications + +platform: + os: linux + arch: amd64 + +steps: +- name: manifest-dockerhub + image: plugins/manifest + settings: + auto_tag: true + ignore_missing: true + password: + from_secret: docker_password + spec: docker/manifest.tmpl + username: + from_secret: docker_username + when: + status: + - success + +- name: manifest-quay + image: plugins/manifest + settings: + auto_tag: true + ignore_missing: true + password: + from_secret: quay_password + spec: docker/manifest-quay.tmpl + username: + from_secret: quay_username + when: + status: + - success + +- name: pushrm-dockerhub + pull: always + image: chko/docker-pushrm:1 + environment: + DOCKER_PASS: + from_secret: docker_password + DOCKER_USER: + from_secret: docker_username + PUSHRM_FILE: README.md + PUSHRM_SHORT: Prometheus Service Discovery for Proxmox VE + PUSHRM_TARGET: thegeeklab/${DRONE_REPO_NAME} + when: + status: + - success + +- name: pushrm-quay + pull: always + image: chko/docker-pushrm:1 + environment: + APIKEY__QUAY_IO: + from_secret: quay_token + PUSHRM_FILE: README.md + PUSHRM_TARGET: quay.io/thegeeklab/${DRONE_REPO_NAME} + when: + status: + - success + +- name: matrix + image: plugins/matrix + settings: + homeserver: + from_secret: matrix_homeserver + password: + from_secret: matrix_password + roomid: + from_secret: matrix_roomid + template: "Status: **{{ build.status }}**
Build: [{{ repo.Owner }}/{{ repo.Name }}]({{ build.link }}) ({{ build.branch }}) by {{ build.author }}
Message: {{ build.message }}" + username: + from_secret: matrix_username + when: + status: + - success + - failure + +trigger: + ref: + - refs/heads/main + - refs/tags/** + status: + - success + - failure + +depends_on: +- docs + +--- +kind: signature +hmac: 791f2a668d1295b69cb7000c3a38b8e3ca15d99b1c8ac40559c71aa9456fc018 + +... diff --git a/.github/settings.yml b/.github/settings.yml new file mode 100644 index 0000000..5af2e62 --- /dev/null +++ b/.github/settings.yml @@ -0,0 +1,56 @@ +repository: + name: prometheus-pve-sd + description: Prometheus Service Discovery for Proxmox VE + topics: prometheus, proxmox, metrics, sd, python + + private: false + has_issues: true + has_projects: false + has_wiki: false + has_downloads: true + + default_branch: main + + allow_squash_merge: true + allow_merge_commit: true + allow_rebase_merge: true + +labels: + - name: bug + color: d73a4a + description: Something isn't working + - name: documentation + color: 0075ca + description: Improvements or additions to documentation + - name: duplicate + color: cfd3d7 + description: This issue or pull request already exists + - name: enhancement + color: a2eeef + description: New feature or request + - name: good first issue + color: 7057ff + description: Good for newcomers + - name: help wanted + color: 008672 + description: Extra attention is needed + - name: invalid + color: e4e669 + description: This doesn't seem right + - name: question + color: d876e3 + description: Further information is requested + - name: wontfix + color: ffffff + description: This will not be worked on + +branches: + - name: main + protection: + required_pull_request_reviews: null + required_status_checks: + strict: false + contexts: + - continuous-integration/drone/pr + enforce_admins: null + restrictions: null diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ddd7fcf --- /dev/null +++ b/.gitignore @@ -0,0 +1,111 @@ +# ---> Python +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# dotenv +.env + +# virtualenv +.venv +venv/ +ENV/ +env/ +env*/ + +# Spyder project settings +.spyderproject + +# Rope project settings +.ropeproject + +# Ignore ide addons +.server-script +.on-save.json +.vscode +.pytest_cache + +pip-wheel-metadata + +# Hugo documentation +docs/themes/ +docs/public/ +resources/_gen/ + +# Misc +CHANGELOG.md diff --git a/.markdownlint.yml b/.markdownlint.yml new file mode 100644 index 0000000..b59a114 --- /dev/null +++ b/.markdownlint.yml @@ -0,0 +1,6 @@ +--- +default: True +MD013: False +MD041: False +MD004: + style: dash diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..97e0b3e --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +.drone.yml +*.tpl.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..c471f59 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,31 @@ +# Contributing + +## Security + +If you think you have found a **security issue**, please do not mention it in this repository. +Instead, send an email to security@thegeeklab.de with as many details as possible so it can be handled confidential. + +## Bug Reports and Feature Requests + +If you have found a **bug** or have a **feature request** please use the search first in case a similar issue already exists. +If not, please create an issue in this repository + +## Code + +If you would like to fix a bug or implement a feature, please fork the repository and create a Pull Request. + +Before you start any Pull Request, it is recommended that you create an issue to discuss first if you have any +doubts about requirement or implementation. That way you can be sure that the maintainer(s) agree on what to change and how, +and you can hopefully get a quick merge afterwards. + +Pull Requests can only be merged once all status checks are green. + +## Do not force push to your Pull Request branch + +Please do not force push to your Pull Requests branch after you have created your Pull Request, as doing so makes it harder for us to review your work. +Pull Requests will always be squashed by us when we merge your work. Commit as many times as you need in your Pull Request branch. + +## Re-requesting a review + +Please do not ping your reviewer(s) by mentioning them in a new comment. Instead, use the re-request review functionality. +Read more about this in the [GitHub docs, Re-requesting a review](https://docs.github.com/en/free-pro-team@latest/github/collaborating-with-issues-and-pull-requests/incorporating-feedback-in-your-pull-request#re-requesting-a-review). diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8e54586 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Robert Kaussow + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice (including the next +paragraph) shall be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF +OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..df2d36d --- /dev/null +++ b/Makefile @@ -0,0 +1,20 @@ +# renovate: datasource=github-releases depName=thegeeklab/hugo-geekdoc +THEME_VERSION := v0.13.4 +THEME := hugo-geekdoc +BASEDIR := docs +THEMEDIR := $(BASEDIR)/themes + +.PHONY: all +all: doc + +.PHONY: doc +doc: doc-assets + +.PHONY: doc-assets +doc-assets: + mkdir -p $(THEMEDIR)/$(THEME)/ ; \ + curl -sSL "https://github.com/thegeeklab/$(THEME)/releases/download/${THEME_VERSION}/$(THEME).tar.gz" | tar -xz -C $(THEMEDIR)/$(THEME)/ --strip-components=1 + +.PHONY: clean +clean: + rm -rf $(THEMEDIR) && \ diff --git a/README.md b/README.md new file mode 100644 index 0000000..e5a18db --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# prometheus-pve-sd + +Prometheus Service Discovery for Proxmox VE + +[![Build Status](https://img.shields.io/drone/build/thegeeklab/prometheus-pve-sd?logo=drone&server=https%3A%2F%2Fdrone.thegeeklab.de)](https://drone.thegeeklab.de/thegeeklab/prometheus-pve-sd) +[![Docker Hub](https://img.shields.io/badge/dockerhub-latest-blue.svg?logo=docker&logoColor=white)](https://hub.docker.com/r/thegeeklab/prometheus-pve-sd) +[![Quay.io](https://img.shields.io/badge/quay-latest-blue.svg?logo=docker&logoColor=white)](https://quay.io/repository/thegeeklab/prometheus-pve-sd) +[![Python Version](https://img.shields.io/pypi/pyversions/prometheus-pve-sd.svg)](https://pypi.org/project/prometheus-pve-sd/) +[![PyPI Status](https://img.shields.io/pypi/status/prometheus-pve-sd.svg)](https://pypi.org/project/prometheus-pve-sd/) +[![PyPI Release](https://img.shields.io/pypi/v/prometheus-pve-sd.svg)](https://pypi.org/project/prometheus-pve-sd/) +[![GitHub contributors](https://img.shields.io/github/contributors/thegeeklab/prometheus-pve-sd)](https://github.com/thegeeklab/prometheus-pve-sd/graphs/contributors) +[![Source: GitHub](https://img.shields.io/badge/source-github-blue.svg?logo=github&logoColor=white)](https://github.com/thegeeklab/prometheus-pve-sd) +[![License: MIT](https://img.shields.io/github/license/thegeeklab/prometheus-pve-sd)](https://github.com/thegeeklab/prometheus-pve-sd/blob/main/LICENSE) + +TBD + +## Contributors + +Special thanks goes to all [contributors](https://github.com/thegeeklab/prometheus-pve-sd/graphs/contributors). If you would like to contribute, +please see the [instructions](https://github.com/thegeeklab/prometheus-pve-sd/blob/main/CONTRIBUTING.md). + +## License + +This project is licensed under the MIT License - see the [LICENSE](https://github.com/thegeeklab/prometheus-pve-sd/blob/main/LICENSE) file for details. diff --git a/docker/Dockerfile.amd64 b/docker/Dockerfile.amd64 new file mode 100644 index 0000000..603ab49 --- /dev/null +++ b/docker/Dockerfile.amd64 @@ -0,0 +1,23 @@ +FROM amd64/python:3.9-alpine@sha256:d2dfb8f0a8b3ab3e2899bba05e53c2b16bc1b8c1fca83637266edb8d1a57dc86 + +LABEL maintainer="Robert Kaussow " +LABEL org.opencontainers.image.authors="Robert Kaussow " +LABEL org.opencontainers.image.title="prometheus-pve-sd" +LABEL org.opencontainers.image.url="https://github.com/thegeeklab/prometheus-pve-sd/" +LABEL org.opencontainers.image.source="https://github.com/thegeeklab/prometheus-pve-sd/" +LABEL org.opencontainers.image.documentation="https://github.com/thegeeklab/prometheus-pve-sd/" + +ENV PY_COLORS=1 + +ADD dist/prometheus_pve_sd--*.whl / + +RUN apk update && \ + pip install --upgrade --no-cache-dir pip && \ + pip install --no-cache-dir $(find / -name "prometheus_pve_sd--*.whl") && \ + rm -f prometheus_pve_sd--*.whl && \ + rm -rf /var/cache/apk/* && \ + rm -rf /root/.cache/ + +USER root +CMD [] +ENTRYPOINT ["/usr/local/bin/prometheus-pve-sd"] diff --git a/docker/Dockerfile.arm b/docker/Dockerfile.arm new file mode 100644 index 0000000..f6279c4 --- /dev/null +++ b/docker/Dockerfile.arm @@ -0,0 +1,23 @@ +FROM arm32v7/python:3.9-alpine@sha256:183252dde15e315fbe570a5355be839251aa4878b20163c767dd5238de3c988e + +LABEL maintainer="Robert Kaussow " +LABEL org.opencontainers.image.authors="Robert Kaussow " +LABEL org.opencontainers.image.title="prometheus-pve-sd" +LABEL org.opencontainers.image.url="https://github.com/thegeeklab/prometheus-pve-sd/" +LABEL org.opencontainers.image.source="https://github.com/thegeeklab/prometheus-pve-sd/" +LABEL org.opencontainers.image.documentation="https://github.com/thegeeklab/prometheus-pve-sd/" + +ENV PY_COLORS=1 + +ADD dist/prometheus_pve_sd--*.whl / + +RUN apk update && \ + pip install --upgrade --no-cache-dir pip && \ + pip install --no-cache-dir $(find / -name "prometheus_pve_sd--*.whl") && \ + rm -f prometheus_pve_sd--*.whl && \ + rm -rf /var/cache/apk/* && \ + rm -rf /root/.cache/ + +USER root +CMD [] +ENTRYPOINT ["/usr/local/bin/prometheus-pve-sd"] diff --git a/docker/Dockerfile.arm64 b/docker/Dockerfile.arm64 new file mode 100644 index 0000000..ed06e14 --- /dev/null +++ b/docker/Dockerfile.arm64 @@ -0,0 +1,23 @@ +FROM arm64v8/python:3.9-alpine@sha256:2dd55073bf996f367008a7fad490add0343b5c151b708dcf52c133f70ab171a9 + +LABEL maintainer="Robert Kaussow " +LABEL org.opencontainers.image.authors="Robert Kaussow " +LABEL org.opencontainers.image.title="prometheus-pve-sd" +LABEL org.opencontainers.image.url="https://github.com/thegeeklab/prometheus-pve-sd/" +LABEL org.opencontainers.image.source="https://github.com/thegeeklab/prometheus-pve-sd/" +LABEL org.opencontainers.image.documentation="https://github.com/thegeeklab/prometheus-pve-sd/" + +ENV PY_COLORS=1 + +ADD dist/prometheus_pve_sd--*.whl / + +RUN apk update && \ + pip install --upgrade --no-cache-dir pip && \ + pip install --no-cache-dir $(find / -name "prometheus_pve_sd--*.whl") && \ + rm -f prometheus_pve_sd--*.whl && \ + rm -rf /var/cache/apk/* && \ + rm -rf /root/.cache/ + +USER root +CMD [] +ENTRYPOINT ["/usr/local/bin/prometheus-pve-sd"] diff --git a/docker/manifest-quay.tmpl b/docker/manifest-quay.tmpl new file mode 100644 index 0000000..846ae1b --- /dev/null +++ b/docker/manifest-quay.tmpl @@ -0,0 +1,24 @@ +image: quay.io/thegeeklab/prometheus-pve-sd:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}} +{{#if build.tags}} +tags: +{{#each build.tags}} + - {{this}} +{{/each}} +{{/if}} +manifests: + - image: quay.io/thegeeklab/prometheus-pve-sd:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}amd64 + platform: + architecture: amd64 + os: linux + + - image: quay.io/thegeeklab/prometheus-pve-sd:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}arm64 + platform: + architecture: arm64 + os: linux + variant: v8 + + - image: quay.io/thegeeklab/prometheus-pve-sd:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}arm + platform: + architecture: arm + os: linux + variant: v7 diff --git a/docker/manifest.tmpl b/docker/manifest.tmpl new file mode 100644 index 0000000..6714b4a --- /dev/null +++ b/docker/manifest.tmpl @@ -0,0 +1,24 @@ +image: thegeeklab/prometheus-pve-sd:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}} +{{#if build.tags}} +tags: +{{#each build.tags}} + - {{this}} +{{/each}} +{{/if}} +manifests: + - image: thegeeklab/prometheus-pve-sd:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}amd64 + platform: + architecture: amd64 + os: linux + + - image: thegeeklab/prometheus-pve-sd:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}arm64 + platform: + architecture: arm64 + os: linux + variant: v8 + + - image: thegeeklab/prometheus-pve-sd:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}arm + platform: + architecture: arm + os: linux + variant: v7 diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..71f87ab --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1022 @@ +[[package]] +name = "anyconfig" +version = "0.11.0" +description = "Library provides common APIs to load and dump configuration files in various formats" +category = "main" +optional = false +python-versions = "*" + +[package.extras] +devel = ["coveralls", "flake8 (<3.5.0)", "mock", "nose", "pylint", "pycodestyle (<2.4.0)"] +query = ["jmespath"] +schema = ["jsonschema"] +template = ["jinja2"] +toml = ["toml"] +yaml = ["pyyaml"] + +[[package]] +name = "appdirs" +version = "1.4.4" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "atomicwrites" +version = "1.4.0" +description = "Atomic file writes." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "attrs" +version = "21.2.0" +description = "Classes Without Boilerplate" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.extras] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"] +docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] + +[[package]] +name = "bandit" +version = "1.7.0" +description = "Security oriented static analyser for python code." +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""} +GitPython = ">=1.0.1" +PyYAML = ">=5.3.1" +six = ">=1.10.0" +stevedore = ">=1.20.0" + +[[package]] +name = "certifi" +version = "2021.5.30" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "chardet" +version = "4.0.0" +description = "Universal encoding detector for Python 2 and 3" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "colorama" +version = "0.4.4" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "coverage" +version = "5.5" +description = "Code coverage measurement for Python" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.dependencies] +toml = {version = "*", optional = true, markers = "extra == \"toml\""} + +[package.extras] +toml = ["toml"] + +[[package]] +name = "environs" +version = "9.3.2" +description = "simplified environment variable parsing" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +marshmallow = ">=2.7.0" +python-dotenv = "*" + +[package.extras] +dev = ["pytest", "dj-database-url", "dj-email-url", "django-cache-url", "flake8 (==3.9.0)", "flake8-bugbear (==21.3.2)", "mypy (==0.812)", "pre-commit (>=2.4,<3.0)", "tox"] +django = ["dj-database-url", "dj-email-url", "django-cache-url"] +lint = ["flake8 (==3.9.0)", "flake8-bugbear (==21.3.2)", "mypy (==0.812)", "pre-commit (>=2.4,<3.0)"] +tests = ["pytest", "dj-database-url", "dj-email-url", "django-cache-url"] + +[[package]] +name = "eradicate" +version = "2.0.0" +description = "Removes commented-out code." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "flake8" +version = "3.9.2" +description = "the modular source code checker: pep8 pyflakes and co" +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[package.dependencies] +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} +mccabe = ">=0.6.0,<0.7.0" +pycodestyle = ">=2.7.0,<2.8.0" +pyflakes = ">=2.3.0,<2.4.0" + +[[package]] +name = "flake8-blind-except" +version = "0.2.0" +description = "A flake8 extension that checks for blind except: statements" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "flake8-builtins" +version = "1.5.3" +description = "Check for python builtins being used as variables or parameters." +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +flake8 = "*" + +[package.extras] +test = ["coverage", "coveralls", "mock", "pytest", "pytest-cov"] + +[[package]] +name = "flake8-docstrings" +version = "1.6.0" +description = "Extension for flake8 which uses pydocstyle to check docstrings" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +flake8 = ">=3" +pydocstyle = ">=2.1" + +[[package]] +name = "flake8-eradicate" +version = "1.0.0" +description = "Flake8 plugin to find commented out code" +category = "dev" +optional = false +python-versions = ">=3.6,<4.0" + +[package.dependencies] +attrs = "*" +eradicate = ">=2.0,<3.0" +flake8 = ">=3.5,<4.0" + +[[package]] +name = "flake8-isort" +version = "4.0.0" +description = "flake8 plugin that integrates isort ." +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +flake8 = ">=3.2.1,<4" +isort = ">=4.3.5,<6" +testfixtures = ">=6.8.0,<7" + +[package.extras] +test = ["pytest (>=4.0.2,<6)", "toml"] + +[[package]] +name = "flake8-logging-format" +version = "0.6.0" +description = "Flake8 extension to validate (lack of) logging format strings" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "flake8-pep3101" +version = "1.3.0" +description = "Checks for old string formatting." +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +flake8 = ">=3.0" + +[package.extras] +test = ["pytest", "testfixtures"] + +[[package]] +name = "flake8-polyfill" +version = "1.0.2" +description = "Polyfill package for Flake8 plugins" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +flake8 = "*" + +[[package]] +name = "flake8-quotes" +version = "3.2.0" +description = "Flake8 lint for quotes." +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +flake8 = "*" + +[[package]] +name = "gitdb" +version = "4.0.7" +description = "Git Object Database" +category = "dev" +optional = false +python-versions = ">=3.4" + +[package.dependencies] +smmap = ">=3.0.1,<5" + +[[package]] +name = "gitpython" +version = "3.1.17" +description = "Python Git Library" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +gitdb = ">=4.0.1,<5" +typing-extensions = {version = ">=3.7.4.0", markers = "python_version < \"3.8\""} + +[[package]] +name = "idna" +version = "2.10" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "importlib-metadata" +version = "4.5.0" +description = "Read metadata from Python packages" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} +zipp = ">=0.5" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] + +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "isort" +version = "5.8.0" +description = "A Python utility / library to sort Python imports." +category = "dev" +optional = false +python-versions = ">=3.6,<4.0" + +[package.extras] +pipfile_deprecated_finder = ["pipreqs", "requirementslib"] +requirements_deprecated_finder = ["pipreqs", "pip-api"] +colors = ["colorama (>=0.4.3,<0.5.0)"] + +[[package]] +name = "jsonschema" +version = "3.2.0" +description = "An implementation of JSON Schema validation for Python" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +attrs = ">=17.4.0" +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} +pyrsistent = ">=0.14.0" +six = ">=1.11.0" + +[package.extras] +format = ["idna", "jsonpointer (>1.13)", "rfc3987", "strict-rfc3339", "webcolors"] +format_nongpl = ["idna", "jsonpointer (>1.13)", "webcolors", "rfc3986-validator (>0.1.0)", "rfc3339-validator"] + +[[package]] +name = "marshmallow" +version = "3.12.1" +description = "A lightweight library for converting complex datatypes to and from native Python datatypes." +category = "main" +optional = false +python-versions = ">=3.5" + +[package.extras] +dev = ["pytest", "pytz", "simplejson", "mypy (==0.812)", "flake8 (==3.9.2)", "flake8-bugbear (==21.4.3)", "pre-commit (>=2.4,<3.0)", "tox"] +docs = ["sphinx (==4.0.0)", "sphinx-issues (==1.2.0)", "alabaster (==0.7.12)", "sphinx-version-warning (==1.1.2)", "autodocsumm (==0.2.4)"] +lint = ["mypy (==0.812)", "flake8 (==3.9.2)", "flake8-bugbear (==21.4.3)", "pre-commit (>=2.4,<3.0)"] +tests = ["pytest", "pytz", "simplejson"] + +[[package]] +name = "mccabe" +version = "0.6.1" +description = "McCabe checker, plugin for flake8" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "nested-lookup" +version = "0.2.22" +description = "Python functions for working with deeply nested documents (lists and dicts)" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +six = "*" + +[[package]] +name = "packaging" +version = "20.9" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +pyparsing = ">=2.0.2" + +[[package]] +name = "pbr" +version = "5.6.0" +description = "Python Build Reasonableness" +category = "dev" +optional = false +python-versions = ">=2.6" + +[[package]] +name = "pep8-naming" +version = "0.11.1" +description = "Check PEP-8 naming conventions, plugin for flake8" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +flake8-polyfill = ">=1.0.2,<2" + +[[package]] +name = "pluggy" +version = "0.13.1" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} + +[package.extras] +dev = ["pre-commit", "tox"] + +[[package]] +name = "proxmoxer" +version = "1.1.1" +description = "Python Wrapper for the Proxmox 2.x API (HTTP and SSH)" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "py" +version = "1.10.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pycodestyle" +version = "2.7.0" +description = "Python style guide checker" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pydocstyle" +version = "6.1.1" +description = "Python docstring style checker" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +snowballstemmer = "*" + +[package.extras] +toml = ["toml"] + +[[package]] +name = "pyflakes" +version = "2.3.1" +description = "passive checker of Python programs" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pyparsing" +version = "2.4.7" +description = "Python parsing module" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "pyrsistent" +version = "0.17.3" +description = "Persistent/Functional/Immutable data structures" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "pytest" +version = "6.2.4" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<1.0.0a1" +py = ">=1.8.2" +toml = "*" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "2.12.0" +description = "Pytest plugin for measuring coverage." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests (==2.0.2)", "six", "pytest-xdist", "virtualenv"] + +[[package]] +name = "pytest-mock" +version = "3.6.1" +description = "Thin-wrapper around the mock package for easier use with pytest" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pytest = ">=5.0" + +[package.extras] +dev = ["pre-commit", "tox", "pytest-asyncio"] + +[[package]] +name = "python-dotenv" +version = "0.17.1" +description = "Read key-value pairs from a .env file and set them as environment variables" +category = "main" +optional = false +python-versions = "*" + +[package.extras] +cli = ["click (>=5.0)"] + +[[package]] +name = "python-json-logger" +version = "2.0.1" +description = "A python library adding a json log formatter" +category = "main" +optional = false +python-versions = ">=3.4" + +[[package]] +name = "pyyaml" +version = "5.4.1" +description = "YAML parser and emitter for Python" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" + +[[package]] +name = "requests" +version = "2.25.1" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +certifi = ">=2017.4.17" +chardet = ">=3.0.2,<5" +idna = ">=2.5,<3" +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] +socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] + +[[package]] +name = "ruamel.yaml" +version = "0.17.6" +description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" +category = "main" +optional = false +python-versions = ">=3" + +[package.dependencies] +"ruamel.yaml.clib" = {version = ">=0.1.2", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.10\""} + +[package.extras] +docs = ["ryd"] +jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] + +[[package]] +name = "ruamel.yaml.clib" +version = "0.2.2" +description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "smmap" +version = "4.0.0" +description = "A pure Python implementation of a sliding window memory map manager" +category = "dev" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "snowballstemmer" +version = "2.1.0" +description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "stevedore" +version = "3.3.0" +description = "Manage dynamic plugins for Python applications" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +importlib-metadata = {version = ">=1.7.0", markers = "python_version < \"3.8\""} +pbr = ">=2.0.0,<2.1.0 || >2.1.0" + +[[package]] +name = "testfixtures" +version = "6.17.1" +description = "A collection of helpers and mock objects for unit tests and doc tests." +category = "dev" +optional = false +python-versions = "*" + +[package.extras] +build = ["setuptools-git", "wheel", "twine"] +docs = ["sphinx", "zope.component", "sybil", "twisted", "mock", "django (<2)", "django"] +test = ["pytest (>=3.6)", "pytest-cov", "pytest-django", "zope.component", "sybil", "twisted", "mock", "django (<2)", "django"] + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "typing-extensions" +version = "3.10.0.0" +description = "Backported and Experimental Type Hints for Python 3.5+" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "urllib3" +version = "1.26.5" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.extras] +brotli = ["brotlipy (>=0.6.0)"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[[package]] +name = "yapf" +version = "0.31.0" +description = "A formatter for Python code." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "zipp" +version = "3.4.1" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] + +[metadata] +lock-version = "1.1" +python-versions = "^3.6.0" +content-hash = "0470bcbd365b8926e2cfbc88c005f4c780ca192e5ee0d061d4fefe7bcbb03a8f" + +[metadata.files] +anyconfig = [ + {file = "anyconfig-0.11.0-py2.py3-none-any.whl", hash = "sha256:47507d96ff31059a9c5783d3e3a5def88c4069bd4af56e9828ee0f0cca2ed2d2"}, + {file = "anyconfig-0.11.0.tar.gz", hash = "sha256:ec4eaad7250af23c98c86760954781361906b83027ae5b9d4da539e09765d308"}, +] +appdirs = [ + {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, + {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, +] +atomicwrites = [ + {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, + {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, +] +attrs = [ + {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, + {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, +] +bandit = [ + {file = "bandit-1.7.0-py3-none-any.whl", hash = "sha256:216be4d044209fa06cf2a3e51b319769a51be8318140659719aa7a115c35ed07"}, + {file = "bandit-1.7.0.tar.gz", hash = "sha256:8a4c7415254d75df8ff3c3b15cfe9042ecee628a1e40b44c15a98890fbfc2608"}, +] +certifi = [ + {file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"}, + {file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"}, +] +chardet = [ + {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, + {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, +] +colorama = [ + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, +] +coverage = [ + {file = "coverage-5.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf"}, + {file = "coverage-5.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b"}, + {file = "coverage-5.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669"}, + {file = "coverage-5.5-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90"}, + {file = "coverage-5.5-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c"}, + {file = "coverage-5.5-cp27-cp27m-win32.whl", hash = "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a"}, + {file = "coverage-5.5-cp27-cp27m-win_amd64.whl", hash = "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81"}, + {file = "coverage-5.5-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6"}, + {file = "coverage-5.5-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0"}, + {file = "coverage-5.5-cp310-cp310-win_amd64.whl", hash = "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae"}, + {file = "coverage-5.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb"}, + {file = "coverage-5.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160"}, + {file = "coverage-5.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6"}, + {file = "coverage-5.5-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701"}, + {file = "coverage-5.5-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793"}, + {file = "coverage-5.5-cp35-cp35m-win32.whl", hash = "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e"}, + {file = "coverage-5.5-cp35-cp35m-win_amd64.whl", hash = "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3"}, + {file = "coverage-5.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066"}, + {file = "coverage-5.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a"}, + {file = "coverage-5.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465"}, + {file = "coverage-5.5-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb"}, + {file = "coverage-5.5-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821"}, + {file = "coverage-5.5-cp36-cp36m-win32.whl", hash = "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45"}, + {file = "coverage-5.5-cp36-cp36m-win_amd64.whl", hash = "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184"}, + {file = "coverage-5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a"}, + {file = "coverage-5.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53"}, + {file = "coverage-5.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d"}, + {file = "coverage-5.5-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638"}, + {file = "coverage-5.5-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3"}, + {file = "coverage-5.5-cp37-cp37m-win32.whl", hash = "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a"}, + {file = "coverage-5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a"}, + {file = "coverage-5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6"}, + {file = "coverage-5.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2"}, + {file = "coverage-5.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759"}, + {file = "coverage-5.5-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873"}, + {file = "coverage-5.5-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a"}, + {file = "coverage-5.5-cp38-cp38-win32.whl", hash = "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6"}, + {file = "coverage-5.5-cp38-cp38-win_amd64.whl", hash = "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502"}, + {file = "coverage-5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b"}, + {file = "coverage-5.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529"}, + {file = "coverage-5.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b"}, + {file = "coverage-5.5-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff"}, + {file = "coverage-5.5-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b"}, + {file = "coverage-5.5-cp39-cp39-win32.whl", hash = "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6"}, + {file = "coverage-5.5-cp39-cp39-win_amd64.whl", hash = "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03"}, + {file = "coverage-5.5-pp36-none-any.whl", hash = "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079"}, + {file = "coverage-5.5-pp37-none-any.whl", hash = "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4"}, + {file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"}, +] +environs = [ + {file = "environs-9.3.2-py2.py3-none-any.whl", hash = "sha256:6bef733b88cc901e787cf24fb2eaa72621b0656226ea4e332ab24ed0cba36fcf"}, + {file = "environs-9.3.2.tar.gz", hash = "sha256:2eb671afd37e6e9820131b918bbbcaa6658d0fb420ebf35bdfb750ae39c51a66"}, +] +eradicate = [ + {file = "eradicate-2.0.0.tar.gz", hash = "sha256:27434596f2c5314cc9b31410c93d8f7e8885747399773cd088d3adea647a60c8"}, +] +flake8 = [ + {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, + {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, +] +flake8-blind-except = [ + {file = "flake8-blind-except-0.2.0.tar.gz", hash = "sha256:02a860a1a19cb602c006a3fe0778035b0d14d3f57929b4b798bc7d6684f204e5"}, +] +flake8-builtins = [ + {file = "flake8-builtins-1.5.3.tar.gz", hash = "sha256:09998853b2405e98e61d2ff3027c47033adbdc17f9fe44ca58443d876eb00f3b"}, + {file = "flake8_builtins-1.5.3-py2.py3-none-any.whl", hash = "sha256:7706babee43879320376861897e5d1468e396a40b8918ed7bccf70e5f90b8687"}, +] +flake8-docstrings = [ + {file = "flake8-docstrings-1.6.0.tar.gz", hash = "sha256:9fe7c6a306064af8e62a055c2f61e9eb1da55f84bb39caef2b84ce53708ac34b"}, + {file = "flake8_docstrings-1.6.0-py2.py3-none-any.whl", hash = "sha256:99cac583d6c7e32dd28bbfbef120a7c0d1b6dde4adb5a9fd441c4227a6534bde"}, +] +flake8-eradicate = [ + {file = "flake8-eradicate-1.0.0.tar.gz", hash = "sha256:fe7167226676823d50cf540532302a6f576c5a398c5260692571a05ef72c5f5b"}, + {file = "flake8_eradicate-1.0.0-py3-none-any.whl", hash = "sha256:0fc4ab858a18c7ed630621b5345254c8f55be6060ea5c44a25e384d613618d1f"}, +] +flake8-isort = [ + {file = "flake8-isort-4.0.0.tar.gz", hash = "sha256:2b91300f4f1926b396c2c90185844eb1a3d5ec39ea6138832d119da0a208f4d9"}, + {file = "flake8_isort-4.0.0-py2.py3-none-any.whl", hash = "sha256:729cd6ef9ba3659512dee337687c05d79c78e1215fdf921ed67e5fe46cce2f3c"}, +] +flake8-logging-format = [ + {file = "flake8-logging-format-0.6.0.tar.gz", hash = "sha256:ca5f2b7fc31c3474a0aa77d227e022890f641a025f0ba664418797d979a779f8"}, +] +flake8-pep3101 = [ + {file = "flake8-pep3101-1.3.0.tar.gz", hash = "sha256:86e3eb4e42de8326dcd98ebdeaf9a3c6854203a48f34aeb3e7e8ed948107f512"}, + {file = "flake8_pep3101-1.3.0-py2.py3-none-any.whl", hash = "sha256:a5dae1caca1243b2b40108dce926d97cf5a9f52515c4a4cbb1ffe1ca0c54e343"}, +] +flake8-polyfill = [ + {file = "flake8-polyfill-1.0.2.tar.gz", hash = "sha256:e44b087597f6da52ec6393a709e7108b2905317d0c0b744cdca6208e670d8eda"}, + {file = "flake8_polyfill-1.0.2-py2.py3-none-any.whl", hash = "sha256:12be6a34ee3ab795b19ca73505e7b55826d5f6ad7230d31b18e106400169b9e9"}, +] +flake8-quotes = [ + {file = "flake8-quotes-3.2.0.tar.gz", hash = "sha256:3f1116e985ef437c130431ac92f9b3155f8f652fda7405ac22ffdfd7a9d1055e"}, +] +gitdb = [ + {file = "gitdb-4.0.7-py3-none-any.whl", hash = "sha256:6c4cc71933456991da20917998acbe6cf4fb41eeaab7d6d67fbc05ecd4c865b0"}, + {file = "gitdb-4.0.7.tar.gz", hash = "sha256:96bf5c08b157a666fec41129e6d327235284cca4c81e92109260f353ba138005"}, +] +gitpython = [ + {file = "GitPython-3.1.17-py3-none-any.whl", hash = "sha256:29fe82050709760081f588dd50ce83504feddbebdc4da6956d02351552b1c135"}, + {file = "GitPython-3.1.17.tar.gz", hash = "sha256:ee24bdc93dce357630764db659edaf6b8d664d4ff5447ccfeedd2dc5c253f41e"}, +] +idna = [ + {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, + {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, +] +importlib-metadata = [ + {file = "importlib_metadata-4.5.0-py3-none-any.whl", hash = "sha256:833b26fb89d5de469b24a390e9df088d4e52e4ba33b01dc5e0e4f41b81a16c00"}, + {file = "importlib_metadata-4.5.0.tar.gz", hash = "sha256:b142cc1dd1342f31ff04bb7d022492b09920cb64fed867cd3ea6f80fe3ebd139"}, +] +iniconfig = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] +isort = [ + {file = "isort-5.8.0-py3-none-any.whl", hash = "sha256:2bb1680aad211e3c9944dbce1d4ba09a989f04e238296c87fe2139faa26d655d"}, + {file = "isort-5.8.0.tar.gz", hash = "sha256:0a943902919f65c5684ac4e0154b1ad4fac6dcaa5d9f3426b732f1c8b5419be6"}, +] +jsonschema = [ + {file = "jsonschema-3.2.0-py2.py3-none-any.whl", hash = "sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163"}, + {file = "jsonschema-3.2.0.tar.gz", hash = "sha256:c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a"}, +] +marshmallow = [ + {file = "marshmallow-3.12.1-py2.py3-none-any.whl", hash = "sha256:b45cde981d1835145257b4a3c5cb7b80786dcf5f50dd2990749a50c16cb48e01"}, + {file = "marshmallow-3.12.1.tar.gz", hash = "sha256:8050475b70470cc58f4441ee92375db611792ba39ca1ad41d39cad193ea9e040"}, +] +mccabe = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] +nested-lookup = [ + {file = "nested-lookup-0.2.22.tar.gz", hash = "sha256:e39adacd11e879dd6383b0a832cde97edce25d2a29cbc03a9c80fefa3a131e8b"}, +] +packaging = [ + {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"}, + {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"}, +] +pbr = [ + {file = "pbr-5.6.0-py2.py3-none-any.whl", hash = "sha256:c68c661ac5cc81058ac94247278eeda6d2e6aecb3e227b0387c30d277e7ef8d4"}, + {file = "pbr-5.6.0.tar.gz", hash = "sha256:42df03e7797b796625b1029c0400279c7c34fd7df24a7d7818a1abb5b38710dd"}, +] +pep8-naming = [ + {file = "pep8-naming-0.11.1.tar.gz", hash = "sha256:a1dd47dd243adfe8a83616e27cf03164960b507530f155db94e10b36a6cd6724"}, + {file = "pep8_naming-0.11.1-py2.py3-none-any.whl", hash = "sha256:f43bfe3eea7e0d73e8b5d07d6407ab47f2476ccaeff6937c84275cd30b016738"}, +] +pluggy = [ + {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, + {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, +] +proxmoxer = [ + {file = "proxmoxer-1.1.1.tar.gz", hash = "sha256:684a69190129da0f102703fc9861f5eea82a7d804f9f96d35c7fd73452f1da7e"}, +] +py = [ + {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, + {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, +] +pycodestyle = [ + {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, + {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, +] +pydocstyle = [ + {file = "pydocstyle-6.1.1-py3-none-any.whl", hash = "sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4"}, + {file = "pydocstyle-6.1.1.tar.gz", hash = "sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc"}, +] +pyflakes = [ + {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, + {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, +] +pyparsing = [ + {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, + {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, +] +pyrsistent = [ + {file = "pyrsistent-0.17.3.tar.gz", hash = "sha256:2e636185d9eb976a18a8a8e96efce62f2905fea90041958d8cc2a189756ebf3e"}, +] +pytest = [ + {file = "pytest-6.2.4-py3-none-any.whl", hash = "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"}, + {file = "pytest-6.2.4.tar.gz", hash = "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b"}, +] +pytest-cov = [ + {file = "pytest-cov-2.12.0.tar.gz", hash = "sha256:8535764137fecce504a49c2b742288e3d34bc09eed298ad65963616cc98fd45e"}, + {file = "pytest_cov-2.12.0-py2.py3-none-any.whl", hash = "sha256:95d4933dcbbacfa377bb60b29801daa30d90c33981ab2a79e9ab4452c165066e"}, +] +pytest-mock = [ + {file = "pytest-mock-3.6.1.tar.gz", hash = "sha256:40217a058c52a63f1042f0784f62009e976ba824c418cced42e88d5f40ab0e62"}, + {file = "pytest_mock-3.6.1-py3-none-any.whl", hash = "sha256:30c2f2cc9759e76eee674b81ea28c9f0b94f8f0445a1b87762cadf774f0df7e3"}, +] +python-dotenv = [ + {file = "python-dotenv-0.17.1.tar.gz", hash = "sha256:b1ae5e9643d5ed987fc57cc2583021e38db531946518130777734f9589b3141f"}, + {file = "python_dotenv-0.17.1-py2.py3-none-any.whl", hash = "sha256:00aa34e92d992e9f8383730816359647f358f4a3be1ba45e5a5cefd27ee91544"}, +] +python-json-logger = [ + {file = "python-json-logger-2.0.1.tar.gz", hash = "sha256:f26eea7898db40609563bed0a7ca11af12e2a79858632706d835a0f961b7d398"}, +] +pyyaml = [ + {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, + {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"}, + {file = "PyYAML-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8"}, + {file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"}, + {file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"}, + {file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"}, + {file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"}, + {file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"}, + {file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"}, + {file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"}, + {file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"}, + {file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"}, + {file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"}, + {file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"}, + {file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"}, + {file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"}, + {file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"}, + {file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"}, + {file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"}, + {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"}, + {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, +] +requests = [ + {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, + {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, +] +"ruamel.yaml" = [ + {file = "ruamel.yaml-0.17.6-py3-none-any.whl", hash = "sha256:748bbdddf9e7f6e1aad9481dfdd93a42f9c39c45821a0f09a31309cd0086e803"}, + {file = "ruamel.yaml-0.17.6.tar.gz", hash = "sha256:5605cb8ceeebaeed85ae4e97fc80547eca1b3537c163404ffe83f26adf5c9ce7"}, +] +"ruamel.yaml.clib" = [ + {file = "ruamel.yaml.clib-0.2.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:28116f204103cb3a108dfd37668f20abe6e3cafd0d3fd40dba126c732457b3cc"}, + {file = "ruamel.yaml.clib-0.2.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:daf21aa33ee9b351f66deed30a3d450ab55c14242cfdfcd377798e2c0d25c9f1"}, + {file = "ruamel.yaml.clib-0.2.2-cp27-cp27m-win32.whl", hash = "sha256:30dca9bbcbb1cc858717438218d11eafb78666759e5094dd767468c0d577a7e7"}, + {file = "ruamel.yaml.clib-0.2.2-cp27-cp27m-win_amd64.whl", hash = "sha256:f6061a31880c1ed6b6ce341215336e2f3d0c1deccd84957b6fa8ca474b41e89f"}, + {file = "ruamel.yaml.clib-0.2.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:73b3d43e04cc4b228fa6fa5d796409ece6fcb53a6c270eb2048109cbcbc3b9c2"}, + {file = "ruamel.yaml.clib-0.2.2-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:53b9dd1abd70e257a6e32f934ebc482dac5edb8c93e23deb663eac724c30b026"}, + {file = "ruamel.yaml.clib-0.2.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:839dd72545ef7ba78fd2aa1a5dd07b33696adf3e68fae7f31327161c1093001b"}, + {file = "ruamel.yaml.clib-0.2.2-cp35-cp35m-win32.whl", hash = "sha256:b1e981fe1aff1fd11627f531524826a4dcc1f26c726235a52fcb62ded27d150f"}, + {file = "ruamel.yaml.clib-0.2.2-cp35-cp35m-win_amd64.whl", hash = "sha256:4e52c96ca66de04be42ea2278012a2342d89f5e82b4512fb6fb7134e377e2e62"}, + {file = "ruamel.yaml.clib-0.2.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a873e4d4954f865dcb60bdc4914af7eaae48fb56b60ed6daa1d6251c72f5337c"}, + {file = "ruamel.yaml.clib-0.2.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ab845f1f51f7eb750a78937be9f79baea4a42c7960f5a94dde34e69f3cce1988"}, + {file = "ruamel.yaml.clib-0.2.2-cp36-cp36m-win32.whl", hash = "sha256:e9f7d1d8c26a6a12c23421061f9022bb62704e38211fe375c645485f38df34a2"}, + {file = "ruamel.yaml.clib-0.2.2-cp36-cp36m-win_amd64.whl", hash = "sha256:2602e91bd5c1b874d6f93d3086f9830f3e907c543c7672cf293a97c3fabdcd91"}, + {file = "ruamel.yaml.clib-0.2.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:44c7b0498c39f27795224438f1a6be6c5352f82cb887bc33d962c3a3acc00df6"}, + {file = "ruamel.yaml.clib-0.2.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:8e8fd0a22c9d92af3a34f91e8a2594eeb35cba90ab643c5e0e643567dc8be43e"}, + {file = "ruamel.yaml.clib-0.2.2-cp37-cp37m-win32.whl", hash = "sha256:464e66a04e740d754170be5e740657a3b3b6d2bcc567f0c3437879a6e6087ff6"}, + {file = "ruamel.yaml.clib-0.2.2-cp37-cp37m-win_amd64.whl", hash = "sha256:52ae5739e4b5d6317b52f5b040b1b6639e8af68a5b8fd606a8b08658fbd0cab5"}, + {file = "ruamel.yaml.clib-0.2.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4df5019e7783d14b79217ad9c56edf1ba7485d614ad5a385d1b3c768635c81c0"}, + {file = "ruamel.yaml.clib-0.2.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:5254af7d8bdf4d5484c089f929cb7f5bafa59b4f01d4f48adda4be41e6d29f99"}, + {file = "ruamel.yaml.clib-0.2.2-cp38-cp38-win32.whl", hash = "sha256:74161d827407f4db9072011adcfb825b5258a5ccb3d2cd518dd6c9edea9e30f1"}, + {file = "ruamel.yaml.clib-0.2.2-cp38-cp38-win_amd64.whl", hash = "sha256:058a1cc3df2a8aecc12f983a48bda99315cebf55a3b3a5463e37bb599b05727b"}, + {file = "ruamel.yaml.clib-0.2.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c6ac7e45367b1317e56f1461719c853fd6825226f45b835df7436bb04031fd8a"}, + {file = "ruamel.yaml.clib-0.2.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:b4b0d31f2052b3f9f9b5327024dc629a253a83d8649d4734ca7f35b60ec3e9e5"}, + {file = "ruamel.yaml.clib-0.2.2.tar.gz", hash = "sha256:2d24bd98af676f4990c4d715bcdc2a60b19c56a3fb3a763164d2d8ca0e806ba7"}, +] +six = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] +smmap = [ + {file = "smmap-4.0.0-py2.py3-none-any.whl", hash = "sha256:a9a7479e4c572e2e775c404dcd3080c8dc49f39918c2cf74913d30c4c478e3c2"}, + {file = "smmap-4.0.0.tar.gz", hash = "sha256:7e65386bd122d45405ddf795637b7f7d2b532e7e401d46bbe3fb49b9986d5182"}, +] +snowballstemmer = [ + {file = "snowballstemmer-2.1.0-py2.py3-none-any.whl", hash = "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2"}, + {file = "snowballstemmer-2.1.0.tar.gz", hash = "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914"}, +] +stevedore = [ + {file = "stevedore-3.3.0-py3-none-any.whl", hash = "sha256:50d7b78fbaf0d04cd62411188fa7eedcb03eb7f4c4b37005615ceebe582aa82a"}, + {file = "stevedore-3.3.0.tar.gz", hash = "sha256:3a5bbd0652bf552748871eaa73a4a8dc2899786bc497a2aa1fcb4dcdb0debeee"}, +] +testfixtures = [ + {file = "testfixtures-6.17.1-py2.py3-none-any.whl", hash = "sha256:9ed31e83f59619e2fa17df053b241e16e0608f4580f7b5a9333a0c9bdcc99137"}, + {file = "testfixtures-6.17.1.tar.gz", hash = "sha256:5ec3a0dd6f71cc4c304fbc024a10cc293d3e0b852c868014b9f233203e149bda"}, +] +toml = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] +typing-extensions = [ + {file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"}, + {file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"}, + {file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"}, +] +urllib3 = [ + {file = "urllib3-1.26.5-py2.py3-none-any.whl", hash = "sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c"}, + {file = "urllib3-1.26.5.tar.gz", hash = "sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098"}, +] +yapf = [ + {file = "yapf-0.31.0-py2.py3-none-any.whl", hash = "sha256:e3a234ba8455fe201eaa649cdac872d590089a18b661e39bbac7020978dd9c2e"}, + {file = "yapf-0.31.0.tar.gz", hash = "sha256:408fb9a2b254c302f49db83c59f9aa0b4b0fd0ec25be3a5c51181327922ff63d"}, +] +zipp = [ + {file = "zipp-3.4.1-py3-none-any.whl", hash = "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098"}, + {file = "zipp-3.4.1.tar.gz", hash = "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76"}, +] diff --git a/prometheuspvesd/__init__.py b/prometheuspvesd/__init__.py new file mode 100644 index 0000000..0bca9b7 --- /dev/null +++ b/prometheuspvesd/__init__.py @@ -0,0 +1,3 @@ +"""Default package.""" + +__version__ = "0.0.0" diff --git a/prometheuspvesd/cli.py b/prometheuspvesd/cli.py new file mode 100644 index 0000000..ded0180 --- /dev/null +++ b/prometheuspvesd/cli.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +"""Entrypoint and CLI handler.""" + +import argparse +import json +import shutil +import signal +import tempfile +from time import sleep + +import prometheuspvesd.exception +from prometheuspvesd import __version__ +from prometheuspvesd.config import SingleConfig +from prometheuspvesd.discovery import Discovery +from prometheuspvesd.model import HostList +from prometheuspvesd.utils import SingleLog + + +class PrometheusSD: + """Main Prometheus SD object.""" + + def __init__(self): + self.log = SingleLog() + self.logger = self.log.logger + self.args = self._cli_args() + self.config = self._get_config() + + self.discovery = Discovery() + self._fetch() + + def _cli_args(self): + """ + Use argparse for parsing CLI arguments. + + :return: args objec + """ + parser = argparse.ArgumentParser(description="Prometheus Service Discovery for Proxmox VE") + parser.add_argument( + "-c", "--config", dest="config_file", help="location of configuration file" + ) + parser.add_argument( + "-o", "--output", dest="output_file", action="store", help="output file" + ) + parser.add_argument( + "-v", dest="logging.level", action="append_const", const=-1, help="increase log level" + ) + parser.add_argument( + "-q", dest="logging.level", action="append_const", const=1, help="decrease log level" + ) + parser.add_argument( + "--version", action="version", version="%(prog)s {}".format(__version__) + ) + + return parser.parse_args().__dict__ + + def _get_config(self): + try: + config = SingleConfig(args=self.args) + except prometheuspvesd.exception.ConfigError as e: + self.log.sysexit_with_message(e) + + try: + self.log.set_level(config.config["logging"]["level"]) + except ValueError as e: + self.log.sysexit_with_message("Can not set log level.\n{}".format(str(e))) + + required = [("pve.server", config.config["pve"]["server"]), + ("pve.user", config.config["pve"]["user"]), + ("pve.password", config.config["pve"]["password"])] + for name, value in required: + if not value: + self.log.sysexit_with_message("Option '{}' is required but not set".format(name)) + + self.logger.info("Using config file {}".format(config.config_file)) + + return config + + def _fetch(self): + signal.signal(signal.SIGINT, self._terminate) + signal.signal(signal.SIGTERM, self._terminate) + + loop_delay = self.config.config["loop_delay"] + output_file = self.config.config["output_file"] + + self.logger.info("Writes targets to {}".format(output_file)) + + while True: + self.logger.debug("Propagate from PVE") + self._write(self.discovery.propagate()) + + self.logger.info("Waiting {} seconds for next loop".format(loop_delay)) + sleep(self.config.config["loop_delay"]) + + def _write(self, host_list: HostList): + output = [] + for host in host_list.hosts: + output.append(host.to_sd_json()) + + # Write to tmp file and move after write + temp_file = tempfile.NamedTemporaryFile(mode="w", prefix="prometheus-pve-sd", delete=False) + with temp_file as tf: + json.dump(output, tf, indent=4) + + shutil.move(temp_file.name, self.config.config["output_file"]) + + def _terminate(self, signal, frame): + self.log.sysexit_with_message("Terminating", code=0) + + +def main(): + PrometheusSD() diff --git a/prometheuspvesd/config.py b/prometheuspvesd/config.py new file mode 100644 index 0000000..69d67dc --- /dev/null +++ b/prometheuspvesd/config.py @@ -0,0 +1,247 @@ +#!/usr/bin/env python3 +"""Global settings definition.""" + +import os +from pathlib import Path +from pathlib import PurePath + +import anyconfig +import environs +import jsonschema.exceptions +import ruamel.yaml +from appdirs import AppDirs +from jsonschema._utils import format_as_index + +import prometheuspvesd.exception +from prometheuspvesd.utils import Singleton + +config_dir = AppDirs("prometheus-pve-sd").user_config_dir +default_config_file = os.path.join(config_dir, "config.yml") +cache_dir = AppDirs("prometheus-pve-sd").user_cache_dir +default_output_file = os.path.join(cache_dir, "pve.json") + + +class Config(): + """ + Create an object with all necessary settings. + + Settings are loade from multiple locations in defined order (last wins): + - default settings defined by `self._get_defaults()` + - yaml config file, defaults to OS specific user config dir (https://pypi.org/project/appdirs/) + - provides cli parameters + """ + + SETTINGS = { + "config_file": { + "default": "", + "env": "CONFIG_FILE", + "type": environs.Env().str + }, + "logging.level": { + "default": "WARNING", + "env": "LOG_LEVEL", + "file": True, + "type": environs.Env().str + }, + "logging.json": { + "default": False, + "env": "LOG_JSON", + "file": True, + "type": environs.Env().bool + }, + "output_file": { + "default": default_output_file, + "env": "output_file", + "file": True, + "type": environs.Env().str + }, + "loop_delay": { + "default": 300, + "env": "LOOP_DELAY", + "file": True, + "type": environs.Env().int + }, + "exclude_state": { + "default": [], + "env": "EXCLUDE_STATE", + "file": True, + "type": environs.Env().list + }, + "exclude_vmid": { + "default": [], + "env": "EXCLUDE_STATE", + "file": True, + "type": environs.Env().list + }, + "pve.server": { + "default": "", + "env": "PVE_SERVER", + "file": True, + "type": environs.Env().str + }, + "pve.user": { + "default": "", + "env": "PVE_USER", + "file": True, + "type": environs.Env().str + }, + "pve.password": { + "default": "", + "env": "PVE_PASSWORD", + "file": True, + "type": environs.Env().str + }, + "pve.auth_timeout": { + "default": 5, + "env": "PVE_AUTH_TIMEOUT", + "file": True, + "type": environs.Env().int + }, + "pve.verify_ssl": { + "default": True, + "env": "PVE_VERIFY_SSL", + "file": True, + "type": environs.Env().bool + }, + } + + def __init__(self, args={}): + """ + Initialize a new settings class. + + :param args: An optional dict of options, arguments and commands from the CLI. + :param config_file: An optional path to a yaml config file. + :returns: None + + """ + self._args = args + self._schema = None + self.config_file = default_config_file + self.config = None + self._set_config() + + def _get_args(self, args): + cleaned = dict(filter(lambda item: item[1] is not None, args.items())) + + normalized = {} + for key, value in cleaned.items(): + normalized = self._add_dict_branch(normalized, key.split("."), value) + + # Override correct log level from argparse + levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] + log_level = levels.index(self.SETTINGS["logging.level"]["default"]) + if normalized.get("logging"): + for adjustment in normalized["logging"]["level"]: + log_level = min(len(levels) - 1, max(log_level + adjustment, 0)) + normalized["logging"]["level"] = levels[log_level] + + return normalized + + def _get_defaults(self): + normalized = {} + for key, item in self.SETTINGS.items(): + normalized = self._add_dict_branch(normalized, key.split("."), item["default"]) + + self.schema = anyconfig.gen_schema(normalized) + return normalized + + def _get_envs(self): + normalized = {} + for key, item in self.SETTINGS.items(): + if item.get("env"): + prefix = "PROMETHEUS_PVE_SD_" + envname = prefix + item["env"] + try: + value = item["type"](envname) + normalized = self._add_dict_branch(normalized, key.split("."), value) + except environs.EnvError as e: + if '"{}" not set'.format(envname) in str(e): + pass + else: + raise prometheuspvesd.exception.ConfigError( + "Unable to read environment variable", str(e) + ) + + return normalized + + def _set_config(self): + args = self._get_args(self._args) + envs = self._get_envs() + defaults = self._get_defaults() + + # preset config file path + if envs.get("config_file"): + self.config_file = self._normalize_path(envs.get("config_file")) + + if args.get("config_file"): + self.config_file = self._normalize_path(args.get("config_file")) + + source_files = [] + source_files.append(self.config_file) + + for config in source_files: + if config and os.path.exists(config): + with open(config, "r", encoding="utf8") as stream: + s = stream.read() + try: + file_dict = ruamel.yaml.safe_load(s) + except ( + ruamel.yaml.composer.ComposerError, ruamel.yaml.scanner.ScannerError + ) as e: + message = "{} {}".format(e.context, e.problem) + raise prometheuspvesd.exception.ConfigError( + "Unable to read config file {}".format(config), message + ) + + if self._validate(file_dict): + anyconfig.merge(defaults, file_dict, ac_merge=anyconfig.MS_DICTS) + defaults["logging"]["level"] = defaults["logging"]["level"].upper() + + if self._validate(envs): + anyconfig.merge(defaults, envs, ac_merge=anyconfig.MS_DICTS) + + if self._validate(args): + anyconfig.merge(defaults, args, ac_merge=anyconfig.MS_DICTS) + + if "config_file" in defaults: + defaults.pop("config_file") + + defaults["logging"]["level"] = defaults["logging"]["level"].upper() + + Path(PurePath(self.config_file).parent).mkdir(parents=True, exist_ok=True) + Path(PurePath(defaults["output_file"]).parent).mkdir(parents=True, exist_ok=True) + + self.config = defaults + + def _normalize_path(self, path): + if not os.path.isabs(path): + base = os.path.join(os.getcwd(), path) + return os.path.abspath(os.path.expanduser(os.path.expandvars(base))) + else: + return path + + def _validate(self, config): + try: + anyconfig.validate(config, self.schema, ac_schema_safe=False) + except jsonschema.exceptions.ValidationError as e: + schema_error = "Failed validating '{validator}' in schema{schema}\n{message}".format( + validator=e.validator, + schema=format_as_index(list(e.relative_schema_path)[:-1]), + message=e.message + ) + raise prometheuspvesd.exception.ConfigError("Configuration error", schema_error) + + return True + + def _add_dict_branch(self, tree, vector, value): + key = vector[0] + tree[key] = value \ + if len(vector) == 1 \ + else self._add_dict_branch(tree[key] if key in tree else {}, vector[1:], value) + return tree + + +class SingleConfig(Config, metaclass=Singleton): + """Singleton config class.""" + + pass diff --git a/prometheuspvesd/discovery.py b/prometheuspvesd/discovery.py new file mode 100644 index 0000000..e66199a --- /dev/null +++ b/prometheuspvesd/discovery.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +"""Prometheus Discovery.""" + +import json +import re +import socket +from collections import defaultdict + +from prometheuspvesd.config import SingleConfig +from prometheuspvesd.model import Host +from prometheuspvesd.model import HostList +from prometheuspvesd.utils import SingleLog +from prometheuspvesd.utils import to_bool + +try: + from proxmoxer import ProxmoxAPI + HAS_PROXMOXER = True +except ImportError: + HAS_PROXMOXER = False + + +class Discovery(): + """Prometheus PVE Service Discovery.""" + + def __init__(self): + if not HAS_PROXMOXER: + self.logger.error( + "The Proxmox VE Prometheus SD requires proxmoxer: " + "https://pypi.org/project/proxmoxer/" + ) + + self.config = SingleConfig() + self.log = SingleLog() + self.logger = SingleLog().logger + self.client = self._auth() + self.host_list = HostList() + + def _auth(self): + return ProxmoxAPI( + self.config.config["pve"]["server"], + user=self.config.config["pve"]["user"], + password=self.config.config["pve"]["password"], + verify_ssl=to_bool(self.config.config["pve"]["verify_ssl"]), + timeout=self.config.config["pve"]["auth_timeout"] + ) + + def _get_names(self, pve_list, pve_type): + names = [] + + if pve_type == "node": + names = [node["node"] for node in pve_list] + elif pve_type == "pool": + names = [pool["poolid"] for pool in pve_list] + + return names + + def _get_variables(self, pve_list, pve_type): + variables = {} + + if pve_type in ["qemu", "container"]: + for vm in pve_list: + nested = {} + for key, value in vm.items(): + nested["proxmox_" + key] = value + variables[vm["name"]] = nested + + return variables + + def _get_ip_address(self, pve_type, pve_node, vmid): + + def validate(address): + try: + # IP address validation + if socket.inet_aton(address): + # Ignore localhost + if address != "127.0.0.1": + return address + except socket.error: + return False + + address = False + networks = False + if pve_type == "qemu": + # If qemu agent is enabled, try to gather the IP address + try: + if self.client.nodes(pve_node).get(pve_type, vmid, "agent", "info") is not None: + networks = self.client.nodes(pve_node).get( + "qemu", vmid, "agent", "network-get-interfaces" + )["result"] + except Exception: # noqa + pass + + if networks: + if type(networks) is list: + for network in networks: + for ip_address in network["ip-addresses"]: + address = validate(ip_address["ip-address"]) + + if not address: + try: + config = self.client.nodes(pve_node).get(pve_type, vmid, "config") + sources = [config["net0"], config["ipconfig0"]] + + for s in sources: + find = re.search(r"ip=(\d*\.\d*\.\d*\.\d*)", str(sources)) + if find and find.group(1): + address = find.group(1) + break + except Exception: # noqa + pass + + return address + + def _exclude(self, pve_list): + filtered = [] + for item in pve_list: + obj = defaultdict(dict, item) + if obj["template"] == 1: + continue + + if obj["status"] in self.config.config["exclude_state"]: + continue + + if obj["vmid"] in self.config.config["exclude_vmid"]: + continue + + filtered.append(item.copy()) + return filtered + + def propagate(self): + self.host_list.clear() + + for node in self._get_names(self.client.nodes.get(), "node"): + try: + qemu_list = self._exclude(self.client.nodes(node).qemu.get()) + container_list = self._exclude(self.client.nodes(node).lxc.get()) + except Exception as e: # noqa + self.logger.error("Proxmoxer API error: {0}".format(str(e))) + + # Merge QEMU and Containers lists from this node + instances = self._get_variables(qemu_list, "qemu").copy() + instances.update(self._get_variables(container_list, "container")) + + self.logger.info("Found {} targets".format(len(instances))) + for host in instances: + host_meta = instances[host] + vmid = host_meta["proxmox_vmid"] + + try: + pve_type = host_meta["proxmox_type"] + except KeyError: + pve_type = "qemu" + + config = self.client.nodes(node).get(pve_type, vmid, "config") + + try: + description = (config["description"]) + except KeyError: + description = None + except Exception as e: # noqa + self.logger.error("Proxmoxer API error: {0}".format(str(e))) + + try: + metadata = json.loads(description) + except TypeError: + metadata = {} + except ValueError: + metadata = {"notes": description} + + address = self._get_ip_address(pve_type, node, vmid) or host + + prom_host = Host(vmid, host, address, pve_type) + + config_flags = [("cpu", "sockets"), ("cores", "cores"), ("memory", "memory")] + meta_flags = [("status", "proxmox_status")] + + for key, flag in config_flags: + if flag in config: + prom_host.add_label(key, config[flag]) + + for key, flag in meta_flags: + if flag in host_meta: + prom_host.add_label(key, host_meta[flag]) + + if "groups" in metadata: + prom_host.add_label("groups", ",".join(metadata["groups"])) + + self.host_list.add_host(prom_host) + self.logger.debug("Discovered {}".format(prom_host)) + + for pool in self._get_names(self.client.pools.get(), "pool"): + try: + pool_list = self._exclude(self.client.pool(pool).get()["members"]) + except Exception as e: # noqa + self.logger.error("Proxmoxer API error: {0}".format(str(e))) + + members = [ + member["name"] + for member in pool_list + if (member["type"] == "qemu" or member["type"] == "lxc") + ] + + for member in members: + self.inventory.add_host(group=pool, host=member) + + return self.host_list diff --git a/prometheuspvesd/exception.py b/prometheuspvesd/exception.py new file mode 100644 index 0000000..05a6910 --- /dev/null +++ b/prometheuspvesd/exception.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 +"""Custom exceptions.""" + + +class PrometheusSDError(Exception): + """Generic exception class for promtheus-pve-sd.""" + + def __init__(self, msg, original_exception=""): + super(PrometheusSDError, + self).__init__("{msg}\n{org}".format(msg=msg, org=original_exception)) + self.original_exception = original_exception + + +class ConfigError(PrometheusSDError): + """Errors related to config file handling.""" + + pass diff --git a/prometheuspvesd/model.py b/prometheuspvesd/model.py new file mode 100644 index 0000000..732e4ef --- /dev/null +++ b/prometheuspvesd/model.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +"""Prometheus SD object models.""" + + +class Host: + """Represents a virtual machine or container in PVE.""" + + def __init__(self, vmid, hostname, ip_address, pve_type): + self.hostname = hostname + self.ip_address = ip_address + self.vmid = vmid + self.pve_type = pve_type + self.labels = {} + self.labels["__meta_pve_ip"] = ip_address + self.labels["__meta_pve_name"] = hostname + self.labels["__meta_pve_type"] = pve_type + self.labels["__meta_pve_vmid"] = str(vmid) + + def __str__(self): + return f"{self.hostname}({self.vmid}): {self.pve_type} {self.ip_address}" + + def add_label(self, key, value): + key = key.replace("-", "_").replace(" ", "_") + self.labels[f"__meta_pve_{key}"] = str(value) + + def to_sd_json(self): + return {"targets": [self.hostname], "labels": self.labels} + + +class HostList: + """Collection of host objects.""" + + def __init__(self): + self.hosts = [] + + def clear(self): + self.hosts = [] + + def add_host(self, host: Host): + if not self.host_exists(host): + self.hosts.append(host) + + def host_exists(self, host: Host): + """Check if a host is already in the list by id and type.""" + for current in self.hosts: + if current.pve_type == host.pve_type and current.vmid == host.vmid: + return True + return False diff --git a/prometheuspvesd/utils.py b/prometheuspvesd/utils.py new file mode 100644 index 0000000..38c35e9 --- /dev/null +++ b/prometheuspvesd/utils.py @@ -0,0 +1,242 @@ +#!/usr/bin/env python3 +"""Global utility methods and classes.""" + +import logging +import os +import sys +from distutils.util import strtobool + +import colorama +from pythonjsonlogger import jsonlogger + +CONSOLE_FORMAT = "{}{}[%(levelname)s]{} %(message)s" +JSON_FORMAT = "(asctime) (levelname) (message)" + + +def to_bool(string): + return bool(strtobool(str(string))) + + +def _should_do_markup(): + py_colors = os.environ.get("PY_COLORS", None) + if py_colors is not None: + return to_bool(py_colors) + + return sys.stdout.isatty() and os.environ.get("TERM") != "dumb" + + +colorama.init(autoreset=True, strip=not _should_do_markup()) + + +class Singleton(type): + """Meta singleton class.""" + + _instances = {} + + def __call__(cls, *args, **kwargs): + if cls not in cls._instances: + cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) + return cls._instances[cls] + + +class LogFilter(object): + """A custom log filter which excludes log messages above the logged level.""" + + def __init__(self, level): + """ + Initialize a new custom log filter. + + :param level: Log level limit + :returns: None + + """ + self.__level = level + + def filter(self, logRecord): # noqa + # https://docs.python.org/3/library/logging.html#logrecord-attributes + return logRecord.levelno <= self.__level + + +class MultilineFormatter(logging.Formatter): + """Logging Formatter to reset color after newline characters.""" + + def format(self, record): # noqa + record.msg = record.msg.replace("\n", "\n{}... ".format(colorama.Style.RESET_ALL)) + return logging.Formatter.format(self, record) + + +class MultilineJsonFormatter(jsonlogger.JsonFormatter): + """Logging Formatter to remove newline characters.""" + + def format(self, record): # noqa + record.msg = record.msg.replace("\n", " ") + return jsonlogger.JsonFormatter.format(self, record) + + +class Log: + """Handle logging.""" + + def __init__(self, level=logging.WARN, name="prometheuspvesd", json=False): + self.logger = logging.getLogger(name) + self.logger.setLevel(level) + self.logger.addHandler(self._get_error_handler(json=json)) + self.logger.addHandler(self._get_warn_handler(json=json)) + self.logger.addHandler(self._get_info_handler(json=json)) + self.logger.addHandler(self._get_critical_handler(json=json)) + self.logger.addHandler(self._get_debug_handler(json=json)) + self.logger.propagate = False + + def _get_error_handler(self, json=False): + handler = logging.StreamHandler(sys.stderr) + handler.setLevel(logging.ERROR) + handler.addFilter(LogFilter(logging.ERROR)) + handler.setFormatter( + MultilineFormatter( + self.error( + CONSOLE_FORMAT.format( + colorama.Fore.RED, colorama.Style.BRIGHT, colorama.Style.RESET_ALL + ) + ) + ) + ) + + if json: + handler.setFormatter(MultilineJsonFormatter(JSON_FORMAT)) + + return handler + + def _get_warn_handler(self, json=False): + handler = logging.StreamHandler(sys.stdout) + handler.setLevel(logging.WARN) + handler.addFilter(LogFilter(logging.WARN)) + handler.setFormatter( + MultilineFormatter( + self.warn( + CONSOLE_FORMAT.format( + colorama.Fore.YELLOW, colorama.Style.BRIGHT, colorama.Style.RESET_ALL + ) + ) + ) + ) + + if json: + handler.setFormatter(MultilineJsonFormatter(JSON_FORMAT)) + + return handler + + def _get_info_handler(self, json=False): + handler = logging.StreamHandler(sys.stdout) + handler.setLevel(logging.INFO) + handler.addFilter(LogFilter(logging.INFO)) + handler.setFormatter( + MultilineFormatter( + self.info( + CONSOLE_FORMAT.format( + colorama.Fore.CYAN, colorama.Style.BRIGHT, colorama.Style.RESET_ALL + ) + ) + ) + ) + + if json: + handler.setFormatter(MultilineJsonFormatter(JSON_FORMAT)) + + return handler + + def _get_critical_handler(self, json=False): + handler = logging.StreamHandler(sys.stderr) + handler.setLevel(logging.CRITICAL) + handler.addFilter(LogFilter(logging.CRITICAL)) + handler.setFormatter( + MultilineFormatter( + self.critical( + CONSOLE_FORMAT.format( + colorama.Fore.RED, colorama.Style.BRIGHT, colorama.Style.RESET_ALL + ) + ) + ) + ) + + if json: + handler.setFormatter(MultilineJsonFormatter(JSON_FORMAT)) + + return handler + + def _get_debug_handler(self, json=False): + handler = logging.StreamHandler(sys.stderr) + handler.setLevel(logging.DEBUG) + handler.addFilter(LogFilter(logging.DEBUG)) + handler.setFormatter( + MultilineFormatter( + self.critical( + CONSOLE_FORMAT.format( + colorama.Fore.BLUE, colorama.Style.BRIGHT, colorama.Style.RESET_ALL + ) + ) + ) + ) + + if json: + handler.setFormatter(MultilineJsonFormatter(JSON_FORMAT)) + + return handler + + def set_level(self, s): + self.logger.setLevel(s) + + def debug(self, msg): + """Format info messages and return string.""" + return msg + + def critical(self, msg): + """Format critical messages and return string.""" + return msg + + def error(self, msg): + """Format error messages and return string.""" + return msg + + def warn(self, msg): + """Format warn messages and return string.""" + return msg + + def info(self, msg): + """Format info messages and return string.""" + return msg + + def _color_text(self, color, msg): + """ + Colorize strings. + + :param color: colorama color settings + :param msg: string to colorize + :returns: string + + """ + return "{}{}{}".format(color, msg, colorama.Style.RESET_ALL) + + def sysexit(self, code=1): + sys.exit(code) + + def sysexit_with_message(self, msg, code=1): + self.logger.critical(str(msg)) + self.sysexit(code) + + +class SingleLog(Log, metaclass=Singleton): + """Singleton logging class.""" + + pass + + +class UnsafeTag: + """Handle custom yaml unsafe tag.""" + + yaml_tag = u"!unsafe" + + def __init__(self, value): + self.unsafe = value + + @staticmethod + def yaml_constructor(loader, node): + return loader.construct_scalar(node) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..15db565 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,95 @@ +[tool.poetry] +authors = ["Robert Kaussow "] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "License :: OSI Approved :: MIT License", + "Intended Audience :: Developers", + "Intended Audience :: Information Technology", + "Intended Audience :: System Administrators", + "Natural Language :: English", + "Operating System :: POSIX", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Topic :: Utilities", +] +description = "Prometheus Service Discovery for Proxmox VE." +documentation = "https://github.com/thegeeklab/prometheus-pve-sd/" +homepage = "https://github.com/thegeeklab/prometheus-pve-sd/" +include = [ + "LICENSE", +] +keywords = ["prometheus", "sd", "pve", "metrics"] +license = "MIT" +name = "prometheus-pve-sd" +packages = [ + {include = "prometheuspvesd"}, +] +readme = "README.md" +repository = "https://github.com/thegeeklab/prometheus-pve-sd/" +version = "0.0.0" + +[tool.poetry.dependencies] +anyconfig = "0.11.0" +appdirs = "1.4.4" +colorama = "0.4.4" +environs = "9.3.2" +jsonschema = "3.2.0" +nested-lookup = "0.2.22" +proxmoxer = "1.1.1" +python = "^3.6.0" +python-json-logger = "2.0.1" +requests = "2.25.1" +"ruamel.yaml" = "0.17.6" + +[tool.poetry.dev-dependencies] +bandit = "1.7.0" +flake8 = "3.9.2" +flake8-blind-except = "0.2.0" +flake8-builtins = "1.5.3" +flake8-docstrings = "1.6.0" +flake8-eradicate = "1.0.0" +flake8-isort = "4.0.0" +flake8-logging-format = "0.6.0" +flake8-pep3101 = "1.3.0" +flake8-polyfill = "1.0.2" +flake8-quotes = "3.2.0" +pep8-naming = "0.11.1" +pydocstyle = "6.1.1" +pytest = "6.2.4" +pytest-cov = "2.12.0" +pytest-mock = "3.6.1" +yapf = "0.31.0" + +[tool.poetry.scripts] +prometheus-pve-sd = "prometheuspvesd.cli:main" + +[tool.poetry-dynamic-versioning] +enable = true +style = "semver" +vcs = "git" + +[tool.isort] +default_section = "THIRDPARTY" +force_single_line = true +line_length = 99 +sections = ["FUTURE", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"] +skip_glob = ["**/.env*", "**/env/*", "**/.venv/*", "**/docs/*"] + +[tool.pytest.ini_options] +addopts = "prometheuspvesd --cov=prometheuspvesd --cov-report=xml:coverage.xml --cov-report=term --cov-append --no-cov-on-fail" +filterwarnings = [ + "ignore::FutureWarning", + "ignore:.*collections.*:DeprecationWarning", + "ignore:.*pep8.*:FutureWarning", +] + +[tool.coverage.run] +omit = ["**/test/*"] + +[build-system] +build-backend = "poetry.core.masonry.api" +requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning"] diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..bf73d46 --- /dev/null +++ b/renovate.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["github>thegeeklab/renovate-presets"] +} diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..1338815 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,19 @@ +[flake8] +# Explanation of errors +# +# D102: Missing docstring in public method +# D103: Missing docstring in public function +# D107: Missing docstring in __init__ +# D202: No blank lines allowed after function docstring +# W503:Line break occurred before a binary operator +ignore = D102, D103, D107, D202, W503 +max-line-length = 99 +inline-quotes = double +exclude = .git, __pycache__, build, dist, test, *.pyc, *.egg-info, .cache, .eggs, env* + +[yapf] +based_on_style = google +column_limit = 99 +dedent_closing_brackets = true +coalesce_brackets = true +split_before_logical_operator = true