Compare commits

...

62 Commits

Author SHA1 Message Date
Keagan McClelland
21f6560074 fix agent code review 2021-07-13 15:15:19 -06:00
Keagan McClelland
a077600c7e fix build issues 2021-07-13 15:15:19 -06:00
Keagan McClelland
59f0d4e23a change release notes 2021-07-13 15:15:19 -06:00
Keagan McClelland
e64b92c5dd alter semantics of tor update 2021-07-13 15:15:19 -06:00
Keagan McClelland
748379becc preps 0.2.14 messaging and version bumps 2021-07-13 15:15:19 -06:00
Keagan McClelland
5b3163465d updates appmgr to 0.2.14 ceremonial 2021-07-13 15:15:19 -06:00
Keagan McClelland
b00af8980a update appmgr dependency 2021-07-13 15:15:19 -06:00
Keagan McClelland
8708a4de8e agent 0.2.14 2021-07-13 15:15:19 -06:00
Lucy C
d12a7f8931 fix cabal version and update welcome copy (#327)
* fix cabal version and update welcome copy

* fixes autogen'ed cabal file

Co-authored-by: Keagan McClelland <keagan.mcclelland@gmail.com>
2021-05-21 13:01:18 -06:00
Matt Hill
8f9111ce3d Integration/0.2.13 (#324)
* updates to 8.10.4, adjusts dependencies, adds license info feature

* add toJSON

* add licesne info to services

* remove mocks

* adds license info to available show

* prepare upgrade messaging

* better welcome message

* update backend versioning to 0.2.13

* add version migration file

* update ui build scripts

* update eos image

* update eos image with embassy

* add migration files

* update welcome page

* explicity add migration files

Co-authored-by: Keagan McClelland <keagan.mcclelland@gmail.com>
Co-authored-by: Lucy Cifferello <12953208+elvece@users.noreply.github.com>
2021-05-19 11:46:37 -06:00
Lucy Cifferello
7509c3a91e update build guide instructions for ghc 8.10.4 2021-05-18 15:34:40 -06:00
kn0wmad
3126d6138e Update README.md (#318) 2021-05-05 10:01:31 -06:00
Julian Ospald
5d4837d942 Add proper cabal support 2021-04-26 13:50:01 -06:00
@RandyMcMillan
660c0c5ff4 Update BuildGuide.md 2021-04-23 11:28:31 -06:00
Mariusz Kogen
4c6c2768b3 👷 Cleaner look 2021-04-07 15:31:34 -06:00
Chris Guida
6ddf7ce40b Update README.md 2021-04-02 17:21:26 -06:00
Aiden McClelland
4f16d82294 Update README.md 2021-04-02 13:49:03 -06:00
kn0wmad
7845044a3c Added screenshots to README 2021-04-02 13:49:03 -06:00
kn0wmad
20f91b10db Added screenshots to README 2021-04-02 13:49:03 -06:00
kn0wmad
ec2b707353 README edit 2021-04-02 13:49:03 -06:00
kn0wmad
e609d3af1e README edit 2021-04-02 13:49:03 -06:00
kn0wmad
5b5495cd51 Added Instructions to appmgr README 2021-04-02 13:49:03 -06:00
kn0wmad
8ba23f05a4 Added Instructions to appmgr README 2021-04-02 13:49:03 -06:00
Aiden McClelland
a4b1529dc4 Update README.md 2021-04-02 13:44:09 -06:00
Chris Guida
da81aec9cc add ui mock action with only one allowed status 2021-04-02 10:55:52 -06:00
Aiden McClelland
c440f637f3 ui: fix allowed statuses message 2021-04-02 10:55:52 -06:00
Matt Hill
9036c3ffed update welcome message 2021-04-02 10:55:52 -06:00
Keagan McClelland
98a242229a package lock 2021-04-02 10:55:52 -06:00
Keagan McClelland
74659d717a ui ceremonial changes for 0.2.12 2021-04-02 10:55:52 -06:00
Keagan McClelland
e6dbbf125c pin appmgr 0.2.12 2021-04-02 10:55:52 -06:00
Keagan McClelland
80f509a634 0.2.12 ceremonial release 2021-04-02 10:55:52 -06:00
Aiden McClelland
5d6a175585 ui: bump version 2021-04-02 10:55:52 -06:00
Aiden McClelland
6ef46ae309 bump version 2021-04-02 10:55:52 -06:00
Aiden McClelland
13c94241c2 retain x-forwarded-proto 2021-04-02 10:55:52 -06:00
Chris Guida
56041fd503 disable buffering in nginx 2021-03-30 20:14:02 -06:00
Mariusz Kogen
f52bb54a2f 🌠 Image display fix (#274)
Added community matrix and mastodon shields
2021-03-30 08:45:10 -06:00
Mariusz Kogen
7f9f942eb1 🛠️ Adding BuildGuide branch for the ease of use (#252)
* Create BuildGuide.md

* bug fixes

* Update BuildGuide.md

* Update make_image.sh

* Update BuildGuide.md

* Update make_image.sh

additional improvements and "done" message added. Thanks @k0gen!

* Update BuildGuide.md

Added intro notes and made minor adjustments to the guide.

* Update setup.sh

Required adjustments to prevent reboot when following BuildGuide.md

* Update BuildGuide.md

Improvements to final setup steps

* bug fix

additional improvements and "done" message added. Thanks @k0gen!

* Update Makefile

Changes to facilitate building process when using the BuildGuide

* Update BuildGuide.md

Avoiding manual changes to Makefile and cleaning up step 7

* Update BuildGuide.md

Switching from sftp to cp for one line command simplification

* Update BuildGuide.md

Simplified method of transferring .img to desktop. Thanks @k0gen!

* Update BuildGuide.md

update to latest openssl https://www.openssl.org/news/openssl-1.1.1-notes.html

* Update BuildGuide.md

Simplified step 6 and added new required dependency

* Update BuildGuide.md

Added hint on how to check `agent` log

* Update setup.sh

Added missing dependency

* Update BuildGuide.md

Simplified step 6

* Simplifying Rust installation

One line install, reboot is no longer needed.

* make_image.sh +x 

Make it executable before running

* Step no longer needed

chmod +x done by Makefile

* Update BuildGuide.md

Added dependency for Rust setup

* Adding BuildGuide branch for the ease of use

* Forgot about the guide file :)

* Update BuildGuide.md

apt -y by default and some environment add-ons

Co-authored-by: Tommy Smith <63304263+t0mmysm1th@users.noreply.github.com>
2021-03-29 14:21:01 -06:00
Matt Hill
1ca7a699c1 Update use-mocks.json 2021-03-25 10:49:26 -06:00
Mariusz Kogen
481accc9e6 Update CONTRIBUTING.md
Fixing bug tracker broken link, adding active matrix links
2021-03-23 12:32:37 -06:00
Lucy C
11b007a31d Fix/integration/0.2.11 (#265)
* backports tor security fix to 0.2.10, adds functionality to allow for ssh key management during an update (#263)

* actually upgrade to 0.3.5.14-1

* update lan services on backup restore

* reload nginx, update welcome message, move reset lan to handler

* moves lan refresh after backup restore to asynchronous part of restore

* fix certificate generation

* match guards

Co-authored-by: Keagan McClelland <keagan.mcclelland@gmail.com>
2021-03-19 16:50:52 -06:00
Lucy C
5b8f27e53e Appmgr/fix/restart dep on install (#262)
* appmgr: fix: restart dep on install

* appmgr: fix: bind before restart

Co-authored-by: Aiden McClelland <me@drbonez.dev>
2021-03-18 00:46:29 -06:00
Aiden McClelland
9f4523676f appmgr: fix: restart dep on install (#261) 2021-03-17 22:08:12 -06:00
Lucy C
bc5163d800 Appmgr/debug/uninstall (#260)
* with context debuggging

* appmgr: fix errors

* appmgr: add more logging

* appmgr: more logs

* appmgr: make unbind more robust

Co-authored-by: Aiden McClelland <me@drbonez.dev>
2021-03-17 18:55:51 -06:00
Lucy C
9b7fe03c19 copy updates for 0.2.11 (#248)
* copy updates for 0.2.11

* fix moderate npm vulns

* fix mocks and welcome page layout
2021-03-17 12:30:41 -06:00
Lucy Cifferello
9a2aaa08b8 ignore ds_store 2021-03-17 12:30:41 -06:00
Lucy Cifferello
8c87e6653c fix import 2021-03-17 12:30:41 -06:00
Chris Guida
1c3b16e870 Appmgr/bugfix/nginx file too large migrations (#258)
* Nginx: allow infinite body sizes

* add nginx refresh to migration script
2021-03-17 12:30:41 -06:00
Lucy Cifferello
276085f084 fix mocks file 2021-03-17 12:30:41 -06:00
Lucy Cifferello
52fc992090 update welcome page release notes 2021-03-17 12:30:41 -06:00
Chris Guida
af46a375a9 Nginx: allow infinite body sizes (#256) 2021-03-17 12:30:41 -06:00
Aiden McClelland
74a559eade ui: add elements to union page 2021-03-17 12:30:41 -06:00
Aiden McClelland
f12d97122a ui: edited dot on union enum 2021-03-17 12:30:41 -06:00
Aiden McClelland
ba9b3519de ui: fix isEdited for unions 2021-03-17 12:30:41 -06:00
Aiden McClelland
43e89df652 appmgr: handle optional dep unmounts on uninstall 2021-03-17 12:30:41 -06:00
Keagan McClelland
7bdc109bd4 retry once on exit 6 for list 2021-03-16 11:20:33 -06:00
Lucy C
ac5dec476d copy updates for 0.2.11 (#248)
* copy updates for 0.2.11

* fix moderate npm vulns

* fix mocks and welcome page layout
2021-03-12 15:42:18 -07:00
Keagan McClelland
1f56be3cbf 0.2.11 bump 2021-03-12 15:42:18 -07:00
Keagan McClelland
ed46ddbf44 removes default welcome to nginx page (#247) 2021-03-12 15:42:18 -07:00
Keagan McClelland
2973c316a8 catches if either file doesn't exist and runs the sync if so (#245) 2021-03-12 15:42:18 -07:00
Aiden McClelland
7e7a9dc140 appmgr: version bump 2021-03-12 15:42:18 -07:00
Aiden McClelland
b95686282d ui: fix union error message 2021-03-12 15:42:18 -07:00
Aiden McClelland
09f858d28d appmgr: only mount binds if installed
also improve build scripts
2021-03-12 15:42:18 -07:00
79 changed files with 4725 additions and 1119 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
.DS_Store
/*.img
/buster.zip
/product_key

175
BuildGuide.md Normal file
View File

@@ -0,0 +1,175 @@
##### Initial Notes & Recommendations
* Due to issues to cross-compile the image from a desktop, this guide will take you step-by-step through the process of compiling EmbassyOS directly on a Raspberry Pi 4 (4GB or 8GB)
* This process will go faster if you have an SSD/NVMe USB drive available.
* This build guide does **not** require a large microSD card, especially if your final build wil be used on an SSD/NVMe USB drive.
* Basic know-how of linux commands and terminal use is recommended.
* Follow the guide carefully and do not skip any steps.
# :hammer_and_wrench: Build Guide
1. Flash [Raspberry Pi OS Lite](https://www.raspberrypi.org/software/operating-systems/) to a microSD and configure your raspi to boot from SSD/NVMe USB drive
1. After flashing, create an empty text file called `ssh` in the `boot` partition of the microSD, then proceed with booting the raspi with the flashed microSD (check your router for the IP assigned to your raspi)
1. Do the usual initial update/config
```
sudo apt update
sudo raspi-config
```
1. Change `Advanced Options->Boot Order`
1. Select `USB Boot` *(it will try to boot from microSD first if it's available)*
1. Select `Finish`, then `Yes` to reboot
1. After reboot, `sudo shutdown now` to power off the raspi and remove the microSD
2. Flash the *Raspi OS Lite* (from step 1) to your SSD/NVMe drive
> :information_source: Don't worry about rootfs partition size (raspi will increase it for you on initial boot)
> :information_source: Every time you re-flash your SSD/NVMe you need to first boot with a microSD and set *Boot Order* again
1. Don't forget to create the empty `ssh` file
1. Connect the drive (remember to remove the microSD) to the raspi and start it up
1. Use `sudo raspi-config` to change the default password
1. Optional: `sudo apt upgrade -y`
1. Optional: `sudo nano /etc/apt/sources.list.d/vscode.list` comment the last line which contains `packages.microsoft.com`
3. Install GHC
```
sudo apt update
sudo apt install -y ghc
#test:
ghc --version
#example of output:
The Glorious Glasgow Haskell Compilation System, version 8.4.4
```
4. Compile Stack:
1. Install Stack v2.1.3
```
cd ~/
wget -qO- https://raw.githubusercontent.com/commercialhaskell/stack/v2.1.3/etc/scripts/get-stack.sh | sh
#test with
stack --version
#example output:
Version 2.1.3, Git revision 636e3a759d51127df2b62f90772def126cdf6d1f (7735 commits) arm hpack-0.31.2
```
1. Use current Stack to compile Stack v2.5.1:
```
git clone --depth 1 --branch v2.5.1 https://github.com/commercialhaskell/stack.git
cd stack
sudo apt install -y screen
screen
```
> :information_source: Build (>=3.5h total... We are using `screen` in case of session timeout issues)
> :memo: If you get disconected you can reattach last sesion again by executing `screen -r`
```
stack build --stack-yaml=stack-ghc-84.yaml --system-ghc
#Install
stack install --stack-yaml=stack-ghc-84.yaml --system-ghc
export PATH=~/.local/bin:$PATH
```
5. Clone EmbassyOS & try to *make* the `agent`:
1. First attempt
> :information_source: The first time you run **make** you'll get an error
```
sudo apt install -y llvm-9 libgmp-dev
export PATH=/usr/lib/llvm-9/bin:$PATH
cd ~/
git clone https://github.com/Start9Labs/embassy-os.git
cd embassy-os/
make agent
```
> :memo: This will install ghc-8.10.2, then attempt to build but will give errors (in next steps we deal with errors)
1. Confirm your cpu info
```
cat /proc/cpuinfo | grep Hardware
```
1. If your "Hardware" is [BCM2711](https://www.raspberrypi.org/documentation/hardware/raspberrypi/bcm2711/README.md) then:
1. Change `C compiler flags` to `-marm -fno-stack-protector -mcpu=cortex-a7` in the GHC settings:
```
nano ~/.stack/programs/arm-linux/ghc-8.10.4/lib/ghc-8.10.4/settings
```
1. To prevent gcc errors we delete the `setup-exe-src` folder
```
rm -rf ~/.stack/setup-exe-src/
```
6. Install requirements for step 7
1. Install NVM
```
cd ~/ && curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion
nvm --version
```
1. Install Node.js & NPM
```
nvm install node
```
1. Install Ionic CLI
```
npm install -g @ionic/cli
```
1. Install Dependencies
```
sudo apt-get install -y build-essential openssl libssl-dev libc6-dev clang libclang-dev libavahi-client-dev upx ca-certificates
```
1. Install Rust
```
cd ~/ && curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs -o- | bash
#Choose option 1
source $HOME/.cargo/env
#Check rust & cargo versions
rustc --version
cargo --version
```
7. Finally, getting to build the **.img**
1. At this stage you hava a working development environment to build your **embassy.img**.
Before you do that you can choose to enable SSH login for user `pi` in case something will go wrong or just skip to the next step.
```
cd ~/embassy-os
sed -e '/passwd -l pi/ s/^#*/#/' -i setup.sh
```
> :warning: Default password for user `pi` is `raspberry`, change it the next you login.
1. Build the `embassy.img`
```
cd ~/embassy-os
make
#Depending on your hardware this can take 1-2h+
#Wait for the "DONE!" message and take note of your product_key
exit
```
8. Flash the `embassy.img` to a microSD
1. Copy `embassy.img` from the raspi to your PC with scp
```
scp pi@raspi_IP:~/embassy-os/embassy.img .
```
1. Connect to raspi again to do `sudo shutdown now`, after a complete shutdown disconnect SSD/NVMe drive
1. Flash `embassy.img` to a microSD (do this before flashing to the SSD/NVMe, to be sure it works)
9. Prepare for initial setup
1. Boot raspi using flashed microSD
1. After a few minutes, the raspi should reboot itself and make it's first [sounds](#embassy-sounds-explained).
> :information_source: If needed, you can check the `agent` log with: `journalctl -u agent -ef`
1. Proceed with the [initial setup process of EmbassyOS](https://docs.start9labs.com/user-manual/initial-setup.html)
1. If all went well you can safely flash `embassy.img` to an SSD/NVMe and repeat step 9
### Embassy sounds explained
Sound :notes: | Indicating
------- | --------
Bep | Device is powering on
Chime | Device is ready for setup
Mario "Coin" | EmbassyOS has started
Mario "Death" | Device is about to Shutdown/Reboot
Mario "Power Up" | EmbassyOS update sequence
Beethoven | Update failed :(

View File

@@ -77,7 +77,7 @@ A good bug report shouldn't leave others needing to chase you up for more inform
- 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://docs.start9labs.com). 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-osissues?q=label%3Abug).
- 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)
@@ -225,9 +225,9 @@ When a pull request conflicts with the target branch, you may be asked to rebase
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: `#community-dev: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 channel: `#community: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>

View File

@@ -1,3 +1,15 @@
UNAME := $(shell uname -m)
EMBASSY_SRC := buster.img product_key appmgr/target/armv7-unknown-linux-gnueabihf/release/appmgr ui/www agent/dist/agent agent/config/agent.service lifeline/target/armv7-unknown-linux-gnueabihf/release/lifeline lifeline/lifeline.service setup.sh setup.service docker-daemon.json
APPMGR_RELEASE_SRC := appmgr/target/armv7-unknown-linux-gnueabihf/release/appmgr
LIFELINE_RELEASE_SRC := lifeline/target/armv7-unknown-linux-gnueabihf/release/lifeline
ifeq ($(UNAME), armv7l)
EMBASSY_SRC := buster.img product_key appmgr/target/release/appmgr ui/www agent/dist/agent agent/config/agent.service lifeline/target/release/lifeline lifeline/lifeline.service setup.sh setup.service docker-daemon.json
APPMGR_RELEASE_SRC := appmgr/target/release/appmgr
LIFELINE_RELEASE_SRC := lifeline/target/release/lifeline
endif
APPMGR_SRC := $(shell find appmgr/src) appmgr/Cargo.toml appmgr/Cargo.lock
LIFELINE_SRC := $(shell find lifeline/src) lifeline/Cargo.toml lifeline/Cargo.lock
AGENT_SRC := $(shell find agent/src) $(shell find agent/config) agent/stack.yaml agent/package.yaml agent/build.sh
@@ -13,7 +25,8 @@ UI_SRC := $(shell find ui/src) \
all: embassy.img
embassy.img: buster.img product_key appmgr/target/armv7-unknown-linux-gnueabihf/release/appmgr ui/www agent/dist/agent agent/config/agent.service lifeline/target/armv7-unknown-linux-gnueabihf/release/lifeline lifeline/lifeline.service setup.sh setup.service docker-daemon.json
embassy.img: $(EMBASSY_SRC)
chmod +x make_image.sh
sudo ./make_image.sh
buster.img:
@@ -26,11 +39,16 @@ product_key:
echo "X\c" > product_key
cat /dev/random | base32 | head -c11 | tr '[:upper:]' '[:lower:]' >> product_key
appmgr/target/armv7-unknown-linux-gnueabihf/release/appmgr: $(APPMGR_SRC)
$(APPMGR_RELEASE_SRC): $(APPMGR_SRC)
ifeq ($(UNAME), armv7l)
cd appmgr && cargo update && cargo build --release --features=production
arm-linux-gnueabihf-strip appmgr/target/release/appmgr
else
docker run --rm -it -v ~/.cargo/registry:/root/.cargo/registry -v "$(shell pwd)":/home/rust/src start9/rust-arm-cross:latest sh -c "(cd appmgr && cargo build --release --features=production)"
docker run --rm -it -v ~/.cargo/registry:/root/.cargo/registry -v "$(shell pwd)":/home/rust/src start9/rust-arm-cross:latest arm-linux-gnueabi-strip appmgr/target/armv7-unknown-linux-gnueabihf/release/appmgr
endif
appmgr: appmgr/target/armv7-unknown-linux-gnueabihf/release/appmgr
appmgr: $(APPMGR_RELEASE_SRC)
agent/dist/agent: $(AGENT_SRC)
(cd agent && ./build.sh)
@@ -45,9 +63,13 @@ ui/www: $(UI_SRC) ui/node_modules
ui: ui/www
lifeline/target/armv7-unknown-linux-gnueabihf/release/lifeline: $(LIFELINE_SRC)
$(LIFELINE_RELEASE_SRC): $(LIFELINE_SRC)
ifeq ($(UNAME), armv7l)
cd lifeline && cargo build --release
arm-linux-gnueabihf-strip lifeline/target/release/lifeline
else
docker run --rm -it -v ~/.cargo/registry:/root/.cargo/registry -v "$(shell pwd)":/home/rust/src start9/rust-arm-cross:latest sh -c "(cd lifeline && cargo build --release)"
docker run --rm -it -v ~/.cargo/registry:/root/.cargo/registry -v "$(shell pwd)":/home/rust/src start9/rust-arm-cross:latest arm-linux-gnueabi-strip lifeline/target/armv7-unknown-linux-gnueabihf/release/lifeline
endif
lifeline: lifeline/target/armv7-unknown-linux-gnueabihf/release/lifeline
lifeline: $(LIFELINE_RELEASE_SRC)

View File

@@ -1,28 +1,30 @@
# EmbassyOS
[![Version](https://img.shields.io/github/v/tag/Start9Labs/embassy-os?color=success)](https://github.com/Start9Labs/embassy-os/releases)
[![community](https://img.shields.io/badge/community-matrix-yellow)](https://matrix.to/#/#community:matrix.start9labs.com)
[![community](https://img.shields.io/badge/community-telegram-informational)](https://t.me/start9_labs)
[![support](https://img.shields.io/badge/support-docs-important)](https://docs.start9labs.com)
[![developer](https://img.shields.io/badge/developer-matrix-blueviolet)](https://matrix.to/#/#community-dev:matrix.start9labs.com)
[![website](https://img.shields.io/website?down_color=lightgrey&down_message=offline&up_color=success&up_message=online&url=https%3A%2F%2Fstart9labs.com)](https://start9labs.com)
[![website](https://img.shields.io/website?down_color=lightgrey&down_message=offline&up_color=green&up_message=online&url=https%3A%2F%2Fstart9labs.com)](https://start9labs.com)
[![mastodon](https://img.shields.io/mastodon/follow/000000001?domain=https%3A%2F%2Fmastodon.start9labs.com&label=Follow&style=social)](http://mastodon.start9labs.com)
[![twitter](https://img.shields.io/twitter/follow/start9labs?label=Follow)](https://twitter.com/start9labs)
### _Anyone can do it. No one can stop it._ ###
EmbassyOS is a mass-market, graphical operating system designed to facilitate the discovery, installation, configuration, private self-hosting, and reliable operation of open-source software services and applications. It aims to eliminate trust and custodianship from personal computing.
![EmbassyOS image](eos.png?w=600)
<img src="assets/eos.png" width="100%">
## ⚠️ Caution
Some technologies supported by this software, such as [Lightning](https://lightning.network/), are considered in active development and might experience issues. Do not commit any funds you are not willing to loose. Be #reckless at your own risk.
## :warning: Caution
Some technologies supported by this software, such as [Lightning](https://lightning.network/), are considered in active development and might experience issues. Do not commit any funds you are not willing to lose. Be #reckless at your own risk.
## Running EmbassyOS
There are multiple ways to obtain and begin using EmbassyOS.
### Buy an Embassy
### :moneybag: Buy an Embassy
This is the most convenient option. Simply [buy an Embassy](https://start9labs.com) from Start9 Labs and plug it in. Depending on where you live, shipping costs and import duties may vary.
### Build your own Embassy
### :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 want to save on shipping costs.
@@ -31,5 +33,15 @@ While not as convenient as buying an Embassy, this option is easier than you mig
To pursue this option, follow this [guide](https://docs.start9labs.com/getting-started/diy.html).
## Contributing
To build EmbassyOS from source, or to contribute to its development, see [here](https://github.com/Start9Labs/embassy-os/blob/master/CONTRIBUTING.md#building-the-image).
### :hammer_and_wrench: Build EmbassyOS from Source
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/BuildGuide.md).
## :heart: Contributing
To contribute to the development of EmbassyOS, see [here](https://github.com/Start9Labs/embassy-os/blob/master/CONTRIBUTING.md).
## UI Screenshots
<img src="assets/ServicesRunning.png" alt="Embassy Services" width="100%"> | <img src="assets/ServiceDetails.png" alt="Service Details" width="100%">
--- | ---
<img src="assets/Embassy.png" alt="EmbassyOS" width="100%"> | <img src="assets/Marketplace.png" alt="Marketplace" width="100%">

2
agent/.gitignore vendored
View File

@@ -19,9 +19,7 @@ cabal.sandbox.config
*.keter
*~
.vscode
*.cabal
\#*
start9-companion-server.cabal
stack.yaml.lock
*.env
agent_*

View File

@@ -0,0 +1,519 @@
cabal-version: 1.12
-- This file has been generated from package.yaml by hpack version 0.34.4.
--
-- see: https://github.com/sol/hpack
name: ambassador-agent
version: 0.2.14
build-type: Simple
extra-source-files:
./migrations/0.1.0::0.1.0
./migrations/0.1.0::0.1.1
./migrations/0.1.1::0.1.2
./migrations/0.1.2::0.1.3
./migrations/0.1.3::0.1.4
./migrations/0.1.4::0.1.5
./migrations/0.1.5::0.2.0
./migrations/0.2.0::0.2.1
./migrations/0.2.10::0.2.11
./migrations/0.2.11::0.2.12
./migrations/0.2.12::0.2.13
./migrations/0.2.13::0.2.14
./migrations/0.2.1::0.2.2
./migrations/0.2.2::0.2.3
./migrations/0.2.3::0.2.4
./migrations/0.2.4::0.2.5
./migrations/0.2.5::0.2.6
./migrations/0.2.6::0.2.7
./migrations/0.2.7::0.2.8
./migrations/0.2.8::0.2.9
./migrations/0.2.9::0.2.10
flag dev
description: Turn on development settings, like auto-reload templates.
manual: False
default: False
flag disable-auth
description: disable authorization checks
manual: False
default: False
flag library-only
description: Build for use with "yesod devel"
manual: False
default: False
library
exposed-modules:
Application
Auth
Constants
Daemon.AppNotifications
Daemon.RefreshProcDev
Daemon.SslRenew
Daemon.TorHealth
Daemon.ZeroConf
Foundation
Handler.Apps
Handler.Authenticate
Handler.Backups
Handler.Hosts
Handler.Icons
Handler.Login
Handler.Network
Handler.Notifications
Handler.PasswordUpdate
Handler.PowerOff
Handler.Register
Handler.Register.Nginx
Handler.Register.Tor
Handler.SelfUpdate
Handler.SshKeys
Handler.Status
Handler.Tor
Handler.Types.Apps
Handler.Types.HmacSig
Handler.Types.Hosts
Handler.Types.Metrics
Handler.Types.Parse
Handler.Types.Register
Handler.Types.V0.Base
Handler.Types.V0.Specs
Handler.Types.V0.Ssh
Handler.Types.V0.Wifi
Handler.Util
Handler.V0
Handler.Wifi
Lib.Algebra.Domain.AppMgr
Lib.Algebra.Domain.AppMgr.TH
Lib.Algebra.Domain.AppMgr.Types
Lib.Algebra.State.RegistryUrl
Lib.Avahi
Lib.Background
Lib.ClientManifest
Lib.Crypto
Lib.Database
Lib.Error
Lib.External.AppManifest
Lib.External.AppMgr
Lib.External.Metrics.Df
Lib.External.Metrics.Iotop
Lib.External.Metrics.ProcDev
Lib.External.Metrics.Temperature
Lib.External.Metrics.Top
Lib.External.Metrics.Types
Lib.External.Registry
Lib.External.Specs.Common
Lib.External.Specs.CPU
Lib.External.Specs.Memory
Lib.External.Util
Lib.External.WpaSupplicant
Lib.IconCache
Lib.Metrics
Lib.Migration
Lib.Notifications
Lib.Password
Lib.ProductKey
Lib.SelfUpdate
Lib.Sound
Lib.Ssh
Lib.Ssl
Lib.Synchronizers
Lib.SystemCtl
Lib.SystemPaths
Lib.Tor
Lib.TyFam.ConditionalData
Lib.Types.Core
Lib.Types.Emver
Lib.Types.Emver.Orphans
Lib.Types.NetAddress
Lib.Types.ServerApp
Lib.Types.Url
Lib.WebServer
Model
Orphans.Digest
Orphans.UUID
Settings
Startlude
Startlude.ByteStream
Startlude.ByteStream.Char8
Util.Conduit
Util.File
Util.Function
Util.Text
other-modules:
Paths_ambassador_agent
hs-source-dirs:
src
default-extensions:
NoImplicitPrelude
BlockArguments
ConstraintKinds
DataKinds
DeriveAnyClass
DeriveFunctor
DeriveGeneric
DerivingStrategies
EmptyCase
FlexibleContexts
FlexibleInstances
GADTs
GeneralizedNewtypeDeriving
InstanceSigs
KindSignatures
LambdaCase
MultiParamTypeClasses
MultiWayIf
NamedFieldPuns
NumericUnderscores
OverloadedStrings
PolyKinds
RankNTypes
StandaloneDeriving
StandaloneKindSignatures
TupleSections
TypeApplications
TypeFamilies
TypeOperators
build-depends:
aeson
, aeson-flatten
, attoparsec
, base >=4.9.1.0 && <5
, bytestring
, casing
, comonad
, conduit
, conduit-extra
, connection
, containers
, cryptonite
, cryptonite-conduit
, data-default
, directory
, errors
, exceptions
, exinst
, fast-logger
, file-embed
, filelock
, filepath
, fused-effects
, fused-effects-th
, git-embed
, http-api-data
, http-client
, http-client-tls
, http-conduit
, http-types
, interpolate
, iso8601-time
, json-rpc
, lens
, lens-aeson
, lifted-async
, lifted-base
, memory
, mime-types
, monad-control
, monad-logger
, network
, persistent
, persistent-sqlite
, persistent-template
, process
, process-extras
, protolude
, regex-compat
, resourcet
, shell-conduit
, singletons
, stm
, streaming
, streaming-bytestring
, streaming-conduit
, streaming-utils
, tar-conduit
, template-haskell
, text >=0.11 && <2.0
, time
, transformers
, transformers-base
, typed-process
, unix
, unliftio
, unliftio-core
, unordered-containers
, uuid
, wai
, wai-cors
, wai-extra
, warp
, yaml
, yesod
, yesod-auth
, yesod-core
, yesod-form
, yesod-persistent
if (flag(dev)) || (flag(library-only))
ghc-options: -Wall -Wunused-packages -fwarn-tabs -O0 -fdefer-typed-holes
cpp-options: -DDEVELOPMENT
else
ghc-options: -Wall -Wunused-packages -fwarn-tabs -O2 -fdefer-typed-holes
if (flag(disable-auth))
cpp-options: -DDISABLE_AUTH
default-language: Haskell2010
executable agent
main-is: main.hs
hs-source-dirs:
app
default-extensions:
NoImplicitPrelude
BlockArguments
ConstraintKinds
DataKinds
DeriveAnyClass
DeriveFunctor
DeriveGeneric
DerivingStrategies
EmptyCase
FlexibleContexts
FlexibleInstances
GADTs
GeneralizedNewtypeDeriving
InstanceSigs
KindSignatures
LambdaCase
MultiParamTypeClasses
MultiWayIf
NamedFieldPuns
NumericUnderscores
OverloadedStrings
PolyKinds
RankNTypes
StandaloneDeriving
StandaloneKindSignatures
TupleSections
TypeApplications
TypeFamilies
TypeOperators
ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fdefer-typed-holes
build-depends:
aeson
, aeson-flatten
, ambassador-agent
, attoparsec
, base >=4.9.1.0 && <5
, bytestring
, casing
, comonad
, conduit
, conduit-extra
, connection
, containers
, cryptonite
, cryptonite-conduit
, data-default
, directory
, errors
, exceptions
, exinst
, fast-logger
, file-embed
, filelock
, filepath
, fused-effects
, fused-effects-th
, git-embed
, http-api-data
, http-client
, http-client-tls
, http-conduit
, http-types
, interpolate
, iso8601-time
, json-rpc
, lens
, lens-aeson
, lifted-async
, lifted-base
, memory
, mime-types
, monad-control
, monad-logger
, network
, persistent
, persistent-sqlite
, persistent-template
, process
, process-extras
, protolude
, regex-compat
, resourcet
, shell-conduit
, singletons
, stm
, streaming
, streaming-bytestring
, streaming-conduit
, streaming-utils
, tar-conduit
, template-haskell
, text >=0.11 && <2.0
, time
, transformers
, transformers-base
, typed-process
, unix
, unliftio
, unliftio-core
, unordered-containers
, uuid
, wai
, wai-cors
, wai-extra
, warp
, yaml
, yesod
, yesod-auth
, yesod-core
, yesod-form
, yesod-persistent
if flag(library-only)
buildable: False
default-language: Haskell2010
test-suite agent-test
type: exitcode-stdio-1.0
main-is: Main.hs
other-modules:
ChecklistSpec
Lib.External.AppManifestSpec
Lib.SoundSpec
Lib.Types.EmverProp
Live.Metrics
Live.Serialize
Spec
hs-source-dirs:
test
default-extensions:
NoImplicitPrelude
BlockArguments
ConstraintKinds
DataKinds
DeriveAnyClass
DeriveFunctor
DeriveGeneric
DerivingStrategies
EmptyCase
FlexibleContexts
FlexibleInstances
GADTs
GeneralizedNewtypeDeriving
InstanceSigs
KindSignatures
LambdaCase
MultiParamTypeClasses
MultiWayIf
NamedFieldPuns
NumericUnderscores
OverloadedStrings
PolyKinds
RankNTypes
StandaloneDeriving
StandaloneKindSignatures
TupleSections
TypeApplications
TypeFamilies
TypeOperators
ghc-options: -Wall -fdefer-typed-holes
build-depends:
aeson
, aeson-flatten
, ambassador-agent
, attoparsec
, base >=4.9.1.0 && <5
, bytestring
, casing
, comonad
, conduit
, conduit-extra
, connection
, containers
, cryptonite
, cryptonite-conduit
, data-default
, directory
, errors
, exceptions
, exinst
, fast-logger
, file-embed
, filelock
, filepath
, fused-effects
, fused-effects-th
, git-embed
, hedgehog
, hspec >=2.0.0
, hspec-expectations
, http-api-data
, http-client
, http-client-tls
, http-conduit
, http-types
, interpolate
, iso8601-time
, json-rpc
, lens
, lens-aeson
, lifted-async
, lifted-base
, memory
, mime-types
, monad-control
, monad-logger
, network
, persistent
, persistent-sqlite
, persistent-template
, process
, process-extras
, protolude
, random
, regex-compat
, resourcet
, shell-conduit
, singletons
, stm
, streaming
, streaming-bytestring
, streaming-conduit
, streaming-utils
, tar-conduit
, template-haskell
, text >=0.11 && <2.0
, time
, transformers
, transformers-base
, typed-process
, unix
, unliftio
, unliftio-core
, unordered-containers
, uuid
, wai
, wai-cors
, wai-extra
, warp
, yaml
, yesod
, yesod-auth
, yesod-core
, yesod-form
, yesod-persistent
, yesod-test
default-language: Haskell2010

23
agent/cabal.project Normal file
View File

@@ -0,0 +1,23 @@
-- Generated by stackage-to-hackage
index-state: 2021-04-26T18:08:38Z
with-compiler: ghc-8.10.2
packages:
./
source-repository-package
type: git
location: https://github.com/ProofOfKeags/persistent.git
tag: 3b52b13d9ce79cdef14bb1c37cc527657a529462
subdir: persistent-sqlite
allow-older: *
allow-newer: *
package *
ghc-options: -haddock
package ambassador-agent
ghc-options: -fwrite-ide-info

2513
agent/cabal.project.freeze Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -33,5 +33,5 @@ database:
database: "start9_agent.sqlite3"
poolsize: "_env:YESOD_SQLITE_POOLSIZE:10"
app-mgr-version-spec: "=0.2.10"
app-mgr-version-spec: "=0.2.14"
#analytics: UA-YOURCODE

View File

@@ -0,0 +1 @@
SELECT TRUE;

View File

@@ -0,0 +1 @@
SELECT TRUE;

View File

@@ -0,0 +1 @@
SELECT TRUE;

View File

@@ -0,0 +1 @@
SELECT TRUE;

View File

@@ -1,5 +1,5 @@
name: ambassador-agent
version: 0.2.10
version: 0.2.14
default-extensions:
- NoImplicitPrelude
@@ -182,3 +182,4 @@ executables:
condition: flag(library-only)
- condition: false
other-modules: Paths_ambassador_agent
extra-source-files: ./migrations/*

View File

@@ -186,6 +186,8 @@ cutoffDuringUpdate m = do
path <- asks $ pathInfo . reqWaiRequest . handlerRequest
case path of
[v] | v == "v" <> (show . major $ agentVersion) -> m
[auth] | auth == "auth" -> m
(_:ssh:_) | ssh == "sshKeys" -> m
_ -> handleS9ErrT $ throwE UpdateInProgressE
Nothing -> m

View File

@@ -109,7 +109,11 @@ type AllEffects m
( Labelled
"databaseConnection"
(ReaderT ConnectionPool)
(ReaderT AgentCtx (ErrorC S9Error (LiftC m)))
( Labelled
"lanThread"
(ReaderT (MVar ThreadId))
(ReaderT AgentCtx (ErrorC S9Error (LiftC m)))
)
)
)
)
@@ -122,6 +126,8 @@ intoHandler m = do
runM
. handleS9ErrC
. flip runReaderT ctx
. flip runReaderT (appLanThread ctx)
. runLabelled @"lanThread"
. flip runReaderT (appConnPool ctx)
. runLabelled @"databaseConnection"
. flip runReaderT fsbase
@@ -148,8 +154,7 @@ getAvailableAppsLogic :: ( Has (Reader AgentCtx) sig m
getAvailableAppsLogic = do
jobCache <- asks appBackgroundJobs >>= liftIO . readTVarIO
let installCache = inspect SInstalling jobCache
(Reg.AppManifestRes apps, serverApps) <- LAsync.concurrently Reg.getAppManifest
(AppMgr2.list [AppMgr2.flags|-s -d|])
(Reg.AppIndexRes apps, serverApps) <- LAsync.concurrently Reg.getAppIndex (AppMgr2.list [AppMgr2.flags|-s -d|])
let remapped = remapAppMgrInfo jobCache serverApps
pure $ foreach apps $ \app@StoreApp { storeAppId } ->
let installing =
@@ -177,8 +182,9 @@ getAvailableAppByIdLogic appId = do
let storeAppId' = storeAppId
jobCache <- asks appBackgroundJobs >>= liftIO . readTVarIO
let installCache = inspect SInstalling jobCache
(Reg.AppManifestRes storeApps, serverApps) <- LAsync.concurrently Reg.getAppManifest
(AppMgr2.list [AppMgr2.flags|-s -d|])
((Reg.AppIndexRes storeApps, serverApps), AppManifest.AppManifest { appManifestLicenseName, appManifestLicenseLink }) <-
LAsync.concurrently (LAsync.concurrently Reg.getAppIndex (AppMgr2.list [AppMgr2.flags|-s -d|]))
(Reg.getAppManifest appId)
StoreApp {..} <- pure (find ((== appId) . storeAppId) storeApps) `orThrowM` NotFoundE "appId" (show appId)
let remapped = remapAppMgrInfo jobCache serverApps
let installingInfo =
@@ -207,6 +213,8 @@ getAvailableAppByIdLogic appId = do
appId
storeAppTitle
(storeIconUrl appId (storeAppVersionInfoVersion $ extract storeAppVersions))
, appAvailableFullLicenseName = appManifestLicenseName
, appAvailableFullLicenseLink = appManifestLicenseLink
, appAvailableFullInstallInfo = installingInfo
, appAvailableFullVersionLatest = storeAppVersionInfoVersion latest
, appAvailableFullDescriptionShort = storeAppDescriptionShort
@@ -297,6 +305,8 @@ getInstalledAppByIdLogic appId = do
backupTime <- lift $ LAsync.wait backupTime'
hoistMaybe $ HM.lookup appId installCache <&> \(StoreApp {..}, StoreAppVersionInfo {..}) -> AppInstalledFull
{ appInstalledFullBase = AppBase appId storeAppTitle (iconUrl appId storeAppVersionInfoVersion)
, appInstalledFullLicenseName = Nothing
, appInstalledFullLicenseLink = Nothing
, appInstalledFullStatus = AppStatusTmp Installing
, appInstalledFullVersionInstalled = storeAppVersionInfoVersion
, appInstalledFullInstructions = Nothing
@@ -313,7 +323,7 @@ getInstalledAppByIdLogic appId = do
}
serverApps <- AppMgr2.list [AppMgr2.flags|-s -d|]
let remapped = remapAppMgrInfo jobCache serverApps
appManifestFetchCached <- cached Reg.getAppManifest
appManifestFetchCached <- cached Reg.getAppIndex
let
installed = do
(status, version, AppMgr2.InfoRes {..}) <- hoistMaybe (HM.lookup appId remapped)
@@ -327,7 +337,7 @@ getInstalledAppByIdLogic appId = do
fromInstalled = (AppMgr2.infoResTitle &&& AppMgr2.infoResVersion)
<$> hoistMaybe (HM.lookup depId serverApps)
let fromStore = do
Reg.AppManifestRes res <- lift appManifestFetchCached
Reg.AppIndexRes res <- lift appManifestFetchCached
(storeAppTitle &&& storeAppVersionInfoVersion . extract . storeAppVersions)
<$> hoistMaybe (find ((== depId) . storeAppId) res)
(title, v) <- fromInstalled <|> fromStore
@@ -348,6 +358,8 @@ getInstalledAppByIdLogic appId = do
guard (not . null $ lanConfs)
pure $ LanAddress . (".onion" `Text.replace` ".local") . unTorAddress $ addrBase
pure AppInstalledFull { appInstalledFullBase = AppBase appId infoResTitle (iconUrl appId version)
, appInstalledFullLicenseName = AppManifest.appManifestLicenseName manifest
, appInstalledFullLicenseLink = AppManifest.appManifestLicenseLink manifest
, appInstalledFullStatus = status
, appInstalledFullVersionInstalled = version
, appInstalledFullInstructions = instructions
@@ -376,6 +388,7 @@ postUninstallAppLogic :: ( HasFilesystemBase sig m
, MonadIO m
, HasLabelled "databaseConnection" (Reader ConnectionPool) sig m
, HasLabelled "iconTagCache" (Reader (TVar (HM.HashMap AppId (Digest MD5)))) sig m
, HasLabelled "lanThread" (Reader (MVar ThreadId)) sig m
)
=> AppId
-> AppMgr2.DryRun
@@ -413,6 +426,7 @@ postInstallNewAppR appId = do
postInstallNewAppLogic :: forall sig m a
. ( Has (Reader AgentCtx) sig m
, HasLabelled "lanThread" (Reader (MVar ThreadId)) sig m
, HasLabelled "databaseConnection" (Reader ConnectionPool) sig m
, HasLabelled "iconTagCache" (Reader (TVar (HM.HashMap AppId (Digest MD5)))) sig m
, Has (Error S9Error) sig m
@@ -666,8 +680,8 @@ getAvailableAppVersionInfoLogic :: ( Has (Reader AgentCtx) sig m
-> VersionRange
-> m AppVersionInfo
getAvailableAppVersionInfoLogic appId appVersionSpec = do
jobCache <- asks appBackgroundJobs >>= liftIO . readTVarIO
Reg.AppManifestRes storeApps <- Reg.getAppManifest
jobCache <- asks appBackgroundJobs >>= liftIO . readTVarIO
Reg.AppIndexRes storeApps <- Reg.getAppIndex
let titles =
(storeAppTitle &&& storeAppVersionInfoVersion . extract . storeAppVersions) <$> indexBy storeAppId storeApps
StoreApp {..} <- find ((== appId) . storeAppId) storeApps `orThrowPure` NotFoundE "appId" (show appId)

View File

@@ -7,11 +7,11 @@ import Startlude hiding ( Reader
, runReader
)
import Control.Effect.Labelled hiding ( Handler )
import Control.Effect.Reader.Labelled
import Control.Carrier.Error.Church
import Control.Carrier.Lift
import Control.Carrier.Reader ( runReader )
import Control.Effect.Labelled hiding ( Handler )
import Control.Effect.Reader.Labelled
import Data.Aeson
import qualified Data.HashMap.Strict as HM
import Data.UUID.V4
@@ -20,8 +20,13 @@ import Yesod.Auth
import Yesod.Core
import Yesod.Core.Types
import Control.Concurrent.STM
import Exinst
import Foundation
import Handler.Network
import Handler.Util
import qualified Lib.Algebra.Domain.AppMgr as AppMgr2
import Lib.Background
import Lib.Error
import qualified Lib.External.AppMgr as AppMgr
import qualified Lib.Notifications as Notifications
@@ -29,10 +34,6 @@ import Lib.Password
import Lib.Types.Core
import Lib.Types.Emver
import Model
import qualified Lib.Algebra.Domain.AppMgr as AppMgr2
import Lib.Background
import Control.Concurrent.STM
import Exinst
data CreateBackupReq = CreateBackupReq
@@ -58,8 +59,9 @@ instance FromJSON RestoreBackupReq where
pure RestoreBackupReq { .. }
data EjectDiskReq = EjectDiskReq
{ ejectDiskLogicalName :: Text
} deriving (Eq, Show)
{ ejectDiskLogicalName :: Text
}
deriving (Eq, Show)
instance FromJSON EjectDiskReq where
parseJSON = withObject "Eject Disk Req" $ \o -> do
ejectDiskLogicalName <- o .: "logicalName"
@@ -100,6 +102,8 @@ postRestoreBackupR appId = disableEndpointOnFailedUpdate $ do
& runReader appConnPool
& runLabelled @"backgroundJobCache"
& runReader appBackgroundJobs
& runLabelled @"lanThread"
& runReader appLanThread
& handleS9ErrC
& runM
@@ -173,6 +177,7 @@ stopBackupLogic appId = do
restoreBackupLogic :: ( HasLabelled "backgroundJobCache" (Reader (TVar JobCache)) sig m
, HasLabelled "databaseConnection" (Reader ConnectionPool) sig m
, HasLabelled "lanThread" (Reader (MVar ThreadId)) sig m
, Has (Error S9Error) sig m
, Has AppMgr2.AppMgr sig m
, MonadIO m
@@ -181,10 +186,11 @@ restoreBackupLogic :: ( HasLabelled "backgroundJobCache" (Reader (TVar JobCache)
-> RestoreBackupReq
-> m ()
restoreBackupLogic appId RestoreBackupReq {..} = do
jobCache <- ask @"backgroundJobCache"
db <- ask @"databaseConnection"
version <- fmap AppMgr2.infoResVersion $ AppMgr2.info [AppMgr2.flags| |] appId `orThrowM` NotFoundE "appId"
(show appId)
lanThread <- ask @"lanThread"
jobCache <- ask @"backgroundJobCache"
db <- ask @"databaseConnection"
version <- fmap AppMgr2.infoResVersion $ AppMgr2.info [AppMgr2.flags| |] appId `orThrowM` NotFoundE "appId"
(show appId)
res <- liftIO . atomically $ do
(JobCache jobs) <- readTVar jobCache
case HM.lookup appId jobs of
@@ -206,10 +212,13 @@ restoreBackupLogic appId RestoreBackupReq {..} = do
let notif = case appmgrRes of
Left e -> Notifications.RestoreFailed e
Right _ -> Notifications.RestoreSucceeded
resetRes <- runExceptT @S9Error $ runReader lanThread . runLabelled @"lanThread" $ postResetLanLogic
case resetRes of
Left _ -> pure () -- temporarily forbidden is the only possible thing here so ignore it
Right () -> pure ()
flip runSqlPool db $ void $ Notifications.emit appId version notif
liftIO . atomically $ modifyTVar jobCache (insertJob appId Restore tid)
listDisksLogic :: (Has (Error S9Error) sig m, MonadIO m) => m [AppMgr.DiskInfo]
listDisksLogic = runExceptT AppMgr.diskShow >>= liftEither

View File

@@ -14,6 +14,13 @@ import Network.HTTP.Simple
import System.FilePath.Posix
import Yesod.Core
import Control.Carrier.Reader hiding ( asks )
import Control.Concurrent.STM ( modifyTVar
, readTVarIO
)
import Control.Effect.Labelled ( runLabelled )
import Crypto.Hash.Conduit ( hashFile )
import qualified Data.HashMap.Strict as HM
import Foundation
import Lib.Algebra.State.RegistryUrl
import Lib.Error
@@ -21,16 +28,9 @@ import qualified Lib.External.Registry as Reg
import Lib.IconCache
import Lib.SystemPaths hiding ( (</>) )
import Lib.Types.Core
import Lib.Types.Emver
import Lib.Types.ServerApp
import Settings
import Control.Carrier.Reader hiding ( asks )
import Control.Effect.Labelled ( runLabelled )
import qualified Data.HashMap.Strict as HM
import Control.Concurrent.STM ( modifyTVar
, readTVarIO
)
import Crypto.Hash.Conduit ( hashFile )
import Lib.Types.Emver
iconUrl :: AppId -> Version -> Text
iconUrl appId version = (foldMap (T.cons '/') . fst . renderRoute . AppIconR $ appId) <> "?" <> show version
@@ -63,7 +63,7 @@ getAppIconR appId = handleS9ErrT $ do
lift $ respondSource (parseContentType path) $ CB.sourceFile path .| awaitForever sendChunkBS
where
fetchIcon = do
url <- find ((== appId) . storeAppId) . Reg.storeApps <$> Reg.getAppManifest >>= \case
url <- find ((== appId) . storeAppId) . Reg.storeApps <$> Reg.getAppIndex >>= \case
Nothing -> throwError $ NotFoundE "icon" (show appId)
Just x -> pure . toS $ storeAppIconUrl x
bp <- getAbsoluteLocationFor iconBasePath
@@ -84,7 +84,7 @@ getAvailableAppIconR :: AppId -> Handler TypedContent
getAvailableAppIconR appId = handleS9ErrT $ do
s <- getsYesod appSettings
url <- do
find ((== appId) . storeAppId) . Reg.storeApps <$> interp s Reg.getAppManifest >>= \case
find ((== appId) . storeAppId) . Reg.storeApps <$> interp s Reg.getAppIndex >>= \case
Nothing -> throwE $ NotFoundE "icon" (show appId)
Just x -> pure . toS $ storeAppIconUrl x
req <- case parseRequest url of

View File

@@ -1,16 +1,19 @@
module Handler.Network where
import Startlude hiding ( Reader
, ask
, asks
, runReader
)
import Control.Carrier.Lift ( runM )
import Control.Effect.Error
import Control.Carrier.Reader
import Lib.Error
import Yesod.Core ( getYesod )
import Control.Carrier.Reader ( runReader )
import Control.Effect.Labelled ( runLabelled )
import Control.Effect.Reader.Labelled
import Foundation
import qualified Lib.Algebra.Domain.AppMgr as AppMgr2
import Lib.Types.Core
@@ -18,11 +21,12 @@ import Lib.Types.Core
postResetLanR :: Handler ()
postResetLanR = do
ctx <- getYesod
runM . handleS9ErrC . runReader ctx $ postResetLanLogic
runM . handleS9ErrC . runReader (appLanThread ctx) . runLabelled @"lanThread" $ postResetLanLogic
postResetLanLogic :: (MonadIO m, Has (Reader AgentCtx) sig m, Has (Error S9Error) sig m) => m ()
postResetLanLogic :: (MonadIO m, HasLabelled "lanThread" (Reader (MVar ThreadId)) sig m, Has (Error S9Error) sig m)
=> m ()
postResetLanLogic = do
threadVar <- asks appLanThread
threadVar <- ask @"lanThread"
mtid <- liftIO . tryTakeMVar $ threadVar
case mtid of
Nothing -> throwError $ TemporarilyForbiddenE (AppId "LAN") "reset" "being reset"

View File

@@ -74,6 +74,8 @@ instance FromJSON InstallNewAppReq where
data AppAvailableFull = AppAvailableFull
{ appAvailableFullBase :: AppBase
, appAvailableFullLicenseName :: Maybe Text
, appAvailableFullLicenseLink :: Maybe Text
, appAvailableFullInstallInfo :: Maybe (Version, AppStatus)
, appAvailableFullVersionLatest :: Version
, appAvailableFullDescriptionShort :: Text
@@ -88,7 +90,9 @@ instance ToJSON AppAvailableFull where
toJSON AppAvailableFull {..} = mergeTo
(toJSON appAvailableFullBase)
(object
[ "versionInstalled" .= fmap fst appAvailableFullInstallInfo
[ "licenseName" .= appAvailableFullLicenseName
, "licenseLink" .= appAvailableFullLicenseLink
, "versionInstalled" .= fmap fst appAvailableFullInstallInfo
, "status" .= fmap snd appAvailableFullInstallInfo
, "versionLatest" .= appAvailableFullVersionLatest
, "descriptionShort" .= appAvailableFullDescriptionShort
@@ -131,6 +135,8 @@ instance ToJSON (AppDependencyRequirement Keep) where
-- mute violations downstream of version for installing apps
data AppInstalledFull = AppInstalledFull
{ appInstalledFullBase :: AppBase
, appInstalledFullLicenseName :: Maybe Text
, appInstalledFullLicenseLink :: Maybe Text
, appInstalledFullStatus :: AppStatus
, appInstalledFullVersionInstalled :: Version
, appInstalledFullTorAddress :: Maybe TorAddress
@@ -156,6 +162,8 @@ instance ToJSON AppInstalledFull where
, "lanUi" .= appInstalledFullLanUi
, "id" .= appBaseId appInstalledFullBase
, "title" .= appBaseTitle appInstalledFullBase
, "licenseName" .= appInstalledFullLicenseName
, "licenseLink" .= appInstalledFullLicenseLink
, "iconURL" .= appBaseIconUrl appInstalledFullBase
, "versionInstalled" .= appInstalledFullVersionInstalled
, "status" .= appInstalledFullStatus

View File

@@ -11,8 +11,7 @@ module Lib.Algebra.Domain.AppMgr
( module Lib.Algebra.Domain.AppMgr
, module Lib.Algebra.Domain.AppMgr.Types
, module Lib.Algebra.Domain.AppMgr.TH
)
where
) where
import Startlude
@@ -26,31 +25,31 @@ import Data.Singletons.Prelude hiding ( Error )
import Data.Singletons.Prelude.Either
import qualified Data.String as String
import Lib.Algebra.Domain.AppMgr.Types
import Control.Monad.Base ( MonadBase(..) )
import Control.Monad.Fail ( MonadFail(fail) )
import Control.Monad.Trans.Class ( MonadTrans )
import Control.Monad.Trans.Control ( MonadBaseControl(..)
, MonadTransControl(..)
, defaultLiftBaseWith
, defaultRestoreM
)
import Control.Monad.Trans.Resource ( MonadResource(..) )
import qualified Data.ByteString.Char8 as C8
import qualified Data.ByteString.Lazy as LBS
import Data.String.Interpolate.IsString
( i )
import Lib.Algebra.Domain.AppMgr.TH
import Lib.Algebra.Domain.AppMgr.Types
import Lib.Error
import qualified Lib.External.AppManifest as Manifest
import Lib.TyFam.ConditionalData
import Lib.Types.Core ( AppId(..)
, AppContainerStatus(..)
import Lib.Types.Core ( AppContainerStatus(..)
, AppId(..)
)
import Lib.Types.NetAddress
import Lib.Types.Emver
import Control.Monad.Trans.Class ( MonadTrans )
import qualified Data.ByteString.Lazy as LBS
import System.Process.Typed
import Data.String.Interpolate.IsString
( i )
import Control.Monad.Base ( MonadBase(..) )
import Control.Monad.Fail ( MonadFail(fail) )
import Control.Monad.Trans.Resource ( MonadResource(..) )
import Control.Monad.Trans.Control ( defaultLiftBaseWith
, defaultRestoreM
, MonadTransControl(..)
, MonadBaseControl(..)
)
import qualified Data.ByteString.Char8 as C8
import Lib.Types.NetAddress
import System.Process
import System.Process.Typed
type InfoRes :: Either OnlyInfoFlag [IncludeInfoFlag] -> Type
@@ -371,13 +370,16 @@ instance (Has (Error S9Error) sig m, Algebra sig m, MonadIO m) => Algebra (AppMg
(L (List (SRight flags))) -> do
let renderedFlags = (genInclusiveFlag <$> fromSing flags) <> ["--json"]
let args = "list" : renderedFlags
(ec, out) <- readProcessInheritStderr "appmgr" args ""
res <- case ec of
ExitSuccess -> case withSingI flags $ eitherDecodeStrict out of
Left e -> throwError $ AppMgrParseE (toS $ String.unwords args) (decodeUtf8 out) e
Right x -> pure x
ExitFailure n -> throwError $ AppMgrE "list" n
pure $ ctx $> res
let runIt retryCount = do
(ec, out) <- readProcessInheritStderr "appmgr" args ""
case ec of
ExitSuccess -> case withSingI flags $ eitherDecodeStrict out of
Left e -> throwError $ AppMgrParseE (toS $ String.unwords args) (decodeUtf8 out) e
Right x -> pure $ ctx $> x
ExitFailure 6 ->
if retryCount > 0 then runIt (retryCount - 1) else throwError $ AppMgrE "list" 6
ExitFailure n -> throwError $ AppMgrE "list" n
runIt (1 :: Word) -- with 1 retry
(L (Remove dryorpurge appId)) -> do
let args = "remove" : case dryorpurge of
Left (DryRun True) -> ["--dry-run", show appId, "--json"]

View File

@@ -78,6 +78,8 @@ data AppManifest where
AppManifest ::{ appManifestId :: AppId
, appManifestVersion :: Version
, appManifestTitle :: Text
, appManifestLicenseName :: Maybe Text
, appManifestLicenseLink :: Maybe Text
, appManifestDescShort :: Text
, appManifestDescLong :: Text
, appManifestReleaseNotes :: Text
@@ -109,6 +111,8 @@ instance FromJSON AppManifest where
appManifestId <- o .: "id"
appManifestVersion <- o .: "version"
appManifestTitle <- o .: "title"
appManifestLicenseName <- o .:? "license-info" >>= traverse (.: "license")
appManifestLicenseLink <- o .:? "license-info" >>= traverse (.: "url")
appManifestDescShort <- o .: "description" >>= (.: "short")
appManifestDescLong <- o .: "description" >>= (.: "long")
appManifestReleaseNotes <- o .: "release-notes"

View File

@@ -13,8 +13,8 @@ import Startlude.ByteStream hiding ( count )
import Conduit
import Control.Algebra
import Control.Effect.Lift
import Control.Effect.Error
import Control.Effect.Lift
import Control.Effect.Reader.Labelled
import Control.Monad.Fail ( fail )
import Control.Monad.Trans.Resource
@@ -30,15 +30,17 @@ import System.Directory
import System.Process
import Constants
import qualified Data.Aeson.Types ( parseEither )
import Data.Time.ISO8601 ( parseISO8601 )
import Lib.Algebra.State.RegistryUrl
import Lib.Error
import Lib.External.AppManifest
import Lib.SystemPaths
import Lib.Types.Core
import Lib.Types.Emver
import Lib.Types.ServerApp
import Data.Time.ISO8601 ( parseISO8601 )
newtype AppManifestRes = AppManifestRes
newtype AppIndexRes = AppIndexRes
{ storeApps :: [StoreApp] } deriving (Eq, Show)
newtype RegistryVersionForSpecRes = RegistryVersionForSpecRes
@@ -85,8 +87,8 @@ getLifelineBinary avs = do
liftIO $ runConduitRes $ httpSource request getResponseBody .| sinkFile (toS lifelineTarget)
liftIO $ void $ readProcessWithExitCode "chmod" ["700", toS lifelineTarget] ""
getAppManifest :: (MonadIO m, Has (Error S9Error) sig m, Has RegistryUrl sig m) => m AppManifestRes
getAppManifest = do
getAppIndex :: (MonadIO m, Has (Error S9Error) sig m, Has RegistryUrl sig m) => m AppIndexRes
getAppIndex = do
manifestPath <- registryManifestUrl
req <- liftIO $ fmap setUserAgent . parseRequestThrow $ toS manifestPath
val <- (liftIO . try @SomeException) (httpBS req) >>= \case
@@ -96,22 +98,29 @@ getAppManifest = do
Left e -> throwError $ RegistryParseE manifestPath . toS $ e
Right a -> pure a
getAppManifest :: (MonadIO m, Has (Error S9Error) sig m, Has RegistryUrl sig m) => AppId -> m AppManifest
getAppManifest appId = do
let path = "/apps/manifest/" <> unAppId appId
v <- registryRequest path
case Data.Aeson.Types.parseEither parseJSON v of
Left e -> throwError $ RegistryParseE path . toS $ e
Right a -> pure a
getStoreAppInfo :: (MonadIO m, Has RegistryUrl sig m, Has (Error S9Error) sig m) => AppId -> m (Maybe StoreApp)
getStoreAppInfo name = find ((== name) . storeAppId) . storeApps <$> getAppManifest
getStoreAppInfo name = find ((== name) . storeAppId) . storeApps <$> getAppIndex
parseBsManifest :: Has RegistryUrl sig m => ByteString -> m (Either String AppManifestRes)
parseBsManifest :: Has RegistryUrl sig m => ByteString -> m (Either String AppIndexRes)
parseBsManifest bs = do
parseRegistryRes' <- parseRegistryRes
pure $ parseEither parseRegistryRes' . fromJust . decodeThrow $ bs
parseRegistryRes :: Has RegistryUrl sig m => m (Value -> Parser AppManifestRes)
parseRegistryRes :: Has RegistryUrl sig m => m (Value -> Parser AppIndexRes)
parseRegistryRes = do
parseAppData' <- parseAppData
pure $ withObject "app registry response" $ \obj -> do
let keyVals = HM.toList obj
let mManifestApps = fmap (\(k, v) -> parseMaybe (parseAppData' (AppId k)) v) keyVals
pure . AppManifestRes . catMaybes $ mManifestApps
pure . AppIndexRes . catMaybes $ mManifestApps
registryUrl :: (Has RegistryUrl sig m) => m Text
registryUrl = maybe "https://registry.start9labs.com:443" show <$> getRegistryUrl

View File

@@ -24,6 +24,7 @@ import qualified Data.Conduit.Combinators as Conduit
import Data.Conduit.Shell hiding ( arch
, hostname
, patch
, split
, stream
)
import qualified Data.Conduit.Tar as Conduit
@@ -48,7 +49,11 @@ import System.Process ( callCommand )
import Constants
import Control.Effect.Error hiding ( run )
import Control.Effect.Labelled ( runLabelled )
import Daemon.ZeroConf ( getStart9AgentHostname )
import Data.ByteString.Char8 ( split )
import qualified Data.ByteString.Char8 as C8
import Data.Conduit.List ( consume )
import qualified Data.Text as T
import Foundation
import Handler.Network
@@ -97,12 +102,12 @@ parseKernelVersion = do
pure $ KernelVersion (Version (major', minor', patch', 0)) arch
synchronizer :: Synchronizer
synchronizer = sync_0_2_10
synchronizer = sync_0_2_14
{-# INLINE synchronizer #-}
sync_0_2_10 :: Synchronizer
sync_0_2_10 = Synchronizer
"0.2.10"
sync_0_2_14 :: Synchronizer
sync_0_2_14 = Synchronizer
"0.2.14"
[ syncCreateAgentTmp
, syncCreateSshDir
, syncRemoveAvahiSystemdDependency
@@ -126,6 +131,7 @@ sync_0_2_10 = Synchronizer
, syncRestarterService
, syncInstallEject
, syncDropCertificateUniqueness
, syncRemoveDefaultNginxCfg
]
syncCreateAgentTmp :: SyncOp
@@ -437,10 +443,11 @@ syncInstallAppMgr = SyncOp "Install AppMgr" check migrate False
Left _ -> pure True
Right v -> not . (v <||) <$> asks (appMgrVersionSpec . appSettings)
migrate = fmap (either absurd id) . runExceptT . flip catchE failUpdate $ do
lan <- asks appLanThread
avs <- asks $ appMgrVersionSpec . appSettings
av <- AppMgr.installNewAppMgr avs
unless (av <|| avs) $ throwE $ AppMgrVersionE av avs
postResetLanLogic -- to accommodate 0.2.x -> 0.2.9 where previous appmgr didn't correctly set up lan
flip runReaderT lan $ runLabelled @"lanThread" $ postResetLanLogic -- to accommodate 0.2.x -> 0.2.9 where previous appmgr didn't correctly set up lan
syncUpgradeLifeline :: SyncOp
syncUpgradeLifeline = SyncOp "Upgrade Lifeline" check migrate False
@@ -582,19 +589,34 @@ syncRestarterService = SyncOp "Install Restarter Service" check migrate True
liftIO $ callCommand "systemctl enable restarter.timer"
syncUpgradeTor :: SyncOp
syncUpgradeTor = SyncOp "Install Tor 0.3.5.12-1" check migrate False
syncUpgradeTor = SyncOp "Install Latest Tor" check migrate False
where
check =
liftIO
$ ( run (shell [i|dpkg -l|] $| shell [i|grep tor|] $| shell [i|grep 0.3.5.12-1|] $| conduit await)
$> False
)
`catch` \(e :: ProcessException) -> case e of
ProcessException _ (ExitFailure 1) -> pure True
_ -> throwIO e
check = run $ do
shell "apt-get clean"
shell "apt-get update"
mTorVersion <- (shell "dpkg -s tor" $| shell "grep '^Version'" $| shell "cut -d ' ' -f2" $| conduit await)
let torVersion = case mTorVersion of
Nothing -> panic "invalid output from dpkg, can't read tor version"
Just x -> x
pure $ compareTorVersions torVersion "0.3.5.15-1" == LT
migrate = liftIO . run $ do
shell "apt-get update"
shell "apt-get install -y tor=0.3.5.12-1"
availVersions <-
(shell "apt-cache madison tor" $| shell "cut -d '|' -f2" $| shell "xargs" $| conduit consume)
latest <- case lastMay $ sortBy compareTorVersions availVersions of
Nothing -> throwIO $ ErrorCall "No available versions of tor"
Just x -> pure x
shell $ "apt-get install -y tor=" <> if "0.3.5.15-1" `elem` availVersions
then "0.3.5.15-1"
else (C8.unpack latest)
compareTorVersions :: ByteString -> ByteString -> Ordering
compareTorVersions a b =
let a' = (traverse (readMaybe @Int . decodeUtf8) . (split '.' <=< split '-') $ a)
b' = (traverse (readMaybe @Int . decodeUtf8) . (split '.' <=< split '-') $ b)
in case liftA2 compare a' b' of
Nothing -> panic "invalid tor version string"
Just x -> x
syncDropCertificateUniqueness :: SyncOp
syncDropCertificateUniqueness = SyncOp "Eliminate OpenSSL unique_subject=yes" check migrate False
@@ -602,14 +624,33 @@ syncDropCertificateUniqueness = SyncOp "Eliminate OpenSSL unique_subject=yes" ch
uni = "unique_subject = no\n"
check = do
base <- asks $ appFilesystemBase . appSettings
contentsRoot <- liftIO . BS.readFile . toS $ (rootCaDirectory <> "index.txt.attr") `relativeTo` base
contentsInt <- liftIO . BS.readFile . toS $ (intermediateCaDirectory <> "index.txt.attr") `relativeTo` base
pure $ uni /= contentsRoot || uni /= contentsInt
contentsRoot <-
liftIO
$ (fmap Just . BS.readFile . toS $ (rootCaDirectory <> "index.txt.attr") `relativeTo` base)
`catch` \(e :: IOException) -> if isDoesNotExistError e then pure Nothing else throwIO e
contentsInt <-
liftIO
$ (fmap Just . BS.readFile . toS $ (intermediateCaDirectory <> "index.txt.attr") `relativeTo` base)
`catch` \(e :: IOException) -> if isDoesNotExistError e then pure Nothing else throwIO e
case (contentsRoot, contentsInt) of
(Just root, Just int) -> pure $ uni /= root || uni /= int
_ -> pure True
migrate = do
base <- asks $ appFilesystemBase . appSettings
liftIO $ BS.writeFile (toS $ (rootCaDirectory <> "index.txt.attr") `relativeTo` base) uni
liftIO $ BS.writeFile (toS $ (intermediateCaDirectory <> "index.txt.attr") `relativeTo` base) uni
syncRemoveDefaultNginxCfg :: SyncOp
syncRemoveDefaultNginxCfg = SyncOp "Remove Default Nginx Configuration" check migrate False
where
check = do
base <- asks $ appFilesystemBase . appSettings
liftIO $ doesPathExist (toS $ nginxSitesEnabled "default" `relativeTo` base)
migrate = do
base <- asks $ appFilesystemBase . appSettings
liftIO $ removeFileIfExists (toS $ nginxSitesEnabled "default" `relativeTo` base)
liftIO $ systemCtl RestartService "nginx" $> ()
failUpdate :: S9Error -> ExceptT Void (ReaderT AgentCtx IO) ()
failUpdate e = do
ref <- asks appIsUpdateFailed

View File

@@ -1,13 +1,7 @@
-- {-# OPTIONS_GHC -fno-warn-unused-imports #-}
module Startlude.ByteStream
( module Startlude.ByteStream
, module BS
)
where
( module BS
) where
import Data.ByteString.Streaming as BS
import Streaming.ByteString as BS
hiding ( ByteString )
import Data.ByteString.Streaming as X
( ByteString )
type ByteStream m = X.ByteString m

View File

@@ -1,7 +1,5 @@
module Startlude.ByteStream.Char8
( module X
)
where
) where
import Data.ByteString.Streaming.Char8
as X
import Streaming.ByteString.Char8 as X

View File

@@ -1,10 +1,10 @@
resolver: nightly-2020-09-29
resolver: lts-17.10
packages:
- .
- .
extra-deps:
- aeson-1.4.7.1
# - aeson-1.4.7.1
- aeson-flatten-0.1.0.2
- exinst-0.8
- fused-effects-1.1.0.0
@@ -12,13 +12,14 @@ extra-deps:
- git-embed-0.1.0
- json-stream-0.4.2.4
- protolude-0.3.0
- streaming-bytestring-0.1.7
- streaming-conduit-0.1.2.2
- streaming-utils-0.2.0.0
# to avoid the ridiculous bug where stat64 is not found (only affects development)
- git: https://github.com/ProofOfKeags/persistent.git
commit: 3b52b13d9ce79cdef14bb1c37cc527657a529462
subdirs:
- persistent-sqlite
# - git: https://github.com/ProofOfKeags/persistent.git
# commit: 3b52b13d9ce79cdef14bb1c37cc527657a529462
# subdirs:
# - persistent-sqlite
ghc-options:
"$locals": -fwrite-ide-info

1
appmgr/.gitignore vendored
View File

@@ -1,2 +1,3 @@
/target
**/*.rs.bk
.DS_Store

2
appmgr/Cargo.lock generated
View File

@@ -41,7 +41,7 @@ checksum = "afddf7f520a80dbf76e6f50a35bca42a2331ef227a28b3b6dc5c2e2338d114b1"
[[package]]
name = "appmgr"
version = "0.2.10"
version = "0.2.14"
dependencies = [
"async-trait",
"avahi-sys",

View File

@@ -2,7 +2,7 @@
authors = ["Aiden McClelland <me@drbonez.dev>"]
edition = "2018"
name = "appmgr"
version = "0.2.10"
version = "0.2.14"
[lib]
name = "appmgrlib"
@@ -20,7 +20,9 @@ production = []
[dependencies]
async-trait = "0.1.42"
avahi-sys = { git = "https://github.com/Start9Labs/avahi-sys", branch = "feature/dynamic-linking", features = ["dynamic"], optional = true }
avahi-sys = { git = "https://github.com/Start9Labs/avahi-sys", branch = "feature/dynamic-linking", features = [
"dynamic",
], optional = true }
base32 = "0.4.0"
clap = "2.33"
ctrlc = "3.1.7"

View File

@@ -1,5 +1,17 @@
# appmgr
# Instructions
Clone the repo and enter the appmgr directory
`git clone https://github.com/Start9Labs/embassy-os.git`
`cd embassy-os/appmgr`
Install the portable version of appmgr
`cargo install --path=. --features=portable --no-default-features`
## Exit Codes
1. General Error
2. File System IO Error
@@ -7,4 +19,4 @@
4. Config Spec violation
5. Config Rules violation
6. Requested value does not exist
7. Invalid Backup Password
7. Invalid Backup Password

View File

@@ -3,6 +3,11 @@
set -e
shopt -s expand_aliases
if [ "$0" != "./build-dev.sh" ]; then
>&2 echo "Must be run from appmgr directory"
exit 1
fi
alias 'rust-arm-builder'='docker run --rm -it -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$(pwd)":/home/rust/src start9/rust-arm-cross:latest'
cd ..

15
appmgr/build-portable.sh Executable file
View File

@@ -0,0 +1,15 @@
#!/bin/bash
set -e
shopt -s expand_aliases
if [ "$0" != "./build-portable.sh" ]; then
>&2 echo "Must be run from appmgr directory"
exit 1
fi
alias 'rust-musl-builder'='docker run --rm -it -v "$HOME"/.cargo/registry:/root/.cargo/registry -v "$(pwd)":/home/rust/src messense/rust-musl-cross:x86_64-musl'
cd ..
rust-musl-builder sh -c "(cd appmgr && cargo build --release --target=x86_64-unknown-linux-musl --features=portable,production --no-default-features)"
cd appmgr

View File

@@ -3,6 +3,11 @@
set -e
shopt -s expand_aliases
if [ "$0" != "./build-prod.sh" ]; then
>&2 echo "Must be run from appmgr directory"
exit 1
fi
alias 'rust-arm-builder'='docker run --rm -it -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$(pwd)":/home/rust/src start9/rust-arm-cross:latest'
cd ..

View File

@@ -1,3 +1,4 @@
use std::os::unix::process::ExitStatusExt;
use std::path::Path;
use argon2::Config;
@@ -10,6 +11,7 @@ use serde::Serialize;
use crate::util::from_yaml_async_reader;
use crate::util::to_yaml_async_writer;
use crate::util::Invoke;
use crate::util::PersistencePath;
use crate::version::VersionT;
use crate::Error;
use crate::ResultExt;
@@ -224,6 +226,28 @@ pub async fn restore_backup<P: AsRef<Path>>(
}
crate::tor::restart().await?;
// Delete the fullchain certificate, so it can be regenerated with the restored tor pubkey address
PersistencePath::from_ref("apps")
.join(&app_id)
.join("cert-local.fullchain.crt.pem")
.delete()
.await?;
crate::tor::write_lan_services(
&crate::tor::services_map(&PersistencePath::from_ref(crate::SERVICES_YAML)).await?,
)
.await?;
let svc_exit = std::process::Command::new("service")
.args(&["nginx", "reload"])
.status()?;
crate::ensure_code!(
svc_exit.success(),
crate::error::GENERAL_ERROR,
"Failed to Reload Nginx: {}",
svc_exit
.code()
.or_else(|| { svc_exit.signal().map(|a| 128 + a) })
.unwrap_or(0)
);
Ok(())
}

View File

@@ -24,7 +24,6 @@ pub async fn start_app(name: &str, update_metadata: bool) -> Result<(), Error> {
if status == crate::apps::DockerStatus::Stopped {
if update_metadata {
crate::config::configure(name, None, None, false).await?;
crate::dependencies::update_shared(name).await?;
crate::dependencies::update_binds(name).await?;
}
crate::apps::set_needs_restart(name, false).await?;

View File

@@ -188,31 +188,6 @@ pub async fn auto_configure(
crate::config::configure(dependency, Some(dependency_config), None, dry_run).await
}
pub async fn update_shared(dependency_id: &str) -> Result<(), Error> {
let dependency_manifest = crate::apps::manifest(dependency_id).await?;
if let Some(shared) = dependency_manifest.shared {
for dependent_id in &crate::apps::dependents(dependency_id, false).await? {
let dependent_manifest = crate::apps::manifest(&dependent_id).await?;
if dependent_manifest
.dependencies
.0
.get(dependency_id)
.ok_or_else(|| failure::format_err!("failed to index dependent: {}", dependent_id))?
.mount_shared
{
tokio::fs::create_dir_all(
Path::new(crate::VOLUMES)
.join(dependency_id)
.join(&shared)
.join(&dependent_id),
)
.await?;
}
}
}
Ok(())
}
pub async fn update_binds(dependent_id: &str) -> Result<(), Error> {
let dependent_manifest = crate::apps::manifest(dependent_id).await?;
let dependency_manifests = futures::future::try_join_all(
@@ -222,12 +197,19 @@ pub async fn update_binds(dependent_id: &str) -> Result<(), Error> {
.into_iter()
.filter(|(_, info)| info.mount_public || info.mount_shared)
.map(|(id, info)| async {
crate::apps::manifest(&id).await.map(|man| (id, info, man))
Ok::<_, Error>(if crate::apps::list_info().await?.contains_key(&id) {
let man = crate::apps::manifest(&id).await?;
Some((id, info, man))
} else {
None
})
}),
)
.await?;
// i just have a gut feeling this shouldn't be concurrent
for (dependency_id, info, dependency_manifest) in dependency_manifests {
for (dependency_id, info, dependency_manifest) in
dependency_manifests.into_iter().filter_map(|a| a)
{
match (dependency_manifest.public, info.mount_public) {
(Some(public), true) => {
let public_path = Path::new(crate::VOLUMES).join(&dependency_id).join(public);

View File

@@ -1,10 +1,11 @@
use std::path::Path;
use failure::ResultExt as _;
use futures::future::try_join_all;
use crate::util::Invoke;
use crate::Error;
use crate::ResultExt;
use crate::ResultExt as _;
pub const FSTAB: &'static str = "/etc/fstab";
@@ -153,6 +154,11 @@ pub async fn bind<P0: AsRef<Path>, P1: AsRef<Path>>(
dst: P1,
read_only: bool,
) -> Result<(), Error> {
log::info!(
"Binding {} to {}",
src.as_ref().display(),
dst.as_ref().display()
);
let is_mountpoint = tokio::process::Command::new("mountpoint")
.arg(dst.as_ref())
.stdout(std::process::Stdio::null())
@@ -185,6 +191,7 @@ pub async fn bind<P0: AsRef<Path>, P1: AsRef<Path>>(
}
pub async fn unmount<P: AsRef<Path>>(mount_point: P) -> Result<(), Error> {
log::info!("Unmounting {}.", mount_point.as_ref().display());
let umount_output = tokio::process::Command::new("umount")
.arg(mount_point.as_ref())
.output()
@@ -192,10 +199,14 @@ pub async fn unmount<P: AsRef<Path>>(mount_point: P) -> Result<(), Error> {
crate::ensure_code!(
umount_output.status.success(),
crate::error::FILESYSTEM_ERROR,
"Error Unmounting Drive: {}",
"Error Unmounting Drive: {}: {}",
mount_point.as_ref().display(),
std::str::from_utf8(&umount_output.stderr).unwrap_or("Unknown Error")
);
tokio::fs::remove_dir_all(mount_point.as_ref()).await?;
tokio::fs::remove_dir_all(mount_point.as_ref())
.await
.with_context(|e| format!("rm {}: {}", mount_point.as_ref().display(), e))
.with_code(crate::error::FILESYSTEM_ERROR)?;
Ok(())
}

View File

@@ -561,14 +561,17 @@ pub async fn install_v0<R: AsyncRead + Unpin + Send + Sync>(
crate::config::configure(&manifest.id, Some(empty_config), None, false).await?;
}
}
crate::dependencies::update_binds(&manifest.id).await?;
for (dep_id, dep_info) in manifest.dependencies.0 {
if dep_info.mount_shared
&& crate::apps::list_info().await?.get(&dep_id).is_some()
&& crate::apps::manifest(&dep_id).await?.shared.is_some()
&& crate::apps::status(&dep_id, false).await?.status
!= crate::apps::DockerStatus::Stopped
{
crate::apps::set_needs_restart(&dep_id, true).await?;
match crate::apps::status(&dep_id, false).await?.status {
crate::apps::DockerStatus::Stopped => (),
crate::apps::DockerStatus::Running => crate::control::restart_app(&dep_id).await?,
_ => crate::apps::set_needs_restart(&dep_id, true).await?,
}
}
}

View File

@@ -1,3 +1,10 @@
map $http_x_forwarded_proto $real_proto {{
ext+onions ext+onions;
ext+onion ext+onion;
https https;
http http;
default $scheme;
}}
server {{
listen 443 ssl;
server_name {hostname}.local;
@@ -8,7 +15,10 @@ server {{
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto $real_proto;
client_max_body_size 0;
proxy_request_buffering off;
proxy_buffering off;
}}
}}
server {{

View File

@@ -4,5 +4,8 @@ server {{
location / {{
proxy_pass http://{app_ip}:{internal_port}/;
proxy_set_header Host $host;
client_max_body_size 0;
proxy_request_buffering off;
proxy_buffering off;
}}
}}

View File

@@ -1,9 +1,11 @@
use crate::failure::ResultExt;
use std::path::Path;
use linear_map::LinearMap;
use crate::dependencies::{DependencyError, TaggedDependencyError};
use crate::Error;
use crate::ResultExt as _;
pub async fn remove(
name: &str,
@@ -55,48 +57,79 @@ pub async fn remove(
log::info!("Removing tor hidden service.");
crate::tor::rm_svc(name).await?;
log::info!("Removing app metadata.");
tokio::fs::remove_dir_all(Path::new(crate::PERSISTENCE_DIR).join("apps").join(name))
.await?;
log::info!("Destroying mounted volume.");
let metadata_path = Path::new(crate::PERSISTENCE_DIR).join("apps").join(name);
tokio::fs::remove_dir_all(&metadata_path)
.await
.with_context(|e| format!("rm {}: {}", metadata_path.display(), e))
.with_code(crate::error::FILESYSTEM_ERROR)?;
log::info!("Unbinding shared filesystem.");
for (dep, info) in manifest.dependencies.0.iter() {
if info.mount_public {
crate::disks::unmount(
Path::new(crate::VOLUMES)
.join(name)
.join("start9")
.join("public")
.join(&dep),
)
.await?;
let installed_apps = crate::apps::list_info().await?;
for (dep, _) in manifest.dependencies.0.iter() {
let path = Path::new(crate::VOLUMES)
.join(name)
.join("start9")
.join("public")
.join(&dep);
if path.exists() {
crate::disks::unmount(&path).await?;
} else {
log::warn!("{} does not exist, skipping...", path.display());
}
if info.mount_shared {
if let Some(shared) = match crate::apps::manifest(dep).await {
Ok(man) => man.shared,
Err(e) => {
log::error!("Failed to Fetch Dependency Manifest: {}", e);
None
}
} {
let path = Path::new(crate::VOLUMES)
.join(name)
.join("start9")
.join("shared")
.join(&dep);
if path.exists() {
crate::disks::unmount(&path).await?;
}
let path = Path::new(crate::VOLUMES)
.join(name)
.join("start9")
.join("shared")
.join(&dep);
if path.exists() {
crate::disks::unmount(&path).await?;
} else {
log::warn!("{} does not exist, skipping...", path.display());
}
if installed_apps.contains_key(dep) {
let dep_man = crate::apps::manifest(dep).await?;
if let Some(shared) = dep_man.shared {
let path = Path::new(crate::VOLUMES).join(dep).join(&shared).join(name);
if path.exists() {
tokio::fs::remove_dir_all(
Path::new(crate::VOLUMES).join(dep).join(&shared).join(name),
)
.await?;
tokio::fs::remove_dir_all(&path)
.await
.with_context(|e| format!("rm {}: {}", path.display(), e))
.with_code(crate::error::FILESYSTEM_ERROR)?;
}
}
} else {
log::warn!("{} is not installed, skipping...", dep);
}
}
if manifest.public.is_some() || manifest.shared.is_some() {
for dependent in crate::apps::dependents(name, false).await? {
let path = Path::new(crate::VOLUMES)
.join(&dependent)
.join("start9")
.join("public")
.join(name);
if path.exists() {
crate::disks::unmount(&path).await?;
} else {
log::warn!("{} does not exist, skipping...", path.display());
}
let path = Path::new(crate::VOLUMES)
.join(dependent)
.join("start9")
.join("shared")
.join(name);
if path.exists() {
crate::disks::unmount(&path).await?;
} else {
log::warn!("{} does not exist, skipping...", path.display());
}
}
}
tokio::fs::remove_dir_all(Path::new(crate::VOLUMES).join(name)).await?;
log::info!("Destroying mounted volume.");
let volume_path = Path::new(crate::VOLUMES).join(name);
tokio::fs::remove_dir_all(&volume_path)
.await
.with_context(|e| format!("rm {}: {}", volume_path.display(), e))
.with_code(crate::error::FILESYSTEM_ERROR)?;
log::info!("Pruning unused docker images.");
crate::ensure_code!(
std::process::Command::new("docker")

View File

@@ -110,6 +110,14 @@ impl PersistencePath {
pub async fn for_update(self) -> Result<UpdateHandle<ForRead>, Error> {
UpdateHandle::new(self).await
}
pub async fn delete(&self) -> Result<(), Error> {
match tokio::fs::remove_file(self.path()).await {
Ok(()) => Ok(()),
Err(k) if k.kind() == std::io::ErrorKind::NotFound => Ok(()),
e => e.with_code(crate::error::FILESYSTEM_ERROR),
}
}
}
#[derive(Debug)]

View File

@@ -17,7 +17,6 @@ mod v0_1_4;
mod v0_1_5;
mod v0_2_0;
mod v0_2_1;
mod v0_2_10;
mod v0_2_2;
mod v0_2_3;
mod v0_2_4;
@@ -27,7 +26,13 @@ mod v0_2_7;
mod v0_2_8;
mod v0_2_9;
pub use v0_2_10::Version as Current;
mod v0_2_10;
mod v0_2_11;
mod v0_2_12;
mod v0_2_13;
mod v0_2_14;
pub use v0_2_14::Version as Current;
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
@@ -50,6 +55,10 @@ enum Version {
V0_2_8(Wrapper<v0_2_8::Version>),
V0_2_9(Wrapper<v0_2_9::Version>),
V0_2_10(Wrapper<v0_2_10::Version>),
V0_2_11(Wrapper<v0_2_11::Version>),
V0_2_12(Wrapper<v0_2_12::Version>),
V0_2_13(Wrapper<v0_2_13::Version>),
V0_2_14(Wrapper<v0_2_14::Version>),
Other(emver::Version),
}
@@ -162,6 +171,10 @@ pub async fn init() -> Result<(), failure::Error> {
Version::V0_2_8(v) => v.0.migrate_to(&Current::new()).await?,
Version::V0_2_9(v) => v.0.migrate_to(&Current::new()).await?,
Version::V0_2_10(v) => v.0.migrate_to(&Current::new()).await?,
Version::V0_2_11(v) => v.0.migrate_to(&Current::new()).await?,
Version::V0_2_12(v) => v.0.migrate_to(&Current::new()).await?,
Version::V0_2_13(v) => v.0.migrate_to(&Current::new()).await?,
Version::V0_2_14(v) => v.0.migrate_to(&Current::new()).await?,
Version::Other(_) => (),
// TODO find some way to automate this?
}
@@ -253,6 +266,10 @@ pub async fn self_update(requirement: emver::VersionRange) -> Result<(), Error>
Version::V0_2_8(v) => Current::new().migrate_to(&v.0).await?,
Version::V0_2_9(v) => Current::new().migrate_to(&v.0).await?,
Version::V0_2_10(v) => Current::new().migrate_to(&v.0).await?,
Version::V0_2_11(v) => Current::new().migrate_to(&v.0).await?,
Version::V0_2_12(v) => Current::new().migrate_to(&v.0).await?,
Version::V0_2_13(v) => Current::new().migrate_to(&v.0).await?,
Version::V0_2_14(v) => Current::new().migrate_to(&v.0).await?,
Version::Other(_) => (),
// TODO find some way to automate this?
};

View File

@@ -0,0 +1,38 @@
use super::*;
use std::os::unix::process::ExitStatusExt;
const V0_2_11: emver::Version = emver::Version::new(0, 2, 11, 0);
pub struct Version;
#[async_trait]
impl VersionT for Version {
type Previous = v0_2_10::Version;
fn new() -> Self {
Version
}
fn semver(&self) -> &'static emver::Version {
&V0_2_11
}
async fn up(&self) -> Result<(), Error> {
crate::tor::write_lan_services(
&crate::tor::services_map(&PersistencePath::from_ref(crate::SERVICES_YAML)).await?,
)
.await?;
let svc_exit = std::process::Command::new("service")
.args(&["nginx", "reload"])
.status()?;
crate::ensure_code!(
svc_exit.success(),
crate::error::GENERAL_ERROR,
"Failed to Reload Nginx: {}",
svc_exit
.code()
.or_else(|| { svc_exit.signal().map(|a| 128 + a) })
.unwrap_or(0)
);
Ok(())
}
async fn down(&self) -> Result<(), Error> {
Ok(())
}
}

View File

@@ -0,0 +1,38 @@
use super::*;
use std::os::unix::process::ExitStatusExt;
const V0_2_12: emver::Version = emver::Version::new(0, 2, 12, 0);
pub struct Version;
#[async_trait]
impl VersionT for Version {
type Previous = v0_2_11::Version;
fn new() -> Self {
Version
}
fn semver(&self) -> &'static emver::Version {
&V0_2_12
}
async fn up(&self) -> Result<(), Error> {
crate::tor::write_lan_services(
&crate::tor::services_map(&PersistencePath::from_ref(crate::SERVICES_YAML)).await?,
)
.await?;
let svc_exit = std::process::Command::new("service")
.args(&["nginx", "reload"])
.status()?;
crate::ensure_code!(
svc_exit.success(),
crate::error::GENERAL_ERROR,
"Failed to Reload Nginx: {}",
svc_exit
.code()
.or_else(|| { svc_exit.signal().map(|a| 128 + a) })
.unwrap_or(0)
);
Ok(())
}
async fn down(&self) -> Result<(), Error> {
Ok(())
}
}

View File

@@ -0,0 +1,21 @@
use super::*;
const V0_2_13: emver::Version = emver::Version::new(0, 2, 13, 0);
pub struct Version;
#[async_trait]
impl VersionT for Version {
type Previous = v0_2_12::Version;
fn new() -> Self {
Version
}
fn semver(&self) -> &'static emver::Version {
&V0_2_13
}
async fn up(&self) -> Result<(), Error> {
Ok(())
}
async fn down(&self) -> Result<(), Error> {
Ok(())
}
}

View File

@@ -0,0 +1,21 @@
use super::*;
const V0_2_14: emver::Version = emver::Version::new(0, 2, 14, 0);
pub struct Version;
#[async_trait]
impl VersionT for Version {
type Previous = v0_2_13::Version;
fn new() -> Self {
Version
}
fn semver(&self) -> &'static emver::Version {
&V0_2_14
}
async fn up(&self) -> Result<(), Error> {
Ok(())
}
async fn down(&self) -> Result<(), Error> {
Ok(())
}
}

BIN
assets/Embassy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

BIN
assets/Marketplace.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

BIN
assets/ServiceDetails.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

BIN
assets/ServicesRunning.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

BIN
assets/eos.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

BIN
eos.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 657 KiB

View File

@@ -1,5 +1,10 @@
#!/bin/bash
arch=$(uname -m)
if [[ $arch == armv7l ]]; then
dev_target="target"
else
dev_target="target/armv7-unknown-linux-musleabihf"
fi
mv buster.img embassy.img
product_key=$(cat product_key)
loopdev=$(losetup -f -P embassy.img --show)
@@ -9,6 +14,12 @@ mkdir -p "${root_mountpoint}"
mkdir -p "${boot_mountpoint}"
mount "${loopdev}p2" "${root_mountpoint}"
mount "${loopdev}p1" "${boot_mountpoint}"
mkdir -p "${root_mountpoint}/root/agent"
mkdir -p "${root_mountpoint}/etc/docker"
mkdir -p "${root_mountpoint}/home/pi/.ssh"
echo -n "" > "${root_mountpoint}/home/pi/.ssh/authorized_keys"
chown -R pi:pi "${root_mountpoint}/home/pi/.ssh"
echo -n "" > "${boot_mountpoint}/ssh"
echo "${product_key}" > "${root_mountpoint}/root/agent/product_key"
echo -n "start9-" > "${root_mountpoint}/etc/hostname"
echo -n "${product_key}" | shasum -t -a 256 | cut -c1-8 >> "${root_mountpoint}/etc/hostname"
@@ -18,20 +29,23 @@ echo -n "${product_key}" | shasum -t -a 256 | cut -c1-8 >> "${root_mountpoint}/e
mv "${root_mountpoint}/etc/hosts.tmp" "${root_mountpoint}/etc/hosts"
cp agent/dist/agent "${root_mountpoint}/usr/local/bin/agent"
chmod 700 "${root_mountpoint}/usr/local/bin/agent"
cp appmgr/target/armv7-unknown-linux-musleabihf/release/appmgr "${root_mountpoint}/usr/local/bin/appmgr"
cp "appmgr/${dev_target}/release/appmgr" "${root_mountpoint}/usr/local/bin/appmgr"
chmod 700 "${root_mountpoint}/usr/local/bin/appmgr"
cp lifeline/target/armv7-unknown-linux-musleabihf/release/lifeline "${root_mountpoint}/usr/local/bin/lifeline"
cp "lifeline/${dev_target}/release/lifeline" "${root_mountpoint}/usr/local/bin/lifeline"
chmod 700 "${root_mountpoint}/usr/local/bin/lifeline"
cp docker-daemon.json "${root_mountpoint}/etc/docker/daemon.json"
cp setup.sh "${root_mountpoint}/root/setup.sh"
chmod 700 "${root_mountpoint}/root/setup.sh"
cp setup.service /etc/systemd/system/setup.service
cp lifeline/lifeline.service /etc/systemd/system/lifeline.service
cp agent/config/agent.service /etc/systemd/system/agent.service
cat "${boot_mountpoint}/config.txt" | grep -v "dtoverlay=pwm-2chan" > "${boot_mountpoint}/config.txt.tmp"
cp setup.service "${root_mountpoint}/etc/systemd/system/setup.service"
ln -s /etc/systemd/system/setup.service "${root_mountpoint}/etc/systemd/system/getty.target.wants/setup.service"
cp lifeline/lifeline.service "${root_mountpoint}/etc/systemd/system/lifeline.service"
cp agent/config/agent.service "${root_mountpoint}/etc/systemd/system/agent.service"
cat "${boot_mountpoint}/config.txt" | grep -v "dtoverlay=" > "${boot_mountpoint}/config.txt.tmp"
echo "dtoverlay=pwm-2chan" >> "${boot_mountpoint}/config.txt.tmp"
mv "${boot_mountpoint}/config.txt.tmp" "${boot_mountpoint}/config.txt"
umount "${root_mountpoint}"
rm -r "${root_mountpoint}"
umount "${boot_mountpoint}"
rm -r "${boot_mountpoint}"
losetup -d ${loopdev}
losetup -d ${loopdev}
echo "DONE! Here is your EmbassyOS key: ${product_key}"

View File

@@ -1,10 +1,15 @@
[Unit]
Description=Boot process for system setup.
After=rc-local.service
Before=getty.target
ConditionFileNotEmpty=/root/setup.sh
[Service]
Type=oneshot
ExecStart=/root/setup.sh
ExecStartPost=/root/setup-s2.sh
RemainAfterExit=true
[Install]
WantedBy=multi-user.target
WantedBy=basic.target

View File

@@ -1,15 +1,26 @@
#!/bin/bash
apt update
apt install -y libsecp256k1-0
apt install -y tor
apt install -y docker.io
apt install -y iotop
apt install -y bmon
apt autoremove -y
mkdir -p /root/volumes
mkdir -p /root/tmp/appmgr
mkdir -p /root/agent
mkdir -p /root/appmgr/tor
apt-get update -y
apt-get install -y tor
apt-get install -y iotop
apt-get install -y bmon
apt-get install -y libavahi-client3
apt-get install -y libsecp256k1-0
apt-get install -y docker.io needrestart-
mv /root/setup.sh /root/setup-s1.sh.done
cat <<EOT >> /root/setup-s2.sh
#!/bin/bash
apt-get update -y
apt-get install -y tor
apt-get install -y iotop
apt-get install -y bmon
apt-get install -y libavahi-client3
apt-get install -y libsecp256k1-0
apt-get install -y docker.io needrestart-
apt-get autoremove -y
systemctl enable lifeline
systemctl enable agent
systemctl enable ssh
@@ -17,5 +28,8 @@ systemctl enable avahi-daemon
passwd -l root
passwd -l pi
sync
systemctl disable setup.service
reboot
systemctl disable setup
mv /root/setup-s2.sh /root/setup-s2.sh.done
reboot
EOT
chmod +x /root/setup-s2.sh

41
ui/build-send-alpha.sh Executable file
View File

@@ -0,0 +1,41 @@
#!/bin/bash
set -e
echo "turn off mocks"
echo "$( jq '.useMocks = false' use-mocks.json )" > use-mocks.json
echo "$( jq '.skipStartupAlerts = false' use-mocks.json )" > use-mocks.json
echo "FILTER: rm -rf www"
rm -rf www
echo "FILTER: ionic build"
npm run build-prod
echo "FILTER: cp client-manifest.yaml www"
cp client-manifest.yaml www
echo "FILTER: git hash"
touch git-hash.txt
git log | head -n1 > git-hash.txt
mv git-hash.txt www
echo "FILTER: removing mock icons"
rm -rf www/assets/img/service-icons
echo "FILTER: tar -zcvf ambassador-ui.tar.gz www"
tar -zcvf ambassador-ui.tar.gz www
SHA_SUM=$(sha1sum ambassador-ui.tar.gz)
echo "${SHA_SUM}"
echo "Set version"
VERSION=$(jq ".version" package.json)
echo "${VERSION}"
echo "FILTER: mkdir alpha-reg"
ssh root@alpha-registry.start9labs.com "mkdir -p /var/www/html/resources/sys/ambassador-ui.tar.gz/${VERSION}"
echo "FILTER: scp ambassador-ui.tar.gz"
scp ambassador-ui.tar.gz root@alpha-registry.start9labs.com:/var/www/html/resources/sys/ambassador-ui.tar.gz/${VERSION}/ambassador-ui.tar.gz
echo "FILTER: fin"

View File

@@ -1,6 +1,6 @@
manifest-version: 0
app-id: start9-ambassador
app-version: 0.2.10
app-version: 0.2.14
uri-rewrites:
- =/api -> http://{{start9-ambassador}}:5959/authenticate
- /api/ -> http://{{start9-ambassador}}:5959/

1403
ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "embassy-ui",
"version": "0.2.10",
"version": "0.2.14",
"description": "GUI for EmbassyOS",
"author": "Start9 Labs",
"homepage": "https://github.com/Start9Labs/embassy-ui",

View File

@@ -298,11 +298,11 @@ export class ConfigCursor<T extends ValueType> {
const mappedCfg = this.mappedConfig()
if (cfg && mappedCfg && typeof cfg === 'object' && typeof mappedCfg === 'object') {
const spec = this.spec()
let allKeys
let allKeys: Set<string>
if (spec.type === 'union') {
let unionSpec = spec as ValueSpecOf<'union'>
const labelForSelection = unionSpec.tag.id
allKeys = new Set([...Object.keys(unionSpec.variants[cfg[labelForSelection]])])
allKeys = new Set([labelForSelection, ...Object.keys(unionSpec.variants[cfg[labelForSelection]])])
} else {
allKeys = new Set([...Object.keys(cfg), ...Object.keys(mappedCfg)])
}

View File

@@ -17,8 +17,15 @@
<ion-item-group>
<ion-item-divider></ion-item-divider>
<ion-item>
<ion-icon size="small" slot="start" *ngIf="!edited"
style="margin-right: 15px; color: rgba(0,0,0,0); background: radial-gradient(#2a4e8970, #2a4e8970 35%, transparent 35%, transparent);"
name="ellipse"></ion-icon>
<ion-icon size="small" slot="start" *ngIf="edited" style="margin-right: 15px" color="primary" name="ellipse">
</ion-icon>
<ion-label>{{ spec.tag.name }}</ion-label>
<ion-select slot="end" [interfaceOptions]="setSelectOptions()" placeholder="Select One" [(ngModel)]="value[spec.tag.id]" [selectedText]="spec.tag.variantNames[value[spec.tag.id]]" (ngModelChange)="handleUnionChange()">
<ion-select slot="end" [interfaceOptions]="setSelectOptions()" placeholder="Select One"
[(ngModel)]="value[spec.tag.id]" [selectedText]="spec.tag.variantNames[value[spec.tag.id]]"
(ngModelChange)="handleUnionChange()">
<ion-select-option *ngFor="let option of spec.variants | keyvalue: asIsOrder" [value]="option.key">
{{ spec.tag.variantNames[option.key] }}
<span *ngIf="option.key === spec.default"> (default)</span>
@@ -28,4 +35,4 @@
<object-config [cursor]="cursor" (onEdit)="handleObjectEdit()"></object-config>
</ion-item-group>
</ion-content>
</ion-content>

View File

@@ -19,6 +19,7 @@ export class AppConfigUnionPage {
spec: ValueSpecUnion
value: object
error: string
edited: boolean
constructor (
private readonly modalCtrl: ModalController,
@@ -28,6 +29,7 @@ export class AppConfigUnionPage {
this.spec = this.cursor.spec()
this.value = this.cursor.config()
this.error = this.cursor.checkInvalid()
this.edited = this.cursor.seekNext(this.spec.tag.id).isEdited()
}
async dismiss () {
@@ -37,6 +39,8 @@ export class AppConfigUnionPage {
async handleUnionChange () {
this.value = mapUnionSpec(this.spec, this.value)
this.objectConfig.annotations = this.objectConfig.cursor.getAnnotations()
this.error = this.cursor.checkInvalid()
this.edited = this.cursor.seekNext(this.spec.tag.id).isEdited()
}
setSelectOptions () {

View File

@@ -1,7 +1,7 @@
<ion-header>
<ion-toolbar>
<ion-title >
<ion-label style="font-size: 20px;" class="ion-text-wrap">Welcome to 0.2.10!</ion-label>
<ion-label style="font-size: 20px;" class="ion-text-wrap">Welcome to 0.2.14!</ion-label>
</ion-title>
</ion-toolbar>
</ion-header>
@@ -9,14 +9,9 @@
<ion-content class="ion-padding">
<div style="display: flex; flex-direction: column; justify-content: space-between; height: 100%">
<h2>Highlights</h2>
<p class="main-content">
0.2.10 introduces LAN support for services running on the Embassy. A service's LAN address (.local URL) can be accessed while connected to the same network.
This is useful for two reasons: (1) LAN connections are significantly faster than Tor, and (2) if the Tor network is experiencing connectivity issues, you will not be locked out of your services.
It also introduces support for services to define one-time actions that are exposed to the user. This
can be useful for password resets or other types of operations where doing it through the service UI would be
insecure or otherwise undesirable.
</p>
<div class="main-content">
<p>This release contains an important security patch to the tor binaries</p>
</div>
<div class="close-button">
<ion-button fill="outline" (click)="dismiss()">

View File

@@ -18,9 +18,11 @@ export interface AppAvailablePreview extends BaseApp {
}
export type AppAvailableFull =
AppAvailablePreview &
{ descriptionLong: string
AppAvailablePreview & {
descriptionLong: string
versions: string[]
licenseName?: string // @TODO required for 0.3.0
licenseLink?: string // @TODO required for 0.3.0
} &
AppAvailableVersionSpecificInfo
@@ -45,6 +47,8 @@ export interface AppInstalledPreview extends BaseApp {
}
export interface AppInstalledFull extends AppInstalledPreview {
licenseName?: string // @TODO required for 0.3.0
licenseLink?: string // @TODO required for 0.3.0
instructions: string | null
lastBackup: string | null
configuredRequirements: AppDependency[] | null // null if not yet configured

View File

@@ -59,20 +59,23 @@ export class AppActionsPage extends Cleanup {
})
await alert.present()
} else {
const joinStatuses = (statuses: AppStatus[]) => {
const last = statuses.pop()
let s = statuses.join(', ')
if (last) {
if (statuses.length > 1) { // oxford comma
s += ','
}
s += ` or ${last}`
const statuses = [...action.allowedStatuses]
const last = statuses.pop()
let statusesStr = statuses.join(', ')
let error = null
if (statuses.length) {
if (statuses.length > 1) { // oxford comma
statusesStr += ','
}
return s
statusesStr += ` or ${last}`
} else if (last) {
statusesStr = `${last}`
} else {
error = `There is state for which this action may be run. This is a bug. Please file an issue with the service maintainer.`
}
const alert = await this.alertCtrl.create({
header: 'Forbidden',
message: `Action "${action.name}" can only be executed when service is ${joinStatuses(action.allowedStatuses)}`,
message: error || `Action "${action.name}" can only be executed when service is ${statusesStr}`,
buttons: ['OK'],
cssClass: 'alert-error-message',
})

View File

@@ -21,6 +21,26 @@
<ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text>
</ion-item>
<ion-card *ngIf="v1Status.status === 'instructions'" [href]="'https://start9.com/eos-' + v1Status.version" target="_blank" class="instructions-card">
<ion-card-header>
<ion-card-subtitle>Coming Soon...</ion-card-subtitle>
<ion-card-title>EmbassyOS Version {{ v1Status.version }}</ion-card-title>
</ion-card-header>
<ion-card-content>
<b>Get ready. View the update instructions.</b>
</ion-card-content>
</ion-card>
<ion-card *ngIf="v1Status.status === 'available'" [href]="'https://start9.com/eos-' + v1Status.version" target="_blank" class="available-card">
<ion-card-header>
<ion-card-subtitle>Now Available...</ion-card-subtitle>
<ion-card-title>EmbassyOS Version {{ v1Status.version }}</ion-card-title>
</ion-card-header>
<ion-card-content>
<b>View the update instructions.</b>
</ion-card-content>
</ion-card>
<ion-card *ngFor="let app of apps" style="margin: 10px 10px;" [routerLink]="[app.id]">
<ion-item style="--inner-border-width: 0 0 .4px 0; --border-color: #525252;" *ngIf="{ installing: (app.subject.status | async) === 'INSTALLING', installComparison: app.subject | compareInstalledAndLatest | async } as l">
<ion-avatar style="margin-top: 8px;" slot="start">

View File

@@ -3,4 +3,14 @@
font-style: italic;
font-family: 'Open Sans';
padding: 1px 0px 1.5px 0px;
}
.instructions-card {
--background: linear-gradient(45deg, #101010 16%, var(--ion-color-medium) 150%);
margin: 16px 10px;
}
.available-card {
--background: linear-gradient(45deg, #101010 16%, var(--ion-color-danger) 150%);
margin: 16px 10px;
}

View File

@@ -8,6 +8,7 @@ import { Subscription, BehaviorSubject, combineLatest } from 'rxjs'
import { take } from 'rxjs/operators'
import { markAsLoadingDuringP } from 'src/app/services/loader.service'
import { OsUpdateService } from 'src/app/services/os-update.service'
import { V1Status } from 'src/app/services/api/api-types'
@Component({
selector: 'app-available-list',
@@ -20,6 +21,7 @@ export class AppAvailableListPage {
installedAppDeltaSubscription: Subscription
apps: PropertySubjectId<AppAvailablePreview>[] = []
appsInstalled: PropertySubjectId<AppInstalledPreview>[] = []
v1Status: V1Status = { status: 'nothing', version: '' }
constructor (
private readonly apiService: ApiService,
@@ -35,6 +37,7 @@ export class AppAvailableListPage {
markAsLoadingDuringP(this.$loading$, Promise.all([
this.getApps(),
this.checkV1Status(),
this.osUpdateService.checkWhenNotAvailable$().toPromise(), // checks for an os update, banner component renders conditionally
pauseFor(600),
]))
@@ -44,6 +47,14 @@ export class AppAvailableListPage {
this.appModel.getContents().forEach(appInstalled => this.mergeInstalledProps(appInstalled.id))
}
async checkV1Status () {
try {
this.v1Status = await this.apiService.checkV1Status()
} catch (e) {
console.error(e)
}
}
mergeInstalledProps (appInstalledId: string) {
const appAvailable = this.apps.find(app => app.id === appInstalledId)
if (!appAvailable) return

View File

@@ -17,6 +17,8 @@
versionInstalled: $app$.versionInstalled | async,
versionViewing: $app$.versionViewing | async,
descriptionLong: $app$.descriptionLong | async,
licenseName: $app$.licenseName | async,
licenseLink: $app$.licenseLink | async,
serviceRequirements: $app$.serviceRequirements | async,
iconURL: $app$.iconURL | async,
releaseNotes: $app$.releaseNotes | async
@@ -112,9 +114,14 @@
<dependency-list [$loading$]="$dependenciesLoading$" [hostApp]="$app$ | peekProperties" [dependencies]="vars.serviceRequirements"></dependency-list>
</ng-container>
<ion-item-divider></ion-item-divider>
<ion-item *ngIf="vars.licenseLink" lines="none" button [href]="vars.licenseLink" target="_blank">
<ion-icon slot="start" name="newspaper-outline"></ion-icon>
<ion-label>License</ion-label>
<ion-note slot="end">{{ vars.licenseName }}</ion-note>
</ion-item>
<ion-item lines="none" button (click)="presentAlertVersions()">
<ion-icon color="medium" slot="start" name="file-tray-stacked-outline"></ion-icon>
<ion-label color="medium">Other versions</ion-label>
<ion-icon slot="start" name="file-tray-stacked-outline"></ion-icon>
<ion-label>Other versions</ion-label>
</ion-item>
</ion-item-group>
</ng-container>

View File

@@ -15,6 +15,8 @@
torAddress: app.torAddress | async,
status: app.status | async,
versionInstalled: app.versionInstalled | async,
licenseName: app.licenseName | async,
licenseLink: app.licenseLink | async,
configuredRequirements: app.configuredRequirements | async,
lastBackup: app.lastBackup | async,
hasFetchedFull: app.hasFetchedFull | async,
@@ -157,6 +159,12 @@
<ion-icon slot="start" name="storefront-outline" color="primary"></ion-icon>
<ion-label><ion-text color="primary">Marketplace Listing</ion-text></ion-label>
</ion-item>
<!-- license -->
<ion-item *ngIf="vars.licenseLink" lines="none" button [href]="vars.licenseLink" target="_blank">
<ion-icon slot="start" name="newspaper-outline" color="primary"></ion-icon>
<ion-label><ion-text color="primary">License</ion-text></ion-label>
<ion-note slot="end">{{ vars.licenseName }}</ion-note>
</ion-item>
<!-- dependencies -->
<ng-container *ngIf="vars.configuredRequirements && vars.configuredRequirements.length">

View File

@@ -37,3 +37,7 @@ export interface ApiAppConfig {
export type Unit = { never?: never; } // hack for the unit typ
export type V1Status = {
status: 'nothing' | 'instructions' | 'available'
version: string
}

View File

@@ -2,7 +2,7 @@ import { Rules } from '../../models/app-model'
import { AppAvailablePreview, AppAvailableFull, AppInstalledPreview, AppInstalledFull, DependentBreakage, AppAvailableVersionSpecificInfo, ServiceAction } from '../../models/app-types'
import { S9Notification, SSHFingerprint, ServerMetrics, DiskInfo } from '../../models/server-model'
import { Subject, Observable } from 'rxjs'
import { Unit, ApiServer, ApiAppInstalledFull, ApiAppConfig, ApiAppAvailableFull, ApiAppInstalledPreview } from './api-types'
import { Unit, ApiServer, ApiAppInstalledFull, ApiAppConfig, ApiAppAvailableFull, ApiAppInstalledPreview, V1Status } from './api-types'
import { AppMetrics, AppMetricsVersioned } from 'src/app/util/metrics.util'
import { ConfigSpec } from 'src/app/app-config/config-types'
@@ -64,6 +64,7 @@ export abstract class ApiService {
abstract ejectExternalDisk (logicalName: string): Promise<Unit>
abstract serviceAction (appId: string, serviceAction: ServiceAction): Promise<ReqRes.ServiceActionResponse>
abstract refreshLAN (): Promise<Unit>
abstract checkV1Status (): Promise<V1Status>
}
export function isRpcFailure<Error, Result> (arg: { error: Error } | { result: Result }): arg is { error: Error } {

View File

@@ -4,7 +4,7 @@ import { AppModel, AppStatus } from '../../models/app-model'
import { AppAvailablePreview, AppAvailableFull, AppInstalledFull, AppInstalledPreview, DependentBreakage, AppAvailableVersionSpecificInfo, ServiceAction } from '../../models/app-types'
import { S9Notification, SSHFingerprint, ServerModel, DiskInfo } from '../../models/server-model'
import { ApiService, ReqRes } from './api.service'
import { ApiAppInstalledPreview, ApiServer, Unit } from './api-types'
import { ApiAppInstalledPreview, ApiServer, Unit, V1Status } from './api-types'
import { HttpErrorResponse } from '@angular/common/http'
import { isUnauthorized } from 'src/app/util/web.util'
import { Replace } from 'src/app/util/types.util'
@@ -17,7 +17,7 @@ import { ConfigService } from '../config.service'
@Injectable()
export class LiveApiService extends ApiService {
constructor(
constructor (
private readonly http: HttpService,
// TODO remove app + server model from here. updates to state should be done in a separate class wrapping ApiService + App/ServerModel
private readonly appModel: AppModel,
@@ -25,40 +25,40 @@ export class LiveApiService extends ApiService {
private readonly config: ConfigService,
) { super() }
testConnection(url: string): Promise<true> {
testConnection (url: string): Promise<true> {
return this.http.raw.get(url).pipe(mapTo(true as true), catchError(e => catchHttpStatusError(e))).toPromise()
}
// Used to check whether password or key is valid. If so, it will be used implicitly by all other calls.
async getCheckAuth(): Promise<Unit> {
async getCheckAuth (): Promise<Unit> {
return this.http.serverRequest<Unit>({ method: Method.GET, url: '/authenticate' }, { version: '' })
}
async postLogin(password: string): Promise<Unit> {
async postLogin (password: string): Promise<Unit> {
return this.http.serverRequest<Unit>({ method: Method.POST, url: '/auth/login', data: { password } }, { version: '' })
}
async postLogout(): Promise<Unit> {
return this.http.serverRequest<Unit>({ method: Method.POST, url: '/auth/logout' }, { version: '' }).then(() => { this.authenticatedRequestsEnabled = false; return {} })
async postLogout (): Promise<Unit> {
return this.http.serverRequest<Unit>({ method: Method.POST, url: '/auth/logout' }, { version: '' }).then(() => { this.authenticatedRequestsEnabled = false; return { } })
}
async getServer(timeout?: number): Promise<ApiServer> {
async getServer (timeout?: number): Promise<ApiServer> {
return this.authRequest<ReqRes.GetServerRes>({ method: Method.GET, url: '/', readTimeout: timeout })
}
async acknowledgeOSWelcome(version: string): Promise<Unit> {
async acknowledgeOSWelcome (version: string): Promise<Unit> {
return this.authRequest<Unit>({ method: Method.POST, url: `/welcome/${version}` })
}
async getVersionLatest(): Promise<ReqRes.GetVersionLatestRes> {
async getVersionLatest (): Promise<ReqRes.GetVersionLatestRes> {
return this.authRequest<ReqRes.GetVersionLatestRes>({ method: Method.GET, url: '/versionLatest' }, { version: '' })
}
async getServerMetrics(): Promise<ReqRes.GetServerMetricsRes> {
async getServerMetrics (): Promise<ReqRes.GetServerMetricsRes> {
return this.authRequest<ReqRes.GetServerMetricsRes>({ method: Method.GET, url: `/metrics` })
}
async getNotifications(page: number, perPage: number): Promise<S9Notification[]> {
async getNotifications (page: number, perPage: number): Promise<S9Notification[]> {
const params: ReqRes.GetNotificationsReq = {
page: String(page),
perPage: String(perPage),
@@ -66,27 +66,27 @@ export class LiveApiService extends ApiService {
return this.authRequest<ReqRes.GetNotificationsRes>({ method: Method.GET, url: `/notifications`, params })
}
async deleteNotification(id: string): Promise<Unit> {
async deleteNotification (id: string): Promise<Unit> {
return this.authRequest({ method: Method.DELETE, url: `/notifications/${id}` })
}
async getExternalDisks(): Promise<DiskInfo[]> {
async getExternalDisks (): Promise<DiskInfo[]> {
return this.authRequest<ReqRes.GetExternalDisksRes>({ method: Method.GET, url: `/disks` })
}
// TODO: EJECT-DISKS
async ejectExternalDisk(logicalName: string): Promise<Unit> {
async ejectExternalDisk (logicalName: string): Promise<Unit> {
return this.authRequest({ method: Method.POST, url: `/disks/eject`, data: { logicalName } })
}
async updateAgent(version: string): Promise<Unit> {
async updateAgent (version: string): Promise<Unit> {
const data: ReqRes.PostUpdateAgentReq = {
version: `=${version}`,
}
return this.authRequest({ method: Method.POST, url: '/update', data })
}
async getAvailableAppVersionSpecificInfo(appId: string, versionSpec: string): Promise<AppAvailableVersionSpecificInfo> {
async getAvailableAppVersionSpecificInfo (appId: string, versionSpec: string): Promise<AppAvailableVersionSpecificInfo> {
return this
.authRequest<Replace<ReqRes.GetAppAvailableVersionInfoRes, 'versionViewing', 'version'>>({ method: Method.GET, url: `/apps/${appId}/store/${versionSpec}` })
.then(res => ({ ...res, versionViewing: res.version }))
@@ -96,7 +96,7 @@ export class LiveApiService extends ApiService {
})
}
async getAvailableApps(): Promise<AppAvailablePreview[]> {
async getAvailableApps (): Promise<AppAvailablePreview[]> {
const res = await this.authRequest<ReqRes.GetAppsAvailableRes>({ method: Method.GET, url: '/apps/store' })
return res.map(a => {
const latestVersionTimestamp = new Date(a.latestVersionTimestamp)
@@ -105,7 +105,7 @@ export class LiveApiService extends ApiService {
})
}
async getAvailableApp(appId: string): Promise<AppAvailableFull> {
async getAvailableApp (appId: string): Promise<AppAvailableFull> {
return this.authRequest<ReqRes.GetAppAvailableRes>({ method: Method.GET, url: `/apps/${appId}/store` })
.then(res => {
return {
@@ -115,7 +115,7 @@ export class LiveApiService extends ApiService {
})
}
async getInstalledApp(appId: string): Promise<AppInstalledFull> {
async getInstalledApp (appId: string): Promise<AppInstalledFull> {
return this.authRequest<ReqRes.GetAppInstalledRes>({ method: Method.GET, url: `/apps/${appId}/installed` })
.then(app => {
return {
@@ -127,7 +127,7 @@ export class LiveApiService extends ApiService {
})
}
async getInstalledApps(): Promise<AppInstalledPreview[]> {
async getInstalledApps (): Promise<AppInstalledPreview[]> {
return this.authRequest<ReqRes.GetAppsInstalledRes>({ method: Method.GET, url: `/apps/installed` })
.then(apps => {
return apps.map(app => {
@@ -140,24 +140,24 @@ export class LiveApiService extends ApiService {
})
}
async getAppConfig(appId: string): Promise<ReqRes.GetAppConfigRes> {
async getAppConfig (appId: string): Promise<ReqRes.GetAppConfigRes> {
return this.authRequest<ReqRes.GetAppConfigRes>({ method: Method.GET, url: `/apps/${appId}/config` })
}
async getAppLogs(appId: string, params: ReqRes.GetAppLogsReq = {}): Promise<string[]> {
async getAppLogs (appId: string, params: ReqRes.GetAppLogsReq = { }): Promise<string[]> {
return this.authRequest<ReqRes.GetAppLogsRes>({ method: Method.GET, url: `/apps/${appId}/logs`, params: params as any })
}
async getServerLogs(): Promise<string[]> {
async getServerLogs (): Promise<string[]> {
return this.authRequest<ReqRes.GetServerLogsRes>({ method: Method.GET, url: `/logs` })
}
async getAppMetrics(appId: string): Promise<AppMetrics> {
async getAppMetrics (appId: string): Promise<AppMetrics> {
return this.authRequest<ReqRes.GetAppMetricsRes | string>({ method: Method.GET, url: `/apps/${appId}/metrics` })
.then(parseMetricsPermissive)
}
async installApp(appId: string, version: string, dryRun: boolean = false): Promise<AppInstalledFull & { breakages: DependentBreakage[] }> {
async installApp (appId: string, version: string, dryRun: boolean = false): Promise<AppInstalledFull & { breakages: DependentBreakage[] }> {
const data: ReqRes.PostInstallAppReq = {
version,
}
@@ -172,94 +172,94 @@ export class LiveApiService extends ApiService {
})
}
async uninstallApp(appId: string, dryRun: boolean = false): Promise<{ breakages: DependentBreakage[] }> {
async uninstallApp (appId: string, dryRun: boolean = false): Promise<{ breakages: DependentBreakage[] }> {
return this.authRequest({ method: Method.POST, url: `/apps/${appId}/uninstall${dryRunParam(dryRun, true)}`, readTimeout: 60000 })
}
async startApp(appId: string): Promise<Unit> {
async startApp (appId: string): Promise<Unit> {
return this.authRequest({ method: Method.POST, url: `/apps/${appId}/start`, readTimeout: 60000 })
.then(() => this.appModel.update({ id: appId, status: AppStatus.RUNNING }))
.then(() => ({}))
.then(() => ({ }))
}
async stopApp(appId: string, dryRun: boolean = false): Promise<{ breakages: DependentBreakage[] }> {
async stopApp (appId: string, dryRun: boolean = false): Promise<{ breakages: DependentBreakage[] }> {
const res = await this.authRequest<{ breakages: DependentBreakage[] }>({ method: Method.POST, url: `/apps/${appId}/stop${dryRunParam(dryRun, true)}`, readTimeout: 60000 })
if (!dryRun) this.appModel.update({ id: appId, status: AppStatus.STOPPING }, modulateTime(new Date(), 5, 'seconds'))
return res
}
async restartApp(appId: string): Promise<Unit> {
async restartApp (appId: string): Promise<Unit> {
return this.authRequest({ method: Method.POST, url: `/apps/${appId}/restart`, readTimeout: 60000 })
.then(() => ({} as any))
.then(() => ({ } as any))
}
async createAppBackup(appId: string, logicalname: string, password?: string): Promise<Unit> {
async createAppBackup (appId: string, logicalname: string, password?: string): Promise<Unit> {
const data: ReqRes.PostAppBackupCreateReq = {
password: password || undefined,
logicalname,
}
return this.authRequest<ReqRes.PostAppBackupCreateRes>({ method: Method.POST, url: `/apps/${appId}/backup`, data, readTimeout: 60000 })
.then(() => this.appModel.update({ id: appId, status: AppStatus.CREATING_BACKUP }))
.then(() => ({}))
.then(() => ({ }))
}
async stopAppBackup(appId: string): Promise<Unit> {
async stopAppBackup (appId: string): Promise<Unit> {
return this.authRequest<ReqRes.PostAppBackupStopRes>({ method: Method.POST, url: `/apps/${appId}/backup/stop`, readTimeout: 60000 })
.then(() => this.appModel.update({ id: appId, status: AppStatus.STOPPED }))
.then(() => ({}))
.then(() => ({ }))
}
async restoreAppBackup(appId: string, logicalname: string, password?: string): Promise<Unit> {
async restoreAppBackup (appId: string, logicalname: string, password?: string): Promise<Unit> {
const data: ReqRes.PostAppBackupRestoreReq = {
password: password || undefined,
logicalname,
}
return this.authRequest<ReqRes.PostAppBackupRestoreRes>({ method: Method.POST, url: `/apps/${appId}/backup/restore`, data, readTimeout: 60000 })
.then(() => this.appModel.update({ id: appId, status: AppStatus.RESTORING_BACKUP }))
.then(() => ({}))
.then(() => ({ }))
}
async patchAppConfig(app: AppInstalledPreview, config: object, dryRun = false): Promise<{ breakages: DependentBreakage[] }> {
async patchAppConfig (app: AppInstalledPreview, config: object, dryRun = false): Promise<{ breakages: DependentBreakage[] }> {
const data: ReqRes.PatchAppConfigReq = {
config,
}
return this.authRequest({ method: Method.PATCH, url: `/apps/${app.id}/config${dryRunParam(dryRun, true)}`, data, readTimeout: 60000 })
}
async postConfigureDependency(dependencyId: string, dependentId: string, dryRun?: boolean): Promise<{ config: object, breakages: DependentBreakage[] }> {
async postConfigureDependency (dependencyId: string, dependentId: string, dryRun?: boolean): Promise<{ config: object, breakages: DependentBreakage[] }> {
return this.authRequest({ method: Method.POST, url: `/apps/${dependencyId}/autoconfig/${dependentId}${dryRunParam(dryRun, true)}`, readTimeout: 60000 })
}
async patchServerConfig(attr: string, value: any): Promise<Unit> {
async patchServerConfig (attr: string, value: any): Promise<Unit> {
const data: ReqRes.PatchServerConfigReq = {
value,
}
return this.authRequest({ method: Method.PATCH, url: `/${attr}`, data, readTimeout: 60000 })
.then(() => this.serverModel.update({ [attr]: value }))
.then(() => ({}))
.then(() => ({ }))
}
async wipeAppData(app: AppInstalledPreview): Promise<Unit> {
async wipeAppData (app: AppInstalledPreview): Promise<Unit> {
return this.authRequest({ method: Method.POST, url: `/apps/${app.id}/wipe`, readTimeout: 60000 }).then((res) => {
this.appModel.update({ id: app.id, status: AppStatus.NEEDS_CONFIG })
return res
})
}
async toggleAppLAN(appId: string, toggle: 'enable' | 'disable'): Promise<Unit> {
async toggleAppLAN (appId: string, toggle: 'enable' | 'disable'): Promise<Unit> {
return this.authRequest({ method: Method.POST, url: `/apps/${appId}/lan/${toggle}` })
}
async addSSHKey(sshKey: string): Promise<Unit> {
async addSSHKey (sshKey: string): Promise<Unit> {
const data: ReqRes.PostAddSSHKeyReq = {
sshKey,
}
const fingerprint = await this.authRequest<ReqRes.PostAddSSHKeyRes>({ method: Method.POST, url: `/sshKeys`, data })
this.serverModel.update({ ssh: [...this.serverModel.peek().ssh, fingerprint] })
return {}
return { }
}
async addWifi(ssid: string, password: string, country: string, connect: boolean): Promise<Unit> {
async addWifi (ssid: string, password: string, country: string, connect: boolean): Promise<Unit> {
const data: ReqRes.PostAddWifiReq = {
ssid,
password,
@@ -269,30 +269,30 @@ export class LiveApiService extends ApiService {
return this.authRequest({ method: Method.POST, url: `/wifi`, data })
}
async connectWifi(ssid: string): Promise<Unit> {
async connectWifi (ssid: string): Promise<Unit> {
return this.authRequest({ method: Method.POST, url: encodeURI(`/wifi/${ssid}`) })
}
async deleteWifi(ssid: string): Promise<Unit> {
async deleteWifi (ssid: string): Promise<Unit> {
return this.authRequest({ method: Method.DELETE, url: encodeURI(`/wifi/${ssid}`) })
}
async deleteSSHKey(fingerprint: SSHFingerprint): Promise<Unit> {
async deleteSSHKey (fingerprint: SSHFingerprint): Promise<Unit> {
await this.authRequest({ method: Method.DELETE, url: `/sshKeys/${fingerprint.hash}` })
const ssh = this.serverModel.peek().ssh
this.serverModel.update({ ssh: ssh.filter(s => s !== fingerprint) })
return {}
return { }
}
async restartServer(): Promise<Unit> {
async restartServer (): Promise<Unit> {
return this.authRequest({ method: Method.POST, url: '/restart', readTimeout: 60000 })
}
async shutdownServer(): Promise<Unit> {
async shutdownServer (): Promise<Unit> {
return this.authRequest({ method: Method.POST, url: '/shutdown', readTimeout: 60000 })
}
async serviceAction(appId: string, s: ServiceAction): Promise<ReqRes.ServiceActionResponse> {
async serviceAction (appId: string, s: ServiceAction): Promise<ReqRes.ServiceActionResponse> {
const data: ReqRes.ServiceActionRequest = {
jsonrpc: '2.0',
id: uuid.v4(),
@@ -301,11 +301,15 @@ export class LiveApiService extends ApiService {
return this.authRequest({ method: Method.POST, url: `/apps/${appId}/actions`, data, readTimeout: 300000 })
}
async refreshLAN(): Promise<Unit> {
async refreshLAN (): Promise<Unit> {
return this.authRequest({ method: Method.POST, url: '/network/lan/reset' })
}
private async authRequest<T>(opts: HttpOptions, overrides: Partial<{ version: string }> = {}): Promise<T> {
async checkV1Status (): Promise<V1Status> {
return this.http.request({ method: Method.GET, url: 'https://registry.start9labs.com/sys/status' })
}
private async authRequest<T> (opts: HttpOptions, overrides: Partial<{ version: string }> = { }): Promise<T> {
if (!this.authenticatedRequestsEnabled) throw new Error(`Authenticated requests are not enabled. Do you need to login?`)
opts.withCredentials = true
@@ -324,7 +328,7 @@ const dryRunParam = (dryRun: boolean, first: boolean) => {
return first ? `?dryrun` : `&dryrun`
}
function catchHttpStatusError(error: HttpErrorResponse): Observable<true> {
function catchHttpStatusError (error: HttpErrorResponse): Observable<true> {
if (error.error instanceof ErrorEvent) {
// A client-side or network error occurred. Handle it accordingly.
return throwError('Not Connected')

View File

@@ -4,7 +4,7 @@ import { AppAvailablePreview, AppAvailableFull, AppInstalledPreview, AppInstalle
import { S9Notification, SSHFingerprint, ServerStatus, ServerModel, DiskInfo } from '../../models/server-model'
import { pauseFor } from '../../util/misc.util'
import { ApiService, ReqRes } from './api.service'
import { ApiAppInstalledFull, ApiAppInstalledPreview, ApiServer, Unit as EmptyResponse, Unit } from './api-types'
import { ApiAppInstalledFull, ApiAppInstalledPreview, ApiServer, Unit as EmptyResponse, Unit, V1Status } from './api-types'
import { AppMetrics, AppMetricsVersioned, parseMetricsPermissive } from 'src/app/util/metrics.util'
import { mockApiAppAvailableFull, mockApiAppAvailableVersionInfo, mockApiAppInstalledFull, mockAppDependentBreakages, toInstalledPreview } from './mock-app-fixures'
import { ConfigService } from '../config.service'
@@ -273,12 +273,19 @@ export class MockApiService extends ApiService {
return mockRefreshLAN()
}
async checkV1Status (): Promise<V1Status> {
return {
status: 'instructions',
version: '1.0.0',
}
}
private hasUI (app: ApiAppInstalledPreview): boolean {
return app.lanUi || app.torUi
}
private isLaunchable (app: ApiAppInstalledPreview): boolean {
return !this.config.isConsulate &&
return !this.config.isConsulate &&
app.status === AppStatus.RUNNING &&
(
(app.torAddress && app.torUi && this.config.isTor()) ||
@@ -355,7 +362,7 @@ async function mockGetServerLogs (): Promise<ReqRes.GetServerLogsRes> {
async function mockGetAppMetrics (): Promise<ReqRes.GetAppMetricsRes> {
await pauseFor(1000)
return mockApiAppMetricsV1
return mockApiAppMetricsV1 as ReqRes.GetAppMetricsRes
}
async function mockGetAvailableAppVersionInfo (): Promise<ReqRes.GetAppAvailableVersionInfoRes> {
@@ -492,8 +499,8 @@ const mockApiNotifications: ReqRes.GetNotificationsRes = [
const mockApiServer: () => ReqRes.GetServerRes = () => ({
serverId: 'start9-mockxyzab',
name: 'Embassy:12345678',
versionInstalled: '0.2.10',
versionLatest: '0.2.11',
versionInstalled: '0.2.14',
versionLatest: '0.2.14',
status: ServerStatus.RUNNING,
alternativeRegistryUrl: 'beta-registry.start9labs.com',
welcomeAck: true,

View File

@@ -54,6 +54,8 @@ export const bitcoinI: ApiAppInstalledFull = {
versionInstalled: '0.18.1',
lanAddress: undefined,
title: 'Bitcoin Core',
licenseName: 'MIT',
licenseLink: 'https://github.com/bitcoin/bitcoin/blob/master/COPYING',
torAddress: '4acth47i6kxnvkewtm6q7ib2s3ufpo5sqbsnzjpbi7utijcltosqemad.onion',
startAlert: 'Bitcoind could take a loooooong time to start. Please be patient.',
status: AppStatus.STOPPED,
@@ -66,6 +68,7 @@ export const bitcoinI: ApiAppInstalledFull = {
restoreAlert: 'if you restore this app horrible things will happen to the people you love.',
actions: [
{ id: 'sync-chain', name: 'Sync Chain', description: 'this will sync with the chain like from Avatar', allowedStatuses: [ AppStatus.RUNNING, AppStatus.RUNNING, AppStatus.RUNNING, AppStatus.RUNNING ]},
{ id: 'single-status-action', name: 'Single Status Action', description: 'This action has only one allowed status', allowedStatuses: [ AppStatus.RUNNING ]},
{ id: 'off-sync-chain', name: 'Off Sync Chain', description: 'this will off sync with the chain like from Avatar', allowedStatuses: [ AppStatus.STOPPED ]}
],
}
@@ -146,6 +149,8 @@ export const bitcoinA: AppAvailableFull = {
id: 'bitcoind',
versionLatest: '0.19.1.1',
versionInstalled: '0.19.0',
licenseName: 'MIT',
licenseLink: 'https://github.com/bitcoin/bitcoin/blob/master/COPYING',
status: AppStatus.UNKNOWN,
title: 'Bitcoin Core',
descriptionShort: 'Bitcoin is an innovative payment network and new kind of money.',

View File

@@ -5,6 +5,7 @@ import { WizardBaker } from '../components/install-wizard/prebaked-wizards'
import { OSWelcomePage } from '../modals/os-welcome/os-welcome.page'
import { S9Server } from '../models/server-model'
import { displayEmver } from '../pipes/emver.pipe'
import { V1Status } from './api/api-types'
import { ApiService, ReqRes } from './api/api.service'
import { ConfigService } from './config.service'
import { Emver } from './emver.service'
@@ -36,6 +37,13 @@ export class StartupAlertsNotifier {
display: vl => this.displayOsUpdateCheck(vl),
hasRun: this.config.skipStartupAlerts,
}
const v1StatusUpdate: Check<V1Status> = {
name: 'v1Status',
shouldRun: s => this.shouldRunOsUpdateCheck(s),
check: () => this.v1StatusCheck(),
display: s => this.displayV1Check(s),
hasRun: this.config.skipStartupAlerts,
}
const apps: Check<boolean> = {
name: 'apps',
shouldRun: s => this.shouldRunAppsCheck(s),
@@ -43,7 +51,7 @@ export class StartupAlertsNotifier {
display: () => this.displayAppsCheck(),
hasRun: this.config.skipStartupAlerts,
}
this.checks = [welcome, osUpdate, apps]
this.checks = [welcome, osUpdate, v1StatusUpdate, apps]
}
// This takes our three checks and filters down to those that should run.
@@ -85,6 +93,10 @@ export class StartupAlertsNotifier {
return server.autoCheckUpdates
}
private async v1StatusCheck (): Promise<V1Status> {
return this.apiService.checkV1Status()
}
private async osUpdateCheck (s: Readonly<S9Server>): Promise<ReqRes.GetVersionLatestRes | undefined> {
const res = await this.apiService.getVersionLatest()
return this.osUpdateService.updateIsAvailable(s.versionInstalled, res) ? res : undefined
@@ -131,6 +143,33 @@ export class StartupAlertsNotifier {
return true
}
private async displayV1Check (s: V1Status): Promise<boolean> {
return new Promise(async resolve => {
if (s.status !== 'available') return resolve(true)
const alert = await this.alertCtrl.create({
backdropDismiss: true,
header: `EmbassyOS ${s.version} Now Available!`,
message: `Version ${s.version} introduces SSD support and a whole lot more.`,
buttons: [
{
text: 'Cancel',
role: 'cancel',
handler: () => resolve(true),
},
{
text: 'View Instructions',
handler: () => {
window.open(`https://start9.com/eos-${s.version}`, '_blank')
resolve(false)
},
},
],
})
await alert.present()
})
}
private async displayAppsCheck (): Promise<boolean> {
return new Promise(async resolve => {
const alert = await this.alertCtrl.create({