diff --git a/.github/workflows/docker-publish.yaml b/.github/workflows/docker-publish.yaml new file mode 100644 index 0000000..0706d74 --- /dev/null +++ b/.github/workflows/docker-publish.yaml @@ -0,0 +1,86 @@ +name: Publish Docker image + +on: + push: + branches: ["main"] + tags: ["1.1.7"] + pull_request: + branches: ["main"] + +env: + # Use docker.io for Docker Hub if empty + REGISTRY: ghcr.io + # github.repository as / + IMAGE_NAME: ${{ github.repository }} + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Install the cosign tool except on PR + # https://github.com/sigstore/cosign-installer + - name: Install cosign + if: github.event_name != 'pull_request' + uses: sigstore/cosign-installer@6e04d228eb30da1757ee4e1dd75a0ec73a653e06 #v3.1.1 + with: + cosign-release: "v2.1.1" + + # Set up BuildKit Docker container builder to be able to build + # multi-platform images and export cache + # https://github.com/docker/setup-buildx-action + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 + + # Login against a Docker registry except on PR + # https://github.com/docker/login-action + - name: Log into registry ${{ env.REGISTRY }} + if: github.event_name != 'pull_request' + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Extract metadata (tags, labels) for Docker + # https://github.com/docker/metadata-action + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # v5.0.0 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + + # Build and push Docker image with Buildx (don't push on PR) + # https://github.com/docker/build-push-action + - name: Build and push Docker image + id: build-and-push + uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + # Sign the resulting Docker image digest except on PRs. + # This will only write to the public Rekor transparency log when the Docker + # repository is public to avoid leaking data. If you would like to publish + # transparency data even for private images, pass --force to cosign below. + # https://github.com/sigstore/cosign + - name: Sign the published Docker image + if: ${{ github.event_name != 'pull_request' }} + env: + # https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable + TAGS: ${{ steps.meta.outputs.tags }} + DIGEST: ${{ steps.build-and-push.outputs.digest }} + # This step uses the identity token to provision an ephemeral certificate + # against the sigstore community Fulcio instance. + run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST} diff --git a/Dockerfile b/Dockerfile index 05d0887..4b98f3d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,20 @@ FROM docker.io/ubuntu:jammy-20240227 -RUN apt-get update && apt-get install -y wget gpg xvfb libgbm-dev libasound2 && \ - # GC UPS App official setup +RUN apt-get update && apt-get install -y wget gpg xvfb libgbm-dev libasound2 python3-pip && \ wget -qO- https://gcups-static.greencell.global/csgsa-keyring.gpg | gpg --dearmor | dd of=/usr/share/keyrings/csgsa-keyring.gpg && \ echo "deb [arch=amd64 signed-by=/usr/share/keyrings/csgsa-keyring.gpg] https://gcups-static.greencell.global/deb stable non-free" | dd of=/etc/apt/sources.list.d/gcups.list && \ apt-get update -y && apt-get install -y gcups && \ - rm -rf var/lib/apt/lists/* + rm -rf var/lib/apt/lists/* && \ + python3 -m pip install plyvel && \ + apt-get remove -y python3-pip && \ + mkdir -m775 -p /opt/gcups/db/gcups-rxdb-1-settings + +COPY populate-db.py db.txt ./ +RUN python3 populate-db.py + +ENV GCUPS_HTTP_PORT=8080 +ENV GCUPS_PASSWORD=gcups123 + +EXPOSE $GCUPS_HTTP_PORT -EXPOSE 8080 COPY init.sh /opt/init.sh ENTRYPOINT ["/opt/init.sh"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..51b8a57 --- /dev/null +++ b/README.md @@ -0,0 +1,118 @@ +# GreenCell UPS App in docker +GC UPS App is a program that enables preview in real time and displays measurement data including input and output voltages, load frequency of the UPS, its temperature and battery capacity. + +You can setup automatic system shut-off, notifications and email warnings in the event of switching to batter operation. + +> **DISCLAIMER:** +> I am not affiliated with GREENCELL.GLOBAL brand, I don't represent and I was never employed by CSG S.A. nor I was ever contracted by them for doing any work whatsoever + +## About +I took the desktop-only electron app and forced it to run in docker with HTTP webUI enabled. I wrote a [blog entry](https://blog.fajfer.org/2023/09/16/running-green-cell-ups-app-gcups-on-gnu-linux-server/) regarding the subject. + +Table of Contents +================= + + * [About](#about) + * [Quick Start](#quick-start) + * [How To Run It](#how-to-run-it) + * [USB detection](#usb-detection) + * [Custom settings](#custom-settings) + * [Docker-compose](#docker-compose) + * [Versioning](#versioning) + * [FAQ](#faq) + * [TODOs](#todos) + +## Quick Start +For testing purposes (to see if this thing works on your machine) you can run the container in `privileged` mode but this is not recommended, [read](https://learn.snyk.io/lesson/container-runs-in-privileged-mode/). +`docker run --privileged -p 0.0.0.0:8080:8080 ghcr.io/fajfer/gcups:1.1.7` + +Image exposes port **8080** and the default password is **gcups123** + +## How To Run It +Container itself is ready to start, the only thing you need to do is to attach proper USB device: +`docker run --device=/dev/bus/usb/001/011 -p 0.0.0.0:8080:8080 ghcr.io/fajfer/gcups:1.1.7` +How to find which device to use? Read [USB detection](#usb-detection) + +You can also run it via [Docker-compose](#docker-compose), for which I provided an [example](docker-compose.yaml) of. This is a desktop app that was forced inside of a container and it exposes a HTTP connection which makes you unable to configure following stuff: +- enabling/disabling HTTP server +- changing password +- changing HTTP server port + +It's not much of a problem since we're using docker. You can decide to not publish the port and you can always map it to a different port on your host container so it isn't much of a problem. If you wish to change the password, however, then I recommend you read [Custom settings](#custom-settings) part. + +## USB detection + +An example, based on my `UPS05`, on how to find which USB to mount to your docker. + +If you plug your usb and then proceed with `sudo dmesg` you will see something like this: +``` +[21022.370000] usb 1-4: new full-speed USB device number 11 using xhci_hcd +[21022.623977] usb 1-4: New USB device found, idVendor=0001, idProduct=0000, bcdDevice= 1.00 +[21022.623981] usb 1-4: New USB device strings: Mfr=1, Product=2, SerialNumber=0 +[21022.623983] usb 1-4: Product: MEC0003 +[21022.623985] usb 1-4: Manufacturer: MEC +``` +Let's compare this with `lsusb` output: +``` +Bus 006 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub +Bus 005 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub +Bus 004 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub +Bus 003 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub +Bus 002 Device 002: ID 05e3:0626 Genesys Logic, Inc. USB3.1 Hub +Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub +Bus 001 Device 011: ID 0001:0000 Fry's Electronics MEC0003 +Bus 001 Device 006: ID 046d:c31c Logitech, Inc. Keyboard K120 +Bus 001 Device 004: ID 08bb:2902 Texas Instruments PCM2902 Audio Codec +Bus 001 Device 002: ID 05e3:0610 Genesys Logic, Inc. Hub +Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub +``` + +Notice device number 11 from `dmesg`: +`[21022.370000] usb 1-4: new full-speed USB device number 11 using xhci_hcd` + +And that we can see both the product and the manufacturer +``` +[21022.623983] usb 1-4: Product: MEC0003 +[21022.623985] usb 1-4: Manufacturer: MEC +``` +That aligns perfectly with our device: +`Bus 001 Device 011: ID 0001:0000 Fry's Electronics MEC0003` +We need both bus and device number so `--device=/dev/bus/usb/$BUS/$DEVICE` gives us `--device=/dev/bus/usb/001/011` + +## Custom settings +If you didn't read my [blog entry](https://blog.fajfer.org/2023/09/16/running-green-cell-ups-app-gcups-on-gnu-linux-server/) I would recommend scrolling just to the "Generating your own by-sequence" part. Basically you can mount the entire `/opt/gcups/` if you wanted but the only thing you really need is `/opt/gcups/db/gcups-rxdb-1-settings` which is actually a [LevelDB](https://github.com/google/leveldb) database. + +Example: +`docker run --device=/dev/bus/usb/001/011 -p 0.0.0.0:8080:8080 -v /opt/gcups/db/gcups-rxdb-1-settings:/opt/gcups/db/gcups-rxdb-1-settings ghcr.io/fajfer/gcups:1.1.7` + +There's no other way at this moment to change the password during runtime since I don't know how to properly manipulate password hash and salt and it doesn't really bother me. + +## Docker-compose +An example of [docker-compose.yaml](docker-compose.yaml): +```yaml +version: '3' +services: + gcups: + container_name: gcups + image: ghcr.io/fajfer/gcups:1.1.7 + ports: + - '0.0.0.0:8085:8080' + restart: unless-stopped + volumes: + - $HOME/gcups-rxdb-1-settings:/opt/gcups/db/gcups-rxdb-1-settings + devices: + - /dev/bus/usb/001/010:/dev/bus/usb/001/010 +``` +Above example exposes port 8085 on the host, uses a volume and a device. You can safely remove volumes and would need to change your devices based on [USB detection](#usb-detection) + +## Versioning +I'm not a fan of `latest` tag so I will probably not to have it here. I'm going to try to always support the latest version (1.1.7 since 02.06.2023) and this is how the container image is named. If I optimize the image I'm going to update the latest available version based on it and if GreenCell releases the new version I will also build it and provide with a new tag. + +## FAQ +If you are getting the following error: +``` +/opt/gcups/gcups[101]: ../../third_party/electron_node/src/node_api.cc:1332:napi_status napi_release_threadsafe_function(napi_threadsafe_function, napi_threadsafe_function_release_mode): Assertion `(func) != nullptr' failed. +``` +Then you have your UPS connected via USB and provided a wrong `--device` for the docker image. This will obviously make the HTTP server unable to start. +## TODOs +- Optimize image size (it's 1.31G as of now which I think is way too much) diff --git a/db.txt b/db.txt new file mode 100644 index 0000000..8382db7 --- /dev/null +++ b/db.txt @@ -0,0 +1,5 @@ +(b'\xc3\xbfby-sequence\xc3\xbf0000000000000002', b'{"api":{"port":8080,"password":"Elp//afaOhvhwTrsWol+IHrff3DiIWgUeXh9QBDvwTv+ud0qVOCM4+CJNScdOT6Fr2ijXRzaV6lWkR6jyw1gQw==","enable":true,"salt":"6e42d7ca303362864ad0e18c6a686328"},"_attachments":{},"_id":"a733f0a7-4fe2-4120-8603-775370fd871a","_rev":"2-c8ed7472875060f20800fcba40ed958a"}') +(b'\xc3\xbfdocument-store\xc3\xbfa733f0a7-4fe2-4120-8603-775370fd871a', b'{"rev":"2-c8ed7472875060f20800fcba40ed958a","id":"a733f0a7-4fe2-4120-8603-775370fd871a","rev_tree":[{"pos":1,"ids":["f52800793d58ce95987ddde4e0a275a1",{"status":"available"},[["c8ed7472875060f20800fcba40ed958a",{"status":"available"},[]]]]}],"rev_map":{"1-f52800793d58ce95987ddde4e0a275a1":1,"2-c8ed7472875060f20800fcba40ed958a":2},"winningRev":"2-c8ed7472875060f20800fcba40ed958a","deleted":false,"seq":2}') +(b'\xc3\xbfmeta-store\xc3\xbf_local_doc_count', b'1') +(b'\xc3\xbfmeta-store\xc3\xbf_local_last_update_seq', b'2') +(b'\xc3\xbfmeta-store\xc3\xbf_local_uuid', b'"af676769-cd7f-42ad-872d-5ab716082efb"') diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..92b435d --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,12 @@ +version: '3' +services: + gcups: + container_name: gcups + image: ghcr.io/fajfer/gcups:1.1.7 + ports: + - '0.0.0.0:8085:8080' + restart: unless-stopped + volumes: + - $HOME/gcups-rxdb-1-settings:/opt/gcups/db/gcups-rxdb-1-settings + devices: + - /dev/bus/usb/001/010:/dev/bus/usb/001/010 diff --git a/init.sh b/init.sh old mode 100644 new mode 100755 index 3aba3ed..e726528 --- a/init.sh +++ b/init.sh @@ -4,4 +4,8 @@ dbus-daemon --session --fork --print-address 1 > /tmp/dbus-session export DBUS_SESSION_BUS_ADDRESS=$(cat /tmp/dbus-session) service dbus start -xvfb-run gcups -vvv --no-sandbox +echo "GCUPS running on port: $GCUPS_HTTP_PORT" +echo "Default webUI password: $GCUPS_PASSWORD" + +xvfb-run \ + gcups -vvv --no-sandbox diff --git a/populate-db.py b/populate-db.py new file mode 100755 index 0000000..da0a0ea --- /dev/null +++ b/populate-db.py @@ -0,0 +1,10 @@ +import plyvel + +with open("db.txt", "r") as f: + db_txt = f.readlines() + +data = [eval(line) for line in db_txt] + +db = plyvel.DB("/opt/gcups/db/gcups-rxdb-1-settings", create_if_missing=True) +for key, value in data: + db.put(key, value)