From 690f0f3cf504bc559ae6a33f0aa0bc381ae3bbf6 Mon Sep 17 00:00:00 2001 From: Wolfgang Hottgenroth Date: Thu, 19 Aug 2021 16:39:29 +0200 Subject: [PATCH] initial --- .gitignore | 2 ++ .gitlab-ci.yml | 38 ++++++++++++++++++++ Dockerfile | 41 ++++++++++++++++++++++ api.py | 87 ++++++++++++++++++++++++++++++++++++++++++++++ openapi.yaml | 94 ++++++++++++++++++++++++++++++++++++++++++++++++++ server.ini | 6 ++++ server.py | 12 +++++++ test.py | 8 +++++ 8 files changed, 288 insertions(+) create mode 100644 .gitignore create mode 100644 .gitlab-ci.yml create mode 100644 Dockerfile create mode 100644 api.py create mode 100644 openapi.yaml create mode 100644 server.ini create mode 100644 server.py create mode 100644 test.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f3e41d --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +ENV \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..f886d01 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,38 @@ +stages: + - check + - build + +variables: + IMAGE_NAME: $CI_REGISTRY/$CI_PROJECT_PATH + +check: + image: registry.hottis.de/dockerized/base-build-env:latest + stage: check + tags: + - hottis + - linux + - docker + rules: + - if: $CI_COMMIT_TAG + script: + - checksemver.py -v + --versionToValidate "${CI_COMMIT_TAG}" + --validateMessage + --messageToValidate "${CI_COMMIT_MESSAGE}" + +build: + image: registry.hottis.de/dockerized/docker-bash:latest + stage: build + tags: + - hottis + - linux + - docker + script: + - docker build --tag $IMAGE_NAME:latest . + - if [ "$CI_COMMIT_TAG" != "" ]; then + docker tag $IMAGE_NAME:latest $IMAGE_NAME:${CI_COMMIT_TAG}; + docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY; + docker push $IMAGE_NAME:latest; + docker push $IMAGE_NAME:${CI_COMMIT_TAG}; + fi + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b7ce58f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,41 @@ +FROM python:latest + +LABEL Maintainer="Wolfgang Hottgenroth wolfgang.hottgenroth@icloud.com" +LABEL ImageName="registry.hottis.de/wolutator/modbusservice" + +ARG APP_DIR="/opt/app" +ARG CONF_DIR="${APP_DIR}/config" + + + +RUN \ + apt update && \ + pip3 install loguru && \ + pip3 install dateparser && \ + pip3 install connexion && \ + pip3 install connexion[swagger-ui] && \ + pip3 install uwsgi && \ + pip3 install flask-cors && \ + pip3 install six && \ + pip3 install pymodbus + +RUN \ + mkdir -p ${APP_DIR} && \ + mkdir -p ${CONF_DIR} && \ + useradd -d ${APP_DIR} -u 1000 user + +COPY *.py ${APP_DIR}/ +COPY openapi.yaml ${APP_DIR}/ +COPY server.ini ${CONF_DIR}/ + +USER 1000:1000 +WORKDIR ${APP_DIR} +VOLUME ${CONF_DIR} + +EXPOSE 5000 +EXPOSE 9191 + +CMD [ "uwsgi", "./config/server.ini" ] + + + diff --git a/api.py b/api.py new file mode 100644 index 0000000..31538e5 --- /dev/null +++ b/api.py @@ -0,0 +1,87 @@ +import time +import connexion +import json +import werkzeug +import os +from loguru import logger +from pymodbus.client.sync import ModbusTcpClient +from pymodbus.exceptions import ModbusIOException + + +#with open('/opt/app/config/authservice.key', 'r') as f: +# JWT_PRIV_KEY = f.read() + +clients = { + "modbus1": "172.16.2.157" +} + +clientConn = {} + +class UnknownClientException (Exception): pass + +class UnableToConnectException (Exception): pass + +class ConnectionException (Exception): pass + +class IllegalValueException (Exception): pass + + +def connect(name): + try: + if name not in clientConn: + clientConn[name] = ModbusTcpClient(clients[name]) + if not clientConn[name].connect(): + raise UnableToConnectException() + return clientConn[name] + except KeyError as e: + raise UnknownClientException(e) + +def close(name): + try: + clientConn[name].close() + except KeyError as e: + raise UnknownClientException(e) + + +def listClients(): + return [ {"name":k, "address":v} for k,v in clients.items() ] + +def createClient(**args): + try: + body = args["body"] + + name = body["name"] + address = body["address"] + + return "Ok" + except KeyError as e: + raise werkzeug.exceptions.BadRequest("name or address missing") + except Exception as e: + raise werkzeug.exceptions.BadRequest("{}: {}".format(e.__class__.__name__, str(e))) + +def readCoil(client, address): + try: + conn = connect(client) + res = conn.read_coils(address) + if isinstance(res, ModbusIOException): + raise ConnectionException() + return int(res.bits[0]) + except Exception as e: + raise werkzeug.exceptions.BadRequest("{}: {}".format(e.__class__.__name__, str(e))) + finally: + close(client) + +def writeCoil(client, address, **args): + try: + value = args["body"] + if value not in [ 0, 1 ]: + raise IllegalValueException("Value in body must be 0 or 1") + conn = connect(client) + res = conn.write_coil(address, value) + if isinstance(res, ModbusIOException): + raise ConnectionException() + return "Ok" + except Exception as e: + raise werkzeug.exceptions.BadRequest("{}: {}".format(e.__class__.__name__, str(e))) + finally: + close(client) diff --git a/openapi.yaml b/openapi.yaml new file mode 100644 index 0000000..d988351 --- /dev/null +++ b/openapi.yaml @@ -0,0 +1,94 @@ +openapi: 3.0.0 +info: + title: ModbusService + version: "0.1" + +paths: + /client: + get: + tags: [ "Client" ] + summary: List all configured clients + operationId: api.listClients + responses: + '200': + description: All configured clients + content: + 'application/json': + schema: + type: array + items: + $ref: '#/components/schemas/Client' + post: + tags: [ "Client" ] + summary: Create a new client + operationId: api.createClient + requestBody: + content: + 'application/json': + schema: + $ref: '#/components/schemas/Client' + responses: + '200': + description: Client successfully created + /action/coil/{client}/{address}: + get: + tags: [ "Action" ] + summary: Return a coil of client + operationId: api.readCoil + parameters: + - name: client + in: path + required: true + schema: + type: string + - name: address + in: path + required: true + schema: + type: integer + responses: + '200': + description: Status of the coil + content: + 'application/json': + schema: + type: integer + put: + tags: [ "Action" ] + summary: Set a coil of client + operationId: api.writeCoil + parameters: + - name: client + in: path + required: true + schema: + type: string + - name: address + in: path + required: true + schema: + type: integer + requestBody: + content: + 'application/json': + schema: + type: number + responses: + '200': + description: Status of the coil + content: + 'application/json': + schema: + type: number + + +components: + schemas: + Client: + description: Modbus Client + type: object + properties: + name: + type: string + address: + type: string diff --git a/server.ini b/server.ini new file mode 100644 index 0000000..dc9b891 --- /dev/null +++ b/server.ini @@ -0,0 +1,6 @@ +[uwsgi] +http = :5000 +wsgi-file = server.py +processes = 4 +stats = :9191 + diff --git a/server.py b/server.py new file mode 100644 index 0000000..170b48b --- /dev/null +++ b/server.py @@ -0,0 +1,12 @@ +import connexion +from flask_cors import CORS + +# instantiate the webservice +app = connexion.App(__name__) +app.add_api('openapi.yaml', options = {"swagger_ui": True}) + +# CORSify it - otherwise Angular won't accept it +CORS(app.app) + +# provide the webservice application to uwsgi +application = app.app diff --git a/test.py b/test.py new file mode 100644 index 0000000..75cbcaa --- /dev/null +++ b/test.py @@ -0,0 +1,8 @@ +import connexion +import logging + +logging.basicConfig(level=logging.DEBUG) + +app = connexion.App('modbusservice') +app.add_api('./openapi.yaml') +app.run(port=8080)