Compare commits
87 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0f9d9df83b | |||
| f3a097abdf | |||
| 1aed317818 | |||
| c5574c8409 | |||
| 78f0c9ed38 | |||
| de0228ca2a | |||
| df345e37db | |||
| 740b863750 | |||
| fa394dcd27 | |||
| 30a7dee920 | |||
| b568765c75 | |||
| e9c265e9dc | |||
| 42f692b1e0 | |||
| 14fd3df654 | |||
| 56ac9fd460 | |||
| d76a8576f4 | |||
| 289521886b | |||
| ebd21b325e | |||
| b9127dd6f8 | |||
| ddb2b29bfd | |||
| b490ebc7f6 | |||
| c8aa1eb481 | |||
| 5fdabf3e75 | |||
| 6ccffc3162 | |||
| e4b9e4e24d | |||
| 6644df6b96 | |||
| f8ae8fc4be | |||
| 84856e84e5 | |||
| 4a1e079e8f | |||
| f5a0cf0414 | |||
| f05bb111c2 | |||
| 88925ebe22 | |||
| 631c36b3ff | |||
| ee2a3d363b | |||
| 575726358c | |||
| 339c611789 | |||
| ef4d6e782a | |||
| a981244f11 | |||
| b1f60579a1 | |||
| be3b1d8f91 | |||
| dcfc32cf85 | |||
| 368eb35484 | |||
| 642186c530 | |||
| e01f74e763 | |||
| 119217f9f6 | |||
| 2812a027ea | |||
| 74373aebcf | |||
| 16300bbd89 | |||
| f2e1fca658 | |||
| 640e3921d8 | |||
| 7f1a50dbd9 | |||
| 1273802994 | |||
| 4af39ab47a | |||
| 1c97b82c45 | |||
| 5c4b2cec3c | |||
| 1e3df189d0 | |||
| a2e1617d84 | |||
| 67585a4ffa | |||
| b437610812 | |||
| f871689571 | |||
| 8e0524ae15 | |||
| f9bd2d8fb7 | |||
| bf36f1f16a | |||
| f7c09daa46 | |||
| 6b2b400292 | |||
| 8c33d565d6 | |||
| 948e44a3f6 | |||
| 3e2e38c830 | |||
| 2e53e31cdd | |||
| 64b91c2645 | |||
| e16077f40a | |||
| 15ae74b080 | |||
| 379af242ae | |||
| 9ced9f78dc | |||
| 3a449badb1 | |||
| 9996dcf13e | |||
| 7bc708d71d | |||
| d86abb98f0 | |||
| 9589d10165 | |||
| f9d77b6cd9 | |||
| ee00ac59b7 | |||
| 8501d2679a | |||
| c9943aab1f | |||
| d0692b75b0 | |||
| 4a6ed223dc | |||
| c409ee89f6 | |||
| c1d64333c5 |
@@ -1,12 +1,20 @@
|
||||
---
|
||||
kind: pipeline
|
||||
name: default
|
||||
|
||||
workspace:
|
||||
base: /drone/garage
|
||||
|
||||
volumes:
|
||||
- name: cargo_home
|
||||
temp: {}
|
||||
|
||||
steps:
|
||||
- name: restore-cache
|
||||
image: meltwater/drone-cache:dev
|
||||
volumes:
|
||||
- name: cargo_home
|
||||
path: /drone/cargo
|
||||
environment:
|
||||
AWS_ACCESS_KEY_ID:
|
||||
from_secret: cache_aws_access_key_id
|
||||
@@ -20,35 +28,51 @@ steps:
|
||||
cache_key: '{{ .Repo.Name }}_{{ checksum "Cargo.lock" }}_{{ arch }}_{{ os }}_gzip'
|
||||
region: garage
|
||||
mount:
|
||||
- '/drone/cargo'
|
||||
- 'target'
|
||||
- '/drone/cargo/registry/index'
|
||||
- '/drone/cargo/registry/cache'
|
||||
- '/drone/cargo/bin'
|
||||
- '/drone/cargo/git/db'
|
||||
path_style: true
|
||||
endpoint: https://garage.deuxfleurs.fr
|
||||
when:
|
||||
branch:
|
||||
- nonexistent_skip_this_step
|
||||
|
||||
- name: build
|
||||
image: rust:buster
|
||||
- name: code quality
|
||||
image: superboum/garage_builder_amd64:4
|
||||
volumes:
|
||||
- name: cargo_home
|
||||
path: /drone/cargo
|
||||
environment:
|
||||
CARGO_HOME: /drone/cargo
|
||||
commands:
|
||||
- cargo fmt -- --check
|
||||
- cargo clippy -- --deny warnings
|
||||
|
||||
- name: build
|
||||
image: superboum/garage_builder_amd64:4
|
||||
volumes:
|
||||
- name: cargo_home
|
||||
path: /drone/cargo
|
||||
environment:
|
||||
CARGO_HOME: /drone/cargo
|
||||
commands:
|
||||
- apt-get update
|
||||
- apt-get install --yes libsodium-dev
|
||||
- pwd
|
||||
- cargo build
|
||||
|
||||
- name: cargo-test
|
||||
image: rust:buster
|
||||
image: superboum/garage_builder_amd64:4
|
||||
volumes:
|
||||
- name: cargo_home
|
||||
path: /drone/cargo
|
||||
environment:
|
||||
CARGO_HOME: /drone/cargo
|
||||
commands:
|
||||
- apt-get update
|
||||
- apt-get install --yes libsodium-dev
|
||||
- cargo test
|
||||
|
||||
- name: rebuild-cache
|
||||
image: meltwater/drone-cache:dev
|
||||
volumes:
|
||||
- name: cargo_home
|
||||
path: /drone/cargo
|
||||
environment:
|
||||
AWS_ACCESS_KEY_ID:
|
||||
from_secret: cache_aws_access_key_id
|
||||
@@ -62,22 +86,22 @@ steps:
|
||||
cache_key: '{{ .Repo.Name }}_{{ checksum "Cargo.lock" }}_{{ arch }}_{{ os }}_gzip'
|
||||
region: garage
|
||||
mount:
|
||||
- '/drone/cargo'
|
||||
- 'target'
|
||||
- '/drone/cargo/registry/index'
|
||||
- '/drone/cargo/registry/cache'
|
||||
- '/drone/cargo/git/db'
|
||||
- '/drone/cargo/bin'
|
||||
path_style: true
|
||||
endpoint: https://garage.deuxfleurs.fr
|
||||
when:
|
||||
branch:
|
||||
- nonexistent_skip_this_step
|
||||
|
||||
- name: smoke-test
|
||||
image: rust:buster
|
||||
image: superboum/garage_builder_amd64:4
|
||||
volumes:
|
||||
- name: cargo_home
|
||||
path: /drone/cargo
|
||||
environment:
|
||||
CARGO_HOME: /drone/cargo
|
||||
commands:
|
||||
- apt-get update
|
||||
- apt-get install --yes libsodium-dev awscli python-pip
|
||||
- pip install s3cmd
|
||||
- ./script/test-smoke.sh || (cat /tmp/garage.log; false)
|
||||
|
||||
---
|
||||
@@ -106,6 +130,15 @@ steps:
|
||||
endpoint: https://garage.deuxfleurs.fr
|
||||
region: garage
|
||||
when:
|
||||
event:
|
||||
- push
|
||||
branch:
|
||||
- main
|
||||
repo:
|
||||
- Deuxfleurs/garage
|
||||
|
||||
---
|
||||
kind: signature
|
||||
hmac: f0f2e947c8aa8bc5b83d25b4da22f3eb711b3fe1cc80ead4f93428dbd3d44164
|
||||
|
||||
...
|
||||
|
||||
@@ -350,16 +350,16 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "garage"
|
||||
version = "0.2.1"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures",
|
||||
"futures-util",
|
||||
"garage_api",
|
||||
"garage_model",
|
||||
"garage_rpc",
|
||||
"garage_rpc 0.3.0",
|
||||
"garage_table",
|
||||
"garage_util",
|
||||
"garage_util 0.3.0",
|
||||
"garage_web",
|
||||
"git-version",
|
||||
"hex",
|
||||
@@ -376,7 +376,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "garage_api"
|
||||
version = "0.2.1"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"bytes",
|
||||
@@ -387,7 +387,7 @@ dependencies = [
|
||||
"futures-util",
|
||||
"garage_model",
|
||||
"garage_table",
|
||||
"garage_util",
|
||||
"garage_util 0.3.0",
|
||||
"hex",
|
||||
"hmac",
|
||||
"http",
|
||||
@@ -397,7 +397,9 @@ dependencies = [
|
||||
"log",
|
||||
"md-5",
|
||||
"percent-encoding",
|
||||
"quick-xml",
|
||||
"roxmltree",
|
||||
"serde",
|
||||
"sha2",
|
||||
"tokio",
|
||||
"url",
|
||||
@@ -405,14 +407,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "garage_model"
|
||||
version = "0.2.1"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"futures",
|
||||
"futures-util",
|
||||
"garage_rpc",
|
||||
"garage_rpc 0.3.0",
|
||||
"garage_table",
|
||||
"garage_util",
|
||||
"garage_util 0.3.0",
|
||||
"hex",
|
||||
"log",
|
||||
"rand",
|
||||
@@ -426,12 +428,40 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "garage_rpc"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48c182633cebe4abed9594afb14770fc45402513765d38a4b19659ae0ccb2a2f"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"bytes",
|
||||
"futures",
|
||||
"futures-util",
|
||||
"garage_util",
|
||||
"garage_util 0.2.1",
|
||||
"gethostname",
|
||||
"hex",
|
||||
"http",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"log",
|
||||
"rmp-serde",
|
||||
"rustls",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-stream",
|
||||
"webpki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "garage_rpc"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"bytes",
|
||||
"futures",
|
||||
"futures-util",
|
||||
"garage_rpc 0.2.1",
|
||||
"garage_util 0.3.0",
|
||||
"gethostname",
|
||||
"hex",
|
||||
"http",
|
||||
@@ -450,13 +480,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "garage_table"
|
||||
version = "0.2.1"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures",
|
||||
"futures-util",
|
||||
"garage_rpc",
|
||||
"garage_util",
|
||||
"garage_rpc 0.3.0",
|
||||
"garage_util 0.3.0",
|
||||
"hexdump",
|
||||
"log",
|
||||
"rand",
|
||||
@@ -470,6 +500,33 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "garage_util"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aef76d3779e406a16fdcaffe8d86b8ae2943a549d2b33f2c20930838764464c0"
|
||||
dependencies = [
|
||||
"blake2",
|
||||
"chrono",
|
||||
"err-derive",
|
||||
"futures",
|
||||
"hex",
|
||||
"http",
|
||||
"hyper",
|
||||
"log",
|
||||
"rand",
|
||||
"rmp-serde",
|
||||
"rustls",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"sled",
|
||||
"tokio",
|
||||
"toml",
|
||||
"webpki",
|
||||
"xxhash-rust",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "garage_util"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"blake2",
|
||||
"chrono",
|
||||
@@ -494,14 +551,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "garage_web"
|
||||
version = "0.2.1"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"err-derive",
|
||||
"futures",
|
||||
"garage_api",
|
||||
"garage_model",
|
||||
"garage_table",
|
||||
"garage_util",
|
||||
"garage_util 0.3.0",
|
||||
"http",
|
||||
"hyper",
|
||||
"idna",
|
||||
@@ -1034,6 +1091,16 @@ version = "1.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0452695941410a58c8ce4391707ba9bad26a247173bd9886a05a5e8a8babec75"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.9"
|
||||
|
||||
@@ -1,20 +1,10 @@
|
||||
BIN=target/release/garage
|
||||
DOCKER=lxpz/garage_amd64
|
||||
.PHONY: doc
|
||||
|
||||
all:
|
||||
clear; RUSTFLAGS="-C link-arg=-fuse-ld=lld -C target-cpu=x86-64 -C target-feature=+sse2" cargo build --no-default-features
|
||||
clear; cargo build
|
||||
|
||||
$(BIN):
|
||||
doc:
|
||||
cd doc/book; mdbook build
|
||||
|
||||
release:
|
||||
RUSTFLAGS="-C link-arg=-fuse-ld=lld -C target-cpu=x86-64 -C target-feature=+sse2" cargo build --release --no-default-features
|
||||
|
||||
$(BIN).stripped: $(BIN)
|
||||
cp $^ $@
|
||||
strip $@
|
||||
|
||||
docker: $(BIN).stripped
|
||||
docker pull archlinux:latest
|
||||
docker build -t $(DOCKER):$(TAG) .
|
||||
docker push $(DOCKER):$(TAG)
|
||||
docker tag $(DOCKER):$(TAG) $(DOCKER):latest
|
||||
docker push $(DOCKER):latest
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Garage [](https://drone.deuxfleurs.fr/Deuxfleurs/garage)
|
||||
Garage [](https://drone.deuxfleurs.fr/Deuxfleurs/garage)
|
||||
===
|
||||
|
||||
<p align="center" style="text-align:center;">
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
block_size = 1048576 # objects are split in blocks of maximum this number of bytes
|
||||
|
||||
metadata_dir = "/tmp/garage-meta"
|
||||
data_dir = "/tmp/garage-data"
|
||||
|
||||
rpc_bind_addr = "[::]:3901" # the port other Garage nodes will use to talk to this node
|
||||
|
||||
bootstrap_peers = []
|
||||
|
||||
max_concurrent_rpc_requests = 12
|
||||
data_replication_factor = 3
|
||||
meta_replication_factor = 3
|
||||
meta_epidemic_fanout = 3
|
||||
|
||||
[s3_api]
|
||||
api_bind_addr = "[::1]:3900" # the S3 API port, HTTP without TLS. Add a reverse proxy for the TLS part.
|
||||
s3_region = "garage" # set this to anything. S3 API calls will fail if they are not made against the region set here.
|
||||
|
||||
[s3_web]
|
||||
bind_addr = "[::1]:3902"
|
||||
root_domain = ".garage.tld"
|
||||
index = "index.html"
|
||||
@@ -2,22 +2,22 @@
|
||||
|
||||
[The Garage Data Store](./intro.md)
|
||||
|
||||
- [Getting Started](./getting_started/index.md)
|
||||
- [Get a binary](./getting_started/binary.md)
|
||||
- [Configure the daemon](./getting_started/daemon.md)
|
||||
- [Control the daemon](./getting_started/control.md)
|
||||
- [Configure a cluster](./getting_started/cluster.md)
|
||||
- [Create buckets and keys](./getting_started/bucket.md)
|
||||
- [Handle files](./getting_started/files.md)
|
||||
- [Quick start](./quick_start/index.md)
|
||||
|
||||
- [Cookbook](./cookbook/index.md)
|
||||
- [Host a website](./cookbook/website.md)
|
||||
- [Deploying Garage](./cookbook/real_world.md)
|
||||
- [Configuring S3 clients](./cookbook/clients.md)
|
||||
- [Hosting a website](./cookbook/website.md)
|
||||
- [Recovering from failures](./cookbook/recovering.md)
|
||||
- [Building from source](./cookbook/from_source.md)
|
||||
- [Starting with Systemd](./cookbook/systemd.md)
|
||||
- [Integrate as a media backend]()
|
||||
- [Operate a cluster]()
|
||||
|
||||
- [Reference Manual](./reference_manual/index.md)
|
||||
- [Garage CLI]()
|
||||
- [S3 API](./reference_manual/s3_compatibility.md)
|
||||
- [Garage configuration file](./reference_manual/configuration.md)
|
||||
- [Garage CLI](./reference_manual/cli.md)
|
||||
- [S3 compatibility status](./reference_manual/s3_compatibility.md)
|
||||
|
||||
- [Design](./design/index.md)
|
||||
- [Related Work](./design/related_work.md)
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
# Configuring S3 clients to interact with Garage
|
||||
|
||||
To configure an S3 client to interact with Garage, you will need the following
|
||||
parameters:
|
||||
|
||||
- An **API endpoint**: this corresponds to the HTTP or HTTPS address
|
||||
used to contact the Garage server. When runing Garage locally this will usually
|
||||
be `http://127.0.0.1:3900`. In a real-world setting, you would usually have a reverse-proxy
|
||||
that adds TLS support and makes your Garage server available under a public hostname
|
||||
such as `https://garage.example.com`.
|
||||
|
||||
- An **API access key** and its associated **secret key**. These usually look something
|
||||
like this: `GK3515373e4c851ebaad366558` (access key),
|
||||
`7d37d093435a41f2aab8f13c19ba067d9776c90215f56614adad6ece597dbb34` (secret key).
|
||||
These keys are created and managed using the `garage` CLI, as explained in the
|
||||
[quick start](../quick_start/index.md) guide.
|
||||
|
||||
Most S3 clients can be configured easily with these parameters,
|
||||
provided that you follow the following guidelines:
|
||||
|
||||
- **Force path style:** Garage does not support DNS-style buckets, which are now by default
|
||||
on Amazon S3. Instead, Garage uses the legacy path-style bucket addressing.
|
||||
Remember to configure your client to acknowledge this fact.
|
||||
|
||||
- **Configuring the S3 region:** Garage requires your client to talk to the correct "S3 region",
|
||||
which is set in the configuration file. This is often set just to `garage`.
|
||||
If this is not configured explicitly, clients usually try to talk to region `us-east-1`.
|
||||
Garage should normally redirect your client to the correct region,
|
||||
but in case your client does not support this you might have to configure it manually.
|
||||
|
||||
We will now provide example configurations for the most common S3 clients.
|
||||
|
||||
## AWS CLI
|
||||
|
||||
Export the following environment variables:
|
||||
|
||||
```bash
|
||||
export AWS_ACCESS_KEY_ID=<access key>
|
||||
export AWS_SECRET_ACCESS_KEY=<secret key>
|
||||
export AWS_DEFAULT_REGION=<region>
|
||||
```
|
||||
|
||||
Now invoke `aws` as follows:
|
||||
|
||||
```bash
|
||||
aws --endpoint-url <endpoint> s3 <command...>
|
||||
```
|
||||
|
||||
For instance: `aws --endpoint-url http://127.0.0.1:3901 s3 ls s3://my-bucket/`.
|
||||
|
||||
## Minio client
|
||||
|
||||
Use the following command to set an "alias", i.e. define a new S3 server to be
|
||||
used by the Minio client:
|
||||
|
||||
```bash
|
||||
mc alias set \
|
||||
garage \
|
||||
<endpoint> \
|
||||
<access key> \
|
||||
<secret key> \
|
||||
--api S3v4
|
||||
```
|
||||
|
||||
Remember that `mc` is sometimes called `mcli` (such as on Arch Linux), to avoid conflicts
|
||||
with the Midnight Commander.
|
||||
|
||||
|
||||
## `rclone`
|
||||
|
||||
`rclone` can be configured using the interactive assistant invoked using `rclone configure`.
|
||||
|
||||
You can also configure `rclone` by writing directly its configuration file.
|
||||
Here is a template `rclone.ini` configuration file:
|
||||
|
||||
```ini
|
||||
[garage]
|
||||
type = s3
|
||||
provider = Other
|
||||
env_auth = false
|
||||
access_key_id = <access key>
|
||||
secret_access_key = <secret key>
|
||||
region = <region>
|
||||
endpoint = <endpoint>
|
||||
force_path_style = true
|
||||
acl = private
|
||||
bucket_acl = private
|
||||
```
|
||||
|
||||
## Cyberduck
|
||||
|
||||
TODO
|
||||
|
||||
## `s3cmd`
|
||||
|
||||
Here is a template for the `s3cmd.cfg` file to talk with Garage:
|
||||
|
||||
```ini
|
||||
[default]
|
||||
access_key = <access key>
|
||||
secret_key = <secret key>
|
||||
host_base = <endpoint without http(s)://>
|
||||
host_bucket = <same as host_base>
|
||||
use_https = False | True
|
||||
```
|
||||
@@ -0,0 +1,51 @@
|
||||
# Compiling Garage from source
|
||||
|
||||
|
||||
Garage is a standard Rust project.
|
||||
First, you need `rust` and `cargo`.
|
||||
For instance on Debian:
|
||||
|
||||
```bash
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y rustc cargo
|
||||
```
|
||||
|
||||
You can also use [Rustup](https://rustup.rs/) to setup a Rust toolchain easily.
|
||||
|
||||
## Using source from `crates.io`
|
||||
|
||||
Garage's source code is published on `crates.io`, Rust's official package repository.
|
||||
This means you can simply ask `cargo` to download and build this source code for you:
|
||||
|
||||
```bash
|
||||
cargo install garage
|
||||
```
|
||||
|
||||
That's all, `garage` should be in `$HOME/.cargo/bin`.
|
||||
|
||||
You can add this folder to your `$PATH` or copy the binary somewhere else on your system.
|
||||
For instance:
|
||||
|
||||
```bash
|
||||
sudo cp $HOME/.cargo/bin/garage /usr/local/bin/garage
|
||||
```
|
||||
|
||||
|
||||
## Using source from the Gitea repository
|
||||
|
||||
The primary location for Garage's source code is the
|
||||
[Gitea repository](https://git.deuxfleurs.fr/Deuxfleurs/garage).
|
||||
|
||||
Clone the repository and build Garage with the following commands:
|
||||
|
||||
```bash
|
||||
git clone https://git.deuxfleurs.fr/Deuxfleurs/garage.git
|
||||
cd garage
|
||||
cargo build
|
||||
```
|
||||
|
||||
Be careful, as this will make a debug build of Garage, which will be extremely slow!
|
||||
To make a release build, invoke `cargo build --release` (this takes much longer).
|
||||
|
||||
The binaries built this way are found in `target/{debug,release}/garage`.
|
||||
|
||||
@@ -2,4 +2,24 @@
|
||||
|
||||
A cookbook, when you cook, is a collection of recipes.
|
||||
Similarly, Garage's cookbook contains a collection of recipes that are known to works well!
|
||||
This chapter could also be referred as "Tutorials" or "Best practises".
|
||||
This chapter could also be referred as "Tutorials" or "Best practices".
|
||||
|
||||
- **[Deploying Garage](real_world.md):** This page will walk you through all of the necessary
|
||||
steps to deploy Garage in a real-world setting.
|
||||
|
||||
- **[Configuring S3 clients](clients.md):** This page will explain how to configure
|
||||
popular S3 clients to interact with a Garage server.
|
||||
|
||||
- **[Hosting a website](website.md):** This page explains how to use Garage
|
||||
to host a static website.
|
||||
|
||||
- **[Recovering from failures](recovering.md):** Garage's first selling point is resilience
|
||||
to hardware failures. This section explains how to recover from such a failure in the
|
||||
best possible way.
|
||||
|
||||
- **[Building from source](from_source.md):** This page explains how to build Garage from
|
||||
source in case a binary is not provided for your architecture, or if you want to
|
||||
hack with us!
|
||||
|
||||
- **[Starting with Systemd](from_source.md):** This page explains how to run Garage
|
||||
as a Systemd service (instead of as a Docker container).
|
||||
|
||||
@@ -0,0 +1,305 @@
|
||||
# Deploying Garage on a real-world cluster
|
||||
|
||||
To run Garage in cluster mode, we recommend having at least 3 nodes.
|
||||
This will allow you to setup Garage for three-way replication of your data,
|
||||
the safest and most available mode proposed by Garage.
|
||||
|
||||
We recommend first following the [quick start guide](../quick_start/index.md) in order
|
||||
to get familiar with Garage's command line and usage patterns.
|
||||
|
||||
|
||||
|
||||
## Prerequisites
|
||||
|
||||
To run a real-world deployment, make sure you the following conditions are met:
|
||||
|
||||
- You have at least three machines with sufficient storage space available.
|
||||
|
||||
- Each machine has a public IP address which is reachable by other machines.
|
||||
Running behind a NAT is possible, but having several Garage nodes behind a single NAT
|
||||
is slightly more involved as each will have to have a different RPC port number
|
||||
(the local port number of a node must be the same as the port number exposed publicly
|
||||
by the NAT).
|
||||
|
||||
- Ideally, each machine should have a SSD available in addition to the HDD you are dedicating
|
||||
to Garage. This will allow for faster access to metadata and has the potential
|
||||
to drastically reduce Garage's response times.
|
||||
|
||||
- This guide will assume you are using Docker containers to deploy Garage on each node.
|
||||
Garage can also be run independently, for instance as a [Systemd service](systemd.md).
|
||||
You can also use an orchestrator such as Nomad or Kubernetes to automatically manage
|
||||
Docker containers on a fleet of nodes.
|
||||
|
||||
Before deploying Garage on your infrastructure, you must inventory your machines.
|
||||
For our example, we will suppose the following infrastructure with IPv6 connectivity:
|
||||
|
||||
| Location | Name | IP Address | Disk Space |
|
||||
|----------|---------|------------|------------|
|
||||
| Paris | Mercury | fc00:1::1 | 1 To |
|
||||
| Paris | Venus | fc00:1::2 | 2 To |
|
||||
| London | Earth | fc00:B::1 | 2 To |
|
||||
| Brussels | Mars | fc00:F::1 | 1.5 To |
|
||||
|
||||
|
||||
|
||||
## Get a Docker image
|
||||
|
||||
Our docker image is currently named `lxpz/garage_amd64` and is stored on the [Docker Hub](https://hub.docker.com/r/lxpz/garage_amd64/tags?page=1&ordering=last_updated).
|
||||
We encourage you to use a fixed tag (eg. `v0.3.0`) and not the `latest` tag.
|
||||
For this example, we will use the latest published version at the time of the writing which is `v0.3.0` but it's up to you
|
||||
to check [the most recent versions on the Docker Hub](https://hub.docker.com/r/lxpz/garage_amd64/tags?page=1&ordering=last_updated).
|
||||
|
||||
For example:
|
||||
|
||||
```
|
||||
sudo docker pull lxpz/garage_amd64:v0.3.0
|
||||
```
|
||||
|
||||
|
||||
## Generating TLS certificates
|
||||
|
||||
You first need to generate TLS certificates to encrypt traffic between Garage nodes
|
||||
(reffered to as RPC traffic).
|
||||
|
||||
To generate your TLS certificates, run on your machine:
|
||||
|
||||
```
|
||||
wget https://git.deuxfleurs.fr/Deuxfleurs/garage/raw/branch/main/genkeys.sh
|
||||
chmod +x genkeys.sh
|
||||
./genkeys.sh
|
||||
```
|
||||
|
||||
It will creates a folder named `pki/` containing the keys that you will used for the cluster.
|
||||
These files will have to be copied to all of your cluster nodes, as explained below.
|
||||
|
||||
|
||||
## Deploying and configuring Garage
|
||||
|
||||
On each machine, we will have a similar setup,
|
||||
especially you must consider the following folders/files:
|
||||
|
||||
- `/etc/garage/garage.toml`: Garage daemon's configuration (see below)
|
||||
|
||||
- `/etc/garage/pki/`: Folder containing Garage certificates,
|
||||
must be generated on your computer and copied on the servers.
|
||||
Only the files `garage-ca.crt`, `garage.crt` and `garage.key` are necessary.
|
||||
|
||||
- `/var/lib/garage/meta/`: Folder containing Garage's metadata,
|
||||
put this folder on a SSD if possible
|
||||
|
||||
- `/var/lib/garage/data/`: Folder containing Garage's data,
|
||||
this folder will be your main data storage and must be on a large storage (e.g. large HDD)
|
||||
|
||||
|
||||
A valid `/etc/garage/garage.toml` for our cluster would be:
|
||||
|
||||
```toml
|
||||
metadata_dir = "/var/lib/garage/meta"
|
||||
data_dir = "/var/lib/garage/data"
|
||||
|
||||
replication_mode = "3"
|
||||
|
||||
rpc_bind_addr = "[::]:3901"
|
||||
|
||||
bootstrap_peers = [
|
||||
"[fc00:1::1]:3901",
|
||||
"[fc00:1::2]:3901",
|
||||
"[fc00:B::1]:3901",
|
||||
"[fc00:F::1]:3901",
|
||||
]
|
||||
|
||||
[rpc_tls]
|
||||
ca_cert = "/etc/garage/pki/garage-ca.crt"
|
||||
node_cert = "/etc/garage/pki/garage.crt"
|
||||
node_key = "/etc/garage/pki/garage.key"
|
||||
|
||||
[s3_api]
|
||||
s3_region = "garage"
|
||||
api_bind_addr = "[::]:3900"
|
||||
|
||||
[s3_web]
|
||||
bind_addr = "[::]:3902"
|
||||
root_domain = ".web.garage"
|
||||
index = "index.html"
|
||||
```
|
||||
|
||||
Please make sure to change `bootstrap_peers` to **your** IP addresses!
|
||||
|
||||
Check the [configuration file reference documentation](../reference_manual/configuration.md)
|
||||
to learn more about all available configuration options.
|
||||
|
||||
|
||||
## Starting Garage using Docker
|
||||
|
||||
On each machine, you can run the daemon with:
|
||||
|
||||
```bash
|
||||
docker run \
|
||||
-d \
|
||||
--name garaged \
|
||||
--restart always \
|
||||
--network host \
|
||||
-v /etc/garage/pki:/etc/garage/pki \
|
||||
-v /etc/garage/garage.toml:/garage/garage.toml \
|
||||
-v /var/lib/garage/meta:/var/lib/garage/meta \
|
||||
-v /var/lib/garage/data:/var/lib/garage/data \
|
||||
lxpz/garage_amd64:v0.3.0
|
||||
```
|
||||
|
||||
It should be restarted automatically at each reboot.
|
||||
Please note that we use host networking as otherwise Docker containers
|
||||
can not communicate with IPv6.
|
||||
|
||||
Upgrading between Garage versions should be supported transparently,
|
||||
but please check the relase notes before doing so!
|
||||
To upgrade, simply stop and remove this container and
|
||||
start again the command with a new version of Garage.
|
||||
|
||||
|
||||
## Controling the daemon
|
||||
|
||||
The `garage` binary has two purposes:
|
||||
- it acts as a daemon when launched with `garage server ...`
|
||||
- it acts as a control tool for the daemon when launched with any other command
|
||||
|
||||
In this section, we will see how to use the `garage` binary as a control tool for the daemon we just started.
|
||||
You first need to get a shell having access to this binary. For instance, enter the Docker container with:
|
||||
|
||||
```bash
|
||||
sudo docker exec -ti garaged bash
|
||||
```
|
||||
|
||||
You will now have a shell where the Garage binary is available as `/garage/garage`
|
||||
|
||||
*You can also install the binary on your machine to remotely control the cluster.*
|
||||
|
||||
## Talk to the daemon and create an alias
|
||||
|
||||
`garage` requires 4 options to talk with the daemon:
|
||||
|
||||
```
|
||||
--ca-cert <ca-cert>
|
||||
--client-cert <client-cert>
|
||||
--client-key <client-key>
|
||||
-h, --rpc-host <rpc-host>
|
||||
```
|
||||
|
||||
The 3 first ones are certificates and keys needed by TLS, the last one is simply the address of Garage's RPC endpoint.
|
||||
|
||||
If you are invoking `garage` from a server node directly, you do not need to set `--rpc-host`
|
||||
as the default value `127.0.0.1:3901` will allow it to contact Garage correctly.
|
||||
|
||||
To avoid typing the 3 first options each time we want to run a command,
|
||||
you can use the following alias:
|
||||
|
||||
```bash
|
||||
alias garagectl='/garage/garage \
|
||||
--ca-cert /etc/garage/pki/garage-ca.crt \
|
||||
--client-cert /etc/garage/pki/garage.crt \
|
||||
--client-key /etc/garage/pki/garage.key'
|
||||
```
|
||||
|
||||
You can now use all of the commands presented in the [quick start guide](../quick_start/index.md),
|
||||
simply replace occurences of `garage` by `garagectl`.
|
||||
|
||||
#### Test the alias
|
||||
|
||||
You can test your alias by running a simple command such as:
|
||||
|
||||
```
|
||||
garagectl status
|
||||
```
|
||||
|
||||
You should get something like that as result:
|
||||
|
||||
```
|
||||
Healthy nodes:
|
||||
8781c50c410a41b3… Mercury [fc00:1::1]:3901 UNCONFIGURED/REMOVED
|
||||
2a638ed6c775b69a… Venus [fc00:1::2]:3901 UNCONFIGURED/REMOVED
|
||||
68143d720f20c89d… Earth [fc00:B::1]:3901 UNCONFIGURED/REMOVED
|
||||
212f7572f0c89da9… Mars [fc00:F::1]:3901 UNCONFIGURED/REMOVED
|
||||
```
|
||||
|
||||
|
||||
## Configuring a cluster
|
||||
|
||||
We will now inform Garage of the disk space available on each node of the cluster
|
||||
as well as the zone (e.g. datacenter) in which each machine is located.
|
||||
|
||||
For our example, we will suppose we have the following infrastructure (Capacity, Identifier and Datacenter are specific values to Garage described in the following):
|
||||
|
||||
| Location | Name | Disk Space | `Capacity` | `Identifier` | `Zone` |
|
||||
|----------|---------|------------|------------|--------------|--------------|
|
||||
| Paris | Mercury | 1 To | `2` | `8781c5` | `par1` |
|
||||
| Paris | Venus | 2 To | `4` | `2a638e` | `par1` |
|
||||
| London | Earth | 2 To | `4` | `68143d` | `lon1` |
|
||||
| Brussels | Mars | 1.5 To | `3` | `212f75` | `bru1` |
|
||||
|
||||
#### Node identifiers
|
||||
|
||||
After its first launch, Garage generates a random and unique identifier for each nodes, such as:
|
||||
|
||||
```
|
||||
8781c50c410a41b363167e9d49cc468b6b9e4449b6577b64f15a249a149bdcbc
|
||||
```
|
||||
|
||||
Often a shorter form can be used, containing only the beginning of the identifier, like `8781c5`,
|
||||
which identifies the server "Mercury" located in "Paris" according to our previous table.
|
||||
|
||||
The most simple way to match an identifier to a node is to run:
|
||||
|
||||
```
|
||||
garagectl status
|
||||
```
|
||||
|
||||
It will display the IP address associated with each node;
|
||||
from the IP address you will be able to recognize the node.
|
||||
|
||||
#### Zones
|
||||
|
||||
Zones are simply a user-chosen identifier that identify a group of server that are grouped together logically.
|
||||
It is up to the system administrator deploying Garage to identify what does "grouped together" means.
|
||||
|
||||
In most cases, a zone will correspond to a geographical location (i.e. a datacenter).
|
||||
Behind the scene, Garage will use zone definition to try to store the same data on different zones,
|
||||
in order to provide high availability despite failure of a zone.
|
||||
|
||||
#### Capacity
|
||||
|
||||
Garage reasons on an abstract metric about disk storage that is named the *capacity* of a node.
|
||||
The capacity configured in Garage must be proportional to the disk space dedicated to the node.
|
||||
Due to the way the Garage allocation algorithm works, capacity values must
|
||||
be **integers**, and must be **as small as possible**, for instance with
|
||||
1 representing the size of your smallest server.
|
||||
|
||||
Here we chose that 1 unit of capacity = 0.5 To, so that we can express servers of size
|
||||
1 To and 2 To, as wel as the intermediate size 1.5 To, with the integer values 2, 4 and
|
||||
3 respectively (see table above).
|
||||
|
||||
Note that the amount of data stored by Garage on each server may not be strictly proportional to
|
||||
its capacity value, as Garage will priorize having 3 copies of data in different zones,
|
||||
even if this means that capacities will not be strictly respected. For example in our above examples,
|
||||
nodes Earth and Mars will always store a copy of everything each, and the third copy will
|
||||
have 66% chance of being stored by Venus and 33% chance of being stored by Mercury.
|
||||
|
||||
#### Injecting the topology
|
||||
|
||||
Given the information above, we will configure our cluster as follow:
|
||||
|
||||
```
|
||||
garagectl node configure -z par1 -c 2 -t mercury 8781c5
|
||||
garagectl node configure -z par1 -c 4 -t venus 2a638e
|
||||
garagectl node configure -z lon1 -c 4 -t earth 68143d
|
||||
garagectl node configure -z bru1 -c 3 -t mars 212f75
|
||||
```
|
||||
|
||||
|
||||
## Using your Garage cluster
|
||||
|
||||
Creating buckets and managing keys is done using the `garagectl` CLI,
|
||||
and is covered in the [quick start guide](../quick_start/index.md).
|
||||
Remember also that the CLI is self-documented thanks to the `--help` flag and
|
||||
the `help` subcommand (e.g. `garage help`, `garage key --help`).
|
||||
|
||||
Configuring an S3 client to interact with Garage is covered
|
||||
[in the next section](clients.md).
|
||||
@@ -0,0 +1,103 @@
|
||||
# Recovering from failures
|
||||
|
||||
Garage is meant to work on old, second-hand hardware.
|
||||
In particular, this makes it likely that some of your drives will fail, and some manual intervention will be needed.
|
||||
Fear not! For Garage is fully equipped to handle drive failures, in most common cases.
|
||||
|
||||
## A note on availability of Garage
|
||||
|
||||
With nodes dispersed in 3 zones or more, here are the guarantees Garage provides with the 3-way replication strategy (3 copies of all data, which is the recommended replication mode):
|
||||
|
||||
- The cluster remains fully functional as long as the machines that fail are in only one zone. This includes a whole zone going down due to power/Internet outage.
|
||||
- No data is lost as long as the machines that fail are in at most two zones.
|
||||
|
||||
Of course this only works if your Garage nodes are correctly configured to be aware of the zone in which they are located.
|
||||
Make sure this is the case using `garage status` to check on the state of your cluster's configuration.
|
||||
|
||||
In case of temporarily disconnected nodes, Garage should automatically re-synchronize
|
||||
when the nodes come back up. This guide will deal with recovering from disk failures
|
||||
that caused the loss of the data of a node.
|
||||
|
||||
|
||||
## First option: removing a node
|
||||
|
||||
If you don't have spare parts (HDD, SDD) to replace the failed component, and if there are enough remaining nodes in your cluster
|
||||
(at least 3), you can simply remove the failed node from Garage's configuration.
|
||||
Note that if you **do** intend to replace the failed parts by new ones, using this method followed by adding back the node is **not recommended** (although it should work),
|
||||
and you should instead use one of the methods detailed in the next sections.
|
||||
|
||||
Removing a node is done with the following command:
|
||||
|
||||
```
|
||||
garage node remove --yes <node_id>
|
||||
```
|
||||
|
||||
(you can get the `node_id` of the failed node by running `garage status`)
|
||||
|
||||
This will repartition the data and ensure that 3 copies of everything are present on the nodes that remain available.
|
||||
|
||||
|
||||
|
||||
## Replacement scenario 1: only data is lost, metadata is fine
|
||||
|
||||
The recommended deployment for Garage uses an SSD to store metadata, and an HDD to store blocks of data.
|
||||
In the case where only a single HDD crashes, the blocks of data are lost but the metadata is still fine.
|
||||
|
||||
This is very easy to recover by setting up a new HDD to replace the failed one.
|
||||
The node does not need to be fully replaced and the configuration doesn't need to change.
|
||||
We just need to tell Garage to get back all the data blocks and store them on the new HDD.
|
||||
|
||||
First, set up a new HDD to store Garage's data directory on the failed node, and restart Garage using
|
||||
the existing configuration. Then, run:
|
||||
|
||||
```
|
||||
garage repair -a --yes blocks
|
||||
```
|
||||
|
||||
This will re-synchronize blocks of data that are missing to the new HDD, reading them from copies located on other nodes.
|
||||
|
||||
You can check on the advancement of this process by doing the following command:
|
||||
|
||||
```
|
||||
garage stats -a
|
||||
```
|
||||
|
||||
Look out for the following output:
|
||||
|
||||
```
|
||||
Block manager stats:
|
||||
resync queue length: 26541
|
||||
```
|
||||
|
||||
This indicates that one of the Garage node is in the process of retrieving missing data from other nodes.
|
||||
This number decreases to zero when the node is fully synchronized.
|
||||
|
||||
|
||||
## Replacement scenario 2: metadata (and possibly data) is lost
|
||||
|
||||
This scenario covers the case where a full node fails, i.e. both the metadata directory and
|
||||
the data directory are lost, as well as the case where only the metadata directory is lost.
|
||||
|
||||
To replace the lost node, we will start from an empty metadata directory, which means
|
||||
Garage will generate a new node ID for the replacement node.
|
||||
We will thus need to remove the previous node ID from Garage's configuration and replace it by the ID of the new node.
|
||||
|
||||
If your data directory is stored on a separate drive and is still fine, you can keep it, but it is not necessary to do so.
|
||||
In all cases, the data will be rebalanced and the replacement node will not store the same pieces of data
|
||||
as were originally stored on the one that failed. So if you keep the data files, the rebalancing
|
||||
might be faster but most of the pieces will be deleted anyway from the disk and replaced by other ones.
|
||||
|
||||
First, set up a new drive to store the metadata directory for the replacement node (a SSD is recommended),
|
||||
and for the data directory if necessary. You can then start Garage on the new node.
|
||||
The restarted node should generate a new node ID, and it should be shown as `NOT CONFIGURED` in `garage status`.
|
||||
The ID of the lost node should be shown in `garage status` in the section for disconnected/unavailable nodes.
|
||||
|
||||
Then, replace the broken node by the new one, using:
|
||||
|
||||
```
|
||||
garage node configure --replace <old_node_id> \
|
||||
-c <capacity> -z <zone> -t <node_tag> <new_node_id>
|
||||
```
|
||||
|
||||
Garage will then start synchronizing all required data on the new node.
|
||||
This process can be monitored using the `garage stats -a` command.
|
||||
@@ -0,0 +1,39 @@
|
||||
# Starting Garage with systemd instead of Docker
|
||||
|
||||
NOTE: This guide is incomplete. Typicall you would also want to create a separate
|
||||
Unix user to run Garage.
|
||||
|
||||
Make sure you have the Garage binary installed on your system (see [quick start](../quick_start/index.md)), e.g. at `/usr/local/bin/garage`.
|
||||
|
||||
Create a file named `/etc/systemd/system/garage.service`:
|
||||
|
||||
```toml
|
||||
[Unit]
|
||||
Description=Garage Data Store
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Environment='RUST_LOG=garage=info' 'RUST_BACKTRACE=1'
|
||||
ExecStart=/usr/local/bin/garage server -c /etc/garage/garage.toml
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
To start the service then automatically enable it at boot:
|
||||
|
||||
```bash
|
||||
sudo systemctl start garage
|
||||
sudo systemctl enable garage
|
||||
```
|
||||
|
||||
To see if the service is running and to browse its logs:
|
||||
|
||||
```bash
|
||||
sudo systemctl status garage
|
||||
sudo journalctl -u garage
|
||||
```
|
||||
|
||||
If you want to modify the service file, do not forget to run `systemctl daemon-reload`
|
||||
to inform `systemd` of your modifications.
|
||||
@@ -1 +1,3 @@
|
||||
# Host a website
|
||||
# Hosting a website
|
||||
|
||||
TODO
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
# Get a binary
|
||||
|
||||
Currently, only two installations procedures are supported for Garage: from Docker (x86\_64 for Linux) and from source.
|
||||
In the future, we plan to add a third one, by publishing a compiled binary (x86\_64 for Linux).
|
||||
We did not test other architecture/operating system but, as long as your architecture/operating system is supported by Rust, you should be able to run Garage (feel free to report your tests!).
|
||||
|
||||
## From Docker
|
||||
|
||||
Our docker image is currently named `lxpz/garage_amd64` and is stored on the [Docker Hub](https://hub.docker.com/r/lxpz/garage_amd64/tags?page=1&ordering=last_updated).
|
||||
We encourage you to use a fixed tag (eg. `v0.1.1d`) and not the `latest` tag.
|
||||
For this example, we will use the latest published version at the time of the writing which is `v0.1.1d` but it's up to you
|
||||
to check [the most recent versions on the Docker Hub](https://hub.docker.com/r/lxpz/garage_amd64/tags?page=1&ordering=last_updated).
|
||||
|
||||
For example:
|
||||
|
||||
```
|
||||
sudo docker pull lxpz/garage_amd64:v0.1.1d
|
||||
```
|
||||
|
||||
## From source
|
||||
|
||||
Garage is a standard Rust project.
|
||||
First, you need `rust` and `cargo`.
|
||||
On Debian:
|
||||
|
||||
```bash
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y rustc cargo
|
||||
```
|
||||
|
||||
Then, you can ask cargo to install the binary for you:
|
||||
|
||||
```bash
|
||||
cargo install garage
|
||||
```
|
||||
|
||||
That's all, `garage` should be in `$HOME/.cargo/bin`.
|
||||
You can add this folder to your `$PATH` or copy the binary somewhere else on your system.
|
||||
For the following, we will assume you copied it in `/usr/local/bin/garage`:
|
||||
|
||||
```bash
|
||||
sudo cp $HOME/.cargo/bin/garage /usr/local/bin/garage
|
||||
```
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
# Create buckets and keys
|
||||
|
||||
*We use a command named `garagectl` which is in fact an alias you must define as explained in the [Control the daemon](./daemon.md) section.*
|
||||
|
||||
In this section, we will suppose that we want to create a bucket named `nextcloud-bucket`
|
||||
that will be accessed through a key named `nextcloud-app-key`.
|
||||
|
||||
Don't forget that `help` command and `--help` subcommands can help you anywhere, the CLI tool is self-documented! Two examples:
|
||||
|
||||
```
|
||||
garagectl help
|
||||
garagectl bucket allow --help
|
||||
```
|
||||
|
||||
## Create a bucket
|
||||
|
||||
Fine, now let's create a bucket (we imagine that you want to deploy nextcloud):
|
||||
|
||||
```
|
||||
garagectl bucket create nextcloud-bucket
|
||||
```
|
||||
|
||||
Check that everything went well:
|
||||
|
||||
```
|
||||
garagectl bucket list
|
||||
garagectl bucket info nextcloud-bucket
|
||||
```
|
||||
|
||||
## Create an API key
|
||||
|
||||
Now we will generate an API key to access this bucket.
|
||||
Note that API keys are independent of buckets: one key can access multiple buckets, multiple keys can access one bucket.
|
||||
|
||||
Now, let's start by creating a key only for our PHP application:
|
||||
|
||||
```
|
||||
garagectl key new --name nextcloud-app-key
|
||||
```
|
||||
|
||||
You will have the following output (this one is fake, `key_id` and `secret_key` were generated with the openssl CLI tool):
|
||||
|
||||
```javascript
|
||||
Key {
|
||||
key_id: "GK3515373e4c851ebaad366558",
|
||||
secret_key: "7d37d093435a41f2aab8f13c19ba067d9776c90215f56614adad6ece597dbb34",
|
||||
name: "nextcloud-app-key",
|
||||
name_timestamp: 1603280506694,
|
||||
deleted: false,
|
||||
authorized_buckets: []
|
||||
}
|
||||
```
|
||||
|
||||
Check that everything works as intended (be careful, info works only with your key identifier and not with its friendly name!):
|
||||
|
||||
```
|
||||
garagectl key list
|
||||
garagectl key info GK3515373e4c851ebaad366558
|
||||
```
|
||||
|
||||
## Allow a key to access a bucket
|
||||
|
||||
Now that we have a bucket and a key, we need to give permissions to the key on the bucket!
|
||||
|
||||
```
|
||||
garagectl bucket allow \
|
||||
--read \
|
||||
--write
|
||||
nextcloud-bucket \
|
||||
--key GK3515373e4c851ebaad366558
|
||||
```
|
||||
|
||||
You can check at any times allowed keys on your bucket with:
|
||||
|
||||
```
|
||||
garagectl bucket info nextcloud-bucket
|
||||
```
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
# Configure a cluster
|
||||
|
||||
*We use a command named `garagectl` which is in fact an alias you must define as explained in the [Control the daemon](./daemon.md) section.*
|
||||
|
||||
In this section, we will inform garage of the disk space available on each node of the cluster
|
||||
as well as the site (think datacenter) of each machine.
|
||||
|
||||
## Test cluster
|
||||
|
||||
As this part is not relevant for a test cluster, you can use this one-liner to create a basic topology:
|
||||
|
||||
```bash
|
||||
garagectl status | grep UNCONFIGURED | grep -Po '^[0-9a-f]+' | while read id; do
|
||||
garagectl node configure -d dc1 -n 10 $id
|
||||
done
|
||||
```
|
||||
|
||||
## Real-world cluster
|
||||
|
||||
For our example, we will suppose we have the following infrastructure (Tokens, Identifier and Datacenter are specific values to garage described in the following):
|
||||
|
||||
| Location | Name | Disk Space | `Tokens` | `Identifier` | `Datacenter` |
|
||||
|----------|---------|------------|----------|--------------|--------------|
|
||||
| Paris | Mercury | 1 To | `100` | `8781c5` | `par1` |
|
||||
| Paris | Venus | 2 To | `200` | `2a638e` | `par1` |
|
||||
| London | Earth | 2 To | `200` | `68143d` | `lon1` |
|
||||
| Brussels | Mars | 1.5 To | `150` | `212f75` | `bru1` |
|
||||
|
||||
### Identifier
|
||||
|
||||
After its first launch, garage generates a random and unique identifier for each nodes, such as:
|
||||
|
||||
```
|
||||
8781c50c410a41b363167e9d49cc468b6b9e4449b6577b64f15a249a149bdcbc
|
||||
```
|
||||
|
||||
Often a shorter form can be used, containing only the beginning of the identifier, like `8781c5`,
|
||||
which identifies the server "Mercury" located in "Paris" according to our previous table.
|
||||
|
||||
The most simple way to match an identifier to a node is to run:
|
||||
|
||||
```
|
||||
garagectl status
|
||||
```
|
||||
|
||||
It will display the IP address associated with each node; from the IP address you will be able to recognize the node.
|
||||
|
||||
### Tokens
|
||||
|
||||
Garage reasons on an arbitrary metric about disk storage that is named "tokens".
|
||||
The number of tokens must be proportional to the disk space dedicated to the node.
|
||||
Additionaly, ideally the number of tokens must be in the order of magnitude of 100
|
||||
to provide a good trade-off between data load balancing and performances (*this sentence must be verified, it may be wrong*).
|
||||
|
||||
Here we chose 1 token = 10 Go but you are free to select the value that best fit your needs.
|
||||
|
||||
### Datacenter
|
||||
|
||||
Datacenter are simply a user-chosen identifier that identify a group of server that are located in the same place.
|
||||
It is up to the system administrator deploying garage to identify what does "the same place" means.
|
||||
Behind the scene, garage will try to store the same data on different sites to provide high availability despite a data center failure.
|
||||
|
||||
### Inject the topology
|
||||
|
||||
Given the information above, we will configure our cluster as follow:
|
||||
|
||||
```
|
||||
garagectl node configure --datacenter par1 -n 100 -t mercury 8781c5
|
||||
garagectl node configure --datacenter par1 -n 200 -t venus 2a638e
|
||||
garagectl node configure --datacenter lon1 -n 200 -t earth 68143d
|
||||
garagectl node configure --datacenter bru1 -n 150 -t mars 212f75
|
||||
```
|
||||
@@ -1,77 +0,0 @@
|
||||
# Control the daemon
|
||||
|
||||
The `garage` binary has two purposes:
|
||||
- it acts as a daemon when launched with `garage server ...`
|
||||
- it acts as a control tool for the daemon when launched with any other command
|
||||
|
||||
In this section, we will see how to use the `garage` binary as a control tool for the daemon we just started.
|
||||
You first need to get a shell having access to this binary, which depends of your configuration:
|
||||
- with `docker-compose`, run `sudo docker-compose exec g1 bash` then `/garage/garage`
|
||||
- with `docker`, run `sudo docker exec -ti garaged bash` then `/garage/garage`
|
||||
- with `systemd`, simply run `/usr/local/bin/garage` if you followed previous instructions
|
||||
|
||||
*You can also install the binary on your machine to remotely control the cluster.*
|
||||
|
||||
## Talk to the daemon and create an alias
|
||||
|
||||
`garage` requires 4 options to talk with the daemon:
|
||||
|
||||
```
|
||||
--ca-cert <ca-cert>
|
||||
--client-cert <client-cert>
|
||||
--client-key <client-key>
|
||||
-h, --rpc-host <rpc-host>
|
||||
```
|
||||
|
||||
The 3 first ones are certificates and keys needed by TLS, the last one is simply the address of garage's RPC endpoint.
|
||||
Because we configure garage directly from the server, we do not need to set `--rpc-host`.
|
||||
To avoid typing the 3 first options each time we want to run a command, we will create an alias.
|
||||
|
||||
### `docker-compose` alias
|
||||
|
||||
```bash
|
||||
alias garagectl='/garage/garage \
|
||||
--ca-cert /pki/garage-ca.crt \
|
||||
--client-cert /pki/garage.crt \
|
||||
--client-key /pki/garage.key'
|
||||
```
|
||||
|
||||
### `docker` alias
|
||||
|
||||
```bash
|
||||
alias garagectl='/garage/garage \
|
||||
--ca-cert /etc/garage/pki/garage-ca.crt \
|
||||
--client-cert /etc/garage/pki/garage.crt \
|
||||
--client-key /etc/garage/pki/garage.key'
|
||||
```
|
||||
|
||||
|
||||
### raw binary alias
|
||||
|
||||
```bash
|
||||
alias garagectl='/usr/local/bin/garage \
|
||||
--ca-cert /etc/garage/pki/garage-ca.crt \
|
||||
--client-cert /etc/garage/pki/garage.crt \
|
||||
--client-key /etc/garage/pki/garage.key'
|
||||
```
|
||||
|
||||
Of course, if your deployment does not match exactly one of this alias, feel free to adapt it to your needs!
|
||||
|
||||
## Test the alias
|
||||
|
||||
You can test your alias by running a simple command such as:
|
||||
|
||||
```
|
||||
garagectl status
|
||||
```
|
||||
|
||||
You should get something like that as result:
|
||||
|
||||
```
|
||||
Healthy nodes:
|
||||
2a638ed6c775b69a… 37f0ba978d27 [::ffff:172.20.0.101]:3901 UNCONFIGURED/REMOVED
|
||||
68143d720f20c89d… 9795a2f7abb5 [::ffff:172.20.0.103]:3901 UNCONFIGURED/REMOVED
|
||||
8781c50c410a41b3… 758338dde686 [::ffff:172.20.0.102]:3901 UNCONFIGURED/REMOVED
|
||||
```
|
||||
|
||||
...which means that you are ready to configure your cluster!
|
||||
@@ -1,222 +0,0 @@
|
||||
# Configure the daemon
|
||||
|
||||
Garage is a software that can be run only in a cluster and requires at least 3 instances.
|
||||
In our getting started guide, we document two deployment types:
|
||||
- [Test deployment](#test-deployment) though `docker-compose`
|
||||
- [Real-world deployment](#real-world-deployment) through `docker` or `systemd`
|
||||
|
||||
In any case, you first need to generate TLS certificates, as traffic is encrypted between Garage's nodes.
|
||||
|
||||
## Generating a TLS Certificate
|
||||
|
||||
To generate your TLS certificates, run on your machine:
|
||||
|
||||
```
|
||||
wget https://git.deuxfleurs.fr/Deuxfleurs/garage/raw/branch/master/genkeys.sh
|
||||
chmod +x genkeys.sh
|
||||
./genkeys.sh
|
||||
```
|
||||
|
||||
It will creates a folder named `pki` containing the keys that you will used for the cluster.
|
||||
|
||||
## Test deployment
|
||||
|
||||
Single machine deployment is only described through `docker-compose`.
|
||||
|
||||
Before starting, we recommend you create a folder for our deployment:
|
||||
|
||||
```bash
|
||||
mkdir garage-single
|
||||
cd garage-single
|
||||
```
|
||||
|
||||
We start by creating a file named `docker-compose.yml` describing our network and our containers:
|
||||
|
||||
```yml
|
||||
version: '3.4'
|
||||
|
||||
networks: { virtnet: { ipam: { config: [ subnet: 172.20.0.0/24 ]}}}
|
||||
|
||||
services:
|
||||
g1:
|
||||
image: lxpz/garage_amd64:v0.1.1d
|
||||
networks: { virtnet: { ipv4_address: 172.20.0.101 }}
|
||||
volumes:
|
||||
- "./pki:/pki"
|
||||
- "./config.toml:/garage/config.toml"
|
||||
|
||||
g2:
|
||||
image: lxpz/garage_amd64:v0.1.1d
|
||||
networks: { virtnet: { ipv4_address: 172.20.0.102 }}
|
||||
volumes:
|
||||
- "./pki:/pki"
|
||||
- "./config.toml:/garage/config.toml"
|
||||
|
||||
g3:
|
||||
image: lxpz/garage_amd64:v0.1.1d
|
||||
networks: { virtnet: { ipv4_address: 172.20.0.103 }}
|
||||
volumes:
|
||||
- "./pki:/pki"
|
||||
- "./config.toml:/garage/config.toml"
|
||||
```
|
||||
|
||||
*We define a static network here which is not considered as a best practise on Docker.
|
||||
The rational is that Garage only supports IP address and not domain names in its configuration, so we need to know the IP address in advance.*
|
||||
|
||||
and then create the `config.toml` file next to it as follow:
|
||||
|
||||
```toml
|
||||
metadata_dir = "/garage/meta"
|
||||
data_dir = "/garage/data"
|
||||
rpc_bind_addr = "[::]:3901"
|
||||
bootstrap_peers = [
|
||||
"172.20.0.101:3901",
|
||||
"172.20.0.102:3901",
|
||||
"172.20.0.103:3901",
|
||||
]
|
||||
|
||||
[rpc_tls]
|
||||
ca_cert = "/pki/garage-ca.crt"
|
||||
node_cert = "/pki/garage.crt"
|
||||
node_key = "/pki/garage.key"
|
||||
|
||||
[s3_api]
|
||||
s3_region = "garage"
|
||||
api_bind_addr = "[::]:3900"
|
||||
|
||||
[s3_web]
|
||||
bind_addr = "[::]:3902"
|
||||
root_domain = ".web.garage"
|
||||
index = "index.html"
|
||||
```
|
||||
|
||||
*Please note that we have not mounted `/garage/meta` or `/garage/data` on the host: data will be lost when the container will be destroyed.*
|
||||
|
||||
And that's all, you are ready to launch your cluster!
|
||||
|
||||
```
|
||||
sudo docker-compose up
|
||||
```
|
||||
|
||||
While your daemons are up, your cluster is still not configured yet.
|
||||
However, you can check that your services are still listening as expected by querying them from your host:
|
||||
|
||||
```bash
|
||||
curl http://172.20.0.{101,102,103}:3902
|
||||
```
|
||||
|
||||
which should give you:
|
||||
|
||||
```
|
||||
Not found
|
||||
Not found
|
||||
Not found
|
||||
```
|
||||
|
||||
That's all, you are ready to [configure your cluster!](./cluster.md).
|
||||
|
||||
## Real-world deployment
|
||||
|
||||
Before deploying garage on your infrastructure, you must inventory your machines.
|
||||
For our example, we will suppose the following infrastructure:
|
||||
|
||||
| Location | Name | IP Address | Disk Space |
|
||||
|----------|---------|------------|------------|
|
||||
| Paris | Mercury | fc00:1::1 | 1 To |
|
||||
| Paris | Venus | fc00:1::2 | 2 To |
|
||||
| London | Earth | fc00:1::2 | 2 To |
|
||||
| Brussels | Mars | fc00:B::1 | 1.5 To |
|
||||
|
||||
On each machine, we will have a similar setup, especially you must consider the following folders/files:
|
||||
- `/etc/garage/pki`: Garage certificates, must be generated on your computer and copied on the servers
|
||||
- `/etc/garage/config.toml`: Garage daemon's configuration (defined below)
|
||||
- `/etc/systemd/system/garage.service`: Service file to start garage at boot automatically (defined below, not required if you use docker)
|
||||
- `/var/lib/garage/meta`: Contains Garage's metadata, put this folder on a SSD if possible
|
||||
- `/var/lib/garage/data`: Contains Garage's data, this folder will grows and must be on a large storage, possibly big HDDs.
|
||||
|
||||
A valid `/etc/garage/config.toml` for our cluster would be:
|
||||
|
||||
```toml
|
||||
metadata_dir = "/var/lib/garage/meta"
|
||||
data_dir = "/var/lib/garage/data"
|
||||
rpc_bind_addr = "[::]:3901"
|
||||
bootstrap_peers = [
|
||||
"[fc00:1::1]:3901",
|
||||
"[fc00:1::2]:3901",
|
||||
"[fc00:B::1]:3901",
|
||||
"[fc00:F::1]:3901",
|
||||
]
|
||||
|
||||
[rpc_tls]
|
||||
ca_cert = "/etc/garage/pki/garage-ca.crt"
|
||||
node_cert = "/etc/garage/pki/garage.crt"
|
||||
node_key = "/etc/garage/pki/garage.key"
|
||||
|
||||
[s3_api]
|
||||
s3_region = "garage"
|
||||
api_bind_addr = "[::]:3900"
|
||||
|
||||
[s3_web]
|
||||
bind_addr = "[::]:3902"
|
||||
root_domain = ".web.garage"
|
||||
index = "index.html"
|
||||
```
|
||||
|
||||
Please make sure to change `bootstrap_peers` to **your** IP addresses!
|
||||
|
||||
### For docker users
|
||||
|
||||
On each machine, you can run the daemon with:
|
||||
|
||||
```bash
|
||||
docker run \
|
||||
-d \
|
||||
--name garaged \
|
||||
--restart always \
|
||||
--network host \
|
||||
-v /etc/garage/pki:/etc/garage/pki \
|
||||
-v /etc/garage/config.toml:/garage/config.toml \
|
||||
-v /var/lib/garage/meta:/var/lib/garage/meta \
|
||||
-v /var/lib/garage/data:/var/lib/garage/data \
|
||||
lxpz/garage_amd64:v0.1.1d
|
||||
```
|
||||
|
||||
It should be restart automatically at each reboot.
|
||||
Please note that we use host networking as otherwise Docker containers can no communicate with IPv6.
|
||||
|
||||
To upgrade, simply stop and remove this container and start again the command with a new version of garage.
|
||||
|
||||
### For systemd/raw binary users
|
||||
|
||||
Create a file named `/etc/systemd/system/garage.service`:
|
||||
|
||||
```toml
|
||||
[Unit]
|
||||
Description=Garage Data Store
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Environment='RUST_LOG=garage=info' 'RUST_BACKTRACE=1'
|
||||
ExecStart=/usr/local/bin/garage server -c /etc/garage/config.toml
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
To start the service then automatically enable it at boot:
|
||||
|
||||
```bash
|
||||
sudo systemctl start garage
|
||||
sudo systemctl enable garage
|
||||
```
|
||||
|
||||
To see if the service is running and to browse its logs:
|
||||
|
||||
```bash
|
||||
sudo systemctl status garage
|
||||
sudo journalctl -u garage
|
||||
```
|
||||
|
||||
If you want to modify the service file, do not forget to run `systemctl daemon-reload`
|
||||
to inform `systemd` of your modifications.
|
||||
@@ -1,42 +0,0 @@
|
||||
# Handle files
|
||||
|
||||
We recommend the use of MinIO Client to interact with Garage files (`mc`).
|
||||
Instructions to install it and use it are provided on the [MinIO website](https://docs.min.io/docs/minio-client-quickstart-guide.html).
|
||||
Before reading the following, you need a working `mc` command on your path.
|
||||
|
||||
## Configure `mc`
|
||||
|
||||
You need your access key and secret key created in the [previous section](bucket.md).
|
||||
You also need to set the endpoint: it must match the IP address of one of the node of the cluster and the API port (3900 by default).
|
||||
For this whole configuration, you must set an alias name: we chose `my-garage`, that you will used for all commands.
|
||||
|
||||
Adapt the following command accordingly and run it:
|
||||
|
||||
```bash
|
||||
mc alias set \
|
||||
my-garage \
|
||||
http://172.20.0.101:3900 \
|
||||
<access key> \
|
||||
<secret key> \
|
||||
--api S3v4
|
||||
```
|
||||
|
||||
You must also add an environment variable to your configuration to inform MinIO of our region (`garage` by default).
|
||||
The best way is to add the following snippet to your `$HOME/.bash_profile` or `$HOME/.bashrc` file:
|
||||
|
||||
```bash
|
||||
export MC_REGION=garage
|
||||
```
|
||||
|
||||
## Use `mc`
|
||||
|
||||
You can not list buckets from `mc` currently.
|
||||
|
||||
But the following commands and many more should work:
|
||||
|
||||
```bash
|
||||
mc cp image.png my-garage/nextcloud-bucket
|
||||
mc cp my-garage/nextcloud-bucket/image.png .
|
||||
mc ls my-garage/nextcloud-bucket
|
||||
mc mirror localdir/ my-garage/another-bucket
|
||||
```
|
||||
@@ -1,5 +0,0 @@
|
||||
# Getting Started
|
||||
|
||||
Let's start your Garage journey!
|
||||
In this chapter, we explain how to deploy a simple garage cluster and start interacting with it.
|
||||
Our goal is to introduce you to Garage's workflows.
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 34 KiB |
@@ -4,63 +4,75 @@
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center" style="text-align:center;">
|
||||
[ <a href="https://git.deuxfleurs.fr/Deuxfleurs/garage">Git repository</a>
|
||||
| <a href="https://matrix.to/#/%23garage:deuxfleurs.fr">Matrix channel</a>
|
||||
| <a href="https://drone.deuxfleurs.fr/Deuxfleurs/garage">Drone CI</a>
|
||||
]
|
||||
</p>
|
||||
|
||||
```
|
||||
This very website is hosted using Garage. In other words: the doc is the PoC!
|
||||
```
|
||||
|
||||
# The Garage Geo-Distributed Data Store
|
||||
|
||||
Garage is a lightweight geo-distributed data store.
|
||||
It comes from the observation that despite numerous object stores
|
||||
many people have broken data management policies (backup/replication on a single site or none at all).
|
||||
To promote better data management policies, with focused on the following desirable properties:
|
||||
To promote better data management policies, we focused on the following **desirable properties**:
|
||||
|
||||
- **Self-contained & lightweight**: works everywhere and integrates well in existing environments to target hyperconverged infrastructures
|
||||
- **Highly resilient**: highly resilient to network failures, network latency, disk failures, sysadmin failures
|
||||
- **Simple**: simple to understand, simple to operate, simple to debug
|
||||
- **Internet enabled**: made for multi-sites (eg. datacenter, offices, etc.) interconnected through a regular internet connection.
|
||||
- **Self-contained & lightweight**: works everywhere and integrates well in existing environments to target [hyperconverged infrastructures](https://en.wikipedia.org/wiki/Hyper-converged_infrastructure).
|
||||
- **Highly resilient**: highly resilient to network failures, network latency, disk failures, sysadmin failures.
|
||||
- **Simple**: simple to understand, simple to operate, simple to debug.
|
||||
- **Internet enabled**: made for multi-sites (eg. datacenters, offices, households, etc.) interconnected through regular Internet connections.
|
||||
|
||||
We also noted that the pursuit of some other goals are detrimental to our initial goals.
|
||||
The following have been identified has non-goals, if it matters to you, you should not use Garage:
|
||||
The following has been identified as **non-goals** (if these points matter to you, you should not use Garage):
|
||||
|
||||
- **Extreme performances**: high performances constrain a lot the design and the infrastructure; we seek performances through minimalism only.
|
||||
- **Feature extensiveness**: complete implementation of the S3 API or any other API to make garage a drop-in replacement is not targeted as it could lead to decisions impacting our desirable properties.
|
||||
- **Storage optimizations**: erasure coding or any other coding technique both increase the difficulty of placing data and synchronizing; we limit ourselves to duplication.
|
||||
- **POSIX/Filesystem compatibility**: we do not aim at being POSIX compatible or to emulate any kind of filesystem. Indeed, in a distributed environment, such syncronizations are translated in network messages that impose severe constraints on the deployment.
|
||||
- **POSIX/Filesystem compatibility**: we do not aim at being POSIX compatible or to emulate any kind of filesystem. Indeed, in a distributed environment, such synchronizations are translated in network messages that impose severe constraints on the deployment.
|
||||
|
||||
## Supported and planned protocols
|
||||
|
||||
Garage speaks (or will speak) the following protocols:
|
||||
|
||||
- [S3](https://docs.aws.amazon.com/AmazonS3/latest/API/Welcome.html) - *SUPPORTED* - Enable applications to store large blobs such as pictures, video, images, documents, etc. S3 is versatile enough to also be used to publish a static website.
|
||||
- [IMAP](https://github.com/go-pluto/pluto) - *PLANNED* - email storage is quite complex to get good oerformances.
|
||||
To keep performances optimals, most imap servers only support on-disk storage.
|
||||
- [IMAP](https://github.com/go-pluto/pluto) - *PLANNED* - email storage is quite complex to get good performances.
|
||||
To keep performances optimal, most IMAP servers only support on-disk storage.
|
||||
We plan to add logic to Garage to make it a viable solution for email storage.
|
||||
- *More to come*
|
||||
|
||||
## Use Cases
|
||||
|
||||
**[Deuxfleurs](https://deuxfleurs.fr) :** Garage is used by Deuxfleurs which is a non-profit hosting organization.
|
||||
Especially, it is used to host their main website, this documentation and some of its members's blogs. Additionally,
|
||||
Garage is used as a [backend for Nextcloud](https://docs.nextcloud.com/server/20/admin_manual/configuration_files/primary_storage.html). Deuxfleurs also plans to use Garage as their [Matrix's media backend](https://github.com/matrix-org/synapse-s3-storage-provider) and has the backend of [OCIS](https://github.com/owncloud/ocis).
|
||||
**[Deuxfleurs](https://deuxfleurs.fr):** Garage is used by Deuxfleurs which is a non-profit hosting organization.
|
||||
Especially, it is used to host their main website, this documentation and some of its members' blogs.
|
||||
Additionally, Garage is used as a [backend for Nextcloud](https://docs.nextcloud.com/server/20/admin_manual/configuration_files/primary_storage.html).
|
||||
Deuxfleurs also plans to use Garage as their [Matrix's media backend](https://github.com/matrix-org/synapse-s3-storage-provider) and as the backend of [OCIS](https://github.com/owncloud/ocis).
|
||||
|
||||
*Are you using Garage? [Open a pull request](https://git.deuxfleurs.fr/Deuxfleurs/garage/) to add your organization here!*
|
||||
|
||||
## Comparison to existing software
|
||||
|
||||
**[Minio](https://min.io/) :** Minio shares our *self-contained & lightweight* goal but selected two of our non-goals: *storage optimizations* through erasure coding and *POSIX/Filesystem compatibility* through strong consistency.
|
||||
However, by pursuing these two non-goals, minio do not reach our desirable properties.
|
||||
First, it fails on the *simple* property: due to the erasure coding, minio has severe limitations on how drives can be added or deleted from a cluster.
|
||||
Second, it fails on the *interned enabled* property: due to its strong consistency, minio is latency sensitive.
|
||||
Furthermore, minio has no knowledge of "sites" and thus can not distribute data to minimize the failure of a given site.
|
||||
**[MinIO](https://min.io/):** MinIO shares our *Self-contained & lightweight* goal but selected two of our non-goals: *Storage optimizations* through erasure coding and *POSIX/Filesystem compatibility* through strong consistency.
|
||||
However, by pursuing these two non-goals, MinIO do not reach our desirable properties.
|
||||
Firstly, it fails on the *Simple* property: due to the erasure coding, MinIO has severe limitations on how drives can be added or deleted from a cluster.
|
||||
Secondly, it fails on the *Internet enabled* property: due to its strong consistency, MinIO is latency sensitive.
|
||||
Furthermore, MinIO has no knowledge of "sites" and thus can not distribute data to minimize the failure of a given site.
|
||||
|
||||
**[Openstack Swift](https://docs.openstack.org/swift/latest/) :**
|
||||
OpenStack Swift at least fails on the *self-contained & lightweight* goal.
|
||||
Starting it requires around 8Gb of RAM, which is too much especially in an hyperconverged infrastructure.
|
||||
It seems also to be far from *Simple*.
|
||||
**[Openstack Swift](https://docs.openstack.org/swift/latest/):**
|
||||
OpenStack Swift at least fails on the *Self-contained & lightweight* goal.
|
||||
Starting it requires around 8GB of RAM, which is too much especially in an hyperconverged infrastructure.
|
||||
We also do not classify Swift as *Simple*.
|
||||
|
||||
**[Ceph](https://ceph.io/ceph-storage/object-storage/) :**
|
||||
**[Ceph](https://ceph.io/ceph-storage/object-storage/):**
|
||||
This review holds for the whole Ceph stack, including the RADOS paper, Ceph Object Storage module, the RADOS Gateway, etc.
|
||||
At is core, Ceph has been designed to provide *POSIX/Filesystem compatibility* which requires strong consistency, which in turn
|
||||
makes Ceph latency sensitive and fails our *Internet enabled* goal.
|
||||
Due to its industry oriented design, Ceph is also far from being *Simple* to operate and from being *self-contained & lightweight* which makes it hard to integrate it in an hyperconverged infrastructure.
|
||||
In a certain way, Ceph and Minio are closer togethers than they are from Garage or OpenStack Swift.
|
||||
At its core, Ceph has been designed to provide *POSIX/Filesystem compatibility* which requires strong consistency, which in turn
|
||||
makes Ceph latency-sensitive and fails our *Internet enabled* goal.
|
||||
Due to its industry oriented design, Ceph is also far from being *Simple* to operate and from being *Self-contained & lightweight* which makes it hard to integrate it in an hyperconverged infrastructure.
|
||||
In a certain way, Ceph and MinIO are closer together than they are from Garage or OpenStack Swift.
|
||||
|
||||
*More comparisons are available in our [Related Work](design/related_work.md) chapter.*
|
||||
|
||||
@@ -71,29 +83,46 @@ We reference here other places on the Internet where you can learn more about Ga
|
||||
|
||||
### Rust API (docs.rs)
|
||||
|
||||
If you encounter a specific bug in Garage or plan to patch it, you may jump directly to the source code documentation!
|
||||
If you encounter a specific bug in Garage or plan to patch it, you may jump directly to the source code's documentation!
|
||||
|
||||
- [garage\_api](https://docs.rs/garage_api/latest/garage_api/) - contains the S3 standard API endpoint
|
||||
- [garage\_model](https://docs.rs/garage_model/latest/garage_model/) - contains Garage's model built on the table abstraction
|
||||
- [garage\_rpc](https://docs.rs/garage_rpc/latest/garage_rpc/) - contains Garage's federation protocol
|
||||
- [garage\_table](https://docs.rs/garage_table/latest/garage_table/) - contains core Garage's CRDT datatypes
|
||||
- [garage\_util](https://docs.rs/garage_util/latest/garage_util/) - contains garage entrypoints (daemon, cli)
|
||||
- [garage\_util](https://docs.rs/garage_util/latest/garage_util/) - contains garage helpers
|
||||
- [garage\_web](https://docs.rs/garage_web/latest/garage_web/) - contains the S3 website endpoint
|
||||
|
||||
### Talks
|
||||
|
||||
We love to talk and hear about Garage, that's why we keep a log here:
|
||||
|
||||
- [(fr, 2020-12-02) Garage : jouer dans la cour des grands quand on est un hébergeur associatif](https://git.deuxfleurs.fr/Deuxfleurs/garage/src/branch/master/doc/20201202_talk/talk.pdf)
|
||||
- [(en, 2021-04-28) Distributed object storage is centralised](https://git.deuxfleurs.fr/Deuxfleurs/garage/raw/commit/b1f60579a13d3c5eba7f74b1775c84639ea9b51a/doc/talks/2021-04-28_spirals-team/talk.pdf)
|
||||
|
||||
- [(fr, 2020-12-02) Garage : jouer dans la cour des grands quand on est un hébergeur associatif](https://git.deuxfleurs.fr/Deuxfleurs/garage/raw/commit/b1f60579a13d3c5eba7f74b1775c84639ea9b51a/doc/talks/2020-12-02_wide-team/talk.pdf)
|
||||
|
||||
*Did you write or talk about Garage? [Open a pull request](https://git.deuxfleurs.fr/Deuxfleurs/garage/) to add a link here!*
|
||||
|
||||
## Community
|
||||
|
||||
If you want to discuss with us, you can join our Matrix channel at [#garage:deuxfleurs.fr](https://matrix.to/#/#garage:deuxfleurs.fr).
|
||||
Our code and our issue tracker, which is the place where you should report bugs, are managed on [Deuxfleurs' Gitea](https://git.deuxfleurs.fr/Deuxfleurs/garage).
|
||||
Our code repository and issue tracker, which is the place where you should report bugs, is managed on [Deuxfleurs' Gitea](https://git.deuxfleurs.fr/Deuxfleurs/garage).
|
||||
|
||||
## License
|
||||
|
||||
Garage, all the source code, is released under the [AGPL v3 License](https://www.gnu.org/licenses/agpl-3.0.en.html).
|
||||
Garage's source code, is released under the [AGPL v3 License](https://www.gnu.org/licenses/agpl-3.0.en.html).
|
||||
Please note that if you patch Garage and then use it to provide any service over a network, you must share your code!
|
||||
|
||||
# Funding
|
||||
|
||||
The Deuxfleurs association has received a grant promise to fund 3 people working on Garage for a year, from October 2021 to September 2022.
|
||||
|
||||
<div style="display: flex; justify-content: space-around">
|
||||
<a href="https://pointer.ngi.eu/">
|
||||
<img style="height:100px" src="img/ngi-logo.png" alt="NGI Pointer logo">
|
||||
</a>
|
||||
<a href="https://ec.europa.eu/programmes/horizon2020/what-horizon-2020">
|
||||
<img style="height:100px" src="img/eu-flag-logo.png" alt="EU flag logo">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
_This project has received funding from the European Union’s Horizon 2020 research and innovation programme within the framework of the NGI-POINTER Project funded under grant agreement No 871528._
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
# Quick Start
|
||||
|
||||
Let's start your Garage journey!
|
||||
In this chapter, we explain how to deploy Garage as a single-node server
|
||||
and how to interact with it.
|
||||
|
||||
Our goal is to introduce you to Garage's workflows.
|
||||
Following this guide is recommended before moving on to
|
||||
[configuring a real-world deployment](../cookbook/real_world.md).
|
||||
|
||||
Note that this kind of deployment should not be used in production, as it provides
|
||||
no redundancy for your data!
|
||||
We will also skip intra-cluster TLS configuration, meaning that if you add nodes
|
||||
to your cluster, communication between them will not be secure.
|
||||
|
||||
## Get a binary
|
||||
|
||||
Download the latest Garage binary from the release pages on our repository:
|
||||
|
||||
<https://git.deuxfleurs.fr/Deuxfleurs/garage/releases>
|
||||
|
||||
Place this binary somewhere in your `$PATH` so that you can invoke the `garage`
|
||||
command directly (for instance you can copy the binary in `/usr/local/bin`
|
||||
or in `~/.local/bin`).
|
||||
|
||||
If a binary of the last version is not available for your architecture,
|
||||
you can [build Garage from source](../cookbook/from_source.md).
|
||||
|
||||
|
||||
## Writing a first configuration file
|
||||
|
||||
This first configuration file should allow you to get started easily with the simplest
|
||||
possible Garage deployment:
|
||||
|
||||
```toml
|
||||
metadata_dir = "/tmp/meta"
|
||||
data_dir = "/tmp/data"
|
||||
|
||||
replication_mode = "none"
|
||||
|
||||
rpc_bind_addr = "[::]:3901"
|
||||
|
||||
bootstrap_peers = [
|
||||
"127.0.0.1:3901",
|
||||
]
|
||||
|
||||
[s3_api]
|
||||
s3_region = "garage"
|
||||
api_bind_addr = "[::]:3900"
|
||||
|
||||
[s3_web]
|
||||
bind_addr = "[::]:3902"
|
||||
root_domain = ".web.garage"
|
||||
index = "index.html"
|
||||
```
|
||||
|
||||
Save your configuration file as `garage.toml`.
|
||||
|
||||
As you can see in the `metadata_dir` and `data_dir` parameters, we are saving Garage's data
|
||||
in `/tmp` which gets erased when your system reboots. This means that data stored on this
|
||||
Garage server will not be persistent. Change these to locations on your local disk if you want
|
||||
your data to be persisted properly.
|
||||
|
||||
|
||||
## Launching the Garage server
|
||||
|
||||
Use the following command to launch the Garage server with our configuration file:
|
||||
|
||||
```
|
||||
RUST_LOG=garage=info garage server -c garage.toml
|
||||
```
|
||||
|
||||
You can tune Garage's verbosity as follows (from less verbose to more verbose):
|
||||
|
||||
```
|
||||
RUST_LOG=garage=info garage server -c garage.toml
|
||||
RUST_LOG=garage=debug garage server -c garage.toml
|
||||
RUST_LOG=garage=trace garage server -c garage.toml
|
||||
```
|
||||
|
||||
Log level `info` is recommended for most use cases.
|
||||
Log level `debug` can help you check why your S3 API calls are not working.
|
||||
|
||||
|
||||
## Checking that Garage runs correctly
|
||||
|
||||
The `garage` utility is also used as a CLI tool to configure your Garage deployment.
|
||||
It tries to connect to a Garage server through the RPC protocol, by default looking
|
||||
for a Garage server at `localhost:3901`.
|
||||
|
||||
Since our deployment already binds to port 3901, the following command should be sufficient
|
||||
to show Garage's status:
|
||||
|
||||
```
|
||||
garage status
|
||||
```
|
||||
|
||||
This should show something like this:
|
||||
|
||||
```
|
||||
Healthy nodes:
|
||||
2a638ed6c775b69a… linuxbox 127.0.0.1:3901 UNCONFIGURED/REMOVED
|
||||
```
|
||||
|
||||
## Configuring your Garage node
|
||||
|
||||
Configuring the nodes in a Garage deployment means informing Garage
|
||||
of the disk space available on each node of the cluster
|
||||
as well as the zone (e.g. datacenter) each machine is located in.
|
||||
|
||||
For our test deployment, we are using only one node. The way in which we configure
|
||||
it does not matter, you can simply write:
|
||||
|
||||
```bash
|
||||
garage node configure -z dc1 -c 1 <node_id>
|
||||
```
|
||||
|
||||
where `<node_id>` corresponds to the identifier of the node shown by `garage status` (first column).
|
||||
You can enter simply a prefix of that identifier.
|
||||
For instance here you could write just `garage node configure -z dc1 -c 1 2a63`.
|
||||
|
||||
|
||||
|
||||
|
||||
## Creating buckets and keys
|
||||
|
||||
In this section, we will suppose that we want to create a bucket named `nextcloud-bucket`
|
||||
that will be accessed through a key named `nextcloud-app-key`.
|
||||
|
||||
Don't forget that `help` command and `--help` subcommands can help you anywhere,
|
||||
the CLI tool is self-documented! Two examples:
|
||||
|
||||
```
|
||||
garage help
|
||||
garage bucket allow --help
|
||||
```
|
||||
|
||||
#### Create a bucket
|
||||
|
||||
Let's take an example where we want to deploy NextCloud using Garage as the
|
||||
main data storage.
|
||||
|
||||
First, create a bucket with the following command:
|
||||
|
||||
```
|
||||
garage bucket create nextcloud-bucket
|
||||
```
|
||||
|
||||
Check that everything went well:
|
||||
|
||||
```
|
||||
garage bucket list
|
||||
garage bucket info nextcloud-bucket
|
||||
```
|
||||
|
||||
#### Create an API key
|
||||
|
||||
The `nextcloud-bucket` bucket now exists on the Garage server,
|
||||
however it cannot be accessed until we add an API key with the proper access rights.
|
||||
|
||||
Note that API keys are independent of buckets:
|
||||
one key can access multiple buckets, multiple keys can access one bucket.
|
||||
|
||||
Create an API key using the following command:
|
||||
|
||||
```
|
||||
garage key new --name nextcloud-app-key
|
||||
```
|
||||
|
||||
The output should look as follows:
|
||||
|
||||
```
|
||||
Key name: nextcloud-app-key
|
||||
Key ID: GK3515373e4c851ebaad366558
|
||||
Secret key: 7d37d093435a41f2aab8f13c19ba067d9776c90215f56614adad6ece597dbb34
|
||||
Authorized buckets:
|
||||
```
|
||||
|
||||
Check that everything works as intended:
|
||||
|
||||
```
|
||||
garage key list
|
||||
garage key info nextcloud-app-key
|
||||
```
|
||||
|
||||
#### Allow a key to access a bucket
|
||||
|
||||
Now that we have a bucket and a key, we need to give permissions to the key on the bucket:
|
||||
|
||||
```
|
||||
garage bucket allow \
|
||||
--read \
|
||||
--write
|
||||
nextcloud-bucket \
|
||||
--key nextcloud-app-key
|
||||
```
|
||||
|
||||
You can check at any time the allowed keys on your bucket with:
|
||||
|
||||
```
|
||||
garage bucket info nextcloud-bucket
|
||||
```
|
||||
|
||||
|
||||
## Uploading and downlading from Garage
|
||||
|
||||
We recommend the use of MinIO Client to interact with Garage files (`mc`).
|
||||
Instructions to install it and use it are provided on the
|
||||
[MinIO website](https://docs.min.io/docs/minio-client-quickstart-guide.html).
|
||||
Before reading the following, you need a working `mc` command on your path.
|
||||
|
||||
Note that on certain Linux distributions such as Arch Linux, the Minio client binary
|
||||
is called `mcli` instead of `mc` (to avoid name clashes with the Midnight Commander).
|
||||
|
||||
#### Configure `mc`
|
||||
|
||||
You need your access key and secret key created above.
|
||||
We will assume you are invoking `mc` on the same machine as the Garage server,
|
||||
your S3 API endpoint is therefore `http://127.0.0.1:3900`.
|
||||
For this whole configuration, you must set an alias name: we chose `my-garage`, that you will used for all commands.
|
||||
|
||||
Adapt the following command accordingly and run it:
|
||||
|
||||
```bash
|
||||
mc alias set \
|
||||
my-garage \
|
||||
http://127.0.0.1:3900 \
|
||||
<access key> \
|
||||
<secret key> \
|
||||
--api S3v4
|
||||
```
|
||||
|
||||
You must also add an environment variable to your configuration to
|
||||
inform MinIO of our region (`garage` by default, corresponding to the `s3_region` parameter
|
||||
in the configuration file).
|
||||
The best way is to add the following snippet to your `$HOME/.bash_profile`
|
||||
or `$HOME/.bashrc` file:
|
||||
|
||||
```bash
|
||||
export MC_REGION=garage
|
||||
```
|
||||
|
||||
#### Use `mc`
|
||||
|
||||
You can not list buckets from `mc` currently.
|
||||
|
||||
But the following commands and many more should work:
|
||||
|
||||
```bash
|
||||
mc cp image.png my-garage/nextcloud-bucket
|
||||
mc cp my-garage/nextcloud-bucket/image.png .
|
||||
mc ls my-garage/nextcloud-bucket
|
||||
mc mirror localdir/ my-garage/another-bucket
|
||||
```
|
||||
|
||||
|
||||
#### Other tools for interacting with Garage
|
||||
|
||||
The following tools can also be used to send and recieve files from/to Garage:
|
||||
|
||||
- the [AWS CLI](https://aws.amazon.com/cli/)
|
||||
- [`rclone`](https://rclone.org/)
|
||||
- [Cyberduck](https://cyberduck.io/)
|
||||
- [`s3cmd`](https://s3tools.org/s3cmd)
|
||||
|
||||
Refer to the ["configuring clients"](../cookbook/clients.md) page to learn how to configure
|
||||
these clients to interact with a Garage server.
|
||||
@@ -0,0 +1,4 @@
|
||||
# Garage CLI
|
||||
|
||||
The Garage CLI is mostly self-documented. Make use of the `help` subcommand
|
||||
and the `--help` flag to discover all available options.
|
||||
@@ -0,0 +1,196 @@
|
||||
# Garage configuration file format reference
|
||||
|
||||
Here is an example `garage.toml` configuration file that illustrates all of the possible options:
|
||||
|
||||
```toml
|
||||
metadata_dir = "/var/lib/garage/meta"
|
||||
data_dir = "/var/lib/garage/data"
|
||||
|
||||
block_size = 1048576
|
||||
|
||||
replication_mode = "3"
|
||||
|
||||
rpc_bind_addr = "[::]:3901"
|
||||
|
||||
bootstrap_peers = [
|
||||
"[fc00:1::1]:3901",
|
||||
"[fc00:1::2]:3901",
|
||||
"[fc00:B::1]:3901",
|
||||
"[fc00:F::1]:3901",
|
||||
]
|
||||
|
||||
consul_host = "consul.service"
|
||||
consul_service_name = "garage-daemon"
|
||||
|
||||
max_concurrent_rpc_requests = 12
|
||||
|
||||
sled_cache_capacity = 134217728
|
||||
sled_flush_every_ms = 2000
|
||||
|
||||
[rpc_tls]
|
||||
ca_cert = "/etc/garage/pki/garage-ca.crt"
|
||||
node_cert = "/etc/garage/pki/garage.crt"
|
||||
node_key = "/etc/garage/pki/garage.key"
|
||||
|
||||
[s3_api]
|
||||
s3_region = "garage"
|
||||
api_bind_addr = "[::]:3900"
|
||||
|
||||
[s3_web]
|
||||
bind_addr = "[::]:3902"
|
||||
root_domain = ".web.garage"
|
||||
index = "index.html"
|
||||
```
|
||||
|
||||
The following gives details about each available configuration option.
|
||||
|
||||
## Available configuration options
|
||||
|
||||
#### `metadata_dir`
|
||||
|
||||
The directory in which Garage will store its metadata. This contains the node identifier,
|
||||
the network configuration and the peer list, the list of buckets and keys as well
|
||||
as the index of all objects, object version and object blocks.
|
||||
|
||||
Store this folder on a fast SSD drive if possible to maximize Garage's performance.
|
||||
|
||||
#### `data_dir`
|
||||
|
||||
The directory in which Garage will store the data blocks of objects.
|
||||
This folder can be placed on an HDD. The space available for `data_dir`
|
||||
should be counted to determine a node's capacity
|
||||
when [configuring it](../getting_started/05_cluster.md).
|
||||
|
||||
#### `block_size`
|
||||
|
||||
Garage splits stored objects in consecutive chunks of size `block_size` (except the last
|
||||
one which might be standard). The default size is 1MB and should work in most cases.
|
||||
If you are interested in tuning this, feel free to do so (and remember to report your
|
||||
findings to us!)
|
||||
|
||||
#### `replication_mode`
|
||||
|
||||
Garage supports the following replication modes:
|
||||
|
||||
- `none` or `1`: data stored on Garage is stored on a single node. There is no redundancy,
|
||||
and data will be unavailable as soon as one node fails or its network is disconnected.
|
||||
Do not use this for anything else than test deployments.
|
||||
|
||||
- `2`: data stored on Garage will be stored on two different nodes, if possible in different
|
||||
zones. Garage tolerates one node failure before losing data. Data should be available
|
||||
read-only when one node is down, but write operations will fail.
|
||||
Use this only if you really have to.
|
||||
|
||||
- `3`: data stored on Garage will be stored on three different nodes, if possible each in
|
||||
a different zones.
|
||||
Garage tolerates two node failure before losing data. Data should be available
|
||||
read-only when two nodes are down, and writes should be possible if only a single node
|
||||
is down.
|
||||
|
||||
Note that in modes `2` and `3`,
|
||||
if at least the same number of zones are available, an arbitrary number of failures in
|
||||
any given zone is tolerated as copies of data will be spread over several zones.
|
||||
|
||||
**Make sure `replication_mode` is the same in the configuration files of all nodes.
|
||||
Never run a Garage cluster where that is not the case.**
|
||||
|
||||
Changing the `replication_mode` of a cluster might work (make sure to shut down all nodes
|
||||
and changing it everywhere at the time), but is not officially supported.
|
||||
|
||||
#### `rpc_bind_addr`
|
||||
|
||||
The address and port on which to bind for inter-cluster communcations
|
||||
(reffered to as RPC for remote procedure calls).
|
||||
The port specified here should be the same one that other nodes will used to contact
|
||||
the node, even in the case of a NAT: the NAT should be configured to forward the external
|
||||
port number to the same internal port nubmer. This means that if you have several nodes running
|
||||
behind a NAT, they should each use a different RPC port number.
|
||||
|
||||
#### `bootstrap_peers`
|
||||
|
||||
A list of IPs and ports on which to contact other Garage peers of this cluster.
|
||||
This should correspond to the RPC ports set up with `rpc_bind_addr`.
|
||||
|
||||
#### `consul_host` and `consul_service_name`
|
||||
|
||||
Garage supports discovering other nodes of the cluster using Consul.
|
||||
This works only when nodes are announced in Consul by an orchestrator such as Nomad,
|
||||
as Garage is not able to announce itself.
|
||||
|
||||
The `consul_host` parameter should be set to the hostname of the Consul server,
|
||||
and `consul_service_name` should be set to the service name under which Garage's
|
||||
RPC ports are announced.
|
||||
|
||||
#### `max_concurrent_rpc_requests`
|
||||
|
||||
Garage implements rate limiting for RPC requests: no more than
|
||||
`max_concurrent_rpc_requests` concurrent outbound RPC requests will be made
|
||||
by a Garage node (additionnal requests will be put in a waiting queue).
|
||||
|
||||
#### `sled_cache_capacity`
|
||||
|
||||
This parameter can be used to tune the capacity of the cache used by
|
||||
[sled](https://sled.rs), the database Garage uses internally to store metadata.
|
||||
Tune this to fit the RAM you wish to make available to your Garage instance.
|
||||
More cache means faster Garage, but the default value (128MB) should be plenty
|
||||
for most use cases.
|
||||
|
||||
#### `sled_flush_every_ms`
|
||||
|
||||
This parameters can be used to tune the flushing interval of sled.
|
||||
Increase this if sled is thrashing your SSD, at the risk of losing more data in case
|
||||
of a power outage (though this should not matter much as data is replicated on other
|
||||
nodes). The default value, 2000ms, should be appropriate for most use cases.
|
||||
|
||||
|
||||
## The `[rpc_tls]` section
|
||||
|
||||
This section should be used to configure the TLS certificates used to encrypt
|
||||
intra-cluster traffic (RPC traffic). The following parameters should be set:
|
||||
|
||||
- `ca_cert`: the certificate of the CA that is allowed to sign individual node certificates
|
||||
- `node_cert`: the node certificate for the current node
|
||||
- `node_key`: the key associated with the node certificate
|
||||
|
||||
Note tha several nodes may use the same node certificate, as long as it is signed
|
||||
by the CA.
|
||||
|
||||
If this section is absent, TLS is not used to encrypt intra-cluster traffic.
|
||||
|
||||
|
||||
## The `[s3_api]` section
|
||||
|
||||
#### `api_bind_addr`
|
||||
|
||||
The IP and port on which to bind for accepting S3 API calls.
|
||||
This endpoint does not suport TLS: a reverse proxy should be used to provide it.
|
||||
|
||||
#### `s3_region`
|
||||
|
||||
Garage will accept S3 API calls that are targetted to the S3 region defined here.
|
||||
API calls targetted to other regions will fail with a AuthorizationHeaderMalformed error
|
||||
message that redirects the client to the correct region.
|
||||
|
||||
|
||||
## The `[s3_web]` section
|
||||
|
||||
Garage allows to publish content of buckets as websites. This section configures the
|
||||
behaviour of this module.
|
||||
|
||||
#### `bind_addr`
|
||||
|
||||
The IP and port on which to bind for accepting HTTP requests to buckets configured
|
||||
for website access.
|
||||
This endpoint does not suport TLS: a reverse proxy should be used to provide it.
|
||||
|
||||
#### `root_domain`
|
||||
|
||||
The optionnal suffix appended to bucket names for the corresponding HTTP Host.
|
||||
|
||||
For instance, if `root_domain` is `web.garage.eu`, a bucket called `deuxfleurs.fr`
|
||||
will be accessible either with hostname `deuxfleurs.fr.web.garage.eu`
|
||||
or with hostname `deuxfleurs.fr`.
|
||||
|
||||
#### `index`
|
||||
|
||||
The name of the index file to return for requests ending with `/` (usually `index.html`).
|
||||
@@ -1,6 +1,6 @@
|
||||
## S3 Compatibility status
|
||||
# S3 Compatibility status
|
||||
|
||||
### Global S3 features
|
||||
## Global S3 features
|
||||
|
||||
Implemented:
|
||||
|
||||
@@ -14,11 +14,12 @@ Not implemented:
|
||||
|
||||
- vhost-style URLs (`bucket.garage.tld/key`)
|
||||
- object-level ACL
|
||||
- object versioning
|
||||
- encryption
|
||||
- most `x-amz-` headers
|
||||
|
||||
|
||||
### Endpoint implementation
|
||||
## Endpoint implementation
|
||||
|
||||
All APIs that are not mentionned are not implemented and will return a 400 bad request.
|
||||
|
||||
@@ -54,6 +55,15 @@ Implemented.
|
||||
|
||||
Implemented.
|
||||
|
||||
#### GetBucketLocation
|
||||
|
||||
Implemented.
|
||||
|
||||
#### GetBucketVersioning
|
||||
|
||||
Stub implementation (Garage does not yet support versionning so this always returns
|
||||
"versionning not enabled").
|
||||
|
||||
#### GetObject
|
||||
|
||||
Implemented.
|
||||
@@ -66,6 +76,10 @@ Implemented.
|
||||
|
||||
Implemented.
|
||||
|
||||
#### ListBuckets
|
||||
|
||||
Implemented.
|
||||
|
||||
#### ListObjects
|
||||
|
||||
Implemented, but there isn't a very good specification of what `encoding-type=url` covers so there might be some encoding bugs. In our implementation the url-encoded fields are in the same in ListObjects as they are in ListObjectsV2.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
## Load Balancing Data (planned for version 0.2)
|
||||
# Load Balancing Data (planned for version 0.2)
|
||||
|
||||
I have conducted a quick study of different methods to load-balance data over different Garage nodes using consistent hashing.
|
||||
|
||||
### Requirements
|
||||
## Requirements
|
||||
|
||||
- *good balancing*: two nodes that have the same announced capacity should receive close to the same number of items
|
||||
|
||||
@@ -15,9 +15,9 @@ I have conducted a quick study of different methods to load-balance data over di
|
||||
replicas, independently of the order in which nodes were added/removed (this
|
||||
is to keep the implementation simple)
|
||||
|
||||
### Methods
|
||||
## Methods
|
||||
|
||||
#### Naive multi-DC ring walking strategy
|
||||
### Naive multi-DC ring walking strategy
|
||||
|
||||
This strategy can be used with any ring-like algorithm to make it aware of the *multi-datacenter* requirement:
|
||||
|
||||
@@ -38,7 +38,7 @@ This method was implemented in the first version of Garage, with the basic
|
||||
ring construction from Dynamo DB that consists in associating `n_token` random positions to
|
||||
each node (I know it's not optimal, the Dynamo paper already studies this).
|
||||
|
||||
#### Better rings
|
||||
### Better rings
|
||||
|
||||
The ring construction that selects `n_token` random positions for each nodes gives a ring of positions that
|
||||
is not well-balanced: the space between the tokens varies a lot, and some partitions are thus bigger than others.
|
||||
@@ -150,7 +150,7 @@ removing grisou gipsie : 49.22% 36.52% 12.79% 1.46%
|
||||
on average: 62.94% 27.89% 8.61% 0.57% <-- WORSE THAN PREVIOUSLY
|
||||
```
|
||||
|
||||
#### The magical solution: multi-DC aware MagLev
|
||||
### The magical solution: multi-DC aware MagLev
|
||||
|
||||
Suppose we want to select three replicas for each partition (this is what we do in our simulation and in most Garage deployments).
|
||||
We apply MagLev three times consecutively, one for each replica selection.
|
||||
|
||||
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 360 KiB After Width: | Height: | Size: 360 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
@@ -0,0 +1,10 @@
|
||||
*.bbl
|
||||
*.aux
|
||||
*.bcf
|
||||
*.blg
|
||||
*.log
|
||||
*.run.xml
|
||||
*.synctex.gz
|
||||
build/
|
||||
main.pdf
|
||||
figures/crdt.pdf
|
||||
@@ -0,0 +1,33 @@
|
||||
.PHONY: all viewpdf pdf clean
|
||||
|
||||
TARGET = main
|
||||
SOURCE_FILES = $(shell find . -type f -name "*.tex" -print)
|
||||
CLASS_FILES = $(shell find . -type f -name "*.cls" -print)
|
||||
BIB_FILES = $(shell find . -type f -name "*.bib" -print)
|
||||
FIGURES = $(shell find . -path "./figures/*" -type f -print)
|
||||
BUILD_PATH = build
|
||||
BUILD_FILES = $(shell find $(BUILD_PATH) -type f -print)
|
||||
|
||||
BIB_PROCESSOR := biber
|
||||
|
||||
.PHONY: all pdf clean figures
|
||||
|
||||
all: pdf
|
||||
|
||||
pdf: $(TARGET).pdf
|
||||
|
||||
clean:
|
||||
@rm $(TARGET).pdf $(BUILD_FILES) > /dev/null 2>&1 || exit 0
|
||||
|
||||
figures: figures/crdt.pdf
|
||||
|
||||
$(TARGET).pdf: figures $(FIGURES) $(SOURCE_FILES) $(BIB_FILES) $(CLASS_FILES)
|
||||
@mkdir -p $(BUILD_PATH) > /dev/null 2>&1 || exit 0
|
||||
@pdflatex -interaction=nonstopmode -jobname=$(TARGET) -output-directory $(BUILD_PATH) $(TARGET).tex
|
||||
@$(BIB_PROCESSOR) --output-directory $(BUILD_PATH) $(TARGET)
|
||||
@pdflatex -interaction=nonstopmode -jobname=$(TARGET) -output-directory $(BUILD_PATH) $(TARGET).tex # For biber
|
||||
@pdflatex -interaction=nonstopmode -jobname=$(TARGET) -output-directory $(BUILD_PATH) $(TARGET).tex # For biber
|
||||
@ln -fs $(BUILD_PATH)/$(TARGET).pdf $(TARGET).pdf
|
||||
|
||||
figures/crdt.pdf: figures/svg/crdt.svg
|
||||
@inkscape -C --file=$< --export-pdf=$@
|
||||
@@ -0,0 +1 @@
|
||||
Presentation of Garage by Adrien on April, 28th 2021, for his research team [Spirals](https://team.inria.fr/spirals/).
|
||||
@@ -0,0 +1,27 @@
|
||||
|
||||
@inproceedings{brewer_towards_2000,
|
||||
title = {Towards {{Robust Distributed Systems}}},
|
||||
booktitle = {{{ACM PODC}}},
|
||||
author = {Brewer, Eric},
|
||||
year = {2000}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@incollection{defago_conflict-free_2011,
|
||||
title = {Conflict-{{Free Replicated Data Types}}},
|
||||
booktitle = {Stabilization, {{Safety}}, and {{Security}} of {{Distributed Systems}}},
|
||||
author = {Shapiro, Marc and Pregui{\c c}a, Nuno and Baquero, Carlos and Zawirski, Marek},
|
||||
year = {2011},
|
||||
address = {{Berlin, Heidelberg}},
|
||||
}
|
||||
|
||||
|
||||
@inproceedings{decandia_dynamo:_2007,
|
||||
title = {Dynamo: {{Amazon}}'s {{Highly Available Key}}-Value {{Store}}},
|
||||
booktitle = {{ACM SOSP}},
|
||||
author = {DeCandia, Giuseppe and Hastorun, Deniz and Jampani, Madan and Kakulapati, Gunavardhan and Lakshman, Avinash and Pilchin, Alex and Sivasubramanian, Swaminathan and Vosshall, Peter and Vogels, Werner},
|
||||
year = {2007},
|
||||
address = {{New York, USA}},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
\section{Conclusion}
|
||||
|
||||
\begin{frame}{The future is cooler when we bend it our way}
|
||||
|
||||
Contributions welcome! :D
|
||||
|
||||
\end{frame}
|
||||
@@ -0,0 +1,124 @@
|
||||
\section{Escaping the cloud}
|
||||
|
||||
% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
% \begin{frame}{Down to Earth with home-hosting}
|
||||
|
||||
|
||||
% \todo{Stanley Parabole reference?}
|
||||
|
||||
% \end{frame}
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
\begin{frame}{Why?}
|
||||
|
||||
\begin{itemize}
|
||||
\item \textbf{Privacy}: no prying eyes besides your ISP
|
||||
\item \textbf{Control} of your infrastructure
|
||||
\item \textbf{Ecology}: reuse old hardware
|
||||
\end{itemize}
|
||||
|
||||
\vfill
|
||||
\begin{block}{\emph{Tim Berners-Lee} (1994)}
|
||||
``Now, if someone tries to monopolize the Web, for example pushes proprietary variations on network protocols, then that would make me unhappy.''
|
||||
\end{block}
|
||||
|
||||
\begin{itemize}
|
||||
\item Make Tim Berners-Lee happy
|
||||
\end{itemize}
|
||||
|
||||
\end{frame}
|
||||
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
\begin{frame}{What?}
|
||||
|
||||
\centering\Large
|
||||
A data store for commodity hardware on heterogenous household connections.
|
||||
|
||||
|
||||
\vfill\raggedright\normalsize
|
||||
|
||||
|
||||
\begin{block}{Targetting user-facing services}
|
||||
\begin{itemize}
|
||||
\item Static sites
|
||||
\item E-mails
|
||||
\item Instant communication
|
||||
%\item Video streaming % No need for a data store
|
||||
\item Collaboration
|
||||
\end{itemize}
|
||||
\end{block}
|
||||
\vfill
|
||||
|
||||
Nothing fancy like sensors data streams, AI or IoT.
|
||||
|
||||
\end{frame}
|
||||
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
\begin{frame}{What?}
|
||||
|
||||
|
||||
\begin{block}{Requirements}
|
||||
\begin{itemize}
|
||||
\item \textbf{No single point of failure} / flat hierarchy:
|
||||
|
||||
Any node can die for extended periods of time.
|
||||
\item \textbf{Multi-site}: cluster spans regions/countries.
|
||||
\item \textbf{Acceptable performance}.
|
||||
\item \textbf{Lightweight}: targets legacy hardware.
|
||||
\item \textbf{Conceptually simple}: built for low-tech organisations.
|
||||
|
||||
Adding/maintaining cluster nodes should be easy.
|
||||
\end{itemize}
|
||||
\end{block}
|
||||
\vfill
|
||||
|
||||
\begin{block}{Non-goals}
|
||||
\begin{itemize}
|
||||
\item \textbf{Super badass performance}.
|
||||
\item \textbf{NAT traversal} etc.: we require full-mesh connectivity.
|
||||
\end{itemize}
|
||||
\end{block}
|
||||
\end{frame}
|
||||
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
\begin{frame}{How?}
|
||||
|
||||
\begin{itemize}
|
||||
\item Theoretically possible with object storage \& CRDTs.
|
||||
\vfill
|
||||
\item Household uplinks are getting decent (optical fibers).
|
||||
\end{itemize}
|
||||
|
||||
|
||||
|
||||
\end{frame}
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
\begin{frame}{Research Questions}
|
||||
|
||||
\begin{itemize}
|
||||
\item Decent performance despite bad inter-node connectivity.
|
||||
\vfill
|
||||
|
||||
\item Tailoring workloads as a function of nodes' capabilities:
|
||||
|
||||
\begin{itemize}
|
||||
\item Make use of low-end nodes (e.g. Raspberry Pis),
|
||||
\item Avoid impeding global performance because of low-end nodes.
|
||||
\end{itemize}
|
||||
\vfill
|
||||
|
||||
\item Building CRDTs for target use-cases:
|
||||
|
||||
\begin{itemize}
|
||||
\item Software engineering: DSL or native code?
|
||||
\item Provide APIs to data store users? Risky?
|
||||
\end{itemize}
|
||||
\vfill
|
||||
|
||||
\item Cluster management: effortless UX, low perf. overhead.
|
||||
\end{itemize}
|
||||
\end{frame}
|
||||
|
After Width: | Height: | Size: 147 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 32 KiB |
@@ -0,0 +1,930 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="119.30722mm"
|
||||
height="121.49291mm"
|
||||
viewBox="0 0 119.30722 121.49291"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
|
||||
sodipodi:docname="conflict.svg">
|
||||
<defs
|
||||
id="defs2">
|
||||
<marker
|
||||
inkscape:stockid="Arrow1Mend"
|
||||
orient="auto"
|
||||
refY="0"
|
||||
refX="0"
|
||||
id="marker2999"
|
||||
style="overflow:visible"
|
||||
inkscape:isstock="true">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path2997"
|
||||
d="M 0,0 5,-5 -12.5,0 5,5 Z"
|
||||
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
|
||||
transform="matrix(-0.4,0,0,-0.4,-4,0)" />
|
||||
</marker>
|
||||
<marker
|
||||
inkscape:stockid="Arrow1Mend"
|
||||
orient="auto"
|
||||
refY="0"
|
||||
refX="0"
|
||||
id="marker2964"
|
||||
style="overflow:visible"
|
||||
inkscape:isstock="true">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path2962"
|
||||
d="M 0,0 5,-5 -12.5,0 5,5 Z"
|
||||
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
|
||||
transform="matrix(-0.4,0,0,-0.4,-4,0)" />
|
||||
</marker>
|
||||
<marker
|
||||
inkscape:stockid="Arrow1Mend"
|
||||
orient="auto"
|
||||
refY="0"
|
||||
refX="0"
|
||||
id="marker1467"
|
||||
style="overflow:visible"
|
||||
inkscape:isstock="true">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path1465"
|
||||
d="M 0,0 5,-5 -12.5,0 5,5 Z"
|
||||
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
|
||||
transform="matrix(-0.4,0,0,-0.4,-4,0)" />
|
||||
</marker>
|
||||
<marker
|
||||
inkscape:isstock="true"
|
||||
style="overflow:visible"
|
||||
id="marker1449"
|
||||
refX="0"
|
||||
refY="0"
|
||||
orient="auto"
|
||||
inkscape:stockid="Arrow1Mend">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
transform="matrix(-0.4,0,0,-0.4,-4,0)"
|
||||
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
|
||||
d="M 0,0 5,-5 -12.5,0 5,5 Z"
|
||||
id="path1447" />
|
||||
</marker>
|
||||
<marker
|
||||
inkscape:isstock="true"
|
||||
style="overflow:visible"
|
||||
id="marker12071"
|
||||
refX="0"
|
||||
refY="0"
|
||||
orient="auto"
|
||||
inkscape:stockid="Arrow1Mend"
|
||||
inkscape:collect="always">
|
||||
<path
|
||||
transform="matrix(-0.4,0,0,-0.4,-4,0)"
|
||||
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
|
||||
d="M 0,0 5,-5 -12.5,0 5,5 Z"
|
||||
id="path12069"
|
||||
inkscape:connector-curvature="0" />
|
||||
</marker>
|
||||
<marker
|
||||
inkscape:isstock="true"
|
||||
style="overflow:visible"
|
||||
id="marker9668"
|
||||
refX="0"
|
||||
refY="0"
|
||||
orient="auto"
|
||||
inkscape:stockid="Arrow1Mend"
|
||||
inkscape:collect="always">
|
||||
<path
|
||||
transform="matrix(-0.4,0,0,-0.4,-4,0)"
|
||||
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
|
||||
d="M 0,0 5,-5 -12.5,0 5,5 Z"
|
||||
id="path9666"
|
||||
inkscape:connector-curvature="0" />
|
||||
</marker>
|
||||
<marker
|
||||
inkscape:stockid="Arrow1Mend"
|
||||
orient="auto"
|
||||
refY="0"
|
||||
refX="0"
|
||||
id="marker6943"
|
||||
style="overflow:visible"
|
||||
inkscape:isstock="true">
|
||||
<path
|
||||
id="path6941"
|
||||
d="M 0,0 5,-5 -12.5,0 5,5 Z"
|
||||
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
|
||||
transform="matrix(-0.4,0,0,-0.4,-4,0)"
|
||||
inkscape:connector-curvature="0" />
|
||||
</marker>
|
||||
<marker
|
||||
inkscape:isstock="true"
|
||||
style="overflow:visible"
|
||||
id="marker5445"
|
||||
refX="0"
|
||||
refY="0"
|
||||
orient="auto"
|
||||
inkscape:stockid="Arrow1Mend">
|
||||
<path
|
||||
transform="matrix(-0.4,0,0,-0.4,-4,0)"
|
||||
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
|
||||
d="M 0,0 5,-5 -12.5,0 5,5 Z"
|
||||
id="path5443"
|
||||
inkscape:connector-curvature="0" />
|
||||
</marker>
|
||||
<marker
|
||||
inkscape:stockid="Arrow1Mend"
|
||||
orient="auto"
|
||||
refY="0"
|
||||
refX="0"
|
||||
id="marker1115"
|
||||
style="overflow:visible"
|
||||
inkscape:isstock="true"
|
||||
inkscape:collect="always">
|
||||
<path
|
||||
id="path861"
|
||||
d="M 0,0 5,-5 -12.5,0 5,5 Z"
|
||||
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
|
||||
transform="matrix(-0.4,0,0,-0.4,-4,0)"
|
||||
inkscape:connector-curvature="0" />
|
||||
</marker>
|
||||
<marker
|
||||
inkscape:stockid="Arrow1Mend"
|
||||
orient="auto"
|
||||
refY="0"
|
||||
refX="0"
|
||||
id="Arrow1Mend"
|
||||
style="overflow:visible"
|
||||
inkscape:isstock="true">
|
||||
<path
|
||||
id="path826"
|
||||
d="M 0,0 5,-5 -12.5,0 5,5 Z"
|
||||
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
|
||||
transform="matrix(-0.4,0,0,-0.4,-4,0)"
|
||||
inkscape:connector-curvature="0" />
|
||||
</marker>
|
||||
<marker
|
||||
inkscape:stockid="Arrow1Lend"
|
||||
orient="auto"
|
||||
refY="0"
|
||||
refX="0"
|
||||
id="Arrow1Lend"
|
||||
style="overflow:visible"
|
||||
inkscape:isstock="true">
|
||||
<path
|
||||
id="path820"
|
||||
d="M 0,0 5,-5 -12.5,0 5,5 Z"
|
||||
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
|
||||
transform="matrix(-0.8,0,0,-0.8,-10,0)"
|
||||
inkscape:connector-curvature="0" />
|
||||
</marker>
|
||||
<marker
|
||||
inkscape:stockid="Arrow1Lstart"
|
||||
orient="auto"
|
||||
refY="0"
|
||||
refX="0"
|
||||
id="marker1101"
|
||||
style="overflow:visible"
|
||||
inkscape:isstock="true">
|
||||
<path
|
||||
id="path1099"
|
||||
d="M 0,0 5,-5 -12.5,0 5,5 Z"
|
||||
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
|
||||
transform="matrix(0.8,0,0,0.8,10,0)"
|
||||
inkscape:connector-curvature="0" />
|
||||
</marker>
|
||||
<marker
|
||||
inkscape:stockid="Arrow1Lstart"
|
||||
orient="auto"
|
||||
refY="0"
|
||||
refX="0"
|
||||
id="Arrow1Lstart"
|
||||
style="overflow:visible"
|
||||
inkscape:isstock="true">
|
||||
<path
|
||||
id="path817"
|
||||
d="M 0,0 5,-5 -12.5,0 5,5 Z"
|
||||
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
|
||||
transform="matrix(0.8,0,0,0.8,10,0)"
|
||||
inkscape:connector-curvature="0" />
|
||||
</marker>
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="1.4"
|
||||
inkscape:cx="244.86596"
|
||||
inkscape:cy="205.06317"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer5"
|
||||
showgrid="false"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1055"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0"
|
||||
inkscape:snap-global="false" />
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer2"
|
||||
inkscape:label="Common"
|
||||
transform="translate(-43.663712,-16.067619)"
|
||||
style="display:inline">
|
||||
<path
|
||||
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.5291667;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 99.856918,34.684511 h 4.913692"
|
||||
id="path1257"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.79374999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Mend)"
|
||||
d="M 102.31376,23.208557 V 135.9963"
|
||||
id="path815"
|
||||
inkscape:connector-curvature="0" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
x="90.524658"
|
||||
y="19.57423"
|
||||
id="text838"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan836"
|
||||
x="90.524658"
|
||||
y="19.57423"
|
||||
style="font-weight:bold;stroke-width:0.26458332px"># vaccines</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
x="46.748138"
|
||||
y="19.57423"
|
||||
id="text13775"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan13773"
|
||||
x="46.748138"
|
||||
y="19.57423"
|
||||
style="font-style:italic;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Iwona;-inkscape-font-specification:'Iwona Italic';stroke-width:0.26458332px">Paris</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
x="148.20457"
|
||||
y="19.57423"
|
||||
id="text13798"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan13796"
|
||||
x="148.20457"
|
||||
y="19.57423"
|
||||
style="font-style:italic;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Iwona;-inkscape-font-specification:'Iwona Italic';stroke-width:0.26458332px">Lille</tspan></text>
|
||||
</g>
|
||||
<g
|
||||
inkscape:label="The problem"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
style="display:none"
|
||||
transform="translate(-43.663712,-16.067619)">
|
||||
<g
|
||||
id="g13753"
|
||||
transform="translate(0,12.170833)">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path850"
|
||||
d="M 97.820644,51.619712 75.828475,64.316897"
|
||||
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.52969581;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#marker1115)" />
|
||||
<text
|
||||
y="-108.69946"
|
||||
x="-183.00148"
|
||||
transform="rotate(180,91.874093,54.229103)"
|
||||
id="text7803"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
xml:space="preserve"><textPath
|
||||
id="textPath7973"
|
||||
xlink:href="#path850"><tspan
|
||||
id="tspan7801"
|
||||
style="font-weight:bold;stroke-width:0.26458332px">R</tspan></textPath></text>
|
||||
<path
|
||||
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.52969581;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#marker9668)"
|
||||
d="M 75.056888,72.78638 97.049057,85.483565"
|
||||
id="path9498"
|
||||
inkscape:connector-curvature="0" />
|
||||
<text
|
||||
transform="translate(9.4212508,3.3408691)"
|
||||
y="2.986012"
|
||||
x="8.5186663"
|
||||
id="text10668"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
xml:space="preserve"><textPath
|
||||
id="textPath10836"
|
||||
xlink:href="#path9498"><tspan
|
||||
id="tspan10666"
|
||||
style="font-weight:bold;stroke-width:0.26458332px">W</tspan></textPath></text>
|
||||
</g>
|
||||
<g
|
||||
id="g13763"
|
||||
transform="translate(0,12.170833)">
|
||||
<path
|
||||
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.52969581;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#marker12071)"
|
||||
d="m 106.80689,51.619712 21.99218,12.697185"
|
||||
id="path11899"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path11901"
|
||||
d="M 129.57066,72.78638 107.57848,85.483565"
|
||||
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.52969581;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#marker9668)" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
id="text13737"
|
||||
transform="rotate(-115.89278,108.31787,47.643413)"
|
||||
x="-372.04132"
|
||||
y="-217.15767"><textPath
|
||||
xlink:href="#path850"
|
||||
id="textPath13735"><tspan
|
||||
style="font-weight:bold;stroke-width:0.26458332px"
|
||||
id="tspan13733">R</tspan></textPath></text>
|
||||
<text
|
||||
transform="rotate(-58.856754,100.88692,38.870944)"
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
id="text13743"
|
||||
x="46.604572"
|
||||
y="6.6609678"><textPath
|
||||
xlink:href="#path9498"
|
||||
id="textPath13741"><tspan
|
||||
style="font-weight:bold;stroke-width:0.26458332px"
|
||||
id="tspan13739">W</tspan></textPath></text>
|
||||
</g>
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path13768"
|
||||
d="m 99.856918,112.89149 h 4.913692"
|
||||
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.5291667;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
x="43.564934"
|
||||
y="53.51746"
|
||||
id="text13802"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan13800"
|
||||
x="43.564934"
|
||||
y="53.51746"
|
||||
style="stroke-width:0.26458332px">We acquired 1000 doses!</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
x="49.979401"
|
||||
y="82.649841"
|
||||
id="text13806"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan13804"
|
||||
x="49.979401"
|
||||
y="82.649841"
|
||||
style="stroke-width:0.26458332px">x + 1000</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
x="136.842"
|
||||
y="82.649841"
|
||||
id="text13810"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan13808"
|
||||
x="136.842"
|
||||
y="82.649841"
|
||||
style="stroke-width:0.26458332px">x + 500</tspan></text>
|
||||
<text
|
||||
id="text13814"
|
||||
y="53.51746"
|
||||
x="112.88577"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
xml:space="preserve"><tspan
|
||||
style="stroke-width:0.26458332px"
|
||||
y="53.51746"
|
||||
x="112.88577"
|
||||
id="tspan13812"
|
||||
sodipodi:role="line">We acquired 500 doses!</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
x="108.70142"
|
||||
y="114.62009"
|
||||
id="text13818"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan13816"
|
||||
x="108.70142"
|
||||
y="114.62009"
|
||||
style="font-weight:bold;stroke-width:0.26458332px">???</tspan></text>
|
||||
<text
|
||||
id="text3109"
|
||||
y="36.388439"
|
||||
x="108.80019"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.23333311px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;display:inline;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
xml:space="preserve"><tspan
|
||||
style="font-weight:normal;font-size:4.93888903px;stroke-width:0.26458332px"
|
||||
y="36.388439"
|
||||
x="108.80019"
|
||||
id="tspan3107"
|
||||
sodipodi:role="line">T0: 5000</tspan></text>
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer3"
|
||||
inkscape:label="ACID"
|
||||
style="display:none">
|
||||
<rect
|
||||
ry="5.2916665"
|
||||
y="67.923912"
|
||||
x="61.174206"
|
||||
height="39.498508"
|
||||
width="54.42857"
|
||||
id="rect14092"
|
||||
style="opacity:1;vector-effect:none;fill:#f9f9f9;fill-opacity:0.97409327;fill-rule:nonzero;stroke:#000000;stroke-width:0.26458332;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.91191711;paint-order:fill markers stroke" />
|
||||
<rect
|
||||
style="opacity:1;vector-effect:none;fill:#f9f9f9;fill-opacity:0.97409327;fill-rule:nonzero;stroke:#000000;stroke-width:0.26458332;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.91191711;paint-order:fill markers stroke"
|
||||
id="rect14090"
|
||||
width="54.42857"
|
||||
height="39.498508"
|
||||
x="1.6973262"
|
||||
y="21.886396"
|
||||
ry="5.2916665" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path14037"
|
||||
d="m 56.193207,64.654408 h 4.913692"
|
||||
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.5291667;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.5291667;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 56.193207,110.69192 h 4.913692"
|
||||
id="path14040"
|
||||
inkscape:connector-curvature="0" />
|
||||
<g
|
||||
id="g14075"
|
||||
transform="translate(0,-1.9988201)">
|
||||
<g
|
||||
id="g13993"
|
||||
transform="translate(-43.663711,-25.06345)"
|
||||
style="display:inline">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path13977"
|
||||
d="M 97.820644,51.619712 75.828475,64.316897"
|
||||
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.52969581;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#marker1115)" />
|
||||
<text
|
||||
y="-108.69946"
|
||||
x="-183.00148"
|
||||
transform="rotate(180,91.874093,54.229103)"
|
||||
id="text13983"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
xml:space="preserve"><textPath
|
||||
id="textPath13981"
|
||||
xlink:href="#path850"><tspan
|
||||
id="tspan13979"
|
||||
style="font-weight:bold;stroke-width:0.26458332px">R</tspan></textPath></text>
|
||||
<path
|
||||
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.52969581;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#marker9668)"
|
||||
d="M 75.056888,72.78638 97.049057,85.483565"
|
||||
id="path13985"
|
||||
inkscape:connector-curvature="0" />
|
||||
<text
|
||||
transform="translate(9.4212508,3.3408691)"
|
||||
y="2.986012"
|
||||
x="8.5186663"
|
||||
id="text13991"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
xml:space="preserve"><textPath
|
||||
id="textPath13989"
|
||||
xlink:href="#path9498"><tspan
|
||||
id="tspan13987"
|
||||
style="font-weight:bold;stroke-width:0.26458332px">W</tspan></textPath></text>
|
||||
</g>
|
||||
<text
|
||||
id="text14044"
|
||||
y="29.833508"
|
||||
x="3.4844763"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
xml:space="preserve"><tspan
|
||||
style="stroke-width:0.26458332px"
|
||||
y="29.833508"
|
||||
x="3.4844763"
|
||||
id="tspan14042"
|
||||
sodipodi:role="line">AcqDoses(1000)</tspan></text>
|
||||
</g>
|
||||
<g
|
||||
id="g14088"
|
||||
transform="translate(0,-0.41130478)">
|
||||
<g
|
||||
id="g14011"
|
||||
transform="translate(-43.663711,19.386549)"
|
||||
style="display:inline">
|
||||
<path
|
||||
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.52969581;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#marker12071)"
|
||||
d="m 106.80689,51.619712 21.99218,12.697185"
|
||||
id="path13995"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path13997"
|
||||
d="M 129.57066,72.78638 107.57848,85.483565"
|
||||
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.52969581;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#marker9668)" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
id="text14003"
|
||||
transform="rotate(-115.89278,108.31787,47.643413)"
|
||||
x="-372.04132"
|
||||
y="-217.15767"><textPath
|
||||
xlink:href="#path850"
|
||||
id="textPath14001"><tspan
|
||||
style="font-weight:bold;stroke-width:0.26458332px"
|
||||
id="tspan13999">R</tspan></textPath></text>
|
||||
<text
|
||||
transform="rotate(-58.856754,100.88692,38.870944)"
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
id="text14009"
|
||||
x="46.604572"
|
||||
y="6.6609678"><textPath
|
||||
xlink:href="#path9498"
|
||||
id="textPath14007"><tspan
|
||||
style="font-weight:bold;stroke-width:0.26458332px"
|
||||
id="tspan14005">W</tspan></textPath></text>
|
||||
</g>
|
||||
<text
|
||||
id="text14044-8"
|
||||
y="74.283508"
|
||||
x="82.76487"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
xml:space="preserve"><tspan
|
||||
style="stroke-width:0.26458332px"
|
||||
y="74.283508"
|
||||
x="82.76487"
|
||||
id="tspan14042-8"
|
||||
sodipodi:role="line">AcqDoses(500)</tspan></text>
|
||||
</g>
|
||||
<text
|
||||
transform="translate(-43.663711,-16.067619)"
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.23333311px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
x="108.80019"
|
||||
y="82.757004"
|
||||
id="text832-6"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan830-8"
|
||||
x="108.80019"
|
||||
y="82.757004"
|
||||
style="font-weight:normal;font-size:4.93888903px;stroke-width:0.26458332px">T1: 6000</tspan></text>
|
||||
<text
|
||||
transform="translate(-43.663711,-16.067619)"
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.23333311px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
x="108.80019"
|
||||
y="128.8701"
|
||||
id="text832-3"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan830-83"
|
||||
x="108.80019"
|
||||
y="128.8701"
|
||||
style="font-weight:normal;font-size:4.93888903px;stroke-width:0.26458332px">T2: 6500</tspan></text>
|
||||
<text
|
||||
transform="translate(-43.663711,-16.067619)"
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.23333311px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;display:inline;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
x="108.80019"
|
||||
y="36.388439"
|
||||
id="text3117"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan3115"
|
||||
x="108.80019"
|
||||
y="36.388439"
|
||||
style="font-weight:normal;font-size:4.93888903px;stroke-width:0.26458332px">T0: 5000</tspan></text>
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer4"
|
||||
inkscape:label="BASE"
|
||||
style="display:none">
|
||||
<g
|
||||
id="g1463"
|
||||
transform="translate(0,-21.166667)">
|
||||
<path
|
||||
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.52969581;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#marker1449)"
|
||||
d="M 54.156933,47.722926 32.164764,60.420111"
|
||||
id="path1369"
|
||||
inkscape:connector-curvature="0" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
id="text1375"
|
||||
transform="rotate(180,70.308075,52.117558)"
|
||||
x="-279.42224"
|
||||
y="-209.36409"><textPath
|
||||
xlink:href="#path850"
|
||||
id="textPath1373"><tspan
|
||||
style="font-weight:bold;stroke-width:0.26458332px"
|
||||
id="tspan1371">R</tspan></textPath></text>
|
||||
</g>
|
||||
<g
|
||||
id="g1481"
|
||||
transform="translate(0,-21.166667)">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path1377"
|
||||
d="M 31.393177,68.889594 53.385346,81.586779"
|
||||
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.52969581;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#marker1467)" />
|
||||
<text
|
||||
transform="translate(-34.395836)"
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
id="text1383"
|
||||
x="17.939917"
|
||||
y="6.3268809"><textPath
|
||||
xlink:href="#path9498"
|
||||
id="textPath1381"><tspan
|
||||
style="font-weight:bold;stroke-width:0.26458332px"
|
||||
id="tspan1379">W</tspan></textPath></text>
|
||||
</g>
|
||||
<g
|
||||
style="display:inline"
|
||||
transform="translate(-43.663711,-25.063453)"
|
||||
id="g1403">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path1387"
|
||||
d="m 106.80689,51.619712 21.99218,12.697185"
|
||||
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.52969581;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#marker12071)" />
|
||||
<path
|
||||
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.52969581;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#marker9668)"
|
||||
d="M 129.57066,72.78638 107.57848,85.483565"
|
||||
id="path1389"
|
||||
inkscape:connector-curvature="0" />
|
||||
<text
|
||||
y="-217.15767"
|
||||
x="-372.04132"
|
||||
transform="rotate(-115.89278,108.31787,47.643413)"
|
||||
id="text1395"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
xml:space="preserve"><textPath
|
||||
id="textPath1393"
|
||||
xlink:href="#path850"><tspan
|
||||
id="tspan1391"
|
||||
style="font-weight:bold;stroke-width:0.26458332px">R</tspan></textPath></text>
|
||||
<text
|
||||
y="6.6609678"
|
||||
x="46.604572"
|
||||
id="text1401"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
xml:space="preserve"
|
||||
transform="rotate(-58.856754,100.88692,38.870944)"><textPath
|
||||
id="textPath1399"
|
||||
xlink:href="#path9498"><tspan
|
||||
id="tspan1397"
|
||||
style="font-weight:bold;stroke-width:0.26458332px">W</tspan></textPath></text>
|
||||
</g>
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path1503"
|
||||
d="m 56.193207,73.650233 h 4.913692"
|
||||
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.5291667;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<text
|
||||
id="text1507"
|
||||
y="75.354156"
|
||||
x="64.607315"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.23333311px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
xml:space="preserve"><tspan
|
||||
style="font-weight:normal;font-size:4.93888903px;stroke-width:0.26458332px"
|
||||
y="75.354156"
|
||||
x="64.607315"
|
||||
id="tspan1505"
|
||||
sodipodi:role="line">T1: [(T0, 6000),(T0, 5500)]</tspan></text>
|
||||
<g
|
||||
id="g1530"
|
||||
transform="translate(0,38.100002)">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path1522"
|
||||
d="M 54.156933,47.722926 32.164764,60.420111"
|
||||
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.52969581;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#marker1449)" />
|
||||
<text
|
||||
y="-209.36409"
|
||||
x="-279.42224"
|
||||
transform="rotate(180,70.308075,52.117558)"
|
||||
id="text1528"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
xml:space="preserve"><textPath
|
||||
id="textPath1526"
|
||||
xlink:href="#path850"><tspan
|
||||
id="tspan1524"
|
||||
style="font-weight:bold;stroke-width:0.26458332px">R</tspan></textPath></text>
|
||||
</g>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
x="8.5044641"
|
||||
y="109.58665"
|
||||
id="text1534"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan1532"
|
||||
x="8.5044641"
|
||||
y="109.58665"
|
||||
style="font-weight:normal;stroke-width:0.26458332px">Now solve this!</tspan><tspan
|
||||
sodipodi:role="line"
|
||||
x="8.5044641"
|
||||
y="115.76025"
|
||||
style="font-weight:normal;stroke-width:0.26458332px"
|
||||
id="tspan3159">And write back!</tspan></text>
|
||||
<text
|
||||
id="text1538"
|
||||
y="44.886383"
|
||||
x="6.3156896"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;display:inline;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
xml:space="preserve"><tspan
|
||||
style="stroke-width:0.26458332px"
|
||||
y="44.886383"
|
||||
x="6.3156896"
|
||||
id="tspan1536"
|
||||
sodipodi:role="line">x + 1000</tspan></text>
|
||||
<text
|
||||
id="text1542"
|
||||
y="44.886383"
|
||||
x="93.178284"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;display:inline;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
xml:space="preserve"><tspan
|
||||
style="stroke-width:0.26458332px"
|
||||
y="44.886383"
|
||||
x="93.178284"
|
||||
id="tspan1540"
|
||||
sodipodi:role="line">x + 500</tspan></text>
|
||||
<text
|
||||
transform="translate(-43.663711,-16.067619)"
|
||||
id="text3103"
|
||||
y="36.388439"
|
||||
x="108.80019"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.23333311px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;display:inline;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
xml:space="preserve"><tspan
|
||||
style="font-weight:normal;font-size:4.93888903px;stroke-width:0.26458332px"
|
||||
y="36.388439"
|
||||
x="108.80019"
|
||||
id="tspan3101"
|
||||
sodipodi:role="line">T0: (⊥, 5000)</tspan></text>
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer5"
|
||||
inkscape:label="CRDT"
|
||||
style="display:inline">
|
||||
<g
|
||||
style="display:inline"
|
||||
transform="translate(0,-21.166667)"
|
||||
id="g2920">
|
||||
<path
|
||||
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.52969581;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#marker1467)"
|
||||
d="M 31.393177,68.889594 53.385346,81.586779"
|
||||
id="path2912"
|
||||
inkscape:connector-curvature="0" />
|
||||
<text
|
||||
y="6.3268809"
|
||||
x="17.939917"
|
||||
id="text2918"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
xml:space="preserve"
|
||||
transform="translate(-34.395836)"><textPath
|
||||
id="textPath2916"
|
||||
xlink:href="#path9498"><tspan
|
||||
id="tspan2914"
|
||||
style="font-weight:bold;stroke-width:0.26458332px">W</tspan></textPath></text>
|
||||
</g>
|
||||
<g
|
||||
id="g3013">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path2924"
|
||||
d="M 85.906949,47.722927 63.914769,60.420112"
|
||||
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.52969581;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#marker2999)" />
|
||||
<text
|
||||
transform="rotate(-58.856754,57.228059,65.935492)"
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
id="text2936"
|
||||
x="7.1585193"
|
||||
y="48.970455"><textPath
|
||||
xlink:href="#path9498"
|
||||
id="textPath2934"><tspan
|
||||
style="font-weight:bold;stroke-width:0.26458332px"
|
||||
id="tspan2932">W</tspan></textPath></text>
|
||||
</g>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
x="15.648211"
|
||||
y="44.007793"
|
||||
id="text2976"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan2974"
|
||||
x="15.648211"
|
||||
y="44.007793"
|
||||
style="font-weight:normal;stroke-width:0.26458332px">incr(1000)</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
x="82.927979"
|
||||
y="44.007793"
|
||||
id="text2980"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan2978"
|
||||
x="82.927979"
|
||||
y="44.007793"
|
||||
style="font-weight:normal;stroke-width:0.26458332px">incr(500)</tspan></text>
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path2982"
|
||||
d="m 56.193207,72.591896 h 4.913692"
|
||||
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.5291667;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<text
|
||||
id="text2986"
|
||||
y="74.295815"
|
||||
x="65.136482"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.23333311px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
xml:space="preserve"><tspan
|
||||
style="font-weight:normal;font-size:4.93888903px;stroke-width:0.26458332px"
|
||||
y="74.295815"
|
||||
x="65.136482"
|
||||
sodipodi:role="line"
|
||||
id="tspan2995">T1: (5000, </tspan><tspan
|
||||
style="font-weight:normal;font-size:4.93888903px;stroke-width:0.26458332px"
|
||||
y="79.777985"
|
||||
x="65.136482"
|
||||
sodipodi:role="line"
|
||||
id="tspan1204"> {(a, 1000), (b, 500)})</tspan></text>
|
||||
<g
|
||||
style="display:inline"
|
||||
transform="translate(0,38.100003)"
|
||||
id="g3042">
|
||||
<path
|
||||
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.52969581;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#marker1449)"
|
||||
d="M 54.156933,47.722926 32.164764,60.420111"
|
||||
id="path3034"
|
||||
inkscape:connector-curvature="0" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
id="text3040"
|
||||
transform="rotate(180,70.308075,52.117558)"
|
||||
x="-279.42224"
|
||||
y="-209.36409"><textPath
|
||||
xlink:href="#path850"
|
||||
id="textPath3038"><tspan
|
||||
style="font-weight:bold;stroke-width:0.26458332px"
|
||||
id="tspan3036">R</tspan></textPath></text>
|
||||
</g>
|
||||
<text
|
||||
id="text3051"
|
||||
y="102.21611"
|
||||
x="17.764879"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
xml:space="preserve"><tspan
|
||||
style="font-weight:normal;stroke-width:0.26458332px"
|
||||
y="102.21611"
|
||||
x="17.764879"
|
||||
id="tspan3049"
|
||||
sodipodi:role="line">6500</tspan></text>
|
||||
<text
|
||||
transform="translate(-43.663711,-16.067619)"
|
||||
id="text3097"
|
||||
y="36.388439"
|
||||
x="108.80019"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.23333311px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
xml:space="preserve"><tspan
|
||||
style="font-weight:normal;font-size:4.93888903px;stroke-width:0.26458332px"
|
||||
y="36.388439"
|
||||
x="108.80019"
|
||||
id="tspan3095"
|
||||
sodipodi:role="line">T0: (5000, ∅)</tspan></text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 52 KiB |
@@ -0,0 +1,227 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="82.010674mm"
|
||||
height="17.561125mm"
|
||||
viewBox="0 0 82.010674 17.561125"
|
||||
version="1.1"
|
||||
id="svg1579"
|
||||
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
|
||||
sodipodi:docname="crdt.svg">
|
||||
<defs
|
||||
id="defs1573">
|
||||
<marker
|
||||
inkscape:isstock="true"
|
||||
style="overflow:visible"
|
||||
id="marker2645"
|
||||
refX="0"
|
||||
refY="0"
|
||||
orient="auto"
|
||||
inkscape:stockid="Arrow2Mend">
|
||||
<path
|
||||
transform="scale(-0.6)"
|
||||
d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
|
||||
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
|
||||
id="path2643"
|
||||
inkscape:connector-curvature="0" />
|
||||
</marker>
|
||||
<marker
|
||||
inkscape:isstock="true"
|
||||
style="overflow:visible"
|
||||
id="marker2605"
|
||||
refX="0"
|
||||
refY="0"
|
||||
orient="auto"
|
||||
inkscape:stockid="Arrow2Mstart">
|
||||
<path
|
||||
transform="scale(0.6)"
|
||||
d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
|
||||
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
|
||||
id="path2603"
|
||||
inkscape:connector-curvature="0" />
|
||||
</marker>
|
||||
<marker
|
||||
inkscape:stockid="Arrow2Mend"
|
||||
orient="auto"
|
||||
refY="0"
|
||||
refX="0"
|
||||
id="Arrow2Mend"
|
||||
style="overflow:visible"
|
||||
inkscape:isstock="true"
|
||||
inkscape:collect="always">
|
||||
<path
|
||||
id="path2206"
|
||||
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
|
||||
d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
|
||||
transform="scale(-0.6)"
|
||||
inkscape:connector-curvature="0" />
|
||||
</marker>
|
||||
<marker
|
||||
inkscape:stockid="Arrow2Mstart"
|
||||
orient="auto"
|
||||
refY="0"
|
||||
refX="0"
|
||||
id="Arrow2Mstart"
|
||||
style="overflow:visible"
|
||||
inkscape:isstock="true"
|
||||
inkscape:collect="always">
|
||||
<path
|
||||
id="path2203"
|
||||
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
|
||||
d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
|
||||
transform="scale(0.6)"
|
||||
inkscape:connector-curvature="0" />
|
||||
</marker>
|
||||
<marker
|
||||
inkscape:stockid="Arrow2Lstart"
|
||||
orient="auto"
|
||||
refY="0"
|
||||
refX="0"
|
||||
id="Arrow2Lstart"
|
||||
style="overflow:visible"
|
||||
inkscape:isstock="true">
|
||||
<path
|
||||
id="path2197"
|
||||
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
|
||||
d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
|
||||
transform="matrix(1.1,0,0,1.1,1.1,0)"
|
||||
inkscape:connector-curvature="0" />
|
||||
</marker>
|
||||
<marker
|
||||
inkscape:stockid="Arrow1Sstart"
|
||||
orient="auto"
|
||||
refY="0"
|
||||
refX="0"
|
||||
id="Arrow1Sstart"
|
||||
style="overflow:visible"
|
||||
inkscape:isstock="true">
|
||||
<path
|
||||
id="path2191"
|
||||
d="M 0,0 5,-5 -12.5,0 5,5 Z"
|
||||
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.00000003pt;stroke-opacity:1"
|
||||
transform="matrix(0.2,0,0,0.2,1.2,0)"
|
||||
inkscape:connector-curvature="0" />
|
||||
</marker>
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="2.8"
|
||||
inkscape:cx="148.82928"
|
||||
inkscape:cy="-32.722297"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
inkscape:snap-bbox="true"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1055"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1" />
|
||||
<metadata
|
||||
id="metadata1576">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-21.438257,-19.857903)">
|
||||
<path
|
||||
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.26458332;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#Arrow2Mstart);marker-end:url(#Arrow2Mend)"
|
||||
d="M 44.979166,28.87314 H 58.822544"
|
||||
id="path2177"
|
||||
inkscape:connector-curvature="0" />
|
||||
<g
|
||||
id="g2153">
|
||||
<rect
|
||||
rx="0"
|
||||
ry="3.8793426"
|
||||
y="19.990194"
|
||||
x="21.570549"
|
||||
height="17.296541"
|
||||
width="23.537758"
|
||||
id="rect2124"
|
||||
style="opacity:1;vector-effect:none;fill:#f9f9f9;fill-opacity:0.97409327;fill-rule:nonzero;stroke:#000000;stroke-width:0.26458332;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.91191711;paint-order:fill markers stroke" />
|
||||
<text
|
||||
id="text2132"
|
||||
y="29.799105"
|
||||
x="29.366098"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
xml:space="preserve"><tspan
|
||||
style="font-weight:normal;stroke-width:0.26458332px"
|
||||
y="29.799105"
|
||||
x="29.366098"
|
||||
id="tspan2130"
|
||||
sodipodi:role="line">App</tspan></text>
|
||||
</g>
|
||||
<rect
|
||||
rx="0"
|
||||
ry="3.8793426"
|
||||
y="19.990194"
|
||||
x="58.612213"
|
||||
height="17.296541"
|
||||
width="23.537758"
|
||||
id="rect2155"
|
||||
style="opacity:1;vector-effect:none;fill:#f9f9f9;fill-opacity:0.97409327;fill-rule:nonzero;stroke:#000000;stroke-width:0.26458332;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.91191711;paint-order:fill markers stroke" />
|
||||
<text
|
||||
id="text2159"
|
||||
y="30.367077"
|
||||
x="63.842014"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
xml:space="preserve"><tspan
|
||||
style="font-weight:normal;stroke-width:0.26458332px"
|
||||
y="30.367077"
|
||||
x="63.842014"
|
||||
id="tspan2157"
|
||||
sodipodi:role="line">CRDT</tspan></text>
|
||||
<g
|
||||
id="g4137"
|
||||
transform="translate(-15.875)">
|
||||
<rect
|
||||
rx="0"
|
||||
ry="3.8793426"
|
||||
y="19.990194"
|
||||
x="95.653885"
|
||||
height="17.296541"
|
||||
width="23.537758"
|
||||
id="rect2163"
|
||||
style="opacity:1;vector-effect:none;fill:#f9f9f9;fill-opacity:0.97409327;fill-rule:nonzero;stroke:#000000;stroke-width:0.26458332;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.91191711;paint-order:fill markers stroke" />
|
||||
<text
|
||||
id="text2167"
|
||||
y="30.367077"
|
||||
x="96.969627"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:4.93888855px;line-height:125%;font-family:Iwona;-inkscape-font-specification:'Iwona, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
xml:space="preserve"><tspan
|
||||
style="font-weight:normal;stroke-width:0.26458332px"
|
||||
y="30.367077"
|
||||
x="96.969627"
|
||||
id="tspan2165"
|
||||
sodipodi:role="line">Datastore</tspan></text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 9.4 KiB |
@@ -0,0 +1,147 @@
|
||||
\section{Introducing Garage}
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
\begin{frame}{Brought to you by the Deuxfleurs association}
|
||||
|
||||
\begin{block}{\textbf{deuxfleurs.fr} -- a libre hosting association with a vision}
|
||||
``Shifting the current structure of the Internet from a world of a few very large service providers, to a world where services are hosted by a variety of smaller organisations.''
|
||||
\end{block}
|
||||
|
||||
|
||||
\begin{block}{Our goals}
|
||||
\begin{itemize}
|
||||
\item To propose performant \& reliable libre services for the masses
|
||||
\item To host and administer our infrastructure ourselves
|
||||
\item To allow members to contribute storage/compute nodes
|
||||
\item Resilience: for availability \& the sysadmins' sleep
|
||||
\item Conceptual simplicity to ease onboarding \& demistify hosting
|
||||
\end{itemize}
|
||||
\end{block}
|
||||
|
||||
|
||||
\end{frame}
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
\begin{frame}{The lacking state of the practice}
|
||||
|
||||
\begin{block}{Object storage fitted our needs}
|
||||
\begin{itemize}
|
||||
\item Distributed by design
|
||||
\item Objects are replicated
|
||||
\item Conceptually simple
|
||||
\end{itemize}
|
||||
\end{block}
|
||||
\vfill
|
||||
|
||||
\begin{block}{Existing object stores did not}
|
||||
\begin{itemize}
|
||||
\item Too specific / complex
|
||||
\item Resource hungry
|
||||
\item Hidden constraints
|
||||
\end{itemize}
|
||||
\end{block}
|
||||
\vfill
|
||||
|
||||
We developed Garage, an object store with minimal functionality.
|
||||
|
||||
It works, and serves our static sites and media.
|
||||
|
||||
\end{frame}
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
\begin{frame}{Introducing Garage}
|
||||
|
||||
\centering
|
||||
\url{garagehq.deuxfleurs.fr}
|
||||
|
||||
\url{git.deuxfleurs.fr/Deuxfleurs/garage}
|
||||
|
||||
\includegraphics[width=.4\columnwidth]{figures/garage_distributed.png}
|
||||
\vfill
|
||||
|
||||
\raggedright
|
||||
\begin{itemize}
|
||||
\item Distributed data store
|
||||
\item Based on DynamoDB object store (P2P!)
|
||||
\item Modular data types/protocols with CRDTs:
|
||||
\begin{itemize}
|
||||
\item Done: objects (media, static sites, backups...) via S3 API
|
||||
\item To do: e-mails via IMAP protocol, and more
|
||||
\end{itemize}
|
||||
\end{itemize}
|
||||
|
||||
\end{frame}
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
\begin{frame}[t]{The \textbf{RING}}
|
||||
|
||||
\centering
|
||||
\fullcite{decandia_dynamo:_2007}
|
||||
\vspace{3ex}
|
||||
|
||||
\only<1>{\includegraphics[width=.5\columnwidth]{figures/c1.pdf}}%
|
||||
\only<2>{\includegraphics[width=.5\columnwidth]{figures/c2.pdf}}%
|
||||
\only<3>{\includegraphics[width=.5\columnwidth]{figures/c3.pdf}}%
|
||||
\only<4>{\includegraphics[width=.5\columnwidth]{figures/c4.pdf}}%
|
||||
\vspace{5ex}
|
||||
|
||||
%\raggedright
|
||||
\only<1>{Each node is assigned a unique ID on the circular address space.}%
|
||||
\only<2-3>{When a new object is added to the store...}%
|
||||
\only<3>{\\ It is assigned a unique ID (its \emph{key}) on the address space.}%
|
||||
\only<4>{The $R$ nodes after the object are in charge of replicating it.}%
|
||||
|
||||
\end{frame}
|
||||
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
\begin{frame}{Distributed metadata}
|
||||
|
||||
\centering
|
||||
\includegraphics[width=.8\columnwidth]{figures/garage_tables.pdf}
|
||||
\vfill
|
||||
|
||||
The objects, versions and blocks are all stored in the ring.
|
||||
|
||||
\end{frame}
|
||||
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
\begin{frame}{Written in Rust}
|
||||
|
||||
\begin{columns}
|
||||
\column{.65\columnwidth}
|
||||
Entirely written in Rust!
|
||||
\column{.35\columnwidth}
|
||||
\centering
|
||||
\includegraphics[width=.85\columnwidth]{figures/rustacean-flat-happy.png}
|
||||
\end{columns}
|
||||
\vfill
|
||||
|
||||
\begin{columns}
|
||||
\column[t]{.6\columnwidth}
|
||||
\textbf{Pros:}
|
||||
\begin{itemize}
|
||||
\item Compiled and fast
|
||||
\item Features prevent usual mistakes:
|
||||
|
||||
strongly typed, immutable by default, ownership instead of GC...
|
||||
|
||||
\item Best of several paradigms:
|
||||
|
||||
imperative, OO, functional
|
||||
|
||||
\item Good libraries for network programmings:
|
||||
|
||||
serialization, http, async/await...
|
||||
\end{itemize}
|
||||
\column[t]{.4\columnwidth}
|
||||
\textbf{Cons}:
|
||||
\begin{itemize}
|
||||
\item Steep learning curve
|
||||
\item Long compilation times
|
||||
\item Compiler rage
|
||||
\end{itemize}
|
||||
\end{columns}
|
||||
|
||||
\end{frame}
|
||||
@@ -0,0 +1,134 @@
|
||||
%% Imports %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
\usepackage{graphicx}
|
||||
\usepackage[utf8]{inputenc}
|
||||
\usepackage{csquotes} % Elegant quotes (must be loaded before babel)
|
||||
\usepackage[english]{babel}
|
||||
\usepackage[T1]{fontenc}
|
||||
% \usepackage{amsfonts}
|
||||
% \usepackage{amssymb}
|
||||
%\usepackage{lmodern}
|
||||
\usepackage{iwona}
|
||||
\usepackage{color}
|
||||
\usepackage{xspace}
|
||||
\usepackage{amsmath}
|
||||
\usepackage{hanging}
|
||||
\usepackage{listings}
|
||||
\lstset{basicstyle=\scriptsize}
|
||||
\usepackage{tikz}
|
||||
\usepackage{tikzsymbols} % For emojis
|
||||
%\usepackage{setspace} % Activation disables footnoes
|
||||
\usepackage{perpage} % Reset footnote counter every page
|
||||
\MakePerPage{footnote}
|
||||
\usepackage{nameref} % For printing the sections' name
|
||||
\usepackage{hyperref}
|
||||
|
||||
%% Biblio %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
\usepackage[style=alphabetic,giveninits=true,sorting=none,hyperref,backend=biber,maxnames=3]{biblatex}
|
||||
\addbibresource{bibliography.bib}
|
||||
|
||||
%% Templating %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
\usepackage{pgf}
|
||||
\usepackage{xifthen, tikz}
|
||||
|
||||
\usetheme{default}
|
||||
|
||||
\makeatletter
|
||||
|
||||
|
||||
%%% colors
|
||||
\definecolor{bleuroi}{RGB}{0, 8, 88}
|
||||
\definecolor{vertemeraude}{RGB}{0, 64, 44}
|
||||
\definecolor{lightgray}{RGB}{245,245,245}
|
||||
\definecolor{encre_de_chine}{RGB}{9,36,53}
|
||||
\definecolor{im_lost_in_your_eyes}{RGB}{110,138,159}
|
||||
\definecolor{metallic_grey}{RGB}{209,219,221}
|
||||
\definecolor{metallic_white}{RGB}{229,239,241}
|
||||
\definecolor{boss}{RGB}{229,72,27}
|
||||
\definecolor{toon_eyes}{RGB}{3,4,6}
|
||||
|
||||
\setbeamercolor{frametitle}{bg=bleuroi,fg=white}
|
||||
\setbeamercolor*{normal text}{fg=toon_eyes,bg=white}
|
||||
\setbeamercolor*{block title}{fg=white,bg=vertemeraude}
|
||||
\setbeamercolor*{block body}{fg=toon_eyes,bg=vertemeraude!5}
|
||||
\setbeamercolor{alerted text}{fg=vertemeraude}
|
||||
\setbeamercolor*{example text}{fg=toon_eyes,bg=white}
|
||||
\setbeamercolor*{structure}{fg=bleuroi,bg=white}
|
||||
% sectionnavigation:
|
||||
\setbeamercolor*{section in head/foot}{fg=bleuroi,bg=white}
|
||||
|
||||
% \let\oldtextbf\textbf
|
||||
% \renewcommand{\textbf}[1]{\textcolor{bleuroi}{\oldtextbf{#1}}}
|
||||
|
||||
%%% Rounded boxes with no shading
|
||||
\pgfdeclareverticalshading[lower.bg,upper.bg]{bmb@transition}{200cm}{%
|
||||
color(0pt)=(upper.bg); color(2pt)=(upper.bg); color(4pt)=(upper.bg)}
|
||||
\setbeamertemplate{blocks}[rounded][shadow=false]
|
||||
|
||||
%%% Who on Earth uses navigation bars?
|
||||
\setbeamertemplate{navigation symbols}{}
|
||||
|
||||
|
||||
%%% Headline with sections
|
||||
\setbeamertemplate{headline}
|
||||
{%
|
||||
\begin{beamercolorbox}[wd=\paperwidth,dp=.5ex,ht=2ex]{section in head/foot}
|
||||
\insertsectionnavigationhorizontal{\paperwidth}{\hskip0pt plus1fill}{\hskip0pt plus1fill}\par % Centered
|
||||
%\insertsectionnavigationhorizontal{\paperwidth}{}{\hfill\hfill} % Left aligned
|
||||
\end{beamercolorbox}%
|
||||
}
|
||||
% Set the colors of the section bar
|
||||
\usesectionheadtemplate
|
||||
{\colorbox{fg}{\color{bg} \insertsectionhead}}
|
||||
{\color{fg!40!bg} \insertsectionhead}
|
||||
|
||||
% \newlength\lpg@linewd
|
||||
% \setbox0=\hbox{\strut}
|
||||
% \newlength\strutht \strutht\ht0
|
||||
% \newlength\strutdp \strutdp\dp0
|
||||
|
||||
\setbeamertemplate{frametitle}
|
||||
{
|
||||
\begin{beamercolorbox}[wd=\paperwidth,dp=1ex,ht=2.6ex,leftskip=.7cm,rightskip=.7cm]{frametitle}
|
||||
%\vskip-1ex
|
||||
%[wd=\lpg@linewd, ht=\strutht, dp=\strutdp]{frametitle}
|
||||
%\vskip-.8ex\hskip0.7cm
|
||||
%\vskip-.8ex
|
||||
\usebeamerfont{frametitle}\strut\insertframetitle
|
||||
\ifx\insertframesubtitle\@empty
|
||||
\else
|
||||
\hfill \usebeamerfont{framesubtitle}\strut\insertframesubtitle
|
||||
\fi%
|
||||
\end{beamercolorbox}
|
||||
}
|
||||
|
||||
|
||||
%%% Page numbering in footline
|
||||
\setbeamertemplate{footline}{
|
||||
\hbox{\begin{beamercolorbox}[wd=1\paperwidth,ht=2.5ex,dp=1ex,right]{framenumber in head/foot}%
|
||||
\usebeamerfont{framenumber in head/foot}%
|
||||
%\insertframenumber\space/\space\inserttotalframenumber\hspace{0.3em}
|
||||
\insertframenumber\hspace{0.5em}
|
||||
\end{beamercolorbox}}}
|
||||
|
||||
%%% Reduce foot citation size
|
||||
\renewcommand{\footnotesize}{\tiny}
|
||||
|
||||
%%% Display footnotes (not working, still no footnotes)
|
||||
% \setbeamertemplate{footnote}{%
|
||||
% \hangpara{2em}{1}%
|
||||
% \makebox[2em][l]{\insertfootnotemark}\footnotesize\insertfootnotetext\par%
|
||||
% }
|
||||
|
||||
|
||||
\makeatother
|
||||
|
||||
%% Commentaires %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
\newcommand{\annote}[3]{{
|
||||
\colorbox{#3}{\bfseries\sffamily\footnotesize\textcolor{white}{#2}}
|
||||
\color{#3}
|
||||
$\blacktriangleright$\textit{#1}$\blacktriangleleft$}
|
||||
}
|
||||
%% Uncomment for final version (removes notes)
|
||||
%\renewcommand{\annote}[3]{}
|
||||
|
||||
\newcommand{\todo}[1]{\annote{#1}{TODO}{green}}
|
||||
@@ -0,0 +1,99 @@
|
||||
\section{Introduction}
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
\begin{frame}{A very casual motivation}
|
||||
|
||||
|
||||
\begin{center}
|
||||
\Large
|
||||
I want to host \textbf{resilient web services} with \textbf{acceptable performance} on commodity hardware behind \textbf{household networks}.
|
||||
\end{center}
|
||||
\vfill
|
||||
|
||||
\begin{block}{Keywords}
|
||||
\begin{columns}
|
||||
\column{.5\columnwidth}
|
||||
\begin{itemize}
|
||||
\item Decentralised networks
|
||||
\item Edge computing
|
||||
\end{itemize}
|
||||
\column{.5\columnwidth}
|
||||
\begin{itemize}
|
||||
\item Distributed storage
|
||||
\item Privacy
|
||||
\end{itemize}
|
||||
\end{columns}
|
||||
\end{block}
|
||||
|
||||
\end{frame}
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
\begin{frame}{Context}
|
||||
|
||||
\textbf{Resilience}: Ability to recover quickly from failures and changes.
|
||||
\vspace{1ex}
|
||||
|
||||
Only achievable through distribution of the hosted applications across several physical locations.
|
||||
\vfill
|
||||
|
||||
|
||||
\begin{block}{Application = \textbf{computations} on \textbf{data}}
|
||||
\begin{itemize}
|
||||
\item \textbf{Computation}: Stateless; easy to distribute \& orchestrate.
|
||||
% where it is performed does not matter as long as the application's state is accessible R/W
|
||||
%Computation units are "easy" to distribute and orchestrate.
|
||||
|
||||
\item \textbf{Data}: Stateful; hard to distribute \& full of trade-offs.
|
||||
\end{itemize}
|
||||
\end{block}
|
||||
|
||||
\end{frame}
|
||||
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
\begin{frame}{Concurrent writes example}{How to lose vaccines}
|
||||
|
||||
\centering
|
||||
|
||||
\includegraphics[width=.5\columnwidth]{figures/conflict_problem.pdf}
|
||||
\end{frame}
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
\begin{frame}{The problem}
|
||||
|
||||
% \textbf{Path dependency}: existing data stores are built for data centres.
|
||||
|
||||
% $\implies$ They assume good inter-node connectivity.
|
||||
% \vfill
|
||||
|
||||
\begin{center}
|
||||
\Large
|
||||
Can we design an available data store tailored for adverse network conditions?
|
||||
\end{center}
|
||||
|
||||
\end{frame}
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
% Maybe more framing of the context. What kind of data storage? Object vs Block vs what?
|
||||
% \begin{frame}{``Stateless'', ``serverless'', and the elephant in the room}
|
||||
|
||||
% It seems easy to deploy \& administer web services nowadays ...
|
||||
|
||||
% Because the inherent complexity is shadowed by proprietary ``cloud'' solutions.
|
||||
|
||||
% The IT crowd can gloss over ``statelessness'' \emph{ad nauseam} ...
|
||||
|
||||
% But storing \emph{state} remains an open research problem.
|
||||
|
||||
% Data storage is either:
|
||||
|
||||
% \begin{itemize}
|
||||
% \item A single point of failure;
|
||||
% \item Delegated to proprietary solutions;
|
||||
% \item Pain.
|
||||
% \end{itemize}
|
||||
|
||||
% Today, we will review networked storage's history, and discuss open research questions.
|
||||
|
||||
% \end{frame}
|
||||
@@ -0,0 +1,70 @@
|
||||
\documentclass[compress]{beamer}
|
||||
|
||||
\input{header.tex}
|
||||
|
||||
%% Metadata %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
\title{Distributed object storage is centralised}
|
||||
\subtitle{A quest for autonomy in the modern hosting ecology}
|
||||
\author{Adrien Luxey}
|
||||
\date{Wednesday, 28th April, 2021}
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
%% Begin document %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
\begin{document}
|
||||
|
||||
%% Title page %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
{
|
||||
\setbeamertemplate{footline}{}
|
||||
\setbeamertemplate{headline}{}
|
||||
\begin{frame}
|
||||
\titlepage
|
||||
\end{frame}
|
||||
}
|
||||
|
||||
%% Contents %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
\input{introduction.tex}
|
||||
\input{sota.tex}
|
||||
\input{escaping_the_cloud.tex}
|
||||
\input{garage.tex}
|
||||
\input{conclusion.tex}
|
||||
|
||||
|
||||
%% Bibliography %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
|
||||
% \begin{frame}
|
||||
% \large
|
||||
% \raggedright
|
||||
% Références
|
||||
|
||||
% \printbibliography
|
||||
% \end{frame}
|
||||
|
||||
%% Ending %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
{
|
||||
\setbeamertemplate{footline}{}
|
||||
\setbeamertemplate{headline}{}
|
||||
\begin{frame}
|
||||
\centering
|
||||
|
||||
\vspace{1cm}
|
||||
\Large Thank you for your attention.
|
||||
|
||||
\vspace{2cm}
|
||||
\large Now let's chat!
|
||||
|
||||
\vspace{1cm}
|
||||
\raggedleft
|
||||
\includegraphics[width=.2\textwidth]{figures/m_proxy_gray.pdf}
|
||||
\end{frame}
|
||||
}
|
||||
|
||||
%% Appendices %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
|
||||
% \appendix
|
||||
% \input{sprinkler_appendix.tex}
|
||||
% \input{cascade_appendix.tex}
|
||||
% \input{spores_appendix.tex}
|
||||
|
||||
|
||||
\end{document}
|
||||
@@ -0,0 +1,323 @@
|
||||
\section{State of the art}
|
||||
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
\begin{frame}{The CAP theorem}{Consistency vs. Availability}
|
||||
|
||||
\begin{block}{Eric Brewer's theorem}
|
||||
``A shared-state system can have \textbf{at most two} of the following properties at any given time:
|
||||
|
||||
\begin{itemize}
|
||||
\item \textbf{C}onsistency
|
||||
\item \textbf{A}vailability
|
||||
\item \textbf{P}artition tolerance''
|
||||
\end{itemize}
|
||||
\end{block}
|
||||
|
||||
|
||||
\begin{center}
|
||||
\Large
|
||||
Under network partitions, a distributed data store has to sacrifice either availability or consistency.
|
||||
\end{center}
|
||||
\vfill
|
||||
|
||||
\begin{itemize}
|
||||
\item \textbf{Consistency-first}: Abort incoming queries;
|
||||
\item \textbf{Availability-first}: Return possibly stale data.
|
||||
\end{itemize}
|
||||
|
||||
\end{frame}
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
\begin{frame}{Consistency-first: the ACID model}{Consistency vs. Availability}
|
||||
|
||||
\textbf{Transaction}: unit of work within an ACID data store.
|
||||
%Comprises multiple operations.
|
||||
%E.g. bank transfer.
|
||||
%E.g. a bank transfer from A to B is a transaction involving two operations: withdraw money from A & credit B with the same money amount.
|
||||
\vfill
|
||||
|
||||
\begin{itemize}
|
||||
\item \textbf{\underline{A}tomicity}: Transactions either complete entirely or fail.
|
||||
|
||||
No transaction ever seen as in-progress.
|
||||
|
||||
\item \textbf{\underline{C}onsistency}: Transactions always generate a valid state.
|
||||
|
||||
The database maintains its invariants across transactions.
|
||||
|
||||
\item \textbf{\underline{I}solation}: Concurrent transactions are seen as sequential.
|
||||
|
||||
Transactions are serializable, or sequentially consistent.
|
||||
|
||||
\item \textbf{\underline{D}urability}: Committed transactions are never forgotten.
|
||||
\end{itemize}
|
||||
\vfill\centering
|
||||
|
||||
Reads are fast, writes are slow.
|
||||
|
||||
\vfill\raggedright
|
||||
|
||||
Example: relational databases.
|
||||
\end{frame}
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
\begin{frame}[fragile]{Concurrent writes in ACID}{Consistency vs. Availability}
|
||||
|
||||
|
||||
\begin{columns}
|
||||
\column{.5\columnwidth}
|
||||
\begin{block}{}
|
||||
\begin{lstlisting}
|
||||
transaction AcqDoses(y):
|
||||
x <- SELECT #vaccines;
|
||||
UPDATE #vaccines = (x + y);
|
||||
\end{lstlisting}
|
||||
\end{block}
|
||||
\vspace{5ex}
|
||||
|
||||
Supports compound operations.
|
||||
\column{.5\columnwidth}
|
||||
\centering
|
||||
\includegraphics[width=\columnwidth]{figures/conflict_acid.pdf}
|
||||
\end{columns}
|
||||
|
||||
\end{frame}
|
||||
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
\begin{frame}{Availability-first: the BASE model}{Consistency vs. Availability}
|
||||
|
||||
|
||||
Some apps prefer availability, e.g. Amazon products' reviews.
|
||||
\vfill
|
||||
|
||||
The BASE model trades Consistency \& Isolation for Availability.
|
||||
|
||||
|
||||
%Some applications do not care about strong consistency and prefer being highly available (e.g. Amazon's product reviews).
|
||||
|
||||
%In order to achieve higher availability, the BASE model relaxes consistency constraints of the ACID model: "eventual consistency".
|
||||
\vfill
|
||||
|
||||
\begin{itemize}
|
||||
\item \textbf{\underline{B}asic \underline{A}vailability}:
|
||||
The data store thrives to be available.
|
||||
|
||||
\item \textbf{\underline{S}oft-state}:
|
||||
Replicas can disagree on the valid state.
|
||||
|
||||
\item \textbf{\underline{E}ventual consistency}:
|
||||
In the absence of write queries,
|
||||
the data store will eventually converge to a single valid state.
|
||||
\end{itemize}
|
||||
\vfill\centering
|
||||
|
||||
Writes are fast, reads are slow.
|
||||
|
||||
\vfill\raggedright
|
||||
|
||||
Examples: key-value \& object stores.
|
||||
|
||||
\end{frame}
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
\begin{frame}{Concurrent writes in BASE}{Consistency vs. Availability}
|
||||
|
||||
\begin{columns}
|
||||
\column{.5\columnwidth}
|
||||
\begin{block}{Object}
|
||||
\begin{itemize}
|
||||
\item Unique key
|
||||
\item Arbitrary value
|
||||
\item Metadata
|
||||
\end{itemize}
|
||||
\end{block}
|
||||
\vspace{5ex}
|
||||
|
||||
Conflict resolution = client's job!
|
||||
\vspace{5ex}
|
||||
|
||||
No compound operations.
|
||||
\column{.5\columnwidth}
|
||||
\centering
|
||||
\includegraphics[width=\columnwidth]{figures/conflict_base.pdf}
|
||||
\end{columns}
|
||||
|
||||
% KV storage is another example, distinction is minor here
|
||||
|
||||
% Object = unique key, arbitrary value, metadata.
|
||||
|
||||
% Object storage only provides semantics to investigate causal order of queries *for individual objects*. No compound operations, no transactions.
|
||||
|
||||
% Much easier to distribute, and "scale-out".
|
||||
|
||||
% Write is fast, read is slow (gotta collect all object versions).
|
||||
|
||||
% \todo{vaccines example with BASE model}
|
||||
|
||||
\end{frame}
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
\begin{frame}{Strong Eventual Consistency w/ CRDTs}{Consistency vs. Availability}
|
||||
|
||||
\centering\small
|
||||
|
||||
\fullcite{defago_conflict-free_2011}
|
||||
|
||||
\vfill\raggedright\normalsize
|
||||
|
||||
\begin{block}{Strong Eventual Consistency (SEC)}
|
||||
\begin{itemize}
|
||||
\item CRDTs specify distributed operations
|
||||
\item Conflicts will be solved according to specification
|
||||
\item Proven \& bound eventual convergence
|
||||
\end{itemize}
|
||||
\end{block}
|
||||
|
||||
\vfill\centering
|
||||
\includegraphics[width=.5\columnwidth]{figures/crdt.pdf}
|
||||
|
||||
\end{frame}
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
\begin{frame}[fragile]{Concurrent writes with CRDTs}{Consistency vs. Availability}
|
||||
|
||||
\begin{columns}
|
||||
\column{.5\columnwidth}
|
||||
\begin{block}{}
|
||||
\begin{lstlisting}
|
||||
CRDT Counter(x0):
|
||||
history = {}
|
||||
op. incr(y):
|
||||
history U= {(UUID(), y)}
|
||||
op. decr(y):
|
||||
history U= {(UUID(), -y)}
|
||||
op. read():
|
||||
x = x0
|
||||
for (_, y) in history:
|
||||
x += y
|
||||
return x
|
||||
\end{lstlisting}
|
||||
\end{block}
|
||||
\vspace{2ex}
|
||||
|
||||
Operations commute?
|
||||
|
||||
$\implies$ screw total order!
|
||||
\column{.5\columnwidth}
|
||||
\centering
|
||||
\includegraphics[width=\columnwidth]{figures/conflict_crdt.pdf}
|
||||
\end{columns}
|
||||
|
||||
\end{frame}
|
||||
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
\begin{frame}{A complex CRDT: the DAG}{Consistency vs. Availability}
|
||||
|
||||
\centering
|
||||
\only<1>{\includegraphics[height=\textheight]{figures/dag_crdt.png}}%
|
||||
\only<2>{
|
||||
Just to say I swept a lot under the rug.
|
||||
\vfill
|
||||
|
||||
For details, go read:
|
||||
|
||||
\fullcite{defago_conflict-free_2011}
|
||||
\vfill
|
||||
|
||||
For an implementation, check \textbf{AntidoteDB}.
|
||||
}
|
||||
|
||||
\end{frame}
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
\begin{frame}{State of the practice}{Path dependency to the ``cloud''}
|
||||
|
||||
\begin{block}{The BASE model is fashionable because}
|
||||
\centering
|
||||
|
||||
``\emph{High-performance} object storage for \emph{AI analytics} with PBs of \emph{IoT data streams} at the \emph{edge}, using \emph{5G}.''
|
||||
% \begin{itemize}
|
||||
% \item Highest performance
|
||||
% \item IoT data streams are inherently distributed
|
||||
% \end{itemize}
|
||||
\end{block}
|
||||
|
||||
\vfill\centering
|
||||
|
||||
\includegraphics[width=.9\columnwidth]{figures/minio_edge.png}
|
||||
|
||||
\vfill\raggedright
|
||||
|
||||
|
||||
%\begin{block}{}
|
||||
\begin{itemize}
|
||||
\item Always backed by cloud: high performance network links.
|
||||
\item Edge nodes always seen as clients or data sources, not peers.
|
||||
\end{itemize}
|
||||
%\end{block}
|
||||
|
||||
% There is \textbf{always a central cloud cluster} in these use-cases.
|
||||
|
||||
% Hidden constraint: \textbf{high performance inter-node connectivity}.
|
||||
|
||||
|
||||
|
||||
\end{frame}
|
||||
|
||||
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
% \begin{frame}{A brief history of storage}
|
||||
|
||||
% We keep it short because we'll follow chronological order in the next section too.
|
||||
|
||||
% \end{frame}
|
||||
|
||||
|
||||
% \begin{frame}{In the beginning, there were \emph{monoliths}}
|
||||
|
||||
% \includegraphics[width=.5\columnwidth]{figures/stonehenge.jpg}
|
||||
|
||||
% Web applications used to be monolithic:
|
||||
|
||||
% \begin{itemize}
|
||||
% \item One or two servers;
|
||||
% \item Availability was not an obsession;
|
||||
% \item Latency was acceptable.
|
||||
% \end{itemize}
|
||||
|
||||
% Relational databases were queens.
|
||||
|
||||
% \end{frame}
|
||||
|
||||
|
||||
% \begin{frame}{Then came \emph{expectations}}
|
||||
% Then, the whole world went online, and suddenly: expectations!
|
||||
|
||||
% \begin{itemize}
|
||||
% \item ``Milliseconds matter.'' (Algolia slogan)
|
||||
% \item Critical networked services (healthcare, logistics) need 100\% availability
|
||||
% \end{itemize}
|
||||
|
||||
% $\implies$ Microservices \& horizontal scalability.
|
||||
|
||||
% \todo{Develop on the `herd not sheep' paradigm a bit.}
|
||||
|
||||
% \end{frame}
|
||||
|
||||
|
||||
% \begin{frame}{Distributing state/storage: the remaining unknown}
|
||||
|
||||
% The microservices orchestration game works well for \emph{stateless} services.
|
||||
|
||||
% However, any application requires \emph{state}, persistent data.
|
||||
|
||||
% And this is tough. As we will now see.
|
||||
|
||||
% (Not that it's not well studied: distributed storage has always been fashionable.)
|
||||
|
||||
% \end{frame}
|
||||
@@ -0,0 +1,12 @@
|
||||
*
|
||||
|
||||
!img
|
||||
|
||||
!.gitignore
|
||||
!*.svg
|
||||
!*.png
|
||||
!*.jpg
|
||||
!*.tex
|
||||
!Makefile
|
||||
!.gitignore
|
||||
!talk.pdf
|
||||
@@ -0,0 +1,3 @@
|
||||
talk.pdf: talk.tex
|
||||
pdflatex talk.tex
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
%\nonstopmode
|
||||
\documentclass[aspectratio=169]{beamer}
|
||||
\usepackage[utf8]{inputenc}
|
||||
% \usepackage[frenchb]{babel}
|
||||
\usepackage{amsmath}
|
||||
\usepackage{mathtools}
|
||||
\usepackage{breqn}
|
||||
\usepackage{multirow}
|
||||
\usetheme{boxes}
|
||||
\usepackage{graphicx}
|
||||
%\useoutertheme[footline=authortitle,subsection=false]{miniframes}
|
||||
|
||||
\beamertemplatenavigationsymbolsempty
|
||||
|
||||
\usepackage{tabu}
|
||||
\usepackage{multicol}
|
||||
\usepackage{vwcol}
|
||||
\usepackage{stmaryrd}
|
||||
\usepackage{graphicx}
|
||||
|
||||
\usepackage[normalem]{ulem}
|
||||
|
||||
\title{Presentation of the Garage project}
|
||||
\subtitle{NGI pointer kickoff meeting}
|
||||
\author{Deuxfleurs Association}
|
||||
\date{2021-09-13}
|
||||
|
||||
\begin{document}
|
||||
|
||||
\begin{frame}
|
||||
\centering
|
||||
\includegraphics[width=.3\linewidth]{../../sticker/Garage.pdf}
|
||||
\vspace{1em}
|
||||
|
||||
{\large\bf Deuxfleurs Association}
|
||||
\vspace{1em}
|
||||
|
||||
\url{https://deuxfleurs.fr/}
|
||||
|
||||
\url{https://garagehq.deuxfleurs.fr/}
|
||||
|
||||
Matrix channel: \texttt{\#garage:deuxfleurs.fr}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{Our objective at Deuxfleurs}
|
||||
|
||||
\begin{center}
|
||||
\textbf{Promote self-hosting and small-scale hosting\\
|
||||
as an alternative to large cloud providers}
|
||||
\end{center}
|
||||
\vspace{2em}
|
||||
\visible<2->{
|
||||
Why is it hard?
|
||||
}
|
||||
\visible<3->{
|
||||
\vspace{2em}
|
||||
\begin{center}
|
||||
\textbf{\underline{Resilience}}\\
|
||||
{\footnotesize (we want good uptime/availability with low supervision)}
|
||||
\end{center}
|
||||
}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{How to be resilient (the hard way)}
|
||||
|
||||
Entreprise-grade systems typically employ:
|
||||
\vspace{1em}
|
||||
\begin{itemize}
|
||||
\item Redundant Internet connections
|
||||
\item Redundant electricity
|
||||
\item UPSes
|
||||
\item RAID
|
||||
\item ...
|
||||
\end{itemize}
|
||||
\vspace{1em}
|
||||
$\to$ it's costly and only worth it at DC scale
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{How to be resilient (the \underline{\textbf{cheap}} way)}
|
||||
|
||||
Instead, we use:
|
||||
\vspace{1em}
|
||||
\begin{itemize}
|
||||
\item Commodity hardware (e.g. old desktop PCs)
|
||||
\vspace{.5em}
|
||||
\item<2-> Commodity Internet (e.g. FTTB, FTTH) and electricity
|
||||
\vspace{.5em}
|
||||
\item<3-> \textbf{Geographical redundancy} (multi-site replication)
|
||||
\vspace{.5em}
|
||||
\item<4-> \textbf{Fault-tolerant distributed algorithms}
|
||||
\end{itemize}
|
||||
\vspace{1em}
|
||||
\visible<5->{
|
||||
\centering
|
||||
\underline{\textbf{This is how we build Garage.}}
|
||||
}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\frametitle{But what is Garage, exactly?}
|
||||
\textbf{Garage is a self-hosted drop-in replacement for the Amazon S3 object store}\\
|
||||
\vspace{.5em}
|
||||
that implements resilience through geographical redundancy on commodity hardware
|
||||
|
||||
\vspace{1em}
|
||||
\visible<2->{
|
||||
\begin{center}
|
||||
Current status: technical preview\\
|
||||
Our goal: release a stable v1.0
|
||||
\end{center}
|
||||
}
|
||||
|
||||
\vspace{1em}
|
||||
\visible<3->{
|
||||
\textbf{Comming up next: an e-mail IMAP inbox based on the same principles}
|
||||
}
|
||||
|
||||
\vspace{1em}
|
||||
\visible<4->{
|
||||
\begin{center}
|
||||
Current status: just an idea\\
|
||||
Our goal: at least a PoC, maybe more
|
||||
\end{center}
|
||||
}
|
||||
\end{frame}
|
||||
|
||||
\begin{frame}
|
||||
\centering
|
||||
\includegraphics[width=.3\linewidth]{../../sticker/Garage.pdf}
|
||||
\vspace{1em}
|
||||
|
||||
{\large\bf Deuxfleurs Association}
|
||||
\vspace{1em}
|
||||
|
||||
\url{https://deuxfleurs.fr/}
|
||||
|
||||
\url{https://garagehq.deuxfleurs.fr/}
|
||||
|
||||
Matrix channel: \texttt{\#garage:deuxfleurs.fr}
|
||||
\end{frame}
|
||||
|
||||
\end{document}
|
||||
|
||||
%% vim: set ts=4 sw=4 tw=0 noet spelllang=fr :
|
||||
@@ -11,7 +11,7 @@ cd pki
|
||||
# the RPC protocol will use to authenticate the other side.
|
||||
if [ ! -f garage-ca.key ]; then
|
||||
echo "Generating Garage CA keys..."
|
||||
openssl genrsa -out garage-ca.key 4096
|
||||
openssl genpkey -algorithm ED25519 -out garage-ca.key
|
||||
openssl req -x509 -new -nodes -key garage-ca.key -sha256 -days 3650 -out garage-ca.crt -subj "/C=FR/O=Garage"
|
||||
fi
|
||||
|
||||
@@ -22,7 +22,7 @@ fi
|
||||
if [ ! -f garage.crt ]; then
|
||||
echo "Generating Garage agent keys..."
|
||||
if [ ! -f garage.key ]; then
|
||||
openssl genrsa -out garage.key 4096
|
||||
openssl genpkey -algorithm ED25519 -out garage.key
|
||||
fi
|
||||
openssl req -new -sha256 -key garage.key -subj "/C=FR/O=Garage/CN=garage" \
|
||||
-out garage.csr
|
||||
@@ -56,7 +56,7 @@ fi
|
||||
if [ ! -f garage-client.crt ]; then
|
||||
echo "Generating Garage client keys..."
|
||||
if [ ! -f garage-client.key ]; then
|
||||
openssl genrsa -out garage-client.key 4096
|
||||
openssl genpkey -algorithm ED25519 -out garage-client.key
|
||||
fi
|
||||
openssl req -new -sha256 -key garage-client.key -subj "/C=FR/O=Garage" \
|
||||
-out garage-client.csr
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
#!/bin/sh
|
||||
|
||||
BIN=target/release/garage
|
||||
DOCKER=lxpz/garage_amd64
|
||||
|
||||
TAG=$1
|
||||
if [ -z "$1" ]; then
|
||||
echo "Usage: $0 <tag>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
RUSTFLAGS="-C link-arg=-fuse-ld=lld -C target-cpu=x86-64 -C target-feature=+sse2" cargo build --release --no-default-features
|
||||
cp $BIN $BIN.stripped
|
||||
strip $BIN.stripped
|
||||
|
||||
docker pull archlinux:latest
|
||||
docker build -t $DOCKER:$TAG .
|
||||
docker push $DOCKER:$TAG
|
||||
docker tag $DOCKER:$TAG $DOCKER:latest
|
||||
docker push $DOCKER:latest
|
||||
@@ -0,0 +1,7 @@
|
||||
FROM rust:buster
|
||||
RUN apt-get update && \
|
||||
apt-get install --yes libsodium-dev awscli python-pip wget rclone openssl socat && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
RUN wget https://dl.min.io/client/mc/release/linux-amd64/mc -O /usr/local/bin/mc && chmod +x /usr/local/bin/mc
|
||||
RUN rustup component add rustfmt clippy
|
||||
RUN pip install s3cmd
|
||||
@@ -0,0 +1,8 @@
|
||||
DOCKER=lxpz/garage_builder_amd64
|
||||
|
||||
docker:
|
||||
docker build -t $(DOCKER):$(TAG) .
|
||||
docker push $(DOCKER):$(TAG)
|
||||
docker tag $(DOCKER):$(TAG) $(DOCKER):latest
|
||||
docker push $(DOCKER):latest
|
||||
|
||||
@@ -3,5 +3,6 @@
|
||||
set -ex
|
||||
|
||||
killall -9 garage || echo "garage is not running"
|
||||
killall -9 socat || echo "socat is not running"
|
||||
rm -rf /tmp/garage*
|
||||
rm -rf /tmp/config.*.toml
|
||||
|
||||
@@ -31,9 +31,7 @@ bootstrap_peers = [
|
||||
"127.0.0.1:3903"
|
||||
]
|
||||
max_concurrent_rpc_requests = 12
|
||||
data_replication_factor = 3
|
||||
meta_replication_factor = 3
|
||||
meta_epidemic_fanout = 3
|
||||
replication_mode = "3"
|
||||
|
||||
[s3_api]
|
||||
api_bind_addr = "0.0.0.0:$((3910+$count))" # the S3 API port, HTTP without TLS. Add a reverse proxy for the TLS part.
|
||||
@@ -47,6 +45,21 @@ EOF
|
||||
|
||||
echo -en "$LABEL configuration written to $CONF_PATH\n"
|
||||
|
||||
if [ -z "$SKIP_HTTPS" ]; then
|
||||
echo -en "$LABEL Starting dummy HTTPS reverse proxy\n"
|
||||
mkdir -p /tmp/garagessl
|
||||
openssl req \
|
||||
-new \
|
||||
-x509 \
|
||||
-keyout /tmp/garagessl/test.key \
|
||||
-out /tmp/garagessl/test.crt \
|
||||
-nodes \
|
||||
-subj "/C=XX/ST=XX/L=XX/O=XX/OU=XX/CN=localhost/emailAddress=X@X.XX" \
|
||||
-addext "subjectAltName = DNS:localhost, IP:127.0.0.1"
|
||||
cat /tmp/garagessl/test.key /tmp/garagessl/test.crt > /tmp/garagessl/test.pem
|
||||
socat openssl-listen:4443,reuseaddr,fork,cert=/tmp/garagessl/test.pem,verify=0 tcp4-connect:localhost:3911 &
|
||||
fi
|
||||
|
||||
(garage server -c /tmp/config.$count.toml 2>&1|while read r; do echo -en "$LABEL $r\n"; done) &
|
||||
done
|
||||
|
||||
|
||||
@@ -18,6 +18,6 @@ garage status \
|
||||
| grep UNCONFIGURED \
|
||||
| grep -Po '^[0-9a-f]+' \
|
||||
| while read id; do
|
||||
garage node configure -d dc1 -c 1 $id
|
||||
garage node configure -z dc1 -c 1 $id
|
||||
done
|
||||
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
SCRIPT_FOLDER="`dirname \"${BASH_SOURCE[0]}\"`"
|
||||
REPO_FOLDER="${SCRIPT_FOLDER}/../"
|
||||
GARAGE_DEBUG="${REPO_FOLDER}/target/debug/"
|
||||
GARAGE_RELEASE="${REPO_FOLDER}/target/release/"
|
||||
PATH="${GARAGE_DEBUG}:${GARAGE_RELEASE}:$PATH"
|
||||
|
||||
export AWS_ACCESS_KEY_ID=`cat /tmp/garage.s3 |cut -d' ' -f1`
|
||||
export AWS_SECRET_ACCESS_KEY=`cat /tmp/garage.s3 |cut -d' ' -f2`
|
||||
export AWS_DEFAULT_REGION='garage'
|
||||
|
||||
alias awsgrg="aws s3 \
|
||||
--endpoint-url http://127.0.0.1:3911"
|
||||
function aws { command aws --endpoint-url http://127.0.0.1:3911 $@ ; }
|
||||
|
||||
aws --version
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
mkdir -p /tmp/garage.cyberduck.home/.duck/profiles
|
||||
|
||||
DUCK_ACCESS_KEY=`cat /tmp/garage.s3 |cut -d' ' -f1`
|
||||
DUCK_SECRET_KEY=`cat /tmp/garage.s3 |cut -d' ' -f2`
|
||||
|
||||
cat > /tmp/garage.cyberduck.home/.duck/credentials <<EOF
|
||||
https\://$DUCK_ACCESS_KEY@127.0.0.1\:4443=$DUCK_SECRET_KEY
|
||||
EOF
|
||||
|
||||
cat > /tmp/garage.cyberduck.home/.duck/profiles/garage.cyberduckprofile <<EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Protocol</key>
|
||||
<string>s3</string>
|
||||
<key>Vendor</key>
|
||||
<string>garage</string>
|
||||
<key>Scheme</key>
|
||||
<string>https</string>
|
||||
<key>Description</key>
|
||||
<string>GarageS3</string>
|
||||
<key>Default Hostname</key>
|
||||
<string>127.0.0.1</string>
|
||||
<key>Default Port</key>
|
||||
<string>4443</string>
|
||||
<key>Hostname Configurable</key>
|
||||
<false/>
|
||||
<key>Port Configurable</key>
|
||||
<false/>
|
||||
<key>Username Configurable</key>
|
||||
<true/>
|
||||
<key>Username Placeholder</key>
|
||||
<string>Access Key ID (GK...)</string>
|
||||
<key>Password Placeholder</key>
|
||||
<string>Secret Key</string>
|
||||
<key>Properties</key>
|
||||
<array>
|
||||
<string>s3service.disable-dns-buckets=true</string>
|
||||
</array>
|
||||
<key>Region</key>
|
||||
<string>garage</string>
|
||||
<key>Regions</key>
|
||||
<array>
|
||||
<string>garage</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
function duck { HOME=/tmp/garage.cyberduck.home/ command duck --username $DUCK_ACCESS_KEY $@ ; }
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
ACCESS_KEY=`cat /tmp/garage.s3 |cut -d' ' -f1`
|
||||
SECRET_KEY=`cat /tmp/garage.s3 |cut -d' ' -f2`
|
||||
|
||||
mkdir -p /tmp/garage.mc/certs/CAs
|
||||
|
||||
cat > /tmp/garage.mc/config.json <<EOF
|
||||
{
|
||||
"version": "10",
|
||||
"aliases": {
|
||||
"garage": {
|
||||
"url": "https://localhost:4443",
|
||||
"accessKey": "$ACCESS_KEY",
|
||||
"secretKey": "$SECRET_KEY",
|
||||
"api": "S3v4",
|
||||
"path": "auto"
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
function mc { command mc --insecure --config-dir /tmp/garage.mc $@ ; }
|
||||
mc --version
|
||||
@@ -0,0 +1,21 @@
|
||||
ACCESS_KEY=`cat /tmp/garage.s3 |cut -d' ' -f1`
|
||||
SECRET_KEY=`cat /tmp/garage.s3 |cut -d' ' -f2`
|
||||
|
||||
cat > /tmp/garage.rclone.conf <<EOF
|
||||
[garage]
|
||||
type = s3
|
||||
provider = Other
|
||||
env_auth = false
|
||||
access_key_id = $ACCESS_KEY
|
||||
secret_access_key = $SECRET_KEY
|
||||
endpoint = http://127.0.0.1:3911
|
||||
bucket_acl = private
|
||||
force_path_style = true
|
||||
region = garage
|
||||
no_check_bucket = true
|
||||
EOF
|
||||
# It seems that region is mandatory as rclone does not support redirection
|
||||
|
||||
|
||||
function rclone { command rclone --config /tmp/garage.rclone.conf $@ ; }
|
||||
rclone --version
|
||||
@@ -1,19 +1,15 @@
|
||||
#!/bin/bash
|
||||
|
||||
SCRIPT_FOLDER="`dirname \"${BASH_SOURCE[0]}\"`"
|
||||
REPO_FOLDER="${SCRIPT_FOLDER}/../"
|
||||
GARAGE_DEBUG="${REPO_FOLDER}/target/debug/"
|
||||
GARAGE_RELEASE="${REPO_FOLDER}/target/release/"
|
||||
PATH="${GARAGE_DEBUG}:${GARAGE_RELEASE}:$PATH"
|
||||
|
||||
ACCESS_KEY=`cat /tmp/garage.s3 |cut -d' ' -f1`
|
||||
SECRET_KEY=`cat /tmp/garage.s3 |cut -d' ' -f2`
|
||||
|
||||
alias s3grg="s3cmd \
|
||||
--host 127.0.0.1:3911 \
|
||||
--host-bucket 127.0.0.1:3911 \
|
||||
--access_key=$ACCESS_KEY \
|
||||
--secret_key=$SECRET_KEY \
|
||||
--region=garage \
|
||||
--no-ssl"
|
||||
cat > /tmp/garage.s3cmd.cfg <<EOF
|
||||
[default]
|
||||
access_key = $ACCESS_KEY
|
||||
secret_key = $SECRET_KEY
|
||||
host_base = 127.0.0.1:3911
|
||||
host_bucket = 127.0.0.1:3911
|
||||
use_https = False
|
||||
EOF
|
||||
|
||||
function s3cmd { command s3cmd --config=/tmp/garage.s3cmd.cfg $@ ; }
|
||||
s3cmd --version
|
||||
python3 --version
|
||||
|
||||
@@ -1,76 +1,134 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -ex
|
||||
shopt -s expand_aliases
|
||||
|
||||
export LC_ALL=C.UTF-8
|
||||
export LANG=C.UTF-8
|
||||
SCRIPT_FOLDER="`dirname \"$0\"`"
|
||||
REPO_FOLDER="${SCRIPT_FOLDER}/../"
|
||||
GARAGE_DEBUG="${REPO_FOLDER}/target/debug/"
|
||||
GARAGE_RELEASE="${REPO_FOLDER}/target/release/"
|
||||
PATH="${GARAGE_DEBUG}:${GARAGE_RELEASE}:$PATH"
|
||||
|
||||
echo "setup"
|
||||
# @FIXME Duck is not ready for testing, we have a bug
|
||||
SKIP_DUCK=1
|
||||
|
||||
echo "⏳ Setup"
|
||||
cargo build
|
||||
${SCRIPT_FOLDER}/dev-clean.sh
|
||||
${SCRIPT_FOLDER}/dev-cluster.sh > /tmp/garage.log 2>&1 &
|
||||
${SCRIPT_FOLDER}/dev-configure.sh
|
||||
${SCRIPT_FOLDER}/dev-bucket.sh
|
||||
source ${SCRIPT_FOLDER}/dev-env-aws.sh
|
||||
source ${SCRIPT_FOLDER}/dev-env-s3cmd.sh
|
||||
|
||||
which garage
|
||||
garage status
|
||||
garage key list
|
||||
garage bucket list
|
||||
|
||||
dd if=/dev/urandom of=/tmp/garage.1.rnd bs=1k count=2 # < INLINE_THRESHOLD = 3072 bytes
|
||||
dd if=/dev/urandom of=/tmp/garage.2.rnd bs=1M count=5
|
||||
dd if=/dev/urandom of=/tmp/garage.3.rnd bs=1M count=10
|
||||
dd if=/dev/urandom of=/tmp/garage.1.rnd bs=1k count=2 # No multipart, inline storage (< INLINE_THRESHOLD = 3072 bytes)
|
||||
dd if=/dev/urandom of=/tmp/garage.2.rnd bs=1M count=5 # No multipart but file will be chunked
|
||||
dd if=/dev/urandom of=/tmp/garage.3.rnd bs=1M count=10 # by default, AWS starts using multipart at 8MB
|
||||
|
||||
echo "s3 api testing..."
|
||||
for idx in $(seq 1 3); do
|
||||
# AWS sends
|
||||
awsgrg cp /tmp/garage.$idx.rnd s3://eprouvette/garage.$idx.aws
|
||||
|
||||
awsgrg ls s3://eprouvette
|
||||
|
||||
awsgrg cp s3://eprouvette/garage.$idx.aws /tmp/garage.$idx.dl
|
||||
diff /tmp/garage.$idx.rnd /tmp/garage.$idx.dl
|
||||
rm /tmp/garage.$idx.dl
|
||||
|
||||
s3grg get s3://eprouvette/garage.$idx.aws /tmp/garage.$idx.dl
|
||||
diff /tmp/garage.$idx.rnd /tmp/garage.$idx.dl
|
||||
rm /tmp/garage.$idx.dl
|
||||
|
||||
awsgrg rm s3://eprouvette/garage.$idx.aws
|
||||
echo "🧪 S3 API testing..."
|
||||
|
||||
# S3CMD sends
|
||||
s3grg put /tmp/garage.$idx.rnd s3://eprouvette/garage.$idx.s3cmd
|
||||
# AWS
|
||||
if [ -z "$SKIP_AWS" ]; then
|
||||
echo "🛠️ Testing with awscli"
|
||||
source ${SCRIPT_FOLDER}/dev-env-aws.sh
|
||||
aws s3 ls
|
||||
for idx in $(seq 1 3); do
|
||||
aws s3 cp "/tmp/garage.$idx.rnd" "s3://eprouvette/&+-é\"/garage.$idx.aws"
|
||||
aws s3 ls s3://eprouvette
|
||||
aws s3 cp "s3://eprouvette/&+-é\"/garage.$idx.aws" "/tmp/garage.$idx.dl"
|
||||
diff /tmp/garage.$idx.rnd /tmp/garage.$idx.dl
|
||||
rm /tmp/garage.$idx.dl
|
||||
aws s3 rm "s3://eprouvette/&+-é\"/garage.$idx.aws"
|
||||
done
|
||||
fi
|
||||
|
||||
# S3CMD
|
||||
if [ -z "$SKIP_S3CMD" ]; then
|
||||
echo "🛠️ Testing with s3cmd"
|
||||
source ${SCRIPT_FOLDER}/dev-env-s3cmd.sh
|
||||
s3cmd ls
|
||||
for idx in $(seq 1 3); do
|
||||
s3cmd put "/tmp/garage.$idx.rnd" "s3://eprouvette/&+-é\"/garage.$idx.s3cmd"
|
||||
s3cmd ls s3://eprouvette
|
||||
s3cmd get "s3://eprouvette/&+-é\"/garage.$idx.s3cmd" "/tmp/garage.$idx.dl"
|
||||
diff /tmp/garage.$idx.rnd /tmp/garage.$idx.dl
|
||||
rm /tmp/garage.$idx.dl
|
||||
s3cmd rm "s3://eprouvette/&+-é\"/garage.$idx.s3cmd"
|
||||
done
|
||||
fi
|
||||
|
||||
# Minio Client
|
||||
if [ -z "$SKIP_MC" ]; then
|
||||
echo "🛠️ Testing with mc (minio client)"
|
||||
source ${SCRIPT_FOLDER}/dev-env-mc.sh
|
||||
mc ls garage/
|
||||
for idx in $(seq 1 3); do
|
||||
mc cp "/tmp/garage.$idx.rnd" "garage/eprouvette/&+-é\"/garage.$idx.mc"
|
||||
mc ls garage/eprouvette
|
||||
mc cp "garage/eprouvette/&+-é\"/garage.$idx.mc" "/tmp/garage.$idx.dl"
|
||||
diff /tmp/garage.$idx.rnd /tmp/garage.$idx.dl
|
||||
rm /tmp/garage.$idx.dl
|
||||
mc rm "garage/eprouvette/&+-é\"/garage.$idx.mc"
|
||||
done
|
||||
fi
|
||||
|
||||
# RClone
|
||||
if [ -z "$SKIP_RCLONE" ]; then
|
||||
echo "🛠️ Testing with rclone"
|
||||
source ${SCRIPT_FOLDER}/dev-env-rclone.sh
|
||||
rclone lsd garage:
|
||||
for idx in $(seq 1 3); do
|
||||
cp /tmp/garage.$idx.rnd /tmp/garage.$idx.dl
|
||||
rclone copy "/tmp/garage.$idx.dl" "garage:eprouvette/&+-é\"/"
|
||||
rm /tmp/garage.$idx.dl
|
||||
rclone ls garage:eprouvette
|
||||
rclone copy "garage:eprouvette/&+-é\"/garage.$idx.dl" "/tmp/"
|
||||
diff /tmp/garage.$idx.rnd /tmp/garage.$idx.dl
|
||||
rm /tmp/garage.$idx.dl
|
||||
rclone delete "garage:eprouvette/&+-é\"/garage.$idx.dl"
|
||||
done
|
||||
fi
|
||||
|
||||
# Duck (aka Cyberduck CLI)
|
||||
if [ -z "$SKIP_DUCK" ]; then
|
||||
echo "🛠️ Testing with duck (aka cyberduck cli)"
|
||||
source ${SCRIPT_FOLDER}/dev-env-duck.sh
|
||||
duck --list garage:/
|
||||
duck --mkdir "garage:/eprouvette/duck"
|
||||
for idx in $(seq 1 3); do
|
||||
duck --verbose --upload "garage:/eprouvette/duck/" "/tmp/garage.$idx.rnd"
|
||||
duck --list garage:/eprouvette/duck/
|
||||
duck --download "garage:/eprouvette/duck/garage.$idx.rnd" "/tmp/garage.$idx.dl"
|
||||
diff /tmp/garage.$idx.rnd /tmp/garage.$idx.dl
|
||||
rm /tmp/garage.$idx.dl
|
||||
duck --delete "garage:/eprouvette/duck/garage.$idx.dk"
|
||||
done
|
||||
fi
|
||||
|
||||
s3grg ls s3://eprouvette
|
||||
|
||||
s3grg get s3://eprouvette/garage.$idx.s3cmd /tmp/garage.$idx.dl
|
||||
diff /tmp/garage.$idx.rnd /tmp/garage.$idx.dl
|
||||
rm /tmp/garage.$idx.dl
|
||||
|
||||
awsgrg cp s3://eprouvette/garage.$idx.s3cmd /tmp/garage.$idx.dl
|
||||
diff /tmp/garage.$idx.rnd /tmp/garage.$idx.dl
|
||||
rm /tmp/garage.$idx.dl
|
||||
|
||||
s3grg rm s3://eprouvette/garage.$idx.s3cmd
|
||||
done
|
||||
rm /tmp/garage.{1,2,3}.rnd
|
||||
|
||||
echo "website testing"
|
||||
echo "<h1>hello world</h1>" > /tmp/garage-index.html
|
||||
awsgrg cp /tmp/garage-index.html s3://eprouvette/index.html
|
||||
[ `curl -s -o /dev/null -w "%{http_code}" --header "Host: eprouvette.garage.tld" http://127.0.0.1:3923/ ` == 404 ]
|
||||
garage bucket website --allow eprouvette
|
||||
[ `curl -s -o /dev/null -w "%{http_code}" --header "Host: eprouvette.garage.tld" http://127.0.0.1:3923/ ` == 200 ]
|
||||
garage bucket website --deny eprouvette
|
||||
[ `curl -s -o /dev/null -w "%{http_code}" --header "Host: eprouvette.garage.tld" http://127.0.0.1:3923/ ` == 404 ]
|
||||
awsgrg rm s3://eprouvette/index.html
|
||||
rm /tmp/garage-index.html
|
||||
if [ -z "$SKIP_AWS" ]; then
|
||||
echo "🧪 Website Testing"
|
||||
echo "<h1>hello world</h1>" > /tmp/garage-index.html
|
||||
aws s3 cp /tmp/garage-index.html s3://eprouvette/index.html
|
||||
[ `curl -s -o /dev/null -w "%{http_code}" --header "Host: eprouvette.garage.tld" http://127.0.0.1:3923/ ` == 404 ]
|
||||
garage bucket website --allow eprouvette
|
||||
[ `curl -s -o /dev/null -w "%{http_code}" --header "Host: eprouvette.garage.tld" http://127.0.0.1:3923/ ` == 200 ]
|
||||
garage bucket website --deny eprouvette
|
||||
[ `curl -s -o /dev/null -w "%{http_code}" --header "Host: eprouvette.garage.tld" http://127.0.0.1:3923/ ` == 404 ]
|
||||
aws s3 rm s3://eprouvette/index.html
|
||||
rm /tmp/garage-index.html
|
||||
fi
|
||||
|
||||
echo "teardown"
|
||||
echo "🏁 Teardown"
|
||||
AWS_ACCESS_KEY_ID=`cat /tmp/garage.s3 |cut -d' ' -f1`
|
||||
AWS_SECRET_ACCESS_KEY=`cat /tmp/garage.s3 |cut -d' ' -f2`
|
||||
garage bucket deny --read --write eprouvette --key $AWS_ACCESS_KEY_ID
|
||||
garage bucket delete --yes eprouvette
|
||||
garage key delete --yes $AWS_ACCESS_KEY_ID
|
||||
|
||||
echo "success"
|
||||
echo "✅ Success"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "garage_api"
|
||||
version = "0.2.1"
|
||||
version = "0.3.0"
|
||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||
edition = "2018"
|
||||
license = "AGPL-3.0"
|
||||
@@ -13,9 +13,9 @@ path = "lib.rs"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
garage_model = { version = "0.2.1", path = "../model" }
|
||||
garage_table = { version = "0.2.1", path = "../table" }
|
||||
garage_util = { version = "0.2.1", path = "../util" }
|
||||
garage_model = { version = "0.3.0", path = "../model" }
|
||||
garage_table = { version = "0.3.0", path = "../table" }
|
||||
garage_util = { version = "0.3.0", path = "../util" }
|
||||
|
||||
base64 = "0.13"
|
||||
bytes = "1.0"
|
||||
@@ -38,4 +38,6 @@ http-range = "0.1"
|
||||
hyper = "0.14"
|
||||
percent-encoding = "2.1.0"
|
||||
roxmltree = "0.14"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
quick-xml = { version = "0.21", features = [ "serialize" ] }
|
||||
url = "2.1"
|
||||
|
||||
@@ -14,12 +14,14 @@ use garage_model::garage::Garage;
|
||||
use crate::error::*;
|
||||
use crate::signature::check_signature;
|
||||
|
||||
use crate::s3_bucket::*;
|
||||
use crate::s3_copy::*;
|
||||
use crate::s3_delete::*;
|
||||
use crate::s3_get::*;
|
||||
use crate::s3_list::*;
|
||||
use crate::s3_put::*;
|
||||
|
||||
/// Run the S3 API server
|
||||
pub async fn run_api_server(
|
||||
garage: Arc<Garage>,
|
||||
shutdown_signal: impl Future<Output = ()>,
|
||||
@@ -51,17 +53,21 @@ async fn handler(
|
||||
req: Request<Body>,
|
||||
addr: SocketAddr,
|
||||
) -> Result<Response<Body>, GarageError> {
|
||||
info!("{} {} {}", addr, req.method(), req.uri());
|
||||
let uri = req.uri().clone();
|
||||
info!("{} {} {}", addr, req.method(), uri);
|
||||
debug!("{:?}", req);
|
||||
match handler_inner(garage, req).await {
|
||||
match handler_inner(garage.clone(), req).await {
|
||||
Ok(x) => {
|
||||
debug!("{} {:?}", x.status(), x.headers());
|
||||
Ok(x)
|
||||
}
|
||||
Err(e) => {
|
||||
let body: Body = Body::from(format!("{}\n", e));
|
||||
let mut http_error = Response::new(body);
|
||||
*http_error.status_mut() = e.http_status_code();
|
||||
let body: Body = Body::from(e.aws_xml(&garage.config.s3_api.s3_region, uri.path()));
|
||||
let http_error = Response::builder()
|
||||
.status(e.http_status_code())
|
||||
.header("Content-Type", "application/xml")
|
||||
.body(body)?;
|
||||
|
||||
if e.http_status_code().is_server_error() {
|
||||
warn!("Response: error {}, {}", e.http_status_code(), e);
|
||||
} else {
|
||||
@@ -75,18 +81,20 @@ async fn handler(
|
||||
async fn handler_inner(garage: Arc<Garage>, req: Request<Body>) -> Result<Response<Body>, Error> {
|
||||
let path = req.uri().path().to_string();
|
||||
let path = percent_encoding::percent_decode_str(&path).decode_utf8()?;
|
||||
let (api_key, content_sha256) = check_signature(&garage, &req).await?;
|
||||
if path == "/" {
|
||||
return handle_list_buckets(&api_key);
|
||||
}
|
||||
|
||||
let (bucket, key) = parse_bucket_key(&path)?;
|
||||
|
||||
let (api_key, content_sha256) = check_signature(&garage, &req).await?;
|
||||
let allowed = match req.method() {
|
||||
&Method::HEAD | &Method::GET => api_key.allow_read(&bucket),
|
||||
_ => api_key.allow_write(&bucket),
|
||||
};
|
||||
if !allowed {
|
||||
return Err(Error::Forbidden(format!(
|
||||
"Operation is not allowed for this key."
|
||||
)));
|
||||
return Err(Error::Forbidden(
|
||||
"Operation is not allowed for this key.".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut params = HashMap::new();
|
||||
@@ -98,16 +106,16 @@ async fn handler_inner(garage: Arc<Garage>, req: Request<Body>) -> Result<Respon
|
||||
}
|
||||
|
||||
if let Some(key) = key {
|
||||
match req.method() {
|
||||
&Method::HEAD => {
|
||||
match *req.method() {
|
||||
Method::HEAD => {
|
||||
// HeadObject query
|
||||
Ok(handle_head(garage, &req, &bucket, &key).await?)
|
||||
}
|
||||
&Method::GET => {
|
||||
Method::GET => {
|
||||
// GetObject query
|
||||
Ok(handle_get(garage, &req, &bucket, &key).await?)
|
||||
}
|
||||
&Method::PUT => {
|
||||
Method::PUT => {
|
||||
if params.contains_key(&"partnumber".to_string())
|
||||
&& params.contains_key(&"uploadid".to_string())
|
||||
{
|
||||
@@ -146,7 +154,7 @@ async fn handler_inner(garage: Arc<Garage>, req: Request<Body>) -> Result<Respon
|
||||
Ok(handle_put(garage, req, &bucket, &key, content_sha256).await?)
|
||||
}
|
||||
}
|
||||
&Method::DELETE => {
|
||||
Method::DELETE => {
|
||||
if params.contains_key(&"uploadid".to_string()) {
|
||||
// AbortMultipartUpload query
|
||||
let upload_id = params.get("uploadid").unwrap();
|
||||
@@ -156,7 +164,7 @@ async fn handler_inner(garage: Arc<Garage>, req: Request<Body>) -> Result<Respon
|
||||
Ok(handle_delete(garage, &bucket, &key).await?)
|
||||
}
|
||||
}
|
||||
&Method::POST => {
|
||||
Method::POST => {
|
||||
if params.contains_key(&"uploads".to_string()) {
|
||||
// CreateMultipartUpload call
|
||||
Ok(handle_create_multipart_upload(garage, &req, &bucket, &key).await?)
|
||||
@@ -173,16 +181,16 @@ async fn handler_inner(garage: Arc<Garage>, req: Request<Body>) -> Result<Respon
|
||||
)
|
||||
.await?)
|
||||
} else {
|
||||
Err(Error::BadRequest(format!(
|
||||
"Not a CreateMultipartUpload call, what is it?"
|
||||
)))
|
||||
Err(Error::BadRequest(
|
||||
"Not a CreateMultipartUpload call, what is it?".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
_ => Err(Error::BadRequest(format!("Invalid method"))),
|
||||
_ => Err(Error::BadRequest("Invalid method".to_string())),
|
||||
}
|
||||
} else {
|
||||
match req.method() {
|
||||
&Method::PUT => {
|
||||
match *req.method() {
|
||||
Method::PUT => {
|
||||
// CreateBucket
|
||||
// If we're here, the bucket already exists, so just answer ok
|
||||
debug!(
|
||||
@@ -197,24 +205,32 @@ async fn handler_inner(garage: Arc<Garage>, req: Request<Body>) -> Result<Respon
|
||||
.unwrap();
|
||||
Ok(response)
|
||||
}
|
||||
&Method::HEAD => {
|
||||
Method::HEAD => {
|
||||
// HeadBucket
|
||||
let empty_body: Body = Body::from(vec![]);
|
||||
let response = Response::builder().body(empty_body).unwrap();
|
||||
Ok(response)
|
||||
}
|
||||
&Method::DELETE => {
|
||||
Method::DELETE => {
|
||||
// DeleteBucket query
|
||||
Err(Error::Forbidden(
|
||||
"Cannot delete buckets using S3 api, please talk to Garage directly".into(),
|
||||
))
|
||||
}
|
||||
&Method::GET => {
|
||||
// ListObjects or ListObjectsV2 query
|
||||
let q = parse_list_objects_query(bucket, ¶ms)?;
|
||||
Ok(handle_list(garage, &q).await?)
|
||||
Method::GET => {
|
||||
if params.contains_key("location") {
|
||||
// GetBucketLocation call
|
||||
Ok(handle_get_bucket_location(garage)?)
|
||||
} else if params.contains_key("versioning") {
|
||||
// GetBucketVersioning
|
||||
Ok(handle_get_bucket_versioning()?)
|
||||
} else {
|
||||
// ListObjects or ListObjectsV2 query
|
||||
let q = parse_list_objects_query(bucket, ¶ms)?;
|
||||
Ok(handle_list(garage, &q).await?)
|
||||
}
|
||||
}
|
||||
&Method::POST => {
|
||||
Method::POST => {
|
||||
if params.contains_key(&"delete".to_string()) {
|
||||
// DeleteObjects
|
||||
Ok(handle_delete_objects(garage, bucket, req, content_sha256).await?)
|
||||
@@ -224,10 +240,10 @@ async fn handler_inner(garage: Arc<Garage>, req: Request<Body>) -> Result<Respon
|
||||
std::str::from_utf8(&hyper::body::to_bytes(req.into_body()).await?)
|
||||
.unwrap_or("<invalid utf8>")
|
||||
);
|
||||
Err(Error::BadRequest(format!("Unsupported call")))
|
||||
Err(Error::BadRequest("Unsupported call".to_string()))
|
||||
}
|
||||
}
|
||||
_ => Err(Error::BadRequest(format!("Invalid method"))),
|
||||
_ => Err(Error::BadRequest("Invalid method".to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -242,7 +258,7 @@ fn parse_bucket_key(path: &str) -> Result<(&str, Option<&str>), Error> {
|
||||
let (bucket, key) = match path.find('/') {
|
||||
Some(i) => {
|
||||
let key = &path[i + 1..];
|
||||
if key.len() > 0 {
|
||||
if !key.is_empty() {
|
||||
(&path[..i], Some(key))
|
||||
} else {
|
||||
(&path[..i], None)
|
||||
@@ -250,8 +266,8 @@ fn parse_bucket_key(path: &str) -> Result<(&str, Option<&str>), Error> {
|
||||
}
|
||||
None => (path, None),
|
||||
};
|
||||
if bucket.len() == 0 {
|
||||
return Err(Error::BadRequest(format!("No bucket specified")));
|
||||
if bucket.is_empty() {
|
||||
return Err(Error::BadRequest("No bucket specified".to_string()));
|
||||
}
|
||||
Ok((bucket, key))
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
pub fn xml_escape(s: &str) -> String {
|
||||
s.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
.replace("\"", """)
|
||||
}
|
||||
//! Module containing various helpers for encoding
|
||||
|
||||
/// Encode &str for use in a URI
|
||||
pub fn uri_encode(string: &str, encode_slash: bool) -> String {
|
||||
let mut result = String::with_capacity(string.len() * 2);
|
||||
for c in string.chars() {
|
||||
@@ -23,11 +20,3 @@ pub fn uri_encode(string: &str, encode_slash: bool) -> String {
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
pub fn xml_encode_key(k: &str, urlencode: bool) -> String {
|
||||
if urlencode {
|
||||
uri_encode(k, true)
|
||||
} else {
|
||||
xml_escape(k)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,68 +3,125 @@ use hyper::StatusCode;
|
||||
|
||||
use garage_util::error::Error as GarageError;
|
||||
|
||||
use crate::s3_xml;
|
||||
|
||||
/// Errors of this crate
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
// Category: internal error
|
||||
/// Error related to deeper parts of Garage
|
||||
#[error(display = "Internal error: {}", _0)]
|
||||
InternalError(#[error(source)] GarageError),
|
||||
|
||||
/// Error related to Hyper
|
||||
#[error(display = "Internal error (Hyper error): {}", _0)]
|
||||
Hyper(#[error(source)] hyper::Error),
|
||||
|
||||
/// Error related to HTTP
|
||||
#[error(display = "Internal error (HTTP error): {}", _0)]
|
||||
HTTP(#[error(source)] http::Error),
|
||||
Http(#[error(source)] http::Error),
|
||||
|
||||
// Category: cannot process
|
||||
/// No proper api key was used, or the signature was invalid
|
||||
#[error(display = "Forbidden: {}", _0)]
|
||||
Forbidden(String),
|
||||
|
||||
/// Authorization Header Malformed
|
||||
#[error(display = "Authorization header malformed, expected scope: {}", _0)]
|
||||
AuthorizationHeaderMalformed(String),
|
||||
|
||||
/// The object requested don't exists
|
||||
#[error(display = "Not found")]
|
||||
NotFound,
|
||||
|
||||
// Category: bad request
|
||||
/// The request contained an invalid UTF-8 sequence in its path or in other parameters
|
||||
#[error(display = "Invalid UTF-8: {}", _0)]
|
||||
InvalidUTF8Str(#[error(source)] std::str::Utf8Error),
|
||||
InvalidUtf8Str(#[error(source)] std::str::Utf8Error),
|
||||
|
||||
/// The request used an invalid path
|
||||
#[error(display = "Invalid UTF-8: {}", _0)]
|
||||
InvalidUTF8String(#[error(source)] std::string::FromUtf8Error),
|
||||
InvalidUtf8String(#[error(source)] std::string::FromUtf8Error),
|
||||
|
||||
/// Some base64 encoded data was badly encoded
|
||||
#[error(display = "Invalid base64: {}", _0)]
|
||||
InvalidBase64(#[error(source)] base64::DecodeError),
|
||||
|
||||
/// The client sent invalid XML data
|
||||
#[error(display = "Invalid XML: {}", _0)]
|
||||
InvalidXML(String),
|
||||
InvalidXml(String),
|
||||
|
||||
/// The client sent a header with invalid value
|
||||
#[error(display = "Invalid header value: {}", _0)]
|
||||
InvalidHeader(#[error(source)] hyper::header::ToStrError),
|
||||
|
||||
/// The client sent a range header with invalid value
|
||||
#[error(display = "Invalid HTTP range: {:?}", _0)]
|
||||
InvalidRange(#[error(from)] http_range::HttpRangeParseError),
|
||||
|
||||
/// The client sent an invalid request
|
||||
#[error(display = "Bad request: {}", _0)]
|
||||
BadRequest(String),
|
||||
}
|
||||
|
||||
impl From<roxmltree::Error> for Error {
|
||||
fn from(err: roxmltree::Error) -> Self {
|
||||
Self::InvalidXML(format!("{}", err))
|
||||
Self::InvalidXml(format!("{}", err))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<quick_xml::de::DeError> for Error {
|
||||
fn from(err: quick_xml::de::DeError) -> Self {
|
||||
Self::InvalidXml(format!("{}", err))
|
||||
}
|
||||
}
|
||||
|
||||
impl Error {
|
||||
/// Get the HTTP status code that best represents the meaning of the error for the client
|
||||
pub fn http_status_code(&self) -> StatusCode {
|
||||
match self {
|
||||
Error::NotFound => StatusCode::NOT_FOUND,
|
||||
Error::Forbidden(_) => StatusCode::FORBIDDEN,
|
||||
Error::InternalError(GarageError::RPC(_)) => StatusCode::SERVICE_UNAVAILABLE,
|
||||
Error::InternalError(_) | Error::Hyper(_) | Error::HTTP(_) => {
|
||||
Error::InternalError(GarageError::Rpc(_)) => StatusCode::SERVICE_UNAVAILABLE,
|
||||
Error::InternalError(_) | Error::Hyper(_) | Error::Http(_) => {
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
}
|
||||
_ => StatusCode::BAD_REQUEST,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn aws_code(&self) -> &'static str {
|
||||
match self {
|
||||
Error::NotFound => "NoSuchKey",
|
||||
Error::Forbidden(_) => "AccessDenied",
|
||||
Error::AuthorizationHeaderMalformed(_) => "AuthorizationHeaderMalformed",
|
||||
Error::InternalError(GarageError::Rpc(_)) => "ServiceUnavailable",
|
||||
Error::InternalError(_) | Error::Hyper(_) | Error::Http(_) => "InternalError",
|
||||
_ => "InvalidRequest",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn aws_xml(&self, garage_region: &str, path: &str) -> String {
|
||||
let error = s3_xml::Error {
|
||||
code: s3_xml::Value(self.aws_code().to_string()),
|
||||
message: s3_xml::Value(format!("{}", self)),
|
||||
resource: Some(s3_xml::Value(path.to_string())),
|
||||
region: Some(s3_xml::Value(garage_region.to_string())),
|
||||
};
|
||||
s3_xml::to_xml_with_header(&error).unwrap_or_else(|_| {
|
||||
r#"
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Error>
|
||||
<Code>InternalError</Code>
|
||||
<Message>XML encoding of error failed</Message>
|
||||
</Error>
|
||||
"#
|
||||
.into()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait to map error to the Bad Request error code
|
||||
pub trait OkOrBadRequest {
|
||||
type S2;
|
||||
fn ok_or_bad_request(self, reason: &'static str) -> Self::S2;
|
||||
@@ -88,11 +145,12 @@ impl<T> OkOrBadRequest for Option<T> {
|
||||
fn ok_or_bad_request(self, reason: &'static str) -> Result<T, Error> {
|
||||
match self {
|
||||
Some(x) => Ok(x),
|
||||
None => Err(Error::BadRequest(format!("{}", reason))),
|
||||
None => Err(Error::BadRequest(reason.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait to map an error to an Internal Error code
|
||||
pub trait OkOrInternalError {
|
||||
type S2;
|
||||
fn ok_or_internal_error(self, reason: &'static str) -> Self::S2;
|
||||
@@ -119,10 +177,9 @@ impl<T> OkOrInternalError for Option<T> {
|
||||
fn ok_or_internal_error(self, reason: &'static str) -> Result<T, Error> {
|
||||
match self {
|
||||
Some(x) => Ok(x),
|
||||
None => Err(Error::InternalError(GarageError::Message(format!(
|
||||
"{}",
|
||||
reason
|
||||
)))),
|
||||
None => Err(Error::InternalError(GarageError::Message(
|
||||
reason.to_string(),
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
//! Crate for serving a S3 compatible API
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
pub mod error;
|
||||
mod error;
|
||||
pub use error::Error;
|
||||
|
||||
pub mod encoding;
|
||||
mod encoding;
|
||||
|
||||
pub mod api_server;
|
||||
pub mod signature;
|
||||
mod api_server;
|
||||
pub use api_server::run_api_server;
|
||||
|
||||
pub mod s3_copy;
|
||||
pub mod s3_delete;
|
||||
mod signature;
|
||||
|
||||
mod s3_bucket;
|
||||
mod s3_copy;
|
||||
mod s3_delete;
|
||||
pub mod s3_get;
|
||||
pub mod s3_list;
|
||||
pub mod s3_put;
|
||||
mod s3_list;
|
||||
mod s3_put;
|
||||
mod s3_xml;
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use hyper::{Body, Response};
|
||||
|
||||
use garage_model::garage::Garage;
|
||||
use garage_model::key_table::Key;
|
||||
use garage_util::time::*;
|
||||
|
||||
use crate::error::*;
|
||||
use crate::s3_xml;
|
||||
|
||||
pub fn handle_get_bucket_location(garage: Arc<Garage>) -> Result<Response<Body>, Error> {
|
||||
let loc = s3_xml::LocationConstraint {
|
||||
xmlns: (),
|
||||
region: garage.config.s3_api.s3_region.to_string(),
|
||||
};
|
||||
let xml = s3_xml::to_xml_with_header(&loc)?;
|
||||
|
||||
Ok(Response::builder()
|
||||
.header("Content-Type", "application/xml")
|
||||
.body(Body::from(xml.into_bytes()))?)
|
||||
}
|
||||
|
||||
pub fn handle_get_bucket_versioning() -> Result<Response<Body>, Error> {
|
||||
let versioning = s3_xml::VersioningConfiguration {
|
||||
xmlns: (),
|
||||
status: None,
|
||||
};
|
||||
|
||||
let xml = s3_xml::to_xml_with_header(&versioning)?;
|
||||
|
||||
Ok(Response::builder()
|
||||
.header("Content-Type", "application/xml")
|
||||
.body(Body::from(xml.into_bytes()))?)
|
||||
}
|
||||
|
||||
pub fn handle_list_buckets(api_key: &Key) -> Result<Response<Body>, Error> {
|
||||
let list_buckets = s3_xml::ListAllMyBucketsResult {
|
||||
owner: s3_xml::Owner {
|
||||
display_name: s3_xml::Value(api_key.name.get().to_string()),
|
||||
id: s3_xml::Value(api_key.key_id.to_string()),
|
||||
},
|
||||
buckets: s3_xml::BucketList {
|
||||
entries: api_key
|
||||
.authorized_buckets
|
||||
.items()
|
||||
.iter()
|
||||
.map(|(name, ts, _)| s3_xml::Bucket {
|
||||
creation_date: s3_xml::Value(msec_to_rfc3339(*ts)),
|
||||
name: s3_xml::Value(name.to_string()),
|
||||
})
|
||||
.collect(),
|
||||
},
|
||||
};
|
||||
|
||||
let xml = s3_xml::to_xml_with_header(&list_buckets)?;
|
||||
trace!("xml: {}", xml);
|
||||
|
||||
Ok(Response::builder()
|
||||
.header("Content-Type", "application/xml")
|
||||
.body(Body::from(xml))?)
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
use std::fmt::Write;
|
||||
use std::sync::Arc;
|
||||
|
||||
use hyper::{Body, Request, Response};
|
||||
@@ -14,6 +13,7 @@ use garage_model::version_table::*;
|
||||
|
||||
use crate::error::*;
|
||||
use crate::s3_put::get_headers;
|
||||
use crate::s3_xml;
|
||||
|
||||
pub async fn handle_copy(
|
||||
garage: Arc<Garage>,
|
||||
@@ -33,8 +33,7 @@ pub async fn handle_copy(
|
||||
.versions()
|
||||
.iter()
|
||||
.rev()
|
||||
.filter(|v| v.is_complete())
|
||||
.next()
|
||||
.find(|v| v.is_complete())
|
||||
.ok_or(Error::NotFound)?;
|
||||
|
||||
let source_last_state = match &source_last_v.state {
|
||||
@@ -62,6 +61,8 @@ pub async fn handle_copy(
|
||||
_ => old_meta.clone(),
|
||||
};
|
||||
|
||||
let etag = new_meta.etag.to_string();
|
||||
|
||||
// Save object copy
|
||||
match source_last_state {
|
||||
ObjectVersionData::DeleteMarker => unreachable!(),
|
||||
@@ -159,13 +160,13 @@ pub async fn handle_copy(
|
||||
}
|
||||
|
||||
let last_modified = msec_to_rfc3339(new_timestamp);
|
||||
let mut xml = String::new();
|
||||
writeln!(&mut xml, r#"<?xml version="1.0" encoding="UTF-8"?>"#).unwrap();
|
||||
writeln!(&mut xml, r#"<CopyObjectResult>"#).unwrap();
|
||||
writeln!(&mut xml, "\t<LastModified>{}</LastModified>", last_modified).unwrap();
|
||||
writeln!(&mut xml, "</CopyObjectResult>").unwrap();
|
||||
let result = s3_xml::CopyObjectResult {
|
||||
last_modified: s3_xml::Value(last_modified),
|
||||
etag: s3_xml::Value(etag),
|
||||
};
|
||||
let xml = s3_xml::to_xml_with_header(&result)?;
|
||||
|
||||
Ok(Response::builder()
|
||||
.header("Content-Type", "application/xml")
|
||||
.body(Body::from(xml.into_bytes()))?)
|
||||
.body(Body::from(xml))?)
|
||||
}
|
||||
|
||||