Compare commits

...

18 Commits

Author SHA1 Message Date
Alex Auvolat 0f9d9df83b Update Drone signature 2021-10-11 11:46:05 +02:00
Alex Auvolat f3a097abdf WIP: try to fix #93, and improve S3 ListObjects (v1 and v2) API calls 2021-10-11 11:15:47 +02:00
Alex Auvolat 1aed317818 Small changes on NGI kickoff talk 2021-10-07 11:12:34 +02:00
Quentin c5574c8409 Add links and put logos in a flexbox 2021-09-28 10:21:10 +02:00
Quentin 78f0c9ed38 Add a doc target to the Makefile 2021-09-28 10:13:14 +02:00
mricher de0228ca2a Doc: add funding disclaimer for NGI/EU grant
PNG logs optimized, render to be checked by @quentin. Fix #106.
2021-09-25 17:21:07 +02:00
Alex Auvolat df345e37db Add sticker and NGI kickoff talk 2021-09-12 13:37:33 +02:00
Alex Auvolat 740b863750 Update genkeys.sh to generate ed25519 keys instead of RSA 2021-07-06 11:16:04 +02:00
Alex Auvolat fa394dcd27 Support pkcs8 private keys (allowing for ed25519 to be used for rpc) 2021-07-06 11:16:01 +02:00
trinity-1686a 30a7dee920 exit when inconsistent level of replication is detected (#92)
fix #88

Authored-by: Trinity Pointard <trinity.pointard@gmail.com>
Reviewed-on: https://git.deuxfleurs.fr/Deuxfleurs/garage/pulls/92
Co-authored-by: trinity-1686a <trinity.pointard@gmail.com>
Co-committed-by: trinity-1686a <trinity.pointard@gmail.com>
2021-06-02 13:30:39 +02:00
Trinity Pointard b568765c75 try parsing rpc-host command-line parameter 2021-06-01 23:53:58 +02:00
Trinity Pointard e9c265e9dc format garage status output better 2021-06-01 19:12:27 +02:00
Alex Auvolat 42f692b1e0 Documentation fixes (typo and small reorganization) 2021-05-31 23:55:51 +02:00
Alex Auvolat 14fd3df654 Write about S3 client configuration 2021-05-31 17:41:21 +02:00
Alex Auvolat 56ac9fd460 Updates to documentation 2021-05-31 17:23:35 +02:00
Alex Auvolat d76a8576f4 Reorganize documentation 2021-05-31 17:13:36 +02:00
Trinity Pointard 289521886b make most changes suggested during install-party 2021-05-29 21:37:49 +02:00
Alex Auvolat ebd21b325e Write documentation on configuration file and other improvements 2021-05-28 18:00:59 +02:00
39 changed files with 1482 additions and 661 deletions
+1 -1
View File
@@ -139,6 +139,6 @@ steps:
---
kind: signature
hmac: e919f8a66d20ebfeeec56b291a8a0fdd59a482601da987fcf533d96d24768744
hmac: f0f2e947c8aa8bc5b83d25b4da22f3eb711b3fe1cc80ead4f93428dbd3d44164
...
+5
View File
@@ -1,5 +1,10 @@
.PHONY: doc
all:
clear; cargo build
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
+10 -11
View File
@@ -2,23 +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]()
- [Recovering from failures](./cookbook/recovering.md)
- [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)
+105
View File
@@ -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
```
+51
View File
@@ -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`.
+20
View File
@@ -3,3 +3,23 @@
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 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).
+305
View File
@@ -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).
+9 -5
View File
@@ -6,14 +6,18 @@ Fear not! For Garage is fully equipped to handle drive failures, in most common
## A note on availability of Garage
With nodes dispersed in 3 datacenters or more, here are the guarantees Garage provides with the default replication strategy (3 copies of all data, which is the recommended value):
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 datacenter. This includes a whole datacenter going down due to power/Internet outage.
- No data is lost as long as the machines that fail are in at most two datacenters.
- 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 datacenter in which they are located.
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
@@ -92,7 +96,7 @@ Then, replace the broken node by the new one, using:
```
garage node configure --replace <old_node_id> \
-c <capacity> -d <datacenter> -t <node_tag> <new_node_id>
-c <capacity> -z <zone> -t <node_tag> <new_node_id>
```
Garage will then start synchronizing all required data on the new node.
+39
View File
@@ -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.
+3 -1
View File
@@ -1 +1,3 @@
# Host a website
# Hosting a website
TODO
-44
View File
@@ -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.2.1`) and not the `latest` tag.
For this example, we will use the latest published version at the time of the writing which is `v0.2.1` 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.2.1
```
## 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
```
-74
View File
@@ -1,74 +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):
```
Key name: nextcloud-app-key
Key ID: GK3515373e4c851ebaad366558
Secret key: 7d37d093435a41f2aab8f13c19ba067d9776c90215f56614adad6ece597dbb34
Authorized buckets:
```
Check that everything works as intended:
```
garagectl key list
garagectl 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!
```
garagectl bucket allow \
--read \
--write
nextcloud-bucket \
--key nextcloud-app-key
```
You can check at any times allowed keys on your bucket with:
```
garagectl bucket info nextcloud-bucket
```
-73
View File
@@ -1,73 +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 -c 1 $id
done
```
## Real-world cluster
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` | `Datacenter` |
|----------|---------|------------|------------|--------------|--------------|
| 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` |
### 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.
### Capacity
Garage reasons on an arbitrary 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.
Additionaly, the capacity values used in Garage should be as small as possible, with
1 ideally 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.
### 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 -c 2 -t mercury 8781c5
garagectl node configure --datacenter par1 -c 4 -t venus 2a638e
garagectl node configure --datacenter lon1 -c 4 -t earth 68143d
garagectl node configure --datacenter bru1 -c 3 -t mars 212f75
```
-77
View File
@@ -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!
-222
View File
@@ -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:B::1 | 2 To |
| Brussels | Mars | fc00:F::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.
-42
View File
@@ -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
```
-5
View File
@@ -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.
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

+16 -1
View File
@@ -70,7 +70,7 @@ We also do not classify Swift as *Simple*.
**[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 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.
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.
@@ -111,3 +111,18 @@ Our code repository and issue tracker, which is the place where you should repor
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 Unions Horizon 2020 research and innovation programme within the framework of the NGI-POINTER Project funded under grant agreement No 871528._
+267
View File
@@ -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.
+4
View File
@@ -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.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 74 KiB

@@ -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
Binary file not shown.
+147
View File
@@ -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 :
+3 -3
View File
@@ -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
+160 -58
View File
@@ -68,26 +68,63 @@ pub async fn handle_list(
let mut result_keys = BTreeMap::<String, ListResultInfo>::new();
let mut result_common_prefixes = BTreeSet::<String>::new();
let mut next_chunk_start = if query.is_v2 {
// Determine the key from where we want to start fetch objects
// from the database, and whether the object at this key must
// be included or excluded from the response.
// This key can be the prefix in the base case, or intermediate
// points in the dataset if we are continuing a previous listing.
#[allow(clippy::collapsible_else_if)]
let (mut next_chunk_start, mut next_chunk_exclude_start) = if query.is_v2 {
if let Some(ct) = &query.continuation_token {
String::from_utf8(base64::decode(ct.as_bytes())?)?
// In V2 mode, the continuation token is defined as an opaque
// string in the spec, so we can do whatever we want with it.
// In our case, it is defined as either [ or ] (for include
// and exclude, respectively), followed by a base64 string
// representing the key to start with.
let exclude = match &ct[..1] {
"[" => false,
"]" => true,
_ => return Err(Error::BadRequest("Invalid continuation token".to_string())),
};
(
String::from_utf8(base64::decode(ct[1..].as_bytes())?)?,
exclude,
)
} else if let Some(sa) = &query.start_after {
// StartAfter has defined semantics in the spec:
// start listing at the first key immediately after.
(sa.clone(), true)
} else {
query
.start_after
.clone()
.unwrap_or_else(|| query.prefix.clone())
// In the case where neither is specified, we start
// listing at the specified prefix. If an object has this
// exact same key, we include it. (TODO is this correct?)
(query.prefix.clone(), false)
}
} else {
query.marker.clone().unwrap_or_else(|| query.prefix.clone())
if let Some(mk) = &query.marker {
// In V1 mode, the spec defines the Marker value to mean
// the same thing as the StartAfter value in V2 mode.
(mk.clone(), true)
} else {
// Base case, same as in V2 mode
(query.prefix.clone(), false)
}
};
debug!(
"List request: `{:?}` {} `{}`",
query.delimiter, query.max_keys, query.prefix
"List request: `{:?}` {} `{}`, start from {}, exclude first {}",
query.delimiter, query.max_keys, query.prefix, next_chunk_start, next_chunk_exclude_start
);
// `truncated` is a boolean that determines whether there are
// more items to be added.
let truncated;
// `last_processed_item` is the key of the last item
// that was included in the listing before truncating.
let mut last_processed_item = None;
'query_loop: loop {
// Fetch objects
let objects = garage
.object_table
.get_range(
@@ -103,64 +140,120 @@ pub async fn handle_list(
query.max_keys + 1,
objects.len()
);
let current_chunk_start = next_chunk_start.clone();
// Iterate on returned objects and add them to the response.
// If a delimiter is specified, we take care of grouping objects
// into CommonPrefixes.
for object in objects.iter() {
// If we have retrieved an object that doesn't start with
// the prefix, we know we have finished listing our stuff.
if !object.key.starts_with(&query.prefix) {
truncated = None;
truncated = false;
break 'query_loop;
}
if query.is_v2 && query.start_after.as_ref() == Some(&object.key) {
// Exclude the starting key if we have to.
if object.key == next_chunk_start && next_chunk_exclude_start {
continue;
}
if let Some(version) = object.versions().iter().find(|x| x.is_data()) {
if result_keys.len() + result_common_prefixes.len() >= query.max_keys {
truncated = Some(object.key.to_string());
break 'query_loop;
}
let common_prefix = if let Some(delimiter) = &query.delimiter {
let relative_key = &object.key[query.prefix.len()..];
relative_key
.find(delimiter)
.map(|i| &object.key[..query.prefix.len() + i + delimiter.len()])
} else {
None
};
if let Some(pfx) = common_prefix {
// Find if this object has a currently valid (non-deleted,
// non-still-uploading) version. If not, skip it.
let version = match object.versions().iter().find(|x| x.is_data()) {
Some(v) => v,
None => continue,
};
// If we don't have space to add this object to our response,
// we will need to stop here and mark the key of this object
// as the marker from where
// we want to start again in the next list call.
let cannot_add = result_keys.len() + result_common_prefixes.len() >= query.max_keys;
// Determine whether this object should be grouped inside
// a CommonPrefix because it contains the delimiter,
// or if it should be returned as an object.
let common_prefix = match &query.delimiter {
Some(delimiter) => object.key[query.prefix.len()..]
.find(delimiter)
.map(|i| &object.key[..query.prefix.len() + i + delimiter.len()]),
None => None,
};
if let Some(pfx) = common_prefix {
// In the case where this object must be grouped in a
// common prefix, handle it here.
if !result_common_prefixes.contains(pfx) {
// Determine the first listing key that starts after
// the common prefix, by finding the next possible
// string by alphabetical order.
let mut first_key_after_prefix = pfx.to_string();
let tail = first_key_after_prefix.pop().unwrap();
first_key_after_prefix.push(((tail as u8) + 1) as char);
// If this were the end of the chunk,
// the next chunk should start after this prefix
next_chunk_start = first_key_after_prefix;
next_chunk_exclude_start = false;
if cannot_add {
truncated = true;
break 'query_loop;
}
result_common_prefixes.insert(pfx.to_string());
} else {
let meta = match &version.state {
ObjectVersionState::Complete(ObjectVersionData::Inline(meta, _)) => meta,
ObjectVersionState::Complete(ObjectVersionData::FirstBlock(meta, _)) => {
meta
}
_ => unreachable!(),
};
let info = match result_keys.get(&object.key) {
None => ListResultInfo {
last_modified: version.timestamp,
size: meta.size,
etag: meta.etag.to_string(),
},
Some(_lri) => {
return Err(Error::InternalError(GarageError::Message(format!(
"Duplicate key?? {}",
object.key
))))
}
};
result_keys.insert(object.key.clone(), info);
};
}
last_processed_item = Some(object.key.clone());
continue;
};
// This is not a common prefix, we want to add it to our
// response directly.
next_chunk_start = object.key.clone();
if cannot_add {
truncated = true;
next_chunk_exclude_start = false;
break 'query_loop;
}
let meta = match &version.state {
ObjectVersionState::Complete(ObjectVersionData::Inline(meta, _)) => meta,
ObjectVersionState::Complete(ObjectVersionData::FirstBlock(meta, _)) => meta,
_ => unreachable!(),
};
let info = match result_keys.get(&object.key) {
None => ListResultInfo {
last_modified: version.timestamp,
size: meta.size,
etag: meta.etag.to_string(),
},
Some(_lri) => {
return Err(Error::InternalError(GarageError::Message(format!(
"Duplicate key?? {} (this is a bug, please report it)",
object.key
))))
}
};
result_keys.insert(object.key.clone(), info);
last_processed_item = Some(object.key.clone());
next_chunk_exclude_start = true;
}
// If our database returned less objects than what we were asking for,
// it means that no more objects are in the bucket. So we stop here.
if objects.len() < query.max_keys + 1 {
truncated = None;
truncated = false;
break 'query_loop;
}
if !objects.is_empty() {
next_chunk_start = objects[objects.len() - 1].key.clone();
// Sanity check: we should have added at least an object
// or a prefix to our returned result.
if next_chunk_start == current_chunk_start || last_processed_item.is_none() {
return Err(Error::InternalError(GarageError::Message(format!(
"S3 ListObject: made no progress, still starting at {} (this is a bug, please report it)", next_chunk_start))));
}
// Loop and fetch more objects
}
let mut result = s3_xml::ListBucketResult {
@@ -181,11 +274,10 @@ pub async fn handle_list(
true => Some(s3_xml::Value("url".to_string())),
false => None,
},
key_count: Some(s3_xml::IntValue(
result_keys.len() as i64 + result_common_prefixes.len() as i64,
)),
is_truncated: s3_xml::Value(format!("{}", truncated.is_some())),
is_truncated: s3_xml::Value(format!("{}", truncated)),
contents: vec![],
common_prefixes: vec![],
};
@@ -197,16 +289,27 @@ pub async fn handle_list(
if let Some(sa) = &query.start_after {
result.start_after = Some(uriencode_maybe(sa, query.urlencode_resp));
}
if let Some(nct) = truncated {
result.next_continuation_token = Some(s3_xml::Value(base64::encode(nct.as_bytes())));
if truncated {
let b64 = base64::encode(next_chunk_start.as_bytes());
let nct = if next_chunk_exclude_start {
format!("]{}", b64)
} else {
format!("[{}", b64)
};
result.next_continuation_token = Some(s3_xml::Value(nct));
}
} else {
// TODO: are these supposed to be urlencoded when encoding-type is URL??
if let Some(mkr) = &query.marker {
result.marker = Some(uriencode_maybe(mkr, query.urlencode_resp));
}
if let Some(next_marker) = truncated {
result.next_marker = Some(uriencode_maybe(&next_marker, query.urlencode_resp));
if truncated {
if let Some(lpi) = last_processed_item {
result.next_marker = Some(uriencode_maybe(&lpi, query.urlencode_resp));
} else {
return Err(Error::InternalError(GarageError::Message(
"S3 ListObject: last_processed_item is None but the response was truncated, indicating that many items were processed (this is a bug, please report it)".to_string())));
}
}
}
@@ -221,7 +324,6 @@ pub async fn handle_list(
}
for pfx in result_common_prefixes.iter() {
//TODO: in V1, are these urlencoded when urlencode_resp is true ?? (proably)
result.common_prefixes.push(s3_xml::CommonPrefix {
prefix: uriencode_maybe(pfx, query.urlencode_resp),
});
+60 -23
View File
@@ -1,3 +1,4 @@
use std::cmp::max;
use std::collections::HashSet;
use std::net::SocketAddr;
use std::path::PathBuf;
@@ -125,7 +126,7 @@ pub enum BucketOperation {
#[structopt(name = "allow")]
Allow(PermBucketOpt),
/// Allow key to read or write to bucket
/// Deny key from reading or writing to bucket
#[structopt(name = "deny")]
Deny(PermBucketOpt),
@@ -338,22 +339,45 @@ pub async fn cmd_status(
resp => return Err(Error::Message(format!("Invalid RPC response: {:?}", resp))),
};
let (hostname_len, addr_len, tag_len, zone_len) = status
.iter()
.map(|adv| (adv, config.members.get(&adv.id)))
.map(|(adv, cfg)| {
(
adv.state_info.hostname.len(),
adv.addr.to_string().len(),
cfg.map(|c| c.tag.len()).unwrap_or(0),
cfg.map(|c| c.zone.len()).unwrap_or(0),
)
})
.fold((0, 0, 0, 0), |(h, a, t, z), (mh, ma, mt, mz)| {
(max(h, mh), max(a, ma), max(t, mt), max(z, mz))
});
println!("Healthy nodes:");
for adv in status.iter().filter(|x| x.is_up) {
if let Some(cfg) = config.members.get(&adv.id) {
println!(
"{:?}\t{}\t{}\t[{}]\t{}\t{}",
adv.id,
adv.state_info.hostname,
adv.addr,
cfg.tag,
cfg.zone,
cfg.capacity_string()
"{id:?}\t{host}{h_pad}\t{addr}{a_pad}\t[{tag}]{t_pad}\t{zone}{z_pad}\t{capacity}",
id = adv.id,
host = adv.state_info.hostname,
addr = adv.addr,
tag = cfg.tag,
zone = cfg.zone,
capacity = cfg.capacity_string(),
h_pad = " ".repeat(hostname_len - adv.state_info.hostname.len()),
a_pad = " ".repeat(addr_len - adv.addr.to_string().len()),
t_pad = " ".repeat(tag_len - cfg.tag.len()),
z_pad = " ".repeat(zone_len - cfg.zone.len()),
);
} else {
println!(
"{:?}\t{}\t{}\tUNCONFIGURED/REMOVED",
adv.id, adv.state_info.hostname, adv.addr
"{id:?}\t{h}{h_pad}\t{addr}{a_pad}\tUNCONFIGURED/REMOVED",
id = adv.id,
h = adv.state_info.hostname,
addr = adv.addr,
h_pad = " ".repeat(hostname_len - adv.state_info.hostname.len()),
a_pad = " ".repeat(addr_len - adv.addr.to_string().len()),
);
}
}
@@ -369,25 +393,38 @@ pub async fn cmd_status(
for adv in status.iter().filter(|x| !x.is_up) {
if let Some(cfg) = config.members.get(&adv.id) {
println!(
"{:?}\t{}\t{}\t[{}]\t{}\t{}\tlast seen: {}s ago",
adv.id,
adv.state_info.hostname,
adv.addr,
cfg.tag,
cfg.zone,
cfg.capacity_string(),
(now_msec() - adv.last_seen) / 1000,
"{id:?}\t{host}{h_pad}\t{addr}{a_pad}\t[{tag}]{t_pad}\t{zone}{z_pad}\t{capacity}\tlast seen: {last_seen}s ago",
id=adv.id,
host=adv.state_info.hostname,
addr=adv.addr,
tag=cfg.tag,
zone=cfg.zone,
capacity=cfg.capacity_string(),
last_seen=(now_msec() - adv.last_seen) / 1000,
h_pad=" ".repeat(hostname_len - adv.state_info.hostname.len()),
a_pad=" ".repeat(addr_len - adv.addr.to_string().len()),
t_pad=" ".repeat(tag_len - cfg.tag.len()),
z_pad=" ".repeat(zone_len - cfg.zone.len()),
);
}
}
let (tag_len, zone_len) = config
.members
.iter()
.filter(|(&id, _)| !status.iter().any(|x| x.id == id))
.map(|(_, cfg)| (cfg.tag.len(), cfg.zone.len()))
.fold((0, 0), |(t, z), (mt, mz)| (max(t, mt), max(z, mz)));
for (id, cfg) in config.members.iter() {
if !status.iter().any(|x| x.id == *id) {
println!(
"{:?}\t{}\t{}\t{}\tnever seen",
id,
cfg.tag,
cfg.zone,
cfg.capacity_string(),
"{id:?}\t{tag}{t_pad}\t{zone}{z_pad}\t{capacity}\tnever seen",
id = id,
tag = cfg.tag,
zone = cfg.zone,
capacity = cfg.capacity_string(),
t_pad = " ".repeat(tag_len - cfg.tag.len()),
z_pad = " ".repeat(zone_len - cfg.zone.len()),
);
}
}
+10 -1
View File
@@ -28,7 +28,7 @@ use cli::*;
#[structopt(name = "garage")]
struct Opt {
/// RPC connect to this host to execute client operations
#[structopt(short = "h", long = "rpc-host", default_value = "127.0.0.1:3901")]
#[structopt(short = "h", long = "rpc-host", default_value = "127.0.0.1:3901", parse(try_from_str = parse_address))]
pub rpc_host: SocketAddr,
#[structopt(long = "ca-cert")]
@@ -87,3 +87,12 @@ async fn cli_command(opt: Opt) -> Result<(), Error> {
cli_cmd(opt.cmd, membership_rpc_cli, admin_rpc_cli, opt.rpc_host).await
}
fn parse_address(address: &str) -> Result<SocketAddr, String> {
use std::net::ToSocketAddrs;
address
.to_socket_addrs()
.map_err(|_| format!("Could not resolve {}", address))?
.next()
.ok_or_else(|| format!("Could not resolve {}", address))
}
+16
View File
@@ -141,7 +141,10 @@ impl StatusEntry {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StateInfo {
/// Hostname of the node
pub hostname: String,
/// Replication factor configured on the node
pub replication_factor: Option<usize>, // TODO Option is just for retrocompatibility. It should become a simple usize at some point
}
impl Status {
@@ -269,6 +272,7 @@ impl System {
hostname: gethostname::gethostname()
.into_string()
.unwrap_or_else(|_| "<invalid utf-8>".to_string()),
replication_factor: Some(replication_factor),
};
let ring = Ring::new(net_config, replication_factor);
@@ -504,6 +508,7 @@ impl System {
let update_lock = self.update_lock.lock().await;
let mut status: Status = self.status.borrow().as_ref().clone();
let mut has_changed = false;
let mut max_replication_factor = 0;
for node in adv.iter() {
if node.id == self.id {
@@ -529,11 +534,22 @@ impl System {
// Case 2: the node might have changed address
Some(our_node) => node.is_up && !our_node.is_up() && our_node.addr != node.addr,
};
max_replication_factor = std::cmp::max(
max_replication_factor,
node.state_info.replication_factor.unwrap_or_default(),
);
if ping_them {
to_ping.push((node.addr, Some(node.id)));
}
}
}
if self.replication_factor < max_replication_factor {
error!("Some node have a higher replication factor ({}) than this one ({}). This is not supported and might lead to bugs",
max_replication_factor,
self.replication_factor);
std::process::exit(1);
}
if has_changed {
status.recalculate_hash();
}
+7 -3
View File
@@ -279,9 +279,13 @@ impl RpcHttpClient {
tls_config: &Option<TlsConfig>,
) -> Result<Self, Error> {
let method = if let Some(cf) = tls_config {
let ca_certs = tls_util::load_certs(&cf.ca_cert)?;
let node_certs = tls_util::load_certs(&cf.node_cert)?;
let node_key = tls_util::load_private_key(&cf.node_key)?;
let ca_certs = tls_util::load_certs(&cf.ca_cert).map_err(|e| {
Error::Message(format!("Failed to open CA certificate file: {:?}", e))
})?;
let node_certs = tls_util::load_certs(&cf.node_cert)
.map_err(|e| Error::Message(format!("Failed to open certificate file: {:?}", e)))?;
let node_key = tls_util::load_private_key(&cf.node_key)
.map_err(|e| Error::Message(format!("Failed to open private key file: {:?}", e)))?;
let mut config = rustls::ClientConfig::new();
+9 -8
View File
@@ -38,15 +38,16 @@ pub fn load_certs(filename: &str) -> Result<Vec<rustls::Certificate>, Error> {
}
pub fn load_private_key(filename: &str) -> Result<rustls::PrivateKey, Error> {
let keyfile = fs::File::open(&filename)?;
let mut reader = io::BufReader::new(keyfile);
let keydata = fs::read_to_string(filename)?;
let keys = pemfile::rsa_private_keys(&mut reader).map_err(|_| {
Error::Message(format!(
"Could not decode private key from file: {}",
filename
))
})?;
let mut buf1 = keydata.as_bytes();
let rsa_keys = pemfile::rsa_private_keys(&mut buf1).unwrap_or_default();
let mut buf2 = keydata.as_bytes();
let pkcs8_keys = pemfile::pkcs8_private_keys(&mut buf2).unwrap_or_default();
let mut keys = rsa_keys;
keys.extend(pkcs8_keys.into_iter());
if keys.len() != 1 {
return Err(Error::Message(format!(