diff --git a/.containerignore b/.dockerignore similarity index 100% rename from .containerignore rename to .dockerignore diff --git a/.github/workflows/build_push_release.yml b/.github/workflows/build_push_release.yml index 412f426..25fd967 100644 --- a/.github/workflows/build_push_release.yml +++ b/.github/workflows/build_push_release.yml @@ -21,7 +21,6 @@ jobs: echo "RELEASE_VERSION=${VERSION}" >> $GITHUB_ENV - name: Create GitHub Release - id: create_release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | @@ -30,13 +29,21 @@ jobs: else echo "Release $RELEASE_TAG does not exists. creating..." RELEASE_NOTES=$(sed -e "/^## ${RELEASE_TAG}/,/^## / ! d" CHANGELOG.md | tail -n +2 | head -n -1) - gh release create $RELEASE_TAG \ - --generate-notes \ - --title $RELEASE_TAG \ - --notes "$RELEASE_NOTES" + if [[ ${{ contains(github.ref_name, '-rc') }} == 'true' || ${{ contains(github.ref_name, '-beta') }} == 'true' || ${{ contains(github.ref_name, '-alpha') }} == 'true' ]]; then + gh release create $RELEASE_TAG \ + --generate-notes \ + --title $RELEASE_TAG \ + --notes "$RELEASE_NOTES" \ + --prerelease + else + gh release create $RELEASE_TAG \ + --generate-notes \ + --title $RELEASE_TAG \ + --notes "$RELEASE_NOTES" + fi fi - create_release_artifacts: + create_static: permissions: contents: write runs-on: "${{ matrix.os }}" @@ -63,14 +70,39 @@ jobs: - name: Build release artifacts run: | - docker build --output type=local,dest=docker_output/ -f release/Containerfile-build.${{ matrix.target }} . + docker build --platform linux/${{ matrix.target }} --output type=local,dest=docker_output/ -f release/Containerfile-build-static . - name: Upload Release Asset env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - mv ./docker_output/${{ matrix.target }}/peridiod-${RELEASE_VERSION}.tar.gz ./docker_output/peridiod-${RELEASE_VERSION}-${{ matrix.target }}.tar.gz - gh release upload $RELEASE_TAG ./docker_output/peridiod-${RELEASE_VERSION}-${{ matrix.target }}.tar.gz --clobber + mv ./docker_output/peridiod-${RELEASE_VERSION}.tar.gz ./docker_output/peridiod-${RELEASE_VERSION}-${{ matrix.target }}-static.tar.gz + gh release upload $RELEASE_TAG ./docker_output/peridiod-${RELEASE_VERSION}-${{ matrix.target }}-static.tar.gz --clobber + + create_container_images: + permissions: + contents: write + runs-on: "${{ matrix.os }}" + needs: create_release + + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest-4-cores-arm64 + target: arm64 + - os: ubuntu-latest + target: amd64 + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set Env + run: | + VERSION=$(echo "${GITHUB_REF#refs/*/}" | sed 's/^v//') + echo "RELEASE_TAG=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV + echo "RELEASE_VERSION=${VERSION}" >> $GITHUB_ENV - name: Load docker hub credentials id: op-load-docker-hub-credentials @@ -97,9 +129,9 @@ jobs: tags: | peridio/peridiod:${{ env.RELEASE_TAG }}-${{ matrix.target }} - push_container_images: + create_container_manifest: runs-on: ubuntu-latest - needs: create_release_artifacts + needs: create_container_images steps: - name: Checkout repository @@ -130,8 +162,157 @@ jobs: docker manifest create peridio/peridiod:${{ env.RELEASE_TAG }} \ --amend peridio/peridiod:${{ env.RELEASE_TAG }}-arm64 \ --amend peridio/peridiod:${{ env.RELEASE_TAG }}-amd64 - docker manifest create peridio/peridiod:latest \ - --amend peridio/peridiod:${{ env.RELEASE_TAG }}-arm64 \ - --amend peridio/peridiod:${{ env.RELEASE_TAG }}-amd64 docker manifest push peridio/peridiod:${{ env.RELEASE_TAG }} - docker manifest push peridio/peridiod:latest + + if [[ "${{ env.RELEASE_TAG }}" != *-* ]]; then + docker manifest create peridio/peridiod:latest \ + --amend peridio/peridiod:${{ env.RELEASE_TAG }}-arm64 \ + --amend peridio/peridiod:${{ env.RELEASE_TAG }}-amd64 + docker manifest push peridio/peridiod:latest + else + echo "Skipping latest tag update for pre-release tag ${{ env.RELEASE_TAG }}" + fi + + create_debs: + runs-on: "${{ matrix.os }}" + needs: create_release + + permissions: + contents: write + + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest-4-cores-arm64 + target: arm64 + distro: jammy + - os: ubuntu-latest-4-cores-arm64 + target: arm64 + distro: noble + - os: ubuntu-latest + target: amd64 + distro: jammy + - os: ubuntu-latest + target: amd64 + distro: noble + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set Env + run: | + echo "RELEASE_TAG=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV + + - name: Cache Debian packages + uses: actions/cache@v3 + with: + path: ~/.cache/deb + key: ${{ runner.os }}-deb-cache + restore-keys: | + ${{ runner.os }}-deb-cache + + - name: Install build dependencies + run: | + sudo apt-get update + sudo apt-get install -y dpkg-dev debhelper fakeroot + + - name: Build release artifacts + run: | + docker build --platform linux/${{ matrix.target }} --output type=local,dest=docker_output/ -f release/Containerfile-build-${{ matrix.distro }} . + + - name: Build DEB package + run: | + source release/package-info.sh + PERIDIOD_ARCH=${{ matrix.target }} + PERIDIOD_VERSION=$(echo "${GITHUB_REF#refs/*/}" | sed 's/^v//') + PERIDIOD_PACKAGE_DIR=$(pwd)/package/peridiod_${PERIDIOD_VERSION}_${PERIDIOD_ARCH} + PERIDIOD_RELEASE_NOTES=$(sed -e "/^## ${RELEASE_TAG}/,/^## / ! d" CHANGELOG.md | tail -n +2 | head -n -1) + + export PERIDIOD_RELEASE_NOTES + export PERIDIOD_VERSION + export PERIDIOD_ARCH + + release/build-deb.sh ./docker_output/peridiod-${PERIDIOD_VERSION}.tar.gz $PERIDIOD_PACKAGE_DIR + mv "${PERIDIOD_PACKAGE_DIR}.deb" ./peridiod_${PERIDIOD_VERSION}_${PERIDIOD_ARCH}.${{ matrix.distro }}.deb + echo "PERIDIOD_PACKAGE=peridiod_${PERIDIOD_VERSION}_${PERIDIOD_ARCH}.${{ matrix.distro }}.deb" >> $GITHUB_ENV + + - name: Upload Release Asset + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release upload $RELEASE_TAG $PERIDIOD_PACKAGE --clobber + + create_rpms: + runs-on: "${{ matrix.os }}" + needs: create_release + + permissions: + contents: write + + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest-4-cores-arm64 + target: arm64 + distro: rhel9 + - os: ubuntu-latest + target: amd64 + distro: rhel9 + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set Env + run: | + echo "RELEASE_TAG=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV + + - name: Install build dependencies + run: | + sudo apt-get update + sudo apt-get install -y dpkg-dev debhelper fakeroot + + - name: Install build dependencies + run: | + sudo apt-get update + sudo apt-get install -y dpkg-dev debhelper fakeroot + + - name: Build release artifacts + run: | + docker build --platform linux/${{ matrix.target }} --output type=local,dest=docker_output/ -f release/Containerfile-build-${{ matrix.distro }} . + + - name: Build RPM package + run: | + source release/package-info.sh + PERIDIOD_ARCH=${{ matrix.target }} + if [ "$PERIDIOD_ARCH" == "arm64" ]; then + PERIDIOD_ARCH_RPM="aarch64" + elif [ "$PERIDIOD_ARCH" == "amd64" ]; then + PERIDIOD_ARCH_RPM="x86_64" + else + PERIDIOD_ARCH_RPM="$ARCH" + fi + PERIDIOD_VERSION=$(echo "${GITHUB_REF#refs/*/}" | sed 's/^v//') + PERIDIOD_VERSION_RPM=${PERIDIOD_VERSION//-/_} + PERIDIOD_PACKAGE_DIR=$(pwd)/package/peridiod_${PERIDIOD_VERSION}_${PERIDIOD_ARCH} + PERIDIOD_RELEASE_NOTES=$(sed -e "/^## ${RELEASE_TAG}/,/^## / ! d" CHANGELOG.md | tail -n +2 | head -n -1) + + export PERIDIOD_RELEASE_NOTES + export PERIDIOD_VERSION + export PERIDIOD_VERSION_RPM + export PERIDIOD_ARCH + export PERIDIOD_ARCH_RPM + + release/build-rpm.sh ./docker_output/peridiod-${PERIDIOD_VERSION}.tar.gz $PERIDIOD_PACKAGE_DIR + + mv "${PERIDIOD_PACKAGE_DIR}/RPMS/$PERIDIOD_ARCH_RPM/peridiod-${PERIDIOD_VERSION_RPM}-1.${PERIDIOD_ARCH_RPM}.rpm" peridiod-${PERIDIOD_VERSION_RPM}-1.${PERIDIOD_ARCH_RPM}.${{ matrix.distro }}.rpm + echo "PERIDIOD_PACKAGE=peridiod-${PERIDIOD_VERSION_RPM}-1.${PERIDIOD_ARCH_RPM}.${{ matrix.distro }}.rpm" >> $GITHUB_ENV + + - name: Upload Release Asset + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release upload $RELEASE_TAG $PERIDIOD_PACKAGE --clobber diff --git a/.github/workflows/build_test.yml b/.github/workflows/build_test.yml index d3370fd..3b3adc9 100644 --- a/.github/workflows/build_test.yml +++ b/.github/workflows/build_test.yml @@ -21,8 +21,8 @@ jobs: - name: Set up Elixir uses: erlef/setup-beam@v1 with: - elixir-version: '1.16.2' - otp-version: '26.2.5' + elixir-version: '1.17.2' + otp-version: '27.0.1' - name: Restore dependencies cache uses: actions/cache@v4 with: diff --git a/CHANGELOG.md b/CHANGELOG.md index 742ea0f..d9a1ef8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,92 @@ # peridiod releases +## v3.0.0-rc.0 + +**This is a major update and this release should be thoroughly tested.** + +Add support for Peridio Cloud Releases + +Peridio Releases allow you greater flexibility in how you manage the content installed on your device. + +### Config + +New `peridiod` config keys introduced: + +* `release_poll_enabled`: true | false +* `release_poll_interval`: the interval in ms to automatically check for updates +* `cache_dir`: a writable path where `peridiod` can store release metadata +* `targets`: A list of string target names for peridiod to install as part of a release update +* `trusted_signing_keys`: A list of base64 encoded ed25519 public signing key strings + +### Installers + +Peridiod now has a concept of "Installers", initially supported installer types are `file` and `fwup`. When using releases, you will have to use the `custom_metadata` of a binary, artifact version, or artifact to instruct peridiod how to install the binary content. Here is an example of what custom metadata for installers would look like: + +fwup + +```json +{ + "installer": "fwup", + "installer_opts": { + "devpath": "/dev/mmcblk0", + "extra_args": [], + "env": {} + }, + "reboot_required": true +} +``` + +file + +```json +{ + "installer": "file", + "installer_opts": { + "name": "my_file.txt", + "path": "/opt/my_app", + }, + "reboot_required": false +} +``` + +The custom metadata will need to configured on a Binary, Artifact Version, or Artifact record. You can add this custom metadata to these records using Peridio CLI v0.22.0 or later. + +### U-Boot Environment additions + +peridiod releases will track and expose release metadata in the uboot environment under the following new keys + +* `peridiod_rel_current`: the PRN of the current installed release +* `peridiod_rel_previous`: the PRN of the previous installed release +* `peridiod_rel_progress`: the PRN of the release in progress +* `peridiod_vsn_current`: the semantic version of the current installed release +* `peridiod_vsn_previous`: the semantic version of the previous installed release +* `peridiod_vsn_progress`: the semantic version of the release in progress +* `peridiod_bin_current`: an concatenated key / value paired encoded string of `` internally used to diff installed binaries from release to release + +### Preparing a release + +Peridiod will track installed binaries from release to release by updating the `peridio_bin_current` value in the u-boot-env. When burning in a device firmware for the first time, you can pre-compute this field value with information about the supplied binaries by constructing a concatenated string according to the field specifications. This will prevent peridiod from installing binaries unnecessarily on first boot. + +### Release Install + +The release server will check for an update from Peridio Cloud a the designated interval. When an update is available, the release server will immediately cache the release metadata to the cache_dir and begin processing the release. Currently, the release server is configured to install an update once it is available. This behavior will change before public release and instead be routed through the update client module. The release server will apply an update in the following order: + +* Validate artifact signatures' public key values have been signed by a public key in `trusted_signing_keys` +* Filter the Binaries by uninstalled with a target listed in the `targets` list +* Install Binaries + * Initialize a Download with an Installer + * Begin Download (Download Started Event) + * Download chunks (Download Progress Events) + * Finish Download (Download Finished Event) + * Validate hash (during stream) + * Installer applied (Binary Applied) + * Update Binary status to complete +* Update Release status to complete + +When peridiod installs a release, it will accumulate `reboot_required` and trigger a reboot once all binaries have finished the installation process if any `reboot_required` is true. + +See the [Peridio Docs](https://docs.peridio.com/) for more information on configuring Releases for your organization. + ## v2.5.4 * Enhancement diff --git a/README.md b/README.md index 097ca67..817ffa6 100644 --- a/README.md +++ b/README.md @@ -1,171 +1,13 @@ # Peridio Daemon -## Configuring +`peridiod` is a reference implementation of a Peridio Agent for Embedded Linux. -Peridiod is configured via a json formatted file on the filesystem. The location of the file defaults to `$XDG_CONFIG_HOME/peridio/peridio-config.json`. if `$XDG_CONFIG_HOME` is not set the default path is `$HOME/.config/peridio/peridio-config.json`. This file location can be overwritten by setting `PERIDIO_CONFIG_FILE=/path/to/peridio.json`. The peridiod configuration has the following top level keys: +Peridio offers several ways to integrate peridiod into your build workflow via the following integration paths: -* `version`: The configuration version number. Currently this is 1. -* `device_api`: Configuration for the device api endpoint - * `certificate_path`: Path to the device api ca certificate. - * `url`: The peridio server device api URL. - * `verify`: Enable client side ssl verification for device api connections. -* `fwup`: Keys related to the use of fwup for the last mile. - * `devpath`: The block storage device path to use for applying firmware updates. - * `public_keys`: A list of authorized public keys used when verifying update archives. - * `extra_args`: A list of extra arguments to pass to the fwup command used for applying fwup archives. Helpful when needing to use the --unsafe flag in fwup. - * `env`: A json object of `"ENV_VAR": "value"` pairs to decorate the environment which fwup is executed from. -* `remote_shell`: Enable or disable the remote getty feature. -* `remote_iex`: Enable or disable the remote IEx feature. Useful if you are deploying a Nerves distribution. Enabling this takes precedence over `remote_shell` -* `node`: Node configuration settings - * `key_pair_source`: Options are `file`, `uboot-env`, `pkcs11`. This determines the source of the identity key information. - * `key_pair_config`: Different depending on the `key_pair_source` +* Build system integration with [Yocto](build-tools/yocto) or [Buildroot](build-tools/buildroot) +* Leverage one of our pre compiled artifacts from the [Github releases page](https://github.com/peridio/peridiod/releases) +* Running in a container leveraging one of our [official container images](https://hub.docker.com/r/peridio/peridiod). +* Cross-Compiling as part of your custom build tools. +* As part of an existing [Elixir based application](https://github.com/peridio/peridio-nerves-example). - `key_pair_source: file`: - * `private_key_path`: Path on the filesystem to a PEM encoded private key file. - * `certificate_path`: Path on the filesystem to a PEM encoded x509 certificate file. - - `key_pair_source: uboot-env`: - * `private_key`: The key in the uboot environment which contains a PEM encoded private key. - * `certificate`: The key in the uboot environment which contains a PEM encoded x509 certificate. - - `key_pair_source: pkcs11`: - * `key_id`: The `PKCS11` URI used to for private key operations. - Examples: - ATECCx08 TNG using CryptoAuthLib: `pkcs11:token=MCHP;object=device;type=private` - * `cert_id`: The `PKCS11` URI used for certificate operations. - Examples: - ATECCx08 TNG using CryptoAuthLib: `pkcs11:token=MCHP;object=device;type=cert` - -More information about certificate auth can be found in the [Peridio Documentation](docs.peridio.com) - -### Example Configurations - -#### Common - -```json -{ - "version": 1, - "device_api": { - "certificate_path": "/etc/peridiod/peridio-cert.pem", - "url": "device.cremini.peridio.com", - "verify": true - }, - "fwup": { - "devpath": "/dev/mmcblk1", - "public_keys": ["I93H7n/jHkfNqWik9uZf82Vi/HJuZ24EQBJnAtj9svU="] - }, - "remote_shell": true, - "node": { - // ... see Node Configuration - } -} -``` - -#### Node Configurations - -Filesystem - -```json -"key_pair_source": "file", -"key_pair_config": { - "private_key_path": "/etc/peridiod/device-key.pem", - "certificate_path": "/etc/peridiod/device.pem" -} -``` - -U-Boot Environment - -```json -"key_pair_source": "uboot-env", -"key_pair_config": { - "private_key": "peridio_identity_private_key", - "certificate": "peridio_identity_certificate" -} -``` - -System Environment - -```json -"key_pair_source": "env", -"key_pair_config": { - "private_key": "PERIDIO_PRIVATE_KEY", - "certificate": "PERIDIO_CERTIFICATE" -} -``` - -PKCS11 Identity using ATECC608B TrustAndGo - -```json -"key_pair_source": "pkcs11", -"key_pair_config": { - "key_id": "pkcs11:token=MCHP;object=device;type=private", - "cert_id": "pkcs11:token=MCHP;object=device;type=cert" -} -``` - -### Configuring with Elixir - -The peridiod application can be set using config.exs in a Nerves based application. The following is an example of the keys that can be set: - -```elixir -config :peridiod, - device_api_host: "device.cremini.peridio.com", - device_api_port: 443, - device_api_sni: "device.cremini.peridio.com", - device_api_verify: :verify_peer, - device_api_ca_certificate_path: nil, - key_pair_source: "env", - key_pair_config: %{"private_key" => "PERIDIO_PRIVATE_KEY", "certificate" => "PERIDIO_CERTIFICATE"}, - fwup_public_keys: [], - fwup_devpath: "/dev/mmcblk0", - fwup_env: [], - fwup_extra_args: [], - remote_shell: false, - remote_iex: true, -``` - -## Running with a container orchestrator - -You can debug using {podman | docker} by generating an SSL certificate and private key pair that is trusted by Peridio Cloud and pass it into the container. - -Building the container: - -```bash -podman build --tag peridio/peridiod --build-arg PERIDIO_META_ARCHITECTURE=$(uname -m) --build-arg PERIDIO_META_VERSION=$(cat VERSION | tr -d '\n') . -``` - -Running the container: - -You can pass the device certificate and private key directly: - -```bash -podman run -it --rm --env PERIDIO_CERTIFICATE="$(base64 -w 0 device-certificate.pem)" --env PERIDIO_PRIVATE_KEY="$(base64 -w 0 device-private-key.pem)" --cap-add=NET_ADMIN peridio/peridiod:latest -``` - -You can pass a signing certificate and private key to generate a device identity and JITP: - -```bash -podman run -it --rm --env PERIDIO_SIGNING_CERTIFICATE="$(base64 -w 0 signing-certificate.pem)" --env PERIDIO_SIGNING_PRIVATE_KEY="$(base64 -w 0 signing-private-key.pem)" --cap-add=NET_ADMIN peridio/peridiod:latest -``` - -The `--cap-add=NET_ADMIN` is required for testing remote access tunnels. This is required because peridiod will create new wireguard network interfaces and needs to execute commands with iptables. If this flag is omitted, the feature will not function properly. - -The container will be built using the `peridio.json` configuration file in the support directory. For testing you can modify this as you please. It is configured by default to allow testing for the remote shell and even firmware updates using deployments. You can create firmware to test for deployments using the following: - -```bash -PERIDIO_META_PRODUCT=peridiod \ -PERIDIO_META_DESCRIPTION=peridiod \ -PERIDIO_META_VERSION=1.0.1 \ -PERIDIO_META_PLATFORM=container \ -PERIDIO_META_ARCHITECTURE=$(uname -m) \ -PERIDIO_META_AUTHOR=peridio \ -fwup -c -f support/fwup.conf -o support/peridiod.fw -``` - -Then sign the firmwaare using a key pair that is trusted by Peridio Cloud - -```bash -fwup --sign -i support/peridiod.fw -o support/peridiod-signed.fw --public-key "$(cat ./path/to/fwup-key.pub)" --private-key "$(cat /path/to/fwup-key.priv)" -``` - -You can then upload `support/peridiod-signed.fw` to Peridio Cloud and configure a deployment. The container will not actually apply anything persistent, but it will simulate downloading and applying an update. +See the [Peridio Daemon Docs](https://docs.peridio.com/integration/linux/peridiod) for more information diff --git a/VERSION b/VERSION index fe16b34..3c6eac3 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.5.4 +3.0.0-rc.0 diff --git a/config/config.exs b/config/config.exs index 7d25dca..a225add 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,13 +1,7 @@ import Config config :peridiod, - client: Peridiod.Client.Default, - config_mod: Peridiod.Config, - kv_backend: - {Peridiod.KVBackend.InMemory, - contents: %{ - "peridio_disk_devpath" => "/dev/mmcblk1" - }} + client: Peridiod.Client.Default config :peridio_rat, wireguard_client: Peridio.RAT.WireGuard.Default diff --git a/config/dev.exs b/config/dev.exs index 61e7489..5cd004b 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -2,14 +2,16 @@ import Config config :logger, level: :debug -config :peridiod, +config :peridiod_persistence, kv_backend: - {Peridiod.KVBackend.InMemory, + {PeridiodPersistence.KVBackend.InMemory, contents: %{ "peridio_disk_devpath" => "/dev/mmcblk1", "peridio_vsn_current" => System.get_env("PERIDIO_RELEASE_VERSION"), "peridio_rel_current" => System.get_env("PERIDIO_RELEASE_PRN") - }}, + }} + +config :peridiod, key_pair_source: "env", key_pair_config: %{ "private_key" => "PERIDIO_PRIVATE_KEY", diff --git a/config/prod.exs b/config/prod.exs index e3325c1..26f343c 100644 --- a/config/prod.exs +++ b/config/prod.exs @@ -2,9 +2,6 @@ import Config config :logger, level: :warning -config :peridiod, - kv_backend: Peridiod.KVBackend.UBootEnv - config :erlexec, user: "root", limit_users: ["root"], diff --git a/config/runtime.exs b/config/runtime.exs index bf8fd4c..1416ba7 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -1,6 +1,33 @@ import Config shell = - System.find_executable("bash") || System.find_executable("zsh") || System.find_execurtable("sh") + System.find_executable("bash") || System.find_executable("zsh") || System.find_executable("sh") System.put_env("SHELL", shell) + +log_level = + case System.get_env("PERIDIO_LOG_LEVEL") do + "debug" -> :debug + "warning" -> :warning + "info" -> :info + _ -> :error + end + +kv_backend = + case System.get_env("PERIDIO_KV_BACKEND") do + "filesystem" -> + path = System.get_env("PERIDIO_KV_BACKEND_FILESYSTEM_PATH", "/var/peridiod") + file = System.get_env("PERIDIO_KV_BACKEND_FILESYSTEM_FILE", "peridiod-state") + {PeridiodPersistence.KVBackend.Filesystem, path: path, file: file} + + "ubootenv" -> + PeridiodPersistence.KVBackend.UBootEnv + + _ -> + PeridiodPersistence.KVBackend.UBootEnv + end + +config :peridiod_persistence, + kv_backend: kv_backend + +config :logger, level: log_level diff --git a/config/test.exs b/config/test.exs index 55e4d4e..ae51cec 100644 --- a/config/test.exs +++ b/config/test.exs @@ -4,8 +4,11 @@ System.put_env("PERIDIO_CONFIG_FILE", "test/fixtures/peridio.json") config :logger, level: :error -config :peridiod, +config :peridiod_persistence, kv_backend: - {Peridiod.KVBackend.InMemory, contents: %{"peridio_disk_devpath" => "/dev/mmcblk1"}}, + {PeridiodPersistence.KVBackend.InMemory, + contents: %{"peridio_disk_devpath" => "/dev/mmcblk1"}} + +config :peridiod, cache_dir: Path.expand("../test/workspace/cache", __DIR__), socket_enabled?: false diff --git a/lib/peridiod/application.ex b/lib/peridiod/application.ex index ac4f6ed..273012b 100644 --- a/lib/peridiod/application.ex +++ b/lib/peridiod/application.ex @@ -2,7 +2,6 @@ defmodule Peridiod.Application do use Application alias Peridiod.{ - KV, Cache, Connection, Socket, @@ -16,7 +15,6 @@ defmodule Peridiod.Application do config = struct(Peridiod.Config, application_config) |> Peridiod.Config.new() children = [ - {KV, application_config}, {Cache, config}, Binary.Installer.Supervisor, Binary.Downloader.Supervisor, diff --git a/lib/peridiod/binary.ex b/lib/peridiod/binary.ex index 9ae79c5..d67fe23 100644 --- a/lib/peridiod/binary.ex +++ b/lib/peridiod/binary.ex @@ -1,5 +1,6 @@ defmodule Peridiod.Binary do alias Peridiod.{Binary, Cache, Signature} + alias PeridiodPersistence.KV import Peridiod.Utils, only: [stamp_utc_now: 0] @cache_dir "binary" @@ -215,13 +216,13 @@ defmodule Peridiod.Binary do end) end - def get_all_kv_installed(kv_pid \\ Peridiod.KV, store) when store in @kv_bin_stores do - Peridiod.KV.get(kv_pid, @kv_bin_installed <> to_string(store)) + def get_all_kv_installed(kv_pid \\ KV, store) when store in @kv_bin_stores do + KV.get(kv_pid, @kv_bin_installed <> to_string(store)) |> parse_kv_installed() end def put_kv_installed( - kv_pid \\ Peridiod.KV, + kv_pid \\ KV, %Binary{ prn: prn, custom_metadata_hash: custom_metadata_hash @@ -235,7 +236,7 @@ defmodule Peridiod.Binary do |> id_to_bin() |> Base.encode16(case: :lower) - Peridiod.KV.get_and_update(kv_pid, @kv_bin_installed <> to_string(store), fn installed -> + KV.get_and_update(kv_pid, @kv_bin_installed <> to_string(store), fn installed -> installed |> parse_kv_installed() |> Map.put(id, Base.encode16(custom_metadata_hash, case: :lower)) @@ -243,7 +244,7 @@ defmodule Peridiod.Binary do end) end - def pop_kv_installed(kv_pid \\ Peridiod.KV, %Binary{prn: prn}, store) + def pop_kv_installed(kv_pid \\ KV, %Binary{prn: prn}, store) when store in @kv_bin_stores do id = prn @@ -251,7 +252,7 @@ defmodule Peridiod.Binary do |> id_to_bin() |> Base.encode16(case: :lower) - Peridiod.KV.get_and_update(kv_pid, @kv_bin_installed <> to_string(store), fn installed -> + KV.get_and_update(kv_pid, @kv_bin_installed <> to_string(store), fn installed -> installed |> parse_kv_installed() |> Map.delete(id) diff --git a/lib/peridiod/binary/cache_downloader.ex b/lib/peridiod/binary/cache_downloader.ex index e5c9358..d895e50 100644 --- a/lib/peridiod/binary/cache_downloader.ex +++ b/lib/peridiod/binary/cache_downloader.ex @@ -87,7 +87,13 @@ defmodule Peridiod.Binary.CacheDownloader do signature = List.first(state.binary_metadata.signatures) with true <- state.binary_metadata.hash == hash, - :ok <- Cache.write_stream_finish(state.cache_pid, file, signature) do + :ok <- + Cache.write_stream_finish( + state.cache_pid, + file, + signature.signature, + signature.signing_key.public_der + ) do Binary.stamp_cached(state.cache_pid, state.binary_metadata) send(state.callback, {:download_cache, state.binary_metadata, :complete}) else diff --git a/lib/peridiod/binary/installer.ex b/lib/peridiod/binary/installer.ex index d088212..90ca005 100644 --- a/lib/peridiod/binary/installer.ex +++ b/lib/peridiod/binary/installer.ex @@ -3,7 +3,8 @@ defmodule Peridiod.Binary.Installer do require Logger - alias Peridiod.{Binary, Cache, KV} + alias PeridiodPersistence.KV + alias Peridiod.{Binary, Cache} alias Peridiod.Binary.{Installer, Downloader} defmodule State do @@ -244,9 +245,16 @@ defmodule Peridiod.Binary.Installer do end defp installer_progress({:noreply, state}) do + download_percent = + case state.source do + :cache -> 1.0 + :download -> state.install_percent + end + try_send( state.callback, - {Installer, state.binary_metadata.prn, {:progress, state.install_percent}} + {Installer, state.binary_metadata.prn, + {:progress, {download_percent, state.install_percent}}} ) {:noreply, state} diff --git a/lib/peridiod/binary/installer/fwup.ex b/lib/peridiod/binary/installer/fwup.ex index 2c4feba..010d215 100644 --- a/lib/peridiod/binary/installer/fwup.ex +++ b/lib/peridiod/binary/installer/fwup.ex @@ -5,6 +5,7 @@ defmodule Peridiod.Binary.Installer.Fwup do use Peridiod.Binary.Installer.Behaviour + alias PeridiodPersistence.KV alias __MODULE__ def install_init( @@ -14,8 +15,8 @@ defmodule Peridiod.Binary.Installer.Fwup do config ) do devpath = - opts["devpath"] || config.fwup_devpath || Peridiod.KV.get("peridio_disk_devpath") || - Peridiod.KV.get("nerves_fw_devpath") + opts["devpath"] || config.fwup_devpath || KV.get("peridio_disk_devpath") || + KV.get("nerves_fw_devpath") env = opts["env"] || config.fwup_env extra_args = opts["extra_args"] || config.fwup_extra_args @@ -125,6 +126,10 @@ defmodule Peridiod.Binary.Installer.Fwup do defdelegate send_chunk(pid, chunk), to: Fwup.Stream + def installed?() do + is_binary(System.find_executable("fwup")) + end + def version do {version_string, 0} = System.cmd("fwup", ["--version"]) String.trim(version_string) diff --git a/lib/peridiod/cache.ex b/lib/peridiod/cache.ex index e84063b..40bba6c 100644 --- a/lib/peridiod/cache.ex +++ b/lib/peridiod/cache.ex @@ -1,8 +1,6 @@ defmodule Peridiod.Cache do use GenServer - alias Peridiod.{Binary, Signature} - @hash_algorithm :sha256 @stream_chunk_size 4096 @@ -34,8 +32,8 @@ defmodule Peridiod.Cache do GenServer.call(pid_or_name, {:write_stream_update, file, data}) end - def write_stream_finish(pid_or_name \\ __MODULE__, file, signature) do - GenServer.call(pid_or_name, {:write_stream_finish, file, signature}) + def write_stream_finish(pid_or_name \\ __MODULE__, file, signature, public_key) do + GenServer.call(pid_or_name, {:write_stream_finish, file, signature, public_key}) end def ln_s(pid_or_name \\ __MODULE__, target, link) do @@ -113,14 +111,13 @@ defmodule Peridiod.Cache do {:reply, reply, state} end - def handle_call({:write_stream_finish, file, %Signature{} = signature}, _from, state) do + def handle_call({:write_stream_finish, file, signature, public_key}, _from, state) do file_path = Path.join([state.path, file]) file_sig_path = file_path <> ".sig" reply = with hash <- hash(file_path, state.hash_algorithm), - true <- - Binary.valid_signature?(hash, signature.signature, signature.signing_key.public_der), + true <- :crypto.verify(:eddsa, :sha256, hash, signature, [public_key, :ed25519]), signature <- sign(hash, state.hash_algorithm, state.private_key), :ok <- File.write(file_sig_path, signature) do :ok diff --git a/lib/peridiod/config.ex b/lib/peridiod/config.ex index eb66947..91044d1 100644 --- a/lib/peridiod/config.ex +++ b/lib/peridiod/config.ex @@ -1,12 +1,13 @@ defmodule Peridiod.Config do use Peridiod.Log - alias Peridiod.{Backoff, Cache, KV, SigningKey} + alias PeridiodPersistence.KV + alias Peridiod.{Backoff, Cache, SigningKey} alias __MODULE__ require Logger - defstruct cache_dir: "/tmp", + defstruct cache_dir: "/var/peridiod", cache_private_key: nil, cache_public_key: nil, cache_pid: Cache, diff --git a/lib/peridiod/config/uboot_env.ex b/lib/peridiod/config/uboot_env.ex index b9675d7..375cd78 100644 --- a/lib/peridiod/config/uboot_env.ex +++ b/lib/peridiod/config/uboot_env.ex @@ -1,11 +1,12 @@ defmodule Peridiod.Config.UBootEnv do require Logger + alias PeridiodPersistence.KV import Peridiod.Utils, only: [try_base64_decode: 1] def config(%{"private_key" => key, "certificate" => cert}, base_config) do - key_pem = Peridiod.KV.get(key) |> try_base64_decode() - cert_pem = Peridiod.KV.get(cert) |> try_base64_decode() + key_pem = KV.get(key) |> try_base64_decode() + cert_pem = KV.get(cert) |> try_base64_decode() cert = cert_from_pem(cert_pem) cert_der = X509.Certificate.to_der(cert) diff --git a/lib/peridiod/distribution/server.ex b/lib/peridiod/distribution/server.ex index 80a2583..acf17b4 100644 --- a/lib/peridiod/distribution/server.ex +++ b/lib/peridiod/distribution/server.ex @@ -9,10 +9,9 @@ defmodule Peridiod.Distribution.Server do """ use GenServer - alias Peridiod.Client - alias Peridiod.Binary.Installer.Fwup - alias Peridiod.Distribution - alias Peridiod.Binary.{Downloader, Downloader.Supervisor} + alias Peridiod.{Client, Distribution} + alias Peridiod.Binary.{Downloader, Downloader.Supervisor, Installer.Fwup} + alias PeridiodPersistence.KV require Logger @@ -107,8 +106,8 @@ defmodule Peridiod.Distribution.Server do @impl GenServer def init(config) do fwup_devpath = - config.fwup_devpath || Peridiod.KV.get("peridio_disk_devpath") || - Peridiod.KV.get("nerves_fw_devpath") + config.fwup_devpath || KV.get("peridio_disk_devpath") || + KV.get("nerves_fw_devpath") fwup_config = %Fwup.Config{ fwup_public_keys: config.fwup_public_keys, diff --git a/lib/peridiod/kv.ex b/lib/peridiod/kv.ex deleted file mode 100644 index 35c742a..0000000 --- a/lib/peridiod/kv.ex +++ /dev/null @@ -1,352 +0,0 @@ -defmodule Peridiod.KV do - @moduledoc """ - Key-Value storage for firmware variables - - KV provides functionality to read and modify firmware metadata. - The firmware metadata contains information such as the active firmware - slot, where the application data partition is located, etc. It may also contain - board provisioning information like factory calibration so long as it is not - too large. - - The metadata store is a simple key-value store where both keys and values are - ASCII strings. Writes are expected to be infrequent and primarily done - on firmware updates. If you expect frequent writes, it is highly recommended - to persist the data elsewhere. - - The default KV backend loads and stores metadata to a U-Boot-formatted environment - block. This doesn't mean that the device needs to run U-Boot. It just - happens to be a convenient data format that's well supported. - - There are some expectations on keys. See the Peridiod README.md for - naming conventions and expected key names. These are not enforced - - To change the KV backend, implement the `Peridiod.KVBackend` behaviour and - configure the application environment in your - program's `config.exs` like the following: - - ```elixir - config :peridiod, kv_backend: {MyKeyValueBackend, options} - ``` - - ## Examples - - Getting all firmware metadata: - - iex> Peridiod.KV.get_all() - %{ - "a.peridio_application_part0_devpath" => "/dev/mmcblk0p3", - "a.peridio_application_part0_fstype" => "ext4", - "a.peridio_application_part0_target" => "/root", - "a.peridio_architecture" => "arm", - "a.peridio_author" => "Peridio", - "a.peridio_description" => "", - "a.peridio_misc" => "", - "a.peridio_platform" => "rpi0", - "a.peridio_product" => "test_app", - "a.peridio_uuid" => "d9492bdb-94de-5288-425e-2de6928ef99c", - "a.peridio_vcs_identifier" => "", - "a.peridio_version" => "0.1.0", - "b.peridio_application_part0_devpath" => "/dev/mmcblk0p3", - "b.peridio_application_part0_fstype" => "ext4", - "b.peridio_application_part0_target" => "/root", - "b.peridio_architecture" => "arm", - "b.peridio_author" => "Peridio", - "b.peridio_description" => "", - "b.peridio_misc" => "", - "b.peridio_platform" => "rpi0", - "b.peridio_product" => "test_app", - "b.peridio_uuid" => "4e08ad59-fa3c-5498-4a58-179b43cc1a25", - "b.peridio_vcs_identifier" => "", - "b.peridio_version" => "0.1.1", - "peridio_active" => "b", - "peridio_devpath" => "/dev/mmcblk0", - "peridio_serial_number" => "" - } - - Parts of the firmware metadata are global, while others pertain to a - specific firmware slot. This is indicated by the key - data which describes - firmware of a specific slot have keys prefixed with the name of the - firmware slot. In the above example, `"peridio_active"` and - `"peridio_serial_number"` are global, while `"a.peridio_version"` and - `"b.peridio_version"` apply to the "a" and "b" firmware slots, - respectively. - - It is also possible to get firmware metadata that only pertains to the - currently active firmware slot: - - iex> Peridiod.KV.get_all_active() - %{ - "peridio_application_part0_devpath" => "/dev/mmcblk0p3", - "peridio_application_part0_fstype" => "ext4", - "peridio_application_part0_target" => "/root", - "peridio_architecture" => "arm", - "peridio_author" => "Peridio", - "peridio_description" => "", - "peridio_misc" => "", - "peridio_platform" => "rpi0", - "peridio_product" => "test_app", - "peridio_uuid" => "4e08ad59-fa3c-5498-4a58-179b43cc1a25", - "peridio_vcs_identifier" => "", - "peridio_version" => "0.1.1" - } - - Note that `get_all_active/0` strips out the `a.` and `b.` prefixes. - - Further, the two functions `get/1` and `get_active/1` allow you to get a - specific key from the firmware metadata. `get/1` requires specifying the - entire key name, while `get_active/1` will prepend the slot prefix for you: - - iex> Peridiod.KV.get("peridio_active") - "b" - iex> Peridiod.KV.get("b.peridio_uuid") - "4e08ad59-fa3c-5498-4a58-179b43cc1a25" - iex> Peridiod.KV.get_active("peridio_uuid") - "4e08ad59-fa3c-5498-4a58-179b43cc1a25" - - Aside from reading values from the KV store, it is also possible to write - new values to the firmware metadata. New values may either have unique keys, - in which case they will be added to the firmware metadata, or re-use a key, - in which case they will overwrite the current value with that key: - - iex> :ok = Peridiod.KV.put("my_firmware_key", "my_value") - iex> :ok = Peridiod.KV.put("peridio_serial_number", "my_new_serial_number") - iex> Peridiod.KV.get("my_firmware_key") - "my_value" - iex> Peridiod.KV.get("peridio_serial_number") - "my_new_serial_number" - - It is possible to write a collection of values at once, in order to - minimize number of writes: - - iex> :ok = Peridiod.KV.put_map(%{"one_key" => "one_val", "two_key" => "two_val"}) - iex> Peridiod.KV.get("one_key") - "one_val" - - Lastly, `put_active/3` and `put_active_map/2` allow you to write firmware metadata to the - currently active firmware slot without specifying the slot prefix yourself: - - iex> :ok = Peridiod.KV.put_active("peridio_misc", "Peridio is awesome") - iex> Peridiod.KV.get_active("peridio_misc") - "Peridio is awesome" - """ - use GenServer - - require Logger - - @typedoc """ - The KV store is a string -> string map - - Since the KV store is backed by things like the U-Boot environment blocks, - the keys and values can't be just any string. For example, characters with - the value `0` (i.e., `NULL`) are disallowed. The `=` sign is also disallowed - in keys. Values may have embedded new lines. In general, it's recommended to - stick with ASCII values to avoid causing trouble when working with C programs - which also access the variables. - """ - @type string_map() :: %{String.t() => String.t()} - - @typedoc false - @type state() :: %{backend: module(), options: keyword(), contents: string_map()} - - @doc """ - Start the KV store server - - Options: - * `:kv_backend` - a KV backend of the form `{module, options}` or just `module` - """ - @spec start_link(keyword()) :: GenServer.on_start() - def start_link(opts, genserver_opts \\ [name: __MODULE__]) do - GenServer.start_link(__MODULE__, opts, genserver_opts) - end - - @doc """ - Get the key for only the active firmware slot - """ - @spec get_active(String.t()) :: String.t() | nil - def get_active(pid_or_name \\ __MODULE__, key) when is_binary(key) do - GenServer.call(pid_or_name, {:get_active, key}) - end - - @doc """ - Get the key regardless of firmware slot - """ - @spec get(String.t()) :: String.t() | nil - def get(pid_or_name \\ __MODULE__, key) when is_binary(key) do - GenServer.call(pid_or_name, {:get, key}) - end - - @doc """ - Get and update a key in a single transaction - """ - @spec get_and_update(pid(), String.t(), (String.t() | :pop -> map())) :: String.t() | nil - def get_and_update(pid_or_name \\ __MODULE__, key, fun) do - GenServer.call(pid_or_name, {:get_and_update, key, fun}) - end - - @doc """ - Get all key value pairs for only the active firmware slot - """ - @spec get_all_active(pid()) :: string_map() - def get_all_active(pid_or_name \\ __MODULE__) do - GenServer.call(pid_or_name, :get_all_active) - end - - @doc """ - Get all keys regardless of firmware slot - """ - @spec get_all(pid()) :: string_map() - def get_all(pid_or_name \\ __MODULE__) do - GenServer.call(pid_or_name, :get_all) - end - - @doc """ - Get and update all keys in a single transaction - """ - @spec get_all_and_update(pid(), (String.t() | :pop -> map())) :: String.t() | nil - def get_all_and_update(pid_or_name \\ __MODULE__, fun) do - GenServer.call(pid_or_name, {:get_all_and_update, fun}) - end - - @doc """ - Write a key-value pair to the firmware metadata - """ - @spec put(pid(), String.t(), String.t()) :: :ok - def put(pid_or_name \\ __MODULE__, key, value) when is_binary(key) and is_binary(value) do - GenServer.call(pid_or_name, {:put, %{key => value}}) - end - - @doc """ - Write a collection of key-value pairs to the firmware metadata - """ - @spec put_map(pid(), string_map()) :: :ok - def put_map(pid_or_name \\ __MODULE__, kv) when is_map(kv) do - GenServer.call(pid_or_name, {:put, kv}) - end - - @doc """ - Write a key-value pair to the active firmware slot - """ - @spec put_active(pid(), String.t(), String.t()) :: :ok - def put_active(pid_or_name \\ __MODULE__, key, value) - when is_binary(key) and is_binary(value) do - GenServer.call(pid_or_name, {:put_active, %{key => value}}) - end - - @doc """ - Write a collection of key-value pairs to the active firmware slot - """ - @spec put_active_map(pid(), string_map()) :: :ok - def put_active_map(pid_or_name \\ __MODULE__, kv) when is_map(kv) do - GenServer.call(pid_or_name, {:put_active, kv}) - end - - @impl GenServer - def init(opts) do - {:ok, initial_state(opts)} - end - - @impl GenServer - def handle_call({:get_active, key}, _from, s) do - {:reply, active(key, s), s} - end - - def handle_call({:get, key}, _from, s) do - {:reply, Map.get(s.contents, key), s} - end - - def handle_call({:get_and_update, key, fun}, _from, s) do - current = Map.get(s.contents, key) - - {reply, s} = - case fun.(current) do - :pop -> - {:ok, Map.delete(current, key)} - - value -> - do_put(%{key => value}, s) - end - - {:reply, {reply, s}, s} - end - - def handle_call(:get_all_active, _from, s) do - active = active(s) <> "." - reply = filter_trim_active(s, active) - {:reply, reply, s} - end - - def handle_call(:get_all, _from, s) do - {:reply, s.contents, s} - end - - def handle_call({:get_all_and_update, fun}, _from, s) do - kv = fun.(s.contents) - {reply, s} = do_put(kv, s) - {:reply, {reply, s}, s} - end - - def handle_call({:put, kv}, _from, s) do - {reply, s} = do_put(kv, s) - {:reply, reply, s} - end - - def handle_call({:put_active, kv}, _from, s) do - {reply, s} = - Map.new(kv, fn {key, value} -> {"#{active(s)}.#{key}", value} end) - |> do_put(s) - - {:reply, reply, s} - end - - defp active(s), - do: Map.get(s.contents, "peridio_active") || Map.get(s.contents, "nerves_fw_active", "") - - defp active(key, s) do - Map.get(s.contents, "#{active(s)}.#{key}") - end - - defp filter_trim_active(s, active) do - Enum.filter(s.contents, fn {k, _} -> - String.starts_with?(k, active) - end) - |> Enum.map(fn {k, v} -> {String.replace_leading(k, active, ""), v} end) - |> Enum.into(%{}) - end - - defp do_put(kv, s) do - case s.backend.save(kv, s.options) do - :ok -> {:ok, %{s | contents: Map.merge(s.contents, kv)}} - error -> {error, s} - end - end - - defguardp is_module(v) when is_atom(v) and not is_nil(v) - - defp initial_state(options) do - case options[:kv_backend] do - {backend, opts} when is_module(backend) and is_list(opts) -> - initialize(backend, opts) - - backend when is_module(backend) -> - initialize(backend, []) - - _ -> - initialize(Peridiod.KVBackend.UBootEnv, []) - end - rescue - error -> - Logger.error("Peridiod has a bad KV configuration: #{inspect(error)}") - initialize(Peridiod.KVBackend.InMemory, []) - end - - defp initialize(backend, options) do - case backend.load(options) do - {:ok, contents} -> - %{backend: backend, options: options, contents: contents} - - {:error, reason} -> - Logger.error("Peridiod failed to load KV: #{inspect(reason)}") - %{backend: Peridiod.KVBackend.InMemory, options: [], contents: %{}} - end - end -end diff --git a/lib/peridiod/kv_backend.ex b/lib/peridiod/kv_backend.ex deleted file mode 100644 index d94946f..0000000 --- a/lib/peridiod/kv_backend.ex +++ /dev/null @@ -1,24 +0,0 @@ -defmodule Peridiod.KVBackend do - @moduledoc """ - Behaviour for customizing the Peridio's key-value store - """ - alias Peridiod.KV - - @doc """ - Load the KV store and return its contents - - This will be called on boot and should return all persisted key/value pairs. - The results will be cached and if a change should be persisted, `c:save/2` will - be called with the update. - """ - @callback load(options :: keyword) :: - {:ok, contents :: KV.string_map()} | {:error, reason :: any} - - @doc """ - Persist the updated KV pairs - - The KV map contains the KV pairs returned by `c:load/1` with any changes made - by users of `Peridiod.KV`. - """ - @callback save(contents :: KV.string_map(), options :: keyword) :: :ok | {:error, reason :: any} -end diff --git a/lib/peridiod/kv_backend/in_memory.ex b/lib/peridiod/kv_backend/in_memory.ex deleted file mode 100644 index d02535f..0000000 --- a/lib/peridiod/kv_backend/in_memory.ex +++ /dev/null @@ -1,25 +0,0 @@ -defmodule Peridiod.KVBackend.InMemory do - @moduledoc """ - In-memory KV store - - This KV store keeps everything in memory. Use it by specifying it - as a backend in the application configuration. Specifying an initial - set of contents is optional. - - ```elixir - config :peridiod, :kv_backend, {Peridiod.KV.InMemory, contents: %{"key" => "value"}} - ``` - """ - @behaviour Peridiod.KVBackend - - @impl Peridiod.KVBackend - def load(options) do - case Keyword.fetch(options, :contents) do - {:ok, contents} when is_map(contents) -> {:ok, contents} - _ -> {:ok, %{}} - end - end - - @impl Peridiod.KVBackend - def save(_new_state, _options), do: :ok -end diff --git a/lib/peridiod/kv_backend/uboot_env.ex b/lib/peridiod/kv_backend/uboot_env.ex deleted file mode 100644 index 0592bd2..0000000 --- a/lib/peridiod/kv_backend/uboot_env.ex +++ /dev/null @@ -1,24 +0,0 @@ -defmodule Peridiod.KVBackend.UBootEnv do - @moduledoc """ - U-Boot environment block KV store - - This is the default KV store. It delegates to the `UBootEnv` library - for loading and saving to a U-Boot formatted environment block. There's - nothing to configure. It will find the block by reading `/etc/fw_env.config`. - """ - - @behaviour Peridiod.KVBackend - - @impl Peridiod.KVBackend - def load(_options) do - UBootEnv.read() - end - - @impl Peridiod.KVBackend - def save(%{} = kv, _options) do - with {:ok, current_kv} <- UBootEnv.read() do - merged_kv = Map.merge(current_kv, kv) - UBootEnv.write(merged_kv) - end - end -end diff --git a/lib/peridiod/release.ex b/lib/peridiod/release.ex index 8b0c61b..aa73086 100644 --- a/lib/peridiod/release.ex +++ b/lib/peridiod/release.ex @@ -1,5 +1,6 @@ defmodule Peridiod.Release do - alias Peridiod.{KV, Binary, Cache, Release} + alias Peridiod.{Binary, Cache, Release} + alias PeridiodPersistence.KV import Peridiod.Utils, only: [stamp_utc_now: 0] @@ -156,7 +157,7 @@ defmodule Peridiod.Release do def filter_binaries_by_targets(%__MODULE__{binaries: binaries}, []), do: binaries def filter_binaries_by_targets(%__MODULE__{binaries: binaries}, targets) do - Enum.filter(binaries, &(&1.target in targets)) + Enum.filter(binaries, &(&1.target in [nil, "" | targets])) end def kv_progress(kv_pid \\ KV, %__MODULE__{} = release_metadata) do diff --git a/lib/peridiod/release/server.ex b/lib/peridiod/release/server.ex index 5bcc0fb..0e4698a 100644 --- a/lib/peridiod/release/server.ex +++ b/lib/peridiod/release/server.ex @@ -22,9 +22,10 @@ defmodule Peridiod.Release.Server do alias Peridiod.{Binary, SigningKey, Socket, Release} alias Peridiod.Binary.{Installer, CacheDownloader} + alias PeridiodPersistence.KV - @update_poll_interval 300_000 - @progress_message_interval 100 + @update_poll_interval 30 * 60 * 1000 + @progress_message_interval 1500 @doc false @spec start_link(any(), any()) :: GenServer.on_start() @@ -79,9 +80,9 @@ defmodule Peridiod.Release.Server do poll_interval = config.release_poll_interval || @update_poll_interval progress_message_interval = @progress_message_interval - current_release_prn = Peridiod.KV.get("peridio_rel_current") - current_release_version = Peridiod.KV.get("peridio_vsn_current") - progress_release_prn = Peridiod.KV.get("peridio_rel_progress") + current_release_prn = KV.get("peridio_rel_current") || "" + current_release_version = KV.get("peridio_vsn_current") || "" + progress_release_prn = KV.get("peridio_rel_progress") current_release = load_release_metadata_from_cache(current_release_prn, cache_pid) progress_release = load_release_metadata_from_cache(progress_release_prn, cache_pid) @@ -279,23 +280,22 @@ defmodule Peridiod.Release.Server do Logger.error("Error downloading to cache: #{inspect(error)}") - progress_message = - state.progress_message - |> Map.delete(binary_metadata.prn) - state = %{ state - | processing_binaries: Map.delete(state.processing_binaries, binary_metadata.prn), - progress_message: progress_message + | processing_binaries: Map.delete(state.processing_binaries, binary_metadata.prn) } {:noreply, state} end - def handle_info({Installer, binary_prn, {:progress, install_percent}}, state) do + def handle_info( + {Installer, binary_prn, {:progress, {download_percent, install_percent}}}, + state + ) do binary_progress = state.progress_message - |> Map.get(binary_prn, %{download_percent: 1.0, install_percent: 0.0}) + |> Map.get(binary_prn, %{download_percent: 0.0, install_percent: 0.0}) + |> Map.put(:download_percent, download_percent) |> Map.put(:install_percent, install_percent) progress_message = Map.put(state.progress_message, binary_prn, binary_progress) @@ -311,8 +311,7 @@ defmodule Peridiod.Release.Server do ) progress_message = - state.progress_message - |> Map.delete(binary_prn) + Map.put(state.progress_message, binary_prn, %{download_percent: 1.0, install_percent: 1.0}) state = %{ state @@ -389,8 +388,16 @@ defmodule Peridiod.Release.Server do # Logger.debug("Release Manager: update #{inspect(body)}") {:ok, release_metadata} = Release.metadata_from_manifest(body) Release.metadata_to_cache(state.cache_pid, release_metadata) - {:ok, state} = do_install_release(release_metadata, self(), state) - {:updating, %{state | progress_release: release_metadata}} + + case do_install_release(release_metadata, self(), state) do + {:ok, state} -> + Logger.info("Release Manager: Installing Update") + {:updating, %{state | progress_release: release_metadata}} + + {{:error, error}, state} -> + Logger.error("Release Manager: Error #{inspect(error)}") + {:no_update, state} + end end defp update_response({:ok, %{status: 200, body: %{"status" => "no_update"}}}, state) do @@ -398,6 +405,11 @@ defmodule Peridiod.Release.Server do {:no_update, state} end + defp update_response({:error, %{reason: reason}}, state) do + Logger.error("Release Manager: error checking for update #{inspect(reason)}") + {:no_update, state} + end + defp load_trusted_signing_keys([]) do Logger.warning(""" Trusted signing keys are unspecified @@ -521,7 +533,7 @@ defmodule Peridiod.Release.Server do end defp finish_release(%{installing_release: {release_metadata, [], callback}} = state) do - Logger.debug("Release Install complete") + Logger.info("Release Manager: Install complete") Release.stamp_installed(state.cache_pid, release_metadata) Release.kv_advance(state.kv_pid) diff --git a/lib/peridiod/socket.ex b/lib/peridiod/socket.ex index cf8440a..04cb8de 100644 --- a/lib/peridiod/socket.ex +++ b/lib/peridiod/socket.ex @@ -2,10 +2,9 @@ defmodule Peridiod.Socket do use Slipstream require Logger - alias Peridiod.Client - alias Peridiod.Distribution - alias Peridiod.RemoteConsole + alias Peridiod.{Client, Distribution, RemoteConsole} alias Peridiod.Binary.Installer.Fwup + alias PeridiodPersistence.KV @rejoin_after Application.compile_env(:peridiod, :rejoin_after, 5_000) @device_api_version "1.0.0" @@ -71,17 +70,22 @@ defmodule Peridiod.Socket do ] peridio_uuid = - Peridiod.KV.get_active("peridio_uuid") || Peridiod.KV.get_active("nerves_fw_uuid") + KV.get_active("peridio_uuid") || KV.get_active("nerves_fw_uuid") params = - Peridiod.KV.get_all_active() + KV.get_all_active() |> Map.put("nerves_fw_uuid", peridio_uuid) - |> Map.put("fwup_version", Fwup.version()) |> Map.put("device_api_version", @device_api_version) |> Map.put("console_version", @console_version) - current_release_prn = Peridiod.KV.get("peridio_rel_current") - current_release_version = Peridiod.KV.get("peridio_vsn_current") + params = + case Fwup.installed?() do + true -> Map.put(params, "fwup_version", Fwup.version()) + false -> params + end + + current_release_prn = KV.get("peridio_rel_current") + current_release_version = KV.get("peridio_vsn_current") sdk_client = config.sdk_client diff --git a/mix.exs b/mix.exs index 1657403..af5cd6f 100644 --- a/mix.exs +++ b/mix.exs @@ -15,6 +15,7 @@ defmodule Peridiod.MixProject do releases: [ peridiod: [ applications: [peridiod: :permanent], + include_executables_for: [:unix], steps: [:assemble, :tar], include_erts: System.get_env("MIX_TARGET_INCLUDE_ERTS") || true ] @@ -34,6 +35,7 @@ defmodule Peridiod.MixProject do {:extty, "~> 0.2"}, {:uuid, "~> 1.0"}, {:telemetry, "~> 1.0"}, + {:peridiod_persistence, github: "peridio/peridiod-persistence", branch: "main"}, {:peridio_rat, github: "peridio/peridio-rat", branch: "main"}, {:peridio_sdk, github: "peridio/peridio-elixir", branch: "main"}, {:erlexec, github: "peridio/erlexec"}, @@ -41,7 +43,6 @@ defmodule Peridiod.MixProject do {:castore, "~> 1.0"}, {:jason, "~> 1.0"}, {:hackney, "~> 1.10"}, - {:uboot_env, "~> 1.0"}, {:slipstream, "~> 1.0 or ~> 0.8"}, {:x509, "~> 0.8"}, {:plug, "~> 1.11", only: :test}, diff --git a/mix.lock b/mix.lock index 3848a63..9942a13 100644 --- a/mix.lock +++ b/mix.lock @@ -7,12 +7,12 @@ "cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"}, "elixir_make": {:hex, :elixir_make, "0.8.4", "4960a03ce79081dee8fe119d80ad372c4e7badb84c493cc75983f9d3bc8bde0f", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "6e7f1d619b5f61dfabd0a20aa268e575572b542ac31723293a4c1a567d5ef040"}, "erlexec": {:git, "https://github.com/peridio/erlexec.git", "a75db49025c3d99fb1f26233dc383072ac21de02", []}, - "extty": {:hex, :extty, "0.3.0", "55bd8881d00e70f92f9f60a9a3d6907d5cb6964c7992a4c812cc4f0a119696ec", [:mix], [], "hexpm", "d8ad88e38159b30aa75d33845f6342e429f832cd5ed043768d6fb57374d0c862"}, + "extty": {:hex, :extty, "0.4.1", "46aefe0a6056a04d687101deedb3ca5664233e1e4b3130ddca0232b9ee72433a", [:mix], [], "hexpm", "c049f2fad4068f4036a2314786e49b0cbde349ada397186e772a112652f2b6e8"}, "gen_stage": {:hex, :gen_stage, "1.2.1", "19d8b5e9a5996d813b8245338a28246307fd8b9c99d1237de199d21efc4c76a1", [:mix], [], "hexpm", "83e8be657fa05b992ffa6ac1e3af6d57aa50aace8f691fcf696ff02f8335b001"}, "hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"}, "hpax": {:hex, :hpax, "1.0.0", "28dcf54509fe2152a3d040e4e3df5b265dcb6cb532029ecbacf4ce52caea3fd2", [:mix], [], "hexpm", "7f1314731d711e2ca5fdc7fd361296593fc2542570b3105595bb0bc6d0fad601"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, - "jason": {:hex, :jason, "1.4.3", "d3f984eeb96fe53b85d20e0b049f03e57d075b5acda3ac8d465c969a2536c17b", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "9a90e868927f7c777689baa16d86f4d0e086d968db5c05d917ccff6d443e58a3"}, + "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, "mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"}, @@ -20,8 +20,9 @@ "mint_web_socket": {:hex, :mint_web_socket, "1.0.4", "0b539116dbb3d3f861cdf5e15e269a933cb501c113a14db7001a3157d96ffafd", [:mix], [{:mint, ">= 1.4.1 and < 2.0.0-0", [hex: :mint, repo: "hexpm", optional: false]}], "hexpm", "027d4c5529c45a4ba0ce27a01c0f35f284a5468519c045ca15f43decb360a991"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, - "peridio_rat": {:git, "https://github.com/peridio/peridio-rat.git", "6954b9f515ea032a350661cd0853de472295c3c0", [branch: "main"]}, - "peridio_sdk": {:git, "https://github.com/peridio/peridio-elixir.git", "838ad710823b3c7f5fb68af1f7dbc3cae506d762", [branch: "main"]}, + "peridio_rat": {:git, "https://github.com/peridio/peridio-rat.git", "6ed7b060710c4c59ae732c8d709d4a6ffee9a402", [branch: "main"]}, + "peridio_sdk": {:git, "https://github.com/peridio/peridio-elixir.git", "3a83f3b05a0358b54f7a70ef5e7e35bb9ef0d4ef", [branch: "main"]}, + "peridiod_persistence": {:git, "https://github.com/peridio/peridiod-persistence.git", "54ae22de2c78d8832f6cb6c447da135008918f8c", [branch: "main"]}, "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"}, "plug_cowboy": {:hex, :plug_cowboy, "2.7.1", "87677ffe3b765bc96a89be7960f81703223fe2e21efa42c125fcd0127dd9d6b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "02dbd5f9ab571b864ae39418db7811618506256f6d13b4a45037e5fe78dc5de3"}, "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, @@ -29,7 +30,7 @@ "slipstream": {:hex, :slipstream, "1.1.1", "7e56f62f1a9ee81351e3c36f57b9b187e00dc2f470e70ba46ea7ad16e80b061f", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mint_web_socket, "~> 0.2 or ~> 1.0", [hex: :mint_web_socket, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.1 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c20e420cde1654329d38ec3aa1c0e4debbd4c91ca421491e7984ad4644e638a6"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, - "tesla": {:hex, :tesla, "1.11.1", "902ec0cd9fb06ba534be765f0eb78acd9d0ef70118230dc3a73fdc9afc91d036", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "c02d7dd149633c55c40adfaad6c3ce2615cfc89258b67a7f428c14bb835c398c"}, + "tesla": {:hex, :tesla, "1.12.0", "170d64a3f566b9b614d60f53b206b2b91db8e29a48fed7553c40baf5b4c5f1fd", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "2fd07d9e3fdb0e39dd2cb16064e28ce02808a26cf33a44d269ef3b1fc20fc914"}, "uboot_env": {:hex, :uboot_env, "1.0.1", "b0e136cf1a561412ff7db23ed2b6df18d7c7ce2fc59941afd851006788a67f3d", [:mix], [], "hexpm", "b6d4fe7c24123be57ed946c48116d23173e37944bc945b8b76fccc437909c60b"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, "uuid": {:hex, :uuid, "1.1.8", "e22fc04499de0de3ed1116b770c7737779f226ceefa0badb3592e64d5cfb4eb9", [:mix], [], "hexpm", "c790593b4c3b601f5dc2378baae7efaf5b3d73c4c6456ba85759905be792f2ac"}, diff --git a/rel/env.sh b/rel/env.sh new file mode 100644 index 0000000..2e8280e --- /dev/null +++ b/rel/env.sh @@ -0,0 +1,20 @@ +#!/bin/sh + +# # Sets and enables heart (recommended only in daemon mode) +# case $RELEASE_COMMAND in +# daemon*) +# HEART_COMMAND="$RELEASE_ROOT/bin/$RELEASE_NAME $RELEASE_COMMAND" +# export HEART_COMMAND +# export ELIXIR_ERL_OPTIONS="-heart" +# ;; +# *) +# ;; +# esac + +# # Set the release to load code on demand (interactive) instead of preloading (embedded). +# export RELEASE_MODE=interactive + +# # Set the release to work across nodes. +# # RELEASE_DISTRIBUTION must be "sname" (local), "name" (distributed) or "none". +# export RELEASE_DISTRIBUTION=name +# export RELEASE_NODE=peridiod diff --git a/rel/remote.vm.args.eex b/rel/remote.vm.args.eex new file mode 100644 index 0000000..5db82b3 --- /dev/null +++ b/rel/remote.vm.args.eex @@ -0,0 +1 @@ +-start_epmd false -erl_epmd_port 6789 -dist_listen false diff --git a/rel/vm.args.eex b/rel/vm.args.eex new file mode 100644 index 0000000..fb7d23f --- /dev/null +++ b/rel/vm.args.eex @@ -0,0 +1 @@ + -start_epmd false -erl_epmd_port 6789 diff --git a/release/Containerfile-build.amd64 b/release/Containerfile-build-jammy similarity index 59% rename from release/Containerfile-build.amd64 rename to release/Containerfile-build-jammy index 1d929c7..01410e7 100644 --- a/release/Containerfile-build.amd64 +++ b/release/Containerfile-build-jammy @@ -1,7 +1,6 @@ -FROM --platform=linux/amd64 hexpm/elixir:1.16.3-erlang-26.2.5.1-ubuntu-noble-20240605 AS build +FROM hexpm/elixir:1.17.2-erlang-27.0.1-ubuntu-jammy-20240530 AS build ARG MIX_ENV=prod -ARG UBOOT_ENV_SIZE=0x20000 ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update \ @@ -9,7 +8,7 @@ RUN apt-get update \ git make build-essential RUN mix archive.install github hexpm/hex branch latest --force -RUN /usr/local/bin/mix local.rebar --force +RUN mix local.rebar --force RUN mkdir -p /opt/app @@ -21,4 +20,4 @@ RUN mix deps.get --only $MIX_ENV RUN mix release --overwrite FROM scratch AS app -COPY --from=build /opt/app/_build/prod/peridiod-*.tar.gz ./amd64/ +COPY --from=build /opt/app/_build/prod/peridiod-*.tar.gz . diff --git a/release/Containerfile-build.arm64 b/release/Containerfile-build-noble similarity index 59% rename from release/Containerfile-build.arm64 rename to release/Containerfile-build-noble index 67327d4..f74bbac 100644 --- a/release/Containerfile-build.arm64 +++ b/release/Containerfile-build-noble @@ -1,7 +1,6 @@ -FROM --platform=linux/arm64/v8 hexpm/elixir:1.16.3-erlang-26.2.5.1-ubuntu-noble-20240605 AS build +FROM hexpm/elixir:1.17.2-erlang-27.0.1-ubuntu-noble-20240605 AS build ARG MIX_ENV=prod -ARG UBOOT_ENV_SIZE=0x20000 ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update \ @@ -9,7 +8,7 @@ RUN apt-get update \ git make build-essential RUN mix archive.install github hexpm/hex branch latest --force -RUN /usr/local/bin/mix local.rebar --force +RUN mix local.rebar --force RUN mkdir -p /opt/app @@ -21,4 +20,4 @@ RUN mix deps.get --only $MIX_ENV RUN mix release --overwrite FROM scratch AS app -COPY --from=build /opt/app/_build/prod/peridiod-*.tar.gz ./arm64/ +COPY --from=build /opt/app/_build/prod/peridiod-*.tar.gz . diff --git a/release/Containerfile-build-rhel9 b/release/Containerfile-build-rhel9 new file mode 100644 index 0000000..3b4cb0e --- /dev/null +++ b/release/Containerfile-build-rhel9 @@ -0,0 +1,51 @@ +FROM fedora:34 AS build + +ARG MIX_ENV=prod +ARG ERLANG_VERSION=27.0.1 +ARG ELIXIR_VERSION=1.17.2-otp-27 + +RUN dnf groupinstall -y 'Development Tools' && \ + dnf install -y \ + gcc-c++ \ + autoconf \ + gcc \ + glibc-devel \ + make \ + ncurses-devel \ + openssl-devel \ + git \ + unzip + +SHELL ["/bin/bash", "-lc"] + +RUN git clone https://github.com/asdf-vm/asdf.git $HOME/.asdf --branch v0.14.0 + +RUN chmod +x $HOME/.asdf/asdf.sh +RUN chmod +x $HOME/.asdf/completions/asdf.bash + +RUN echo '. $HOME/.asdf/asdf.sh' >> /root/.bashrc +RUN echo '. $HOME/.asdf/asdf.sh' >> /root/.zshrc + + +RUN asdf plugin add erlang +RUN asdf plugin add elixir + +RUN asdf install erlang ${ERLANG_VERSION} && \ + asdf global erlang ${ERLANG_VERSION} + +RUN asdf install elixir ${ELIXIR_VERSION} && \ + asdf global elixir ${ELIXIR_VERSION} + +RUN mix archive.install github hexpm/hex branch latest --force +RUN mix local.rebar --force + +RUN mkdir -p /opt/peridiod +ADD . /opt/peridiod/ + +WORKDIR /opt/peridiod + +RUN mix deps.get --only $MIX_ENV +RUN mix release --overwrite + +FROM scratch +COPY --from=build /opt/peridiod/_build/prod/peridiod-*.tar.gz . diff --git a/release/Containerfile-build-static b/release/Containerfile-build-static new file mode 100644 index 0000000..6e35228 --- /dev/null +++ b/release/Containerfile-build-static @@ -0,0 +1,94 @@ +FROM fedora:40 AS build + +ARG MIX_ENV=prod +ARG OPENSSL_VERSION=1.1.1v +ARG ERLANG_VERSION=27.0.1 +ARG ELIXIR_VERSION=1.17.2 + +RUN dnf groupinstall -y 'Development Tools' && \ + dnf install -y \ + glibc-static \ + glibc-locale-source \ + glibc-langpack-en \ + libstdc++-devel \ + libstdc++-static \ + gcc-c++ \ + wget \ + perl + +ENV LANG=en_US.UTF-8 \ + LANGUAGE=en_US:en \ + LC_ALL=en_US.UTF-8 + +RUN localedef -i en_US -f UTF-8 en_US.UTF-8 + +RUN wget https://www.openssl.org/source/openssl-${OPENSSL_VERSION}.tar.gz && \ + tar zxvf openssl-${OPENSSL_VERSION}.tar.gz + +RUN cd openssl-${OPENSSL_VERSION} && \ + ./config \ + no-shared \ + no-pinshared \ + --prefix=/opt/openssl && \ + make && make install + +RUN wget https://ftp.gnu.org/pub/gnu/ncurses/ncurses-6.4.tar.gz && \ + tar -xzvf ncurses-6.4.tar.gz + +RUN cd ncurses-6.4 && \ + ./configure \ + --prefix=/opt/ncurses \ + --enable-static \ + --disable-shared \ + --with-normal \ + --with-cxx-binding \ + --with-cxx-shared && \ + make && make install + +RUN wget https://github.com/erlang/otp/archive/refs/tags/OTP-${ERLANG_VERSION}.tar.gz && \ + tar zxvf OTP-${ERLANG_VERSION}.tar.gz + +RUN cd otp-OTP-${ERLANG_VERSION} && \ + CFLAGS="-O2 -Wno-error -I/opt/ncurses/include" \ + CXXFLAGS="$CFLAGS -static" \ + LDFLAGS="-L/usr/lib64 -static -L/opt/ncurses/lib" \ + LIBS="/opt/openssl/lib/libcrypto.a" \ + ./configure \ + --without-debugger \ + --without-dialyzer \ + --without-jinterface \ + --without-megaco \ + --without-observer \ + --without-odbc \ + --without-typer \ + --without-wx \ + --without-et \ + --without-javac \ + --enable-static \ + --enable-static-nifs \ + --enable-static-drivers \ + --disable-dynamic-ssl-lib \ + --with-crypto \ + --with-ssl-incl=/opt/openssl \ + --with-ssl=/opt/openssl && \ + make && make install + +RUN wget https://github.com/elixir-lang/elixir/archive/refs/tags/v${ELIXIR_VERSION}.tar.gz -O elixir-v${ELIXIR_VERSION}.tar.gz && \ + tar zxvf elixir-v${ELIXIR_VERSION}.tar.gz + +RUN cd elixir-${ELIXIR_VERSION} && \ + make clean compile && make install + +RUN mix archive.install github hexpm/hex branch latest --force +RUN /usr/local/bin/mix local.rebar --force + +RUN mkdir -p /opt/peridiod +ADD . /opt/peridiod/ + +WORKDIR /opt/peridiod + +RUN mix deps.get --only $MIX_ENV +RUN CFLAGS="-static" CXXFLAGS="-static" LDFLAGS="-static" mix release --overwrite + +FROM scratch AS app +COPY --from=build /opt/peridiod/_build/prod/peridiod-*.tar.gz . diff --git a/release/Containerfile-release b/release/Containerfile-release index b9cebe6..9f8aeb6 100644 --- a/release/Containerfile-release +++ b/release/Containerfile-release @@ -1,4 +1,4 @@ -FROM elixir:1.16.3-alpine AS build +FROM elixir:1.17.2-alpine AS build ARG MIX_ENV=prod @@ -40,7 +40,7 @@ WORKDIR /opt/app RUN mix deps.get --only $MIX_ENV RUN mix release --overwrite -FROM alpine:3.20 as app +FROM alpine:3.20 RUN apk add --no-cache \ agetty \ @@ -66,7 +66,7 @@ RUN ln -s /etc/peridiod/fw_env.config /etc/fw_env.config COPY --from=build /opt/app/priv/peridio-cert.pem /etc/peridiod/peridio-cert.pem COPY --from=build /opt/app/_build/prod/rel/peridiod /opt/peridiod -COPY --from=build /opt/app/release/peridio.json /etc/peridiod/peridio.json +COPY --from=build /opt/app/release/container-peridio.json /etc/peridiod/peridio.json COPY --from=build /opt/fwup/src/fwup /usr/bin/fwup WORKDIR /opt/peridiod diff --git a/release/build-deb.sh b/release/build-deb.sh new file mode 100755 index 0000000..a728321 --- /dev/null +++ b/release/build-deb.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env sh + +set -e + +# Args +# 1) PERIDIOD_RELEASE_ARCHIVE: path to the release archive +# 2) PERIDIOD_PACKAGE_DIR: path to the package build dir + +PERIDIOD_RELEASE_ARCHIVE=$1 +PERIDIOD_PACKAGE_DIR=$2 + +SCRIPT_DIR=$(dirname "$(readlink -f "$0")") + +mkdir -p "$PERIDIOD_PACKAGE_DIR/DEBIAN" \ + "$PERIDIOD_PACKAGE_DIR/usr/lib/peridiod" \ + "$PERIDIOD_PACKAGE_DIR/etc/peridiod" \ + "$PERIDIOD_PACKAGE_DIR/usr/lib/systemd/system/" + +envsubst < "$SCRIPT_DIR/deb/control" > "$PERIDIOD_PACKAGE_DIR/DEBIAN/control" +envsubst < "$SCRIPT_DIR/deb/changelog" > "$PERIDIOD_PACKAGE_DIR/DEBIAN/changelog" +cp "$SCRIPT_DIR/deb/postinstall" "$PERIDIOD_PACKAGE_DIR/DEBIAN" +cp "$SCRIPT_DIR/deb/prerm" "$PERIDIOD_PACKAGE_DIR/DEBIAN" + +tar -xvf "$PERIDIOD_RELEASE_ARCHIVE" -C "$PERIDIOD_PACKAGE_DIR/usr/lib/peridiod" +cp "$SCRIPT_DIR/peridiod.service" "$PERIDIOD_PACKAGE_DIR/usr/lib/systemd/system/" +cp "$SCRIPT_DIR/peridiod.env" "$PERIDIOD_PACKAGE_DIR/etc/peridiod/" +cp "$SCRIPT_DIR/pkg-peridio.json" "$PERIDIOD_PACKAGE_DIR/etc/peridiod/peridio.json" + +dpkg-deb --build "$PERIDIOD_PACKAGE_DIR" diff --git a/release/build-rpm.sh b/release/build-rpm.sh new file mode 100755 index 0000000..9f842dd --- /dev/null +++ b/release/build-rpm.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env sh + +set -e + +# Args +# 1) PERIDIOD_RELEASE_ARCHIVE: path to the release archive +# 2) PERIDIOD_PACKAGE_DIR: path to the package build dir + +PERIDIOD_RELEASE_ARCHIVE=$1 +PERIDIOD_PACKAGE_DIR=$2 + +SCRIPT_DIR=$(dirname "$(readlink -f "$0")") + +mkdir -p "$PERIDIOD_PACKAGE_DIR/peridiod-$PERIDIOD_VERSION_RPM/peridiod" +mkdir -p "$PERIDIOD_PACKAGE_DIR/RPMS" "$PERIDIOD_PACKAGE_DIR/SOURCES" "$PERIDIOD_PACKAGE_DIR/SPECS" +tar -xvf "$PERIDIOD_RELEASE_ARCHIVE" -C "$PERIDIOD_PACKAGE_DIR/peridiod-$PERIDIOD_VERSION_RPM/peridiod" +cp "$SCRIPT_DIR/peridiod.service" "$PERIDIOD_PACKAGE_DIR/peridiod-$PERIDIOD_VERSION_RPM" +cp "$SCRIPT_DIR/peridiod.env" "$PERIDIOD_PACKAGE_DIR/peridiod-$PERIDIOD_VERSION_RPM" +cp "$SCRIPT_DIR/pkg-peridio.json" "$PERIDIOD_PACKAGE_DIR/peridiod-$PERIDIOD_VERSION_RPM/peridio.json" + +tar -czvf "$PERIDIOD_PACKAGE_DIR/SOURCES/peridiod-$PERIDIOD_VERSION_RPM.tar.gz" -C "$PERIDIOD_PACKAGE_DIR" "peridiod-$PERIDIOD_VERSION_RPM" +rm -rf "$PERIDIOD_PACKAGE_DIR/peridiod-$PERIDIOD_VERSION_RPM" + +envsubst < "$SCRIPT_DIR/rpm/spec" > "$PERIDIOD_PACKAGE_DIR/SPECS/peridiod.spec" + +rpmbuild -vv -ba --define "_topdir $PERIDIOD_PACKAGE_DIR" "$PERIDIOD_PACKAGE_DIR/SPECS/peridiod.spec" +rpm -qlp "$PERIDIOD_PACKAGE_DIR/RPMS/$PERIDIOD_ARCH_RPM/peridiod-$PERIDIOD_VERSION_RPM-1.$PERIDIOD_ARCH_RPM.rpm" diff --git a/release/peridio.json b/release/container-peridio.json similarity index 100% rename from release/peridio.json rename to release/container-peridio.json diff --git a/release/deb/changelog b/release/deb/changelog new file mode 100644 index 0000000..46fcf16 --- /dev/null +++ b/release/deb/changelog @@ -0,0 +1,5 @@ +peridiod ($PERIDIOD_VERSION) ; urgency=medium + +$PERIDIOD_RELEASE_NOTES + + -- $PERIDIOD_MAINTAINER $(date -R) diff --git a/release/deb/control b/release/deb/control new file mode 100644 index 0000000..527e73b --- /dev/null +++ b/release/deb/control @@ -0,0 +1,11 @@ +Package: peridiod +Version: $PERIDIOD_VERSION +License: $PERIDIOD_LICENSE +Vendor: $PERIDIOD_VENDOR +Architecture: $PERIDIOD_ARCH +Maintainer: $PERIDIOD_MAINTAINER +Depends: libc6, libncurses5 | libncurses6, libssl1.1 | libssl3, zlib1g +Section: devel +Priority: optional +Homepage: $PERIDIOD_HOMEPAGE +Description: $PERIDIOD_DESCRIPTION diff --git a/release/deb/postinstall b/release/deb/postinstall new file mode 100755 index 0000000..946cda2 --- /dev/null +++ b/release/deb/postinstall @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -e + +# Enable and start the systemd service +if [ "$1" = "configure" ]; then + systemctl enable peridiod.service + systemctl start peridiod.service +fi diff --git a/release/deb/prerm b/release/deb/prerm new file mode 100755 index 0000000..e8ca991 --- /dev/null +++ b/release/deb/prerm @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -e + +# Stop and disable the systemd service +if [ "$1" = "remove" ]; then + systemctl stop peridiod.service + systemctl disable peridiod.service +fi diff --git a/release/package-info.sh b/release/package-info.sh new file mode 100755 index 0000000..e6a33c1 --- /dev/null +++ b/release/package-info.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env sh + +PERIDIOD_LICENSE="Apache 2.0" +PERIDIOD_VENDOR="support@peridio.com" +PERIDIOD_MAINTAINER="Peridio " +PERIDIOD_HOMEPAGE="https://www.peridio.com" +PERIDIOD_DESCRIPTION="Peridio Daemon for Managing Embedded Linux Products" + +export PERIDIOD_LICENSE +export PERIDIOD_VENDOR +export PERIDIOD_MAINTAINER +export PERIDIOD_HOMEPAGE +export PERIDIOD_DESCRIPTION diff --git a/release/peridiod.env b/release/peridiod.env new file mode 100644 index 0000000..73ee963 --- /dev/null +++ b/release/peridiod.env @@ -0,0 +1,2 @@ +PERIDIO_KV_BACKEND=filesystem +PERIDIO_CONFIG_FILE=/etc/peridiod/peridio.json diff --git a/release/peridiod.service b/release/peridiod.service new file mode 100644 index 0000000..09d1e2f --- /dev/null +++ b/release/peridiod.service @@ -0,0 +1,13 @@ +[Unit] +Description=Peridio Daemon + +[Service] +Restart=on-failure +RestartSec=5s + +EnvironmentFile=/etc/peridiod/peridiod.env +ExecStart=/opt/peridiod/bin/peridiod start +User=root + +[Install] +WantedBy=multi-user.target diff --git a/release/pkg-peridio.json b/release/pkg-peridio.json new file mode 100644 index 0000000..aa54792 --- /dev/null +++ b/release/pkg-peridio.json @@ -0,0 +1,20 @@ +{ + "version": 1, + "cache_dir": "/var/peridiod", + "remote_shell": false, + "release_poll_enabled": true, + "targets": ["portable"], + "trusted_signing_keys": [], + "remote_access_tunnels": { + "enabled": false, + "service_ports": [22] + }, + + "node": { + "key_pair_source": "file", + "key_pair_config": { + "private_key_path": "/etc/peridiod/device-certificate.pem", + "certificate_path": "/etc/peridiod/device-private-key.pem" + } + } +} diff --git a/release/rpm/spec b/release/rpm/spec new file mode 100644 index 0000000..9a648a4 --- /dev/null +++ b/release/rpm/spec @@ -0,0 +1,41 @@ +Name: peridiod +Version: $PERIDIOD_VERSION_RPM +Release: 1%{?dist} +Summary: $PERIDIOD_DESCRIPTION + +License: $PERIDIOD_LICENSE +URL: $PERIDIOD_HOMEPAGE +Source0: %{name}-%{version}.tar.gz + +BuildArch: $PERIDIOD_ARCH_RPM +Requires: glibc +Requires: openssl +Requires: zlib + +%description +$PERIDIOD_DESCRIPTION + +%prep +%setup -q + +%build +# No build required for precompiled sources + +%install +mkdir -p %{buildroot}/usr/lib/systemd/system +mkdir -p %{buildroot}/usr/lib/peridiod +mkdir -p %{buildroot}/etc/peridiod + +cp -r peridiod/* %{buildroot}/usr/lib/peridiod +cp peridiod.service %{buildroot}/usr/lib/systemd/system +cp peridiod.env %{buildroot}/etc/peridiod +cp peridio.json %{buildroot}/etc/peridiod + +%files +/usr/lib/peridiod/* +/usr/lib/systemd/system/peridiod.service +/etc/peridiod/peridio.json +/etc/peridiod/peridiod.env + +%changelog +$PERIDIOD_CHANGELOG diff --git a/test/peridiod/binary/installer_test.exs b/test/peridiod/binary/installer_test.exs index dbe9075..1a0785e 100644 --- a/test/peridiod/binary/installer_test.exs +++ b/test/peridiod/binary/installer_test.exs @@ -8,6 +8,7 @@ defmodule Peridiod.Binary.InstallerTest do describe "fwup" do setup :start_cache + setup :start_kv setup :setup_fwup test "install task upgrade", %{binary_metadata: binary_metadata, opts: opts} = context do diff --git a/test/peridiod/release/server_test.exs b/test/peridiod/release/server_test.exs index 5ddb9e1..ebafc02 100644 --- a/test/peridiod/release/server_test.exs +++ b/test/peridiod/release/server_test.exs @@ -74,6 +74,7 @@ defmodule Peridiod.Release.ServerTest do describe "release" do setup :start_cache + setup :start_kv setup :load_release_metadata_from_manifest setup :start_release_server @@ -190,6 +191,7 @@ defmodule Peridiod.Release.ServerTest do describe "release install cache" do setup :start_cache + setup :start_kv setup :load_release_metadata_from_manifest setup :start_release_server setup :cache_binary @@ -217,6 +219,7 @@ defmodule Peridiod.Release.ServerTest do describe "release install" do setup :start_cache + setup :start_kv setup :load_release_metadata_from_manifest setup :start_release_server diff --git a/test/support/test_case.ex b/test/support/test_case.ex index d2edf2a..f9e0c06 100644 --- a/test/support/test_case.ex +++ b/test/support/test_case.ex @@ -1,7 +1,8 @@ defmodule PeridiodTest.Case do use ExUnit.CaseTemplate - alias Peridiod.{KV, Cache, Release} + alias PeridiodPersistence.KV + alias Peridiod.{Cache, Release} alias PeridiodTest.StaticRouter alias Peridiod.TestFixtures @@ -58,8 +59,8 @@ defmodule PeridiodTest.Case do end def start_kv(context) do - application_config = Application.get_all_env(:peridiod) - {:ok, kv_pid} = KV.start_link(application_config, []) + persistence_config = Application.get_all_env(:peridiod_persistence) + {:ok, kv_pid} = KV.start_link(persistence_config, []) Map.put(context, :kv_pid, kv_pid) end @@ -69,11 +70,12 @@ defmodule PeridiodTest.Case do config = struct(Peridiod.Config, application_config) |> Peridiod.Config.new() config = Map.put(config, :cache_pid, cache_pid) - if kv_pid = context[:kv_pid] do - Map.put(config, :kv_pid, kv_pid) - else - config - end + config = + if kv_pid = context[:kv_pid] do + Map.put(config, :kv_pid, kv_pid) + else + config + end {:ok, pid} = Release.Server.start_link(config, [])