├── .cargo └── config.toml ├── .github ├── codecov.yaml ├── dependabot.yml └── workflows │ ├── check.yaml │ ├── ci.yaml │ ├── coverage.yaml │ ├── dco.yaml │ ├── e2e-test.yaml │ ├── reuse-lint.yaml │ ├── test.yaml │ └── warmup-caches.yaml ├── .gitignore ├── .reuse └── templates │ └── apache-2.jinja2 ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.lock.license ├── Cargo.toml ├── LICENSE ├── LICENSES ├── Apache-2.0.txt └── CC0-1.0.txt ├── README.md ├── REUSE.toml ├── SECURITY.md ├── assets └── example.toml ├── build.rs ├── cellular-modems-service ├── Cargo.toml ├── README.md ├── scripts │ ├── get.sh │ ├── insert.sh │ └── list.sh └── src │ └── main.rs ├── doc └── os_requirements.md ├── e2e-test-containers ├── Cargo.toml ├── README.md ├── assets │ ├── data.json │ ├── delete.json │ ├── isolate-no-net.json │ ├── rollback.json │ ├── stop.json │ └── update.json ├── scripts │ ├── entrypoint.sh │ └── export-env.sh └── src │ ├── cli.rs │ ├── main.rs │ ├── receive.rs │ └── send.rs ├── e2e-test-forwarder ├── Cargo.toml └── src │ └── main.rs ├── e2e-test ├── Cargo.toml └── src │ └── main.rs ├── edgehog-device-runtime-containers ├── CHANGELOG.md ├── Cargo.toml ├── README.md └── src │ ├── client.rs │ ├── docker │ ├── container.rs │ ├── image.rs │ ├── mod.rs │ ├── network.rs │ └── volume.rs │ ├── error.rs │ ├── events │ ├── deployment.rs │ └── mod.rs │ ├── lib.rs │ ├── mock.rs │ ├── properties │ ├── container.rs │ ├── deployment.rs │ ├── image.rs │ ├── mod.rs │ ├── network.rs │ └── volume.rs │ ├── requests │ ├── container.rs │ ├── deployment.rs │ ├── image.rs │ ├── mod.rs │ ├── network.rs │ └── volume.rs │ ├── resource │ ├── container.rs │ ├── deployment.rs │ ├── image.rs │ ├── mod.rs │ ├── network.rs │ └── volume.rs │ ├── service │ ├── events.rs │ └── mod.rs │ └── store │ ├── container.rs │ ├── deployment.rs │ ├── image.rs │ ├── mod.rs │ ├── network.rs │ └── volume.rs ├── edgehog-device-runtime-forwarder ├── Cargo.toml ├── README.md ├── src │ ├── astarte.rs │ ├── collection.rs │ ├── connection │ │ ├── http.rs │ │ ├── mod.rs │ │ └── websocket.rs │ ├── connections_manager.rs │ ├── lib.rs │ ├── messages.rs │ ├── test_utils.rs │ └── tls.rs └── tests │ ├── http_test.rs │ └── ws_test.rs ├── edgehog-device-runtime-store ├── Cargo.toml ├── README.md ├── assets │ ├── init-reader.sql │ ├── init-writer.sql │ └── schema.patch ├── diesel.toml ├── migrations │ └── 2024-11-29-163409_initial │ │ ├── down.sql │ │ └── up.sql ├── scripts │ ├── create-db.sh │ └── run-migrations.sh └── src │ ├── conversions.rs │ ├── db.rs │ ├── lib.rs │ ├── models │ ├── containers │ │ ├── container.rs │ │ ├── deployment.rs │ │ ├── image.rs │ │ ├── mod.rs │ │ ├── network.rs │ │ └── volume.rs │ └── mod.rs │ └── schema │ ├── containers.rs │ └── mod.rs ├── hardware-id-service ├── Cargo.toml ├── README.md ├── io.edgehog.Device.conf ├── scripts │ └── get-hardware-id.sh └── src │ └── main.rs ├── led-manager-service ├── Cargo.toml ├── README.md ├── scripts │ ├── insert.sh │ ├── list.sh │ └── set.sh └── src │ └── main.rs ├── scripts ├── copyright.sh ├── coverage.sh ├── e2e.sh └── register-device.sh └── src ├── cli.rs ├── commands.rs ├── config.rs ├── containers.rs ├── controller ├── actor.rs ├── event.rs └── mod.rs ├── data ├── astarte_device_sdk_lib.rs ├── astarte_message_hub_node.rs └── mod.rs ├── device.rs ├── error.rs ├── forwarder.rs ├── led_behavior.rs ├── lib.rs ├── main.rs ├── ota ├── event.rs ├── mod.rs ├── ota_handler.rs ├── ota_handler_test.rs └── rauc.rs ├── power_management.rs ├── repository ├── file_state_repository.rs └── mod.rs ├── systemd_wrapper.rs └── telemetry ├── battery_status.rs ├── cellular_properties.rs ├── event.rs ├── hardware_info.rs ├── mod.rs ├── net_interfaces.rs ├── os_release.rs ├── runtime_info.rs ├── sender.rs ├── storage_usage.rs ├── system_info.rs ├── system_status.rs ├── upower ├── device.rs └── mod.rs └── wifi_scan.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | # This file is part of Edgehog. 2 | # 3 | # Copyright 2023 SECO Mind Srl 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # SPDX-License-Identifier: Apache-2.0 18 | 19 | [alias] 20 | e2e-test = "run -p e2e-test -- " 21 | e2e-test-containers = "run -p e2e-test-containers -- " 22 | -------------------------------------------------------------------------------- /.github/codecov.yaml: -------------------------------------------------------------------------------- 1 | # This file is part of Edgehog. 2 | # 3 | # Copyright 2024 SECO Mind Srl 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # SPDX-License-Identifier: Apache-2.0 18 | 19 | # ref: https://docs.codecov.com/docs/codecovyml-reference 20 | coverage: 21 | range: 60..100 22 | round: down 23 | precision: 1 24 | status: 25 | # ref: https://docs.codecov.com/docs/commit-status 26 | project: 27 | default: 28 | # Avoid false negatives 29 | threshold: 1% 30 | branches: 31 | - "!main" 32 | 33 | # Test files aren't important for coverage 34 | ignore: 35 | - "tests" 36 | - "e2e-test" 37 | - "benches" 38 | 39 | # Make comments less noisy 40 | comment: 41 | layout: "header, diff, files, components" 42 | require_changes: yes 43 | 44 | component_management: 45 | default_rules: # default rules that will be inherited by all components 46 | statuses: 47 | - type: project 48 | target: auto 49 | branches: 50 | - "!main" 51 | individual_components: 52 | - component_id: edgehog-device-runtime 53 | name: runtime 54 | paths: 55 | - src/** 56 | - src/* 57 | statuses: # the core component has its own statuses 58 | - type: project 59 | target: auto 60 | - type: patch 61 | - component_id: edgehog-device-runtime-containers 62 | name: containers 63 | paths: 64 | - edgehog-device-runtime-containers/** 65 | - component_id: edgehog-device-runtime-forwarder 66 | name: forwarder 67 | paths: 68 | - edgehog-device-runtime-forwarder/** 69 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # This file is part of Edgehog. 2 | # 3 | # Copyright 2022-2024 SECO Mind Srl 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # SPDX-License-Identifier: Apache-2.0 18 | 19 | version: 2 20 | updates: 21 | - package-ecosystem: github-actions 22 | directory: / 23 | schedule: 24 | interval: weekly 25 | - package-ecosystem: cargo 26 | directory: / 27 | schedule: 28 | interval: weekly 29 | -------------------------------------------------------------------------------- /.github/workflows/check.yaml: -------------------------------------------------------------------------------- 1 | # This file is part of Edgehog. 2 | # 3 | # Copyright 2022 - 2025 SECO Mind Srl 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # SPDX-License-Identifier: Apache-2.0 18 | 19 | name: check 20 | on: 21 | workflow_call: 22 | workflow_dispatch: 23 | permissions: 24 | contents: read 25 | env: 26 | CARGO_TERM_COLOR: always 27 | SCCACHE_GHA_ENABLED: "true" 28 | RUSTC_WRAPPER: "sccache" 29 | jobs: 30 | fmt: 31 | runs-on: ubuntu-24.04 32 | name: stable / fmt 33 | steps: 34 | - uses: actions/checkout@v4 35 | - name: Install stable 36 | uses: dtolnay/rust-toolchain@stable 37 | with: 38 | components: rustfmt 39 | - name: Check formatting 40 | run: cargo fmt --check --all 41 | clippy: 42 | runs-on: ubuntu-24.04 43 | name: ${{ matrix.toolchain }} / clippy 44 | strategy: 45 | fail-fast: false 46 | matrix: 47 | toolchain: [stable, beta] 48 | steps: 49 | - uses: actions/checkout@v4 50 | - name: Install system dependencies 51 | run: | 52 | sudo apt update 53 | sudo apt-get install -y libsqlite3-dev libssl-dev libudev-dev libsystemd-dev upower 54 | - name: Install ${{ matrix.toolchain }} 55 | uses: dtolnay/rust-toolchain@master 56 | with: 57 | toolchain: ${{ matrix.toolchain }} 58 | components: clippy 59 | - name: Install sccache-cache 60 | uses: mozilla-actions/sccache-action@v0.0.9 61 | - name: cargo clippy 62 | run: cargo clippy --all-targets --all-features --workspace -- -D warnings 63 | doc: 64 | runs-on: ubuntu-24.04 65 | name: nightly / doc 66 | env: 67 | RUSTDOCFLAGS: -Dwarnings 68 | steps: 69 | - uses: actions/checkout@v4 70 | - name: Install system dependencies 71 | run: | 72 | sudo apt update 73 | sudo apt-get install -y libsqlite3-dev libssl-dev libudev-dev libsystemd-dev upower 74 | - name: Install nightly 75 | uses: dtolnay/rust-toolchain@nightly 76 | - name: Install sccache-cache 77 | uses: mozilla-actions/sccache-action@v0.0.9 78 | - name: Install cargo-docs-rs 79 | uses: dtolnay/install@cargo-docs-rs 80 | - run: cargo docs-rs 81 | hack: 82 | runs-on: ubuntu-24.04 83 | name: ubuntu / stable / features 84 | steps: 85 | - uses: actions/checkout@v4 86 | - name: Install system dependencies 87 | run: | 88 | sudo apt update 89 | sudo apt-get install -y libsqlite3-dev libssl-dev libudev-dev libsystemd-dev upower 90 | - name: Install stable 91 | uses: dtolnay/rust-toolchain@stable 92 | - name: Install sccache-cache 93 | uses: mozilla-actions/sccache-action@v0.0.9 94 | - name: cargo install cargo-hack 95 | uses: taiki-e/install-action@cargo-hack 96 | - name: cargo hack test 97 | # Doesn't test all combination of features, but the space is becoming too large and it takes 98 | # too long 99 | run: cargo hack --each-feature test 100 | msrv: 101 | runs-on: ubuntu-24.04 102 | strategy: 103 | matrix: 104 | msrv: [1.78] 105 | name: ubuntu / ${{ matrix.msrv }} 106 | steps: 107 | - uses: actions/checkout@v4 108 | - name: Install system dependencies 109 | run: | 110 | sudo apt update 111 | sudo apt-get install -y libsqlite3-dev libssl-dev libudev-dev libsystemd-dev upower 112 | - name: Install ${{ matrix.msrv }} 113 | uses: dtolnay/rust-toolchain@master 114 | with: 115 | toolchain: ${{ matrix.msrv }} 116 | - name: Install sccache-cache 117 | uses: mozilla-actions/sccache-action@v0.0.9 118 | - name: cargo +${{ matrix.msrv }} check 119 | # the compatibility with the MSRV needs to be checked only for the binary 120 | run: cargo +${{ matrix.msrv }} check --all-features -p edgehog-device-runtime 121 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | # This file is part of Edgehog. 2 | # 3 | # Copyright 2024 - 2025 SECO Mind Srl 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # SPDX-License-Identifier: Apache-2.0 18 | 19 | name: ci 20 | on: 21 | workflow_dispatch: 22 | pull_request: 23 | push: 24 | branches: 25 | - main 26 | - release-* 27 | permissions: 28 | contents: read 29 | # Spend CI time only on latest ref 30 | concurrency: 31 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 32 | cancel-in-progress: true 33 | defaults: 34 | run: 35 | shell: bash 36 | jobs: 37 | dco: 38 | uses: ./.github/workflows/dco.yaml 39 | permissions: 40 | actions: read 41 | pull-requests: read 42 | with: 43 | pr: ${{ github.event.pull_request.number }} 44 | reuse: 45 | uses: ./.github/workflows/reuse-lint.yaml 46 | warmup-caches: 47 | uses: ./.github/workflows/warmup-caches.yaml 48 | needs: [reuse, dco] 49 | check: 50 | uses: ./.github/workflows/check.yaml 51 | needs: [warmup-caches] 52 | test: 53 | uses: ./.github/workflows/test.yaml 54 | secrets: inherit 55 | needs: [warmup-caches] 56 | e2e-test: 57 | uses: ./.github/workflows/e2e-test.yaml 58 | # doesn't need to wait for the cache since creating the astarte cluster takes longer 59 | needs: [reuse, dco] 60 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yaml: -------------------------------------------------------------------------------- 1 | # This file is part of Edgehog. 2 | # 3 | # Copyright 2024 - 2025 SECO Mind Srl 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # SPDX-License-Identifier: Apache-2.0 18 | 19 | name: coverage 20 | defaults: 21 | run: 22 | shell: bash 23 | on: 24 | workflow_run: 25 | workflows: ["ci"] 26 | types: [completed] 27 | permissions: 28 | contents: read 29 | actions: read 30 | jobs: 31 | upload: 32 | runs-on: ubuntu-24.04 33 | # Run only if originated from a PR 34 | if: ${{ github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' }} 35 | steps: 36 | - uses: actions/checkout@v4 37 | with: 38 | ref: ${{ github.event.workflow_run.head_sha }} 39 | # Checkout codecov.yaml config from master 40 | - uses: actions/checkout@v4 41 | with: 42 | path: master 43 | sparse-checkout: | 44 | .github/codecov.yaml 45 | sparse-checkout-cone-mode: false 46 | - name: Download coverage artifact 47 | uses: actions/download-artifact@v4 48 | with: 49 | name: coverage 50 | github-token: ${{ github.token }} 51 | run-id: ${{ github.event.workflow_run.id }} 52 | - name: Get PR number 53 | run: | 54 | echo "PR_NUMBER=$(cat ./pr_number)" >> "$GITHUB_ENV" 55 | - name: Upload to codecov.io 56 | uses: codecov/codecov-action@v5 57 | with: 58 | codecov_yml_path: master/.github/codecov.yaml 59 | token: ${{secrets.CODECOV_TOKEN}} 60 | fail_ci_if_error: true 61 | override_branch: ${{ github.event.workflow_run.head_branch }} 62 | override_commit: ${{ github.event.workflow_run.head_sha }} 63 | override_pr: ${{ env.PR_NUMBER }} 64 | -------------------------------------------------------------------------------- /.github/workflows/dco.yaml: -------------------------------------------------------------------------------- 1 | # This file is part of Edgehog. 2 | # 3 | # Copyright 2024 - 2025 SECO Mind Srl 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # SPDX-License-Identifier: Apache-2.0 18 | 19 | name: dco 20 | on: 21 | workflow_dispatch: 22 | inputs: 23 | pr: 24 | type: string 25 | description: Number of the PR that triggered the job 26 | workflow_call: 27 | inputs: 28 | pr: 29 | type: string 30 | permissions: 31 | actions: read 32 | pull-requests: read 33 | env: 34 | GH_TOKEN: ${{ github.token }} 35 | jobs: 36 | check: 37 | runs-on: ubuntu-24.04 38 | steps: 39 | - run: echo 'Checking DCO status for PR \#'${{ inputs.pr }}'' 40 | - name: check DCO passed 41 | # Check the DCO only for PRs 42 | if: inputs.pr != '' 43 | shell: bash 44 | # Use the gh to view the status checks on the PR, find the one named DCO and check that the 45 | # conclusion is "SUCCESS" and not something like "ACTION_REQUIRED" 46 | run: | 47 | DCO="$(gh pr view '${{ inputs.pr }}' --json statusCheckRollup \ 48 | --jq '.statusCheckRollup.[] | select ( .name == "DCO" ) | .conclusion ' \ 49 | --repo ${{ github.repository }} 50 | )" 51 | echo "DCO status is '$DCO'" 52 | test "$DCO" == "SUCCESS" 53 | -------------------------------------------------------------------------------- /.github/workflows/e2e-test.yaml: -------------------------------------------------------------------------------- 1 | # This file is part of Edgehog. 2 | # 3 | # Copyright 2022 - 2025 SECO Mind Srl 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # SPDX-License-Identifier: Apache-2.0 18 | 19 | name: e2e-test 20 | on: 21 | workflow_call: 22 | workflow_dispatch: 23 | permissions: 24 | contents: read 25 | env: 26 | CARGO_TERM_COLOR: always 27 | SCCACHE_GHA_ENABLED: "true" 28 | RUSTC_WRAPPER: "sccache" 29 | RUST_LOG: "debug" 30 | E2E_REALM_NAME: "test" 31 | ASTARTE_API_URL: "https://api.autotest.astarte-platform.org" 32 | E2E_ASTARTE_API_URL: "https://api.autotest.astarte-platform.org" 33 | E2E_INTERFACE_DIR: "./edgehog/astarte-interfaces" 34 | jobs: 35 | e2e-test: 36 | runs-on: ubuntu-24.04 37 | steps: 38 | - name: Create Astarte Cluster 39 | id: astarte 40 | uses: astarte-platform/astarte-cluster-action@v1.2.0 41 | - uses: actions/checkout@v4 42 | - name: Checkout edgehog-astarte-interfaces 43 | uses: actions/checkout@v4 44 | with: 45 | repository: edgehog-device-manager/edgehog-astarte-interfaces.git 46 | # Update ref when updated interfaces are required. 47 | ref: v0.5.2 48 | path: ./edgehog/astarte-interfaces 49 | - name: Install system dependencies 50 | run: | 51 | sudo apt update 52 | sudo apt-get -y install libsqlite3-dev libssl-dev libudev-dev libsystemd-dev upower 53 | - name: Install interface 54 | run: | 55 | astartectl realm-management interfaces sync $GITHUB_WORKSPACE/edgehog/astarte-interfaces/*.json --non-interactive 56 | astartectl realm-management interfaces ls 57 | - name: Register device 58 | run: | 59 | DEVICE_ID=$(astartectl utils device-id generate-random) 60 | echo "E2E_DEVICE_ID=$DEVICE_ID" >> $GITHUB_ENV 61 | CREDENTIALS_SECRET=$(astartectl pairing agent register --compact-output -- "$DEVICE_ID") 62 | echo "E2E_CREDENTIALS_SECRET=$CREDENTIALS_SECRET" >> $GITHUB_ENV 63 | TOKEN=$(astartectl utils gen-jwt appengine) 64 | echo "E2E_TOKEN=$TOKEN" >> $GITHUB_ENV 65 | - name: Install stable 66 | uses: dtolnay/rust-toolchain@stable 67 | - name: Install sccache-cache 68 | uses: mozilla-actions/sccache-action@v0.0.9 69 | - name: Run test 70 | # use the full command to use the sccache 71 | run: | 72 | cargo run -p e2e-test 73 | -------------------------------------------------------------------------------- /.github/workflows/reuse-lint.yaml: -------------------------------------------------------------------------------- 1 | # This file is part of Edgehog. 2 | # 3 | # Copyright 2025 SECO Mind Srl 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # SPDX-License-Identifier: Apache-2.0 18 | 19 | name: REUSE Compliance Check 20 | on: 21 | workflow_call: 22 | workflow_dispatch: 23 | permissions: 24 | contents: read 25 | jobs: 26 | test: 27 | runs-on: ubuntu-24.04 28 | steps: 29 | - uses: actions/checkout@v4 30 | - name: REUSE Compliance Check 31 | uses: fsfe/reuse-action@v5 32 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | # This file is part of Edgehog. 2 | # 3 | # Copyright 2023 - 2025 SECO Mind Srl 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # SPDX-License-Identifier: Apache-2.0 18 | 19 | name: test 20 | on: 21 | workflow_call: 22 | workflow_dispatch: 23 | permissions: 24 | contents: read 25 | env: 26 | CARGO_TERM_COLOR: always 27 | SCCACHE_GHA_ENABLED: "true" 28 | RUSTC_WRAPPER: "sccache" 29 | # Enable logging otherwise the logging lines will count as not covered in the test coverage 30 | RUST_LOG: trace 31 | defaults: 32 | run: 33 | shell: bash 34 | jobs: 35 | required: 36 | runs-on: ubuntu-24.04 37 | name: ubuntu / ${{ matrix.toolchain }} 38 | strategy: 39 | matrix: 40 | toolchain: [stable, beta] 41 | steps: 42 | - uses: actions/checkout@v4 43 | - name: Install system dependencies 44 | run: | 45 | sudo apt update 46 | sudo apt-get install -y libsqlite3-dev libssl-dev libudev-dev libsystemd-dev upower 47 | - name: Install ${{ matrix.toolchain }} 48 | uses: dtolnay/rust-toolchain@master 49 | with: 50 | toolchain: ${{ matrix.toolchain }} 51 | - name: Install sccache-cache 52 | uses: mozilla-actions/sccache-action@v0.0.9 53 | - name: cargo generate-lockfile 54 | if: hashFiles('Cargo.lock') == '' 55 | run: cargo generate-lockfile 56 | - name: Run cargo test --locked 57 | run: cargo test --locked --all-features --workspace 58 | # https://github.com/rust-lang/cargo/issues/6669 59 | - name: Run cargo test --doc 60 | run: cargo test --locked --all-features --doc --workspace 61 | coverage: 62 | runs-on: ubuntu-24.04 63 | name: ubuntu / nightly / coverage 64 | steps: 65 | - uses: actions/checkout@v4 66 | - name: Install system dependencies 67 | run: | 68 | sudo apt update 69 | sudo apt-get install -y libsqlite3-dev libssl-dev libudev-dev libsystemd-dev upower 70 | - name: Install nightly 71 | uses: dtolnay/rust-toolchain@nightly 72 | with: 73 | components: llvm-tools 74 | - name: Install sccache-cache 75 | uses: mozilla-actions/sccache-action@v0.0.9 76 | - name: Install grcov 77 | uses: taiki-e/install-action@grcov 78 | - name: cargo generate-lockfile 79 | if: hashFiles('Cargo.lock') == '' 80 | run: cargo generate-lockfile 81 | - name: Generate coverage 82 | run: | 83 | EXPORT_FOR_CI=1 ./scripts/coverage.sh 84 | # Upload the coverage if we are not a PR from a fork, see ".github/workflows/coverage.yaml" 85 | - name: Upload to codecov.io 86 | if: ${{ github.event_name == 'push' }} 87 | uses: codecov/codecov-action@v5 88 | with: 89 | token: ${{secrets.CODECOV_TOKEN}} 90 | fail_ci_if_error: true 91 | # Save data to use in workflow_run 92 | - name: Save PR number 93 | if: ${{ github.event_name == 'pull_request' }} 94 | env: 95 | PR_NUMBER: ${{ github.event.number }} 96 | run: | 97 | echo "$PR_NUMBER" > ./pr_number 98 | - name: Upload coverage artifact 99 | if: ${{ github.event_name == 'pull_request' }} 100 | uses: actions/upload-artifact@v4 101 | with: 102 | name: coverage 103 | path: | 104 | pr_number 105 | coverage-edgehog-device-runtime.info 106 | coverage-edgehog-device-runtime-containers.info 107 | coverage-edgehog-device-runtime-forwarder.info 108 | -------------------------------------------------------------------------------- /.github/workflows/warmup-caches.yaml: -------------------------------------------------------------------------------- 1 | # This file is part of Edgehog. 2 | # 3 | # Copyright 2025 SECO Mind Srl 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # SPDX-License-Identifier: Apache-2.0 18 | 19 | name: warmup-caches 20 | on: 21 | workflow_call: 22 | workflow_dispatch: 23 | permissions: 24 | contents: read 25 | env: 26 | CARGO_TERM_COLOR: always 27 | SCCACHE_GHA_ENABLED: "true" 28 | RUSTC_WRAPPER: "sccache" 29 | jobs: 30 | build: 31 | runs-on: ubuntu-24.04 32 | strategy: 33 | matrix: 34 | toolchain: [stable, beta, nightly] 35 | name: ${{ matrix.toolchain }} / build 36 | steps: 37 | - uses: actions/checkout@v4 38 | - name: Install system dependencies 39 | run: | 40 | sudo apt update 41 | sudo apt-get -y install libsqlite3-dev libssl-dev libudev-dev libsystemd-dev upower 42 | - name: Install stable 43 | uses: dtolnay/rust-toolchain@master 44 | with: 45 | toolchain: ${{ matrix.toolchain }} 46 | - name: Install sccache-cache 47 | uses: mozilla-actions/sccache-action@v0.0.9 48 | - name: Build with all features 49 | run: cargo build --all-targets --all-features --workspace 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Copyright 2022 SECO Mind Srl 2 | # 3 | # SPDX-License-Identifier: CC0-1.0 4 | 5 | debug/ 6 | target/ 7 | 8 | # These are backup files generated by rustfmt 9 | **/*.rs.bk 10 | 11 | # MSVC Windows builds of rustc generate these, which store debugging information 12 | *.pdb 13 | 14 | # RustRover IDE 15 | .idea/ 16 | 17 | /telemetry.json 18 | 19 | # GitHub actions checks before committing 20 | /.pre-commit-config.yaml 21 | 22 | # Local coverage file 23 | tarpaulin-report.html 24 | -------------------------------------------------------------------------------- /.reuse/templates/apache-2.jinja2: -------------------------------------------------------------------------------- 1 | This file is part of Edgehog. 2 | 3 | {% for copyright_line in copyright_lines %} 4 | {{ copyright_line }} 5 | {% endfor %} 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | 19 | {% for expression in spdx_expressions %} 20 | SPDX-License-Identifier: {{ expression }} 21 | {% endfor %} 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project 6 | adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [0.10.0] - Unreleased 9 | 10 | ### Changed 11 | 12 | - Update the MSRV to rust 1.78 and do a major version bump 13 | 14 | ## [0.9.0] - 2025-03-11 15 | 16 | ### Added 17 | 18 | - Add support for the `edgehog-device-runtime-containers` [#504] 19 | 20 | ### Changed 21 | 22 | - Update OS requirements to specify ttyd minimum version 23 | - Update the astarte-device-sdk to v0.9.6 [#504] 24 | - Add support for the 25 | [`CellularConnectionProperties`](https://github.com/edgehog-device-manager/edgehog-astarte-interfaces/blob/ed3b0a413a3d5586267d88d10f85c310584cb80b/io.edgehog.devicemanager.CellularConnectionProperties.json) 26 | via the D-Bus service `CellularModems` 27 | [#402](https://github.com/edgehog-device-manager/edgehog-device-runtime/pull/402) 28 | 29 | [#504]: https://github.com/edgehog-device-manager/edgehog-device-runtime/pull/504 30 | 31 | [0.8.3] - 2025-02-28 32 | 33 | ### Changed 34 | 35 | - Bump the `astarte-device-sdk-rust` version to `v0.8.5`. 36 | 37 | ## [0.8.2] - 2025-02-27 38 | 39 | ### Changed 40 | 41 | - Bump the `astarte-device-sdk-rust` version to `v0.8.4`. 42 | [#493](https://github.com/edgehog-device-manager/edgehog-device-runtime/pull/493) 43 | 44 | ## [0.8.1] - 2024-06-10 45 | 46 | ### Changed 47 | 48 | - Substitute alpha version with 0.1.0 of edgehog-device-forwarder-proto dependency 49 | 50 | ## [0.7.2] - 2024-05-28 51 | 52 | ### Fixed 53 | 54 | - Update sdk dependency to fix a purge property bug 55 | [#341](https://github.com/astarte-platform/astarte-device-sdk-rust/issues/341) 56 | 57 | ## [0.8.0] - 2024-03-25 58 | 59 | ### Added 60 | 61 | - Add support for `io.edgehog.devicemanager.ForwarderSessionRequest` interface 62 | - Add support for `io.edgehog.devicemanager.ForwarderSessionState` interface 63 | - Add remote terminal support 64 | 65 | ### Changed 66 | 67 | - Update the MSRV to rust 1.72.0 68 | 69 | ## [0.7.1] - 2023-07-03 70 | 71 | ### Added 72 | 73 | - Add Astarte Message Hub library support. 74 | 75 | ## [0.7.0] - 2023-06-05 76 | 77 | ### Added 78 | 79 | - Add support for `io.edgehog.devicemanager.OTAEvent` interface. 80 | - Add support for update/cancel operation in `io.edgehog.devicemanager.OTARequest` interface. 81 | 82 | ### Removed 83 | 84 | - Remove support for `io.edgehog.devicemanager.OTAResponse` interface. 85 | 86 | ## [0.6.0] - 2023-02-10 87 | 88 | ### Changed 89 | 90 | - Update Astarte Device SDK to 0.5.1 release. 91 | 92 | ## [0.5.0] - 2022-10-10 93 | 94 | ### Added 95 | 96 | - Initial Edgehog Device Runtime release. 97 | -------------------------------------------------------------------------------- /Cargo.lock.license: -------------------------------------------------------------------------------- 1 | This file is part of Edgehog. 2 | 3 | Copyright 2022 - 2025 SECO Mind Srl 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | SPDX-License-Identifier: CC0-1.0 18 | -------------------------------------------------------------------------------- /REUSE.toml: -------------------------------------------------------------------------------- 1 | # This file is part of Edgehog. 2 | # 3 | # Copyright 2024 SECO Mind Srl 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # SPDX-License-Identifier: Apache-2.0 18 | 19 | version = 1 20 | SPDX-PackageName = "edgehog-device-runtime" 21 | SPDX-PackageDownloadLocation = "https://github.com/edgehog-device-manager/edgehog-device-runtime" 22 | 23 | [[annotations]] 24 | path = ["CHANGELOG.md", "edgehog-device-runtime-containers/CHANGELOG.md"] 25 | precedence = "aggregate" 26 | SPDX-FileCopyrightText = "2022-2024 SECO Mind Srl" 27 | SPDX-License-Identifier = "CC0-1.0" 28 | 29 | [[annotations]] 30 | path = ["e2e-test-containers/assets/*.json"] 31 | precedence = "aggregate" 32 | SPDX-FileCopyrightText = "2024 SECO Mind Srl" 33 | SPDX-License-Identifier = "CC0-1.0" 34 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # Security Policy 8 | 9 | ## Supported Versions 10 | 11 | | Version | Supported | 12 | | ------- | ------------------ | 13 | | < 0.5 | :x: | 14 | | 0.5.x | :white_check_mark: | 15 | 16 | ## Reporting a Vulnerability 17 | 18 | To submit a vulnerability report, please contact us at security AT secomind.com. Please, **do not 19 | use GitHub issues for vulnerability reports**. Your submission will be promptly reviewed and 20 | validated by a member of our team. 21 | -------------------------------------------------------------------------------- /assets/example.toml: -------------------------------------------------------------------------------- 1 | # This file is part of Edgehog. 2 | # 3 | # Copyright 2024 SECO Mind Srl 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # SPDX-License-Identifier: Apache-2.0 18 | 19 | [[telemetry_config]] 20 | interface_name = "io.edgehog.devicemanager.SystemStatus" 21 | enabled = true 22 | period = 60 23 | 24 | [[telemetry_config]] 25 | interface_name = "io.edgehog.devicemanager.StorageUsage" 26 | enabled = true 27 | period = 60 28 | 29 | [[telemetry_config]] 30 | interface_name = "io.edgehog.devicemanager.BatteryStatus" 31 | enabled = true 32 | period = 60 33 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | // This file is part of Edgehog. 2 | // 3 | // Copyright 2024 SECO Mind Srl 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | // SPDX-License-Identifier: Apache-2.0 18 | 19 | fn main() { 20 | // Export the src/telemetry/runtime_info.rs RUSTC version 21 | let version = rustc_version::version().expect("couldn't read RUSTC version"); 22 | 23 | println!("cargo:rustc-env=EDGEHOG_RUSTC_VERSION={version}"); 24 | } 25 | -------------------------------------------------------------------------------- /cellular-modems-service/Cargo.toml: -------------------------------------------------------------------------------- 1 | # This file is part of Edgehog. 2 | # 3 | # Copyright 2024 SECO Mind Srl 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # SPDX-License-Identifier: Apache-2.0 18 | 19 | [package] 20 | name = "cellular-modems-service" 21 | version.workspace = true 22 | edition.workspace = true 23 | homepage.workspace = true 24 | publish = false 25 | rust-version.workspace = true 26 | 27 | [dependencies] 28 | stable-eyre = { workspace = true } 29 | tokio = { workspace = true, features = ["macros", "rt-multi-thread", "signal"] } 30 | tracing = { workspace = true } 31 | tracing-subscriber = { workspace = true, features = ["env-filter"] } 32 | zbus = { workspace = true, default-features = false, features = ["tokio"] } 33 | -------------------------------------------------------------------------------- /cellular-modems-service/README.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | # Cellular Properties dbus Service Example 7 | 8 | Reference project of a service that expose APIs to add and retrieve cellular modems data. 9 | 10 | # Setup 11 | 12 | Build with cargo 13 | 14 | ```bash 15 | cargo build --release 16 | ``` 17 | 18 | Run 19 | 20 | ```bash 21 | cargo run 22 | ``` 23 | 24 | # Usage 25 | 26 | **Service name**: `io.edgehog.CellularModems` 27 | 28 | **Service path**: `/io/edgehog/CellularModems` 29 | 30 | **Service interface**: `io.edgehog.CellularModems1` 31 | 32 | ## Methods 33 | 34 | ``` 35 | Insert ( IN String modem_id 36 | IN String apn 37 | IN String imei 38 | IN String imsi ); 39 | List (OUT Array modem_ids); 40 | Get ( IN String modem_id 41 | OUT Dict modem_properties ) 42 | ``` 43 | 44 | ### Insert 45 | 46 | Add a new modem dictionary. 47 | 48 | ```bash 49 | $ dbus-send --print-reply --dest=io.edgehog.CellularModems /io/edgehog/CellularModems io.edgehog.CellularModems1.Insert \ 50 | string:"1" string:"apn.cxn" string:"100474636527166" string:"310170845466094" 51 | ``` 52 | 53 | ### List 54 | 55 | List all available modems 56 | 57 | ```bash 58 | $ dbus-send --print-reply --dest=io.edgehog.CellularModems /io/edgehog/CellularModems io.edgehog.CellularModems1.List 59 | 60 | method return time=1663161698.312568 sender=:1.1 -> destination=:1.12 serial=20 reply_serial=2 61 | array [ 62 | string "1" 63 | ] 64 | ``` 65 | 66 | ### Get 67 | 68 | Get a modem dictionary 69 | 70 | ```bash 71 | $ dbus-send --print-reply --dest=io.edgehog.CellularModems /io/edgehog/CellularModems io.edgehog.CellularModems1.Get string:1 72 | 73 | method return time=1663161753.656539 sender=:1.1 -> destination=:1.13 serial=21 reply_serial=2 74 | array [ 75 | dict entry( 76 | string "apn" 77 | variant string "apn.cxn" 78 | ) 79 | dict entry( 80 | string "imei" 81 | variant string "100474636527166" 82 | ) 83 | dict entry( 84 | string "imsi" 85 | variant string "310170845466094" 86 | ) 87 | ] 88 | ``` 89 | -------------------------------------------------------------------------------- /cellular-modems-service/scripts/get.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # This file is part of Edgehog. 3 | # 4 | # Copyright 2024 SECO Mind Srl 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | 20 | set -exEuo pipefail 21 | 22 | dbus-send --print-reply --dest=io.edgehog.CellularModems \ 23 | /io/edgehog/CellularModems io.edgehog.CellularModems1.Get \ 24 | string:1 25 | -------------------------------------------------------------------------------- /cellular-modems-service/scripts/insert.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # This file is part of Edgehog. 3 | # 4 | # Copyright 2024 SECO Mind Srl 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | 20 | set -exEuo pipefail 21 | 22 | dbus-send --print-reply --dest=io.edgehog.CellularModems \ 23 | /io/edgehog/CellularModems io.edgehog.CellularModems1.Insert \ 24 | string:"1" string:"apn.cxn" string:"100474636527166" string:"310170845466094" 25 | -------------------------------------------------------------------------------- /cellular-modems-service/scripts/list.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # This file is part of Edgehog. 3 | # 4 | # Copyright 2024 SECO Mind Srl 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | 20 | set -exEuo pipefail 21 | 22 | dbus-send --print-reply --dest=io.edgehog.CellularModems \ 23 | /io/edgehog/CellularModems io.edgehog.CellularModems1.List 24 | -------------------------------------------------------------------------------- /cellular-modems-service/src/main.rs: -------------------------------------------------------------------------------- 1 | // This file is part of Edgehog. 2 | // 3 | // Copyright 2024 SECO Mind Srl 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | // SPDX-License-Identifier: Apache-2.0 18 | 19 | use std::collections::HashMap; 20 | 21 | use tracing::{debug, info, level_filters::LevelFilter}; 22 | use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; 23 | use zbus::{ 24 | interface, 25 | zvariant::{DeserializeDict, SerializeDict, Type}, 26 | }; 27 | 28 | pub const SERVICE_NAME: &str = "io.edgehog.CellularModems1"; 29 | 30 | #[derive(Debug, Default, Clone, DeserializeDict, SerializeDict, Type)] 31 | #[zvariant(signature = "dict")] 32 | struct ModemProperties { 33 | apn: String, 34 | imei: String, 35 | imsi: String, 36 | } 37 | 38 | struct CellularModems { 39 | modems: HashMap, 40 | } 41 | 42 | #[interface(name = "io.edgehog.CellularModems1")] 43 | impl CellularModems { 44 | fn list(&self) -> Vec { 45 | self.modems.keys().cloned().collect() 46 | } 47 | 48 | fn get(&self, id: String) -> ModemProperties { 49 | self.modems.get(&id).cloned().unwrap_or_else(|| { 50 | debug!("modem {id} not found"); 51 | 52 | ModemProperties::default() 53 | }) 54 | } 55 | 56 | fn insert(&mut self, id: String, apn: String, imei: String, imsi: String) { 57 | info!("inserting modem {id}"); 58 | 59 | self.modems.insert(id, ModemProperties { apn, imei, imsi }); 60 | } 61 | } 62 | 63 | #[tokio::main] 64 | async fn main() -> stable_eyre::Result<()> { 65 | stable_eyre::install()?; 66 | tracing_subscriber::registry() 67 | .with(tracing_subscriber::fmt::layer()) 68 | .with( 69 | EnvFilter::builder() 70 | .with_default_directive(LevelFilter::INFO.into()) 71 | .from_env_lossy(), 72 | ) 73 | .try_init()?; 74 | 75 | let cellular_modems = CellularModems { 76 | modems: HashMap::new(), 77 | }; 78 | 79 | let _conn = zbus::connection::Builder::session()? 80 | .name(SERVICE_NAME)? 81 | .serve_at("/io/edgehog/CellularModems", cellular_modems)? 82 | .build() 83 | .await?; 84 | 85 | info!("Service {SERVICE_NAME} started"); 86 | 87 | tokio::signal::ctrl_c().await?; 88 | 89 | Ok(()) 90 | } 91 | -------------------------------------------------------------------------------- /doc/os_requirements.md: -------------------------------------------------------------------------------- 1 | 20 | 21 | # OS Requirements 22 | 23 | Edgehog Device Runtime has a number of requirements in order to provide device management features. 24 | 25 | ## Linux 26 | 27 | ### Dependencies 28 | 29 | - **Rust** >= 1.78 30 | - **libsystemd** (optional) 31 | - **libudev**: Gathering information about network interfaces. 32 | - **ttyd** >= 1.7.4 33 | 34 | ### Runtime Dependencies 35 | 36 | - **[dbus](https://www.freedesktop.org/wiki/Software/dbus/)** (optional): Needed for communicating 37 | with 3rd party services, such as RAUC. 38 | - **[RAUC](https://rauc.io/) ~> v1.5** (optional): Needed for OS updates. 39 | - **[UPower](https://upower.freedesktop.org/)**: (optional) Needed to gather information about the 40 | battery status. 41 | 42 | ### Filesystem Layout 43 | 44 | - **/tmp**: Software updates will be downloaded here. 45 | - **/data**: Edgehog Device Runtime will store its state here during the OTA update process. 46 | - **[/etc/os-release](https://www.freedesktop.org/software/systemd/man/os-release.html)**: NAME, 47 | VERSION_ID, BUILD_ID, IMAGE_ID, IMAGE_VERSION entries are used for OSInfo and BaseImage. 48 | 49 | ### Optional features 50 | 51 | #### Systemd 52 | 53 | If `edgehog-device-runtime` is a `systemd` service, it can notify `systemd` of its status changes. 54 | This is provided via the `rust-systemd` crate, a Rust interface to `libsystemd/libelogind` APIs. To 55 | build the `runtime` make sure you have `libsystemd-dev` installed on your system and the systemd 56 | feature enabled. 57 | 58 | ```sh 59 | cargo build --features systemd 60 | ``` 61 | 62 | To allow systemd to receive status changes from service, you need to set the 63 | [NotifyAccess](https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html#NotifyAccess=) option to 64 | either `exec` or `main`. 65 | 66 | ``` 67 | [Service] 68 | ... 69 | NotifyAccess=exec 70 | ``` 71 | 72 | #### Forwarder 73 | 74 | The forwarder requires [ttyd](https://github.com/tsl0922/ttyd) for sharing terminal over the web. 75 | 76 | To provide the remote terminal functionality, the program must run on the default port `7681` with 77 | the flag `-W` enabled, which enables the write-mode on the remote terminal. To start ttyd locally 78 | run the command `ttyd -W bash` (the `-W` option is only available for ttyd >= 1.7.4). Note: It is 79 | possible to specify a different kind of shell, depending on the ones installed. To build the 80 | `runtime` make sure you have ttyd installed on your system and the forwarder feature enabled. 81 | 82 | ```sh 83 | cargo build --features forwarder 84 | ``` 85 | 86 | #### Containers 87 | 88 | To enable the container service you'll need to enable the `containers` features and a container 89 | runtime with a [Docker compatible REST API](https://docs.docker.com/reference/api/engine/). 90 | 91 | To specify the host where the container you need to export the `DOCKER_HOST` environment variable, 92 | pointing to either a Docker API Endpoint or a unix domain socket. 93 | -------------------------------------------------------------------------------- /e2e-test-containers/Cargo.toml: -------------------------------------------------------------------------------- 1 | # This file is part of Edgehog. 2 | # 3 | # Copyright 2023 - 2025 SECO Mind Srl 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # SPDX-License-Identifier: Apache-2.0 18 | 19 | [package] 20 | name = "e2e-test-containers" 21 | version = "0.1.0" 22 | edition = "2021" 23 | publish = false 24 | 25 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 26 | 27 | [dependencies] 28 | astarte-device-sdk = { workspace = true } 29 | clap = { workspace = true, features = ["derive", "env"] } 30 | color-eyre = { workspace = true } 31 | edgehog-containers = { workspace = true } 32 | edgehog-store = { workspace = true } 33 | reqwest = { workspace = true, features = ["rustls-tls-native-roots-no-provider"] } 34 | serde = { workspace = true, features = ["derive"] } 35 | serde_json = { workspace = true } 36 | tempfile = { workspace = true } 37 | tokio = { workspace = true, features = ["macros", "signal"] } 38 | tracing = { workspace = true } 39 | tracing-subscriber = { workspace = true, features = ["env-filter"] } 40 | uuid = { workspace = true, features = ["v4", "fast-rng"] } 41 | -------------------------------------------------------------------------------- /e2e-test-containers/README.md: -------------------------------------------------------------------------------- 1 | 20 | 21 | # e2e-test-containers 22 | 23 | Tests the container service by sending and receiving the container interfaces. 24 | 25 | ## Config 26 | 27 | To test run `cargo e2e-test-containers` with the appropriate flags. 28 | 29 | ``` 30 | $ cargo e2e-test-containers -h 31 | Usage: e2e-test-containers --realm --device-id --credentials-secret --pairing-url --interfaces-dir --store-dir 32 | 33 | Commands: 34 | send Send the data to astarte 35 | receive 36 | help Print this message or the help of the given subcommand(s) 37 | 38 | Options: 39 | --realm 40 | Realm of the device [env: ASTARTE_REALM=] 41 | --device-id 42 | Astarte device id [env: ASTARTE_DEVICE_ID=2TBn-jNESuuHamE2Zo1anA] 43 | --credentials-secret 44 | Credential secret [env: ASTARTE_CREDENTIALS_SECRET=3pBM0vTYVZbD3B4QaORgCi2yBvFNCbBXbt+sqcjsXac=] 45 | --pairing-url 46 | Astarte pairing url [env: ASTARTE_PAIRING_URL=http://api.astarte.localhost/pairing] 47 | --interfaces-dir 48 | Astarte interfaces directory [env: ASTARTE_INTERFACES_DIR=] 49 | --store-dir 50 | Astarte storage directory [env: ASTARTE_STORE_DIR=] 51 | -h, --help 52 | Print help 53 | ``` 54 | -------------------------------------------------------------------------------- /e2e-test-containers/assets/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "events": [ 3 | { 4 | "interface": "io.edgehog.devicemanager.apps.CreateImageRequest", 5 | "path": "/image", 6 | "data": { 7 | "id": "4f3279aa-b8ba-45ca-bc1c-57b7adb9adeb", 8 | "deploymentId": "6a9b6d3c-4894-4fd0-af1d-44b326282c19", 9 | "reference": "docker.io/library/nginx:stable-alpine-slim", 10 | "registryAuth": "" 11 | } 12 | }, 13 | { 14 | "interface": "io.edgehog.devicemanager.apps.CreateContainerRequest", 15 | "path": "/container", 16 | "data": { 17 | "id": "03aba87c-1ebe-45e9-ab8c-c4eb89752af9", 18 | "deploymentId": "6a9b6d3c-4894-4fd0-af1d-44b326282c19", 19 | "imageId": "4f3279aa-b8ba-45ca-bc1c-57b7adb9adeb", 20 | "networkIds": [], 21 | "volumeIds": [], 22 | "hostname": "", 23 | "restartPolicy": "", 24 | "env": [], 25 | "binds": [], 26 | "portBindings": ["9000:80"], 27 | "networkMode": "bridge", 28 | "privileged": false 29 | } 30 | }, 31 | { 32 | "interface": "io.edgehog.devicemanager.apps.CreateDeploymentRequest", 33 | "path": "/deployment", 34 | "data": { 35 | "id": "6a9b6d3c-4894-4fd0-af1d-44b326282c19", 36 | "containers": ["03aba87c-1ebe-45e9-ab8c-c4eb89752af9"] 37 | } 38 | }, 39 | { 40 | "interface": "io.edgehog.devicemanager.apps.DeploymentCommand", 41 | "path": "/6a9b6d3c-4894-4fd0-af1d-44b326282c19/command", 42 | "data": "Start" 43 | } 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /e2e-test-containers/assets/delete.json: -------------------------------------------------------------------------------- 1 | { 2 | "events": [ 3 | { 4 | "interface": "io.edgehog.devicemanager.apps.DeploymentCommand", 5 | "path": "/6a9b6d3c-4894-4fd0-af1d-44b326282c19/command", 6 | "data": "Delete" 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /e2e-test-containers/assets/isolate-no-net.json: -------------------------------------------------------------------------------- 1 | { 2 | "events": [ 3 | { 4 | "interface": "io.edgehog.devicemanager.apps.CreateImageRequest", 5 | "path": "/image", 6 | "data": { 7 | "id": "4f3279aa-b8ba-45ca-bc1c-57b7adb9adeb", 8 | "deploymentId": "6a9b6d3c-4894-4fd0-af1d-44b326282c19", 9 | "reference": "docker.io/library/nginx:stable-alpine-slim", 10 | "registryAuth": "" 11 | } 12 | }, 13 | { 14 | "interface": "io.edgehog.devicemanager.apps.CreateNetworkRequest", 15 | "path": "/network", 16 | "data": { 17 | "id": "78549b2b-3888-4976-8bfe-236afab0f91e", 18 | "deploymentId": "6a9b6d3c-4894-4fd0-af1d-44b326282c19", 19 | "driver": "bridge", 20 | "checkDuplicate": false, 21 | "internal": true, 22 | "enableIpv6": false, 23 | "options": ["isolate=true"] 24 | } 25 | }, 26 | { 27 | "interface": "io.edgehog.devicemanager.apps.CreateContainerRequest", 28 | "path": "/container", 29 | "data": { 30 | "id": "03aba87c-1ebe-45e9-ab8c-c4eb89752af9", 31 | "deploymentId": "6a9b6d3c-4894-4fd0-af1d-44b326282c19", 32 | "imageId": "4f3279aa-b8ba-45ca-bc1c-57b7adb9adeb", 33 | "networkIds": ["78549b2b-3888-4976-8bfe-236afab0f91e"], 34 | "volumeIds": [], 35 | "hostname": "", 36 | "restartPolicy": "", 37 | "env": [], 38 | "binds": [], 39 | "networkMode": "bridge", 40 | "portBindings": ["9000:80"], 41 | "privileged": false 42 | } 43 | }, 44 | { 45 | "interface": "io.edgehog.devicemanager.apps.CreateDeploymentRequest", 46 | "path": "/deployment", 47 | "data": { 48 | "id": "6a9b6d3c-4894-4fd0-af1d-44b326282c19", 49 | "containers": ["03aba87c-1ebe-45e9-ab8c-c4eb89752af9"] 50 | } 51 | }, 52 | { 53 | "interface": "io.edgehog.devicemanager.apps.DeploymentCommand", 54 | "path": "/6a9b6d3c-4894-4fd0-af1d-44b326282c19/command", 55 | "data": "Start" 56 | } 57 | ] 58 | } 59 | -------------------------------------------------------------------------------- /e2e-test-containers/assets/rollback.json: -------------------------------------------------------------------------------- 1 | { 2 | "events": [ 3 | { 4 | "interface": "io.edgehog.devicemanager.apps.DeploymentUpdate", 5 | "path": "/deployment", 6 | "data": { 7 | "from": "b25dcb96-bafd-41a3-8a4b-4712eb3da81e", 8 | "to": "6a9b6d3c-4894-4fd0-af1d-44b326282c19" 9 | } 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /e2e-test-containers/assets/stop.json: -------------------------------------------------------------------------------- 1 | { 2 | "events": [ 3 | { 4 | "interface": "io.edgehog.devicemanager.apps.DeploymentCommand", 5 | "path": "/6a9b6d3c-4894-4fd0-af1d-44b326282c19/command", 6 | "data": "Stop" 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /e2e-test-containers/assets/update.json: -------------------------------------------------------------------------------- 1 | { 2 | "events": [ 3 | { 4 | "interface": "io.edgehog.devicemanager.apps.CreateImageRequest", 5 | "path": "/image", 6 | "data": { 7 | "id": "fb28e4ff-006d-4688-947a-eb792d750484", 8 | "deploymentId": "b25dcb96-bafd-41a3-8a4b-4712eb3da81e", 9 | "reference": "docker.io/library/httpd:alpine", 10 | "registryAuth": "" 11 | } 12 | }, 13 | { 14 | "interface": "io.edgehog.devicemanager.apps.CreateContainerRequest", 15 | "path": "/container", 16 | "data": { 17 | "id": "88d85d32-f2c0-4210-b0c9-f8f27700be5a", 18 | "deploymentId": "b25dcb96-bafd-41a3-8a4b-4712eb3da81e", 19 | "imageId": "fb28e4ff-006d-4688-947a-eb792d750484", 20 | "networkIds": [], 21 | "volumeIds": [], 22 | "hostname": "", 23 | "restartPolicy": "", 24 | "env": [], 25 | "binds": [], 26 | "portBindings": ["9000:80"], 27 | "networkMode": "bridge", 28 | "privileged": false 29 | } 30 | }, 31 | { 32 | "interface": "io.edgehog.devicemanager.apps.CreateDeploymentRequest", 33 | "path": "/deployment", 34 | "data": { 35 | "id": "b25dcb96-bafd-41a3-8a4b-4712eb3da81e", 36 | "containers": ["88d85d32-f2c0-4210-b0c9-f8f27700be5a"] 37 | } 38 | }, 39 | { 40 | "interface": "io.edgehog.devicemanager.apps.DeploymentUpdate", 41 | "path": "/deployment", 42 | "data": { 43 | "from": "6a9b6d3c-4894-4fd0-af1d-44b326282c19", 44 | "to": "b25dcb96-bafd-41a3-8a4b-4712eb3da81e" 45 | } 46 | } 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /e2e-test-containers/scripts/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # This file is part of Edgehog. 4 | # 5 | # Copyright 2023 SECO Mind Srl 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | # SPDX-License-Identifier: Apache-2.0 20 | set -ex 21 | 22 | curl "$NGINX_HOST" 23 | -------------------------------------------------------------------------------- /e2e-test-containers/scripts/export-env.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # This file is part of Edgehog. 3 | # 4 | # Copyright 2024 SECO Mind Srl 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | 20 | #### 21 | # Wrapper to run a command with the environment variables set to have a device registered with 22 | # astarte. 23 | # 24 | # This scrips need the following environment variables to be set: 25 | # 26 | # - KEY: path to the private key for astarte 27 | # - INTERFACES_DIR: path to the interfaces to sync with astarte 28 | # 29 | # Example: 30 | # 31 | # ./scripts/register-device.sh cargo run --example retention 32 | 33 | set -exEuo pipefail 34 | 35 | if [[ -z $KEY ]]; then 36 | echo "Export the \$KEY environment variable as the path to the private key for astarte" 37 | exit 1 38 | fi 39 | 40 | export RUST_LOG=${RUST_LOG:-debug} 41 | astartectl realm-management interfaces sync -y \ 42 | -u http://api.astarte.localhost \ 43 | -r test \ 44 | -k "$KEY" \ 45 | "$INTERFACES"/*.json 46 | 47 | export ASTARTE_REALM='test' 48 | export ASTARTE_API_URL='http://api.astarte.localhost/appengine' 49 | export ASTARTE_PAIRING_URL='http://api.astarte.localhost/pairing' 50 | export ASTARTE_IGNORE_SSL=true 51 | export ASTARTE_INTERFACES_DIR=$INTERFACES 52 | 53 | ASTARTE_DEVICE_ID="$(astartectl utils device-id generate-random)" 54 | ASTARTE_CREDENTIALS_SECRET="$(astartectl pairing agent register --compact-output -r test -u http://api.astarte.localhost -k "$KEY" -- "$ASTARTE_DEVICE_ID")" 55 | ASTARTE_TOKEN="$(astartectl utils gen-jwt all-realm-apis -u http://api.astarte.localhost -k "$KEY")" 56 | ASTARTE_STORE_DIR="$(mktemp -d)" 57 | 58 | export ASTARTE_DEVICE_ID 59 | export ASTARTE_CREDENTIALS_SECRET 60 | export ASTARTE_TOKEN 61 | export ASTARTE_STORE_DIR 62 | 63 | "$@" 64 | -------------------------------------------------------------------------------- /e2e-test-containers/src/cli.rs: -------------------------------------------------------------------------------- 1 | // This file is part of Edgehog. 2 | // 3 | // Copyright 2024 SECO Mind Srl 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | // SPDX-License-Identifier: Apache-2.0 18 | 19 | use std::path::PathBuf; 20 | 21 | use clap::{Args, Parser, Subcommand}; 22 | 23 | #[derive(Debug, Clone, Parser)] 24 | pub struct Cli { 25 | #[command(flatten)] 26 | pub astarte: AstarteConfig, 27 | 28 | #[command(subcommand)] 29 | pub command: Command, 30 | } 31 | 32 | #[derive(Debug, Clone, Subcommand)] 33 | pub enum Command { 34 | /// Send the data to astarte. 35 | Send { 36 | /// Token with access to app engine API to send data to Astarte. 37 | #[arg(long, env = "ASTARTE_TOKEN")] 38 | token: String, 39 | /// Specify the App engine api URLs. 40 | #[arg(long, env = "ASTARTE_API_URL")] 41 | appengine_url: String, 42 | /// Prints the requests as "curl" commands. 43 | #[arg(long, default_value = "false")] 44 | curl: bool, 45 | /// Path to a json file containing the data to send. 46 | data: PathBuf, 47 | }, 48 | Receive, 49 | } 50 | 51 | #[derive(Debug, Clone, Args)] 52 | pub struct AstarteConfig { 53 | /// Realm of the device. 54 | #[arg(long, env = "ASTARTE_REALM")] 55 | pub realm: String, 56 | /// Astarte device id. 57 | #[arg(long, env = "ASTARTE_DEVICE_ID")] 58 | pub device_id: String, 59 | /// Credential secret. 60 | #[arg(long, env = "ASTARTE_CREDENTIALS_SECRET")] 61 | pub credentials_secret: String, 62 | /// Astarte pairing url. 63 | #[arg(long, env = "ASTARTE_PAIRING_URL")] 64 | pub pairing_url: String, 65 | /// Astarte interfaces directory. 66 | #[arg(long, env = "ASTARTE_INTERFACES_DIR")] 67 | pub interfaces_dir: PathBuf, 68 | /// Astarte storage directory. 69 | #[arg(long, env = "ASTARTE_STORE_DIR")] 70 | pub store_dir: PathBuf, 71 | } 72 | -------------------------------------------------------------------------------- /e2e-test-containers/src/receive.rs: -------------------------------------------------------------------------------- 1 | // This file is part of Edgehog. 2 | // 3 | // Copyright 2024 - 2025 SECO Mind Srl 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | // SPDX-License-Identifier: Apache-2.0 18 | 19 | use std::{fmt::Debug, path::Path}; 20 | 21 | use astarte_device_sdk::{Client, FromEvent}; 22 | use color_eyre::eyre::bail; 23 | use edgehog_containers::{ 24 | events::RuntimeListener, 25 | requests::ContainerRequest, 26 | service::{events::ServiceHandle, Service}, 27 | store::StateStore, 28 | Docker, 29 | }; 30 | use edgehog_store::db::Handle; 31 | use tokio::task::JoinSet; 32 | use tracing::{error, info}; 33 | 34 | async fn receive_events(device: D, handle: ServiceHandle) -> color_eyre::Result<()> 35 | where 36 | D: Debug + Client + Send + Sync + 'static, 37 | { 38 | loop { 39 | let event = device.recv().await?; 40 | 41 | match ContainerRequest::from_event(event) { 42 | Ok(req) => { 43 | handle.on_event(req).await.inspect_err(|err| { 44 | error!(error = format!("{:#}", err), "couldn't handle the event"); 45 | })?; 46 | } 47 | Err(err) => { 48 | error!( 49 | error = format!("{:#}", color_eyre::Report::new(err)), 50 | "couldn't parse the event" 51 | ); 52 | 53 | bail!("invalid event received"); 54 | } 55 | } 56 | } 57 | } 58 | 59 | async fn handle_events(mut service: Service) -> color_eyre::Result<()> 60 | where 61 | D: Debug + Client + Clone + Send + Sync + 'static, 62 | { 63 | service.init().await?; 64 | 65 | service.handle_events().await; 66 | 67 | Ok(()) 68 | } 69 | 70 | async fn runtime_listen(mut listener: RuntimeListener) -> color_eyre::Result<()> 71 | where 72 | D: Debug + Client + Clone + Send + Sync + 'static, 73 | { 74 | listener.handle_events().await?; 75 | 76 | Ok(()) 77 | } 78 | 79 | pub async fn receive(device: D, store_path: &Path) -> color_eyre::Result<()> 80 | where 81 | D: Debug + Client + Clone + Send + Sync + 'static, 82 | { 83 | let client = Docker::connect().await?; 84 | 85 | let handle = Handle::open(store_path.join("state.db")).await?; 86 | let store = StateStore::new(handle); 87 | 88 | let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); 89 | 90 | let listener = RuntimeListener::new(client.clone(), device.clone(), store.clone_lazy()); 91 | let handle = ServiceHandle::new(device.clone(), store.clone_lazy(), tx); 92 | let service = Service::new(client, device.clone(), rx, store); 93 | 94 | let mut tasks = JoinSet::new(); 95 | 96 | tasks.spawn(handle_events(service)); 97 | tasks.spawn(runtime_listen(listener)); 98 | tasks.spawn(receive_events(device, handle)); 99 | 100 | while let Some(res) = tasks.join_next().await { 101 | match res { 102 | Ok(Ok(())) => { 103 | info!("task exited"); 104 | 105 | tasks.abort_all(); 106 | } 107 | Ok(Err(err)) => { 108 | error!(error = format!("{:#}", err), "joined with error"); 109 | 110 | return Err(err); 111 | } 112 | Err(err) if err.is_cancelled() => {} 113 | Err(err) => { 114 | error!( 115 | error = format!("{:#}", color_eyre::Report::new(err)), 116 | "task panicked" 117 | ); 118 | 119 | bail!("task panicked"); 120 | } 121 | } 122 | } 123 | 124 | Ok(()) 125 | } 126 | -------------------------------------------------------------------------------- /e2e-test-forwarder/Cargo.toml: -------------------------------------------------------------------------------- 1 | # Copyright 2023-2024 SECO Mind Srl 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | [package] 5 | name = "e2e-test-forwarder" 6 | version = "0.1.0" 7 | edition.workspace = true 8 | homepage.workspace = true 9 | publish = false 10 | rust-version.workspace = true 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [dependencies] 15 | clap = { workspace = true, features = ["derive"] } 16 | edgehog-forwarder = { workspace = true, features = ["_test-utils"] } 17 | tokio = { workspace = true } 18 | tracing = { workspace = true } 19 | tracing-subscriber = { workspace = true, features = ["env-filter"] } 20 | -------------------------------------------------------------------------------- /e2e-test-forwarder/src/main.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 SECO Mind Srl 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use clap::Parser; 5 | use edgehog_forwarder::test_utils::con_manager; 6 | use tokio::task::JoinSet; 7 | use tracing::info; 8 | use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; 9 | 10 | #[derive(Parser, Debug)] 11 | struct Cli { 12 | /// Host address of the forwarder server 13 | #[arg(long, short = 'H')] 14 | host: String, 15 | /// Port of the forwarder server 16 | #[arg(short, long, default_value_t = 4000)] 17 | port: u16, 18 | /// Session token 19 | #[arg(short, long)] 20 | token: String, 21 | /// Secure the connection using WSS 22 | #[arg(short, long, default_value_t = false)] 23 | secure: bool, 24 | } 25 | 26 | #[tokio::main] 27 | async fn main() { 28 | tracing_subscriber::registry() 29 | .with(tracing_subscriber::fmt::layer()) 30 | .with(EnvFilter::from_default_env()) 31 | .init(); 32 | 33 | let Cli { 34 | host, 35 | port, 36 | token, 37 | secure, 38 | } = Cli::parse(); 39 | 40 | let url = format!( 41 | "{}://{host}:{port}/device/websocket?session={token}", 42 | if secure { "wss" } else { "ws" } 43 | ); 44 | let mut js = JoinSet::new(); 45 | 46 | js.spawn(con_manager(url, secure)); 47 | 48 | while let Some(res) = js.join_next().await { 49 | info!("{res:?}"); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /e2e-test/Cargo.toml: -------------------------------------------------------------------------------- 1 | # This file is part of Edgehog. 2 | # 3 | # Copyright 2023-2024 SECO Mind Srl 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # SPDX-License-Identifier: Apache-2.0 18 | 19 | [package] 20 | name = "e2e-test" 21 | version = "0.1.0" 22 | edition = { workspace = true } 23 | rust-version = { workspace = true } 24 | 25 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 26 | 27 | [dependencies] 28 | astarte-device-sdk = { workspace = true, features = ["derive"] } 29 | clap = { workspace = true, features = ["derive", "env"] } 30 | color-eyre = { workspace = true } 31 | edgehog-device-runtime = { path = ".." } 32 | reqwest = { workspace = true, features = ["rustls-tls-native-roots-no-provider"] } 33 | serde = { workspace = true } 34 | serde_json = { workspace = true } 35 | tempdir = { workspace = true } 36 | tokio = { workspace = true, features = ["full"] } 37 | tracing = { workspace = true } 38 | tracing-subscriber = { workspace = true, features = ["env-filter"] } 39 | url = { workspace = true } 40 | 41 | [features] 42 | message-hub = ["edgehog-device-runtime/message-hub"] 43 | # It's not strictly required, but in development it will show an error in the device manager option 44 | # when compiling with the flags all-features and workspace enabled 45 | containers = ["edgehog-device-runtime/containers"] 46 | -------------------------------------------------------------------------------- /edgehog-device-runtime-containers/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project 6 | adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [0.9.0] - 2025-03-11 9 | 10 | ### Added 11 | 12 | - Store the information into a SQLite database 13 | - Create the container service to manage and deploy containers 14 | - Added support for the following container interfaces: 15 | - `io.edgehog.devicemanager.apps.AvailableContainers` 16 | - `io.edgehog.devicemanager.apps.AvailableDeployments` 17 | - `io.edgehog.devicemanager.apps.AvailableImages` 18 | - `io.edgehog.devicemanager.apps.AvailableNetworks` 19 | - `io.edgehog.devicemanager.apps.AvailableVolumes` 20 | - `io.edgehog.devicemanager.apps.CreateContainerRequest` 21 | - `io.edgehog.devicemanager.apps.CreateDeploymentRequest` 22 | - `io.edgehog.devicemanager.apps.CreateImageRequest` 23 | - `io.edgehog.devicemanager.apps.CreateNetworkRequest` 24 | - `io.edgehog.devicemanager.apps.CreateVolumeRequest` 25 | - `io.edgehog.devicemanager.apps.DeploymentCommand` 26 | - `io.edgehog.devicemanager.apps.DeploymentEvent` 27 | - `io.edgehog.devicemanager.apps.DeploymentUpdate` 28 | -------------------------------------------------------------------------------- /edgehog-device-runtime-containers/Cargo.toml: -------------------------------------------------------------------------------- 1 | # This file is part of Edgehog. 2 | # 3 | # Copyright 2023 - 2025 SECO Mind Srl 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # SPDX-License-Identifier: Apache-2.0 18 | 19 | [package] 20 | name = "edgehog-device-runtime-containers" 21 | version = { workspace = true } 22 | edition = { workspace = true } 23 | homepage = { workspace = true } 24 | rust-version = { workspace = true } 25 | 26 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 27 | 28 | [dependencies] 29 | astarte-device-sdk = { workspace = true, features = ["derive"] } 30 | async-trait = { workspace = true } 31 | base64 = { workspace = true } 32 | bollard = { workspace = true } 33 | cfg-if = { workspace = true } 34 | diesel = { workspace = true } 35 | displaydoc = { workspace = true } 36 | edgehog-store = { workspace = true, features = ["containers"] } 37 | eyre = { workspace = true } 38 | futures = { workspace = true } 39 | hyper = { workspace = true, optional = true } 40 | indexmap = { workspace = true } 41 | itertools = { workspace = true } 42 | mockall = { workspace = true, optional = true } 43 | serde = { workspace = true, features = ["derive", "rc"] } 44 | serde_json = { workspace = true } 45 | thiserror = { workspace = true } 46 | tokio = { workspace = true, features = ["macros", "fs", "io-util"] } 47 | tracing = { workspace = true, features = ["log"] } 48 | uuid = { workspace = true } 49 | 50 | [dev-dependencies] 51 | astarte-device-sdk-mock = { workspace = true } 52 | pretty_assertions = { workspace = true } 53 | tempfile = { workspace = true } 54 | tracing-subscriber = { workspace = true } 55 | # Generate random unique names for the containers 56 | uuid = { workspace = true, features = ["v7"] } 57 | 58 | [features] 59 | mock = ["dep:mockall", "dep:hyper"] 60 | # Required for libsqlite3 on ubuntu 24.04 seg faulting with multi join queries 61 | # See: https://bugs.launchpad.net/ubuntu/+source/sqlite3/+bug/2087772 62 | vendored = ["edgehog-store/vendored"] 63 | -------------------------------------------------------------------------------- /edgehog-device-runtime-containers/README.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # Edgehog Device Runtime Containers 8 | 9 | Library for container manager in the Edgehog Device Runtime. 10 | -------------------------------------------------------------------------------- /edgehog-device-runtime-containers/src/client.rs: -------------------------------------------------------------------------------- 1 | // This file is part of Edgehog. 2 | // 3 | // Copyright 2023-2024 SECO Mind Srl 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | // SPDX-License-Identifier: Apache-2.0 18 | 19 | //! Client to use to communicate with the Docker daemon. 20 | 21 | // Mock the client, this is behind a feature flag so we can test with both the real docker daemon 22 | // and the mocked one. 23 | #[cfg(feature = "mock")] 24 | pub(crate) use crate::mock::{DockerTrait, MockDocker as Client}; 25 | #[cfg(not(feature = "mock"))] 26 | pub(crate) use bollard::Docker as Client; 27 | -------------------------------------------------------------------------------- /edgehog-device-runtime-containers/src/error.rs: -------------------------------------------------------------------------------- 1 | // This file is part of Edgehog. 2 | // 3 | // Copyright 2023-2024 SECO Mind Srl 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | // SPDX-License-Identifier: Apache-2.0 18 | 19 | //! Error returned when interacting with the docker daemon 20 | 21 | use crate::{ 22 | container::ContainerError, image::ImageError, network::NetworkError, volume::VolumeError, 23 | }; 24 | 25 | /// Error returned form the docker daemon 26 | #[non_exhaustive] 27 | #[derive(Debug, displaydoc::Display, thiserror::Error)] 28 | pub enum DockerError { 29 | /// couldn't connect to the docker daemon docker 30 | Connection(#[source] bollard::errors::Error), 31 | /// couldn't negotiate a supported version with the daemon docker 32 | Version(#[source] bollard::errors::Error), 33 | /// couldn't listen to events 34 | Envents(#[source] bollard::errors::Error), 35 | /// couldn't ping the docker daemon 36 | Ping(#[source] bollard::errors::Error), 37 | /// couldn't complete the image operation 38 | Image(#[from] ImageError), 39 | /// couldn't complete the volume operation 40 | Volume(#[from] VolumeError), 41 | /// couldn't complete the network operation 42 | Network(#[from] NetworkError), 43 | /// couldn't complete the container operation 44 | Container(#[from] ContainerError), 45 | } 46 | -------------------------------------------------------------------------------- /edgehog-device-runtime-containers/src/events/deployment.rs: -------------------------------------------------------------------------------- 1 | // This file is part of Edgehog. 2 | // 3 | // Copyright 2024 SECO Mind Srl 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | // SPDX-License-Identifier: Apache-2.0 18 | 19 | //! Events sent from the device to Astarte 20 | 21 | use std::fmt::Display; 22 | 23 | use astarte_device_sdk::{AstarteAggregate, AstarteType}; 24 | use tracing::error; 25 | use uuid::Uuid; 26 | 27 | use crate::properties::Client; 28 | 29 | const INTERFACE: &str = "io.edgehog.devicemanager.apps.DeploymentEvent"; 30 | 31 | /// Deployment status event 32 | #[derive(Debug, Clone, AstarteAggregate)] 33 | pub(crate) struct DeploymentEvent { 34 | pub(crate) status: EventStatus, 35 | pub(crate) message: String, 36 | } 37 | 38 | impl DeploymentEvent { 39 | pub(crate) fn new(status: EventStatus, message: impl Into) -> Self { 40 | Self { 41 | status, 42 | message: message.into(), 43 | } 44 | } 45 | 46 | pub(crate) async fn send(self, id: &Uuid, device: &D) 47 | where 48 | D: Client + Sync + 'static, 49 | { 50 | let res = device 51 | .send_object(INTERFACE, &format!("/{id}"), self.clone()) 52 | .await; 53 | 54 | if let Err(err) = res { 55 | error!( 56 | error = format!("{:#}", eyre::Report::new(err)), 57 | "couldn't send {self}, with id {id}" 58 | ) 59 | } 60 | } 61 | } 62 | 63 | impl Display for DeploymentEvent { 64 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 65 | write!( 66 | f, 67 | "deployment event ({}) with message {}", 68 | self.status, self.message 69 | ) 70 | } 71 | } 72 | 73 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 74 | pub(crate) enum EventStatus { 75 | Starting, 76 | Stopping, 77 | Updating, 78 | Deleting, 79 | Error, 80 | } 81 | 82 | impl Display for EventStatus { 83 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 84 | match self { 85 | EventStatus::Starting => write!(f, "Starting"), 86 | EventStatus::Stopping => write!(f, "Stopping"), 87 | EventStatus::Updating => write!(f, "Updating"), 88 | EventStatus::Deleting => write!(f, "Deleting"), 89 | EventStatus::Error => write!(f, "Error"), 90 | } 91 | } 92 | } 93 | 94 | impl From for AstarteType { 95 | fn from(value: EventStatus) -> Self { 96 | AstarteType::String(value.to_string()) 97 | } 98 | } 99 | 100 | #[cfg(test)] 101 | mod tests { 102 | use astarte_device_sdk::store::SqliteStore; 103 | use astarte_device_sdk_mock::mockall::Sequence; 104 | use astarte_device_sdk_mock::MockDeviceClient; 105 | 106 | use super::*; 107 | 108 | #[tokio::test] 109 | async fn should_send_event() { 110 | let mut client = MockDeviceClient::::new(); 111 | let mut seq = Sequence::new(); 112 | 113 | let id = Uuid::new_v4(); 114 | 115 | let exp_p = format!("/{id}"); 116 | client 117 | .expect_send_object::() 118 | .once() 119 | .in_sequence(&mut seq) 120 | .withf(move |interface, path, data| { 121 | interface == "io.edgehog.devicemanager.apps.DeploymentEvent" 122 | && path == exp_p 123 | && data.status == EventStatus::Starting 124 | && data.message.is_empty() 125 | }) 126 | .returning(|_, _, _| Ok(())); 127 | 128 | let event = DeploymentEvent::new(EventStatus::Starting, ""); 129 | 130 | event.send(&id, &client).await; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /edgehog-device-runtime-containers/src/lib.rs: -------------------------------------------------------------------------------- 1 | // This file is part of Edgehog. 2 | // 3 | // Copyright 2023-2024 SECO Mind Srl 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | // SPDX-License-Identifier: Apache-2.0 18 | 19 | #![warn( 20 | missing_docs, 21 | rustdoc::missing_crate_level_docs, 22 | clippy::dbg_macro, 23 | clippy::todo 24 | )] 25 | 26 | //! # Edgehog Device Runtime Containers 27 | //! 28 | //! Library to manage container for the `edgehog-device-runtime`. 29 | //! 30 | //! It will handle communications with the container runtime and solve the requests received from 31 | //! Astarte. 32 | 33 | pub(crate) mod client; 34 | pub mod docker; 35 | pub mod error; 36 | pub mod events; 37 | pub mod properties; 38 | pub mod requests; 39 | pub mod resource; 40 | pub mod service; 41 | pub mod store; 42 | 43 | #[cfg(feature = "mock")] 44 | mod mock; 45 | 46 | /// Re-export third parties dependencies 47 | pub use bollard; 48 | 49 | /// Re-export internal structs 50 | pub use self::docker::*; 51 | -------------------------------------------------------------------------------- /edgehog-device-runtime-containers/src/properties/deployment.rs: -------------------------------------------------------------------------------- 1 | // This file is part of Edgehog. 2 | // 3 | // Copyright 2024 SECO Mind Srl 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | // SPDX-License-Identifier: Apache-2.0 18 | 19 | //! Available [`Image`](crate::docker::image::Image) property. 20 | 21 | use std::fmt::Display; 22 | 23 | use astarte_device_sdk::AstarteType; 24 | use async_trait::async_trait; 25 | use uuid::Uuid; 26 | 27 | use super::AvailableProp; 28 | 29 | const INTERFACE: &str = "io.edgehog.devicemanager.apps.AvailableDeployments"; 30 | 31 | /// Available deployment property. 32 | #[derive(Debug, Clone, PartialEq, Eq)] 33 | pub(crate) struct AvailableDeployment<'a> { 34 | id: &'a Uuid, 35 | } 36 | 37 | impl<'a> AvailableDeployment<'a> { 38 | pub(crate) fn new(id: &'a Uuid) -> Self { 39 | Self { id } 40 | } 41 | } 42 | 43 | #[async_trait] 44 | impl AvailableProp for AvailableDeployment<'_> { 45 | type Data = DeploymentStatus; 46 | 47 | fn interface() -> &'static str { 48 | INTERFACE 49 | } 50 | 51 | fn field() -> &'static str { 52 | "status" 53 | } 54 | 55 | fn id(&self) -> &Uuid { 56 | self.id 57 | } 58 | } 59 | 60 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 61 | pub(crate) enum DeploymentStatus { 62 | Stopped, 63 | Started, 64 | } 65 | 66 | impl Display for DeploymentStatus { 67 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 68 | match self { 69 | DeploymentStatus::Started => write!(f, "Started"), 70 | DeploymentStatus::Stopped => write!(f, "Stopped"), 71 | } 72 | } 73 | } 74 | 75 | impl From for AstarteType { 76 | fn from(value: DeploymentStatus) -> Self { 77 | AstarteType::String(value.to_string()) 78 | } 79 | } 80 | 81 | #[cfg(test)] 82 | mod tests { 83 | use astarte_device_sdk::store::SqliteStore; 84 | use astarte_device_sdk_mock::{mockall::Sequence, MockDeviceClient}; 85 | use uuid::Uuid; 86 | 87 | use super::*; 88 | 89 | #[tokio::test] 90 | async fn should_store_deployment() { 91 | let id = Uuid::new_v4(); 92 | 93 | let deployment = AvailableDeployment { id: &id }; 94 | 95 | let mut client = MockDeviceClient::::new(); 96 | let mut seq = Sequence::new(); 97 | 98 | client 99 | .expect_send() 100 | .once() 101 | .in_sequence(&mut seq) 102 | .withf(move |interface, path, status: &DeploymentStatus| { 103 | interface == "io.edgehog.devicemanager.apps.AvailableDeployments" 104 | && path == format!("/{id}/status") 105 | && *status == DeploymentStatus::Stopped 106 | }) 107 | .returning(|_, _, _| Ok(())); 108 | 109 | deployment 110 | .send(&client, DeploymentStatus::Stopped) 111 | .await 112 | .unwrap(); 113 | } 114 | 115 | #[tokio::test] 116 | async fn should_unset_deployment() { 117 | let id = Uuid::new_v4(); 118 | 119 | let deployment = AvailableDeployment { id: &id }; 120 | 121 | let mut client = MockDeviceClient::::new(); 122 | let mut seq = Sequence::new(); 123 | 124 | client 125 | .expect_unset() 126 | .once() 127 | .in_sequence(&mut seq) 128 | .withf(move |interface, path| { 129 | interface == "io.edgehog.devicemanager.apps.AvailableDeployments" 130 | && path == format!("/{id}/status") 131 | }) 132 | .returning(|_, _| Ok(())); 133 | 134 | deployment.unset(&client).await.unwrap(); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /edgehog-device-runtime-containers/src/properties/image.rs: -------------------------------------------------------------------------------- 1 | // This file is part of Edgehog. 2 | // 3 | // Copyright 2024 SECO Mind Srl 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | // SPDX-License-Identifier: Apache-2.0 18 | 19 | //! Available [`Image`](crate::docker::image::Image) property. 20 | 21 | use async_trait::async_trait; 22 | use uuid::Uuid; 23 | 24 | use super::AvailableProp; 25 | 26 | const INTERFACE: &str = "io.edgehog.devicemanager.apps.AvailableImages"; 27 | 28 | /// Available 29 | #[derive(Debug, Clone, PartialEq, Eq)] 30 | pub(crate) struct AvailableImage<'a> { 31 | id: &'a Uuid, 32 | } 33 | 34 | impl<'a> AvailableImage<'a> { 35 | pub(crate) fn new(id: &'a Uuid) -> Self { 36 | Self { id } 37 | } 38 | } 39 | 40 | #[async_trait] 41 | impl AvailableProp for AvailableImage<'_> { 42 | type Data = bool; 43 | 44 | fn interface() -> &'static str { 45 | INTERFACE 46 | } 47 | 48 | fn field() -> &'static str { 49 | "pulled" 50 | } 51 | 52 | fn id(&self) -> &Uuid { 53 | self.id 54 | } 55 | } 56 | 57 | #[cfg(test)] 58 | mod tests { 59 | use astarte_device_sdk::store::SqliteStore; 60 | use astarte_device_sdk_mock::{mockall::Sequence, MockDeviceClient}; 61 | use uuid::Uuid; 62 | 63 | use super::*; 64 | 65 | #[tokio::test] 66 | async fn should_store_image() { 67 | let id = Uuid::new_v4(); 68 | 69 | let image = AvailableImage::new(&id); 70 | 71 | let mut client = MockDeviceClient::::new(); 72 | let mut seq = Sequence::new(); 73 | 74 | client 75 | .expect_send() 76 | .once() 77 | .in_sequence(&mut seq) 78 | .withf(move |interface, path, pulled: &bool| { 79 | interface == "io.edgehog.devicemanager.apps.AvailableImages" 80 | && path == format!("/{id}/pulled") 81 | && *pulled 82 | }) 83 | .returning(|_, _, _| Ok(())); 84 | 85 | image.send(&client, true).await.unwrap(); 86 | } 87 | 88 | #[tokio::test] 89 | async fn should_unset_image() { 90 | let id = Uuid::new_v4(); 91 | 92 | let image = AvailableImage::new(&id); 93 | 94 | let mut client = MockDeviceClient::::new(); 95 | let mut seq = Sequence::new(); 96 | 97 | client 98 | .expect_unset() 99 | .once() 100 | .in_sequence(&mut seq) 101 | .withf(move |interface, path| { 102 | interface == "io.edgehog.devicemanager.apps.AvailableImages" 103 | && path == format!("/{id}/pulled") 104 | }) 105 | .returning(|_, _| Ok(())); 106 | 107 | image.unset(&client).await.unwrap(); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /edgehog-device-runtime-containers/src/properties/mod.rs: -------------------------------------------------------------------------------- 1 | // This file is part of Edgehog. 2 | // 3 | // Copyright 2023-2024 SECO Mind Srl 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | // SPDX-License-Identifier: Apache-2.0 18 | 19 | //! Container properties sent from the device to Astarte. 20 | 21 | use astarte_device_sdk::AstarteType; 22 | use async_trait::async_trait; 23 | use tracing::error; 24 | use uuid::Uuid; 25 | 26 | cfg_if::cfg_if! { 27 | if #[cfg(test)] { 28 | pub use astarte_device_sdk_mock::Client; 29 | } else { 30 | pub use astarte_device_sdk::Client; 31 | } 32 | } 33 | 34 | pub(crate) mod container; 35 | pub(crate) mod deployment; 36 | pub(crate) mod image; 37 | pub(crate) mod network; 38 | pub(crate) mod volume; 39 | 40 | /// Error returned when failing to publish a property 41 | #[derive(Debug, thiserror::Error, displaydoc::Display)] 42 | pub enum PropertyError { 43 | /// couldn't send property {interface}{endpoint} 44 | Send { 45 | /// Interface of the property 46 | interface: String, 47 | /// Endpoint of the property 48 | endpoint: String, 49 | }, 50 | /// couldn't unset property {interface}{endpoint} 51 | Unset { 52 | /// Interface of the property 53 | interface: String, 54 | /// Endpoint of the property 55 | endpoint: String, 56 | }, 57 | } 58 | 59 | #[async_trait] 60 | pub(crate) trait AvailableProp { 61 | type Data: Into + Send + 'static; 62 | 63 | fn interface() -> &'static str; 64 | 65 | fn field() -> &'static str; 66 | 67 | fn id(&self) -> &Uuid; 68 | 69 | async fn send(&self, device: &D, data: Self::Data) -> Result<(), PropertyError> 70 | where 71 | D: Client + Sync + 'static, 72 | { 73 | self.send_field(device, Self::field(), data).await 74 | } 75 | 76 | async fn send_field(&self, device: &D, field: &str, data: T) -> Result<(), PropertyError> 77 | where 78 | D: Client + Sync + 'static, 79 | T: Into + Send + 'static, 80 | { 81 | let interface = Self::interface(); 82 | let endpoint = format!("/{}/{}", self.id(), field); 83 | 84 | device 85 | .send(interface, &endpoint, data) 86 | .await 87 | .map_err(|err| { 88 | error!( 89 | error = format!("{:#}", eyre::Report::new(err)), 90 | "couldn't send data for {interface}{endpoint}" 91 | ); 92 | 93 | PropertyError::Send { 94 | interface: interface.to_string(), 95 | endpoint, 96 | } 97 | }) 98 | } 99 | 100 | async fn unset(&self, device: &D) -> Result<(), PropertyError> 101 | where 102 | D: Client + Sync + 'static, 103 | { 104 | let interface = Self::interface(); 105 | let field = Self::field(); 106 | let endpoint = format!("/{}/{}", self.id(), field); 107 | 108 | device.unset(interface, &endpoint).await.map_err(|err| { 109 | error!( 110 | error = format!("{:#}", eyre::Report::new(err)), 111 | "couldn't unset data for {interface}{endpoint}" 112 | ); 113 | 114 | PropertyError::Unset { 115 | interface: interface.to_string(), 116 | endpoint, 117 | } 118 | }) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /edgehog-device-runtime-containers/src/properties/network.rs: -------------------------------------------------------------------------------- 1 | // This file is part of Edgehog. 2 | // 3 | // Copyright 2024 SECO Mind Srl 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | // SPDX-License-Identifier: Apache-2.0 18 | 19 | //! Available [`Image`](crate::docker::image::Image) property. 20 | 21 | use async_trait::async_trait; 22 | use uuid::Uuid; 23 | 24 | use super::AvailableProp; 25 | 26 | const INTERFACE: &str = "io.edgehog.devicemanager.apps.AvailableNetworks"; 27 | 28 | /// Available network property. 29 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 30 | pub(crate) struct AvailableNetwork<'a> { 31 | id: &'a Uuid, 32 | } 33 | 34 | impl<'a> AvailableNetwork<'a> { 35 | pub(crate) fn new(id: &'a Uuid) -> Self { 36 | Self { id } 37 | } 38 | } 39 | 40 | #[async_trait] 41 | impl AvailableProp for AvailableNetwork<'_> { 42 | type Data = bool; 43 | 44 | fn interface() -> &'static str { 45 | INTERFACE 46 | } 47 | 48 | fn id(&self) -> &Uuid { 49 | self.id 50 | } 51 | 52 | fn field() -> &'static str { 53 | "created" 54 | } 55 | } 56 | 57 | #[cfg(test)] 58 | mod tests { 59 | use astarte_device_sdk::store::SqliteStore; 60 | use astarte_device_sdk_mock::{mockall::Sequence, MockDeviceClient}; 61 | use uuid::Uuid; 62 | 63 | use super::*; 64 | 65 | #[tokio::test] 66 | async fn should_store_network() { 67 | let id = Uuid::new_v4(); 68 | 69 | let network = AvailableNetwork::new(&id); 70 | 71 | let mut client = MockDeviceClient::::new(); 72 | let mut seq = Sequence::new(); 73 | 74 | client 75 | .expect_send() 76 | .once() 77 | .in_sequence(&mut seq) 78 | .withf(move |interface, path, pulled: &bool| { 79 | interface == "io.edgehog.devicemanager.apps.AvailableNetworks" 80 | && path == format!("/{id}/created") 81 | && *pulled 82 | }) 83 | .returning(|_, _, _| Ok(())); 84 | 85 | network.send(&client, true).await.unwrap(); 86 | } 87 | 88 | #[tokio::test] 89 | async fn should_unset_network() { 90 | let id = Uuid::new_v4(); 91 | 92 | let network = AvailableNetwork::new(&id); 93 | 94 | let mut client = MockDeviceClient::::new(); 95 | let mut seq = Sequence::new(); 96 | 97 | client 98 | .expect_unset() 99 | .once() 100 | .in_sequence(&mut seq) 101 | .withf(move |interface, path| { 102 | interface == "io.edgehog.devicemanager.apps.AvailableNetworks" 103 | && path == format!("/{id}/created") 104 | }) 105 | .returning(|_, _| Ok(())); 106 | 107 | network.unset(&client).await.unwrap(); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /edgehog-device-runtime-containers/src/properties/volume.rs: -------------------------------------------------------------------------------- 1 | // This file is part of Edgehog. 2 | // 3 | // Copyright 2024 SECO Mind Srl 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | // SPDX-License-Identifier: Apache-2.0 18 | 19 | //! Available [`Image`](crate::docker::image::Image) property. 20 | 21 | use async_trait::async_trait; 22 | use uuid::Uuid; 23 | 24 | use super::AvailableProp; 25 | 26 | const INTERFACE: &str = "io.edgehog.devicemanager.apps.AvailableVolumes"; 27 | 28 | /// Available volume property. 29 | #[derive(Debug, Clone, PartialEq, Eq)] 30 | pub(crate) struct AvailableVolume<'a> { 31 | id: &'a Uuid, 32 | } 33 | 34 | impl<'a> AvailableVolume<'a> { 35 | pub(crate) fn new(id: &'a Uuid) -> Self { 36 | Self { id } 37 | } 38 | } 39 | 40 | #[async_trait] 41 | impl AvailableProp for AvailableVolume<'_> { 42 | type Data = bool; 43 | 44 | fn interface() -> &'static str { 45 | INTERFACE 46 | } 47 | 48 | fn id(&self) -> &Uuid { 49 | self.id 50 | } 51 | 52 | fn field() -> &'static str { 53 | "created" 54 | } 55 | } 56 | 57 | #[cfg(test)] 58 | mod tests { 59 | use astarte_device_sdk::store::SqliteStore; 60 | use astarte_device_sdk_mock::{mockall::Sequence, MockDeviceClient}; 61 | use uuid::Uuid; 62 | 63 | use super::*; 64 | 65 | #[tokio::test] 66 | async fn should_store_volume() { 67 | let id = Uuid::new_v4(); 68 | 69 | let volume = AvailableVolume::new(&id); 70 | 71 | let mut client = MockDeviceClient::::new(); 72 | let mut seq = Sequence::new(); 73 | 74 | client 75 | .expect_send() 76 | .once() 77 | .in_sequence(&mut seq) 78 | .withf(move |interface, path, pulled: &bool| { 79 | interface == "io.edgehog.devicemanager.apps.AvailableVolumes" 80 | && path == format!("/{id}/created") 81 | && *pulled 82 | }) 83 | .returning(|_, _, _| Ok(())); 84 | 85 | volume.send(&client, true).await.unwrap(); 86 | } 87 | 88 | #[tokio::test] 89 | async fn should_unset_volume() { 90 | let id = Uuid::new_v4(); 91 | 92 | let volume = AvailableVolume::new(&id); 93 | 94 | let mut client = MockDeviceClient::::new(); 95 | let mut seq = Sequence::new(); 96 | 97 | client 98 | .expect_unset() 99 | .once() 100 | .in_sequence(&mut seq) 101 | .withf(move |interface, path| { 102 | interface == "io.edgehog.devicemanager.apps.AvailableVolumes" 103 | && path == format!("/{id}/created") 104 | }) 105 | .returning(|_, _| Ok(())); 106 | 107 | volume.unset(&client).await.unwrap(); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /edgehog-device-runtime-containers/src/requests/image.rs: -------------------------------------------------------------------------------- 1 | // This file is part of Edgehog. 2 | // 3 | // Copyright 2024 SECO Mind Srl 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | // SPDX-License-Identifier: Apache-2.0 18 | 19 | //! Create image request 20 | 21 | use astarte_device_sdk::FromEvent; 22 | 23 | use super::ReqUuid; 24 | 25 | /// Request to pull a Docker Image. 26 | #[derive(Debug, Clone, FromEvent, PartialEq, Eq, PartialOrd, Ord)] 27 | #[from_event( 28 | interface = "io.edgehog.devicemanager.apps.CreateImageRequest", 29 | path = "/image", 30 | rename_all = "camelCase" 31 | )] 32 | pub struct CreateImage { 33 | pub(crate) id: ReqUuid, 34 | pub(crate) deployment_id: ReqUuid, 35 | pub(crate) reference: String, 36 | pub(crate) registry_auth: String, 37 | } 38 | 39 | #[cfg(test)] 40 | pub(crate) mod tests { 41 | use std::fmt::Display; 42 | 43 | use astarte_device_sdk::{DeviceEvent, Value}; 44 | use uuid::Uuid; 45 | 46 | use super::*; 47 | 48 | pub fn create_image_request_event( 49 | id: impl Display, 50 | deployment_id: impl Display, 51 | reference: &str, 52 | auth: &str, 53 | ) -> DeviceEvent { 54 | let fields = [ 55 | ("id", id.to_string()), 56 | ("deploymentId", deployment_id.to_string()), 57 | ("reference", reference.to_string()), 58 | ("registryAuth", auth.to_string()), 59 | ] 60 | .into_iter() 61 | .map(|(k, v)| (k.to_string(), v.into())) 62 | .collect(); 63 | 64 | DeviceEvent { 65 | interface: "io.edgehog.devicemanager.apps.CreateImageRequest".to_string(), 66 | path: "/image".to_string(), 67 | data: Value::Object(fields), 68 | } 69 | } 70 | 71 | #[test] 72 | fn create_image_request() { 73 | let id = Uuid::new_v4(); 74 | let deployment_id = Uuid::new_v4(); 75 | let event = 76 | create_image_request_event(id.to_string(), deployment_id, "reference", "registry_auth"); 77 | 78 | let request = CreateImage::from_event(event).unwrap(); 79 | 80 | let expect = CreateImage { 81 | id: ReqUuid(id), 82 | deployment_id: ReqUuid(deployment_id), 83 | reference: "reference".to_string(), 84 | registry_auth: "registry_auth".to_string(), 85 | }; 86 | 87 | assert_eq!(request, expect); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /edgehog-device-runtime-containers/src/requests/network.rs: -------------------------------------------------------------------------------- 1 | // This file is part of Edgehog. 2 | // 3 | // Copyright 2024 SECO Mind Srl 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | // SPDX-License-Identifier: Apache-2.0 18 | 19 | //! Create image request 20 | 21 | use astarte_device_sdk::FromEvent; 22 | 23 | use super::ReqUuid; 24 | 25 | /// Request to pull a Docker Network. 26 | #[derive(Debug, Clone, FromEvent, PartialEq, Eq, PartialOrd, Ord)] 27 | #[from_event( 28 | interface = "io.edgehog.devicemanager.apps.CreateNetworkRequest", 29 | path = "/network", 30 | rename_all = "camelCase" 31 | )] 32 | pub struct CreateNetwork { 33 | pub(crate) id: ReqUuid, 34 | pub(crate) deployment_id: ReqUuid, 35 | pub(crate) driver: String, 36 | pub(crate) internal: bool, 37 | pub(crate) enable_ipv6: bool, 38 | pub(crate) options: Vec, 39 | } 40 | 41 | #[cfg(test)] 42 | pub(crate) mod tests { 43 | 44 | use std::fmt::Display; 45 | 46 | use astarte_device_sdk::{AstarteType, DeviceEvent, Value}; 47 | use itertools::Itertools; 48 | use uuid::Uuid; 49 | 50 | use super::*; 51 | 52 | pub fn create_network_request_event( 53 | id: impl Display, 54 | deployment_id: impl Display, 55 | driver: &str, 56 | options: &[&str], 57 | ) -> DeviceEvent { 58 | let options = options.iter().map(|s| s.to_string()).collect_vec(); 59 | 60 | let fields = [ 61 | ("id", AstarteType::String(id.to_string())), 62 | ( 63 | "deploymentId", 64 | AstarteType::String(deployment_id.to_string()), 65 | ), 66 | ("driver", AstarteType::String(driver.to_string())), 67 | ("checkDuplicate", AstarteType::Boolean(false)), 68 | ("internal", AstarteType::Boolean(false)), 69 | ("enableIpv6", AstarteType::Boolean(false)), 70 | ("options", AstarteType::StringArray(options)), 71 | ] 72 | .into_iter() 73 | .map(|(k, v)| (k.to_string(), v)) 74 | .collect(); 75 | 76 | DeviceEvent { 77 | interface: "io.edgehog.devicemanager.apps.CreateNetworkRequest".to_string(), 78 | path: "/network".to_string(), 79 | data: Value::Object(fields), 80 | } 81 | } 82 | 83 | #[test] 84 | fn create_network_request() { 85 | let id = Uuid::new_v4(); 86 | let deployment_id = Uuid::new_v4(); 87 | let event = 88 | create_network_request_event(id, deployment_id, "driver", &["foo=bar", "some="]); 89 | 90 | let request = CreateNetwork::from_event(event).unwrap(); 91 | 92 | let expect = CreateNetwork { 93 | id: ReqUuid(id), 94 | deployment_id: ReqUuid(deployment_id), 95 | driver: "driver".to_string(), 96 | internal: false, 97 | enable_ipv6: false, 98 | options: ["foo=bar", "some="].map(str::to_string).to_vec(), 99 | }; 100 | 101 | assert_eq!(request, expect); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /edgehog-device-runtime-containers/src/requests/volume.rs: -------------------------------------------------------------------------------- 1 | // This file is part of Edgehog. 2 | // 3 | // Copyright 2024 SECO Mind Srl 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | // SPDX-License-Identifier: Apache-2.0 18 | 19 | //! Create image request 20 | 21 | use astarte_device_sdk::FromEvent; 22 | 23 | use super::ReqUuid; 24 | 25 | /// Request to pull a Docker Volume. 26 | #[derive(Debug, Clone, FromEvent, PartialEq, Eq, PartialOrd, Ord)] 27 | #[from_event( 28 | interface = "io.edgehog.devicemanager.apps.CreateVolumeRequest", 29 | path = "/volume", 30 | rename_all = "camelCase" 31 | )] 32 | pub struct CreateVolume { 33 | pub(crate) id: ReqUuid, 34 | pub(crate) deployment_id: ReqUuid, 35 | pub(crate) driver: String, 36 | pub(crate) options: Vec, 37 | } 38 | 39 | #[cfg(test)] 40 | pub(crate) mod tests { 41 | use std::fmt::Display; 42 | 43 | use astarte_device_sdk::{AstarteType, DeviceEvent, Value}; 44 | use itertools::Itertools; 45 | use uuid::Uuid; 46 | 47 | use super::*; 48 | 49 | pub fn create_volume_request_event( 50 | id: impl Display, 51 | deployment_id: impl Display, 52 | driver: &str, 53 | options: &[&str], 54 | ) -> DeviceEvent { 55 | let options = options.iter().map(|s| s.to_string()).collect_vec(); 56 | 57 | let fields = [ 58 | ("id".to_string(), AstarteType::String(id.to_string())), 59 | ( 60 | "deploymentId".to_string(), 61 | AstarteType::String(deployment_id.to_string()), 62 | ), 63 | ( 64 | "driver".to_string(), 65 | AstarteType::String(driver.to_string()), 66 | ), 67 | ("options".to_string(), AstarteType::StringArray(options)), 68 | ] 69 | .into_iter() 70 | .collect(); 71 | 72 | DeviceEvent { 73 | interface: "io.edgehog.devicemanager.apps.CreateVolumeRequest".to_string(), 74 | path: "/volume".to_string(), 75 | data: Value::Object(fields), 76 | } 77 | } 78 | 79 | #[test] 80 | fn create_volume_request() { 81 | let id = Uuid::new_v4(); 82 | let deployment_id = Uuid::new_v4(); 83 | let event = create_volume_request_event(id, deployment_id, "driver", &["foo=bar", "some="]); 84 | 85 | let request = CreateVolume::from_event(event).unwrap(); 86 | 87 | let expect = CreateVolume { 88 | id: ReqUuid(id), 89 | deployment_id: ReqUuid(deployment_id), 90 | driver: "driver".to_string(), 91 | options: ["foo=bar", "some="].map(str::to_string).to_vec(), 92 | }; 93 | 94 | assert_eq!(request, expect); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /edgehog-device-runtime-containers/src/resource/deployment.rs: -------------------------------------------------------------------------------- 1 | // This file is part of Edgehog. 2 | // 3 | // Copyright 2025 SECO Mind Srl 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | // SPDX-License-Identifier: Apache-2.0 18 | 19 | use std::collections::HashSet; 20 | 21 | use async_trait::async_trait; 22 | use edgehog_store::{ 23 | conversions::SqlUuid, 24 | models::containers::{ 25 | container::{ContainerNetwork, ContainerVolume}, 26 | deployment::DeploymentStatus, 27 | }, 28 | }; 29 | use uuid::Uuid; 30 | 31 | use crate::properties::{ 32 | deployment::{AvailableDeployment, DeploymentStatus as PropertyStatus}, 33 | AvailableProp, Client, 34 | }; 35 | 36 | use super::{Context, Resource, Result}; 37 | 38 | /// Row for a deployment 39 | /// 40 | /// It's made of the columns: container_id, image_id, network_id, volume_id 41 | pub(crate) type DeploymentRow = ( 42 | SqlUuid, 43 | SqlUuid, 44 | Option, 45 | Option, 46 | ); 47 | 48 | #[derive(Debug, Default)] 49 | pub(crate) struct Deployment { 50 | pub(crate) containers: HashSet, 51 | pub(crate) images: HashSet, 52 | pub(crate) volumes: HashSet, 53 | pub(crate) networks: HashSet, 54 | } 55 | 56 | impl From> for Deployment { 57 | fn from(value: Vec) -> Self { 58 | value.into_iter().fold( 59 | Self::default(), 60 | |mut acc, (container_id, image_id, c_network, c_volume)| { 61 | acc.containers.insert(*container_id); 62 | acc.images.insert(*image_id); 63 | 64 | if let Some(c_network) = c_network { 65 | acc.networks.insert(*c_network.network_id); 66 | } 67 | 68 | if let Some(c_volume) = c_volume { 69 | acc.volumes.insert(*c_volume.volume_id); 70 | } 71 | acc 72 | }, 73 | ) 74 | } 75 | } 76 | 77 | #[async_trait] 78 | impl Resource for Deployment 79 | where 80 | D: Client + Sync + 'static, 81 | { 82 | async fn publish(ctx: Context<'_, D>) -> Result<()> { 83 | AvailableDeployment::new(&ctx.id) 84 | .send(ctx.device, PropertyStatus::Stopped) 85 | .await?; 86 | 87 | ctx.store 88 | .update_deployment_status(ctx.id, DeploymentStatus::Stopped) 89 | .await?; 90 | 91 | Ok(()) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /edgehog-device-runtime-containers/src/resource/image.rs: -------------------------------------------------------------------------------- 1 | // This file is part of Edgehog. 2 | // 3 | // Copyright 2025 SECO Mind Srl 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | // SPDX-License-Identifier: Apache-2.0 18 | 19 | use async_trait::async_trait; 20 | use edgehog_store::models::containers::image::ImageStatus; 21 | 22 | use crate::{ 23 | image::Image, 24 | properties::{image::AvailableImage, AvailableProp, Client}, 25 | }; 26 | 27 | use super::{Context, Create, Resource, ResourceError, Result, State}; 28 | 29 | #[derive(Debug, Clone)] 30 | pub(crate) struct ImageResource { 31 | pub(crate) image: Image, 32 | } 33 | 34 | impl ImageResource { 35 | pub(crate) fn new(image: Image) -> Self { 36 | Self { image } 37 | } 38 | } 39 | 40 | #[async_trait] 41 | impl Resource for ImageResource 42 | where 43 | D: Client + Sync + 'static, 44 | { 45 | async fn publish(ctx: Context<'_, D>) -> Result<()> { 46 | AvailableImage::new(&ctx.id).send(ctx.device, false).await?; 47 | 48 | ctx.store 49 | .update_image_status(ctx.id, ImageStatus::Published) 50 | .await?; 51 | 52 | Ok(()) 53 | } 54 | } 55 | 56 | #[async_trait] 57 | impl Create for ImageResource 58 | where 59 | D: Client + Sync + 'static, 60 | { 61 | async fn fetch(ctx: &mut Context<'_, D>) -> Result<(State, Self)> { 62 | let mut resource = ctx 63 | .store 64 | .find_image(ctx.id) 65 | .await? 66 | .ok_or(ResourceError::Missing { 67 | id: ctx.id, 68 | resource: "image", 69 | })?; 70 | 71 | let exists = resource.image.inspect(ctx.client).await?.is_some(); 72 | 73 | if exists { 74 | ctx.store 75 | .update_image_local_id(ctx.id, resource.image.id.clone()) 76 | .await?; 77 | 78 | Ok((State::Created, resource)) 79 | } else { 80 | Ok((State::Missing, resource)) 81 | } 82 | } 83 | 84 | async fn create(&mut self, ctx: &mut Context<'_, D>) -> Result<()> { 85 | self.image.pull(ctx.client).await?; 86 | 87 | ctx.store 88 | .update_image_local_id(ctx.id, self.image.id.clone()) 89 | .await?; 90 | 91 | AvailableImage::new(&ctx.id).send(ctx.device, true).await?; 92 | 93 | ctx.store 94 | .update_image_status(ctx.id, ImageStatus::Pulled) 95 | .await?; 96 | 97 | Ok(()) 98 | } 99 | 100 | async fn delete(&mut self, ctx: &mut Context<'_, D>) -> Result<()> { 101 | self.image.remove(ctx.client).await?; 102 | 103 | AvailableImage::new(&ctx.id).unset(ctx.device).await?; 104 | 105 | ctx.store.delete_image(ctx.id).await?; 106 | 107 | Ok(()) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /edgehog-device-runtime-containers/src/resource/mod.rs: -------------------------------------------------------------------------------- 1 | // This file is part of Edgehog. 2 | // 3 | // Copyright 2025 SECO Mind Srl 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | // SPDX-License-Identifier: Apache-2.0 18 | 19 | //! Resource for the service. 20 | //! 21 | //! Handles and generalizes the operation on the various type of resources. 22 | 23 | use async_trait::async_trait; 24 | use tracing::debug; 25 | use uuid::Uuid; 26 | 27 | use crate::{ 28 | error::DockerError, 29 | properties::{Client, PropertyError}, 30 | store::{StateStore, StoreError}, 31 | Docker, 32 | }; 33 | 34 | pub(crate) mod container; 35 | pub(crate) mod deployment; 36 | pub(crate) mod image; 37 | pub(crate) mod network; 38 | pub(crate) mod volume; 39 | 40 | /// Error returned from a [`Resource`] operation. 41 | #[derive(Debug, thiserror::Error, displaydoc::Display)] 42 | pub enum ResourceError { 43 | /// couldn't publish resource property 44 | Property(#[from] PropertyError), 45 | /// couldn't complete the store operation 46 | Store(#[from] StoreError), 47 | /// couldn't complete docker operation 48 | Docker(#[source] DockerError), 49 | /// couldn't fetch the {resource} with id {id} 50 | Missing { 51 | /// Id of the resource 52 | id: Uuid, 53 | /// Type of the resource 54 | resource: &'static str, 55 | }, 56 | } 57 | 58 | #[derive(Debug)] 59 | pub(crate) struct Context<'a, D> { 60 | pub(crate) id: Uuid, 61 | pub(crate) store: &'a mut StateStore, 62 | pub(crate) device: &'a D, 63 | pub(crate) client: &'a Docker, 64 | } 65 | 66 | /// State of the object for the request. 67 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 68 | pub(crate) enum State { 69 | // The resource was published and saved to the state file 70 | Missing, 71 | // The resource was created 72 | Created, 73 | } 74 | 75 | impl From for ResourceError 76 | where 77 | T: Into, 78 | { 79 | fn from(value: T) -> Self { 80 | ResourceError::Docker(value.into()) 81 | } 82 | } 83 | 84 | type Result = std::result::Result; 85 | 86 | #[async_trait] 87 | pub(crate) trait Resource: Sized 88 | where 89 | D: Client + Sync + 'static, 90 | { 91 | async fn publish(ctx: Context<'_, D>) -> Result<()>; 92 | } 93 | 94 | #[async_trait] 95 | pub(crate) trait Create: Resource 96 | where 97 | D: Client + Sync + 'static, 98 | { 99 | async fn fetch(ctx: &mut Context<'_, D>) -> Result<(State, Self)>; 100 | async fn create(&mut self, ctx: &mut Context<'_, D>) -> Result<()>; 101 | async fn delete(&mut self, ctx: &mut Context<'_, D>) -> Result<()>; 102 | 103 | async fn up(mut ctx: Context<'_, D>) -> Result { 104 | let (state, mut resource) = Self::fetch(&mut ctx).await?; 105 | 106 | match state { 107 | State::Missing => { 108 | debug!("resource missing, creating it"); 109 | 110 | resource.create(&mut ctx).await?; 111 | } 112 | State::Created => { 113 | debug!("resource already created"); 114 | } 115 | } 116 | 117 | Ok(resource) 118 | } 119 | 120 | async fn down(mut ctx: Context<'_, D>) -> Result<()> { 121 | let (state, mut resource) = Self::fetch(&mut ctx).await?; 122 | 123 | match state { 124 | State::Missing => { 125 | debug!("resource already missing"); 126 | } 127 | State::Created => { 128 | debug!("resource found, deleting it"); 129 | 130 | resource.delete(&mut ctx).await? 131 | } 132 | } 133 | 134 | Ok(()) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /edgehog-device-runtime-containers/src/resource/network.rs: -------------------------------------------------------------------------------- 1 | // This file is part of Edgehog. 2 | // 3 | // Copyright 2025 SECO Mind Srl 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | // SPDX-License-Identifier: Apache-2.0 18 | 19 | use async_trait::async_trait; 20 | use edgehog_store::models::containers::network::NetworkStatus; 21 | 22 | use crate::{ 23 | network::Network, 24 | properties::{network::AvailableNetwork, AvailableProp, Client}, 25 | }; 26 | 27 | use super::{Context, Create, Resource, ResourceError, Result, State}; 28 | 29 | #[derive(Debug, Clone)] 30 | pub(crate) struct NetworkResource { 31 | pub(crate) network: Network, 32 | } 33 | 34 | impl NetworkResource { 35 | pub(crate) fn new(network: Network) -> Self { 36 | Self { network } 37 | } 38 | } 39 | 40 | #[async_trait] 41 | impl Resource for NetworkResource 42 | where 43 | D: Client + Sync + 'static, 44 | { 45 | async fn publish(ctx: Context<'_, D>) -> Result<()> { 46 | AvailableNetwork::new(&ctx.id) 47 | .send(ctx.device, false) 48 | .await?; 49 | 50 | ctx.store 51 | .update_network_status(ctx.id, NetworkStatus::Published) 52 | .await?; 53 | 54 | Ok(()) 55 | } 56 | } 57 | 58 | #[async_trait] 59 | impl Create for NetworkResource 60 | where 61 | D: Client + Sync + 'static, 62 | { 63 | async fn fetch(ctx: &mut Context<'_, D>) -> Result<(State, Self)> { 64 | let mut resource = ctx 65 | .store 66 | .find_network(ctx.id) 67 | .await? 68 | .ok_or(ResourceError::Missing { 69 | id: ctx.id, 70 | resource: "network", 71 | })?; 72 | 73 | let exists = resource.network.inspect(ctx.client).await?.is_some(); 74 | 75 | if exists { 76 | ctx.store 77 | .update_network_local_id(ctx.id, resource.network.id.id.clone()) 78 | .await?; 79 | 80 | Ok((State::Created, resource)) 81 | } else { 82 | Ok((State::Missing, resource)) 83 | } 84 | } 85 | async fn create(&mut self, ctx: &mut Context<'_, D>) -> Result<()> { 86 | self.network.create(ctx.client).await?; 87 | 88 | ctx.store 89 | .update_network_local_id(ctx.id, self.network.id.id.clone()) 90 | .await?; 91 | 92 | AvailableNetwork::new(&ctx.id) 93 | .send(ctx.device, true) 94 | .await?; 95 | 96 | ctx.store 97 | .update_network_status(ctx.id, NetworkStatus::Created) 98 | .await?; 99 | 100 | Ok(()) 101 | } 102 | 103 | async fn delete(&mut self, ctx: &mut Context<'_, D>) -> Result<()> { 104 | self.network.remove(ctx.client).await?; 105 | 106 | AvailableNetwork::new(&ctx.id).unset(ctx.device).await?; 107 | 108 | ctx.store.delete_network(ctx.id).await?; 109 | 110 | Ok(()) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /edgehog-device-runtime-containers/src/resource/volume.rs: -------------------------------------------------------------------------------- 1 | // This file is part of Edgehog. 2 | // 3 | // Copyright 2025 SECO Mind Srl 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | // SPDX-License-Identifier: Apache-2.0 18 | 19 | use async_trait::async_trait; 20 | use edgehog_store::models::containers::volume::VolumeStatus; 21 | 22 | use crate::{ 23 | properties::{volume::AvailableVolume, AvailableProp, Client}, 24 | volume::Volume, 25 | }; 26 | 27 | use super::{Context, Create, Resource, ResourceError, Result, State}; 28 | 29 | #[derive(Debug, Clone)] 30 | pub(crate) struct VolumeResource { 31 | pub(crate) volume: Volume, 32 | } 33 | 34 | impl VolumeResource { 35 | pub(crate) fn new(volume: Volume) -> Self { 36 | Self { volume } 37 | } 38 | } 39 | 40 | #[async_trait] 41 | impl Resource for VolumeResource 42 | where 43 | D: Client + Sync + 'static, 44 | { 45 | async fn publish(ctx: Context<'_, D>) -> Result<()> { 46 | AvailableVolume::new(&ctx.id) 47 | .send(ctx.device, false) 48 | .await?; 49 | 50 | ctx.store 51 | .update_volume_status(ctx.id, VolumeStatus::Published) 52 | .await?; 53 | 54 | Ok(()) 55 | } 56 | } 57 | 58 | #[async_trait] 59 | impl Create for VolumeResource 60 | where 61 | D: Client + Sync + 'static, 62 | { 63 | async fn fetch(ctx: &mut Context<'_, D>) -> Result<(State, Self)> { 64 | let resource = ctx 65 | .store 66 | .find_volume(ctx.id) 67 | .await? 68 | .ok_or(ResourceError::Missing { 69 | id: ctx.id, 70 | resource: "volume", 71 | })?; 72 | 73 | let exists = resource.volume.inspect(ctx.client).await?.is_some(); 74 | 75 | if exists { 76 | Ok((State::Created, resource)) 77 | } else { 78 | Ok((State::Missing, resource)) 79 | } 80 | } 81 | 82 | async fn create(&mut self, ctx: &mut Context<'_, D>) -> Result<()> { 83 | self.volume.create(ctx.client).await?; 84 | 85 | AvailableVolume::new(&ctx.id).send(ctx.device, true).await?; 86 | 87 | ctx.store 88 | .update_volume_status(ctx.id, VolumeStatus::Created) 89 | .await?; 90 | 91 | Ok(()) 92 | } 93 | 94 | async fn delete(&mut self, ctx: &mut Context<'_, D>) -> Result<()> { 95 | self.volume.remove(ctx.client).await?; 96 | 97 | AvailableVolume::new(&ctx.id).unset(ctx.device).await?; 98 | 99 | ctx.store.delete_volume(ctx.id).await?; 100 | 101 | Ok(()) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /edgehog-device-runtime-forwarder/Cargo.toml: -------------------------------------------------------------------------------- 1 | # Copyright 2023 SECO Mind Srl 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | [package] 5 | name = "edgehog-device-runtime-forwarder" 6 | version = { workspace = true } 7 | edition = { workspace = true } 8 | homepage = { workspace = true } 9 | rust-version = { workspace = true } 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [dependencies] 14 | astarte-device-sdk = { workspace = true, features = ["derive"] } 15 | async-trait = { workspace = true } 16 | backoff = { workspace = true, features = ["tokio"] } 17 | displaydoc = { workspace = true } 18 | edgehog-device-forwarder-proto = { workspace = true } 19 | futures = { workspace = true } 20 | hex = { workspace = true } 21 | http = { workspace = true } 22 | httpmock = { workspace = true, optional = true } 23 | reqwest = { workspace = true, features = ["rustls-tls-native-roots-no-provider"] } 24 | rustls = { workspace = true } 25 | rustls-native-certs = { workspace = true } 26 | rustls-pemfile = { workspace = true } 27 | thiserror = { workspace = true } 28 | tokio = { workspace = true } 29 | tokio-tungstenite = { workspace = true, features = ["rustls-tls-native-roots", "url"] } 30 | tracing = { workspace = true, features = ["log"] } 31 | url = { workspace = true } 32 | 33 | [dev-dependencies] 34 | httpmock = { workspace = true } 35 | tokio = { workspace = true, features = ["test-util"] } 36 | tracing-subscriber = { workspace = true } 37 | 38 | [features] 39 | _test-utils = ["dep:httpmock", "tokio/test-util"] 40 | 41 | [[test]] 42 | name = "http-test" 43 | path = "tests/http_test.rs" 44 | required-features = ["_test-utils"] 45 | 46 | [[test]] 47 | name = "ws-test" 48 | path = "tests/ws_test.rs" 49 | required-features = ["_test-utils"] 50 | -------------------------------------------------------------------------------- /edgehog-device-runtime-forwarder/src/connection/http.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 SECO Mind Srl 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! Define the necessary structs and traits to represent an HTTP connection. 5 | 6 | use async_trait::async_trait; 7 | use tokio::sync::mpsc::Sender; 8 | use tracing::{debug, instrument, trace}; 9 | 10 | use super::{ 11 | Connection, ConnectionError, ConnectionHandle, Transport, TransportBuilder, WriteHandle, 12 | }; 13 | use crate::messages::{ 14 | Http as ProtoHttp, HttpMessage as ProtoHttpMessage, HttpRequest as ProtoHttpRequest, 15 | HttpResponse as ProtoHttpResponse, Id, ProtoMessage, 16 | }; 17 | 18 | /// Builder for an [`Http`] connection. 19 | #[derive(Debug)] 20 | pub(crate) struct HttpBuilder { 21 | request: reqwest::RequestBuilder, 22 | } 23 | 24 | impl HttpBuilder { 25 | fn new(request: reqwest::RequestBuilder) -> Self { 26 | Self { request } 27 | } 28 | } 29 | 30 | #[async_trait] 31 | impl TransportBuilder for HttpBuilder { 32 | type Connection = Http; 33 | 34 | /// The id and the channel are only useful for [`WebSocket`] connections. 35 | async fn build( 36 | self, 37 | _id: &Id, 38 | _tx_ws: Sender, 39 | ) -> Result { 40 | Ok(Http::new(self.request)) 41 | } 42 | } 43 | 44 | impl TryFrom for HttpBuilder { 45 | type Error = ConnectionError; 46 | 47 | fn try_from(value: ProtoHttpRequest) -> Result { 48 | value 49 | .request_builder() 50 | .map(HttpBuilder::new) 51 | .map_err(ConnectionError::from) 52 | } 53 | } 54 | 55 | /// HTTP connection protocol 56 | #[derive(Debug)] 57 | pub(crate) struct Http { 58 | // to send the request the builder must be consumed, so the option can be replaced with None. 59 | request: Option, 60 | } 61 | 62 | impl Http { 63 | /// Store the HTTP request the connection will respond to once executed. 64 | pub(crate) fn new(request: reqwest::RequestBuilder) -> Self { 65 | Self { 66 | request: Some(request), 67 | } 68 | } 69 | } 70 | 71 | #[async_trait] 72 | impl Transport for Http { 73 | /// Send the [HTTP request](reqwest::Request), wait for a response and return it. 74 | #[instrument(skip(self))] 75 | async fn next(&mut self, id: &Id) -> Result, ConnectionError> { 76 | let Some(request) = self.request.take() else { 77 | return Ok(None); 78 | }; 79 | 80 | trace!("sending HTTP request"); 81 | match request.send().await { 82 | Ok(http_res) => { 83 | // create the protobuf response to be sent to Edgehog 84 | let proto_res = ProtoHttpResponse::from_reqw_response(http_res).await?; 85 | 86 | let proto_msg = ProtoMessage::Http(ProtoHttp::new( 87 | id.clone(), 88 | ProtoHttpMessage::Response(proto_res), 89 | )); 90 | 91 | Ok(Some(proto_msg)) 92 | } 93 | Err(err) => { 94 | debug!("HTTP request failed: {err}"); 95 | let proto_msg = ProtoMessage::Http(ProtoHttp::bad_gateway(id.clone())); 96 | Ok(Some(proto_msg)) 97 | } 98 | } 99 | } 100 | } 101 | 102 | impl Connection { 103 | /// Initialize a new Http connection. 104 | #[instrument(skip(tx_ws, http_req))] 105 | pub(crate) fn with_http( 106 | id: Id, 107 | tx_ws: Sender, 108 | http_req: ProtoHttpRequest, 109 | ) -> Result { 110 | let http_builder = HttpBuilder::try_from(http_req)?; 111 | let con = Self::new(id, tx_ws, http_builder); 112 | Ok(con.spawn(WriteHandle::Http)) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /edgehog-device-runtime-forwarder/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 SECO Mind Srl 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | #![warn(missing_docs, rustdoc::missing_crate_level_docs)] 5 | 6 | //! Edgehog Device Runtime Forwarder 7 | //! 8 | //! Implement forwarder functionality on a device. 9 | 10 | pub mod astarte; 11 | pub mod collection; 12 | pub mod connection; 13 | pub mod connections_manager; 14 | mod messages; 15 | pub mod tls; 16 | 17 | // re-exported dependencies 18 | pub use astarte_device_sdk; 19 | 20 | #[cfg(feature = "_test-utils")] 21 | pub mod test_utils; 22 | -------------------------------------------------------------------------------- /edgehog-device-runtime-forwarder/src/tls.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 SECO Mind Srl 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! Module implementing TLS functionality. 5 | //! 6 | //! This module provides the necessary functionalities to establish a TLS layer on top of the 7 | //! WebSocket communication between a device and Edgehog. 8 | 9 | use rustls::{ClientConfig, RootCertStore}; 10 | use std::io::BufReader; 11 | use std::sync::Arc; 12 | 13 | use tokio_tungstenite::Connector; 14 | use tracing::{debug, info, instrument, warn}; 15 | 16 | /// TLS error 17 | #[derive(displaydoc::Display, thiserror::Error, Debug)] 18 | pub enum Error { 19 | /// Error while loading digital certificate or private key. 20 | WrongItem(String), 21 | 22 | /// Error while establishing a TLS connection. 23 | RustTls(#[from] rustls::Error), 24 | 25 | /// Error while accepting a TLS connection. 26 | AcceptTls(#[source] std::io::Error), 27 | 28 | /// Error while reading from file. 29 | ReadFile(#[source] std::io::Error), 30 | 31 | /// Read certificate 32 | ReadCert(#[source] std::io::Error), 33 | 34 | /// Couldn't load root certificate. 35 | RootCert(#[source] rustls::Error), 36 | } 37 | 38 | /// Given the CA certificate, compute the device TLS configuration and return a Device connector. 39 | #[instrument(skip_all)] 40 | pub fn device_tls_config() -> Result { 41 | let mut root_certs = RootCertStore::empty(); 42 | 43 | // add native root certificates 44 | let load_native_certs = rustls_native_certs::load_native_certs(); 45 | 46 | for err in load_native_certs.errors { 47 | warn!("couldn't load a certificate: {err}"); 48 | } 49 | 50 | for cert in load_native_certs.certs { 51 | root_certs.add(cert)?; 52 | } 53 | debug!("native root certificates added"); 54 | 55 | // add custom roots certificate if necessary 56 | if let Some(ca_cert_file) = option_env!("EDGEHOG_FORWARDER_CA_PATH") { 57 | // I'm using std::fs because rustls-pemfile requires a sync read call 58 | info!("{ca_cert_file}"); 59 | let file = std::fs::File::open(ca_cert_file).map_err(Error::ReadFile)?; 60 | let mut reader = BufReader::new(file); 61 | 62 | let certs = rustls_pemfile::certs(&mut reader); 63 | for cert in certs { 64 | let cert = cert.map_err(Error::ReadCert)?; 65 | root_certs.add(cert)?; 66 | debug!("added cert to root certificates"); 67 | } 68 | } 69 | 70 | let config = ClientConfig::builder() 71 | .with_root_certificates(root_certs) 72 | .with_no_client_auth(); 73 | 74 | Ok(Connector::Rustls(Arc::new(config))) 75 | } 76 | -------------------------------------------------------------------------------- /edgehog-device-runtime-forwarder/tests/http_test.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 SECO Mind Srl 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use edgehog_device_forwarder_proto::http::Message as HttpMessage; 5 | use edgehog_device_forwarder_proto::{ 6 | http::Response as HttpResponse, message::Protocol, prost::Message, Http, 7 | Message as ProtoMessage, 8 | }; 9 | use edgehog_device_runtime_forwarder::test_utils::create_http_req; 10 | use futures::{SinkExt, StreamExt}; 11 | 12 | #[tokio::test] 13 | async fn test_connect() { 14 | use edgehog_device_runtime_forwarder::test_utils::TestConnections; 15 | 16 | // TODO: in the feature this will change, for now just set the default to make the tests pass 17 | // Set default crypto provider 18 | if rustls::crypto::CryptoProvider::get_default().is_none() { 19 | let _ = rustls::crypto::aws_lc_rs::default_provider() 20 | .install_default() 21 | .inspect_err(|_| tracing::error!("couldn't install default crypto provider")); 22 | } 23 | 24 | let test_connections = TestConnections::::init().await; 25 | let endpoint = test_connections.endpoint(); 26 | 27 | let test_url = test_connections 28 | .mock_server 29 | .url("/remote-terminal?session=abcd"); 30 | 31 | let mut ws = test_connections.mock_ws_server().await; 32 | 33 | let request_id = "3647edbb-6747-4827-a3ef-dbb6239e3326".as_bytes().to_vec(); 34 | let http_req = create_http_req(request_id, &test_url, Vec::new()); 35 | 36 | ws.send(http_req.clone()) 37 | .await 38 | .expect("failed to send over ws"); 39 | 40 | // send again another request with the same ID. This should cause an IdAlreadyUsed error 41 | // which is internally handled (print in debug mode) 42 | ws.send(http_req).await.expect("failed to send over ws"); 43 | 44 | // the 1st request is correctly handled 45 | let http_res = ws 46 | .next() 47 | .await 48 | .expect("ws already closed") 49 | .expect("failed to receive from ws") 50 | .into_data(); 51 | 52 | let proto_res = 53 | ProtoMessage::decode(http_res).expect("failed to decode tung message into protobuf"); 54 | 55 | assert!(matches!( 56 | proto_res, 57 | ProtoMessage { 58 | protocol: Some(Protocol::Http(Http { 59 | message: Some(HttpMessage::Response(HttpResponse { 60 | status_code: 200, 61 | .. 62 | })), 63 | .. 64 | })), 65 | } 66 | )); 67 | 68 | ws.close(None).await.expect("failed to close ws"); 69 | 70 | endpoint.assert(); 71 | test_connections.assert().await; 72 | } 73 | 74 | #[tokio::test] 75 | async fn test_max_sizes() { 76 | use edgehog_device_runtime_forwarder::test_utils::TestConnections; 77 | 78 | let test_connections = TestConnections::::init().await; 79 | let mut ws = test_connections.mock_ws_server().await; 80 | 81 | let request_id = "3647edbb-6747-4827-a3ef-dbb6239e3326".as_bytes().to_vec(); 82 | let test_url = test_connections 83 | .mock_server 84 | .url("/remote-terminal?session=abcd"); 85 | // create an HTTP request with a big body 86 | let http_req = create_http_req(request_id.clone(), &test_url, vec![0u8; 16777216]); 87 | 88 | // sending a frame bigger than the maximum frame size will cause a connection reset error. 89 | let res = ws.send(http_req.clone()).await; 90 | assert!(res.is_err(), "expected error {res:?}"); 91 | } 92 | -------------------------------------------------------------------------------- /edgehog-device-runtime-forwarder/tests/ws_test.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 SECO Mind Srl 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use edgehog_device_forwarder_proto as proto; 5 | use edgehog_device_forwarder_proto::message::Protocol; 6 | use edgehog_device_forwarder_proto::{ 7 | message::Protocol as ProtobufProtocol, web_socket::Message as ProtobufWsMessage, 8 | WebSocket as ProtobufWebSocket, 9 | }; 10 | use tokio_tungstenite::tungstenite::Bytes; 11 | use tokio_tungstenite::tungstenite::Message as TungMessage; 12 | 13 | use edgehog_device_runtime_forwarder::test_utils::create_ws_msg; 14 | use edgehog_device_runtime_forwarder::test_utils::{ 15 | create_http_upgrade_req, is_ws_upgrade_response, send_ws_and_wait_next, MockWebSocket, 16 | TestConnections, 17 | }; 18 | 19 | #[tokio::test] 20 | async fn test_internal_ws() { 21 | // Mock server for ttyd 22 | let mut test_connections = TestConnections::::init().await; 23 | 24 | // Edgehog 25 | let mut ws_bridge = test_connections.mock_ws_server().await; 26 | // Device listener (waiting for Edgehog -> Device) 27 | let listener = test_connections.mock_server.device_listener().unwrap(); 28 | let connection_handle = MockWebSocket::open_ws_device(listener); 29 | 30 | let request_id = "3647edbb-6747-4827-a3ef-dbb6239e3326".as_bytes().to_vec(); 31 | let url = format!( 32 | "ws://localhost:{}/remote-terminal?session=abcd", 33 | test_connections.mock_server.port().unwrap() 34 | ); 35 | let http_req = create_http_upgrade_req(request_id.clone(), &url) 36 | .expect("failed to create http upgrade request"); 37 | 38 | let protobuf_res = send_ws_and_wait_next(&mut ws_bridge, http_req).await; 39 | 40 | let (socket_id, protobuf_http) = match protobuf_res.protocol.unwrap() { 41 | Protocol::Http(http) => (http.request_id, http.message.unwrap()), 42 | Protocol::Ws(_) => panic!("should be http upgrade response"), 43 | }; 44 | 45 | // check if the response id is the same as the request one 46 | assert_eq!(request_id, socket_id); 47 | assert!(is_ws_upgrade_response(protobuf_http)); 48 | 49 | // retrieve the ID of the connection and try sending a WebSocket message 50 | test_connections.mock(connection_handle).await; 51 | 52 | let content = Bytes::from_static(b"data"); 53 | let data = TungMessage::Binary(content.clone()); 54 | let ws_binary_msg = create_ws_msg(socket_id.clone(), data); 55 | let protobuf_res = send_ws_and_wait_next(&mut ws_bridge, ws_binary_msg).await; 56 | 57 | assert_eq!( 58 | protobuf_res, 59 | proto::Message { 60 | protocol: Some(ProtobufProtocol::Ws(ProtobufWebSocket { 61 | message: Some(ProtobufWsMessage::Binary(content.to_vec())), 62 | socket_id: socket_id.clone() 63 | })) 64 | } 65 | ); 66 | 67 | // sending ping 68 | let content = Bytes::from_static(b"ping"); 69 | let data = TungMessage::Ping(content.clone()); 70 | let ws_ping_msg = create_ws_msg(socket_id.clone(), data.clone()); 71 | let protobuf_res = send_ws_and_wait_next(&mut ws_bridge, ws_ping_msg).await; 72 | 73 | // check that a pong is received 74 | assert_eq!( 75 | protobuf_res, 76 | proto::Message { 77 | protocol: Some(ProtobufProtocol::Ws(ProtobufWebSocket { 78 | message: Some(ProtobufWsMessage::Pong(content.to_vec())), 79 | socket_id: socket_id.clone() 80 | })) 81 | } 82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /edgehog-device-runtime-store/Cargo.toml: -------------------------------------------------------------------------------- 1 | # This file is part of Edgehog. 2 | # 3 | # Copyright 2025 SECO Mind Srl 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # SPDX-License-Identifier: Apache-2.0 18 | 19 | [package] 20 | name = "edgehog-device-runtime-store" 21 | version.workspace = true 22 | edition.workspace = true 23 | homepage.workspace = true 24 | rust-version.workspace = true 25 | 26 | [dependencies] 27 | diesel = { workspace = true, features = ["sqlite", "uuid"] } 28 | diesel_migrations = { workspace = true, features = ["sqlite"] } 29 | displaydoc = { workspace = true } 30 | rusqlite = { workspace = true, features = ["modern_sqlite", "buildtime_bindgen"] } 31 | sync_wrapper = { workspace = true } 32 | thiserror = { workspace = true } 33 | tokio = { workspace = true, features = ["rt-multi-thread", "sync"] } 34 | tracing = { workspace = true } 35 | uuid = { workspace = true } 36 | 37 | [dev-dependencies] 38 | tempfile = { workspace = true } 39 | 40 | [features] 41 | containers = [] 42 | vendored = ["rusqlite/bundled"] 43 | -------------------------------------------------------------------------------- /edgehog-device-runtime-store/README.md: -------------------------------------------------------------------------------- 1 | 20 | 21 | # edgehog-device-runtime-containers-store 22 | 23 | Separate crate with the persistent state for the container service. 24 | 25 | This is in its own separate crate to reduce the compile times for the main container service. 26 | 27 | ## Tools 28 | 29 | It uses the [diesel](https://docs.rs/diesel/latest/diesel/) ORM to generate safe SQL queries. 30 | 31 | To generate new migration you'll need the `diesel-cli` 32 | 33 | ```sh 34 | export DATABASE_URL=sqlite:///tmp/edgehog-containers/state.db 35 | diesel migration generate create_some_migration 36 | ``` 37 | 38 | There are also script to run in the `scripts` directory to generate the SQLite file or run all the 39 | migrations. 40 | -------------------------------------------------------------------------------- /edgehog-device-runtime-store/assets/init-reader.sql: -------------------------------------------------------------------------------- 1 | -- This file is part of Edgehog. 2 | -- 3 | -- Copyright 2025 SECO Mind Srl 4 | -- 5 | -- Licensed under the Apache License, Version 2.0 (the "License"); 6 | -- you may not use this file except in compliance with the License. 7 | -- You may obtain a copy of the License at 8 | -- 9 | -- http://www.apache.org/licenses/LICENSE-2.0 10 | -- 11 | -- Unless required by applicable law or agreed to in writing, software 12 | -- distributed under the License is distributed on an "AS IS" BASIS, 13 | -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | -- See the License for the specific language governing permissions and 15 | -- limitations under the License. 16 | -- 17 | -- SPDX-License-Identifier: Apache-2.0 18 | 19 | -- Reader config per connection 20 | PRAGMA foreign_keys = ON; 21 | PRAGMA temp_store = MEMORY; 22 | PRAGMA busy_timeout = 5000; 23 | PRAGMA cache_size = -2000; 24 | -------------------------------------------------------------------------------- /edgehog-device-runtime-store/assets/init-writer.sql: -------------------------------------------------------------------------------- 1 | -- This file is part of Edgehog. 2 | -- 3 | -- Copyright 2025 SECO Mind Srl 4 | -- 5 | -- Licensed under the Apache License, Version 2.0 (the "License"); 6 | -- you may not use this file except in compliance with the License. 7 | -- You may obtain a copy of the License at 8 | -- 9 | -- http://www.apache.org/licenses/LICENSE-2.0 10 | -- 11 | -- Unless required by applicable law or agreed to in writing, software 12 | -- distributed under the License is distributed on an "AS IS" BASIS, 13 | -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | -- See the License for the specific language governing permissions and 15 | -- limitations under the License. 16 | -- 17 | -- SPDX-License-Identifier: Apache-2.0 18 | 19 | -- writer config 20 | PRAGMA journal_mode = WAL; 21 | PRAGMA synchronous = NORMAL; 22 | -- 64 megabytes default is -1 23 | PRAGMA journal_size_limit = 67108864; 24 | -- Reduces the size of the database 25 | PRAGMA auto_vacuum = INCREMENTAL; 26 | -- Per connection 27 | PRAGMA foreign_keys = ON; 28 | PRAGMA temp_store = MEMORY; 29 | PRAGMA busy_timeout = 5000; 30 | PRAGMA cache_size = -2000; 31 | 32 | VACUUM; 33 | -------------------------------------------------------------------------------- /edgehog-device-runtime-store/assets/schema.patch: -------------------------------------------------------------------------------- 1 | diff --git a/edgehog-device-runtime-store/src/schema/containers.rs b/edgehog-device-runtime-store/src/schema/containers.rs 2 | index 4fb7c1d2ed..e394092d6e 100644 3 | --- a/edgehog-device-runtime-store/src/schema/containers.rs 4 | +++ b/edgehog-device-runtime-store/src/schema/containers.rs 5 | @@ -1,3 +1,23 @@ 6 | +// This file is part of Edgehog. 7 | +// 8 | +// Copyright 2025 SECO Mind Srl 9 | +// 10 | +// Licensed under the Apache License, Version 2.0 (the "License"); 11 | +// you may not use this file except in compliance with the License. 12 | +// You may obtain a copy of the License at 13 | +// 14 | +// http://www.apache.org/licenses/LICENSE-2.0 15 | +// 16 | +// Unless required by applicable law or agreed to in writing, software 17 | +// distributed under the License is distributed on an "AS IS" BASIS, 18 | +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 19 | +// See the License for the specific language governing permissions and 20 | +// limitations under the License. 21 | +// 22 | +// SPDX-License-Identifier: Apache-2.0 23 | + 24 | +//! Schema for the containers tables 25 | + 26 | // @generated automatically by Diesel CLI. 27 | 28 | diesel::table! { 29 | -------------------------------------------------------------------------------- /edgehog-device-runtime-store/diesel.toml: -------------------------------------------------------------------------------- 1 | # This file is part of Edgehog. 2 | # 3 | # Copyright 2025 SECO Mind Srl 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # SPDX-License-Identifier: Apache-2.0 18 | 19 | # For documentation on how to configure this file, 20 | # see https://diesel.rs/guides/configuring-diesel-cli 21 | 22 | [print_schema] 23 | file = "src/schema/containers.rs" 24 | custom_type_derives = ["diesel::query_builder::QueryId", "Clone"] 25 | with_docs = true 26 | patch_file = "assets/schema.patch" 27 | 28 | [migrations_directory] 29 | dir = "migrations" 30 | -------------------------------------------------------------------------------- /edgehog-device-runtime-store/migrations/2024-11-29-163409_initial/down.sql: -------------------------------------------------------------------------------- 1 | -- This file is part of Edgehog. 2 | -- 3 | -- Copyright 2025 SECO Mind Srl 4 | -- 5 | -- Licensed under the Apache License, Version 2.0 (the "License"); 6 | -- you may not use this file except in compliance with the License. 7 | -- You may obtain a copy of the License at 8 | -- 9 | -- http://www.apache.org/licenses/LICENSE-2.0 10 | -- 11 | -- Unless required by applicable law or agreed to in writing, software 12 | -- distributed under the License is distributed on an "AS IS" BASIS, 13 | -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | -- See the License for the specific language governing permissions and 15 | -- limitations under the License. 16 | -- 17 | -- SPDX-License-Identifier: Apache-2.0 18 | 19 | -- This file should undo anything in `up.sql` 20 | DROP TABLE deployment_containers; 21 | DROP TABLE deployments; 22 | DROP TABLE container_port_bindings; 23 | DROP TABLE container_binds; 24 | DROP TABLE container_env; 25 | DROP TABLE container_missing_images; 26 | DROP TABLE container_missing_networks; 27 | DROP TABLE container_missing_volumes; 28 | DROP TABLE container_volumes; 29 | DROP TABLE container_networks; 30 | DROP TABLE containers; 31 | DROP TABLE volume_driver_opts; 32 | DROP TABLE volumes; 33 | DROP TABLE network_driver_opts; 34 | DROP TABLE networks; 35 | DROP TABLE images; 36 | -------------------------------------------------------------------------------- /edgehog-device-runtime-store/scripts/create-db.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This file is part of Edgehog. 4 | # 5 | # Copyright 2025 SECO Mind Srl 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | # SPDX-License-Identifier: Apache-2.0 20 | 21 | set -exEuo pipefail 22 | 23 | dbfile=$(echo "$DATABASE_URL" | sed -e's|^sqlite://||') 24 | 25 | sqlite3 "$dbfile" ) -> Self { 41 | Self(uuid.into()) 42 | } 43 | } 44 | 45 | impl Deref for SqlUuid { 46 | type Target = Uuid; 47 | 48 | fn deref(&self) -> &Self::Target { 49 | &self.0 50 | } 51 | } 52 | 53 | impl Borrow for SqlUuid { 54 | fn borrow(&self) -> &Uuid { 55 | &self.0 56 | } 57 | } 58 | 59 | impl From<&Uuid> for SqlUuid { 60 | fn from(value: &Uuid) -> Self { 61 | SqlUuid(*value) 62 | } 63 | } 64 | 65 | impl From for SqlUuid { 66 | fn from(value: Uuid) -> Self { 67 | SqlUuid(value) 68 | } 69 | } 70 | impl From for Uuid { 71 | fn from(value: SqlUuid) -> Self { 72 | value.0 73 | } 74 | } 75 | 76 | impl Display for SqlUuid { 77 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 78 | write!(f, "{}", self.0) 79 | } 80 | } 81 | 82 | impl FromSql for SqlUuid { 83 | fn from_sql(bytes: ::RawValue<'_>) -> diesel::deserialize::Result { 84 | let data = Vec::::from_sql(bytes)?; 85 | 86 | Uuid::from_slice(&data).map(SqlUuid).map_err(Into::into) 87 | } 88 | } 89 | 90 | impl ToSql for SqlUuid { 91 | fn to_sql<'b>( 92 | &'b self, 93 | out: &mut diesel::serialize::Output<'b, '_, Sqlite>, 94 | ) -> diesel::serialize::Result { 95 | let bytes = self.as_bytes().as_slice(); 96 | 97 | <[u8] as ToSql>::to_sql(bytes, out) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /edgehog-device-runtime-store/src/lib.rs: -------------------------------------------------------------------------------- 1 | // This file is part of Edgehog. 2 | // 3 | // Copyright 2024 - 2025 SECO Mind Srl 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | // SPDX-License-Identifier: Apache-2.0 18 | 19 | #![warn( 20 | missing_docs, 21 | rustdoc::missing_crate_level_docs, 22 | clippy::dbg_macro, 23 | clippy::todo 24 | )] 25 | 26 | //! Separate crate for the database definitions 27 | 28 | pub use diesel; 29 | 30 | pub mod conversions; 31 | pub mod db; 32 | pub mod models; 33 | pub mod schema; 34 | -------------------------------------------------------------------------------- /edgehog-device-runtime-store/src/models/containers/mod.rs: -------------------------------------------------------------------------------- 1 | // This file is part of Edgehog. 2 | // 3 | // Copyright 2024 - 2025 SECO Mind Srl 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | // SPDX-License-Identifier: Apache-2.0 18 | 19 | //! Container models definitions. 20 | 21 | pub mod container; 22 | pub mod deployment; 23 | pub mod image; 24 | pub mod network; 25 | pub mod volume; 26 | -------------------------------------------------------------------------------- /edgehog-device-runtime-store/src/models/mod.rs: -------------------------------------------------------------------------------- 1 | // This file is part of Edgehog. 2 | // 3 | // Copyright 2024 - 2025 SECO Mind Srl 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | // SPDX-License-Identifier: Apache-2.0 18 | 19 | //! Models for all the resources. 20 | 21 | use diesel::{ 22 | dsl::{exists, BareSelect, Eq, Filter}, 23 | query_dsl::methods::FilterDsl, 24 | sql_types::Binary, 25 | AppearsOnTable, Expression, ExpressionMethods, Table, 26 | }; 27 | 28 | use crate::conversions::SqlUuid; 29 | 30 | #[cfg(feature = "containers")] 31 | pub mod containers; 32 | 33 | type ById<'a, Id> = Eq; 34 | type FilterById<'a, Table, Id> = Filter>; 35 | type ExistsFilterById<'a, Table, Id> = BareSelect>>; 36 | 37 | /// General implementation for utility functions on a model. 38 | pub trait QueryModel { 39 | /// Table to generate the queries for the model 40 | type Table: Table + Default; 41 | /// Primary id of the table 42 | type Id: AppearsOnTable + Expression + Default; 43 | 44 | /// Query type returned for the exists method. 45 | type ExistsQuery<'a>; 46 | 47 | /// Returns the filter table by id. 48 | fn by_id(id: &SqlUuid) -> ById<'_, Self::Id> { 49 | Self::Id::default().eq(id) 50 | } 51 | 52 | /// Returns the filtered table by id. 53 | fn find_id<'a>(id: &'a SqlUuid) -> FilterById<'a, Self::Table, Self::Id> 54 | where 55 | Self::Table: FilterDsl>, 56 | { 57 | Self::Table::default().filter(Self::by_id(id)) 58 | } 59 | 60 | /// Returns the deployment exists query. 61 | /// 62 | // TODO: this could be made into a default implementation too if the trait bound are satisfied 63 | fn exists(id: &SqlUuid) -> Self::ExistsQuery<'_>; 64 | } 65 | -------------------------------------------------------------------------------- /edgehog-device-runtime-store/src/schema/mod.rs: -------------------------------------------------------------------------------- 1 | // This file is part of Edgehog. 2 | // 3 | // Copyright 2024 - 2025 SECO Mind Srl 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | // SPDX-License-Identifier: Apache-2.0 18 | 19 | //! Database schema definitions 20 | 21 | #[cfg(feature = "containers")] 22 | pub mod containers; 23 | 24 | /// Embedded migrations 25 | #[cfg(feature = "containers")] 26 | pub(crate) const CONTAINER_MIGRATIONS: diesel_migrations::EmbeddedMigrations = 27 | diesel_migrations::embed_migrations!("migrations"); 28 | -------------------------------------------------------------------------------- /hardware-id-service/Cargo.toml: -------------------------------------------------------------------------------- 1 | # This file is part of Edgehog. 2 | # 3 | # Copyright 2022 SECO Mind Srl 4 | # 5 | # SPDX-License-Identifier: CC0-1.0 6 | 7 | [package] 8 | name = "hardware-id-service" 9 | version = "0.1.0" 10 | edition = { workspace = true } 11 | publish = false 12 | rust-version = { workspace = true } 13 | 14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 15 | 16 | [dependencies] 17 | base64 = { workspace = true } 18 | clap = { workspace = true, features = ["derive"] } 19 | procfs = { workspace = true } 20 | stable-eyre = { workspace = true } 21 | tokio = { workspace = true, features = ["macros", "rt-multi-thread", "signal", "fs"] } 22 | tracing = { workspace = true } 23 | tracing-subscriber = { workspace = true, features = ["env-filter"] } 24 | uuid = { workspace = true, features = ["v5", "v4"] } 25 | zbus = { workspace = true, default-features = false, features = ["tokio"] } 26 | -------------------------------------------------------------------------------- /hardware-id-service/README.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # Hardware ID dbus Service Example 8 | 9 | Reference project of a service that either fetches hardware id from SMBIOS/DMI or from the kernel 10 | command line. 11 | 12 | ## Setup 13 | 14 | Copy `io.edgehog.Device.conf` to the dbus system configuration directory and reboot your system so 15 | dbus reloads its configuration. 16 | 17 | ```bash 18 | cp ./io.edgehog.Device.conf /etc/dbus-1/system.d/ 19 | reboot 20 | ``` 21 | 22 | Build with cargo 23 | 24 | ```bash 25 | cargo build --release 26 | ``` 27 | 28 | Run as superuser 29 | 30 | ```bash 31 | sudo target/release/hardware_id_service 32 | ``` 33 | 34 | ## Troubleshooting 35 | 36 | ### Unable to read file 37 | 38 | ```rust 39 | thread 'main' panicked at 'Unable to read file: Os { code: 13, kind: PermissionDenied, message: "Permission denied" }', src/main.rs:22:73 40 | ``` 41 | 42 | Run the service as superuser. 43 | 44 | ### Connection is not allowed to own the service 45 | 46 | ```rust 47 | Error: FDO(AccessDenied("Connection \":1.96\" is not allowed to own the service \"io.edgehog.Device\" due to security policies in the configuration file")) 48 | ``` 49 | 50 | Install dbus configuration file. 51 | -------------------------------------------------------------------------------- /hardware-id-service/io.edgehog.Device.conf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /hardware-id-service/scripts/get-hardware-id.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # This file is part of Edgehog. 3 | # 4 | # Copyright 2024 SECO Mind Srl 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | 20 | set -exEuo pipefail 21 | 22 | dbus-send --print-reply --dest=io.edgehog.Device /io/edgehog/Device io.edgehog.Device1.GetHardwareId \ 23 | 'string:f79ad91f-c638-4889-ae74-9d001a3b4cf8' 24 | -------------------------------------------------------------------------------- /led-manager-service/Cargo.toml: -------------------------------------------------------------------------------- 1 | # This file is part of Edgehog. 2 | # 3 | # Copyright 2022 SECO Mind Srl 4 | # 5 | # SPDX-License-Identifier: CC0-1.0 6 | 7 | [package] 8 | name = "led-manager-service" 9 | version = "0.1.0" 10 | edition = { workspace = true } 11 | publish = false 12 | rust-version = { workspace = true } 13 | 14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 15 | 16 | [dependencies] 17 | stable-eyre = { workspace = true } 18 | tokio = { workspace = true, features = ["macros", "rt-multi-thread", "signal"] } 19 | tracing = { workspace = true } 20 | tracing-subscriber = { workspace = true, features = ["env-filter"] } 21 | zbus = { workspace = true, default-features = false, features = ["tokio"] } 22 | -------------------------------------------------------------------------------- /led-manager-service/README.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # Led Manager D-Bus Service Example 8 | 9 | Reference project of a service that exposes available LEDs and allows to set them. 10 | 11 | **Caveat** 12 | 13 | In this example the `Set` method is mocked and always returns `true`. 14 | 15 | ## Setup 16 | 17 | Run with cargo 18 | 19 | ```bash 20 | cargo run --release 21 | ``` 22 | 23 | # Usage 24 | 25 | **Service name**: `io.edgehog.LedManager` 26 | 27 | **Service path**: `/io/edgehog/LedManager` 28 | 29 | **Service interface**: `io.edgehog.LedManager1` 30 | 31 | ## Methods 32 | 33 | ``` 34 | Insert ( IN String led_id); 35 | List (OUT Array led_id); 36 | Set (IN String led_id 37 | IN Boolean status 38 | OUT Boolean result); 39 | ``` 40 | 41 | ### Insert 42 | 43 | Add a new LED id. 44 | 45 | ```bash 46 | $ dbus-send --print-reply --dest=io.edgehog.LedManager \ 47 | /io/edgehog/LedManager io.edgehog.LedManager1.Insert \ 48 | string:gpio1 49 | 50 | method return time=1664275555.355858 sender=:1.1 -> destination=:1.3 serial=4 reply_serial=2 51 | ``` 52 | 53 | ### List 54 | 55 | List all available LEds. 56 | 57 | ```bash 58 | $ dbus-send --print-reply --dest=io.edgehog.LedManager \ 59 | /io/edgehog/LedManager io.edgehog.LedManager1.List 60 | 61 | method return time=1664275587.650016 sender=:1.1 -> destination=:1.4 serial=5 reply_serial=2 62 | array [ 63 | string "gpio1" 64 | ] 65 | ``` 66 | 67 | ### Set 68 | 69 | Set the status of the given LED. 70 | 71 | ```bash 72 | $ dbus-send --print-reply --dest=io.edgehog.LedManager \ 73 | /io/edgehog/LedManager io.edgehog.LedManager1.Set \ 74 | string:gpio1 boolean:true 75 | 76 | method return time=1664275640.809741 sender=:1.1 -> destination=:1.6 serial=6 reply_serial=2 77 | boolean true 78 | ``` 79 | -------------------------------------------------------------------------------- /led-manager-service/scripts/insert.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # This file is part of Edgehog. 3 | # 4 | # Copyright 2024 SECO Mind Srl 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | 20 | set -exEuo pipefail 21 | 22 | dbus-send --print-reply --dest=io.edgehog.LedManager \ 23 | /io/edgehog/LedManager io.edgehog.LedManager1.Insert \ 24 | string:gpio1 25 | -------------------------------------------------------------------------------- /led-manager-service/scripts/list.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # This file is part of Edgehog. 3 | # 4 | # Copyright 2024 SECO Mind Srl 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | 20 | set -exEuo pipefail 21 | 22 | dbus-send --print-reply --dest=io.edgehog.LedManager \ 23 | /io/edgehog/LedManager io.edgehog.LedManager1.List 24 | -------------------------------------------------------------------------------- /led-manager-service/scripts/set.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # This file is part of Edgehog. 3 | # 4 | # Copyright 2024 SECO Mind Srl 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | 20 | set -exEuo pipefail 21 | 22 | dbus-send --print-reply --dest=io.edgehog.LedManager \ 23 | /io/edgehog/LedManager io.edgehog.LedManager1.Set \ 24 | string:gpio1 boolean:true 25 | -------------------------------------------------------------------------------- /led-manager-service/src/main.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Edgehog. 3 | * 4 | * Copyright 2022 SECO Mind Srl 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | * 18 | * SPDX-License-Identifier: Apache-2.0 19 | */ 20 | 21 | use tracing::{debug, info, level_filters::LevelFilter}; 22 | use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; 23 | use zbus::interface; 24 | 25 | pub const SERVICE_NAME: &str = "io.edgehog.LedManager"; 26 | 27 | #[derive(Debug)] 28 | struct LedManager { 29 | leds: Vec, 30 | } 31 | 32 | #[interface(name = "io.edgehog.LedManager1")] 33 | impl LedManager { 34 | fn list(&self) -> Vec { 35 | debug!("listing {} leds", self.leds.len()); 36 | 37 | self.leds.clone() 38 | } 39 | 40 | fn insert(&mut self, id: String) { 41 | debug!("adding led {id}"); 42 | 43 | self.leds.push(id); 44 | } 45 | 46 | fn set(&self, id: String, status: bool) -> bool { 47 | const RESULT: bool = true; 48 | 49 | info!("SET {id} -> {status}: result {RESULT}"); 50 | 51 | RESULT 52 | } 53 | } 54 | 55 | #[tokio::main] 56 | async fn main() -> stable_eyre::Result<()> { 57 | stable_eyre::install()?; 58 | tracing_subscriber::registry() 59 | .with(tracing_subscriber::fmt::layer()) 60 | .with( 61 | EnvFilter::builder() 62 | .with_default_directive(LevelFilter::INFO.into()) 63 | .from_env_lossy(), 64 | ) 65 | .try_init()?; 66 | 67 | let leds = LedManager { leds: Vec::new() }; 68 | 69 | let _conn = zbus::connection::Builder::session()? 70 | .name(SERVICE_NAME)? 71 | .serve_at("/io/edgehog/LedManager", leds)? 72 | .build() 73 | .await?; 74 | 75 | info!("Service {SERVICE_NAME} started"); 76 | 77 | tokio::signal::ctrl_c().await?; 78 | 79 | Ok(()) 80 | } 81 | -------------------------------------------------------------------------------- /scripts/copyright.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This file is part of Edgehog. 4 | # 5 | # Copyright 2025 SECO Mind Srl 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | # SPDX-License-Identifier: Apache-2.0 20 | 21 | ## 22 | # Annotates the files passed from stdin 23 | # 24 | # For example 25 | # 26 | # git status --short | cut -f 2 -d ' ' | ./scripts/copyright.sh 27 | # 28 | 29 | set -exEuo pipefail 30 | 31 | annotate() { 32 | reuse annotate \ 33 | --copyright 'SECO Mind Srl' \ 34 | --copyright-prefix string \ 35 | --merge-copyrights \ 36 | --license 'Apache-2.0' \ 37 | --template apache-2 \ 38 | "$@" 39 | } 40 | 41 | if [[ $# != 0 ]]; then 42 | annotate "$@" 43 | 44 | exit 45 | fi 46 | 47 | # Read from stdin line by line 48 | while read -r line; do 49 | if [[ $line == '' ]]; then 50 | echo "Empty line, skipping" 1>&2 51 | continue 52 | fi 53 | 54 | annotate "$line" 55 | done " 24 | exit 1 25 | fi 26 | 27 | KEY=$(realpath -e "$1") 28 | INTERFACES=$(realpath -e "$2") 29 | 30 | export RUST_LOG=${RUST_LOG:-debug} 31 | 32 | astartectl realm-management interfaces sync -y \ 33 | -u http://api.astarte.localhost \ 34 | -r test \ 35 | -k "$KEY" \ 36 | "$INTERFACES"/*.json 37 | 38 | export E2E_REALM_NAME='test' 39 | export E2E_ASTARTE_API_URL='http://api.astarte.localhost' 40 | export E2E_IGNORE_SSL=true 41 | export E2E_INTERFACE_DIR=$INTERFACES 42 | 43 | E2E_DEVICE_ID="$(astartectl utils device-id generate-random)" 44 | E2E_CREDENTIALS_SECRET="$(astartectl pairing agent register --compact-output -r test -u http://api.astarte.localhost -k "$KEY" -- "$E2E_DEVICE_ID")" 45 | E2E_TOKEN="$(astartectl utils gen-jwt all-realm-apis -u http://api.astarte.localhost -k "$KEY")" 46 | E2E_STORE_DIR="$(mktemp -d)" 47 | 48 | export E2E_DEVICE_ID 49 | export E2E_CREDENTIALS_SECRET 50 | export E2E_TOKEN 51 | export E2E_STORE_DIR 52 | 53 | cargo e2e-test 54 | -------------------------------------------------------------------------------- /scripts/register-device.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # This file is part of Edgehog. 3 | # 4 | # Copyright 2024 SECO Mind Srl 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | 20 | #### 21 | # Wrapper to run a command with the environment variables set to have a device registered with 22 | # astarte. 23 | # 24 | # This scrips need the following environment variables to be set: 25 | # 26 | # - KEY: path to the private key for astarte 27 | # - INTERFACES_DIR: path to the interfaces to sync with astarte 28 | # 29 | # Example: 30 | # 31 | # ./scripts/register-device.sh cargo run --example retention 32 | 33 | set -exEuo pipefail 34 | 35 | if [[ -z $KEY ]]; then 36 | echo "Export the \$KEY environment variable as the path to the private key for astarte" 37 | exit 1 38 | fi 39 | 40 | export RUST_LOG=${RUST_LOG:-debug} 41 | astartectl realm-management interfaces sync -y \ 42 | -u http://api.astarte.localhost \ 43 | -r test \ 44 | -k "$KEY" \ 45 | "$INTERFACES"/*.json 46 | 47 | export EDGEHOG_REALM='test' 48 | export EDGEHOG_API_URL='http://api.astarte.localhost/appengine' 49 | export EDGEHOG_PAIRING_URL='http://api.astarte.localhost/pairing' 50 | export EDGEHOG_IGNORE_SSL=true 51 | export EDGEHOG_INTERFACES_DIR=$INTERFACES 52 | 53 | EDGEHOG_DEVICE_ID="$(astartectl utils device-id generate-random)" 54 | EDGEHOG_CREDENTIALS_SECRET="$(astartectl pairing agent register --compact-output -r test -u http://api.astarte.localhost -k "$KEY" -- "$EDGEHOG_DEVICE_ID")" 55 | EDGEHOG_TOKEN="$(astartectl utils gen-jwt all-realm-apis -u http://api.astarte.localhost -k "$KEY")" 56 | EDGEHOG_STORE_DIR="$(mktemp -d)" 57 | 58 | export EDGEHOG_DEVICE_ID 59 | export EDGEHOG_CREDENTIALS_SECRET 60 | export EDGEHOG_TOKEN 61 | export EDGEHOG_STORE_DIR 62 | 63 | "$@" 64 | -------------------------------------------------------------------------------- /src/commands.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Edgehog. 3 | * 4 | * Copyright 2022 SECO Mind Srl 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | * 18 | * SPDX-License-Identifier: Apache-2.0 19 | */ 20 | 21 | use astarte_device_sdk::{types::TypeError, AstarteType, FromEvent}; 22 | use tracing::error; 23 | 24 | use crate::error::DeviceManagerError; 25 | 26 | #[derive(Debug, Clone, FromEvent, PartialEq, Eq)] 27 | #[from_event( 28 | interface = "io.edgehog.devicemanager.Commands", 29 | aggregation = "individual" 30 | )] 31 | pub enum Commands { 32 | #[mapping(endpoint = "/request")] 33 | Request(CmdReq), 34 | } 35 | 36 | impl Commands { 37 | /// Returns `true` if the cmd req is [`Reboot`]. 38 | /// 39 | /// [`Reboot`]: CmdReq::Reboot 40 | #[cfg(all(feature = "zbus", target_os = "linux"))] 41 | #[must_use] 42 | pub fn is_reboot(&self) -> bool { 43 | matches!(self, Self::Request(CmdReq::Reboot)) 44 | } 45 | } 46 | 47 | #[derive(Debug, Clone, PartialEq, Eq)] 48 | pub enum CmdReq { 49 | Reboot, 50 | } 51 | 52 | impl TryFrom for CmdReq { 53 | type Error = TypeError; 54 | 55 | fn try_from(value: AstarteType) -> Result { 56 | let value = String::try_from(value)?; 57 | 58 | match value.as_str() { 59 | "Reboot" => Ok(CmdReq::Reboot), 60 | _ => { 61 | error!("unrecognize Commands request value {value}"); 62 | 63 | Err(TypeError::Conversion) 64 | } 65 | } 66 | } 67 | } 68 | 69 | /// handle io.edgehog.devicemanager.Commands 70 | pub(crate) async fn execute_command(cmd: Commands) -> Result<(), DeviceManagerError> { 71 | match cmd { 72 | Commands::Request(CmdReq::Reboot) => crate::power_management::reboot().await, 73 | } 74 | } 75 | 76 | #[cfg(test)] 77 | mod tests { 78 | use astarte_device_sdk::{DeviceEvent, Value}; 79 | 80 | use crate::controller::event::RuntimeEvent; 81 | 82 | use super::*; 83 | 84 | #[test] 85 | fn should_convert_command_from_event() { 86 | let event = DeviceEvent { 87 | interface: "io.edgehog.devicemanager.Commands".to_string(), 88 | path: "/request".to_string(), 89 | data: Value::Individual("Reboot".into()), 90 | }; 91 | 92 | let res = RuntimeEvent::from_event(event).unwrap(); 93 | 94 | assert_eq!( 95 | res, 96 | RuntimeEvent::Command(Commands::Request(CmdReq::Reboot)) 97 | ); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/controller/actor.rs: -------------------------------------------------------------------------------- 1 | // This file is part of Edgehog. 2 | // 3 | // Copyright 2024 SECO Mind Srl 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | // SPDX-License-Identifier: Apache-2.0 18 | 19 | //! Trait to generalize one task on the runtime. 20 | 21 | use async_trait::async_trait; 22 | use stable_eyre::eyre::Context; 23 | use tokio::sync::mpsc; 24 | use tracing::{debug, trace}; 25 | 26 | #[async_trait] 27 | pub trait Actor: Sized { 28 | type Msg: Send + 'static; 29 | 30 | fn task() -> &'static str; 31 | 32 | async fn init(&mut self) -> stable_eyre::Result<()>; 33 | 34 | async fn handle(&mut self, msg: Self::Msg) -> stable_eyre::Result<()>; 35 | 36 | async fn spawn(mut self, mut channel: mpsc::Receiver) -> stable_eyre::Result<()> { 37 | self.init() 38 | .await 39 | .wrap_err_with(|| format!("init for {} task failed", Self::task()))?; 40 | 41 | while let Some(msg) = channel.recv().await { 42 | trace!("message received for {} task", Self::task()); 43 | 44 | self.handle(msg) 45 | .await 46 | .wrap_err_with(|| format!("handle for {} task failed", Self::task()))?; 47 | } 48 | 49 | debug!("task {} disconnected, closing", Self::task()); 50 | 51 | Ok(()) 52 | } 53 | 54 | #[cfg(feature = "containers")] 55 | async fn spawn_unbounded( 56 | mut self, 57 | mut channel: mpsc::UnboundedReceiver, 58 | ) -> stable_eyre::Result<()> { 59 | self.init() 60 | .await 61 | .wrap_err_with(|| format!("init for {} task failed", Self::task()))?; 62 | 63 | while let Some(msg) = channel.recv().await { 64 | trace!("message received for {} task", Self::task()); 65 | 66 | self.handle(msg) 67 | .await 68 | .wrap_err_with(|| format!("handle for {} task failed", Self::task()))?; 69 | } 70 | 71 | debug!("task {} disconnected, closing", Self::task()); 72 | 73 | Ok(()) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/controller/event.rs: -------------------------------------------------------------------------------- 1 | // This file is part of Edgehog. 2 | // 3 | // Copyright 2024 SECO Mind Srl 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | // SPDX-License-Identifier: Apache-2.0 18 | 19 | use astarte_device_sdk::{event::FromEventError, DeviceEvent, FromEvent}; 20 | 21 | use crate::{commands::Commands, telemetry::event::TelemetryEvent}; 22 | 23 | #[derive(Debug, Clone, PartialEq)] 24 | pub enum RuntimeEvent { 25 | Command(Commands), 26 | Telemetry(TelemetryEvent), 27 | #[cfg(all(feature = "zbus", target_os = "linux"))] 28 | Ota(crate::ota::event::OtaRequest), 29 | #[cfg(all(feature = "zbus", target_os = "linux"))] 30 | Led(crate::led_behavior::LedEvent), 31 | #[cfg(all(feature = "containers", target_os = "linux"))] 32 | Container(Box), 33 | #[cfg(feature = "forwarder")] 34 | Session(edgehog_forwarder::astarte::SessionInfo), 35 | } 36 | 37 | impl FromEvent for RuntimeEvent { 38 | type Err = FromEventError; 39 | 40 | fn from_event(event: DeviceEvent) -> Result { 41 | match event.interface.as_str() { 42 | "io.edgehog.devicemanager.Commands" => { 43 | Commands::from_event(event).map(RuntimeEvent::Command) 44 | } 45 | "io.edgehog.devicemanager.config.Telemetry" => { 46 | TelemetryEvent::from_event(event).map(RuntimeEvent::Telemetry) 47 | } 48 | #[cfg(all(feature = "zbus", target_os = "linux"))] 49 | "io.edgehog.devicemanager.LedBehavior" => { 50 | crate::led_behavior::LedEvent::from_event(event).map(RuntimeEvent::Led) 51 | } 52 | #[cfg(all(feature = "zbus", target_os = "linux"))] 53 | "io.edgehog.devicemanager.OTARequest" => { 54 | crate::ota::event::OtaRequest::from_event(event).map(RuntimeEvent::Ota) 55 | } 56 | #[cfg(all(feature = "containers", target_os = "linux"))] 57 | interface if interface.starts_with("io.edgehog.devicemanager.apps") => { 58 | edgehog_containers::requests::ContainerRequest::from_event(event) 59 | .map(|event| RuntimeEvent::Container(Box::new(event))) 60 | } 61 | #[cfg(feature = "forwarder")] 62 | "io.edgehog.devicemanager.ForwarderSessionRequest" => { 63 | edgehog_forwarder::astarte::SessionInfo::from_event(event) 64 | .map(RuntimeEvent::Session) 65 | } 66 | _ => Err(FromEventError::Interface(event.interface)), 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/device.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Edgehog. 3 | * 4 | * Copyright 2022 SECO Mind Srl 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | * 18 | * SPDX-License-Identifier: Apache-2.0 19 | */ 20 | 21 | use zbus::proxy; 22 | 23 | #[proxy( 24 | interface = "io.edgehog.Device1", 25 | default_service = "io.edgehog.Device", 26 | default_path = "/io/edgehog/Device" 27 | )] 28 | pub trait Device { 29 | async fn get_hardware_id(&self, namespace: &str) -> zbus::Result; 30 | } 31 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Edgehog. 3 | * 4 | * Copyright 2022 SECO Mind Srl 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | * 18 | * SPDX-License-Identifier: Apache-2.0 19 | */ 20 | 21 | use astarte_device_sdk::event::FromEventError; 22 | use thiserror::Error; 23 | 24 | use crate::data::astarte_device_sdk_lib::DeviceSdkError; 25 | 26 | #[derive(Error, Debug)] 27 | pub enum DeviceManagerError { 28 | #[error(transparent)] 29 | Astarte(#[from] astarte_device_sdk::error::Error), 30 | 31 | #[error(transparent)] 32 | Procfs(#[from] procfs::ProcError), 33 | 34 | #[error(transparent)] 35 | Io(#[from] std::io::Error), 36 | 37 | #[cfg(all(feature = "zbus", target_os = "linux"))] 38 | #[error(transparent)] 39 | Zbus(#[from] zbus::Error), 40 | 41 | #[error("unrecoverable error ({0})")] 42 | Fatal(String), 43 | 44 | #[error(transparent)] 45 | Reqwest(#[from] reqwest::Error), 46 | 47 | #[error(transparent)] 48 | SerdeJson(#[from] serde_json::Error), 49 | 50 | #[cfg(all(feature = "zbus", target_os = "linux"))] 51 | #[error(transparent)] 52 | Ota(#[from] crate::ota::OtaError), 53 | 54 | #[error("configuration file error")] 55 | ConfigFile(#[from] toml::de::Error), 56 | 57 | #[error("integer parse error")] 58 | ParseInt(#[from] std::num::ParseIntError), 59 | 60 | #[error("device SDK error")] 61 | DeviceSdk(#[from] DeviceSdkError), 62 | 63 | #[cfg(feature = "message-hub")] 64 | #[error("message hub error")] 65 | MessageHub(#[from] crate::data::astarte_message_hub_node::MessageHubError), 66 | 67 | #[error("the connection was closed")] 68 | Disconnected, 69 | 70 | #[error("couldn't connect to the store")] 71 | Store(#[from] crate::data::StoreError), 72 | 73 | #[error("couldn't convert device event")] 74 | FromEvent(#[from] FromEventError), 75 | 76 | #[cfg(feature = "containers")] 77 | #[error("container operation failed")] 78 | Containers(#[from] edgehog_containers::service::ServiceError), 79 | 80 | #[cfg(feature = "forwarder")] 81 | #[error("forwarder error")] 82 | Forwarder(#[from] crate::forwarder::ForwarderError), 83 | } 84 | -------------------------------------------------------------------------------- /src/power_management.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Edgehog. 3 | * 4 | * Copyright 2022 SECO Mind Srl 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | * 18 | * SPDX-License-Identifier: Apache-2.0 19 | */ 20 | 21 | use std::time::Duration; 22 | 23 | use tracing::{debug, error, info}; 24 | 25 | use crate::error::DeviceManagerError; 26 | 27 | pub async fn reboot() -> Result<(), DeviceManagerError> { 28 | debug!("waiting 5 secs before reboot"); 29 | 30 | // This function blocks the caller, so we can wait that other tasks finish. 31 | tokio::time::sleep(Duration::from_secs(5)).await; 32 | 33 | if std::env::var("DM_NO_REBOOT").is_ok() { 34 | info!("Dry run, exiting"); 35 | 36 | std::process::exit(0); 37 | } 38 | 39 | // TODO: use systemd api 40 | let output = tokio::process::Command::new("shutdown") 41 | .args(["-r", "now"]) 42 | .output() 43 | .await?; 44 | 45 | if output.status.success() && output.stderr.is_empty() { 46 | info!("Reboot command was successful, bye"); 47 | } else { 48 | error!("Reboot failed {:?}", output.stderr); 49 | } 50 | 51 | Ok(()) 52 | } 53 | -------------------------------------------------------------------------------- /src/repository/mod.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Edgehog. 3 | * 4 | * Copyright 2022 SECO Mind Srl 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | * 18 | * SPDX-License-Identifier: Apache-2.0 19 | */ 20 | 21 | use std::error::Error; 22 | 23 | use async_trait::async_trait; 24 | #[cfg(test)] 25 | use mockall::automock; 26 | 27 | pub(crate) mod file_state_repository; 28 | 29 | #[cfg_attr(test, automock(type Err = self::file_state_repository::FileStateError;))] 30 | #[async_trait] 31 | pub trait StateRepository: Send + Sync { 32 | type Err: Error; 33 | 34 | async fn write(&self, value: &T) -> Result<(), Self::Err>; 35 | async fn read(&self) -> Result; 36 | async fn exists(&self) -> bool; 37 | async fn clear(&self) -> Result<(), Self::Err>; 38 | } 39 | -------------------------------------------------------------------------------- /src/systemd_wrapper.rs: -------------------------------------------------------------------------------- 1 | // This file is part of Edgehog. 2 | // 3 | // Copyright 2023 SECO Mind Srl 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | // SPDX-License-Identifier: Apache-2.0 18 | 19 | //! Wrapper to notify systemd for the status 20 | 21 | use std::io; 22 | 23 | use systemd::daemon; 24 | use systemd::daemon::{STATE_ERRNO, STATE_READY, STATE_STATUS}; 25 | use tracing::error; 26 | 27 | /// Check the result of the call to [`daemon::notify`]. 28 | /// 29 | /// The notify function, internally calls `sd_notify` which can return: 30 | /// - `result > 0` systemd was notified successfully 31 | /// - `result < 0` with the corresponding io error 32 | /// - `result = 0` couldn't contact systemd, it's probably not running 33 | /// 34 | /// See more here [systemd sd-daemon.h](https://github.com/systemd/systemd/blob/000680a68dbdb07d77807868df0b4f978180e4cd/src/systemd/sd-daemon.h#L237) 35 | fn check_notify_result(notify: Result) { 36 | match notify { 37 | Ok(true) => {} 38 | Ok(false) => { 39 | // Result of `sd_notify == 0`, systemd is probably not running 40 | error!("couldn't notify systemd"); 41 | } 42 | Err(err) => { 43 | error!("couldn't notify status to systemd: {err}"); 44 | } 45 | } 46 | } 47 | 48 | pub fn systemd_notify_status(service_status: &str) { 49 | let systemd_state_pairs = [(STATE_STATUS, service_status)]; 50 | let notify = daemon::notify(false, systemd_state_pairs.iter()); 51 | 52 | check_notify_result(notify); 53 | } 54 | 55 | pub fn systemd_notify_ready_status(service_status: &str) { 56 | let systemd_state_pairs = [(STATE_READY, "1"), (STATE_STATUS, service_status)]; 57 | let notify = daemon::notify(false, systemd_state_pairs.iter()); 58 | 59 | check_notify_result(notify); 60 | } 61 | 62 | pub fn systemd_notify_errno_status(err_no: i32, service_status: &str) { 63 | let systemd_state_pairs = [ 64 | (STATE_ERRNO, err_no.to_string()), 65 | (STATE_STATUS, service_status.to_string()), 66 | ]; 67 | let notify = daemon::notify(false, systemd_state_pairs.iter()); 68 | 69 | check_notify_result(notify); 70 | } 71 | -------------------------------------------------------------------------------- /src/telemetry/event.rs: -------------------------------------------------------------------------------- 1 | // This file is part of Edgehog. 2 | // 3 | // Copyright 2024 SECO Mind Srl 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | // SPDX-License-Identifier: Apache-2.0 18 | 19 | //! Telemetry event from Astarte. 20 | 21 | use std::time::Duration; 22 | 23 | use astarte_device_sdk::{ 24 | event::FromEventError, types::TypeError, AstarteType, DeviceEvent, FromEvent, 25 | }; 26 | use tracing::warn; 27 | 28 | #[derive(Debug, Clone, PartialEq, Eq)] 29 | pub struct TelemetryEvent { 30 | pub interface: String, 31 | pub config: TelemetryConfig, 32 | } 33 | 34 | impl FromEvent for TelemetryEvent { 35 | type Err = FromEventError; 36 | 37 | fn from_event(event: DeviceEvent) -> Result { 38 | let interface = TelemetryConfig::interface_from_path(&event.path).ok_or_else(|| { 39 | FromEventError::Path { 40 | interface: "io.edgehog.devicemanager.config.Telemetry", 41 | base_path: event.path.clone(), 42 | } 43 | })?; 44 | 45 | TelemetryConfig::from_event(event).map(|config| TelemetryEvent { interface, config }) 46 | } 47 | } 48 | 49 | #[derive(Debug, Clone, FromEvent, PartialEq, Eq)] 50 | #[from_event( 51 | interface = "io.edgehog.devicemanager.config.Telemetry", 52 | aggregation = "individual" 53 | )] 54 | pub enum TelemetryConfig { 55 | #[mapping(endpoint = "/request/%{interface_name}/enable", allow_unset = true)] 56 | Enable(Option), 57 | #[mapping( 58 | endpoint = "/request/%{interface_name}/periodSeconds", 59 | allow_unset = true 60 | )] 61 | Period(Option), 62 | } 63 | 64 | impl TelemetryConfig { 65 | fn interface_from_path(path: &str) -> Option { 66 | path.strip_prefix('/') 67 | .and_then(|path| path.split('/').nth(1)) 68 | .map(str::to_string) 69 | } 70 | } 71 | 72 | #[derive(Debug, Clone, PartialEq, Eq)] 73 | pub struct TelemetryPeriod(pub Duration); 74 | 75 | impl TryFrom for TelemetryPeriod { 76 | type Error = TypeError; 77 | 78 | fn try_from(value: AstarteType) -> Result { 79 | let secs = i64::try_from(value)?; 80 | let secs = u64::try_from(secs).unwrap_or_else(|_| { 81 | warn!("Telemetry period seconds value too big {secs}, capping to u64::MAX"); 82 | 83 | u64::MAX 84 | }); 85 | 86 | Ok(Self(Duration::from_secs(secs))) 87 | } 88 | } 89 | 90 | #[cfg(test)] 91 | mod tests { 92 | use astarte_device_sdk::Value; 93 | 94 | use crate::controller::event::RuntimeEvent; 95 | 96 | use super::*; 97 | 98 | #[test] 99 | fn should_convert_telemetry_from_event() { 100 | let event = DeviceEvent { 101 | interface: "io.edgehog.devicemanager.config.Telemetry".to_string(), 102 | path: "/request/foo/enable".to_string(), 103 | data: Value::Individual(true.into()), 104 | }; 105 | 106 | let res = RuntimeEvent::from_event(event).unwrap(); 107 | 108 | assert_eq!( 109 | res, 110 | RuntimeEvent::Telemetry(TelemetryEvent { 111 | interface: "foo".to_string(), 112 | config: TelemetryConfig::Enable(Some(true)) 113 | }) 114 | ); 115 | 116 | let event = DeviceEvent { 117 | interface: "io.edgehog.devicemanager.config.Telemetry".to_string(), 118 | path: "/request/foo/periodSeconds".to_string(), 119 | data: Value::Individual(AstarteType::LongInteger(42)), 120 | }; 121 | 122 | let res = RuntimeEvent::from_event(event).unwrap(); 123 | 124 | assert_eq!( 125 | res, 126 | RuntimeEvent::Telemetry(TelemetryEvent { 127 | interface: "foo".to_string(), 128 | config: TelemetryConfig::Period(Some(TelemetryPeriod(Duration::from_secs(42)))) 129 | }) 130 | ); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/telemetry/runtime_info.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Edgehog. 3 | * 4 | * Copyright 2022 SECO Mind Srl 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | * 18 | * SPDX-License-Identifier: Apache-2.0 19 | */ 20 | 21 | use std::borrow::Cow; 22 | 23 | use serde::Deserialize; 24 | 25 | use crate::data::{publish, Publisher}; 26 | 27 | const INTERFACE: &str = "io.edgehog.devicemanager.RuntimeInfo"; 28 | 29 | pub const RUNTIME_INFO: RuntimeInfo<'static> = RuntimeInfo::read(); 30 | 31 | #[derive(Debug, Clone, Deserialize)] 32 | pub struct RuntimeInfo<'a> { 33 | pub name: Cow<'a, str>, 34 | pub url: Cow<'a, str>, 35 | pub version: Cow<'a, str>, 36 | pub environment: Cow<'a, str>, 37 | } 38 | 39 | impl RuntimeInfo<'static> { 40 | /// Get structured data for `io.edgehog.devicemanager.RuntimeInfo` interface 41 | pub const fn read() -> Self { 42 | Self { 43 | name: Cow::Borrowed(env!("CARGO_PKG_NAME")), 44 | url: Cow::Borrowed(env!("CARGO_PKG_HOMEPAGE")), 45 | version: Cow::Borrowed(env!("CARGO_PKG_VERSION")), 46 | environment: Cow::Borrowed(env!("EDGEHOG_RUSTC_VERSION")), 47 | } 48 | } 49 | 50 | pub async fn send(self, client: &T) 51 | where 52 | T: Publisher, 53 | { 54 | let values = [ 55 | ("/name", self.name), 56 | ("/url", self.url), 57 | ("/version", self.version), 58 | ("/environment", self.environment), 59 | ]; 60 | 61 | for (path, data) in values { 62 | publish(client, INTERFACE, path, data.as_ref()).await; 63 | } 64 | } 65 | } 66 | 67 | #[cfg(test)] 68 | pub(crate) mod tests { 69 | use astarte_device_sdk::AstarteType; 70 | use mockall::{predicate, Sequence}; 71 | 72 | use crate::data::tests::MockPubSub; 73 | 74 | use super::*; 75 | 76 | pub(crate) fn mock_runtime_info_telemtry(client: &mut MockPubSub, seq: &mut Sequence) { 77 | client 78 | .expect_send() 79 | .with( 80 | predicate::eq("io.edgehog.devicemanager.RuntimeInfo"), 81 | predicate::eq("/name"), 82 | predicate::eq(AstarteType::from(env!("CARGO_PKG_NAME"))), 83 | ) 84 | .once() 85 | .in_sequence(seq) 86 | .returning(|_, _, _| Ok(())); 87 | 88 | client 89 | .expect_send() 90 | .with( 91 | predicate::eq("io.edgehog.devicemanager.RuntimeInfo"), 92 | predicate::eq("/url"), 93 | predicate::eq(AstarteType::from(env!("CARGO_PKG_HOMEPAGE"))), 94 | ) 95 | .once() 96 | .in_sequence(seq) 97 | .returning(|_, _, _| Ok(())); 98 | 99 | client 100 | .expect_send() 101 | .with( 102 | predicate::eq("io.edgehog.devicemanager.RuntimeInfo"), 103 | predicate::eq("/version"), 104 | predicate::eq(AstarteType::from(env!("CARGO_PKG_VERSION"))), 105 | ) 106 | .once() 107 | .in_sequence(seq) 108 | .returning(|_, _, _| Ok(())); 109 | 110 | client 111 | .expect_send() 112 | .with( 113 | predicate::eq("io.edgehog.devicemanager.RuntimeInfo"), 114 | predicate::eq("/environment"), 115 | predicate::eq(AstarteType::from(env!("EDGEHOG_RUSTC_VERSION"))), 116 | ) 117 | .once() 118 | .in_sequence(seq) 119 | .returning(|_, _, _| Ok(())); 120 | } 121 | 122 | #[tokio::test] 123 | async fn should_send_runtime_info() { 124 | let mut client = MockPubSub::new(); 125 | let mut seq = Sequence::new(); 126 | 127 | mock_runtime_info_telemtry(&mut client, &mut seq); 128 | 129 | RUNTIME_INFO.send(&client).await; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/telemetry/storage_usage.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Edgehog. 3 | * 4 | * Copyright 2022 SECO Mind Srl 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | * 18 | * SPDX-License-Identifier: Apache-2.0 19 | */ 20 | 21 | use astarte_device_sdk::AstarteAggregate; 22 | use std::collections::HashMap; 23 | use sysinfo::Disks; 24 | use tracing::{error, warn}; 25 | 26 | use crate::data::Publisher; 27 | 28 | const INTERFACE: &str = "io.edgehog.devicemanager.StorageUsage"; 29 | 30 | #[derive(Debug, AstarteAggregate)] 31 | #[astarte_aggregate(rename_all = "camelCase")] 32 | pub struct DiskUsage { 33 | pub total_bytes: i64, 34 | pub free_bytes: i64, 35 | } 36 | 37 | #[derive(Debug)] 38 | pub struct StorageUsage { 39 | disks: HashMap, 40 | } 41 | 42 | impl StorageUsage { 43 | /// Get structured data for `io.edgehog.devicemanager.StorageUsage` interface. 44 | /// 45 | /// The `/dev/` prefix is excluded from the device names since it is common for all devices. 46 | pub fn read() -> Self { 47 | let disks = Disks::new_with_refreshed_list(); 48 | 49 | let disks = disks 50 | .list() 51 | .iter() 52 | .filter_map(|disk| { 53 | let name = disk.name().to_string_lossy(); 54 | let name = name.strip_prefix("/dev/").unwrap_or(&name); 55 | 56 | // remove disks with a higher depth 57 | if name.contains('/') { 58 | warn!("not simple disks device, ignoring"); 59 | return None; 60 | } 61 | 62 | let Ok(total_bytes) = disk.total_space().try_into() else { 63 | error!("disk size too big, ignoring"); 64 | return None; 65 | }; 66 | 67 | let Ok(free_bytes) = disk.available_space().try_into() else { 68 | error!("available space too big, ignoring"); 69 | return None; 70 | }; 71 | 72 | Some(( 73 | // Format to be send as aggregate object path 74 | format!("/{name}"), 75 | DiskUsage { 76 | total_bytes, 77 | free_bytes, 78 | }, 79 | )) 80 | }) 81 | .collect(); 82 | 83 | Self { disks } 84 | } 85 | 86 | /// Sends all the disks. 87 | pub async fn send(self, client: &T) 88 | where 89 | T: Publisher, 90 | { 91 | for (path, v) in self.disks { 92 | if let Err(err) = client.send_object(INTERFACE, &path, v).await { 93 | error!( 94 | "couldn't send {}: {}", 95 | INTERFACE, 96 | stable_eyre::Report::new(err) 97 | ) 98 | } 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/telemetry/upower/device.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Edgehog. 3 | * 4 | * Copyright 2022 SECO Mind Srl 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | * 18 | * SPDX-License-Identifier: Apache-2.0 19 | */ 20 | 21 | use zbus::proxy; 22 | use zbus::zvariant::OwnedValue; 23 | 24 | #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, OwnedValue)] 25 | #[repr(u32)] 26 | pub enum BatteryState { 27 | Unknown = 0, 28 | Charging = 1, 29 | Discharging = 2, 30 | Empty = 3, 31 | FullyCharged = 4, 32 | PendingCharge = 5, 33 | PendingDischarge = 6, 34 | } 35 | 36 | #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, OwnedValue)] 37 | #[repr(u32)] 38 | pub enum PowerDeviceType { 39 | Unknown = 0, 40 | LinePower = 1, 41 | Battery = 2, 42 | Ups = 3, 43 | Monitor = 4, 44 | Mouse = 5, 45 | Keyboard = 6, 46 | Pda = 7, 47 | Phone = 8, 48 | } 49 | 50 | #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, OwnedValue)] 51 | #[repr(u32)] 52 | pub enum BatteryLevel { 53 | Unknown = 0, 54 | None = 1, 55 | Low = 3, 56 | Critical = 4, 57 | Normal = 5, 58 | High = 7, 59 | Full = 8, 60 | } 61 | 62 | #[proxy( 63 | interface = "org.freedesktop.UPower.Device", 64 | default_service = "org.freedesktop.UPower" 65 | )] 66 | pub trait Device { 67 | /// The level of the battery for devices which do not report a percentage but rather a coarse battery level. 68 | /// If the value is None, then the device does not support coarse battery reporting, and the percentage should be used instead. 69 | #[zbus(property)] 70 | fn battery_level(&self) -> zbus::Result; 71 | 72 | ///If the power source is present in the bay. This field is required as some batteries are hot-removable, for example expensive UPS and most laptop batteries. 73 | // 74 | // This property is only valid if the property type has the value "battery". 75 | #[zbus(property)] 76 | fn is_present(&self) -> zbus::Result; 77 | 78 | /// The amount of energy left in the power source expressed as a percentage between 0 and 100. 79 | /// Typically this is the same as (energy - energy-empty) / (energy-full - energy-empty). 80 | /// However, some primitive power sources are capable of only reporting percentages and in this case the energy-* properties will be unset while this property is set. 81 | // 82 | // This property is only valid if the property type has the value "battery". 83 | // 84 | // The percentage will be an approximation if the battery level is set to something other than None. 85 | #[zbus(property)] 86 | fn percentage(&self) -> zbus::Result; 87 | 88 | /// If the power device is used to supply the system. This would be set TRUE for laptop batteries and UPS devices, but set FALSE for wireless mice or PDAs. 89 | #[zbus(property)] 90 | fn power_supply(&self) -> zbus::Result; 91 | 92 | /// Refreshes the data collected from the power source. 93 | fn refresh(&self) -> zbus::Result<()>; 94 | 95 | ///Unique serial number of the battery. 96 | #[zbus(property)] 97 | fn serial(&self) -> zbus::Result; 98 | 99 | /// The battery power state. 100 | #[zbus(property)] 101 | fn state(&self) -> zbus::Result; 102 | 103 | /// If the value is set to "Battery", you will need to verify that the property power-supply has the value "true" before considering it as a laptop battery. 104 | /// Otherwise it will likely be the battery for a device of an unknown type. 105 | #[zbus(property, name = "Type")] 106 | fn power_device_type(&self) -> zbus::Result; 107 | } 108 | -------------------------------------------------------------------------------- /src/telemetry/upower/mod.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Edgehog. 3 | * 4 | * Copyright 2022 SECO Mind Srl 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | * 18 | * SPDX-License-Identifier: Apache-2.0 19 | */ 20 | 21 | use zbus::proxy; 22 | 23 | pub(crate) mod device; 24 | 25 | #[proxy( 26 | interface = "org.freedesktop.UPower", 27 | default_service = "org.freedesktop.UPower", 28 | default_path = "/org/freedesktop/UPower" 29 | )] 30 | pub trait UPower { 31 | /// Enumerate all power objects on the system. 32 | fn enumerate_devices(&self) -> zbus::Result>; 33 | 34 | /// Indicates whether the system is running on battery power. This property is provided for convenience. 35 | #[zbus(property)] 36 | fn on_battery(&self) -> zbus::Result; 37 | } 38 | --------------------------------------------------------------------------------