diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..3347a18 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +.circleci/ +.git/ +test/ diff --git a/.github/workflows/container-build.yaml b/.github/workflows/container-build.yaml new file mode 100644 index 0000000..4a16230 --- /dev/null +++ b/.github/workflows/container-build.yaml @@ -0,0 +1,95 @@ +name: Create and publish Container image + +on: + push: + branches: + - master + tags: + - 'v*' + pull_request: + branches: + - master + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + TEST_TAG: ${{ github.repository }}:test_tag + +jobs: + build-and-push-image: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + strategy: + matrix: + include: + - container: Containerfile.debian + autotag: false + suffix: -debian + - container: Containerfile.alpine + autotag: auto + runuser: apk add --no-cache bash runuser + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + lfs: true + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to Container registry + uses: docker/login-action@v2 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Docker meta + id: meta + uses: docker/metadata-action@v4 + with: + images: | + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=edge + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + flavor: | + latest=${{ matrix.autotag }} + suffix=${{ matrix.suffix }} + + - name: Build and export + uses: docker/build-push-action@v4 + with: + context: . + file: Containerfile + load: true + tags: ${{ env.TEST_TAG }} + + - name: Test + run: > + docker container run \ + --rm \ + --user 'root:root' \ + --volume "$(pwd)/test:/usr/local/src/semonitor/test" \ + ${{ env.TEST_TAG }} \ + '/bin/sh' -c '$${{ matrix.runuser }} runuser --user semonitor -- ./test/test.sh' + + - name: Build and push + uses: docker/build-push-action@v4 + with: + platforms: linux/amd64,linux/386,linux/arm64,linux/arm/v7,linux/arm/v6 + context: . + file: Containerfile + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/Containerfile b/Containerfile new file mode 120000 index 0000000..83ac7f4 --- /dev/null +++ b/Containerfile @@ -0,0 +1 @@ +Containerfile.alpine \ No newline at end of file diff --git a/Containerfile.alpine b/Containerfile.alpine new file mode 100644 index 0000000..9314d87 --- /dev/null +++ b/Containerfile.alpine @@ -0,0 +1,38 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later +# +# Copyright (C) 2023 Olliver Schinagl + +ARG PYTHON_VERSION="3-alpine" + +FROM index.docker.io/library/python:${PYTHON_VERSION} + +COPY requirements.txt /tmp/ + +RUN apk add --no-cache --virtual .build-deps \ + gcc \ + musl-dev \ + linux-headers \ + && \ + pip --no-cache-dir install --requirement '/tmp/requirements.txt' && \ + rm '/tmp/requirements.txt' && \ + apk del .build-deps && \ + install -d -m 0775 '/usr/local/src' && \ + addgroup -S 'semonitor' && \ + adduser -D -G 'semonitor' -h '/usr/local/src/semonitor' -s '/bin/nologin' -S 'semonitor' && \ + adduser 'semonitor' 'usb' && \ + install -d -m 0775 -g 'semonitor' -o 'semonitor' '/var/lib/semonitor' + +VOLUME /var/lib/semonitor + +WORKDIR /usr/local/src/semonitor/ + +COPY conversion /usr/local/src/semonitor/conversion +COPY scripts/semonitor.sh /usr/local/bin/semonitor.sh +COPY se /usr/local/src/semonitor/se +COPY semonitor.py /usr/local/src/semonitor/semonitor.py +COPY services/container-entrypoint.sh /init +COPY utilities /usr/local/src/semonitor/utilities + +USER semonitor + +ENTRYPOINT [ "/init" ] diff --git a/Containerfile.debian b/Containerfile.debian new file mode 100644 index 0000000..8996937 --- /dev/null +++ b/Containerfile.debian @@ -0,0 +1,37 @@ +ARG PYTHON_VERSION="3-slim" + +FROM index.docker.io/library/python:${PYTHON_VERSION} + +COPY requirements.txt /tmp/ + +RUN ARCH="$(dpkg --print-architecture | \ + sed -e 's|armel|rpi|g' -e 's|armhf|armmp|g' -e 's|i386|686|g')" && \ + BUILD_DEPS=" \ + gcc \ + linux-headers-${ARCH} \ + " && \ + apt-get update && apt-get install --yes ${BUILD_DEPS:+${BUILD_DEPS}} && \ + pip --no-cache-dir install --requirement '/tmp/requirements.txt' && \ + rm '/tmp/requirements.txt' && \ + apt-get purge --yes ${BUILD_DEPS:+${BUILD_DEPS}} && \ + apt-get autoremove --yes && \ + apt-get clean && \ + rm -f -r '/var/lib/apt' && \ + install -d -m 0775 '/usr/local/src' && \ + useradd -d '/usr/local/src/semonitor' -m -r -s '/usr/sbin/nologin' 'semonitor' && \ + install -d -m 0775 -g 'semonitor' -o 'semonitor' '/var/lib/semonitor' + +VOLUME /var/lib/semonitor + +WORKDIR /usr/local/src/semonitor/ + +COPY conversion /usr/local/src/semonitor/conversion +COPY scripts/semonitor.sh /usr/local/bin/semonitor.sh +COPY se /usr/local/src/semonitor/se +COPY semonitor.py /usr/local/src/semonitor/semonitor.py +COPY services/container-entrypoint.sh /init +COPY utilities /usr/local/src/semonitor/utilities + +USER semonitor + +ENTRYPOINT [ "/init" ] diff --git a/Dockerfile b/Dockerfile new file mode 120000 index 0000000..3673ae3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1 @@ +Containerfile.debian \ No newline at end of file diff --git a/README.Docker.md b/README.Docker.md new file mode 100644 index 0000000..db9753e --- /dev/null +++ b/README.Docker.md @@ -0,0 +1,100 @@ +# Running semonitor in docker +SEMonitor can be run within a Docker container. This provides isolation from +other processes by running it in a containerized environment. As this is not +and in-depth tutorial on docker, those with Docker, containers or cgroups see +[docker.com][docker]. + +This guide is not a comprehensive guide, but just lists the most basic things! + + +## Building the container +Building the container is only needed if the official one is not sufficient, +or when developing on semonitor. + +To build the image, the following can be used, where `ISSUE-123` is just used +as an example. It is important in that it will be re-used later. + +```sh +docker image build \ + --file 'Containerfile' \ + --rm \ + --tag 'semonitor:ISSUE-123' \ + './' +``` + + +## Running tests +The tests are not included in the container image, so we volume mount them to +make them available to the installed semonitor application. + +```sh +docker container run \ + --interactive \ + --rm \ + --tty \ + --user 'root:root' \ + --volume "$(pwd)/test:/usr/local/src/semonitor/test" \ + 'semonitor:ISSUE-123' \ + '/bin/sh' -c 'runuser --user semonitor -- ./test/test.sh' +``` + + +## Running serial device +Running the semonitor on a serial device using the official latest image can +be done as follows. + +```sh +docker container run \ + --device '/dev/solaredge0:/dev/ttyUSB0' \ + --interactive \ + --rm \ + --tty \ + --volume "$(pwd)/semonitor_logs/:/semonitor/" \ + 'ghcr.io/jbuehl/solaredge:latest' \ + semonitor.sh \ + -a \ + -b 115200 \ + -m \ + -o "/semonitor/json/$(date +%Y%m%d).json" \ + -r "/semonitor/rec/$(date +%Y%m%d).rec" \ + -s '1234567' \ + -t 4 \ + '/dev/ttyUSB0' +``` + +## Using compose +It is also possible to run the container using `docker compose`. Here an +example. The device used is a udev symlinked serial to USB adapter, whith +the appropriate permissions. + +```yaml +networks: + semonitor: {} + +volumes: + semonitor: + +services: + sslh: + image: ghcr.io/jbuehl/solaredge:master + cap_drop: + - all + ulimits: + nproc: 64 + nofile: + soft: 4194304 + hard: 16777216 + devices: + - /dev/solaredge:/dev/ttyUSB0 + env_file: + - common.env + volumes: + - semonitor:/var/lib/semonitor:rw + networks: + - semonitor + expose: + - "80/tcp" + - "22221-22222/tcp" + command: -a -b 115200 -m -o "/var/lib/semonitor/json/__date__.json" -r "/var/lib/semonitor/rec/__date__.rec" -s '1234567' -t 4 '/dev/ttyUSB0' + restart: unless-stopped +``` diff --git a/scripts/semonitor.sh b/scripts/semonitor.sh new file mode 100755 index 0000000..6fc8860 --- /dev/null +++ b/scripts/semonitor.sh @@ -0,0 +1,24 @@ +#!/bin/sh + +set -eu +if [ -n "${DEBUG_TRACE_SH:-}" ] && \ + [ "${DEBUG_TRACE_SH:-}" != "${DEBUG_TRACE_SH#*"$(basename "${0}")"*}" ] || \ + [ "${DEBUG_TRACE_SH:-}" = 'all' ]; then + set -x +fi + +se_date_fmt="${SE_DATE_FMT:-%Y%m%d}" +se_start_year="${SE_START_YEAR:-2023}" + +# Wait for a reasonable date to be set +while [ "$(date '+%Y')" -lt "${se_start_year}" ]; do + echo "Date is to far in the past ($(date '+%Y') < ${se_start_year})." + echo 'Override by setting "SE_START_YEAR" environment variable.' + sleep 1 +done + +args="$(echo "${@}" | sed "s|__date__|$(date "+${se_date_fmt}")|g")" + +exec '/usr/local/src/semonitor/semonitor.py' ${args:+${args}} + +exit 0 diff --git a/services/container-entrypoint.sh b/services/container-entrypoint.sh new file mode 100755 index 0000000..3fffe72 --- /dev/null +++ b/services/container-entrypoint.sh @@ -0,0 +1,24 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0-or-later +# +# Copyright (C) 2023 Olliver Schinagl +# +# A beginning user should be able to docker run image bash (or sh) without +# needing to learn about --entrypoint +# https://github.com/docker-library/official-images#consistency + +set -eu + +bin='semonitor.sh' + +# run command if it is not starting with a "-" and is an executable in PATH +if [ "${#}" -le 0 ] || \ + [ "${1#-}" != "${1}" ] || \ + [ -d "${1}" ] || \ + ! command -v "${1}" > '/dev/null' 2>&1; then + entrypoint='true' +fi + +exec ${entrypoint:+${bin:?}} "${@}" + +exit 0