diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f508c25 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,29 @@ +**/__pycache__ +**/.venv +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/bin +**/charts +**/docker-compose* +**/compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE + +# Part of Setup.py and therefore important for plugin installation. +# README.md \ No newline at end of file diff --git a/.github/workflows/pub-pypi.yml b/.github/workflows/pub-pypi.yml index ebabf78..f06738b 100644 --- a/.github/workflows/pub-pypi.yml +++ b/.github/workflows/pub-pypi.yml @@ -10,10 +10,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@main - - name: Set up Python 3.7 + - name: Set up Python 3.12 uses: actions/setup-python@v1 with: - python-version: 3.7 + python-version: 3.12 - name: Install pypa/build run: >- python -m diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..2324b25 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,26 @@ +// Is required for debugging Docker-Compose in VSCode. +{ + "configurations": [ + { + + "name": "Docker: Python - Django", + "type": "debugpy", + "request": "attach", + "connect": { + "host": "localhost", + "port": 5678 + }, + + "pathMappings": [ + { + "localRoot": "${workspaceFolder}", + "remoteRoot": "/source" + } + ], + + "django": true, + + "justMyCode": true, + } + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..734d611 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,19 @@ +// Is required for debugging Docker-Compose in VSCode. +{ + "version": "2.0.0", + "tasks": [ + { + "label": "docker-run: debug", + "type": "docker-compose", + "dockerCompose": { + "up": { + "detached": true, + "build": true + }, + "files": [ + "${workspaceFolder}/develop/docker-compose.yml", + ] + } + } + ] + } diff --git a/CODEOWNERS b/CODEOWNERS index 1d3b141..eeda945 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1 +1 @@ -* @k01ek @cruse1977 @natm +* @k01ek @cruse1977 @cvitan diff --git a/Makefile b/Makefile index 414332d..c15f746 100644 --- a/Makefile +++ b/Makefile @@ -1,46 +1,70 @@ +# General: The Makefile is used for plugin development is used to simplify the execution of command sequences. +# Recommended VSCode plugin: Makefile-Tools (extension ID: ms-vscode.makefile-tools) +# To trigger a Makefile command (target), the following line must be written in the terminal window of VSCode, for example: +# make cbuild + +# Parameter PYTHON_VER?=3.12 -NETBOX_VER?=v4.0.2 +NETBOX_VER?=v4.3.1 NAME=netbox-qrcode COMPOSE_FILE=./develop/docker-compose.yml +COMPOSE_FILE_DEBUG=./develop/docker-compose-debug.yml BUILD_NAME=netbox_qrcode VERFILE=./netbox_qrcode/version.py - +# Build Docker with the specific Python and Netbox version cbuild: docker-compose -f ${COMPOSE_FILE} \ -p ${BUILD_NAME} build \ --build-arg netbox_ver=${NETBOX_VER} \ --build-arg python_ver=${PYTHON_VER} -debug: +# Start Docker with terminal window output +debug: cbuild @echo "Starting Netbox .. " docker-compose -f ${COMPOSE_FILE} -p ${BUILD_NAME} up +# Start Docker with terminal window output. +# Brakepoints in e.g. Python files are supported in VSCode. Changes in Python and HTML are applied after saving. +debug-vscode: cbuild + @echo "Starting Netbox debug for VSCode.. " + docker-compose -f ${COMPOSE_FILE} -p ${BUILD_NAME} -f ${COMPOSE_FILE_DEBUG} up +# --build + +# Start Docker without connecting to the terminal window. (Runs independently of the terminal window.). start: @echo "Starting Netbox in detached mode.. " docker-compose -f ${COMPOSE_FILE} -p ${BUILD_NAME} up -d +# Stop Docker Container stop: docker-compose -f ${COMPOSE_FILE} -p ${BUILD_NAME} down +# Stop Docker and remove containers destroy: docker-compose -f ${COMPOSE_FILE} -p ${BUILD_NAME} down docker volume rm -f ${BUILD_NAME}_pgdata_netbox_qrcode +# Calls the Netbox shell. Exit with exit() +# NetBox includes a Python shell within which objects can be directly queried, created, modified, and deleted. nbshell: docker-compose -f ${COMPOSE_FILE} -p ${BUILD_NAME} run netbox python manage.py nbshell +# Calls the Python shell. Exit with exit() shell: docker-compose -f ${COMPOSE_FILE} -p ${BUILD_NAME} run netbox python manage.py shell +# To create the Django Superuser to be able to log on to Netbox Web. adduser: docker-compose -f ${COMPOSE_FILE} -p ${BUILD_NAME} run netbox python manage.py createsuperuser +# Django collectstatic collectstatic: docker-compose -f ${COMPOSE_FILE} -p ${BUILD_NAME} run netbox python manage.py collectstatic +# Migrate the Netbox system depending on the Django model migrations: docker-compose -f ${COMPOSE_FILE} -p ${BUILD_NAME} up -d postgres docker-compose -f ${COMPOSE_FILE} -p ${BUILD_NAME} \ diff --git a/README.md b/README.md index 2b2f6dc..98e263c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Netbox QR Code Plugin -[Netbox](https://github.com/netbox-community/netbox) plugin for generate QR codes for objects: Rack, Device, Cable. +[Netbox](https://github.com/netbox-community/netbox) plugin for generate QR codes for objects: Device, Module, Cable, Powerfeed, Powerpanel, Location This plugin depends on [qrcode](https://github.com/lincolnloop/python-qrcode) and [Pillow](https://github.com/python-pillow/Pillow) python library @@ -10,10 +10,14 @@ This plugin depends on [qrcode](https://github.com/lincolnloop/python-qrcode) an ## Compatibility -| Plugin Version | NetBox Version | Tested on | -| ------------- |:-------------| :-----:| -| 0.0.11 | >= 3.7.0 | 3.7.x | -| 0.0.13 | >= 4.0.2 | v4.0.2 | +| Plugin Version | NetBox Version | Tested on | +| ------------- |:-------------| :-----------:| +| 0.0.11 | 3.7.x | 3.7.x | +| 0.0.14 | 4.0.x | 4.0.11 | +| 0.0.15 | 4.1.x | 4.1.6 | +| 0.0.17 | 4.2.x | 4.2.4 | +| 0.0.18 | 4.3.x | 4.3.1 | + ## Installation @@ -36,80 +40,72 @@ Restart NetBox and add `netbox-qrcode` to your local_requirements.txt ## Configuration -The following options are available: +### Label Design -* `with_text`: Boolean (default True). Text label will be added to QR code image if enabled. -* `text_template`: [Jinja2](https://jinja.palletsprojects.com/) template with {{ obj }} as context for device, rack, etc., using it ignores `text_fields` and `custom_text`. +Extensive label customisation is possible, it's also possible to include different labels for each object type, for example 2 labels for the Device view. - Example to output name and site in two lines with caption (See also screenshots below): - ``` - 'text_template': 'Name: {{ obj.name }}\nSite: {{ obj.site }}', - ``` -* `text_fields`: List of String (default ['name']). Text fields of an object that will be added as text label to QR image. It's possible to use custom field values. -* `font`: String (default TahomaBold) Font name for text label ( Some font include in package, see fonts dir). -* `text_location`: Where to render the text, relative to the QR code. Valid values are `"right"` (default), `"left"`", `"up"`, and `"down"`. -* `custom_text`: String or None (default None) additional text label to QR code image (will be added after text_fields). -* `qr_version`: Integer (default 1) parameter is an integer from 1 to 40 that controls the size of -the QR Code (the smallest, version 1, is a 21x21 matrix). -* `qr_error_correction`: Integer (default 0), controls the error correction used for the -QR Code. The following values are available: +For advice on configuration please see the two links below: - 1 - About 7% or less errors can be corrected. - 0 - About 15% or less errors can be corrected. - 2 - About 30% or less errors can be corrected. - 3 - About 25% or less errors can be corrected. +- [General Configuration >>](docs/README_Subpages/README_Configuration.md) +- [Configuration Examples >>](docs/README_Subpages/README_Configuration_ExampleLabelConf.md) -* `qr_box_size`: Integer (default 6), controls how many pixels each "box" of the QR code -is. -* `qr_border`: Integer (default 4), controls how many boxes thick the border should be -(the default is 4, which is the minimum according to the specs). + -### Per object options -Per object options override default options. Per object options dictionary can contains any of default options inside. +### Printing + +#### Setting the label printer + +If the print does not look like the preview in the Netbox, first try to get a perfect print using Word. As many printer settings also have an influence on the print result. Borderless printing is possible if the printer (e.g. thermal transfer printer) supports this. + + + + +Here is an example of what needs to be considered to print borderless from a Word document. [Go to: Example Zebra ZM400 300dpi label printer and a label 56x32mm. >>](/docs/img/Configuration_Printer_ZM400.png) + + +#### Setting Browser Print Settings + +When you press the “Print” button, there are some print properties that are added by the browser. However, these interfere with the print result. They should therefore be deactivated. + +##### Firefox: + +| Parameter | Value | +| --------------------------------------------- | --------------------------- | +| Orientation | Portrait | +| Paper size | User defined | +| Margins | none | +| Scale | Fit to page width or 100% | +| Options --> Print headers and footers | disable | +| Options --> Print backgrounds | disable | + +##### Chrome: +Chrome can alter settings between printing and the print preview, therefore the below settings are recomended + +| Parameter | Value | +| --------------------------------------------- | --------------------------- | +| Layout | Portrait | +| Paper size | empty !!! | +| Pages per sheet | 1 | +| Margins | none | +| Scale | Default or 100% | +| Options --> PBackground grafics | disable | + + + -* `device`: Dict or None (default {'text_fields': ['name', 'serial']}), set None to disble QR code -* `rack`: Dict or None (default {'text_fields': ['name']}) , set None to disble QR code -* `cable`: Dict or None ( defaul {'text_fields': ['_termination_a_device', 'termination_a', '_termination_b_device', 'termination_b',]}), set None to disble QR code -Configuration example: -``` -PLUGINS_CONFIG = { - 'netbox_qrcode': { - 'with_text': True, - 'text_fields': ['name', 'serial'], - 'font': 'ArialMT', - 'font_size': 12, # If the value is 0 or the line does not exist, then the text is automatically adjusted - 'custom_text': 'Property of SomeCompany\ntel.8.800333554-CALL', - 'text_location': 'up', - 'qr_version': 1, - 'qr_error_correction': 0, - 'qr_box_size': 4, - 'qr_border': 4, - # per object options - 'cable': None, # disable QR code for Cable object - 'rack': { - 'text_fields': [ - 'site', - 'name', - 'facility_id', - 'tenant', - 'cf.cf_name' - ] - }, - 'device': { - 'qr_box_size': 6, - 'custom_text': None, - } - } -} -``` ## Contributing -Developing tools for this project based on [ntc-netbox-plugin-onboarding](https://github.com/networktocode/ntc-netbox-plugin-onboarding) repo. Issues and pull requests are welcomed. +## Development + +You would like to help with the development of the plugin. Here you can find further information such as how to set up debugging in vscode and much more. + +- [Go to Development >>](docs/README_Subpages/README_Development.md) + ## Screenshots Device QR code with text label @@ -120,6 +116,3 @@ Rack QR code Cable QR code  - -Device QR code via Jinja2 "text_template" Parameter (Multiline and labeled) - \ No newline at end of file diff --git a/develop/Dockerfile b/develop/Dockerfile index a2c56d2..49a950a 100644 --- a/develop/Dockerfile +++ b/develop/Dockerfile @@ -1,12 +1,14 @@ ARG python_ver=3.12 FROM python:${python_ver} -ARG netbox_ver=master +ARG netbox_ver=main ENV PYTHONUNBUFFERED 1 RUN mkdir -p /opt RUN pip install --upgrade pip +RUN pip install setuptools + # ------------------------------------------------------------------------------------- # Install NetBox @@ -23,6 +25,6 @@ WORKDIR /source COPY . /source #RUN pip install -r requirements.txt -RUN python setup.py develop +RUN python -m pip install -e . WORKDIR /opt/netbox/netbox/ diff --git a/develop/Netbox_Icon.png b/develop/Netbox_Icon.png new file mode 100644 index 0000000..f77561b Binary files /dev/null and b/develop/Netbox_Icon.png differ diff --git a/develop/configuration.py b/develop/configuration.py index b564c3f..fdc1217 100644 --- a/develop/configuration.py +++ b/develop/configuration.py @@ -157,7 +157,225 @@ # 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 = {} +PLUGINS_CONFIG = { + + 'netbox_qrcode': { + + ## Example Template for Devices + + 'device_2': { + 'title': 'Example 2 (Template for Device)', + 'text_template': '
😝 My label design 😝
', + + 'label_width': '56mm', + 'label_height': '32mm', + 'label_edge_top': '0mm', + 'label_edge_left': '0mm', + 'label_edge_right': '0mm', + }, + + + ## Example Template for Cables + + 'cable': { + 'title': 'Example 1 (Template for Cable)', + 'with_qr': False, + 'label_edge_left': '0.00mm', + 'label_edge_right': '0.00mm', + 'label_edge_top': '0.00mm', + 'text_align_vertical': 'middle', + 'text_align_horizontal': 'center', + 'text_template': '' + '{{ obj.label }}' + '{{ obj.label }}' + '{{ obj.label }}' + '{{ obj.label }}' + '{{ obj.label }}' + '{{ obj.label }}' + '{{ obj.label }}' + '{{ obj.label }}' + '{{ obj.label }}' + '{{ obj.label }}' + '{{ obj.label }}' + '{{ obj.label }}' + '{{ obj.label }}' + '' + }, + + 'cable_2': { + 'title': 'Example 2 (Template for Cable)', + 'with_qr': False, + 'label_edge_left': '0.00mm', + 'label_edge_right': '0.00mm', + 'label_edge_top': '0.00mm', + 'text_align_vertical': 'middle', + 'text_align_horizontal': 'center', + + # QR-Code Image File + 'qr_version': 1, + 'qr_error_correction': 1, + 'qr_box_size': 2, + 'qr_border': 0, + + 'text_template': '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + } + } +} # When determining the primary IP address for a device, IPv6 is preferred over IPv4 by default. Set this to True to # prefer IPv4 instead. diff --git a/develop/docker-compose-debug.yml b/develop/docker-compose-debug.yml new file mode 100644 index 0000000..eb5357a --- /dev/null +++ b/develop/docker-compose-debug.yml @@ -0,0 +1,34 @@ +# This Docker Compose Debug File supports you with debugging in VSCode For more details see README.md + +# Overwrites parts of the service from the Docker-Compose file: +--- +services: + netbox: + # The following commands are executed + # - Install Current Version of DebugPy for Debug in Visual Studio Code https://github.com/microsoft/debugpy + # - PYDEVD_DISABLE_FILE_VALIDATION=1 --> You’re telling the debugger to skip this validation and proceed with debugging + # - Migrate Netbox + # - Create a user (superuser) for Netbox / Django so that you can log in directly to Netbox. More Infos: makesuperuser.py + # - Start Netbox with Modul DebugPy (Xfrozen_modules=off --> Disable Frozen Moduls in DebugPy Mode; 5678 is Debug Port) + command: > + sh -c "echo Run Netbox Django in debug mode && + pip install debugpy && + PYDEVD_DISABLE_FILE_VALIDATION=1 && + python manage.py migrate && + python manage.py makesuperuser && + python -Xfrozen_modules=off -m debugpy --listen 0.0.0.0:5678 manage.py runserver 0.0.0.0:8000 --insecure" + ports: + - '8000:8000' + - '5678:5678' + worker: + # Enables the synchronization of source code files with the Docker container during debugging. + # Without this option, changed files in the Docker container are only transferred to VSCode. + # Currently HTML files are changed live. After saving a *.py file, the container is automatically rebuilt. I don't know if this can be optimized. Greetings LHBL2003. + # https://www.docker.com/blog/docker-compose-experiment-sync-files-and-automatically-rebuild-services-with-watch-mode/ + x-develop: + watch: + - action: sync + path: ../netbox_qrcode + target: /source/netbox_qrcode + #- action: rebuild + #path: package.json diff --git a/develop/docker-compose.yml b/develop/docker-compose.yml index 4b7e984..d80ef99 100644 --- a/develop/docker-compose.yml +++ b/develop/docker-compose.yml @@ -5,8 +5,14 @@ services: build: context: ../ dockerfile: develop/Dockerfile + + # The following commands are executed + # - Migrate Netbox + # - Create a user (superuser) for Netbox / Django so that you can log in directly to Netbox. More Infos: makesuperuser.py + # - Start Netbox command: > sh -c "python manage.py migrate && + python manage.py makesuperuser && python manage.py runserver 0.0.0.0:8000" ports: - '8000:8000' @@ -17,7 +23,10 @@ services: - ./dev.env volumes: - ./configuration.py:/opt/netbox/netbox/netbox/configuration.py + - ../docs/img/Netbox_Icon_Example.png:/opt/netbox/netbox/media/image-attachments/Netbox_Icon_Example.png - ../netbox_qrcode:/source/netbox_qrcode + # For automatic setup of the Django / Netbox SuperUser + - ../develop/management:/source/netbox_qrcode/management tty: true worker: build: @@ -33,7 +42,7 @@ services: - ../netbox_qrcode:/source/netbox_qrcode tty: true postgres: - image: postgres:12 + image: postgres:16 env_file: dev.env volumes: - pgdata_netbox_qrcode:/var/lib/postgresql/data diff --git a/develop/management/__init__.py b/develop/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/develop/management/commands/__init__.py b/develop/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/develop/management/commands/makesuperuser.py b/develop/management/commands/makesuperuser.py new file mode 100644 index 0000000..7147ca0 --- /dev/null +++ b/develop/management/commands/makesuperuser.py @@ -0,0 +1,37 @@ +# This script creates the Django Super User which is also used for the first login to Netbox. +# This means that no user needs to be created in the development phase, as this is done when Docker-Compose is started. +# The script is terminated prematurely if a user already exists. +# User name and password see parameters below "Username" and "password" +# +# This script must be located in the Django plugin path .../management/comands at development time. +# e.g.: Docker-Compose volumes: ../develop/management:/source/netbox_qrcode/management +# e.g.: Docker-Compose command: "python manage.py makesuperuser". + +from django.contrib.auth import get_user_model +from django.core.management.base import BaseCommand + +User = get_user_model() + +class bcolors: + PURPLE = '\033[95m' + RED = '\033[91m' + +class Command(BaseCommand): + def handle(self, *args, **options): + username = 'admin' + email = 'admin@admin.local' + password = 'admin' + try: + u = None + if not User.objects.filter(username=username).exists() and not User.objects.filter(is_superuser=True).exists(): + print("admin user not found, creating one") + + u = User.objects.create_superuser(username, email, password) + print(f"{bcolors.PURPLE}===================================") + print(f"{bcolors.PURPLE}A superuser for NetBox / Django '{username}' was created with email '{email}' and password '{password}'") + print(f"{bcolors.PURPLE}===================================") + else: + print(f"{bcolors.PURPLE}A superuser for NetBox / Django Admin user already exists or was created during the first Docker-Compose start.") + print(u) + except Exception as e: + print(f"{bcolors.RED}There was an error: {e}") \ No newline at end of file diff --git a/docs/README_Subpages/README_Configuration.md b/docs/README_Subpages/README_Configuration.md new file mode 100644 index 0000000..e9b9c41 --- /dev/null +++ b/docs/README_Subpages/README_Configuration.md @@ -0,0 +1,662 @@ +[<< Back to README start](/README.md) + +# Configuration +The plugin is configured in the Netbox configuration file: configuration.py +If you have followed the standard installation of Netbox, the configuration file is located under: /opt/netbox/netbox/netbox/configuration.py + +The following options are available. If the same variable name is specified several times in several lines, these are different examples. Entries with ‘# DEFAULT’ are system defaults. + +## General Plugin + +* `title`: + + A title is displayed in the label window. This can be expanded with information. Useful if you want to provide several label variants for devices, for example. + ```Python + 'title': '', # DEFAULT + 'title': 'My text extension in the plugin heading.', + ``` + +## Text content + +* `with_text`: + + Text label will be added to QR code image if enabled. + + ```Python + 'with_text': True, # DEFAULT + 'with_text': False, + ``` + +* `text_location`: + + Where to render the text, relative to the QR code. + + ```Python + 'text_location': 'right', # DEFAULT + 'text_location': 'left', + 'text_location': 'up', + 'text_location': 'down', + ``` + +* `text_align_horizontal`: + + Defines where the text is to be displayed horizontally in the text area. + + ```Python + 'text_align_horizontal': 'left', # DEFAULT + 'text_align_horizontal': 'center', + 'text_align_horizontal': 'right', + ``` + +* `text_align_vertical`: + + Defines where the text is to be displayed vertically in the text area. + + ```Python + 'text_align_horizontal': 'middle', # DEFAULT + 'text_align_horizontal': 'top', + 'text_align_horizontal': 'bottom', + ``` + + ### Text source (Option A) + +* `text_fields`: + + Text fields of an object that will be added as text label to QR image. It's possible to use custom field values. + + ```Python + 'text_fields': ['name', 'serial'], # DEFAULT + 'text_fields': ['name'], + 'text_fields': ['site', + 'name', + 'id'] + ``` + +* `custom_text`: Additional text label to QR code image (will be added after text_fields). + ```Python + 'custom_text': None, # DEFAULT + 'custom_text': 'My Text', + ``` + ### Text source (Option B) + +* `text_template`: + + [Jinja2](https://jinja.palletsprojects.com/) template with {{ obj }} as context for device, rack, etc., using it ignores `text_fields` and `custom_text`. + + **{{ obj.xyz }} :** + + Value of the object/module (e.g. device, rack, etc.). Which values can be read depends on the Netbox module. Many names can be found during mass import, e.g. https://server/dcim/devices/import/, i.e. the “Field Options” names such as “status”, i.e. “obj.status”. Writing HTML text is permitted. + + ```Python + # Example to output name and site in two lines with caption (See also screenshots below): + + 'text_template': 'Name: {{ obj.name }}\nSite: {{ obj.site }}', + ``` + +  + + **{{ logo }}** : + + The value stored in the logo parameter. + + ```Python +😝 My label design 😝
', + + 'label_width': '56mm', + 'label_height': '32mm', + 'label_edge_top': '0mm', + 'label_edge_left': '0mm', + 'label_edge_right': '0mm', + }, +``` + +## Example 1 (Cable) - Cable Label vertical writing. + +This example shows how to write vertically, e.g. for cable labeling on a wide but not so high single label. + + + +```Python + 'cable': { + 'title': 'Example 1 (Template for Cable)', + 'with_qr': False, + 'label_edge_left': '0.00mm', + 'label_edge_right': '0.00mm', + 'label_edge_top': '0.00mm', + 'text_align_vertical': 'middle', + 'text_align_horizontal': 'center', + 'text_template': '' + '{{ obj.label }}' + '{{ obj.label }}' + '{{ obj.label }}' + '{{ obj.label }}' + '{{ obj.label }}' + '{{ obj.label }}' + '{{ obj.label }}' + '{{ obj.label }}' + '{{ obj.label }}' + '{{ obj.label }}' + '{{ obj.label }}' + '{{ obj.label }}' + '{{ obj.label }}' + '' + }, +``` + +## Example 2 (Cable) - Cable Label vertical writing. + +This example shows the creation of a Cabel label with barcode 128. Please note that an online function is used here (internet may be necessary). Barcode 128 is not created on the Netbox server. + + + +```Python + 'cable_2': { + 'title': 'Example 2 (Template for Cable)', + 'with_qr': False, + 'label_edge_left': '0.00mm', + 'label_edge_right': '0.00mm', + 'label_edge_top': '0.00mm', + 'text_align_vertical': 'middle', + 'text_align_horizontal': 'center', + + # QR-Code Image File + 'qr_version': 1, + 'qr_error_correction': 1, + 'qr_box_size': 2, + 'qr_border': 0, + + 'text_template': '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + } +``` + +[<< Back to General Configuration](README_Configuration.md) \ No newline at end of file diff --git a/docs/README_Subpages/README_Development.md b/docs/README_Subpages/README_Development.md new file mode 100644 index 0000000..7f96914 --- /dev/null +++ b/docs/README_Subpages/README_Development.md @@ -0,0 +1,49 @@ +[<< Back to README start](/README.md) +# Development +## Contributing +Developing tools for this project based on [ntc-netbox-plugin-onboarding](https://github.com/networktocode/ntc-netbox-plugin-onboarding) repo. + +Issues and pull requests are welcomed. + +## Recommended extensions for VSCode: +With the extension ID you can search for the extension more easily. +- GitHub Pull Request (Extension ID: GitHub.vscode-pull-request-github) +- Docker (Extension ID: ms-azuretools.vscode-docker) +- Python (Extension ID: ms-python.python) +- Python Debugger (Extension ID: ms-python.debugpy) +- Django (Extension ID: batisteo.vscode-django) +- Makefile-Tools (extension ID: ms-vscode.makefile-tools) + +## Easy start of the development environment +Take a look at the topic "Makefile" and pay attention to the "Makefile" file in the project to be able to quickly start Netbox with the Docker-Compose. + +## Default Login (Netbox, DB, PostgreSQL, Redis) +If the project is started via Docker-Compose, the first login to Netbox is possible with the login data from the makesuperuser.py file. +Other login data for the database, PostgreSQL and Redis can be found in the dev.env file. + +Default Netbox Login (When start via Docker-Compose): +- User: admin +- PW: admin + +## Debugging with Breakpoint in VSCode +The following describes how to start the debugging mode in VSCode. + +*Start Container* +Write "make debug-vscode" without quotation marks in terminal window of VSCode (See also Makefile) +Wait until all containers are started and Starting development server at http://0.0.0.0:8000/ is displayed. + +*Start Debug:* +- Go to "Run and Debug" and start the debug "Docker: Python -Django" (The footer of vsCode should become Orang.) +- Set a breakpoint in the code +- Open the NetBox page with the plugin. +- VSCode will stop at the stopping point. + +If you change something in an HTML file, this will be displayed immediately after a reload of the website. If you change something in a *.py file, the web server is automatically restarted after saving the file. It may be advisable to deactivate this in VSCode under File --> Auto Save so that the web server does not restart so often. + +*Helpful documentary:* +- https://medium.com/django-unleashed/debug-django-application-in-docker-container-using-vscode-ca5967340262 +- https://testdriven.io/blog/django-debugging-vs-code/ +- https://github.com/microsoft/debugpy +- https://docs.python.org/3/using/cmdline.html#cmdoption-X + +[<< Back to README start](/README.md) \ No newline at end of file diff --git a/docs/img/Configuration_Browser_Print_Settings.png b/docs/img/Configuration_Browser_Print_Settings.png new file mode 100644 index 0000000..b767a4f Binary files /dev/null and b/docs/img/Configuration_Browser_Print_Settings.png differ diff --git a/docs/img/Configuration_Label_Example_01.png b/docs/img/Configuration_Label_Example_01.png new file mode 100644 index 0000000..2e406ea Binary files /dev/null and b/docs/img/Configuration_Label_Example_01.png differ diff --git a/docs/img/Configuration_Label_Example_02.png b/docs/img/Configuration_Label_Example_02.png new file mode 100644 index 0000000..92ee978 Binary files /dev/null and b/docs/img/Configuration_Label_Example_02.png differ diff --git a/docs/img/Configuration_Label_Example_03.png b/docs/img/Configuration_Label_Example_03.png new file mode 100644 index 0000000..43d5ae7 Binary files /dev/null and b/docs/img/Configuration_Label_Example_03.png differ diff --git a/docs/img/Configuration_Label_Example_04.png b/docs/img/Configuration_Label_Example_04.png new file mode 100644 index 0000000..8573cc3 Binary files /dev/null and b/docs/img/Configuration_Label_Example_04.png differ diff --git a/docs/img/Configuration_Label_Example_05.png b/docs/img/Configuration_Label_Example_05.png new file mode 100644 index 0000000..f221a8a Binary files /dev/null and b/docs/img/Configuration_Label_Example_05.png differ diff --git a/docs/img/Configuration_Label_Example_06.png b/docs/img/Configuration_Label_Example_06.png new file mode 100644 index 0000000..ec04bc5 Binary files /dev/null and b/docs/img/Configuration_Label_Example_06.png differ diff --git a/docs/img/Configuration_Label_Example_07.png b/docs/img/Configuration_Label_Example_07.png new file mode 100644 index 0000000..58df97e Binary files /dev/null and b/docs/img/Configuration_Label_Example_07.png differ diff --git a/docs/img/Configuration_Label_Example_08.png b/docs/img/Configuration_Label_Example_08.png new file mode 100644 index 0000000..35211c2 Binary files /dev/null and b/docs/img/Configuration_Label_Example_08.png differ diff --git a/docs/img/Configuration_Label_Example_09.png b/docs/img/Configuration_Label_Example_09.png new file mode 100644 index 0000000..18f2376 Binary files /dev/null and b/docs/img/Configuration_Label_Example_09.png differ diff --git a/docs/img/Configuration_Label_Example_10.png b/docs/img/Configuration_Label_Example_10.png new file mode 100644 index 0000000..ee48583 Binary files /dev/null and b/docs/img/Configuration_Label_Example_10.png differ diff --git a/docs/img/Configuration_Label_Example_11.png b/docs/img/Configuration_Label_Example_11.png new file mode 100644 index 0000000..4edaca5 Binary files /dev/null and b/docs/img/Configuration_Label_Example_11.png differ diff --git a/docs/img/Configuration_Label_Example_12.png b/docs/img/Configuration_Label_Example_12.png new file mode 100644 index 0000000..8585935 Binary files /dev/null and b/docs/img/Configuration_Label_Example_12.png differ diff --git a/docs/img/Configuration_Printer_WordPreview.png b/docs/img/Configuration_Printer_WordPreview.png new file mode 100644 index 0000000..ca7045d Binary files /dev/null and b/docs/img/Configuration_Printer_WordPreview.png differ diff --git a/docs/img/Configuration_Printer_ZM400.png b/docs/img/Configuration_Printer_ZM400.png new file mode 100644 index 0000000..d904fd3 Binary files /dev/null and b/docs/img/Configuration_Printer_ZM400.png differ diff --git a/docs/img/Netbox_Icon_Example.png b/docs/img/Netbox_Icon_Example.png new file mode 100644 index 0000000..aaca2d2 Binary files /dev/null and b/docs/img/Netbox_Icon_Example.png differ diff --git a/netbox_qrcode/__init__.py b/netbox_qrcode/__init__.py index 6d16a96..baf6d37 100644 --- a/netbox_qrcode/__init__.py +++ b/netbox_qrcode/__init__.py @@ -10,22 +10,75 @@ class QRCodeConfig(PluginConfig): author = 'Nikolay Yuzefovich' author_email = 'mgk.kolek@gmail.com' required_settings = [] + min_version = '4.3.0' + max_version = '4.3.99' default_settings = { + + ################################## + # General Plugin + 'title': '', + + ################################## + # Text content 'with_text': True, + 'text_location': 'right', + 'text_align_horizontal': 'left', + 'text_align_vertical': 'middle', + + # Text source (Option A) 'text_fields': ['name', 'serial'], - 'font': 'TahomaBold', 'custom_text': None, - 'text_location': 'right', + + # Text source (Option B) + 'text_template': None, + + ################################## + # Font + 'font': 'TahomaBold', + 'font_size': '3mm', + 'font_weight': 'normal', + 'font_color': 'black', + + ################################## + # QR-Code + 'with_qr': True, + + # QR-Code alternative source + 'url_template': None, + + # QR-Code Image File 'qr_version': 1, 'qr_error_correction': 0, - 'qr_box_size': 6, - 'qr_border': 4, + 'qr_box_size': 4, + 'qr_border': 0, + + ################################## + # Label Layout + + # Label dimensions + 'label_qr_width': '12mm', + 'label_qr_height': '12mm', + + # Label edge + 'label_edge_top': '0mm', + 'label_edge_left': '1.5mm', + 'label_edge_right': '1.5mm', + 'label_edge_bottom': '0mm', + + # Label QR code positioning + 'label_width': '56mm', + 'label_height': '32mm', + 'label_qr_text_distance': '1mm', + + # Module-dependent configuration 'device': { 'text_fields': ['name', 'serial'] }, + 'rack': { 'text_fields': ['name'] }, + 'cable': { 'text_fields': [ '_termination_a_device', @@ -38,15 +91,22 @@ class QRCodeConfig(PluginConfig): 'b_terminations' ] }, + 'location': { 'text_fields': ['name'] }, + 'powerfeed': { - 'text_fields': ['name'], + 'text_fields': ['name'] }, + 'powerpanel': { 'text_fields': ['name'] - } + }, + + 'module': { + }, + 'logo': '', } config = QRCodeConfig # noqa E305 diff --git a/netbox_qrcode/fonts/ArialBlack.ttf b/netbox_qrcode/fonts/ArialBlack.ttf deleted file mode 100644 index 8877277..0000000 Binary files a/netbox_qrcode/fonts/ArialBlack.ttf and /dev/null differ diff --git a/netbox_qrcode/fonts/ArialMT.ttf b/netbox_qrcode/fonts/ArialMT.ttf deleted file mode 100644 index 084f062..0000000 Binary files a/netbox_qrcode/fonts/ArialMT.ttf and /dev/null differ diff --git a/netbox_qrcode/fonts/ComicSansMSBold.ttf b/netbox_qrcode/fonts/ComicSansMSBold.ttf deleted file mode 100644 index e68979c..0000000 Binary files a/netbox_qrcode/fonts/ComicSansMSBold.ttf and /dev/null differ diff --git a/netbox_qrcode/fonts/CourierNewBold.ttf b/netbox_qrcode/fonts/CourierNewBold.ttf deleted file mode 100644 index 894c455..0000000 Binary files a/netbox_qrcode/fonts/CourierNewBold.ttf and /dev/null differ diff --git a/netbox_qrcode/fonts/Tahoma.ttf b/netbox_qrcode/fonts/Tahoma.ttf deleted file mode 100644 index d204f95..0000000 Binary files a/netbox_qrcode/fonts/Tahoma.ttf and /dev/null differ diff --git a/netbox_qrcode/fonts/TahomaBold.ttf b/netbox_qrcode/fonts/TahomaBold.ttf deleted file mode 100644 index e4024e2..0000000 Binary files a/netbox_qrcode/fonts/TahomaBold.ttf and /dev/null differ diff --git a/netbox_qrcode/template_content.py b/netbox_qrcode/template_content.py index d4b44ba..3226782 100644 --- a/netbox_qrcode/template_content.py +++ b/netbox_qrcode/template_content.py @@ -1,124 +1,176 @@ from packaging import version - from django.conf import settings from django.core.exceptions import ObjectDoesNotExist -from django.template import engines - from netbox.plugins import PluginTemplateExtension +from .template_content_functions import create_text, create_url, config_for_modul, create_QRCode -from .utilities import get_img_b64, get_qr, get_qr_text, get_concat - +# ****************************************************************************************** +# Contains the main functionalities of the plugin and thus creates the content for the +# individual modules, e.g: Device, Rack etc. +# ****************************************************************************************** +################################## +# Class for creating the plugin content class QRCode(PluginTemplateExtension): - def x_page(self): - config = self.context['config'] - obj = self.context['object'] - request = self.context['request'] - url = request.build_absolute_uri(obj.get_absolute_url()) - # get object settings - obj_cfg = config.get(self.model.replace('dcim.', '')) - if obj_cfg is None: - return '' - # and ovverride default - config.update(obj_cfg) - - qr_args = {} - for k, v in config.items(): - if k.startswith('qr_'): - qr_args[k.replace('qr_', '')] = v - - qr_img = get_qr(url, **qr_args) - if config.get('with_text'): - if config.get('text_template'): - django_engine = engines["django"] - template = django_engine.from_string(config.get('text_template')) - text = template.render({'obj': obj}) - else: - text = [] - for text_field in config.get('text_fields', []): - cfn = None - if '.' in text_field: - try: - text_field, cfn = text_field.split('.') - except ValueError: - cfn = None - if getattr(obj, text_field, None): - if cfn: - try: - if getattr(obj, text_field).get(cfn): - text.append('{}'.format(getattr(obj, text_field).get(cfn))) - except AttributeError: - # fix for nb3.3: trying to get cable termination and device in same way as custom field - if type(getattr(obj, text_field)) is list: - first_element = next(iter(getattr(obj, text_field)), None) - if first_element and getattr(first_element, cfn, None): - text.append('{}'.format(getattr(first_element, cfn))) - else: - text.append('{}'.format(getattr(obj, text_field))) - custom_text = config.get('custom_text') - if custom_text: - text.append(custom_text) - text = '\n'.join(text) - text_img = get_qr_text(qr_img.size, text, config.get('font'), config.get('font_size', 0)) - qr_with_text = get_concat(qr_img, text_img, config.get('text_location', 'right')) - - img = get_img_b64(qr_with_text) - else: - img = get_img_b64(qr_img) + ################################## + # Creates a plug-in view for a label. + # -------------------------------- + # Parameter: + # labelDesignNo: Which label design should be loaded. + def Create_SubPluginContent(self, labelDesignNo): + + thisSelf = self + + obj = self.context['object'] # An object of the type Device, Rack etc. + + # Config suitable for the module + config = config_for_modul(thisSelf, labelDesignNo) + + # Abort if no config data. + if config is None: + return '' + + # Get URL for QR code + url = create_url(thisSelf, config, obj) + + # Create a QR code + qrCode = create_QRCode(url, config) + + # Create the text for the label if required. + text = create_text(config, obj, qrCode) + + # Create plugin using template try: - if version.parse(settings.VERSION).major >= 3: - return self.render( - 'netbox_qrcode/qrcode3.html', extra_context={'image': img} + if version.parse(settings.RELEASE.version).major >= 3: + + render = self.render( + 'netbox_qrcode/qrcode3.html', extra_context={ + 'title': config.get('title'), + 'labelDesignNo': labelDesignNo, + 'qrCode': qrCode, + 'with_text': config.get('with_text'), + 'text': text, + 'text_location': config.get('text_location'), + 'text_align_horizontal': config.get('text_align_horizontal'), + 'text_align_vertical': config.get('text_align_vertical'), + 'font': config.get('font'), + 'font_size': config.get('font_size'), + 'font_weight': config.get('font_weight'), + 'font_color': config.get('font_color'), + 'with_qr': config.get('with_qr'), + 'label_qr_width': config.get('label_qr_width'), + 'label_qr_height': config.get('label_qr_height'), + 'label_qr_text_distance': config.get('label_qr_text_distance'), + 'label_width': config.get('label_width'), + 'label_height': config.get('label_height'), + 'label_edge_top': config.get('label_edge_top'), + 'label_edge_left': config.get('label_edge_left'), + 'label_edge_right': config.get('label_edge_right'), + 'label_edge_bottom': config.get('label_edge_bottom') + } + ) + + return render else: + # Versions 1 and 2 are no longer supported. return self.render( - 'netbox_qrcode/qrcode.html', extra_context={'image': img} + 'netbox_qrcode/qrcode.html', extra_context={'image': qrCode} ) except ObjectDoesNotExist: return '' + ################################## + # Create plugin content + # - First, a plugin view is created for the first label. + # - If there are further configuration entries for the object/model (e.g. device, rack etc.), + # further label views are also created as additional plugin views. + def Create_PluginContent(self): + + # First Plugin Content + pluginContent = QRCode.Create_SubPluginContent(self, 1) + + # Check whether there is another configuration for the object, e.g. device, rack, etc. + # Support up to 10 additional label configurations (objectName_2 to ..._10) per object (e.g. device, rack, etc.). + + config = self.context['config'] # Django configuration + for i in range(2, 11): + + configName = self.models[0].replace('dcim.', '') + '_' + str(i) + obj_cfg = config.get(configName) # Load configuration for additional label if possible. + + if(obj_cfg): + pluginContent += QRCode.Create_SubPluginContent(self, i) # Add another plugin view + else: + break + + return pluginContent + +################################## +# The following section serves to integrate the plugin into Netbox Core. + +# Class for creating a QR code for the model: Device class DeviceQRCode(QRCode): - model = 'dcim.device' + models = ('dcim.device',) def right_page(self): - return self.x_page() - + return self.Create_PluginContent() +# Class for creating a QR code for the model: Rack class RackQRCode(QRCode): - model = 'dcim.rack' + models = ('dcim.rack',) def right_page(self): - return self.x_page() - + return self.Create_PluginContent() +# Class for creating a QR code for the model: Cable class CableQRCode(QRCode): - model = 'dcim.cable' + models = ('dcim.cable',) def left_page(self): - return self.x_page() - + return self.Create_PluginContent() +# Class for creating a QR code for the model: Location class LocationQRCode(QRCode): - model = 'dcim.location' + models = ('dcim.location',) def left_page(self): - return self.x_page() - + return self.Create_PluginContent() +# Class for creating a QR code for the model: Power Feed class PowerFeedQRCode(QRCode): - model = 'dcim.powerfeed' + models = ('dcim.powerfeed',) def right_page(self): - return self.x_page() - + return self.Create_PluginContent() +# Class for creating a QR code for the model: Power Panel class PowerPanelQRCode(QRCode): - model = 'dcim.powerpanel' + models = ('dcim.powerpanel',) def right_page(self): - return self.x_page() + return self.Create_PluginContent() +# Class for dcim.module +class ModuleQRCode(QRCode): + models = ('dcim.module',) -template_extensions = [DeviceQRCode, RackQRCode, CableQRCode, LocationQRCode, PowerFeedQRCode, PowerPanelQRCode] + def right_page(self): + return self.Create_PluginContent() + +################################## +# Other plugins support + +# Commenting out (for now) - make this work on core models first. +# Class for creating a QR code for the Plugin: Netbox-Inventory (https://github.com/ArnesSI/netbox-inventory) +#class Plugin_Netbox_Inventory(QRCode): +# models = ()'netbox_inventory.asset' # Info for Netbox in which model the plugin should be integrated. +# +# def right_page(self): +# return self.Create_PluginContent() + +# Connects Netbox Core with the plug-in classes +# Removed , Plugin_Netbox_Inventory] +template_extensions = [DeviceQRCode, ModuleQRCode, RackQRCode, CableQRCode, LocationQRCode, PowerFeedQRCode, PowerPanelQRCode] \ No newline at end of file diff --git a/netbox_qrcode/template_content_functions.py b/netbox_qrcode/template_content_functions.py new file mode 100644 index 0000000..95774c5 --- /dev/null +++ b/netbox_qrcode/template_content_functions.py @@ -0,0 +1,146 @@ +from .utilities import get_img_b64, get_qr +from django.template import engines + +# ****************************************************************************************** +# For better clarity, the sub-functions of template_content.py have been outsourced. +# ****************************************************************************************** + +################################## +# The configuration is taken and all fields that are module-specific (e.g. Device, Rack, etc.) are replaced. +# -------------------------------- +# Parameter: +# labelDesignNo: Which label design should be loaded. +# parentSelf: Self from Parrent Function +def config_for_modul(parentSelf, labelDesignNo): + + # Copy so that the Runtime data is not changed. + config = parentSelf.context['config'].copy() # From Netbox Config File + + # Create suffix to read the correct module configuration. + confModulsufix = str() # None if the first standard label + + if(labelDesignNo >= 2): + confModulsufix = '_' + str(labelDesignNo) + + # Collect the QR code plugin configuration for the specific object such as device, rack etc. + # and overwrite the default configuration fields. + obj_cfg = config.get(parentSelf.models[0].replace('dcim.', '') + confModulsufix) # get spezific object settings + + if obj_cfg is not None: + config.update(obj_cfg) # Ovverride default confiv Values + return config + else: + return config # No module customisation + +################################## +# Create QR-Code +# -------------------------------- +# Parameter: +# text: Text for QR-Code +# config: From the Netbox configuration file +def create_QRCode(text, config): + + # Collect the configuration entries that begin with "qr_. + # These are required to generate the QR code. + qr_args = {} + for k, v in config.items(): + if k.startswith('qr_'): + qr_args[k.replace('qr_', '')] = v + + # Create a QR code + qrCode = get_qr(text, **qr_args) + return get_img_b64(qrCode) + + +################################## +# Create URL for QR code +# -------------------------------- +# Parameter: +# config: From the Netbox configuration file +# obj: Data from the model (e.g. device, rack, etc.) +# request: HTML Request Information +def create_url(parentSelf, config, obj): + + request = parentSelf.context['request'] # HTML Request Informations + + if config.get('url_template'): + # A user-defined design specification of the URL is provided in ninja2 format. + django_engine = engines["django"] + template = django_engine.from_string(config.get('url_template')) # Custom template for URL design. + return template.render({'obj': obj}) # Replace placeholder + else: + return request.build_absolute_uri(obj.get_absolute_url()) # URL to the requested page + +################################## +# Create text for label +# -------------------------------- +# Parameter: +# config: From the Netbox configuration file +# obj: Data from the model (e.g. device, rack, etc.) +# qrCode: QR-Code Image +def create_text(config, obj, qrCode): + + text = str() + + if config.get('with_text'): + if config.get('text_template'): + return get_text_template(config, obj, qrCode) # Create text content based on the Ninja2 template from the user + else: + return get_text_fields(config, obj) # Use the list of variables from the Config. + +################################## +# A user-defined design specification of the text is provided in ninja2 format. +# -------------------------------- +# Parameter: +# config: From the Netbox configuration file +# obj: Data from the model (e.g. device, rack, etc.) +# qrCode: QR-Code Image (To create a freely defined label with QR code.) +def get_text_template(config, obj, qrCode): + + django_engine = engines["django"] + template = django_engine.from_string(config.get('text_template')) # Get Custom Template + logo = config.get('logo') + return template.render({'obj': obj, + 'logo': logo, + 'qrCode': qrCode}) # Replace placeholder + +################################## +# Retrieves all values from the object (e.g. device, rack, etc.) +# depending on the configuration parameter that are to be displayed in list form and prepares them. +# -------------------------------- +# Parameter: +# config: From the Netbox configuration file +# obj: Data from the model (e.g. device, rack, etc.) +def get_text_fields(config, obj): + + text = [] + + for text_field in config.get('text_fields', []): + cfn = None + if '.' in text_field: + try: + text_field, cfn = text_field.split('.') + except ValueError: + cfn = None + if getattr(obj, text_field, None): + if cfn: + try: + if getattr(obj, text_field).get(cfn): + text.append('{}'.format(getattr(obj, text_field).get(cfn))) + except AttributeError: + # fix for nb3.3: trying to get cable termination and device in same way as custom field + if type(getattr(obj, text_field)) is list: + first_element = next(iter(getattr(obj, text_field)), None) + if first_element and getattr(first_element, cfn, None): + text.append('{}'.format(getattr(first_element, cfn))) + else: + text.append('{}'.format(getattr(obj, text_field))) + + # Append user-defined text to the end. + custom_text = config.get('custom_text') + + if custom_text: + text.append(custom_text) + + # Convert text list to string with line breaks. + return '