From 5f648395936519e2d0e9d05b7ab8a2aca6e3012e Mon Sep 17 00:00:00 2001 From: Reuben Miller Date: Mon, 20 Feb 2023 18:04:40 +0100 Subject: [PATCH] init. commit --- .github/workflows/release.yaml | 57 +++++++++++++++ .gitignore | 1 + README.md | 57 ++++++++++++++- ci/build.sh | 33 +++++++++ ci/publish.sh | 130 +++++++++++++++++++++++++++++++++ nfpm.yaml | 36 +++++++++ src/c8y-command | 64 ++++++++++++++++ src/c8y_Command | 4 + src/env | 5 ++ 9 files changed, 385 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/release.yaml create mode 100644 .gitignore create mode 100755 ci/build.sh create mode 100755 ci/publish.sh create mode 100644 nfpm.yaml create mode 100755 src/c8y-command create mode 100644 src/c8y_Command create mode 100644 src/env diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..631976e --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,57 @@ +name: release +permissions: + contents: write +on: + push: + tags: + - "*" + workflow_dispatch: +jobs: + release: + name: Package and release + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - uses: actions/setup-go@v3 + with: + go-version: '>=1.17.0' + - run: go install github.com/goreleaser/nfpm/v2/cmd/nfpm@latest + name: Install dependencies + + - name: Set version + run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV + + - name: Package + run: ./ci/build.sh + env: + SEMVER: ${{ env.RELEASE_VERSION }} + + - name: Upload artifacts + uses: actions/upload-artifact@v3 + with: + name: packages + path: dist/* + + - name: Release + uses: softprops/action-gh-release@v1 + if: startsWith(github.ref, 'refs/tags/') + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + generate_release_notes: true + draft: true + files: | + ./dist/* + + - name: Publish + if: startsWith(github.ref, 'refs/tags/') && env.PUBLISH_TOKEN + env: + PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }} + PUBLISH_REPO: ${{ secrets.PUBLISH_REPO }} + PUBLISH_OWNER: ${{ secrets.PUBLISH_OWNER }} + run: | + ./ci/publish.sh ./dist --repo "$PUBLISH_REPO" --owner "$PUBLISH_OWNER" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7773828 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +dist/ \ No newline at end of file diff --git a/README.md b/README.md index 613436a..da63413 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,55 @@ -# c8y-shell-plugin -thin-edge.io Cumulocity shell plugin +# c8y-command-plugin + +thin-edge.io Cumulocity IoT shell plugin to process the `c8y_Command` operation. + +## Plugin summary + +### What will be deployed to the device? + +* Cumulocity IoT command handler (binary) that allows users to execute a command in a shell + +**Technical summary** + +The following details the technical aspects of the plugin to get an idea what systems it supports. + +||| +|--|--| +|**Languages**|`shell` (posix compatible)| +|**CPU Architectures**|`all/noarch`. Not CPU specific| +|**Supported init systems**|`N/A`| +|**Required Dependencies**|-| +|**Optional Dependencies (feature specific)**|-| + +### How to do I get it? + +The following linux package formats are provided on the releases page and also in the [tedge-community](https://cloudsmith.io/~thinedge/repos/community/packages/) repository: + +|Operating System|Repository link| +|--|--| +|Debian/Raspian (deb)|[![Latest version of 'c8y-command-plugin' @ Cloudsmith](https://api-prd.cloudsmith.io/v1/badges/version/thinedge/community/deb/c8y-command-plugin/latest/a=all;d=any-distro%252Fany-version;t=binary/?render=true&show_latest=true)](https://cloudsmith.io/~thinedge/repos/community/packages/detail/deb/c8y-command-plugin/latest/a=all;d=any-distro%252Fany-version;t=binary/)| +|Alpine Linux (apk)|[![Latest version of 'c8y-command-plugin' @ Cloudsmith](https://api-prd.cloudsmith.io/v1/badges/version/thinedge/community/alpine/c8y-command-plugin/latest/a=noarch;d=alpine%252Fany-version/?render=true&show_latest=true)](https://cloudsmith.io/~thinedge/repos/community/packages/detail/alpine/c8y-command-plugin/latest/a=noarch;d=alpine%252Fany-version/)| +|RHEL/CentOS/Fedora (rpm)|[![Latest version of 'c8y-command-plugin' @ Cloudsmith](https://api-prd.cloudsmith.io/v1/badges/version/thinedge/community/rpm/c8y-command-plugin/latest/a=noarch;d=any-distro%252Fany-version;t=binary/?render=true&show_latest=true)](https://cloudsmith.io/~thinedge/repos/community/packages/detail/rpm/c8y-command-plugin/latest/a=noarch;d=any-distro%252Fany-version;t=binary/)| + +#### Configuration + +The Cumulocity IoT shell plugin can be configured with the following properties. + +|Property|Value|Description| +|--|--|--| +|`SHELL_BIN`|`string`|Default shell to be used to execute the received command. If left blank, then the shell will be auto-detected. If a non-empty value is used. If the shell does not exist, then an error will be raised. The shell will be used using ` -c ""`.| +|`SHELL_OPTIONS`|Whitespace separated list|List of shells to check if they exist. The plugin will use the first detected shell| + +The configuration is managed from the following file, and an example of the contents are shown below. + +**File** + +```sh +/etc/c8y-command-plugin/env +``` + +**Contents** + +```sh +SHELL_BIN="" +SHELL_OPTIONS="bash zsh ash dash sh /my/custom/shell/interpreter" +``` diff --git a/ci/build.sh b/ci/build.sh new file mode 100755 index 0000000..78a02ba --- /dev/null +++ b/ci/build.sh @@ -0,0 +1,33 @@ +#!/bin/bash +# ------------------------------------------- +# Build linux packages +# ------------------------------------------- +set -e + +# clean dist +if [ -d dist ]; then + rm -rf dist +fi + +mkdir -p dist + +if [ $# -gt 0 ]; then + export SEMVER="$1" +fi + +if [ -n "$SEMVER" ]; then + echo "Using version: $SEMVER" +fi + +packages=( + deb + apk + rpm +) + +for package_type in "${packages[@]}"; do + echo "" + nfpm package --packager "$package_type" --target ./dist/ +done + +echo "Created all linux packages" diff --git a/ci/publish.sh b/ci/publish.sh new file mode 100755 index 0000000..8b0dfd6 --- /dev/null +++ b/ci/publish.sh @@ -0,0 +1,130 @@ +#!/bin/bash +# ----------------------------------------------- +# Publish package to Cloudsmith.io +# ----------------------------------------------- +help() { + cat < Debian access token used to authenticate the commands + --owner Debian repository owner + --repo Name of the debian repository to publish to + --help|-h Show this help + +Optional Environment variables (instead of flags) + +PUBLISH_TOKEN Equivalent to --token flag +PUBLISH_OWNER Equivalent to --owner flag +PUBLISH_REPO Equivalent to --repo flag + +Examples: + $0 \\ + --token "mywonderfultoken" \\ + --repo "community" \\ + --path ./dist + + \$ Publish all debian/alpine/rpm packages found under ./dist +EOF +} + +PUBLISH_TOKEN="${PUBLISH_TOKEN:-}" +PUBLISH_OWNER="${PUBLISH_OWNER:-thinedge}" +PUBLISH_REPO="${PUBLISH_REPO:-community}" +SOURCE_PATH="./" + +# +# Argument parsing +# +POSITIONAL=() +while [[ $# -gt 0 ]] +do + case "$1" in + # Repository owner + --owner) + PUBLISH_OWNER="$2" + shift + ;; + + # Token used to authenticate publishing commands + --token) + PUBLISH_TOKEN="$2" + shift + ;; + + # Where to look for the debian files to publish + --path) + SOURCE_PATH="$2" + shift + ;; + + # Which debian repo to publish to (under the given host url) + --repo) + PUBLISH_REPO="$2" + shift + ;; + + --help|-h) + help + exit 0 + ;; + + -*) + echo "Unrecognized flag" >&2 + help + exit 1 + ;; + + *) + POSITIONAL+=("$1") + ;; + esac + shift +done +set -- "${POSITIONAL[@]}" + +# Add local tools path +LOCAL_TOOLS_PATH="$HOME/.local/bin" +export PATH="$LOCAL_TOOLS_PATH:$PATH" + +# Install tooling if missing +if ! [ -x "$(command -v cloudsmith)" ]; then + echo 'Install cloudsmith cli' >&2 + if command -v pip3 &>/dev/null; then + pip3 install --upgrade cloudsmith-cli + elif command -v pip &>/dev/null; then + pip install --upgrade cloudsmith-cli + else + echo "Could not install cloudsmith cli. Reason: pip3/pip is not installed" + exit 2 + fi +fi + + +publish() { + local sourcedir="$1" + local pattern="$2" + local package_type="$3" + local distribution="$4" + local distribution_version="$5" + + # Notes: Currently Cloudsmith does not support the following (this might change in the future) + # * distribution and distribution_version must be selected from values in the list. use `cloudsmith list distros` to get the list + # * The component can not be set and is currently fixed to 'main' + find "$sourcedir" -name "$pattern" -print0 | while read -r -d $'\0' file + do + cloudsmith upload "$package_type" "${PUBLISH_OWNER}/${PUBLISH_REPO}/${distribution}/${distribution_version}" "$file" \ + --no-wait-for-sync \ + --api-key "${PUBLISH_TOKEN}" + done +} + + +publish "$SOURCE_PATH" "*.deb" deb "any-distro" "any-version" +publish "$SOURCE_PATH" "*.rpm" rpm "any-distro" "any-version" +publish "$SOURCE_PATH" "*.apk" alpine "alpine" "any-version" diff --git a/nfpm.yaml b/nfpm.yaml new file mode 100644 index 0000000..3a0ff3c --- /dev/null +++ b/nfpm.yaml @@ -0,0 +1,36 @@ +name: c8y-command-plugin +arch: all +platform: linux +version: ${SEMVER} +section: misc +priority: optional +maintainer: Reuben Miller +description: thin-edge.io Cumulocity IoT Shell/Command operation plugin +vendor: thin-edge.io +homepage: https://github.com/reubenmiller/c8y-command-plugin +license: MIT +apk: + # Use noarch instead of "all" + arch: noarch +contents: + - src: ./src/c8y_Command + dst: /etc/tedge/operations/c8y/ + file_info: + mode: 0644 + owner: tedge + group: tedge + + - src: ./src/c8y-command + dst: /usr/bin/c8y-command + file_info: + mode: 0755 + owner: tedge + group: tedge + + - src: ./src/env + dst: /etc/c8y-command-plugin/env + type: config|noreplace + file_info: + mode: 0644 + owner: tedge + group: tedge diff --git a/src/c8y-command b/src/c8y-command new file mode 100755 index 0000000..1e07008 --- /dev/null +++ b/src/c8y-command @@ -0,0 +1,64 @@ +#!/bin/sh +set -e + +info() { + echo "$(date --iso-8601=seconds 2>/dev/null || date -Iseconds) INFO $*" >&2 +} + +info "Received message: $*" + +# Parse the smart rest message, ignore the first two field, and everything afterwards is the command +COMMAND="${1#*,*,}" + +# Check if command is wrapped with quotes, if so then remove them +# Use a case statement, as it is posix compatiable +case "$COMMAND" in + '"'*'"') + # Remove the first char + COMMAND="${COMMAND#?}" + # Remove the last char + COMMAND="${COMMAND%?}" + ;; +esac + +# Default values (can be overriden by the settings file) +SHELL_OPTIONS="bash sh" +SHELL_BIN= + +# Load settings file +SETTINGS_FILE=/etc/c8y-command-plugin/env +if [ -f "$SETTINGS_FILE" ]; then + FOUND_FILE=$(find "$SETTINGS_FILE" -perm 644 | head -1) + + if [ -n "$FOUND_FILE" ]; then + info "Loading settings: $FOUND_FILE" + # shellcheck disable=SC1090 + . "$FOUND_FILE" ||: + fi +fi + +# Auto detect the shell. Match on the first available shell +# If the shell bin is invalid, then just let it fail (this might be useful to disable the shell function on the device) +if [ -z "$SHELL_BIN" ]; then + for NAME in $SHELL_OPTIONS; do + if command -V "$NAME" >/dev/null 2>&1; then + SHELL_BIN="$NAME" + break + fi + done +fi + +if [ -z "$SHELL_BIN" ]; then + SHELL_BIN="sh" +fi + +info "Using shell: $SHELL_BIN" + +EXIT_CODE=0 +"$SHELL_BIN" -c "$COMMAND" || EXIT_CODE=$? + +if [ "${EXIT_CODE}" -ne 0 ]; then + info "Command returned a non-zero exit code. code=$EXIT_CODE" +fi + +exit "$EXIT_CODE" diff --git a/src/c8y_Command b/src/c8y_Command new file mode 100644 index 0000000..372b05e --- /dev/null +++ b/src/c8y_Command @@ -0,0 +1,4 @@ +[exec] +topic = "c8y/s/ds" +on_message = "511" +command = "/usr/bin/c8y-command" \ No newline at end of file diff --git a/src/env b/src/env new file mode 100644 index 0000000..3a440c1 --- /dev/null +++ b/src/env @@ -0,0 +1,5 @@ +# Which shell to use. Blank means that the first shell option found will be used +#SHELL_BIN="" + +# List of shells to check. Use the first available shell. This is only used if SHELL_BIN is not defined +#SHELL_OPTIONS="bash sh"