commit a556518577ad24fcb1ae0e91a743fd145e498aae Author: Ivan Schaller Date: Wed Jun 21 23:31:47 2023 +0200 init Signed-off-by: Ivan Schaller diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d156b71 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +development/ +dist/ +.*/ + +!development/config/ diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..a63eb96 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use asdf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..668d105 --- /dev/null +++ b/.gitignore @@ -0,0 +1,161 @@ +test.ipynb +test.py +mangadexdlp/__pycache__/ +test/ +downloads/ +.vscode/ +.pytest_cache& +.vscode/ +__pycache__/ +.pytest_cache/ +chaps.txt +mangas.txt +.idea/ +venv +test.sh +.ruff_cache/ +development/db-data/ +development/redis-data/ +development/reports/ +development/scripts/ +development/media/ + +### Python template +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# 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/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..e817e63 --- /dev/null +++ b/.tool-versions @@ -0,0 +1,5 @@ +python 3.9.13 3.10.5 3.8.13 +shellcheck 0.9.0 +shfmt 3.6.0 +direnv 2.32.2 +just 1.13.0 \ No newline at end of file diff --git a/.woodpecker/publish_release.yml b/.woodpecker/publish_release.yml new file mode 100644 index 0000000..0cd0c1a --- /dev/null +++ b/.woodpecker/publish_release.yml @@ -0,0 +1,76 @@ +################### +# publish release # +################### +# branch: master +# event: tag + +platform: linux/amd64 + +depends_on: + - tests + +clone: + git: + image: woodpeckerci/plugin-git:v1.6.0 + when: + event: tag + +pipeline: + # build wheel and dist + build-pypi: + image: cr.44net.ch/ci-plugins/tests + pull: true + when: + event: tag + commands: + - python3 -m hatch build --clean + + # create release-notes + create-release-notes: + image: cr.44net.ch/baseimages/debian-base + pull: true + when: + event: tag + commands: + - bash get_release_notes.sh ${CI_COMMIT_TAG%%-dev} + + # publish release on github (github.com/olofvndrhr/manga-dlp) + publish-release-github: + image: woodpeckerci/plugin-github-release + pull: true + when: + event: tag + settings: + api_key: + from_secret: github-olofvndrhr-token + files: dist/* + title: ${CI_COMMIT_TAG} + note: RELEASENOTES.md + + # publish release on gitea (git.44net.ch/olofvndrhr/manga-dlp) + publish-release-gitea: + image: woodpeckerci/plugin-gitea-release + pull: true + when: + event: tag + settings: + api_key: + from_secret: gitea-olofvndrhr-token + base_url: https://git.44net.ch + files: dist/* + title: ${CI_COMMIT_TAG} + note: RELEASENOTES.md + + # release pypi + release-pypi: + image: cr.44net.ch/ci-plugins/tests + pull: true + when: + event: tag + secrets: + - source: pypi_username + target: HATCH_INDEX_USER + - source: pypi_token + target: HATCH_INDEX_AUTH + commands: + - python3 -m hatch publish --no-prompt --yes diff --git a/.woodpecker/test_release.yml b/.woodpecker/test_release.yml new file mode 100644 index 0000000..c1bed47 --- /dev/null +++ b/.woodpecker/test_release.yml @@ -0,0 +1,39 @@ +################ +# test release # +################ +# branch: master +# event: pull_request + +platform: linux/amd64 + +depends_on: + - tests + +clone: + git: + image: woodpeckerci/plugin-git:v1.6.0 + when: + branch: master + event: pull_request + +pipeline: + # build wheel and dist + test-build-pypi: + image: cr.44net.ch/ci-plugins/tests + pull: true + when: + branch: master + event: pull_request + commands: + - python3 -m hatch build --clean + + # create release-notes + test-create-release-notes: + image: cr.44net.ch/baseimages/debian-base + pull: true + when: + branch: master + event: pull_request + commands: + - bash get_release_notes.sh latest + - cat RELEASENOTES.md diff --git a/.woodpecker/tests.yml b/.woodpecker/tests.yml new file mode 100644 index 0000000..ece0977 --- /dev/null +++ b/.woodpecker/tests.yml @@ -0,0 +1,51 @@ +############################## +# code testing and analysis # +############################# +# branch: all +# event: all + +platform: linux/amd64 + +clone: + git: + image: woodpeckerci/plugin-git:v1.6.0 + +pipeline: + # check code style - shell + test-shfmt: + image: cr.44net.ch/ci-plugins/tests + pull: true + commands: + - just test_shfmt + + # check code style - python + test-black: + image: cr.44net.ch/ci-plugins/tests + pull: true + commands: + - just test_black + + # check static typing - python + test-pyright: + image: cr.44net.ch/ci-plugins/tests + pull: true + commands: + - just install_deps + - just test_pyright + + # ruff test - python + test-ruff: + image: cr.44net.ch/ci-plugins/tests + pull: true + commands: + - just test_ruff + + # analyse code with sonarqube and upload it + sonarqube-analysis: + image: cr.44net.ch/ci-plugins/sonar-scanner + pull: true + settings: + sonar_host: https://sonarqube.44net.ch + sonar_token: + from_secret: sq-44net-token + usingProperties: true diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..8e78070 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,24 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres +to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +- x + +## [0.0.1] - 2023-07-01 + +### Added + +- x + +### Fixed + +- x + +### Changed + +- x diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d3926d0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Ivan Schaller + +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 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/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..cde6f92 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +recursive-include netbox_plugin_qrcode *.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..934f067 --- /dev/null +++ b/README.md @@ -0,0 +1,80 @@ +# netbox_qrgen - python script to download mangas + +> Full docs: https://netbox_qrgen.ivn.sh + +CI/CD + +[![status-badge](https://img.shields.io/drone/build/olofvndrhr/netbox_qrgen?label=tests&server=https%3A%2F%2Fci.44net.ch)](https://ci.44net.ch/olofvndrhr/netbox_qrgen) +[![Last Release](https://img.shields.io/github/release-date/olofvndrhr/netbox_qrgen?label=last%20release)](https://github.com/olofvndrhr/netbox_qrgen/releases) +[![Version](https://img.shields.io/github/v/release/olofvndrhr/netbox_qrgen?label=git%20release)](https://github.com/olofvndrhr/netbox_qrgen/releases) +[![Version PyPi](https://img.shields.io/pypi/v/netbox_qrgen?label=pypi%20release)](https://pypi.org/project/netbox_qrgen/) + +Code Analysis + +[![Quality Gate Status](https://sonarqube.44net.ch/api/project_badges/measure?project=olofvndrhr%3Anetbox_qrgen&metric=alert_status&token=f9558470580eea5b4899cf33f190eee16011346d)](https://sonarqube.44net.ch/dashboard?id=olofvndrhr%3Anetbox_qrgen) +[![Coverage](https://sonarqube.44net.ch/api/project_badges/measure?project=olofvndrhr%3Anetbox_qrgen&metric=coverage&token=f9558470580eea5b4899cf33f190eee16011346d)](https://sonarqube.44net.ch/dashboard?id=olofvndrhr%3Anetbox_qrgen) +[![Bugs](https://sonarqube.44net.ch/api/project_badges/measure?project=olofvndrhr%3Anetbox_qrgen&metric=bugs&token=f9558470580eea5b4899cf33f190eee16011346d)](https://sonarqube.44net.ch/dashboard?id=olofvndrhr%3Anetbox_qrgen) +[![Security](https://img.shields.io/snyk/vulnerabilities/github/olofvndrhr/netbox_qrgen)](https://app.snyk.io/org/olofvndrhr-t6h/project/aae9609d-a4e4-41f8-b1ac-f2561b2ad4e3) + +Meta + +[![Code style](https://img.shields.io/badge/code%20style-black-black)](https://github.com/psf/black) +[![Linter](https://img.shields.io/badge/linter-ruff-red)](https://github.com/charliermarsh/ruff) +[![Types](https://img.shields.io/badge/types-pyright-blue)](https://github.com/microsoft/pyright) +[![License](https://img.shields.io/badge/license-MIT-9400d3.svg)](https://snyk.io/learn/what-is-mit-license/) +[![Compatibility](https://img.shields.io/pypi/pyversions/netbox_qrgen)](https://pypi.org/project/netbox_qrgen/) + +--- + +## Description + +test + +## Features (not complete) + +- test + +## Usage + +### With GitHub + +```sh +git clone https://github.com/olofvndrhr/netbox_qrgen.git # clone the repository + +cd netbox_qrgen # go in the directory + +pip install -r requirements.txt # install required packages + +# on windows +python netbox_qrgen.py +# on unix +python3 netbox_qrgen.py +``` + +### With pip ([pypi](https://pypi.org/project/netbox_qrgen/)) + +```sh +python3 -m pip install netbox_qrgen # download the package from pypi + +python3 -m mangadlp # start the script as a module +OR +netbox_qrgen # call script directly +OR +mangadlp # call script directly +``` + +## Contribution / Bugs + +For suggestions for improvement, just open a pull request. + +If you want to add support for a new site, there is an api [template file](contrib/api_template.py) which you can use. +And more infos and tools are in the contrib [README.md](contrib/README.md) + +Otherwise, you can open an issue with the name of the site which you want support for (not guaranteed to be +implemented). + +If you encounter any bugs, also just open an issue with a description of the problem. + +## TODO's + +- test diff --git a/development/Dockerfile b/development/Dockerfile new file mode 100644 index 0000000..52e7088 --- /dev/null +++ b/development/Dockerfile @@ -0,0 +1,29 @@ +FROM netboxcommunity/netbox:v3.5.4 + +COPY . /tmp/build/ + +# install plugins +RUN \ + echo "**** installing plugins ****" \ + && /opt/netbox/venv/bin/pip install --no-warn-script-location \ + qrcode \ + pillow \ + netbox-inventory \ + /tmp/build/ + +# cleanup installation +RUN \ + echo "**** cleanup ****" \ + && apt-get purge --auto-remove -y \ + && apt-get clean \ + && rm -rf \ + /tmp/* \ + /var/lib/apt/lists/* \ + /var/tmp/* + +# activate plugins +ARG SECRET_KEY="dummydummydummydummydummydummydummydummydummydummy" +COPY development/config/plugins.py /etc/netbox/config/plugins.py +RUN \ + echo "**** activating plugins ****" \ + && /opt/netbox/venv/bin/python /opt/netbox/netbox/manage.py collectstatic --no-input diff --git a/development/config.env b/development/config.env new file mode 100644 index 0000000..c7e9147 --- /dev/null +++ b/development/config.env @@ -0,0 +1,45 @@ +DEBUG = True + +# server +ALLOWED_HOSTS='localhost ::1 127.0.0.1' +CORS_ORIGIN_ALLOW_ALL=True + +# db +DB_HOST=netbox-dev-db +DB_NAME=netbox +DB_PASSWORD="1234" +DB_USER=netbox + +# redis +REDIS_DATABASE=0 +REDIS_HOST=netbox-dev-redis +REDIS_INSECURE_SKIP_TLS_VERIFY=false +REDIS_PASSWORD= +REDIS_SSL=false + +REDIS_CACHE_DATABASE=1 +REDIS_CACHE_HOST=netbox-dev-redis +REDIS_CACHE_INSECURE_SKIP_TLS_VERIFY=false +REDIS_CACHE_PASSWORD= +REDIS_CACHE_SSL=false + +# other +GRAPHQL_ENABLED=true +HOUSEKEEPING_INTERVAL=86400 +MEDIA_ROOT=/opt/netbox/netbox/media +METRICS_ENABLED=false +RELEASE_CHECK_URL=https://api.github.com/repos/netbox-community/netbox/releases +PREFER_IPV4=true +POWERFEED_DEFAULT_AMPERAGE=10 +POWERFEED_DEFAULT_VOLTAGE=230 +TIME_ZONE=Europe/Zurich + +# security +SECRET_KEY='*&J8r3Z%YCqM#GW6J%b7@aC4*&J8r3Z%YCqM#GW6J%b7@aC4*&J8r3Z%YCqM#GW6J%b7@aC4' +SKIP_SUPERUSER=false +SUPERUSER_API_TOKEN='*&J8r3Z%YCqM#GW6J%b7@aC4' +SUPERUSER_EMAIL=admin@abc.net +SUPERUSER_NAME=admin +SUPERUSER_PASSWORD="1234" +WEBHOOKS_ENABLED=true +LOGIN_REQUIRED=true diff --git a/development/config/configuration.py b/development/config/configuration.py new file mode 100644 index 0000000..ca6b134 --- /dev/null +++ b/development/config/configuration.py @@ -0,0 +1,351 @@ +#### +## We recommend to not edit this file. +## Create separate files to overwrite the settings. +## See `extra.py` as an example. +#### + +import re +from os import environ +from os.path import abspath, dirname, join +from typing import Any, Callable + +# For reference see https://docs.netbox.dev/en/stable/configuration/ +# Based on https://github.com/netbox-community/netbox/blob/develop/netbox/netbox/configuration_example.py + +### +# NetBox-Docker Helper functions +### + + +# Read secret from file +def _read_secret(secret_name: str, default: str | None = None) -> str | None: + try: + f = open("/run/secrets/" + secret_name, "r", encoding="utf-8") + except EnvironmentError: + return default + else: + with f: + return f.readline().strip() + + +# If the `map_fn` isn't defined, then the value that is read from the environment (or the default value if not found) is returned. +# If the `map_fn` is defined, then `map_fn` is invoked and the value (that was read from the environment or the default value if not found) +# is passed to it as a parameter. The value returned from `map_fn` is then the return value of this function. +# The `map_fn` is not invoked, if the value (that was read from the environment or the default value if not found) is None. +def _environ_get_and_map( + variable_name: str, default: str | None = None, map_fn: Callable[[str], Any | None] = None +) -> Any | None: + env_value = environ.get(variable_name, default) + + if env_value is None: + return env_value + + if not map_fn: + return env_value + + return map_fn(env_value) + + +def _AS_BOOL(value): + return value.lower() == "true" + + +def _AS_INT(value): + return int(value) + + +def _AS_LIST(value): + return list(filter(None, value.split(" "))) + + +_BASE_DIR = dirname(dirname(abspath(__file__))) + +######################### +# # +# Required settings # +# # +######################### + +# This is a list of valid fully-qualified domain names (FQDNs) for the NetBox server. NetBox will not permit write +# access to the server via any other hostnames. The first FQDN in the list will be treated as the preferred name. +# +# Example: ALLOWED_HOSTS = ['netbox.example.com', 'netbox.internal.local'] +ALLOWED_HOSTS = environ.get("ALLOWED_HOSTS", "*").split(" ") +# ensure that '*' or 'localhost' is always in ALLOWED_HOSTS (needed for health checks) +if "*" not in ALLOWED_HOSTS and "localhost" not in ALLOWED_HOSTS: + ALLOWED_HOSTS.append("localhost") + +# PostgreSQL database configuration. See the Django documentation for a complete list of available parameters: +# https://docs.djangoproject.com/en/stable/ref/settings/#databases +DATABASE = { + "NAME": environ.get("DB_NAME", "netbox"), # Database name + "USER": environ.get("DB_USER", ""), # PostgreSQL username + "PASSWORD": _read_secret("db_password", environ.get("DB_PASSWORD", "")), + # PostgreSQL password + "HOST": environ.get("DB_HOST", "localhost"), # Database server + "PORT": environ.get("DB_PORT", ""), # Database port (leave blank for default) + "OPTIONS": {"sslmode": environ.get("DB_SSLMODE", "prefer")}, + # Database connection SSLMODE + "CONN_MAX_AGE": _environ_get_and_map("DB_CONN_MAX_AGE", "300", _AS_INT), + # Max database connection age + "DISABLE_SERVER_SIDE_CURSORS": _environ_get_and_map( + "DB_DISABLE_SERVER_SIDE_CURSORS", "False", _AS_BOOL + ), + # Disable the use of server-side cursors transaction pooling +} + +# Redis database settings. Redis is used for caching and for queuing background tasks such as webhook events. A separate +# configuration exists for each. Full connection details are required in both sections, and it is strongly recommended +# to use two separate database IDs. +REDIS = { + "tasks": { + "HOST": environ.get("REDIS_HOST", "localhost"), + "PORT": _environ_get_and_map("REDIS_PORT", 6379, _AS_INT), + "USERNAME": environ.get("REDIS_USERNAME", ""), + "PASSWORD": _read_secret("redis_password", environ.get("REDIS_PASSWORD", "")), + "DATABASE": _environ_get_and_map("REDIS_DATABASE", 0, _AS_INT), + "SSL": _environ_get_and_map("REDIS_SSL", "False", _AS_BOOL), + "INSECURE_SKIP_TLS_VERIFY": _environ_get_and_map( + "REDIS_INSECURE_SKIP_TLS_VERIFY", "False", _AS_BOOL + ), + }, + "caching": { + "HOST": environ.get("REDIS_CACHE_HOST", environ.get("REDIS_HOST", "localhost")), + "PORT": _environ_get_and_map( + "REDIS_CACHE_PORT", environ.get("REDIS_PORT", "6379"), _AS_INT + ), + "USERNAME": environ.get("REDIS_CACHE_USERNAME", environ.get("REDIS_USERNAME", "")), + "PASSWORD": _read_secret( + "redis_cache_password", + environ.get("REDIS_CACHE_PASSWORD", environ.get("REDIS_PASSWORD", "")), + ), + "DATABASE": _environ_get_and_map("REDIS_CACHE_DATABASE", "1", _AS_INT), + "SSL": _environ_get_and_map("REDIS_CACHE_SSL", environ.get("REDIS_SSL", "False"), _AS_BOOL), + "INSECURE_SKIP_TLS_VERIFY": _environ_get_and_map( + "REDIS_CACHE_INSECURE_SKIP_TLS_VERIFY", + environ.get("REDIS_INSECURE_SKIP_TLS_VERIFY", "False"), + _AS_BOOL, + ), + }, +} + +# This key is used for secure generation of random numbers and strings. It must never be exposed outside of this file. +# For optimal security, SECRET_KEY should be at least 50 characters in length and contain a mix of letters, numbers, and +# symbols. NetBox will not run without this defined. For more information, see +# https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-SECRET_KEY +SECRET_KEY = _read_secret("secret_key", environ.get("SECRET_KEY", "")) + + +######################### +# # +# Optional settings # +# # +######################### + +# # Specify one or more name and email address tuples representing NetBox administrators. These people will be notified of +# # application errors (assuming correct email settings are provided). +# ADMINS = [ +# # ['John Doe', 'jdoe@example.com'], +# ] + +if "ALLOWED_URL_SCHEMES" in environ: + ALLOWED_URL_SCHEMES = _environ_get_and_map("ALLOWED_URL_SCHEMES", None, _AS_LIST) + +# Optionally display a persistent banner at the top and/or bottom of every page. HTML is allowed. To display the same +# content in both banners, define BANNER_TOP and set BANNER_BOTTOM = BANNER_TOP. +if "BANNER_TOP" in environ: + BANNER_TOP = environ.get("BANNER_TOP", None) +if "BANNER_BOTTOM" in environ: + BANNER_BOTTOM = environ.get("BANNER_BOTTOM", None) + +# Text to include on the login page above the login form. HTML is allowed. +if "BANNER_LOGIN" in environ: + BANNER_LOGIN = environ.get("BANNER_LOGIN", None) + +# Maximum number of days to retain logged changes. Set to 0 to retain changes indefinitely. (Default: 90) +if "CHANGELOG_RETENTION" in environ: + CHANGELOG_RETENTION = _environ_get_and_map("CHANGELOG_RETENTION", None, _AS_INT) + +# Maximum number of days to retain job results (scripts and reports). Set to 0 to retain job results in the database indefinitely. (Default: 90) +if "JOBRESULT_RETENTION" in environ: + JOBRESULT_RETENTION = _environ_get_and_map("JOBRESULT_RETENTION", None, _AS_INT) + +# API Cross-Origin Resource Sharing (CORS) settings. If CORS_ORIGIN_ALLOW_ALL is set to True, all origins will be +# allowed. Otherwise, define a list of allowed origins using either CORS_ORIGIN_WHITELIST or +# CORS_ORIGIN_REGEX_WHITELIST. For more information, see https://github.com/ottoyiu/django-cors-headers +CORS_ORIGIN_ALLOW_ALL = _environ_get_and_map("CORS_ORIGIN_ALLOW_ALL", "False", _AS_BOOL) +CORS_ORIGIN_WHITELIST = _environ_get_and_map("CORS_ORIGIN_WHITELIST", "https://localhost", _AS_LIST) +CORS_ORIGIN_REGEX_WHITELIST = [ + re.compile(r) for r in _environ_get_and_map("CORS_ORIGIN_REGEX_WHITELIST", "", _AS_LIST) +] + +# Set to True to enable server debugging. WARNING: Debugging introduces a substantial performance penalty and may reveal +# sensitive information about your installation. Only enable debugging while performing testing. +# Never enable debugging on a production system. +DEBUG = _environ_get_and_map("DEBUG", "False", _AS_BOOL) + +# This parameter serves as a safeguard to prevent some potentially dangerous behavior, +# such as generating new database schema migrations. +# Set this to True only if you are actively developing the NetBox code base. +DEVELOPER = _environ_get_and_map("DEVELOPER", "False", _AS_BOOL) + +# Email settings +EMAIL = { + "SERVER": environ.get("EMAIL_SERVER", "localhost"), + "PORT": _environ_get_and_map("EMAIL_PORT", 25, _AS_INT), + "USERNAME": environ.get("EMAIL_USERNAME", ""), + "PASSWORD": _read_secret("email_password", environ.get("EMAIL_PASSWORD", "")), + "USE_SSL": _environ_get_and_map("EMAIL_USE_SSL", "False", _AS_BOOL), + "USE_TLS": _environ_get_and_map("EMAIL_USE_TLS", "False", _AS_BOOL), + "SSL_CERTFILE": environ.get("EMAIL_SSL_CERTFILE", ""), + "SSL_KEYFILE": environ.get("EMAIL_SSL_KEYFILE", ""), + "TIMEOUT": _environ_get_and_map("EMAIL_TIMEOUT", 10, _AS_INT), # seconds + "FROM_EMAIL": environ.get("EMAIL_FROM", ""), +} + +# Enforcement of unique IP space can be toggled on a per-VRF basis. To enforce unique IP space within the global table +# (all prefixes and IP addresses not assigned to a VRF), set ENFORCE_GLOBAL_UNIQUE to True. +if "ENFORCE_GLOBAL_UNIQUE" in environ: + ENFORCE_GLOBAL_UNIQUE = _environ_get_and_map("ENFORCE_GLOBAL_UNIQUE", None, _AS_BOOL) + +# Exempt certain models from the enforcement of view permissions. Models listed here will be viewable by all users and +# by anonymous users. List models in the form `.`. Add '*' to this list to exempt all models. +EXEMPT_VIEW_PERMISSIONS = _environ_get_and_map("EXEMPT_VIEW_PERMISSIONS", "", _AS_LIST) + +# HTTP proxies NetBox should use when sending outbound HTTP requests (e.g. for webhooks). +# HTTP_PROXIES = { +# 'http': 'http://10.10.1.10:3128', +# 'https': 'http://10.10.1.10:1080', +# } + +# IP addresses recognized as internal to the system. The debugging toolbar will be available only to clients accessing +# NetBox from an internal IP. +INTERNAL_IPS = _environ_get_and_map("INTERNAL_IPS", "127.0.0.1 ::1", _AS_LIST) + +# Enable GraphQL API. +if "GRAPHQL_ENABLED" in environ: + GRAPHQL_ENABLED = _environ_get_and_map("GRAPHQL_ENABLED", None, _AS_BOOL) + +# # Enable custom logging. Please see the Django documentation for detailed guidance on configuring custom logs: +# # https://docs.djangoproject.com/en/stable/topics/logging/ +# LOGGING = {} + +# Automatically reset the lifetime of a valid session upon each authenticated request. Enables users to remain +# authenticated to NetBox indefinitely. +LOGIN_PERSISTENCE = _environ_get_and_map("LOGIN_PERSISTENCE", "False", _AS_BOOL) + +# Setting this to True will permit only authenticated users to access any part of NetBox. By default, anonymous users +# are permitted to access most data in NetBox (excluding secrets) but not make any changes. +LOGIN_REQUIRED = _environ_get_and_map("LOGIN_REQUIRED", "False", _AS_BOOL) + +# The length of time (in seconds) for which a user will remain logged into the web UI before being prompted to +# re-authenticate. (Default: 1209600 [14 days]) +LOGIN_TIMEOUT = _environ_get_and_map("LOGIN_TIMEOUT", 1209600, _AS_INT) + +# Setting this to True will display a "maintenance mode" banner at the top of every page. +if "MAINTENANCE_MODE" in environ: + MAINTENANCE_MODE = _environ_get_and_map("MAINTENANCE_MODE", None, _AS_BOOL) + +# Maps provider +if "MAPS_URL" in environ: + MAPS_URL = environ.get("MAPS_URL", None) + +# An API consumer can request an arbitrary number of objects =by appending the "limit" parameter to the URL (e.g. +# "?limit=1000"). This setting defines the maximum limit. Setting it to 0 or None will allow an API consumer to request +# all objects by specifying "?limit=0". +if "MAX_PAGE_SIZE" in environ: + MAX_PAGE_SIZE = _environ_get_and_map("MAX_PAGE_SIZE", None, _AS_INT) + +# The file path where uploaded media such as image attachments are stored. A trailing slash is not needed. Note that +# the default value of this setting is derived from the installed location. +MEDIA_ROOT = environ.get("MEDIA_ROOT", join(_BASE_DIR, "media")) + +# Expose Prometheus monitoring metrics at the HTTP endpoint '/metrics' +METRICS_ENABLED = _environ_get_and_map("METRICS_ENABLED", "False", _AS_BOOL) + +# Determine how many objects to display per page within a list. (Default: 50) +if "PAGINATE_COUNT" in environ: + PAGINATE_COUNT = _environ_get_and_map("PAGINATE_COUNT", None, _AS_INT) + +# # Enable installed plugins. Add the name of each plugin to the list. +# PLUGINS = [] + +# # Plugins configuration settings. These settings are used by various plugins that the user may have installed. +# # Each key in the dictionary is the name of an installed plugin and its value is a dictionary of settings. +# PLUGINS_CONFIG = { +# } + +# When determining the primary IP address for a device, IPv6 is preferred over IPv4 by default. Set this to True to +# prefer IPv4 instead. +if "PREFER_IPV4" in environ: + PREFER_IPV4 = _environ_get_and_map("PREFER_IPV4", None, _AS_BOOL) + +# The default value for the amperage field when creating new power feeds. +if "POWERFEED_DEFAULT_AMPERAGE" in environ: + POWERFEED_DEFAULT_AMPERAGE = _environ_get_and_map("POWERFEED_DEFAULT_AMPERAGE", None, _AS_INT) + +# The default value (percentage) for the max_utilization field when creating new power feeds. +if "POWERFEED_DEFAULT_MAX_UTILIZATION" in environ: + POWERFEED_DEFAULT_MAX_UTILIZATION = _environ_get_and_map( + "POWERFEED_DEFAULT_MAX_UTILIZATION", None, _AS_INT + ) + +# The default value for the voltage field when creating new power feeds. +if "POWERFEED_DEFAULT_VOLTAGE" in environ: + POWERFEED_DEFAULT_VOLTAGE = _environ_get_and_map("POWERFEED_DEFAULT_VOLTAGE", None, _AS_INT) + +# Rack elevation size defaults, in pixels. For best results, the ratio of width to height should be roughly 10:1. +if "RACK_ELEVATION_DEFAULT_UNIT_HEIGHT" in environ: + RACK_ELEVATION_DEFAULT_UNIT_HEIGHT = _environ_get_and_map( + "RACK_ELEVATION_DEFAULT_UNIT_HEIGHT", None, _AS_INT + ) +if "RACK_ELEVATION_DEFAULT_UNIT_WIDTH" in environ: + RACK_ELEVATION_DEFAULT_UNIT_WIDTH = _environ_get_and_map( + "RACK_ELEVATION_DEFAULT_UNIT_WIDTH", None, _AS_INT + ) + +# Remote authentication support +REMOTE_AUTH_ENABLED = _environ_get_and_map("REMOTE_AUTH_ENABLED", "False", _AS_BOOL) +REMOTE_AUTH_BACKEND = environ.get("REMOTE_AUTH_BACKEND", "netbox.authentication.RemoteUserBackend") +REMOTE_AUTH_HEADER = environ.get("REMOTE_AUTH_HEADER", "HTTP_REMOTE_USER") +REMOTE_AUTH_AUTO_CREATE_USER = _environ_get_and_map( + "REMOTE_AUTH_AUTO_CREATE_USER", "True", _AS_BOOL +) +REMOTE_AUTH_DEFAULT_GROUPS = _environ_get_and_map("REMOTE_AUTH_DEFAULT_GROUPS", "", _AS_LIST) +# REMOTE_AUTH_DEFAULT_PERMISSIONS = {} + +# This repository is used to check whether there is a new release of NetBox available. Set to None to disable the +# version check or use the URL below to check for release in the official NetBox repository. +RELEASE_CHECK_URL = environ.get("RELEASE_CHECK_URL", None) +# RELEASE_CHECK_URL = 'https://api.github.com/repos/netbox-community/netbox/releases' + +# Maximum execution time for background tasks, in seconds. +RQ_DEFAULT_TIMEOUT = _environ_get_and_map("RQ_DEFAULT_TIMEOUT", 300, _AS_INT) + +# The name to use for the csrf token cookie. +CSRF_COOKIE_NAME = environ.get("CSRF_COOKIE_NAME", "csrftoken") + +# Cross-Site-Request-Forgery-Attack settings. If Netbox is sitting behind a reverse proxy, you might need to set the CSRF_TRUSTED_ORIGINS flag. +# Django 4.0 requires to specify the URL Scheme in this setting. An example environment variable could be specified like: +# CSRF_TRUSTED_ORIGINS=https://demo.netbox.dev http://demo.netbox.dev +CSRF_TRUSTED_ORIGINS = _environ_get_and_map("CSRF_TRUSTED_ORIGINS", "", _AS_LIST) + +# The name to use for the session cookie. +SESSION_COOKIE_NAME = environ.get("SESSION_COOKIE_NAME", "sessionid") + +# By default, NetBox will store session data in the database. Alternatively, a file path can be specified here to use +# local file storage instead. (This can be useful for enabling authentication on a standby instance with read-only +# database access.) Note that the user as which NetBox runs must have read and write permissions to this path. +SESSION_FILE_PATH = environ.get("SESSION_FILE_PATH", environ.get("SESSIONS_ROOT", None)) + +# Time zone (default: UTC) +TIME_ZONE = environ.get("TIME_ZONE", "UTC") + +# Date/time formatting. See the following link for supported formats: +# https://docs.djangoproject.com/en/stable/ref/templates/builtins/#date +DATE_FORMAT = environ.get("DATE_FORMAT", "N j, Y") +SHORT_DATE_FORMAT = environ.get("SHORT_DATE_FORMAT", "Y-m-d") +TIME_FORMAT = environ.get("TIME_FORMAT", "g:i a") +SHORT_TIME_FORMAT = environ.get("SHORT_TIME_FORMAT", "H:i:s") +DATETIME_FORMAT = environ.get("DATETIME_FORMAT", "N j, Y g:i a") +SHORT_DATETIME_FORMAT = environ.get("SHORT_DATETIME_FORMAT", "Y-m-d H:i") diff --git a/development/config/logging.py b/development/config/logging.py new file mode 100644 index 0000000..f762e22 --- /dev/null +++ b/development/config/logging.py @@ -0,0 +1,55 @@ +# # Remove first comment(#) on each line to implement this working logging example. +# # Add LOGLEVEL environment variable to netbox if you use this example & want a different log level. +# from os import environ + +LOGLEVEL = "DEBUG" + +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "verbose": { + "format": "{levelname} {asctime} {module} {process:d} {thread:d} {message}", + "style": "{", + }, + "simple": { + "format": "{levelname} {message}", + "style": "{", + }, + }, + "filters": { + "require_debug_false": { + "()": "django.utils.log.RequireDebugFalse", + }, + }, + "handlers": { + "console": { + "level": LOGLEVEL, + "filters": ["require_debug_false"], + "class": "logging.StreamHandler", + "formatter": "simple", + }, + "mail_admins": { + "level": LOGLEVEL, + "class": "django.utils.log.AdminEmailHandler", + "filters": ["require_debug_false"], + }, + }, + "loggers": { + "django": { + "handlers": ["console"], + "propagate": True, + }, + "django.request": { + "handlers": ["mail_admins"], + "level": LOGLEVEL, + "propagate": False, + }, + "django_auth_ldap": { + "handlers": [ + "console", + ], + "level": LOGLEVEL, + }, + }, +} diff --git a/development/config/plugins.py b/development/config/plugins.py new file mode 100644 index 0000000..2040f88 --- /dev/null +++ b/development/config/plugins.py @@ -0,0 +1,49 @@ +PLUGINS = [ + "netbox_inventory", + "netbox_qrgen", +] + +PLUGINS_CONFIG = { + "netbox_inventory": { + "top_level_menu": True, + "used_status_name": "used", + "stored_status_name": "stored", + "sync_hardware_serial_asset_tag": True, + "asset_import_create_purchase": True, + "asset_import_create_device_type": True, + "asset_import_create_module_type": True, + "asset_import_create_inventoryitem_type": True, + "asset_import_create_tenant": True, + "prefill_asset_name_create_inventoryitem": True, + "prefill_asset_tag_create_inventoryitem": True, + }, + "netbox_qrgen": { + "qr_with_text": True, + "qr_text_fields": ["name", "serial"], + "qr_font": "TahomaBold", + "qr_custom_text": None, + "qr_text_location": "bottom", + "qr_version": 2, + "qr_error_correction": 2, + "qr_box_size": 6, + "qr_border_size": 4, + "labels": { + "dcim.cable": [ + "tenant", + "a_terminations.device", + "a_terminations.name", + "b_terminations.device", + "b_terminations.name", + ], + "dcim.rack": [ + "tenant", + "site", + "facility_id", + "name", + ], + "dcim.device": ["tenant", "name", "serial"], + "dcim.inventoryitem": ["tenant", "name", "serial"], + "circuits.circuit": ["tenant", "name", "serial"], + }, + }, +} diff --git a/development/docker-compose.yml b/development/docker-compose.yml new file mode 100644 index 0000000..07a5fe9 --- /dev/null +++ b/development/docker-compose.yml @@ -0,0 +1,85 @@ +version: "3" + +x-netbox: + &netbox + image: netbox-dev:3.5 + build: . + restart: unless-stopped + user: "unit:root" + env_file: config.env + security_opt: [ "no-new-privileges:true" ] + networks: + - netbox + volumes: + - ./config/:/etc/netbox/config/:ro + - ./reports/:/opt/netbox/netbox/reports/ + - ./scripts/:/opt/netbox/netbox/scripts/ + - ./media/:/opt/netbox/netbox/media/ + +services: + + web: + <<: *netbox + container_name: netbox-dev-web + security_opt: [ "no-new-privileges:true" ] + depends_on: + - db + - redis + ports: + - "8080:8080" + + worker: + <<: *netbox + container_name: netbox-dev-worker + depends_on: + - web + command: + [ + "/opt/netbox/venv/bin/python", + "/opt/netbox/netbox/manage.py", + "rqworker" + ] + + housekeeping: + <<: *netbox + container_name: netbox-dev-housekeeping + depends_on: + - web + command: [ "/opt/netbox/housekeeping.sh" ] + + db: + image: bitnami/postgresql:14.6.0 + container_name: netbox-dev-db + restart: unless-stopped + security_opt: [ "no-new-privileges:true" ] + user: "1000" + networks: + - netbox + volumes: + - ./db-data/:/bitnami/postgresql/ + - /etc/localtime:/etc/localtime:ro + environment: + - TZ=Europe/Zurich + - POSTGRESQL_POSTGRES_PASSWORD=${DB_ROOT_PASSWORD} + - POSTGRES_DB=netbox + - POSTGRES_USER=netbox + - POSTGRES_PASSWORD=${DB_PASSWORD} + + redis: + image: bitnami/redis:7.0 + container_name: netbox-dev-redis + restart: unless-stopped + security_opt: [ "no-new-privileges:true" ] + user: "1000" + networks: + - netbox + volumes: + - ./redis-data/:/bitnami/redis/data/ + environment: + - TZ=Europe/Zurich + - ALLOW_EMPTY_PASSWORD=yes + +networks: + netbox: + name: netbox-dev + driver: bridge diff --git a/get_release_notes.sh b/get_release_notes.sh new file mode 100755 index 0000000..6fa7a0c --- /dev/null +++ b/get_release_notes.sh @@ -0,0 +1,52 @@ +#!/bin/bash +# shellcheck disable=SC2016 + +# script to extract the release notes from the changelog + +# show script help +function show_help() { + cat << EOF + + Script to generate release-notes from a changelog (CHANGELOG.md) + + Usage: + ./get_release_notes.sh + + + Example: + ./get_release_notes.sh "2.0.5" + or + ./get_release_notes.sh "latest" + +EOF + exit 0 +} + +# create changelog for release +function get_release_notes() { + local l_version="${1}" + + printf 'Creating release-notes\n' + # check for version + if [[ -z "${l_version}" ]]; then + printf 'You need to specify a version with $1\n' + exit 1 + fi + if [[ ${l_version,,} == "latest" ]]; then + l_version="$(grep -o -E "^##\s\[[0-9]{1,2}.[0-9]{1,2}.[0-9]{1,2}\]" CHANGELOG.md | head -n 1 | grep -o -E "[0-9]{1,2}.[0-9]{1,2}.[0-9]{1,2}")" + fi + awk -v ver="[${l_version}]" \ + '/^## / { if (p) { exit }; if ($2 == ver) { p=1 } } p && NF' \ + 'CHANGELOG.md' > 'RELEASENOTES.md' + printf 'Done\n' +} + +# check options +case "${1}" in + '--help' | '-h' | 'help') + show_help + ;; + *) + get_release_notes "${@}" + ;; +esac diff --git a/justfile b/justfile new file mode 100755 index 0000000..f44f968 --- /dev/null +++ b/justfile @@ -0,0 +1,131 @@ +#!/usr/bin/env just --justfile + +default: show_receipts + +set shell := ["bash", "-uc"] +set dotenv-load := true +#set export + +# aliases +alias s := show_receipts +alias i := show_system_info +alias p := prepare_workspace +alias l := lint +alias b := build + +# variables +export asdf_version := "v0.11.2" + +# default recipe to display help information +show_receipts: + @just --list + +show_system_info: + @echo "==================================" + @echo "os : {{os()}}" + @echo "arch: {{arch()}}" + @echo "home: ${HOME}" + @echo "project dir: {{justfile_directory()}}" + @echo "==================================" + +install_asdf: + @if asdf --version; then \ + echo "asdf already installed installed"; exit 1 \ + ;fi + @echo "installing asdf" + @echo "asdf version: ${asdf_version}" + @git clone https://github.com/asdf-vm/asdf.git ~/.asdf --branch "${asdf_version}" + @echo "adding asdf to .bashrc" + @if ! grep -q ".asdf/asdf.sh" "${HOME}/.bashrc"; then \ + echo -e '\n# source asdf' >> "${HOME}/.bashrc" \ + ;echo 'source "${HOME}/.asdf/asdf.sh"' >> "${HOME}/.bashrc" \ + ;echo -e 'source "${HOME}/.asdf/completions/asdf.bash"\n' >> "${HOME}/.bashrc" \ + ;fi + @echo "to load asdf either restart your shell or do: 'source \${HOME}/.bashrc'" + +setup_asdf: + @echo "installing asdf bins" + # add plugins + -@asdf plugin add python + -@asdf plugin add just + -@asdf plugin add direnv + # install bins + @asdf install + # setup direnv + @asdf direnv setup --shell bash --version latest + +setup_hooks: + @echo "preparing repo hooks" + @if ! lefthook version; then \ + echo "lefthook not installed. please install it: https://github.com/evilmartians/lefthook"; exit 1 \ + ;fi + @echo "installing pre-commit hooks" \ + @lefthook install + +create_venv: + @echo "creating venv" + @python3 -m pip install --upgrade pip setuptools wheel + @python3 -m venv venv + +install_deps: + @echo "installing dependencies" + @pip3 install -r requirements.txt + +install_deps_dev: + @echo "installing dev dependencies" + @pip3 install -r requirements_dev.txt + +create_reqs: + @echo "creating requirements" + @pipreqs --mode gt --force + +test_shfmt: + @find . -type f \( -name "**.sh" -and -not -path "./.**" -and -not -path "./venv**" \) -exec shfmt -d -i 4 -bn -ci -sr "{}" \+; + +test_black: + @python3 -m black --check --diff . + +test_pyright: + @python3 -m pyright . + +test_ruff: + @python3 -m ruff --diff . + +test_ci_conf: + @woodpecker-cli lint .woodpecker/ + +test_pytest: + @python3 -m tox -e basic + +test_coverage: + @python3 -m tox -e coverage + +test_tox: + @python3 -m tox + +build_package: + @python3 -m hatch build --clean + +build_container: + @docker build . -f development/Dockerfile -t netbox-dev:3.5 + +# install dependecies and set everything up +prepare_workspace: + just show_system_info + -just install_asdf + just setup_asdf + just create_venv + just setup_hooks + @echo -e "\n\033[0;32m=== ALL DONE ===\033[0m\n" + +lint: + just show_system_info + -just test_ci_conf + just test_black + just test_pyright + just test_ruff + @echo -e "\n\033[0;32m=== ALL DONE ===\033[0m\n" + +build: + just build_container + @echo -e "\n\033[0;32m=== ALL DONE ===\033[0m\n" diff --git a/netbox_qrgen/__about__.py b/netbox_qrgen/__about__.py new file mode 100644 index 0000000..f102a9c --- /dev/null +++ b/netbox_qrgen/__about__.py @@ -0,0 +1 @@ +__version__ = "0.0.1" diff --git a/netbox_qrgen/__init__.py b/netbox_qrgen/__init__.py new file mode 100644 index 0000000..8ddbf07 --- /dev/null +++ b/netbox_qrgen/__init__.py @@ -0,0 +1,44 @@ +from extras.plugins import PluginConfig + + +class NetBoxQRGenConfig(PluginConfig): + name = "netbox_qrgen" + verbose_name = "NetBox QRGen" + description = "NetBox plugin to generate QR codes for assets" + version = "0.0.1" + base_url = "qrgen" + min_version = "3.4.0" + author = "Ivan Schaller" + author_email = "ivan@schaller.sh" + default_settings = { + "qr_with_text": True, + "qr_text_fields": ["name", "serial"], + "qr_font": "Tahoma", + "qr_custom_text": None, + "qr_text_location": "right", + "qr_version": 2, + "qr_error_correction": 1, + "qr_box_size": 6, + "qr_border_size": 4, + "labels": { + "dcim.cable": [ + "tenant", + "a_terminations.device", + "a_terminations.name", + "b_terminations.device", + "b_terminations.name", + ], + "dcim.rack": [ + "tenant", + "site", + "facility_id", + "name", + ], + "dcim.device": ["tenant", "name", "serial"], + "dcim.inventoryitem": ["tenant", "name", "serial"], + "circuits.circuit": ["tenant", "name", "serial"], + }, + } + + +config = NetBoxQRGenConfig diff --git a/netbox_qrgen/fonts/ArialBlack.ttf b/netbox_qrgen/fonts/ArialBlack.ttf new file mode 100644 index 0000000..b51c8c2 Binary files /dev/null and b/netbox_qrgen/fonts/ArialBlack.ttf differ diff --git a/netbox_qrgen/fonts/ArialMT.ttf b/netbox_qrgen/fonts/ArialMT.ttf new file mode 100644 index 0000000..b51dd89 Binary files /dev/null and b/netbox_qrgen/fonts/ArialMT.ttf differ diff --git a/netbox_qrgen/fonts/JetBrainsMono.ttf b/netbox_qrgen/fonts/JetBrainsMono.ttf new file mode 100644 index 0000000..dff66cc Binary files /dev/null and b/netbox_qrgen/fonts/JetBrainsMono.ttf differ diff --git a/netbox_qrgen/fonts/JetBrainsMonoBold.ttf b/netbox_qrgen/fonts/JetBrainsMonoBold.ttf new file mode 100644 index 0000000..8c93043 Binary files /dev/null and b/netbox_qrgen/fonts/JetBrainsMonoBold.ttf differ diff --git a/netbox_qrgen/fonts/Tahoma.ttf b/netbox_qrgen/fonts/Tahoma.ttf new file mode 100755 index 0000000..2d23b2d Binary files /dev/null and b/netbox_qrgen/fonts/Tahoma.ttf differ diff --git a/netbox_qrgen/fonts/TahomaBold.ttf b/netbox_qrgen/fonts/TahomaBold.ttf new file mode 100755 index 0000000..016a41f Binary files /dev/null and b/netbox_qrgen/fonts/TahomaBold.ttf differ diff --git a/netbox_qrgen/template_content.py b/netbox_qrgen/template_content.py new file mode 100644 index 0000000..95e5d97 --- /dev/null +++ b/netbox_qrgen/template_content.py @@ -0,0 +1,62 @@ +from django.conf import settings +from extras.plugins import PluginTemplateExtension + +from .utils import generate_qrcode, get_base64 + +plugin_settings = settings.PLUGINS_CONFIG.get("netbox_qrgen", {}) + + +class QRGen(PluginTemplateExtension): + def get_url(self) -> str: + obj = self.context["object"] + request = self.context["request"] + url: str = request.build_absolute_uri(obj.get_absolute_url()) + + return url + + def get_qrcode(self, url: str) -> str: + img = generate_qrcode(url=url, **plugin_settings) + b64 = get_base64(img) + + return b64 + + def right_page(self): + qr_img = self.get_qrcode(self.get_url()) + return self.render( + "netbox_qrgen/qrgen.html", + extra_context={"image": qr_img}, + ) + + +class QRGenDevice(QRGen): + model = "dcim.device" + # kind = "device" + + +class QRGenRack(QRGen): + model = "dcim.rack" + # kind = "rack" + + +class QRGenCable(QRGen): + model = "dcim.cable" + # kind = "cable" + + +class QRGenInventoryItem(QRGen): + model = "dcim.inventoryitem" + # kind = "inventoryitem" + + +class QRGenCircuit(QRGen): + model = "circuits.circuit" + # kind = "circuit" + + +template_extensions = [ + QRGenDevice, + QRGenRack, + QRGenCable, + QRGenInventoryItem, + QRGenCircuit, +] diff --git a/netbox_qrgen/templates/netbox_qrgen/qrgen.html b/netbox_qrgen/templates/netbox_qrgen/qrgen.html new file mode 100644 index 0000000..9548e4f --- /dev/null +++ b/netbox_qrgen/templates/netbox_qrgen/qrgen.html @@ -0,0 +1,26 @@ + +
+
QR Code
+
+ +
+ +
diff --git a/netbox_qrgen/utils.py b/netbox_qrgen/utils.py new file mode 100644 index 0000000..c00e5f3 --- /dev/null +++ b/netbox_qrgen/utils.py @@ -0,0 +1,31 @@ +import base64 +from io import BytesIO +from typing import Union + +import qrcode +from qrcode.image.pure import PyPNGImage +from qrcode.image.svg import SvgPathFillImage + + +def generate_qrcode(url: str, **kwargs) -> PyPNGImage: + qr = qrcode.QRCode( + version=kwargs["qr_version"], + error_correction=kwargs["qr_error_correction"], + box_size=kwargs["qr_box_size"], + border=kwargs["qr_border_size"], + image_factory=PyPNGImage, + ) + qr.add_data(url) + qr.make(fit=True) + + img = qr.make_image(fill_color="black", back_color="white") + + return img + + +def get_base64(img: Union[SvgPathFillImage, PyPNGImage]) -> str: + stream = BytesIO() + img.save(stream) + base64_value = base64.b64encode(stream.getvalue()).decode(encoding="ascii") + + return base64_value diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..f62a5dd --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,142 @@ +[build-system] +requires = ["hatchling>=1.11.0"] +build-backend = "hatchling.build" + +[project] +dynamic = ["version"] +name = "netbox-qrgen" +description = "A netbox plugin to generate qrcodes for assets" +readme = "README.md" +license = "MIT" +requires-python = ">=3.9" +authors = [{ name = "Ivan Schaller", email = "ivan@schaller.sh" }] +keywords = ["netbox", "plugin", "qrcode"] +classifiers = [ + "License :: OSI Approved :: MIT License", + "Natural Language :: English", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", +] +dependencies = ["qrcode>=7.4.2", "pillow>=9.5.0"] + +[project.urls] +Homepage = "https://github.com/olofvndrhr/manga-dlp" +History = "https://github.com/olofvndrhr/manga-dlp/commits/master" +Tracker = "https://github.com/olofvndrhr/manga-dlp/issues" +Source = "https://github.com/olofvndrhr/manga-dlp" + +[tool.hatch.version] +path = "netbox_qrgen/__about__.py" + +[tool.hatch.build] +ignore-vcs = true + +[tool.hatch.build.targets.sdist] +packages = ["netbox_qrgen"] + +[tool.hatch.build.targets.wheel] +packages = ["netbox_qrgen"] + +# black + +[tool.black] +line-length = 100 +target-version = ["py39"] + +# pyright + +[tool.pyright] +typeCheckingMode = "basic" +pythonVersion = "3.9" +reportUnnecessaryTypeIgnoreComment = true +reportShadowedImports = true +reportUnusedExpression = true +reportMatchNotExhaustive = true +# venvPath = "." +# venv = "venv" + +# ruff + +[tool.ruff] +target-version = "py39" +select = [ + "E", # pycodetyle err + "W", # pycodetyle warn + "D", # pydocstyle + "C90", # mccabe + "I", # isort + "PLE", # pylint err + "PLW", # pylint warn + "PLC", # pylint convention + "PLR", # pylint refactor + "F", # pyflakes + "RUF", # ruff specific +] +line-length = 100 +fix = true +show-fixes = true +format = "grouped" +ignore-init-module-imports = true +respect-gitignore = true +ignore = ["E501", "D103", "D100", "D102", "PLR2004", "D403"] +#unfixable = ["F401"] +exclude = [ + ".direnv", + ".git", + ".mypy_cache", + ".ruff_cache", + ".svn", + ".venv", + "venv", + "__pypackages__", + "build", + "dist", + "venv", +] + +[tool.ruff.per-file-ignores] +"__init__.py" = ["D104"] + +[tool.ruff.pylint] +max-args = 10 + +[tool.ruff.mccabe] +max-complexity = 10 + +[tool.ruff.pydocstyle] +convention = "google" + +[tool.ruff.pycodestyle] +max-doc-length = 100 + +# pytest + +[tool.pytest.ini_options] +pythonpath = ["."] + +# coverage + +[tool.coverage.run] +source = ["mangadlp"] +branch = true +command_line = "-m pytest --exitfirst" + +[tool.coverage.report] +# Regexes for lines to exclude from consideration +exclude_lines = [ + # Have to re-enable the standard pragma + "pragma: no cover", + # Don't complain about missing debug-only code: + "def __repr__", + "if self.debug", + # Don't complain if tests don't hit defensive assertion code: + "raise AssertionError", + "raise NotImplementedError", + # Don't complain if non-runnable code isn't run: + "if 0:", + "if __name__ == .__main__.:", + # Don't complain about abstract methods, they aren't run: + "@(abc.)?abstractmethod", +] +ignore_errors = true diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..69366a6 --- /dev/null +++ b/renovate.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["local>44net-assets/docker-renovate-conf"] +} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..1eaad21 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +qrcode>=7.4.2 +pillow>=9.5.0 diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..3341d45 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,12 @@ +sonar.projectKey=olofvndrhr:netbox-qrgen +sonar.projectName=olofvndrhr/netbox-qrgen +sonar.links.homepage=https://github.com/olofvndrhr/netbox-qrgen +sonar.links.scm=https://github.com/olofvndrhr/netbox-qrgen +sonar.links.issue=https://github.com/olofvndrhr/netbox-qrgen/issues +sonar.links.ci=https://ci.44net.ch/olofvndrhr/netbox-qrgen +# +sonar.sources=netbox_qrgen +#sonar.tests= +#sonar.exclusions= +sonar.python.version=3.9 +#sonar.python.coverage.reportPaths=coverage.xml