From 69c793389459f1533f8314a8a67c4d0bf0d20689 Mon Sep 17 00:00:00 2001 From: Philipp Fruck Date: Tue, 1 Nov 2022 21:42:53 +0100 Subject: [PATCH 1/5] make container setup suck less migrated to Postgres because of binary availability of postgres python client. Also implemented development compose file and refactored production python image --- Dockerfile | 13 +- Dockerfile-DB | 2 - FabAccess.sql | 5 - docker-compose.yml | 565 +++------------------------------------------ sql/FabAccess.sql | 11 + 5 files changed, 57 insertions(+), 539 deletions(-) delete mode 100644 Dockerfile-DB delete mode 100644 FabAccess.sql create mode 100644 sql/FabAccess.sql diff --git a/Dockerfile b/Dockerfile index 4903558..311b1cc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,9 @@ -FROM python:3.11.0a3-bullseye -RUN pip install keycloak_wrapper paho-mqtt python-keycloak mysql.connector -RUN mkdir /app/ -COPY main.py /app/ +FROM python:3.11-alpine +WORKDIR /app + +COPY requirements.txt . +RUN pip install -r requirements.txt +COPY ./fab_access/* /app + ENTRYPOINT [ "python" ] -CMD [ "-u", "/app/main.py" ] \ No newline at end of file +CMD [ "-u", "main.py" ] diff --git a/Dockerfile-DB b/Dockerfile-DB deleted file mode 100644 index ae58143..0000000 --- a/Dockerfile-DB +++ /dev/null @@ -1,2 +0,0 @@ -FROM mysql:5.7 -ADD FabAccess.sql /docker-entrypoint-initdb.d \ No newline at end of file diff --git a/FabAccess.sql b/FabAccess.sql deleted file mode 100644 index 4cfdb37..0000000 --- a/FabAccess.sql +++ /dev/null @@ -1,5 +0,0 @@ -DROP DATABASE IF EXISTS FabAccess; -CREATE DATABASE IF NOT EXISTS FabAccess; -USE FabAccess; -CREATE TABLE `ReaderPlug` (`ReaderID` INT NOT NULL , `PlugName` VARCHAR(255) NOT NULL, `PermissionPath` VARCHAR(255) NOT NULL, `Status` BOOLEAN NOT NULL, `LastUser` VARCHAR(255) NOT NULL) ENGINE = InnoDB; -INSERT INTO `ReaderPlug` (`ReaderID`, `PlugName`, `PermissionPath`, `Status`, `LastUser`) VALUES ("001", "lasercutter", "sfz.lasercutter.trotec", 0, "luca.lutz"); \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 9454629..887004b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,536 +1,47 @@ version: '3' - -networks: - auth: - FabDB: - FabLoggingDB: - FabLoggingTelegraf: - reverse-proxy: - mediawiki: - nextcloud: - gitea: - octofarm: - octoproxy: - ipam: - config: - - subnet: 172.42.0.0/24 - odoo: - roundcube: - partdb: - -volumes: - build-trigger: - build-output: - db: - nextcloud: - -secrets: - samba-admin-password: - file: ./samba-admin-password - services: - AD: - image: instantlinux/samba-dc:latest - restart: always - network_mode: host - cap_add: - - CAP_SYS_ADMIN - hostname: dc.sfz.lab - environment: - DOMAIN_ACTION: provision - INTERFACES: -lo eth0 - REALM: ad.sfz.lab - TZ: Europe/Berlin - WORKGROUP: AD - volumes: - - ./data/samba/config:/etc/samba - - ./data/samba/data:/var/lib/samba - secrets: - - samba-admin-password - - reverse-proxy: - image: nginx-openid - build: - context: ./data/reverse-proxy/ - dockerfile: Dockerfile - ports: - - "80:80" - - "443:443" - volumes: - - ./data/reverse-proxy/content/:/usr/share/nginx/html/ - - ./data/reverse-proxy/config/:/etc/nginx/conf.d/ - - /etc/letsencrypt:/etc/nginx/certs - - /etc/localtime:/etc/localtime:ro - - ./data/mirror/config/:/mirror/config_web/ - networks: - reverse-proxy: - restart: unless-stopped - - - mysql: - image: mysql:5.7 - volumes: - - ./data/keycloak/DB/:/var/lib/mysql - environment: - MYSQL_RANDOM_ROOT_PASSWORD: "yes" - MYSQL_DATABASE: ${KEYCLOAK_DB_NAME} - MYSQL_USER: ${KEYCLOAK_DB_USER} - MYSQL_PASSWORD: ${KEYCLOAK_DB_PW} - networks: - auth: - - keycloak: - image: keycloak-sfz - build: - context: ./data/keycloak/ - dockerfile: Dockerfile - environment: - - DB_VENDOR=MYSQL - - DB_ADDR=mysql - - DB_DATABASE=${KEYCLOAK_DB_NAME} - - DB_USER=${KEYCLOAK_DB_USER} - - DB_PASSWORD=${KEYCLOAK_DB_PW} - - KEYCLOAK_USER=${KEYCLOAK_USER_NAME} - - KEYCLOAK_PASSWORD=${KEYCLOAK_USER_PW} - - PROXY_ADDRESS_FORWARDING=true + backend: + build: . depends_on: - - mysql - networks: - auth: - reverse-proxy: - volumes: - - ./data/keycloak/data/:/lib/jvm/jre-11/lib/security/ - - ./data/keycloak/cert/:/etc/pki/java/ - - - mosquitto: - image: hivemq/hivemq4 - ports: - - 1883:1883 - - 9001:9001 - networks: - - FabDB - - FabBackend: - image: fabbackend - build: - context: ./data/FabBackend - dockerfile: Dockerfile + - db + - mqtt environment: - KEYCLOAK_USER_NAME: ${KEYCLOAK_USER_NAME} - KEYCLOAK_USER_PW: ${KEYCLOAK_USER_PW} - KEYCLOAK_REALM: ${KEYCLOAK_REALM} - FABDB_DB_USER_NAME: ${FABDB_DB_USER_NAME} - FABDB_DB_USER_PW: ${FABDB_DB_USER_PW} - FABDB_DB_NAME: ${FABDB_DB_NAME} - networks: - - FabDB - - auth + # Keycloak config + KEYCLOAK_URL: ${KEYCLOAK_URL:?err} + KEYCLOAK_USER_NAME: ${KEYCLOAK_USER_NAME:?err} + KEYCLOAK_USER_PW: ${KEYCLOAK_USER_PW:?err} + KEYCLOAK_REALM: ${KEYCLOAK_REALM:?err} + # DB config + DB_HOSTNAME: ${DB_HOSTNAME:?err} + DB_USERNAME: ${DB_USERNAME:?err} + DB_PASSWORD: ${DB_PASSWORD:?err} + DB_DATABASE: ${DB_DATABASE:?err} + # MQTT config + MQTT_USERNAME: ${MQTT_USERNAME:?err} + MQTT_PASSWORD: ${MQTT_PASSWORD:?err} + MQTT_BROKER: ${MQTT_BROKER:?err} + MQTT_CLIENT: ${MQTT_CLIENT:?err} restart: unless-stopped - - FabDB: - image: fabdb - build: - context: ./data/FabBackend - dockerfile: Dockerfile-DB - volumes: - - ./data/FabBackend/DB/:/var/lib/mysql - environment: - MYSQL_RANDOM_ROOT_PASSWORD: "yes" - MYSQL_DATABASE: ${FABDB_DB_NAME} - MYSQL_USER: ${FABDB_DB_USER_NAME} - MYSQL_PASSWORD: ${FABDB_DB_USER_PW} - networks: - - FabDB - - FabLoggingDB: - image: influxdb:1.5 - volumes: - - ./data/FabLogging/DB/data/:/var/lib/influxdb/ - - ./data/FabLogging/DB/config/:/etc/influxdb/ - restart: always - networks: - - FabLoggingDB - - FabLoggingTelegraf - - FabLoggingDBTelegraf: - image: telegraf - volumes: - - ./data/FabLogging/telegraf/telegraf.conf:/etc/telegraf/telegraf.conf - - /var/run/docker.sock:/var/run/docker.sock - restart: always - user: "0" - networks: - - FabDB - - FabLoggingTelegraf - - grafana: - image: grafana/grafana - user: "0" - #ports: - # - "3000:3000" - volumes: - - ./data/FabLogging/data/grafana:/var/lib/grafana - restart: always - networks: - - FabLoggingDB - - buildserver-worker: - image: buildserver - build: - context: ./data/buildserver/worker/ - dockerfile: Dockerfile - volumes: - - build-trigger:/trigger/ - - build-output:/output/ - - /etc/localtime:/etc/localtime:ro - restart: unless-stopped - - buildserver-web-trigger: - image: buildserver-web-trigger - build: - context: ./data/buildserver/trigger/ - dockerfile: Dockerfile - networks: - reverse-proxy: - volumes: - - build-trigger:/var/www/trigger/ - - /etc/localtime:/etc/localtime:ro - environment: - - DEBUG=true - - HISTCONTROL=ignoredups - restart: unless-stopped - - buildserver-web-server: - image: httpd:latest - networks: - reverse-proxy: - volumes: - - build-output:/usr/local/apache2/htdocs/ - - /etc/localtime:/etc/localtime:ro - restart: unless-stopped - - mediawiki: - image: mediawikisfz - build: - dockerfile: Dockerfile - context: ./data/mediawiki/ - networks: - reverse-proxy: - mediawiki: - auth: - depends_on: - - mediawiki-mysql - restart: unless-stopped - volumes: - - ./data/mediawiki/images:/var/www/html/images/ - - mediawiki-mysql: - image: mariadb - volumes: - - ./data/mediawiki/DB:/var/lib/mysql - environment: - MYSQL_ROOT_PASSWORD: ${MEDIAWIKIDB_ROOT_PW} - networks: - mediawiki: - hostname: mediawiki-mysql - restart: unless-stopped - - cdn01: - image: httpd - networks: - reverse-proxy: - volumes: - - ./data/CDN:/usr/local/apache2/htdocs/ - restart: unless-stopped - - cdn02: - image: httpd - networks: - reverse-proxy: - volumes: - - ./data/CDN:/usr/local/apache2/htdocs/ - restart: unless-stopped - - cdn03: - image: httpd - networks: - reverse-proxy: - volumes: - - ./data/CDN:/usr/local/apache2/htdocs/ - restart: unless-stopped - db: - image: mariadb:10.5 - command: --transaction-isolation=READ-COMMITTED --binlog-format=ROW - restart: always + image: postgres:15-alpine volumes: - - db:/var/lib/mysql - networks: - nextcloud: + - ./sql/:/docker-entrypoint-initdb.d/:ro,Z environment: - - MYSQL_RANDOM_ROOT_PASSWORD="yes" - - MYSQL_PASSWORD=${NEXTCLOUD_MYSQL_PW} - - MYSQL_DATABASE=nextcloud - - MYSQL_USER=nextcloud - - redis: - image: redis:alpine - restart: always - networks: - nextcloud: - - app: - image: nextcloud:fpm-alpine - restart: always - volumes: - - nextcloud:/var/www/html + POSTGRES_DB: ${DB_DATABASE} + POSTGRES_USER: ${DB_USERNAME} + POSTGRES_PASSWORD: ${DB_PASSWORD} + mqtt: + image: eclipse-mosquitto:2 environment: - - MYSQL_HOST=db - - REDIS_HOST=redis - - MYSQL_PASSWORD=${NEXTCLOUD_MYSQL_PW} - - MYSQL_DATABASE=nextcloud - - MYSQL_USER=nextcloud - depends_on: - - db - - redis - networks: - nextcloud: - - web: - build: ./data/nextcloud/ - restart: always - volumes: - - nextcloud:/var/www/html:ro - depends_on: - - app - networks: - reverse-proxy: - nextcloud: - - cron: - image: nextcloud:fpm-alpine - restart: always - volumes: - - nextcloud:/var/www/html - entrypoint: /cron.sh - depends_on: - - db - - redis - - gitea: - image: gitea/gitea:1.15.9 - environment: - - USER_UID=1000 - - USER_GID=1000 - - GITEA__database__DB_TYPE=mysql - - GITEA__database__HOST=git-db:3306 - - GITEA__database__NAME=gitea - - GITEA__database__USER=gitea - - GITEA__database__PASSWD=${GIT_DB_PW} - restart: always - networks: - - gitea - - reverse-proxy - volumes: - - ./data/gitea/data:/data - - /etc/timezone:/etc/timezone:ro - - /etc/localtime:/etc/localtime:ro - depends_on: - - git-db - - git-db: - image: mysql:8 - restart: always - environment: - - MYSQL_RANDOM_ROOT_PASSWORD="yes" - - MYSQL_USER=gitea - - MYSQL_PASSWORD=${GIT_DB_PW} - - MYSQL_DATABASE=gitea - networks: - - gitea - volumes: - - ./data/gitea/db:/var/lib/mysql - - octofarm-db: - image: mongo:4.4 - environment: - MONGO_INITDB_ROOT_USERNAME: octofarm - MONGO_INITDB_ROOT_PASSWORD: ${OCTOFARM_DB_PW} - MONGO_INITDB_DATABASE: octofarm - volumes: - - ./data/OctoFarm/data/mongodb-data:/data/db - restart: unless-stopped - networks: - octofarm: - - octofarm: - image: octofarm/octofarm:latest - restart: unless-stopped - mem_limit: 400m # Feel free to adjust! 400 MB is quite high and a safety limit. - networks: - reverse-proxy: - octofarm: - octoproxy: - ipv4_address: 172.42.0.3 - environment: - - MONGO=mongodb://octofarm:${OCTOFARM_DB_PW}@octofarm-db:27017/octofarm?authSource=admin - ports: - - 4000:4000 - expose: - - 4000 - volumes: - - ./data/OctoFarm/logs:/app/logs - - ./data/OctoFarm/scripts:/app/scripts - - ./data/OctoFarm/images:/app/images - - ./data/OctoFarm/hosts:/etc/hosts:ro - - octoproxy: - image: nginx:latest - volumes: - - ./data/octoproxy/config/:/etc/nginx/conf.d/ - - /etc/localtime:/etc/localtime:ro - - /etc/letsencrypt:/etc/nginx/certs - networks: - octoproxy: - ipv4_address: 172.42.0.2 - restart: unless-stopped - - octostreamer: - image: gersilex/cvlc - command: rtsp://admin:@10.10.42.60 --sout '#transcode{vcodec=MJPG,venc=ffmpeg{strict=1}}:standard{access=http{mime=multipart/x-mixed-replace;boundary=--7b3cc56e5f51db803f790dad720ed50a},mux=mpjpeg,dst=:8080/}' - networks: - reverse-proxy: - environment: - - RS_SNAPSHOT_INTERVAL=1000 - - octorestreamer: - image: datarhei/restreamer:latest - restart: always - networks: - reverse-proxy: - environment: - - RS_USERNAME=admin - - RS_PASSWORD=${OCTORESTREAMER_PW} - ports: - - 8087:8080 - volumes: - - ./data/restreamer/db:/restreamer/db - -# docker-zabbix-agent: -# restart: always -# ports: -# - '10060:10050' -# volumes: -# - /etc/localtime:/etc/localtime:ro -# - /etc/timezone:/etc/timezone:ro -# environment: -# - ZBX_SERVER_HOST=172.21.0.1 -# - ZBX_HOSTNAME=USV -# image: apcupsd -# devices: -# - /dev/usb/hiddev0 -# build: -# context: ./data/usv/ -# dockerfile: Dockerfile - - docker-zabbix-agent2: - restart: always - ports: - - '10061:10050' - volumes: - - /etc/localtime:/etc/localtime:ro - - /etc/timezone:/etc/timezone:ro - environment: - - ZBX_SERVER_HOST=192.168.64.1 - - ZBX_HOSTNAME=SSL - image: zabbix/zabbix-agent2 - - odoo: - image: odoo-sfz - build: - context: ./data/odoo - dockerfile: Dockerfile - depends_on: - - odoo-db - environment: - - HOST=odoo-db - - USER=odoo - - PASSWORD=${ODOO_DB_PW} - networks: - reverse-proxy: - odoo: - volumes: - - ./data/odoo/addons:/var/lib/odoo/.local/share/Odoo/addons/14.0/ - - ./data/odoo/conf:/etc/odoo - - ./data/odoo/data:/var/lib/odoo - - odoo-db: - image: postgres:13 - environment: - - POSTGRES_DB=postgres - - POSTGRES_PASSWORD=${ODOO_DB_PW} - - POSTGRES_USER=odoo - networks: - odoo: - volumes: - - ./data/odoo/db:/var/lib/postgresql/data - - roundcubedb: - image: mysql:5.7 - container_name: roundcubedb - restart: unless-stopped - volumes: - - ./data/webmail/db/mysql:/var/lib/mysql - environment: - - MYSQL_ROOT_PASSWORD=${WEBMAIL_PW} - - MYSQL_DATABASE=roundcubemail - networks: - roundcube: - - documentserver: - restart: always - image: onlyoffice/documentserver - networks: - reverse-proxy: - - roundcubemail: - image: roundcube/roundcubemail:latest - container_name: roundcubemail - restart: unless-stopped - networks: - reverse-proxy: - roundcube: - depends_on: - - roundcubedb - volumes: - - ./data/webmail/www:/var/www/html - environment: - - ROUNDCUBEMAIL_DB_TYPE=mysql - - ROUNDCUBEMAIL_DB_HOST=roundcubedb - - ROUNDCUBEMAIL_DB_PASSWORD=${WEBMAIL_PW} - - ROUNDCUBEMAIL_SKIN=elastic - - ROUNDCUBEMAIL_DEFAULT_HOST=tls://mail.sfz-aalen.space - - ROUNDCUBEMAIL_SMTP_SERVER=tls://mail.sfz-aalen.space - - partdb-db: - image: mysql - command: --default-authentication-plugin=mysql_native_password - restart: always - networks: - partdb: - environment: - MYSQL_ROOT_PASSWORD: ${PARTDB_ROOT_PW} - - partdb: - container_name: partdb - # By default Part-DB will be running under Port 8080, you can change it here - image: jbtronics/part-db1:master - volumes: - # By default - - ./uploads:/var/www/html/uploads - - ./public_media:/var/www/html/public/media - restart: unless-stopped - networks: - reverse-proxy: - partdb: + MQTT_USERNAME: ${MQTT_USERNAME} + MQTT_PASSWORD: ${MQTT_PASSWORD} + entrypoint: + - sh + - -c + - | + touch /mosquitto/config/passwd + mosquitto_passwd -b /mosquitto/config/passwd $${MQTT_USERNAME:?err} $${MQTT_PASSWORD:?err} + echo "bind_address 0.0.0.0" > /mosquitto/config/mosquitto.conf + echo "password_file /mosquitto/config/passwd" >> /mosquitto/config/mosquitto.conf + echo "allow_anonymous false" >> /mosquitto/config/mosquitto.conf + /usr/sbin/mosquitto -c /mosquitto/config/mosquitto.conf diff --git a/sql/FabAccess.sql b/sql/FabAccess.sql new file mode 100644 index 0000000..d1d9358 --- /dev/null +++ b/sql/FabAccess.sql @@ -0,0 +1,11 @@ +CREATE TABLE ReaderPlug( + ReaderID INT NOT NULL, + PlugName VARCHAR(255) NOT NULL, + PermissionPath VARCHAR(255) NOT NULL, + Status BOOLEAN NOT NULL, + LastUser VARCHAR(255) NOT NULL +); + +INSERT INTO ReaderPlug( + ReaderID, PlugName, PermissionPath, Status, LastUser +) VALUES (1, 'lasercutter', 'sfz.lasercutter.trotec', false, 'luca.lutz'); From bb144d78bcdd14e08154cad254e19e050b448d44 Mon Sep 17 00:00:00 2001 From: Philipp Fruck Date: Tue, 1 Nov 2022 21:44:04 +0100 Subject: [PATCH 2/5] refactor python codebase variable configuration through config.py and code splitting into multiple files. Also added requirements.txt and gitignore. --- .gitignore | 160 ++++++++++++++++++++++++++++++++++ fab_access/config.py | 33 +++++++ fab_access/main.py | 153 ++++++++++++++++++++++++++++++++ fab_access/mqtt_helper.py | 32 +++++++ main.py | 178 -------------------------------------- requirements.txt | 4 + 6 files changed, 382 insertions(+), 178 deletions(-) create mode 100644 .gitignore create mode 100644 fab_access/config.py create mode 100644 fab_access/main.py create mode 100644 fab_access/mqtt_helper.py delete mode 100644 main.py create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..68bc17f --- /dev/null +++ b/.gitignore @@ -0,0 +1,160 @@ +# 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 + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__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/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/fab_access/config.py b/fab_access/config.py new file mode 100644 index 0000000..084d7b4 --- /dev/null +++ b/fab_access/config.py @@ -0,0 +1,33 @@ +import os + +def _read_from_env(key: str, default: str = None) -> str: + if (value := os.environ.get(key, default)) is not None: + return value + raise Exception(f'[Config] Error: Cannot find required value {key}') + +def get_keycloak_config(): + return { + 'server_url': _read_from_env('KEYCLOAK_URL'), + 'username': _read_from_env('KEYCLOAK_USERNAME'), + 'password': _read_from_env('KEYCLOAK_PASSWORD'), + 'realm_name': _read_from_env('REALM'), + } + +def get_database_config(): + return { + 'host': _read_from_env('DB_HOSTNAME'), + 'user': _read_from_env('DB_USERNAME'), + 'port': int(_read_from_env('DB_PORT', '3306')), + 'password': _read_from_env('DB_PASSWORD'), + 'dbname': _read_from_env('DB_DATABASE'), + } + +def get_mqtt_config(): + print("port", int(_read_from_env('MQTT_PORT', '1883'))) + return { + 'client_name': _read_from_env('MQTT_CLIENT'), + 'username': _read_from_env('MQTT_USERNAME', ''), + 'password': _read_from_env('MQTT_PASSWORD', ''), + 'broker': _read_from_env('MQTT_BROKER'), + 'port': int(_read_from_env('MQTT_PORT', '1883')), + } diff --git a/fab_access/main.py b/fab_access/main.py new file mode 100644 index 0000000..3903aa7 --- /dev/null +++ b/fab_access/main.py @@ -0,0 +1,153 @@ +from keycloak import KeycloakAdmin +import json +import psycopg +import os +from paho.mqtt.client import Client as MQTTClient + +import config +from mqtt_helper import MQTTHelper + +conn: psycopg.Connection +keycloak_admin: KeycloakAdmin + +def publish(client, topic,msg): + result = client.publish(topic, msg) + status: 0 | 1 = result[0] + if status == 0: + #print(f"Send `{msg}` to topic `{topic}`") + pass + else: + print(f"Failed to send message to topic {topic}") + +def changePlug(client: MQTTClient, plug_id: str, is_running: bool): + publish(client,f"/FabLogging/{plug_id}/POWER", is_running) + cmd = "On" if is_running else "Off" + publish(client, f"/aktoren/{plug_id}/cmnd/POWER", cmd) + +def changeDisplay(client: MQTTClient, reader_id: str, status: int, fab_card_id: str): + publish(client, f"/cmnd/reader/{reader_id}", '{"Cmd": "message", "MssgID": %s , "ClrTxt":"" , "AddnTxt":"%s"}' % (status, fab_card_id)) + +def hasPermission(UserPermsJSON, PermissionPath): + # ToDo: refactor this + permission = False + error = 0 + MachinePermArray = PermissionPath.split(".") + for UserPerm in UserPermsJSON: + #print(f"check {UserPerm}") + UserPermArray = UserPerm.split(".") + x = 0 + for UserPermPart in UserPermArray: + if(error == 0): + #print(f"Compare {UserPermPart} and {MachinePermArray[x]}") + if not (MachinePermArray[x] == UserPermPart): + if(UserPermPart == "*"): + #print("* regelt") + permission = True + else: + error = 1 + #print(f"MISmatch between {MachinePermArray[x]} and {UserPermPart}") + else: + pass + #print(f"Match between {MachinePermArray[x]} and {UserPermPart}") + x = x + 1 + if(error == 1): + pass + #print("Error") + else: + #print("Hurra") + permission = True + error = 0 + return permission + +def handle_msg(client: MQTTClient, userdata, msg): + global conn + + payload = msg.payload.decode() + print(f"Received `{payload}` from `{msg.topic}` topic") + + fab_card_id = json.loads(msg.payload.decode())["UID"] + reader_id = msg.topic.split("/")[-1] + + # check user exists + users = keycloak_admin.get_users({ 'attributes': { 'FabCard': fab_card_id } }) + if (len(users) != 1): + print(f'Found {len(users)} users with {fab_card_id=}') + changeDisplay(client, reader_id, 16, fab_card_id) + return + + # retrieve user attributes from DB + user = users[0] + print(f"FabCard matches with user {user['username']}") + with conn.cursor() as cursor: + cursor.execute(f'SELECT PlugName,PermissionPath,Status,LastUser FROM ReaderPlug WHERE ReaderID=?;', (reader_id,)) + res = cursor.fetchone() + if res is None: + print(f'Error fetching card info from db for {reader_id=}') + return + plug_id, permission_path, is_running, last_user = res + + # check permissions + user_permissions = user['attributes']['FabPermissions'][0] + if not hasPermission(user_permissions, permission_path): + changeDisplay(client, reader_id, 7, "") + return + + # check for mentor + if is_running and last_user != user['username']: + try: + if (keycloak_admin.get_user_groups(user_id=user['id'])[0]['name'] == 'Mentoren'): + print('Overrided becouse of "Mentor" Group') + else: + print(f'Bereits benutzt von {last_user}') + error = True + except IndexError: + print("No groups available") + error = True + if(error): + changeDisplay(client, reader_id, 9, last_user) + + # toggle machine state + print(f"Turn Plug {'off' if is_running else 'on'}") + changePlug(client, plug_id, is_running) + publish(client, f"/FabLogging/{plug_id}/USER", user['username']) + + # ToDo: refactor display name construction + try: + firstCombo = user['firstName']+" " + DisplayUser = firstCombo+user['lastName'] + if(len(firstCombo) >= 7): + DisplayUser = user['firstName'][:7] + except KeyError: + DisplayUser = user['username'] + if(len(DisplayUser) > 7): + DisplayUser = DisplayUser[0:7] + "." + DisplayUser[7+1: ] + DisplayUser = DisplayUser[:8] + + changeDisplay(client, reader_id, 20, f"Login\n{DisplayUser}" if is_running else "Bitte anmelden") + + # write new status to db + with conn.cursor() as cursor: + cursor.execute( + f'UPDATE ReaderPlug SET Status=?, LastUser="?" WHERE ReaderID="?";', + (is_running, user['username'], reader_id), + ) + conn.commit() + +def main(): + global conn + + mqtt_client = MQTTHelper(**config.get_mqtt_config()) + mqtt_client.subscribe("/rfid_reader/#", handle_msg) + mqtt_client.loop_forever() + + try: + conn = psycopg.connect(**config.get_database_config()) + except mariadb.Error as e: + print(f"Error connecting to MariaDB Platform") + raise e + + keycloak_admin = KeycloakAdmin(**config.get_keycloak_config(), verify=True) + + +if __name__ == '__main__': + main() diff --git a/fab_access/mqtt_helper.py b/fab_access/mqtt_helper.py new file mode 100644 index 0000000..bd5fc50 --- /dev/null +++ b/fab_access/mqtt_helper.py @@ -0,0 +1,32 @@ +from paho.mqtt import client as mqtt_client + +import config + + +def on_connect(client, userdata, flags, rc): + if rc == 0: + print("Connected to MQTT Broker!") + else: + raise Exception("Failed to connect, return code %d\n", rc) + + +class MQTTHelper(): + def __init__(self, client_name: str, username: str, password: str, broker: str, port: int): + client = mqtt_client.Client(client_name) + if username and password: + client.username_pw_set(username, password) + else: + print("Connecting to MQTT without credentials") + client.on_connect = on_connect + try: + client.connect(broker, port) + except Exception as e: + raise Exception(f"Error connecting to MQTT {broker}:{port}") from e + self.client = client + + def subscribe(self, topic: str, handler): + self.client.subscribe(topic) + self.client.on_message = handler + + def loop_forever(self): + self.client.loop_forever() diff --git a/main.py b/main.py deleted file mode 100644 index 9ce93f7..0000000 --- a/main.py +++ /dev/null @@ -1,178 +0,0 @@ -from paho.mqtt import client as mqtt_client -from keycloak import KeycloakAdmin -import json -import mysql.connector -import os - -broker = 'mosquitto' -port = 1883 -client_id = f'FabMan' - -KEYCLOAK_URL = "http://keycloak:8080/auth/" -KEYCLOAK_USERNAME = os.environ['KEYCLOAK_USER_NAME'] -KEYCLOAK_PASSWORD = os.environ['KEYCLOAK_USER_PW'] -REALM = os.environ['KEYCLOAK_REALM'] - -FabDB = mysql.connector.connect( - host="FabDB", - user=os.environ['FABDB_DB_USER_NAME'], - password=os.environ['FABDB_DB_USER_PW'], - database=os.environ['FABDB_DB_NAME'] -) - -def connect_mqtt(): - def on_connect(client, userdata, flags, rc): - if rc == 0: - print("Connected to MQTT Broker!") - else: - print("Failed to connect, return code %d\n", rc) - - client = mqtt_client.Client(client_id) - client.username_pw_set("admin", "user") - client.on_connect = on_connect - client.connect(broker, port) - return client - -def publish(client,topic,msg): - result = client.publish(topic, msg) - # result: [0, 1] - status = result[0] - if status == 0: - #print(f"Send `{msg}` to topic `{topic}`") - pass - else: - print(f"Failed to send message to topic {topic}") - -def changePlug(client,PlugID,state): - publish(client,f"/FabLogging/{PlugID}/POWER",state) - cmd = ["OFF", "On"][state] - publish(client,f"/aktoren/{PlugID}/cmnd/POWER",cmd) - -def changeDisplay(client,MachineID,status,text): - publish(client,f"/cmnd/reader/{MachineID}",'{"Cmd": "message", "MssgID": %s , "ClrTxt":"" , "AddnTxt":"%s"}' % ((status), (text))) - -def checkPermission(UserPermsJSON,PermissionPath): - permission = False - error = 0 - MachinePermArray = PermissionPath.split(".") - for UserPerm in UserPermsJSON: - #print(f"check {UserPerm}") - UserPermArray = UserPerm.split(".") - x = 0 - for UserPermPart in UserPermArray: - if(error == 0): - #print(f"Compare {UserPermPart} and {MachinePermArray[x]}") - if not (MachinePermArray[x] == UserPermPart): - if(UserPermPart == "*"): - #print("* regelt") - permission = True - else: - error = 1 - #print(f"MISmatch between {MachinePermArray[x]} and {UserPermPart}") - else: - pass - #print(f"Match between {MachinePermArray[x]} and {UserPermPart}") - x = x + 1 - if(error == 1): - pass - #print("Error") - else: - #print("Hurra") - permission = True - error = 0 - return permission - -def msg_handler(msg,client): - print("") - print(f"Received `{msg.payload.decode()}` from `{msg.topic}` topic") - topic = msg.topic - MQTT_KEY = json.loads(msg.payload.decode())["UID"] - keycloak_admin = KeycloakAdmin( server_url=KEYCLOAK_URL, username=KEYCLOAK_USERNAME, password=KEYCLOAK_PASSWORD, realm_name=REALM, verify=True) - users = keycloak_admin.get_users({}) - knownUser = False - ReaderID = msg.topic.split("/")[-1] - for user in users: - try: - FabCardID = user['attributes']['FabCard'][0] - UserPermissions = user['attributes']['FabPermissions'][0] - if (FabCardID == MQTT_KEY): - knownUser = True - print(f"FabCard matches with user {user['username']}") - - mycursor = FabDB.cursor() - mycursor.execute(f'SELECT PlugName,PermissionPath,Status,LastUser FROM ReaderPlug WHERE ReaderID="{ReaderID}";') - myValues = mycursor.fetchall()[0] - PlugID = myValues[0] - PermissionPath = myValues[1] - MachineStatus = myValues[2] - LastUser = myValues[3] - #print(PermissionPath) - UserPermsJSON = json.loads(UserPermissions) - permission = checkPermission(UserPermsJSON, PermissionPath) - print('System "use" access %s' % ['denied','granted'][permission]) - - if(permission): - error = False - status = MachineStatus - ActualUser = user['username'] - if(status): - status = False - if(LastUser == ActualUser): - error = False - else: - try: - if (keycloak_admin.get_user_groups(user_id=user['id'])[0]['name'] == 'Mentoren'): - print('Overrided becouse of "Mentor" Group') - else: - print(f'Bereits benutzt von {LastUser}') - error = True - except IndexError: - print("No groups available") - error = True - if(error): - changeDisplay(client, ReaderID, 9, LastUser) - else: - status = True - error = False - if(error == False): - #KSstatus ^= True - switch = ["off", "on"][status] - print(f"Turn Plug {switch}") - #print(status) - changePlug(client, PlugID, status) - publish(client,f"/FabLogging/{PlugID}/USER",ActualUser) - try: - firstCombo = user['firstName']+" " - DisplayUser = firstCombo+user['lastName'] - if(len(firstCombo) >= 7): - DisplayUser = user['firstName'][:7] - except KeyError: - DisplayUser = user['username'] - if(len(DisplayUser) > 7): - DisplayUser = DisplayUser[0:7] + "." + DisplayUser[7+1: ] - DisplayUser = DisplayUser[:8] - changeDisplay(client, ReaderID, [20, 20][status], ["Bitte anmelden", f"Login\n{DisplayUser}"][status]) - mycursor.execute(f'UPDATE ReaderPlug SET Status={status}, LastUser="{ActualUser}" WHERE ReaderID="{ReaderID}";') - FabDB.commit() - else: - changeDisplay(client, ReaderID, 7, "") - except KeyError: - # Key is not present - pass - if not (knownUser): - changeDisplay(client, ReaderID, 16, MQTT_KEY) - -def subscribe(client: mqtt_client, topic): - def on_message(client, userdata, msg): - msg_handler(msg,client) - - client.subscribe(topic) - client.on_message = on_message - -def run(): - client = connect_mqtt() - subscribe(client,"/rfid_reader/#") - client.loop_forever() - -if __name__ == '__main__': - run() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c57afff --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +keycloak_wrapper +paho-mqtt +psycopg[binary] +python-keycloak From 6d97cc850d6129791e924ed0642788ef5aab553a Mon Sep 17 00:00:00 2001 From: Philipp Fruck Date: Tue, 1 Nov 2022 21:52:17 +0100 Subject: [PATCH 3/5] add .env sample to README --- README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/README.md b/README.md index e69de29..ba67db8 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,21 @@ +# Fab Access + +The backend implementation that handles access control for the FabLab. In order to test, a `.env` file with the following content must be created. A test setup can then be started using `docker-compose up --build` + + +```sh +KEYCLOAK_URL=auth.sfz-aalen.space +KEYCLOAK_USER_NAME=YOUR_USERNAME +KEYCLOAK_USER_PW=YOUR_PASSWORD +KEYCLOAK_REALM=master + +DB_HOSTNAME=db +DB_USERNAME=postgres +DB_PASSWORD=postgres +DB_DATABASE=fab_access + +MQTT_USERNAME=user +MQTT_PASSWORD=password +MQTT_BROKER=mqtt +MQTT_CLIENT=FabMan +``` From 615cbc359658846bcbe5704d5c6139fdedce7ad0 Mon Sep 17 00:00:00 2001 From: Luca Lutz Date: Tue, 1 Nov 2022 21:57:16 +0000 Subject: [PATCH 4/5] Add new file --- Jenkinsfile | 54 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 Jenkinsfile diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..0a62865 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,54 @@ +pipeline { + agent any + post { + failure { + updateGitlabCommitStatus name: 'build', state: 'failed' + } + success { + updateGitlabCommitStatus name: 'build', state: 'success' + } + aborted { + updateGitlabCommitStatus name: 'build', state: 'canceled' + } + } + options { + gitLabConnection('GitLab') + } + triggers { + gitlab(triggerOnPush: true, triggerOnMergeRequest: true, branchFilterType: 'All') + } + stages { + stage("build") { + steps { + updateGitlabCommitStatus name: 'build', state: 'running' + podTemplate( + containers: [ + containerTemplate(name: 'docker-image-build', image: 'mgoltzsche/podman', ttyEnabled: true, command: 'cat', privileged: true), + ]) { + node(POD_LABEL) { + updateGitlabCommitStatus name: 'build', state: 'pending' + checkout([$class: 'GitSCM', branches: [ + [name: '*/main'] + ], userRemoteConfigs: [ + [url: 'https://gitlab.com/luca_lutz/fabaccess.git'] + ]]) + container('docker-image-build') { + withCredentials([ + usernamePassword(credentialsId: 'docker.credentials', + usernameVariable: 'DOCKER_USERNAME', + passwordVariable: 'DOCKER_PASSWORD') + ]) { + sh 'echo $(date +%s) > /time' + sh 'echo "nameserver 10.12.42.2" > /etc/resolv.conf' + sh 'podman build --tag docker.sfz-aalen.space/hackwerk/fabaccess:$(cat /time) .' + sh 'podman login -u ${DOCKER_USERNAME} -p ${DOCKER_PASSWORD} docker.sfz-aalen.space' + sh 'podman push docker.sfz-aalen.space/hackwerk/fabaccess:$(cat /time)' + } + } + } + } + } + } + } +} + From f5bfd1145f43f5a4a8fdc7b61515235a5742beb1 Mon Sep 17 00:00:00 2001 From: Luca Lutz Date: Wed, 2 Nov 2022 20:37:41 +0100 Subject: [PATCH 5/5] Fixing shit --- fab_access/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/fab_access/main.py b/fab_access/main.py index 3903aa7..054a4e8 100644 --- a/fab_access/main.py +++ b/fab_access/main.py @@ -135,6 +135,7 @@ def handle_msg(client: MQTTClient, userdata, msg): def main(): global conn + global keycloak_admin mqtt_client = MQTTHelper(**config.get_mqtt_config()) mqtt_client.subscribe("/rfid_reader/#", handle_msg)