Compare commits
284 Commits
v0.3.1.1
...
v0.3.4-rc.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0ba0936ca | ||
|
|
b08556861f | ||
|
|
c96628ad49 | ||
|
|
a615882b3f | ||
|
|
2bcc8e0d30 | ||
|
|
de519edf78 | ||
|
|
caf47943c3 | ||
|
|
427ab12724 | ||
|
|
eba16c0cc3 | ||
|
|
a485de6359 | ||
|
|
1a985f7e82 | ||
|
|
7867411095 | ||
|
|
2f6ebd16c1 | ||
|
|
878b235614 | ||
|
|
75f9c6b0fb | ||
|
|
7c1e2bf96f | ||
|
|
181b44e117 | ||
|
|
f7793976fb | ||
|
|
8ffcd9b60a | ||
|
|
52d3c4d62d | ||
|
|
0fb3e75253 | ||
|
|
2c40e403c4 | ||
|
|
d1c519ed0d | ||
|
|
27470ef934 | ||
|
|
8a1da87702 | ||
|
|
c8d89f805b | ||
|
|
c9fceafc16 | ||
|
|
bbb9980941 | ||
|
|
da55d6f7cd | ||
|
|
eeacdc1359 | ||
|
|
ee1e92e1cb | ||
|
|
705802e584 | ||
|
|
b2e509f055 | ||
|
|
cca70764d4 | ||
|
|
3ac94710fb | ||
|
|
ca73a47785 | ||
|
|
1ef67fc8e9 | ||
|
|
8f3c2f4f3d | ||
|
|
e42b98ec17 | ||
|
|
efb318a979 | ||
|
|
3c0a82293c | ||
|
|
e867f31c31 | ||
|
|
aeb6da111b | ||
|
|
2736fa5202 | ||
|
|
4d3df867da | ||
|
|
62f78e4312 | ||
|
|
d223ac4675 | ||
|
|
c16404bb2d | ||
|
|
cf70933e21 | ||
|
|
46222e9352 | ||
|
|
212e94756b | ||
|
|
b42abbd4a2 | ||
|
|
730a55e721 | ||
|
|
06cf83b901 | ||
|
|
673e5af030 | ||
|
|
a0bc16c255 | ||
|
|
76b5234f7b | ||
|
|
928de47d1d | ||
|
|
274db6f606 | ||
|
|
89ca0ca927 | ||
|
|
8047008fa5 | ||
|
|
f914110626 | ||
|
|
5656fd0b96 | ||
|
|
c3d8c72302 | ||
|
|
1eefff9025 | ||
|
|
1dc7c7b0a4 | ||
|
|
011bac7b4f | ||
|
|
dc2d6e60d8 | ||
|
|
7809b6e50f | ||
|
|
f7f0370bf5 | ||
|
|
6300fc5364 | ||
|
|
16270cbd1a | ||
|
|
3b226dd2c0 | ||
|
|
4ac61d18ff | ||
|
|
fd7abdb8a4 | ||
|
|
92cd85b204 | ||
|
|
4bb7998208 | ||
|
|
91b22311af | ||
|
|
ddd00d4c25 | ||
|
|
428997f26a | ||
|
|
c9d35d8096 | ||
|
|
761b3bd591 | ||
|
|
a440e6f115 | ||
|
|
837b1a9a73 | ||
|
|
bed37184d1 | ||
|
|
785ed480bb | ||
|
|
d8c39c42a1 | ||
|
|
4b06138d35 | ||
|
|
bd5668d15d | ||
|
|
1d6c61cc5b | ||
|
|
ed22e53cb6 | ||
|
|
d18a34785c | ||
|
|
79fb8de7b7 | ||
|
|
07f5f3f1bb | ||
|
|
8fffa40502 | ||
|
|
6680b32579 | ||
|
|
af618f42bd | ||
|
|
aafcce871e | ||
|
|
71d1418559 | ||
|
|
e0678cc869 | ||
|
|
74ddf7114c | ||
|
|
837d4c1597 | ||
|
|
ccb85737f7 | ||
|
|
f9a4699e84 | ||
|
|
bab3aea8ff | ||
|
|
c52cf1fc3f | ||
|
|
3fe43a5b57 | ||
|
|
1a8b6d2fe7 | ||
|
|
570a4b7915 | ||
|
|
63859b81ad | ||
|
|
d8d13f8bf6 | ||
|
|
c3ce44e202 | ||
|
|
3372cdc0df | ||
|
|
82fc945d73 | ||
|
|
040bd52705 | ||
|
|
415cfcb72f | ||
|
|
2b0efb32c1 | ||
|
|
a3a4fdd7fc | ||
|
|
78f6bbf7fe | ||
|
|
43606d26e4 | ||
|
|
b77c409257 | ||
|
|
96f77a6275 | ||
|
|
2336e36314 | ||
|
|
9146c31abf | ||
|
|
bd4c431eb4 | ||
|
|
b620e5319a | ||
|
|
f12df8ded4 | ||
|
|
0ecd920ad9 | ||
|
|
b40be8c494 | ||
|
|
f7c5e64fbc | ||
|
|
6eea2526f6 | ||
|
|
be9db47276 | ||
|
|
35cb81518c | ||
|
|
4042b8f026 | ||
|
|
a3d1b2d671 | ||
|
|
eec8c41e20 | ||
|
|
4f9fe7245b | ||
|
|
6e1ae69691 | ||
|
|
65a1fcfda5 | ||
|
|
373e11495d | ||
|
|
8b6eac3c1c | ||
|
|
43bae7fb01 | ||
|
|
18ee1e2685 | ||
|
|
5b91b5f436 | ||
|
|
54749dfd1e | ||
|
|
f86212dfe1 | ||
|
|
9ed2e2b0ca | ||
|
|
a29cd622c3 | ||
|
|
6cea0139d1 | ||
|
|
45a6a930c9 | ||
|
|
22b273b145 | ||
|
|
ca71c88744 | ||
|
|
20b93e9fba | ||
|
|
05b29a7e9a | ||
|
|
913ef5c817 | ||
|
|
60534597e0 | ||
|
|
a7173b6bc9 | ||
|
|
6deb51428a | ||
|
|
2f00a642be | ||
|
|
4e47960440 | ||
|
|
67b54ac1eb | ||
|
|
0e82b6981f | ||
|
|
d6bf52c11f | ||
|
|
c1ac66f6e5 | ||
|
|
b9e4a66fdc | ||
|
|
9c363be16f | ||
|
|
affab384cf | ||
|
|
0fc546962e | ||
|
|
d215d96b9b | ||
|
|
327e873ef6 | ||
|
|
a2f65de1ce | ||
|
|
bc23129759 | ||
|
|
3e7b184ab4 | ||
|
|
fe0b0d1157 | ||
|
|
55b1c021ec | ||
|
|
21cf4cd2ce | ||
|
|
defc98ab0e | ||
|
|
74af03408f | ||
|
|
1d151d8fa6 | ||
|
|
e5aeced045 | ||
|
|
17d39143ac | ||
|
|
26c37ba824 | ||
|
|
d380cc31fa | ||
|
|
aa2fedee9d | ||
|
|
14fa0e478a | ||
|
|
ac878d46a5 | ||
|
|
6da0a473be | ||
|
|
2642ec85e5 | ||
|
|
26d2152a36 | ||
|
|
1cfd404321 | ||
|
|
207020b7a0 | ||
|
|
6ad9a5952e | ||
|
|
0511680fc5 | ||
|
|
ad14503e9f | ||
|
|
9221f25e35 | ||
|
|
95eec90a62 | ||
|
|
927cb51b5d | ||
|
|
9f4025fdfb | ||
|
|
b57336f6cf | ||
|
|
6e1c2fd7fd | ||
|
|
50e3b7cd5a | ||
|
|
8beda5b0ae | ||
|
|
9998ed177b | ||
|
|
e2db3d84d8 | ||
|
|
141a390105 | ||
|
|
78ad5d5879 | ||
|
|
2ddd38796d | ||
|
|
35b220d7a5 | ||
|
|
8093faee19 | ||
|
|
10a7bd2eff | ||
|
|
2f8a25ae26 | ||
|
|
19bf80dfaf | ||
|
|
fbfaac9859 | ||
|
|
0c3d0dd525 | ||
|
|
1388632562 | ||
|
|
771ecaf3e5 | ||
|
|
2000a8f3ed | ||
|
|
719cd5512c | ||
|
|
afb4536247 | ||
|
|
71b19e6582 | ||
|
|
f37cfda365 | ||
|
|
f63a841cb5 | ||
|
|
d469e802ad | ||
|
|
1702c07481 | ||
|
|
31c5aebe90 | ||
|
|
8cf84a6cf2 | ||
|
|
18336e4d0a | ||
|
|
abf297d095 | ||
|
|
061a350cc6 | ||
|
|
c85491cc71 | ||
|
|
8b794c2299 | ||
|
|
11b11375fd | ||
|
|
c728f1a694 | ||
|
|
28f9fa35e5 | ||
|
|
f8ea2ebf62 | ||
|
|
7575e8c1de | ||
|
|
395db5f1cf | ||
|
|
ee1acda7aa | ||
|
|
1150f4c438 | ||
|
|
f04b90d9c6 | ||
|
|
53463077df | ||
|
|
e326c5be4a | ||
|
|
e199dbc37b | ||
|
|
2e8bfcc74d | ||
|
|
ca53793e32 | ||
|
|
a5f31fbf4e | ||
|
|
40d47c9f44 | ||
|
|
67743b37bb | ||
|
|
36911d7ed6 | ||
|
|
5564154da2 | ||
|
|
27f9869b38 | ||
|
|
f274747af3 | ||
|
|
05832b8b4b | ||
|
|
b9ce2bf2dc | ||
|
|
5442459b2d | ||
|
|
f0466aaa56 | ||
|
|
50111e37da | ||
|
|
76682ebef0 | ||
|
|
705653465a | ||
|
|
8cd2fac9b9 | ||
|
|
b2d7f4f606 | ||
|
|
2dd31fa93f | ||
|
|
df20d4f100 | ||
|
|
3ddeb5fa94 | ||
|
|
70baed88f4 | ||
|
|
5ba0d594a2 | ||
|
|
6505c4054f | ||
|
|
e1c30a918b | ||
|
|
f812e208fa | ||
|
|
9e7526c191 | ||
|
|
07194e52cd | ||
|
|
2f8d825970 | ||
|
|
c44eb3a2c3 | ||
|
|
8207770369 | ||
|
|
365952bbe9 | ||
|
|
5404ebce1c | ||
|
|
13411f1830 | ||
|
|
43090c9873 | ||
|
|
34000fb9f0 | ||
|
|
c2f9c6a38d | ||
|
|
a5c97d4c24 | ||
|
|
9514b97ca0 | ||
|
|
22e84cc922 | ||
|
|
13b97296f5 |
6
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -1,5 +1,5 @@
|
||||
name: 🐛 Bug Report
|
||||
description: Create a report to help us improve EmbassyOS
|
||||
description: Create a report to help us improve embassyOS
|
||||
title: '[bug]: '
|
||||
labels: [Bug, Needs Triage]
|
||||
assignees:
|
||||
@@ -14,8 +14,8 @@ body:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: EmbassyOS Version
|
||||
description: What version of EmbassyOS are you running?
|
||||
label: embassyOS Version
|
||||
description: What version of embassyOS are you running?
|
||||
placeholder: e.g. 0.3.0
|
||||
validations:
|
||||
required: true
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
@@ -1,5 +1,5 @@
|
||||
name: 💡 Feature Request
|
||||
description: Suggest an idea for EmbassyOS
|
||||
description: Suggest an idea for embassyOS
|
||||
title: '[feat]: '
|
||||
labels: [Enhancement]
|
||||
assignees:
|
||||
@@ -27,7 +27,7 @@ body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe Preferred Solution
|
||||
description: How you want this feature added to EmbassyOS?
|
||||
description: How you want this feature added to embassyOS?
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe Alternatives
|
||||
|
||||
21
.github/workflows/README.md
vendored
@@ -1,20 +1,25 @@
|
||||
# This folder contains GitHub Actions workflows for building the project
|
||||
|
||||
## backend-pr
|
||||
Runs: when a pull request targets the master branch and changes the libs/ and/or backend/ folders
|
||||
## backend
|
||||
Runs: manually (on: workflow_dispatch) or called by product-pipeline (on: workflow_call)
|
||||
|
||||
This workflow uses the actions docker/setup-qemu-action@v1 and docker/setup-buildx-action@v1 to prepare the environment for aarch64 cross complilation using docker buildx.
|
||||
A matrix-strategy has been used for building the v8 snapshot instead of the makefile to allow parallel job execution.
|
||||
This workflow uses the actions and docker/setup-buildx-action@v1 to prepare the environment for aarch64 cross complilation using docker buildx.
|
||||
When execution of aarch64 containers is required the action docker/setup-qemu-action@v1 is added.
|
||||
A matrix-strategy has been used to build for both x86_64 and aarch64 platforms in parallel.
|
||||
|
||||
## frontend-pr
|
||||
Runs: when a pull request targets the master branch and changes the frontend/ folder
|
||||
### Running unittests
|
||||
|
||||
Unittests are run using [cargo-nextest]( https://nexte.st/). First the sources are (cross-)compiled and archived. The archive is then run on the correct platform.
|
||||
|
||||
## frontend
|
||||
Runs: manually (on: workflow_dispatch) or called by product-pipeline (on: workflow_call)
|
||||
|
||||
This workflow builds the frontends.
|
||||
|
||||
## product
|
||||
Runs: when a change to the master branch is made
|
||||
Runs: when a pull request targets the master or next branch and when a change to the master or next branch is made
|
||||
|
||||
This workflow builds everything, re-using the backend-pr and frontend-pr workflows.
|
||||
This workflow builds everything, re-using the backend and frontend workflows.
|
||||
The download and extraction order of artifacts is relevant to `make`, as it checks the file timestamps to decide which targets need to be executed.
|
||||
|
||||
Result: eos.img
|
||||
|
||||
98
.github/workflows/backend-pr.yaml
vendored
@@ -1,98 +0,0 @@
|
||||
name: Backend PR
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
libs:
|
||||
name: Build libs
|
||||
strategy:
|
||||
matrix:
|
||||
target: [amd64, arm64]
|
||||
include:
|
||||
- target: amd64
|
||||
snapshot_command: ./build-v8-snapshot.sh
|
||||
artifact_name: js_snapshot
|
||||
artifact_path: libs/js_engine/src/artifacts/JS_SNAPSHOT.bin
|
||||
- target: arm64
|
||||
snapshot_command: ./build-arm-v8-snapshot.sh
|
||||
artifact_name: arm_js_snapshot
|
||||
artifact_path: libs/js_engine/src/artifacts/ARM_JS_SNAPSHOT.bin
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
libs/target/
|
||||
key: ${{ runner.os }}-cargo-libs-${{ matrix.target }}-${{ hashFiles('libs/Cargo.lock') }}
|
||||
|
||||
- name: Build v8 snapshot
|
||||
run: ${{ matrix.snapshot_command }}
|
||||
working-directory: libs
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ matrix.artifact_name }}
|
||||
path: ${{ matrix.artifact_path }}
|
||||
|
||||
backend:
|
||||
name: Build backend
|
||||
runs-on: ubuntu-latest
|
||||
needs: libs
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Download arm_js_snapshot artifact
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: arm_js_snapshot
|
||||
path: libs/js_engine/src/artifacts/
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
backend/target/
|
||||
key: ${{ runner.os }}-cargo-backend-${{ hashFiles('backend/Cargo.lock') }}
|
||||
|
||||
- name: Build backend
|
||||
run: make backend
|
||||
|
||||
- name: 'Tar files to preserve file permissions'
|
||||
run: tar -cvf backend.tar ENVIRONMENT.txt GIT_HASH.txt backend/target/aarch64-unknown-linux-gnu/release/embassy*
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: backend
|
||||
path: backend.tar
|
||||
233
.github/workflows/backend.yaml
vendored
Normal file
@@ -0,0 +1,233 @@
|
||||
name: Backend
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
RUST_VERSION: "1.67.1"
|
||||
ENVIRONMENT: "dev"
|
||||
|
||||
jobs:
|
||||
build_libs:
|
||||
name: Build libs
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
target: [x86_64, aarch64]
|
||||
include:
|
||||
- target: x86_64
|
||||
snapshot_command: ./build-v8-snapshot.sh
|
||||
artifact_name: js_snapshot
|
||||
artifact_path: libs/js_engine/src/artifacts/JS_SNAPSHOT.bin
|
||||
- target: aarch64
|
||||
snapshot_command: ./build-arm-v8-snapshot.sh
|
||||
artifact_name: arm_js_snapshot
|
||||
artifact_path: libs/js_engine/src/artifacts/ARM_JS_SNAPSHOT.bin
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
if: ${{ matrix.target == 'aarch64' }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
if: ${{ matrix.target == 'aarch64' }}
|
||||
|
||||
- name: "Install Rust"
|
||||
run: |
|
||||
rustup toolchain install ${{ env.RUST_VERSION }} --profile minimal --no-self-update
|
||||
rustup default ${{ inputs.rust }}
|
||||
shell: bash
|
||||
if: ${{ matrix.target == 'x86_64' }}
|
||||
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
libs/target/
|
||||
key: ${{ runner.os }}-cargo-libs-${{ matrix.target }}-${{ hashFiles('libs/Cargo.lock') }}
|
||||
|
||||
- name: Build v8 snapshot
|
||||
run: ${{ matrix.snapshot_command }}
|
||||
working-directory: libs
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ matrix.artifact_name }}
|
||||
path: ${{ matrix.artifact_path }}
|
||||
|
||||
build_backend:
|
||||
name: Build backend
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
target: [x86_64, aarch64]
|
||||
include:
|
||||
- target: x86_64
|
||||
snapshot_download: js_snapshot
|
||||
- target: aarch64
|
||||
snapshot_download: arm_js_snapshot
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 120
|
||||
needs: build_libs
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Download ${{ matrix.snapshot_download }} artifact
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: ${{ matrix.snapshot_download }}
|
||||
path: libs/js_engine/src/artifacts/
|
||||
|
||||
- name: "Install Rust"
|
||||
run: |
|
||||
rustup toolchain install ${{ env.RUST_VERSION }} --profile minimal --no-self-update
|
||||
rustup default ${{ inputs.rust }}
|
||||
shell: bash
|
||||
if: ${{ matrix.target == 'x86_64' }}
|
||||
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
backend/target/
|
||||
key: ${{ runner.os }}-cargo-backend-${{ matrix.target }}-${{ hashFiles('backend/Cargo.lock') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install libavahi-client-dev
|
||||
if: ${{ matrix.target == 'x86_64' }}
|
||||
|
||||
- name: Check Git Hash
|
||||
run: ./check-git-hash.sh
|
||||
|
||||
- name: Check Environment
|
||||
run: ./check-environment.sh
|
||||
|
||||
- name: Build backend
|
||||
run: make ARCH=${{ matrix.target }} backend
|
||||
|
||||
- name: 'Tar files to preserve file permissions'
|
||||
run: make ARCH=${{ matrix.target }} backend-${{ matrix.target }}.tar
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: backend-${{ matrix.target }}
|
||||
path: backend-${{ matrix.target }}.tar
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Build and archive tests
|
||||
run: cargo nextest archive --archive-file nextest-archive-${{ matrix.target }}.tar.zst --target ${{ matrix.target }}-unknown-linux-gnu
|
||||
working-directory: backend
|
||||
if: ${{ matrix.target == 'x86_64' }}
|
||||
|
||||
- name: Build and archive tests
|
||||
run: |
|
||||
docker run --rm \
|
||||
-v "$HOME/.cargo/registry":/root/.cargo/registry \
|
||||
-v "$(pwd)":/home/rust/src \
|
||||
-P start9/rust-arm-cross:aarch64 \
|
||||
sh -c 'cd /home/rust/src/backend &&
|
||||
rustup install ${{ env.RUST_VERSION }} &&
|
||||
rustup override set ${{ env.RUST_VERSION }} &&
|
||||
rustup target add aarch64-unknown-linux-gnu &&
|
||||
curl -LsSf https://get.nexte.st/latest/linux | tar zxf - -C ${CARGO_HOME:-~/.cargo}/bin &&
|
||||
cargo nextest archive --archive-file nextest-archive-${{ matrix.target }}.tar.zst --target ${{ matrix.target }}-unknown-linux-gnu'
|
||||
if: ${{ matrix.target == 'aarch64' }}
|
||||
|
||||
- name: Reset permissions
|
||||
run: sudo chown -R $USER target
|
||||
working-directory: backend
|
||||
if: ${{ matrix.target == 'aarch64' }}
|
||||
|
||||
- name: Upload archive to workflow
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: nextest-archive-${{ matrix.target }}
|
||||
path: backend/nextest-archive-${{ matrix.target }}.tar.zst
|
||||
|
||||
run_tests_backend:
|
||||
name: Test backend
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
target: [x86_64, aarch64]
|
||||
include:
|
||||
- target: x86_64
|
||||
- target: aarch64
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
needs: build_backend
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
if: ${{ matrix.target == 'aarch64' }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
if: ${{ matrix.target == 'aarch64' }}
|
||||
|
||||
- run: mkdir -p ~/.cargo/bin
|
||||
if: ${{ matrix.target == 'x86_64' }}
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: nextest@0.9.47
|
||||
if: ${{ matrix.target == 'x86_64' }}
|
||||
|
||||
- name: Download archive
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: nextest-archive-${{ matrix.target }}
|
||||
|
||||
- name: Download nextest (aarch64)
|
||||
run: wget -O nextest-aarch64.tar.gz https://get.nexte.st/0.9.47/linux-arm
|
||||
if: ${{ matrix.target == 'aarch64' }}
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
${CARGO_HOME:-~/.cargo}/bin/cargo-nextest nextest run --no-fail-fast --archive-file nextest-archive-${{ matrix.target }}.tar.zst \
|
||||
--filter-expr 'not (test(system::test_get_temp) | test(net::tor::test) | test(system::test_get_disk_usage) | test(net::ssl::certificate_details_persist) | test(net::ssl::ca_details_persist))'
|
||||
if: ${{ matrix.target == 'x86_64' }}
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
docker run --rm --platform linux/arm64/v8 \
|
||||
-v "/home/runner/.cargo/registry":/usr/local/cargo/registry \
|
||||
-v "$(pwd)":/home/rust/src \
|
||||
-e CARGO_TERM_COLOR=${{ env.CARGO_TERM_COLOR }} \
|
||||
-P ubuntu:20.04 \
|
||||
sh -c '
|
||||
apt-get update &&
|
||||
apt-get install -y ca-certificates &&
|
||||
apt-get install -y rsync &&
|
||||
cd /home/rust/src &&
|
||||
mkdir -p ~/.cargo/bin &&
|
||||
tar -zxvf nextest-aarch64.tar.gz -C ${CARGO_HOME:-~/.cargo}/bin &&
|
||||
${CARGO_HOME:-~/.cargo}/bin/cargo-nextest nextest run --archive-file nextest-archive-${{ matrix.target }}.tar.zst \
|
||||
--filter-expr "not (test(system::test_get_temp) | test(net::tor::test) | test(system::test_get_disk_usage) | test(net::ssl::certificate_details_persist) | test(net::ssl::ca_details_persist))"'
|
||||
if: ${{ matrix.target == 'aarch64' }}
|
||||
63
.github/workflows/debian.yaml
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
name: Debian Package
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
NODEJS_VERSION: '16.11.0'
|
||||
ENVIRONMENT: "dev"
|
||||
|
||||
jobs:
|
||||
dpkg:
|
||||
name: Build dpkg
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
repository: Start9Labs/embassy-os-deb
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
path: embassyos-0.3.x
|
||||
- run: |
|
||||
cp -r debian embassyos-0.3.x/
|
||||
VERSION=0.3.x ./control.sh
|
||||
cp embassyos-0.3.x/backend/embassyd.service embassyos-0.3.x/debian/embassyos.embassyd.service
|
||||
cp embassyos-0.3.x/backend/embassy-init.service embassyos-0.3.x/debian/embassyos.embassy-init.service
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ env.NODEJS_VERSION }}
|
||||
|
||||
- name: Get npm cache directory
|
||||
id: npm-cache-dir
|
||||
run: |
|
||||
echo "dir=$(npm config get cache)" >> $GITHUB_OUTPUT
|
||||
- uses: actions/cache@v3
|
||||
id: npm-cache
|
||||
with:
|
||||
path: ${{ steps.npm-cache-dir.outputs.dir }}
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install debmake debhelper-compat
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Run build
|
||||
run: "make VERSION=0.3.x TAG=${{ github.ref_name }}"
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: deb
|
||||
path: embassyos_0.3.x-1_amd64.deb
|
||||
@@ -1,13 +1,18 @@
|
||||
name: Frontend PR
|
||||
name: Frontend
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
NODEJS_VERSION: '16.11.0'
|
||||
ENVIRONMENT: "dev"
|
||||
|
||||
jobs:
|
||||
frontend:
|
||||
name: Build frontend
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
@@ -15,12 +20,12 @@ jobs:
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
node-version: ${{ env.NODEJS_VERSION }}
|
||||
|
||||
- name: Get npm cache directory
|
||||
id: npm-cache-dir
|
||||
run: |
|
||||
echo "::set-output name=dir::$(npm config get cache)"
|
||||
echo "dir=$(npm config get cache)" >> $GITHUB_OUTPUT
|
||||
- uses: actions/cache@v3
|
||||
id: npm-cache
|
||||
with:
|
||||
@@ -33,7 +38,7 @@ jobs:
|
||||
run: make frontends
|
||||
|
||||
- name: 'Tar files to preserve file permissions'
|
||||
run: tar -cvf frontend.tar ENVIRONMENT.txt GIT_HASH.txt frontend/dist frontend/config.json
|
||||
run: tar -cvf frontend.tar ENVIRONMENT.txt GIT_HASH.txt VERSION.txt frontend/dist frontend/config.json
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
110
.github/workflows/product.yaml
vendored
@@ -11,76 +11,42 @@ on:
|
||||
- master
|
||||
- next
|
||||
|
||||
env:
|
||||
ENVIRONMENT: "dev"
|
||||
|
||||
jobs:
|
||||
compat:
|
||||
name: Build compat.tar
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
target/
|
||||
key: ${{ runner.os }}-cargo-compat-${{ hashFiles('**/system-images/compat/Cargo.lock') }}
|
||||
|
||||
- name: Build image
|
||||
run: make system-images/compat/compat.tar
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: compat.tar
|
||||
path: system-images/compat/compat.tar
|
||||
uses: ./.github/workflows/reusable-workflow.yaml
|
||||
with:
|
||||
build_command: make system-images/compat/docker-images/aarch64.tar
|
||||
artifact_name: compat.tar
|
||||
artifact_path: system-images/compat/docker-images/aarch64.tar
|
||||
|
||||
utils:
|
||||
name: Build utils.tar
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: ./.github/workflows/reusable-workflow.yaml
|
||||
with:
|
||||
build_command: make system-images/utils/docker-images/aarch64.tar
|
||||
artifact_name: utils.tar
|
||||
artifact_path: system-images/utils/docker-images/aarch64.tar
|
||||
|
||||
- name: Build image
|
||||
run: make system-images/utils/utils.tar
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: utils.tar
|
||||
path: system-images/utils/utils.tar
|
||||
binfmt:
|
||||
uses: ./.github/workflows/reusable-workflow.yaml
|
||||
with:
|
||||
build_command: make system-images/binfmt/docker-images/aarch64.tar
|
||||
artifact_name: binfmt.tar
|
||||
artifact_path: system-images/binfmt/docker-images/aarch64.tar
|
||||
|
||||
backend:
|
||||
uses: ./.github/workflows/backend-pr.yaml
|
||||
|
||||
uses: ./.github/workflows/backend.yaml
|
||||
|
||||
frontend:
|
||||
uses: ./.github/workflows/frontend-pr.yaml
|
||||
uses: ./.github/workflows/frontend.yaml
|
||||
|
||||
image:
|
||||
name: Build image
|
||||
runs-on: ubuntu-latest
|
||||
needs: [compat,utils,backend,frontend]
|
||||
timeout-minutes: 60
|
||||
needs: [compat,utils,binfmt,backend,frontend]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
@@ -90,13 +56,19 @@ jobs:
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: compat.tar
|
||||
path: system-images/compat
|
||||
|
||||
path: system-images/compat/docker-images/
|
||||
|
||||
- name: Download utils.tar artifact
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: utils.tar
|
||||
path: system-images/utils
|
||||
path: system-images/utils/docker-images/
|
||||
|
||||
- name: Download binfmt.tar artifact
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: binfmt.tar
|
||||
path: system-images/binfmt/docker-images/
|
||||
|
||||
- name: Download js_snapshot artifact
|
||||
uses: actions/download-artifact@v3
|
||||
@@ -113,11 +85,11 @@ jobs:
|
||||
- name: Download backend artifact
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: backend
|
||||
name: backend-aarch64
|
||||
|
||||
- name: 'Extract backend'
|
||||
run:
|
||||
tar -mxvf backend.tar
|
||||
tar -mxvf backend-aarch64.tar
|
||||
|
||||
- name: Download frontend artifact
|
||||
uses: actions/download-artifact@v3
|
||||
@@ -135,6 +107,10 @@ jobs:
|
||||
run: |
|
||||
tar -mxvf frontend.tar frontend/config.json
|
||||
tar -mxvf frontend.tar frontend/dist
|
||||
tar -xvf frontend.tar GIT_HASH.txt
|
||||
tar -xvf frontend.tar ENVIRONMENT.txt
|
||||
tar -xvf frontend.tar VERSION.txt
|
||||
rm frontend.tar
|
||||
|
||||
- name: Cache raspiOS
|
||||
id: cache-raspios
|
||||
@@ -144,12 +120,10 @@ jobs:
|
||||
key: cache-raspios
|
||||
|
||||
- name: Build image
|
||||
run: "make V=1 NO_KEY=1 eos.img --debug"
|
||||
|
||||
- name: Compress image
|
||||
run: "gzip eos.img"
|
||||
run: |
|
||||
make V=1 eos_raspberrypi-uninit.img --debug
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: image
|
||||
path: eos.img.gz
|
||||
path: eos_raspberrypi-uninit.img
|
||||
|
||||
70
.github/workflows/pureos-iso.yaml
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
name: PureOS Based ISO
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- next
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- next
|
||||
|
||||
env:
|
||||
ENVIRONMENT: "dev"
|
||||
|
||||
jobs:
|
||||
dpkg:
|
||||
uses: ./.github/workflows/debian.yaml
|
||||
|
||||
iso:
|
||||
name: Build iso
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [dpkg]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
repository: Start9Labs/eos-image-recipes
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt update
|
||||
wget http://ftp.us.debian.org/debian/pool/main/d/debspawn/debspawn_0.6.1-1_all.deb
|
||||
sha256sum ./debspawn_0.6.1-1_all.deb | grep fb8a3f588438ff9ef51e713ec1d83306db893f0aa97447565e28bbba9c6e90c6
|
||||
sudo apt-get install -y ./debspawn_0.6.1-1_all.deb
|
||||
wget https://repo.pureos.net/pureos/pool/main/d/debootstrap/debootstrap_1.0.125pureos1_all.deb
|
||||
sudo apt-get install -y --allow-downgrades ./debootstrap_1.0.125pureos1_all.deb
|
||||
wget https://repo.pureos.net/pureos/pool/main/p/pureos-archive-keyring/pureos-archive-keyring_2021.11.0_all.deb
|
||||
sudo apt-get install -y ./pureos-archive-keyring_2021.11.0_all.deb
|
||||
|
||||
- name: Configure debspawn
|
||||
run: |
|
||||
sudo mkdir -p /etc/debspawn/
|
||||
echo "AllowUnsafePermissions=true" | sudo tee /etc/debspawn/global.toml
|
||||
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: /var/lib/debspawn
|
||||
key: ${{ runner.os }}-debspawn-init-byzantium
|
||||
|
||||
- name: Make build container
|
||||
run: "debspawn list | grep byzantium || debspawn create --with-init byzantium"
|
||||
|
||||
- run: "mkdir -p overlays/vendor/root"
|
||||
|
||||
- name: Download dpkg
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: deb
|
||||
path: overlays/vendor/root
|
||||
|
||||
- name: Run build
|
||||
run: |
|
||||
./run-local-build.sh --no-fakemachine byzantium none custom "" true
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: iso
|
||||
path: results/*.iso
|
||||
37
.github/workflows/reusable-workflow.yaml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
name: Reusable Workflow
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
build_command:
|
||||
required: true
|
||||
type: string
|
||||
artifact_name:
|
||||
required: true
|
||||
type: string
|
||||
artifact_path:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
env:
|
||||
ENVIRONMENT: "dev"
|
||||
|
||||
jobs:
|
||||
generic_build_job:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Build image
|
||||
run: ${{ inputs.build_command }}
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ inputs.artifact_name }}
|
||||
path: ${{ inputs.artifact_path }}
|
||||
15
.gitignore
vendored
@@ -1,5 +1,8 @@
|
||||
.DS_Store
|
||||
.idea
|
||||
system-images/binfmt/binfmt.tar
|
||||
system-images/compat/compat.tar
|
||||
system-images/util/util.tar
|
||||
/*.img
|
||||
/*.img.gz
|
||||
/*.img.xz
|
||||
@@ -13,5 +16,13 @@ deploy_web.sh
|
||||
secrets.db
|
||||
.vscode/
|
||||
/cargo-deps/**/*
|
||||
ENVIRONMENT.txt
|
||||
GIT_HASH.txt
|
||||
/ENVIRONMENT.txt
|
||||
/GIT_HASH.txt
|
||||
/VERSION.txt
|
||||
/embassyos-*.tar.gz
|
||||
/eos-*.tar.gz
|
||||
/*.deb
|
||||
/target
|
||||
/*.squashfs
|
||||
/debian
|
||||
/DEBIAN
|
||||
429
CHANGELOG.md
Normal file
@@ -0,0 +1,429 @@
|
||||
# v0.3.3
|
||||
## Highlights
|
||||
- x86_64 architecture compatibility
|
||||
- Kiosk mode - use your Embassy with monitor, keyboard, and mouse (available on x86 builds only, disabled on Raspberry Pi)
|
||||
- "Updates" tab - view all service updates from all registries in one place
|
||||
- Various UI/UX improvements
|
||||
- Various bugfixes and optimizations
|
||||
|
||||
## What's Changed
|
||||
- Minor typo fixes by @kn0wmad in #1887
|
||||
- Update build pipeline by @moerketh in #1896
|
||||
- Feature/setup migrate by @elvece in #1841
|
||||
- Feat/patch migration by @Blu-J in #1890
|
||||
- make js cancellable by @dr-bonez in #1901
|
||||
- wip: Making Injectable exec by @Blu-J in #1897
|
||||
- Fix/debug by @Blu-J in #1909
|
||||
- chore: Fix on the rsync not having stdout. by @Blu-J in #1911
|
||||
- install wizard project by @MattDHill in #1893
|
||||
- chore: Remove the duplicate loggging information that is making usele… by @Blu-J in #1912
|
||||
- Http proxy by @redragonx in #1772
|
||||
- fix(marketplace): loosen type in categories component by @waterplea in #1918
|
||||
- set custom meta title by @MattDHill in #1915
|
||||
- Feature/git hash by @dr-bonez in #1919
|
||||
- closes #1900 by @dr-bonez in #1920
|
||||
- feature/marketplace icons by @dr-bonez in #1921
|
||||
- Bugfix/0.3.3 migration by @dr-bonez in #1922
|
||||
- feat: Exposing the rsync that we have to the js by @Blu-J in #1907
|
||||
- Feature/install wizard disk info by @dr-bonez in #1923
|
||||
- bump shared and marketplace npm versions by @dr-bonez in #1924
|
||||
- fix error handling when store unreachable by @dr-bonez in #1925
|
||||
- wait for network online before launching init by @dr-bonez in #1930
|
||||
- silence service crash notifications by @dr-bonez in #1929
|
||||
- disable efi by @dr-bonez in #1931
|
||||
- Tor daemon fix by @redragonx in #1934
|
||||
- wait for url to be available before launching kiosk by @dr-bonez in #1933
|
||||
- fix migration to support portable fatties by @dr-bonez in #1935
|
||||
- Add guid to partition type by @MattDHill in #1932
|
||||
- add localhost support to the http server by @redragonx in #1939
|
||||
- refactor setup wizard by @dr-bonez in #1937
|
||||
- feat(shared): Ticker add new component and use it in marketplace by @waterplea in #1940
|
||||
- feat: For ota update using rsyncd by @Blu-J in #1938
|
||||
- Feat/update progress by @MattDHill in #1944
|
||||
- Fix/app show hidden by @MattDHill in #1948
|
||||
- create dpkg and iso workflows by @dr-bonez in #1941
|
||||
- changing ip addr type by @redragonx in #1950
|
||||
- Create mountpoints first by @k0gen in #1949
|
||||
- Hard code registry icons by @MattDHill in #1951
|
||||
- fix: Cleanup by sending a command and kill when dropped by @Blu-J in #1945
|
||||
- Update setup wizard styling by @elvece in #1954
|
||||
- Feature/homepage by @elvece in #1956
|
||||
- Fix millis by @Blu-J in #1960
|
||||
- fix accessing dev tools by @MattDHill in #1966
|
||||
- Update/misc UI fixes by @elvece in #1961
|
||||
- Embassy-init typo by @redragonx in #1959
|
||||
- feature: 0.3.2 -> 0.3.3 upgrade by @dr-bonez in #1958
|
||||
- Fix/migrate by @Blu-J in #1962
|
||||
- chore: Make validation reject containers by @Blu-J in #1970
|
||||
- get pubkey and encrypt password on login by @elvece in #1965
|
||||
- Multiple bugs and styling by @MattDHill in #1975
|
||||
- filter out usb stick during install by @dr-bonez in #1974
|
||||
- fix http upgrades by @dr-bonez in #1980
|
||||
- restore interfaces before creating manager by @dr-bonez in #1982
|
||||
- fuckit: no patch db locks by @dr-bonez in #1969
|
||||
- fix websocket hangup error by @dr-bonez in #1981
|
||||
- revert app show to use header and fix back button by @MattDHill in #1984
|
||||
- Update/marketplace info by @elvece in #1983
|
||||
- force docker image removal by @dr-bonez in #1985
|
||||
- do not error if cannot determine live usb device by @dr-bonez in #1986
|
||||
- remove community registry from FE defaults by @MattDHill in #1988
|
||||
- check environment by @dr-bonez in #1990
|
||||
- fix marketplace search and better category disabling by @MattDHill in #1991
|
||||
- better migration progress bar by @dr-bonez in #1993
|
||||
- bump cargo version by @dr-bonez in #1995
|
||||
- preload icons and pause on setup complete for kiosk mode by @MattDHill in #1997
|
||||
- use squashfs for rpi updates by @dr-bonez in #1998
|
||||
- do not start progress at 0 before diff complete by @dr-bonez in #1999
|
||||
- user must click continue in kiosk on success page by @MattDHill in #2001
|
||||
- fix regex in image rip script by @dr-bonez in #2002
|
||||
- fix bug with showing embassy drives and center error text by @MattDHill in #2006
|
||||
- fix partition type by @dr-bonez in #2007
|
||||
- lowercase service for alphabetic sorting by @MattDHill in #2008
|
||||
- dont add updates cat by @MattDHill in #2009
|
||||
- make downloaded page a full html doc by @MattDHill in #2011
|
||||
- wait for monitor to be attached before launching firefox by @chrisguida in #2005
|
||||
- UI fixes by @elvece in #2014
|
||||
- fix: Stop service before by @Blu-J in #2019
|
||||
- shield links update by @k0gen in #2018
|
||||
- fix: Undoing the breaking introduced by trying to stopp by @Blu-J in #2023
|
||||
- update link rename from embassy -> system by @elvece in #2027
|
||||
- initialize embassy before restoring packages by @dr-bonez in #2029
|
||||
- make procfs an optional dependency so sdk can build on macos by @elvece in #2028
|
||||
- take(1) for recover select by @MattDHill in #2030
|
||||
- take one from server info to prevent multiple reqs to registries by @MattDHill in #2032
|
||||
- remove write lock during backup by @MattDHill in #2033
|
||||
- fix: Ensure that during migration we make the urls have a trailing slash by @Blu-J in #2036
|
||||
- fix: Make the restores limited # restore at a time by @Blu-J in #2037
|
||||
- fix error and display of unknown font weight on success page by @elvece in #2038
|
||||
|
||||
## Checksums
|
||||
```
|
||||
8602e759d3ece7cf503b9ca43e8419109f14e424617c2703b3771c8801483d7e embassyos_amd64.deb
|
||||
b5c0d8d1af760881a1b5cf32bd7c5b1d1cf6468f6da594a1b4895a866d03a58c embassyos_amd64.iso
|
||||
fe518453a7e1a8d8c2be43223a1a12adff054468f8082df0560e1ec50df3dbfd embassyos_raspberrypi.img
|
||||
7b1ff0ada27b6714062aa991ec31c2d95ac4edf254cd464a4fa251905aa47ebd embassyos_raspberrypi.tar.gz
|
||||
```
|
||||
|
||||
# v0.3.2.1
|
||||
## What's Changed
|
||||
- Update index.html copy and styling by @elvece in #1855
|
||||
- increase maximum avahi entry group size by @dr-bonez in #1869
|
||||
- bump version by @dr-bonez in #1871
|
||||
|
||||
### Linux and Mac
|
||||
|
||||
Download the `eos.tar.gz` file, then extract and flash the resulting eos.img to your SD Card
|
||||
Windows
|
||||
|
||||
Download the `eos.zip` file, then extract and flash the resulting eos.img to your SD Card
|
||||
|
||||
## SHA-256 Checksums
|
||||
```
|
||||
c4b17658910dd10c37df134d5d5fdd6478f962ba1b803d24477d563d44430f96 eos.tar.gz
|
||||
3a8b29878fe222a9d7cbf645c975b12805704b0f39c7daa46033d22380f9828c eos.zip
|
||||
dedff3eb408ea411812b8f46e6c6ed32bfbd97f61ec2b85a6be40373c0528256 eos.img
|
||||
```
|
||||
|
||||
# v0.3.2
|
||||
## Highlights
|
||||
- Autoscrolling for logs
|
||||
- Improved connectivity between browser and Embassy
|
||||
- Switch to Postgres for EOS database for better performance
|
||||
- Multiple bug fixes and under-the-hood improvements
|
||||
- Various UI/UX enhancements
|
||||
- Removal of product keys
|
||||
|
||||
Update Hash (SHA256): `d8ce908b06baee6420b45be1119e5eb9341ba8df920d1e255f94d1ffb7cc4de9`
|
||||
|
||||
Image Hash (SHA256): `e035cd764e5ad9eb1c60e2f7bc3b9bd7248f42a91c69015c8a978a0f94b90bbb`
|
||||
|
||||
Note: This image was uploaded as a gzipped POSIX sparse TAR file. The recommended command for unpacking it on systems that support sparse files is `tar --format=posix --sparse -zxvf eos.tar.gz`
|
||||
|
||||
## What's Changed
|
||||
- formatting by @dr-bonez in #1698
|
||||
- Update README.md by @kn0wmad in #1705
|
||||
- Update README.md by @dr-bonez in #1703
|
||||
- feat: migrate to Angular 14 and RxJS 7 by @waterplea in #1681
|
||||
- 0312 multiple FE by @MattDHill in #1712
|
||||
- Fix http requests by @MattDHill in #1717
|
||||
- Add build-essential to README.md by @chrisguida in #1716
|
||||
- write image to sparse-aware archive format by @dr-bonez in #1709
|
||||
- fix: Add modification to the max_user_watches by @Blu-J in #1695
|
||||
- [Feat] follow logs by @chrisguida in #1714
|
||||
- Update README.md by @dr-bonez in #1728
|
||||
- fix build for patch-db client for consistency by @elvece in #1722
|
||||
- fix cli install by @chrisguida in #1720
|
||||
- highlight instructions if not viewed by @MattDHill in #1731
|
||||
- Feat: HttpReader by @redragonx in #1733
|
||||
- Bugfix/dns by @dr-bonez in #1741
|
||||
- add x86 build and run unittests to backend pipeline by @moerketh in #1682
|
||||
- [Fix] websocket connecting and patchDB connection monitoring by @MattDHill in #1738
|
||||
- Set pipeline job timeouts and add ca-certificates to test container by @moerketh in #1753
|
||||
- Disable bluetooth properly #862 by @redragonx in #1745
|
||||
- [feat]: resumable downloads by @dr-bonez in #1746
|
||||
- Fix/empty properties by @elvece in #1764
|
||||
- use hostname from patchDB as default server name by @MattDHill in #1758
|
||||
- switch to postgresql by @dr-bonez in #1763
|
||||
- remove product key from setup flow by @MattDHill in #1750
|
||||
- pinning cargo dep versions for CLI by @redragonx in #1775
|
||||
- fix: Js deep dir by @Blu-J in #1784
|
||||
- 0.3.2 final cleanup by @dr-bonez in #1782
|
||||
- expect ui marketplace to be undefined by @MattDHill in #1787
|
||||
- fix init to exit on failure by @dr-bonez in #1788
|
||||
- fix search to return more accurate results by @MattDHill in #1792
|
||||
- update backend dependencies by @dr-bonez in #1796
|
||||
- use base64 for HTTP headers by @dr-bonez in #1795
|
||||
- fix: Bad cert of *.local.local is now fixed to correct. by @Blu-J in #1798
|
||||
- fix duplicate patch updates, add scroll button to setup success by @MattDHill in #1800
|
||||
- level_slider reclaiming that precious RAM memory by @k0gen in #1799
|
||||
- stop leaking avahi clients by @dr-bonez in #1802
|
||||
- fix: Deep is_parent was wrong and could be escapped by @Blu-J in #1801
|
||||
- prevent cfg str generation from running forever by @dr-bonez in #1804
|
||||
- better RPC error message by @MattDHill in #1803
|
||||
- Bugfix/marketplace add by @elvece in #1805
|
||||
- fix mrketplace swtiching by @MattDHill in #1810
|
||||
- clean up code and logs by @MattDHill in #1809
|
||||
- fix: Minor fix that matt wanted by @Blu-J in #1808
|
||||
- onion replace instead of adding tor repository by @k0gen in #1813
|
||||
- bank Start as embassy hostname from the begining by @k0gen in #1814
|
||||
- add descriptions to marketplace list page by @elvece in #1812
|
||||
- Fix/encryption by @elvece in #1811
|
||||
- restructure initialization by @dr-bonez in #1816
|
||||
- update license by @MattDHill in #1819
|
||||
- perform system rebuild after updating by @dr-bonez in #1820
|
||||
- ignore file not found error for delete by @dr-bonez in #1822
|
||||
- Multiple by @MattDHill in #1823
|
||||
- Bugfix/correctly package backend job by @moerketh in #1826
|
||||
- update patch-db by @dr-bonez in #1831
|
||||
- give name to logs file by @MattDHill in #1833
|
||||
- play song during update by @dr-bonez in #1832
|
||||
- Seed patchdb UI data by @elvece in #1835
|
||||
- update patch db and enable logging by @dr-bonez in #1837
|
||||
- reduce patch-db log level to warn by @dr-bonez in #1840
|
||||
- update ts matches to fix properties ordering bug by @elvece in #1843
|
||||
- handle multiple image tags having the same hash and increase timeout by @dr-bonez in #1844
|
||||
- retry pgloader up to 5x by @dr-bonez in #1845
|
||||
- show connection bar right away by @MattDHill in #1849
|
||||
- dizzy Rebranding to embassyOS by @k0gen in #1851
|
||||
- update patch db by @MattDHill in #1852
|
||||
- camera_flash screenshots update by @k0gen in #1853
|
||||
- disable concurrency and delete tmpdir before retry by @dr-bonez in #1846
|
||||
|
||||
## New Contributors
|
||||
|
||||
- @redragonx made their first contribution in #1733
|
||||
|
||||
|
||||
# v0.3.1.1
|
||||
## What's Changed
|
||||
|
||||
- whale2 docker stats fix by @k0gen in #1630
|
||||
- update backend dependencies by @dr-bonez in #1637
|
||||
- Fix/receipts health by @Blu-J in #1616
|
||||
- return correct error on failed os download by @dr-bonez in #1636
|
||||
- fix build by @dr-bonez in #1639
|
||||
- Update product.yaml by @dr-bonez in #1638
|
||||
- handle case where selected union enum is invalid after migration by @MattDHill in #1658
|
||||
- fix: Resolve fighting with NM by @Blu-J in #1660
|
||||
- sdk: don't allow mounts in inject actions by @chrisguida in #1653
|
||||
- feat: Variable args by @Blu-J in #1667
|
||||
- add readme to system-images folder by @elvece in #1665
|
||||
- Mask chars beyond 16 by @MattDHill in #1666
|
||||
- chore: Update to have the new version 0.3.1.1 by @Blu-J in #1668
|
||||
- feat: Make the rename effect by @Blu-J in #1669
|
||||
- fix migration, add logging by @dr-bonez in #1674
|
||||
- run build checks only when relevant FE changes by @elvece in #1664
|
||||
- trust local ca by @dr-bonez in #1670
|
||||
- lower log level for docker deser fallback message by @dr-bonez in #1672
|
||||
- refactor build process by @dr-bonez in #1675
|
||||
- chore: enable strict mode by @waterplea in #1569
|
||||
- draft releases notes for 0311 by @MattDHill in #1677
|
||||
- add standby mode by @dr-bonez in #1671
|
||||
- feat: atomic writing by @Blu-J in #1673
|
||||
- allow server.update to update to current version by @dr-bonez in #1679
|
||||
- allow falsey rpc response by @dr-bonez in #1680
|
||||
- issue notification when individual package restore fails by @dr-bonez in #1685
|
||||
- replace bang with question mark in html by @MattDHill in #1683
|
||||
- only validate mounts for inject if eos >=0.3.1.1 by @dr-bonez in #1686
|
||||
- add marketplace_url to backup metadata for service by @dr-bonez in #1688
|
||||
- marketplace published at for service by @MattDHill in #1689
|
||||
- sync data to fs before shutdown by @dr-bonez in #1690
|
||||
- messaging for restart, shutdown, rebuild by @MattDHill in #1691
|
||||
- honor shutdown from diagnostic ui by @dr-bonez in #1692
|
||||
- ask for sudo password immediately during make by @dr-bonez in #1693
|
||||
- sync blockdev after update by @dr-bonez in #1694
|
||||
- set Matt as default assignee by @MattDHill in #1697
|
||||
- NO_KEY for CI images by @dr-bonez in #1700
|
||||
- fix typo by @dr-bonez in #1702
|
||||
|
||||
# v0.3.1
|
||||
## What's Changed
|
||||
- Feat bulk locking by @Blu-J in #1422
|
||||
- Switching SSH keys to start9 user by @k0gen in #1321
|
||||
- chore: Convert from ajv to ts-matches by @Blu-J in #1415
|
||||
- Fix/id params by @elvece in #1414
|
||||
- make nicer update sound by @ProofOfKeags in #1438
|
||||
- adds product key to error message in setup flow when there is mismatch by @dr-bonez in #1436
|
||||
- Update README.md to include yq by @cryptodread in #1385
|
||||
- yin_yang For the peace of mind yin_yang by @k0gen in #1444
|
||||
- Feature/update sound by @ProofOfKeags in #1439
|
||||
- Feature/script packing by @ProofOfKeags in #1435
|
||||
- rename ActionImplementation to PackageProcedure by @dr-bonez in #1448
|
||||
- Chore/warning cleanse by @ProofOfKeags in #1447
|
||||
- refactor packing to async by @ProofOfKeags in #1453
|
||||
- Add nginx config for proxy redirect by @yzernik in #1421
|
||||
- Proxy local frontend to remote backend by @elvece in #1452
|
||||
- Feat/js action by @Blu-J in #1437
|
||||
- Fix/making js work by @Blu-J in #1456
|
||||
- fix: Dependency vs dependents by @Blu-J in #1462
|
||||
- refactor: isolate network toast and login redirect to separate services by @waterplea in #1412
|
||||
- Fix links in CONTRIBUTING.md, update ToC by @BBlackwo in #1463
|
||||
- Feature/require script consistency by @ProofOfKeags in #1451
|
||||
- Chore/version 0 3 1 0 by @Blu-J in #1475
|
||||
- remove interactive TTY requirement from scripts by @moerketh in #1469
|
||||
- Disable view in marketplace button when side-loaded by @BBlackwo in #1471
|
||||
- Link to tor address on LAN setup page (#1277) by @BBlackwo in #1466
|
||||
- UI version updates and welcome message for 0.3.1 by @elvece in #1479
|
||||
- Update contribution and frontend readme by @BBlackwo in #1467
|
||||
- Clean up config by @MattDHill in #1484
|
||||
- Enable Control Groups for Docker containers by @k0gen in #1468
|
||||
- Fix/patch db unwrap remove by @Blu-J in #1481
|
||||
- handles spaces in working dir in make-image.sh by @moerketh in #1487
|
||||
- UI cosmetic improvements by @MattDHill in #1486
|
||||
- chore: fix the master by @Blu-J in #1495
|
||||
- generate unique ca names based off of server id by @ProofOfKeags in #1500
|
||||
- allow embassy-cli not as root by @dr-bonez in #1501
|
||||
- fix: potential fix for the docker leaking the errors and such by @Blu-J in #1496
|
||||
- Fix/memory leak docker by @Blu-J in #1505
|
||||
- fixes serialization of regex pattern + description by @ProofOfKeags in #1509
|
||||
- allow interactive TTY if available by @dr-bonez in #1508
|
||||
- fix "missing proxy" error in embassy-cli by @dr-bonez in #1516
|
||||
- Feat/js known errors by @Blu-J in #1514
|
||||
- fixes a bug where nginx will crash if eos goes into diagnostic mode a… by @dr-bonez in #1506
|
||||
- fix: restart/ uninstall sometimes didn't work by @Blu-J in #1527
|
||||
- add "error_for_status" to static file downloads by @dr-bonez in #1532
|
||||
- fixes #1169 by @dr-bonez in #1533
|
||||
- disable unnecessary services by @dr-bonez in #1535
|
||||
- chore: Update types to match embassyd by @Blu-J in #1539
|
||||
- fix: found a unsaturaded args fix by @Blu-J in #1540
|
||||
- chore: Update the lite types to include the union and enum by @Blu-J in #1542
|
||||
- Feat: Make the js check for health by @Blu-J in #1543
|
||||
- fix incorrect error message for deserialization in ValueSpecString by @dr-bonez in #1547
|
||||
- fix dependency/dependent id issue by @dr-bonez in #1546
|
||||
- add textarea to ValueSpecString by @dr-bonez in #1534
|
||||
- Feat/js metadata by @Blu-J in #1548
|
||||
- feat: uid/gid/mode added to metadata by @Blu-J in #1551
|
||||
- Strict null checks by @waterplea in #1464
|
||||
- fix backend builds for safe git config by @elvece in #1549
|
||||
- update should send version not version spec by @elvece in #1559
|
||||
- chore: Add tracing for debuging the js procedure slowness by @Blu-J in #1552
|
||||
- Reset password through setup wizard by @MattDHill in #1490
|
||||
- feat: Make sdk by @Blu-J in #1564
|
||||
- fix: Missing a feature flat cfg by @Blu-J in #1563
|
||||
- fixed sentence that didn't make sense by @BitcoinMechanic in #1565
|
||||
- refactor(patch-db): use PatchDB class declaratively by @waterplea in #1562
|
||||
- fix bugs with config and clean up dev options by @MattDHill in #1558
|
||||
- fix: Make it so we only need the password on the backup by @Blu-J in #1566
|
||||
- kill all sessions and remove ripple effect by @MattDHill in #1567
|
||||
- adjust service marketplace button for installation source relevance by @elvece in #1571
|
||||
- fix connection failure display monitoring and other style changes by @MattDHill in #1573
|
||||
- add dns server to embassy-os by @dr-bonez in #1572
|
||||
- Fix/mask generic inputs by @elvece in #1570
|
||||
- Fix/sideload icon type by @elvece in #1577
|
||||
- add avahi conditional compilation flags to dns by @dr-bonez in #1579
|
||||
- selective backups and better drive selection interface by @MattDHill in #1576
|
||||
- Feat/use modern tor by @kn0wmad in #1575
|
||||
- update welcome notes for 031 by @MattDHill in #1580
|
||||
- fix: Properties had a null description by @Blu-J in #1581
|
||||
- fix backup lock ordering by @dr-bonez in #1582
|
||||
- Bugfix/backup lock order by @dr-bonez in #1583
|
||||
- preload redacted and visibility hidden by @MattDHill in #1584
|
||||
- turn chevron red in config if error by @MattDHill in #1586
|
||||
- switch to utc by @dr-bonez in #1587
|
||||
- update patchdb for array patch fix by @elvece in #1588
|
||||
- filter package ids when backing up by @dr-bonez in #1589
|
||||
- add select/deselect all to backups and enum lists by @elvece in #1590
|
||||
- fix: Stop the buffer from dropped pre-maturly by @Blu-J in #1591
|
||||
- chore: commit the snapshots by @Blu-J in #1592
|
||||
- nest new entries and message updates better by @MattDHill in #1595
|
||||
- fix html parsing in logs by @elvece in #1598
|
||||
- don't crash service if io-format is set for main by @dr-bonez in #1599
|
||||
- strip html from colors from logs by @elvece in #1604
|
||||
- feat: fetch effect by @Blu-J in #1605
|
||||
- Fix/UI misc by @elvece in #1606
|
||||
- display bottom item in backup list and refactor for cleanliness by @MattDHill in #1609
|
||||
|
||||
# v0.3.0.3
|
||||
## What's Changed
|
||||
- refactor: decompose app component by @waterplea in #1359
|
||||
- Update Makefile by @kn0wmad in #1400
|
||||
- ⬐ smarter wget by @k0gen in #1401
|
||||
- prevent the kernel from OOMKilling embassyd by @dr-bonez in #1402
|
||||
- attempt to heal when health check passes by @dr-bonez in #1420
|
||||
- Feat new locking by @Blu-J in #1384
|
||||
- version bump by @dr-bonez in #1423
|
||||
- Update server-show.page.ts by @chrisguida in #1424
|
||||
- Bump async from 2.6.3 to 2.6.4 in /frontend by @dependabot in #1426
|
||||
- Update index.html by @mirkoRainer in #1419
|
||||
|
||||
## New Contributors
|
||||
- @dependabot made their first contribution in #1426
|
||||
- @mirkoRainer made their first contribution in #1419
|
||||
|
||||
# v0.3.0.2
|
||||
- Minor compatibility fixes
|
||||
- #1392
|
||||
- #1390
|
||||
- #1388
|
||||
|
||||
# v0.3.0.1
|
||||
Minor bugfixes and performance improvements
|
||||
|
||||
# v0.3.0
|
||||
- Websockets
|
||||
- Real-time sync
|
||||
- Patch DB
|
||||
- Closely mirror FE and BE state. Most operating systems are connected to their GUI. Here it is served over the web. Patch DB and websockets serve to close the perceptual gap of this inherent challenge.
|
||||
- Switch kernel from Raspbian to Ubuntu
|
||||
- 64 bit
|
||||
- Possibility for alternative hardware
|
||||
- Merging of lifeline, agent, and appmgr into embassyd
|
||||
- Elimination of Haskell in favor of pure Rust
|
||||
- Unified API for interacting with the OS
|
||||
- Easier to build from source
|
||||
- OS (quarantined from OS and service data)
|
||||
- Kernel/boot
|
||||
- Persistent metadata (disk guid, product key)
|
||||
- Rootfs (the os)
|
||||
- Reserved (for updates) - swaps with rootfs
|
||||
- Revamped OS updates
|
||||
- Progress indicators
|
||||
- Non-blocking
|
||||
- Simple swap on reboot
|
||||
- Revamped setup flow
|
||||
- Elimination of Setup App (Apple/Google dependencies gone)
|
||||
- Setup Wizard on http://embassy.local
|
||||
- Revamped service config
|
||||
- Dynamic, validated forms
|
||||
- Diagnostic UI
|
||||
- Missing disk, wrong disk, corrupt disk
|
||||
- Turing complete API for actions, backup/restore, config, properties, notifications, health checks, and dependency requirements
|
||||
- Optional, arbitrary inputs for actions
|
||||
- Install, update, recover progress for apps
|
||||
- Multiple interfaces
|
||||
- E.g. rpc, p2p, ui
|
||||
- Health checks
|
||||
- Developer defined
|
||||
- Internal, dependencies, and/or external
|
||||
- Full Embassy backup (diff-based)
|
||||
- External drive support/requirement
|
||||
- Single at first
|
||||
- Groundwork for extension and mirror drives
|
||||
- Disk encryption
|
||||
- Random key encrypted with static value
|
||||
- Groundwork for swapping static value with chosen password
|
||||
- Session Management
|
||||
- List all active sessions
|
||||
- Option to kill
|
||||
- More robust and extensive logs
|
||||
- Donations
|
||||
252
CONTRIBUTING.md
@@ -1,11 +1,20 @@
|
||||
<!-- omit in toc -->
|
||||
|
||||
# Contributing to Embassy OS
|
||||
|
||||
First off, thanks for taking the time to contribute! ❤️
|
||||
|
||||
All types of contributions are encouraged and valued. See the [Table of Contents](#table-of-contents) for different ways to help and details about how this project handles them. Please make sure to read the relevant section before making your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions. 🎉
|
||||
All types of contributions are encouraged and valued. See the
|
||||
[Table of Contents](#table-of-contents) for different ways to help and details
|
||||
about how this project handles them. Please make sure to read the relevant
|
||||
section before making your contribution. It will make it a lot easier for us
|
||||
maintainers and smooth out the experience for all involved. The community looks
|
||||
forward to your contributions. 🎉
|
||||
|
||||
> And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about:
|
||||
> And if you like the project, but just don't have time to contribute, that's
|
||||
> fine. There are other easy ways to support the project and show your
|
||||
> appreciation, which we would also be very happy about:
|
||||
>
|
||||
> - Star the project
|
||||
> - Tweet about it
|
||||
> - Refer this project in your project's readme
|
||||
@@ -13,6 +22,7 @@ All types of contributions are encouraged and valued. See the [Table of Contents
|
||||
> - Buy an [Embassy](https://start9labs.com)
|
||||
|
||||
<!-- omit in toc -->
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [I Have a Question](#i-have-a-question)
|
||||
@@ -33,15 +43,19 @@ All types of contributions are encouraged and valued. See the [Table of Contents
|
||||
- [Join The Discussion](#join-the-discussion)
|
||||
- [Join The Project Team](#join-the-project-team)
|
||||
|
||||
|
||||
|
||||
## I Have a Question
|
||||
|
||||
> If you want to ask a question, we assume that you have read the available [Documentation](https://docs.start9labs.com).
|
||||
> If you want to ask a question, we assume that you have read the available
|
||||
> [Documentation](https://docs.start9labs.com).
|
||||
|
||||
Before you ask a question, it is best to search for existing [Issues](https://github.com/Start9Labs/embassy-os/issues) that might help you. In case you have found a suitable issue and still need clarification, you can write your question in this issue. It is also advisable to search the internet for answers first.
|
||||
Before you ask a question, it is best to search for existing
|
||||
[Issues](https://github.com/Start9Labs/embassy-os/issues) that might help you.
|
||||
In case you have found a suitable issue and still need clarification, you can
|
||||
write your question in this issue. It is also advisable to search the internet
|
||||
for answers first.
|
||||
|
||||
If you then still feel the need to ask a question and need clarification, we recommend the following:
|
||||
If you then still feel the need to ask a question and need clarification, we
|
||||
recommend the following:
|
||||
|
||||
- Open an [Issue](https://github.com/Start9Labs/embassy-os/issues/new).
|
||||
- Provide as much context as you can about what you're running into.
|
||||
@@ -67,100 +81,180 @@ Depending on how large the project is, you may want to outsource the questioning
|
||||
## I Want To Contribute
|
||||
|
||||
> ### Legal Notice <!-- omit in toc -->
|
||||
> When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project license.
|
||||
>
|
||||
> When contributing to this project, you must agree that you have authored 100%
|
||||
> of the content, that you have the necessary rights to the content and that the
|
||||
> content you contribute may be provided under the project license.
|
||||
|
||||
### Reporting Bugs
|
||||
|
||||
<!-- omit in toc -->
|
||||
|
||||
#### Before Submitting a Bug Report
|
||||
|
||||
A good bug report shouldn't leave others needing to chase you up for more information. Therefore, we ask you to investigate carefully, collect information and describe the issue in detail in your report. Please complete the following steps in advance to help us fix any potential bug as fast as possible.
|
||||
A good bug report shouldn't leave others needing to chase you up for more
|
||||
information. Therefore, we ask you to investigate carefully, collect information
|
||||
and describe the issue in detail in your report. Please complete the following
|
||||
steps in advance to help us fix any potential bug as fast as possible.
|
||||
|
||||
- Make sure that you are using the latest version.
|
||||
- Determine if your bug is really a bug and not an error on your side e.g. using incompatible environment components/versions (Make sure that you have read the [documentation](https://start9.com/latest/user-manual). If you are looking for support, you might want to check [this section](#i-have-a-question)).
|
||||
- To see if other users have experienced (and potentially already solved) the same issue you are having, check if there is not already a bug report existing for your bug or error in the [bug tracker](https://github.com/Start9Labs/embassy-os/issues?q=label%3Abug).
|
||||
- Also make sure to search the internet (including Stack Overflow) to see if users outside of the GitHub community have discussed the issue.
|
||||
- Determine if your bug is really a bug and not an error on your side e.g. using
|
||||
incompatible environment components/versions (Make sure that you have read the
|
||||
[documentation](https://start9.com/latest/user-manual). If you are looking for
|
||||
support, you might want to check [this section](#i-have-a-question)).
|
||||
- To see if other users have experienced (and potentially already solved) the
|
||||
same issue you are having, check if there is not already a bug report existing
|
||||
for your bug or error in the
|
||||
[bug tracker](https://github.com/Start9Labs/embassy-os/issues?q=label%3Abug).
|
||||
- Also make sure to search the internet (including Stack Overflow) to see if
|
||||
users outside of the GitHub community have discussed the issue.
|
||||
- Collect information about the bug:
|
||||
- Stack trace (Traceback)
|
||||
- Client OS, Platform and Version (Windows/Linux/macOS/iOS/Android, Firefox/Tor Browser/Consulate)
|
||||
- Version of the interpreter, compiler, SDK, runtime environment, package manager, depending on what seems relevant.
|
||||
- Client OS, Platform and Version (Windows/Linux/macOS/iOS/Android,
|
||||
Firefox/Tor Browser/Consulate)
|
||||
- Version of the interpreter, compiler, SDK, runtime environment, package
|
||||
manager, depending on what seems relevant.
|
||||
- Possibly your input and the output
|
||||
- Can you reliably reproduce the issue? And can you also reproduce it with older versions?
|
||||
- Can you reliably reproduce the issue? And can you also reproduce it with
|
||||
older versions?
|
||||
|
||||
<!-- omit in toc -->
|
||||
|
||||
#### How Do I Submit a Good Bug Report?
|
||||
|
||||
> You must never report security related issues, vulnerabilities or bugs to the issue tracker, or elsewhere in public. Instead sensitive bugs must be sent by email to <security@start9labs.com>.
|
||||
> You must never report security related issues, vulnerabilities or bugs to the
|
||||
> issue tracker, or elsewhere in public. Instead sensitive bugs must be sent by
|
||||
> email to <security@start9labs.com>.
|
||||
|
||||
<!-- You may add a PGP key to allow the messages to be sent encrypted as well. -->
|
||||
|
||||
We use GitHub issues to track bugs and errors. If you run into an issue with the project:
|
||||
We use GitHub issues to track bugs and errors. If you run into an issue with the
|
||||
project:
|
||||
|
||||
- Open an [Issue](https://github.com/Start9Labs/embassy-os/issues/new/choose) selecting the appropriate type.
|
||||
- Open an [Issue](https://github.com/Start9Labs/embassy-os/issues/new/choose)
|
||||
selecting the appropriate type.
|
||||
- Explain the behavior you would expect and the actual behavior.
|
||||
- Please provide as much context as possible and describe the *reproduction steps* that someone else can follow to recreate the issue on their own. This usually includes your code. For good bug reports you should isolate the problem and create a reduced test case.
|
||||
- Please provide as much context as possible and describe the _reproduction
|
||||
steps_ that someone else can follow to recreate the issue on their own. This
|
||||
usually includes your code. For good bug reports you should isolate the
|
||||
problem and create a reduced test case.
|
||||
- Provide the information you collected in the previous section.
|
||||
|
||||
Once it's filed:
|
||||
|
||||
- The project team will label the issue accordingly.
|
||||
- A team member will try to reproduce the issue with your provided steps. If there are no reproduction steps or no obvious way to reproduce the issue, the team will ask you for those steps and mark the issue as `Question`. Bugs with the `Question` tag will not be addressed until they are answered.
|
||||
- If the team is able to reproduce the issue, it will be marked a scoping level tag, as well as possibly other tags (such as `Security`), and the issue will be left to be [implemented by someone](#your-first-code-contribution).
|
||||
- A team member will try to reproduce the issue with your provided steps. If
|
||||
there are no reproduction steps or no obvious way to reproduce the issue, the
|
||||
team will ask you for those steps and mark the issue as `Question`. Bugs with
|
||||
the `Question` tag will not be addressed until they are answered.
|
||||
- If the team is able to reproduce the issue, it will be marked a scoping level
|
||||
tag, as well as possibly other tags (such as `Security`), and the issue will
|
||||
be left to be [implemented by someone](#your-first-code-contribution).
|
||||
|
||||
<!-- You might want to create an issue template for bugs and errors that can be used as a guide and that defines the structure of the information to be included. If you do so, reference it here in the description. -->
|
||||
|
||||
|
||||
### Suggesting Enhancements
|
||||
|
||||
This section guides you through submitting an enhancement suggestion for Embassy OS, **including completely new features and minor improvements to existing functionality**. Following these guidelines will help maintainers and the community to understand your suggestion and find related suggestions.
|
||||
This section guides you through submitting an enhancement suggestion for Embassy
|
||||
OS, **including completely new features and minor improvements to existing
|
||||
functionality**. Following these guidelines will help maintainers and the
|
||||
community to understand your suggestion and find related suggestions.
|
||||
|
||||
<!-- omit in toc -->
|
||||
|
||||
#### Before Submitting an Enhancement
|
||||
|
||||
- Make sure that you are using the latest version.
|
||||
- Read the [documentation](https://start9.com/latest/user-manual) carefully and find out if the functionality is already covered, maybe by an individual configuration.
|
||||
- Perform a [search](https://github.com/Start9Labs/embassy-os/issues) to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one.
|
||||
- Find out whether your idea fits with the scope and aims of the project. It's up to you to make a strong case to convince the project's developers of the merits of this feature. Keep in mind that we want features that will be useful to the majority of our users and not just a small subset. If you're just targeting a minority of users, consider writing an add-on/plugin library.
|
||||
- Read the [documentation](https://start9.com/latest/user-manual) carefully and
|
||||
find out if the functionality is already covered, maybe by an individual
|
||||
configuration.
|
||||
- Perform a [search](https://github.com/Start9Labs/embassy-os/issues) to see if
|
||||
the enhancement has already been suggested. If it has, add a comment to the
|
||||
existing issue instead of opening a new one.
|
||||
- Find out whether your idea fits with the scope and aims of the project. It's
|
||||
up to you to make a strong case to convince the project's developers of the
|
||||
merits of this feature. Keep in mind that we want features that will be useful
|
||||
to the majority of our users and not just a small subset. If you're just
|
||||
targeting a minority of users, consider writing an add-on/plugin library.
|
||||
|
||||
<!-- omit in toc -->
|
||||
|
||||
#### How Do I Submit a Good Enhancement Suggestion?
|
||||
|
||||
Enhancement suggestions are tracked as [GitHub issues](https://github.com/Start9Labs/embassy-os/issues).
|
||||
Enhancement suggestions are tracked as
|
||||
[GitHub issues](https://github.com/Start9Labs/embassy-os/issues).
|
||||
|
||||
- Use a **clear and descriptive title** for the issue to identify the suggestion.
|
||||
- Provide a **step-by-step description of the suggested enhancement** in as many details as possible.
|
||||
- **Describe the current behavior** and **explain which behavior you expected to see instead** and why. At this point you can also tell which alternatives do not work for you.
|
||||
- You may want to **include screenshots and animated GIFs** which help you demonstrate the steps or point out the part which the suggestion is related to. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux. <!-- this should only be included if the project has a GUI -->
|
||||
- **Explain why this enhancement would be useful** to most Embassy OS users. You may also want to point out the other projects that solved it better and which could serve as inspiration.
|
||||
- Use a **clear and descriptive title** for the issue to identify the
|
||||
suggestion.
|
||||
- Provide a **step-by-step description of the suggested enhancement** in as many
|
||||
details as possible.
|
||||
- **Describe the current behavior** and **explain which behavior you expected to
|
||||
see instead** and why. At this point you can also tell which alternatives do
|
||||
not work for you.
|
||||
- You may want to **include screenshots and animated GIFs** which help you
|
||||
demonstrate the steps or point out the part which the suggestion is related
|
||||
to. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on
|
||||
macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast)
|
||||
or [this tool](https://github.com/GNOME/byzanz) on Linux.
|
||||
<!-- this should only be included if the project has a GUI -->
|
||||
- **Explain why this enhancement would be useful** to most Embassy OS users. You
|
||||
may also want to point out the other projects that solved it better and which
|
||||
could serve as inspiration.
|
||||
|
||||
<!-- You might want to create an issue template for enhancement suggestions that can be used as a guide and that defines the structure of the information to be included. If you do so, reference it here in the description. -->
|
||||
|
||||
### Project Structure
|
||||
|
||||
EmbassyOS is composed of the following components. Please visit the README for each component to understand the dependency requirements and installation instructions.
|
||||
- [`ui`](frontend/README.md) (Typescript Ionic Angular) is the code that is deployed to the browser to provide the user interface for EmbassyOS.
|
||||
- [`backend`](backend/README.md) (Rust) is a command line utility, daemon, and software development kit that sets up and manages services and their environments, provides the interface for the ui, manages system state, and provides utilities for packaging services for EmbassyOS.
|
||||
- `patch-db` - A diff based data store that is used to synchronize data between the front and backend.
|
||||
- Notably, `patch-db` has a [client](https://github.com/Start9Labs/patch-db/tree/master/client) with its own dependency and installation requirements.
|
||||
- `rpc-toolkit` - A library for generating an rpc server with cli bindings from Rust functions.
|
||||
- `system-images` - (Docker, Rust) A suite of utility Docker images that are preloaded with EmbassyOS to assist with functions relating to services (eg. configuration, backups, health checks).
|
||||
- [`setup-wizard`](frontend/README.md)- Code for the user interface that is displayed during the setup and recovery process for EmbassyOS.
|
||||
- [`diagnostic-ui`](frontend/README.md) - Code for the user interface that is displayed when something has gone wrong with starting up EmbassyOS, which provides helpful debugging tools.
|
||||
embassyOS is composed of the following components. Please visit the README for
|
||||
each component to understand the dependency requirements and installation
|
||||
instructions.
|
||||
|
||||
- [`backend`](backend/README.md) (Rust) is a command line utility, daemon, and
|
||||
software development kit that sets up and manages services and their
|
||||
environments, provides the interface for the ui, manages system state, and
|
||||
provides utilities for packaging services for embassyOS.
|
||||
- [`build`](build/README.md) contains scripts and necessary for deploying
|
||||
embassyOS to a debian/raspbian system.
|
||||
- [`frontend`](frontend/README.md) (Typescript Ionic Angular) is the code that
|
||||
is deployed to the browser to provide the user interface for embassyOS.
|
||||
- `projects/ui` - Code for the user interface that is displayed when embassyOS
|
||||
is running normally.
|
||||
- `projects/setup-wizard`(frontend/README.md) - Code for the user interface
|
||||
that is displayed during the setup and recovery process for embassyOS.
|
||||
- `projects/diagnostic-ui` - Code for the user interface that is displayed
|
||||
when something has gone wrong with starting up embassyOS, which provides
|
||||
helpful debugging tools.
|
||||
- `libs` (Rust) is a set of standalone crates that were separated out of
|
||||
`backend` for the purpose of portability
|
||||
- `patch-db` - A diff based data store that is used to synchronize data between
|
||||
the front and backend.
|
||||
- Notably, `patch-db` has a
|
||||
[client](https://github.com/Start9Labs/patch-db/tree/master/client) with its
|
||||
own dependency and installation requirements.
|
||||
- `system-images` - (Docker, Rust) A suite of utility Docker images that are
|
||||
preloaded with embassyOS to assist with functions relating to services (eg.
|
||||
configuration, backups, health checks).
|
||||
|
||||
### Your First Code Contribution
|
||||
|
||||
#### Setting Up Your Development Environment
|
||||
|
||||
First, clone the EmbassyOS repository and from the project root, pull in the submodules for dependent libraries.
|
||||
First, clone the embassyOS repository and from the project root, pull in the
|
||||
submodules for dependent libraries.
|
||||
|
||||
```sh
|
||||
git clone https://github.com/Start9Labs/embassy-os.git
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
Depending on which component of the ecosystem you are interested in contributing to, follow the installation requirements listed in that component's README (linked [above](#project-structure))
|
||||
Depending on which component of the ecosystem you are interested in contributing
|
||||
to, follow the installation requirements listed in that component's README
|
||||
(linked [above](#project-structure))
|
||||
|
||||
#### Building The Image
|
||||
This step is for setting up an environment in which to test your code changes if you do not yet have a EmbassyOS.
|
||||
#### Building The Raspberry Pi Image
|
||||
|
||||
This step is for setting up an environment in which to test your code changes if
|
||||
you do not yet have a embassyOS.
|
||||
|
||||
- Requirements
|
||||
- `ext4fs` (available if running on the Linux kernel)
|
||||
@@ -168,41 +262,79 @@ This step is for setting up an environment in which to test your code changes if
|
||||
- GNU Make
|
||||
- Building
|
||||
- see setup instructions [here](build/README.md)
|
||||
- run `make` from the project root
|
||||
- run `make embassyos-raspi.img ARCH=aarch64` from the project root
|
||||
|
||||
### Improving The Documentation
|
||||
You can find the repository for Start9's documentation [here](https://github.com/Start9Labs/documentation). If there is something you would like to see added, let us know, or create an issue yourself. Welcome are contributions for lacking or incorrect information, broken links, requested additions, or general style improvements.
|
||||
|
||||
Contributions in the form of setup guides for integrations with external applications are highly encouraged. If you struggled through a process and would like to share your steps with others, check out the docs for each [service](https://github.com/Start9Labs/documentation/blob/master/source/user-manuals/available-services/index.rst) we support. The wrapper repos contain sections for adding integration guides, such as this [one](https://github.com/Start9Labs/bitcoind-wrapper/tree/master/docs). These not only help out others in the community, but inform how we can create a more seamless and intuitive experience.
|
||||
You can find the repository for Start9's documentation
|
||||
[here](https://github.com/Start9Labs/documentation). If there is something you
|
||||
would like to see added, let us know, or create an issue yourself. Welcome are
|
||||
contributions for lacking or incorrect information, broken links, requested
|
||||
additions, or general style improvements.
|
||||
|
||||
Contributions in the form of setup guides for integrations with external
|
||||
applications are highly encouraged. If you struggled through a process and would
|
||||
like to share your steps with others, check out the docs for each
|
||||
[service](https://github.com/Start9Labs/documentation/blob/master/source/user-manuals/available-services/index.rst)
|
||||
we support. The wrapper repos contain sections for adding integration guides,
|
||||
such as this
|
||||
[one](https://github.com/Start9Labs/bitcoind-wrapper/tree/master/docs). These
|
||||
not only help out others in the community, but inform how we can create a more
|
||||
seamless and intuitive experience.
|
||||
|
||||
## Styleguides
|
||||
|
||||
### Formatting
|
||||
Each component of EmbassyOS contains its own style guide. Code must be formatted with the formatter designated for each component. These are outlined within each component folder's README.
|
||||
|
||||
Each component of embassyOS contains its own style guide. Code must be formatted
|
||||
with the formatter designated for each component. These are outlined within each
|
||||
component folder's README.
|
||||
|
||||
### Atomic Commits
|
||||
Commits [should be atomic](https://en.wikipedia.org/wiki/Atomic_commit#Atomic_commit_convention) and diffs should be easy to read.
|
||||
Do not mix any formatting fixes or code moves with actual code changes.
|
||||
|
||||
Commits
|
||||
[should be atomic](https://en.wikipedia.org/wiki/Atomic_commit#Atomic_commit_convention)
|
||||
and diffs should be easy to read. Do not mix any formatting fixes or code moves
|
||||
with actual code changes.
|
||||
|
||||
### Commit Messages
|
||||
If a commit touches only 1 component, prefix the message with the affected component. i.e. `backend: update to tokio v0.3`.
|
||||
|
||||
If a commit touches only 1 component, prefix the message with the affected
|
||||
component. i.e. `backend: update to tokio v0.3`.
|
||||
|
||||
### Pull Requests
|
||||
The body of a pull request should contain sufficient description of what the changes do, as well as a justification.
|
||||
You should include references to any relevant [issues](https://github.com/Start9Labs/embassy-os/issues).
|
||||
|
||||
The body of a pull request should contain sufficient description of what the
|
||||
changes do, as well as a justification. You should include references to any
|
||||
relevant [issues](https://github.com/Start9Labs/embassy-os/issues).
|
||||
|
||||
### Rebasing Changes
|
||||
When a pull request conflicts with the target branch, you may be asked to rebase it on top of the current target branch. The `git rebase` command will take care of rebuilding your commits on top of the new base.
|
||||
|
||||
This project aims to have a clean git history, where code changes are only made in non-merge commits. This simplifies auditability because merge commits can be assumed to not contain arbitrary code changes.
|
||||
When a pull request conflicts with the target branch, you may be asked to rebase
|
||||
it on top of the current target branch. The `git rebase` command will take care
|
||||
of rebuilding your commits on top of the new base.
|
||||
|
||||
This project aims to have a clean git history, where code changes are only made
|
||||
in non-merge commits. This simplifies auditability because merge commits can be
|
||||
assumed to not contain arbitrary code changes.
|
||||
|
||||
## Join The Discussion
|
||||
Current or aspiring contributors? Join our community developer [Matrix channel](https://matrix.to/#/#community-dev:matrix.start9labs.com).
|
||||
|
||||
Just interested in or using the project? Join our community [Telegram](https://t.me/start9_labs) or [Matrix](https://matrix.to/#/#community:matrix.start9labs.com).
|
||||
Current or aspiring contributors? Join our community developer
|
||||
[Matrix channel](https://matrix.to/#/#community-dev:matrix.start9labs.com).
|
||||
|
||||
Just interested in or using the project? Join our community
|
||||
[Telegram](https://t.me/start9_labs) or
|
||||
[Matrix](https://matrix.to/#/#community:matrix.start9labs.com).
|
||||
|
||||
## Join The Project Team
|
||||
Interested in becoming a part of the Start9 Labs team? Send an email to <jobs@start9labs.com>
|
||||
|
||||
Interested in becoming a part of the Start9 Labs team? Send an email to
|
||||
<jobs@start9labs.com>
|
||||
|
||||
<!-- omit in toc -->
|
||||
|
||||
## Attribution
|
||||
This guide is based on the **contributing-gen**. [Make your own](https://github.com/bttger/contributing-gen)!
|
||||
|
||||
This guide is based on the **contributing-gen**.
|
||||
[Make your own](https://github.com/bttger/contributing-gen)!
|
||||
|
||||
5607
Cargo.lock
generated
Normal file
55
LICENSE.md
@@ -1,25 +1,42 @@
|
||||
# START9 PERSONAL USE LICENSE v1.0
|
||||
# START9 NON-COMMERCIAL LICENSE v1
|
||||
Version 1, 22 September 2022
|
||||
|
||||
This license governs the use of the accompanying Software. If you use the Software, you accept this license. If you do not accept the license, do not use the Software.
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. **Definitions.**
|
||||
1. “Licensor” means the copyright owner, Start9 Labs, Inc, or its successor(s) in interest, or a future assignee of the copyright.
|
||||
2. “Source Code” means the preferred form of the Software for making modifications to it.
|
||||
3. “Object Code” means any non-source form of the Software, including the machine-language output by a compiler or assembler.
|
||||
4. “Distribute” means to convey or to publish and generally has the same meaning here as under U.S. Copyright law.
|
||||
5. “Sell” means practicing any or all of the rights granted to you under the License to provide to third parties, for a fee or other consideration (including without limitation fees for hosting or consulting/support services related to the Software), a product or service whose value derives, entirely or substantially, from the functionality of the Software.
|
||||
### 1.Definitions
|
||||
|
||||
2. **Grant of Rights.** Subject to the terms of this license, the Licensor grants you, the licensee, a non-exclusive, worldwide, royalty-free copyright license to:
|
||||
1. Access, audit, copy, modify, compile, or distribute the Source Code or modifications to the Source Code.
|
||||
2. Run, test, or otherwise use the Object Code.
|
||||
"License" means version 1 of the Start9 Non-Commercial License.
|
||||
|
||||
3. **Limitations.**
|
||||
1. The grant of rights under the License will NOT include, and the License does NOT grant you the right to:
|
||||
1. Sell the Software or any derivative works based thereon.
|
||||
2. Distribute the Object Code.
|
||||
2. If you Distribute the Source Code, or if permission is separately granted to Distribute the Object Code, you expressly undertake not to remove, or modify, in any manner, the copyright notices attached to the Source Code, and displayed in any output of the Object Code when run, and to reproduce these notices, in an identical manner, in any distributed copies of the Software together with a copy of this license. If you Distribute a modified copy of the Software, or a derivative work based thereon, the work must carry prominent notices stating that you modified it, and giving a relevant date.
|
||||
3. The terms of this license will apply to anyone who comes into possession of a copy of the Software, and any modifications or derivative works based thereon, made by anyone.
|
||||
"Licensor" means the Start9 Labs, Inc, or its successor(s) in interest, or a future assignee of the copyright.
|
||||
|
||||
4. **Contributions.** You hereby grant to Licensor a perpetual, irrevocable, worldwide, non-exclusive, royalty-free license to use and exploit any modifications or derivative works based on the Source Code of which you are the author.
|
||||
"You" (or "Your") means an individual or organization exercising permissions granted by this License.
|
||||
|
||||
5. **Disclaimer.** THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. LICENSOR HAS NO OBLIGATION TO SUPPORT RECIPIENTS OF THE SOFTWARE.
|
||||
"Source Code" for a work means the preferred form of the work for making modifications to it.
|
||||
|
||||
"Object Code" means any non-source form of a work, including the machine-language output by a compiler or assembler.
|
||||
|
||||
"Work" means any work of authorship, whether in Source or Object form, made available under this License.
|
||||
|
||||
"Derivative Work" means any work, whether in Source or Object form, that is based on (or derived from) the Work.
|
||||
|
||||
"Distribute" means to convey or to publish and generally has the same meaning here as under U.S. Copyright law.
|
||||
|
||||
"Sell" means practicing any or all of the rights granted to you under the License to provide to third parties, for a fee or other consideration (including, without limitation, fees for hosting, consulting, or support services), a product or service whose value derives, entirely or substantially, from the functionality of the Work or Derivative Work.
|
||||
|
||||
### 2. Grant of Rights
|
||||
|
||||
Subject to the terms of this license, the Licensor grants you, the licensee, a non-exclusive, worldwide, royalty-free copyright license to access, audit, copy, modify, compile, run, test, distribute, or otherwise use the Software.
|
||||
|
||||
### 3. Limitations
|
||||
|
||||
1. The grant of rights under the License does NOT include, and the License does NOT grant You the right to Sell the Work or Derivative Work.
|
||||
2. If you Distribute the Work or Derivative Work, you expressly undertake not to remove or modify, in any manner, the copyright notices attached to the Work or displayed in any output of the Work when run, and to reproduce these notices, in an identical manner, in any distributed copies of the Work or Derivative Work together with a copy of this License.
|
||||
3. If you Distribute a Derivative Work, it must carry prominent notices stating that it has been modified from the Work, providing a relevant date.
|
||||
|
||||
### 4. Contributions
|
||||
|
||||
You hereby grant to Licensor a perpetual, irrevocable, worldwide, non-exclusive, royalty-free license to use and exploit any Derivative Work of which you are the author.
|
||||
|
||||
### 5. Disclaimer
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. LICENSOR HAS NO OBLIGATION TO SUPPORT RECIPIENTS OF THE SOFTWARE.
|
||||
|
||||
190
Makefile
@@ -1,69 +1,163 @@
|
||||
ENVIRONMENT_FILE := $(shell ./check-environment.sh)
|
||||
GIT_HASH_FILE := $(shell ./check-git-hash.sh)
|
||||
EMBASSY_BINS := backend/target/aarch64-unknown-linux-gnu/release/embassyd backend/target/aarch64-unknown-linux-gnu/release/embassy-init backend/target/aarch64-unknown-linux-gnu/release/embassy-cli backend/target/aarch64-unknown-linux-gnu/release/embassy-sdk
|
||||
EMBASSY_UIS := frontend/dist/ui frontend/dist/setup-wizard frontend/dist/diagnostic-ui
|
||||
EMBASSY_SRC := raspios.img product_key.txt $(EMBASSY_BINS) backend/embassyd.service backend/embassy-init.service $(EMBASSY_UIS) $(shell find build)
|
||||
COMPAT_SRC := $(shell find system-images/compat/src)
|
||||
UTILS_SRC := $(shell find system-images/utils/Dockerfile)
|
||||
BACKEND_SRC := $(shell find backend/src) $(shell find patch-db/*/src) backend/Cargo.toml backend/Cargo.lock
|
||||
FRONTEND_SHARED_SRC := $(shell find frontend/projects/shared) $(shell find frontend/assets) $(shell ls -p frontend/ | grep -v / | sed 's/^/frontend\//g') frontend/node_modules frontend/config.json patch-db/client/dist
|
||||
RASPI_TARGETS := eos_raspberrypi-uninit.img eos_raspberrypi-uninit.tar.gz
|
||||
OS_ARCH := $(shell if echo $(RASPI_TARGETS) | grep -qw "$(MAKECMDGOALS)"; then echo raspberrypi; else uname -m; fi)
|
||||
ARCH := $(shell if [ "$(OS_ARCH)" = "raspberrypi" ]; then echo aarch64; else echo $(OS_ARCH); fi)
|
||||
ENVIRONMENT_FILE = $(shell ./check-environment.sh)
|
||||
GIT_HASH_FILE = $(shell ./check-git-hash.sh)
|
||||
VERSION_FILE = $(shell ./check-version.sh)
|
||||
EMBASSY_BINS := backend/target/$(ARCH)-unknown-linux-gnu/release/embassyd backend/target/$(ARCH)-unknown-linux-gnu/release/embassy-init backend/target/$(ARCH)-unknown-linux-gnu/release/embassy-cli backend/target/$(ARCH)-unknown-linux-gnu/release/embassy-sdk backend/target/$(ARCH)-unknown-linux-gnu/release/avahi-alias libs/target/aarch64-unknown-linux-musl/release/embassy_container_init libs/target/x86_64-unknown-linux-musl/release/embassy_container_init
|
||||
EMBASSY_UIS := frontend/dist/ui frontend/dist/setup-wizard frontend/dist/diagnostic-ui frontend/dist/install-wizard
|
||||
BUILD_SRC := $(shell find build)
|
||||
EMBASSY_SRC := backend/embassyd.service backend/embassy-init.service $(EMBASSY_UIS) $(BUILD_SRC)
|
||||
COMPAT_SRC := $(shell find system-images/compat/ -not -path 'system-images/compat/target/*' -and -not -name *.tar -and -not -name target)
|
||||
UTILS_SRC := $(shell find system-images/utils/ -not -name *.tar)
|
||||
BINFMT_SRC := $(shell find system-images/binfmt/ -not -name *.tar)
|
||||
BACKEND_SRC := $(shell find backend/src) $(shell find backend/migrations) $(shell find patch-db/*/src) $(shell find libs/*/src) libs/*/Cargo.toml backend/Cargo.toml backend/Cargo.lock
|
||||
FRONTEND_SHARED_SRC := $(shell find frontend/projects/shared) $(shell ls -p frontend/ | grep -v / | sed 's/^/frontend\//g') frontend/package.json frontend/node_modules frontend/config.json patch-db/client/dist frontend/patchdb-ui-seed.json
|
||||
FRONTEND_UI_SRC := $(shell find frontend/projects/ui)
|
||||
FRONTEND_SETUP_WIZARD_SRC := $(shell find frontend/projects/setup-wizard)
|
||||
FRONTEND_DIAGNOSTIC_UI_SRC := $(shell find frontend/projects/diagnostic-ui)
|
||||
FRONTEND_INSTALL_WIZARD_SRC := $(shell find frontend/projects/install-wizard)
|
||||
PATCH_DB_CLIENT_SRC := $(shell find patch-db/client -not -path patch-db/client/dist)
|
||||
$(shell sudo true)
|
||||
GZIP_BIN := $(shell which pigz || which gzip)
|
||||
ALL_TARGETS := $(EMBASSY_BINS) system-images/compat/docker-images/$(ARCH).tar system-images/utils/docker-images/$(ARCH).tar system-images/binfmt/docker-images/$(ARCH).tar $(EMBASSY_SRC) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE) $(VERSION_FILE)
|
||||
|
||||
ifeq ($(REMOTE),)
|
||||
mkdir = mkdir -p $1
|
||||
rm = rm -rf $1
|
||||
cp = cp -r $1 $2
|
||||
else
|
||||
mkdir = ssh $(REMOTE) 'mkdir -p $1'
|
||||
rm = ssh $(REMOTE) 'sudo rm -rf $1'
|
||||
define cp
|
||||
tar --transform "s|^$1|x|" -czv -f- $1 | ssh $(REMOTE) "sudo tar --transform 's|^x|$2|' -xzv -f- -C /"
|
||||
endef
|
||||
endif
|
||||
|
||||
.DELETE_ON_ERROR:
|
||||
|
||||
all: eos.img
|
||||
.PHONY: all gzip install clean format sdk snapshots frontends ui backend reflash eos_raspberrypi.img sudo
|
||||
|
||||
gzip: eos.img
|
||||
gzip -k eos.img
|
||||
all: $(ALL_TARGETS)
|
||||
|
||||
sudo:
|
||||
sudo true
|
||||
|
||||
clean:
|
||||
rm -f eos.img
|
||||
rm -f 2022-01-28-raspios-bullseye-arm64-lite.zip
|
||||
rm -f raspios.img
|
||||
rm -f eos_raspberrypi-uninit.img
|
||||
rm -f eos_raspberrypi-uninit.tar.gz
|
||||
rm -f ubuntu.img
|
||||
rm -f product_key.txt
|
||||
rm -f system-images/**/*.tar
|
||||
sudo rm -f $(EMBASSY_BINS)
|
||||
rm -rf system-images/compat/target
|
||||
rm -rf backend/target
|
||||
rm -rf frontend/.angular
|
||||
rm -f frontend/config.json
|
||||
rm -rf frontend/node_modules
|
||||
rm -rf frontend/dist
|
||||
rm -rf libs/target
|
||||
rm -rf patch-db/client/node_modules
|
||||
rm -rf patch-db/client/dist
|
||||
sudo rm -rf cargo-deps
|
||||
rm -rf patch-db/target
|
||||
rm -rf cargo-deps
|
||||
rm ENVIRONMENT.txt
|
||||
rm GIT_HASH.txt
|
||||
rm VERSION.txt
|
||||
|
||||
sdk:
|
||||
format:
|
||||
cd backend && cargo +nightly fmt
|
||||
cd libs && cargo +nightly fmt
|
||||
|
||||
sdk:
|
||||
cd backend/ && ./install-sdk.sh
|
||||
|
||||
eos.img: $(EMBASSY_SRC) system-images/compat/compat.tar system-images/utils/utils.tar cargo-deps/aarch64-unknown-linux-gnu/release/nc-broadcast $(ENVIRONMENT_FILE) $(GIT_HASH_FILE)
|
||||
! test -f eos.img || rm eos.img
|
||||
if [ "$(NO_KEY)" = "1" ]; then NO_KEY=1 ./build/make-image.sh; else ./build/make-image.sh; fi
|
||||
eos_raspberrypi-uninit.img: $(ALL_TARGETS) raspios.img cargo-deps/aarch64-unknown-linux-gnu/release/nc-broadcast cargo-deps/aarch64-unknown-linux-gnu/release/pi-beep | sudo
|
||||
! test -f eos_raspberrypi-uninit.img || rm eos_raspberrypi-uninit.img
|
||||
./build/raspberry-pi/make-image.sh
|
||||
|
||||
system-images/compat/compat.tar: $(COMPAT_SRC)
|
||||
cd system-images/compat && ./build.sh
|
||||
cd system-images/compat && DOCKER_CLI_EXPERIMENTAL=enabled docker buildx build --tag start9/x_system/compat --platform=linux/arm64 -o type=docker,dest=compat.tar .
|
||||
lite-upgrade.img: raspios.img cargo-deps/aarch64-unknown-linux-gnu/release/nc-broadcast cargo-deps/aarch64-unknown-linux-gnu/release/pi-beep $(BUILD_SRC) eos.raspberrypi.squashfs
|
||||
! test -f lite-upgrade.img || rm lite-upgrade.img
|
||||
./build/raspberry-pi/make-upgrade-image.sh
|
||||
|
||||
system-images/utils/utils.tar: $(UTILS_SRC)
|
||||
cd system-images/utils && DOCKER_CLI_EXPERIMENTAL=enabled docker buildx build --tag start9/x_system/utils --platform=linux/arm64 -o type=docker,dest=utils.tar .
|
||||
eos_raspberrypi.img: raspios.img $(BUILD_SRC) eos.raspberrypi.squashfs $(VERSION_FILE) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE) | sudo
|
||||
! test -f eos_raspberrypi.img || rm eos_raspberrypi.img
|
||||
./build/raspberry-pi/make-initialized-image.sh
|
||||
|
||||
# For creating os images. DO NOT USE
|
||||
install: $(ALL_TARGETS)
|
||||
$(call mkdir,$(DESTDIR)/usr/bin)
|
||||
$(call cp,backend/target/$(ARCH)-unknown-linux-gnu/release/embassy-init,$(DESTDIR)/usr/bin/embassy-init)
|
||||
$(call cp,backend/target/$(ARCH)-unknown-linux-gnu/release/embassyd,$(DESTDIR)/usr/bin/embassyd)
|
||||
$(call cp,backend/target/$(ARCH)-unknown-linux-gnu/release/embassy-cli,$(DESTDIR)/usr/bin/embassy-cli)
|
||||
$(call cp,backend/target/$(ARCH)-unknown-linux-gnu/release/avahi-alias,$(DESTDIR)/usr/bin/avahi-alias)
|
||||
|
||||
$(call mkdir,$(DESTDIR)/usr/lib)
|
||||
$(call rm,$(DESTDIR)/usr/lib/embassy)
|
||||
$(call cp,build/lib,$(DESTDIR)/usr/lib/embassy)
|
||||
|
||||
$(call cp,ENVIRONMENT.txt,$(DESTDIR)/usr/lib/embassy/ENVIRONMENT.txt)
|
||||
$(call cp,GIT_HASH.txt,$(DESTDIR)/usr/lib/embassy/GIT_HASH.txt)
|
||||
$(call cp,VERSION.txt,$(DESTDIR)/usr/lib/embassy/VERSION.txt)
|
||||
|
||||
$(call mkdir,$(DESTDIR)/usr/lib/embassy/container)
|
||||
$(call cp,libs/target/aarch64-unknown-linux-musl/release/embassy_container_init,$(DESTDIR)/usr/lib/embassy/container/embassy_container_init.arm64)
|
||||
$(call cp,libs/target/x86_64-unknown-linux-musl/release/embassy_container_init,$(DESTDIR)/usr/lib/embassy/container/embassy_container_init.amd64)
|
||||
|
||||
$(call mkdir,$(DESTDIR)/usr/lib/embassy/system-images)
|
||||
$(call cp,system-images/compat/docker-images/$(ARCH).tar,$(DESTDIR)/usr/lib/embassy/system-images/compat.tar)
|
||||
$(call cp,system-images/utils/docker-images/$(ARCH).tar,$(DESTDIR)/usr/lib/embassy/system-images/utils.tar)
|
||||
$(call cp,system-images/binfmt/docker-images/$(ARCH).tar,$(DESTDIR)/usr/lib/embassy/system-images/binfmt.tar)
|
||||
|
||||
$(call mkdir,$(DESTDIR)/var/www/html)
|
||||
$(call cp,frontend/dist/diagnostic-ui,$(DESTDIR)/var/www/html/diagnostic)
|
||||
$(call cp,frontend/dist/setup-wizard,$(DESTDIR)/var/www/html/setup)
|
||||
$(call cp,frontend/dist/install-wizard,$(DESTDIR)/var/www/html/install)
|
||||
$(call cp,frontend/dist/ui,$(DESTDIR)/var/www/html/main)
|
||||
$(call cp,index.html,$(DESTDIR)/var/www/html/index.html)
|
||||
|
||||
update-overlay:
|
||||
@echo "\033[33m!!! THIS WILL ONLY REFLASH YOUR DEVICE IN MEMORY !!!\033[0m"
|
||||
@echo "\033[33mALL CHANGES WILL BE REVERTED IF YOU RESTART THE DEVICE\033[0m"
|
||||
@if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi
|
||||
@if [ "`ssh $(REMOTE) 'cat /usr/lib/embassy/VERSION.txt'`" != "`cat ./VERSION.txt`" ]; then >&2 echo "Embassy requires migrations: update-overlay is unavailable." && false; fi
|
||||
@if ssh $(REMOTE) "pidof embassy-init"; then >&2 echo "Embassy in INIT: update-overlay is unavailable." && false; fi
|
||||
ssh $(REMOTE) "sudo systemctl stop embassyd"
|
||||
$(MAKE) install REMOTE=$(REMOTE) OS_ARCH=$(OS_ARCH)
|
||||
ssh $(REMOTE) "sudo systemctl start embassyd"
|
||||
|
||||
update:
|
||||
@if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi
|
||||
ssh $(REMOTE) "sudo rsync -a --delete --force --info=progress2 /media/embassy/embassyfs/current/ /media/embassy/next/"
|
||||
$(MAKE) install REMOTE=$(REMOTE) DESTDIR=/media/embassy/next OS_ARCH=$(OS_ARCH)
|
||||
ssh $(REMOTE) "sudo touch /media/embassy/config/upgrade && sudo sync && sudo reboot"
|
||||
|
||||
emulate-reflash:
|
||||
@if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi
|
||||
ssh $(REMOTE) "sudo rsync -a --delete --force --info=progress2 /media/embassy/embassyfs/current/ /media/embassy/next/"
|
||||
$(MAKE) install REMOTE=$(REMOTE) DESTDIR=/media/embassy/next OS_ARCH=$(OS_ARCH)
|
||||
ssh $(REMOTE) "sudo touch /media/embassy/config/upgrade && sudo rm -f /media/embassy/config/disk.guid && sudo sync && sudo reboot"
|
||||
|
||||
system-images/compat/docker-images/aarch64.tar system-images/compat/docker-images/x86_64.tar: $(COMPAT_SRC)
|
||||
cd system-images/compat && make
|
||||
|
||||
system-images/utils/docker-images/aarch64.tar system-images/utils/docker-images/x86_64.tar: $(UTILS_SRC)
|
||||
cd system-images/utils && make
|
||||
|
||||
system-images/binfmt/docker-images/aarch64.tar system-images/binfmt/docker-images/x86_64.tar: $(BINFMT_SRC)
|
||||
cd system-images/binfmt && make
|
||||
|
||||
raspios.img:
|
||||
wget --continue https://downloads.raspberrypi.org/raspios_lite_arm64/images/raspios_lite_arm64-2022-01-28/2022-01-28-raspios-bullseye-arm64-lite.zip
|
||||
unzip 2022-01-28-raspios-bullseye-arm64-lite.zip
|
||||
mv 2022-01-28-raspios-bullseye-arm64-lite.img raspios.img
|
||||
|
||||
product_key.txt:
|
||||
$(shell which echo) -n "X" > product_key.txt
|
||||
cat /dev/urandom | base32 | head -c11 | tr '[:upper:]' '[:lower:]' >> product_key.txt
|
||||
if [ "$(KEY)" != "" ]; then $(shell which echo) -n "$(KEY)" > product_key.txt; fi
|
||||
echo >> product_key.txt
|
||||
|
||||
snapshots: libs/snapshot-creator/Cargo.toml
|
||||
snapshots: libs/snapshot_creator/Cargo.toml
|
||||
cd libs/ && ./build-v8-snapshot.sh
|
||||
cd libs/ && ./build-arm-v8-snapshot.sh
|
||||
|
||||
$(EMBASSY_BINS): $(BACKEND_SRC) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE)
|
||||
cd backend && ./build-prod.sh
|
||||
$(EMBASSY_BINS): $(BACKEND_SRC) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE) frontend/patchdb-ui-seed.json
|
||||
cd backend && ARCH=$(ARCH) ./build-prod.sh
|
||||
touch $(EMBASSY_BINS)
|
||||
|
||||
frontend/node_modules: frontend/package.json
|
||||
@@ -73,22 +167,35 @@ frontend/dist/ui: $(FRONTEND_UI_SRC) $(FRONTEND_SHARED_SRC) $(ENVIRONMENT_FILE)
|
||||
npm --prefix frontend run build:ui
|
||||
|
||||
frontend/dist/setup-wizard: $(FRONTEND_SETUP_WIZARD_SRC) $(FRONTEND_SHARED_SRC) $(ENVIRONMENT_FILE)
|
||||
npm --prefix frontend run build:setup-wizard
|
||||
npm --prefix frontend run build:setup
|
||||
|
||||
frontend/dist/diagnostic-ui: $(FRONTEND_DIAGNOSTIC_UI_SRC) $(FRONTEND_SHARED_SRC) $(ENVIRONMENT_FILE)
|
||||
npm --prefix frontend run build:diagnostic-ui
|
||||
npm --prefix frontend run build:dui
|
||||
|
||||
frontend/dist/install-wizard: $(FRONTEND_INSTALL_WIZARD_SRC) $(FRONTEND_SHARED_SRC) $(ENVIRONMENT_FILE)
|
||||
npm --prefix frontend run build:install-wiz
|
||||
|
||||
frontend/config.json: $(GIT_HASH_FILE) frontend/config-sample.json
|
||||
jq '.useMocks = false' frontend/config-sample.json > frontend/config.json
|
||||
jq '.packageArch = "$(ARCH)"' frontend/config.json > frontend/config.json.tmp
|
||||
jq '.osArch = "$(OS_ARCH)"' frontend/config.json.tmp > frontend/config.json
|
||||
rm frontend/config.json.tmp
|
||||
npm --prefix frontend run-script build-config
|
||||
|
||||
frontend/patchdb-ui-seed.json: frontend/package.json
|
||||
jq '."ack-welcome" = "$(shell yq '.version' frontend/package.json)"' frontend/patchdb-ui-seed.json > ui-seed.tmp
|
||||
mv ui-seed.tmp frontend/patchdb-ui-seed.json
|
||||
|
||||
patch-db/client/node_modules: patch-db/client/package.json
|
||||
npm --prefix patch-db/client ci
|
||||
|
||||
patch-db/client/dist: $(PATCH_DB_CLIENT_SRC) patch-db/client/node_modules
|
||||
! test -d patch-db/client/dist || rm -rf patch-db/client/dist
|
||||
rm -rf frontend/.angular/cache
|
||||
npm --prefix patch-db/client run build
|
||||
npm --prefix frontend run build:deps
|
||||
|
||||
# used by github actions
|
||||
backend-$(ARCH).tar: $(EMBASSY_BINS)
|
||||
tar -cvf $@ $^
|
||||
|
||||
# this is a convenience step to build all frontends - it is not referenced elsewhere in this file
|
||||
frontends: $(EMBASSY_UIS)
|
||||
@@ -96,8 +203,11 @@ frontends: $(EMBASSY_UIS)
|
||||
# this is a convenience step to build the UI
|
||||
ui: frontend/dist/ui
|
||||
|
||||
# this is a convenience step to build the backend
|
||||
# used by github actions
|
||||
backend: $(EMBASSY_BINS)
|
||||
|
||||
cargo-deps/aarch64-unknown-linux-gnu/release/nc-broadcast:
|
||||
./build-cargo-dep.sh nc-broadcast
|
||||
./build-cargo-dep.sh nc-broadcast
|
||||
|
||||
cargo-deps/aarch64-unknown-linux-gnu/release/pi-beep:
|
||||
./build-cargo-dep.sh pi-beep
|
||||
34
README.md
@@ -1,49 +1,51 @@
|
||||
# EmbassyOS
|
||||
[](https://github.com/Start9Labs/embassy-os/releases)
|
||||
# embassyOS
|
||||
[](https://github.com/Start9Labs/embassy-os/releases)
|
||||
[](https://github.com/Start9Labs/embassy-os/actions/workflows/product.yaml)
|
||||
[](https://matrix.to/#/#community:matrix.start9labs.com)
|
||||
[](https://t.me/start9_labs)
|
||||
[](https://docs.start9labs.com)
|
||||
[](https://docs.start9.com)
|
||||
[](https://matrix.to/#/#community-dev:matrix.start9labs.com)
|
||||
[](https://start9labs.com)
|
||||
[](https://start9.com)
|
||||
|
||||
[](http://mastodon.start9labs.com)
|
||||
[](https://twitter.com/start9labs)
|
||||
|
||||
### _Welcome to the era of Sovereign Computing_ ###
|
||||
|
||||
EmbassyOS is a browser-based, graphical operating system for a personal server. EmbassyOS facilitates the discovery, installation, network configuration, service configuration, data backup, dependency management, and health monitoring of self-hosted software services. It is the most advanced, secure, reliable, and user friendly personal server OS in the world.
|
||||
embassyOS is a browser-based, graphical operating system for a personal server. embassyOS facilitates the discovery, installation, network configuration, service configuration, data backup, dependency management, and health monitoring of self-hosted software services. It is the most advanced, secure, reliable, and user friendly personal server OS in the world.
|
||||
|
||||
## Running EmbassyOS
|
||||
There are multiple ways to get your hands on EmbassyOS.
|
||||
## Running embassyOS
|
||||
There are multiple ways to get your hands on embassyOS.
|
||||
|
||||
### :moneybag: Buy an Embassy
|
||||
This is the most convenient option. Simply [buy an Embassy](https://start9.com) from Start9 and plug it in. Depending on where you live, shipping costs and import duties will vary.
|
||||
|
||||
### :construction_worker: Build your own Embassy
|
||||
While not as convenient as buying an Embassy, this option is easier than you might imagine, and there are 4 reasons why you might prefer it:
|
||||
1. You already have a Raspberry Pi and would like to re-purpose it.
|
||||
1. You already have your own hardware.
|
||||
1. You want to save on shipping costs.
|
||||
1. You prefer not to divulge your physical address.
|
||||
1. You just like building things.
|
||||
|
||||
To pursue this option, follow this [guide](https://start9.com/latest/diy).
|
||||
To pursue this option, follow one of our [DIY guides](https://start9.com/latest/diy).
|
||||
|
||||
### :hammer_and_wrench: Build EmbassyOS from Source
|
||||
### :hammer_and_wrench: Build embassyOS from Source
|
||||
|
||||
EmbassyOS can be built from source, for personal use, for free.
|
||||
embassyOS can be built from source, for personal use, for free.
|
||||
A detailed guide for doing so can be found [here](https://github.com/Start9Labs/embassy-os/blob/master/build/README.md).
|
||||
|
||||
## :heart: Contributing
|
||||
There are multiple ways to contribute: work directly on EmbassyOS, package a service for the marketplace, or help with documentation and guides. To learn more about contributing, see [here](https://github.com/Start9Labs/embassy-os/blob/master/CONTRIBUTING.md).
|
||||
There are multiple ways to contribute: work directly on embassyOS, package a service for the marketplace, or help with documentation and guides. To learn more about contributing, see [here](https://docs.start9.com/latest/contribute/) or [here](https://github.com/Start9Labs/embassy-os/blob/master/CONTRIBUTING.md).
|
||||
|
||||
## UI Screenshots
|
||||
<p align="center">
|
||||
<img src="assets/EmbassyOS.png" alt="EmbassyOS" width="65%">
|
||||
<img src="assets/embassyOS.png" alt="embassyOS" width="85%">
|
||||
</p>
|
||||
<p align="center">
|
||||
<img src="assets/eos-services.png" alt="Embassy Services" width="45%">
|
||||
<img src="assets/eos-preferences.png" alt="Embassy Preferences" width="45%">
|
||||
<img src="assets/eOS-preferences.png" alt="Embassy Preferences" width="49%">
|
||||
<img src="assets/eOS-ghost.png" alt="Embassy Ghost Service" width="49%">
|
||||
</p>
|
||||
<p align="center">
|
||||
<img src="assets/eos-bitcoind-health-check.png" alt="Embassy Bitcoin Health Checks" width="45%"> <img src="assets/eos-logs.png" alt="Embassy Logs" width="45%">
|
||||
<img src="assets/eOS-synapse-health-check.png" alt="Embassy Synapse Health Checks" width="49%">
|
||||
<img src="assets/eOS-sideload.png" alt="Embassy Sideload Service" width="49%">
|
||||
</p>
|
||||
|
||||
|
Before Width: | Height: | Size: 285 KiB |
BIN
assets/eOS-ghost.png
Normal file
|
After Width: | Height: | Size: 281 KiB |
BIN
assets/eOS-preferences.png
Normal file
|
After Width: | Height: | Size: 266 KiB |
BIN
assets/eOS-sideload.png
Normal file
|
After Width: | Height: | Size: 154 KiB |
BIN
assets/eOS-synapse-health-check.png
Normal file
|
After Width: | Height: | Size: 213 KiB |
BIN
assets/embassyOS.png
Normal file
|
After Width: | Height: | Size: 191 KiB |
|
Before Width: | Height: | Size: 334 KiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 347 KiB |
|
Before Width: | Height: | Size: 599 KiB |
2531
backend/Cargo.lock
generated
@@ -2,7 +2,7 @@
|
||||
authors = ["Aiden McClelland <me@drbonez.dev>"]
|
||||
description = "The core of the Start9 Embassy Operating System"
|
||||
documentation = "https://docs.rs/embassy-os"
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
keywords = [
|
||||
"self-hosted",
|
||||
"raspberry-pi",
|
||||
@@ -14,7 +14,7 @@ keywords = [
|
||||
name = "embassy-os"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/Start9Labs/embassy-os"
|
||||
version = "0.3.1-rev.1"
|
||||
version = "0.3.4"
|
||||
|
||||
[lib]
|
||||
name = "embassy"
|
||||
@@ -36,16 +36,23 @@ path = "src/bin/embassy-sdk.rs"
|
||||
name = "embassy-cli"
|
||||
path = "src/bin/embassy-cli.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "avahi-alias"
|
||||
path = "src/bin/avahi-alias.rs"
|
||||
|
||||
[features]
|
||||
avahi = ["avahi-sys"]
|
||||
default = ["avahi", "sound", "metal", "js_engine"]
|
||||
default = ["avahi", "js_engine"]
|
||||
dev = []
|
||||
metal = []
|
||||
sound = []
|
||||
unstable = ["patch-db/unstable"]
|
||||
|
||||
[dependencies]
|
||||
aes = { version = "0.7.5", features = ["ctr"] }
|
||||
async-compression = { version = "0.3.15", features = [
|
||||
"gzip",
|
||||
"brotli",
|
||||
"tokio",
|
||||
] }
|
||||
async-stream = "0.3.3"
|
||||
async-trait = "0.1.56"
|
||||
avahi-sys = { git = "https://github.com/Start9Labs/avahi-sys", version = "0.10.0", branch = "feature/dynamic-linking", features = [
|
||||
@@ -56,37 +63,47 @@ base64 = "0.13.0"
|
||||
base64ct = "1.5.1"
|
||||
basic-cookies = "0.1.4"
|
||||
bollard = "0.13.0"
|
||||
bytes = "1"
|
||||
chrono = { version = "0.4.19", features = ["serde"] }
|
||||
clap = "3.2.8"
|
||||
color-eyre = "0.6.1"
|
||||
cookie_store = "0.16.1"
|
||||
cookie = "0.16.2"
|
||||
cookie_store = "0.19.0"
|
||||
current_platform = "0.2.0"
|
||||
digest = "0.10.3"
|
||||
digest-old = { package = "digest", version = "0.9.0" }
|
||||
divrem = "1.0.0"
|
||||
ed25519 = { version = "1.5.2", features = ["pkcs8", "pem", "alloc"] }
|
||||
ed25519-dalek = { version = "1.0.1", features = ["serde"] }
|
||||
emver = { version = "0.1.6", features = ["serde"] }
|
||||
emver = { version = "0.1.7", git = "https://github.com/Start9Labs/emver-rs.git", features = [
|
||||
"serde",
|
||||
] }
|
||||
fd-lock-rs = "0.1.4"
|
||||
futures = "0.3.21"
|
||||
git-version = "0.3.5"
|
||||
gpt = "3.0.0"
|
||||
helpers = { path = "../libs/helpers" }
|
||||
embassy_container_init = { path = "../libs/embassy_container_init" }
|
||||
hex = "0.4.3"
|
||||
hmac = "0.12.1"
|
||||
http = "0.2.8"
|
||||
hyper = "0.14.20"
|
||||
hyper = { version = "0.14.20", features = ["full"] }
|
||||
hyper-ws-listener = "0.2.0"
|
||||
imbl = "2.0.0"
|
||||
indexmap = { version = "1.9.1", features = ["serde"] }
|
||||
ipnet = { version = "2.7.1", features = ["serde"] }
|
||||
iprange = { version = "0.6.7", features = ["serde"] }
|
||||
isocountry = "0.3.2"
|
||||
itertools = "0.10.3"
|
||||
josekit = "0.8.1"
|
||||
js_engine = { path = '../libs/js_engine', optional = true }
|
||||
jsonpath_lib = "0.3.0"
|
||||
lazy_static = "1.4.0"
|
||||
libc = "0.2.126"
|
||||
log = "0.4.17"
|
||||
mbrman = "0.5.0"
|
||||
models = { version = "*", path = "../libs/models" }
|
||||
nix = "0.24.1"
|
||||
nix = "0.25.0"
|
||||
nom = "7.1.1"
|
||||
num = "0.4.0"
|
||||
num_enum = "0.5.7"
|
||||
@@ -95,27 +112,28 @@ openssl = { version = "0.10.41", features = ["vendored"] }
|
||||
patch-db = { version = "*", path = "../patch-db/patch-db", features = [
|
||||
"trace",
|
||||
] }
|
||||
p256 = { version = "0.12.0", features = ["pem"] }
|
||||
pbkdf2 = "0.11.0"
|
||||
pin-project = "1.0.11"
|
||||
pkcs8 = { version = "0.9.0", features = ["std"] }
|
||||
prettytable-rs = "0.8.0"
|
||||
prettytable-rs = "0.10.0"
|
||||
proptest = "1.0.0"
|
||||
proptest-derive = "0.3.0"
|
||||
rand = { version = "0.8.5", features = ["std"] }
|
||||
rand-old = { package = "rand", version = "0.7.3" }
|
||||
regex = "1.6.0"
|
||||
reqwest = { version = "0.11.11", features = ["stream", "json", "socks"] }
|
||||
reqwest_cookie_store = "0.3.0"
|
||||
rpassword = "6.0.1"
|
||||
rpc-toolkit = "0.2.0"
|
||||
reqwest_cookie_store = "0.5.0"
|
||||
rpassword = "7.0.0"
|
||||
rpc-toolkit = "0.2.2"
|
||||
rust-argon2 = "1.0.0"
|
||||
scopeguard = "1.1" # because avahi-sys fucks your shit up
|
||||
serde = { version = "1.0.139", features = ["derive", "rc"] }
|
||||
serde_cbor = { package = "ciborium", version = "0.2.0" }
|
||||
serde_json = "1.0.82"
|
||||
serde_toml = { package = "toml", version = "0.5.9" }
|
||||
serde_with = { version = "1.14.0", features = ["macros", "json"] }
|
||||
serde_yaml = "0.8.25"
|
||||
serde_with = { version = "2.0.1", features = ["macros", "json"] }
|
||||
serde_yaml = "0.9.11"
|
||||
sha2 = "0.10.2"
|
||||
sha2-old = { package = "sha2", version = "0.9.9" }
|
||||
simple-logging = "2.0.2"
|
||||
@@ -123,24 +141,34 @@ sqlx = { version = "0.6.0", features = [
|
||||
"chrono",
|
||||
"offline",
|
||||
"runtime-tokio-rustls",
|
||||
"sqlite",
|
||||
"postgres",
|
||||
] }
|
||||
ssh-key = { version = "0.5.1", features = ["ed25519"] }
|
||||
stderrlog = "0.5.3"
|
||||
tar = "0.4.38"
|
||||
thiserror = "1.0.31"
|
||||
tokio = { version = "1.19.2", features = ["full"] }
|
||||
tokio-stream = { version = "0.1.9", features = ["io-util", "sync"] }
|
||||
tokio = { version = "1.23", features = ["full"] }
|
||||
tokio-stream = { version = "0.1.11", features = ["io-util", "sync", "net"] }
|
||||
tokio-tar = { git = "https://github.com/dr-bonez/tokio-tar.git" }
|
||||
tokio-tungstenite = "0.17.1"
|
||||
tokio-tungstenite = { version = "0.17.1", features = ["native-tls"] }
|
||||
tokio-rustls = "0.23.4"
|
||||
tokio-util = { version = "0.7.3", features = ["io"] }
|
||||
torut = "0.2.1"
|
||||
tracing = "0.1.35"
|
||||
tracing-error = "0.2.0"
|
||||
tracing-futures = "0.2.5"
|
||||
tracing-subscriber = { version = "0.3.14", features = ["env-filter"] }
|
||||
trust-dns-server = "0.21.2"
|
||||
trust-dns-server = "0.22.0"
|
||||
typed-builder = "0.10.0"
|
||||
url = { version = "2.2.2", features = ["serde"] }
|
||||
uuid = { version = "1.1.2", features = ["v4"] }
|
||||
zeroize = "1.5.7"
|
||||
|
||||
[profile.test]
|
||||
opt-level = 3
|
||||
|
||||
[profile.dev.package.backtrace]
|
||||
opt-level = 3
|
||||
|
||||
[profile.dev.package.sqlx-macros]
|
||||
opt-level = 3
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# EmbassyOS Backend
|
||||
# embassyOS Backend
|
||||
|
||||
- Requirements:
|
||||
- [Install Rust](https://rustup.rs)
|
||||
@@ -6,18 +6,22 @@
|
||||
- [Docker](https://docs.docker.com/get-docker/)
|
||||
- [Rust ARM64 Build Container](https://github.com/Start9Labs/rust-arm-builder)
|
||||
- Scripts (run withing the `./backend` directory)
|
||||
- `build-prod.sh` - compiles a release build of the artifacts for running on ARM64
|
||||
- `build-dev.sh` - compiles a development build of the artifacts for running on ARM64
|
||||
- `build-prod.sh` - compiles a release build of the artifacts for running on
|
||||
ARM64
|
||||
- A Linux computer or VM
|
||||
|
||||
## Structure
|
||||
|
||||
The EmbassyOS backend is broken up into 4 different binaries:
|
||||
The embassyOS backend is broken up into 4 different binaries:
|
||||
|
||||
- embassyd: This is the main workhorse of EmbassyOS - any new functionality you want will likely go here
|
||||
- embassy-init: This is the component responsible for allowing you to set up your device, and handles system initialization on startup
|
||||
- embassy-cli: This is a CLI tool that will allow you to issue commands to embassyd and control it similarly to the UI
|
||||
- embassy-sdk: This is a CLI tool that aids in building and packaging services you wish to deploy to the Embassy
|
||||
- embassyd: This is the main workhorse of embassyOS - any new functionality you
|
||||
want will likely go here
|
||||
- embassy-init: This is the component responsible for allowing you to set up
|
||||
your device, and handles system initialization on startup
|
||||
- embassy-cli: This is a CLI tool that will allow you to issue commands to
|
||||
embassyd and control it similarly to the UI
|
||||
- embassy-sdk: This is a CLI tool that aids in building and packaging services
|
||||
you wish to deploy to the Embassy
|
||||
|
||||
Finally there is a library `embassy` that supports all four of these tools.
|
||||
|
||||
@@ -25,11 +29,14 @@ See [here](/backend/Cargo.toml) for details.
|
||||
|
||||
## Building
|
||||
|
||||
You can build the entire operating system image using `make` from the root of the EmbassyOS project. This will subsequently invoke the build scripts above to actually create the requisite binaries and put them onto the final operating system image.
|
||||
You can build the entire operating system image using `make` from the root of
|
||||
the embassyOS project. This will subsequently invoke the build scripts above to
|
||||
actually create the requisite binaries and put them onto the final operating
|
||||
system image.
|
||||
|
||||
## Questions
|
||||
|
||||
If you have questions about how various pieces of the backend system work. Open an issue and tag the following people
|
||||
If you have questions about how various pieces of the backend system work. Open
|
||||
an issue and tag the following people
|
||||
|
||||
- dr-bonez
|
||||
- ProofOfKeags
|
||||
|
||||
@@ -16,7 +16,7 @@ fi
|
||||
alias 'rust-arm64-builder'='docker run $USE_TTY --rm -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$(pwd)":/home/rust/src start9/rust-arm-cross:aarch64'
|
||||
|
||||
cd ..
|
||||
rust-arm64-builder sh -c "(cd backend && cargo build)"
|
||||
rust-arm64-builder sh -c "(cd backend && cargo build --locked)"
|
||||
cd backend
|
||||
|
||||
sudo chown -R $USER target
|
||||
|
||||
@@ -16,7 +16,7 @@ fi
|
||||
alias 'rust-musl-builder'='docker run $USE_TTY --rm -v "$HOME"/.cargo/registry:/root/.cargo/registry -v "$(pwd)":/home/rust/src start9/rust-musl-cross:x86_64-musl'
|
||||
|
||||
cd ..
|
||||
rust-musl-builder sh -c "(cd backend && cargo +beta build --target=x86_64-unknown-linux-musl --no-default-features)"
|
||||
rust-musl-builder sh -c "(cd backend && cargo +beta build --target=x86_64-unknown-linux-musl --no-default-features --locked)"
|
||||
cd backend
|
||||
|
||||
sudo chown -R $USER target
|
||||
|
||||
@@ -16,7 +16,7 @@ fi
|
||||
alias 'rust-musl-builder'='docker run $USE_TTY --rm -v "$HOME"/.cargo/registry:/root/.cargo/registry -v "$(pwd)":/home/rust/src start9/rust-musl-cross:x86_64-musl'
|
||||
|
||||
cd ..
|
||||
rust-musl-builder sh -c "(cd backend && cargo +beta build --release --target=x86_64-unknown-linux-musl --no-default-features)"
|
||||
rust-musl-builder sh -c "(cd backend && cargo +beta build --release --target=x86_64-unknown-linux-musl --no-default-features --locked)"
|
||||
cd backend
|
||||
|
||||
sudo chown -R $USER target
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
set -e
|
||||
shopt -s expand_aliases
|
||||
|
||||
if [ -z "$ARCH" ]; then
|
||||
ARCH=$(uname -m)
|
||||
fi
|
||||
|
||||
if [ "$0" != "./build-prod.sh" ]; then
|
||||
>&2 echo "Must be run from backend directory"
|
||||
exit 1
|
||||
@@ -13,7 +17,8 @@ if tty -s; then
|
||||
USE_TTY="-it"
|
||||
fi
|
||||
|
||||
alias 'rust-arm64-builder'='docker run $USE_TTY --rm -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$(pwd)":/home/rust/src -P start9/rust-arm-cross:aarch64'
|
||||
alias 'rust-gnu-builder'='docker run $USE_TTY --rm -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$(pwd)":/home/rust/src -P start9/rust-arm-cross:aarch64'
|
||||
alias 'rust-musl-builder'='docker run $USE_TTY --rm -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$(pwd)":/home/rust/src -P messense/rust-musl-cross:$ARCH-musl'
|
||||
|
||||
cd ..
|
||||
FLAGS=""
|
||||
@@ -23,15 +28,44 @@ fi
|
||||
if [[ "$ENVIRONMENT" =~ (^|-)dev($|-) ]]; then
|
||||
FLAGS="dev,$FLAGS"
|
||||
fi
|
||||
|
||||
set +e
|
||||
fail=
|
||||
if [[ "$FLAGS" = "" ]]; then
|
||||
rust-arm64-builder sh -c "(git config --global --add safe.directory '*'; cd backend && cargo build --release)"
|
||||
rust-gnu-builder sh -c "(git config --global --add safe.directory '*'; cd backend && cargo build --release --locked --target=$ARCH-unknown-linux-gnu)"
|
||||
if test $? -ne 0; then
|
||||
fail=true
|
||||
fi
|
||||
for ARCH in x86_64 aarch64
|
||||
do
|
||||
rust-musl-builder sh -c "(git config --global --add safe.directory '*'; cd libs && cargo build --release --locked --bin embassy_container_init )"
|
||||
if test $? -ne 0; then
|
||||
fail=true
|
||||
fi
|
||||
done
|
||||
else
|
||||
echo "FLAGS=$FLAGS"
|
||||
rust-arm64-builder sh -c "(git config --global --add safe.directory '*'; cd backend && cargo build --release --features $FLAGS)"
|
||||
rust-gnu-builder sh -c "(git config --global --add safe.directory '*'; cd backend && cargo build --release --features $FLAGS --locked --target=$ARCH-unknown-linux-gnu)"
|
||||
if test $? -ne 0; then
|
||||
fail=true
|
||||
fi
|
||||
for ARCH in x86_64 aarch64
|
||||
do
|
||||
rust-musl-builder sh -c "(git config --global --add safe.directory '*'; cd libs && cargo build --release --features $FLAGS --locked --bin embassy_container_init)"
|
||||
if test $? -ne 0; then
|
||||
fail=true
|
||||
fi
|
||||
done
|
||||
fi
|
||||
set -e
|
||||
cd backend
|
||||
|
||||
sudo chown -R $USER target
|
||||
sudo chown -R $USER ~/.cargo
|
||||
sudo chown -R $USER ../libs/target
|
||||
|
||||
if [ -n "$fail" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
#rust-arm64-builder aarch64-linux-gnu-strip target/aarch64-unknown-linux-gnu/release/embassyd
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
[Unit]
|
||||
Description=Embassy Init
|
||||
After=network.target
|
||||
Requires=network.target
|
||||
Wants=avahi-daemon.service nginx.service tor.service
|
||||
After=network-online.target
|
||||
Requires=network-online.target
|
||||
Wants=avahi-daemon.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
Environment=RUST_LOG=embassy_init=debug,embassy=debug,js_engine=debug
|
||||
ExecStart=/usr/local/bin/embassy-init
|
||||
Environment=RUST_LOG=embassy_init=debug,embassy=debug,js_engine=debug,patch_db=warn
|
||||
ExecStart=/usr/bin/embassy-init
|
||||
RemainAfterExit=true
|
||||
StandardOutput=file:/var/log/embassy-init.out.log
|
||||
StandardError=file:/var/log/embassy-init.error.log
|
||||
StandardOutput=append:/var/log/embassy-init.log
|
||||
|
||||
[Install]
|
||||
WantedBy=embassyd.service
|
||||
|
||||
@@ -5,8 +5,8 @@ Requires=embassy-init.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
Environment=RUST_LOG=embassyd=debug,embassy=debug,js_engine=debug
|
||||
ExecStart=/usr/local/bin/embassyd
|
||||
Environment=RUST_LOG=embassyd=debug,embassy=debug,js_engine=debug,patch_db=warn
|
||||
ExecStart=/usr/bin/embassyd
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
ManagedOOMPreference=avoid
|
||||
|
||||
@@ -8,4 +8,4 @@ if [ "$0" != "./install-sdk.sh" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cargo install --bin=embassy-sdk --bin=embassy-cli --path=. --no-default-features --features=js_engine
|
||||
cargo install --bin=embassy-sdk --bin=embassy-cli --path=. --no-default-features --features=js_engine --locked
|
||||
|
||||
@@ -1,45 +1,47 @@
|
||||
-- Add migration script here
|
||||
CREATE TABLE IF NOT EXISTS tor
|
||||
(
|
||||
package TEXT NOT NULL,
|
||||
interface TEXT NOT NULL,
|
||||
key BLOB NOT NULL CHECK (length(key) = 64),
|
||||
CREATE TABLE IF NOT EXISTS tor (
|
||||
package TEXT NOT NULL,
|
||||
interface TEXT NOT NULL,
|
||||
key BYTEA NOT NULL CHECK (length(key) = 64),
|
||||
PRIMARY KEY (package, interface)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS session
|
||||
(
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
|
||||
CREATE TABLE IF NOT EXISTS session (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
logged_in TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
logged_out TIMESTAMP,
|
||||
last_active TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
user_agent TEXT,
|
||||
metadata TEXT NOT NULL DEFAULT 'null'
|
||||
metadata TEXT NOT NULL DEFAULT 'null'
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS account
|
||||
(
|
||||
id INTEGER PRIMARY KEY CHECK (id = 0),
|
||||
|
||||
CREATE TABLE IF NOT EXISTS account (
|
||||
id SERIAL PRIMARY KEY CHECK (id = 0),
|
||||
password TEXT NOT NULL,
|
||||
tor_key BLOB NOT NULL CHECK (length(tor_key) = 64)
|
||||
tor_key BYTEA NOT NULL CHECK (length(tor_key) = 64)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS ssh_keys
|
||||
(
|
||||
fingerprint TEXT NOT NULL,
|
||||
openssh_pubkey TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ssh_keys (
|
||||
fingerprint TEXT NOT NULL,
|
||||
openssh_pubkey TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
PRIMARY KEY (fingerprint)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS certificates
|
||||
(
|
||||
id INTEGER PRIMARY KEY, -- Root = 0, Int = 1, Other = 2..
|
||||
|
||||
CREATE TABLE IF NOT EXISTS certificates (
|
||||
id SERIAL PRIMARY KEY,
|
||||
-- Root = 0, Int = 1, Other = 2..
|
||||
priv_key_pem TEXT NOT NULL,
|
||||
certificate_pem TEXT NOT NULL,
|
||||
lookup_string TEXT UNIQUE,
|
||||
created_at TEXT,
|
||||
updated_at TEXT
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS notifications
|
||||
(
|
||||
id INTEGER PRIMARY KEY,
|
||||
|
||||
ALTER SEQUENCE certificates_id_seq START 2 RESTART 2;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS notifications (
|
||||
id SERIAL PRIMARY KEY,
|
||||
package_id TEXT,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
code INTEGER NOT NULL,
|
||||
@@ -48,9 +50,9 @@ CREATE TABLE IF NOT EXISTS notifications
|
||||
message TEXT NOT NULL,
|
||||
data TEXT
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS cifs_shares
|
||||
(
|
||||
id INTEGER PRIMARY KEY,
|
||||
|
||||
CREATE TABLE IF NOT EXISTS cifs_shares (
|
||||
id SERIAL PRIMARY KEY,
|
||||
hostname TEXT NOT NULL,
|
||||
path TEXT NOT NULL,
|
||||
username TEXT NOT NULL,
|
||||
|
||||
62
backend/migrations/20230118185232_NetworkKeys.sql
Normal file
@@ -0,0 +1,62 @@
|
||||
-- Add migration script here
|
||||
CREATE EXTENSION pgcrypto;
|
||||
|
||||
ALTER TABLE
|
||||
account
|
||||
ADD
|
||||
COLUMN server_id TEXT,
|
||||
ADD
|
||||
COLUMN hostname TEXT,
|
||||
ADD
|
||||
COLUMN network_key BYTEA CHECK (length(network_key) = 32),
|
||||
ADD
|
||||
COLUMN root_ca_key_pem TEXT,
|
||||
ADD
|
||||
COLUMN root_ca_cert_pem TEXT;
|
||||
|
||||
UPDATE
|
||||
account
|
||||
SET
|
||||
network_key = gen_random_bytes(32),
|
||||
root_ca_key_pem = (
|
||||
SELECT
|
||||
priv_key_pem
|
||||
FROM
|
||||
certificates
|
||||
WHERE
|
||||
id = 0
|
||||
),
|
||||
root_ca_cert_pem = (
|
||||
SELECT
|
||||
certificate_pem
|
||||
FROM
|
||||
certificates
|
||||
WHERE
|
||||
id = 0
|
||||
)
|
||||
WHERE
|
||||
id = 0;
|
||||
|
||||
ALTER TABLE
|
||||
account
|
||||
ALTER COLUMN
|
||||
tor_key DROP NOT NULL,
|
||||
ALTER COLUMN
|
||||
network_key
|
||||
SET
|
||||
NOT NULL,
|
||||
ALTER COLUMN
|
||||
root_ca_key_pem
|
||||
SET
|
||||
NOT NULL,
|
||||
ALTER COLUMN
|
||||
root_ca_cert_pem
|
||||
SET
|
||||
NOT NULL;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS network_keys (
|
||||
package TEXT NOT NULL,
|
||||
interface TEXT NOT NULL,
|
||||
key BYTEA NOT NULL CHECK (length(key) = 32),
|
||||
PRIMARY KEY (package, interface)
|
||||
);
|
||||
120
backend/src/account.rs
Normal file
@@ -0,0 +1,120 @@
|
||||
use ed25519_dalek::{ExpandedSecretKey, SecretKey};
|
||||
use models::ResultExt;
|
||||
use openssl::pkey::{PKey, Private};
|
||||
use openssl::x509::X509;
|
||||
use sqlx::PgExecutor;
|
||||
|
||||
use crate::hostname::{generate_hostname, generate_id, Hostname};
|
||||
use crate::net::keys::Key;
|
||||
use crate::net::ssl::{generate_key, make_root_cert};
|
||||
use crate::Error;
|
||||
|
||||
fn hash_password(password: &str) -> Result<String, Error> {
|
||||
argon2::hash_encoded(
|
||||
password.as_bytes(),
|
||||
&rand::random::<[u8; 16]>()[..],
|
||||
&argon2::Config::default(),
|
||||
)
|
||||
.with_kind(crate::ErrorKind::PasswordHashGeneration)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AccountInfo {
|
||||
pub server_id: String,
|
||||
pub hostname: Hostname,
|
||||
pub password: String,
|
||||
pub key: Key,
|
||||
pub root_ca_key: PKey<Private>,
|
||||
pub root_ca_cert: X509,
|
||||
}
|
||||
impl AccountInfo {
|
||||
pub fn new(password: &str) -> Result<Self, Error> {
|
||||
let server_id = generate_id();
|
||||
let hostname = generate_hostname();
|
||||
let root_ca_key = generate_key()?;
|
||||
let root_ca_cert = make_root_cert(&root_ca_key, &hostname)?;
|
||||
Ok(Self {
|
||||
server_id,
|
||||
hostname,
|
||||
password: hash_password(password)?,
|
||||
key: Key::new(None),
|
||||
root_ca_key,
|
||||
root_ca_cert,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn load(secrets: impl PgExecutor<'_>) -> Result<Self, Error> {
|
||||
let r = sqlx::query!("SELECT * FROM account WHERE id = 0")
|
||||
.fetch_one(secrets)
|
||||
.await?;
|
||||
|
||||
let server_id = r.server_id.unwrap_or_else(generate_id);
|
||||
let hostname = r.hostname.map(Hostname).unwrap_or_else(generate_hostname);
|
||||
let password = r.password;
|
||||
let network_key = SecretKey::from_bytes(&r.network_key)?;
|
||||
let tor_key = if let Some(k) = &r.tor_key {
|
||||
ExpandedSecretKey::from_bytes(k)?
|
||||
} else {
|
||||
ExpandedSecretKey::from(&network_key)
|
||||
};
|
||||
let key = Key::from_pair(None, network_key.to_bytes(), tor_key.to_bytes());
|
||||
let root_ca_key = PKey::private_key_from_pem(r.root_ca_key_pem.as_bytes())?;
|
||||
let root_ca_cert = X509::from_pem(r.root_ca_cert_pem.as_bytes())?;
|
||||
|
||||
Ok(Self {
|
||||
server_id,
|
||||
hostname,
|
||||
password,
|
||||
key,
|
||||
root_ca_key,
|
||||
root_ca_cert,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn save(&self, secrets: impl PgExecutor<'_>) -> Result<(), Error> {
|
||||
let server_id = self.server_id.as_str();
|
||||
let hostname = self.hostname.0.as_str();
|
||||
let password = self.password.as_str();
|
||||
let network_key = self.key.as_bytes();
|
||||
let network_key = network_key.as_slice();
|
||||
let root_ca_key = String::from_utf8(self.root_ca_key.private_key_to_pem_pkcs8()?)?;
|
||||
let root_ca_cert = String::from_utf8(self.root_ca_cert.to_pem()?)?;
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO account (
|
||||
id,
|
||||
server_id,
|
||||
hostname,
|
||||
password,
|
||||
network_key,
|
||||
root_ca_key_pem,
|
||||
root_ca_cert_pem
|
||||
) VALUES (
|
||||
0, $1, $2, $3, $4, $5, $6
|
||||
) ON CONFLICT (id) DO UPDATE SET
|
||||
server_id = EXCLUDED.server_id,
|
||||
hostname = EXCLUDED.hostname,
|
||||
password = EXCLUDED.password,
|
||||
network_key = EXCLUDED.network_key,
|
||||
root_ca_key_pem = EXCLUDED.root_ca_key_pem,
|
||||
root_ca_cert_pem = EXCLUDED.root_ca_cert_pem
|
||||
"#,
|
||||
server_id,
|
||||
hostname,
|
||||
password,
|
||||
network_key,
|
||||
root_ca_key,
|
||||
root_ca_cert,
|
||||
)
|
||||
.execute(secrets)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_password(&mut self, password: &str) -> Result<(), Error> {
|
||||
self.password = hash_password(password)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -3,21 +3,21 @@ use std::collections::{BTreeMap, BTreeSet};
|
||||
use clap::ArgMatches;
|
||||
use color_eyre::eyre::eyre;
|
||||
use indexmap::IndexSet;
|
||||
pub use models::ActionId;
|
||||
use models::ImageId;
|
||||
use rpc_toolkit::command;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::config::{Config, ConfigSpec};
|
||||
use crate::context::RpcContext;
|
||||
use crate::id::ImageId;
|
||||
use crate::procedure::docker::DockerContainers;
|
||||
use crate::procedure::{PackageProcedure, ProcedureName};
|
||||
use crate::s9pk::manifest::PackageId;
|
||||
use crate::util::serde::{display_serializable, parse_stdin_deserializable, IoFormat};
|
||||
use crate::util::Version;
|
||||
use crate::volume::Volumes;
|
||||
use crate::{Error, ResultExt};
|
||||
|
||||
pub use models::ActionId;
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
pub struct Actions(pub BTreeMap<ActionId, Action>);
|
||||
|
||||
@@ -56,15 +56,16 @@ pub struct Action {
|
||||
pub input_spec: ConfigSpec,
|
||||
}
|
||||
impl Action {
|
||||
#[instrument]
|
||||
#[instrument(skip_all)]
|
||||
pub fn validate(
|
||||
&self,
|
||||
container: &Option<DockerContainers>,
|
||||
eos_version: &Version,
|
||||
volumes: &Volumes,
|
||||
image_ids: &BTreeSet<ImageId>,
|
||||
) -> Result<(), Error> {
|
||||
self.implementation
|
||||
.validate(eos_version, volumes, image_ids, true)
|
||||
.validate(container, eos_version, volumes, image_ids, true)
|
||||
.with_ctx(|_| {
|
||||
(
|
||||
crate::ErrorKind::ValidateS9pk,
|
||||
@@ -73,7 +74,7 @@ impl Action {
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(ctx))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn execute(
|
||||
&self,
|
||||
ctx: &RpcContext,
|
||||
@@ -96,7 +97,6 @@ impl Action {
|
||||
ProcedureName::Action(action_id.clone()),
|
||||
volumes,
|
||||
input,
|
||||
true,
|
||||
None,
|
||||
)
|
||||
.await?
|
||||
@@ -120,7 +120,7 @@ fn display_action_result(action_result: ActionResult, matches: &ArgMatches) {
|
||||
}
|
||||
|
||||
#[command(about = "Executes an action", display(display_action_result))]
|
||||
#[instrument(skip(ctx))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn action(
|
||||
#[context] ctx: RpcContext,
|
||||
#[arg(rename = "id")] pkg_id: PackageId,
|
||||
@@ -139,9 +139,10 @@ pub async fn action(
|
||||
.await
|
||||
.with_kind(crate::ErrorKind::NotFound)?
|
||||
.manifest()
|
||||
.get(&mut db, true)
|
||||
.get(&mut db)
|
||||
.await?
|
||||
.to_owned();
|
||||
|
||||
if let Some(action) = manifest.actions.0.get(&action_id) {
|
||||
action
|
||||
.execute(
|
||||
|
||||
1296
backend/src/assets/adjectives.txt
Normal file
7776
backend/src/assets/nouns.txt
Normal file
@@ -4,30 +4,77 @@ use std::marker::PhantomData;
|
||||
use chrono::{DateTime, Utc};
|
||||
use clap::ArgMatches;
|
||||
use color_eyre::eyre::eyre;
|
||||
use josekit::jwk::Jwk;
|
||||
use patch_db::{DbHandle, LockReceipt};
|
||||
use rpc_toolkit::command;
|
||||
use rpc_toolkit::command_helpers::prelude::{RequestParts, ResponseParts};
|
||||
use rpc_toolkit::yajrc::RpcError;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use sqlx::{Executor, Sqlite};
|
||||
use sqlx::{Executor, Postgres};
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::context::{CliContext, RpcContext};
|
||||
use crate::middleware::auth::{AsLogoutSessionId, HasLoggedOutSessions, HashSessionToken};
|
||||
use crate::middleware::encrypt::EncryptedWire;
|
||||
use crate::util::display_none;
|
||||
use crate::util::serde::{display_serializable, IoFormat};
|
||||
use crate::{ensure_code, Error, ResultExt};
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum PasswordType {
|
||||
EncryptedWire(EncryptedWire),
|
||||
String(String),
|
||||
}
|
||||
impl PasswordType {
|
||||
pub fn decrypt(self, current_secret: impl AsRef<Jwk>) -> Result<String, Error> {
|
||||
match self {
|
||||
PasswordType::String(x) => Ok(x),
|
||||
PasswordType::EncryptedWire(x) => x.decrypt(current_secret).ok_or_else(|| {
|
||||
Error::new(
|
||||
color_eyre::eyre::eyre!("Couldn't decode password"),
|
||||
crate::ErrorKind::Unknown,
|
||||
)
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Default for PasswordType {
|
||||
fn default() -> Self {
|
||||
PasswordType::String(String::default())
|
||||
}
|
||||
}
|
||||
impl std::fmt::Debug for PasswordType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "<REDACTED_PASSWORD>")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[command(subcommands(login, logout, session, reset_password))]
|
||||
impl std::str::FromStr for PasswordType {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(match serde_json::from_str(s) {
|
||||
Ok(a) => a,
|
||||
Err(_) => PasswordType::String(s.to_string()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[command(subcommands(login, logout, session, reset_password, get_pubkey))]
|
||||
pub fn auth() -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn parse_metadata(_: &str, _: &ArgMatches) -> Result<Value, Error> {
|
||||
Ok(serde_json::json!({
|
||||
pub fn cli_metadata() -> Value {
|
||||
serde_json::json!({
|
||||
"platforms": ["cli"],
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn parse_metadata(_: &str, _: &ArgMatches) -> Result<Value, Error> {
|
||||
Ok(cli_metadata())
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -43,14 +90,14 @@ fn gen_pwd() {
|
||||
)
|
||||
}
|
||||
|
||||
#[instrument(skip(ctx, password))]
|
||||
#[instrument(skip_all)]
|
||||
async fn cli_login(
|
||||
ctx: CliContext,
|
||||
password: Option<String>,
|
||||
password: Option<PasswordType>,
|
||||
metadata: Value,
|
||||
) -> Result<(), RpcError> {
|
||||
let password = if let Some(password) = password {
|
||||
password
|
||||
password.decrypt(&ctx)?
|
||||
} else {
|
||||
rpassword::prompt_password("Password: ")?
|
||||
};
|
||||
@@ -83,7 +130,7 @@ pub fn check_password(hash: &str, password: &str) -> Result<(), Error> {
|
||||
|
||||
pub async fn check_password_against_db<Ex>(secrets: &mut Ex, password: &str) -> Result<(), Error>
|
||||
where
|
||||
for<'a> &'a mut Ex: Executor<'a, Database = Sqlite>,
|
||||
for<'a> &'a mut Ex: Executor<'a, Database = Postgres>,
|
||||
{
|
||||
let pw_hash = sqlx::query!("SELECT password FROM account")
|
||||
.fetch_one(secrets)
|
||||
@@ -98,20 +145,20 @@ where
|
||||
display(display_none),
|
||||
metadata(authenticated = false)
|
||||
)]
|
||||
#[instrument(skip(ctx, password))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn login(
|
||||
#[context] ctx: RpcContext,
|
||||
#[request] req: &RequestParts,
|
||||
#[response] res: &mut ResponseParts,
|
||||
#[arg] password: Option<String>,
|
||||
#[arg] password: Option<PasswordType>,
|
||||
#[arg(
|
||||
parse(parse_metadata),
|
||||
default = "",
|
||||
default = "cli_metadata",
|
||||
help = "RPC Only: This value cannot be overidden from the cli"
|
||||
)]
|
||||
metadata: Value,
|
||||
) -> Result<(), Error> {
|
||||
let password = password.unwrap_or_default();
|
||||
let password = password.unwrap_or_default().decrypt(&ctx)?;
|
||||
let mut handle = ctx.secret_store.acquire().await?;
|
||||
check_password_against_db(&mut handle, &password).await?;
|
||||
|
||||
@@ -120,7 +167,7 @@ pub async fn login(
|
||||
let metadata = serde_json::to_string(&metadata).with_kind(crate::ErrorKind::Database)?;
|
||||
let hash_token_hashed = hash_token.hashed();
|
||||
sqlx::query!(
|
||||
"INSERT INTO session (id, user_agent, metadata) VALUES (?, ?, ?)",
|
||||
"INSERT INTO session (id, user_agent, metadata) VALUES ($1, $2, $3)",
|
||||
hash_token_hashed,
|
||||
user_agent,
|
||||
metadata,
|
||||
@@ -136,7 +183,7 @@ pub async fn login(
|
||||
}
|
||||
|
||||
#[command(display(display_none), metadata(authenticated = false))]
|
||||
#[instrument(skip(ctx))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn logout(
|
||||
#[context] ctx: RpcContext,
|
||||
#[request] req: &RequestParts,
|
||||
@@ -199,11 +246,11 @@ fn display_sessions(arg: SessionList, matches: &ArgMatches) {
|
||||
}
|
||||
table.add_row(row);
|
||||
}
|
||||
table.print_tty(false);
|
||||
table.print_tty(false).unwrap();
|
||||
}
|
||||
|
||||
#[command(display(display_sessions))]
|
||||
#[instrument(skip(ctx))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn list(
|
||||
#[context] ctx: RpcContext,
|
||||
#[request] req: &RequestParts,
|
||||
@@ -249,7 +296,7 @@ impl AsLogoutSessionId for KillSessionId {
|
||||
}
|
||||
|
||||
#[command(display(display_none))]
|
||||
#[instrument(skip(ctx))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn kill(
|
||||
#[context] ctx: RpcContext,
|
||||
#[arg(parse(parse_comma_separated))] ids: Vec<String>,
|
||||
@@ -258,20 +305,20 @@ pub async fn kill(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(ctx, old_password, new_password))]
|
||||
#[instrument(skip_all)]
|
||||
async fn cli_reset_password(
|
||||
ctx: CliContext,
|
||||
old_password: Option<String>,
|
||||
new_password: Option<String>,
|
||||
old_password: Option<PasswordType>,
|
||||
new_password: Option<PasswordType>,
|
||||
) -> Result<(), RpcError> {
|
||||
let old_password = if let Some(old_password) = old_password {
|
||||
old_password
|
||||
old_password.decrypt(&ctx)?
|
||||
} else {
|
||||
rpassword::prompt_password("Current Password: ")?
|
||||
};
|
||||
|
||||
let new_password = if let Some(new_password) = new_password {
|
||||
new_password
|
||||
new_password.decrypt(&ctx)?
|
||||
} else {
|
||||
let new_password = rpassword::prompt_password("New Password: ")?;
|
||||
if new_password != rpassword::prompt_password("Confirm: ")? {
|
||||
@@ -317,53 +364,48 @@ impl SetPasswordReceipt {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn set_password<Db: DbHandle, Ex>(
|
||||
db: &mut Db,
|
||||
receipt: &SetPasswordReceipt,
|
||||
secrets: &mut Ex,
|
||||
password: &str,
|
||||
) -> Result<(), Error>
|
||||
where
|
||||
for<'a> &'a mut Ex: Executor<'a, Database = Sqlite>,
|
||||
{
|
||||
let password = argon2::hash_encoded(
|
||||
password.as_bytes(),
|
||||
&rand::random::<[u8; 16]>()[..],
|
||||
&argon2::Config::default(),
|
||||
)
|
||||
.with_kind(crate::ErrorKind::PasswordHashGeneration)?;
|
||||
|
||||
sqlx::query!("UPDATE account SET password = ?", password,)
|
||||
.execute(secrets)
|
||||
.await?;
|
||||
|
||||
receipt.0.set(db, password).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command(
|
||||
rename = "reset-password",
|
||||
custom_cli(cli_reset_password(async, context(CliContext))),
|
||||
display(display_none)
|
||||
)]
|
||||
#[instrument(skip(ctx, old_password, new_password))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn reset_password(
|
||||
#[context] ctx: RpcContext,
|
||||
#[arg(rename = "old-password")] old_password: Option<String>,
|
||||
#[arg(rename = "new-password")] new_password: Option<String>,
|
||||
#[arg(rename = "old-password")] old_password: Option<PasswordType>,
|
||||
#[arg(rename = "new-password")] new_password: Option<PasswordType>,
|
||||
) -> Result<(), Error> {
|
||||
let old_password = old_password.unwrap_or_default();
|
||||
let new_password = new_password.unwrap_or_default();
|
||||
let old_password = old_password.unwrap_or_default().decrypt(&ctx)?;
|
||||
let new_password = new_password.unwrap_or_default().decrypt(&ctx)?;
|
||||
|
||||
let mut secrets = ctx.secret_store.acquire().await?;
|
||||
check_password_against_db(&mut secrets, &old_password).await?;
|
||||
|
||||
let mut db = ctx.db.handle();
|
||||
|
||||
let set_password_receipt = SetPasswordReceipt::new(&mut db).await?;
|
||||
|
||||
set_password(&mut db, &set_password_receipt, &mut secrets, &new_password).await?;
|
||||
let mut account = ctx.account.write().await;
|
||||
if !argon2::verify_encoded(&account.password, old_password.as_bytes())
|
||||
.with_kind(crate::ErrorKind::IncorrectPassword)?
|
||||
{
|
||||
return Err(Error::new(
|
||||
eyre!("Incorrect Password"),
|
||||
crate::ErrorKind::IncorrectPassword,
|
||||
));
|
||||
}
|
||||
account.set_password(&new_password)?;
|
||||
account.save(&ctx.secret_store).await?;
|
||||
crate::db::DatabaseModel::new()
|
||||
.server_info()
|
||||
.password_hash()
|
||||
.put(&mut ctx.db.handle(), &account.password)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command(
|
||||
rename = "get-pubkey",
|
||||
display(display_none),
|
||||
metadata(authenticated = false)
|
||||
)]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn get_pubkey(#[context] ctx: RpcContext) -> Result<Jwk, RpcError> {
|
||||
let secret = ctx.as_ref().clone();
|
||||
let pub_key = secret.to_public_key()?;
|
||||
Ok(pub_key)
|
||||
}
|
||||
|
||||
@@ -1,28 +1,22 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use chrono::Utc;
|
||||
use clap::ArgMatches;
|
||||
use color_eyre::eyre::eyre;
|
||||
use helpers::AtomicFile;
|
||||
use openssl::pkey::{PKey, Private};
|
||||
use openssl::x509::X509;
|
||||
use patch_db::{DbHandle, LockType, PatchDbHandle, Revision};
|
||||
use patch_db::{DbHandle, LockType, PatchDbHandle};
|
||||
use rpc_toolkit::command;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use torut::onion::TorSecretKeyV3;
|
||||
use tracing::instrument;
|
||||
|
||||
use super::target::BackupTargetId;
|
||||
use super::PackageBackupReport;
|
||||
use crate::auth::check_password_against_db;
|
||||
use crate::backup::os::OsBackup;
|
||||
use crate::backup::{BackupReport, ServerBackupReport};
|
||||
use crate::context::RpcContext;
|
||||
use crate::db::model::BackupProgress;
|
||||
use crate::db::util::WithRevision;
|
||||
use crate::disk::mount::backup::BackupMountGuard;
|
||||
use crate::disk::mount::filesystem::ReadWrite;
|
||||
use crate::disk::mount::guard::TmpMountGuard;
|
||||
@@ -34,88 +28,6 @@ use crate::util::serde::IoFormat;
|
||||
use crate::version::VersionT;
|
||||
use crate::{Error, ErrorKind, ResultExt};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct OsBackup {
|
||||
pub tor_key: TorSecretKeyV3,
|
||||
pub root_ca_key: PKey<Private>,
|
||||
pub root_ca_cert: X509,
|
||||
pub ui: Value,
|
||||
}
|
||||
impl<'de> Deserialize<'de> for OsBackup {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename = "kebab-case")]
|
||||
struct OsBackupDe {
|
||||
tor_key: String,
|
||||
root_ca_key: String,
|
||||
root_ca_cert: String,
|
||||
ui: Value,
|
||||
}
|
||||
let int = OsBackupDe::deserialize(deserializer)?;
|
||||
let key_vec = base32::decode(base32::Alphabet::RFC4648 { padding: true }, &int.tor_key)
|
||||
.ok_or_else(|| {
|
||||
serde::de::Error::invalid_value(
|
||||
serde::de::Unexpected::Str(&int.tor_key),
|
||||
&"an RFC4648 encoded string",
|
||||
)
|
||||
})?;
|
||||
if key_vec.len() != 64 {
|
||||
return Err(serde::de::Error::invalid_value(
|
||||
serde::de::Unexpected::Str(&int.tor_key),
|
||||
&"a 64 byte value encoded as an RFC4648 string",
|
||||
));
|
||||
}
|
||||
let mut key_slice = [0; 64];
|
||||
key_slice.clone_from_slice(&key_vec);
|
||||
Ok(OsBackup {
|
||||
tor_key: TorSecretKeyV3::from(key_slice),
|
||||
root_ca_key: PKey::<Private>::private_key_from_pem(int.root_ca_key.as_bytes())
|
||||
.map_err(serde::de::Error::custom)?,
|
||||
root_ca_cert: X509::from_pem(int.root_ca_cert.as_bytes())
|
||||
.map_err(serde::de::Error::custom)?,
|
||||
ui: int.ui,
|
||||
})
|
||||
}
|
||||
}
|
||||
impl Serialize for OsBackup {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename = "kebab-case")]
|
||||
struct OsBackupSer<'a> {
|
||||
tor_key: String,
|
||||
root_ca_key: String,
|
||||
root_ca_cert: String,
|
||||
ui: &'a Value,
|
||||
}
|
||||
OsBackupSer {
|
||||
tor_key: base32::encode(
|
||||
base32::Alphabet::RFC4648 { padding: true },
|
||||
&self.tor_key.as_bytes(),
|
||||
),
|
||||
root_ca_key: String::from_utf8(
|
||||
self.root_ca_key
|
||||
.private_key_to_pem_pkcs8()
|
||||
.map_err(serde::ser::Error::custom)?,
|
||||
)
|
||||
.map_err(serde::ser::Error::custom)?,
|
||||
root_ca_cert: String::from_utf8(
|
||||
self.root_ca_cert
|
||||
.to_pem()
|
||||
.map_err(serde::ser::Error::custom)?,
|
||||
)
|
||||
.map_err(serde::ser::Error::custom)?,
|
||||
ui: &self.ui,
|
||||
}
|
||||
.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_comma_separated(arg: &str, _: &ArgMatches) -> Result<BTreeSet<PackageId>, Error> {
|
||||
arg.split(',')
|
||||
.map(|s| s.trim().parse().map_err(Error::from))
|
||||
@@ -123,32 +35,40 @@ fn parse_comma_separated(arg: &str, _: &ArgMatches) -> Result<BTreeSet<PackageId
|
||||
}
|
||||
|
||||
#[command(rename = "create", display(display_none))]
|
||||
#[instrument(skip(ctx, old_password, password))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn backup_all(
|
||||
#[context] ctx: RpcContext,
|
||||
#[arg(rename = "target-id")] target_id: BackupTargetId,
|
||||
#[arg(rename = "old-password", long = "old-password")] old_password: Option<String>,
|
||||
#[arg(rename = "old-password", long = "old-password")] old_password: Option<
|
||||
crate::auth::PasswordType,
|
||||
>,
|
||||
#[arg(
|
||||
rename = "package-ids",
|
||||
long = "package-ids",
|
||||
parse(parse_comma_separated)
|
||||
)]
|
||||
package_ids: Option<BTreeSet<PackageId>>,
|
||||
#[arg] password: String,
|
||||
) -> Result<WithRevision<()>, Error> {
|
||||
#[arg] password: crate::auth::PasswordType,
|
||||
) -> Result<(), Error> {
|
||||
let mut db = ctx.db.handle();
|
||||
let old_password_decrypted = old_password
|
||||
.as_ref()
|
||||
.unwrap_or(&password)
|
||||
.clone()
|
||||
.decrypt(&ctx)?;
|
||||
let password = password.decrypt(&ctx)?;
|
||||
check_password_against_db(&mut ctx.secret_store.acquire().await?, &password).await?;
|
||||
let fs = target_id
|
||||
.load(&mut ctx.secret_store.acquire().await?)
|
||||
.await?;
|
||||
let mut backup_guard = BackupMountGuard::mount(
|
||||
TmpMountGuard::mount(&fs, ReadWrite).await?,
|
||||
old_password.as_ref().unwrap_or(&password),
|
||||
&old_password_decrypted,
|
||||
)
|
||||
.await?;
|
||||
let all_packages = crate::db::DatabaseModel::new()
|
||||
.package_data()
|
||||
.get(&mut db, false)
|
||||
.get(&mut db)
|
||||
.await?
|
||||
.0
|
||||
.keys()
|
||||
@@ -159,7 +79,7 @@ pub async fn backup_all(
|
||||
if old_password.is_some() {
|
||||
backup_guard.change_password(&password)?;
|
||||
}
|
||||
let revision = assure_backing_up(&mut db, &package_ids).await?;
|
||||
assure_backing_up(&mut db, &package_ids).await?;
|
||||
tokio::task::spawn(async move {
|
||||
let backup_res = perform_backup(&ctx, &mut db, backup_guard, &package_ids).await;
|
||||
let backup_progress = crate::db::DatabaseModel::new()
|
||||
@@ -238,17 +158,14 @@ pub async fn backup_all(
|
||||
.await
|
||||
.expect("failed to change server status");
|
||||
});
|
||||
Ok(WithRevision {
|
||||
response: (),
|
||||
revision,
|
||||
})
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(db, packages))]
|
||||
#[instrument(skip_all)]
|
||||
async fn assure_backing_up(
|
||||
db: &mut PatchDbHandle,
|
||||
packages: impl IntoIterator<Item = &PackageId>,
|
||||
) -> Result<Option<Arc<Revision>>, Error> {
|
||||
) -> Result<(), Error> {
|
||||
let mut tx = db.begin().await?;
|
||||
let mut backing_up = crate::db::DatabaseModel::new()
|
||||
.server_info()
|
||||
@@ -279,10 +196,11 @@ async fn assure_backing_up(
|
||||
.collect(),
|
||||
);
|
||||
backing_up.save(&mut tx).await?;
|
||||
Ok(tx.commit(None).await?)
|
||||
tx.commit().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(ctx, db, backup_guard))]
|
||||
#[instrument(skip_all)]
|
||||
async fn perform_backup<Db: DbHandle>(
|
||||
ctx: &RpcContext,
|
||||
mut db: Db,
|
||||
@@ -290,10 +208,9 @@ async fn perform_backup<Db: DbHandle>(
|
||||
package_ids: &BTreeSet<PackageId>,
|
||||
) -> Result<BTreeMap<PackageId, PackageBackupReport>, Error> {
|
||||
let mut backup_report = BTreeMap::new();
|
||||
|
||||
for package_id in crate::db::DatabaseModel::new()
|
||||
.package_data()
|
||||
.keys(&mut db, false)
|
||||
.keys(&mut db)
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|id| package_ids.contains(id))
|
||||
@@ -313,7 +230,7 @@ async fn perform_backup<Db: DbHandle>(
|
||||
let main_status_model = installed_model.clone().status().main();
|
||||
|
||||
main_status_model.lock(&mut tx, LockType::Write).await?;
|
||||
let (started, health) = match main_status_model.get(&mut tx, true).await?.into_owned() {
|
||||
let (started, health) = match main_status_model.get(&mut tx).await?.into_owned() {
|
||||
MainStatus::Starting { .. } => (Some(Utc::now()), Default::default()),
|
||||
MainStatus::Running { started, health } => (Some(started), health.clone()),
|
||||
MainStatus::Stopped | MainStatus::Stopping | MainStatus::Restarting => {
|
||||
@@ -342,11 +259,7 @@ async fn perform_backup<Db: DbHandle>(
|
||||
.await?;
|
||||
tx.save().await?; // drop locks
|
||||
|
||||
let manifest = installed_model
|
||||
.clone()
|
||||
.manifest()
|
||||
.get(&mut db, false)
|
||||
.await?;
|
||||
let manifest = installed_model.clone().manifest().get(&mut db).await?;
|
||||
|
||||
ctx.managers
|
||||
.get(&(manifest.id.clone(), manifest.version.clone()))
|
||||
@@ -422,11 +335,12 @@ async fn perform_backup<Db: DbHandle>(
|
||||
tx.save().await?;
|
||||
}
|
||||
|
||||
crate::db::DatabaseModel::new()
|
||||
.lock(&mut db, LockType::Write)
|
||||
.await?;
|
||||
let ui = crate::db::DatabaseModel::new()
|
||||
.ui()
|
||||
.get(&mut db)
|
||||
.await?
|
||||
.into_owned();
|
||||
|
||||
let (root_ca_key, root_ca_cert) = ctx.net_controller.ssl.export_root_ca().await?;
|
||||
let mut os_backup_file = AtomicFile::new(
|
||||
backup_guard.as_ref().join("os-backup.cbor"),
|
||||
None::<PathBuf>,
|
||||
@@ -434,18 +348,10 @@ async fn perform_backup<Db: DbHandle>(
|
||||
.await
|
||||
.with_kind(ErrorKind::Filesystem)?;
|
||||
os_backup_file
|
||||
.write_all(
|
||||
&IoFormat::Cbor.to_vec(&OsBackup {
|
||||
tor_key: ctx.net_controller.tor.embassyd_tor_key().await,
|
||||
root_ca_key,
|
||||
root_ca_cert,
|
||||
ui: crate::db::DatabaseModel::new()
|
||||
.ui()
|
||||
.get(&mut db, true)
|
||||
.await?
|
||||
.into_owned(),
|
||||
})?,
|
||||
)
|
||||
.write_all(&IoFormat::Cbor.to_vec(&OsBackup {
|
||||
account: ctx.account.read().await.clone(),
|
||||
ui,
|
||||
})?)
|
||||
.await?;
|
||||
os_backup_file
|
||||
.save()
|
||||
|
||||
@@ -4,11 +4,11 @@ use std::path::{Path, PathBuf};
|
||||
use chrono::{DateTime, Utc};
|
||||
use color_eyre::eyre::eyre;
|
||||
use helpers::AtomicFile;
|
||||
use patch_db::{DbHandle, HasModel, LockType};
|
||||
use models::ImageId;
|
||||
use patch_db::{DbHandle, HasModel};
|
||||
use reqwest::Url;
|
||||
use rpc_toolkit::command;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{Executor, Sqlite};
|
||||
use tokio::fs::File;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tracing::instrument;
|
||||
@@ -16,18 +16,20 @@ use tracing::instrument;
|
||||
use self::target::PackageBackupInfo;
|
||||
use crate::context::RpcContext;
|
||||
use crate::dependencies::reconfigure_dependents_with_live_pointers;
|
||||
use crate::id::ImageId;
|
||||
use crate::install::PKG_ARCHIVE_DIR;
|
||||
use crate::net::interface::{InterfaceId, Interfaces};
|
||||
use crate::net::keys::Key;
|
||||
use crate::procedure::docker::DockerContainers;
|
||||
use crate::procedure::{NoOutput, PackageProcedure, ProcedureName};
|
||||
use crate::s9pk::manifest::PackageId;
|
||||
use crate::util::serde::IoFormat;
|
||||
use crate::util::serde::{Base32, Base64, IoFormat};
|
||||
use crate::util::Version;
|
||||
use crate::version::{Current, VersionT};
|
||||
use crate::volume::{backup_dir, Volume, VolumeId, Volumes, BACKUP_DIR};
|
||||
use crate::{Error, ErrorKind, ResultExt};
|
||||
|
||||
pub mod backup_bulk;
|
||||
pub mod os;
|
||||
pub mod restore;
|
||||
pub mod target;
|
||||
|
||||
@@ -61,7 +63,10 @@ pub fn package_backup() -> Result<(), Error> {
|
||||
#[derive(Deserialize, Serialize)]
|
||||
struct BackupMetadata {
|
||||
pub timestamp: DateTime<Utc>,
|
||||
pub tor_keys: BTreeMap<InterfaceId, String>,
|
||||
#[serde(default)]
|
||||
pub network_keys: BTreeMap<InterfaceId, Base64<[u8; 32]>>,
|
||||
#[serde(default)]
|
||||
pub tor_keys: BTreeMap<InterfaceId, Base32<[u8; 64]>>, // DEPRECATED
|
||||
pub marketplace_url: Option<Url>,
|
||||
}
|
||||
|
||||
@@ -73,20 +78,21 @@ pub struct BackupActions {
|
||||
impl BackupActions {
|
||||
pub fn validate(
|
||||
&self,
|
||||
container: &Option<DockerContainers>,
|
||||
eos_version: &Version,
|
||||
volumes: &Volumes,
|
||||
image_ids: &BTreeSet<ImageId>,
|
||||
) -> Result<(), Error> {
|
||||
self.create
|
||||
.validate(eos_version, volumes, image_ids, false)
|
||||
.validate(container, eos_version, volumes, image_ids, false)
|
||||
.with_ctx(|_| (crate::ErrorKind::ValidateS9pk, "Backup Create"))?;
|
||||
self.restore
|
||||
.validate(eos_version, volumes, image_ids, false)
|
||||
.validate(container, eos_version, volumes, image_ids, false)
|
||||
.with_ctx(|_| (crate::ErrorKind::ValidateS9pk, "Backup Restore"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(ctx, db))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn create<Db: DbHandle>(
|
||||
&self,
|
||||
ctx: &RpcContext,
|
||||
@@ -111,23 +117,22 @@ impl BackupActions {
|
||||
ProcedureName::CreateBackup,
|
||||
&volumes,
|
||||
None,
|
||||
false,
|
||||
None,
|
||||
)
|
||||
.await?
|
||||
.map_err(|e| eyre!("{}", e.1))
|
||||
.with_kind(crate::ErrorKind::Backup)?;
|
||||
let tor_keys = interfaces
|
||||
.tor_keys(&mut ctx.secret_store.acquire().await?, pkg_id)
|
||||
let (network_keys, tor_keys) = Key::for_package(&ctx.secret_store, pkg_id)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|(id, key)| {
|
||||
(
|
||||
id,
|
||||
base32::encode(base32::Alphabet::RFC4648 { padding: true }, &key.as_bytes()),
|
||||
)
|
||||
.filter_map(|k| {
|
||||
let interface = k.interface().map(|(_, i)| i)?;
|
||||
Some((
|
||||
(interface.clone(), Base64(k.as_bytes())),
|
||||
(interface, Base32(k.tor_key().as_bytes())),
|
||||
))
|
||||
})
|
||||
.collect();
|
||||
.unzip();
|
||||
let marketplace_url = crate::db::DatabaseModel::new()
|
||||
.package_data()
|
||||
.idx_model(pkg_id)
|
||||
@@ -137,7 +142,7 @@ impl BackupActions {
|
||||
.expect(db)
|
||||
.await?
|
||||
.marketplace_url()
|
||||
.get(db, true)
|
||||
.get(db)
|
||||
.await?
|
||||
.into_owned();
|
||||
let tmp_path = Path::new(BACKUP_DIR)
|
||||
@@ -170,6 +175,7 @@ impl BackupActions {
|
||||
outfile
|
||||
.write_all(&IoFormat::Cbor.to_vec(&BackupMetadata {
|
||||
timestamp,
|
||||
network_keys,
|
||||
tor_keys,
|
||||
marketplace_url,
|
||||
})?)
|
||||
@@ -183,20 +189,16 @@ impl BackupActions {
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(ctx, db, secrets))]
|
||||
pub async fn restore<Ex, Db: DbHandle>(
|
||||
#[instrument(skip_all)]
|
||||
pub async fn restore<Db: DbHandle>(
|
||||
&self,
|
||||
ctx: &RpcContext,
|
||||
db: &mut Db,
|
||||
secrets: &mut Ex,
|
||||
pkg_id: &PackageId,
|
||||
pkg_version: &Version,
|
||||
interfaces: &Interfaces,
|
||||
volumes: &Volumes,
|
||||
) -> Result<(), Error>
|
||||
where
|
||||
for<'a> &'a mut Ex: Executor<'a, Database = Sqlite>,
|
||||
{
|
||||
) -> Result<(), Error> {
|
||||
let mut volumes = volumes.clone();
|
||||
volumes.insert(VolumeId::Backup, Volume::Backup { readonly: true });
|
||||
self.restore
|
||||
@@ -207,7 +209,6 @@ impl BackupActions {
|
||||
ProcedureName::RestoreBackup,
|
||||
&volumes,
|
||||
None,
|
||||
false,
|
||||
None,
|
||||
)
|
||||
.await?
|
||||
@@ -222,27 +223,6 @@ impl BackupActions {
|
||||
)
|
||||
})?,
|
||||
)?;
|
||||
for (iface, key) in metadata.tor_keys {
|
||||
let key_vec = base32::decode(base32::Alphabet::RFC4648 { padding: true }, &key)
|
||||
.ok_or_else(|| {
|
||||
Error::new(
|
||||
eyre!("invalid base32 string"),
|
||||
crate::ErrorKind::Deserialization,
|
||||
)
|
||||
})?;
|
||||
sqlx::query!(
|
||||
"REPLACE INTO tor (package, interface, key) VALUES (?, ?, ?)",
|
||||
**pkg_id,
|
||||
*iface,
|
||||
key_vec,
|
||||
)
|
||||
.execute(&mut *secrets)
|
||||
.await?;
|
||||
}
|
||||
crate::db::DatabaseModel::new()
|
||||
.package_data()
|
||||
.lock(db, LockType::Write)
|
||||
.await?;
|
||||
let pde = crate::db::DatabaseModel::new()
|
||||
.package_data()
|
||||
.idx_model(pkg_id)
|
||||
@@ -251,10 +231,6 @@ impl BackupActions {
|
||||
.installed()
|
||||
.expect(db)
|
||||
.await?;
|
||||
pde.clone()
|
||||
.interface_addresses()
|
||||
.put(db, &interfaces.install(&mut *secrets, pkg_id).await?)
|
||||
.await?;
|
||||
pde.marketplace_url()
|
||||
.put(db, &metadata.marketplace_url)
|
||||
.await?;
|
||||
@@ -267,7 +243,7 @@ impl BackupActions {
|
||||
.installed()
|
||||
.expect(db)
|
||||
.await?
|
||||
.get(db, true)
|
||||
.get(db)
|
||||
.await?;
|
||||
|
||||
let receipts = crate::config::ConfigReceipts::new(db).await?;
|
||||
|
||||
121
backend/src/backup/os.rs
Normal file
@@ -0,0 +1,121 @@
|
||||
use crate::account::AccountInfo;
|
||||
use crate::hostname::{generate_hostname, generate_id, Hostname};
|
||||
use crate::net::keys::Key;
|
||||
use crate::util::serde::Base64;
|
||||
use crate::Error;
|
||||
use openssl::pkey::PKey;
|
||||
use openssl::x509::X509;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
pub struct OsBackup {
|
||||
pub account: AccountInfo,
|
||||
pub ui: Value,
|
||||
}
|
||||
impl<'de> Deserialize<'de> for OsBackup {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let tagged = OsBackupSerDe::deserialize(deserializer)?;
|
||||
match tagged.version {
|
||||
0 => serde_json::from_value::<OsBackupV0>(tagged.rest)
|
||||
.map_err(serde::de::Error::custom)?
|
||||
.project()
|
||||
.map_err(serde::de::Error::custom),
|
||||
1 => serde_json::from_value::<OsBackupV1>(tagged.rest)
|
||||
.map_err(serde::de::Error::custom)?
|
||||
.project()
|
||||
.map_err(serde::de::Error::custom),
|
||||
v => Err(serde::de::Error::custom(&format!(
|
||||
"Unknown backup version {v}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Serialize for OsBackup {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
OsBackupSerDe {
|
||||
version: 1,
|
||||
rest: serde_json::to_value(
|
||||
&OsBackupV1::unproject(self).map_err(serde::ser::Error::custom)?,
|
||||
)
|
||||
.map_err(serde::ser::Error::custom)?,
|
||||
}
|
||||
.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
struct OsBackupSerDe {
|
||||
#[serde(default)]
|
||||
version: usize,
|
||||
#[serde(flatten)]
|
||||
rest: Value,
|
||||
}
|
||||
|
||||
/// V0
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename = "kebab-case")]
|
||||
struct OsBackupV0 {
|
||||
// tor_key: Base32<[u8; 64]>,
|
||||
root_ca_key: String, // PEM Encoded OpenSSL Key
|
||||
root_ca_cert: String, // PEM Encoded OpenSSL X509 Certificate
|
||||
ui: Value, // JSON Value
|
||||
}
|
||||
impl OsBackupV0 {
|
||||
fn project(self) -> Result<OsBackup, Error> {
|
||||
Ok(OsBackup {
|
||||
account: AccountInfo {
|
||||
server_id: generate_id(),
|
||||
hostname: generate_hostname(),
|
||||
password: Default::default(),
|
||||
key: Key::new(None),
|
||||
root_ca_key: PKey::private_key_from_pem(self.root_ca_key.as_bytes())?,
|
||||
root_ca_cert: X509::from_pem(self.root_ca_cert.as_bytes())?,
|
||||
},
|
||||
ui: self.ui,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// V1
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[serde(rename = "kebab-case")]
|
||||
struct OsBackupV1 {
|
||||
server_id: String, // uuidv4
|
||||
hostname: String, // embassy-<adjective>-<noun>
|
||||
net_key: Base64<[u8; 32]>, // Ed25519 Secret Key
|
||||
root_ca_key: String, // PEM Encoded OpenSSL Key
|
||||
root_ca_cert: String, // PEM Encoded OpenSSL X509 Certificate
|
||||
ui: Value, // JSON Value
|
||||
// TODO add more
|
||||
}
|
||||
impl OsBackupV1 {
|
||||
fn project(self) -> Result<OsBackup, Error> {
|
||||
Ok(OsBackup {
|
||||
account: AccountInfo {
|
||||
server_id: self.server_id,
|
||||
hostname: Hostname(self.hostname),
|
||||
password: Default::default(),
|
||||
key: Key::from_bytes(None, self.net_key.0),
|
||||
root_ca_key: PKey::private_key_from_pem(self.root_ca_key.as_bytes())?,
|
||||
root_ca_cert: X509::from_pem(self.root_ca_cert.as_bytes())?,
|
||||
},
|
||||
ui: self.ui,
|
||||
})
|
||||
}
|
||||
fn unproject(backup: &OsBackup) -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
server_id: backup.account.server_id.clone(),
|
||||
hostname: backup.account.hostname.0.clone(),
|
||||
net_key: Base64(backup.account.key.as_bytes()),
|
||||
root_ca_key: String::from_utf8(backup.account.root_ca_key.private_key_to_pem_pkcs8()?)?,
|
||||
root_ca_cert: String::from_utf8(backup.account.root_ca_cert.to_pem()?)?,
|
||||
ui: backup.ui.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -6,31 +6,33 @@ use std::time::Duration;
|
||||
|
||||
use clap::ArgMatches;
|
||||
use color_eyre::eyre::eyre;
|
||||
use futures::future::BoxFuture;
|
||||
use futures::FutureExt;
|
||||
use futures::{future::BoxFuture, stream};
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use openssl::x509::X509;
|
||||
use patch_db::{DbHandle, PatchDbHandle, Revision};
|
||||
use patch_db::{DbHandle, PatchDbHandle};
|
||||
use rpc_toolkit::command;
|
||||
use sqlx::Connection;
|
||||
use tokio::fs::File;
|
||||
use tokio::task::JoinHandle;
|
||||
use torut::onion::OnionAddressV3;
|
||||
use tracing::instrument;
|
||||
|
||||
use super::target::BackupTargetId;
|
||||
use crate::backup::backup_bulk::OsBackup;
|
||||
use crate::backup::os::OsBackup;
|
||||
use crate::backup::BackupMetadata;
|
||||
use crate::context::rpc::RpcContextConfig;
|
||||
use crate::context::{RpcContext, SetupContext};
|
||||
use crate::db::model::{PackageDataEntry, StaticFiles};
|
||||
use crate::db::util::WithRevision;
|
||||
use crate::disk::mount::backup::{BackupMountGuard, PackageBackupMountGuard};
|
||||
use crate::disk::mount::filesystem::ReadOnly;
|
||||
use crate::disk::mount::filesystem::ReadWrite;
|
||||
use crate::disk::mount::guard::TmpMountGuard;
|
||||
use crate::hostname::Hostname;
|
||||
use crate::init::init;
|
||||
use crate::install::progress::InstallProgress;
|
||||
use crate::install::{download_install_s9pk, PKG_PUBLIC_DIR};
|
||||
use crate::net::ssl::SslManager;
|
||||
use crate::notifications::NotificationLevel;
|
||||
use crate::s9pk::manifest::{Manifest, PackageId};
|
||||
use crate::s9pk::reader::S9pkReader;
|
||||
use crate::setup::RecoveryStatus;
|
||||
use crate::setup::SetupStatus;
|
||||
use crate::util::display_none;
|
||||
use crate::util::io::dir_size;
|
||||
use crate::util::serde::IoFormat;
|
||||
@@ -44,69 +46,58 @@ fn parse_comma_separated(arg: &str, _: &ArgMatches) -> Result<Vec<PackageId>, Er
|
||||
}
|
||||
|
||||
#[command(rename = "restore", display(display_none))]
|
||||
#[instrument(skip(ctx, password))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn restore_packages_rpc(
|
||||
#[context] ctx: RpcContext,
|
||||
#[arg(parse(parse_comma_separated))] ids: Vec<PackageId>,
|
||||
#[arg(rename = "target-id")] target_id: BackupTargetId,
|
||||
#[arg] password: String,
|
||||
) -> Result<WithRevision<()>, Error> {
|
||||
) -> Result<(), Error> {
|
||||
let mut db = ctx.db.handle();
|
||||
let fs = target_id
|
||||
.load(&mut ctx.secret_store.acquire().await?)
|
||||
.await?;
|
||||
let backup_guard = BackupMountGuard::mount(
|
||||
TmpMountGuard::mount(&fs, ReadOnly).await?,
|
||||
&password,
|
||||
)
|
||||
.await?;
|
||||
let backup_guard =
|
||||
BackupMountGuard::mount(TmpMountGuard::mount(&fs, ReadWrite).await?, &password).await?;
|
||||
|
||||
let (revision, backup_guard, tasks, _) =
|
||||
restore_packages(&ctx, &mut db, backup_guard, ids).await?;
|
||||
let (backup_guard, tasks, _) = restore_packages(&ctx, &mut db, backup_guard, ids).await?;
|
||||
|
||||
tokio::spawn(async move {
|
||||
let res = futures::future::join_all(tasks).await;
|
||||
for res in res {
|
||||
match res.with_kind(crate::ErrorKind::Unknown) {
|
||||
Ok((Ok(_), _)) => (),
|
||||
Ok((Err(err), package_id)) => {
|
||||
if let Err(err) = ctx.notification_manager.notify(
|
||||
&mut db,
|
||||
Some(package_id.clone()),
|
||||
NotificationLevel::Error,
|
||||
"Restoration Failure".to_string(), format!("Error restoring package {}: {}", package_id,err), (), None).await{
|
||||
tracing::error!("Failed to notify: {}", err);
|
||||
tracing::debug!("{:?}", err);
|
||||
stream::iter(tasks.into_iter().map(|x| (x, ctx.clone())))
|
||||
.for_each_concurrent(5, |(res, ctx)| async move {
|
||||
let mut db = ctx.db.handle();
|
||||
match res.await {
|
||||
(Ok(_), _) => (),
|
||||
(Err(err), package_id) => {
|
||||
if let Err(err) = ctx
|
||||
.notification_manager
|
||||
.notify(
|
||||
&mut db,
|
||||
Some(package_id.clone()),
|
||||
NotificationLevel::Error,
|
||||
"Restoration Failure".to_string(),
|
||||
format!("Error restoring package {}: {}", package_id, err),
|
||||
(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!("Failed to notify: {}", err);
|
||||
tracing::debug!("{:?}", err);
|
||||
};
|
||||
tracing::error!("Error restoring package {}: {}", package_id, err);
|
||||
tracing::debug!("{:?}", err);
|
||||
},
|
||||
Err(e) => {
|
||||
if let Err(err) = ctx.notification_manager.notify(
|
||||
&mut db,
|
||||
None,
|
||||
NotificationLevel::Error,
|
||||
"Restoration Failure".to_string(), format!("Error during restoration: {}", e), (), None).await {
|
||||
|
||||
tracing::error!("Failed to notify: {}", err);
|
||||
tracing::error!("Error restoring package {}: {}", package_id, err);
|
||||
tracing::debug!("{:?}", err);
|
||||
}
|
||||
tracing::error!("Error restoring packages: {}", e);
|
||||
tracing::debug!("{:?}", e);
|
||||
},
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.await;
|
||||
if let Err(e) = backup_guard.unmount().await {
|
||||
tracing::error!("Error unmounting backup drive: {}", e);
|
||||
tracing::debug!("{:?}", e);
|
||||
}
|
||||
});
|
||||
|
||||
Ok(WithRevision {
|
||||
response: (),
|
||||
revision,
|
||||
})
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn approximate_progress(
|
||||
@@ -134,7 +125,7 @@ async fn approximate_progress_loop(
|
||||
tracing::error!("Failed to approximate restore progress: {}", e);
|
||||
tracing::debug!("{:?}", e);
|
||||
} else {
|
||||
*ctx.recovery_status.write().await = Some(Ok(starting_info.flatten()));
|
||||
*ctx.setup_status.write().await = Some(Ok(starting_info.flatten()));
|
||||
}
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
}
|
||||
@@ -147,7 +138,7 @@ struct ProgressInfo {
|
||||
target_volume_size: BTreeMap<PackageId, u64>,
|
||||
}
|
||||
impl ProgressInfo {
|
||||
fn flatten(&self) -> RecoveryStatus {
|
||||
fn flatten(&self) -> SetupStatus {
|
||||
let mut total_bytes = 0;
|
||||
let mut bytes_transferred = 0;
|
||||
|
||||
@@ -170,22 +161,22 @@ impl ProgressInfo {
|
||||
bytes_transferred = total_bytes;
|
||||
}
|
||||
|
||||
RecoveryStatus {
|
||||
total_bytes,
|
||||
SetupStatus {
|
||||
total_bytes: Some(total_bytes),
|
||||
bytes_transferred,
|
||||
complete: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(ctx))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn recover_full_embassy(
|
||||
ctx: SetupContext,
|
||||
disk_guid: Arc<String>,
|
||||
embassy_password: String,
|
||||
recovery_source: TmpMountGuard,
|
||||
recovery_password: Option<String>,
|
||||
) -> Result<(OnionAddressV3, X509, BoxFuture<'static, Result<(), Error>>), Error> {
|
||||
) -> Result<(Arc<String>, Hostname, OnionAddressV3, X509), Error> {
|
||||
let backup_guard = BackupMountGuard::mount(
|
||||
recovery_source,
|
||||
recovery_password.as_deref().unwrap_or_default(),
|
||||
@@ -193,7 +184,7 @@ pub async fn recover_full_embassy(
|
||||
.await?;
|
||||
|
||||
let os_backup_path = backup_guard.as_ref().join("os-backup.cbor");
|
||||
let os_backup: OsBackup =
|
||||
let mut os_backup: OsBackup =
|
||||
IoFormat::Cbor.from_slice(&tokio::fs::read(&os_backup_path).await.with_ctx(|_| {
|
||||
(
|
||||
crate::ErrorKind::Filesystem,
|
||||
@@ -201,92 +192,72 @@ pub async fn recover_full_embassy(
|
||||
)
|
||||
})?)?;
|
||||
|
||||
let password = argon2::hash_encoded(
|
||||
os_backup.account.password = argon2::hash_encoded(
|
||||
embassy_password.as_bytes(),
|
||||
&rand::random::<[u8; 16]>()[..],
|
||||
&argon2::Config::default(),
|
||||
)
|
||||
.with_kind(crate::ErrorKind::PasswordHashGeneration)?;
|
||||
let key_vec = os_backup.tor_key.as_bytes().to_vec();
|
||||
let secret_store = ctx.secret_store().await?;
|
||||
sqlx::query!(
|
||||
"REPLACE INTO account (id, password, tor_key) VALUES (?, ?, ?)",
|
||||
0,
|
||||
password,
|
||||
key_vec,
|
||||
)
|
||||
.execute(&mut secret_store.acquire().await?)
|
||||
.await?;
|
||||
|
||||
SslManager::import_root_ca(
|
||||
secret_store.clone(),
|
||||
os_backup.root_ca_key,
|
||||
os_backup.root_ca_cert.clone(),
|
||||
)
|
||||
.await?;
|
||||
let secret_store = ctx.secret_store().await?;
|
||||
|
||||
os_backup.account.save(&secret_store).await?;
|
||||
|
||||
secret_store.close().await;
|
||||
|
||||
Ok((
|
||||
os_backup.tor_key.public().get_onion_address(),
|
||||
os_backup.root_ca_cert,
|
||||
async move {
|
||||
let rpc_ctx = RpcContext::init(ctx.config_path.as_ref(), disk_guid).await?;
|
||||
let mut db = rpc_ctx.db.handle();
|
||||
let cfg = RpcContextConfig::load(ctx.config_path.clone()).await?;
|
||||
|
||||
let ids = backup_guard
|
||||
.metadata
|
||||
.package_backups
|
||||
.keys()
|
||||
.cloned()
|
||||
.collect();
|
||||
let (_, backup_guard, tasks, progress_info) = restore_packages(
|
||||
&rpc_ctx,
|
||||
&mut db,
|
||||
backup_guard,
|
||||
ids,
|
||||
)
|
||||
.await?;
|
||||
init(&cfg).await?;
|
||||
|
||||
tokio::select! {
|
||||
res = futures::future::join_all(tasks) => {
|
||||
for res in res {
|
||||
match res.with_kind(crate::ErrorKind::Unknown) {
|
||||
Ok((Ok(_), _)) => (),
|
||||
Ok((Err(err), package_id)) => {
|
||||
if let Err(err) = rpc_ctx.notification_manager.notify(
|
||||
&mut db,
|
||||
Some(package_id.clone()),
|
||||
NotificationLevel::Error,
|
||||
"Restoration Failure".to_string(), format!("Error restoring package {}: {}", package_id,err), (), None).await{
|
||||
tracing::error!("Failed to notify: {}", err);
|
||||
tracing::debug!("{:?}", err);
|
||||
};
|
||||
tracing::error!("Error restoring package {}: {}", package_id, err);
|
||||
let rpc_ctx = RpcContext::init(ctx.config_path.clone(), disk_guid.clone()).await?;
|
||||
|
||||
let mut db = rpc_ctx.db.handle();
|
||||
|
||||
let ids = backup_guard
|
||||
.metadata
|
||||
.package_backups
|
||||
.keys()
|
||||
.cloned()
|
||||
.collect();
|
||||
let (backup_guard, tasks, progress_info) =
|
||||
restore_packages(&rpc_ctx, &mut db, backup_guard, ids).await?;
|
||||
let task_consumer_rpc_ctx = rpc_ctx.clone();
|
||||
tokio::select! {
|
||||
_ = async move {
|
||||
stream::iter(tasks.into_iter().map(|x| (x, task_consumer_rpc_ctx.clone())))
|
||||
.for_each_concurrent(5, |(res, ctx)| async move {
|
||||
let mut db = ctx.db.handle();
|
||||
match res.await {
|
||||
(Ok(_), _) => (),
|
||||
(Err(err), package_id) => {
|
||||
if let Err(err) = ctx.notification_manager.notify(
|
||||
&mut db,
|
||||
Some(package_id.clone()),
|
||||
NotificationLevel::Error,
|
||||
"Restoration Failure".to_string(), format!("Error restoring package {}: {}", package_id,err), (), None).await{
|
||||
tracing::error!("Failed to notify: {}", err);
|
||||
tracing::debug!("{:?}", err);
|
||||
},
|
||||
Err(e) => {
|
||||
if let Err(err) = rpc_ctx.notification_manager.notify(
|
||||
&mut db,
|
||||
None,
|
||||
NotificationLevel::Error,
|
||||
"Restoration Failure".to_string(), format!("Error during restoration: {}", e), (), None).await {
|
||||
|
||||
tracing::error!("Failed to notify: {}", err);
|
||||
tracing::debug!("{:?}", err);
|
||||
}
|
||||
tracing::error!("Error restoring packages: {}", e);
|
||||
tracing::debug!("{:?}", e);
|
||||
},
|
||||
|
||||
}
|
||||
};
|
||||
tracing::error!("Error restoring package {}: {}", package_id, err);
|
||||
tracing::debug!("{:?}", err);
|
||||
},
|
||||
}
|
||||
},
|
||||
_ = approximate_progress_loop(&ctx, &rpc_ctx, progress_info) => unreachable!(concat!(module_path!(), "::approximate_progress_loop should not terminate")),
|
||||
}
|
||||
}).await;
|
||||
|
||||
backup_guard.unmount().await?;
|
||||
rpc_ctx.shutdown().await
|
||||
}.boxed()
|
||||
} => {
|
||||
|
||||
},
|
||||
_ = approximate_progress_loop(&ctx, &rpc_ctx, progress_info) => unreachable!(concat!(module_path!(), "::approximate_progress_loop should not terminate")),
|
||||
}
|
||||
|
||||
backup_guard.unmount().await?;
|
||||
rpc_ctx.shutdown().await?;
|
||||
|
||||
Ok((
|
||||
disk_guid,
|
||||
os_backup.account.hostname,
|
||||
os_backup.account.key.tor_address(),
|
||||
os_backup.account.root_ca_cert,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -297,14 +268,13 @@ async fn restore_packages(
|
||||
ids: Vec<PackageId>,
|
||||
) -> Result<
|
||||
(
|
||||
Option<Arc<Revision>>,
|
||||
BackupMountGuard<TmpMountGuard>,
|
||||
Vec<JoinHandle<(Result<(), Error>, PackageId)>>,
|
||||
Vec<BoxFuture<'static, (Result<(), Error>, PackageId)>>,
|
||||
ProgressInfo,
|
||||
),
|
||||
Error,
|
||||
> {
|
||||
let (revision, guards) = assure_restoring(ctx, db, ids, &backup_guard).await?;
|
||||
let guards = assure_restoring(ctx, db, ids, &backup_guard).await?;
|
||||
|
||||
let mut progress_info = ProgressInfo::default();
|
||||
|
||||
@@ -318,7 +288,7 @@ async fn restore_packages(
|
||||
.insert(id.clone(), dir_size(backup_dir(&id)).await?);
|
||||
progress_info.target_volume_size.insert(id.clone(), 0);
|
||||
let package_id = id.clone();
|
||||
tasks.push(tokio::spawn(
|
||||
tasks.push(
|
||||
async move {
|
||||
if let Err(e) = task.await {
|
||||
tracing::error!("Error restoring package {}: {}", id, e);
|
||||
@@ -328,26 +298,21 @@ async fn restore_packages(
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
.map(|x| (x, package_id)),
|
||||
));
|
||||
.map(|x| (x, package_id))
|
||||
.boxed(),
|
||||
);
|
||||
}
|
||||
|
||||
Ok((revision, backup_guard, tasks, progress_info))
|
||||
Ok((backup_guard, tasks, progress_info))
|
||||
}
|
||||
|
||||
#[instrument(skip(ctx, db, backup_guard))]
|
||||
#[instrument(skip_all)]
|
||||
async fn assure_restoring(
|
||||
ctx: &RpcContext,
|
||||
db: &mut PatchDbHandle,
|
||||
ids: Vec<PackageId>,
|
||||
backup_guard: &BackupMountGuard<TmpMountGuard>,
|
||||
) -> Result<
|
||||
(
|
||||
Option<Arc<Revision>>,
|
||||
Vec<(Manifest, PackageBackupMountGuard)>,
|
||||
),
|
||||
Error,
|
||||
> {
|
||||
) -> Result<Vec<(Manifest, PackageBackupMountGuard)>, Error> {
|
||||
let mut tx = db.begin().await?;
|
||||
|
||||
let mut guards = Vec::with_capacity(ids.len());
|
||||
@@ -407,18 +372,56 @@ async fn assure_restoring(
|
||||
guards.push((manifest, guard));
|
||||
}
|
||||
|
||||
Ok((tx.commit(None).await?, guards))
|
||||
tx.commit().await?;
|
||||
Ok(guards)
|
||||
}
|
||||
|
||||
#[instrument(skip(ctx, guard))]
|
||||
#[instrument(skip_all)]
|
||||
async fn restore_package<'a>(
|
||||
ctx: RpcContext,
|
||||
manifest: Manifest,
|
||||
guard: PackageBackupMountGuard,
|
||||
) -> Result<(Arc<InstallProgress>, BoxFuture<'static, Result<(), Error>>), Error> {
|
||||
let id = manifest.id.clone();
|
||||
let s9pk_path = Path::new(BACKUP_DIR)
|
||||
.join(&manifest.id)
|
||||
.join(format!("{}.s9pk", manifest.id));
|
||||
.join(format!("{}.s9pk", id));
|
||||
|
||||
let metadata_path = Path::new(BACKUP_DIR).join(&id).join("metadata.cbor");
|
||||
let metadata: BackupMetadata =
|
||||
IoFormat::Cbor.from_slice(&tokio::fs::read(&metadata_path).await.with_ctx(|_| {
|
||||
(
|
||||
crate::ErrorKind::Filesystem,
|
||||
metadata_path.display().to_string(),
|
||||
)
|
||||
})?)?;
|
||||
|
||||
let mut secrets = ctx.secret_store.acquire().await?;
|
||||
let mut secrets_tx = secrets.begin().await?;
|
||||
for (iface, key) in metadata.network_keys {
|
||||
let k = key.0.as_slice();
|
||||
sqlx::query!(
|
||||
"INSERT INTO network_keys (package, interface, key) VALUES ($1, $2, $3) ON CONFLICT (package, interface) DO NOTHING",
|
||||
*id,
|
||||
*iface,
|
||||
k,
|
||||
)
|
||||
.execute(&mut secrets_tx).await?;
|
||||
}
|
||||
// DEPRECATED
|
||||
for (iface, key) in metadata.tor_keys {
|
||||
let k = key.0.as_slice();
|
||||
sqlx::query!(
|
||||
"INSERT INTO tor (package, interface, key) VALUES ($1, $2, $3) ON CONFLICT (package, interface) DO NOTHING",
|
||||
*id,
|
||||
*iface,
|
||||
k,
|
||||
)
|
||||
.execute(&mut secrets_tx).await?;
|
||||
}
|
||||
secrets_tx.commit().await?;
|
||||
drop(secrets);
|
||||
|
||||
let len = tokio::fs::metadata(&s9pk_path)
|
||||
.await
|
||||
.with_ctx(|_| {
|
||||
|
||||
@@ -4,7 +4,7 @@ use color_eyre::eyre::eyre;
|
||||
use futures::TryStreamExt;
|
||||
use rpc_toolkit::command;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{Executor, Sqlite};
|
||||
use sqlx::{Executor, Postgres};
|
||||
|
||||
use super::{BackupTarget, BackupTargetId};
|
||||
use crate::context::RpcContext;
|
||||
@@ -49,8 +49,8 @@ pub async fn add(
|
||||
let embassy_os = recovery_info(&guard).await?;
|
||||
guard.unmount().await?;
|
||||
let path_string = Path::new("/").join(&cifs.path).display().to_string();
|
||||
let id: u32 = sqlx::query!(
|
||||
"INSERT INTO cifs_shares (hostname, path, username, password) VALUES (?, ?, ?, ?) RETURNING id AS \"id: u32\"",
|
||||
let id: i32 = sqlx::query!(
|
||||
"INSERT INTO cifs_shares (hostname, path, username, password) VALUES ($1, $2, $3, $4) RETURNING id",
|
||||
cifs.hostname,
|
||||
path_string,
|
||||
cifs.username,
|
||||
@@ -98,7 +98,7 @@ pub async fn update(
|
||||
guard.unmount().await?;
|
||||
let path_string = Path::new("/").join(&cifs.path).display().to_string();
|
||||
if sqlx::query!(
|
||||
"UPDATE cifs_shares SET hostname = ?, path = ?, username = ?, password = ? WHERE id = ?",
|
||||
"UPDATE cifs_shares SET hostname = $1, path = $2, username = $3, password = $4 WHERE id = $5",
|
||||
cifs.hostname,
|
||||
path_string,
|
||||
cifs.username,
|
||||
@@ -137,7 +137,7 @@ pub async fn remove(#[context] ctx: RpcContext, #[arg] id: BackupTargetId) -> Re
|
||||
crate::ErrorKind::NotFound,
|
||||
));
|
||||
};
|
||||
if sqlx::query!("DELETE FROM cifs_shares WHERE id = ?", id)
|
||||
if sqlx::query!("DELETE FROM cifs_shares WHERE id = $1", id)
|
||||
.execute(&ctx.secret_store)
|
||||
.await?
|
||||
.rows_affected()
|
||||
@@ -151,12 +151,12 @@ pub async fn remove(#[context] ctx: RpcContext, #[arg] id: BackupTargetId) -> Re
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn load<Ex>(secrets: &mut Ex, id: u32) -> Result<Cifs, Error>
|
||||
pub async fn load<Ex>(secrets: &mut Ex, id: i32) -> Result<Cifs, Error>
|
||||
where
|
||||
for<'a> &'a mut Ex: Executor<'a, Database = Sqlite>,
|
||||
for<'a> &'a mut Ex: Executor<'a, Database = Postgres>,
|
||||
{
|
||||
let record = sqlx::query!(
|
||||
"SELECT hostname, path, username, password FROM cifs_shares WHERE id = ?",
|
||||
"SELECT hostname, path, username, password FROM cifs_shares WHERE id = $1",
|
||||
id
|
||||
)
|
||||
.fetch_one(secrets)
|
||||
@@ -170,14 +170,13 @@ where
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn list<Ex>(secrets: &mut Ex) -> Result<Vec<(u32, CifsBackupTarget)>, Error>
|
||||
pub async fn list<Ex>(secrets: &mut Ex) -> Result<Vec<(i32, CifsBackupTarget)>, Error>
|
||||
where
|
||||
for<'a> &'a mut Ex: Executor<'a, Database = Sqlite>,
|
||||
for<'a> &'a mut Ex: Executor<'a, Database = Postgres>,
|
||||
{
|
||||
let mut records = sqlx::query!(
|
||||
"SELECT id AS \"id: u32\", hostname, path, username, password FROM cifs_shares"
|
||||
)
|
||||
.fetch_many(secrets);
|
||||
let mut records =
|
||||
sqlx::query!("SELECT id, hostname, path, username, password FROM cifs_shares")
|
||||
.fetch_many(secrets);
|
||||
|
||||
let mut cifs = Vec::new();
|
||||
while let Some(query_result) = records.try_next().await? {
|
||||
|
||||
@@ -10,7 +10,7 @@ use digest::OutputSizeUser;
|
||||
use rpc_toolkit::command;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::Sha256;
|
||||
use sqlx::{Executor, Sqlite};
|
||||
use sqlx::{Executor, Postgres};
|
||||
use tracing::instrument;
|
||||
|
||||
use self::cifs::CifsBackupTarget;
|
||||
@@ -18,7 +18,7 @@ use crate::context::RpcContext;
|
||||
use crate::disk::mount::backup::BackupMountGuard;
|
||||
use crate::disk::mount::filesystem::block_dev::BlockDev;
|
||||
use crate::disk::mount::filesystem::cifs::Cifs;
|
||||
use crate::disk::mount::filesystem::{FileSystem, MountType, ReadOnly};
|
||||
use crate::disk::mount::filesystem::{FileSystem, MountType, ReadWrite};
|
||||
use crate::disk::mount::guard::TmpMountGuard;
|
||||
use crate::disk::util::PartitionInfo;
|
||||
use crate::s9pk::manifest::PackageId;
|
||||
@@ -45,12 +45,12 @@ pub enum BackupTarget {
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum BackupTargetId {
|
||||
Disk { logicalname: PathBuf },
|
||||
Cifs { id: u32 },
|
||||
Cifs { id: i32 },
|
||||
}
|
||||
impl BackupTargetId {
|
||||
pub async fn load<Ex>(self, secrets: &mut Ex) -> Result<BackupTargetFS, Error>
|
||||
where
|
||||
for<'a> &'a mut Ex: Executor<'a, Database = Sqlite>,
|
||||
for<'a> &'a mut Ex: Executor<'a, Database = Postgres>,
|
||||
{
|
||||
Ok(match self {
|
||||
BackupTargetId::Disk { logicalname } => {
|
||||
@@ -134,16 +134,16 @@ pub fn target() -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// TODO: incorporate reconnect into this response as well
|
||||
#[command(display(display_serializable))]
|
||||
pub async fn list(
|
||||
#[context] ctx: RpcContext,
|
||||
) -> Result<BTreeMap<BackupTargetId, BackupTarget>, Error> {
|
||||
let mut sql_handle = ctx.secret_store.acquire().await?;
|
||||
let (disks_res, cifs) =
|
||||
tokio::try_join!(crate::disk::util::list(), cifs::list(&mut sql_handle),)?;
|
||||
let (disks_res, cifs) = tokio::try_join!(
|
||||
crate::disk::util::list(&ctx.os_partitions),
|
||||
cifs::list(&mut sql_handle),
|
||||
)?;
|
||||
Ok(disks_res
|
||||
.disks
|
||||
.into_iter()
|
||||
.flat_map(|mut disk| {
|
||||
std::mem::take(&mut disk.partitions)
|
||||
@@ -219,11 +219,11 @@ fn display_backup_info(info: BackupInfo, matches: &ArgMatches) {
|
||||
];
|
||||
table.add_row(row);
|
||||
}
|
||||
table.print_tty(false);
|
||||
table.print_tty(false).unwrap();
|
||||
}
|
||||
|
||||
#[command(display(display_backup_info))]
|
||||
#[instrument(skip(ctx, password))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn info(
|
||||
#[context] ctx: RpcContext,
|
||||
#[arg(rename = "target-id")] target_id: BackupTargetId,
|
||||
@@ -234,7 +234,7 @@ pub async fn info(
|
||||
&target_id
|
||||
.load(&mut ctx.secret_store.acquire().await?)
|
||||
.await?,
|
||||
ReadOnly,
|
||||
ReadWrite,
|
||||
)
|
||||
.await?,
|
||||
&password,
|
||||
|
||||
163
backend/src/bin/avahi-alias.rs
Normal file
@@ -0,0 +1,163 @@
|
||||
use avahi_sys::{
|
||||
self, avahi_client_errno, avahi_entry_group_add_service, avahi_entry_group_commit,
|
||||
avahi_strerror, AvahiClient,
|
||||
};
|
||||
|
||||
fn log_str_error(action: &str, e: i32) {
|
||||
unsafe {
|
||||
let e_str = avahi_strerror(e);
|
||||
eprintln!(
|
||||
"Could not {}: {:?}",
|
||||
action,
|
||||
std::ffi::CStr::from_ptr(e_str)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let aliases: Vec<_> = std::env::args().skip(1).collect();
|
||||
unsafe {
|
||||
let simple_poll = avahi_sys::avahi_simple_poll_new();
|
||||
let poll = avahi_sys::avahi_simple_poll_get(simple_poll);
|
||||
let mut box_err = Box::pin(0 as i32);
|
||||
let err_c: *mut i32 = box_err.as_mut().get_mut();
|
||||
let avahi_client = avahi_sys::avahi_client_new(
|
||||
poll,
|
||||
avahi_sys::AvahiClientFlags::AVAHI_CLIENT_NO_FAIL,
|
||||
Some(client_callback),
|
||||
std::ptr::null_mut(),
|
||||
err_c,
|
||||
);
|
||||
if avahi_client == std::ptr::null_mut::<AvahiClient>() {
|
||||
log_str_error("create Avahi client", *box_err);
|
||||
panic!("Failed to create Avahi Client");
|
||||
}
|
||||
let group = avahi_sys::avahi_entry_group_new(
|
||||
avahi_client,
|
||||
Some(entry_group_callback),
|
||||
std::ptr::null_mut(),
|
||||
);
|
||||
if group == std::ptr::null_mut() {
|
||||
log_str_error("create Avahi entry group", avahi_client_errno(avahi_client));
|
||||
panic!("Failed to create Avahi Entry Group");
|
||||
}
|
||||
let mut hostname_buf = vec![0];
|
||||
let hostname_raw = avahi_sys::avahi_client_get_host_name_fqdn(avahi_client);
|
||||
hostname_buf.extend_from_slice(std::ffi::CStr::from_ptr(hostname_raw).to_bytes_with_nul());
|
||||
let buflen = hostname_buf.len();
|
||||
debug_assert!(hostname_buf.ends_with(b".local\0"));
|
||||
debug_assert!(!hostname_buf[..(buflen - 7)].contains(&b'.'));
|
||||
// assume fixed length prefix on hostname due to local address
|
||||
hostname_buf[0] = (buflen - 8) as u8; // set the prefix length to len - 8 (leading byte, .local, nul) for the main address
|
||||
hostname_buf[buflen - 7] = 5; // set the prefix length to 5 for "local"
|
||||
let mut res;
|
||||
let http_tcp_cstr =
|
||||
std::ffi::CString::new("_http._tcp").expect("Could not cast _http._tcp to c string");
|
||||
res = avahi_entry_group_add_service(
|
||||
group,
|
||||
avahi_sys::AVAHI_IF_UNSPEC,
|
||||
avahi_sys::AVAHI_PROTO_UNSPEC,
|
||||
avahi_sys::AvahiPublishFlags_AVAHI_PUBLISH_USE_MULTICAST,
|
||||
hostname_raw,
|
||||
http_tcp_cstr.as_ptr(),
|
||||
std::ptr::null(),
|
||||
std::ptr::null(),
|
||||
443,
|
||||
// below is a secret final argument that the type signature of this function does not tell you that it
|
||||
// needs. This is because the C lib function takes a variable number of final arguments indicating the
|
||||
// desired TXT records to add to this service entry. The way it decides when to stop taking arguments
|
||||
// from the stack and dereferencing them is when it finds a null pointer...because fuck you, that's why.
|
||||
// The consequence of this is that forgetting this last argument will cause segfaults or other undefined
|
||||
// behavior. Welcome back to the stone age motherfucker.
|
||||
std::ptr::null::<libc::c_char>(),
|
||||
);
|
||||
if res < avahi_sys::AVAHI_OK {
|
||||
log_str_error("add service to Avahi entry group", res);
|
||||
panic!("Failed to load Avahi services");
|
||||
}
|
||||
eprintln!("Published {:?}", std::ffi::CStr::from_ptr(hostname_raw));
|
||||
for alias in aliases {
|
||||
let lan_address = alias + ".local";
|
||||
let lan_address_ptr = std::ffi::CString::new(lan_address)
|
||||
.expect("Could not cast lan address to c string");
|
||||
res = avahi_sys::avahi_entry_group_add_record(
|
||||
group,
|
||||
avahi_sys::AVAHI_IF_UNSPEC,
|
||||
avahi_sys::AVAHI_PROTO_UNSPEC,
|
||||
avahi_sys::AvahiPublishFlags_AVAHI_PUBLISH_USE_MULTICAST
|
||||
| avahi_sys::AvahiPublishFlags_AVAHI_PUBLISH_ALLOW_MULTIPLE,
|
||||
lan_address_ptr.as_ptr(),
|
||||
avahi_sys::AVAHI_DNS_CLASS_IN as u16,
|
||||
avahi_sys::AVAHI_DNS_TYPE_CNAME as u16,
|
||||
avahi_sys::AVAHI_DEFAULT_TTL,
|
||||
hostname_buf.as_ptr().cast(),
|
||||
hostname_buf.len(),
|
||||
);
|
||||
if res < avahi_sys::AVAHI_OK {
|
||||
log_str_error("add CNAME record to Avahi entry group", res);
|
||||
panic!("Failed to load Avahi services");
|
||||
}
|
||||
eprintln!("Published {:?}", lan_address_ptr);
|
||||
}
|
||||
let commit_err = avahi_entry_group_commit(group);
|
||||
if commit_err < avahi_sys::AVAHI_OK {
|
||||
log_str_error("reset Avahi entry group", commit_err);
|
||||
panic!("Failed to load Avahi services: reset");
|
||||
}
|
||||
}
|
||||
std::thread::park()
|
||||
}
|
||||
|
||||
unsafe extern "C" fn entry_group_callback(
|
||||
_group: *mut avahi_sys::AvahiEntryGroup,
|
||||
state: avahi_sys::AvahiEntryGroupState,
|
||||
_userdata: *mut core::ffi::c_void,
|
||||
) {
|
||||
match state {
|
||||
avahi_sys::AvahiEntryGroupState_AVAHI_ENTRY_GROUP_FAILURE => {
|
||||
eprintln!("AvahiCallback: EntryGroupState = AVAHI_ENTRY_GROUP_FAILURE");
|
||||
}
|
||||
avahi_sys::AvahiEntryGroupState_AVAHI_ENTRY_GROUP_COLLISION => {
|
||||
eprintln!("AvahiCallback: EntryGroupState = AVAHI_ENTRY_GROUP_COLLISION");
|
||||
}
|
||||
avahi_sys::AvahiEntryGroupState_AVAHI_ENTRY_GROUP_UNCOMMITED => {
|
||||
eprintln!("AvahiCallback: EntryGroupState = AVAHI_ENTRY_GROUP_UNCOMMITED");
|
||||
}
|
||||
avahi_sys::AvahiEntryGroupState_AVAHI_ENTRY_GROUP_ESTABLISHED => {
|
||||
eprintln!("AvahiCallback: EntryGroupState = AVAHI_ENTRY_GROUP_ESTABLISHED");
|
||||
}
|
||||
avahi_sys::AvahiEntryGroupState_AVAHI_ENTRY_GROUP_REGISTERING => {
|
||||
eprintln!("AvahiCallback: EntryGroupState = AVAHI_ENTRY_GROUP_REGISTERING");
|
||||
}
|
||||
other => {
|
||||
eprintln!("AvahiCallback: EntryGroupState = {}", other);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe extern "C" fn client_callback(
|
||||
_group: *mut avahi_sys::AvahiClient,
|
||||
state: avahi_sys::AvahiClientState,
|
||||
_userdata: *mut core::ffi::c_void,
|
||||
) {
|
||||
match state {
|
||||
avahi_sys::AvahiClientState_AVAHI_CLIENT_FAILURE => {
|
||||
eprintln!("AvahiCallback: ClientState = AVAHI_CLIENT_FAILURE");
|
||||
}
|
||||
avahi_sys::AvahiClientState_AVAHI_CLIENT_S_RUNNING => {
|
||||
eprintln!("AvahiCallback: ClientState = AVAHI_CLIENT_S_RUNNING");
|
||||
}
|
||||
avahi_sys::AvahiClientState_AVAHI_CLIENT_CONNECTING => {
|
||||
eprintln!("AvahiCallback: ClientState = AVAHI_CLIENT_CONNECTING");
|
||||
}
|
||||
avahi_sys::AvahiClientState_AVAHI_CLIENT_S_COLLISION => {
|
||||
eprintln!("AvahiCallback: ClientState = AVAHI_CLIENT_S_COLLISION");
|
||||
}
|
||||
avahi_sys::AvahiClientState_AVAHI_CLIENT_S_REGISTERING => {
|
||||
eprintln!("AvahiCallback: ClientState = AVAHI_CLIENT_S_REGISTERING");
|
||||
}
|
||||
other => {
|
||||
eprintln!("AvahiCallback: ClientState = {}", other);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,83 +1,73 @@
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use embassy::context::rpc::RpcContextConfig;
|
||||
use embassy::context::{DiagnosticContext, SetupContext};
|
||||
use embassy::context::{DiagnosticContext, InstallContext, SetupContext};
|
||||
use embassy::disk::fsck::RepairStrategy;
|
||||
use embassy::disk::main::DEFAULT_PASSWORD;
|
||||
use embassy::disk::REPAIR_DISK_PATH;
|
||||
use embassy::hostname::get_product_key;
|
||||
use embassy::init::STANDBY_MODE_PATH;
|
||||
use embassy::middleware::cors::cors;
|
||||
use embassy::middleware::diagnostic::diagnostic;
|
||||
use embassy::middleware::encrypt::encrypt;
|
||||
#[cfg(feature = "avahi")]
|
||||
use embassy::net::mdns::MdnsController;
|
||||
use embassy::net::web_server::WebServer;
|
||||
use embassy::shutdown::Shutdown;
|
||||
use embassy::sound::CHIME;
|
||||
use embassy::util::logger::EmbassyLogger;
|
||||
use embassy::util::Invoke;
|
||||
use embassy::{Error, ErrorKind, ResultExt};
|
||||
use http::StatusCode;
|
||||
use rpc_toolkit::rpc_server;
|
||||
use embassy::{Error, ErrorKind, ResultExt, IS_RASPBERRY_PI};
|
||||
use tokio::process::Command;
|
||||
use tracing::instrument;
|
||||
|
||||
fn status_fn(_: i32) -> StatusCode {
|
||||
StatusCode::OK
|
||||
}
|
||||
#[instrument(skip_all)]
|
||||
async fn setup_or_init(cfg_path: Option<PathBuf>) -> Result<(), Error> {
|
||||
if tokio::fs::metadata("/cdrom").await.is_ok() {
|
||||
let ctx = InstallContext::init(cfg_path).await?;
|
||||
|
||||
let server = WebServer::install(([0, 0, 0, 0], 80).into(), ctx.clone()).await?;
|
||||
|
||||
#[instrument]
|
||||
async fn setup_or_init(cfg_path: Option<&str>) -> Result<(), Error> {
|
||||
if tokio::fs::metadata("/embassy-os/disk.guid").await.is_err() {
|
||||
#[cfg(feature = "avahi")]
|
||||
let _mdns = MdnsController::init();
|
||||
tokio::fs::write(
|
||||
"/etc/nginx/sites-available/default",
|
||||
include_str!("../nginx/setup-wizard.conf"),
|
||||
)
|
||||
.await
|
||||
.with_ctx(|_| {
|
||||
(
|
||||
embassy::ErrorKind::Filesystem,
|
||||
"/etc/nginx/sites-available/default",
|
||||
)
|
||||
})?;
|
||||
Command::new("systemctl")
|
||||
.arg("reload")
|
||||
.arg("nginx")
|
||||
.invoke(embassy::ErrorKind::Nginx)
|
||||
.await?;
|
||||
let ctx = SetupContext::init(cfg_path).await?;
|
||||
let keysource_ctx = ctx.clone();
|
||||
let keysource = move || {
|
||||
let ctx = keysource_ctx.clone();
|
||||
async move { ctx.product_key().await }
|
||||
};
|
||||
let encrypt = encrypt(keysource);
|
||||
tokio::time::sleep(Duration::from_secs(1)).await; // let the record state that I hate this
|
||||
CHIME.play().await?;
|
||||
rpc_server!({
|
||||
command: embassy::setup_api,
|
||||
context: ctx.clone(),
|
||||
status: status_fn,
|
||||
middleware: [
|
||||
cors,
|
||||
encrypt,
|
||||
]
|
||||
})
|
||||
.with_graceful_shutdown({
|
||||
let mut shutdown = ctx.shutdown.subscribe();
|
||||
async move {
|
||||
shutdown.recv().await.expect("context dropped");
|
||||
}
|
||||
})
|
||||
|
||||
ctx.shutdown
|
||||
.subscribe()
|
||||
.recv()
|
||||
.await
|
||||
.expect("context dropped");
|
||||
|
||||
server.shutdown().await;
|
||||
|
||||
Command::new("reboot")
|
||||
.invoke(embassy::ErrorKind::Unknown)
|
||||
.await?;
|
||||
} else if tokio::fs::metadata("/media/embassy/config/disk.guid")
|
||||
.await
|
||||
.with_kind(embassy::ErrorKind::Network)?;
|
||||
.is_err()
|
||||
{
|
||||
let ctx = SetupContext::init(cfg_path).await?;
|
||||
|
||||
let server = WebServer::setup(([0, 0, 0, 0], 80).into(), ctx.clone()).await?;
|
||||
|
||||
tokio::time::sleep(Duration::from_secs(1)).await; // let the record state that I hate this
|
||||
CHIME.play().await?;
|
||||
ctx.shutdown
|
||||
.subscribe()
|
||||
.recv()
|
||||
.await
|
||||
.expect("context dropped");
|
||||
|
||||
server.shutdown().await;
|
||||
|
||||
tokio::task::yield_now().await;
|
||||
if let Err(e) = Command::new("killall")
|
||||
.arg("firefox-esr")
|
||||
.invoke(ErrorKind::NotFound)
|
||||
.await
|
||||
{
|
||||
tracing::error!("Failed to kill kiosk: {}", e);
|
||||
tracing::debug!("{:?}", e);
|
||||
}
|
||||
} else {
|
||||
let cfg = RpcContextConfig::load(cfg_path).await?;
|
||||
let guid_string = tokio::fs::read_to_string("/embassy-os/disk.guid") // unique identifier for volume group - keeps track of the disk that goes with your embassy
|
||||
let guid_string = tokio::fs::read_to_string("/media/embassy/config/disk.guid") // unique identifier for volume group - keeps track of the disk that goes with your embassy
|
||||
.await?;
|
||||
let guid = guid_string.trim();
|
||||
let requires_reboot = embassy::disk::main::import(
|
||||
@@ -103,7 +93,7 @@ async fn setup_or_init(cfg_path: Option<&str>) -> Result<(), Error> {
|
||||
.await?;
|
||||
}
|
||||
tracing::info!("Loaded Disk");
|
||||
embassy::init::init(&cfg, &get_product_key().await?).await?;
|
||||
embassy::init::init(&cfg).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -127,9 +117,9 @@ async fn run_script_if_exists<P: AsRef<Path>>(path: P) {
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
async fn inner_main(cfg_path: Option<&str>) -> Result<Option<Shutdown>, Error> {
|
||||
if tokio::fs::metadata(STANDBY_MODE_PATH).await.is_ok() {
|
||||
#[instrument(skip_all)]
|
||||
async fn inner_main(cfg_path: Option<PathBuf>) -> Result<Option<Shutdown>, Error> {
|
||||
if *IS_RASPBERRY_PI && tokio::fs::metadata(STANDBY_MODE_PATH).await.is_ok() {
|
||||
tokio::fs::remove_file(STANDBY_MODE_PATH).await?;
|
||||
Command::new("sync").invoke(ErrorKind::Filesystem).await?;
|
||||
embassy::sound::SHUTDOWN.play().await?;
|
||||
@@ -138,36 +128,22 @@ async fn inner_main(cfg_path: Option<&str>) -> Result<Option<Shutdown>, Error> {
|
||||
|
||||
embassy::sound::BEP.play().await?;
|
||||
|
||||
run_script_if_exists("/embassy-os/preinit.sh").await;
|
||||
run_script_if_exists("/media/embassy/config/preinit.sh").await;
|
||||
|
||||
let res = if let Err(e) = setup_or_init(cfg_path).await {
|
||||
async {
|
||||
let res = if let Err(e) = setup_or_init(cfg_path.clone()).await {
|
||||
async move {
|
||||
tracing::error!("{}", e.source);
|
||||
tracing::debug!("{}", e.source);
|
||||
embassy::sound::BEETHOVEN.play().await?;
|
||||
#[cfg(feature = "avahi")]
|
||||
let _mdns = MdnsController::init();
|
||||
tokio::fs::write(
|
||||
"/etc/nginx/sites-available/default",
|
||||
include_str!("../nginx/diagnostic-ui.conf"),
|
||||
)
|
||||
.await
|
||||
.with_ctx(|_| {
|
||||
(
|
||||
embassy::ErrorKind::Filesystem,
|
||||
"/etc/nginx/sites-available/default",
|
||||
)
|
||||
})?;
|
||||
Command::new("systemctl")
|
||||
.arg("reload")
|
||||
.arg("nginx")
|
||||
.invoke(embassy::ErrorKind::Nginx)
|
||||
.await?;
|
||||
|
||||
let ctx = DiagnosticContext::init(
|
||||
cfg_path,
|
||||
if tokio::fs::metadata("/embassy-os/disk.guid").await.is_ok() {
|
||||
if tokio::fs::metadata("/media/embassy/config/disk.guid")
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
Some(Arc::new(
|
||||
tokio::fs::read_to_string("/embassy-os/disk.guid") // unique identifier for volume group - keeps track of the disk that goes with your embassy
|
||||
tokio::fs::read_to_string("/media/embassy/config/disk.guid") // unique identifier for volume group - keeps track of the disk that goes with your embassy
|
||||
.await?
|
||||
.trim()
|
||||
.to_owned(),
|
||||
@@ -178,44 +154,27 @@ async fn inner_main(cfg_path: Option<&str>) -> Result<Option<Shutdown>, Error> {
|
||||
e,
|
||||
)
|
||||
.await?;
|
||||
let mut shutdown_recv = ctx.shutdown.subscribe();
|
||||
rpc_server!({
|
||||
command: embassy::diagnostic_api,
|
||||
context: ctx.clone(),
|
||||
status: status_fn,
|
||||
middleware: [
|
||||
cors,
|
||||
diagnostic,
|
||||
]
|
||||
})
|
||||
.with_graceful_shutdown({
|
||||
let mut shutdown = ctx.shutdown.subscribe();
|
||||
async move {
|
||||
shutdown.recv().await.expect("context dropped");
|
||||
}
|
||||
})
|
||||
.await
|
||||
.with_kind(embassy::ErrorKind::Network)?;
|
||||
|
||||
Ok::<_, Error>(
|
||||
shutdown_recv
|
||||
.recv()
|
||||
.await
|
||||
.with_kind(embassy::ErrorKind::Network)?,
|
||||
)
|
||||
let server = WebServer::diagnostic(([0, 0, 0, 0], 80).into(), ctx.clone()).await?;
|
||||
|
||||
let shutdown = ctx.shutdown.subscribe().recv().await.unwrap();
|
||||
|
||||
server.shutdown().await;
|
||||
|
||||
Ok(shutdown)
|
||||
}
|
||||
.await
|
||||
} else {
|
||||
Ok(None)
|
||||
};
|
||||
|
||||
run_script_if_exists("/embassy-os/postinit.sh").await;
|
||||
run_script_if_exists("/media/embassy/config/postinit.sh").await;
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let matches = clap::App::new("embassyd")
|
||||
let matches = clap::App::new("embassy-init")
|
||||
.arg(
|
||||
clap::Arg::with_name("config")
|
||||
.short('c')
|
||||
@@ -226,7 +185,7 @@ fn main() {
|
||||
|
||||
EmbassyLogger::init();
|
||||
|
||||
let cfg_path = matches.value_of("config");
|
||||
let cfg_path = matches.value_of("config").map(|p| Path::new(p).to_owned());
|
||||
let res = {
|
||||
let rt = tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
|
||||
@@ -1,56 +1,33 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use color_eyre::eyre::eyre;
|
||||
use embassy::context::{DiagnosticContext, RpcContext};
|
||||
use embassy::core::rpc_continuations::RequestGuid;
|
||||
use embassy::db::subscribe;
|
||||
use embassy::middleware::auth::auth;
|
||||
use embassy::middleware::cors::cors;
|
||||
use embassy::middleware::diagnostic::diagnostic;
|
||||
#[cfg(feature = "avahi")]
|
||||
use embassy::net::mdns::MdnsController;
|
||||
use embassy::net::tor::tor_health_check;
|
||||
use embassy::net::web_server::WebServer;
|
||||
use embassy::shutdown::Shutdown;
|
||||
use embassy::system::launch_metrics_task;
|
||||
use embassy::util::logger::EmbassyLogger;
|
||||
use embassy::util::{daemon, Invoke};
|
||||
use embassy::{static_server, Error, ErrorKind, ResultExt};
|
||||
use embassy::{Error, ErrorKind, ResultExt};
|
||||
use futures::{FutureExt, TryFutureExt};
|
||||
use reqwest::{Client, Proxy};
|
||||
use rpc_toolkit::hyper::{Body, Response, Server, StatusCode};
|
||||
use rpc_toolkit::rpc_server;
|
||||
use tokio::process::Command;
|
||||
use tokio::signal::unix::signal;
|
||||
use tracing::instrument;
|
||||
|
||||
fn status_fn(_: i32) -> StatusCode {
|
||||
StatusCode::OK
|
||||
}
|
||||
|
||||
fn err_to_500(e: Error) -> Response<Body> {
|
||||
tracing::error!("{}", e);
|
||||
tracing::debug!("{:?}", e);
|
||||
Response::builder()
|
||||
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
.body(Body::empty())
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
async fn inner_main(cfg_path: Option<&str>) -> Result<Option<Shutdown>, Error> {
|
||||
let (rpc_ctx, shutdown) = {
|
||||
embassy::hostname::sync_hostname().await?;
|
||||
#[instrument(skip_all)]
|
||||
async fn inner_main(cfg_path: Option<PathBuf>) -> Result<Option<Shutdown>, Error> {
|
||||
let (rpc_ctx, server, shutdown) = {
|
||||
let rpc_ctx = RpcContext::init(
|
||||
cfg_path,
|
||||
Arc::new(
|
||||
tokio::fs::read_to_string("/embassy-os/disk.guid") // unique identifier for volume group - keeps track of the disk that goes with your embassy
|
||||
tokio::fs::read_to_string("/media/embassy/config/disk.guid") // unique identifier for volume group - keeps track of the disk that goes with your embassy
|
||||
.await?
|
||||
.trim()
|
||||
.to_owned(),
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
embassy::hostname::sync_hostname(&*rpc_ctx.account.read().await).await?;
|
||||
let server = WebServer::main(([0, 0, 0, 0], 80).into(), rpc_ctx.clone()).await?;
|
||||
|
||||
let mut shutdown_recv = rpc_ctx.shutdown.subscribe();
|
||||
|
||||
let sig_handler_ctx = rpc_ctx.clone();
|
||||
@@ -66,7 +43,7 @@ async fn inner_main(cfg_path: Option<&str>) -> Result<Option<Shutdown>, Error> {
|
||||
.map(|s| {
|
||||
async move {
|
||||
signal(*s)
|
||||
.expect(&format!("register {:?} handler", s))
|
||||
.unwrap_or_else(|_| panic!("register {:?} handler", s))
|
||||
.recv()
|
||||
.await
|
||||
}
|
||||
@@ -81,29 +58,6 @@ async fn inner_main(cfg_path: Option<&str>) -> Result<Option<Shutdown>, Error> {
|
||||
.expect("send shutdown signal");
|
||||
});
|
||||
|
||||
let mut db = rpc_ctx.db.handle();
|
||||
let receipts = embassy::context::rpc::RpcSetNginxReceipts::new(&mut db).await?;
|
||||
|
||||
rpc_ctx.set_nginx_conf(&mut db, receipts).await?;
|
||||
drop(db);
|
||||
let auth = auth(rpc_ctx.clone());
|
||||
let ctx = rpc_ctx.clone();
|
||||
let server = rpc_server!({
|
||||
command: embassy::main_api,
|
||||
context: ctx,
|
||||
status: status_fn,
|
||||
middleware: [
|
||||
cors,
|
||||
auth,
|
||||
]
|
||||
})
|
||||
.with_graceful_shutdown({
|
||||
let mut shutdown = rpc_ctx.shutdown.subscribe();
|
||||
async move {
|
||||
shutdown.recv().await.expect("context dropped");
|
||||
}
|
||||
});
|
||||
|
||||
let metrics_ctx = rpc_ctx.clone();
|
||||
let metrics_task = tokio::spawn(async move {
|
||||
launch_metrics_task(&metrics_ctx.metrics_cache, || {
|
||||
@@ -112,174 +66,32 @@ async fn inner_main(cfg_path: Option<&str>) -> Result<Option<Shutdown>, Error> {
|
||||
.await
|
||||
});
|
||||
|
||||
let rev_cache_ctx = rpc_ctx.clone();
|
||||
let revision_cache_task = tokio::spawn(async move {
|
||||
let mut sub = rev_cache_ctx.db.subscribe();
|
||||
let mut shutdown = rev_cache_ctx.shutdown.subscribe();
|
||||
loop {
|
||||
let rev = match tokio::select! {
|
||||
a = sub.recv() => a,
|
||||
_ = shutdown.recv() => break,
|
||||
} {
|
||||
Ok(a) => a,
|
||||
Err(_) => {
|
||||
rev_cache_ctx.revision_cache.write().await.truncate(0);
|
||||
continue;
|
||||
}
|
||||
}; // TODO: handle falling behind
|
||||
let mut cache = rev_cache_ctx.revision_cache.write().await;
|
||||
cache.push_back(rev);
|
||||
if cache.len() > rev_cache_ctx.revision_cache_size {
|
||||
cache.pop_front();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let ws_ctx = rpc_ctx.clone();
|
||||
let ws_server = {
|
||||
let builder = Server::bind(&ws_ctx.bind_ws);
|
||||
|
||||
let make_svc = ::rpc_toolkit::hyper::service::make_service_fn(move |_| {
|
||||
let ctx = ws_ctx.clone();
|
||||
async move {
|
||||
Ok::<_, ::rpc_toolkit::hyper::Error>(::rpc_toolkit::hyper::service::service_fn(
|
||||
move |req| {
|
||||
let ctx = ctx.clone();
|
||||
async move {
|
||||
tracing::debug!("Request to {}", req.uri().path());
|
||||
match req.uri().path() {
|
||||
"/ws/db" => {
|
||||
Ok(subscribe(ctx, req).await.unwrap_or_else(err_to_500))
|
||||
}
|
||||
path if path.starts_with("/rest/rpc/") => {
|
||||
match RequestGuid::from(
|
||||
path.strip_prefix("/rest/rpc/").unwrap(),
|
||||
) {
|
||||
None => {
|
||||
tracing::debug!("No Guid Path");
|
||||
Response::builder()
|
||||
.status(StatusCode::BAD_REQUEST)
|
||||
.body(Body::empty())
|
||||
}
|
||||
Some(guid) => {
|
||||
match ctx
|
||||
.rpc_stream_continuations
|
||||
.lock()
|
||||
.await
|
||||
.remove(&guid)
|
||||
{
|
||||
None => Response::builder()
|
||||
.status(StatusCode::NOT_FOUND)
|
||||
.body(Body::empty()),
|
||||
Some(cont) => match (cont.handler)(req).await {
|
||||
Ok(r) => Ok(r),
|
||||
Err(e) => Response::builder()
|
||||
.status(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
.body(Body::from(format!("{}", e))),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => Response::builder()
|
||||
.status(StatusCode::NOT_FOUND)
|
||||
.body(Body::empty()),
|
||||
}
|
||||
}
|
||||
},
|
||||
))
|
||||
}
|
||||
});
|
||||
builder.serve(make_svc)
|
||||
}
|
||||
.with_graceful_shutdown({
|
||||
let mut shutdown = rpc_ctx.shutdown.subscribe();
|
||||
async move {
|
||||
shutdown.recv().await.expect("context dropped");
|
||||
}
|
||||
});
|
||||
|
||||
let file_server_ctx = rpc_ctx.clone();
|
||||
let file_server = {
|
||||
static_server::init(file_server_ctx, {
|
||||
let mut shutdown = rpc_ctx.shutdown.subscribe();
|
||||
async move {
|
||||
shutdown.recv().await.expect("context dropped");
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let tor_health_ctx = rpc_ctx.clone();
|
||||
let tor_client = Client::builder()
|
||||
.proxy(
|
||||
Proxy::http(format!(
|
||||
"socks5h://{}:{}",
|
||||
rpc_ctx.tor_socks.ip(),
|
||||
rpc_ctx.tor_socks.port()
|
||||
))
|
||||
.with_kind(crate::ErrorKind::Network)?,
|
||||
)
|
||||
.build()
|
||||
.with_kind(crate::ErrorKind::Network)?;
|
||||
let tor_health_daemon = daemon(
|
||||
move || {
|
||||
let ctx = tor_health_ctx.clone();
|
||||
let client = tor_client.clone();
|
||||
async move { tor_health_check(&client, &ctx.net_controller.tor).await }
|
||||
},
|
||||
Duration::from_secs(300),
|
||||
rpc_ctx.shutdown.subscribe(),
|
||||
);
|
||||
|
||||
embassy::sound::CHIME.play().await?;
|
||||
|
||||
futures::try_join!(
|
||||
server
|
||||
.map_err(|e| Error::new(e, ErrorKind::Network))
|
||||
.map_ok(|_| tracing::debug!("RPC Server Shutdown")),
|
||||
metrics_task
|
||||
.map_err(|e| Error::new(
|
||||
metrics_task
|
||||
.map_err(|e| {
|
||||
Error::new(
|
||||
eyre!("{}", e).wrap_err("Metrics daemon panicked!"),
|
||||
ErrorKind::Unknown
|
||||
))
|
||||
.map_ok(|_| tracing::debug!("Metrics daemon Shutdown")),
|
||||
revision_cache_task
|
||||
.map_err(|e| Error::new(
|
||||
eyre!("{}", e).wrap_err("Revision Cache daemon panicked!"),
|
||||
ErrorKind::Unknown
|
||||
))
|
||||
.map_ok(|_| tracing::debug!("Revision Cache daemon Shutdown")),
|
||||
ws_server
|
||||
.map_err(|e| Error::new(e, ErrorKind::Network))
|
||||
.map_ok(|_| tracing::debug!("WebSocket Server Shutdown")),
|
||||
file_server
|
||||
.map_err(|e| Error::new(e, ErrorKind::Network))
|
||||
.map_ok(|_| tracing::debug!("Static File Server Shutdown")),
|
||||
tor_health_daemon
|
||||
.map_err(|e| Error::new(
|
||||
e.wrap_err("Tor Health daemon panicked!"),
|
||||
ErrorKind::Unknown
|
||||
))
|
||||
.map_ok(|_| tracing::debug!("Tor Health daemon Shutdown")),
|
||||
)?;
|
||||
ErrorKind::Unknown,
|
||||
)
|
||||
})
|
||||
.map_ok(|_| tracing::debug!("Metrics daemon Shutdown"))
|
||||
.await?;
|
||||
|
||||
let mut shutdown = shutdown_recv
|
||||
let shutdown = shutdown_recv
|
||||
.recv()
|
||||
.await
|
||||
.with_kind(crate::ErrorKind::Unknown)?;
|
||||
|
||||
sig_handler.abort();
|
||||
|
||||
if let Some(shutdown) = &mut shutdown {
|
||||
drop(shutdown.db_handle.take());
|
||||
}
|
||||
|
||||
(rpc_ctx, shutdown)
|
||||
(rpc_ctx, server, shutdown)
|
||||
};
|
||||
server.shutdown().await;
|
||||
rpc_ctx.shutdown().await?;
|
||||
|
||||
tracing::info!("RPC Context is dropped");
|
||||
|
||||
Ok(shutdown)
|
||||
}
|
||||
|
||||
@@ -295,7 +107,7 @@ fn main() {
|
||||
|
||||
EmbassyLogger::init();
|
||||
|
||||
let cfg_path = matches.value_of("config");
|
||||
let cfg_path = matches.value_of("config").map(|p| Path::new(p).to_owned());
|
||||
|
||||
let res = {
|
||||
let rt = tokio::runtime::Builder::new_multi_thread()
|
||||
@@ -303,36 +115,21 @@ fn main() {
|
||||
.build()
|
||||
.expect("failed to initialize runtime");
|
||||
rt.block_on(async {
|
||||
match inner_main(cfg_path).await {
|
||||
match inner_main(cfg_path.clone()).await {
|
||||
Ok(a) => Ok(a),
|
||||
Err(e) => {
|
||||
(|| async {
|
||||
async {
|
||||
tracing::error!("{}", e.source);
|
||||
tracing::debug!("{:?}", e.source);
|
||||
embassy::sound::BEETHOVEN.play().await?;
|
||||
#[cfg(feature = "avahi")]
|
||||
let _mdns = MdnsController::init();
|
||||
tokio::fs::write(
|
||||
"/etc/nginx/sites-available/default",
|
||||
include_str!("../nginx/diagnostic-ui.conf"),
|
||||
)
|
||||
.await
|
||||
.with_ctx(|_| {
|
||||
(
|
||||
embassy::ErrorKind::Filesystem,
|
||||
"/etc/nginx/sites-available/default",
|
||||
)
|
||||
})?;
|
||||
Command::new("systemctl")
|
||||
.arg("reload")
|
||||
.arg("nginx")
|
||||
.invoke(embassy::ErrorKind::Nginx)
|
||||
.await?;
|
||||
let ctx = DiagnosticContext::init(
|
||||
cfg_path,
|
||||
if tokio::fs::metadata("/embassy-os/disk.guid").await.is_ok() {
|
||||
if tokio::fs::metadata("/media/embassy/config/disk.guid")
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
Some(Arc::new(
|
||||
tokio::fs::read_to_string("/embassy-os/disk.guid") // unique identifier for volume group - keeps track of the disk that goes with your embassy
|
||||
tokio::fs::read_to_string("/media/embassy/config/disk.guid") // unique identifier for volume group - keeps track of the disk that goes with your embassy
|
||||
.await?
|
||||
.trim()
|
||||
.to_owned(),
|
||||
@@ -343,26 +140,19 @@ fn main() {
|
||||
e,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let server =
|
||||
WebServer::diagnostic(([0, 0, 0, 0], 80).into(), ctx.clone()).await?;
|
||||
|
||||
let mut shutdown = ctx.shutdown.subscribe();
|
||||
rpc_server!({
|
||||
command: embassy::diagnostic_api,
|
||||
context: ctx.clone(),
|
||||
status: status_fn,
|
||||
middleware: [
|
||||
cors,
|
||||
diagnostic,
|
||||
]
|
||||
})
|
||||
.with_graceful_shutdown({
|
||||
let mut shutdown = ctx.shutdown.subscribe();
|
||||
async move {
|
||||
shutdown.recv().await.expect("context dropped");
|
||||
}
|
||||
})
|
||||
.await
|
||||
.with_kind(embassy::ErrorKind::Network)?;
|
||||
Ok::<_, Error>(shutdown.recv().await.with_kind(crate::ErrorKind::Unknown)?)
|
||||
})()
|
||||
|
||||
let shutdown =
|
||||
shutdown.recv().await.with_kind(crate::ErrorKind::Unknown)?;
|
||||
|
||||
server.shutdown().await;
|
||||
|
||||
Ok::<_, Error>(shutdown)
|
||||
}
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use color_eyre::eyre::eyre;
|
||||
use models::ImageId;
|
||||
use nix::sys::signal::Signal;
|
||||
use patch_db::HasModel;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -9,7 +10,7 @@ use tracing::instrument;
|
||||
use super::{Config, ConfigSpec};
|
||||
use crate::context::RpcContext;
|
||||
use crate::dependencies::Dependencies;
|
||||
use crate::id::ImageId;
|
||||
use crate::procedure::docker::DockerContainers;
|
||||
use crate::procedure::{PackageProcedure, ProcedureName};
|
||||
use crate::s9pk::manifest::PackageId;
|
||||
use crate::status::health_check::HealthCheckId;
|
||||
@@ -30,22 +31,23 @@ pub struct ConfigActions {
|
||||
pub set: PackageProcedure,
|
||||
}
|
||||
impl ConfigActions {
|
||||
#[instrument]
|
||||
#[instrument(skip_all)]
|
||||
pub fn validate(
|
||||
&self,
|
||||
container: &Option<DockerContainers>,
|
||||
eos_version: &Version,
|
||||
volumes: &Volumes,
|
||||
image_ids: &BTreeSet<ImageId>,
|
||||
) -> Result<(), Error> {
|
||||
self.get
|
||||
.validate(eos_version, volumes, image_ids, true)
|
||||
.validate(container, eos_version, volumes, image_ids, true)
|
||||
.with_ctx(|_| (crate::ErrorKind::ValidateS9pk, "Config Get"))?;
|
||||
self.set
|
||||
.validate(eos_version, volumes, image_ids, true)
|
||||
.validate(container, eos_version, volumes, image_ids, true)
|
||||
.with_ctx(|_| (crate::ErrorKind::ValidateS9pk, "Config Set"))?;
|
||||
Ok(())
|
||||
}
|
||||
#[instrument(skip(ctx))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn get(
|
||||
&self,
|
||||
ctx: &RpcContext,
|
||||
@@ -61,7 +63,6 @@ impl ConfigActions {
|
||||
ProcedureName::GetConfig,
|
||||
volumes,
|
||||
None::<()>,
|
||||
false,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
@@ -70,7 +71,7 @@ impl ConfigActions {
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(ctx))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn set(
|
||||
&self,
|
||||
ctx: &RpcContext,
|
||||
@@ -89,7 +90,6 @@ impl ConfigActions {
|
||||
ProcedureName::SetConfig,
|
||||
volumes,
|
||||
Some(input),
|
||||
false,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -15,13 +15,13 @@ use tracing::instrument;
|
||||
|
||||
use crate::context::RpcContext;
|
||||
use crate::db::model::{CurrentDependencies, CurrentDependencyInfo, CurrentDependents};
|
||||
use crate::db::util::WithRevision;
|
||||
use crate::dependencies::{
|
||||
add_dependent_to_current_dependents_lists, break_transitive, heal_all_dependents_transitive,
|
||||
BreakTransitiveReceipts, BreakageRes, Dependencies, DependencyConfig, DependencyError,
|
||||
DependencyErrors, DependencyReceipt, TaggedDependencyError, TryHealReceipts,
|
||||
};
|
||||
use crate::install::cleanup::{remove_from_current_dependents_lists, UpdateDependencyReceipts};
|
||||
use crate::procedure::docker::DockerContainers;
|
||||
use crate::s9pk::manifest::{Manifest, PackageId};
|
||||
use crate::util::display_none;
|
||||
use crate::util::serde::{display_serializable, parse_stdin_deserializable, IoFormat};
|
||||
@@ -214,7 +214,7 @@ impl ConfigGetReceipts {
|
||||
}
|
||||
|
||||
#[command(display(display_serializable))]
|
||||
#[instrument(skip(ctx))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn get(
|
||||
#[context] ctx: RpcContext,
|
||||
#[parent_data] id: PackageId,
|
||||
@@ -237,9 +237,10 @@ pub async fn get(
|
||||
|
||||
#[command(
|
||||
subcommands(self(set_impl(async, context(RpcContext))), set_dry),
|
||||
display(display_none)
|
||||
display(display_none),
|
||||
metadata(sync_db = true)
|
||||
)]
|
||||
#[instrument]
|
||||
#[instrument(skip_all)]
|
||||
pub fn set(
|
||||
#[parent_data] id: PackageId,
|
||||
#[allow(unused_variables)]
|
||||
@@ -247,9 +248,8 @@ pub fn set(
|
||||
format: Option<IoFormat>,
|
||||
#[arg(long = "timeout")] timeout: Option<crate::util::serde::Duration>,
|
||||
#[arg(stdin, parse(parse_stdin_deserializable))] config: Option<Config>,
|
||||
#[arg(rename = "expire-id", long = "expire-id")] expire_id: Option<String>,
|
||||
) -> Result<(PackageId, Option<Config>, Option<Duration>, Option<String>), Error> {
|
||||
Ok((id, config, timeout.map(|d| *d), expire_id))
|
||||
) -> Result<(PackageId, Option<Config>, Option<Duration>), Error> {
|
||||
Ok((id, config, timeout.map(|d| *d)))
|
||||
}
|
||||
|
||||
/// So, the new locking finds all the possible locks and lifts them up into a bundle of locks.
|
||||
@@ -275,6 +275,7 @@ pub struct ConfigReceipts {
|
||||
pub current_dependencies: LockReceipt<CurrentDependencies, String>,
|
||||
dependency_errors: LockReceipt<DependencyErrors, String>,
|
||||
manifest_dependencies_config: LockReceipt<DependencyConfig, (String, String)>,
|
||||
docker_containers: LockReceipt<DockerContainers, String>,
|
||||
}
|
||||
|
||||
impl ConfigReceipts {
|
||||
@@ -379,6 +380,13 @@ impl ConfigReceipts {
|
||||
.and_then(|x| x.manifest().dependencies().star().config())
|
||||
.make_locker(LockType::Write)
|
||||
.add_to_keys(locks);
|
||||
let docker_containers = crate::db::DatabaseModel::new()
|
||||
.package_data()
|
||||
.star()
|
||||
.installed()
|
||||
.and_then(|x| x.manifest().containers())
|
||||
.make_locker(LockType::Write)
|
||||
.add_to_keys(locks);
|
||||
|
||||
move |skeleton_key| {
|
||||
Ok(Self {
|
||||
@@ -398,21 +406,17 @@ impl ConfigReceipts {
|
||||
current_dependencies: current_dependencies.verify(skeleton_key)?,
|
||||
dependency_errors: dependency_errors.verify(skeleton_key)?,
|
||||
manifest_dependencies_config: manifest_dependencies_config.verify(skeleton_key)?,
|
||||
docker_containers: docker_containers.verify(skeleton_key)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[command(rename = "dry", display(display_serializable))]
|
||||
#[instrument(skip(ctx))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn set_dry(
|
||||
#[context] ctx: RpcContext,
|
||||
#[parent_data] (id, config, timeout, _): (
|
||||
PackageId,
|
||||
Option<Config>,
|
||||
Option<Duration>,
|
||||
Option<String>,
|
||||
),
|
||||
#[parent_data] (id, config, timeout): (PackageId, Option<Config>, Option<Duration>),
|
||||
) -> Result<BreakageRes, Error> {
|
||||
let mut db = ctx.db.handle();
|
||||
let mut tx = db.begin().await?;
|
||||
@@ -436,11 +440,11 @@ pub async fn set_dry(
|
||||
Ok(BreakageRes(breakages))
|
||||
}
|
||||
|
||||
#[instrument(skip(ctx))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn set_impl(
|
||||
ctx: RpcContext,
|
||||
(id, config, timeout, expire_id): (PackageId, Option<Config>, Option<Duration>, Option<String>),
|
||||
) -> Result<WithRevision<()>, Error> {
|
||||
(id, config, timeout): (PackageId, Option<Config>, Option<Duration>),
|
||||
) -> Result<(), Error> {
|
||||
let mut db = ctx.db.handle();
|
||||
let mut tx = db.begin().await?;
|
||||
let mut breakages = BTreeMap::new();
|
||||
@@ -457,13 +461,11 @@ pub async fn set_impl(
|
||||
&locks,
|
||||
)
|
||||
.await?;
|
||||
Ok(WithRevision {
|
||||
response: (),
|
||||
revision: tx.commit(expire_id).await?,
|
||||
})
|
||||
tx.commit().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(ctx, db, receipts))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn configure<'a, Db: DbHandle>(
|
||||
ctx: &RpcContext,
|
||||
db: &'a mut Db,
|
||||
@@ -483,7 +485,7 @@ pub async fn configure<'a, Db: DbHandle>(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(ctx, db, receipts))]
|
||||
#[instrument(skip_all)]
|
||||
pub fn configure_rec<'a, Db: DbHandle>(
|
||||
ctx: &'a RpcContext,
|
||||
db: &'a mut Db,
|
||||
@@ -679,6 +681,8 @@ pub fn configure_rec<'a, Db: DbHandle>(
|
||||
.unwrap_or_default();
|
||||
let next = Value::Object(config.clone());
|
||||
for (dependent, dep_info) in dependents.0.iter().filter(|(dep_id, _)| dep_id != &id) {
|
||||
let dependent_container = receipts.docker_containers.get(db, &dependent).await?;
|
||||
let dependent_container = &dependent_container;
|
||||
// check if config passes dependent check
|
||||
if let Some(cfg) = receipts
|
||||
.manifest_dependencies_config
|
||||
@@ -693,6 +697,7 @@ pub fn configure_rec<'a, Db: DbHandle>(
|
||||
if let Err(error) = cfg
|
||||
.check(
|
||||
ctx,
|
||||
dependent_container,
|
||||
dependent,
|
||||
&manifest.version,
|
||||
&manifest.volumes,
|
||||
@@ -766,7 +771,7 @@ pub fn configure_rec<'a, Db: DbHandle>(
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
#[instrument]
|
||||
#[instrument(skip_all)]
|
||||
pub fn not_found() -> Error {
|
||||
Error::new(eyre!("Could not find"), crate::ErrorKind::Incoherent)
|
||||
}
|
||||
|
||||
@@ -18,13 +18,14 @@ use regex::Regex;
|
||||
use serde::de::{MapAccess, Visitor};
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use serde_json::{Number, Value};
|
||||
use sqlx::SqlitePool;
|
||||
use sqlx::PgPool;
|
||||
|
||||
use super::util::{self, CharSet, NumRange, UniqueBy, STATIC_NULL};
|
||||
use super::{Config, MatchError, NoMatchWithPath, TimeoutError, TypeOf};
|
||||
use crate::config::ConfigurationError;
|
||||
use crate::context::RpcContext;
|
||||
use crate::net::interface::InterfaceId;
|
||||
use crate::net::keys::Key;
|
||||
use crate::s9pk::manifest::{Manifest, PackageId};
|
||||
use crate::Error;
|
||||
|
||||
@@ -1334,6 +1335,8 @@ impl DefaultableWith for ValueSpecString {
|
||||
if &now.elapsed() > timeout {
|
||||
return Err(TimeoutError);
|
||||
}
|
||||
} else {
|
||||
return Ok(Value::String(candidate));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -2050,29 +2053,26 @@ impl TorKeyPointer {
|
||||
async fn deref(
|
||||
&self,
|
||||
source_package: &PackageId,
|
||||
secrets: &SqlitePool,
|
||||
secrets: &PgPool,
|
||||
) -> Result<Value, ConfigurationError> {
|
||||
if &self.package_id != source_package {
|
||||
return Err(ConfigurationError::PermissionDenied(
|
||||
ValueSpecPointer::Package(PackagePointerSpec::TorKey(self.clone())),
|
||||
));
|
||||
}
|
||||
let x = sqlx::query!(
|
||||
"SELECT key FROM tor WHERE package = ? AND interface = ?",
|
||||
*self.package_id,
|
||||
*self.interface
|
||||
let key = Key::for_interface(
|
||||
&mut secrets
|
||||
.acquire()
|
||||
.await
|
||||
.map_err(|e| ConfigurationError::SystemError(e.into()))?,
|
||||
Some((self.package_id.clone(), self.interface.clone())),
|
||||
)
|
||||
.fetch_optional(secrets)
|
||||
.await
|
||||
.map_err(|e| ConfigurationError::SystemError(e.into()))?;
|
||||
if let Some(x) = x {
|
||||
Ok(Value::String(base32::encode(
|
||||
base32::Alphabet::RFC4648 { padding: false },
|
||||
&x.key,
|
||||
)))
|
||||
} else {
|
||||
Ok(Value::Null)
|
||||
}
|
||||
.map_err(ConfigurationError::SystemError)?;
|
||||
Ok(Value::String(base32::encode(
|
||||
base32::Alphabet::RFC4648 { padding: false },
|
||||
&key.tor_key().as_bytes(),
|
||||
)))
|
||||
}
|
||||
}
|
||||
impl fmt::Display for TorKeyPointer {
|
||||
|
||||
@@ -147,6 +147,44 @@ impl serde::ser::Serialize for CharSet {
|
||||
}
|
||||
}
|
||||
|
||||
pub trait MergeWith {
|
||||
fn merge_with(&mut self, other: &serde_json::Value);
|
||||
}
|
||||
|
||||
impl MergeWith for serde_json::Value {
|
||||
fn merge_with(&mut self, other: &serde_json::Value) {
|
||||
use serde_json::Value::Object;
|
||||
if let (Object(orig), Object(ref other)) = (self, other) {
|
||||
for (key, val) in other.into_iter() {
|
||||
match (orig.get_mut(key), val) {
|
||||
(Some(new_orig @ Object(_)), other @ Object(_)) => {
|
||||
new_orig.merge_with(other);
|
||||
}
|
||||
(None, _) => {
|
||||
orig.insert(key.clone(), val.clone());
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_with_tests() {
|
||||
use serde_json::json;
|
||||
|
||||
let mut a = json!(
|
||||
{"a": 1, "c": {"d": "123"}, "i": [1,2,3], "j": {}, "k":[1,2,3], "l": "test"}
|
||||
);
|
||||
a.merge_with(
|
||||
&json!({"a":"a", "b": "b", "c":{"d":"d", "e":"e"}, "f":{"g":"g"}, "h": [1,2,3], "i":"i", "j":[1,2,3], "k":{}}),
|
||||
);
|
||||
assert_eq!(
|
||||
a,
|
||||
json!({"a": 1, "c": {"d": "123", "e":"e"}, "b":"b", "f": {"g":"g"}, "h":[1,2,3], "i":[1,2,3], "j": {}, "k":[1,2,3], "l": "test"})
|
||||
)
|
||||
}
|
||||
pub mod serde_regex {
|
||||
use regex::Regex;
|
||||
use serde::*;
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
use std::fs::File;
|
||||
use std::io::BufReader;
|
||||
use std::net::{Ipv4Addr, SocketAddr};
|
||||
use std::net::Ipv4Addr;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use clap::ArgMatches;
|
||||
use color_eyre::eyre::eyre;
|
||||
use cookie::Cookie;
|
||||
use cookie_store::CookieStore;
|
||||
use josekit::jwk::Jwk;
|
||||
use reqwest::Proxy;
|
||||
use reqwest_cookie_store::CookieStoreMutex;
|
||||
use rpc_toolkit::reqwest::{Client, Url};
|
||||
@@ -15,13 +17,15 @@ use rpc_toolkit::Context;
|
||||
use serde::Deserialize;
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::middleware::auth::LOCAL_AUTH_COOKIE_PATH;
|
||||
use crate::util::config::{load_config_from_paths, local_config_path};
|
||||
use crate::ResultExt;
|
||||
|
||||
use super::setup::CURRENT_SECRET;
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct CliContextConfig {
|
||||
pub bind_rpc: Option<SocketAddr>,
|
||||
pub host: Option<Url>,
|
||||
#[serde(deserialize_with = "crate::util::serde::deserialize_from_str_opt")]
|
||||
#[serde(default)]
|
||||
@@ -64,7 +68,7 @@ const DEFAULT_PORT: u16 = 5959;
|
||||
pub struct CliContext(Arc<CliContextSeed>);
|
||||
impl CliContext {
|
||||
/// BLOCKING
|
||||
#[instrument(skip(matches))]
|
||||
#[instrument(skip_all)]
|
||||
pub fn init(matches: &ArgMatches) -> Result<Self, crate::Error> {
|
||||
let local_config_path = local_config_path();
|
||||
let base: CliContextConfig = load_config_from_paths(
|
||||
@@ -81,11 +85,7 @@ impl CliContext {
|
||||
} else if let Some(host) = base.host {
|
||||
host
|
||||
} else {
|
||||
format!(
|
||||
"http://{}",
|
||||
base.bind_rpc.unwrap_or(([127, 0, 0, 1], 80).into())
|
||||
)
|
||||
.parse()?
|
||||
"http://localhost".parse()?
|
||||
};
|
||||
let proxy = if let Some(proxy) = matches.value_of("proxy") {
|
||||
Some(proxy.parse()?)
|
||||
@@ -102,9 +102,15 @@ impl CliContext {
|
||||
.join(".cookies.json")
|
||||
});
|
||||
let cookie_store = Arc::new(CookieStoreMutex::new(if cookie_path.exists() {
|
||||
CookieStore::load_json(BufReader::new(File::open(&cookie_path)?))
|
||||
let mut store = CookieStore::load_json(BufReader::new(File::open(&cookie_path)?))
|
||||
.map_err(|e| eyre!("{}", e))
|
||||
.with_kind(crate::ErrorKind::Deserialization)?
|
||||
.with_kind(crate::ErrorKind::Deserialization)?;
|
||||
if let Ok(local) = std::fs::read_to_string(LOCAL_AUTH_COOKIE_PATH) {
|
||||
store
|
||||
.insert_raw(&Cookie::new("local", local), &"http://localhost".parse()?)
|
||||
.with_kind(crate::ErrorKind::Network)?;
|
||||
}
|
||||
store
|
||||
} else {
|
||||
CookieStore::default()
|
||||
}));
|
||||
@@ -131,6 +137,11 @@ impl CliContext {
|
||||
})))
|
||||
}
|
||||
}
|
||||
impl AsRef<Jwk> for CliContext {
|
||||
fn as_ref(&self) -> &Jwk {
|
||||
&*CURRENT_SECRET
|
||||
}
|
||||
}
|
||||
impl std::ops::Deref for CliContext {
|
||||
type Target = CliContextSeed;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use std::ops::Deref;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
@@ -6,38 +5,36 @@ use std::sync::Arc;
|
||||
use rpc_toolkit::yajrc::RpcError;
|
||||
use rpc_toolkit::Context;
|
||||
use serde::Deserialize;
|
||||
use tokio::fs::File;
|
||||
use tokio::sync::broadcast::Sender;
|
||||
use tracing::instrument;
|
||||
use url::Host;
|
||||
|
||||
use crate::shutdown::Shutdown;
|
||||
use crate::util::io::from_toml_async_reader;
|
||||
use crate::util::AsyncFileExt;
|
||||
use crate::{Error, ResultExt};
|
||||
use crate::util::config::load_config_from_paths;
|
||||
use crate::Error;
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct DiagnosticContextConfig {
|
||||
pub bind_rpc: Option<SocketAddr>,
|
||||
pub datadir: Option<PathBuf>,
|
||||
}
|
||||
impl DiagnosticContextConfig {
|
||||
#[instrument(skip(path))]
|
||||
pub async fn load<P: AsRef<Path>>(path: Option<P>) -> Result<Self, Error> {
|
||||
let cfg_path = path
|
||||
.as_ref()
|
||||
.map(|p| p.as_ref())
|
||||
.unwrap_or(Path::new(crate::util::config::CONFIG_PATH));
|
||||
if let Some(f) = File::maybe_open(cfg_path)
|
||||
.await
|
||||
.with_ctx(|_| (crate::ErrorKind::Filesystem, cfg_path.display().to_string()))?
|
||||
{
|
||||
from_toml_async_reader(f).await
|
||||
} else {
|
||||
Ok(Self::default())
|
||||
}
|
||||
#[instrument(skip_all)]
|
||||
pub async fn load<P: AsRef<Path> + Send + 'static>(path: Option<P>) -> Result<Self, Error> {
|
||||
tokio::task::spawn_blocking(move || {
|
||||
load_config_from_paths(
|
||||
path.as_ref()
|
||||
.into_iter()
|
||||
.map(|p| p.as_ref())
|
||||
.chain(std::iter::once(Path::new(
|
||||
crate::util::config::DEVICE_CONFIG_PATH,
|
||||
)))
|
||||
.chain(std::iter::once(Path::new(crate::util::config::CONFIG_PATH))),
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn datadir(&self) -> &Path {
|
||||
self.datadir
|
||||
.as_deref()
|
||||
@@ -46,7 +43,6 @@ impl DiagnosticContextConfig {
|
||||
}
|
||||
|
||||
pub struct DiagnosticContextSeed {
|
||||
pub bind_rpc: SocketAddr,
|
||||
pub datadir: PathBuf,
|
||||
pub shutdown: Sender<Option<Shutdown>>,
|
||||
pub error: Arc<RpcError>,
|
||||
@@ -56,18 +52,20 @@ pub struct DiagnosticContextSeed {
|
||||
#[derive(Clone)]
|
||||
pub struct DiagnosticContext(Arc<DiagnosticContextSeed>);
|
||||
impl DiagnosticContext {
|
||||
#[instrument(skip(path))]
|
||||
pub async fn init<P: AsRef<Path>>(
|
||||
#[instrument(skip_all)]
|
||||
pub async fn init<P: AsRef<Path> + Send + 'static>(
|
||||
path: Option<P>,
|
||||
disk_guid: Option<Arc<String>>,
|
||||
error: Error,
|
||||
) -> Result<Self, Error> {
|
||||
tracing::error!("Error: {}: Starting diagnostic UI", error);
|
||||
tracing::debug!("{:?}", error);
|
||||
|
||||
let cfg = DiagnosticContextConfig::load(path).await?;
|
||||
|
||||
let (shutdown, _) = tokio::sync::broadcast::channel(1);
|
||||
|
||||
Ok(Self(Arc::new(DiagnosticContextSeed {
|
||||
bind_rpc: cfg.bind_rpc.unwrap_or(([127, 0, 0, 1], 5959).into()),
|
||||
datadir: cfg.datadir().to_owned(),
|
||||
shutdown,
|
||||
disk_guid,
|
||||
@@ -76,17 +74,7 @@ impl DiagnosticContext {
|
||||
}
|
||||
}
|
||||
|
||||
impl Context for DiagnosticContext {
|
||||
fn host(&self) -> Host<&str> {
|
||||
match self.0.bind_rpc.ip() {
|
||||
IpAddr::V4(a) => Host::Ipv4(a),
|
||||
IpAddr::V6(a) => Host::Ipv6(a),
|
||||
}
|
||||
}
|
||||
fn port(&self) -> u16 {
|
||||
self.0.bind_rpc.port()
|
||||
}
|
||||
}
|
||||
impl Context for DiagnosticContext {}
|
||||
impl Deref for DiagnosticContext {
|
||||
type Target = DiagnosticContextSeed;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
|
||||
58
backend/src/context/install.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
use std::ops::Deref;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use rpc_toolkit::Context;
|
||||
use serde::Deserialize;
|
||||
use tokio::sync::broadcast::Sender;
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::net::utils::find_eth_iface;
|
||||
use crate::util::config::load_config_from_paths;
|
||||
use crate::Error;
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct InstallContextConfig {}
|
||||
impl InstallContextConfig {
|
||||
#[instrument(skip_all)]
|
||||
pub async fn load<P: AsRef<Path> + Send + 'static>(path: Option<P>) -> Result<Self, Error> {
|
||||
tokio::task::spawn_blocking(move || {
|
||||
load_config_from_paths(
|
||||
path.as_ref()
|
||||
.into_iter()
|
||||
.map(|p| p.as_ref())
|
||||
.chain(std::iter::once(Path::new(crate::util::config::CONFIG_PATH))),
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct InstallContextSeed {
|
||||
pub ethernet_interface: String,
|
||||
pub shutdown: Sender<()>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct InstallContext(Arc<InstallContextSeed>);
|
||||
impl InstallContext {
|
||||
#[instrument(skip_all)]
|
||||
pub async fn init<P: AsRef<Path> + Send + 'static>(path: Option<P>) -> Result<Self, Error> {
|
||||
let _cfg = InstallContextConfig::load(path.as_ref().map(|p| p.as_ref().to_owned())).await?;
|
||||
let (shutdown, _) = tokio::sync::broadcast::channel(1);
|
||||
Ok(Self(Arc::new(InstallContextSeed {
|
||||
ethernet_interface: find_eth_iface().await?,
|
||||
shutdown,
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
impl Context for InstallContext {}
|
||||
impl Deref for InstallContext {
|
||||
type Target = InstallContextSeed;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&*self.0
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
pub mod cli;
|
||||
pub mod diagnostic;
|
||||
pub mod install;
|
||||
pub mod rpc;
|
||||
pub mod sdk;
|
||||
pub mod setup;
|
||||
|
||||
pub use cli::CliContext;
|
||||
pub use diagnostic::DiagnosticContext;
|
||||
pub use install::InstallContext;
|
||||
pub use rpc::RpcContext;
|
||||
pub use sdk::SdkContext;
|
||||
pub use setup::SetupContext;
|
||||
@@ -35,3 +37,8 @@ impl From<SetupContext> for () {
|
||||
()
|
||||
}
|
||||
}
|
||||
impl From<InstallContext> for () {
|
||||
fn from(_: InstallContext) -> Self {
|
||||
()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::collections::{BTreeMap, VecDeque};
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4};
|
||||
use std::collections::BTreeMap;
|
||||
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
|
||||
use std::ops::Deref;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
@@ -8,42 +8,45 @@ use std::time::Duration;
|
||||
|
||||
use bollard::Docker;
|
||||
use helpers::to_tmp_path;
|
||||
use josekit::jwk::Jwk;
|
||||
use patch_db::json_ptr::JsonPointer;
|
||||
use patch_db::{DbHandle, LockReceipt, LockType, PatchDb, Revision};
|
||||
use patch_db::{DbHandle, LockReceipt, LockType, PatchDb};
|
||||
use reqwest::Url;
|
||||
use rpc_toolkit::url::Host;
|
||||
use rpc_toolkit::Context;
|
||||
use serde::Deserialize;
|
||||
use sqlx::sqlite::SqliteConnectOptions;
|
||||
use sqlx::SqlitePool;
|
||||
use tokio::fs::File;
|
||||
use tokio::process::Command;
|
||||
use sqlx::postgres::PgConnectOptions;
|
||||
use sqlx::PgPool;
|
||||
use tokio::sync::{broadcast, oneshot, Mutex, RwLock};
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::core::rpc_continuations::{RequestGuid, RpcContinuation};
|
||||
use crate::account::AccountInfo;
|
||||
use crate::core::rpc_continuations::{RequestGuid, RestHandler, RpcContinuation};
|
||||
use crate::db::model::{Database, InstalledPackageDataEntry, PackageDataEntry};
|
||||
use crate::hostname::{derive_hostname, derive_id, get_product_key};
|
||||
use crate::disk::OsPartitionInfo;
|
||||
use crate::init::{init_postgres, pgloader};
|
||||
use crate::install::cleanup::{cleanup_failed, uninstall, CleanupFailedReceipts};
|
||||
use crate::manager::ManagerMap;
|
||||
use crate::middleware::auth::HashSessionToken;
|
||||
use crate::net::tor::os_key;
|
||||
use crate::net::net_controller::NetController;
|
||||
use crate::net::ssl::SslManager;
|
||||
use crate::net::wifi::WpaCli;
|
||||
use crate::net::NetController;
|
||||
use crate::notifications::NotificationManager;
|
||||
use crate::setup::password_hash;
|
||||
use crate::shutdown::Shutdown;
|
||||
use crate::status::{MainStatus, Status};
|
||||
use crate::util::io::from_yaml_async_reader;
|
||||
use crate::util::{AsyncFileExt, Invoke};
|
||||
use crate::util::config::load_config_from_paths;
|
||||
use crate::{Error, ErrorKind, ResultExt};
|
||||
|
||||
use super::setup::CURRENT_SECRET;
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct RpcContextConfig {
|
||||
pub wifi_interface: Option<String>,
|
||||
pub ethernet_interface: String,
|
||||
pub os_partitions: OsPartitionInfo,
|
||||
pub migration_batch_rows: Option<usize>,
|
||||
pub migration_prefetch_rows: Option<usize>,
|
||||
pub bind_rpc: Option<SocketAddr>,
|
||||
pub bind_ws: Option<SocketAddr>,
|
||||
pub bind_static: Option<SocketAddr>,
|
||||
pub tor_control: Option<SocketAddr>,
|
||||
pub tor_socks: Option<SocketAddr>,
|
||||
pub dns_bind: Option<Vec<SocketAddr>>,
|
||||
@@ -52,85 +55,81 @@ pub struct RpcContextConfig {
|
||||
pub log_server: Option<Url>,
|
||||
}
|
||||
impl RpcContextConfig {
|
||||
pub async fn load<P: AsRef<Path>>(path: Option<P>) -> Result<Self, Error> {
|
||||
let cfg_path = path
|
||||
.as_ref()
|
||||
.map(|p| p.as_ref())
|
||||
.unwrap_or(Path::new(crate::util::config::CONFIG_PATH));
|
||||
if let Some(f) = File::maybe_open(cfg_path)
|
||||
.await
|
||||
.with_ctx(|_| (crate::ErrorKind::Filesystem, cfg_path.display().to_string()))?
|
||||
{
|
||||
from_yaml_async_reader(f).await
|
||||
} else {
|
||||
Ok(Self::default())
|
||||
}
|
||||
pub async fn load<P: AsRef<Path> + Send + 'static>(path: Option<P>) -> Result<Self, Error> {
|
||||
tokio::task::spawn_blocking(move || {
|
||||
load_config_from_paths(
|
||||
path.as_ref()
|
||||
.into_iter()
|
||||
.map(|p| p.as_ref())
|
||||
.chain(std::iter::once(Path::new(
|
||||
crate::util::config::DEVICE_CONFIG_PATH,
|
||||
)))
|
||||
.chain(std::iter::once(Path::new(crate::util::config::CONFIG_PATH))),
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
pub fn datadir(&self) -> &Path {
|
||||
self.datadir
|
||||
.as_deref()
|
||||
.unwrap_or_else(|| Path::new("/embassy-data"))
|
||||
}
|
||||
pub async fn db(&self, secret_store: &SqlitePool, product_key: &str) -> Result<PatchDb, Error> {
|
||||
let sid = derive_id(product_key);
|
||||
let hostname = derive_hostname(&sid);
|
||||
pub async fn db(&self, account: &AccountInfo) -> Result<PatchDb, Error> {
|
||||
let db_path = self.datadir().join("main").join("embassy.db");
|
||||
let db = PatchDb::open(&db_path)
|
||||
.await
|
||||
.with_ctx(|_| (crate::ErrorKind::Filesystem, db_path.display().to_string()))?;
|
||||
if !db.exists(&<JsonPointer>::default()).await? {
|
||||
db.put(
|
||||
&<JsonPointer>::default(),
|
||||
&Database::init(
|
||||
sid,
|
||||
&hostname,
|
||||
&os_key(&mut secret_store.acquire().await?).await?,
|
||||
password_hash(&mut secret_store.acquire().await?).await?,
|
||||
),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
if !db.exists(&<JsonPointer>::default()).await {
|
||||
db.put(&<JsonPointer>::default(), &Database::init(account))
|
||||
.await?;
|
||||
}
|
||||
Ok(db)
|
||||
}
|
||||
#[instrument]
|
||||
pub async fn secret_store(&self) -> Result<SqlitePool, Error> {
|
||||
let secret_store = SqlitePool::connect_with(
|
||||
SqliteConnectOptions::new()
|
||||
.filename(self.datadir().join("main").join("secrets.db"))
|
||||
.create_if_missing(true)
|
||||
.busy_timeout(Duration::from_secs(30)),
|
||||
)
|
||||
.await?;
|
||||
#[instrument(skip_all)]
|
||||
pub async fn secret_store(&self) -> Result<PgPool, Error> {
|
||||
init_postgres(self.datadir()).await?;
|
||||
let secret_store =
|
||||
PgPool::connect_with(PgConnectOptions::new().database("secrets").username("root"))
|
||||
.await?;
|
||||
sqlx::migrate!()
|
||||
.run(&secret_store)
|
||||
.await
|
||||
.with_kind(crate::ErrorKind::Database)?;
|
||||
let old_db_path = self.datadir().join("main/secrets.db");
|
||||
if tokio::fs::metadata(&old_db_path).await.is_ok() {
|
||||
pgloader(
|
||||
&old_db_path,
|
||||
self.migration_batch_rows.unwrap_or(25000),
|
||||
self.migration_prefetch_rows.unwrap_or(100_000),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(secret_store)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RpcContextSeed {
|
||||
is_closed: AtomicBool,
|
||||
pub bind_rpc: SocketAddr,
|
||||
pub bind_ws: SocketAddr,
|
||||
pub bind_static: SocketAddr,
|
||||
pub os_partitions: OsPartitionInfo,
|
||||
pub wifi_interface: Option<String>,
|
||||
pub ethernet_interface: String,
|
||||
pub datadir: PathBuf,
|
||||
pub disk_guid: Arc<String>,
|
||||
pub db: PatchDb,
|
||||
pub secret_store: SqlitePool,
|
||||
pub secret_store: PgPool,
|
||||
pub account: RwLock<AccountInfo>,
|
||||
pub docker: Docker,
|
||||
pub net_controller: NetController,
|
||||
pub net_controller: Arc<NetController>,
|
||||
pub managers: ManagerMap,
|
||||
pub revision_cache_size: usize,
|
||||
pub revision_cache: RwLock<VecDeque<Arc<Revision>>>,
|
||||
pub metrics_cache: RwLock<Option<crate::system::Metrics>>,
|
||||
pub shutdown: broadcast::Sender<Option<Shutdown>>,
|
||||
pub tor_socks: SocketAddr,
|
||||
pub notification_manager: NotificationManager,
|
||||
pub open_authed_websockets: Mutex<BTreeMap<HashSessionToken, Vec<oneshot::Sender<()>>>>,
|
||||
pub rpc_stream_continuations: Mutex<BTreeMap<RequestGuid, RpcContinuation>>,
|
||||
pub wifi_manager: Arc<RwLock<WpaCli>>,
|
||||
pub wifi_manager: Option<Arc<RwLock<WpaCli>>>,
|
||||
pub current_secret: Arc<Jwk>,
|
||||
}
|
||||
|
||||
pub struct RpcCleanReceipts {
|
||||
@@ -171,38 +170,11 @@ impl RpcCleanReceipts {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RpcSetNginxReceipts {
|
||||
server_info: LockReceipt<crate::db::model::ServerInfo, ()>,
|
||||
}
|
||||
|
||||
impl RpcSetNginxReceipts {
|
||||
pub async fn new(db: &'_ mut impl DbHandle) -> Result<Self, Error> {
|
||||
let mut locks = Vec::new();
|
||||
|
||||
let setup = Self::setup(&mut locks);
|
||||
Ok(setup(&db.lock_all(locks).await?)?)
|
||||
}
|
||||
|
||||
pub fn setup(
|
||||
locks: &mut Vec<patch_db::LockTargetId>,
|
||||
) -> impl FnOnce(&patch_db::Verifier) -> Result<Self, Error> {
|
||||
let server_info = crate::db::DatabaseModel::new()
|
||||
.server_info()
|
||||
.make_locker(LockType::Read)
|
||||
.add_to_keys(locks);
|
||||
move |skeleton_key| {
|
||||
Ok(Self {
|
||||
server_info: server_info.verify(skeleton_key)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RpcContext(Arc<RpcContextSeed>);
|
||||
impl RpcContext {
|
||||
#[instrument(skip(cfg_path))]
|
||||
pub async fn init<P: AsRef<Path>>(
|
||||
#[instrument(skip_all)]
|
||||
pub async fn init<P: AsRef<Path> + Send + 'static>(
|
||||
cfg_path: Option<P>,
|
||||
disk_guid: Arc<String>,
|
||||
) -> Result<Self, Error> {
|
||||
@@ -214,24 +186,27 @@ impl RpcContext {
|
||||
)));
|
||||
let (shutdown, _) = tokio::sync::broadcast::channel(1);
|
||||
let secret_store = base.secret_store().await?;
|
||||
tracing::info!("Opened Sqlite DB");
|
||||
let db = base.db(&secret_store, &get_product_key().await?).await?;
|
||||
tracing::info!("Opened Pg DB");
|
||||
let account = AccountInfo::load(&secret_store).await?;
|
||||
let db = base.db(&account).await?;
|
||||
tracing::info!("Opened PatchDB");
|
||||
let docker = Docker::connect_with_unix_defaults()?;
|
||||
let mut docker = Docker::connect_with_unix_defaults()?;
|
||||
docker.set_timeout(Duration::from_secs(600));
|
||||
tracing::info!("Connected to Docker");
|
||||
let net_controller = NetController::init(
|
||||
([127, 0, 0, 1], 80).into(),
|
||||
crate::net::tor::os_key(&mut secret_store.acquire().await?).await?,
|
||||
base.tor_control
|
||||
.unwrap_or(SocketAddr::from(([127, 0, 0, 1], 9051))),
|
||||
base.dns_bind
|
||||
.as_ref()
|
||||
.map(|v| v.as_slice())
|
||||
.unwrap_or(&[SocketAddr::from(([127, 0, 0, 1], 53))]),
|
||||
secret_store.clone(),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
let net_controller = Arc::new(
|
||||
NetController::init(
|
||||
base.tor_control
|
||||
.unwrap_or(SocketAddr::from(([127, 0, 0, 1], 9051))),
|
||||
base.dns_bind
|
||||
.as_ref()
|
||||
.map(|v| v.as_slice())
|
||||
.unwrap_or(&[SocketAddr::from(([127, 0, 0, 1], 53))]),
|
||||
SslManager::new(&account)?,
|
||||
&account.hostname,
|
||||
&account.key,
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
tracing::info!("Initialized Net Controller");
|
||||
let managers = ManagerMap::default();
|
||||
let metrics_cache = RwLock::new(None);
|
||||
@@ -239,25 +214,36 @@ impl RpcContext {
|
||||
tracing::info!("Initialized Notification Manager");
|
||||
let seed = Arc::new(RpcContextSeed {
|
||||
is_closed: AtomicBool::new(false),
|
||||
bind_rpc: base.bind_rpc.unwrap_or(([127, 0, 0, 1], 5959).into()),
|
||||
bind_ws: base.bind_ws.unwrap_or(([127, 0, 0, 1], 5960).into()),
|
||||
bind_static: base.bind_static.unwrap_or(([127, 0, 0, 1], 5961).into()),
|
||||
datadir: base.datadir().to_path_buf(),
|
||||
os_partitions: base.os_partitions,
|
||||
wifi_interface: base.wifi_interface.clone(),
|
||||
ethernet_interface: base.ethernet_interface,
|
||||
disk_guid,
|
||||
db,
|
||||
secret_store,
|
||||
account: RwLock::new(account),
|
||||
docker,
|
||||
net_controller,
|
||||
managers,
|
||||
revision_cache_size: base.revision_cache_size.unwrap_or(512),
|
||||
revision_cache: RwLock::new(VecDeque::new()),
|
||||
metrics_cache,
|
||||
shutdown,
|
||||
tor_socks: tor_proxy,
|
||||
notification_manager,
|
||||
open_authed_websockets: Mutex::new(BTreeMap::new()),
|
||||
rpc_stream_continuations: Mutex::new(BTreeMap::new()),
|
||||
wifi_manager: Arc::new(RwLock::new(WpaCli::init("wlan0".to_string()))),
|
||||
wifi_manager: base
|
||||
.wifi_interface
|
||||
.map(|i| Arc::new(RwLock::new(WpaCli::init(i)))),
|
||||
current_secret: Arc::new(
|
||||
Jwk::generate_ec_key(josekit::jwk::alg::ec::EcCurve::P256).map_err(|e| {
|
||||
tracing::debug!("{:?}", e);
|
||||
tracing::error!("Couldn't generate ec key");
|
||||
Error::new(
|
||||
color_eyre::eyre::eyre!("Couldn't generate ec key"),
|
||||
crate::ErrorKind::Unknown,
|
||||
)
|
||||
})?,
|
||||
),
|
||||
});
|
||||
|
||||
let res = Self(seed);
|
||||
@@ -274,43 +260,17 @@ impl RpcContext {
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[instrument(skip(self, db, receipts))]
|
||||
pub async fn set_nginx_conf<Db: DbHandle>(
|
||||
&self,
|
||||
db: &mut Db,
|
||||
receipts: RpcSetNginxReceipts,
|
||||
) -> Result<(), Error> {
|
||||
tokio::fs::write("/etc/nginx/sites-available/default", {
|
||||
let info = receipts.server_info.get(db).await?;
|
||||
format!(
|
||||
include_str!("../nginx/main-ui.conf.template"),
|
||||
lan_hostname = info.lan_address.host_str().unwrap(),
|
||||
tor_hostname = info.tor_address.host_str().unwrap(),
|
||||
)
|
||||
})
|
||||
.await
|
||||
.with_ctx(|_| {
|
||||
(
|
||||
crate::ErrorKind::Filesystem,
|
||||
"/etc/nginx/sites-available/default",
|
||||
)
|
||||
})?;
|
||||
Command::new("systemctl")
|
||||
.arg("reload")
|
||||
.arg("nginx")
|
||||
.invoke(crate::ErrorKind::Nginx)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
#[instrument(skip(self))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn shutdown(self) -> Result<(), Error> {
|
||||
self.managers.empty().await?;
|
||||
self.secret_store.close().await;
|
||||
self.is_closed.store(true, Ordering::SeqCst);
|
||||
tracing::info!("RPC Context is shutdown");
|
||||
// TODO: shutdown http servers
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn cleanup(&self) -> Result<(), Error> {
|
||||
let mut db = self.db.handle();
|
||||
let receipts = RpcCleanReceipts::new(&mut db).await?;
|
||||
@@ -387,18 +347,65 @@ impl RpcContext {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl Context for RpcContext {
|
||||
fn host(&self) -> Host<&str> {
|
||||
match self.0.bind_rpc.ip() {
|
||||
IpAddr::V4(a) => Host::Ipv4(a),
|
||||
IpAddr::V6(a) => Host::Ipv6(a),
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn clean_continuations(&self) {
|
||||
let mut continuations = self.rpc_stream_continuations.lock().await;
|
||||
let mut to_remove = Vec::new();
|
||||
for (guid, cont) in &*continuations {
|
||||
if cont.is_timed_out() {
|
||||
to_remove.push(guid.clone());
|
||||
}
|
||||
}
|
||||
for guid in to_remove {
|
||||
continuations.remove(&guid);
|
||||
}
|
||||
}
|
||||
fn port(&self) -> u16 {
|
||||
self.0.bind_rpc.port()
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn add_continuation(&self, guid: RequestGuid, handler: RpcContinuation) {
|
||||
self.clean_continuations().await;
|
||||
self.rpc_stream_continuations
|
||||
.lock()
|
||||
.await
|
||||
.insert(guid, handler);
|
||||
}
|
||||
|
||||
pub async fn get_continuation_handler(&self, guid: &RequestGuid) -> Option<RestHandler> {
|
||||
let mut continuations = self.rpc_stream_continuations.lock().await;
|
||||
if let Some(cont) = continuations.remove(guid) {
|
||||
cont.into_handler().await
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_ws_continuation_handler(&self, guid: &RequestGuid) -> Option<RestHandler> {
|
||||
let continuations = self.rpc_stream_continuations.lock().await;
|
||||
if matches!(continuations.get(guid), Some(RpcContinuation::WebSocket(_))) {
|
||||
drop(continuations);
|
||||
self.get_continuation_handler(guid).await
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_rest_continuation_handler(&self, guid: &RequestGuid) -> Option<RestHandler> {
|
||||
let continuations = self.rpc_stream_continuations.lock().await;
|
||||
if matches!(continuations.get(guid), Some(RpcContinuation::Rest(_))) {
|
||||
drop(continuations);
|
||||
self.get_continuation_handler(guid).await
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
impl AsRef<Jwk> for RpcContext {
|
||||
fn as_ref(&self) -> &Jwk {
|
||||
&*CURRENT_SECRET
|
||||
}
|
||||
}
|
||||
impl Context for RpcContext {}
|
||||
impl Deref for RpcContext {
|
||||
type Target = RpcContextSeed;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
|
||||
@@ -25,7 +25,7 @@ pub struct SdkContextSeed {
|
||||
pub struct SdkContext(Arc<SdkContextSeed>);
|
||||
impl SdkContext {
|
||||
/// BLOCKING
|
||||
#[instrument(skip(matches))]
|
||||
#[instrument(skip_all)]
|
||||
pub fn init(matches: &ArgMatches) -> Result<Self, crate::Error> {
|
||||
let local_config_path = local_config_path();
|
||||
let base: SdkContextConfig = load_config_from_paths(
|
||||
@@ -49,7 +49,7 @@ impl SdkContext {
|
||||
})))
|
||||
}
|
||||
/// BLOCKING
|
||||
#[instrument]
|
||||
#[instrument(skip_all)]
|
||||
pub fn developer_key(&self) -> Result<ed25519_dalek::Keypair, Error> {
|
||||
if !self.developer_key_path.exists() {
|
||||
return Err(Error::new(eyre!("Developer Key does not exist! Please run `embassy-sdk init` before running this command."), crate::ErrorKind::Uninitialized));
|
||||
|
||||
@@ -1,30 +1,35 @@
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use std::ops::Deref;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use josekit::jwk::Jwk;
|
||||
use patch_db::json_ptr::JsonPointer;
|
||||
use patch_db::PatchDb;
|
||||
use rpc_toolkit::yajrc::RpcError;
|
||||
use rpc_toolkit::Context;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::sqlite::SqliteConnectOptions;
|
||||
use sqlx::SqlitePool;
|
||||
use tokio::fs::File;
|
||||
use sqlx::postgres::PgConnectOptions;
|
||||
use sqlx::PgPool;
|
||||
use tokio::sync::broadcast::Sender;
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::instrument;
|
||||
use url::Host;
|
||||
|
||||
use crate::account::AccountInfo;
|
||||
use crate::db::model::Database;
|
||||
use crate::hostname::{derive_hostname, derive_id, get_product_key};
|
||||
use crate::net::tor::os_key;
|
||||
use crate::setup::{password_hash, RecoveryStatus};
|
||||
use crate::util::io::from_yaml_async_reader;
|
||||
use crate::util::AsyncFileExt;
|
||||
use crate::disk::OsPartitionInfo;
|
||||
use crate::init::{init_postgres, pgloader};
|
||||
use crate::setup::SetupStatus;
|
||||
use crate::util::config::load_config_from_paths;
|
||||
use crate::{Error, ResultExt};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref CURRENT_SECRET: Jwk = Jwk::generate_ec_key(josekit::jwk::alg::ec::EcCurve::P256).unwrap_or_else(|e| {
|
||||
tracing::debug!("{:?}", e);
|
||||
tracing::error!("Couldn't generate ec key");
|
||||
panic!("Couldn't generate ec key")
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct SetupResult {
|
||||
@@ -36,24 +41,27 @@ pub struct SetupResult {
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct SetupContextConfig {
|
||||
pub bind_rpc: Option<SocketAddr>,
|
||||
pub os_partitions: OsPartitionInfo,
|
||||
pub migration_batch_rows: Option<usize>,
|
||||
pub migration_prefetch_rows: Option<usize>,
|
||||
pub datadir: Option<PathBuf>,
|
||||
}
|
||||
impl SetupContextConfig {
|
||||
#[instrument(skip(path))]
|
||||
pub async fn load<P: AsRef<Path>>(path: Option<P>) -> Result<Self, Error> {
|
||||
let cfg_path = path
|
||||
.as_ref()
|
||||
.map(|p| p.as_ref())
|
||||
.unwrap_or(Path::new(crate::util::config::CONFIG_PATH));
|
||||
if let Some(f) = File::maybe_open(cfg_path)
|
||||
.await
|
||||
.with_ctx(|_| (crate::ErrorKind::Filesystem, cfg_path.display().to_string()))?
|
||||
{
|
||||
from_yaml_async_reader(f).await
|
||||
} else {
|
||||
Ok(Self::default())
|
||||
}
|
||||
#[instrument(skip_all)]
|
||||
pub async fn load<P: AsRef<Path> + Send + 'static>(path: Option<P>) -> Result<Self, Error> {
|
||||
tokio::task::spawn_blocking(move || {
|
||||
load_config_from_paths(
|
||||
path.as_ref()
|
||||
.into_iter()
|
||||
.map(|p| p.as_ref())
|
||||
.chain(std::iter::once(Path::new(
|
||||
crate::util::config::DEVICE_CONFIG_PATH,
|
||||
)))
|
||||
.chain(std::iter::once(Path::new(crate::util::config::CONFIG_PATH))),
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
pub fn datadir(&self) -> &Path {
|
||||
self.datadir
|
||||
@@ -63,104 +71,81 @@ impl SetupContextConfig {
|
||||
}
|
||||
|
||||
pub struct SetupContextSeed {
|
||||
pub os_partitions: OsPartitionInfo,
|
||||
pub config_path: Option<PathBuf>,
|
||||
pub bind_rpc: SocketAddr,
|
||||
pub migration_batch_rows: usize,
|
||||
pub migration_prefetch_rows: usize,
|
||||
pub shutdown: Sender<()>,
|
||||
pub datadir: PathBuf,
|
||||
pub selected_v2_drive: RwLock<Option<PathBuf>>,
|
||||
pub cached_product_key: RwLock<Option<Arc<String>>>,
|
||||
pub recovery_status: RwLock<Option<Result<RecoveryStatus, RpcError>>>,
|
||||
pub setup_status: RwLock<Option<Result<SetupStatus, RpcError>>>,
|
||||
pub setup_result: RwLock<Option<(Arc<String>, SetupResult)>>,
|
||||
}
|
||||
|
||||
impl AsRef<Jwk> for SetupContextSeed {
|
||||
fn as_ref(&self) -> &Jwk {
|
||||
&*CURRENT_SECRET
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SetupContext(Arc<SetupContextSeed>);
|
||||
impl SetupContext {
|
||||
#[instrument(skip(path))]
|
||||
pub async fn init<P: AsRef<Path>>(path: Option<P>) -> Result<Self, Error> {
|
||||
let cfg = SetupContextConfig::load(path.as_ref()).await?;
|
||||
#[instrument(skip_all)]
|
||||
pub async fn init<P: AsRef<Path> + Send + 'static>(path: Option<P>) -> Result<Self, Error> {
|
||||
let cfg = SetupContextConfig::load(path.as_ref().map(|p| p.as_ref().to_owned())).await?;
|
||||
let (shutdown, _) = tokio::sync::broadcast::channel(1);
|
||||
let datadir = cfg.datadir().to_owned();
|
||||
Ok(Self(Arc::new(SetupContextSeed {
|
||||
os_partitions: cfg.os_partitions,
|
||||
config_path: path.as_ref().map(|p| p.as_ref().to_owned()),
|
||||
bind_rpc: cfg.bind_rpc.unwrap_or(([127, 0, 0, 1], 5959).into()),
|
||||
migration_batch_rows: cfg.migration_batch_rows.unwrap_or(25000),
|
||||
migration_prefetch_rows: cfg.migration_prefetch_rows.unwrap_or(100_000),
|
||||
shutdown,
|
||||
datadir,
|
||||
selected_v2_drive: RwLock::new(None),
|
||||
cached_product_key: RwLock::new(None),
|
||||
recovery_status: RwLock::new(None),
|
||||
setup_status: RwLock::new(None),
|
||||
setup_result: RwLock::new(None),
|
||||
})))
|
||||
}
|
||||
#[instrument(skip(self))]
|
||||
pub async fn db(&self, secret_store: &SqlitePool) -> Result<PatchDb, Error> {
|
||||
#[instrument(skip_all)]
|
||||
pub async fn db(&self, account: &AccountInfo) -> Result<PatchDb, Error> {
|
||||
let db_path = self.datadir.join("main").join("embassy.db");
|
||||
let db = PatchDb::open(&db_path)
|
||||
.await
|
||||
.with_ctx(|_| (crate::ErrorKind::Filesystem, db_path.display().to_string()))?;
|
||||
if !db.exists(&<JsonPointer>::default()).await? {
|
||||
let pkey = self.product_key().await?;
|
||||
let sid = derive_id(&*pkey);
|
||||
let hostname = derive_hostname(&sid);
|
||||
db.put(
|
||||
&<JsonPointer>::default(),
|
||||
&Database::init(
|
||||
sid,
|
||||
&hostname,
|
||||
&os_key(&mut secret_store.acquire().await?).await?,
|
||||
password_hash(&mut secret_store.acquire().await?).await?,
|
||||
),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
if !db.exists(&<JsonPointer>::default()).await {
|
||||
db.put(&<JsonPointer>::default(), &Database::init(account))
|
||||
.await?;
|
||||
}
|
||||
Ok(db)
|
||||
}
|
||||
#[instrument(skip(self))]
|
||||
pub async fn secret_store(&self) -> Result<SqlitePool, Error> {
|
||||
let secret_store = SqlitePool::connect_with(
|
||||
SqliteConnectOptions::new()
|
||||
.filename(self.datadir.join("main").join("secrets.db"))
|
||||
.create_if_missing(true)
|
||||
.busy_timeout(Duration::from_secs(30)),
|
||||
)
|
||||
.await?;
|
||||
#[instrument(skip_all)]
|
||||
pub async fn secret_store(&self) -> Result<PgPool, Error> {
|
||||
init_postgres(&self.datadir).await?;
|
||||
let secret_store =
|
||||
PgPool::connect_with(PgConnectOptions::new().database("secrets").username("root"))
|
||||
.await?;
|
||||
sqlx::migrate!()
|
||||
.run(&secret_store)
|
||||
.await
|
||||
.with_kind(crate::ErrorKind::Database)?;
|
||||
let old_db_path = self.datadir.join("main/secrets.db");
|
||||
if tokio::fs::metadata(&old_db_path).await.is_ok() {
|
||||
pgloader(
|
||||
&old_db_path,
|
||||
self.migration_batch_rows,
|
||||
self.migration_prefetch_rows,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(secret_store)
|
||||
}
|
||||
#[instrument(skip(self))]
|
||||
pub async fn product_key(&self) -> Result<Arc<String>, Error> {
|
||||
Ok(
|
||||
if let Some(k) = {
|
||||
let guard = self.cached_product_key.read().await;
|
||||
let res = guard.clone();
|
||||
drop(guard);
|
||||
res
|
||||
} {
|
||||
k
|
||||
} else {
|
||||
let k = Arc::new(get_product_key().await?);
|
||||
*self.cached_product_key.write().await = Some(k.clone());
|
||||
k
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Context for SetupContext {
|
||||
fn host(&self) -> Host<&str> {
|
||||
match self.0.bind_rpc.ip() {
|
||||
IpAddr::V4(a) => Host::Ipv4(a),
|
||||
IpAddr::V6(a) => Host::Ipv6(a),
|
||||
}
|
||||
}
|
||||
fn port(&self) -> u16 {
|
||||
self.0.bind_rpc.port()
|
||||
}
|
||||
}
|
||||
impl Context for SetupContext {}
|
||||
impl Deref for SetupContext {
|
||||
type Target = SetupContextSeed;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
|
||||
@@ -6,7 +6,6 @@ use rpc_toolkit::command;
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::context::RpcContext;
|
||||
use crate::db::util::WithRevision;
|
||||
use crate::dependencies::{
|
||||
break_all_dependents_transitive, heal_all_dependents_transitive, BreakageRes, DependencyError,
|
||||
DependencyReceipt, TaggedDependencyError,
|
||||
@@ -61,12 +60,9 @@ impl StartReceipts {
|
||||
}
|
||||
}
|
||||
|
||||
#[command(display(display_none))]
|
||||
#[instrument(skip(ctx))]
|
||||
pub async fn start(
|
||||
#[context] ctx: RpcContext,
|
||||
#[arg] id: PackageId,
|
||||
) -> Result<WithRevision<()>, Error> {
|
||||
#[command(display(display_none), metadata(sync_db = true))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn start(#[context] ctx: RpcContext, #[arg] id: PackageId) -> Result<(), Error> {
|
||||
let mut db = ctx.db.handle();
|
||||
let mut tx = db.begin().await?;
|
||||
let receipts = StartReceipts::new(&mut tx, &id).await?;
|
||||
@@ -77,7 +73,7 @@ pub async fn start(
|
||||
.await?;
|
||||
heal_all_dependents_transitive(&ctx, &mut tx, &id, &receipts.dependency_receipt).await?;
|
||||
|
||||
let revision = tx.commit(None).await?;
|
||||
tx.commit().await?;
|
||||
drop(receipts);
|
||||
|
||||
ctx.managers
|
||||
@@ -87,10 +83,7 @@ pub async fn start(
|
||||
.synchronize()
|
||||
.await;
|
||||
|
||||
Ok(WithRevision {
|
||||
revision,
|
||||
response: (),
|
||||
})
|
||||
Ok(())
|
||||
}
|
||||
#[derive(Clone)]
|
||||
pub struct StopReceipts {
|
||||
@@ -127,14 +120,15 @@ impl StopReceipts {
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(db))]
|
||||
async fn stop_common<Db: DbHandle>(
|
||||
#[instrument(skip_all)]
|
||||
pub async fn stop_common<Db: DbHandle>(
|
||||
db: &mut Db,
|
||||
id: &PackageId,
|
||||
breakages: &mut BTreeMap<PackageId, TaggedDependencyError>,
|
||||
) -> Result<(), Error> {
|
||||
) -> Result<MainStatus, Error> {
|
||||
let mut tx = db.begin().await?;
|
||||
let receipts = StopReceipts::new(&mut tx, id).await?;
|
||||
let last_status = receipts.status.get(&mut tx).await?;
|
||||
receipts.status.set(&mut tx, MainStatus::Stopping).await?;
|
||||
|
||||
tx.save().await?;
|
||||
@@ -147,16 +141,20 @@ async fn stop_common<Db: DbHandle>(
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
Ok(last_status)
|
||||
}
|
||||
|
||||
#[command(subcommands(self(stop_impl(async)), stop_dry), display(display_none))]
|
||||
#[command(
|
||||
subcommands(self(stop_impl(async)), stop_dry),
|
||||
display(display_none),
|
||||
metadata(sync_db = true)
|
||||
)]
|
||||
pub fn stop(#[arg] id: PackageId) -> Result<PackageId, Error> {
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
#[command(rename = "dry", display(display_serializable))]
|
||||
#[instrument(skip(ctx))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn stop_dry(
|
||||
#[context] ctx: RpcContext,
|
||||
#[parent_data] id: PackageId,
|
||||
@@ -172,24 +170,20 @@ pub async fn stop_dry(
|
||||
Ok(BreakageRes(breakages))
|
||||
}
|
||||
|
||||
#[instrument(skip(ctx))]
|
||||
pub async fn stop_impl(ctx: RpcContext, id: PackageId) -> Result<WithRevision<()>, Error> {
|
||||
#[instrument(skip_all)]
|
||||
pub async fn stop_impl(ctx: RpcContext, id: PackageId) -> Result<MainStatus, Error> {
|
||||
let mut db = ctx.db.handle();
|
||||
let mut tx = db.begin().await?;
|
||||
|
||||
stop_common(&mut tx, &id, &mut BTreeMap::new()).await?;
|
||||
let last_statuts = stop_common(&mut tx, &id, &mut BTreeMap::new()).await?;
|
||||
|
||||
Ok(WithRevision {
|
||||
revision: tx.commit(None).await?,
|
||||
response: (),
|
||||
})
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(last_statuts)
|
||||
}
|
||||
|
||||
#[command(display(display_none))]
|
||||
pub async fn restart(
|
||||
#[context] ctx: RpcContext,
|
||||
#[arg] id: PackageId,
|
||||
) -> Result<WithRevision<()>, Error> {
|
||||
#[command(display(display_none), metadata(sync_db = true))]
|
||||
pub async fn restart(#[context] ctx: RpcContext, #[arg] id: PackageId) -> Result<(), Error> {
|
||||
let mut db = ctx.db.handle();
|
||||
let mut tx = db.begin().await?;
|
||||
|
||||
@@ -208,9 +202,7 @@ pub async fn restart(
|
||||
}
|
||||
*status = Some(MainStatus::Restarting);
|
||||
status.save(&mut tx).await?;
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(WithRevision {
|
||||
revision: tx.commit(None).await?,
|
||||
response: (),
|
||||
})
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,20 +1,27 @@
|
||||
use std::time::Instant;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use futures::future::BoxFuture;
|
||||
use http::{Request, Response};
|
||||
use hyper::Body;
|
||||
use futures::FutureExt;
|
||||
use helpers::TimedResource;
|
||||
use hyper::upgrade::Upgraded;
|
||||
use hyper::{Body, Error as HyperError, Request, Response};
|
||||
use rand::RngCore;
|
||||
use tokio::task::JoinError;
|
||||
use tokio_tungstenite::WebSocketStream;
|
||||
|
||||
use crate::{Error, ResultExt};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
|
||||
pub struct RequestGuid<T: AsRef<str> = String>(T);
|
||||
pub struct RequestGuid<T: AsRef<str> = String>(Arc<T>);
|
||||
impl RequestGuid {
|
||||
pub fn new() -> Self {
|
||||
let mut buf = [0; 40];
|
||||
rand::thread_rng().fill_bytes(&mut buf);
|
||||
RequestGuid(base32::encode(
|
||||
RequestGuid(Arc::new(base32::encode(
|
||||
base32::Alphabet::RFC4648 { padding: false },
|
||||
&buf,
|
||||
))
|
||||
)))
|
||||
}
|
||||
|
||||
pub fn from(r: &str) -> Option<RequestGuid> {
|
||||
@@ -26,7 +33,7 @@ impl RequestGuid {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Some(RequestGuid(r.to_owned()))
|
||||
Some(RequestGuid(Arc::new(r.to_owned())))
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
@@ -39,15 +46,71 @@ fn parse_guid() {
|
||||
|
||||
impl<T: AsRef<str>> std::fmt::Display for RequestGuid<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.0.as_ref().fmt(f)
|
||||
(&*self.0).as_ref().fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RpcContinuation {
|
||||
pub created_at: Instant,
|
||||
pub handler: Box<
|
||||
dyn FnOnce(Request<Body>) -> BoxFuture<'static, Result<Response<Body>, crate::Error>>
|
||||
+ Send
|
||||
+ Sync,
|
||||
>,
|
||||
pub type RestHandler = Box<
|
||||
dyn FnOnce(Request<Body>) -> BoxFuture<'static, Result<Response<Body>, crate::Error>> + Send,
|
||||
>;
|
||||
|
||||
pub type WebSocketHandler = Box<
|
||||
dyn FnOnce(
|
||||
BoxFuture<'static, Result<Result<WebSocketStream<Upgraded>, HyperError>, JoinError>>,
|
||||
) -> BoxFuture<'static, Result<(), Error>>
|
||||
+ Send,
|
||||
>;
|
||||
|
||||
pub enum RpcContinuation {
|
||||
Rest(TimedResource<RestHandler>),
|
||||
WebSocket(TimedResource<WebSocketHandler>),
|
||||
}
|
||||
impl RpcContinuation {
|
||||
pub fn rest(handler: RestHandler, timeout: Duration) -> Self {
|
||||
RpcContinuation::Rest(TimedResource::new(handler, timeout))
|
||||
}
|
||||
pub fn ws(handler: WebSocketHandler, timeout: Duration) -> Self {
|
||||
RpcContinuation::WebSocket(TimedResource::new(handler, timeout))
|
||||
}
|
||||
pub fn is_timed_out(&self) -> bool {
|
||||
match self {
|
||||
RpcContinuation::Rest(a) => a.is_timed_out(),
|
||||
RpcContinuation::WebSocket(a) => a.is_timed_out(),
|
||||
}
|
||||
}
|
||||
pub async fn into_handler(self) -> Option<RestHandler> {
|
||||
match self {
|
||||
RpcContinuation::Rest(handler) => handler.get().await,
|
||||
RpcContinuation::WebSocket(handler) => {
|
||||
if let Some(handler) = handler.get().await {
|
||||
Some(Box::new(
|
||||
|req: Request<Body>| -> BoxFuture<'static, Result<Response<Body>, Error>> {
|
||||
async move {
|
||||
let (parts, body) = req.into_parts();
|
||||
let req = Request::from_parts(parts, body);
|
||||
let (res, ws_fut) = hyper_ws_listener::create_ws(req)
|
||||
.with_kind(crate::ErrorKind::Network)?;
|
||||
if let Some(ws_fut) = ws_fut {
|
||||
tokio::task::spawn(async move {
|
||||
match handler(ws_fut.boxed()).await {
|
||||
Ok(()) => (),
|
||||
Err(e) => {
|
||||
tracing::error!("WebSocket Closed: {}", e);
|
||||
tracing::debug!("{:?}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
.boxed()
|
||||
},
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,92 +1,61 @@
|
||||
pub mod model;
|
||||
pub mod package;
|
||||
pub mod util;
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::future::Future;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use color_eyre::eyre::eyre;
|
||||
use futures::{FutureExt, SinkExt, StreamExt};
|
||||
use patch_db::json_ptr::JsonPointer;
|
||||
use patch_db::{Dump, Revision};
|
||||
use rpc_toolkit::command;
|
||||
use rpc_toolkit::hyper::upgrade::Upgraded;
|
||||
use rpc_toolkit::hyper::{Body, Error as HyperError, Request, Response};
|
||||
use rpc_toolkit::yajrc::{GenericRpcMethod, RpcError, RpcResponse};
|
||||
use rpc_toolkit::yajrc::RpcError;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use tokio::sync::{broadcast, oneshot};
|
||||
use tokio::sync::oneshot;
|
||||
use tokio::task::JoinError;
|
||||
use tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode;
|
||||
use tokio_tungstenite::tungstenite::protocol::CloseFrame;
|
||||
use tokio_tungstenite::tungstenite::Message;
|
||||
use tokio_tungstenite::WebSocketStream;
|
||||
use tracing::instrument;
|
||||
|
||||
pub use self::model::DatabaseModel;
|
||||
use self::util::WithRevision;
|
||||
use crate::context::RpcContext;
|
||||
use crate::middleware::auth::{HasValidSession, HashSessionToken};
|
||||
use crate::util::serde::{display_serializable, IoFormat};
|
||||
use crate::{Error, ResultExt};
|
||||
|
||||
#[instrument(skip(ctx, ws_fut))]
|
||||
#[instrument(skip_all)]
|
||||
async fn ws_handler<
|
||||
WSFut: Future<Output = Result<Result<WebSocketStream<Upgraded>, HyperError>, JoinError>>,
|
||||
>(
|
||||
ctx: RpcContext,
|
||||
session: Option<(HasValidSession, HashSessionToken)>,
|
||||
ws_fut: WSFut,
|
||||
) -> Result<(), Error> {
|
||||
let (dump, sub) = ctx.db.dump_and_sub().await;
|
||||
let (dump, sub) = ctx.db.dump_and_sub().await?;
|
||||
let mut stream = ws_fut
|
||||
.await
|
||||
.with_kind(crate::ErrorKind::Network)?
|
||||
.with_kind(crate::ErrorKind::Unknown)?;
|
||||
|
||||
let (has_valid_session, token) = loop {
|
||||
if let Some(Message::Text(cookie)) = stream
|
||||
.next()
|
||||
if let Some((session, token)) = session {
|
||||
let kill = subscribe_to_session_kill(&ctx, token).await;
|
||||
send_dump(session, &mut stream, dump).await?;
|
||||
|
||||
deal_with_messages(session, kill, sub, stream).await?;
|
||||
} else {
|
||||
stream
|
||||
.close(Some(CloseFrame {
|
||||
code: CloseCode::Error,
|
||||
reason: "UNAUTHORIZED".into(),
|
||||
}))
|
||||
.await
|
||||
.transpose()
|
||||
.with_kind(crate::ErrorKind::Network)?
|
||||
{
|
||||
let cookie_str = serde_json::from_str::<Cow<str>>(&cookie)
|
||||
.with_kind(crate::ErrorKind::Deserialization)?;
|
||||
.with_kind(crate::ErrorKind::Network)?;
|
||||
}
|
||||
|
||||
let id = basic_cookies::Cookie::parse(&cookie_str)
|
||||
.with_kind(crate::ErrorKind::Authorization)?
|
||||
.into_iter()
|
||||
.find(|c| c.get_name() == "session")
|
||||
.ok_or_else(|| {
|
||||
Error::new(eyre!("UNAUTHORIZED"), crate::ErrorKind::Authorization)
|
||||
})?;
|
||||
let authenticated_session = HashSessionToken::from_cookie(&id);
|
||||
match HasValidSession::from_session(&authenticated_session, &ctx).await {
|
||||
Err(e) => {
|
||||
stream
|
||||
.send(Message::Text(
|
||||
serde_json::to_string(
|
||||
&RpcResponse::<GenericRpcMethod<String>>::from_result(Err::<
|
||||
_,
|
||||
RpcError,
|
||||
>(
|
||||
e.into()
|
||||
)),
|
||||
)
|
||||
.with_kind(crate::ErrorKind::Serialization)?,
|
||||
))
|
||||
.await
|
||||
.with_kind(crate::ErrorKind::Network)?;
|
||||
return Ok(());
|
||||
}
|
||||
Ok(has_validation) => break (has_validation, authenticated_session),
|
||||
}
|
||||
}
|
||||
};
|
||||
let kill = subscribe_to_session_kill(&ctx, token).await;
|
||||
send_dump(has_valid_session, &mut stream, dump).await?;
|
||||
|
||||
deal_with_messages(has_valid_session, kill, sub, stream).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -104,50 +73,36 @@ async fn subscribe_to_session_kill(
|
||||
recv
|
||||
}
|
||||
|
||||
#[instrument(skip(_has_valid_authentication, kill, sub, stream))]
|
||||
#[instrument(skip_all)]
|
||||
async fn deal_with_messages(
|
||||
_has_valid_authentication: HasValidSession,
|
||||
mut kill: oneshot::Receiver<()>,
|
||||
mut sub: broadcast::Receiver<Arc<Revision>>,
|
||||
mut sub: patch_db::Subscriber,
|
||||
mut stream: WebSocketStream<Upgraded>,
|
||||
) -> Result<(), Error> {
|
||||
loop {
|
||||
futures::select! {
|
||||
_ = (&mut kill).fuse() => {
|
||||
tracing::info!("Closing WebSocket: Reason: Session Terminated");
|
||||
stream
|
||||
.close(Some(CloseFrame {
|
||||
code: CloseCode::Error,
|
||||
reason: "UNAUTHORIZED".into(),
|
||||
}))
|
||||
.await
|
||||
.with_kind(crate::ErrorKind::Network)?;
|
||||
return Ok(())
|
||||
}
|
||||
new_rev = sub.recv().fuse() => {
|
||||
let rev = new_rev.with_kind(crate::ErrorKind::Database)?;
|
||||
let rev = new_rev.expect("UNREACHABLE: patch-db is dropped");
|
||||
stream
|
||||
.send(Message::Text(
|
||||
serde_json::to_string(
|
||||
&RpcResponse::<GenericRpcMethod<String>>::from_result(Ok::<_, RpcError>(
|
||||
serde_json::to_value(&rev).with_kind(crate::ErrorKind::Serialization)?,
|
||||
)),
|
||||
)
|
||||
.with_kind(crate::ErrorKind::Serialization)?,
|
||||
))
|
||||
.send(Message::Text(serde_json::to_string(&rev).with_kind(crate::ErrorKind::Serialization)?))
|
||||
.await
|
||||
.with_kind(crate::ErrorKind::Network)?;
|
||||
}
|
||||
message = stream.next().fuse() => {
|
||||
let message = message.transpose().with_kind(crate::ErrorKind::Network)?;
|
||||
match message {
|
||||
Some(Message::Ping(a)) => {
|
||||
stream
|
||||
.send(Message::Pong(a))
|
||||
.await
|
||||
.with_kind(crate::ErrorKind::Network)?;
|
||||
}
|
||||
Some(Message::Close(frame)) => {
|
||||
if let Some(reason) = frame.as_ref() {
|
||||
tracing::info!("Closing WebSocket: Reason: {} {}", reason.code, reason.reason);
|
||||
} else {
|
||||
tracing::info!("Closing WebSocket: Reason: Unknown");
|
||||
}
|
||||
return Ok(())
|
||||
}
|
||||
None => {
|
||||
tracing::info!("Closing WebSocket: Stream Finished");
|
||||
return Ok(())
|
||||
@@ -155,12 +110,6 @@ async fn deal_with_messages(
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
_ = tokio::time::sleep(Duration::from_secs(10)).fuse() => {
|
||||
stream
|
||||
.send(Message::Ping(Vec::new()))
|
||||
.await
|
||||
.with_kind(crate::ErrorKind::Network)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -172,13 +121,7 @@ async fn send_dump(
|
||||
) -> Result<(), Error> {
|
||||
stream
|
||||
.send(Message::Text(
|
||||
serde_json::to_string(&RpcResponse::<GenericRpcMethod<String>>::from_result(Ok::<
|
||||
_,
|
||||
RpcError,
|
||||
>(
|
||||
serde_json::to_value(&dump).with_kind(crate::ErrorKind::Serialization)?,
|
||||
)))
|
||||
.with_kind(crate::ErrorKind::Serialization)?,
|
||||
serde_json::to_string(&dump).with_kind(crate::ErrorKind::Serialization)?,
|
||||
))
|
||||
.await
|
||||
.with_kind(crate::ErrorKind::Network)?;
|
||||
@@ -187,11 +130,27 @@ async fn send_dump(
|
||||
|
||||
pub async fn subscribe(ctx: RpcContext, req: Request<Body>) -> Result<Response<Body>, Error> {
|
||||
let (parts, body) = req.into_parts();
|
||||
let session = match async {
|
||||
let token = HashSessionToken::from_request_parts(&parts)?;
|
||||
let session = HasValidSession::from_request_parts(&parts, &ctx).await?;
|
||||
Ok::<_, Error>((session, token))
|
||||
}
|
||||
.await
|
||||
{
|
||||
Ok(a) => Some(a),
|
||||
Err(e) => {
|
||||
if e.kind != crate::ErrorKind::Authorization {
|
||||
tracing::error!("Error Authenticating Websocket: {}", e);
|
||||
tracing::debug!("{:?}", e);
|
||||
}
|
||||
None
|
||||
}
|
||||
};
|
||||
let req = Request::from_parts(parts, body);
|
||||
let (res, ws_fut) = hyper_ws_listener::create_ws(req).with_kind(crate::ErrorKind::Network)?;
|
||||
if let Some(ws_fut) = ws_fut {
|
||||
tokio::task::spawn(async move {
|
||||
match ws_handler(ctx, ws_fut).await {
|
||||
match ws_handler(ctx, session, ws_fut).await {
|
||||
Ok(()) => (),
|
||||
Err(e) => {
|
||||
tracing::error!("WebSocket Closed: {}", e);
|
||||
@@ -223,24 +182,11 @@ pub async fn revisions(
|
||||
#[allow(unused_variables)]
|
||||
#[arg(long = "format")]
|
||||
format: Option<IoFormat>,
|
||||
) -> Result<RevisionsRes, RpcError> {
|
||||
let cache = ctx.revision_cache.read().await;
|
||||
if cache
|
||||
.front()
|
||||
.map(|rev| rev.id <= since + 1)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
Ok(RevisionsRes::Revisions(
|
||||
cache
|
||||
.iter()
|
||||
.skip_while(|rev| rev.id < since + 1)
|
||||
.cloned()
|
||||
.collect(),
|
||||
))
|
||||
} else {
|
||||
drop(cache);
|
||||
Ok(RevisionsRes::Dump(ctx.db.dump().await))
|
||||
}
|
||||
) -> Result<RevisionsRes, Error> {
|
||||
Ok(match ctx.db.sync(since).await? {
|
||||
Ok(revs) => RevisionsRes::Revisions(revs),
|
||||
Err(dump) => RevisionsRes::Dump(dump),
|
||||
})
|
||||
}
|
||||
|
||||
#[command(display(display_serializable))]
|
||||
@@ -249,8 +195,8 @@ pub async fn dump(
|
||||
#[allow(unused_variables)]
|
||||
#[arg(long = "format")]
|
||||
format: Option<IoFormat>,
|
||||
) -> Result<Dump, RpcError> {
|
||||
Ok(ctx.db.dump().await)
|
||||
) -> Result<Dump, Error> {
|
||||
Ok(ctx.db.dump().await?)
|
||||
}
|
||||
|
||||
#[command(subcommands(ui))]
|
||||
@@ -259,7 +205,7 @@ pub fn put() -> Result<(), RpcError> {
|
||||
}
|
||||
|
||||
#[command(display(display_serializable))]
|
||||
#[instrument(skip(ctx))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn ui(
|
||||
#[context] ctx: RpcContext,
|
||||
#[arg] pointer: JsonPointer,
|
||||
@@ -267,13 +213,11 @@ pub async fn ui(
|
||||
#[allow(unused_variables)]
|
||||
#[arg(long = "format")]
|
||||
format: Option<IoFormat>,
|
||||
) -> Result<WithRevision<()>, Error> {
|
||||
) -> Result<(), Error> {
|
||||
let ptr = "/ui"
|
||||
.parse::<JsonPointer>()
|
||||
.with_kind(crate::ErrorKind::Database)?
|
||||
+ &pointer;
|
||||
Ok(WithRevision {
|
||||
response: (),
|
||||
revision: ctx.db.put(&ptr, &value, None).await?,
|
||||
})
|
||||
ctx.db.put(&ptr, &value).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,24 +1,31 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
use std::sync::Arc;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use emver::VersionRange;
|
||||
use ipnet::{Ipv4Net, Ipv6Net};
|
||||
use isocountry::CountryCode;
|
||||
use itertools::Itertools;
|
||||
use openssl::hash::MessageDigest;
|
||||
use patch_db::json_ptr::JsonPointer;
|
||||
use patch_db::{HasModel, Map, MapModel, OptionModel};
|
||||
use reqwest::Url;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use torut::onion::TorSecretKeyV3;
|
||||
use ssh_key::public::Ed25519PublicKey;
|
||||
|
||||
use crate::account::AccountInfo;
|
||||
use crate::config::spec::{PackagePointerSpec, SystemPointerSpec};
|
||||
use crate::install::progress::InstallProgress;
|
||||
use crate::net::interface::InterfaceId;
|
||||
use crate::net::utils::{get_iface_ipv4_addr, get_iface_ipv6_addr};
|
||||
use crate::s9pk::manifest::{Manifest, ManifestModel, PackageId};
|
||||
use crate::status::health_check::HealthCheckId;
|
||||
use crate::status::Status;
|
||||
use crate::util::Version;
|
||||
use crate::version::{Current, VersionT};
|
||||
use crate::Error;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, HasModel)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
@@ -27,29 +34,25 @@ pub struct Database {
|
||||
pub server_info: ServerInfo,
|
||||
#[model]
|
||||
pub package_data: AllPackageData,
|
||||
#[model]
|
||||
pub recovered_packages: BTreeMap<PackageId, RecoveredPackageInfo>,
|
||||
pub ui: Value,
|
||||
}
|
||||
impl Database {
|
||||
pub fn init(
|
||||
id: String,
|
||||
hostname: &str,
|
||||
tor_key: &TorSecretKeyV3,
|
||||
password_hash: String,
|
||||
) -> Self {
|
||||
pub fn init(account: &AccountInfo) -> Self {
|
||||
let lan_address = account.hostname.lan_address().parse().unwrap();
|
||||
// TODO
|
||||
Database {
|
||||
server_info: ServerInfo {
|
||||
id,
|
||||
id: account.server_id.clone(),
|
||||
version: Current::new().semver().into(),
|
||||
hostname: Some(account.hostname.no_dot_host_name()),
|
||||
last_backup: None,
|
||||
last_wifi_region: None,
|
||||
eos_version_compat: Current::new().compat().clone(),
|
||||
lan_address: format!("https://{}.local", hostname).parse().unwrap(),
|
||||
tor_address: format!("http://{}", tor_key.public().get_onion_address())
|
||||
lan_address,
|
||||
tor_address: format!("http://{}", account.key.tor_address())
|
||||
.parse()
|
||||
.unwrap(),
|
||||
ip_info: BTreeMap::new(),
|
||||
status_info: ServerStatus {
|
||||
backup_progress: None,
|
||||
updated: false,
|
||||
@@ -65,11 +68,22 @@ impl Database {
|
||||
tor: Vec::new(),
|
||||
clearnet: Vec::new(),
|
||||
},
|
||||
password_hash,
|
||||
password_hash: account.password.clone(),
|
||||
pubkey: ssh_key::PublicKey::from(Ed25519PublicKey::from(&account.key.ssh_key()))
|
||||
.to_openssh()
|
||||
.unwrap(),
|
||||
ca_fingerprint: account
|
||||
.root_ca_cert
|
||||
.digest(MessageDigest::sha256())
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|x| format!("{x:X}"))
|
||||
.join(":"),
|
||||
system_start_time: Utc::now().to_rfc3339(),
|
||||
},
|
||||
package_data: AllPackageData::default(),
|
||||
recovered_packages: BTreeMap::new(),
|
||||
ui: Value::Object(Default::default()),
|
||||
ui: serde_json::from_str(include_str!("../../../frontend/patchdb-ui-seed.json"))
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -83,6 +97,7 @@ impl DatabaseModel {
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct ServerInfo {
|
||||
pub id: String,
|
||||
pub hostname: Option<String>,
|
||||
pub version: Version,
|
||||
pub last_backup: Option<DateTime<Utc>>,
|
||||
/// Used in the wifi to determine the region to set the system to
|
||||
@@ -91,12 +106,38 @@ pub struct ServerInfo {
|
||||
pub lan_address: Url,
|
||||
pub tor_address: Url,
|
||||
#[model]
|
||||
pub ip_info: BTreeMap<String, IpInfo>,
|
||||
#[model]
|
||||
#[serde(default)]
|
||||
pub status_info: ServerStatus,
|
||||
pub wifi: WifiInfo,
|
||||
pub unread_notification_count: u64,
|
||||
pub connection_addresses: ConnectionAddresses,
|
||||
pub password_hash: String,
|
||||
pub pubkey: String,
|
||||
pub ca_fingerprint: String,
|
||||
pub system_start_time: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, HasModel)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct IpInfo {
|
||||
pub ipv4_range: Option<Ipv4Net>,
|
||||
pub ipv4: Option<Ipv4Addr>,
|
||||
pub ipv6_range: Option<Ipv6Net>,
|
||||
pub ipv6: Option<Ipv6Addr>,
|
||||
}
|
||||
impl IpInfo {
|
||||
pub async fn for_interface(iface: &str) -> Result<Self, Error> {
|
||||
let (ipv4, ipv4_range) = get_iface_ipv4_addr(iface).await?.unzip();
|
||||
let (ipv6, ipv6_range) = get_iface_ipv6_addr(iface).await?.unzip();
|
||||
Ok(Self {
|
||||
ipv4_range,
|
||||
ipv4,
|
||||
ipv6_range,
|
||||
ipv6,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize, HasModel)]
|
||||
@@ -238,6 +279,15 @@ impl PackageDataEntry {
|
||||
PackageDataEntry::Installed { manifest, .. } => manifest,
|
||||
}
|
||||
}
|
||||
pub fn manifest_borrow(&self) -> &Manifest {
|
||||
match self {
|
||||
PackageDataEntry::Installing { manifest, .. } => manifest,
|
||||
PackageDataEntry::Updating { manifest, .. } => manifest,
|
||||
PackageDataEntry::Restoring { manifest, .. } => manifest,
|
||||
PackageDataEntry::Removing { manifest, .. } => manifest,
|
||||
PackageDataEntry::Installed { manifest, .. } => manifest,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl PackageDataEntryModel {
|
||||
pub fn installed(self) -> OptionModel<InstalledPackageDataEntry> {
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use patch_db::Revision;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct WithRevision<T> {
|
||||
pub response: T,
|
||||
pub revision: Option<Arc<Revision>>,
|
||||
}
|
||||
@@ -19,6 +19,7 @@ use crate::config::spec::PackagePointerSpec;
|
||||
use crate::config::{not_found, Config, ConfigReceipts, ConfigSpec};
|
||||
use crate::context::RpcContext;
|
||||
use crate::db::model::{CurrentDependencies, CurrentDependents, InstalledPackageDataEntry};
|
||||
use crate::procedure::docker::DockerContainers;
|
||||
use crate::procedure::{NoOutput, PackageProcedure, ProcedureName};
|
||||
use crate::s9pk::manifest::{Manifest, PackageId};
|
||||
use crate::status::health_check::{HealthCheckId, HealthCheckResult};
|
||||
@@ -63,6 +64,7 @@ pub struct TryHealReceipts {
|
||||
manifest_version: LockReceipt<Version, String>,
|
||||
current_dependencies: LockReceipt<CurrentDependencies, String>,
|
||||
dependency_errors: LockReceipt<DependencyErrors, String>,
|
||||
docker_containers: LockReceipt<DockerContainers, String>,
|
||||
}
|
||||
|
||||
impl TryHealReceipts {
|
||||
@@ -110,6 +112,13 @@ impl TryHealReceipts {
|
||||
.map(|x| x.status().dependency_errors())
|
||||
.make_locker(LockType::Write)
|
||||
.add_to_keys(locks);
|
||||
let docker_containers = crate::db::DatabaseModel::new()
|
||||
.package_data()
|
||||
.star()
|
||||
.installed()
|
||||
.and_then(|x| x.manifest().containers())
|
||||
.make_locker(LockType::Write)
|
||||
.add_to_keys(locks);
|
||||
move |skeleton_key| {
|
||||
Ok(Self {
|
||||
status: status.verify(skeleton_key)?,
|
||||
@@ -117,6 +126,7 @@ impl TryHealReceipts {
|
||||
current_dependencies: current_dependencies.verify(skeleton_key)?,
|
||||
manifest: manifest.verify(skeleton_key)?,
|
||||
dependency_errors: dependency_errors.verify(skeleton_key)?,
|
||||
docker_containers: docker_containers.verify(skeleton_key)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -181,7 +191,7 @@ impl DependencyError {
|
||||
(DependencyError::Transitive, _) => DependencyError::Transitive,
|
||||
}
|
||||
}
|
||||
#[instrument(skip(ctx, db, receipts))]
|
||||
#[instrument(skip_all)]
|
||||
pub fn try_heal<'a, Db: DbHandle>(
|
||||
self,
|
||||
ctx: &'a RpcContext,
|
||||
@@ -193,6 +203,7 @@ impl DependencyError {
|
||||
receipts: &'a TryHealReceipts,
|
||||
) -> BoxFuture<'a, Result<Option<Self>, Error>> {
|
||||
async move {
|
||||
let container = receipts.docker_containers.get(db, id).await?;
|
||||
Ok(match self {
|
||||
DependencyError::NotInstalled => {
|
||||
if receipts.status.get(db, dependency).await?.is_some() {
|
||||
@@ -254,6 +265,7 @@ impl DependencyError {
|
||||
if let Err(error) = cfg_req
|
||||
.check(
|
||||
ctx,
|
||||
&container,
|
||||
id,
|
||||
&dependent_manifest.version,
|
||||
&dependent_manifest.volumes,
|
||||
@@ -494,6 +506,7 @@ impl DependencyConfig {
|
||||
pub async fn check(
|
||||
&self,
|
||||
ctx: &RpcContext,
|
||||
container: &Option<DockerContainers>,
|
||||
dependent_id: &PackageId,
|
||||
dependent_version: &Version,
|
||||
dependent_volumes: &Volumes,
|
||||
@@ -503,6 +516,7 @@ impl DependencyConfig {
|
||||
Ok(self
|
||||
.check
|
||||
.sandboxed(
|
||||
container,
|
||||
ctx,
|
||||
dependent_id,
|
||||
dependent_version,
|
||||
@@ -517,6 +531,7 @@ impl DependencyConfig {
|
||||
pub async fn auto_configure(
|
||||
&self,
|
||||
ctx: &RpcContext,
|
||||
container: &Option<DockerContainers>,
|
||||
dependent_id: &PackageId,
|
||||
dependent_version: &Version,
|
||||
dependent_volumes: &Volumes,
|
||||
@@ -524,6 +539,7 @@ impl DependencyConfig {
|
||||
) -> Result<Config, Error> {
|
||||
self.auto_configure
|
||||
.sandboxed(
|
||||
container,
|
||||
ctx,
|
||||
dependent_id,
|
||||
dependent_version,
|
||||
@@ -545,6 +561,7 @@ pub struct DependencyConfigReceipts {
|
||||
dependency_config_action: LockReceipt<ConfigActions, ()>,
|
||||
package_volumes: LockReceipt<Volumes, ()>,
|
||||
package_version: LockReceipt<Version, ()>,
|
||||
docker_containers: LockReceipt<DockerContainers, String>,
|
||||
}
|
||||
|
||||
impl DependencyConfigReceipts {
|
||||
@@ -607,6 +624,13 @@ impl DependencyConfigReceipts {
|
||||
.map(|x| x.manifest().version())
|
||||
.make_locker(LockType::Write)
|
||||
.add_to_keys(locks);
|
||||
let docker_containers = crate::db::DatabaseModel::new()
|
||||
.package_data()
|
||||
.star()
|
||||
.installed()
|
||||
.and_then(|x| x.manifest().containers())
|
||||
.make_locker(LockType::Write)
|
||||
.add_to_keys(locks);
|
||||
move |skeleton_key| {
|
||||
Ok(Self {
|
||||
config: config(skeleton_key)?,
|
||||
@@ -616,6 +640,7 @@ impl DependencyConfigReceipts {
|
||||
dependency_config_action: dependency_config_action.verify(&skeleton_key)?,
|
||||
package_volumes: package_volumes.verify(&skeleton_key)?,
|
||||
package_version: package_version.verify(&skeleton_key)?,
|
||||
docker_containers: docker_containers.verify(&skeleton_key)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -668,7 +693,7 @@ pub struct ConfigDryRes {
|
||||
}
|
||||
|
||||
#[command(rename = "dry", display(display_serializable))]
|
||||
#[instrument(skip(ctx))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn configure_dry(
|
||||
#[context] ctx: RpcContext,
|
||||
#[parent_data] (pkg_id, dependency_id): (PackageId, PackageId),
|
||||
@@ -690,6 +715,7 @@ pub async fn configure_logic(
|
||||
let dependency_version = receipts.dependency_version.get(db).await?;
|
||||
let dependency_volumes = receipts.dependency_volumes.get(db).await?;
|
||||
let dependencies = receipts.dependencies.get(db).await?;
|
||||
let pkg_docker_container = receipts.docker_containers.get(db, &*pkg_id).await?;
|
||||
|
||||
let dependency = dependencies
|
||||
.0
|
||||
@@ -740,6 +766,7 @@ pub async fn configure_logic(
|
||||
let new_config = dependency
|
||||
.auto_configure
|
||||
.sandboxed(
|
||||
&pkg_docker_container,
|
||||
&ctx,
|
||||
&pkg_id,
|
||||
&pkg_version,
|
||||
@@ -757,7 +784,7 @@ pub async fn configure_logic(
|
||||
spec,
|
||||
})
|
||||
}
|
||||
#[instrument(skip(db, current_dependencies, current_dependent_receipt))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn add_dependent_to_current_dependents_lists<'a, Db: DbHandle>(
|
||||
db: &mut Db,
|
||||
dependent_id: &PackageId,
|
||||
@@ -892,7 +919,7 @@ impl BreakTransitiveReceipts {
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(db, receipts))]
|
||||
#[instrument(skip_all)]
|
||||
pub fn break_transitive<'a, Db: DbHandle>(
|
||||
db: &'a mut Db,
|
||||
id: &'a PackageId,
|
||||
@@ -959,7 +986,7 @@ pub fn break_transitive<'a, Db: DbHandle>(
|
||||
.boxed()
|
||||
}
|
||||
|
||||
#[instrument(skip(ctx, db, locks))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn heal_all_dependents_transitive<'a, Db: DbHandle>(
|
||||
ctx: &'a RpcContext,
|
||||
db: &'a mut Db,
|
||||
@@ -977,7 +1004,7 @@ pub async fn heal_all_dependents_transitive<'a, Db: DbHandle>(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(ctx, db, receipts))]
|
||||
#[instrument(skip_all)]
|
||||
pub fn heal_transitive<'a, Db: DbHandle>(
|
||||
ctx: &'a RpcContext,
|
||||
db: &'a mut Db,
|
||||
|
||||
@@ -12,7 +12,7 @@ use crate::util::display_none;
|
||||
use crate::{Error, ResultExt};
|
||||
|
||||
#[command(cli_only, blocking, display(display_none))]
|
||||
#[instrument(skip(ctx))]
|
||||
#[instrument(skip_all)]
|
||||
pub fn init(#[context] ctx: SdkContext) -> Result<(), Error> {
|
||||
if !ctx.developer_key_path.exists() {
|
||||
let parent = ctx.developer_key_path.parent().unwrap_or(Path::new("/"));
|
||||
|
||||
@@ -6,14 +6,15 @@ use rpc_toolkit::yajrc::RpcError;
|
||||
|
||||
use crate::context::DiagnosticContext;
|
||||
use crate::disk::repair;
|
||||
use crate::logs::{display_logs, fetch_logs, LogResponse, LogSource};
|
||||
use crate::init::SYSTEM_REBUILD_PATH;
|
||||
use crate::logs::{fetch_logs, LogResponse, LogSource};
|
||||
use crate::shutdown::Shutdown;
|
||||
use crate::util::display_none;
|
||||
use crate::Error;
|
||||
|
||||
pub const SYSTEMD_UNIT: &'static str = "embassy-init";
|
||||
|
||||
#[command(subcommands(error, logs, exit, restart, forget_disk, disk))]
|
||||
#[command(subcommands(error, logs, exit, restart, forget_disk, disk, rebuild))]
|
||||
pub fn diagnostic() -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
@@ -23,19 +24,13 @@ pub fn error(#[context] ctx: DiagnosticContext) -> Result<Arc<RpcError>, Error>
|
||||
Ok(ctx.error.clone())
|
||||
}
|
||||
|
||||
#[command(display(display_logs))]
|
||||
#[command(rpc_only)]
|
||||
pub async fn logs(
|
||||
#[arg] limit: Option<usize>,
|
||||
#[arg] cursor: Option<String>,
|
||||
#[arg] before_flag: Option<bool>,
|
||||
#[arg] before: bool,
|
||||
) -> Result<LogResponse, Error> {
|
||||
Ok(fetch_logs(
|
||||
LogSource::Service(SYSTEMD_UNIT),
|
||||
limit,
|
||||
cursor,
|
||||
before_flag.unwrap_or(false),
|
||||
)
|
||||
.await?)
|
||||
Ok(fetch_logs(LogSource::Service(SYSTEMD_UNIT), limit, cursor, before).await?)
|
||||
}
|
||||
|
||||
#[command(display(display_none))]
|
||||
@@ -50,13 +45,18 @@ pub fn restart(#[context] ctx: DiagnosticContext) -> Result<(), Error> {
|
||||
.send(Some(Shutdown {
|
||||
datadir: ctx.datadir.clone(),
|
||||
disk_guid: ctx.disk_guid.clone(),
|
||||
db_handle: None,
|
||||
restart: true,
|
||||
}))
|
||||
.expect("receiver dropped");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command(display(display_none))]
|
||||
pub async fn rebuild(#[context] ctx: DiagnosticContext) -> Result<(), Error> {
|
||||
tokio::fs::write(SYSTEM_REBUILD_PATH, b"").await?;
|
||||
restart(ctx)
|
||||
}
|
||||
|
||||
#[command(subcommands(forget_disk, repair))]
|
||||
pub fn disk() -> Result<(), Error> {
|
||||
Ok(())
|
||||
@@ -64,7 +64,7 @@ pub fn disk() -> Result<(), Error> {
|
||||
|
||||
#[command(rename = "forget", display(display_none))]
|
||||
pub async fn forget_disk() -> Result<(), Error> {
|
||||
let disk_guid = Path::new("/embassy-os/disk.guid");
|
||||
let disk_guid = Path::new("/media/embassy/config/disk.guid");
|
||||
if tokio::fs::metadata(disk_guid).await.is_ok() {
|
||||
tokio::fs::remove_file(disk_guid).await?;
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ impl RepairStrategy {
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn e2fsck_preen(
|
||||
logicalname: impl AsRef<Path> + std::fmt::Debug,
|
||||
) -> Result<RequiresReboot, Error> {
|
||||
@@ -59,11 +59,11 @@ fn backup_existing_undo_file<'a>(path: &'a Path) -> BoxFuture<'a, Result<(), Err
|
||||
.boxed()
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn e2fsck_aggressive(
|
||||
logicalname: impl AsRef<Path> + std::fmt::Debug,
|
||||
) -> Result<RequiresReboot, Error> {
|
||||
let undo_path = Path::new("/embassy-os")
|
||||
let undo_path = Path::new("/media/embassy/config")
|
||||
.join(
|
||||
logicalname
|
||||
.as_ref()
|
||||
|
||||
@@ -17,7 +17,7 @@ pub const PASSWORD_PATH: &'static str = "/etc/embassy/password";
|
||||
pub const DEFAULT_PASSWORD: &'static str = "password";
|
||||
pub const MAIN_FS_SIZE: FsSize = FsSize::Gigabytes(8);
|
||||
|
||||
#[instrument(skip(disks, datadir, password))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn create<I, P>(
|
||||
disks: &I,
|
||||
pvscan: &BTreeMap<PathBuf, Option<String>>,
|
||||
@@ -34,7 +34,7 @@ where
|
||||
Ok(guid)
|
||||
}
|
||||
|
||||
#[instrument(skip(disks))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn create_pool<I, P>(
|
||||
disks: &I,
|
||||
pvscan: &BTreeMap<PathBuf, Option<String>>,
|
||||
@@ -84,7 +84,7 @@ pub enum FsSize {
|
||||
FreePercentage(usize),
|
||||
}
|
||||
|
||||
#[instrument(skip(datadir, password))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn create_fs<P: AsRef<Path>>(
|
||||
guid: &str,
|
||||
datadir: P,
|
||||
@@ -139,7 +139,7 @@ pub async fn create_fs<P: AsRef<Path>>(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(datadir, password))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn create_all_fs<P: AsRef<Path>>(
|
||||
guid: &str,
|
||||
datadir: P,
|
||||
@@ -157,7 +157,7 @@ pub async fn create_all_fs<P: AsRef<Path>>(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(datadir))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn unmount_fs<P: AsRef<Path>>(guid: &str, datadir: P, name: &str) -> Result<(), Error> {
|
||||
unmount(datadir.as_ref().join(name)).await?;
|
||||
Command::new("cryptsetup")
|
||||
@@ -170,7 +170,7 @@ pub async fn unmount_fs<P: AsRef<Path>>(guid: &str, datadir: P, name: &str) -> R
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(datadir))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn unmount_all_fs<P: AsRef<Path>>(guid: &str, datadir: P) -> Result<(), Error> {
|
||||
unmount_fs(guid, &datadir, "main").await?;
|
||||
unmount_fs(guid, &datadir, "package-data").await?;
|
||||
@@ -181,7 +181,7 @@ pub async fn unmount_all_fs<P: AsRef<Path>>(guid: &str, datadir: P) -> Result<()
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(datadir))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn export<P: AsRef<Path>>(guid: &str, datadir: P) -> Result<(), Error> {
|
||||
Command::new("sync").invoke(ErrorKind::Filesystem).await?;
|
||||
unmount_all_fs(guid, datadir).await?;
|
||||
@@ -197,7 +197,7 @@ pub async fn export<P: AsRef<Path>>(guid: &str, datadir: P) -> Result<(), Error>
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(datadir, password))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn import<P: AsRef<Path>>(
|
||||
guid: &str,
|
||||
datadir: P,
|
||||
@@ -254,7 +254,7 @@ pub async fn import<P: AsRef<Path>>(
|
||||
mount_all_fs(guid, datadir, repair, password).await
|
||||
}
|
||||
|
||||
#[instrument(skip(datadir, password))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn mount_fs<P: AsRef<Path>>(
|
||||
guid: &str,
|
||||
datadir: P,
|
||||
@@ -285,7 +285,7 @@ pub async fn mount_fs<P: AsRef<Path>>(
|
||||
Ok(reboot)
|
||||
}
|
||||
|
||||
#[instrument(skip(datadir, password))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn mount_all_fs<P: AsRef<Path>>(
|
||||
guid: &str,
|
||||
datadir: P,
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use clap::ArgMatches;
|
||||
use rpc_toolkit::command;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use self::util::DiskListResponse;
|
||||
use crate::context::RpcContext;
|
||||
use crate::disk::util::DiskInfo;
|
||||
use crate::util::display_none;
|
||||
use crate::util::serde::{display_serializable, IoFormat};
|
||||
use crate::Error;
|
||||
@@ -9,18 +13,35 @@ use crate::Error;
|
||||
pub mod fsck;
|
||||
pub mod main;
|
||||
pub mod mount;
|
||||
pub mod quirks;
|
||||
pub mod util;
|
||||
|
||||
pub const BOOT_RW_PATH: &str = "/media/boot-rw";
|
||||
pub const REPAIR_DISK_PATH: &str = "/embassy-os/repair-disk";
|
||||
pub const REPAIR_DISK_PATH: &str = "/media/embassy/config/repair-disk";
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct OsPartitionInfo {
|
||||
pub efi: Option<PathBuf>,
|
||||
pub boot: PathBuf,
|
||||
pub root: PathBuf,
|
||||
}
|
||||
impl OsPartitionInfo {
|
||||
pub fn contains(&self, logicalname: impl AsRef<Path>) -> bool {
|
||||
self.efi
|
||||
.as_ref()
|
||||
.map(|p| p == logicalname.as_ref())
|
||||
.unwrap_or(false)
|
||||
|| &*self.boot == logicalname.as_ref()
|
||||
|| &*self.root == logicalname.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
#[command(subcommands(list, repair))]
|
||||
pub fn disk() -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn display_disk_info(info: DiskListResponse, matches: &ArgMatches) {
|
||||
fn display_disk_info(info: Vec<DiskInfo>, matches: &ArgMatches) {
|
||||
use prettytable::*;
|
||||
|
||||
if matches.is_present("format") {
|
||||
@@ -35,7 +56,7 @@ fn display_disk_info(info: DiskListResponse, matches: &ArgMatches) {
|
||||
"USED",
|
||||
"EMBASSY OS VERSION"
|
||||
]);
|
||||
for disk in info.disks {
|
||||
for disk in info {
|
||||
let row = row![
|
||||
disk.logicalname.display(),
|
||||
"N/A",
|
||||
@@ -71,16 +92,17 @@ fn display_disk_info(info: DiskListResponse, matches: &ArgMatches) {
|
||||
table.add_row(row);
|
||||
}
|
||||
}
|
||||
table.print_tty(false);
|
||||
table.print_tty(false).unwrap();
|
||||
}
|
||||
|
||||
#[command(display(display_disk_info))]
|
||||
pub async fn list(
|
||||
#[context] ctx: RpcContext,
|
||||
#[allow(unused_variables)]
|
||||
#[arg]
|
||||
format: Option<IoFormat>,
|
||||
) -> Result<DiskListResponse, Error> {
|
||||
crate::disk::util::list().await
|
||||
) -> Result<Vec<DiskInfo>, Error> {
|
||||
crate::disk::util::list(&ctx.os_partitions).await
|
||||
}
|
||||
|
||||
#[command(display(display_none))]
|
||||
|
||||
@@ -35,7 +35,7 @@ impl<G: GenericMountGuard> BackupMountGuard<G> {
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(password))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn mount(backup_disk_mount_guard: G, password: &str) -> Result<Self, Error> {
|
||||
let backup_disk_path = backup_disk_mount_guard.as_ref();
|
||||
let unencrypted_metadata_path =
|
||||
@@ -145,7 +145,7 @@ impl<G: GenericMountGuard> BackupMountGuard<G> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn mount_package_backup(
|
||||
&self,
|
||||
id: &PackageId,
|
||||
@@ -159,7 +159,7 @@ impl<G: GenericMountGuard> BackupMountGuard<G> {
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn save(&self) -> Result<(), Error> {
|
||||
let metadata_path = self.as_ref().join("metadata.cbor");
|
||||
let backup_disk_path = self.backup_disk_path();
|
||||
@@ -180,7 +180,7 @@ impl<G: GenericMountGuard> BackupMountGuard<G> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn unmount(mut self) -> Result<(), Error> {
|
||||
if let Some(guard) = self.encrypted_guard.take() {
|
||||
guard.unmount().await?;
|
||||
@@ -191,7 +191,7 @@ impl<G: GenericMountGuard> BackupMountGuard<G> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn save_and_unmount(self) -> Result<(), Error> {
|
||||
self.save().await?;
|
||||
self.unmount().await?;
|
||||
|
||||
54
backend/src/disk/mount/filesystem/bind.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
use std::path::Path;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use digest::generic_array::GenericArray;
|
||||
use digest::{Digest, OutputSizeUser};
|
||||
use sha2::Sha256;
|
||||
|
||||
use super::{FileSystem, MountType, ReadOnly};
|
||||
use crate::disk::mount::util::bind;
|
||||
use crate::{Error, ResultExt};
|
||||
|
||||
pub struct Bind<SrcDir: AsRef<Path>> {
|
||||
src_dir: SrcDir,
|
||||
}
|
||||
impl<SrcDir: AsRef<Path>> Bind<SrcDir> {
|
||||
pub fn new(src_dir: SrcDir) -> Self {
|
||||
Self { src_dir }
|
||||
}
|
||||
}
|
||||
#[async_trait]
|
||||
impl<SrcDir: AsRef<Path> + Send + Sync> FileSystem for Bind<SrcDir> {
|
||||
async fn mount<P: AsRef<Path> + Send + Sync>(
|
||||
&self,
|
||||
mountpoint: P,
|
||||
mount_type: MountType,
|
||||
) -> Result<(), Error> {
|
||||
bind(
|
||||
self.src_dir.as_ref(),
|
||||
mountpoint,
|
||||
matches!(mount_type, ReadOnly),
|
||||
)
|
||||
.await
|
||||
}
|
||||
async fn source_hash(
|
||||
&self,
|
||||
) -> Result<GenericArray<u8, <Sha256 as OutputSizeUser>::OutputSize>, Error> {
|
||||
let mut sha = Sha256::new();
|
||||
sha.update("Bind");
|
||||
sha.update(
|
||||
tokio::fs::canonicalize(self.src_dir.as_ref())
|
||||
.await
|
||||
.with_ctx(|_| {
|
||||
(
|
||||
crate::ErrorKind::Filesystem,
|
||||
self.src_dir.as_ref().display().to_string(),
|
||||
)
|
||||
})?
|
||||
.as_os_str()
|
||||
.as_bytes(),
|
||||
);
|
||||
Ok(sha.finalize())
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ async fn resolve_hostname(hostname: &str) -> Result<IpAddr, Error> {
|
||||
.parse()?)
|
||||
}
|
||||
|
||||
#[instrument(skip(path, password, mountpoint))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn mount_cifs(
|
||||
hostname: &str,
|
||||
path: impl AsRef<Path>,
|
||||
|
||||
40
backend/src/disk/mount/filesystem/efivarfs.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
use std::path::Path;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use digest::generic_array::GenericArray;
|
||||
use digest::{Digest, OutputSizeUser};
|
||||
use sha2::Sha256;
|
||||
|
||||
use super::{FileSystem, MountType, ReadOnly};
|
||||
use crate::util::Invoke;
|
||||
use crate::{Error, ResultExt};
|
||||
|
||||
pub struct EfiVarFs;
|
||||
#[async_trait]
|
||||
impl FileSystem for EfiVarFs {
|
||||
async fn mount<P: AsRef<Path> + Send + Sync>(
|
||||
&self,
|
||||
mountpoint: P,
|
||||
mount_type: MountType,
|
||||
) -> Result<(), Error> {
|
||||
tokio::fs::create_dir_all(mountpoint.as_ref()).await?;
|
||||
let mut cmd = tokio::process::Command::new("mount");
|
||||
cmd.arg("-t")
|
||||
.arg("efivarfs")
|
||||
.arg("efivarfs")
|
||||
.arg(mountpoint.as_ref());
|
||||
if mount_type == ReadOnly {
|
||||
cmd.arg("-o").arg("ro");
|
||||
}
|
||||
cmd.invoke(crate::ErrorKind::Filesystem).await?;
|
||||
Ok(())
|
||||
}
|
||||
async fn source_hash(
|
||||
&self,
|
||||
) -> Result<GenericArray<u8, <Sha256 as OutputSizeUser>::OutputSize>, Error> {
|
||||
let mut sha = Sha256::new();
|
||||
sha.update("EfiVarFs");
|
||||
Ok(sha.finalize())
|
||||
}
|
||||
}
|
||||
52
backend/src/disk/mount/filesystem/httpdirfs.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
use std::path::Path;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use digest::generic_array::GenericArray;
|
||||
use digest::{Digest, OutputSizeUser};
|
||||
use reqwest::Url;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::Sha256;
|
||||
|
||||
use super::{FileSystem, MountType};
|
||||
use crate::util::Invoke;
|
||||
use crate::Error;
|
||||
|
||||
pub async fn mount_httpdirfs(url: &Url, mountpoint: impl AsRef<Path>) -> Result<(), Error> {
|
||||
tokio::fs::create_dir_all(mountpoint.as_ref()).await?;
|
||||
let mut cmd = tokio::process::Command::new("httpdirfs");
|
||||
cmd.arg("--cache")
|
||||
.arg("--single-file-mode")
|
||||
.arg(url.as_str())
|
||||
.arg(mountpoint.as_ref());
|
||||
cmd.invoke(crate::ErrorKind::Filesystem).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct HttpDirFS {
|
||||
url: Url,
|
||||
}
|
||||
impl HttpDirFS {
|
||||
pub fn new(url: Url) -> Self {
|
||||
HttpDirFS { url }
|
||||
}
|
||||
}
|
||||
#[async_trait]
|
||||
impl FileSystem for HttpDirFS {
|
||||
async fn mount<P: AsRef<Path> + Send + Sync>(
|
||||
&self,
|
||||
mountpoint: P,
|
||||
_mount_type: MountType,
|
||||
) -> Result<(), Error> {
|
||||
mount_httpdirfs(&self.url, mountpoint).await
|
||||
}
|
||||
async fn source_hash(
|
||||
&self,
|
||||
) -> Result<GenericArray<u8, <Sha256 as OutputSizeUser>::OutputSize>, Error> {
|
||||
let mut sha = Sha256::new();
|
||||
sha.update("HttpDirFS");
|
||||
sha.update(self.url.as_str());
|
||||
Ok(sha.finalize())
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,12 @@ use sha2::Sha256;
|
||||
|
||||
use crate::Error;
|
||||
|
||||
pub mod bind;
|
||||
pub mod block_dev;
|
||||
pub mod cifs;
|
||||
pub mod ecryptfs;
|
||||
pub mod efivarfs;
|
||||
pub mod httpdirfs;
|
||||
pub mod label;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Weak};
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use models::ResultExt;
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::instrument;
|
||||
|
||||
@@ -11,7 +12,7 @@ use super::util::unmount;
|
||||
use crate::util::Invoke;
|
||||
use crate::Error;
|
||||
|
||||
pub const TMP_MOUNTPOINT: &'static str = "/media/embassy-os/tmp";
|
||||
pub const TMP_MOUNTPOINT: &'static str = "/media/embassy/tmp";
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait GenericMountGuard: AsRef<Path> + std::fmt::Debug + Send + Sync + 'static {
|
||||
@@ -36,9 +37,21 @@ impl MountGuard {
|
||||
mounted: true,
|
||||
})
|
||||
}
|
||||
pub async fn unmount(mut self) -> Result<(), Error> {
|
||||
pub async fn unmount(mut self, delete_mountpoint: bool) -> Result<(), Error> {
|
||||
if self.mounted {
|
||||
unmount(&self.mountpoint).await?;
|
||||
if delete_mountpoint {
|
||||
match tokio::fs::remove_dir(&self.mountpoint).await {
|
||||
Err(e) if e.raw_os_error() == Some(39) => Ok(()), // directory not empty
|
||||
a => a,
|
||||
}
|
||||
.with_ctx(|_| {
|
||||
(
|
||||
crate::ErrorKind::Filesystem,
|
||||
format!("rm {}", self.mountpoint.display()),
|
||||
)
|
||||
})?;
|
||||
}
|
||||
self.mounted = false;
|
||||
}
|
||||
Ok(())
|
||||
@@ -60,7 +73,7 @@ impl Drop for MountGuard {
|
||||
#[async_trait::async_trait]
|
||||
impl GenericMountGuard for MountGuard {
|
||||
async fn unmount(mut self) -> Result<(), Error> {
|
||||
MountGuard::unmount(self).await
|
||||
MountGuard::unmount(self, false).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +95,7 @@ pub struct TmpMountGuard {
|
||||
}
|
||||
impl TmpMountGuard {
|
||||
/// DRAGONS: if you try to mount something as ro and rw at the same time, the ro mount will be upgraded to rw.
|
||||
#[instrument(skip(filesystem))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn mount(filesystem: &impl FileSystem, mount_type: MountType) -> Result<Self, Error> {
|
||||
let mountpoint = tmp_mountpoint(filesystem).await?;
|
||||
let mut tmp_mounts = TMP_MOUNTS.lock().await;
|
||||
@@ -111,7 +124,7 @@ impl TmpMountGuard {
|
||||
}
|
||||
pub async fn unmount(self) -> Result<(), Error> {
|
||||
if let Ok(guard) = Arc::try_unwrap(self.guard) {
|
||||
guard.unmount().await?;
|
||||
guard.unmount(true).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use tracing::instrument;
|
||||
use crate::util::Invoke;
|
||||
use crate::{Error, ResultExt};
|
||||
|
||||
#[instrument(skip(src, dst))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn bind<P0: AsRef<Path>, P1: AsRef<Path>>(
|
||||
src: P0,
|
||||
dst: P1,
|
||||
@@ -40,7 +40,7 @@ pub async fn bind<P0: AsRef<Path>, P1: AsRef<Path>>(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(mountpoint))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn unmount<P: AsRef<Path>>(mountpoint: P) -> Result<(), Error> {
|
||||
tracing::debug!("Unmounting {}.", mountpoint.as_ref().display());
|
||||
tokio::process::Command::new("umount")
|
||||
@@ -48,13 +48,5 @@ pub async fn unmount<P: AsRef<Path>>(mountpoint: P) -> Result<(), Error> {
|
||||
.arg(mountpoint.as_ref())
|
||||
.invoke(crate::ErrorKind::Filesystem)
|
||||
.await?;
|
||||
tokio::fs::remove_dir_all(mountpoint.as_ref())
|
||||
.await
|
||||
.with_ctx(|_| {
|
||||
(
|
||||
crate::ErrorKind::Filesystem,
|
||||
format!("rm {}", mountpoint.as_ref().display()),
|
||||
)
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,172 +0,0 @@
|
||||
use std::collections::BTreeSet;
|
||||
use std::num::ParseIntError;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use color_eyre::eyre::eyre;
|
||||
use helpers::AtomicFile;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tracing::instrument;
|
||||
|
||||
use super::BOOT_RW_PATH;
|
||||
use crate::{Error, ErrorKind, ResultExt};
|
||||
|
||||
pub const QUIRK_PATH: &'static str = "/sys/module/usb_storage/parameters/quirks";
|
||||
|
||||
pub const WHITELIST: [(VendorId, ProductId); 5] = [
|
||||
(VendorId(0x1d6b), ProductId(0x0002)), // root hub usb2
|
||||
(VendorId(0x1d6b), ProductId(0x0003)), // root hub usb3
|
||||
(VendorId(0x2109), ProductId(0x3431)),
|
||||
(VendorId(0x1058), ProductId(0x262f)), // western digital black HDD
|
||||
(VendorId(0x04e8), ProductId(0x4001)), // Samsung T7
|
||||
];
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct VendorId(u16);
|
||||
impl std::str::FromStr for VendorId {
|
||||
type Err = ParseIntError;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
u16::from_str_radix(s.trim(), 16).map(VendorId)
|
||||
}
|
||||
}
|
||||
impl std::fmt::Display for VendorId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:04x}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct ProductId(u16);
|
||||
impl std::str::FromStr for ProductId {
|
||||
type Err = ParseIntError;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
u16::from_str_radix(s.trim(), 16).map(ProductId)
|
||||
}
|
||||
}
|
||||
impl std::fmt::Display for ProductId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:04x}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Quirks(BTreeSet<(VendorId, ProductId)>);
|
||||
impl Quirks {
|
||||
pub fn add(&mut self, vendor: VendorId, product: ProductId) {
|
||||
self.0.insert((vendor, product));
|
||||
}
|
||||
pub fn remove(&mut self, vendor: VendorId, product: ProductId) {
|
||||
self.0.remove(&(vendor, product));
|
||||
}
|
||||
pub fn contains(&self, vendor: VendorId, product: ProductId) -> bool {
|
||||
self.0.contains(&(vendor, product))
|
||||
}
|
||||
}
|
||||
impl std::fmt::Display for Quirks {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut comma = false;
|
||||
for (vendor, product) in &self.0 {
|
||||
if comma {
|
||||
write!(f, ",")?;
|
||||
} else {
|
||||
comma = true;
|
||||
}
|
||||
write!(f, "{}:{}:u", vendor, product)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl std::str::FromStr for Quirks {
|
||||
type Err = Error;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let s = s.trim();
|
||||
let mut quirks = BTreeSet::new();
|
||||
for item in s.split(",") {
|
||||
if let [vendor, product, "u"] = item.splitn(3, ":").collect::<Vec<_>>().as_slice() {
|
||||
quirks.insert((vendor.parse()?, product.parse()?));
|
||||
} else {
|
||||
return Err(Error::new(
|
||||
eyre!("Invalid quirk: `{}`", item),
|
||||
crate::ErrorKind::DiskManagement,
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(Quirks(quirks))
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
pub async fn update_quirks(quirks: &mut Quirks) -> Result<Vec<String>, Error> {
|
||||
let mut usb_devices = tokio::fs::read_dir("/sys/bus/usb/devices/").await?;
|
||||
let mut to_reconnect = Vec::new();
|
||||
while let Some(usb_device) = usb_devices.next_entry().await? {
|
||||
if tokio::fs::metadata(usb_device.path().join("idVendor"))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let vendor = tokio::fs::read_to_string(usb_device.path().join("idVendor"))
|
||||
.await?
|
||||
.parse()?;
|
||||
let product = tokio::fs::read_to_string(usb_device.path().join("idProduct"))
|
||||
.await?
|
||||
.parse()?;
|
||||
if WHITELIST.contains(&(vendor, product)) {
|
||||
quirks.remove(vendor, product);
|
||||
continue;
|
||||
}
|
||||
if quirks.contains(vendor, product) {
|
||||
continue;
|
||||
}
|
||||
quirks.add(vendor, product);
|
||||
{
|
||||
// write quirks to sysfs
|
||||
let mut quirk_file = tokio::fs::File::create(QUIRK_PATH).await?;
|
||||
quirk_file.write_all(quirks.to_string().as_bytes()).await?;
|
||||
quirk_file.sync_all().await?;
|
||||
drop(quirk_file);
|
||||
}
|
||||
|
||||
disconnect_usb(usb_device.path()).await?;
|
||||
let (vendor_name, product_name) = tokio::try_join!(
|
||||
tokio::fs::read_to_string(usb_device.path().join("manufacturer")),
|
||||
tokio::fs::read_to_string(usb_device.path().join("product")),
|
||||
)?;
|
||||
to_reconnect.push(format!("{} {}", vendor_name, product_name));
|
||||
}
|
||||
Ok(to_reconnect)
|
||||
}
|
||||
|
||||
#[instrument(skip(usb_device_path))]
|
||||
pub async fn disconnect_usb(usb_device_path: impl AsRef<Path>) -> Result<(), Error> {
|
||||
let authorized_path = usb_device_path.as_ref().join("bConfigurationValue");
|
||||
let mut authorized_file = tokio::fs::File::create(&authorized_path).await?;
|
||||
authorized_file.write_all(b"0").await?;
|
||||
authorized_file.sync_all().await?;
|
||||
drop(authorized_file);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
pub async fn fetch_quirks() -> Result<Quirks, Error> {
|
||||
Ok(tokio::fs::read_to_string(QUIRK_PATH).await?.parse()?)
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
pub async fn save_quirks(quirks: &Quirks) -> Result<(), Error> {
|
||||
let orig_path = Path::new(BOOT_RW_PATH).join("cmdline.txt.orig");
|
||||
let target_path = Path::new(BOOT_RW_PATH).join("cmdline.txt");
|
||||
if tokio::fs::metadata(&orig_path).await.is_err() {
|
||||
tokio::fs::copy(&target_path, &orig_path).await?;
|
||||
}
|
||||
let cmdline = tokio::fs::read_to_string(&orig_path).await?;
|
||||
let mut target = AtomicFile::new(&target_path, None::<PathBuf>)
|
||||
.await
|
||||
.with_kind(ErrorKind::Filesystem)?;
|
||||
target
|
||||
.write_all(format!("usb-storage.quirks={} {}", quirks, cmdline).as_bytes())
|
||||
.await?;
|
||||
target.save().await.with_kind(ErrorKind::Filesystem)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -12,30 +12,29 @@ use nom::sequence::{pair, preceded, terminated};
|
||||
use nom::IResult;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::fs::File;
|
||||
use tokio::process::Command;
|
||||
use tracing::instrument;
|
||||
|
||||
use super::mount::filesystem::block_dev::BlockDev;
|
||||
use super::mount::filesystem::ReadOnly;
|
||||
use super::mount::guard::TmpMountGuard;
|
||||
use super::quirks::{fetch_quirks, save_quirks, update_quirks};
|
||||
use crate::util::io::from_yaml_async_reader;
|
||||
use crate::disk::OsPartitionInfo;
|
||||
use crate::util::serde::IoFormat;
|
||||
use crate::util::{Invoke, Version};
|
||||
use crate::{Error, ResultExt as _};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct DiskListResponse {
|
||||
pub disks: Vec<DiskInfo>,
|
||||
pub reconnect: Vec<String>,
|
||||
pub enum PartitionTable {
|
||||
Mbr,
|
||||
Gpt,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct DiskInfo {
|
||||
pub logicalname: PathBuf,
|
||||
pub partition_table: Option<PartitionTable>,
|
||||
pub vendor: Option<String>,
|
||||
pub model: Option<String>,
|
||||
pub partitions: Vec<PartitionInfo>,
|
||||
@@ -51,6 +50,7 @@ pub struct PartitionInfo {
|
||||
pub capacity: u64,
|
||||
pub used: Option<u64>,
|
||||
pub embassy_os: Option<EmbassyOsRecoveryInfo>,
|
||||
pub guid: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
@@ -69,7 +69,25 @@ lazy_static::lazy_static! {
|
||||
static ref PARTITION_REGEX: Regex = Regex::new("-part[0-9]+$").unwrap();
|
||||
}
|
||||
|
||||
#[instrument(skip(path))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn get_partition_table<P: AsRef<Path>>(path: P) -> Result<Option<PartitionTable>, Error> {
|
||||
Ok(String::from_utf8(
|
||||
Command::new("fdisk")
|
||||
.arg("-l")
|
||||
.arg(path.as_ref())
|
||||
.invoke(crate::ErrorKind::BlockDevice)
|
||||
.await?,
|
||||
)?
|
||||
.lines()
|
||||
.find_map(|l| l.strip_prefix("Disklabel type:"))
|
||||
.and_then(|t| match t.trim() {
|
||||
"dos" => Some(PartitionTable::Mbr),
|
||||
"gpt" => Some(PartitionTable::Gpt),
|
||||
_ => None,
|
||||
}))
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn get_vendor<P: AsRef<Path>>(path: P) -> Result<Option<String>, Error> {
|
||||
let vendor = tokio::fs::read_to_string(
|
||||
Path::new(SYS_BLOCK_PATH)
|
||||
@@ -92,7 +110,7 @@ pub async fn get_vendor<P: AsRef<Path>>(path: P) -> Result<Option<String>, Error
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(path))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn get_model<P: AsRef<Path>>(path: P) -> Result<Option<String>, Error> {
|
||||
let model = tokio::fs::read_to_string(
|
||||
Path::new(SYS_BLOCK_PATH)
|
||||
@@ -111,7 +129,7 @@ pub async fn get_model<P: AsRef<Path>>(path: P) -> Result<Option<String>, Error>
|
||||
Ok(if model.is_empty() { None } else { Some(model) })
|
||||
}
|
||||
|
||||
#[instrument(skip(path))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn get_capacity<P: AsRef<Path>>(path: P) -> Result<u64, Error> {
|
||||
Ok(String::from_utf8(
|
||||
Command::new("blockdev")
|
||||
@@ -124,7 +142,7 @@ pub async fn get_capacity<P: AsRef<Path>>(path: P) -> Result<u64, Error> {
|
||||
.parse::<u64>()?)
|
||||
}
|
||||
|
||||
#[instrument(skip(path))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn get_label<P: AsRef<Path>>(path: P) -> Result<Option<String>, Error> {
|
||||
let label = String::from_utf8(
|
||||
Command::new("lsblk")
|
||||
@@ -139,7 +157,7 @@ pub async fn get_label<P: AsRef<Path>>(path: P) -> Result<Option<String>, Error>
|
||||
Ok(if label.is_empty() { None } else { Some(label) })
|
||||
}
|
||||
|
||||
#[instrument(skip(path))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn get_used<P: AsRef<Path>>(path: P) -> Result<u64, Error> {
|
||||
Ok(String::from_utf8(
|
||||
Command::new("df")
|
||||
@@ -157,7 +175,7 @@ pub async fn get_used<P: AsRef<Path>>(path: P) -> Result<u64, Error> {
|
||||
.parse::<u64>()?)
|
||||
}
|
||||
|
||||
#[instrument(skip(path))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn get_available<P: AsRef<Path>>(path: P) -> Result<u64, Error> {
|
||||
Ok(String::from_utf8(
|
||||
Command::new("df")
|
||||
@@ -175,7 +193,7 @@ pub async fn get_available<P: AsRef<Path>>(path: P) -> Result<u64, Error> {
|
||||
.parse::<u64>()?)
|
||||
}
|
||||
|
||||
#[instrument(skip(path))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn get_percentage<P: AsRef<Path>>(path: P) -> Result<u64, Error> {
|
||||
Ok(String::from_utf8(
|
||||
Command::new("df")
|
||||
@@ -194,7 +212,7 @@ pub async fn get_percentage<P: AsRef<Path>>(path: P) -> Result<u64, Error> {
|
||||
.parse::<u64>()?)
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn pvscan() -> Result<BTreeMap<PathBuf, Option<String>>, Error> {
|
||||
let pvscan_out = Command::new("pvscan")
|
||||
.invoke(crate::ErrorKind::DiskManagement)
|
||||
@@ -226,24 +244,16 @@ pub async fn recovery_info(
|
||||
)?,
|
||||
));
|
||||
}
|
||||
let version_path = mountpoint.as_ref().join("root/appmgr/version");
|
||||
if tokio::fs::metadata(&version_path).await.is_ok() {
|
||||
return Ok(Some(EmbassyOsRecoveryInfo {
|
||||
version: from_yaml_async_reader(File::open(&version_path).await?).await?,
|
||||
full: true,
|
||||
password_hash: None,
|
||||
wrapped_key: None,
|
||||
}));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
pub async fn list() -> Result<DiskListResponse, Error> {
|
||||
let mut quirks = fetch_quirks().await?;
|
||||
let reconnect = update_quirks(&mut quirks).await?;
|
||||
save_quirks(&mut quirks).await?;
|
||||
#[instrument(skip_all)]
|
||||
pub async fn list(os: &OsPartitionInfo) -> Result<Vec<DiskInfo>, Error> {
|
||||
struct DiskIndex {
|
||||
parts: IndexSet<PathBuf>,
|
||||
internal: bool,
|
||||
}
|
||||
let disk_guids = pvscan().await?;
|
||||
let disks = tokio_stream::wrappers::ReadDirStream::new(
|
||||
tokio::fs::read_dir(DISK_PATH)
|
||||
@@ -256,128 +266,171 @@ pub async fn list() -> Result<DiskListResponse, Error> {
|
||||
crate::ErrorKind::Filesystem,
|
||||
)
|
||||
})
|
||||
.try_fold(BTreeMap::new(), |mut disks, dir_entry| async move {
|
||||
if let Some(disk_path) = dir_entry.path().file_name().and_then(|s| s.to_str()) {
|
||||
let (disk_path, part_path) = if let Some(end) = PARTITION_REGEX.find(disk_path) {
|
||||
(
|
||||
disk_path.strip_suffix(end.as_str()).unwrap_or_default(),
|
||||
Some(disk_path),
|
||||
)
|
||||
} else {
|
||||
(disk_path, None)
|
||||
};
|
||||
let disk_path = Path::new(DISK_PATH).join(disk_path);
|
||||
let disk = tokio::fs::canonicalize(&disk_path).await.with_ctx(|_| {
|
||||
(
|
||||
crate::ErrorKind::Filesystem,
|
||||
disk_path.display().to_string(),
|
||||
)
|
||||
})?;
|
||||
if &*disk == Path::new("/dev/mmcblk0") {
|
||||
return Ok(disks);
|
||||
}
|
||||
if !disks.contains_key(&disk) {
|
||||
disks.insert(disk.clone(), IndexSet::new());
|
||||
}
|
||||
if let Some(part_path) = part_path {
|
||||
let part_path = Path::new(DISK_PATH).join(part_path);
|
||||
let part = tokio::fs::canonicalize(&part_path).await.with_ctx(|_| {
|
||||
.try_fold(
|
||||
BTreeMap::<PathBuf, DiskIndex>::new(),
|
||||
|mut disks, dir_entry| async move {
|
||||
if let Some(disk_path) = dir_entry.path().file_name().and_then(|s| s.to_str()) {
|
||||
let (disk_path, part_path) = if let Some(end) = PARTITION_REGEX.find(disk_path) {
|
||||
(
|
||||
disk_path.strip_suffix(end.as_str()).unwrap_or_default(),
|
||||
Some(disk_path),
|
||||
)
|
||||
} else {
|
||||
(disk_path, None)
|
||||
};
|
||||
let disk_path = Path::new(DISK_PATH).join(disk_path);
|
||||
let disk = tokio::fs::canonicalize(&disk_path).await.with_ctx(|_| {
|
||||
(
|
||||
crate::ErrorKind::Filesystem,
|
||||
part_path.display().to_string(),
|
||||
disk_path.display().to_string(),
|
||||
)
|
||||
})?;
|
||||
disks.get_mut(&disk).unwrap().insert(part);
|
||||
let part = if let Some(part_path) = part_path {
|
||||
let part_path = Path::new(DISK_PATH).join(part_path);
|
||||
let part = tokio::fs::canonicalize(&part_path).await.with_ctx(|_| {
|
||||
(
|
||||
crate::ErrorKind::Filesystem,
|
||||
part_path.display().to_string(),
|
||||
)
|
||||
})?;
|
||||
Some(part)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if !disks.contains_key(&disk) {
|
||||
disks.insert(
|
||||
disk.clone(),
|
||||
DiskIndex {
|
||||
parts: IndexSet::new(),
|
||||
internal: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
if let Some(part) = part {
|
||||
if os.contains(&part) {
|
||||
disks.get_mut(&disk).unwrap().internal = true;
|
||||
} else {
|
||||
disks.get_mut(&disk).unwrap().parts.insert(part);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(disks)
|
||||
})
|
||||
Ok(disks)
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut res = Vec::with_capacity(disks.len());
|
||||
for (disk, parts) in disks {
|
||||
let mut guid: Option<String> = None;
|
||||
let mut partitions = Vec::with_capacity(parts.len());
|
||||
let vendor = get_vendor(&disk)
|
||||
.await
|
||||
.map_err(|e| tracing::warn!("Could not get vendor of {}: {}", disk.display(), e.source))
|
||||
.unwrap_or_default();
|
||||
let model = get_model(&disk)
|
||||
.await
|
||||
.map_err(|e| tracing::warn!("Could not get model of {}: {}", disk.display(), e.source))
|
||||
.unwrap_or_default();
|
||||
let capacity = get_capacity(&disk)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::warn!("Could not get capacity of {}: {}", disk.display(), e.source)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
if let Some(g) = disk_guids.get(&disk) {
|
||||
guid = g.clone();
|
||||
} else {
|
||||
for part in parts {
|
||||
let mut embassy_os = None;
|
||||
let label = get_label(&part).await?;
|
||||
let capacity = get_capacity(&part)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::warn!("Could not get capacity of {}: {}", part.display(), e.source)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let mut used = None;
|
||||
|
||||
match TmpMountGuard::mount(&BlockDev::new(&part), ReadOnly).await {
|
||||
Err(e) => tracing::warn!("Could not collect usage information: {}", e.source),
|
||||
Ok(mount_guard) => {
|
||||
used = get_used(&mount_guard)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::warn!(
|
||||
"Could not get usage of {}: {}",
|
||||
part.display(),
|
||||
e.source
|
||||
)
|
||||
})
|
||||
.ok();
|
||||
if let Some(recovery_info) = match recovery_info(&mount_guard).await {
|
||||
Ok(a) => a,
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
"Error fetching unencrypted backup metadata: {}",
|
||||
e
|
||||
);
|
||||
None
|
||||
}
|
||||
} {
|
||||
embassy_os = Some(recovery_info)
|
||||
}
|
||||
mount_guard.unmount().await?;
|
||||
}
|
||||
for (disk, index) in disks {
|
||||
if index.internal {
|
||||
for part in index.parts {
|
||||
let mut disk_info = disk_info(disk.clone()).await;
|
||||
disk_info.logicalname = part;
|
||||
if let Some(g) = disk_guids.get(&disk_info.logicalname) {
|
||||
disk_info.guid = g.clone();
|
||||
} else {
|
||||
disk_info.partitions = vec![part_info(disk_info.logicalname.clone()).await];
|
||||
}
|
||||
|
||||
partitions.push(PartitionInfo {
|
||||
logicalname: part,
|
||||
label,
|
||||
capacity,
|
||||
used,
|
||||
embassy_os,
|
||||
});
|
||||
res.push(disk_info);
|
||||
}
|
||||
} else {
|
||||
let mut disk_info = disk_info(disk).await;
|
||||
disk_info.partitions = Vec::with_capacity(index.parts.len());
|
||||
if let Some(g) = disk_guids.get(&disk_info.logicalname) {
|
||||
disk_info.guid = g.clone();
|
||||
} else {
|
||||
for part in index.parts {
|
||||
let mut part_info = part_info(part).await;
|
||||
if let Some(g) = disk_guids.get(&part_info.logicalname) {
|
||||
part_info.guid = g.clone();
|
||||
}
|
||||
disk_info.partitions.push(part_info);
|
||||
}
|
||||
}
|
||||
res.push(disk_info);
|
||||
}
|
||||
res.push(DiskInfo {
|
||||
logicalname: disk,
|
||||
vendor,
|
||||
model,
|
||||
partitions,
|
||||
capacity,
|
||||
guid,
|
||||
})
|
||||
}
|
||||
|
||||
Ok(DiskListResponse {
|
||||
disks: res,
|
||||
reconnect,
|
||||
})
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
async fn disk_info(disk: PathBuf) -> DiskInfo {
|
||||
let partition_table = get_partition_table(&disk)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::warn!(
|
||||
"Could not get partition table of {}: {}",
|
||||
disk.display(),
|
||||
e.source
|
||||
)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let vendor = get_vendor(&disk)
|
||||
.await
|
||||
.map_err(|e| tracing::warn!("Could not get vendor of {}: {}", disk.display(), e.source))
|
||||
.unwrap_or_default();
|
||||
let model = get_model(&disk)
|
||||
.await
|
||||
.map_err(|e| tracing::warn!("Could not get model of {}: {}", disk.display(), e.source))
|
||||
.unwrap_or_default();
|
||||
let capacity = get_capacity(&disk)
|
||||
.await
|
||||
.map_err(|e| tracing::warn!("Could not get capacity of {}: {}", disk.display(), e.source))
|
||||
.unwrap_or_default();
|
||||
DiskInfo {
|
||||
logicalname: disk,
|
||||
partition_table,
|
||||
vendor,
|
||||
model,
|
||||
partitions: Vec::new(),
|
||||
capacity,
|
||||
guid: None,
|
||||
}
|
||||
}
|
||||
|
||||
async fn part_info(part: PathBuf) -> PartitionInfo {
|
||||
let mut embassy_os = None;
|
||||
let label = get_label(&part)
|
||||
.await
|
||||
.map_err(|e| tracing::warn!("Could not get label of {}: {}", part.display(), e.source))
|
||||
.unwrap_or_default();
|
||||
let capacity = get_capacity(&part)
|
||||
.await
|
||||
.map_err(|e| tracing::warn!("Could not get capacity of {}: {}", part.display(), e.source))
|
||||
.unwrap_or_default();
|
||||
let mut used = None;
|
||||
|
||||
match TmpMountGuard::mount(&BlockDev::new(&part), ReadOnly).await {
|
||||
Err(e) => tracing::warn!("Could not collect usage information: {}", e.source),
|
||||
Ok(mount_guard) => {
|
||||
used = get_used(&mount_guard)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::warn!("Could not get usage of {}: {}", part.display(), e.source)
|
||||
})
|
||||
.ok();
|
||||
if let Some(recovery_info) = match recovery_info(&mount_guard).await {
|
||||
Ok(a) => a,
|
||||
Err(e) => {
|
||||
tracing::error!("Error fetching unencrypted backup metadata: {}", e);
|
||||
None
|
||||
}
|
||||
} {
|
||||
embassy_os = Some(recovery_info)
|
||||
}
|
||||
if let Err(e) = mount_guard.unmount().await {
|
||||
tracing::error!("Error unmounting partition {}: {}", part.display(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PartitionInfo {
|
||||
logicalname: part,
|
||||
label,
|
||||
capacity,
|
||||
used,
|
||||
embassy_os,
|
||||
guid: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_pvscan_output(pvscan_output: &str) -> BTreeMap<PathBuf, Option<String>> {
|
||||
|
||||
@@ -1,264 +1,5 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use color_eyre::eyre::eyre;
|
||||
use models::InvalidId;
|
||||
use patch_db::Revision;
|
||||
use rpc_toolkit::yajrc::RpcError;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ErrorKind {
|
||||
Unknown = 1,
|
||||
Filesystem = 2,
|
||||
Docker = 3,
|
||||
ConfigSpecViolation = 4,
|
||||
ConfigRulesViolation = 5,
|
||||
NotFound = 6,
|
||||
IncorrectPassword = 7,
|
||||
VersionIncompatible = 8,
|
||||
Network = 9,
|
||||
Registry = 10,
|
||||
Serialization = 11,
|
||||
Deserialization = 12,
|
||||
Utf8 = 13,
|
||||
ParseVersion = 14,
|
||||
IncorrectDisk = 15,
|
||||
Nginx = 16,
|
||||
Dependency = 17,
|
||||
ParseS9pk = 18,
|
||||
ParseUrl = 19,
|
||||
DiskNotAvailable = 20,
|
||||
BlockDevice = 21,
|
||||
InvalidOnionAddress = 22,
|
||||
Pack = 23,
|
||||
ValidateS9pk = 24,
|
||||
DiskCorrupted = 25, // Remove
|
||||
Tor = 26,
|
||||
ConfigGen = 27,
|
||||
ParseNumber = 28,
|
||||
Database = 29,
|
||||
InvalidPackageId = 30,
|
||||
InvalidSignature = 31,
|
||||
Backup = 32,
|
||||
Restore = 33,
|
||||
Authorization = 34,
|
||||
AutoConfigure = 35,
|
||||
Action = 36,
|
||||
RateLimited = 37,
|
||||
InvalidRequest = 38,
|
||||
MigrationFailed = 39,
|
||||
Uninitialized = 40,
|
||||
ParseNetAddress = 41,
|
||||
ParseSshKey = 42,
|
||||
SoundError = 43,
|
||||
ParseTimestamp = 44,
|
||||
ParseSysInfo = 45,
|
||||
Wifi = 46,
|
||||
Journald = 47,
|
||||
DiskManagement = 48,
|
||||
OpenSsl = 49,
|
||||
PasswordHashGeneration = 50,
|
||||
DiagnosticMode = 51,
|
||||
ParseDbField = 52,
|
||||
Duplicate = 53,
|
||||
MultipleErrors = 54,
|
||||
Incoherent = 55,
|
||||
InvalidBackupTargetId = 56,
|
||||
ProductKeyMismatch = 57,
|
||||
LanPortConflict = 58,
|
||||
Javascript = 59,
|
||||
Pem = 60,
|
||||
}
|
||||
impl ErrorKind {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
use ErrorKind::*;
|
||||
match self {
|
||||
Unknown => "Unknown Error",
|
||||
Filesystem => "Filesystem I/O Error",
|
||||
Docker => "Docker Error",
|
||||
ConfigSpecViolation => "Config Spec Violation",
|
||||
ConfigRulesViolation => "Config Rules Violation",
|
||||
NotFound => "Not Found",
|
||||
IncorrectPassword => "Incorrect Password",
|
||||
VersionIncompatible => "Version Incompatible",
|
||||
Network => "Network Error",
|
||||
Registry => "Registry Error",
|
||||
Serialization => "Serialization Error",
|
||||
Deserialization => "Deserialization Error",
|
||||
Utf8 => "UTF-8 Parse Error",
|
||||
ParseVersion => "Version Parsing Error",
|
||||
IncorrectDisk => "Incorrect Disk",
|
||||
Nginx => "Nginx Error",
|
||||
Dependency => "Dependency Error",
|
||||
ParseS9pk => "S9PK Parsing Error",
|
||||
ParseUrl => "URL Parsing Error",
|
||||
DiskNotAvailable => "Disk Not Available",
|
||||
BlockDevice => "Block Device Error",
|
||||
InvalidOnionAddress => "Invalid Onion Address",
|
||||
Pack => "Pack Error",
|
||||
ValidateS9pk => "S9PK Validation Error",
|
||||
DiskCorrupted => "Disk Corrupted", // Remove
|
||||
Tor => "Tor Daemon Error",
|
||||
ConfigGen => "Config Generation Error",
|
||||
ParseNumber => "Number Parsing Error",
|
||||
Database => "Database Error",
|
||||
InvalidPackageId => "Invalid Package ID",
|
||||
InvalidSignature => "Invalid Signature",
|
||||
Backup => "Backup Error",
|
||||
Restore => "Restore Error",
|
||||
Authorization => "Unauthorized",
|
||||
AutoConfigure => "Auto-Configure Error",
|
||||
Action => "Action Failed",
|
||||
RateLimited => "Rate Limited",
|
||||
InvalidRequest => "Invalid Request",
|
||||
MigrationFailed => "Migration Failed",
|
||||
Uninitialized => "Uninitialized",
|
||||
ParseNetAddress => "Net Address Parsing Error",
|
||||
ParseSshKey => "SSH Key Parsing Error",
|
||||
SoundError => "Sound Interface Error",
|
||||
ParseTimestamp => "Timestamp Parsing Error",
|
||||
ParseSysInfo => "System Info Parsing Error",
|
||||
Wifi => "WiFi Internal Error",
|
||||
Journald => "Journald Error",
|
||||
DiskManagement => "Disk Management Error",
|
||||
OpenSsl => "OpenSSL Internal Error",
|
||||
PasswordHashGeneration => "Password Hash Generation Error",
|
||||
DiagnosticMode => "Embassy is in Diagnostic Mode",
|
||||
ParseDbField => "Database Field Parse Error",
|
||||
Duplicate => "Duplication Error",
|
||||
MultipleErrors => "Multiple Errors",
|
||||
Incoherent => "Incoherent",
|
||||
InvalidBackupTargetId => "Invalid Backup Target ID",
|
||||
ProductKeyMismatch => "Incompatible Product Keys",
|
||||
LanPortConflict => "Incompatible LAN Port Configuration",
|
||||
Javascript => "Javascript Engine Error",
|
||||
Pem => "PEM Encoding Error",
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Display for ErrorKind {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Error {
|
||||
pub source: color_eyre::eyre::Error,
|
||||
pub kind: ErrorKind,
|
||||
pub revision: Option<Revision>,
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}: {}", self.kind.as_str(), self.source)
|
||||
}
|
||||
}
|
||||
impl Error {
|
||||
pub fn new<E: Into<color_eyre::eyre::Error>>(source: E, kind: ErrorKind) -> Self {
|
||||
Error {
|
||||
source: source.into(),
|
||||
kind,
|
||||
revision: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<InvalidId> for Error {
|
||||
fn from(err: InvalidId) -> Self {
|
||||
Error::new(err, crate::error::ErrorKind::InvalidPackageId)
|
||||
}
|
||||
}
|
||||
impl From<std::io::Error> for Error {
|
||||
fn from(e: std::io::Error) -> Self {
|
||||
Error::new(e, ErrorKind::Filesystem)
|
||||
}
|
||||
}
|
||||
impl From<std::str::Utf8Error> for Error {
|
||||
fn from(e: std::str::Utf8Error) -> Self {
|
||||
Error::new(e, ErrorKind::Utf8)
|
||||
}
|
||||
}
|
||||
impl From<std::string::FromUtf8Error> for Error {
|
||||
fn from(e: std::string::FromUtf8Error) -> Self {
|
||||
Error::new(e, ErrorKind::Utf8)
|
||||
}
|
||||
}
|
||||
impl From<emver::ParseError> for Error {
|
||||
fn from(e: emver::ParseError) -> Self {
|
||||
Error::new(e, ErrorKind::ParseVersion)
|
||||
}
|
||||
}
|
||||
impl From<rpc_toolkit::url::ParseError> for Error {
|
||||
fn from(e: rpc_toolkit::url::ParseError) -> Self {
|
||||
Error::new(e, ErrorKind::ParseUrl)
|
||||
}
|
||||
}
|
||||
impl From<std::num::ParseIntError> for Error {
|
||||
fn from(e: std::num::ParseIntError) -> Self {
|
||||
Error::new(e, ErrorKind::ParseNumber)
|
||||
}
|
||||
}
|
||||
impl From<std::num::ParseFloatError> for Error {
|
||||
fn from(e: std::num::ParseFloatError) -> Self {
|
||||
Error::new(e, ErrorKind::ParseNumber)
|
||||
}
|
||||
}
|
||||
impl From<patch_db::Error> for Error {
|
||||
fn from(e: patch_db::Error) -> Self {
|
||||
Error::new(e, ErrorKind::Database)
|
||||
}
|
||||
}
|
||||
impl From<sqlx::Error> for Error {
|
||||
fn from(e: sqlx::Error) -> Self {
|
||||
Error::new(e, ErrorKind::Database)
|
||||
}
|
||||
}
|
||||
impl From<ed25519_dalek::SignatureError> for Error {
|
||||
fn from(e: ed25519_dalek::SignatureError) -> Self {
|
||||
Error::new(e, ErrorKind::InvalidSignature)
|
||||
}
|
||||
}
|
||||
impl From<bollard::errors::Error> for Error {
|
||||
fn from(e: bollard::errors::Error) -> Self {
|
||||
Error::new(e, ErrorKind::Docker)
|
||||
}
|
||||
}
|
||||
impl From<torut::control::ConnError> for Error {
|
||||
fn from(e: torut::control::ConnError) -> Self {
|
||||
Error::new(eyre!("{:?}", e), ErrorKind::Tor)
|
||||
}
|
||||
}
|
||||
impl From<std::net::AddrParseError> for Error {
|
||||
fn from(e: std::net::AddrParseError) -> Self {
|
||||
Error::new(e, ErrorKind::ParseNetAddress)
|
||||
}
|
||||
}
|
||||
impl From<openssl::error::ErrorStack> for Error {
|
||||
fn from(e: openssl::error::ErrorStack) -> Self {
|
||||
Error::new(eyre!("OpenSSL ERROR:\n{}", e), ErrorKind::OpenSsl)
|
||||
}
|
||||
}
|
||||
impl From<Error> for RpcError {
|
||||
fn from(e: Error) -> Self {
|
||||
let mut data_object = serde_json::Map::with_capacity(3);
|
||||
data_object.insert("details".to_owned(), format!("{}", e.source).into());
|
||||
data_object.insert("debug".to_owned(), format!("{:?}", e.source).into());
|
||||
data_object.insert(
|
||||
"revision".to_owned(),
|
||||
match serde_json::to_value(&e.revision) {
|
||||
Ok(a) => a,
|
||||
Err(e) => {
|
||||
tracing::warn!("Error serializing revision for Error object: {}", e);
|
||||
serde_json::Value::Null
|
||||
}
|
||||
},
|
||||
);
|
||||
RpcError {
|
||||
code: e.kind as i32,
|
||||
message: e.kind.as_str().into(),
|
||||
data: Some(data_object.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
pub use models::{Error, ErrorKind, ResultExt};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ErrorCollection(Vec<Error>);
|
||||
@@ -309,46 +50,6 @@ impl std::fmt::Display for ErrorCollection {
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ResultExt<T, E>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn with_kind(self, kind: ErrorKind) -> Result<T, Error>;
|
||||
fn with_ctx<F: FnOnce(&E) -> (ErrorKind, D), D: Display + Send + Sync + 'static>(
|
||||
self,
|
||||
f: F,
|
||||
) -> Result<T, Error>;
|
||||
}
|
||||
impl<T, E> ResultExt<T, E> for Result<T, E>
|
||||
where
|
||||
color_eyre::eyre::Error: From<E>,
|
||||
{
|
||||
fn with_kind(self, kind: ErrorKind) -> Result<T, Error> {
|
||||
self.map_err(|e| Error {
|
||||
source: e.into(),
|
||||
kind,
|
||||
revision: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn with_ctx<F: FnOnce(&E) -> (ErrorKind, D), D: Display + Send + Sync + 'static>(
|
||||
self,
|
||||
f: F,
|
||||
) -> Result<T, Error> {
|
||||
self.map_err(|e| {
|
||||
let (kind, ctx) = f(&e);
|
||||
let source = color_eyre::eyre::Error::from(e);
|
||||
let ctx = format!("{}: {}", ctx, source);
|
||||
let source = source.wrap_err(ctx);
|
||||
Error {
|
||||
kind,
|
||||
source: source.into(),
|
||||
revision: None,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! ensure_code {
|
||||
($x:expr, $c:expr, $fmt:expr $(, $arg:expr)*) => {
|
||||
|
||||
@@ -1,34 +1,61 @@
|
||||
use digest::Digest;
|
||||
use tokio::fs::File;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use patch_db::DbHandle;
|
||||
use rand::{thread_rng, Rng};
|
||||
use tokio::process::Command;
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::account::AccountInfo;
|
||||
use crate::util::Invoke;
|
||||
use crate::{Error, ErrorKind, ResultExt};
|
||||
use crate::{Error, ErrorKind};
|
||||
#[derive(Clone, serde::Deserialize, serde::Serialize, Debug)]
|
||||
pub struct Hostname(pub String);
|
||||
|
||||
pub const PRODUCT_KEY_PATH: &'static str = "/embassy-os/product_key.txt";
|
||||
|
||||
#[instrument]
|
||||
pub async fn get_hostname() -> Result<String, Error> {
|
||||
Ok(derive_hostname(&get_id().await?))
|
||||
lazy_static::lazy_static! {
|
||||
static ref ADJECTIVES: Vec<String> = include_str!("./assets/adjectives.txt").lines().map(|x| x.to_string()).collect();
|
||||
static ref NOUNS: Vec<String> = include_str!("./assets/nouns.txt").lines().map(|x| x.to_string()).collect();
|
||||
}
|
||||
impl AsRef<str> for Hostname {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn derive_hostname(id: &str) -> String {
|
||||
format!("embassy-{}", id)
|
||||
impl Hostname {
|
||||
pub fn lan_address(&self) -> String {
|
||||
format!("https://{}.local", self.0)
|
||||
}
|
||||
|
||||
pub fn local_domain_name(&self) -> String {
|
||||
format!("{}.local", self.0)
|
||||
}
|
||||
pub fn no_dot_host_name(&self) -> String {
|
||||
self.0.to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
pub async fn get_current_hostname() -> Result<String, Error> {
|
||||
pub fn generate_hostname() -> Hostname {
|
||||
let mut rng = thread_rng();
|
||||
let adjective = &ADJECTIVES[rng.gen_range(0..ADJECTIVES.len())];
|
||||
let noun = &NOUNS[rng.gen_range(0..NOUNS.len())];
|
||||
Hostname(format!("{adjective}-{noun}"))
|
||||
}
|
||||
|
||||
pub fn generate_id() -> String {
|
||||
let id = uuid::Uuid::new_v4();
|
||||
id.to_string()
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn get_current_hostname() -> Result<Hostname, Error> {
|
||||
let out = Command::new("hostname")
|
||||
.invoke(ErrorKind::ParseSysInfo)
|
||||
.await?;
|
||||
let out_string = String::from_utf8(out)?;
|
||||
Ok(out_string.trim().to_owned())
|
||||
Ok(Hostname(out_string.trim().to_owned()))
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
pub async fn set_hostname(hostname: &str) -> Result<(), Error> {
|
||||
#[instrument(skip_all)]
|
||||
pub async fn set_hostname(hostname: &Hostname) -> Result<(), Error> {
|
||||
let hostname: &String = &hostname.0;
|
||||
let _out = Command::new("hostnamectl")
|
||||
.arg("set-hostname")
|
||||
.arg(hostname)
|
||||
@@ -37,38 +64,9 @@ pub async fn set_hostname(hostname: &str) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
pub async fn get_product_key() -> Result<String, Error> {
|
||||
let out = tokio::fs::read_to_string(PRODUCT_KEY_PATH)
|
||||
.await
|
||||
.with_ctx(|_| (crate::ErrorKind::Filesystem, PRODUCT_KEY_PATH))?;
|
||||
Ok(out.trim().to_owned())
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
pub async fn set_product_key(key: &str) -> Result<(), Error> {
|
||||
let mut pkey_file = File::create(PRODUCT_KEY_PATH).await?;
|
||||
pkey_file.write_all(key.as_bytes()).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn derive_id(key: &str) -> String {
|
||||
let mut hasher = sha2::Sha256::new();
|
||||
hasher.update(key.as_bytes());
|
||||
let res = hasher.finalize();
|
||||
hex::encode(&res[0..4])
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
pub async fn get_id() -> Result<String, Error> {
|
||||
let key = get_product_key().await?;
|
||||
Ok(derive_id(&key))
|
||||
}
|
||||
|
||||
// cat /embassy-os/product_key.txt | shasum -a 256 | head -c 8 | awk '{print "embassy-"$1}' | xargs hostnamectl set-hostname && systemctl restart avahi-daemon
|
||||
#[instrument]
|
||||
pub async fn sync_hostname() -> Result<(), Error> {
|
||||
set_hostname(&format!("embassy-{}", get_id().await?)).await?;
|
||||
#[instrument(skip_all)]
|
||||
pub async fn sync_hostname(account: &AccountInfo) -> Result<(), Error> {
|
||||
set_hostname(&account.hostname).await?;
|
||||
Command::new("systemctl")
|
||||
.arg("restart")
|
||||
.arg("avahi-daemon")
|
||||
|
||||
@@ -1,16 +1,30 @@
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::fs::Permissions;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::path::Path;
|
||||
use std::process::Stdio;
|
||||
use std::time::Duration;
|
||||
|
||||
use color_eyre::eyre::eyre;
|
||||
use helpers::NonDetachingJoinHandle;
|
||||
use models::ResultExt;
|
||||
use patch_db::{DbHandle, LockReceipt, LockType};
|
||||
use rand::random;
|
||||
use sqlx::{Pool, Postgres};
|
||||
use tokio::process::Command;
|
||||
|
||||
use crate::account::AccountInfo;
|
||||
use crate::context::rpc::RpcContextConfig;
|
||||
use crate::db::model::ServerStatus;
|
||||
use crate::install::PKG_DOCKER_DIR;
|
||||
use crate::db::model::{IpInfo, ServerStatus};
|
||||
use crate::install::PKG_ARCHIVE_DIR;
|
||||
use crate::middleware::auth::LOCAL_AUTH_COOKIE_PATH;
|
||||
use crate::sound::BEP;
|
||||
use crate::system::time;
|
||||
use crate::util::Invoke;
|
||||
use crate::Error;
|
||||
use crate::{Error, ARCH};
|
||||
|
||||
pub const SYSTEM_REBUILD_PATH: &str = "/embassy-os/system-rebuild";
|
||||
pub const STANDBY_MODE_PATH: &str = "/embassy-os/standby";
|
||||
pub const SYSTEM_REBUILD_PATH: &str = "/media/embassy/config/system-rebuild";
|
||||
pub const STANDBY_MODE_PATH: &str = "/media/embassy/config/standby";
|
||||
|
||||
pub async fn check_time_is_synchronized() -> Result<bool, Error> {
|
||||
Ok(String::from_utf8(
|
||||
@@ -30,6 +44,8 @@ pub struct InitReceipts {
|
||||
pub version_range: LockReceipt<emver::VersionRange, ()>,
|
||||
pub last_wifi_region: LockReceipt<Option<isocountry::CountryCode>, ()>,
|
||||
pub status_info: LockReceipt<ServerStatus, ()>,
|
||||
pub ip_info: LockReceipt<BTreeMap<String, IpInfo>, ()>,
|
||||
pub system_start_time: LockReceipt<String, ()>,
|
||||
}
|
||||
impl InitReceipts {
|
||||
pub async fn new(db: &mut impl DbHandle) -> Result<Self, Error> {
|
||||
@@ -50,30 +66,245 @@ impl InitReceipts {
|
||||
.last_wifi_region()
|
||||
.make_locker(LockType::Write)
|
||||
.add_to_keys(&mut locks);
|
||||
let ip_info = crate::db::DatabaseModel::new()
|
||||
.server_info()
|
||||
.ip_info()
|
||||
.make_locker(LockType::Write)
|
||||
.add_to_keys(&mut locks);
|
||||
let status_info = crate::db::DatabaseModel::new()
|
||||
.server_info()
|
||||
.status_info()
|
||||
.into_model()
|
||||
.make_locker(LockType::Write)
|
||||
.add_to_keys(&mut locks);
|
||||
let system_start_time = crate::db::DatabaseModel::new()
|
||||
.server_info()
|
||||
.system_start_time()
|
||||
.make_locker(LockType::Write)
|
||||
.add_to_keys(&mut locks);
|
||||
|
||||
let skeleton_key = db.lock_all(locks).await?;
|
||||
Ok(Self {
|
||||
server_version: server_version.verify(&skeleton_key)?,
|
||||
version_range: version_range.verify(&skeleton_key)?,
|
||||
ip_info: ip_info.verify(&skeleton_key)?,
|
||||
status_info: status_info.verify(&skeleton_key)?,
|
||||
last_wifi_region: last_wifi_region.verify(&skeleton_key)?,
|
||||
system_start_time: system_start_time.verify(&skeleton_key)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn init(cfg: &RpcContextConfig, product_key: &str) -> Result<(), Error> {
|
||||
let should_rebuild = tokio::fs::metadata(SYSTEM_REBUILD_PATH).await.is_ok();
|
||||
pub async fn pgloader(
|
||||
old_db_path: impl AsRef<Path>,
|
||||
batch_rows: usize,
|
||||
prefetch_rows: usize,
|
||||
) -> Result<(), Error> {
|
||||
tokio::fs::write(
|
||||
"/etc/embassy/migrate.load",
|
||||
format!(
|
||||
include_str!("migrate.load"),
|
||||
sqlite_path = old_db_path.as_ref().display(),
|
||||
batch_rows = batch_rows,
|
||||
prefetch_rows = prefetch_rows
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
match tokio::fs::remove_dir_all("/tmp/pgloader").await {
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
|
||||
a => a,
|
||||
}?;
|
||||
tracing::info!("Running pgloader");
|
||||
let out = Command::new("pgloader")
|
||||
.arg("-v")
|
||||
.arg("/etc/embassy/migrate.load")
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.output()
|
||||
.await?;
|
||||
let stdout = String::from_utf8(out.stdout)?;
|
||||
for line in stdout.lines() {
|
||||
tracing::debug!("pgloader: {}", line);
|
||||
}
|
||||
let stderr = String::from_utf8(out.stderr)?;
|
||||
for line in stderr.lines() {
|
||||
tracing::debug!("pgloader err: {}", line);
|
||||
}
|
||||
tracing::debug!("pgloader exited with code {:?}", out.status);
|
||||
if let Some(err) = stdout.lines().chain(stderr.lines()).find_map(|l| {
|
||||
if l.split_ascii_whitespace()
|
||||
.any(|word| word == "ERROR" || word == "FATAL")
|
||||
{
|
||||
Some(l)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) {
|
||||
return Err(Error::new(
|
||||
eyre!("pgloader error: {}", err),
|
||||
crate::ErrorKind::Database,
|
||||
));
|
||||
}
|
||||
tokio::fs::rename(
|
||||
old_db_path.as_ref(),
|
||||
old_db_path.as_ref().with_extension("bak"),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// must be idempotent
|
||||
pub async fn init_postgres(datadir: impl AsRef<Path>) -> Result<(), Error> {
|
||||
let db_dir = datadir.as_ref().join("main/postgresql");
|
||||
let is_mountpoint = || async {
|
||||
Ok::<_, Error>(
|
||||
tokio::process::Command::new("mountpoint")
|
||||
.arg("/var/lib/postgresql")
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.status()
|
||||
.await?
|
||||
.success(),
|
||||
)
|
||||
};
|
||||
let exists = tokio::fs::metadata(&db_dir).await.is_ok();
|
||||
if !exists {
|
||||
Command::new("cp")
|
||||
.arg("-ra")
|
||||
.arg("/var/lib/postgresql")
|
||||
.arg(&db_dir)
|
||||
.invoke(crate::ErrorKind::Filesystem)
|
||||
.await?;
|
||||
}
|
||||
if !is_mountpoint().await? {
|
||||
crate::disk::mount::util::bind(&db_dir, "/var/lib/postgresql", false).await?;
|
||||
}
|
||||
Command::new("chown")
|
||||
.arg("-R")
|
||||
.arg("postgres")
|
||||
.arg("/var/lib/postgresql")
|
||||
.invoke(crate::ErrorKind::Database)
|
||||
.await?;
|
||||
Command::new("systemctl")
|
||||
.arg("start")
|
||||
.arg("postgresql")
|
||||
.invoke(crate::ErrorKind::Database)
|
||||
.await?;
|
||||
if !exists {
|
||||
Command::new("sudo")
|
||||
.arg("-u")
|
||||
.arg("postgres")
|
||||
.arg("createuser")
|
||||
.arg("root")
|
||||
.invoke(crate::ErrorKind::Database)
|
||||
.await?;
|
||||
Command::new("sudo")
|
||||
.arg("-u")
|
||||
.arg("postgres")
|
||||
.arg("createdb")
|
||||
.arg("secrets")
|
||||
.arg("-O")
|
||||
.arg("root")
|
||||
.invoke(crate::ErrorKind::Database)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub struct InitResult {
|
||||
pub secret_store: Pool<Postgres>,
|
||||
pub db: patch_db::PatchDb,
|
||||
}
|
||||
|
||||
pub async fn init(cfg: &RpcContextConfig) -> Result<InitResult, Error> {
|
||||
tokio::fs::create_dir_all("/run/embassy")
|
||||
.await
|
||||
.with_ctx(|_| (crate::ErrorKind::Filesystem, "mkdir -p /run/embassy"))?;
|
||||
if tokio::fs::metadata(LOCAL_AUTH_COOKIE_PATH).await.is_err() {
|
||||
tokio::fs::write(
|
||||
LOCAL_AUTH_COOKIE_PATH,
|
||||
base64::encode(random::<[u8; 32]>()).as_bytes(),
|
||||
)
|
||||
.await
|
||||
.with_ctx(|_| {
|
||||
(
|
||||
crate::ErrorKind::Filesystem,
|
||||
format!("write {}", LOCAL_AUTH_COOKIE_PATH),
|
||||
)
|
||||
})?;
|
||||
tokio::fs::set_permissions(LOCAL_AUTH_COOKIE_PATH, Permissions::from_mode(0o046)).await?;
|
||||
Command::new("chown")
|
||||
.arg("root:embassy")
|
||||
.arg(LOCAL_AUTH_COOKIE_PATH)
|
||||
.invoke(crate::ErrorKind::Filesystem)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let secret_store = cfg.secret_store().await?;
|
||||
let log_dir = cfg.datadir().join("main").join("logs");
|
||||
tracing::info!("Opened Postgres");
|
||||
|
||||
crate::ssh::sync_keys_from_db(&secret_store, "/home/start9/.ssh/authorized_keys").await?;
|
||||
tracing::info!("Synced SSH Keys");
|
||||
|
||||
let account = AccountInfo::load(&secret_store).await?;
|
||||
let db = cfg.db(&account).await?;
|
||||
tracing::info!("Opened PatchDB");
|
||||
let mut handle = db.handle();
|
||||
crate::db::DatabaseModel::new()
|
||||
.server_info()
|
||||
.lock(&mut handle, LockType::Write)
|
||||
.await?;
|
||||
let receipts = InitReceipts::new(&mut handle).await?;
|
||||
|
||||
// write to ca cert store
|
||||
tokio::fs::write(
|
||||
"/usr/local/share/ca-certificates/embassy-root-ca.crt",
|
||||
account.root_ca_cert.to_pem()?,
|
||||
)
|
||||
.await?;
|
||||
Command::new("update-ca-certificates")
|
||||
.invoke(crate::ErrorKind::OpenSsl)
|
||||
.await?;
|
||||
|
||||
if let Some(wifi_interface) = &cfg.wifi_interface {
|
||||
crate::net::wifi::synchronize_wpa_supplicant_conf(
|
||||
&cfg.datadir().join("main"),
|
||||
wifi_interface,
|
||||
&receipts.last_wifi_region.get(&mut handle).await?,
|
||||
)
|
||||
.await?;
|
||||
tracing::info!("Synchronized WiFi");
|
||||
}
|
||||
|
||||
let should_rebuild = tokio::fs::metadata(SYSTEM_REBUILD_PATH).await.is_ok()
|
||||
|| &*receipts.server_version.get(&mut handle).await? < &emver::Version::new(0, 3, 2, 0)
|
||||
|| (*ARCH == "x86_64"
|
||||
&& &*receipts.server_version.get(&mut handle).await?
|
||||
< &emver::Version::new(0, 3, 4, 0));
|
||||
|
||||
let song = if should_rebuild {
|
||||
Some(NonDetachingJoinHandle::from(tokio::spawn(async {
|
||||
loop {
|
||||
BEP.play().await.unwrap();
|
||||
BEP.play().await.unwrap();
|
||||
tokio::time::sleep(Duration::from_secs(60)).await;
|
||||
}
|
||||
})))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let log_dir = cfg.datadir().join("main/logs");
|
||||
if tokio::fs::metadata(&log_dir).await.is_err() {
|
||||
tokio::fs::create_dir_all(&log_dir).await?;
|
||||
}
|
||||
let current_machine_id = tokio::fs::read_to_string("/etc/machine-id").await?;
|
||||
let mut machine_ids = tokio::fs::read_dir(&log_dir).await?;
|
||||
while let Some(machine_id) = machine_ids.next_entry().await? {
|
||||
if machine_id.file_name().to_string_lossy().trim() != current_machine_id.trim() {
|
||||
tokio::fs::remove_dir_all(machine_id.path()).await?;
|
||||
}
|
||||
}
|
||||
crate::disk::mount::util::bind(&log_dir, "/var/log/journal", false).await?;
|
||||
Command::new("systemctl")
|
||||
.arg("restart")
|
||||
@@ -81,22 +312,15 @@ pub async fn init(cfg: &RpcContextConfig, product_key: &str) -> Result<(), Error
|
||||
.invoke(crate::ErrorKind::Journald)
|
||||
.await?;
|
||||
tracing::info!("Mounted Logs");
|
||||
|
||||
let tmp_dir = cfg.datadir().join("package-data/tmp");
|
||||
if tokio::fs::metadata(&tmp_dir).await.is_err() {
|
||||
tokio::fs::create_dir_all(&tmp_dir).await?;
|
||||
}
|
||||
let tmp_docker = cfg.datadir().join("package-data/tmp/docker");
|
||||
let tmp_docker_exists = tokio::fs::metadata(&tmp_docker).await.is_ok();
|
||||
if should_rebuild || !tmp_docker_exists {
|
||||
if tmp_docker_exists {
|
||||
tokio::fs::remove_dir_all(&tmp_docker).await?;
|
||||
}
|
||||
Command::new("cp")
|
||||
.arg("-r")
|
||||
.arg("/var/lib/docker")
|
||||
.arg(&tmp_docker)
|
||||
.invoke(crate::ErrorKind::Filesystem)
|
||||
.await?;
|
||||
if should_rebuild && tmp_docker_exists {
|
||||
tokio::fs::remove_dir_all(&tmp_docker).await?;
|
||||
}
|
||||
Command::new("systemctl")
|
||||
.arg("stop")
|
||||
@@ -117,39 +341,48 @@ pub async fn init(cfg: &RpcContextConfig, product_key: &str) -> Result<(), Error
|
||||
tracing::info!("Mounted Docker Data");
|
||||
|
||||
if should_rebuild || !tmp_docker_exists {
|
||||
tracing::info!("Creating Docker Network");
|
||||
bollard::Docker::connect_with_unix_defaults()?
|
||||
.create_network(bollard::network::CreateNetworkOptions {
|
||||
name: "start9",
|
||||
driver: "bridge",
|
||||
ipam: bollard::models::Ipam {
|
||||
config: Some(vec![bollard::models::IpamConfig {
|
||||
subnet: Some("172.18.0.1/24".into()),
|
||||
..Default::default()
|
||||
}]),
|
||||
..Default::default()
|
||||
},
|
||||
options: {
|
||||
let mut m = HashMap::new();
|
||||
m.insert("com.docker.network.bridge.name", "br-start9");
|
||||
m
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
tracing::info!("Created Docker Network");
|
||||
|
||||
tracing::info!("Loading System Docker Images");
|
||||
crate::install::load_images("/var/lib/embassy/system-images").await?;
|
||||
crate::install::load_images("/usr/lib/embassy/system-images").await?;
|
||||
tracing::info!("Loaded System Docker Images");
|
||||
|
||||
tracing::info!("Loading Package Docker Images");
|
||||
crate::install::load_images(cfg.datadir().join(PKG_DOCKER_DIR)).await?;
|
||||
crate::install::load_images(cfg.datadir().join(PKG_ARCHIVE_DIR)).await?;
|
||||
tracing::info!("Loaded Package Docker Images");
|
||||
}
|
||||
|
||||
crate::ssh::sync_keys_from_db(&secret_store, "/home/start9/.ssh/authorized_keys").await?;
|
||||
tracing::info!("Synced SSH Keys");
|
||||
let db = cfg.db(&secret_store, product_key).await?;
|
||||
|
||||
let mut handle = db.handle();
|
||||
let receipts = InitReceipts::new(&mut handle).await?;
|
||||
|
||||
crate::net::wifi::synchronize_wpa_supplicant_conf(
|
||||
&cfg.datadir().join("main"),
|
||||
&receipts.last_wifi_region.get(&mut handle).await?,
|
||||
)
|
||||
.await?;
|
||||
tracing::info!("Synchronized wpa_supplicant.conf");
|
||||
receipts
|
||||
.status_info
|
||||
.set(
|
||||
&mut handle,
|
||||
ServerStatus {
|
||||
updated: false,
|
||||
update_progress: None,
|
||||
backup_progress: None,
|
||||
},
|
||||
)
|
||||
tracing::info!("Enabling Docker QEMU Emulation");
|
||||
Command::new("docker")
|
||||
.arg("run")
|
||||
.arg("--privileged")
|
||||
.arg("--rm")
|
||||
.arg("start9/x_system/binfmt")
|
||||
.arg("--install")
|
||||
.arg("all")
|
||||
.invoke(crate::ErrorKind::Docker)
|
||||
.await?;
|
||||
tracing::info!("Enabled Docker QEMU Emulation");
|
||||
|
||||
let mut warn_time_not_synced = true;
|
||||
for _ in 0..60 {
|
||||
@@ -165,13 +398,46 @@ pub async fn init(cfg: &RpcContextConfig, product_key: &str) -> Result<(), Error
|
||||
tracing::info!("Syncronized system clock");
|
||||
}
|
||||
|
||||
crate::version::init(&mut handle, &receipts).await?;
|
||||
Command::new("systemctl")
|
||||
.arg("start")
|
||||
.arg("tor")
|
||||
.invoke(crate::ErrorKind::Tor)
|
||||
.await?;
|
||||
|
||||
receipts
|
||||
.ip_info
|
||||
.set(&mut handle, crate::net::dhcp::init_ips().await?)
|
||||
.await?;
|
||||
receipts
|
||||
.status_info
|
||||
.set(
|
||||
&mut handle,
|
||||
ServerStatus {
|
||||
updated: false,
|
||||
update_progress: None,
|
||||
backup_progress: None,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
receipts
|
||||
.system_start_time
|
||||
.set(&mut handle, time().await?)
|
||||
.await?;
|
||||
|
||||
crate::version::init(&mut handle, &secret_store, &receipts).await?;
|
||||
|
||||
if should_rebuild {
|
||||
tokio::fs::remove_file(SYSTEM_REBUILD_PATH).await?;
|
||||
match tokio::fs::remove_file(SYSTEM_REBUILD_PATH).await {
|
||||
Ok(()) => Ok(()),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
|
||||
Err(e) => Err(e),
|
||||
}?;
|
||||
}
|
||||
|
||||
drop(song);
|
||||
|
||||
tracing::info!("System initialized.");
|
||||
|
||||
Ok(())
|
||||
Ok(InitResult { secret_store, db })
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use bollard::image::ListImagesOptions;
|
||||
use bollard::image::{ListImagesOptions, RemoveImageOptions};
|
||||
use patch_db::{DbHandle, LockReceipt, LockTargetId, LockType, PatchDbHandle, Verifier};
|
||||
use sqlx::{Executor, Sqlite};
|
||||
use sqlx::{Executor, Postgres};
|
||||
use tracing::instrument;
|
||||
|
||||
use super::{PKG_ARCHIVE_DIR, PKG_DOCKER_DIR};
|
||||
use super::PKG_ARCHIVE_DIR;
|
||||
use crate::config::{not_found, ConfigReceipts};
|
||||
use crate::context::RpcContext;
|
||||
use crate::db::model::{
|
||||
@@ -60,7 +62,7 @@ impl UpdateDependencyReceipts {
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(ctx, db, deps, receipts))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn update_dependency_errors_of_dependents<'a, Db: DbHandle>(
|
||||
ctx: &RpcContext,
|
||||
db: &mut Db,
|
||||
@@ -97,7 +99,7 @@ pub async fn update_dependency_errors_of_dependents<'a, Db: DbHandle>(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(ctx))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn cleanup(ctx: &RpcContext, id: &PackageId, version: &Version) -> Result<(), Error> {
|
||||
let mut errors = ErrorCollection::new();
|
||||
ctx.managers.remove(&(id.clone(), version.clone())).await;
|
||||
@@ -119,10 +121,29 @@ pub async fn cleanup(ctx: &RpcContext, id: &PackageId, version: &Version) -> Res
|
||||
.await
|
||||
.apply(|res| errors.handle(res));
|
||||
errors.extend(
|
||||
futures::future::join_all(images.into_iter().flatten().map(|image| async {
|
||||
let image = image; // move into future
|
||||
ctx.docker.remove_image(&image.id, None, None).await
|
||||
}))
|
||||
futures::future::join_all(
|
||||
images
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.flat_map(|image| image.repo_tags)
|
||||
.filter(|tag| {
|
||||
tag.starts_with(&format!("start9/{}/", id))
|
||||
&& tag.ends_with(&format!(":{}", version))
|
||||
})
|
||||
.map(|tag| async {
|
||||
let tag = tag; // move into future
|
||||
ctx.docker
|
||||
.remove_image(
|
||||
&tag,
|
||||
Some(RemoveImageOptions {
|
||||
force: true,
|
||||
noprune: false,
|
||||
}),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}),
|
||||
)
|
||||
.await,
|
||||
);
|
||||
let pkg_archive_dir = ctx
|
||||
@@ -135,16 +156,6 @@ pub async fn cleanup(ctx: &RpcContext, id: &PackageId, version: &Version) -> Res
|
||||
.await
|
||||
.apply(|res| errors.handle(res));
|
||||
}
|
||||
let docker_path = ctx
|
||||
.datadir
|
||||
.join(PKG_DOCKER_DIR)
|
||||
.join(id)
|
||||
.join(version.as_str());
|
||||
if tokio::fs::metadata(&docker_path).await.is_ok() {
|
||||
tokio::fs::remove_dir_all(&docker_path)
|
||||
.await
|
||||
.apply(|res| errors.handle(res));
|
||||
}
|
||||
let assets_path = asset_dir(&ctx.datadir, id, version);
|
||||
if tokio::fs::metadata(&assets_path).await.is_ok() {
|
||||
tokio::fs::remove_dir_all(&assets_path)
|
||||
@@ -193,7 +204,7 @@ impl CleanupFailedReceipts {
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(ctx, db, receipts))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn cleanup_failed<Db: DbHandle>(
|
||||
ctx: &RpcContext,
|
||||
db: &mut Db,
|
||||
@@ -261,7 +272,7 @@ pub async fn cleanup_failed<Db: DbHandle>(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(db, current_dependencies, current_dependent_receipt))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn remove_from_current_dependents_lists<'a, Db: DbHandle>(
|
||||
db: &mut Db,
|
||||
id: &'a PackageId,
|
||||
@@ -329,7 +340,7 @@ impl UninstallReceipts {
|
||||
}
|
||||
}
|
||||
}
|
||||
#[instrument(skip(ctx, secrets, db))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn uninstall<Ex>(
|
||||
ctx: &RpcContext,
|
||||
db: &mut PatchDbHandle,
|
||||
@@ -337,9 +348,14 @@ pub async fn uninstall<Ex>(
|
||||
id: &PackageId,
|
||||
) -> Result<(), Error>
|
||||
where
|
||||
for<'a> &'a mut Ex: Executor<'a, Database = Sqlite>,
|
||||
for<'a> &'a mut Ex: Executor<'a, Database = Postgres>,
|
||||
{
|
||||
let mut tx = db.begin().await?;
|
||||
crate::db::DatabaseModel::new()
|
||||
.package_data()
|
||||
.idx_model(&id)
|
||||
.lock(&mut tx, LockType::Write)
|
||||
.await?;
|
||||
let receipts = UninstallReceipts::new(&mut tx, id).await?;
|
||||
let entry = receipts.removing.get(&mut tx).await?;
|
||||
cleanup(ctx, &entry.manifest.id, &entry.manifest.version).await?;
|
||||
@@ -349,6 +365,14 @@ where
|
||||
packages.0.remove(id);
|
||||
packages
|
||||
};
|
||||
let dependents_paths: Vec<PathBuf> = entry
|
||||
.current_dependents
|
||||
.0
|
||||
.keys()
|
||||
.flat_map(|x| packages.0.get(x))
|
||||
.flat_map(|x| x.manifest_borrow().volumes.values())
|
||||
.flat_map(|x| x.pointer_path(&ctx.datadir))
|
||||
.collect();
|
||||
receipts.packages.set(&mut tx, packages).await?;
|
||||
// once we have removed the package entry, we can change all the dependent pointers to null
|
||||
reconfigure_dependents_with_live_pointers(ctx, &mut tx, &receipts.config, &entry).await?;
|
||||
@@ -372,22 +396,61 @@ where
|
||||
.datadir
|
||||
.join(crate::volume::PKG_VOLUME_DIR)
|
||||
.join(&entry.manifest.id);
|
||||
if tokio::fs::metadata(&volumes).await.is_ok() {
|
||||
tokio::fs::remove_dir_all(&volumes).await?;
|
||||
}
|
||||
tx.commit(None).await?;
|
||||
|
||||
tracing::debug!("Cleaning up {:?} at {:?}", volumes, dependents_paths);
|
||||
cleanup_folder(volumes, Arc::new(dependents_paths)).await;
|
||||
remove_tor_keys(secrets, &entry.manifest.id).await?;
|
||||
tx.commit().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(secrets))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn remove_tor_keys<Ex>(secrets: &mut Ex, id: &PackageId) -> Result<(), Error>
|
||||
where
|
||||
for<'a> &'a mut Ex: Executor<'a, Database = Sqlite>,
|
||||
for<'a> &'a mut Ex: Executor<'a, Database = Postgres>,
|
||||
{
|
||||
let id_str = id.as_str();
|
||||
sqlx::query!("DELETE FROM tor WHERE package = ?", id_str)
|
||||
sqlx::query!("DELETE FROM tor WHERE package = $1", id_str)
|
||||
.execute(secrets)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Needed to remove, without removing the folders that are mounted in the other docker containers
|
||||
pub fn cleanup_folder(
|
||||
path: PathBuf,
|
||||
dependents_volumes: Arc<Vec<PathBuf>>,
|
||||
) -> futures::future::BoxFuture<'static, ()> {
|
||||
Box::pin(async move {
|
||||
let meta_data = match tokio::fs::metadata(&path).await {
|
||||
Ok(a) => a,
|
||||
Err(_e) => {
|
||||
return;
|
||||
}
|
||||
};
|
||||
if !meta_data.is_dir() {
|
||||
tracing::error!("is_not dir, remove {:?}", path);
|
||||
let _ = tokio::fs::remove_file(&path).await;
|
||||
return;
|
||||
}
|
||||
if !dependents_volumes
|
||||
.iter()
|
||||
.any(|v| v.starts_with(&path) || v == &path)
|
||||
{
|
||||
tracing::error!("No parents, remove {:?}", path);
|
||||
let _ = tokio::fs::remove_dir_all(&path).await;
|
||||
return;
|
||||
}
|
||||
let mut read_dir = match tokio::fs::read_dir(&path).await {
|
||||
Ok(a) => a,
|
||||
Err(_e) => {
|
||||
return;
|
||||
}
|
||||
};
|
||||
tracing::error!("Parents, recurse {:?}", path);
|
||||
while let Some(entry) = read_dir.next_entry().await.ok().flatten() {
|
||||
let entry_path = entry.path();
|
||||
cleanup_folder(entry_path, dependents_volumes.clone()).await;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::path::{Path, PathBuf};
|
||||
use std::process::Stdio;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
use std::time::Duration;
|
||||
|
||||
use color_eyre::eyre::eyre;
|
||||
use emver::VersionRange;
|
||||
@@ -14,10 +14,10 @@ use futures::{FutureExt, StreamExt, TryStreamExt};
|
||||
use http::header::CONTENT_LENGTH;
|
||||
use http::{Request, Response, StatusCode};
|
||||
use hyper::Body;
|
||||
use patch_db::{DbHandle, LockReceipt, LockType};
|
||||
use patch_db::{DbHandle, LockType};
|
||||
use reqwest::Url;
|
||||
use rpc_toolkit::command;
|
||||
use rpc_toolkit::yajrc::RpcError;
|
||||
use rpc_toolkit::{command, Context};
|
||||
use tokio::fs::{File, OpenOptions};
|
||||
use tokio::io::{AsyncRead, AsyncSeek, AsyncSeekExt};
|
||||
use tokio::process::Command;
|
||||
@@ -30,9 +30,8 @@ use crate::context::{CliContext, RpcContext};
|
||||
use crate::core::rpc_continuations::{RequestGuid, RpcContinuation};
|
||||
use crate::db::model::{
|
||||
CurrentDependencies, CurrentDependencyInfo, CurrentDependents, InstalledPackageDataEntry,
|
||||
PackageDataEntry, RecoveredPackageInfo, StaticDependencyInfo, StaticFiles,
|
||||
PackageDataEntry, StaticDependencyInfo, StaticFiles,
|
||||
};
|
||||
use crate::db::util::WithRevision;
|
||||
use crate::dependencies::{
|
||||
add_dependent_to_current_dependents_lists, break_all_dependents_transitive,
|
||||
reconfigure_dependents_with_live_pointers, BreakTransitiveReceipts, BreakageRes,
|
||||
@@ -46,7 +45,7 @@ use crate::s9pk::reader::S9pkReader;
|
||||
use crate::status::{MainStatus, Status};
|
||||
use crate::util::io::{copy_and_shutdown, response_to_reader};
|
||||
use crate::util::serde::{display_serializable, Port};
|
||||
use crate::util::{display_none, AsyncFileExt, Version};
|
||||
use crate::util::{assure_send, display_none, AsyncFileExt, Version};
|
||||
use crate::version::{Current, VersionT};
|
||||
use crate::volume::{asset_dir, script_dir};
|
||||
use crate::{Error, ErrorKind, ResultExt};
|
||||
@@ -57,7 +56,6 @@ pub mod update;
|
||||
|
||||
pub const PKG_ARCHIVE_DIR: &str = "package-data/archive";
|
||||
pub const PKG_PUBLIC_DIR: &str = "package-data/public";
|
||||
pub const PKG_DOCKER_DIR: &str = "package-data/docker";
|
||||
pub const PKG_WASM_DIR: &str = "package-data/wasm";
|
||||
|
||||
#[command(display(display_serializable))]
|
||||
@@ -65,7 +63,7 @@ pub async fn list(#[context] ctx: RpcContext) -> Result<Vec<(PackageId, Version)
|
||||
let mut hdl = ctx.db.handle();
|
||||
let package_data = crate::db::DatabaseModel::new()
|
||||
.package_data()
|
||||
.get(&mut hdl, true)
|
||||
.get(&mut hdl)
|
||||
.await?;
|
||||
|
||||
Ok(package_data
|
||||
@@ -115,9 +113,10 @@ impl std::fmt::Display for MinMax {
|
||||
|
||||
#[command(
|
||||
custom_cli(cli_install(async, context(CliContext))),
|
||||
display(display_none)
|
||||
display(display_none),
|
||||
metadata(sync_db = true)
|
||||
)]
|
||||
#[instrument(skip(ctx))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn install(
|
||||
#[context] ctx: RpcContext,
|
||||
#[arg] id: String,
|
||||
@@ -127,7 +126,7 @@ pub async fn install(
|
||||
String,
|
||||
>,
|
||||
#[arg(long = "version-priority", rename = "version-priority")] version_priority: Option<MinMax>,
|
||||
) -> Result<WithRevision<()>, Error> {
|
||||
) -> Result<(), Error> {
|
||||
let version_str = match &version_spec {
|
||||
None => "*",
|
||||
Some(v) => &*v,
|
||||
@@ -287,7 +286,7 @@ pub async fn install(
|
||||
}
|
||||
}
|
||||
pde.save(&mut tx).await?;
|
||||
let res = tx.commit(None).await?;
|
||||
tx.commit().await?;
|
||||
drop(db_handle);
|
||||
|
||||
tokio::spawn(async move {
|
||||
@@ -323,14 +322,11 @@ pub async fn install(
|
||||
}
|
||||
});
|
||||
|
||||
Ok(WithRevision {
|
||||
revision: res,
|
||||
response: (),
|
||||
})
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command(rpc_only, display(display_none))]
|
||||
#[instrument(skip(ctx))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn sideload(
|
||||
#[context] ctx: RpcContext,
|
||||
#[arg] manifest: Manifest,
|
||||
@@ -427,7 +423,7 @@ pub async fn sideload(
|
||||
}
|
||||
}
|
||||
pde.save(&mut tx).await?;
|
||||
tx.commit(None).await?;
|
||||
tx.commit().await?;
|
||||
|
||||
if let Err(e) = download_install_s9pk(
|
||||
&new_ctx,
|
||||
@@ -478,27 +474,15 @@ pub async fn sideload(
|
||||
}
|
||||
.boxed()
|
||||
});
|
||||
let cont = RpcContinuation {
|
||||
created_at: Instant::now(), // TODO
|
||||
handler,
|
||||
};
|
||||
// gc the map
|
||||
let mut guard = ctx.rpc_stream_continuations.lock().await;
|
||||
let garbage_collected = std::mem::take(&mut *guard)
|
||||
.into_iter()
|
||||
.filter(|(_, v)| v.created_at.elapsed() < Duration::from_secs(30))
|
||||
.collect::<BTreeMap<RequestGuid, RpcContinuation>>();
|
||||
*guard = garbage_collected;
|
||||
drop(guard);
|
||||
// insert the new continuation
|
||||
ctx.rpc_stream_continuations
|
||||
.lock()
|
||||
.await
|
||||
.insert(guid.clone(), cont);
|
||||
ctx.add_continuation(
|
||||
guid.clone(),
|
||||
RpcContinuation::rest(handler, Duration::from_secs(30)),
|
||||
)
|
||||
.await;
|
||||
Ok(guid)
|
||||
}
|
||||
|
||||
#[instrument(skip(ctx))]
|
||||
#[instrument(skip_all)]
|
||||
async fn cli_install(
|
||||
ctx: CliContext,
|
||||
target: String,
|
||||
@@ -537,12 +521,7 @@ async fn cli_install(
|
||||
let body = Body::wrap_stream(tokio_util::io::ReaderStream::new(file));
|
||||
let res = ctx
|
||||
.client
|
||||
.post(format!(
|
||||
"{}://{}/rest/rpc/{}",
|
||||
ctx.protocol(),
|
||||
ctx.host(),
|
||||
guid
|
||||
))
|
||||
.post(format!("{}rest/rpc/{}", ctx.base_url, guid,))
|
||||
.header(CONTENT_LENGTH, content_length)
|
||||
.body(body)
|
||||
.send()
|
||||
@@ -576,7 +555,7 @@ async fn cli_install(
|
||||
ctx,
|
||||
"package.install",
|
||||
params,
|
||||
PhantomData::<WithRevision<()>>,
|
||||
PhantomData::<()>,
|
||||
)
|
||||
.await?
|
||||
.result?;
|
||||
@@ -587,14 +566,15 @@ async fn cli_install(
|
||||
|
||||
#[command(
|
||||
subcommands(self(uninstall_impl(async)), uninstall_dry),
|
||||
display(display_none)
|
||||
display(display_none),
|
||||
metadata(sync_db = true)
|
||||
)]
|
||||
pub async fn uninstall(#[arg] id: PackageId) -> Result<PackageId, Error> {
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
#[command(rename = "dry", display(display_serializable))]
|
||||
#[instrument(skip(ctx))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn uninstall_dry(
|
||||
#[context] ctx: RpcContext,
|
||||
#[parent_data] id: PackageId,
|
||||
@@ -617,10 +597,15 @@ pub async fn uninstall_dry(
|
||||
Ok(BreakageRes(breakages))
|
||||
}
|
||||
|
||||
#[instrument(skip(ctx))]
|
||||
pub async fn uninstall_impl(ctx: RpcContext, id: PackageId) -> Result<WithRevision<()>, Error> {
|
||||
#[instrument(skip_all)]
|
||||
pub async fn uninstall_impl(ctx: RpcContext, id: PackageId) -> Result<(), Error> {
|
||||
let mut handle = ctx.db.handle();
|
||||
let mut tx = handle.begin().await?;
|
||||
crate::db::DatabaseModel::new()
|
||||
.package_data()
|
||||
.idx_model(&id)
|
||||
.lock(&mut tx, LockType::Write)
|
||||
.await?;
|
||||
|
||||
let mut pde = crate::db::DatabaseModel::new()
|
||||
.package_data()
|
||||
@@ -646,7 +631,7 @@ pub async fn uninstall_impl(ctx: RpcContext, id: PackageId) -> Result<WithRevisi
|
||||
removing: installed,
|
||||
});
|
||||
pde.save(&mut tx).await?;
|
||||
let res = tx.commit(None).await?;
|
||||
tx.commit().await?;
|
||||
drop(handle);
|
||||
|
||||
tokio::spawn(async move {
|
||||
@@ -683,46 +668,7 @@ pub async fn uninstall_impl(ctx: RpcContext, id: PackageId) -> Result<WithRevisi
|
||||
}
|
||||
});
|
||||
|
||||
Ok(WithRevision {
|
||||
revision: res,
|
||||
response: (),
|
||||
})
|
||||
}
|
||||
|
||||
#[command(rename = "delete-recovered", display(display_none))]
|
||||
pub async fn delete_recovered(
|
||||
#[context] ctx: RpcContext,
|
||||
#[arg] id: PackageId,
|
||||
) -> Result<WithRevision<()>, Error> {
|
||||
let mut handle = ctx.db.handle();
|
||||
let mut tx = handle.begin().await?;
|
||||
let mut sql_tx = ctx.secret_store.begin().await?;
|
||||
|
||||
let mut recovered_packages = crate::db::DatabaseModel::new()
|
||||
.recovered_packages()
|
||||
.get_mut(&mut tx)
|
||||
.await?;
|
||||
recovered_packages.remove(&id).ok_or_else(|| {
|
||||
Error::new(
|
||||
eyre!("{} not in recovered-packages", id),
|
||||
crate::ErrorKind::NotFound,
|
||||
)
|
||||
})?;
|
||||
recovered_packages.save(&mut tx).await?;
|
||||
|
||||
let volumes = ctx.datadir.join(crate::volume::PKG_VOLUME_DIR).join(&id);
|
||||
if tokio::fs::metadata(&volumes).await.is_ok() {
|
||||
tokio::fs::remove_dir_all(&volumes).await?;
|
||||
}
|
||||
cleanup::remove_tor_keys(&mut sql_tx, &id).await?;
|
||||
|
||||
let res = tx.commit(None).await?;
|
||||
sql_tx.commit().await?;
|
||||
|
||||
Ok(WithRevision {
|
||||
revision: res,
|
||||
response: (),
|
||||
})
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub struct DownloadInstallReceipts {
|
||||
@@ -754,7 +700,7 @@ impl DownloadInstallReceipts {
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(ctx, temp_manifest, s9pk))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn download_install_s9pk(
|
||||
ctx: &RpcContext,
|
||||
temp_manifest: &Manifest,
|
||||
@@ -764,8 +710,21 @@ pub async fn download_install_s9pk(
|
||||
) -> Result<(), Error> {
|
||||
let pkg_id = &temp_manifest.id;
|
||||
let version = &temp_manifest.version;
|
||||
let mut previous_state: Option<MainStatus> = None;
|
||||
|
||||
if let Err(e) = async {
|
||||
if crate::db::DatabaseModel::new()
|
||||
.package_data()
|
||||
.idx_model(&pkg_id)
|
||||
.and_then(|x| x.installed())
|
||||
.exists(&mut ctx.db.handle())
|
||||
.await
|
||||
.unwrap_or(false)
|
||||
{
|
||||
previous_state = crate::control::stop_impl(ctx.clone(), pkg_id.clone())
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
let mut db_handle = ctx.db.handle();
|
||||
let mut tx = db_handle.begin().await?;
|
||||
let receipts = DownloadInstallReceipts::new(&mut tx, &pkg_id).await?;
|
||||
@@ -788,7 +747,7 @@ pub async fn download_install_s9pk(
|
||||
for (p, lan) in cfg {
|
||||
if p.0 == 80 && lan.ssl || p.0 == 443 && !lan.ssl {
|
||||
return Err(Error::new(
|
||||
eyre!("SSL Conflict with EmbassyOS"),
|
||||
eyre!("SSL Conflict with embassyOS"),
|
||||
ErrorKind::LanPortConflict,
|
||||
));
|
||||
}
|
||||
@@ -867,6 +826,10 @@ pub async fn download_install_s9pk(
|
||||
}
|
||||
.await
|
||||
{
|
||||
if previous_state.map(|x| x.running()).unwrap_or(false) {
|
||||
crate::control::start(ctx.clone(), pkg_id.clone()).await?;
|
||||
}
|
||||
|
||||
let mut handle = ctx.db.handle();
|
||||
let mut tx = handle.begin().await?;
|
||||
let receipts = cleanup::CleanupFailedReceipts::new(&mut tx).await?;
|
||||
@@ -875,18 +838,19 @@ pub async fn download_install_s9pk(
|
||||
tracing::error!("Failed to clean up {}@{}: {}", pkg_id, version, e);
|
||||
tracing::debug!("{:?}", e);
|
||||
} else {
|
||||
tx.commit(None).await?;
|
||||
tx.commit().await?;
|
||||
}
|
||||
Err(e)
|
||||
} else {
|
||||
if previous_state.map(|x| x.running()).unwrap_or(false) {
|
||||
crate::control::start(ctx.clone(), pkg_id.clone()).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct InstallS9Receipts {
|
||||
config: ConfigReceipts,
|
||||
|
||||
recovered_packages: LockReceipt<BTreeMap<PackageId, RecoveredPackageInfo>, ()>,
|
||||
}
|
||||
|
||||
impl InstallS9Receipts {
|
||||
@@ -901,23 +865,16 @@ impl InstallS9Receipts {
|
||||
locks: &mut Vec<patch_db::LockTargetId>,
|
||||
) -> impl FnOnce(&patch_db::Verifier) -> Result<Self, Error> {
|
||||
let config = ConfigReceipts::setup(locks);
|
||||
|
||||
let recovered_packages = crate::db::DatabaseModel::new()
|
||||
.recovered_packages()
|
||||
.make_locker(LockType::Write)
|
||||
.add_to_keys(locks);
|
||||
|
||||
move |skeleton_key| {
|
||||
Ok(Self {
|
||||
config: config(skeleton_key)?,
|
||||
recovered_packages: recovered_packages.verify(skeleton_key)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(ctx, rdr))]
|
||||
pub async fn install_s9pk<R: AsyncRead + AsyncSeek + Unpin>(
|
||||
#[instrument(skip_all)]
|
||||
pub async fn install_s9pk<R: AsyncRead + AsyncSeek + Unpin + Send + Sync>(
|
||||
ctx: &RpcContext,
|
||||
pkg_id: &PackageId,
|
||||
version: &Version,
|
||||
@@ -947,7 +904,7 @@ pub async fn install_s9pk<R: AsyncRead + AsyncSeek + Unpin>(
|
||||
.package_data()
|
||||
.idx_model(dep)
|
||||
.map::<_, Manifest>(|pde| pde.manifest())
|
||||
.get(&mut ctx.db.handle(), false)
|
||||
.get(&mut ctx.db.handle())
|
||||
.await?
|
||||
.into_owned()
|
||||
{
|
||||
@@ -1081,44 +1038,11 @@ pub async fn install_s9pk<R: AsyncRead + AsyncSeek + Unpin>(
|
||||
tracing::info!("Install {}@{}: Unpacking Docker Images", pkg_id, version);
|
||||
progress
|
||||
.track_read_during(progress_model.clone(), &ctx.db, || async {
|
||||
let image_tar_dir = ctx
|
||||
.datadir
|
||||
.join(PKG_DOCKER_DIR)
|
||||
.join(pkg_id)
|
||||
.join(version.as_str());
|
||||
if tokio::fs::metadata(&image_tar_dir).await.is_err() {
|
||||
tokio::fs::create_dir_all(&image_tar_dir)
|
||||
.await
|
||||
.with_ctx(|_| {
|
||||
(
|
||||
crate::ErrorKind::Filesystem,
|
||||
image_tar_dir.display().to_string(),
|
||||
)
|
||||
})?;
|
||||
}
|
||||
let image_tar_path = image_tar_dir.join("image.tar");
|
||||
let mut tee = Command::new("tee")
|
||||
.arg(&image_tar_path)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()?;
|
||||
let mut load = Command::new("docker")
|
||||
.arg("load")
|
||||
.stdin(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()?;
|
||||
let tee_in = tee.stdin.take().ok_or_else(|| {
|
||||
Error::new(
|
||||
eyre!("Could not write to stdin of tee"),
|
||||
crate::ErrorKind::Docker,
|
||||
)
|
||||
})?;
|
||||
let mut tee_out = tee.stdout.take().ok_or_else(|| {
|
||||
Error::new(
|
||||
eyre!("Could not read from stdout of tee"),
|
||||
crate::ErrorKind::Docker,
|
||||
)
|
||||
})?;
|
||||
let load_in = load.stdin.take().ok_or_else(|| {
|
||||
Error::new(
|
||||
eyre!("Could not write to stdin of docker load"),
|
||||
@@ -1126,10 +1050,7 @@ pub async fn install_s9pk<R: AsyncRead + AsyncSeek + Unpin>(
|
||||
)
|
||||
})?;
|
||||
let mut docker_rdr = rdr.docker_images().await?;
|
||||
tokio::try_join!(
|
||||
copy_and_shutdown(&mut docker_rdr, tee_in),
|
||||
copy_and_shutdown(&mut tee_out, load_in),
|
||||
)?;
|
||||
copy_and_shutdown(&mut docker_rdr, load_in).await?;
|
||||
let res = load.wait_with_output().await?;
|
||||
if !res.status.success() {
|
||||
Err(Error::new(
|
||||
@@ -1164,7 +1085,7 @@ pub async fn install_s9pk<R: AsyncRead + AsyncSeek + Unpin>(
|
||||
if let Some(mut hdl) = rdr.scripts().await? {
|
||||
tokio::io::copy(
|
||||
&mut hdl,
|
||||
&mut File::create(dbg!(script_dir.join("embassy.js"))).await?,
|
||||
&mut File::create(script_dir.join("embassy.js")).await?,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
@@ -1195,13 +1116,7 @@ pub async fn install_s9pk<R: AsyncRead + AsyncSeek + Unpin>(
|
||||
tracing::info!("Install {}@{}: Installed interfaces", pkg_id, version);
|
||||
|
||||
tracing::info!("Install {}@{}: Creating manager", pkg_id, version);
|
||||
ctx.managers
|
||||
.add(
|
||||
ctx.clone(),
|
||||
manifest.clone(),
|
||||
manifest.interfaces.tor_keys(&mut sql_tx, pkg_id).await?,
|
||||
)
|
||||
.await?;
|
||||
ctx.managers.add(ctx.clone(), manifest.clone()).await?;
|
||||
tracing::info!("Install {}@{}: Created manager", pkg_id, version);
|
||||
|
||||
let static_files = StaticFiles::local(pkg_id, version, manifest.assets.icon_type());
|
||||
@@ -1223,7 +1138,7 @@ pub async fn install_s9pk<R: AsyncRead + AsyncSeek + Unpin>(
|
||||
let mut deps = BTreeMap::new();
|
||||
for package in crate::db::DatabaseModel::new()
|
||||
.package_data()
|
||||
.keys(&mut tx, true)
|
||||
.keys(&mut tx)
|
||||
.await?
|
||||
{
|
||||
// update dependency_info on dependents
|
||||
@@ -1257,7 +1172,7 @@ pub async fn install_s9pk<R: AsyncRead + AsyncSeek + Unpin>(
|
||||
.await?
|
||||
.installed()
|
||||
.and_then(|i| i.current_dependencies().idx_model(pkg_id))
|
||||
.get(&mut tx, true)
|
||||
.get(&mut tx)
|
||||
.await?
|
||||
.to_owned()
|
||||
{
|
||||
@@ -1349,6 +1264,7 @@ pub async fn install_s9pk<R: AsyncRead + AsyncSeek + Unpin>(
|
||||
let migration = manifest
|
||||
.migrations
|
||||
.from(
|
||||
&manifest.containers,
|
||||
ctx,
|
||||
&prev.manifest.version,
|
||||
pkg_id,
|
||||
@@ -1434,7 +1350,6 @@ pub async fn install_s9pk<R: AsyncRead + AsyncSeek + Unpin>(
|
||||
.restore(
|
||||
ctx,
|
||||
&mut tx,
|
||||
&mut sql_tx,
|
||||
pkg_id,
|
||||
version,
|
||||
&manifest.interfaces,
|
||||
@@ -1456,38 +1371,6 @@ pub async fn install_s9pk<R: AsyncRead + AsyncSeek + Unpin>(
|
||||
&receipts.config.update_dependency_receipts,
|
||||
)
|
||||
.await?;
|
||||
} else if let Some(recovered) = {
|
||||
receipts
|
||||
.recovered_packages
|
||||
.get(&mut tx)
|
||||
.await?
|
||||
.remove(pkg_id)
|
||||
} {
|
||||
handle_recovered_package(
|
||||
recovered,
|
||||
manifest,
|
||||
ctx,
|
||||
pkg_id,
|
||||
version,
|
||||
&mut tx,
|
||||
&receipts.config,
|
||||
)
|
||||
.await?;
|
||||
add_dependent_to_current_dependents_lists(
|
||||
&mut tx,
|
||||
pkg_id,
|
||||
¤t_dependencies,
|
||||
&receipts.config.current_dependents,
|
||||
)
|
||||
.await?;
|
||||
update_dependency_errors_of_dependents(
|
||||
ctx,
|
||||
&mut tx,
|
||||
pkg_id,
|
||||
¤t_dependents,
|
||||
&receipts.config.update_dependency_receipts,
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
add_dependent_to_current_dependents_lists(
|
||||
&mut tx,
|
||||
@@ -1506,67 +1389,20 @@ pub async fn install_s9pk<R: AsyncRead + AsyncSeek + Unpin>(
|
||||
.await?;
|
||||
}
|
||||
|
||||
let recovered_packages = {
|
||||
let mut r = receipts.recovered_packages.get(&mut tx).await?;
|
||||
r.remove(pkg_id);
|
||||
r
|
||||
};
|
||||
receipts
|
||||
.recovered_packages
|
||||
.set(&mut tx, recovered_packages)
|
||||
.await?;
|
||||
|
||||
if let Some(installed) = pde.installed() {
|
||||
reconfigure_dependents_with_live_pointers(ctx, &mut tx, &receipts.config, installed)
|
||||
.await?;
|
||||
}
|
||||
|
||||
sql_tx.commit().await?;
|
||||
tx.commit(None).await?;
|
||||
tx.commit().await?;
|
||||
|
||||
tracing::info!("Install {}@{}: Complete", pkg_id, version);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(ctx, tx, receipts))]
|
||||
async fn handle_recovered_package(
|
||||
recovered: RecoveredPackageInfo,
|
||||
manifest: Manifest,
|
||||
ctx: &RpcContext,
|
||||
pkg_id: &PackageId,
|
||||
version: &Version,
|
||||
tx: &mut patch_db::Transaction<&mut patch_db::PatchDbHandle>,
|
||||
receipts: &ConfigReceipts,
|
||||
) -> Result<(), Error> {
|
||||
let configured = if let Some(migration) =
|
||||
manifest
|
||||
.migrations
|
||||
.from(ctx, &recovered.version, pkg_id, version, &manifest.volumes)
|
||||
{
|
||||
migration.await?.configured
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if configured && manifest.config.is_some() {
|
||||
crate::config::configure(
|
||||
ctx,
|
||||
tx,
|
||||
pkg_id,
|
||||
None,
|
||||
&None,
|
||||
false,
|
||||
&mut BTreeMap::new(),
|
||||
&mut BTreeMap::new(),
|
||||
&receipts,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(datadir))]
|
||||
#[instrument(skip_all)]
|
||||
pub fn load_images<'a, P: AsRef<Path> + 'a + Send + Sync>(
|
||||
datadir: P,
|
||||
) -> BoxFuture<'a, Result<(), Error>> {
|
||||
@@ -1580,7 +1416,9 @@ pub fn load_images<'a, P: AsRef<Path> + 'a + Send + Sync>(
|
||||
.try_for_each(|entry| async move {
|
||||
let m = entry.metadata().await?;
|
||||
if m.is_file() {
|
||||
if entry.path().extension().and_then(|ext| ext.to_str()) == Some("tar") {
|
||||
let path = entry.path();
|
||||
let ext = path.extension().and_then(|ext| ext.to_str());
|
||||
if ext == Some("tar") || ext == Some("s9pk") {
|
||||
let mut load = Command::new("docker")
|
||||
.arg("load")
|
||||
.stdin(Stdio::piped())
|
||||
@@ -1592,8 +1430,24 @@ pub fn load_images<'a, P: AsRef<Path> + 'a + Send + Sync>(
|
||||
crate::ErrorKind::Docker,
|
||||
)
|
||||
})?;
|
||||
let mut docker_rdr = File::open(&entry.path()).await?;
|
||||
copy_and_shutdown(&mut docker_rdr, load_in).await?;
|
||||
match ext {
|
||||
Some("tar") => {
|
||||
copy_and_shutdown(&mut File::open(&path).await?, load_in)
|
||||
.await?
|
||||
}
|
||||
Some("s9pk") => {
|
||||
copy_and_shutdown(
|
||||
&mut S9pkReader::open(&path, false)
|
||||
.await?
|
||||
.docker_images()
|
||||
.await?,
|
||||
load_in,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
let res = load.wait_with_output().await?;
|
||||
if !res.status.success() {
|
||||
Err(Error::new(
|
||||
|
||||
@@ -113,6 +113,7 @@ impl InstallProgress {
|
||||
}
|
||||
|
||||
#[pin_project::pin_project]
|
||||
#[derive(Debug)]
|
||||
pub struct InstallProgressTracker<RW> {
|
||||
#[pin]
|
||||
inner: RW,
|
||||
|
||||
@@ -60,7 +60,7 @@ pub async fn update() -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(ctx))]
|
||||
#[instrument(skip_all)]
|
||||
#[command(display(display_serializable))]
|
||||
pub async fn dry(
|
||||
#[context] ctx: RpcContext,
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
pub const DEFAULT_MARKETPLACE: &str = "https://marketplace.start9.com";
|
||||
#![recursion_limit = "256"]
|
||||
|
||||
pub const DEFAULT_MARKETPLACE: &str = "https://registry.start9.com";
|
||||
// pub const COMMUNITY_MARKETPLACE: &str = "https://community-registry.start9.com";
|
||||
pub const BUFFER_SIZE: usize = 1024;
|
||||
pub const HOST_IP: [u8; 4] = [172, 18, 0, 1];
|
||||
pub const TARGET: &str = current_platform::CURRENT_PLATFORM;
|
||||
@@ -7,8 +10,12 @@ lazy_static::lazy_static! {
|
||||
let (arch, _) = TARGET.split_once("-").unwrap();
|
||||
arch
|
||||
};
|
||||
pub static ref IS_RASPBERRY_PI: bool = {
|
||||
*ARCH == "aarch64"
|
||||
};
|
||||
}
|
||||
|
||||
pub mod account;
|
||||
pub mod action;
|
||||
pub mod auth;
|
||||
pub mod backup;
|
||||
@@ -23,7 +30,6 @@ pub mod diagnostic;
|
||||
pub mod disk;
|
||||
pub mod error;
|
||||
pub mod hostname;
|
||||
pub mod id;
|
||||
pub mod init;
|
||||
pub mod inspect;
|
||||
pub mod install;
|
||||
@@ -34,6 +40,7 @@ pub mod middleware;
|
||||
pub mod migration;
|
||||
pub mod net;
|
||||
pub mod notifications;
|
||||
pub mod os_install;
|
||||
pub mod procedure;
|
||||
pub mod properties;
|
||||
pub mod s9pk;
|
||||
@@ -41,7 +48,6 @@ pub mod setup;
|
||||
pub mod shutdown;
|
||||
pub mod sound;
|
||||
pub mod ssh;
|
||||
pub mod static_server;
|
||||
pub mod status;
|
||||
pub mod system;
|
||||
pub mod update;
|
||||
@@ -80,6 +86,7 @@ pub fn main_api() -> Result<(), RpcError> {
|
||||
}
|
||||
|
||||
#[command(subcommands(
|
||||
system::time,
|
||||
system::logs,
|
||||
system::kernel_logs,
|
||||
system::metrics,
|
||||
@@ -97,7 +104,6 @@ pub fn server() -> Result<(), RpcError> {
|
||||
install::install,
|
||||
install::sideload,
|
||||
install::uninstall,
|
||||
install::delete_recovered,
|
||||
install::list,
|
||||
install::update::update,
|
||||
config::config,
|
||||
@@ -133,3 +139,8 @@ pub fn diagnostic_api() -> Result<(), RpcError> {
|
||||
pub fn setup_api() -> Result<(), RpcError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command(subcommands(version::git_info, echo, os_install::install))]
|
||||
pub fn install_api() -> Result<(), RpcError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,22 +1,119 @@
|
||||
use std::future::Future;
|
||||
use std::marker::PhantomData;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::process::Stdio;
|
||||
use std::time::{Duration, UNIX_EPOCH};
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use clap::ArgMatches;
|
||||
use color_eyre::eyre::eyre;
|
||||
use futures::TryStreamExt;
|
||||
use futures::stream::BoxStream;
|
||||
use futures::{FutureExt, SinkExt, Stream, StreamExt, TryStreamExt};
|
||||
use hyper::upgrade::Upgraded;
|
||||
use hyper::Error as HyperError;
|
||||
use rpc_toolkit::command;
|
||||
use rpc_toolkit::yajrc::RpcError;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
use tokio::process::Command;
|
||||
use tokio::process::{Child, Command};
|
||||
use tokio::task::JoinError;
|
||||
use tokio_stream::wrappers::LinesStream;
|
||||
use tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode;
|
||||
use tokio_tungstenite::tungstenite::protocol::CloseFrame;
|
||||
use tokio_tungstenite::tungstenite::Message;
|
||||
use tokio_tungstenite::WebSocketStream;
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::context::{CliContext, RpcContext};
|
||||
use crate::core::rpc_continuations::{RequestGuid, RpcContinuation};
|
||||
use crate::error::ResultExt;
|
||||
use crate::procedure::docker::DockerProcedure;
|
||||
use crate::s9pk::manifest::PackageId;
|
||||
use crate::util::display_none;
|
||||
use crate::util::serde::Reversible;
|
||||
use crate::Error;
|
||||
use crate::{Error, ErrorKind};
|
||||
|
||||
#[pin_project::pin_project]
|
||||
struct LogStream {
|
||||
_child: Child,
|
||||
#[pin]
|
||||
entries: BoxStream<'static, Result<JournalctlEntry, Error>>,
|
||||
}
|
||||
impl Deref for LogStream {
|
||||
type Target = BoxStream<'static, Result<JournalctlEntry, Error>>;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.entries
|
||||
}
|
||||
}
|
||||
impl DerefMut for LogStream {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.entries
|
||||
}
|
||||
}
|
||||
impl Stream for LogStream {
|
||||
type Item = Result<JournalctlEntry, Error>;
|
||||
fn poll_next(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Option<Self::Item>> {
|
||||
let this = self.project();
|
||||
Stream::poll_next(this.entries, cx)
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
self.entries.size_hint()
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
async fn ws_handler<
|
||||
WSFut: Future<Output = Result<Result<WebSocketStream<Upgraded>, HyperError>, JoinError>>,
|
||||
>(
|
||||
first_entry: Option<LogEntry>,
|
||||
mut logs: LogStream,
|
||||
ws_fut: WSFut,
|
||||
) -> Result<(), Error> {
|
||||
let mut stream = ws_fut
|
||||
.await
|
||||
.with_kind(crate::ErrorKind::Network)?
|
||||
.with_kind(crate::ErrorKind::Unknown)?;
|
||||
|
||||
if let Some(first_entry) = first_entry {
|
||||
stream
|
||||
.send(Message::Text(
|
||||
serde_json::to_string(&first_entry).with_kind(ErrorKind::Serialization)?,
|
||||
))
|
||||
.await
|
||||
.with_kind(ErrorKind::Network)?;
|
||||
}
|
||||
|
||||
let mut ws_closed = false;
|
||||
while let Some(entry) = tokio::select! {
|
||||
a = logs.try_next() => Some(a?),
|
||||
a = stream.try_next() => { a.with_kind(crate::ErrorKind::Network)?; ws_closed = true; None }
|
||||
} {
|
||||
if let Some(entry) = entry {
|
||||
let (_, log_entry) = entry.log_entry()?;
|
||||
stream
|
||||
.send(Message::Text(
|
||||
serde_json::to_string(&log_entry).with_kind(ErrorKind::Serialization)?,
|
||||
))
|
||||
.await
|
||||
.with_kind(ErrorKind::Network)?;
|
||||
}
|
||||
}
|
||||
|
||||
if !ws_closed {
|
||||
stream
|
||||
.close(Some(CloseFrame {
|
||||
code: CloseCode::Normal,
|
||||
reason: "Log Stream Finished".into(),
|
||||
}))
|
||||
.await
|
||||
.with_kind(ErrorKind::Network)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
@@ -25,6 +122,12 @@ pub struct LogResponse {
|
||||
start_cursor: Option<String>,
|
||||
end_cursor: Option<String>,
|
||||
}
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct LogFollowResponse {
|
||||
start_cursor: Option<String>,
|
||||
guid: RequestGuid,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
||||
pub struct LogEntry {
|
||||
@@ -111,38 +214,145 @@ pub enum LogSource {
|
||||
Container(PackageId),
|
||||
}
|
||||
|
||||
pub fn display_logs(all: LogResponse, _: &ArgMatches) {
|
||||
for entry in all.entries.iter() {
|
||||
println!("{}", entry);
|
||||
}
|
||||
}
|
||||
|
||||
#[command(display(display_logs))]
|
||||
#[command(
|
||||
custom_cli(cli_logs(async, context(CliContext))),
|
||||
subcommands(self(logs_nofollow(async)), logs_follow),
|
||||
display(display_none)
|
||||
)]
|
||||
pub async fn logs(
|
||||
#[arg] id: PackageId,
|
||||
#[arg] limit: Option<usize>,
|
||||
#[arg] cursor: Option<String>,
|
||||
#[arg] before_flag: Option<bool>,
|
||||
#[arg(short = 'l', long = "limit")] limit: Option<usize>,
|
||||
#[arg(short = 'c', long = "cursor")] cursor: Option<String>,
|
||||
#[arg(short = 'B', long = "before", default)] before: bool,
|
||||
#[arg(short = 'f', long = "follow", default)] follow: bool,
|
||||
) -> Result<(PackageId, Option<usize>, Option<String>, bool, bool), Error> {
|
||||
Ok((id, limit, cursor, before, follow))
|
||||
}
|
||||
pub async fn cli_logs(
|
||||
ctx: CliContext,
|
||||
(id, limit, cursor, before, follow): (PackageId, Option<usize>, Option<String>, bool, bool),
|
||||
) -> Result<(), RpcError> {
|
||||
if follow {
|
||||
if cursor.is_some() {
|
||||
return Err(RpcError::from(Error::new(
|
||||
eyre!("The argument '--cursor <cursor>' cannot be used with '--follow'"),
|
||||
crate::ErrorKind::InvalidRequest,
|
||||
)));
|
||||
}
|
||||
if before {
|
||||
return Err(RpcError::from(Error::new(
|
||||
eyre!("The argument '--before' cannot be used with '--follow'"),
|
||||
crate::ErrorKind::InvalidRequest,
|
||||
)));
|
||||
}
|
||||
cli_logs_generic_follow(ctx, "package.logs.follow", Some(id), limit).await
|
||||
} else {
|
||||
cli_logs_generic_nofollow(ctx, "package.logs", Some(id), limit, cursor, before).await
|
||||
}
|
||||
}
|
||||
pub async fn logs_nofollow(
|
||||
_ctx: (),
|
||||
(id, limit, cursor, before, _): (PackageId, Option<usize>, Option<String>, bool, bool),
|
||||
) -> Result<LogResponse, Error> {
|
||||
Ok(fetch_logs(
|
||||
LogSource::Container(id),
|
||||
limit,
|
||||
cursor,
|
||||
before_flag.unwrap_or(false),
|
||||
)
|
||||
.await?)
|
||||
fetch_logs(LogSource::Container(id), limit, cursor, before).await
|
||||
}
|
||||
#[command(rpc_only, rename = "follow", display(display_none))]
|
||||
pub async fn logs_follow(
|
||||
#[context] ctx: RpcContext,
|
||||
#[parent_data] (id, limit, _, _, _): (PackageId, Option<usize>, Option<String>, bool, bool),
|
||||
) -> Result<LogFollowResponse, Error> {
|
||||
follow_logs(ctx, LogSource::Container(id), limit).await
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
pub async fn fetch_logs(
|
||||
id: LogSource,
|
||||
pub async fn cli_logs_generic_nofollow(
|
||||
ctx: CliContext,
|
||||
method: &str,
|
||||
id: Option<PackageId>,
|
||||
limit: Option<usize>,
|
||||
cursor: Option<String>,
|
||||
before_flag: bool,
|
||||
) -> Result<LogResponse, Error> {
|
||||
let mut cmd = Command::new("journalctl");
|
||||
before: bool,
|
||||
) -> Result<(), RpcError> {
|
||||
let res = rpc_toolkit::command_helpers::call_remote(
|
||||
ctx.clone(),
|
||||
method,
|
||||
serde_json::json!({
|
||||
"id": id,
|
||||
"limit": limit,
|
||||
"cursor": cursor,
|
||||
"before": before,
|
||||
}),
|
||||
PhantomData::<LogResponse>,
|
||||
)
|
||||
.await?
|
||||
.result?;
|
||||
|
||||
let limit = limit.unwrap_or(50);
|
||||
for entry in res.entries.iter() {
|
||||
println!("{}", entry);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn cli_logs_generic_follow(
|
||||
ctx: CliContext,
|
||||
method: &str,
|
||||
id: Option<PackageId>,
|
||||
limit: Option<usize>,
|
||||
) -> Result<(), RpcError> {
|
||||
let res = rpc_toolkit::command_helpers::call_remote(
|
||||
ctx.clone(),
|
||||
method,
|
||||
serde_json::json!({
|
||||
"id": id,
|
||||
"limit": limit,
|
||||
}),
|
||||
PhantomData::<LogFollowResponse>,
|
||||
)
|
||||
.await?
|
||||
.result?;
|
||||
|
||||
let mut base_url = ctx.base_url.clone();
|
||||
let ws_scheme = match base_url.scheme() {
|
||||
"https" => "wss",
|
||||
"http" => "ws",
|
||||
_ => {
|
||||
return Err(Error::new(
|
||||
eyre!("Cannot parse scheme from base URL"),
|
||||
crate::ErrorKind::ParseUrl,
|
||||
)
|
||||
.into())
|
||||
}
|
||||
};
|
||||
base_url.set_scheme(ws_scheme).or_else(|_| {
|
||||
Err(Error::new(
|
||||
eyre!("Cannot set URL scheme"),
|
||||
crate::ErrorKind::ParseUrl,
|
||||
))
|
||||
})?;
|
||||
let (mut stream, _) =
|
||||
// base_url is "http://127.0.0.1/", with a trailing slash, so we don't put a leading slash in this path:
|
||||
tokio_tungstenite::connect_async(format!("{}ws/rpc/{}", base_url, res.guid)).await?;
|
||||
while let Some(log) = stream.try_next().await? {
|
||||
match log {
|
||||
Message::Text(log) => {
|
||||
println!("{}", serde_json::from_str::<LogEntry>(&log)?);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn journalctl(
|
||||
id: LogSource,
|
||||
limit: usize,
|
||||
cursor: Option<&str>,
|
||||
before: bool,
|
||||
follow: bool,
|
||||
) -> Result<LogStream, Error> {
|
||||
let mut cmd = Command::new("journalctl");
|
||||
cmd.kill_on_drop(true);
|
||||
|
||||
cmd.arg("--output=json");
|
||||
cmd.arg("--output-fields=MESSAGE");
|
||||
@@ -163,16 +373,15 @@ pub async fn fetch_logs(
|
||||
}
|
||||
};
|
||||
|
||||
let cursor_formatted = format!("--after-cursor={}", cursor.clone().unwrap_or("".to_owned()));
|
||||
let mut get_prev_logs_and_reverse = false;
|
||||
let cursor_formatted = format!("--after-cursor={}", cursor.clone().unwrap_or(""));
|
||||
if cursor.is_some() {
|
||||
cmd.arg(&cursor_formatted);
|
||||
if before_flag {
|
||||
get_prev_logs_and_reverse = true;
|
||||
if before {
|
||||
cmd.arg("--reverse");
|
||||
}
|
||||
}
|
||||
if get_prev_logs_and_reverse {
|
||||
cmd.arg("--reverse");
|
||||
if follow {
|
||||
cmd.arg("--follow");
|
||||
}
|
||||
|
||||
let mut child = cmd.stdout(Stdio::piped()).spawn()?;
|
||||
@@ -185,7 +394,7 @@ pub async fn fetch_logs(
|
||||
|
||||
let journalctl_entries = LinesStream::new(out.lines());
|
||||
|
||||
let mut deserialized_entries = journalctl_entries
|
||||
let deserialized_entries = journalctl_entries
|
||||
.map_err(|e| Error::new(e, crate::ErrorKind::Journald))
|
||||
.and_then(|s| {
|
||||
futures::future::ready(
|
||||
@@ -194,16 +403,37 @@ pub async fn fetch_logs(
|
||||
)
|
||||
});
|
||||
|
||||
Ok(LogStream {
|
||||
_child: child,
|
||||
entries: deserialized_entries.boxed(),
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn fetch_logs(
|
||||
id: LogSource,
|
||||
limit: Option<usize>,
|
||||
cursor: Option<String>,
|
||||
before: bool,
|
||||
) -> Result<LogResponse, Error> {
|
||||
let limit = limit.unwrap_or(50);
|
||||
let mut stream = journalctl(id, limit, cursor.as_deref(), before, false).await?;
|
||||
|
||||
let mut entries = Vec::with_capacity(limit);
|
||||
let mut start_cursor = None;
|
||||
|
||||
if let Some(first) = deserialized_entries.try_next().await? {
|
||||
if let Some(first) = tokio::time::timeout(Duration::from_secs(1), stream.try_next())
|
||||
.await
|
||||
.ok()
|
||||
.transpose()?
|
||||
.flatten()
|
||||
{
|
||||
let (cursor, entry) = first.log_entry()?;
|
||||
start_cursor = Some(cursor);
|
||||
entries.push(entry);
|
||||
}
|
||||
|
||||
let (mut end_cursor, entries) = deserialized_entries
|
||||
let (mut end_cursor, entries) = stream
|
||||
.try_fold(
|
||||
(start_cursor.clone(), entries),
|
||||
|(_, mut acc), entry| async move {
|
||||
@@ -215,7 +445,7 @@ pub async fn fetch_logs(
|
||||
.await?;
|
||||
let mut entries = Reversible::new(entries);
|
||||
// reverse again so output is always in increasing chronological order
|
||||
if get_prev_logs_and_reverse {
|
||||
if cursor.is_some() && before {
|
||||
entries.reverse();
|
||||
std::mem::swap(&mut start_cursor, &mut end_cursor);
|
||||
}
|
||||
@@ -226,21 +456,81 @@ pub async fn fetch_logs(
|
||||
})
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
pub async fn test_logs() {
|
||||
let response = fetch_logs(
|
||||
// change `tor.service` to an actual journald unit on your machine
|
||||
// LogSource::Service("tor.service"),
|
||||
// first run `docker run --name=hello-world.embassy --log-driver=journald hello-world`
|
||||
LogSource::Container("hello-world".parse().unwrap()),
|
||||
// Some(5),
|
||||
None,
|
||||
None,
|
||||
// Some("s=1b8c418e28534400856c27b211dd94fd;i=5a7;b=97571c13a1284f87bc0639b5cff5acbe;m=740e916;t=5ca073eea3445;x=f45bc233ca328348".to_owned()),
|
||||
false,
|
||||
#[instrument(skip_all)]
|
||||
pub async fn follow_logs(
|
||||
ctx: RpcContext,
|
||||
id: LogSource,
|
||||
limit: Option<usize>,
|
||||
) -> Result<LogFollowResponse, Error> {
|
||||
let limit = limit.unwrap_or(50);
|
||||
let mut stream = journalctl(id, limit, None, false, true).await?;
|
||||
|
||||
let mut start_cursor = None;
|
||||
let mut first_entry = None;
|
||||
|
||||
if let Some(first) = tokio::time::timeout(Duration::from_secs(1), stream.try_next())
|
||||
.await
|
||||
.ok()
|
||||
.transpose()?
|
||||
.flatten()
|
||||
{
|
||||
let (cursor, entry) = first.log_entry()?;
|
||||
start_cursor = Some(cursor);
|
||||
first_entry = Some(entry);
|
||||
}
|
||||
|
||||
let guid = RequestGuid::new();
|
||||
ctx.add_continuation(
|
||||
guid.clone(),
|
||||
RpcContinuation::ws(
|
||||
Box::new(move |ws_fut| ws_handler(first_entry, stream, ws_fut).boxed()),
|
||||
Duration::from_secs(30),
|
||||
),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let serialized = serde_json::to_string_pretty(&response).unwrap();
|
||||
println!("{}", serialized);
|
||||
.await;
|
||||
Ok(LogFollowResponse { start_cursor, guid })
|
||||
}
|
||||
|
||||
// #[tokio::test]
|
||||
// pub async fn test_logs() {
|
||||
// let response = fetch_logs(
|
||||
// // change `tor.service` to an actual journald unit on your machine
|
||||
// // LogSource::Service("tor.service"),
|
||||
// // first run `docker run --name=hello-world.embassy --log-driver=journald hello-world`
|
||||
// LogSource::Container("hello-world".parse().unwrap()),
|
||||
// // Some(5),
|
||||
// None,
|
||||
// None,
|
||||
// // Some("s=1b8c418e28534400856c27b211dd94fd;i=5a7;b=97571c13a1284f87bc0639b5cff5acbe;m=740e916;t=5ca073eea3445;x=f45bc233ca328348".to_owned()),
|
||||
// false,
|
||||
// true,
|
||||
// )
|
||||
// .await
|
||||
// .unwrap();
|
||||
// let serialized = serde_json::to_string_pretty(&response).unwrap();
|
||||
// println!("{}", serialized);
|
||||
// }
|
||||
|
||||
// #[tokio::test]
|
||||
// pub async fn test_logs() {
|
||||
// let mut cmd = Command::new("journalctl");
|
||||
// cmd.kill_on_drop(true);
|
||||
|
||||
// cmd.arg("-f");
|
||||
// cmd.arg("CONTAINER_NAME=hello-world.embassy");
|
||||
|
||||
// let mut child = cmd.stdout(Stdio::piped()).spawn().unwrap();
|
||||
// let out = BufReader::new(
|
||||
// child
|
||||
// .stdout
|
||||
// .take()
|
||||
// .ok_or_else(|| Error::new(eyre!("No stdout available"), crate::ErrorKind::Journald))
|
||||
// .unwrap(),
|
||||
// );
|
||||
|
||||
// let mut journalctl_entries = LinesStream::new(out.lines());
|
||||
|
||||
// while let Some(line) = journalctl_entries.try_next().await.unwrap() {
|
||||
// dbg!(line);
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -90,7 +90,7 @@ impl HealthCheckStatusReceipt {
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(ctx, db))]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn check<Db: DbHandle>(
|
||||
ctx: &RpcContext,
|
||||
db: &mut Db,
|
||||
@@ -113,7 +113,14 @@ pub async fn check<Db: DbHandle>(
|
||||
let health_results = if let Some(started) = started {
|
||||
manifest
|
||||
.health_checks
|
||||
.check_all(ctx, started, id, &manifest.version, &manifest.volumes)
|
||||
.check_all(
|
||||
ctx,
|
||||
&manifest.containers,
|
||||
started,
|
||||
id,
|
||||
&manifest.version,
|
||||
&manifest.volumes,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
return Ok(());
|
||||
@@ -128,20 +135,17 @@ pub async fn check<Db: DbHandle>(
|
||||
|
||||
let status = receipts.status.get(&mut checkpoint).await?;
|
||||
|
||||
match status {
|
||||
MainStatus::Running { health, started } => {
|
||||
receipts
|
||||
.status
|
||||
.set(
|
||||
&mut checkpoint,
|
||||
MainStatus::Running {
|
||||
health: health_results.clone(),
|
||||
started,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
_ => (),
|
||||
if let MainStatus::Running { health: _, started } = status {
|
||||
receipts
|
||||
.status
|
||||
.set(
|
||||
&mut checkpoint,
|
||||
MainStatus::Running {
|
||||
health: health_results.clone(),
|
||||
started,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
let current_dependents = receipts.current_dependents.get(&mut checkpoint).await?;
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::convert::TryInto;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
|
||||
use chrono::Utc;
|
||||
@@ -11,10 +9,10 @@ use crate::Error;
|
||||
|
||||
/// Allocates a db handle. DO NOT CALL with a db handle already in scope
|
||||
async fn synchronize_once(shared: &ManagerSharedState) -> Result<Status, Error> {
|
||||
let mut db = shared.ctx.db.handle();
|
||||
let mut db = shared.seed.ctx.db.handle();
|
||||
let mut status = crate::db::DatabaseModel::new()
|
||||
.package_data()
|
||||
.idx_model(&shared.manifest.id)
|
||||
.idx_model(&shared.seed.manifest.id)
|
||||
.expect(&mut db)
|
||||
.await?
|
||||
.installed()
|
||||
@@ -24,7 +22,7 @@ async fn synchronize_once(shared: &ManagerSharedState) -> Result<Status, Error>
|
||||
.main()
|
||||
.get_mut(&mut db)
|
||||
.await?;
|
||||
let manager_status = shared.status.load(Ordering::SeqCst).try_into().unwrap();
|
||||
let manager_status = *shared.status.1.borrow();
|
||||
match manager_status {
|
||||
Status::Stopped => match &mut *status {
|
||||
MainStatus::Stopped => (),
|
||||
@@ -83,17 +81,19 @@ async fn synchronize_once(shared: &ManagerSharedState) -> Result<Status, Error>
|
||||
}
|
||||
|
||||
pub async fn synchronizer(shared: &ManagerSharedState) {
|
||||
let mut status_recv = shared.status.0.subscribe();
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = tokio::time::sleep(Duration::from_secs(5)) => (),
|
||||
_ = shared.synchronize_now.notified() => (),
|
||||
_ = status_recv.changed() => (),
|
||||
}
|
||||
let status = match synchronize_once(shared).await {
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
"Synchronizer for {}@{} failed: {}",
|
||||
shared.manifest.id,
|
||||
shared.manifest.version,
|
||||
shared.seed.manifest.id,
|
||||
shared.seed.manifest.version,
|
||||
e
|
||||
);
|
||||
tracing::debug!("{:?}", e);
|
||||
@@ -101,7 +101,7 @@ pub async fn synchronizer(shared: &ManagerSharedState) {
|
||||
}
|
||||
Ok(status) => status,
|
||||
};
|
||||
tracing::trace!("{} status synchronized", shared.manifest.id);
|
||||
tracing::trace!("{} status synchronized", shared.seed.manifest.id);
|
||||
shared.synchronized.notify_waiters();
|
||||
match status {
|
||||
Status::Shutdown => {
|
||||
|
||||
@@ -12,7 +12,9 @@ use rpc_toolkit::command_helpers::prelude::RequestParts;
|
||||
use rpc_toolkit::hyper::header::COOKIE;
|
||||
use rpc_toolkit::hyper::http::Error as HttpError;
|
||||
use rpc_toolkit::hyper::{Body, Request, Response};
|
||||
use rpc_toolkit::rpc_server_helpers::{noop3, to_response, DynMiddleware, DynMiddlewareStage2};
|
||||
use rpc_toolkit::rpc_server_helpers::{
|
||||
noop4, to_response, DynMiddleware, DynMiddlewareStage2, DynMiddlewareStage3,
|
||||
};
|
||||
use rpc_toolkit::yajrc::RpcMethod;
|
||||
use rpc_toolkit::Metadata;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -21,6 +23,9 @@ use tokio::sync::Mutex;
|
||||
|
||||
use crate::context::RpcContext;
|
||||
use crate::{Error, ResultExt};
|
||||
|
||||
pub const LOCAL_AUTH_COOKIE_PATH: &str = "/run/embassy/rpc.authcookie";
|
||||
|
||||
pub trait AsLogoutSessionId {
|
||||
fn as_logout_session_id(self) -> String;
|
||||
}
|
||||
@@ -39,7 +44,7 @@ impl HasLoggedOutSessions {
|
||||
for session in logged_out_sessions {
|
||||
let session = session.as_logout_session_id();
|
||||
sqlx::query!(
|
||||
"UPDATE session SET logged_out = CURRENT_TIMESTAMP WHERE id = ?",
|
||||
"UPDATE session SET logged_out = CURRENT_TIMESTAMP WHERE id = $1",
|
||||
session
|
||||
)
|
||||
.execute(&mut sqlx_conn)
|
||||
@@ -61,12 +66,34 @@ impl HasValidSession {
|
||||
request_parts: &RequestParts,
|
||||
ctx: &RpcContext,
|
||||
) -> Result<Self, Error> {
|
||||
Self::from_session(&HashSessionToken::from_request_parts(request_parts)?, ctx).await
|
||||
if let Some(cookie_header) = request_parts.headers.get(COOKIE) {
|
||||
let cookies = Cookie::parse(
|
||||
cookie_header
|
||||
.to_str()
|
||||
.with_kind(crate::ErrorKind::Authorization)?,
|
||||
)
|
||||
.with_kind(crate::ErrorKind::Authorization)?;
|
||||
if let Some(cookie) = cookies.iter().find(|c| c.get_name() == "local") {
|
||||
if let Ok(s) = Self::from_local(cookie).await {
|
||||
return Ok(s);
|
||||
}
|
||||
}
|
||||
if let Some(cookie) = cookies.iter().find(|c| c.get_name() == "session") {
|
||||
if let Ok(s) = Self::from_session(&HashSessionToken::from_cookie(cookie), ctx).await
|
||||
{
|
||||
return Ok(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(Error::new(
|
||||
eyre!("UNAUTHORIZED"),
|
||||
crate::ErrorKind::Authorization,
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn from_session(session: &HashSessionToken, ctx: &RpcContext) -> Result<Self, Error> {
|
||||
let session_hash = session.hashed();
|
||||
let session = sqlx::query!("UPDATE session SET last_active = CURRENT_TIMESTAMP WHERE id = ? AND logged_out IS NULL OR logged_out > CURRENT_TIMESTAMP", session_hash)
|
||||
let session = sqlx::query!("UPDATE session SET last_active = CURRENT_TIMESTAMP WHERE id = $1 AND logged_out IS NULL OR logged_out > CURRENT_TIMESTAMP", session_hash)
|
||||
.execute(&mut ctx.secret_store.acquire().await?)
|
||||
.await?;
|
||||
if session.rows_affected() == 0 {
|
||||
@@ -77,6 +104,18 @@ impl HasValidSession {
|
||||
}
|
||||
Ok(Self(()))
|
||||
}
|
||||
|
||||
pub async fn from_local(local: &Cookie<'_>) -> Result<Self, Error> {
|
||||
let token = tokio::fs::read_to_string(LOCAL_AUTH_COOKIE_PATH).await?;
|
||||
if local.get_value() == &*token {
|
||||
Ok(Self(()))
|
||||
} else {
|
||||
Err(Error::new(
|
||||
eyre!("UNAUTHORIZED"),
|
||||
crate::ErrorKind::Authorization,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// When we have a need to create a new session,
|
||||
@@ -198,8 +237,7 @@ pub fn auth<M: Metadata>(ctx: RpcContext) -> DynMiddleware<M> {
|
||||
|_| StatusCode::OK,
|
||||
)?));
|
||||
} else if rpc_req.method.as_str() == "auth.login" {
|
||||
let mut guard = rate_limiter.lock().await;
|
||||
guard.0 += 1;
|
||||
let guard = rate_limiter.lock().await;
|
||||
if guard.1.elapsed() < Duration::from_secs(20) {
|
||||
if guard.0 >= 3 {
|
||||
let (res_parts, _) = Response::new(()).into_parts();
|
||||
@@ -216,13 +254,25 @@ pub fn auth<M: Metadata>(ctx: RpcContext) -> DynMiddleware<M> {
|
||||
|_| StatusCode::OK,
|
||||
)?));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let m3: DynMiddlewareStage3 = Box::new(move |_, res| {
|
||||
async move {
|
||||
let mut guard = rate_limiter.lock().await;
|
||||
if guard.1.elapsed() < Duration::from_secs(20) {
|
||||
if res.is_err() {
|
||||
guard.0 += 1;
|
||||
}
|
||||
} else {
|
||||
guard.0 = 0;
|
||||
}
|
||||
guard.1 = Instant::now();
|
||||
Ok(Ok(noop4()))
|
||||
}
|
||||
}
|
||||
Ok(Ok(noop3()))
|
||||
.boxed()
|
||||
});
|
||||
Ok(Ok(m3))
|
||||
}
|
||||
.boxed()
|
||||
});
|
||||
|
||||
82
backend/src/middleware/db.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
use color_eyre::eyre::eyre;
|
||||
use futures::future::BoxFuture;
|
||||
use futures::FutureExt;
|
||||
use http::HeaderValue;
|
||||
use rpc_toolkit::hyper::http::Error as HttpError;
|
||||
use rpc_toolkit::hyper::{Body, Request, Response};
|
||||
use rpc_toolkit::rpc_server_helpers::{
|
||||
noop4, DynMiddleware, DynMiddlewareStage2, DynMiddlewareStage3,
|
||||
};
|
||||
use rpc_toolkit::yajrc::RpcMethod;
|
||||
use rpc_toolkit::Metadata;
|
||||
|
||||
use crate::context::RpcContext;
|
||||
use crate::{Error, ResultExt};
|
||||
|
||||
pub fn db<M: Metadata>(ctx: RpcContext) -> DynMiddleware<M> {
|
||||
Box::new(
|
||||
move |_: &mut Request<Body>,
|
||||
metadata: M|
|
||||
-> BoxFuture<Result<Result<DynMiddlewareStage2, Response<Body>>, HttpError>> {
|
||||
let ctx = ctx.clone();
|
||||
async move {
|
||||
let m2: DynMiddlewareStage2 = Box::new(move |req, rpc_req| {
|
||||
async move {
|
||||
let seq = req.headers.remove("x-patch-sequence");
|
||||
let sync_db = metadata
|
||||
.get(rpc_req.method.as_str(), "sync_db")
|
||||
.unwrap_or(false);
|
||||
|
||||
let m3: DynMiddlewareStage3 = Box::new(move |res, _| {
|
||||
async move {
|
||||
if sync_db && seq.is_some() {
|
||||
match async {
|
||||
let seq = seq
|
||||
.ok_or_else(|| {
|
||||
Error::new(
|
||||
eyre!("Missing X-Patch-Sequence"),
|
||||
crate::ErrorKind::InvalidRequest,
|
||||
)
|
||||
})?
|
||||
.to_str()
|
||||
.with_kind(crate::ErrorKind::InvalidRequest)?
|
||||
.parse()?;
|
||||
let res = ctx.db.sync(seq).await?;
|
||||
let json = match res {
|
||||
Ok(revs) => serde_json::to_vec(&revs),
|
||||
Err(dump) => serde_json::to_vec(&[dump]),
|
||||
}
|
||||
.with_kind(crate::ErrorKind::Serialization)?;
|
||||
Ok::<_, Error>(base64::encode_config(
|
||||
&json,
|
||||
base64::URL_SAFE,
|
||||
))
|
||||
}
|
||||
.await
|
||||
{
|
||||
Ok(a) => res
|
||||
.headers
|
||||
.append("X-Patch-Updates", HeaderValue::from_str(&a)?),
|
||||
Err(e) => res.headers.append(
|
||||
"X-Patch-Error",
|
||||
HeaderValue::from_str(&base64::encode_config(
|
||||
&e.to_string(),
|
||||
base64::URL_SAFE,
|
||||
))?,
|
||||
),
|
||||
};
|
||||
}
|
||||
Ok(Ok(noop4()))
|
||||
}
|
||||
.boxed()
|
||||
});
|
||||
Ok(Ok(m3))
|
||||
}
|
||||
.boxed()
|
||||
});
|
||||
Ok(Ok(m2))
|
||||
}
|
||||
.boxed()
|
||||
},
|
||||
)
|
||||
}
|
||||