commit b566f81a6b7ff82a197477bdf3e650f22c639514 Author: Robert Kaussow Date: Wed Jun 9 20:44:10 2021 +0200 inital commit 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