v0.2.0-rc1

This commit is contained in:
Ben Johnson 2013-11-13 22:07:46 -05:00
commit aa047b124d
294 changed files with 17900 additions and 33648 deletions

6
.gitignore vendored
View File

@ -1,10 +1,6 @@
src/
pkg/
/etcd
/server/release_version.go
/go-bindata
release_version.go
/machine*
.vagrant/
conf
info
log

33
CHANGELOG Normal file
View File

@ -0,0 +1,33 @@
v0.2
* Support directory creation and removal.
* Add Compare-and-Swap (CAS) support.
* Support recursive GETs.
* Support fully consistent GETs.
* Allow clients to watch specific paths.
* Allow clients to watch for key expiration.
* Unique key generation.
* Support hidden paths.
* Refactor low-level data store.
* Modularize store, server and API code.
* Integrate Gorilla Web Toolkit.
* Add tiered configuration (command line args, env variables, config file).
* Add peer protocol versioning.
* Add rolling upgrade support for future versions.
* Sync key expiration across cluster.
* Significantly improve test coverage.
* Improve migration testing.
* Configurable snapshot count.
* Reduce TCP connection count.
* Fix TCP connection leak.
* Bug Fixes: https://github.com/coreos/etcd/issues?milestone=1&state=closed
Contributors:
* Xiang Li (@xiangli-cmu)
* Ben Johnson (@benbjohnson)
* Brandon Philips (@philips)
* Yifan (@yifan-gu)
* Rob Szumski
* Hongchao Deng (@fengjingchao)
* Kelsey Hightower (@kelseyhightower)
* Adrián (@adrianlzt)
* Antonio Terreno (@aterreno)

View File

@ -0,0 +1,58 @@
Error Code
======
This document describes the error code in **Etcd** project.
It's categorized into four groups:
- Command Related Error
- Post Form Related Error
- Raft Related Error
- Etcd Related Error
Error code corresponding strerror
------
const (
EcodeKeyNotFound = 100
EcodeTestFailed = 101
EcodeNotFile = 102
EcodeNoMoreMachine = 103
EcodeNotDir = 104
EcodeNodeExist = 105
EcodeKeyIsPreserved = 106
EcodeValueRequired = 200
EcodePrevValueRequired = 201
EcodeTTLNaN = 202
EcodeIndexNaN = 203
EcodeRaftInternal = 300
EcodeLeaderElect = 301
EcodeWatcherCleared = 400
EcodeEventIndexCleared = 401
)
// command related errors
errors[100] = "Key Not Found"
errors[101] = "Test Failed" //test and set
errors[102] = "Not A File"
errors[103] = "Reached the max number of machines in the cluster"
errors[104] = "Not A Directory"
errors[105] = "Already exists" // create
errors[106] = "The prefix of given key is a keyword in etcd"
// Post form related errors
errors[200] = "Value is Required in POST form"
errors[201] = "PrevValue is Required in POST form"
errors[202] = "The given TTL in POST form is not a number"
errors[203] = "The given index in POST form is not a number"
// raft related errors
errors[300] = "Raft Internal Error"
errors[301] = "During Leader Election"
// etcd related errors
errors[400] = "watcher is cleared due to etcd recovery"
errors[401] = "The event in requested index is outdated and cleared"

View File

@ -0,0 +1,101 @@
#Etcd File System
## Structure
[TODO]
![alt text](./img/etcd_fs_structure.jpg "etcd file system structure")
## Node
In **Etcd**, the **Node** is the rudimentary element constructing the whole.
Currently **Etcd** file system is comprised in a Unix-like way of files and directories, and they are two kinds of nodes different in:
- **File Node** has data associated with it.
- **Directory Node** has children nodes associated with it.
Besides the file and directory difference, all nodes have common attributes and operations as follows:
### Attributes:
- **Expiration Time** [optional]
The node will be deleted when it expires.
- **ACL**
The path of access control list of the node.
### Operation:
- **Get** (path, recursive, sorted)
Get the content of the node
- If the node is a file, the data of the file will be returned.
- If the node is a directory, the child nodes of the directory will be returned.
- If recursive is true, it will recursively get the nodes of the directory.
- If sorted is true, the result will be sorted based on the path.
- **Create** (path, value[optional], ttl [optional])
Create a file. Create operation will help to create intermediate directories with no expiration time.
- If the file already exists, create will fail.
- If the value is given, set will create a file.
- If the value is not given, set will crate a directory.
- If ttl is given, the node will be deleted when it expires.
- **Update** (path, value[optional], ttl [optional])
Update the content of the node.
- If the value is given, the value of the key will be updated.
- If ttl is given, the expiration time of the node will be updated.
- **Delete** (path, recursive)
Delete the node of given path.
- If the node is a directory:
- If recursive is true, the operation will delete all nodes under the directory.
- If recursive is false, error will be returned.
- **TestAndSet** (path, prevValue [prevIndex], value, ttl)
Atomic *test and set* value to a file. If test succeeds, this operation will change the previous value of the file to the given value.
- If the prevValue is given, it will test against previous value of
the node.
- If the prevValue is empty, it will test if the node is not existing.
- If the prevValue is not empty, it will test if the prevValue is equal to the current value of the file.
- If the prevIndex is given, it will test if the create/last modified index of the node is equal to prevIndex.
- **Renew** (path, ttl)
Set the node's expiration time to (current time + ttl)
## ACL
### Theory
Etcd exports a Unix-like file system interface consisting of files and directories, collectively called nodes.
Each node has various meta-data, including three names of access control lists used to control reading, writing and changing (change ACL names for the node).
We are storing the ACL names for nodes under a special *ACL* directory.
Each node has ACL name corresponding to one file within *ACL* dir.
Unless overridden, a node naturally inherits the ACL names of its parent directory on creation.
For each ACL name, it has three children: *R (Reading)*, *W (Writing)*, *C (Changing)*
Each permission is also a node. Under the node it contains the users who have this permission for the file refering to this ACL name.
### Example
[TODO]
### Diagram
[TODO]
### Interface
Testing permissions:
- (node *Node) get_perm()
- (node *Node) has_perm(perm string, user string)
Setting/Changing permissions:
- (node *Node) set_perm(perm string)
- (node *Node) change_ACLname(aclname string)
## User Group
[TODO]

View File

@ -0,0 +1,104 @@
# Etcd Configuration
Configuration options can be set in three places:
1. Command line flags
2. Environment variables
3. Configuration file
Options set on the command line take precedence over all other sources.
Options set in environment variables take precedence over options set in
configuration files.
## Command Line Flags
### Required
* `-n` - The node name. Defaults to `default-name`.
### Optional
* `-c` - The advertised public hostname:port for client communication. Defaults to `127.0.0.1:4001`.
* `-cl` - The listening hostname for client communication. Defaults to advertised ip.
* `-C` - A comma separated list of machines in the cluster (i.e `"203.0.113.101:7001,203.0.113.102:7001"`).
* `-CF` - The file path containing a comma separated list of machines in the cluster.
* `-clientCAFile` - The path of the client CAFile. Enables client cert authentication when present.
* `-clientCert` - The cert file of the client.
* `-clientKey` - The key file of the client.
* `-configfile` - The path of the etcd config file. Defaults to `/etc/etcd/etcd.conf`.
* `-cors` - A comma separated white list of origins for cross-origin resource sharing.
* `-cpuprofile` - The path to a file to output cpu profile data. Enables cpu profiling when present.
* `-d` - The directory to store log and snapshot. Defaults to the current working directory.
* `-m` - The max size of result buffer. Defaults to `1024`.
* `-maxsize` - The max size of the cluster. Defaults to `9`.
* `-r` - The max retry attempts when trying to join a cluster. Defaults to `3`.
* `-s` - The advertised public hostname:port for server communication. Defaults to `127.0.0.1:7001`.
* `-sl` - The listening hostname for server communication. Defaults to advertised ip.
* `-serverCAFile` - The path of the CAFile. Enables client/peer cert authentication when present.
* `-serverCert` - The cert file of the server.
* `-serverKey` - The key file of the server.
* `-snapshot` - Open or close snapshot. Defaults to `false`.
* `-v` - Enable verbose logging. Defaults to `false`.
* `-vv` - Enable very verbose logging. Defaults to `false`.
* `-version` - Print the version and exit.
* `-w` - The hostname:port of web interface.
## Configuration File
The etcd configuration file is written in [TOML](https://github.com/mojombo/toml)
and read from `/etc/etcd/etcd.conf` by default.
```TOML
advertised_url = "127.0.0.1:4001"
ca_file = ""
cert_file = ""
cors = []
cpu_profile_file = ""
datadir = "."
key_file = ""
listen_host = "127.0.0.1:4001"
machines = []
machines_file = ""
max_cluster_size = 9
max_result_buffer = 1024
max_retry_attempts = 3
name = "default-name"
snapshot = false
verbose = false
very_verbose = false
web_url = ""
[peer]
advertised_url = "127.0.0.1:7001"
ca_file = ""
cert_file = ""
key_file = ""
listen_host = "127.0.0.1:7001"
```
## Environment Variables
* `ETCD_ADVERTISED_URL`
* `ETCD_CA_FILE`
* `ETCD_CERT_FILE`
* `ETCD_CORS`
* `ETCD_CONFIG_FILE`
* `ETCD_CPU_PROFILE_FILE`
* `ETCD_DATADIR`
* `ETCD_KEY_FILE`
* `ETCD_LISTEN_HOST`
* `ETCD_MACHINES`
* `ETCD_MACHINES_FILE`
* `ETCD_MAX_RETRY_ATTEMPTS`
* `ETCD_MAX_CLUSTER_SIZE`
* `ETCD_MAX_RESULT_BUFFER`
* `ETCD_NAME`
* `ETCD_SNAPSHOT`
* `ETCD_VERBOSE`
* `ETCD_VERY_VERBOSE`
* `ETCD_WEB_URL`
* `ETCD_PEER_ADVERTISED_URL`
* `ETCD_PEER_CA_FILE`
* `ETCD_PEER_CERT_FILE`
* `ETCD_PEER_KEY_FILE`
* `ETCD_PEER_LISTEN_HOST`

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

400
README.md
View File

@ -1,26 +1,26 @@
# etcd
README version 0.1.0
README version 0.2.0
[![Build Status](https://travis-ci.org/coreos/etcd.png)](https://travis-ci.org/coreos/etcd)
A highly-available key value store for shared configuration and service discovery. etcd is inspired by zookeeper and doozer, with a focus on:
A highly-available key value store for shared configuration and service discovery.
etcd is inspired by zookeeper and doozer, with a focus on:
* Simple: curl'able user facing API (HTTP+JSON)
* Secure: optional SSL client cert authentication
* Fast: benchmarked 1000s of writes/s per instance
* Reliable: Properly distributed using Raft
Etcd is written in Go and uses the [raft][raft] consensus algorithm to manage a highly-available replicated log.
Etcd is written in Go and uses the [Raft][raft] consensus algorithm to manage a highly-available replicated log.
See [etcdctl][etcdctl] for a simple command line client. Or feel free to just use curl, as in the examples below.
See [etcdctl][etcdctl] for a simple command line client.
Or feel free to just use curl, as in the examples below.
[raft]: https://github.com/coreos/go-raft
[etcdctl]: http://coreos.com/docs/etcdctl/
## Contact
- Mailing list: http://coreos.com/lists/etcd-dev/
- IRC: #coreos on irc.freenode.net
## Getting Started
@ -30,6 +30,7 @@ The latest release is available as a binary at [Github][github-release].
[github-release]: https://github.com/coreos/etcd/releases/
### Building
You can build etcd from source:
@ -48,9 +49,11 @@ _NOTE_: you need go 1.1+. Please check your installation with
go version
```
### Running a single node
These examples will use a single node cluster to show you the basics of the etcd REST API. Lets start etcd:
These examples will use a single node cluster to show you the basics of the etcd REST API.
Let's start etcd:
```sh
./etcd -d node0 -n node0
@ -60,249 +63,328 @@ This will bring up an etcd node listening on port 4001 for client communication
The `-d node0` argument tells etcd to write node configuration, logs and snapshots to the `./node0/` directory.
The `-n node0` tells the rest of the cluster that this node is named node0.
## Usage
### Setting the value to a key
Lets set the first key-value pair to the node. In this case the key is `/message` and the value is `Hello world`.
Lets set the first key-value pair to the node.
In this case the key is `/message` and the value is `Hello world`.
```sh
curl -L http://127.0.0.1:4001/v1/keys/message -d value="Hello world"
curl -L http://127.0.0.1:4001/v2/keys/message -X PUT -d value="Hello world"
```
```json
{"action":"SET","key":"/message","value":"Hello world","newKey":true,"index":3}
{"action":"set","key":"/message","value":"Hello world","modifiedIndex":2}
```
This response contains five fields. We will introduce three more fields as we try more commands.
This response contains four fields.
We will introduce three more fields as we try more commands.
1. The action of the request; we set the value via a POST request, thus the action is `SET`.
1. The action of the request; we set the value via a `PUT` request, thus the action is `set`.
2. The key of the request; we set `/message` to `Hello world!`, so the key field is `/message`.
Notice we use a file system like structure to represent the key-value pairs. So each key starts with `/`.
2. The key of the request; we set `/message` to `Hello world`, so the key field is `/message`.
We use a file system like structure to represent the key-value pairs so each key starts with `/`.
3. The current value of the key; we set the value to`Hello world`.
4. If we set a new key; `/message` did not exist before, so this is a new key.
4. Modified Index is a unique, monotonically incrementing index created for each change to etcd.
Requests that change the index include `set`, `delete`, `update`, `create` and `compareAndSwap`.
Since the `get` and `watch` commands do not change state in the store, they do not change the index.
You may notice that in this example the index is `2` even though it is the first request you sent to the server.
This is because there are internal commands that also change the state like adding and syncing servers.
5. Index is the unique internal log index of the set request. Requests that change the log index include `SET`, `DELETE` and `TESTANDSET`. The `GET`, `LIST` and `WATCH` commands do not change state in the store and so they do not change the index. You may notice that in this example the index is 3, although it is the first request you sent to the server. This is because there are internal commands that also change the state like adding and syncing servers.
### Get the value of a key
Get the value that we just set in `/message` by issuing a GET:
We can get the value that we just set in `/message` by issuing a `GET` request:
```sh
curl -L http://127.0.0.1:4001/v1/keys/message
curl -L http://127.0.0.1:4001/v2/keys/message
```
```json
{"action":"GET","key":"/message","value":"Hello world","index":3}
{"action":"get","key":"/message","value":"Hello world","modifiedIndex":2}
```
### Change the value of a key
Change the value of `/message` from `Hello world` to `Hello etcd` with another POST to the key:
### Changing the value of a key
You can change the value of `/message` from `Hello world` to `Hello etcd` with another `PUT` request to the key:
```sh
curl -L http://127.0.0.1:4001/v1/keys/message -d value="Hello etcd"
curl -L http://127.0.0.1:4001/v1/keys/message -XPUT -d value="Hello etcd"
```
```json
{"action":"SET","key":"/message","prevValue":"Hello world","value":"Hello etcd","index":4}
{"action":"set","key":"/message","prevValue":"Hello world","value":"Hello etcd","index":3}
```
Notice that the `prevValue` is set to `Hello world`.
Notice that the `prevValue` is set to the previous value of the key - `Hello world`.
It is useful when you want to atomically set a value to a key and get its old value.
### Delete a key
Remove the `/message` key with a DELETE:
### Deleting a key
You can remove the `/message` key with a `DELETE` request:
```sh
curl -L http://127.0.0.1:4001/v1/keys/message -X DELETE
curl -L http://127.0.0.1:4001/v2/keys/message -XDELETE
```
```json
{"action":"DELETE","key":"/message","prevValue":"Hello etcd","index":5}
{"action":"delete","key":"/message","prevValue":"Hello etcd","modifiedIndex":4}
```
### Using key TTL
Keys in etcd can be set to expire after a specified number of seconds. That is done by setting a TTL (time to live) on the key when you POST:
Keys in etcd can be set to expire after a specified number of seconds.
You can do this by setting a TTL (time to live) on the key when send a `PUT` request:
```sh
curl -L http://127.0.0.1:4001/v1/keys/foo -d value=bar -d ttl=5
curl -L http://127.0.0.1:4001/v2/keys/foo -XPUT -d value=bar -d ttl=5
```
```json
{"action":"SET","key":"/foo","value":"bar","newKey":true,"expiration":"2013-07-11T20:31:12.156146039-07:00","ttl":4,"index":6}
{"action":"set","key":"/foo","value":"bar","expiration":"2013-11-12T20:21:22.629352334-05:00","ttl":5,"modifiedIndex":5}
```
Note the last two new fields in response:
Note the two new fields in response:
1. The expiration is the time that this key will expire and be deleted.
1. The `expiration` is the time that this key will expire and be deleted.
2. The ttl is the time to live of the key.
2. The `ttl` is the time to live for the key, in seconds.
Now you can try to get the key by sending:
_NOTE_: Keys can only be expired by a cluster leader so if a node gets disconnected from the cluster, its keys will not expire until it rejoins.
Now you can try to get the key by sending a `GET` request:
```sh
curl -L http://127.0.0.1:4001/v1/keys/foo
curl -L http://127.0.0.1:4001/v2/keys/foo
```
If the TTL has expired, the key will be deleted, and you will be returned a 100.
```json
{"errorCode":100,"message":"Key Not Found","cause":"/foo"}
{"errorCode":100,"message":"Key Not Found","cause":"/foo","index":6}
```
### Watching a prefix
We can watch a path prefix and get notifications if any key change under that prefix.
### Waiting for a change
In one terminal, we send a watch request:
We can watch for a change on a key and receive a notification by using long polling.
This also works for child keys by passing `recursive=true` in curl.
In one terminal, we send a get request with `wait=true` :
```sh
curl -L http://127.0.0.1:4001/v1/watch/foo
curl -L http://127.0.0.1:4001/v2/keys/foo?wait=true
```
Now, we are watching at the path prefix `/foo` and wait for any changes under this path.
Now we are waiting for any changes at path `/foo`.
In another terminal, we set a key `/foo/foo` to `barbar` to see what will happen:
In another terminal, we set a key `/foo` with value `bar`:
```sh
curl -L http://127.0.0.1:4001/v1/keys/foo/foo -d value=barbar
curl -L http://127.0.0.1:4001/v2/keys/foo -XPUT -d value=bar
```
The first terminal should get the notification and return with the same response as the set request.
```json
{"action":"SET","key":"/foo/foo","value":"barbar","newKey":true,"index":7}
{"action":"set","key":"/foo","value":"bar","modifiedIndex":7}
```
However, the watch command can do more than this. Using the the index we can watch for commands that has happened in the past. This is useful for ensuring you don't miss events between watch commands.
However, the watch command can do more than this.
Using the the index we can watch for commands that has happened in the past.
This is useful for ensuring you don't miss events between watch commands.
Let's try to watch for the set command of index 6 again:
Let's try to watch for the set command of index 7 again:
```sh
curl -L http://127.0.0.1:4001/v1/watch/foo -d index=6
curl -L http://127.0.0.1:4001/v2/keys/foo?wait=true\&waitIndex=7
```
The watch command returns immediately with the same response as previous.
### Atomic Test and Set
Etcd can be used as a centralized coordination service in a cluster and `TestAndSet` is the most basic operation to build distributed lock service. This command will set the value only if the client provided `prevValue` is equal the current key value.
### Atomic Compare-and-Swap (CAS)
Here is a simple example. Let's create a key-value pair first: `foo=one`.
Etcd can be used as a centralized coordination service in a cluster and `CompareAndSwap` is the most basic operation to build distributed lock service.
This command will set the value of a key only if the client-provided conditions are equal to the current conditions.
The current comparable conditions are:
1. `prevValue` - checks the previous value of the key.
2. `prevIndex` - checks the previous index of the key.
3. `prevExist` - checks existence of the key: if `prevExist` is true, it is a `update` request; if prevExist is `false`, it is a `create` request.
Here is a simple example.
Let's create a key-value pair first: `foo=one`.
```sh
curl -L http://127.0.0.1:4001/v1/keys/foo -d value=one
curl -L http://127.0.0.1:4001/v1/keys/foo -XPUT -d value=one
```
Let's try an invalid `TestAndSet` command.
We can give another parameter prevValue to set command to make it a TestAndSet command.
Let's try an invalid `CompareAndSwap` command first.
We can provide the `prevValue` parameter to the set command to make it a `CompareAndSwap` command.
```sh
curl -L http://127.0.0.1:4001/v1/keys/foo -d prevValue=two -d value=three
curl -L http://127.0.0.1:4001/v1/keys/foo?prevValue=two -XPUT -d value=three
```
This will try to test if the previous of the key is two, it is change it to three.
This will try to compare the previous value of the key and the previous value we provided. If they are equal, the value of the key will change to three.
```json
{"errorCode":101,"message":"The given PrevValue is not equal to the value of the key","cause":"TestAndSet: one!=two"}
{"errorCode":101,"message":"Test Failed","cause":"[two != one] [0 != 8]","index":8}
```
which means `testAndSet` failed.
which means `CompareAndSwap` failed.
Let us try a valid one.
Let's try a valid condition:
```sh
curl -L http://127.0.0.1:4001/v1/keys/foo -d prevValue=one -d value=two
curl -L http://127.0.0.1:4001/v2/keys/foo?prevValue=one -XPUT -d value=two
```
The response should be
```json
{"action":"SET","key":"/foo","prevValue":"one","value":"two","index":10}
{"action":"compareAndSwap","key":"/foo","prevValue":"one","value":"two","modifiedIndex":9}
```
We successfully changed the value from “one” to “two”, since we give the correct previous value.
We successfully changed the value from “one” to “two” since we gave the correct previous value.
To set a key to a given value only if it does not exist, simply supply an empty prevValue parameter.
```sh
curl -L http://127.0.0.1:4001/v1/keys/bar -d prevValue= -d value=four
```
Since the key "bar" does not exist, the response should be
```json
{"action":"SET","key":"/bar","value":"four","newKey":true,"index":11}
```
However, using a empty prevValue with a key that does exist will fail.
```sh
curl -L http://127.0.0.1:4001/v1/keys/bar -d prevValue= -d value=five
```
will result in
```json
{"errorCode":101,"message":"The given PrevValue is not equal to the value of the key","cause":"TestAndSet: four!="}
```
### Listing a directory
Last we provide a simple List command to list all the keys under a prefix path.
In etcd we can store two types of things: keys and directories.
Keys store a single string value.
Directories store a set of keys and/or other directories.
Let us create some keys first.
In this example, let's first create some keys:
We already have `/foo/foo=barbar`
We create another one `/foo/foo_dir/foo=barbarbar`
We already have `/foo=two` so now we'll create another one called `/foo_dir/foo` with the value of `bar`:
```sh
curl -L http://127.0.0.1:4001/v1/keys/foo/foo_dir/bar -d value=barbarbar
curl -L http://127.0.0.1:4001/v2/keys/foo_dir/foo -XPUT -d value=bar
```
Now list the keys under `/foo`
```sh
curl -L http://127.0.0.1:4001/v1/keys/foo/
```
We should see the response as an array of items
```json
[{"action":"GET","key":"/foo/foo","value":"barbar","index":10},{"action":"GET","key":"/foo/foo_dir","dir":true,"index":10}]
{"action":"set","key":"/foo_dir/foo","value":"bar","modifiedIndex":10}
```
which meas `foo=barbar` is a key-value pair under `/foo` and `foo_dir` is a directory.
Now we can list the keys under root `/`:
```sh
curl -L http://127.0.0.1:4001/v2/keys/
```
We should see the response as an array of items:
```json
{"action":"get","key":"/","dir":true,"kvs":[{"key":"/foo","value":"two","modifiedIndex":9},{"key":"/foo_dir","dir":true,"modifiedIndex":10}],"modifiedIndex":0}
```
Here we can see `/foo` is a key-value pair under `/` and `/foo_dir` is a directory.
We can also recursively get all the contents under a directory by adding `recursive=true`.
```sh
curl -L http://127.0.0.1:4001/v2/keys/?recursive=true
```
```json
{"action":"get","key":"/","dir":true,"kvs":[{"key":"/foo","value":"two","modifiedIndex":9},{"key":"/foo_dir","dir":true,"kvs":[{"key":"/foo_dir/foo","value":"bar","modifiedIndex":10}],"modifiedIndex":10}],"modifiedIndex":0}
```
### Deleting a directory
Now let's try to delete the directory `/foo_dir`.
To delete a directory, we must add `recursive=true`.
```sh
curl -L http://127.0.0.1:4001/v2/keys/foo_dir?recursive=true -XDELETE
```
```json
{"action":"delete","key":"/foo_dir","dir":true,"modifiedIndex":11}
```
### Creating a hidden node
We can create a hidden key-value pair or directory by add a `_` prefix.
The hidden item will not be listed when sending a `GET` request for a directory.
First we'll add a hidden key named `/_message`:
```sh
curl -L http://127.0.0.1:4001/v2/keys/_message -XPUT -d value="Hello hidden world"
```
```json
{"action":"set","key":"/_message","value":"Hello hidden world","modifiedIndex":12}
```
Next we'll add a regular key named `/message`:
```sh
curl -L http://127.0.0.1:4001/v2/keys/message -XPUT -d value="Hello world"
```
```json
{"action":"set","key":"/message","value":"Hello world","modifiedIndex":13}
```
Now let's try to get a listing of keys under the root directory, `/`:
```sh
curl -L http://127.0.0.1:4001/v2/keys/
```
```json
{"action":"get","key":"/","dir":true,"kvs":[{"key":"/foo","value":"two","modifiedIndex":9},{"key":"/message","value":"Hello world","modifiedIndex":13}],"modifiedIndex":0}
```
Here we see the `/message` key but our hidden `/_message` key is not returned.
## Advanced Usage
### Transport security with HTTPS
Etcd supports SSL/TLS and client cert authentication for clients to server, as well as server to server communication
Etcd supports SSL/TLS and client cert authentication for clients to server, as well as server to server communication.
First, you need to have a CA cert `clientCA.crt` and signed key pair `client.crt`, `client.key`. This site has a good reference for how to generate self-signed key pairs:
First, you need to have a CA cert `clientCA.crt` and signed key pair `client.crt`, `client.key`.
This site has a good reference for how to generate self-signed key pairs:
http://www.g-loaded.eu/2005/11/10/be-your-own-ca/
For testing you can use the certificates in the `fixtures/ca` directory.
Next, lets configure etcd to use this keypair:
Let's configure etcd to use this keypair:
```sh
./etcd -n node0 -d node0 -clientCert=./fixtures/ca/server.crt -clientKey=./fixtures/ca/server.key.insecure -f
```
`-f` forces new node configuration if existing configuration is found (WARNING: data loss!)
`-clientCert` and `-clientKey` are the key and cert for transport layer security between client and server
There are a few new options we're using:
You can now test the configuration using https:
* `-f` - forces a new node configuration, even if an existing configuration is found. (WARNING: data loss!)
* `-clientCert` and `-clientKey` specify the location of the cert and key files to be used for for transport layer security between the client and server.
You can now test the configuration using HTTPS:
```sh
curl --cacert fixtures/ca/ca.crt https://127.0.0.1:4001/v1/keys/foo -d value=bar -v
curl --cacert ./fixtures/ca/server-chain.pem https://127.0.0.1:4001/v2/keys/foo -XPUT -d value=bar -v
```
You should be able to see the handshake succeed.
@ -313,15 +395,17 @@ SSLv3, TLS handshake, Finished (20):
...
```
And also the response from the etcd server.
And also the response from the etcd server:
```json
{"action":"SET","key":"/foo","value":"bar","newKey":true,"index":3}
{"action":"set","key":"/foo","prevValue":"bar","value":"bar","modifiedIndex":3}
```
### Authentication with HTTPS client certificates
We can also do authentication using CA certs. The clients will provide their cert to the server and the server will check whether the cert is signed by the CA and decide whether to serve the request.
We can also do authentication using CA certs.
The clients will provide their cert to the server and the server will check whether the cert is signed by the CA and decide whether to serve the request.
```sh
./etcd -n node0 -d node0 -clientCAFile=./fixtures/ca/ca.crt -clientCert=./fixtures/ca/server.crt -clientKey=./fixtures/ca/server.key.insecure -f
@ -332,7 +416,7 @@ We can also do authentication using CA certs. The clients will provide their cer
Try the same request to this server:
```sh
curl --cacert fixtures/ca/ca.crt https://127.0.0.1:4001/v1/keys/foo -d value=bar -v
curl --cacert ./fixtures/ca/server-chain.pem https://127.0.0.1:4001/v2/keys/foo -XPUT -d value=bar -v
```
The request should be rejected by the server.
@ -346,10 +430,11 @@ routines:SSL3_READ_BYTES:sslv3 alert bad certificate
We need to give the CA signed cert to the server.
```sh
curl -L https://127.0.0.1:4001/v1/keys/foo -d value=bar -v --key myclient.key --cert myclient.crt -cacert clientCA.crt
curl --key ./fixtures/ca/server2.key.insecure --cert ./fixtures/ca/server2.crt --cacert ./fixtures/ca/server-chain.pem -L https://127.0.0.1:4001/v1/keys/foo -XPUT -d value=bar -v
```
You should able to see
You should able to see:
```
...
SSLv3, TLS handshake, CERT verify (15):
@ -360,16 +445,18 @@ TLS handshake, Finished (20)
And also the response from the server:
```json
{"action":"SET","key":"/foo","value":"bar","newKey":true,"index":3}
{"action":"set","key":"/foo","prevValue":"bar","value":"bar","modifiedIndex":3}
```
## Clustering
### Example cluster of three machines
Let's explore the use of etcd clustering. We use go-raft as the underlying distributed protocol which provides consistency and persistence of the data across all of the etcd instances.
Let's explore the use of etcd clustering.
We use Raft as the underlying distributed protocol which provides consistency and persistence of the data across all of the etcd instances.
We'll start by creating 3 new etcd instances.
Let start by creating 3 new etcd instances.
We use -s to specify server port and -c to specify client port and -d to specify the directory to store the log and info of the node in the cluster
@ -377,17 +464,17 @@ We use -s to specify server port and -c to specify client port and -d to specify
./etcd -s 127.0.0.1:7001 -c 127.0.0.1:4001 -d nodes/node1 -n node1
```
**Note:** If you want to run etcd on external IP address and still have access locally you need to add `-cl 0.0.0.0` so that it will listen on both external and localhost addresses.
**Note:** If you want to run etcd on an external IP address and still have access locally, you'll need to add `-cl 0.0.0.0` so that it will listen on both external and localhost addresses.
A similar argument `-sl` is used to setup the listening address for the server port.
Let's join two more nodes to this cluster using the -C argument:
Let's join two more nodes to this cluster using the `-C` argument:
```sh
./etcd -s 127.0.0.1:7002 -c 127.0.0.1:4002 -C 127.0.0.1:7001 -d nodes/node2 -n node2
./etcd -s 127.0.0.1:7003 -c 127.0.0.1:4003 -C 127.0.0.1:7001 -d nodes/node3 -n node3
```
Get the machines in the cluster:
We can retrieve a list of machines in the cluster using the HTTP API:
```sh
curl -L http://127.0.0.1:4001/v1/machines
@ -399,24 +486,23 @@ We should see there are three nodes in the cluster
http://127.0.0.1:4001, http://127.0.0.1:4002, http://127.0.0.1:4003
```
The machine list is also available via this API:
The machine list is also available via the main key API:
```sh
curl -L http://127.0.0.1:4001/v1/keys/_etcd/machines
```
```json
[{"action":"GET","key":"/_etcd/machines/node1","value":"raft=http://127.0.0.1:7001&etcd=http://127.0.0.1:4001","index":4},{"action":"GET","key":"/_etcd/machines/node2","value":"raft=http://127.0.0.1:7002&etcd=http://127.0.0.1:4002","index":4},{"action":"GET","key":"/_etcd/machines/node3","value":"raft=http://127.0.0.1:7003&etcd=http://127.0.0.1:4003","index":4}]
[{"action":"get","key":"/_etcd/machines/node1","value":"raft=http://127.0.0.1:7001\u0026etcd=http://127.0.0.1:4001","index":1},{"action":"get","key":"/_etcd/machines/node2","value":"raft=http://127.0.0.1:7002\u0026etcd=http://127.0.0.1:4002","index":1},{"action":"get","key":"/_etcd/machines/node3","value":"raft=http://127.0.0.1:7003\u0026etcd=http://127.0.0.1:4003","index":1}]
```
The key of the machine is based on the ```commit index``` when it was added. The value of the machine is ```hostname```, ```raft port``` and ```client port```.
Also try to get the current leader in the cluster
We can also get the current leader in the cluster:
```
curl -L http://127.0.0.1:4001/v1/leader
curl -L http://127.0.0.1:4001/v2/leader
```
The first server we set up should be the leader, if it has not died during these commands.
The first server we set up should still be the leader unless it has died during these commands.
```
http://127.0.0.1:7001
@ -425,25 +511,26 @@ http://127.0.0.1:7001
Now we can do normal SET and GET operations on keys as we explored earlier.
```sh
curl -L http://127.0.0.1:4001/v1/keys/foo -d value=bar
curl -L http://127.0.0.1:4001/v2/keys/foo -XPUT -d value=bar
```
```json
{"action":"SET","key":"/foo","value":"bar","newKey":true,"index":5}
{"action":"set","key":"/foo","value":"bar","modifiedIndex":4}
```
### Killing Nodes in the Cluster
Let's kill the leader of the cluster and get the value from the other machine:
Now if we kill the leader of the cluster, we can get the value from one of the other two machines:
```sh
curl -L http://127.0.0.1:4002/v1/keys/foo
```
A new leader should have been elected.
We can also see that a new leader has been elected:
```
curl -L http://127.0.0.1:4001/v1/leader
curl -L http://127.0.0.1:4002/v1/leader
```
```
@ -456,17 +543,11 @@ or
http://127.0.0.1:7003
```
You should be able to see this:
```json
{"action":"GET","key":"/foo","value":"bar","index":5}
```
It succeeded!
### Testing Persistence
OK. Next let us kill all the nodes to test persistence. Restart all the nodes using the same command as before.
Next we'll kill all the nodes to test persistence.
Type `CTRL-C` on each terminal and then rerun the same command you used to start each node.
Your request for the `foo` key will return the correct value:
@ -475,19 +556,24 @@ curl -L http://127.0.0.1:4002/v1/keys/foo
```
```json
{"action":"GET","key":"/foo","value":"bar","index":5}
{"action":"get","key":"/foo","value":"bar","index":4}
```
### Using HTTPS between servers
In the previous example we showed how to use SSL client certs for client to server communication. Etcd can also do internal server to server communication using SSL client certs. To do this just change the ```-client*``` flags to ```-server*```.
In the previous example we showed how to use SSL client certs for client-to-server communication.
Etcd can also do internal server-to-server communication using SSL client certs.
To do this just change the `-client*` flags to `-server*`.
If you are using SSL for server-to-server communication, you must use it on all instances of etcd.
If you are using SSL for server to server communication, you must use it on all instances of etcd.
## Contributing
See [CONTRIBUTING](https://github.com/coreos/etcd/blob/master/CONTRIBUTING.md) for details on submitting patches and contacting developers via IRC and mailing lists.
## Libraries and Tools
**Tools**
@ -503,7 +589,6 @@ See [CONTRIBUTING](https://github.com/coreos/etcd/blob/master/CONTRIBUTING.md) f
- [justinsb/jetcd](https://github.com/justinsb/jetcd)
- [diwakergupta/jetcd](https://github.com/diwakergupta/jetcd)
**Python libraries**
- [transitorykris/etcd-py](https://github.com/transitorykris/etcd-py)
@ -528,10 +613,6 @@ See [CONTRIBUTING](https://github.com/coreos/etcd/blob/master/CONTRIBUTING.md) f
- [aterreno/etcd-clojure](https://github.com/aterreno/etcd-clojure)
**Erlang libraries**
- [marshall-lee/etcd.erl](https://github.com/marshall-lee/etcd.erl)
**Chef Integration**
- [coderanger/etcd-chef](https://github.com/coderanger/etcd-chef)
@ -552,45 +633,49 @@ See [CONTRIBUTING](https://github.com/coreos/etcd/blob/master/CONTRIBUTING.md) f
- [mattn/etcdenv](https://github.com/mattn/etcdenv) - "env" shebang with etcd integration
- [kelseyhightower/confd](https://github.com/kelseyhightower/confd) - Manage local app config files using templates and data from etcd
## FAQ
### What size cluster should I use?
Every command the client sends to the master is broadcast to all of the followers.
But, the command is not committed until the majority of the cluster machines receive that command.
The command is not committed until the majority of the cluster machines receive that command.
Because of this majority voting property the ideal cluster should be kept small to keep speed up and be made up of an odd number of machines.
Because of this majority voting property, the ideal cluster should be kept small to keep speed up and be made up of an odd number of machines.
Odd numbers are good because if you have 8 machines the majority will be 5 and if you have 9 machines the majority will be 5.
Odd numbers are good because if you have 8 machines the majority will be 5 and if you have 9 machines the majority will still be 5.
The result is that an 8 machine cluster can tolerate 3 machine failures and a 9 machine cluster can tolerate 4 nodes failures.
And in the best case when all 9 machines are responding the cluster will perform at the speed of the fastest 5 nodes.
### Why SSLv3 alert handshake failure when using SSL client auth?
The `TLS` pacakge of `golang` checks the key usage of certificate public key before using it. To use the certificate public key to do client auth, we need to add `clientAuth` to `Extended Key Usage` when creating the certificate public key.
The `crypto/tls` package of `golang` checks the key usage of the certificate public key before using it.
To use the certificate public key to do client auth, we need to add `clientAuth` to `Extended Key Usage` when creating the certificate public key.
Here is how to do it:
Add the following section to your openssl.cnf:
```
[ ssl_client ]
[ ssl_client ]
...
extendedKeyUsage = clientAuth
...
```
When creating the cert be sure to reference it in the -extensions flag:
When creating the cert be sure to reference it in the `-extensions` flag:
```
openssl ca -config openssl.cnf -policy policy_anything -extensions ssl_client -out certs/node.crt -infiles node.csr
```
## Project Details
### Versioning
etcd uses [semantic versioning][semver].
When we release v1.0.0 of etcd we will promise not to break the "v1" REST API.
New minor versions may add additional features to the API however.
You can get the version of etcd by issuing a request to /version:
@ -599,10 +684,11 @@ You can get the version of etcd by issuing a request to /version:
curl -L http://127.0.0.1:4001/version
```
During the v0 series of releases we may break the API as we fix bugs and get feedback.
During the pre-v1.0.0 series of releases we may break the API as we fix bugs and get feedback.
[semver]: http://semver.org/
### License
etcd is under the Apache 2.0 license. See the [LICENSE][license] file for details.

31
Vagrantfile vendored
View File

@ -1,31 +0,0 @@
# This Vagrantfile is targeted at developers. It can be used to build and run etcd in an isolated VM.
$provision = <<SCRIPT
apt-get update
apt-get install -y python-software-properties git
add-apt-repository -y ppa:duh/golang
apt-get update
apt-get install -y golang
cd /vagrant && ./build
/vagrant/etcd -c 0.0.0.0:4001 -s 0.0.0.0:7001 &
SCRIPT
Vagrant.configure("2") do |config|
config.vm.box = 'precise64'
config.vm.box_url = 'http://files.vagrantup.com/precise64.box'
config.vm.provider "virtualbox" do |vbox|
vbox.customize ["modifyvm", :id, "--memory", "1024"]
end
config.vm.provision "shell", inline: $provision
config.vm.network "forwarded_port", guest: 4001, host: 4001, auto_correct: true
config.vm.network "forwarded_port", guest: 7001, host: 7001, auto_correct: true
end

3
build
View File

@ -1,4 +1,5 @@
#!/bin/sh
set -e
ETCD_PACKAGE=github.com/coreos/etcd
export GOPATH="${PWD}"
@ -21,5 +22,5 @@ for i in third_party/*; do
cp -R "$i" src/
done
./scripts/release-version > release_version.go
./scripts/release-version > server/release_version.go
go build "${ETCD_PACKAGE}"

View File

@ -20,5 +20,5 @@ foreach($i in (ls third_party/*)){
cp -Recurse -force "$i" src/
}
./scripts/release-version.ps1 | Out-File -Encoding UTF8 release_version.go
./scripts/release-version.ps1 | Out-File -Encoding UTF8 server/release_version.go
go build -v "${ETCD_PACKAGE}"

View File

@ -1,254 +0,0 @@
/*
Copyright 2013 CoreOS Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"encoding/binary"
"encoding/json"
"fmt"
"os"
"path"
"time"
etcdErr "github.com/coreos/etcd/error"
"github.com/coreos/etcd/store"
"github.com/coreos/go-raft"
)
const commandPrefix = "etcd:"
func commandName(name string) string {
return commandPrefix + name
}
// A command represents an action to be taken on the replicated state machine.
type Command interface {
CommandName() string
Apply(server *raft.Server) (interface{}, error)
}
// Set command
type SetCommand struct {
Key string `json:"key"`
Value string `json:"value"`
ExpireTime time.Time `json:"expireTime"`
}
// The name of the set command in the log
func (c *SetCommand) CommandName() string {
return commandName("set")
}
// Set the key-value pair
func (c *SetCommand) Apply(server *raft.Server) (interface{}, error) {
return etcdStore.Set(c.Key, c.Value, c.ExpireTime, server.CommitIndex())
}
// TestAndSet command
type TestAndSetCommand struct {
Key string `json:"key"`
Value string `json:"value"`
PrevValue string `json: prevValue`
ExpireTime time.Time `json:"expireTime"`
}
// The name of the testAndSet command in the log
func (c *TestAndSetCommand) CommandName() string {
return commandName("testAndSet")
}
// Set the key-value pair if the current value of the key equals to the given prevValue
func (c *TestAndSetCommand) Apply(server *raft.Server) (interface{}, error) {
return etcdStore.TestAndSet(c.Key, c.PrevValue, c.Value, c.ExpireTime, server.CommitIndex())
}
// Get command
type GetCommand struct {
Key string `json:"key"`
}
// The name of the get command in the log
func (c *GetCommand) CommandName() string {
return commandName("get")
}
// Get the value of key
func (c *GetCommand) Apply(server *raft.Server) (interface{}, error) {
return etcdStore.Get(c.Key)
}
// Delete command
type DeleteCommand struct {
Key string `json:"key"`
}
// The name of the delete command in the log
func (c *DeleteCommand) CommandName() string {
return commandName("delete")
}
// Delete the key
func (c *DeleteCommand) Apply(server *raft.Server) (interface{}, error) {
return etcdStore.Delete(c.Key, server.CommitIndex())
}
// Watch command
type WatchCommand struct {
Key string `json:"key"`
SinceIndex uint64 `json:"sinceIndex"`
}
// The name of the watch command in the log
func (c *WatchCommand) CommandName() string {
return commandName("watch")
}
func (c *WatchCommand) Apply(server *raft.Server) (interface{}, error) {
// create a new watcher
watcher := store.NewWatcher()
// add to the watchers list
etcdStore.AddWatcher(c.Key, watcher, c.SinceIndex)
// wait for the notification for any changing
res := <-watcher.C
if res == nil {
return nil, fmt.Errorf("Clearing watch")
}
return json.Marshal(res)
}
// JoinCommand
type JoinCommand struct {
RaftVersion string `json:"raftVersion"`
Name string `json:"name"`
RaftURL string `json:"raftURL"`
EtcdURL string `json:"etcdURL"`
}
func newJoinCommand() *JoinCommand {
return &JoinCommand{
RaftVersion: r.version,
Name: r.name,
RaftURL: r.url,
EtcdURL: e.url,
}
}
// The name of the join command in the log
func (c *JoinCommand) CommandName() string {
return commandName("join")
}
// Join a server to the cluster
func (c *JoinCommand) Apply(raftServer *raft.Server) (interface{}, error) {
// check if the join command is from a previous machine, who lost all its previous log.
response, _ := etcdStore.RawGet(path.Join("_etcd/machines", c.Name))
b := make([]byte, 8)
binary.PutUvarint(b, raftServer.CommitIndex())
if response != nil {
return b, nil
}
// check machine number in the cluster
num := machineNum()
if num == maxClusterSize {
debug("Reject join request from ", c.Name)
return []byte{0}, etcdErr.NewError(103, "")
}
addNameToURL(c.Name, c.RaftVersion, c.RaftURL, c.EtcdURL)
// add peer in raft
err := raftServer.AddPeer(c.Name, "")
// add machine in etcd storage
key := path.Join("_etcd/machines", c.Name)
value := fmt.Sprintf("raft=%s&etcd=%s&raftVersion=%s", c.RaftURL, c.EtcdURL, c.RaftVersion)
etcdStore.Set(key, value, time.Unix(0, 0), raftServer.CommitIndex())
// add peer stats
if c.Name != r.Name() {
r.followersStats.Followers[c.Name] = &raftFollowerStats{}
r.followersStats.Followers[c.Name].Latency.Minimum = 1 << 63
}
return b, err
}
func (c *JoinCommand) NodeName() string {
return c.Name
}
// RemoveCommand
type RemoveCommand struct {
Name string `json:"name"`
}
// The name of the remove command in the log
func (c *RemoveCommand) CommandName() string {
return commandName("remove")
}
// Remove a server from the cluster
func (c *RemoveCommand) Apply(raftServer *raft.Server) (interface{}, error) {
// remove machine in etcd storage
key := path.Join("_etcd/machines", c.Name)
_, err := etcdStore.Delete(key, raftServer.CommitIndex())
// delete from stats
delete(r.followersStats.Followers, c.Name)
if err != nil {
return []byte{0}, err
}
// remove peer in raft
err = raftServer.RemovePeer(c.Name)
if err != nil {
return []byte{0}, err
}
if c.Name == raftServer.Name() {
// the removed node is this node
// if the node is not replaying the previous logs
// and the node has sent out a join request in this
// start. It is sure that this node received a new remove
// command and need to be removed
if raftServer.CommitIndex() > r.joinIndex && r.joinIndex != 0 {
debugf("server [%s] is removed", raftServer.Name())
os.Exit(0)
} else {
// else ignore remove
debugf("ignore previous remove command.")
}
}
b := make([]byte, 8)
binary.PutUvarint(b, raftServer.CommitIndex())
return b, err
}

156
config.go
View File

@ -1,156 +0,0 @@
/*
Copyright 2013 CoreOS Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"crypto/tls"
"crypto/x509"
"encoding/json"
"encoding/pem"
"io/ioutil"
"os"
"path/filepath"
)
//--------------------------------------
// Config
//--------------------------------------
// Get the server info from previous conf file
// or from the user
func getInfo(path string) *Info {
infoPath := filepath.Join(path, "info")
if force {
// Delete the old configuration if exist
logPath := filepath.Join(path, "log")
confPath := filepath.Join(path, "conf")
snapshotPath := filepath.Join(path, "snapshot")
os.Remove(infoPath)
os.Remove(logPath)
os.Remove(confPath)
os.RemoveAll(snapshotPath)
} else if info := readInfo(infoPath); info != nil {
infof("Found node configuration in '%s'. Ignoring flags", infoPath)
return info
}
// Read info from command line
info := &argInfo
// Write to file.
content, _ := json.MarshalIndent(info, "", " ")
content = []byte(string(content) + "\n")
if err := ioutil.WriteFile(infoPath, content, 0644); err != nil {
fatalf("Unable to write info to file: %v", err)
}
infof("Wrote node configuration to '%s'", infoPath)
return info
}
// readInfo reads from info file and decode to Info struct
func readInfo(path string) *Info {
file, err := os.Open(path)
if err != nil {
if os.IsNotExist(err) {
return nil
}
fatal(err)
}
defer file.Close()
info := &Info{}
content, err := ioutil.ReadAll(file)
if err != nil {
fatalf("Unable to read info: %v", err)
return nil
}
if err = json.Unmarshal(content, &info); err != nil {
fatalf("Unable to parse info: %v", err)
return nil
}
return info
}
func tlsConfigFromInfo(info TLSInfo) (t TLSConfig, ok bool) {
var keyFile, certFile, CAFile string
var tlsCert tls.Certificate
var err error
t.Scheme = "http"
keyFile = info.KeyFile
certFile = info.CertFile
CAFile = info.CAFile
// If the user do not specify key file, cert file and
// CA file, the type will be HTTP
if keyFile == "" && certFile == "" && CAFile == "" {
return t, true
}
// both the key and cert must be present
if keyFile == "" || certFile == "" {
return t, false
}
tlsCert, err = tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
fatal(err)
}
t.Scheme = "https"
t.Server.ClientAuth, t.Server.ClientCAs = newCertPool(CAFile)
// The client should trust the RootCA that the Server uses since
// everyone is a peer in the network.
t.Client.Certificates = []tls.Certificate{tlsCert}
t.Client.RootCAs = t.Server.ClientCAs
return t, true
}
// newCertPool creates x509 certPool and corresponding Auth Type.
// If the given CAfile is valid, add the cert into the pool and verify the clients'
// certs against the cert in the pool.
// If the given CAfile is empty, do not verify the clients' cert.
// If the given CAfile is not valid, fatal.
func newCertPool(CAFile string) (tls.ClientAuthType, *x509.CertPool) {
if CAFile == "" {
return tls.NoClientCert, nil
}
pemByte, err := ioutil.ReadFile(CAFile)
check(err)
block, pemByte := pem.Decode(pemByte)
cert, err := x509.ParseCertificate(block.Bytes)
check(err)
certPool := x509.NewCertPool()
certPool.AddCert(cert)
return tls.RequireAndVerifyClientCert, certPool
}

View File

@ -18,37 +18,60 @@ package error
import (
"encoding/json"
"fmt"
"net/http"
)
var errors map[int]string
const ()
const (
EcodeKeyNotFound = 100
EcodeTestFailed = 101
EcodeNotFile = 102
EcodeNoMoreMachine = 103
EcodeNotDir = 104
EcodeNodeExist = 105
EcodeKeyIsPreserved = 106
EcodeValueRequired = 200
EcodePrevValueRequired = 201
EcodeTTLNaN = 202
EcodeIndexNaN = 203
EcodeValueOrTTLRequired = 204
EcodeRaftInternal = 300
EcodeLeaderElect = 301
EcodeWatcherCleared = 400
EcodeEventIndexCleared = 401
)
func init() {
errors = make(map[int]string)
// command related errors
errors[100] = "Key Not Found"
errors[101] = "The given PrevValue is not equal to the value of the key"
errors[102] = "Not A File"
errors[103] = "Reached the max number of machines in the cluster"
errors[EcodeKeyNotFound] = "Key Not Found"
errors[EcodeTestFailed] = "Test Failed" //test and set
errors[EcodeNotFile] = "Not A File"
errors[EcodeNoMoreMachine] = "Reached the max number of machines in the cluster"
errors[EcodeNotDir] = "Not A Directory"
errors[EcodeNodeExist] = "Already exists" // create
errors[EcodeKeyIsPreserved] = "The prefix of given key is a keyword in etcd"
// Post form related errors
errors[200] = "Value is Required in POST form"
errors[201] = "PrevValue is Required in POST form"
errors[202] = "The given TTL in POST form is not a number"
errors[203] = "The given index in POST form is not a number"
errors[EcodeValueRequired] = "Value is Required in POST form"
errors[EcodePrevValueRequired] = "PrevValue is Required in POST form"
errors[EcodeTTLNaN] = "The given TTL in POST form is not a number"
errors[EcodeIndexNaN] = "The given index in POST form is not a number"
errors[EcodeValueOrTTLRequired] = "Value or TTL is required in POST form"
// raft related errors
errors[300] = "Raft Internal Error"
errors[301] = "During Leader Election"
// keyword
errors[400] = "The prefix of the given key is a keyword in etcd"
errors[EcodeRaftInternal] = "Raft Internal Error"
errors[EcodeLeaderElect] = "During Leader Election"
// etcd related errors
errors[500] = "watcher is cleared due to etcd recovery"
errors[EcodeWatcherCleared] = "watcher is cleared due to etcd recovery"
errors[EcodeEventIndexCleared] = "The event in requested index is outdated and cleared"
}
@ -56,13 +79,15 @@ type Error struct {
ErrorCode int `json:"errorCode"`
Message string `json:"message"`
Cause string `json:"cause,omitempty"`
Index uint64 `json:"index"`
}
func NewError(errorCode int, cause string) Error {
return Error{
func NewError(errorCode int, cause string, index uint64) *Error {
return &Error{
ErrorCode: errorCode,
Message: errors[errorCode],
Cause: cause,
Index: index,
}
}
@ -81,6 +106,7 @@ func (e Error) toJsonString() string {
}
func (e Error) Write(w http.ResponseWriter) {
w.Header().Add("X-Etcd-Index", fmt.Sprint(e.Index))
// 3xx is reft internal error
if e.ErrorCode/100 == 3 {
http.Error(w, e.toJsonString(), http.StatusInternalServerError)

321
etcd.go
View File

@ -17,240 +17,127 @@ limitations under the License.
package main
import (
"crypto/tls"
"flag"
"fmt"
"io/ioutil"
"net/url"
"os"
"strings"
"time"
"os/signal"
"runtime/pprof"
"github.com/coreos/etcd/log"
"github.com/coreos/etcd/server"
"github.com/coreos/etcd/store"
"github.com/coreos/go-raft"
)
//------------------------------------------------------------------------------
//
// Initialization
//
//------------------------------------------------------------------------------
var (
verbose bool
veryVerbose bool
machines string
machinesFile string
cluster []string
argInfo Info
dirPath string
force bool
printVersion bool
maxSize int
snapshot bool
retryTimes int
maxClusterSize int
cpuprofile string
cors string
corsList map[string]bool
)
func init() {
flag.BoolVar(&printVersion, "version", false, "print the version and exit")
flag.BoolVar(&verbose, "v", false, "verbose logging")
flag.BoolVar(&veryVerbose, "vv", false, "very verbose logging")
flag.StringVar(&machines, "C", "", "the ip address and port of a existing machines in the cluster, sepearate by comma")
flag.StringVar(&machinesFile, "CF", "", "the file contains a list of existing machines in the cluster, seperate by comma")
flag.StringVar(&argInfo.Name, "n", "default-name", "the node name (required)")
flag.StringVar(&argInfo.EtcdURL, "c", "127.0.0.1:4001", "the advertised public hostname:port for etcd client communication")
flag.StringVar(&argInfo.RaftURL, "s", "127.0.0.1:7001", "the advertised public hostname:port for raft server communication")
flag.StringVar(&argInfo.EtcdListenHost, "cl", "", "the listening hostname for etcd client communication (defaults to advertised ip)")
flag.StringVar(&argInfo.RaftListenHost, "sl", "", "the listening hostname for raft server communication (defaults to advertised ip)")
flag.StringVar(&argInfo.WebURL, "w", "", "the hostname:port of web interface")
flag.StringVar(&argInfo.RaftTLS.CAFile, "serverCAFile", "", "the path of the CAFile")
flag.StringVar(&argInfo.RaftTLS.CertFile, "serverCert", "", "the cert file of the server")
flag.StringVar(&argInfo.RaftTLS.KeyFile, "serverKey", "", "the key file of the server")
flag.StringVar(&argInfo.EtcdTLS.CAFile, "clientCAFile", "", "the path of the client CAFile")
flag.StringVar(&argInfo.EtcdTLS.CertFile, "clientCert", "", "the cert file of the client")
flag.StringVar(&argInfo.EtcdTLS.KeyFile, "clientKey", "", "the key file of the client")
flag.StringVar(&dirPath, "d", ".", "the directory to store log and snapshot")
flag.BoolVar(&force, "f", false, "force new node configuration if existing is found (WARNING: data loss!)")
flag.BoolVar(&snapshot, "snapshot", false, "open or close snapshot")
flag.IntVar(&maxSize, "m", 1024, "the max size of result buffer")
flag.IntVar(&retryTimes, "r", 3, "the max retry attempts when trying to join a cluster")
flag.IntVar(&maxClusterSize, "maxsize", 9, "the max size of the cluster")
flag.StringVar(&cpuprofile, "cpuprofile", "", "write cpu profile to file")
flag.StringVar(&cors, "cors", "", "whitelist origins for cross-origin resource sharing (e.g. '*' or 'http://localhost:8001,etc')")
}
const (
ElectionTimeout = 200 * time.Millisecond
HeartbeatTimeout = 50 * time.Millisecond
RetryInterval = 10
)
//------------------------------------------------------------------------------
//
// Typedefs
//
//------------------------------------------------------------------------------
type TLSInfo struct {
CertFile string `json:"CertFile"`
KeyFile string `json:"KeyFile"`
CAFile string `json:"CAFile"`
}
type Info struct {
Name string `json:"name"`
RaftURL string `json:"raftURL"`
EtcdURL string `json:"etcdURL"`
WebURL string `json:"webURL"`
RaftListenHost string `json:"raftListenHost"`
EtcdListenHost string `json:"etcdListenHost"`
RaftTLS TLSInfo `json:"raftTLS"`
EtcdTLS TLSInfo `json:"etcdTLS"`
}
type TLSConfig struct {
Scheme string
Server tls.Config
Client tls.Config
}
//------------------------------------------------------------------------------
//
// Variables
//
//------------------------------------------------------------------------------
var etcdStore *store.Store
//------------------------------------------------------------------------------
//
// Functions
//
//------------------------------------------------------------------------------
//--------------------------------------
// Main
//--------------------------------------
func main() {
flag.Parse()
parseFlags()
if printVersion {
fmt.Println(releaseVersion)
// Load configuration.
var config = server.NewConfig()
if err := config.Load(os.Args[1:]); err != nil {
log.Fatal("Configuration error:", err)
}
// Turn on logging.
if config.VeryVerbose {
log.Verbose = true
raft.SetLogLevel(raft.Debug)
} else if config.Verbose {
log.Verbose = true
}
// Load info object.
info, err := config.Info()
if err != nil {
log.Fatal("info:", err)
}
if info.Name == "" {
host, err := os.Hostname()
if err != nil || host == "" {
log.Fatal("Machine name required and hostname not set. e.g. '-n=machine_name'")
}
log.Warnf("Using hostname %s as the machine name. You must ensure this name is unique among etcd machines.", host)
info.Name = host
}
// Setup a default directory based on the machine name
if config.DataDir == "" {
config.DataDir = info.Name + ".etcd"
log.Warnf("Using the directory %s as the etcd configuration directory because a directory was not specified. ", config.DataDir)
}
// Create data directory if it doesn't already exist.
if err := os.MkdirAll(config.DataDir, 0744); err != nil {
log.Fatalf("Unable to create path: %s", err)
}
// Retrieve TLS configuration.
tlsConfig, err := info.EtcdTLS.Config()
if err != nil {
log.Fatal("Client TLS:", err)
}
peerTLSConfig, err := info.RaftTLS.Config()
if err != nil {
log.Fatal("Peer TLS:", err)
}
// Create etcd key-value store and registry.
store := store.New()
registry := server.NewRegistry(store)
// Create peer server.
ps := server.NewPeerServer(info.Name, config.DataDir, info.RaftURL, info.RaftListenHost, &peerTLSConfig, &info.RaftTLS, registry, store, config.SnapCount)
ps.MaxClusterSize = config.MaxClusterSize
ps.RetryTimes = config.MaxRetryAttempts
// Create client server.
s := server.New(info.Name, info.EtcdURL, info.EtcdListenHost, &tlsConfig, &info.EtcdTLS, ps, registry, store)
if err := s.AllowOrigins(config.Cors); err != nil {
panic(err)
}
ps.SetServer(s)
// Run peer server in separate thread while the client server blocks.
go func() {
log.Fatal(ps.ListenAndServe(config.Snapshot, config.Machines))
}()
log.Fatal(s.ListenAndServe())
}
// Parses non-configuration flags.
func parseFlags() {
var versionFlag bool
var cpuprofile string
f := flag.NewFlagSet(os.Args[0], -1)
f.SetOutput(ioutil.Discard)
f.BoolVar(&versionFlag, "version", false, "print the version and exit")
f.StringVar(&cpuprofile, "cpuprofile", "", "write cpu profile to file")
f.Parse(os.Args[1:])
// Print version if necessary.
if versionFlag {
fmt.Println(server.ReleaseVersion)
os.Exit(0)
}
// Begin CPU profiling if specified.
if cpuprofile != "" {
runCPUProfile()
}
if veryVerbose {
verbose = true
raft.SetLogLevel(raft.Debug)
}
parseCorsFlag()
if machines != "" {
cluster = strings.Split(machines, ",")
} else if machinesFile != "" {
b, err := ioutil.ReadFile(machinesFile)
f, err := os.Create(cpuprofile)
if err != nil {
fatalf("Unable to read the given machines file: %s", err)
log.Fatal(err)
}
cluster = strings.Split(string(b), ",")
}
pprof.StartCPUProfile(f)
// Check TLS arguments
raftTLSConfig, ok := tlsConfigFromInfo(argInfo.RaftTLS)
if !ok {
fatal("Please specify cert and key file or cert and key file and CAFile or none of the three")
}
etcdTLSConfig, ok := tlsConfigFromInfo(argInfo.EtcdTLS)
if !ok {
fatal("Please specify cert and key file or cert and key file and CAFile or none of the three")
}
argInfo.Name = strings.TrimSpace(argInfo.Name)
if argInfo.Name == "" {
fatal("ERROR: server name required. e.g. '-n=server_name'")
}
// Check host name arguments
argInfo.RaftURL = sanitizeURL(argInfo.RaftURL, raftTLSConfig.Scheme)
argInfo.EtcdURL = sanitizeURL(argInfo.EtcdURL, etcdTLSConfig.Scheme)
argInfo.WebURL = sanitizeURL(argInfo.WebURL, "http")
argInfo.RaftListenHost = sanitizeListenHost(argInfo.RaftListenHost, argInfo.RaftURL)
argInfo.EtcdListenHost = sanitizeListenHost(argInfo.EtcdListenHost, argInfo.EtcdURL)
// Read server info from file or grab it from user.
if err := os.MkdirAll(dirPath, 0744); err != nil {
fatalf("Unable to create path: %s", err)
}
info := getInfo(dirPath)
// Create etcd key-value store
etcdStore = store.CreateStore(maxSize)
snapConf = newSnapshotConf()
// Create etcd and raft server
e = newEtcdServer(info.Name, info.EtcdURL, info.EtcdListenHost, &etcdTLSConfig, &info.EtcdTLS)
r = newRaftServer(info.Name, info.RaftURL, info.RaftListenHost, &raftTLSConfig, &info.RaftTLS)
startWebInterface()
r.ListenAndServe()
e.ListenAndServe()
}
// parseCorsFlag gathers up the cors whitelist and puts it into the corsList.
func parseCorsFlag() {
if cors != "" {
corsList = make(map[string]bool)
list := strings.Split(cors, ",")
for _, v := range list {
fmt.Println(v)
if v != "*" {
_, err := url.Parse(v)
if err != nil {
panic(fmt.Sprintf("bad cors url: %s", err))
}
}
corsList[v] = true
}
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
go func() {
sig := <-c
log.Infof("captured %v, stopping profiler and exiting..", sig)
pprof.StopCPUProfile()
os.Exit(1)
}()
}
}

View File

@ -1,338 +0,0 @@
/*
Copyright 2013 CoreOS Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"fmt"
"net/http"
"strconv"
"strings"
etcdErr "github.com/coreos/etcd/error"
"github.com/coreos/etcd/store"
"github.com/coreos/etcd/mod"
"github.com/coreos/go-raft"
)
//-------------------------------------------------------------------
// Handlers to handle etcd-store related request via etcd url
//-------------------------------------------------------------------
func NewEtcdMuxer() *http.ServeMux {
// external commands
etcdMux := http.NewServeMux()
etcdMux.Handle("/"+version+"/keys/", errorHandler(Multiplexer))
etcdMux.Handle("/"+version+"/watch/", errorHandler(WatchHttpHandler))
etcdMux.Handle("/"+version+"/leader", errorHandler(LeaderHttpHandler))
etcdMux.Handle("/"+version+"/machines", errorHandler(MachinesHttpHandler))
etcdMux.Handle("/"+version+"/stats/", errorHandler(StatsHttpHandler))
etcdMux.Handle("/version", errorHandler(VersionHttpHandler))
etcdMux.HandleFunc("/test/", TestHttpHandler)
// TODO: Use a mux in 0.2 that can handle this
etcdMux.Handle("/etcd/mod/dashboard/", *mod.ServeMux)
return etcdMux
}
type errorHandler func(http.ResponseWriter, *http.Request) error
// addCorsHeader parses the request Origin header and loops through the user
// provided allowed origins and sets the Access-Control-Allow-Origin header if
// there is a match.
func addCorsHeader(w http.ResponseWriter, r *http.Request) {
addHeaders := func(origin string) bool {
val, ok := corsList[origin]
if val == false || ok == false {
return false
}
w.Header().Add("Access-Control-Allow-Origin", origin)
w.Header().Add("Access-Control-Allow-Methods", "POST, GET, PUT, DELETE, OPTIONS")
return true
}
if addHeaders("*") == true {
return
}
addHeaders(r.Header.Get("Origin"))
}
func (fn errorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
addCorsHeader(w, r)
if e := fn(w, r); e != nil {
if etcdErr, ok := e.(etcdErr.Error); ok {
debug("Return error: ", etcdErr.Error())
etcdErr.Write(w)
} else {
http.Error(w, e.Error(), http.StatusInternalServerError)
}
}
}
// Multiplex GET/POST/DELETE request to corresponding handlers
func Multiplexer(w http.ResponseWriter, req *http.Request) error {
switch req.Method {
case "GET":
return GetHttpHandler(w, req)
case "POST":
return SetHttpHandler(w, req)
case "PUT":
return SetHttpHandler(w, req)
case "DELETE":
return DeleteHttpHandler(w, req)
default:
w.WriteHeader(http.StatusMethodNotAllowed)
return nil
}
}
//--------------------------------------
// State sensitive handlers
// Set/Delete will dispatch to leader
//--------------------------------------
// Set Command Handler
func SetHttpHandler(w http.ResponseWriter, req *http.Request) error {
key := req.URL.Path[len("/v1/keys/"):]
if store.CheckKeyword(key) {
return etcdErr.NewError(400, "Set")
}
debugf("[recv] POST %v/v1/keys/%s [%s]", e.url, key, req.RemoteAddr)
req.ParseForm()
value := req.Form.Get("value")
if len(value) == 0 {
return etcdErr.NewError(200, "Set")
}
strDuration := req.Form.Get("ttl")
expireTime, err := durationToExpireTime(strDuration)
if err != nil {
return etcdErr.NewError(202, "Set")
}
if prevValueArr, ok := req.Form["prevValue"]; ok && len(prevValueArr) > 0 {
command := &TestAndSetCommand{
Key: key,
Value: value,
PrevValue: prevValueArr[0],
ExpireTime: expireTime,
}
return dispatch(command, w, req, true)
} else {
command := &SetCommand{
Key: key,
Value: value,
ExpireTime: expireTime,
}
return dispatch(command, w, req, true)
}
}
// Delete Handler
func DeleteHttpHandler(w http.ResponseWriter, req *http.Request) error {
key := req.URL.Path[len("/v1/keys/"):]
debugf("[recv] DELETE %v/v1/keys/%s [%s]", e.url, key, req.RemoteAddr)
command := &DeleteCommand{
Key: key,
}
return dispatch(command, w, req, true)
}
// Dispatch the command to leader
func dispatch(c Command, w http.ResponseWriter, req *http.Request, etcd bool) error {
if r.State() == raft.Leader {
if body, err := r.Do(c); err != nil {
return err
} else {
if body == nil {
return etcdErr.NewError(300, "Empty result from raft")
} else {
body, _ := body.([]byte)
w.WriteHeader(http.StatusOK)
w.Write(body)
return nil
}
}
} else {
leader := r.Leader()
// current no leader
if leader == "" {
return etcdErr.NewError(300, "")
}
redirect(leader, etcd, w, req)
return nil
}
return etcdErr.NewError(300, "")
}
//--------------------------------------
// State non-sensitive handlers
// will not dispatch to leader
// TODO: add sensitive version for these
// command?
//--------------------------------------
// Handler to return the current leader's raft address
func LeaderHttpHandler(w http.ResponseWriter, req *http.Request) error {
leader := r.Leader()
if leader != "" {
w.WriteHeader(http.StatusOK)
raftURL, _ := nameToRaftURL(leader)
w.Write([]byte(raftURL))
return nil
} else {
return etcdErr.NewError(301, "")
}
}
// Handler to return all the known machines in the current cluster
func MachinesHttpHandler(w http.ResponseWriter, req *http.Request) error {
machines := getMachines(nameToEtcdURL)
w.WriteHeader(http.StatusOK)
w.Write([]byte(strings.Join(machines, ", ")))
return nil
}
// Handler to return the current version of etcd
func VersionHttpHandler(w http.ResponseWriter, req *http.Request) error {
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "etcd %s", releaseVersion)
return nil
}
// Handler to return the basic stats of etcd
func StatsHttpHandler(w http.ResponseWriter, req *http.Request) error {
option := req.URL.Path[len("/v1/stats/"):]
switch option {
case "self":
w.WriteHeader(http.StatusOK)
w.Write(r.Stats())
case "leader":
if r.State() == raft.Leader {
w.Write(r.PeerStats())
} else {
leader := r.Leader()
// current no leader
if leader == "" {
return etcdErr.NewError(300, "")
}
redirect(leader, true, w, req)
}
case "store":
w.WriteHeader(http.StatusOK)
w.Write(etcdStore.Stats())
}
return nil
}
// Get Handler
func GetHttpHandler(w http.ResponseWriter, req *http.Request) error {
key := req.URL.Path[len("/v1/keys/"):]
debugf("[recv] GET %s/v1/keys/%s [%s]", e.url, key, req.RemoteAddr)
command := &GetCommand{
Key: key,
}
if body, err := command.Apply(r.Server); err != nil {
return err
} else {
body, _ := body.([]byte)
w.WriteHeader(http.StatusOK)
w.Write(body)
return nil
}
}
// Watch handler
func WatchHttpHandler(w http.ResponseWriter, req *http.Request) error {
key := req.URL.Path[len("/v1/watch/"):]
command := &WatchCommand{
Key: key,
}
if req.Method == "GET" {
debugf("[recv] GET %s/watch/%s [%s]", e.url, key, req.RemoteAddr)
command.SinceIndex = 0
} else if req.Method == "POST" {
// watch from a specific index
debugf("[recv] POST %s/watch/%s [%s]", e.url, key, req.RemoteAddr)
content := req.FormValue("index")
sinceIndex, err := strconv.ParseUint(string(content), 10, 64)
if err != nil {
return etcdErr.NewError(203, "Watch From Index")
}
command.SinceIndex = sinceIndex
} else {
w.WriteHeader(http.StatusMethodNotAllowed)
return nil
}
if body, err := command.Apply(r.Server); err != nil {
return etcdErr.NewError(500, key)
} else {
w.WriteHeader(http.StatusOK)
body, _ := body.([]byte)
w.Write(body)
return nil
}
}
// TestHandler
func TestHttpHandler(w http.ResponseWriter, req *http.Request) {
testType := req.URL.Path[len("/test/"):]
if testType == "speed" {
directSet()
w.WriteHeader(http.StatusOK)
w.Write([]byte("speed test success"))
return
}
w.WriteHeader(http.StatusBadRequest)
}

View File

@ -1,57 +0,0 @@
/*
Copyright 2013 CoreOS Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"net/http"
)
type etcdServer struct {
http.Server
name string
url string
tlsConf *TLSConfig
tlsInfo *TLSInfo
}
var e *etcdServer
func newEtcdServer(name string, urlStr string, listenHost string, tlsConf *TLSConfig, tlsInfo *TLSInfo) *etcdServer {
return &etcdServer{
Server: http.Server{
Handler: NewEtcdMuxer(),
TLSConfig: &tlsConf.Server,
Addr: listenHost,
},
name: name,
url: urlStr,
tlsConf: tlsConf,
tlsInfo: tlsInfo,
}
}
// Start to listen and response etcd client command
func (e *etcdServer) ListenAndServe() {
infof("etcd server [name %s, listen on %s, advertised url %s]", e.name, e.Server.Addr, e.url)
if e.tlsConf.Scheme == "http" {
fatal(e.Server.ListenAndServe())
} else {
fatal(e.Server.ListenAndServeTLS(e.tlsInfo.CertFile, e.tlsInfo.KeyFile))
}
}

View File

@ -1,619 +0,0 @@
/*
Copyright 2013 CoreOS Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"fmt"
"math/rand"
"net/http"
"net/http/httptest"
"net/url"
"os"
"strconv"
"strings"
"testing"
"time"
"github.com/coreos/etcd/test"
"github.com/coreos/go-etcd/etcd"
)
// Create a single node and try to set value
func TestSingleNode(t *testing.T) {
procAttr := new(os.ProcAttr)
procAttr.Files = []*os.File{nil, os.Stdout, os.Stderr}
args := []string{"etcd", "-n=node1", "-f", "-d=/tmp/node1"}
process, err := os.StartProcess("etcd", args, procAttr)
if err != nil {
t.Fatal("start process failed:" + err.Error())
return
}
defer process.Kill()
time.Sleep(time.Second)
c := etcd.NewClient()
c.SyncCluster()
// Test Set
result, err := c.Set("foo", "bar", 100)
if err != nil || result.Key != "/foo" || result.Value != "bar" || result.TTL < 95 {
if err != nil {
t.Fatal(err)
}
t.Fatalf("Set 1 failed with %s %s %v", result.Key, result.Value, result.TTL)
}
time.Sleep(time.Second)
result, err = c.Set("foo", "bar", 100)
if err != nil || result.Key != "/foo" || result.Value != "bar" || result.PrevValue != "bar" || result.TTL != 99 {
if err != nil {
t.Fatal(err)
}
t.Fatalf("Set 2 failed with %s %s %v", result.Key, result.Value, result.TTL)
}
// Add a test-and-set test
// First, we'll test we can change the value if we get it write
result, match, err := c.TestAndSet("foo", "bar", "foobar", 100)
if err != nil || result.Key != "/foo" || result.Value != "foobar" || result.PrevValue != "bar" || result.TTL != 99 || !match {
if err != nil {
t.Fatal(err)
}
t.Fatalf("Set 3 failed with %s %s %v", result.Key, result.Value, result.TTL)
}
// Next, we'll make sure we can't set it without the correct prior value
_, _, err = c.TestAndSet("foo", "bar", "foofoo", 100)
if err == nil {
t.Fatalf("Set 4 expecting error when setting key with incorrect previous value")
}
// Finally, we'll make sure a blank previous value still counts as a test-and-set and still has to match
_, _, err = c.TestAndSet("foo", "", "barbar", 100)
if err == nil {
t.Fatalf("Set 5 expecting error when setting key with blank (incorrect) previous value")
}
}
// TestInternalVersionFail will ensure that etcd does not come up if the internal raft
// versions do not match.
func TestInternalVersionFail(t *testing.T) {
checkedVersion := false
testMux := http.NewServeMux()
testMux.HandleFunc("/version", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "This is not a version number")
checkedVersion = true
})
testMux.HandleFunc("/join", func(w http.ResponseWriter, r *http.Request) {
t.Fatal("should not attempt to join!")
})
ts := httptest.NewServer(testMux)
defer ts.Close()
fakeURL, _ := url.Parse(ts.URL)
procAttr := new(os.ProcAttr)
procAttr.Files = []*os.File{nil, os.Stdout, os.Stderr}
args := []string{"etcd", "-n=node1", "-f", "-d=/tmp/node1", "-vv", "-C=" + fakeURL.Host}
process, err := os.StartProcess("etcd", args, procAttr)
if err != nil {
t.Fatal("start process failed:" + err.Error())
return
}
defer process.Kill()
time.Sleep(time.Second)
_, err = http.Get("http://127.0.0.1:4001")
if err == nil {
t.Fatal("etcd node should not be up")
return
}
if checkedVersion == false {
t.Fatal("etcd did not check the version")
return
}
}
// This test creates a single node and then set a value to it.
// Then this test kills the node and restart it and tries to get the value again.
func TestSingleNodeRecovery(t *testing.T) {
procAttr := new(os.ProcAttr)
procAttr.Files = []*os.File{nil, os.Stdout, os.Stderr}
args := []string{"etcd", "-n=node1", "-d=/tmp/node1"}
process, err := os.StartProcess("etcd", append(args, "-f"), procAttr)
if err != nil {
t.Fatal("start process failed:" + err.Error())
return
}
time.Sleep(time.Second)
c := etcd.NewClient()
c.SyncCluster()
// Test Set
result, err := c.Set("foo", "bar", 100)
if err != nil || result.Key != "/foo" || result.Value != "bar" || result.TTL < 95 {
if err != nil {
t.Fatal(err)
}
t.Fatalf("Set 1 failed with %s %s %v", result.Key, result.Value, result.TTL)
}
time.Sleep(time.Second)
process.Kill()
process, err = os.StartProcess("etcd", args, procAttr)
defer process.Kill()
if err != nil {
t.Fatal("start process failed:" + err.Error())
return
}
time.Sleep(time.Second)
results, err := c.Get("foo")
if err != nil {
t.Fatal("get fail: " + err.Error())
return
}
result = results[0]
if err != nil || result.Key != "/foo" || result.Value != "bar" || result.TTL > 99 {
if err != nil {
t.Fatal(err)
}
t.Fatalf("Recovery Get failed with %s %s %v", result.Key, result.Value, result.TTL)
}
}
// Create a three nodes and try to set value
func templateTestSimpleMultiNode(t *testing.T, tls bool) {
procAttr := new(os.ProcAttr)
procAttr.Files = []*os.File{nil, os.Stdout, os.Stderr}
clusterSize := 3
_, etcds, err := test.CreateCluster(clusterSize, procAttr, tls)
if err != nil {
t.Fatal("cannot create cluster")
}
defer test.DestroyCluster(etcds)
time.Sleep(time.Second)
c := etcd.NewClient()
c.SyncCluster()
// Test Set
result, err := c.Set("foo", "bar", 100)
if err != nil || result.Key != "/foo" || result.Value != "bar" || result.TTL < 95 {
if err != nil {
t.Fatal(err)
}
t.Fatalf("Set 1 failed with %s %s %v", result.Key, result.Value, result.TTL)
}
time.Sleep(time.Second)
result, err = c.Set("foo", "bar", 100)
if err != nil || result.Key != "/foo" || result.Value != "bar" || result.PrevValue != "bar" || result.TTL != 99 {
if err != nil {
t.Fatal(err)
}
t.Fatalf("Set 2 failed with %s %s %v", result.Key, result.Value, result.TTL)
}
}
func TestSimpleMultiNode(t *testing.T) {
templateTestSimpleMultiNode(t, false)
}
func TestSimpleMultiNodeTls(t *testing.T) {
templateTestSimpleMultiNode(t, true)
}
// Create a five nodes
// Kill all the nodes and restart
func TestMultiNodeKillAllAndRecovery(t *testing.T) {
procAttr := new(os.ProcAttr)
procAttr.Files = []*os.File{nil, os.Stdout, os.Stderr}
clusterSize := 5
argGroup, etcds, err := test.CreateCluster(clusterSize, procAttr, false)
if err != nil {
t.Fatal("cannot create cluster")
}
c := etcd.NewClient()
c.SyncCluster()
time.Sleep(time.Second)
// send 10 commands
for i := 0; i < 10; i++ {
// Test Set
_, err := c.Set("foo", "bar", 0)
if err != nil {
panic(err)
}
}
time.Sleep(time.Second)
// kill all
test.DestroyCluster(etcds)
time.Sleep(time.Second)
stop := make(chan bool)
leaderChan := make(chan string, 1)
all := make(chan bool, 1)
time.Sleep(time.Second)
for i := 0; i < clusterSize; i++ {
etcds[i], err = os.StartProcess("etcd", argGroup[i], procAttr)
}
go test.Monitor(clusterSize, 1, leaderChan, all, stop)
<-all
<-leaderChan
result, err := c.Set("foo", "bar", 0)
if err != nil {
panic(err)
}
if result.Index != 18 {
t.Fatalf("recovery failed! [%d/18]", result.Index)
}
// kill all
test.DestroyCluster(etcds)
}
// Create a five nodes
// Randomly kill one of the node and keep on sending set command to the cluster
func TestMultiNodeKillOne(t *testing.T) {
procAttr := new(os.ProcAttr)
procAttr.Files = []*os.File{nil, os.Stdout, os.Stderr}
clusterSize := 5
argGroup, etcds, err := test.CreateCluster(clusterSize, procAttr, false)
if err != nil {
t.Fatal("cannot create cluster")
}
defer test.DestroyCluster(etcds)
time.Sleep(2 * time.Second)
c := etcd.NewClient()
c.SyncCluster()
stop := make(chan bool)
// Test Set
go test.Set(stop)
for i := 0; i < 10; i++ {
num := rand.Int() % clusterSize
fmt.Println("kill node", num+1)
// kill
etcds[num].Kill()
etcds[num].Release()
time.Sleep(time.Second)
// restart
etcds[num], err = os.StartProcess("etcd", argGroup[num], procAttr)
if err != nil {
panic(err)
}
time.Sleep(time.Second)
}
fmt.Println("stop")
stop <- true
<-stop
}
// This test will kill the current leader and wait for the etcd cluster to elect a new leader for 200 times.
// It will print out the election time and the average election time.
func TestKillLeader(t *testing.T) {
procAttr := new(os.ProcAttr)
procAttr.Files = []*os.File{nil, os.Stdout, os.Stderr}
clusterSize := 5
argGroup, etcds, err := test.CreateCluster(clusterSize, procAttr, false)
if err != nil {
t.Fatal("cannot create cluster")
}
defer test.DestroyCluster(etcds)
stop := make(chan bool)
leaderChan := make(chan string, 1)
all := make(chan bool, 1)
time.Sleep(time.Second)
go test.Monitor(clusterSize, 1, leaderChan, all, stop)
var totalTime time.Duration
leader := "http://127.0.0.1:7001"
for i := 0; i < clusterSize; i++ {
fmt.Println("leader is ", leader)
port, _ := strconv.Atoi(strings.Split(leader, ":")[2])
num := port - 7001
fmt.Println("kill server ", num)
etcds[num].Kill()
etcds[num].Release()
start := time.Now()
for {
newLeader := <-leaderChan
if newLeader != leader {
leader = newLeader
break
}
}
take := time.Now().Sub(start)
totalTime += take
avgTime := totalTime / (time.Duration)(i+1)
fmt.Println("Leader election time is ", take, "with election timeout", ElectionTimeout)
fmt.Println("Leader election time average is", avgTime, "with election timeout", ElectionTimeout)
etcds[num], err = os.StartProcess("etcd", argGroup[num], procAttr)
}
stop <- true
}
// TestKillRandom kills random machines in the cluster and
// restart them after all other machines agree on the same leader
func TestKillRandom(t *testing.T) {
procAttr := new(os.ProcAttr)
procAttr.Files = []*os.File{nil, os.Stdout, os.Stderr}
clusterSize := 9
argGroup, etcds, err := test.CreateCluster(clusterSize, procAttr, false)
if err != nil {
t.Fatal("cannot create cluster")
}
defer test.DestroyCluster(etcds)
stop := make(chan bool)
leaderChan := make(chan string, 1)
all := make(chan bool, 1)
time.Sleep(3 * time.Second)
go test.Monitor(clusterSize, 4, leaderChan, all, stop)
toKill := make(map[int]bool)
for i := 0; i < 20; i++ {
fmt.Printf("TestKillRandom Round[%d/20]\n", i)
j := 0
for {
r := rand.Int31n(9)
if _, ok := toKill[int(r)]; !ok {
j++
toKill[int(r)] = true
}
if j > 3 {
break
}
}
for num, _ := range toKill {
err := etcds[num].Kill()
if err != nil {
panic(err)
}
etcds[num].Wait()
}
time.Sleep(ElectionTimeout)
<-leaderChan
for num, _ := range toKill {
etcds[num], err = os.StartProcess("etcd", argGroup[num], procAttr)
}
toKill = make(map[int]bool)
<-all
}
stop <- true
}
// remove the node and node rejoin with previous log
func TestRemoveNode(t *testing.T) {
procAttr := new(os.ProcAttr)
procAttr.Files = []*os.File{nil, os.Stdout, os.Stderr}
clusterSize := 3
argGroup, etcds, _ := test.CreateCluster(clusterSize, procAttr, false)
time.Sleep(time.Second)
c := etcd.NewClient()
c.SyncCluster()
rmReq, _ := http.NewRequest("DELETE", "http://127.0.0.1:7001/remove/node3", nil)
client := &http.Client{}
for i := 0; i < 2; i++ {
for i := 0; i < 2; i++ {
client.Do(rmReq)
etcds[2].Wait()
resp, err := c.Get("_etcd/machines")
if err != nil {
panic(err)
}
if len(resp) != 2 {
t.Fatal("cannot remove machine")
}
if i == 1 {
// rejoin with log
etcds[2], err = os.StartProcess("etcd", argGroup[2], procAttr)
} else {
// rejoin without log
etcds[2], err = os.StartProcess("etcd", append(argGroup[2], "-f"), procAttr)
}
if err != nil {
panic(err)
}
time.Sleep(time.Second)
resp, err = c.Get("_etcd/machines")
if err != nil {
panic(err)
}
if len(resp) != 3 {
t.Fatal("add machine fails")
}
}
// first kill the node, then remove it, then add it back
for i := 0; i < 2; i++ {
etcds[2].Kill()
etcds[2].Wait()
client.Do(rmReq)
resp, err := c.Get("_etcd/machines")
if err != nil {
panic(err)
}
if len(resp) != 2 {
t.Fatal("cannot remove machine")
}
if i == 1 {
// rejoin with log
etcds[2], err = os.StartProcess("etcd", append(argGroup[2]), procAttr)
} else {
// rejoin without log
etcds[2], err = os.StartProcess("etcd", append(argGroup[2], "-f"), procAttr)
}
if err != nil {
panic(err)
}
time.Sleep(time.Second)
resp, err = c.Get("_etcd/machines")
if err != nil {
panic(err)
}
if len(resp) != 3 {
t.Fatal("add machine fails")
}
}
}
test.DestroyCluster(etcds)
}
func templateBenchmarkEtcdDirectCall(b *testing.B, tls bool) {
procAttr := new(os.ProcAttr)
procAttr.Files = []*os.File{nil, os.Stdout, os.Stderr}
clusterSize := 3
_, etcds, _ := test.CreateCluster(clusterSize, procAttr, tls)
defer test.DestroyCluster(etcds)
time.Sleep(time.Second)
b.ResetTimer()
for i := 0; i < b.N; i++ {
resp, _ := http.Get("http://127.0.0.1:4001/test/speed")
resp.Body.Close()
}
}
func BenchmarkEtcdDirectCall(b *testing.B) {
templateBenchmarkEtcdDirectCall(b, false)
}
func BenchmarkEtcdDirectCallTls(b *testing.B) {
templateBenchmarkEtcdDirectCall(b, true)
}

View File

@ -1,8 +1,10 @@
Testing x509 certs for luacrypto
Testing x509 certs for etcd
The passphrases for the keys are `asdf`.
# Make the CA cert
openssl genrsa -des3 -out ca.key 4096
openssl req -new -x509 -days 365 -key ca.key -out ca.crt -extfile openssl.cnf -extensions v3_ca
openssl req -new -x509 -days 365 -key ca.key -out ca.crt -config openssl.cnf -extensions v3_ca
# Make server cert and signing request
openssl genrsa -des3 -out server.key 4096
@ -16,3 +18,4 @@ openssl rsa -in server.key -out server.key.insecure
# Output "raw" public key from server crt
openssl x509 -pubkey -noout -in server.crt > server.pub

View File

@ -1,31 +1,31 @@
-----BEGIN CERTIFICATE-----
MIIFajCCA1KgAwIBAgIJAL6GUooGHc/oMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
MIIFajCCA1KgAwIBAgIJAKMtnrbM5eQaMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
aWRnaXRzIFB0eSBMdGQwHhcNMTMwODExMDUxMTI0WhcNMTQwODExMDUxMTI0WjBF
aWRnaXRzIFB0eSBMdGQwHhcNMTMxMTEzMTkwNzMwWhcNMTQxMTEzMTkwNzMwWjBF
MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
CgKCAgEAyAJNWQmsBtTBPv/jSjFk+EqCZM9zLcnS9P7bg8snLu1RaDS0NA8RjQFm
1fw+fAoNyOJ5X4FdEep6piMcVaYa/xGgls3DVkUytOvJ0BcdUJgrcyH0CDodDhu4
T/qi1W7I+y3gbjr+VyyBdOSQuybyun9RwRrktcfVDfObaA0AmLt1PtJzMI+tB2As
XRgxPfFLETUTy9nIQc3PQxs11sWeEzvxcVrO595XsumPYZZAan86KNrQzES4r61R
0pOGAIEEfyvT2uU5y7fnFNtRr2xxjdgUj2/ghJX6M49BnYp4edyQuyNQp+weSA6c
3ueTu98gin1vxzMaVJJIaRRerKzekCerXLq3YsFzS7HFzMaR201faPw45b7K83bh
/DJ2wcc8JhyrhnOBM76jCnug4FReiETnCyUAc7fP+iCOCpgCzYky7wi8Jc+MTXWG
RIvpfmcB326gUdyG8n/yvIc95E6ZiQFNx9B75wikaEUcSOkp3pZxG0Fc7l60oe1l
hYpA5kL6YOdaBPSq5y1B6kFT4D6gfLYs+KS3vTWjxeLTpyRhF9eVMdxoOqUviK5X
MVVxc8KkrQbqKQw7VlmqNeA7kIsBGMOfMn3WetRY7pi1OyYMhMr/eG5r9YtaWN6Y
sHicwmyNfVHIi0McJgAS+c+7sAVnGPoHIWUf24xll4z9DUqk65cCAwEAAaNdMFsw
HQYDVR0OBBYEFLOtmFVLJOtj9kytXy/vDJROI8lSMB8GA1UdIwQYMBaAFLOtmFVL
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
-----END CERTIFICATE-----

View File

@ -1,54 +1,54 @@
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: DES-EDE3-CBC,90DB77765661F24C
DEK-Info: DES-EDE3-CBC,5AD5C95753A0AE70
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
-----END RSA PRIVATE KEY-----

View File

@ -1,61 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIFMDCCAxigAwIBAgIBATANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJBVTET
MBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQ
dHkgTHRkMB4XDTEzMDgxMTA1MTE0OVoXDTE0MDgxMTA1MTE0OVowRTELMAkGA1UE
BhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdp
ZGdpdHMgUHR5IEx0ZDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMeY
u5Nby4uG4s5HFuky6eiLf7bMiTl4cDomb6keKMaHuSYN0zEr8jdw4r8TDSguv5MO
E3xWSLvQwLgkOQT0N7opDeEqm5Yj458+M9Z5lqYUSshWpnFNUyu8GBCfARB80YCy
8dLbYp5ORJ2AWzS5fxtp4TXEf8qlrAMoeJz2ZXMvGHMUuv8TFa3KPAajQ5n2DPSu
iFevaDitiRamz6aT48bnOowMO0Enek8UkfpTeR7uh0vOPbWOUIjzuqr7G4MnJkYD
yd7R3KscZN4iXf6NRMj2f6V4PGY0WljUbli/fT7bC8IbBgLkQxT8mO7dcJ9QHKte
Jgdju/9eto3zC3kDC3Yh2RaxfxM1vtmgZm3QT5oz95QjjskzW1gz2giE45wojbOg
nTI+QRtw2EMBX6mzaP/YU6vCvPCqhJ9zrVMsM88EK0TTdkE/NWC25JUmsPXX37Z5
jFXIFamM0FWE/zhDip8Xfl3yqAM+NVQ5xnmWGMpqyHn/A1EaRv0ASVTMsg413N3j
r815qKy/xSCbyRIFhrBmKkwy0bSZVP+9Y0sC8+bBTZNdbNwrdp0It7BH/xKmG0Ma
TNpAgVdkbnnSt5DlqS91Sbta5i+kHibkFml4KnZ6OvJgQuTbC+UsbpD5B1vM8Rd2
64xOQmH9KFNgouDhiBOtwAL3bn+76Pdt8OnMbO0JAgMBAAGjKzApMAkGA1UdEwQC
MAAwCwYDVR0PBAQDAgXgMA8GA1UdEQQIMAaHBH8AAAEwDQYJKoZIhvcNAQEFBQAD
ggIBAJIVOwXJkpI9bPRq6FxN7a0/2NcEU5cC9NvtvEZEjgMFkZwiPMsguwHbbsK/
Hmg+UBo9HdOCleaPq0HhrhqDzqDGzuzCCwqDinYJl/MaTs7dBE5sJw5sCn+oESF0
5S1rCKvvF82o1KSzj458aTWKYpOJpdJYPVu8QEm9sBPPAFcQHhevFRuVp8QBdRJD
6H4+6b4eZyADL1yM+Txt/ucuyx/6A8S/G+Uqe5Lnh1pvhZXFfWO1UF8QmYNUb0H2
7soxruLh4k2mwF8MPSmKw8D3k4rCAMZ7W1P6OEV55Jc4OMVQ5es8tRuj9e2SHD0c
gL84rv9lNYfA/4DEKEviJTko+dD/NyIKrZCyc39Q3MmSBR+ekCNRhdCHWL5IyMB9
o2u5g2ffsKLLjqBNIrOcGQ8vYSTsuX+y1Tonml6FiBHCgtDv7ZcwxXq37jmeorMt
QqpGJsndMObmvTVkYDN8vgEoia/nndhU7SGgi9NIYDLarDzWrU9baLta8Oq7BHaR
oMV44flX7/2Co6SOzK4y2WgQngCUaAxezN0tZPFIhZjwGwc3CbaigIaF8LTKHQ8a
cGIBGQmZ3670IDQ/vgtjHqG6LlMiJ+WR9GtWSJl3cb+4yHM/wu4oFgjYoB1MSWl2
f5fczxP6ZXwER7NwcRaooJ/0C7XDE7ux2HsN422jgDaGT/Zw
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIFajCCA1KgAwIBAgIJAL6GUooGHc/oMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
aWRnaXRzIFB0eSBMdGQwHhcNMTMwODExMDUxMTI0WhcNMTQwODExMDUxMTI0WjBF
MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
CgKCAgEAyAJNWQmsBtTBPv/jSjFk+EqCZM9zLcnS9P7bg8snLu1RaDS0NA8RjQFm
1fw+fAoNyOJ5X4FdEep6piMcVaYa/xGgls3DVkUytOvJ0BcdUJgrcyH0CDodDhu4
T/qi1W7I+y3gbjr+VyyBdOSQuybyun9RwRrktcfVDfObaA0AmLt1PtJzMI+tB2As
XRgxPfFLETUTy9nIQc3PQxs11sWeEzvxcVrO595XsumPYZZAan86KNrQzES4r61R
0pOGAIEEfyvT2uU5y7fnFNtRr2xxjdgUj2/ghJX6M49BnYp4edyQuyNQp+weSA6c
3ueTu98gin1vxzMaVJJIaRRerKzekCerXLq3YsFzS7HFzMaR201faPw45b7K83bh
/DJ2wcc8JhyrhnOBM76jCnug4FReiETnCyUAc7fP+iCOCpgCzYky7wi8Jc+MTXWG
RIvpfmcB326gUdyG8n/yvIc95E6ZiQFNx9B75wikaEUcSOkp3pZxG0Fc7l60oe1l
hYpA5kL6YOdaBPSq5y1B6kFT4D6gfLYs+KS3vTWjxeLTpyRhF9eVMdxoOqUviK5X
MVVxc8KkrQbqKQw7VlmqNeA7kIsBGMOfMn3WetRY7pi1OyYMhMr/eG5r9YtaWN6Y
sHicwmyNfVHIi0McJgAS+c+7sAVnGPoHIWUf24xll4z9DUqk65cCAwEAAaNdMFsw
HQYDVR0OBBYEFLOtmFVLJOtj9kytXy/vDJROI8lSMB8GA1UdIwQYMBaAFLOtmFVL
JOtj9kytXy/vDJROI8lSMAwGA1UdEwQFMAMBAf8wCwYDVR0PBAQDAgEGMA0GCSqG
SIb3DQEBBQUAA4ICAQAN8vTepTHSkJNyU1guATIFZTil/RM1dqnC0mM1B6dkkrJj
H9m2wldxuUJJ6PwkLZXHKBBrkHSP+DaFwFD3AkNJQWoslT+o1rU/bI5fSK0QqaD7
ZHx2BBkvfOsj8as+nzhYArlJy8YH786qG1Gbd8toP3IjzHq2HR58xeiHB0X/rvY0
sfxPfpP0id52vJyh1Cl91hI8+KYFv3b6pesAG9drYnLZIKh0mAIdpmH5n8V9FQ2y
gkGORvRfBQdA+xTmy1MpQFeXgbE9CLHoHDXmTZneKzxSRSqwoqFxsj1fcqXC87lz
aqJWvnL6iF6exaqV7df4iT6xHp9R7sahRLKbkpe7r/jbcr1i/6aaa7Ve+Z4MtZRd
TcrNerwchF9RborO86baM6gDR3SJ4wCnfyncKFqmGJs1rrzg8gEBddZtzVZiSntt
GMup4eh1Yt/0w/AIvX8nxOUhc9P1zw3Fb80Dd7ucxbKdkOXfqZ/cEm5zyh92HMvd
RqkQee31tENYzjpqx8CXfeZ+B/tHq1baOFv6zM7yJ3Hr9KzPhKhLHXooO+qMNk+g
E5QjY82R6pRSVfVRDbJMEfS7xeJ3qrEU8UueJYx9S7qJSxB3lwunf6T4SJ4vqEmU
fwX4jSahFIUIlXGwfIDqM7P+biIyJS2AaMC5KMcatnyXDNbEZzg1ibhhpHLWIA==
-----END CERTIFICATE-----

View File

@ -1,61 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIFMDCCAxigAwIBAgIBAjANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJBVTET
MBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQ
dHkgTHRkMB4XDTEzMDgxMTA1MzE1OVoXDTE0MDgxMTA1MzE1OVowRTELMAkGA1UE
BhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdp
ZGdpdHMgUHR5IEx0ZDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALRv
lwJo+lU+QkAol+sFEAyW+qTNxdTKiSqeHhigpbyuo18/ILvXJRRz4Slrx1sqLjf4
ne00N0wTy3l+DGi9kI39BrVrj8Lg9mTlfJQy/JJSDVIMli/lnkbfjNlsC3miRLmY
YxqKLZJH5onErIR+XCTJ3o4kVk6QMy3oR0LPWWz/cs4PrXNVosL6jl4tTTOyqWAC
4dtDGlDElSFui78KuSQCKO+9sepVvSFXE8Wo32LZWQ1vahg/+J/eagbw6rakl+uu
VJgfin7JH+bFsiBCkOAN4v0QF3JYchMIBeXwQzEq/HpN73Es2wPGuyglB0OGkxpu
nZ0B7bAJSOQMMNL7NkGIu6HNqORt2FzXypiXaIMUCVcIvvf2VqGBpULe+4fdLvbc
Ho/F6MzmmxfDNMwvBb1P+1nPOKc78pKWO2mqN+hOudxTbdzAiYURtjIp6oyEzvl3
Hdgf3UUVmBQe6jPw9Cm17c7y58icPdRERoxCSdhOfwYFuls/fenPwBhMZ53+cRYG
eP2f3TT6cMzcEUkz2ZIZa4XZp0JCox1yQxy8vrmWfLo9sghqE2iRGWqRKexhr7IM
Iv9Q2wL5qcGaX1wA1gOMfpuqySb6zp8LouVEXAII9RfiRFundqYjJjtZg2sosjRJ
Eab94wfwLu9d0lKZtNBkAh65q02+Ys4Ah94IEu+ZAgMBAAGjKzApMAkGA1UdEwQC
MAAwCwYDVR0PBAQDAgXgMA8GA1UdEQQIMAaHBH8AAAEwDQYJKoZIhvcNAQEFBQAD
ggIBALWlvNm8kp3bdzymrJnvE4+sV5p+2AnKx1yZIs3h62rUM22ROU1wCAoJfyCA
LuXQNaXuWVOWUq96BCzlTScTSa1xhB/vbn9RPJVo0t+uni5fx/Pm2CHLAUijrT8z
10BHbaIPjYnmvz0lkii4Y5+Tb4WQ6yLrUYm2dpLexYwyOUhmGQNGRgY750dwf8Fe
2TBFOV9rkXlhgdopWYPhUv0ZmciYGwrJ2+9jULDFhT+PDrdAjbeDARPKcMi0jpZ1
zBHyC6lNT167Gdj9LVV6dIFEHozzrqdMecz5CJrgKPL0s5bM88DRssupS1WgT1RG
qGVxfcuBYRLtz10W5+JBXvA3JRHgaPotkqvKsUqeII/nqvu+qSRDnh7O+i1PJUTr
D+5CSMxUK9DvxH1gUYhnQ5asP9PXZxp8hlGGwyDVu2rYTQpDyiJnHGmsWfSZuSOy
W8ViseuFe3WmdsD0wo6VguyPFMHGzh5Sx/onb4eeASz/BtcGYVPApD4WByF9WlVF
Cg3SfvNPj2fvI92DP6KAKtDgOdcHidzwPAh3XCZGikN19Oz3cCYf+AT+s/KNfvMt
B6DplYeleAlKTXYsS4ycGojGp4DpRzrxSb2mhHdHsz51H/gn9+Rgx4+QAIJGKqxk
yNRnW/UpsJbN7G7hI3pgBEFRD+QE4zvGwkn6+SwxxozhtZZ4
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIFajCCA1KgAwIBAgIJAL6GUooGHc/oMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
aWRnaXRzIFB0eSBMdGQwHhcNMTMwODExMDUxMTI0WhcNMTQwODExMDUxMTI0WjBF
MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
CgKCAgEAyAJNWQmsBtTBPv/jSjFk+EqCZM9zLcnS9P7bg8snLu1RaDS0NA8RjQFm
1fw+fAoNyOJ5X4FdEep6piMcVaYa/xGgls3DVkUytOvJ0BcdUJgrcyH0CDodDhu4
T/qi1W7I+y3gbjr+VyyBdOSQuybyun9RwRrktcfVDfObaA0AmLt1PtJzMI+tB2As
XRgxPfFLETUTy9nIQc3PQxs11sWeEzvxcVrO595XsumPYZZAan86KNrQzES4r61R
0pOGAIEEfyvT2uU5y7fnFNtRr2xxjdgUj2/ghJX6M49BnYp4edyQuyNQp+weSA6c
3ueTu98gin1vxzMaVJJIaRRerKzekCerXLq3YsFzS7HFzMaR201faPw45b7K83bh
/DJ2wcc8JhyrhnOBM76jCnug4FReiETnCyUAc7fP+iCOCpgCzYky7wi8Jc+MTXWG
RIvpfmcB326gUdyG8n/yvIc95E6ZiQFNx9B75wikaEUcSOkp3pZxG0Fc7l60oe1l
hYpA5kL6YOdaBPSq5y1B6kFT4D6gfLYs+KS3vTWjxeLTpyRhF9eVMdxoOqUviK5X
MVVxc8KkrQbqKQw7VlmqNeA7kIsBGMOfMn3WetRY7pi1OyYMhMr/eG5r9YtaWN6Y
sHicwmyNfVHIi0McJgAS+c+7sAVnGPoHIWUf24xll4z9DUqk65cCAwEAAaNdMFsw
HQYDVR0OBBYEFLOtmFVLJOtj9kytXy/vDJROI8lSMB8GA1UdIwQYMBaAFLOtmFVL
JOtj9kytXy/vDJROI8lSMAwGA1UdEwQFMAMBAf8wCwYDVR0PBAQDAgEGMA0GCSqG
SIb3DQEBBQUAA4ICAQAN8vTepTHSkJNyU1guATIFZTil/RM1dqnC0mM1B6dkkrJj
H9m2wldxuUJJ6PwkLZXHKBBrkHSP+DaFwFD3AkNJQWoslT+o1rU/bI5fSK0QqaD7
ZHx2BBkvfOsj8as+nzhYArlJy8YH786qG1Gbd8toP3IjzHq2HR58xeiHB0X/rvY0
sfxPfpP0id52vJyh1Cl91hI8+KYFv3b6pesAG9drYnLZIKh0mAIdpmH5n8V9FQ2y
gkGORvRfBQdA+xTmy1MpQFeXgbE9CLHoHDXmTZneKzxSRSqwoqFxsj1fcqXC87lz
aqJWvnL6iF6exaqV7df4iT6xHp9R7sahRLKbkpe7r/jbcr1i/6aaa7Ve+Z4MtZRd
TcrNerwchF9RborO86baM6gDR3SJ4wCnfyncKFqmGJs1rrzg8gEBddZtzVZiSntt
GMup4eh1Yt/0w/AIvX8nxOUhc9P1zw3Fb80Dd7ucxbKdkOXfqZ/cEm5zyh92HMvd
RqkQee31tENYzjpqx8CXfeZ+B/tHq1baOFv6zM7yJ3Hr9KzPhKhLHXooO+qMNk+g
E5QjY82R6pRSVfVRDbJMEfS7xeJ3qrEU8UueJYx9S7qJSxB3lwunf6T4SJ4vqEmU
fwX4jSahFIUIlXGwfIDqM7P+biIyJS2AaMC5KMcatnyXDNbEZzg1ibhhpHLWIA==
-----END CERTIFICATE-----

View File

@ -0,0 +1,63 @@
-----BEGIN CERTIFICATE-----
MIIFajCCA1KgAwIBAgIJAKMtnrbM5eQaMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
aWRnaXRzIFB0eSBMdGQwHhcNMTMxMTEzMTkwNzMwWhcNMTQxMTEzMTkwNzMwWjBF
MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
CgKCAgEAs3+PRcgMLMquxhHB/1aN7lytduBGlXTsDNky3IQyQCpqFUdYYc5cS3nL
Y0wtwB4v+yNJ2qNOsYOlSUSSPS1nUxDsHWiMMPC6NxsE34wuf1jYI1UQbQiAEf73
wB+LMTuv30ZDG9EMfwiHf1VbOGKUwkSeZcMl8EX/DfmJCB9PONFHvlS1yQHnJwqv
SvIw55UgL/7fRvmblqrMsl0g/cveSanT2bGTV6eNYlDcAfw6SsszYbKA+i5zjt9F
puZA+JbqZ2mQ4RNi228ib9qGiS1S1YgyWqiJqGD8I15nvrWV9fA93z+kYekdc+nM
HXtWnd296vfcGQfuRgKAigp0Q2Xr/r6IfT0etMwNWOT/ikAE7l5hA3xUdEba0xdZ
2PYLmrb+5mtPB8uZ8K0JSrZJU70p1hlk644Nw1S6Je5/XfUZFzwLq8OotGnRzD7Y
dyvY/DEDodiQisLtLTCI9z4F7cjadTKwSSq5Yzr8Pdq1PkahBQth1Eq8KvodOXOR
WTVxP+YBYmbbk7EEOSZ8ZI7ppqngS6/pjcjCHobZfzJdfx8YuTrBWUHucYDqeV6r
xYtvlDiOaxyij8vhaAJ7zLfUuVGHKPfLgHZDAH47a+1qnIq5tM2Zv8p9g7wg56UV
aplu4GwBqNrL+5R10P2YuBgrUOZOjIOv0u5pvBjLZpTeal8KI7sCAwEAAaNdMFsw
HQYDVR0OBBYEFOkUWSDlAWoxFoSsbnbEH9GIN8bfMB8GA1UdIwQYMBaAFOkUWSDl
AWoxFoSsbnbEH9GIN8bfMAwGA1UdEwQFMAMBAf8wCwYDVR0PBAQDAgEGMA0GCSqG
SIb3DQEBBQUAA4ICAQBi30INj8fsPRsX9p1vVtljln2dh/iOJ0shL3gz9NmUM0jV
/UchKo5iykVrrCKVlI7DD/QFXXty20x6PnbSUwfhZpJj+AwT9G8pVD/8DBU60+z0
1zFSUxQ2GN9RDWgoiyz1QZ48n5zl7JVzzvBAf6N3jmdvgmIoKaDpqNLmQxqNsuCW
USMKqvqKwWfqvj8JQNYVmKdDVsoax36glVaj4qHZojul9eWS6SdDOo6a5t/xf0kP
Upxi87dqS4H7qfa6HTVFQhqRL8JuPqTs4csojA6XJt+yYzTfs8Gf3MAyQznuwiWh
E7kIv9lYH5APLW5LXNLizTaZtBS826f05TgBsYuYj3mGeSsr/llP4zb8u7qxL+B3
0Q6OLK3JtPbwtaHCBxs70HOQzjWjiEF6PE3f1MhvXFjMQmQEgGzCuK69EEUWK2s0
cIjoTLJxmQ+voWPms39gjstNLeykAygsyaYryGks/YjgchRrTtrOxUCows8knkmB
lve7RC9xW7yQwmWacPq0ndJUL0smdsWODx+L3J0R/YmbjYIO5N8i9YFqSwFLIC2v
ghirHq7EqZbYAaDxS6KvpeVh+f0pC8AC63FLbsp9+SOayoEQJ/gB8f+s3cxV+rNQ
/Z6077QuDgb1gpjCWCHHjMMELtjvy+HNUmgiRfv6a+mtWOS8g5Ii3OUYFVr2kQ==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIFfDCCA2SgAwIBAgIBATANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJBVTET
MBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQ
dHkgTHRkMB4XDTEzMTExMzE5MDgxMloXDTE0MTExMzE5MDgxMlowZTELMAkGA1UE
BhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdp
ZGdpdHMgUHR5IEx0ZDEeMBwGA1UEAwwVaHR0cDovLzEyNy4wLjAuMTo0MDAxMIIC
IjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1XBtjDav5Sl3H+/fUcGiQO36
oqtZG+YuC9D0z0u89Shq+XNs3tRtonGGCyEIrDtI6R7PItMJa6rQ1VuFoMWPrjmF
f5tFemSOZtQx/DF78H+5tWaIBVDA+Kw1zxdqj1n3/AQjAGsSuqhgcaIQQFqTNNtA
tW40048fDh17jWIDB9baF65az2uArq97uS4deDujG3CHV9svO7hoqpzYt039VDKK
4N+dDMUZFqhEmY2MqjyQySY2bd/gsYBjcGWSIuonALactiYDc4zIusAfNptnXycw
K/aQAqDwhwMcQA9L5YKQ5hoUukDTQFbvoNLJ3vNQDI/o8sjCh94EkMuopSp90tJ/
syTPKRlh/MaGMXvwu8Vab5iPeVop48jTKl3Z+G8NErGM8KKCyd1mQoGisVuIMQPK
uJUi7jOu6wgXlA8ZgUGfSQQDA4v2Q0tV/GlJmvsP5JshA3v7C/sSBY/3AnPHeWTl
ozXlNgXitxps1EwgR2jo+YW2gxrfM//xtgMCjMXjO9g2TsCnWR6j93oXWn88UP/C
eAcyjeTdJjW+piuLdvYOctY6+Yql5gm9Vx0u+w9jTmpzOCoh+9cNtjqmPiOhecFc
Vf71vMf4krMp6lmY/Nq1/km9u0ViNP5CJHk18YXG42vnj7sUgT7WgpLh8g/iPc2p
etH9lMd9te+Jyak/zA8CAwEAAaNXMFUwCQYDVR0TBAIwADALBgNVHQ8EBAMCBeAw
KgYDVR0lAQH/BCAwHgYIKwYBBQUHAwgGCCsGAQUFBwMBBggrBgEFBQcDAjAPBgNV
HREECDAGhwR/AAABMA0GCSqGSIb3DQEBBQUAA4ICAQCmF13fF7kAcgCzRKBCpggz
ObVNSK9btGKUU+xyOZsRgEDWdfeBvG3VzXflLMDv3P45O90CHkcGfckbj1ISctIY
0OSnCn4vmIwnhwUWyYlCagtNB68GVcPH8XLxn/wsnT2MbiA/ohUStKXQtBmBxIUH
rI+7r28qcHW7AnT47G5BbSYzW0xYzlkUwyl8rBxRLDKVTHBJ5GmlREJKdAzKS7CS
6jhcsxa544Rsa7CDvjLKpJO0iploL0/GY+5oj3VdhgdEJgwqwvu3skfd1N8wkxH4
NQuRmyvXxcMSxKv4vbOOUm4PfOqOwwXiVZLoc29ePUv9zCU2AVdS21DD9zAZeKDb
B87VWnQKO6JUvL5vX7xsMnsSbnLHGA2kPv4IDZ6jKuZdVneM+whDZlBWpHRL2RKX
K0JZICf7+EbmrUW3Rwl+dIaIJ55Ni1rfDSZWeIYKFx04Mod6Wbch7ahb/XVvIDe9
SFjLfeNj7L/Iz0i+1lTarAMxIRC521IwcobhAxqxYCv3oNf6f+tz8DyCCvsTCc9W
/OLKX7sukh1ohle+0EFrSYpX5PzkHwZRVZjx55KwkIV/KtwBadJv+z6iwW3qOn/A
/1yC8Mbc2TdCaRPwFO80LAg9cz+XfT5vZoQUvOnOxIrhFnasQ/xiovy0zKCr2Fjg
ePQ4BNEN9wt3SsPp8ig39g==
-----END CERTIFICATE-----

View File

@ -1,31 +1,32 @@
-----BEGIN CERTIFICATE-----
MIIFXDCCA0SgAwIBAgIBATANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJBVTET
MIIFfDCCA2SgAwIBAgIBATANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJBVTET
MBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQ
dHkgTHRkMB4XDTEzMDgxMTE1MzIyNloXDTE0MDgxMTE1MzIyNlowRTELMAkGA1UE
dHkgTHRkMB4XDTEzMTExMzE5MDgxMloXDTE0MTExMzE5MDgxMlowZTELMAkGA1UE
BhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdp
ZGdpdHMgUHR5IEx0ZDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMeY
u5Nby4uG4s5HFuky6eiLf7bMiTl4cDomb6keKMaHuSYN0zEr8jdw4r8TDSguv5MO
E3xWSLvQwLgkOQT0N7opDeEqm5Yj458+M9Z5lqYUSshWpnFNUyu8GBCfARB80YCy
8dLbYp5ORJ2AWzS5fxtp4TXEf8qlrAMoeJz2ZXMvGHMUuv8TFa3KPAajQ5n2DPSu
iFevaDitiRamz6aT48bnOowMO0Enek8UkfpTeR7uh0vOPbWOUIjzuqr7G4MnJkYD
yd7R3KscZN4iXf6NRMj2f6V4PGY0WljUbli/fT7bC8IbBgLkQxT8mO7dcJ9QHKte
Jgdju/9eto3zC3kDC3Yh2RaxfxM1vtmgZm3QT5oz95QjjskzW1gz2giE45wojbOg
nTI+QRtw2EMBX6mzaP/YU6vCvPCqhJ9zrVMsM88EK0TTdkE/NWC25JUmsPXX37Z5
jFXIFamM0FWE/zhDip8Xfl3yqAM+NVQ5xnmWGMpqyHn/A1EaRv0ASVTMsg413N3j
r815qKy/xSCbyRIFhrBmKkwy0bSZVP+9Y0sC8+bBTZNdbNwrdp0It7BH/xKmG0Ma
TNpAgVdkbnnSt5DlqS91Sbta5i+kHibkFml4KnZ6OvJgQuTbC+UsbpD5B1vM8Rd2
64xOQmH9KFNgouDhiBOtwAL3bn+76Pdt8OnMbO0JAgMBAAGjVzBVMAkGA1UdEwQC
MAAwCwYDVR0PBAQDAgXgMCoGA1UdJQEB/wQgMB4GCCsGAQUFBwMIBggrBgEFBQcD
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
ePQ4BNEN9wt3SsPp8ig39g==
-----END CERTIFICATE-----

View File

@ -1,29 +1,30 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIE8DCCAtgCAQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUx
ITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCCAiIwDQYJKoZIhvcN
AQEBBQADggIPADCCAgoCggIBAMeYu5Nby4uG4s5HFuky6eiLf7bMiTl4cDomb6ke
KMaHuSYN0zEr8jdw4r8TDSguv5MOE3xWSLvQwLgkOQT0N7opDeEqm5Yj458+M9Z5
lqYUSshWpnFNUyu8GBCfARB80YCy8dLbYp5ORJ2AWzS5fxtp4TXEf8qlrAMoeJz2
ZXMvGHMUuv8TFa3KPAajQ5n2DPSuiFevaDitiRamz6aT48bnOowMO0Enek8UkfpT
eR7uh0vOPbWOUIjzuqr7G4MnJkYDyd7R3KscZN4iXf6NRMj2f6V4PGY0WljUbli/
fT7bC8IbBgLkQxT8mO7dcJ9QHKteJgdju/9eto3zC3kDC3Yh2RaxfxM1vtmgZm3Q
T5oz95QjjskzW1gz2giE45wojbOgnTI+QRtw2EMBX6mzaP/YU6vCvPCqhJ9zrVMs
M88EK0TTdkE/NWC25JUmsPXX37Z5jFXIFamM0FWE/zhDip8Xfl3yqAM+NVQ5xnmW
GMpqyHn/A1EaRv0ASVTMsg413N3jr815qKy/xSCbyRIFhrBmKkwy0bSZVP+9Y0sC
8+bBTZNdbNwrdp0It7BH/xKmG0MaTNpAgVdkbnnSt5DlqS91Sbta5i+kHibkFml4
KnZ6OvJgQuTbC+UsbpD5B1vM8Rd264xOQmH9KFNgouDhiBOtwAL3bn+76Pdt8OnM
bO0JAgMBAAGgZjBkBgkqhkiG9w0BCQ4xVzBVMAkGA1UdEwQCMAAwCwYDVR0PBAQD
AgXgMCoGA1UdJQEB/wQgMB4GCCsGAQUFBwMIBggrBgEFBQcDAQYIKwYBBQUHAwIw
DwYDVR0RBAgwBocEfwAAATANBgkqhkiG9w0BAQUFAAOCAgEAL5fYwydR3LaAEX0N
RNYbAyeunF0oYSKFz7vL3XF+nDHN8WBd49wfApBb9VCEcjINRInC7/i57xNlw4Bw
phdQiPGTG4dUdtfMw56n9z47RmZpmMs1sfS3CdPuJFb5NcrLGfBJRjRZ9+UgkbmA
+4LP2QHMi4viS3r2DmRhIrKl8Ov+S2TTyY4QdazK4PjtmtqYrB6XeE4I5cT4UsK2
6jB32U4JCK7mUwaFenSkMKOSXNKBz4dzM1508WuO8z+lbTQSoUBW6YK89XHzhye3
URDlLzSQy0BYZ/J4djpEDbJ/t+52vQrXU/mAbSZMuiRaacVs90b2r8MkYqTH/BCM
3e5dtZIk28K27mR54/K0noS46l2TXPbDZIxaCVyaBjw/ogC2FoIEVOcVdISZ7XOj
NigTBW1ndBRqXYpKAAlGVV3dIxe54OH7awt+Arn63S9YXprjXn78N6ohl1OMxG/8
ES+FAY+0Oly7pOZsbg9W08Ao3CTqW5cobVcQE36ZWO2lahb1w6Ya7B843g7S56WS
GatSli9UHN5bUrb135elwwzDixeb/PPnYqBIGG2hOSZJz5oxwnWqxiGR/pApHuRx
Beta1BwPNZ897jbZ/M+JuOep52OGsZYKdJ1dDICrVdnSmEUPbS6L3vG6ZH1SdAix
LRTenXfl/mlG3QW2aLSn+kjtGW8=
MIIFEDCCAvgCAQAwZTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUx
ITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEeMBwGA1UEAwwVaHR0
cDovLzEyNy4wLjAuMTo0MDAxMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC
AgEA1XBtjDav5Sl3H+/fUcGiQO36oqtZG+YuC9D0z0u89Shq+XNs3tRtonGGCyEI
rDtI6R7PItMJa6rQ1VuFoMWPrjmFf5tFemSOZtQx/DF78H+5tWaIBVDA+Kw1zxdq
j1n3/AQjAGsSuqhgcaIQQFqTNNtAtW40048fDh17jWIDB9baF65az2uArq97uS4d
eDujG3CHV9svO7hoqpzYt039VDKK4N+dDMUZFqhEmY2MqjyQySY2bd/gsYBjcGWS
IuonALactiYDc4zIusAfNptnXycwK/aQAqDwhwMcQA9L5YKQ5hoUukDTQFbvoNLJ
3vNQDI/o8sjCh94EkMuopSp90tJ/syTPKRlh/MaGMXvwu8Vab5iPeVop48jTKl3Z
+G8NErGM8KKCyd1mQoGisVuIMQPKuJUi7jOu6wgXlA8ZgUGfSQQDA4v2Q0tV/GlJ
mvsP5JshA3v7C/sSBY/3AnPHeWTlozXlNgXitxps1EwgR2jo+YW2gxrfM//xtgMC
jMXjO9g2TsCnWR6j93oXWn88UP/CeAcyjeTdJjW+piuLdvYOctY6+Yql5gm9Vx0u
+w9jTmpzOCoh+9cNtjqmPiOhecFcVf71vMf4krMp6lmY/Nq1/km9u0ViNP5CJHk1
8YXG42vnj7sUgT7WgpLh8g/iPc2petH9lMd9te+Jyak/zA8CAwEAAaBmMGQGCSqG
SIb3DQEJDjFXMFUwCQYDVR0TBAIwADALBgNVHQ8EBAMCBeAwKgYDVR0lAQH/BCAw
HgYIKwYBBQUHAwgGCCsGAQUFBwMBBggrBgEFBQcDAjAPBgNVHREECDAGhwR/AAAB
MA0GCSqGSIb3DQEBBQUAA4ICAQCeVkI4JwSyax5kJJ1iljkngfK+BdsrqBJWPITj
mrl9DH7/2ev0GdzHEG3oQrti6vMvsowS6vHtpgc1tsbS9UwDY/N0ZgFWgnb5O5k2
4zZfGkma3GHCvG9WXsA9+Gs7KedggsXxfnJTLe4B/sZqtRO0dMD/JZTJQ6reuayh
bYvVBVSmittAjsfer+9xuXkHYYAPNYmW52aUN1AhnIsS3TVp1vHcxgNoFOQglN21
lHwmeh5QbTx/paHFnWLLqLVydbiB/Qzz6x4zsEKESATd02WbN9XKUfGM0G+bG+57
ErtrU7yzsLjPYYPcP9nYg8dzfdwVgfdjg+yw2hdmkqjDQD3YAmxRcat7uK8htVa0
z4dfjdNRO3HhSLALKS/Tl9qpLKpEi8/0ByYErJz6i+Xyf4pkdPQcBQKybkFja136
9xkonhE7DLTo1zQobfAJlnfTMxuJc0mOGvT+DqGSCFmNEl3WgIAgu9m77mp81Bqo
0qwrB3pYSAzL9xHuluwZMn37sdmVFFReEkEaRllRgDTZL9vSQh2yOqtV920083/y
sHPUijSsKysSKz0RuzMBCc3RD07Kcs1TFg/NdZiYKf8V9NDDOgk5LC2Sqoin7v6F
yB4wpnm6RqQ7iSRpp/VBs3PAnK2uJEkoOU5p5jZdQ0IDtesHVzM7bCUIrQIX2pAr
owvMhQ==
-----END CERTIFICATE REQUEST-----

View File

@ -1,54 +1,54 @@
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: DES-EDE3-CBC,34239066AD971D20
DEK-Info: DES-EDE3-CBC,E445BB698AB9CDC4
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
-----END RSA PRIVATE KEY-----

View File

@ -1,51 +1,51 @@
-----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY-----

View File

@ -1,14 +0,0 @@
-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAmmCdKFi6X1mpk5AU6EEH
exou9NTRyiQVaHmTQvyPu6rd9krXB47/TgBDXcGIstkhFkGLwAh3cHDEPEF2jEcw
W27S+/MvQdVgC4SJaN83pmk6ZYOvr0AX6zmPschoLxl84AT9xKHhFJuH5X1eCzP5
DY1rAvNLdB9lFC/DM8m2AySwKHc1kAPhs//j6RPcI8R37yDOEta7e/ikhbAwnOFv
/rs3Aob/nYE0ql2CMpO68uU9vbDYQt2bFdiX/zau402Zi9kU1lAaeNBNM0UP9thU
/SSOYuDFqy+XbRVvItLhjvo5hP46GOw9GLz9ICQQohiXjC33e4Hs8sq2XkM+jYyk
DGRiPEtVwt95x9h/ReCYJowJzJWnaSvQKEPNQaMvwGCV5ZLZ0IlI8cqS3m+ns4ZK
gTQDQjeRJADz0JY3jBpBhLebH2HfrYJGp3EWC7CdhhTvYXN5ZkBK6A7xkzPY0mZj
RAvS5K417LkAc+G0gO6qyJtXplkL4G/Q07Vdt8zc7ZAg5rbGWY83lw8E/7h0Gpu2
JXfANZzdPKiV5P2tB6ZEwdxTABY/kHEk0u0WoPjqqgNv9I/zwLCbjefon8RcIJ5D
EXXm9DibcaCpRYUkq5redFXDG8VHVzYVce2CJdrko8GvWUIOsAh0Y4CbyrXgXepV
BvDtjEvMJUJ/iI33Ytzi6w0CAwEAAQ==
-----END PUBLIC KEY-----

View File

@ -1,14 +0,0 @@
-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtG+XAmj6VT5CQCiX6wUQ
DJb6pM3F1MqJKp4eGKClvK6jXz8gu9clFHPhKWvHWyouN/id7TQ3TBPLeX4MaL2Q
jf0GtWuPwuD2ZOV8lDL8klINUgyWL+WeRt+M2WwLeaJEuZhjGootkkfmicSshH5c
JMnejiRWTpAzLehHQs9ZbP9yzg+tc1WiwvqOXi1NM7KpYALh20MaUMSVIW6Lvwq5
JAIo772x6lW9IVcTxajfYtlZDW9qGD/4n95qBvDqtqSX665UmB+Kfskf5sWyIEKQ
4A3i/RAXclhyEwgF5fBDMSr8ek3vcSzbA8a7KCUHQ4aTGm6dnQHtsAlI5Aww0vs2
QYi7oc2o5G3YXNfKmJdogxQJVwi+9/ZWoYGlQt77h90u9twej8XozOabF8M0zC8F
vU/7Wc84pzvykpY7aao36E653FNt3MCJhRG2MinqjITO+Xcd2B/dRRWYFB7qM/D0
KbXtzvLnyJw91ERGjEJJ2E5/BgW6Wz996c/AGExnnf5xFgZ4/Z/dNPpwzNwRSTPZ
khlrhdmnQkKjHXJDHLy+uZZ8uj2yCGoTaJEZapEp7GGvsgwi/1DbAvmpwZpfXADW
A4x+m6rJJvrOnwui5URcAgj1F+JEW6d2piMmO1mDayiyNEkRpv3jB/Au713SUpm0
0GQCHrmrTb5izgCH3ggS75kCAwEAAQ==
-----END PUBLIC KEY-----

Binary file not shown.

View File

@ -1,31 +1,31 @@
-----BEGIN CERTIFICATE-----
MIIFXDCCA0SgAwIBAgIBAjANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJBVTET
MBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQ
dHkgTHRkMB4XDTEzMDgxMTE1MzMxMVoXDTE0MDgxMTE1MzMxMVowRTELMAkGA1UE
dHkgTHRkMB4XDTEzMTExMzE5MTg1MFoXDTE0MTExMzE5MTg1MFowRTELMAkGA1UE
BhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdp
ZGdpdHMgUHR5IEx0ZDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALRv
lwJo+lU+QkAol+sFEAyW+qTNxdTKiSqeHhigpbyuo18/ILvXJRRz4Slrx1sqLjf4
ne00N0wTy3l+DGi9kI39BrVrj8Lg9mTlfJQy/JJSDVIMli/lnkbfjNlsC3miRLmY
YxqKLZJH5onErIR+XCTJ3o4kVk6QMy3oR0LPWWz/cs4PrXNVosL6jl4tTTOyqWAC
4dtDGlDElSFui78KuSQCKO+9sepVvSFXE8Wo32LZWQ1vahg/+J/eagbw6rakl+uu
VJgfin7JH+bFsiBCkOAN4v0QF3JYchMIBeXwQzEq/HpN73Es2wPGuyglB0OGkxpu
nZ0B7bAJSOQMMNL7NkGIu6HNqORt2FzXypiXaIMUCVcIvvf2VqGBpULe+4fdLvbc
Ho/F6MzmmxfDNMwvBb1P+1nPOKc78pKWO2mqN+hOudxTbdzAiYURtjIp6oyEzvl3
Hdgf3UUVmBQe6jPw9Cm17c7y58icPdRERoxCSdhOfwYFuls/fenPwBhMZ53+cRYG
eP2f3TT6cMzcEUkz2ZIZa4XZp0JCox1yQxy8vrmWfLo9sghqE2iRGWqRKexhr7IM
Iv9Q2wL5qcGaX1wA1gOMfpuqySb6zp8LouVEXAII9RfiRFundqYjJjtZg2sosjRJ
Eab94wfwLu9d0lKZtNBkAh65q02+Ys4Ah94IEu+ZAgMBAAGjVzBVMAkGA1UdEwQC
ZGdpdHMgUHR5IEx0ZDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALY5
FfOgbclrkfZS/XpzPJgWZrs/W+zakuoBDtkeTeIdVMk2lNZ//oA4g+eYVPnf0DSJ
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
-----END CERTIFICATE-----

View File

@ -1,29 +1,29 @@
-----BEGIN CERTIFICATE REQUEST-----
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
djj5eM7u0qX1tjAezwt7OYAHEco=
DwYDVR0RBAgwBocEfwAAATANBgkqhkiG9w0BAQUFAAOCAgEAsjhQOckysiSTZaz/
Ck5APYE13ckBQOxvHqrCvDQSRMqq/yb/eWtItQhCs9u3fKGFGKCefPYwtVdUj6v2
g4SpXzBblcXNP8SXEtbcNuG4lPNsTc2YbDxuLWmgvLX8on2nq0f+TcGmtvvmCaLz
tEdLPlQfkzQUlwfaK4kMJ4qzR8eeHxBrhKJyDyzSreZSa/ZXMcKpjJLhXl6oM+ud
cfi0BO/xOKt/MmGHAxTMNrTFV5HcEWzws+r51sCcIV4BwuELA5rZuG0leaj3CDDy
3dPkpLp3HObbX3KV6yyCCfEAN4pyoZQNut8i87FITsYbJJy4ld7/PWTvg/JMk96X
ZACpeI1ERL/Sr3uzFpmlLszwSQ7t/hv5ykFA6d1QLv0u+EX1mL1O/PZ5Lb3moC4t
7ma3CffPqJpEjvvdXtweol62MI4Rr/1sXyZGR+lvkjcoSY5vzv0j7V4w6/e1In3D
nHY4vmpuy65w5gGm/s2y2cXRfCc8P4we0+A+QbNAgt3eicdJmWZGcWwlMQI10Nmq
6Qnp8cpwvFM/iCIajfIAz0fD7G0UEL5ExSFoh6WhFFDOYbTl5MeXUjMaYmemICE2
r3GVfZxHLvDQzftkq/oSXXiZgLkoYveN0R7VJuUfCgyLOlJL5O8WX2lX8odUKINc
DcZ/FpV+R/u7WGm2j0tvRA4y9Qc=
-----END CERTIFICATE REQUEST-----

View File

@ -1,54 +1,54 @@
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: DES-EDE3-CBC,6357944A31F1FA4D
DEK-Info: DES-EDE3-CBC,F332544C4A2552FC
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
-----END RSA PRIVATE KEY-----

View File

@ -1,51 +1,51 @@
-----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY-----

View File

@ -1,14 +0,0 @@
-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtG+XAmj6VT5CQCiX6wUQ
DJb6pM3F1MqJKp4eGKClvK6jXz8gu9clFHPhKWvHWyouN/id7TQ3TBPLeX4MaL2Q
jf0GtWuPwuD2ZOV8lDL8klINUgyWL+WeRt+M2WwLeaJEuZhjGootkkfmicSshH5c
JMnejiRWTpAzLehHQs9ZbP9yzg+tc1WiwvqOXi1NM7KpYALh20MaUMSVIW6Lvwq5
JAIo772x6lW9IVcTxajfYtlZDW9qGD/4n95qBvDqtqSX665UmB+Kfskf5sWyIEKQ
4A3i/RAXclhyEwgF5fBDMSr8ek3vcSzbA8a7KCUHQ4aTGm6dnQHtsAlI5Aww0vs2
QYi7oc2o5G3YXNfKmJdogxQJVwi+9/ZWoYGlQt77h90u9twej8XozOabF8M0zC8F
vU/7Wc84pzvykpY7aao36E653FNt3MCJhRG2MinqjITO+Xcd2B/dRRWYFB7qM/D0
KbXtzvLnyJw91ERGjEJJ2E5/BgW6Wz996c/AGExnnf5xFgZ4/Z/dNPpwzNwRSTPZ
khlrhdmnQkKjHXJDHLy+uZZ8uj2yCGoTaJEZapEp7GGvsgwi/1DbAvmpwZpfXADW
A4x+m6rJJvrOnwui5URcAgj1F+JEW6d2piMmO1mDayiyNEkRpv3jB/Au713SUpm0
0GQCHrmrTb5izgCH3ggS75kCAwEAAQ==
-----END PUBLIC KEY-----

44
log/log.go Normal file
View File

@ -0,0 +1,44 @@
package log
import (
golog "github.com/coreos/go-log/log"
"os"
)
// The Verbose flag turns on verbose logging.
var Verbose bool = false
var logger *golog.Logger = golog.New("etcd", false,
golog.CombinedSink(os.Stdout, "[%s] %s %-9s | %s\n", []string{"prefix", "time", "priority", "message"}))
func Infof(format string, v ...interface{}) {
logger.Infof(format, v...)
}
func Debugf(format string, v ...interface{}) {
if Verbose {
logger.Debugf(format, v...)
}
}
func Debug(v ...interface{}) {
if Verbose {
logger.Debug(v...)
}
}
func Warnf(format string, v ...interface{}) {
logger.Warningf(format, v...)
}
func Warn(v ...interface{}) {
logger.Warning(v...)
}
func Fatalf(format string, v ...interface{}) {
logger.Fatalf(format, v...)
}
func Fatal(v ...interface{}) {
logger.Fatalln(v...)
}

View File

@ -1,58 +0,0 @@
/*
Copyright 2013 CoreOS Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
// machineNum returns the number of machines in the cluster
func machineNum() int {
response, _ := etcdStore.RawGet("_etcd/machines")
return len(response)
}
// getMachines gets the current machines in the cluster
func getMachines(toURL func(string) (string, bool)) []string {
peers := r.Peers()
machines := make([]string, len(peers)+1)
leader, ok := toURL(r.Leader())
self, _ := toURL(r.Name())
i := 1
if ok {
machines[0] = leader
if leader != self {
machines[1] = self
i = 2
}
} else {
machines[0] = self
}
// Add all peers to the slice
for peerName, _ := range peers {
if machine, ok := toURL(peerName); ok {
// do not add leader twice
if machine != leader {
machines[i] = machine
i++
}
}
}
return machines
}

View File

@ -1,7 +1,7 @@
## Etcd modules
etcd modules (mods) are higher order pieces of functionality that only
speak to the client etcd API and are presented in the `/etcd/mod` HTTP path
speak to the client etcd API and are presented in the `/mod` HTTP path
of the etcd service.
The basic idea is that etcd can ship things like dashboards, master

View File

@ -24,4 +24,4 @@ bower install
Run etcd like you normally would and afterward browse to:
http://localhost:4001/etcd/mod/dashboard/
http://localhost:4001/mod/dashboard/

View File

@ -2,10 +2,10 @@
angular.module('etcd', [])
.factory('EtcdV1', ['$http', function($http) {
var keyPrefix = '/v1/keys/'
var statsPrefix = '/v1/stats/'
var baseURL = '/v1/'
.factory('EtcdV2', ['$http', function($http) {
var keyPrefix = '/v2/keys/'
var statsPrefix = '/v2/stats/'
var baseURL = '/v2/'
delete $http.defaults.headers.common['X-Requested-With'];
@ -15,7 +15,8 @@ angular.module('etcd', [])
return '';
}
parts = parts.filter(function(v){return v!=='';});
return parts.join('/');
parts = parts.join('/');
return parts
}
function newKey(keyName) {
@ -32,7 +33,11 @@ angular.module('etcd', [])
};
self.path = function() {
return '/' + cleanupPath(keyPrefix + self.name);
var path = '/' + cleanupPath(keyPrefix + self.name);
if (path === keyPrefix.substring(0, keyPrefix.length - 1)) {
return keyPrefix
}
return path
};
self.get = function() {
@ -43,7 +48,7 @@ angular.module('etcd', [])
return $http({
url: self.path(),
data: $.param({value: keyValue}),
method: 'POST',
method: 'PUT',
headers: {'Content-Type': 'application/x-www-form-urlencoded'}
});
};

View File

@ -2,7 +2,7 @@
angular.module('etcdBrowser', ['ngRoute', 'etcd', 'timeRelative'])
.constant('keyPrefix', '/v1/keys')
.constant('keyPrefix', '/v2/keys/')
.config(['$routeProvider', 'keyPrefix', function ($routeProvider, keyPrefix) {
//read localstorage
@ -18,7 +18,7 @@ angular.module('etcdBrowser', ['ngRoute', 'etcd', 'timeRelative'])
});
}])
.controller('MainCtrl', ['$scope', '$location', 'EtcdV1', 'keyPrefix', function ($scope, $location, EtcdV1, keyPrefix) {
.controller('MainCtrl', ['$scope', '$location', 'EtcdV2', 'keyPrefix', function ($scope, $location, EtcdV2, keyPrefix) {
$scope.save = 'etcd-save-hide';
$scope.preview = 'etcd-preview-hide';
$scope.enableBack = true;
@ -43,12 +43,12 @@ angular.module('etcdBrowser', ['ngRoute', 'etcd', 'timeRelative'])
// Notify everyone of the update
localStorage.setItem('etcdPath', $scope.etcdPath);
$scope.enableBack = true;
//disable back button if at root (/v1/keys/)
if($scope.etcdPath === '/v1/keys') {
//disable back button if at root (/v2/keys/)
if($scope.etcdPath === keyPrefix) {
$scope.enableBack = false;
}
$scope.key = EtcdV1.getKey(etcdPathKey($scope.etcdPath));
$scope.key = EtcdV2.getKey(etcdPathKey($scope.etcdPath));
});
$scope.$watch('key', function() {
@ -59,14 +59,14 @@ angular.module('etcdBrowser', ['ngRoute', 'etcd', 'timeRelative'])
//hide any errors
$('#etcd-browse-error').hide();
// Looking at a directory if we got an array
if (data.length) {
$scope.list = data;
if (data.dir === true) {
$scope.list = data.kvs;
$scope.preview = 'etcd-preview-hide';
} else {
$scope.singleValue = data.value;
$scope.preview = 'etcd-preview-reveal';
$scope.key.getParent().get().success(function(data) {
$scope.list = data;
$scope.list = data.kvs;
});
}
$scope.previewMessage = 'No key selected.';

View File

@ -14,14 +14,14 @@ angular.module('etcdStats', ['ngRoute', 'etcd'])
});
}])
.controller('StatsCtrl', ['$scope', 'EtcdV1', 'statsVega', function ($scope, EtcdV1, statsVega) {
.controller('StatsCtrl', ['$scope', 'EtcdV2', 'statsVega', function ($scope, EtcdV2, statsVega) {
$scope.graphContainer = '#latency';
$scope.graphVisibility = 'etcd-graph-show';
$scope.tableVisibility = 'etcd-table-hide';
//make requests
function readStats() {
EtcdV1.getStat('leader').get().success(function(data) {
EtcdV2.getStat('leader').get().success(function(data) {
$scope.leaderStats = data;
$scope.leaderName = data.leader;
$scope.machines = [];

View File

@ -56,7 +56,7 @@
</thead>
<tbody>
<tr ng-repeat="key in list | orderBy:'key'">
<td><a ng-class="{true:'directory'}[key.dir]" href="#/v1/keys{{key.key}}" highlight>{{key.key}}</a></td>
<td><a ng-class="{true:'directory'}[key.dir]" href="#/v2/keys{{key.key}}" highlight>{{key.key}}</a></td>
<td ng-switch on="!!key.expiration" class="etcd-ttl">
<div ng-switch-when="true"><time relative datetime="{{key.expiration.substring(0, key.expiration.lastIndexOf('-'))}}"></time></div>
<div ng-switch-default class="etcd-ttl-none">&mdash;</div>

View File

@ -13,5 +13,5 @@ export GOPATH="${DIR}/../../"
for i in `find dist -type f`; do
file=$(echo $i | sed 's#dist/##g' | sed 's#/#-#g')
go build github.com/jteeuwen/go-bindata
./go-bindata -pkg "resources" -toc -out resources/$file.go -prefix dist $i
./go-bindata -nomemcopy -pkg "resources" -toc -out resources/$file.go -prefix dist $i
done

View File

@ -7,10 +7,12 @@ import (
"path"
"time"
"github.com/coreos/etcd/log"
"github.com/coreos/etcd/mod/dashboard/resources"
)
func memoryFileServer(w http.ResponseWriter, req *http.Request) {
log.Debugf("[recv] %s %s [%s]", req.Method, req.URL.Path, req.RemoteAddr)
upath := req.URL.Path
if len(upath) == 0 {
upath = "index.html"
@ -42,6 +44,7 @@ func HttpHandler() (handler http.Handler) {
// Serve the dashboard from a filesystem if the magic env variable is enabled
dashDir := os.Getenv("ETCD_DASHBOARD_DIR")
if len(dashDir) != 0 {
log.Debugf("Using dashboard directory %s", dashDir)
handler = http.FileServer(http.Dir(dashDir))
}

View File

@ -4,73 +4,36 @@ import (
"bytes"
"compress/gzip"
"io"
"reflect"
"unsafe"
)
var _browser_html = "\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x8c\x54\x4f\x6f\xdb\x3e\x0c\xbd\xf7\x53\xb0\xbe\xcb\x3e\xfc\xf0\x43\xdb\x41\x36\xd0\x0d\x3d\xf4\xb6\xcb\x80\x0d\x45\x51\x28\x12\x13\xab\xb3\x45\x4d\xa2\x93\xe5\xdb\x4f\xf2\x9f\xd6\x6d\xb3\x21\x39\xc4\xb4\xc8\xf7\x44\xf2\xbd\x44\x5e\x1a\xd2\x7c\xf4\x08\x2d\xf7\x5d\x73\x21\x2f\x85\x78\xb0\x5b\xe8\x18\xee\xef\xe0\xea\xb1\x81\xf1\x23\x73\x16\x74\xa7\x62\xac\x0b\x47\xe2\x39\xa6\x0a\x61\xf1\x66\x7a\x5c\x4f\x8f\xab\xa2\x01\x79\xf9\x80\xce\xd8\xed\xa3\x10\xaf\x6c\x6b\xaa\x33\xd8\xfe\x41\x73\x7d\x0e\xcd\xdf\xf0\x3b\x9e\x29\xf2\x41\x73\x02\x3f\x02\x85\x78\x03\xce\xf7\xa0\x32\x39\x48\x61\x8f\xac\x40\xb7\x2a\x44\xe4\xba\x18\x78\x2b\x52\xb7\xab\x54\xcb\xec\x05\xfe\x1a\xec\xbe\x2e\xbe\x8b\x6f\xb7\xe2\x0b\xf5\x5e\xb1\xdd\x74\x58\x80\x26\xc7\xe8\x12\xee\xfe\xae\x46\xb3\xc3\x05\xc9\x96\x3b\x6c\x90\xb5\x81\xcf\x81\x0e\x11\x83\xac\xa6\xb3\x15\xb3\x53\x3d\xd6\x85\xc1\xa8\x83\xf5\x6c\xc9\xad\xf8\x8a\x8f\x85\x7b\x8b\x07\x4f\x81\x57\x55\x07\x6b\xb8\xad\x0d\xee\xad\x46\x31\xbe\x2c\xb8\x34\x33\x7c\xed\x94\x46\xd8\xaa\x94\x25\x57\xa6\x2f\x50\xce\x80\xf2\xbe\x43\xc1\x34\xe8\x56\x8c\x09\xef\x76\x60\x1d\x70\x8b\x10\x88\x18\x8c\x0d\xa8\x99\xc2\x11\xf2\xb2\x2e\x5e\xb4\xe9\xac\xfb\x09\x01\xbb\xba\x88\x7c\xec\x30\xb6\x88\xa9\x97\x36\xe0\x76\x39\xa9\x7a\x65\x5d\xa9\x63\xda\xfa\x85\xac\x96\x1d\xcb\x0d\x99\x23\xb8\x9d\x48\x37\xd7\x45\xde\xc9\xbc\x92\x55\xaf\x6f\x0c\x3a\x5f\x29\xfd\xa2\xe4\x66\xac\x6f\x13\xfe\x58\x34\x3f\x68\x00\x15\x10\x86\x68\x53\xe3\xca\x81\x8c\x1c\xc8\xed\x1a\x1a\xd8\x28\x46\x23\xab\xf9\x00\x26\x5c\x28\xd3\x26\x50\x45\x04\xa9\xe6\x76\xb3\xa6\x9f\xaa\x6a\xc5\x5b\x6a\xea\xab\xa2\x19\xfc\x2e\x28\x83\x70\xa4\x21\x2c\x70\x59\xa9\x06\x98\xc0\xf6\x3e\xd0\x7e\xce\xe1\x6f\x8f\xc1\xa2\xd3\x58\xca\xca\x2f\x83\xac\x4c\x76\x62\xb4\x9b\xd7\xd1\x26\xc9\x21\x06\x9d\xa6\xa3\x03\x86\xa7\x74\xbf\x27\x97\x54\x8d\x15\xc6\xff\x45\x6c\x6d\xff\x12\x94\xd9\xc7\x69\xac\x11\x74\x1e\xc7\x73\x24\xf7\x5f\xd5\xd9\xcd\x14\x95\x7d\x12\xe6\x23\xcb\xe9\x8e\xe1\xd6\x98\x69\xca\x68\x19\x81\xc2\xe8\x19\xab\x55\xf6\xe8\x62\x3e\x68\x31\x89\x30\xfd\x9c\x12\xcc\xd8\x3d\x58\x93\xe5\x35\x4f\xf3\xde\x8a\xac\x79\x36\xed\x24\xfa\xa2\x76\x95\x4a\x9b\x57\x57\xad\xa7\x98\xe2\x38\xeb\x12\x44\x4f\x66\x48\xb6\x7a\xd7\xf8\x79\xd8\xf9\xfd\x1d\x56\x56\xd9\x8c\xa3\x3b\xf3\x9f\xe3\x9f\x00\x00\x00\xff\xff\x4a\x5c\x90\x9e\x2c\x05\x00\x00"
// browser_html returns raw, uncompressed file data.
func browser_html() []byte {
gz, err := gzip.NewReader(bytes.NewBuffer([]byte{
0x1f,0x8b,0x08,0x00,0x00,0x09,0x6e,0x88,0x00,0xff,0x8c,0x54,
0x4f,0x6f,0xdb,0x3e,0x0c,0xbd,0xf7,0x53,0xb0,0xbe,0xcb,0x3e,
0xfc,0xf0,0x43,0xdb,0x41,0x36,0xd0,0x0d,0x3d,0xf4,0xb6,0xcb,
0x80,0x0d,0x45,0x51,0x28,0x12,0x13,0xab,0xb3,0x45,0x4d,0xa2,
0x93,0xe5,0xdb,0x4f,0xf2,0x9f,0xd6,0x6d,0xb3,0x21,0x39,0xc4,
0xb4,0xc8,0xf7,0x44,0xf2,0xbd,0x44,0x5e,0x1a,0xd2,0x7c,0xf4,
0x08,0x2d,0xf7,0x5d,0x73,0x21,0x2f,0x85,0x78,0xb0,0x5b,0xe8,
0x18,0xee,0xef,0xe0,0xea,0xb1,0x81,0xf1,0x23,0x73,0x16,0x74,
0xa7,0x62,0xac,0x0b,0x47,0xe2,0x39,0xa6,0x0a,0x61,0xf1,0x66,
0x7a,0x5c,0x4f,0x8f,0xab,0xa2,0x01,0x79,0xf9,0x80,0xce,0xd8,
0xed,0xa3,0x10,0xaf,0x6c,0x6b,0xaa,0x33,0xd8,0xfe,0x41,0x73,
0x7d,0x0e,0xcd,0xdf,0xf0,0x3b,0x9e,0x29,0xf2,0x41,0x73,0x02,
0x3f,0x02,0x85,0x78,0x03,0xce,0xf7,0xa0,0x32,0x39,0x48,0x61,
0x8f,0xac,0x40,0xb7,0x2a,0x44,0xe4,0xba,0x18,0x78,0x2b,0x52,
0xb7,0xab,0x54,0xcb,0xec,0x05,0xfe,0x1a,0xec,0xbe,0x2e,0xbe,
0x8b,0x6f,0xb7,0xe2,0x0b,0xf5,0x5e,0xb1,0xdd,0x74,0x58,0x80,
0x26,0xc7,0xe8,0x12,0xee,0xfe,0xae,0x46,0xb3,0xc3,0x05,0xc9,
0x96,0x3b,0x6c,0x90,0xb5,0x81,0xcf,0x81,0x0e,0x11,0x83,0xac,
0xa6,0xb3,0x15,0xb3,0x53,0x3d,0xd6,0x85,0xc1,0xa8,0x83,0xf5,
0x6c,0xc9,0xad,0xf8,0x8a,0x8f,0x85,0x7b,0x8b,0x07,0x4f,0x81,
0x57,0x55,0x07,0x6b,0xb8,0xad,0x0d,0xee,0xad,0x46,0x31,0xbe,
0x2c,0xb8,0x34,0x33,0x7c,0xed,0x94,0x46,0xd8,0xaa,0x94,0x25,
0x57,0xa6,0x2f,0x50,0xce,0x80,0xf2,0xbe,0x43,0xc1,0x34,0xe8,
0x56,0x8c,0x09,0xef,0x76,0x60,0x1d,0x70,0x8b,0x10,0x88,0x18,
0x8c,0x0d,0xa8,0x99,0xc2,0x11,0xf2,0xb2,0x2e,0x5e,0xb4,0xe9,
0xac,0xfb,0x09,0x01,0xbb,0xba,0x88,0x7c,0xec,0x30,0xb6,0x88,
0xa9,0x97,0x36,0xe0,0x76,0x39,0xa9,0x7a,0x65,0x5d,0xa9,0x63,
0xda,0xfa,0x85,0xac,0x96,0x1d,0xcb,0x0d,0x99,0x23,0xb8,0x9d,
0x48,0x37,0xd7,0x45,0xde,0xc9,0xbc,0x92,0x55,0xaf,0x6f,0x0c,
0x3a,0x5f,0x29,0xfd,0xa2,0xe4,0x66,0xac,0x6f,0x13,0xfe,0x58,
0x34,0x3f,0x68,0x00,0x15,0x10,0x86,0x68,0x53,0xe3,0xca,0x81,
0x8c,0x1c,0xc8,0xed,0x1a,0x1a,0xd8,0x28,0x46,0x23,0xab,0xf9,
0x00,0x26,0x5c,0x28,0xd3,0x26,0x50,0x45,0x04,0xa9,0xe6,0x76,
0xb3,0xa6,0x9f,0xaa,0x6a,0xc5,0x5b,0x6a,0xea,0xab,0xa2,0x19,
0xfc,0x2e,0x28,0x83,0x70,0xa4,0x21,0x2c,0x70,0x59,0xa9,0x06,
0x98,0xc0,0xf6,0x3e,0xd0,0x7e,0xce,0xe1,0x6f,0x8f,0xc1,0xa2,
0xd3,0x58,0xca,0xca,0x2f,0x83,0xac,0x4c,0x76,0x62,0xb4,0x9b,
0xd7,0xd1,0x26,0xc9,0x21,0x06,0x9d,0xa6,0xa3,0x03,0x86,0xa7,
0x74,0xbf,0x27,0x97,0x54,0x8d,0x15,0xc6,0xff,0x45,0x6c,0x6d,
0xff,0x12,0x94,0xd9,0xc7,0x69,0xac,0x11,0x74,0x1e,0xc7,0x73,
0x24,0xf7,0x5f,0xd5,0xd9,0xcd,0x14,0x95,0x7d,0x12,0xe6,0x23,
0xcb,0xe9,0x8e,0xe1,0xd6,0x98,0x69,0xca,0x68,0x19,0x81,0xc2,
0xe8,0x19,0xab,0x55,0xf6,0xe8,0x62,0x3e,0x68,0x31,0x89,0x30,
0xfd,0x9c,0x12,0xcc,0xd8,0x3d,0x58,0x93,0xe5,0x35,0x4f,0xf3,
0xde,0x8a,0xac,0x79,0x36,0xed,0x24,0xfa,0xa2,0x76,0x95,0x4a,
0x9b,0x57,0x57,0xad,0xa7,0x98,0xe2,0x38,0xeb,0x12,0x44,0x4f,
0x66,0x48,0xb6,0x7a,0xd7,0xf8,0x79,0xd8,0xf9,0xfd,0x1d,0x56,
0x56,0xd9,0x8c,0xa3,0x3b,0xf3,0x9f,0xe3,0x9f,0x00,0x00,0x00,
0xff,0xff,0x4a,0x5c,0x90,0x9e,0x2c,0x05,0x00,0x00,
}))
var empty [0]byte
sx := (*reflect.StringHeader)(unsafe.Pointer(&_browser_html))
b := empty[:]
bx := (*reflect.SliceHeader)(unsafe.Pointer(&b))
bx.Data = sx.Data
bx.Len = len(_browser_html)
bx.Cap = bx.Len
gz, err := gzip.NewReader(bytes.NewBuffer(b))
if err != nil {
panic("Decompression failed: " + err.Error())
}
var b bytes.Buffer
io.Copy(&b, gz)
var buf bytes.Buffer
io.Copy(&buf, gz)
gz.Close()
return b.Bytes()
return buf.Bytes()
}
func init() {
go_bindata["/browser.html"] = browser_html
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -4,73 +4,36 @@ import (
"bytes"
"compress/gzip"
"io"
"reflect"
"unsafe"
)
var _stats_html = "\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x8c\x54\xcb\x6e\xdb\x30\x10\xbc\xe7\x2b\x36\xba\xd3\x3c\x14\x45\x92\x82\x12\x90\x16\x39\xe4\x56\xa0\x28\xd0\x22\x08\x02\x9a\x5c\x5b\x4c\x25\x2e\x4b\xae\xec\xfa\xef\x4b\x4a\x56\xa2\x3c\x5a\xd8\x07\x6b\xc5\xe5\xcc\x3e\x66\x6c\x75\x6e\xc9\xf0\x21\x20\xb4\xdc\x77\xcd\x99\x3a\x17\xe2\xce\x6d\xa0\x63\xb8\xbd\x81\x8b\xfb\x06\xc6\x8f\x2a\x59\x30\x9d\x4e\xa9\xae\x3c\x89\xc7\x94\x6f\x08\x87\x57\xd3\xe3\x72\x7a\x5c\x54\x0d\xa8\xf3\x3b\xf4\xd6\x6d\xee\x85\x78\x66\x5b\x52\x9d\xc0\xf6\x1f\x9a\xcb\x53\x68\xfe\x85\xdf\xf2\x91\xa2\x1c\x34\xef\xe0\x47\xa0\x10\x2f\xc0\xa5\x0e\x6a\x5b\x82\x1c\xf6\xc8\x1a\x4c\xab\x63\x42\xae\xab\x81\x37\x22\x77\xbb\x48\xb5\xcc\x41\xe0\xef\xc1\xed\xea\xea\x87\xf8\x7e\x2d\xbe\x50\x1f\x34\xbb\x75\x87\x15\x18\xf2\x8c\x3e\xe3\x6e\x6f\x6a\xb4\x5b\x9c\x91\xec\xb8\xc3\x06\xd9\x58\xf8\x1c\x69\x9f\x30\x2a\x39\x9d\x2d\x98\xbd\xee\xb1\xae\x2c\x26\x13\x5d\x60\x47\x7e\xc1\x57\xbd\xbd\xb8\x73\xb8\x0f\x14\x79\x71\x6b\xef\x2c\xb7\xb5\xc5\x9d\x33\x28\xc6\x97\x19\x97\x67\x86\xaf\x9d\x36\x08\x1b\x9d\xb3\xe4\x57\xf9\x0b\xb4\xb7\xa0\x43\xe8\x50\x30\x0d\xa6\x15\x63\x22\xf8\x2d\x38\x0f\xdc\x22\x44\x22\x06\xeb\x22\x1a\xa6\x78\x80\xb2\xac\xb3\x27\x6d\x3a\xe7\x7f\x41\xc4\xae\xae\x12\x1f\x3a\x4c\x2d\x62\xee\xa5\x8d\xb8\x99\x4f\x64\xaf\x9d\x5f\x99\x94\xb7\x7e\xa6\xe4\xbc\x63\xb5\x26\x7b\x00\xbf\x15\xb9\x72\x5d\x95\x9d\x7c\x63\xcd\x69\xd1\xe9\x0b\x7b\x1e\x0b\xaa\x30\xeb\xb8\x1e\x17\xd8\x66\xf4\xa1\x6a\x7e\xd2\x00\x3a\x22\x0c\xc9\xe5\xb6\xb5\x07\x95\x38\x92\xdf\x36\x34\xb0\xd5\x8c\x56\xc9\xe3\x01\x4c\xb8\xb8\xca\x7b\x40\x9d\x10\x94\x3e\x36\x5b\x14\xfd\x24\xe5\x82\x77\x65\xa8\x97\x55\x33\x84\x6d\xd4\x16\xe1\x40\x43\x9c\xe1\x4a\xea\x06\x98\xc0\xf5\x21\xd2\xee\x98\xc3\x3f\x01\xa3\x43\x6f\x70\xa5\x64\x98\x07\x59\x58\xec\x9d\xd1\xae\x9e\x47\x9b\x04\x87\x14\x4d\x9e\x8e\xf6\x18\x1f\x72\xfd\x40\x3e\x6b\x9a\x24\xa6\x8f\x22\xb5\xae\x7f\x0a\x56\xc5\xc5\x79\xac\x11\x74\x1a\xc7\x63\x22\xff\x41\x76\x6e\x3d\x45\xab\x3e\xcb\xf2\x96\xe5\xfd\x8e\xe1\xda\xda\x69\xca\xe4\x18\x81\xe2\xe8\x18\x67\x74\x71\xe8\x6c\x3d\x68\x31\x8b\x30\xfd\x98\x32\xcc\xba\x1d\x38\x5b\xc4\xb5\x0f\x69\x14\xb7\xe8\x5d\x0c\x3b\x09\x3e\x6b\x2d\xf3\xc5\xe6\xd9\x51\xcb\x19\xa6\x38\xc9\x11\x2e\x7a\xb2\x43\x36\xd4\xab\xa6\x4f\x41\x1e\xdf\x5e\x21\x95\x2c\x26\x1c\x5d\x59\xfe\x14\xff\x06\x00\x00\xff\xff\x95\x89\x83\x4d\x24\x05\x00\x00"
// stats_html returns raw, uncompressed file data.
func stats_html() []byte {
gz, err := gzip.NewReader(bytes.NewBuffer([]byte{
0x1f,0x8b,0x08,0x00,0x00,0x09,0x6e,0x88,0x00,0xff,0x8c,0x54,
0xcb,0x6e,0xdb,0x30,0x10,0xbc,0xe7,0x2b,0x36,0xba,0xd3,0x3c,
0x14,0x45,0x92,0x82,0x12,0x90,0x16,0x39,0xe4,0x56,0xa0,0x28,
0xd0,0x22,0x08,0x02,0x9a,0x5c,0x5b,0x4c,0x25,0x2e,0x4b,0xae,
0xec,0xfa,0xef,0x4b,0x4a,0x56,0xa2,0x3c,0x5a,0xd8,0x07,0x6b,
0xc5,0xe5,0xcc,0x3e,0x66,0x6c,0x75,0x6e,0xc9,0xf0,0x21,0x20,
0xb4,0xdc,0x77,0xcd,0x99,0x3a,0x17,0xe2,0xce,0x6d,0xa0,0x63,
0xb8,0xbd,0x81,0x8b,0xfb,0x06,0xc6,0x8f,0x2a,0x59,0x30,0x9d,
0x4e,0xa9,0xae,0x3c,0x89,0xc7,0x94,0x6f,0x08,0x87,0x57,0xd3,
0xe3,0x72,0x7a,0x5c,0x54,0x0d,0xa8,0xf3,0x3b,0xf4,0xd6,0x6d,
0xee,0x85,0x78,0x66,0x5b,0x52,0x9d,0xc0,0xf6,0x1f,0x9a,0xcb,
0x53,0x68,0xfe,0x85,0xdf,0xf2,0x91,0xa2,0x1c,0x34,0xef,0xe0,
0x47,0xa0,0x10,0x2f,0xc0,0xa5,0x0e,0x6a,0x5b,0x82,0x1c,0xf6,
0xc8,0x1a,0x4c,0xab,0x63,0x42,0xae,0xab,0x81,0x37,0x22,0x77,
0xbb,0x48,0xb5,0xcc,0x41,0xe0,0xef,0xc1,0xed,0xea,0xea,0x87,
0xf8,0x7e,0x2d,0xbe,0x50,0x1f,0x34,0xbb,0x75,0x87,0x15,0x18,
0xf2,0x8c,0x3e,0xe3,0x6e,0x6f,0x6a,0xb4,0x5b,0x9c,0x91,0xec,
0xb8,0xc3,0x06,0xd9,0x58,0xf8,0x1c,0x69,0x9f,0x30,0x2a,0x39,
0x9d,0x2d,0x98,0xbd,0xee,0xb1,0xae,0x2c,0x26,0x13,0x5d,0x60,
0x47,0x7e,0xc1,0x57,0xbd,0xbd,0xb8,0x73,0xb8,0x0f,0x14,0x79,
0x71,0x6b,0xef,0x2c,0xb7,0xb5,0xc5,0x9d,0x33,0x28,0xc6,0x97,
0x19,0x97,0x67,0x86,0xaf,0x9d,0x36,0x08,0x1b,0x9d,0xb3,0xe4,
0x57,0xf9,0x0b,0xb4,0xb7,0xa0,0x43,0xe8,0x50,0x30,0x0d,0xa6,
0x15,0x63,0x22,0xf8,0x2d,0x38,0x0f,0xdc,0x22,0x44,0x22,0x06,
0xeb,0x22,0x1a,0xa6,0x78,0x80,0xb2,0xac,0xb3,0x27,0x6d,0x3a,
0xe7,0x7f,0x41,0xc4,0xae,0xae,0x12,0x1f,0x3a,0x4c,0x2d,0x62,
0xee,0xa5,0x8d,0xb8,0x99,0x4f,0x64,0xaf,0x9d,0x5f,0x99,0x94,
0xb7,0x7e,0xa6,0xe4,0xbc,0x63,0xb5,0x26,0x7b,0x00,0xbf,0x15,
0xb9,0x72,0x5d,0x95,0x9d,0x7c,0x63,0xcd,0x69,0xd1,0xe9,0x0b,
0x7b,0x1e,0x0b,0xaa,0x30,0xeb,0xb8,0x1e,0x17,0xd8,0x66,0xf4,
0xa1,0x6a,0x7e,0xd2,0x00,0x3a,0x22,0x0c,0xc9,0xe5,0xb6,0xb5,
0x07,0x95,0x38,0x92,0xdf,0x36,0x34,0xb0,0xd5,0x8c,0x56,0xc9,
0xe3,0x01,0x4c,0xb8,0xb8,0xca,0x7b,0x40,0x9d,0x10,0x94,0x3e,
0x36,0x5b,0x14,0xfd,0x24,0xe5,0x82,0x77,0x65,0xa8,0x97,0x55,
0x33,0x84,0x6d,0xd4,0x16,0xe1,0x40,0x43,0x9c,0xe1,0x4a,0xea,
0x06,0x98,0xc0,0xf5,0x21,0xd2,0xee,0x98,0xc3,0x3f,0x01,0xa3,
0x43,0x6f,0x70,0xa5,0x64,0x98,0x07,0x59,0x58,0xec,0x9d,0xd1,
0xae,0x9e,0x47,0x9b,0x04,0x87,0x14,0x4d,0x9e,0x8e,0xf6,0x18,
0x1f,0x72,0xfd,0x40,0x3e,0x6b,0x9a,0x24,0xa6,0x8f,0x22,0xb5,
0xae,0x7f,0x0a,0x56,0xc5,0xc5,0x79,0xac,0x11,0x74,0x1a,0xc7,
0x63,0x22,0xff,0x41,0x76,0x6e,0x3d,0x45,0xab,0x3e,0xcb,0xf2,
0x96,0xe5,0xfd,0x8e,0xe1,0xda,0xda,0x69,0xca,0xe4,0x18,0x81,
0xe2,0xe8,0x18,0x67,0x74,0x71,0xe8,0x6c,0x3d,0x68,0x31,0x8b,
0x30,0xfd,0x98,0x32,0xcc,0xba,0x1d,0x38,0x5b,0xc4,0xb5,0x0f,
0x69,0x14,0xb7,0xe8,0x5d,0x0c,0x3b,0x09,0x3e,0x6b,0x2d,0xf3,
0xc5,0xe6,0xd9,0x51,0xcb,0x19,0xa6,0x38,0xc9,0x11,0x2e,0x7a,
0xb2,0x43,0x36,0xd4,0xab,0xa6,0x4f,0x41,0x1e,0xdf,0x5e,0x21,
0x95,0x2c,0x26,0x1c,0x5d,0x59,0xfe,0x14,0xff,0x06,0x00,0x00,
0xff,0xff,0x95,0x89,0x83,0x4d,0x24,0x05,0x00,0x00,
}))
var empty [0]byte
sx := (*reflect.StringHeader)(unsafe.Pointer(&_stats_html))
b := empty[:]
bx := (*reflect.SliceHeader)(unsafe.Pointer(&b))
bx.Data = sx.Data
bx.Len = len(_stats_html)
bx.Cap = bx.Len
gz, err := gzip.NewReader(bytes.NewBuffer(b))
if err != nil {
panic("Decompression failed: " + err.Error())
}
var b bytes.Buffer
io.Copy(&b, gz)
var buf bytes.Buffer
io.Copy(&buf, gz)
gz.Close()
return b.Bytes()
return buf.Bytes()
}
func init() {
go_bindata["/stats.html"] = stats_html
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -4,92 +4,36 @@ import (
"bytes"
"compress/gzip"
"io"
"reflect"
"unsafe"
)
var _views_stats_html = "\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xd4\x56\x4b\x8f\xdb\x36\x10\xbe\xe7\x57\x10\xec\xc1\x09\x20\xc9\x7a\xf8\xb1\x32\x2c\x03\x4d\x0b\xf4\xd2\xed\xa1\x29\x0a\x14\x45\x0f\x34\x45\x5b\xc4\xd2\x94\x4a\xd2\xd6\x3a\xae\xff\x7b\x87\xa2\xec\xae\x64\xc5\xdd\xa0\x7b\xe9\x3e\x40\x71\x38\xf3\xcd\xa7\xe1\xc7\xa1\x96\x39\x3f\x20\x2a\x88\xd6\x19\x66\x86\xe6\x3e\x2d\xa5\x21\x5c\x32\x85\x9a\xa9\x36\xc4\x68\x74\x3a\xd1\x52\xec\x77\x52\x9f\xcf\xf0\x6c\xc8\x5a\xb0\x5f\xb9\xe6\x6b\x2e\xb8\x39\x9e\xcf\x78\xf5\x0e\xc1\xcf\x0d\xd6\xba\xcc\x8f\xed\xda\xe0\xfa\xa6\x54\x3b\x62\x7c\xcd\x04\xa3\xa6\x54\x2f\x5c\x07\xdd\x2f\x7e\x3e\x37\x6c\x87\xba\xa6\xad\x22\x55\x81\x91\xdc\xfa\x54\x70\xfa\x94\x61\x5d\x94\xf5\x0f\xd6\xf8\xfe\x43\x0f\xb7\xc1\xd6\x87\x2d\x3a\x30\xa5\x79\x29\x33\x1c\x05\x11\x46\xcf\x3b\x21\x21\x53\x61\x4c\xb5\x18\x8f\xeb\xba\x0e\xea\x24\x28\xd5\x76\x1c\x87\x61\x38\x06\xff\xd6\x65\xf1\x2c\xb8\x7c\x1a\x72\x8c\xd2\x34\x1d\x37\xab\xe0\x9a\xe1\xb0\x7a\xc6\xe8\xe8\xc6\x1b\x02\x95\x62\x9a\xa9\x03\xfb\x56\x57\xf0\x02\x3f\x13\xc3\xcb\x0c\x3f\x3f\x72\xf9\x1b\xfc\x63\x74\xe0\xac\xfe\x58\x5a\x10\x14\xa2\x99\xfd\x0b\xc2\x70\x8e\x11\x93\xb6\xf6\xfe\x9a\xd0\xa7\xad\x2a\xf7\x32\xcf\xb0\x64\x35\xea\x79\x01\xcf\x85\xae\x08\x65\x19\xbe\xe4\x19\xaa\x41\x45\x4c\x81\x36\x5c\x88\x0c\x7f\x13\x7d\x0f\xbf\x1f\x31\x02\xc0\xc7\x24\xf4\x01\x28\x8d\xa8\x1f\xcd\x82\x59\x1c\x79\xa1\x9f\x58\xc3\xc4\x8b\x92\x60\x32\x9f\x5c\x66\x6e\xa0\xa1\xd7\xba\xb9\x55\xaf\xb3\xda\x0e\x37\xc9\xb5\xb3\xfb\x9d\x98\x16\xf8\xbb\xd9\x35\x5b\xf2\x90\x78\x93\x06\xdd\x51\xf2\x2e\xdc\x3e\x23\xa0\xe9\x4d\xe6\x41\x9c\xa4\xd4\x4f\x83\x69\x94\x02\xcd\xc8\xce\xa7\xfe\x3c\x98\x47\xb3\xcb\xc4\x0d\x37\x04\x3e\xc5\x61\x30\x79\x00\xd2\x71\x30\x9f\x3d\x00\x6e\xfb\x44\x5b\x2c\xcf\xc5\x79\x0d\xd6\x65\xe2\x86\x4f\x89\xf3\x71\xd9\xbd\x2b\x8f\xcf\x78\x3c\x50\x65\x2b\x9d\xd5\xbb\xae\xb2\xc7\x20\xed\xff\x20\xf6\xe6\x00\xf6\xc5\xfe\x8b\x35\xfe\xff\xc5\x3e\x4d\x83\x49\x3a\xb3\xc3\x34\x4c\xee\x0a\xbe\xe7\xf9\x4a\xd1\x2b\x60\xd0\x10\x76\x74\x71\xff\x00\xd4\x3c\x37\x45\x86\x1d\x38\x46\x05\xe3\xdb\xc2\x40\xd5\x92\x20\x8a\xa2\xc1\x0d\xee\x22\xc6\xe0\x68\x03\xdf\x18\x16\x0e\x41\x92\xc6\x6f\x00\xeb\xe4\x78\x47\x8d\xfd\x69\x5f\x98\xae\xd3\xf6\x20\x8a\x78\xf5\x48\x68\x01\xf7\x06\xfa\x91\x18\x26\xe9\x71\x39\x06\xdb\x7d\x89\x37\x48\xff\x5c\x38\x18\x71\xd8\x5d\xe1\xc2\xfb\x09\xbe\x8e\xa3\xe0\xda\xdc\xa3\x08\xcb\x03\xfc\x9a\x63\x85\x28\x13\xa2\x22\x79\xce\xe5\xb6\x29\xbe\x9d\x5b\x5d\xb5\xf3\x7e\x4c\xc1\x48\x3e\x50\x66\x93\x77\x08\x49\xb2\x63\xbe\x75\x85\xd7\xbc\xd2\xf8\x09\x8c\xcb\xb1\x79\x45\xf8\xb5\x28\xd7\xe2\xf6\xa3\xc0\x72\xcb\x64\x69\xec\x0d\x3c\x04\xaf\x6c\xf3\x50\xac\x62\x04\xd4\xb2\x6b\xf9\x70\x89\xda\x47\x3d\x70\x72\x2e\xbc\x20\x50\xd7\xdc\xd0\x02\xd9\x6e\x72\x32\x6a\xcf\x16\x23\xd1\xbc\xda\xc8\x43\x1b\x22\x34\x5b\xa0\xd1\xa6\x14\xa2\xac\xc1\x74\xfe\xdd\xad\xd9\x97\x45\x59\x76\xc9\x10\xd8\x8a\xfc\xf1\x85\x34\x4d\x2a\xbb\xa7\xd7\x5c\x7e\x5d\x30\xc8\x26\xda\x0a\x9e\x4e\x2f\x61\xce\xe7\x25\x6c\x90\xec\x54\xac\x5d\xf7\xcd\xb1\x82\x36\xf0\xde\x05\x7e\x00\xf5\x83\xe3\x6a\xa0\xff\xde\x49\x9d\xb3\x0d\xd9\x0b\x73\x9b\xf4\xcb\x28\xc3\xbb\xda\x56\xf0\x5f\x12\x77\x2e\x81\x3f\xf7\x44\x31\xd7\xe8\xc1\xba\x40\xa7\xd1\x0b\x3b\x1c\x20\xc6\xe4\x68\x71\xad\x69\x2b\x93\x80\xee\x95\x62\xd2\xa0\x25\x8a\xa7\x1e\xea\x84\x94\x8a\xc8\x2d\xbb\x1b\x33\x0b\x7b\x31\x8a\xe5\x77\x02\x56\x19\x44\xc0\x27\xe0\x6b\x8a\x3a\xa0\x69\xff\x40\xc4\x9e\xbd\xdc\xd2\x7e\x82\xbf\x90\xdc\xef\xd6\x4c\x2d\x22\x04\x9f\x9f\x3b\xfd\xd5\x75\x07\xab\xba\x39\x2d\xb7\x27\x03\x8c\xb6\x01\x0c\x76\x99\xf6\xb1\x1d\xfe\x0e\x00\x00\xff\xff\xe8\x36\xb1\x2a\x35\x0b\x00\x00"
// views_stats_html returns raw, uncompressed file data.
func views_stats_html() []byte {
gz, err := gzip.NewReader(bytes.NewBuffer([]byte{
0x1f,0x8b,0x08,0x00,0x00,0x09,0x6e,0x88,0x00,0xff,0xd4,0x56,
0x5b,0x8f,0xe3,0x34,0x14,0x7e,0xdf,0x5f,0x61,0x99,0x87,0x05,
0x29,0x37,0x27,0xbd,0x4c,0xaa,0xa6,0x12,0x0b,0xe2,0x89,0xe1,
0x81,0x95,0x90,0x78,0x42,0xae,0xe3,0x69,0xac,0x71,0x9d,0x60,
0xbb,0xcd,0xcc,0x96,0xfe,0x77,0x4e,0xe2,0xa4,0x4c,0x93,0x10,
0x2d,0x62,0x5f,0x76,0x2e,0x72,0xce,0xed,0x3b,0x9f,0x4f,0x3f,
0x3b,0xdd,0xe6,0xe2,0x8c,0x98,0xa4,0xc6,0x64,0x98,0x5b,0x96,
0xfb,0xac,0x54,0x96,0x0a,0xc5,0x35,0x6a,0x4d,0x63,0xa9,0x35,
0xe8,0x72,0x61,0xa5,0x3c,0x1d,0x95,0xb9,0x5e,0xe1,0xd9,0xd2,
0xbd,0xe4,0xbf,0x09,0x23,0xf6,0x42,0x0a,0xfb,0x7a,0xbd,0xe2,
0xdd,0x3b,0x04,0x3f,0x23,0xac,0x7d,0x99,0xbf,0x76,0xb1,0xc9,
0xf8,0x53,0xa9,0x8f,0xd4,0xfa,0x86,0x4b,0xce,0x6c,0xa9,0xdf,
0xa4,0x4e,0xa6,0xf7,0x79,0xbe,0xb0,0xfc,0x88,0xee,0x5d,0x07,
0x4d,0xab,0x02,0x23,0x75,0xf0,0x99,0x14,0xec,0x39,0xc3,0xa6,
0x28,0xeb,0x3f,0x5a,0xef,0xb7,0xdf,0x0d,0x80,0x5b,0x70,0x73,
0x3e,0xa0,0x33,0xd7,0x46,0x94,0x2a,0xc3,0x24,0x20,0x18,0xbd,
0x1c,0xa5,0x82,0x56,0x85,0xb5,0xd5,0x26,0x0c,0xeb,0xba,0x0e,
0xea,0x24,0x28,0xf5,0x21,0x8c,0xa3,0x28,0x0a,0x21,0xbf,0x4b,
0xd9,0xbc,0x48,0xa1,0x9e,0xa7,0x12,0x49,0x9a,0xa6,0x61,0x1b,
0x85,0xd4,0x0c,0x47,0xd5,0x0b,0x46,0xaf,0x6e,0x1d,0x11,0xa8,
0x34,0x37,0x5c,0x9f,0xf9,0xf7,0xa6,0x82,0x1d,0xfc,0x4a,0xad,
0x28,0x33,0xfc,0xf2,0x28,0xd4,0xef,0xf0,0x8f,0xd1,0x59,0xf0,
0xfa,0x43,0xd9,0x80,0xa0,0x08,0xad,0x9a,0xbf,0x20,0x8a,0xd6,
0x18,0x71,0xd5,0x0c,0xdf,0xdf,0x53,0xf6,0x7c,0xd0,0xe5,0x49,
0xe5,0x19,0x56,0xbc,0x46,0x83,0x2c,0xe0,0xb9,0x31,0x15,0x65,
0x3c,0xc3,0x7d,0x9f,0xa9,0x19,0x54,0xd4,0x16,0xe8,0x49,0x48,
0x99,0xe1,0x6f,0xc8,0x8f,0xf0,0xfb,0x01,0x23,0x00,0x7c,0x4c,
0x22,0x1f,0x80,0x52,0xc2,0x7c,0xb2,0x0a,0x56,0x31,0xf1,0x22,
0x3f,0x69,0x1c,0x0b,0x8f,0x24,0xc1,0x62,0xbd,0xe8,0x2d,0xb7,
0xb0,0xc8,0xeb,0xd2,0x5c,0xd4,0xbb,0x8b,0x76,0xcb,0xa8,0xb9,
0x71,0x7e,0xff,0xae,0xa6,0x03,0xfe,0x61,0x75,0xeb,0x96,0x3c,
0x24,0xde,0xa2,0x45,0x77,0x94,0xbc,0x9e,0xdb,0x27,0x04,0x34,
0xbd,0xc5,0x3a,0x88,0x93,0x94,0xf9,0x69,0xb0,0x24,0x29,0xd0,
0x24,0x8d,0xbd,0xf4,0xd7,0xc1,0x9a,0xac,0x7a,0xc3,0x2d,0x23,
0x02,0x1f,0xe3,0x28,0x58,0x3c,0x00,0xe9,0x38,0x58,0xaf,0x1e,
0x00,0xb7,0x7b,0x62,0x1d,0x96,0xe7,0xea,0xbc,0x16,0xab,0x37,
0xdc,0xf2,0x31,0x71,0x39,0xae,0xbb,0x77,0xe3,0xf1,0x09,0x87,
0x13,0x53,0x6e,0xa4,0xb3,0x7b,0x77,0x2f,0xed,0x10,0xb4,0xfd,
0x3f,0xd4,0xde,0x9e,0xc0,0x91,0xda,0x5b,0xef,0xd7,0xaf,0xf6,
0x65,0x1a,0x2c,0xd2,0x55,0xb3,0x2c,0xa3,0x64,0x56,0xf1,0x83,
0xcc,0xcf,0x54,0xbd,0x06,0x06,0x2d,0x61,0x47,0x17,0x0f,0x4f,
0x40,0x2d,0x72,0x5b,0x64,0xd8,0x81,0x63,0x54,0x70,0x71,0x28,
0x2c,0x4c,0x2d,0x09,0x08,0x21,0x93,0x9f,0xf0,0x3d,0x62,0x0c,
0x89,0x4d,0xe1,0x17,0x86,0x85,0x53,0x90,0xa4,0xf1,0x17,0x80,
0x75,0x7a,0x9c,0x91,0xe3,0xd0,0x1c,0x2a,0xd3,0xdd,0xb5,0x03,
0x88,0x22,0xde,0xfd,0x54,0x4a,0x59,0xd6,0xf0,0xe6,0xf8,0x99,
0x5a,0xae,0xd8,0xeb,0x36,0x04,0xe7,0xbc,0xc8,0x5b,0xa8,0x7f,
0xde,0x39,0x18,0x09,0xf8,0x78,0xa5,0x2b,0x1f,0x76,0xf8,0x6f,
0x24,0xa5,0x30,0x76,0x96,0x23,0xc4,0x27,0x08,0xb6,0x67,0x08,
0x31,0x2e,0x65,0x45,0xf3,0x5c,0xa8,0x43,0x3b,0xfe,0xc6,0x6e,
0x94,0xd5,0xd9,0xc3,0x9a,0x82,0xd3,0x7c,0x62,0xd0,0x36,0xbf,
0x63,0xa4,0xe8,0x91,0xfb,0x4d,0x2a,0xec,0x73,0xf7,0x48,0x59,
0x01,0x3b,0x46,0xbf,0x80,0x73,0x1b,0xda,0xcf,0x28,0xbf,0x4d,
0xe5,0x36,0xdd,0x61,0x15,0x78,0xc6,0x4c,0xb6,0xb6,0x79,0x0b,
0x4f,0xc1,0xeb,0xe6,0xfe,0xd0,0xbc,0xe2,0x14,0xf4,0xf2,0xd4,
0xcf,0x45,0x28,0xd4,0x3f,0x9b,0x89,0xd3,0xd3,0x31,0xdb,0x5d,
0x2e,0x7d,0x56,0xd0,0xec,0xeb,0x7a,0x9d,0xde,0x44,0x9f,0x3e,
0x19,0x68,0x83,0xa3,0x6b,0xef,0xcf,0x13,0xd5,0xdc,0x5d,0x6d,
0xe0,0xdd,0xa0,0xcb,0xfb,0x37,0x7e,0x10,0x0c,0xe7,0xea,0xfd,
0xe6,0xc6,0x31,0xe8,0xc6,0x12,0xb0,0x93,0xd6,0x5c,0x59,0xb4,
0x45,0xb1,0x87,0xee,0x4a,0x4a,0x4d,0xd5,0x81,0xcf,0xd7,0x2c,
0x07,0x35,0x9a,0xe7,0x73,0x05,0xbb,0x0c,0x91,0x08,0xbe,0xf6,
0x4c,0xdc,0xe3,0xb3,0x9b,0xeb,0x70,0xfc,0x33,0x95,0x27,0xb8,
0x9b,0xde,0x0c,0x71,0xd8,0xe1,0x2f,0xa4,0x4e,0xc7,0x3d,0xd7,
0x1b,0x82,0xe0,0x3b,0xd7,0xd1,0xcc,0x74,0xfa,0x17,0xf9,0x84,
0x56,0x8f,0xe4,0x31,0x96,0x02,0x38,0x1b,0xc5,0x4f,0x9e,0xab,
0xee,0xb1,0x5b,0xfe,0x0e,0x00,0x00,0xff,0xff,0xf4,0x18,0x69,
0x45,0x2a,0x0a,0x00,0x00,
}))
var empty [0]byte
sx := (*reflect.StringHeader)(unsafe.Pointer(&_views_stats_html))
b := empty[:]
bx := (*reflect.SliceHeader)(unsafe.Pointer(&b))
bx.Data = sx.Data
bx.Len = len(_views_stats_html)
bx.Cap = bx.Len
gz, err := gzip.NewReader(bytes.NewBuffer(b))
if err != nil {
panic("Decompression failed: " + err.Error())
}
var b bytes.Buffer
io.Copy(&b, gz)
var buf bytes.Buffer
io.Copy(&buf, gz)
gz.Close()
return b.Bytes()
return buf.Bytes()
}
func init() {
go_bindata["/views/stats.html"] = views_stats_html
}

View File

@ -3,13 +3,16 @@ package mod
import (
"net/http"
"github.com/coreos/etcd/mod/dashboard"
"github.com/gorilla/mux"
)
var ServeMux *http.Handler
func init() {
// TODO: Use a Gorilla mux to handle this in 0.2 and remove the strip
handler := http.StripPrefix("/etcd/mod/dashboard/", dashboard.HttpHandler())
ServeMux = &handler
func HttpHandler() (handler http.Handler) {
modMux := mux.NewRouter()
modMux.PathPrefix("/dashboard/").
Handler(http.StripPrefix("/dashboard/", dashboard.HttpHandler()))
return modMux
}

View File

@ -1,86 +0,0 @@
/*
Copyright 2013 CoreOS Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"net/url"
"path"
)
// we map node name to url
type nodeInfo struct {
raftVersion string
raftURL string
etcdURL string
}
var namesMap = make(map[string]*nodeInfo)
// nameToEtcdURL maps node name to its etcd http address
func nameToEtcdURL(name string) (string, bool) {
if info, ok := namesMap[name]; ok {
// first try to read from the map
return info.etcdURL, true
}
// if fails, try to recover from etcd storage
return readURL(name, "etcd")
}
// nameToRaftURL maps node name to its raft http address
func nameToRaftURL(name string) (string, bool) {
if info, ok := namesMap[name]; ok {
// first try to read from the map
return info.raftURL, true
}
// if fails, try to recover from etcd storage
return readURL(name, "raft")
}
// addNameToURL add a name that maps to raftURL and etcdURL
func addNameToURL(name string, version string, raftURL string, etcdURL string) {
namesMap[name] = &nodeInfo{
raftVersion: raftVersion,
raftURL: raftURL,
etcdURL: etcdURL,
}
}
func readURL(nodeName string, urlName string) (string, bool) {
// if fails, try to recover from etcd storage
key := path.Join("/_etcd/machines", nodeName)
resps, err := etcdStore.RawGet(key)
if err != nil {
return "", false
}
m, err := url.ParseQuery(resps[0].Value)
if err != nil {
panic("Failed to parse machines entry")
}
url := m[urlName][0]
return url, true
}

View File

@ -1,203 +0,0 @@
/*
Copyright 2013 CoreOS Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"encoding/json"
"net/http"
"github.com/coreos/go-raft"
)
//-------------------------------------------------------------
// Handlers to handle raft related request via raft server port
//-------------------------------------------------------------
// Get all the current logs
func GetLogHttpHandler(w http.ResponseWriter, req *http.Request) {
debugf("[recv] GET %s/log", r.url)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(r.LogEntries())
}
// Response to vote request
func VoteHttpHandler(w http.ResponseWriter, req *http.Request) {
rvreq := &raft.RequestVoteRequest{}
if _, err := rvreq.Decode(req.Body); err != nil {
http.Error(w, "", http.StatusBadRequest)
warnf("[recv] BADREQUEST %s/vote [%v]", r.url, err)
return
}
debugf("[recv] POST %s/vote [%s]", r.url, rvreq.CandidateName)
resp := r.RequestVote(rvreq)
if resp == nil {
warn("[vote] Error: nil response")
http.Error(w, "", http.StatusInternalServerError)
return
}
if _, err := resp.Encode(w); err != nil {
warn("[vote] Error: %v", err)
http.Error(w, "", http.StatusInternalServerError)
return
}
}
// Response to append entries request
func AppendEntriesHttpHandler(w http.ResponseWriter, req *http.Request) {
aereq := &raft.AppendEntriesRequest{}
if _, err := aereq.Decode(req.Body); err != nil {
http.Error(w, "", http.StatusBadRequest)
warnf("[recv] BADREQUEST %s/log/append [%v]", r.url, err)
return
}
debugf("[recv] POST %s/log/append [%d]", r.url, len(aereq.Entries))
r.serverStats.RecvAppendReq(aereq.LeaderName, int(req.ContentLength))
resp := r.AppendEntries(aereq)
if resp == nil {
warn("[ae] Error: nil response")
http.Error(w, "", http.StatusInternalServerError)
return
}
if !resp.Success {
debugf("[Append Entry] Step back")
}
if _, err := resp.Encode(w); err != nil {
warn("[ae] Error: %v", err)
http.Error(w, "", http.StatusInternalServerError)
return
}
}
// Response to recover from snapshot request
func SnapshotHttpHandler(w http.ResponseWriter, req *http.Request) {
ssreq := &raft.SnapshotRequest{}
if _, err := ssreq.Decode(req.Body); err != nil {
http.Error(w, "", http.StatusBadRequest)
warnf("[recv] BADREQUEST %s/snapshot [%v]", r.url, err)
return
}
debugf("[recv] POST %s/snapshot", r.url)
resp := r.RequestSnapshot(ssreq)
if resp == nil {
warn("[ss] Error: nil response")
http.Error(w, "", http.StatusInternalServerError)
return
}
if _, err := resp.Encode(w); err != nil {
warn("[ss] Error: %v", err)
http.Error(w, "", http.StatusInternalServerError)
return
}
}
// Response to recover from snapshot request
func SnapshotRecoveryHttpHandler(w http.ResponseWriter, req *http.Request) {
ssrreq := &raft.SnapshotRecoveryRequest{}
if _, err := ssrreq.Decode(req.Body); err != nil {
http.Error(w, "", http.StatusBadRequest)
warnf("[recv] BADREQUEST %s/snapshotRecovery [%v]", r.url, err)
return
}
debugf("[recv] POST %s/snapshotRecovery", r.url)
resp := r.SnapshotRecoveryRequest(ssrreq)
if resp == nil {
warn("[ssr] Error: nil response")
http.Error(w, "", http.StatusInternalServerError)
return
}
if _, err := resp.Encode(w); err != nil {
warn("[ssr] Error: %v", err)
http.Error(w, "", http.StatusInternalServerError)
return
}
}
// Get the port that listening for etcd connecting of the server
func EtcdURLHttpHandler(w http.ResponseWriter, req *http.Request) {
debugf("[recv] Get %s/etcdURL/ ", r.url)
w.WriteHeader(http.StatusOK)
w.Write([]byte(argInfo.EtcdURL))
}
// Response to the join request
func JoinHttpHandler(w http.ResponseWriter, req *http.Request) error {
command := &JoinCommand{}
if err := decodeJsonRequest(req, command); err == nil {
debugf("Receive Join Request from %s", command.Name)
return dispatch(command, w, req, false)
} else {
w.WriteHeader(http.StatusInternalServerError)
return nil
}
}
// Response to remove request
func RemoveHttpHandler(w http.ResponseWriter, req *http.Request) {
if req.Method != "DELETE" {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
nodeName := req.URL.Path[len("/remove/"):]
command := &RemoveCommand{
Name: nodeName,
}
debugf("[recv] Remove Request [%s]", command.Name)
dispatch(command, w, req, false)
}
// Response to the name request
func NameHttpHandler(w http.ResponseWriter, req *http.Request) {
debugf("[recv] Get %s/name/ ", r.url)
w.WriteHeader(http.StatusOK)
w.Write([]byte(r.name))
}
// Response to the name request
func RaftVersionHttpHandler(w http.ResponseWriter, req *http.Request) {
debugf("[recv] Get %s/version/ ", r.url)
w.WriteHeader(http.StatusOK)
w.Write([]byte(r.version))
}

View File

@ -1,341 +0,0 @@
/*
Copyright 2013 CoreOS Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"bytes"
"crypto/tls"
"encoding/binary"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"time"
etcdErr "github.com/coreos/etcd/error"
"github.com/coreos/go-raft"
)
type raftServer struct {
*raft.Server
version string
joinIndex uint64
name string
url string
listenHost string
tlsConf *TLSConfig
tlsInfo *TLSInfo
followersStats *raftFollowersStats
serverStats *raftServerStats
}
var r *raftServer
func newRaftServer(name string, url string, listenHost string, tlsConf *TLSConfig, tlsInfo *TLSInfo) *raftServer {
// Create transporter for raft
raftTransporter := newTransporter(tlsConf.Scheme, tlsConf.Client)
// Create raft server
server, err := raft.NewServer(name, dirPath, raftTransporter, etcdStore, nil, "")
check(err)
return &raftServer{
Server: server,
version: raftVersion,
name: name,
url: url,
listenHost: listenHost,
tlsConf: tlsConf,
tlsInfo: tlsInfo,
followersStats: &raftFollowersStats{
Leader: name,
Followers: make(map[string]*raftFollowerStats),
},
serverStats: &raftServerStats{
StartTime: time.Now(),
sendRateQueue: &statsQueue{
back: -1,
},
recvRateQueue: &statsQueue{
back: -1,
},
},
}
}
// Start the raft server
func (r *raftServer) ListenAndServe() {
// Setup commands.
registerCommands()
// LoadSnapshot
if snapshot {
err := r.LoadSnapshot()
if err == nil {
debugf("%s finished load snapshot", r.name)
} else {
debug(err)
}
}
r.SetElectionTimeout(ElectionTimeout)
r.SetHeartbeatTimeout(HeartbeatTimeout)
r.Start()
if r.IsLogEmpty() {
// start as a leader in a new cluster
if len(cluster) == 0 {
startAsLeader()
} else {
startAsFollower()
}
} else {
// rejoin the previous cluster
cluster = getMachines(nameToRaftURL)
for i := 0; i < len(cluster); i++ {
u, err := url.Parse(cluster[i])
if err != nil {
debug("rejoin cannot parse url: ", err)
}
cluster[i] = u.Host
}
ok := joinCluster(cluster)
if !ok {
warn("the entire cluster is down! this machine will restart the cluster.")
}
debugf("%s restart as a follower", r.name)
}
// open the snapshot
if snapshot {
go monitorSnapshot()
}
// start to response to raft requests
go r.startTransport(r.tlsConf.Scheme, r.tlsConf.Server)
}
func startAsLeader() {
// leader need to join self as a peer
for {
_, err := r.Do(newJoinCommand())
if err == nil {
break
}
}
debugf("%s start as a leader", r.name)
}
func startAsFollower() {
// start as a follower in a existing cluster
for i := 0; i < retryTimes; i++ {
ok := joinCluster(cluster)
if ok {
return
}
warnf("cannot join to cluster via given machines, retry in %d seconds", RetryInterval)
time.Sleep(time.Second * RetryInterval)
}
fatalf("Cannot join the cluster via given machines after %x retries", retryTimes)
}
// Start to listen and response raft command
func (r *raftServer) startTransport(scheme string, tlsConf tls.Config) {
infof("raft server [name %s, listen on %s, advertised url %s]", r.name, r.listenHost, r.url)
raftMux := http.NewServeMux()
server := &http.Server{
Handler: raftMux,
TLSConfig: &tlsConf,
Addr: r.listenHost,
}
// internal commands
raftMux.HandleFunc("/name", NameHttpHandler)
raftMux.HandleFunc("/version", RaftVersionHttpHandler)
raftMux.Handle("/join", errorHandler(JoinHttpHandler))
raftMux.HandleFunc("/remove/", RemoveHttpHandler)
raftMux.HandleFunc("/vote", VoteHttpHandler)
raftMux.HandleFunc("/log", GetLogHttpHandler)
raftMux.HandleFunc("/log/append", AppendEntriesHttpHandler)
raftMux.HandleFunc("/snapshot", SnapshotHttpHandler)
raftMux.HandleFunc("/snapshotRecovery", SnapshotRecoveryHttpHandler)
raftMux.HandleFunc("/etcdURL", EtcdURLHttpHandler)
if scheme == "http" {
fatal(server.ListenAndServe())
} else {
fatal(server.ListenAndServeTLS(r.tlsInfo.CertFile, r.tlsInfo.KeyFile))
}
}
// getVersion fetches the raft version of a peer. This works for now but we
// will need to do something more sophisticated later when we allow mixed
// version clusters.
func getVersion(t *transporter, versionURL url.URL) (string, error) {
resp, req, err := t.Get(versionURL.String())
if err != nil {
return "", err
}
defer resp.Body.Close()
t.CancelWhenTimeout(req)
body, err := ioutil.ReadAll(resp.Body)
return string(body), nil
}
func joinCluster(cluster []string) bool {
for _, machine := range cluster {
if len(machine) == 0 {
continue
}
err := joinByMachine(r.Server, machine, r.tlsConf.Scheme)
if err == nil {
debugf("%s success join to the cluster via machine %s", r.name, machine)
return true
} else {
if _, ok := err.(etcdErr.Error); ok {
fatal(err)
}
debugf("cannot join to cluster via machine %s %s", machine, err)
}
}
return false
}
// Send join requests to machine.
func joinByMachine(s *raft.Server, machine string, scheme string) error {
var b bytes.Buffer
// t must be ok
t, _ := r.Transporter().(*transporter)
// Our version must match the leaders version
versionURL := url.URL{Host: machine, Scheme: scheme, Path: "/version"}
version, err := getVersion(t, versionURL)
if err != nil {
return fmt.Errorf("Unable to join: %v", err)
}
// TODO: versioning of the internal protocol. See:
// Documentation/internatl-protocol-versioning.md
if version != r.version {
return fmt.Errorf("Unable to join: internal version mismatch, entire cluster must be running identical versions of etcd")
}
json.NewEncoder(&b).Encode(newJoinCommand())
joinURL := url.URL{Host: machine, Scheme: scheme, Path: "/join"}
debugf("Send Join Request to %s", joinURL.String())
resp, req, err := t.Post(joinURL.String(), &b)
for {
if err != nil {
return fmt.Errorf("Unable to join: %v", err)
}
if resp != nil {
defer resp.Body.Close()
t.CancelWhenTimeout(req)
if resp.StatusCode == http.StatusOK {
b, _ := ioutil.ReadAll(resp.Body)
r.joinIndex, _ = binary.Uvarint(b)
return nil
}
if resp.StatusCode == http.StatusTemporaryRedirect {
address := resp.Header.Get("Location")
debugf("Send Join Request to %s", address)
json.NewEncoder(&b).Encode(newJoinCommand())
resp, req, err = t.Post(address, &b)
} else if resp.StatusCode == http.StatusBadRequest {
debug("Reach max number machines in the cluster")
decoder := json.NewDecoder(resp.Body)
err := &etcdErr.Error{}
decoder.Decode(err)
return *err
} else {
return fmt.Errorf("Unable to join")
}
}
}
return fmt.Errorf("Unable to join: %v", err)
}
func (r *raftServer) Stats() []byte {
r.serverStats.LeaderInfo.Uptime = time.Now().Sub(r.serverStats.LeaderInfo.startTime).String()
queue := r.serverStats.sendRateQueue
r.serverStats.SendingPkgRate, r.serverStats.SendingBandwidthRate = queue.Rate()
queue = r.serverStats.recvRateQueue
r.serverStats.RecvingPkgRate, r.serverStats.RecvingBandwidthRate = queue.Rate()
b, _ := json.Marshal(r.serverStats)
return b
}
func (r *raftServer) PeerStats() []byte {
if r.State() == raft.Leader {
b, _ := json.Marshal(r.followersStats)
return b
}
return nil
}
// Register commands to raft server
func registerCommands() {
raft.RegisterCommand(&JoinCommand{})
raft.RegisterCommand(&RemoveCommand{})
raft.RegisterCommand(&SetCommand{})
raft.RegisterCommand(&GetCommand{})
raft.RegisterCommand(&DeleteCommand{})
raft.RegisterCommand(&WatchCommand{})
raft.RegisterCommand(&TestAndSetCommand{})
}

View File

@ -1,225 +0,0 @@
/*
Copyright 2013 CoreOS Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"math"
"sync"
"time"
"github.com/coreos/go-raft"
)
const (
queueCapacity = 200
)
// packageStats represent the stats we need for a package.
// It has sending time and the size of the package.
type packageStats struct {
sendingTime time.Time
size int
}
// NewPackageStats creates a pacakgeStats and return the pointer to it.
func NewPackageStats(now time.Time, size int) *packageStats {
return &packageStats{
sendingTime: now,
size: size,
}
}
// Time return the sending time of the package.
func (ps *packageStats) Time() time.Time {
return ps.sendingTime
}
type raftServerStats struct {
State string `json:"state"`
StartTime time.Time `json:"startTime"`
LeaderInfo struct {
Name string `json:"leader"`
Uptime string `json:"uptime"`
startTime time.Time
} `json:"leaderInfo"`
RecvAppendRequestCnt uint64 `json:"recvAppendRequestCnt,"`
RecvingPkgRate float64 `json:"recvPkgRate,omitempty"`
RecvingBandwidthRate float64 `json:"recvBandwidthRate,omitempty"`
SendAppendRequestCnt uint64 `json:"sendAppendRequestCnt"`
SendingPkgRate float64 `json:"sendPkgRate,omitempty"`
SendingBandwidthRate float64 `json:"sendBandwidthRate,omitempty"`
sendRateQueue *statsQueue
recvRateQueue *statsQueue
}
func (ss *raftServerStats) RecvAppendReq(leaderName string, pkgSize int) {
ss.State = raft.Follower
if leaderName != ss.LeaderInfo.Name {
ss.LeaderInfo.Name = leaderName
ss.LeaderInfo.startTime = time.Now()
}
ss.recvRateQueue.Insert(NewPackageStats(time.Now(), pkgSize))
ss.RecvAppendRequestCnt++
}
func (ss *raftServerStats) SendAppendReq(pkgSize int) {
now := time.Now()
if ss.State != raft.Leader {
ss.State = raft.Leader
ss.LeaderInfo.Name = r.Name()
ss.LeaderInfo.startTime = now
}
ss.sendRateQueue.Insert(NewPackageStats(now, pkgSize))
ss.SendAppendRequestCnt++
}
type raftFollowersStats struct {
Leader string `json:"leader"`
Followers map[string]*raftFollowerStats `json:"followers"`
}
type raftFollowerStats struct {
Latency struct {
Current float64 `json:"current"`
Average float64 `json:"average"`
averageSquare float64
StandardDeviation float64 `json:"standardDeviation"`
Minimum float64 `json:"minimum"`
Maximum float64 `json:"maximum"`
} `json:"latency"`
Counts struct {
Fail uint64 `json:"fail"`
Success uint64 `json:"success"`
} `json:"counts"`
}
// Succ function update the raftFollowerStats with a successful send
func (ps *raftFollowerStats) Succ(d time.Duration) {
total := float64(ps.Counts.Success) * ps.Latency.Average
totalSquare := float64(ps.Counts.Success) * ps.Latency.averageSquare
ps.Counts.Success++
ps.Latency.Current = float64(d) / (1000000.0)
if ps.Latency.Current > ps.Latency.Maximum {
ps.Latency.Maximum = ps.Latency.Current
}
if ps.Latency.Current < ps.Latency.Minimum {
ps.Latency.Minimum = ps.Latency.Current
}
ps.Latency.Average = (total + ps.Latency.Current) / float64(ps.Counts.Success)
ps.Latency.averageSquare = (totalSquare + ps.Latency.Current*ps.Latency.Current) / float64(ps.Counts.Success)
// sdv = sqrt(avg(x^2) - avg(x)^2)
ps.Latency.StandardDeviation = math.Sqrt(ps.Latency.averageSquare - ps.Latency.Average*ps.Latency.Average)
}
// Fail function update the raftFollowerStats with a unsuccessful send
func (ps *raftFollowerStats) Fail() {
ps.Counts.Fail++
}
type statsQueue struct {
items [queueCapacity]*packageStats
size int
front int
back int
totalPkgSize int
rwl sync.RWMutex
}
func (q *statsQueue) Len() int {
return q.size
}
func (q *statsQueue) PkgSize() int {
return q.totalPkgSize
}
// FrontAndBack gets the front and back elements in the queue
// We must grab front and back together with the protection of the lock
func (q *statsQueue) frontAndBack() (*packageStats, *packageStats) {
q.rwl.RLock()
defer q.rwl.RUnlock()
if q.size != 0 {
return q.items[q.front], q.items[q.back]
}
return nil, nil
}
// Insert function insert a packageStats into the queue and update the records
func (q *statsQueue) Insert(p *packageStats) {
q.rwl.Lock()
defer q.rwl.Unlock()
q.back = (q.back + 1) % queueCapacity
if q.size == queueCapacity { //dequeue
q.totalPkgSize -= q.items[q.front].size
q.front = (q.back + 1) % queueCapacity
} else {
q.size++
}
q.items[q.back] = p
q.totalPkgSize += q.items[q.back].size
}
// Rate function returns the package rate and byte rate
func (q *statsQueue) Rate() (float64, float64) {
front, back := q.frontAndBack()
if front == nil || back == nil {
return 0, 0
}
if time.Now().Sub(back.Time()) > time.Second {
q.Clear()
return 0, 0
}
sampleDuration := back.Time().Sub(front.Time())
pr := float64(q.Len()) / float64(sampleDuration) * float64(time.Second)
br := float64(q.PkgSize()) / float64(sampleDuration) * float64(time.Second)
return pr, br
}
// Clear function clear up the statsQueue
func (q *statsQueue) Clear() {
q.rwl.Lock()
defer q.rwl.Unlock()
q.back = -1
q.front = 0
q.size = 0
q.totalPkgSize = 0
}

2
release_version.go Normal file
View File

@ -0,0 +1,2 @@
package main
const releaseVersion = "v0.1.2-33-g1a2a9d6"

View File

@ -3,6 +3,6 @@
VER=$(git describe --tags HEAD)
cat <<EOF
package main
const releaseVersion = "$VER"
package server
const ReleaseVersion = "$VER"
EOF

405
server/config.go Normal file
View File

@ -0,0 +1,405 @@
package server
import (
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"net"
"net/url"
"os"
"path/filepath"
"reflect"
"strconv"
"strings"
"github.com/BurntSushi/toml"
)
// The default location for the etcd configuration file.
const DefaultSystemConfigPath = "/etc/etcd/etcd.conf"
// Config represents the server configuration.
type Config struct {
SystemPath string
AdvertisedUrl string `toml:"advertised_url" env:"ETCD_ADVERTISED_URL"`
CAFile string `toml:"ca_file" env:"ETCD_CA_FILE"`
CertFile string `toml:"cert_file" env:"ETCD_CERT_FILE"`
Cors []string `toml:"cors" env:"ETCD_CORS"`
DataDir string `toml:"datadir" env:"ETCD_DATADIR"`
KeyFile string `toml:"key_file" env:"ETCD_KEY_FILE"`
ListenHost string `toml:"listen_host" env:"ETCD_LISTEN_HOST"`
Machines []string `toml:"machines" env:"ETCD_MACHINES"`
MachinesFile string `toml:"machines_file" env:"ETCD_MACHINES_FILE"`
MaxClusterSize int `toml:"max_cluster_size" env:"ETCD_MAX_CLUSTER_SIZE"`
MaxResultBuffer int `toml:"max_result_buffer" env:"ETCD_MAX_RESULT_BUFFER"`
MaxRetryAttempts int `toml:"max_retry_attempts" env:"ETCD_MAX_RETRY_ATTEMPTS"`
Name string `toml:"name" env:"ETCD_NAME"`
Snapshot bool `toml:"snapshot" env:"ETCD_SNAPSHOT"`
SnapCount int `toml:"snapshot_count" env:"ETCD_SNAPSHOTCOUNT"`
Verbose bool `toml:"verbose" env:"ETCD_VERBOSE"`
VeryVerbose bool `toml:"very_verbose" env:"ETCD_VERY_VERBOSE"`
WebURL string `toml:"web_url" env:"ETCD_WEB_URL"`
Peer struct {
AdvertisedUrl string `toml:"advertised_url" env:"ETCD_PEER_ADVERTISED_URL"`
CAFile string `toml:"ca_file" env:"ETCD_PEER_CA_FILE"`
CertFile string `toml:"cert_file" env:"ETCD_PEER_CERT_FILE"`
KeyFile string `toml:"key_file" env:"ETCD_PEER_KEY_FILE"`
ListenHost string `toml:"listen_host" env:"ETCD_PEER_LISTEN_HOST"`
}
}
// NewConfig returns a Config initialized with default values.
func NewConfig() *Config {
c := new(Config)
c.SystemPath = DefaultSystemConfigPath
c.AdvertisedUrl = "127.0.0.1:4001"
c.AdvertisedUrl = "127.0.0.1:4001"
c.MaxClusterSize = 9
c.MaxResultBuffer = 1024
c.MaxRetryAttempts = 3
c.Peer.AdvertisedUrl = "127.0.0.1:7001"
c.SnapCount = 10000
return c
}
// Loads the configuration from the system config, command line config,
// environment variables, and finally command line arguments.
func (c *Config) Load(arguments []string) error {
var path string
f := flag.NewFlagSet("etcd", -1)
f.SetOutput(ioutil.Discard)
f.StringVar(&path, "config", "", "path to config file")
f.Parse(arguments)
// Load from system file.
if err := c.LoadSystemFile(); err != nil {
return err
}
// Load from config file specified in arguments.
if path != "" {
if err := c.LoadFile(path); err != nil {
return err
}
}
// Load from the environment variables next.
if err := c.LoadEnv(); err != nil {
return err
}
// Load from command line flags.
if err := c.LoadFlags(arguments); err != nil {
return err
}
// Loads machines if a machine file was specified.
if err := c.LoadMachineFile(); err != nil {
return err
}
// Sanitize all the input fields.
if err := c.Sanitize(); err != nil {
return fmt.Errorf("sanitize:", err)
}
return nil
}
// Loads from the system etcd configuration file if it exists.
func (c *Config) LoadSystemFile() error {
if _, err := os.Stat(c.SystemPath); os.IsNotExist(err) {
return nil
}
return c.LoadFile(c.SystemPath)
}
// Loads configuration from a file.
func (c *Config) LoadFile(path string) error {
_, err := toml.DecodeFile(path, &c)
return err
}
// LoadEnv loads the configuration via environment variables.
func (c *Config) LoadEnv() error {
if err := c.loadEnv(c); err != nil {
return err
}
if err := c.loadEnv(&c.Peer); err != nil {
return err
}
return nil
}
func (c *Config) loadEnv(target interface{}) error {
value := reflect.Indirect(reflect.ValueOf(target))
typ := value.Type()
for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i)
// Retrieve environment variable.
v := strings.TrimSpace(os.Getenv(field.Tag.Get("env")))
if v == "" {
continue
}
// Set the appropriate type.
switch field.Type.Kind() {
case reflect.Bool:
value.Field(i).SetBool(v != "0" && v != "false")
case reflect.Int:
newValue, err := strconv.ParseInt(v, 10, 0)
if err != nil {
return fmt.Errorf("Parse error: %s: %s", field.Tag.Get("env"), err)
}
value.Field(i).SetInt(newValue)
case reflect.String:
value.Field(i).SetString(v)
case reflect.Slice:
value.Field(i).Set(reflect.ValueOf(trimsplit(v, ",")))
}
}
return nil
}
// Loads configuration from command line flags.
func (c *Config) LoadFlags(arguments []string) error {
var machines, cors string
var force bool
f := flag.NewFlagSet(os.Args[0], flag.ContinueOnError)
f.BoolVar(&force, "f", false, "force new node configuration if existing is found (WARNING: data loss!)")
f.BoolVar(&c.Verbose, "v", c.Verbose, "verbose logging")
f.BoolVar(&c.VeryVerbose, "vv", c.Verbose, "very verbose logging")
f.StringVar(&machines, "C", "", "the ip address and port of a existing machines in the cluster, sepearate by comma")
f.StringVar(&c.MachinesFile, "CF", c.MachinesFile, "the file contains a list of existing machines in the cluster, seperate by comma")
f.StringVar(&c.Name, "n", c.Name, "the node name (required)")
f.StringVar(&c.AdvertisedUrl, "c", c.AdvertisedUrl, "the advertised public hostname:port for etcd client communication")
f.StringVar(&c.Peer.AdvertisedUrl, "s", c.Peer.AdvertisedUrl, "the advertised public hostname:port for raft server communication")
f.StringVar(&c.ListenHost, "cl", c.ListenHost, "the listening hostname for etcd client communication (defaults to advertised ip)")
f.StringVar(&c.Peer.ListenHost, "sl", c.Peer.ListenHost, "the listening hostname for raft server communication (defaults to advertised ip)")
f.StringVar(&c.WebURL, "w", c.WebURL, "the hostname:port of web interface")
f.StringVar(&c.Peer.CAFile, "serverCAFile", c.Peer.CAFile, "the path of the CAFile")
f.StringVar(&c.Peer.CertFile, "serverCert", c.Peer.CertFile, "the cert file of the server")
f.StringVar(&c.Peer.KeyFile, "serverKey", c.Peer.KeyFile, "the key file of the server")
f.StringVar(&c.CAFile, "clientCAFile", c.CAFile, "the path of the client CAFile")
f.StringVar(&c.CertFile, "clientCert", c.CertFile, "the cert file of the client")
f.StringVar(&c.KeyFile, "clientKey", c.KeyFile, "the key file of the client")
f.StringVar(&c.DataDir, "d", c.DataDir, "the directory to store log and snapshot")
f.IntVar(&c.MaxResultBuffer, "m", c.MaxResultBuffer, "the max size of result buffer")
f.IntVar(&c.MaxRetryAttempts, "r", c.MaxRetryAttempts, "the max retry attempts when trying to join a cluster")
f.IntVar(&c.MaxClusterSize, "maxsize", c.MaxClusterSize, "the max size of the cluster")
f.StringVar(&cors, "cors", "", "whitelist origins for cross-origin resource sharing (e.g. '*' or 'http://localhost:8001,etc')")
f.BoolVar(&c.Snapshot, "snapshot", c.Snapshot, "open or close snapshot")
f.IntVar(&c.SnapCount, "snapshotCount", c.SnapCount, "save the in memory logs and states to a snapshot file after snapCount transactions")
// These flags are ignored since they were already parsed.
var path string
f.StringVar(&path, "config", "", "path to config file")
f.Parse(arguments)
// Convert some parameters to lists.
if machines != "" {
c.Machines = trimsplit(machines, ",")
}
if cors != "" {
c.Cors = trimsplit(cors, ",")
}
// Force remove server configuration if specified.
if force {
c.Reset()
}
return nil
}
// LoadMachineFile loads the machines listed in the machine file.
func (c *Config) LoadMachineFile() error {
if c.MachinesFile == "" {
return nil
}
b, err := ioutil.ReadFile(c.MachinesFile)
if err != nil {
return fmt.Errorf("Machines file error: %s", err)
}
c.Machines = trimsplit(string(b), ",")
return nil
}
// Reset removes all server configuration files.
func (c *Config) Reset() error {
if err := os.RemoveAll(filepath.Join(c.DataDir, "info")); err != nil {
return err
}
if err := os.RemoveAll(filepath.Join(c.DataDir, "log")); err != nil {
return err
}
if err := os.RemoveAll(filepath.Join(c.DataDir, "conf")); err != nil {
return err
}
if err := os.RemoveAll(filepath.Join(c.DataDir, "snapshot")); err != nil {
return err
}
return nil
}
// Reads the info file from the file system or initializes it based on the config.
func (c *Config) Info() (*Info, error) {
info := &Info{}
path := filepath.Join(c.DataDir, "info")
// Open info file and read it out.
f, err := os.Open(path)
if err != nil && !os.IsNotExist(err) {
return nil, err
} else if f != nil {
defer f.Close()
if err := json.NewDecoder(f).Decode(&info); err != nil {
return nil, err
}
return info, nil
}
// If the file doesn't exist then initialize it.
info.Name = strings.TrimSpace(c.Name)
info.EtcdURL = c.AdvertisedUrl
info.EtcdListenHost = c.ListenHost
info.RaftURL = c.Peer.AdvertisedUrl
info.RaftListenHost = c.Peer.ListenHost
info.WebURL = c.WebURL
info.EtcdTLS = c.TLSInfo()
info.RaftTLS = c.PeerTLSInfo()
// Write to file.
f, err = os.Create(path)
if err != nil {
return nil, err
}
defer f.Close()
if err := json.NewEncoder(f).Encode(info); err != nil {
return nil, err
}
return info, nil
}
// Sanitize cleans the input fields.
func (c *Config) Sanitize() error {
tlsConfig, err := c.TLSConfig()
if err != nil {
return err
}
peerTlsConfig, err := c.PeerTLSConfig()
if err != nil {
return err
}
// Sanitize the URLs first.
if c.AdvertisedUrl, err = sanitizeURL(c.AdvertisedUrl, tlsConfig.Scheme); err != nil {
return fmt.Errorf("Advertised URL: %s", err)
}
if c.ListenHost, err = sanitizeListenHost(c.ListenHost, c.AdvertisedUrl); err != nil {
return fmt.Errorf("Listen Host: %s", err)
}
if c.WebURL, err = sanitizeURL(c.WebURL, "http"); err != nil {
return fmt.Errorf("Web URL: %s", err)
}
if c.Peer.AdvertisedUrl, err = sanitizeURL(c.Peer.AdvertisedUrl, peerTlsConfig.Scheme); err != nil {
return fmt.Errorf("Peer Advertised URL: %s", err)
}
if c.Peer.ListenHost, err = sanitizeListenHost(c.Peer.ListenHost, c.Peer.AdvertisedUrl); err != nil {
return fmt.Errorf("Peer Listen Host: %s", err)
}
return nil
}
// TLSInfo retrieves a TLSInfo object for the client server.
func (c *Config) TLSInfo() TLSInfo {
return TLSInfo{
CAFile: c.CAFile,
CertFile: c.CertFile,
KeyFile: c.KeyFile,
}
}
// ClientTLSConfig generates the TLS configuration for the client server.
func (c *Config) TLSConfig() (TLSConfig, error) {
return c.TLSInfo().Config()
}
// PeerTLSInfo retrieves a TLSInfo object for the peer server.
func (c *Config) PeerTLSInfo() TLSInfo {
return TLSInfo{
CAFile: c.Peer.CAFile,
CertFile: c.Peer.CertFile,
KeyFile: c.Peer.KeyFile,
}
}
// PeerTLSConfig generates the TLS configuration for the peer server.
func (c *Config) PeerTLSConfig() (TLSConfig, error) {
return c.PeerTLSInfo().Config()
}
// sanitizeURL will cleanup a host string in the format hostname:port and
// attach a schema.
func sanitizeURL(host string, defaultScheme string) (string, error) {
// Blank URLs are fine input, just return it
if len(host) == 0 {
return host, nil
}
p, err := url.Parse(host)
if err != nil {
return "", err
}
// Make sure the host is in Host:Port format
_, _, err = net.SplitHostPort(host)
if err != nil {
return "", err
}
p = &url.URL{Host: host, Scheme: defaultScheme}
return p.String(), nil
}
// sanitizeListenHost cleans up the ListenHost parameter and appends a port
// if necessary based on the advertised port.
func sanitizeListenHost(listen string, advertised string) (string, error) {
aurl, err := url.Parse(advertised)
if err != nil {
return "", err
}
ahost, aport, err := net.SplitHostPort(aurl.Host)
if err != nil {
return "", err
}
// If the listen host isn't set use the advertised host
if listen == "" {
listen = ahost
}
return net.JoinHostPort(listen, aport), nil
}

479
server/config_test.go Normal file
View File

@ -0,0 +1,479 @@
package server
import (
"io/ioutil"
"os"
"testing"
"github.com/BurntSushi/toml"
"github.com/stretchr/testify/assert"
)
// Ensures that a configuration can be deserialized from TOML.
func TestConfigTOML(t *testing.T) {
content := `
advertised_url = "127.0.0.1:4002"
ca_file = "/tmp/file.ca"
cert_file = "/tmp/file.cert"
cors = ["*"]
cpu_profile_file = "XXX"
datadir = "/tmp/data"
key_file = "/tmp/file.key"
listen_host = "127.0.0.1:4003"
machines = ["coreos.com:4001", "coreos.com:4002"]
machines_file = "/tmp/machines"
max_cluster_size = 10
max_result_buffer = 512
max_retry_attempts = 5
name = "test-name"
snapshot = true
verbose = true
very_verbose = true
web_url = "/web"
[peer]
advertised_url = "127.0.0.1:7002"
ca_file = "/tmp/peer/file.ca"
cert_file = "/tmp/peer/file.cert"
key_file = "/tmp/peer/file.key"
listen_host = "127.0.0.1:7003"
`
c := NewConfig()
_, err := toml.Decode(content, &c)
assert.Nil(t, err, "")
assert.Equal(t, c.AdvertisedUrl, "127.0.0.1:4002", "")
assert.Equal(t, c.CAFile, "/tmp/file.ca", "")
assert.Equal(t, c.CertFile, "/tmp/file.cert", "")
assert.Equal(t, c.Cors, []string{"*"}, "")
assert.Equal(t, c.DataDir, "/tmp/data", "")
assert.Equal(t, c.KeyFile, "/tmp/file.key", "")
assert.Equal(t, c.ListenHost, "127.0.0.1:4003", "")
assert.Equal(t, c.Machines, []string{"coreos.com:4001", "coreos.com:4002"}, "")
assert.Equal(t, c.MachinesFile, "/tmp/machines", "")
assert.Equal(t, c.MaxClusterSize, 10, "")
assert.Equal(t, c.MaxResultBuffer, 512, "")
assert.Equal(t, c.MaxRetryAttempts, 5, "")
assert.Equal(t, c.Name, "test-name", "")
assert.Equal(t, c.Snapshot, true, "")
assert.Equal(t, c.Verbose, true, "")
assert.Equal(t, c.VeryVerbose, true, "")
assert.Equal(t, c.WebURL, "/web", "")
assert.Equal(t, c.Peer.AdvertisedUrl, "127.0.0.1:7002", "")
assert.Equal(t, c.Peer.CAFile, "/tmp/peer/file.ca", "")
assert.Equal(t, c.Peer.CertFile, "/tmp/peer/file.cert", "")
assert.Equal(t, c.Peer.KeyFile, "/tmp/peer/file.key", "")
assert.Equal(t, c.Peer.ListenHost, "127.0.0.1:7003", "")
}
// Ensures that a configuration can be retrieved from environment variables.
func TestConfigEnv(t *testing.T) {
os.Setenv("ETCD_CA_FILE", "/tmp/file.ca")
os.Setenv("ETCD_CERT_FILE", "/tmp/file.cert")
os.Setenv("ETCD_CPU_PROFILE_FILE", "XXX")
os.Setenv("ETCD_CORS", "localhost:4001,localhost:4002")
os.Setenv("ETCD_DATADIR", "/tmp/data")
os.Setenv("ETCD_KEY_FILE", "/tmp/file.key")
os.Setenv("ETCD_LISTEN_HOST", "127.0.0.1:4003")
os.Setenv("ETCD_MACHINES", "coreos.com:4001,coreos.com:4002")
os.Setenv("ETCD_MACHINES_FILE", "/tmp/machines")
os.Setenv("ETCD_MAX_CLUSTER_SIZE", "10")
os.Setenv("ETCD_MAX_RESULT_BUFFER", "512")
os.Setenv("ETCD_MAX_RETRY_ATTEMPTS", "5")
os.Setenv("ETCD_NAME", "test-name")
os.Setenv("ETCD_SNAPSHOT", "true")
os.Setenv("ETCD_VERBOSE", "1")
os.Setenv("ETCD_VERY_VERBOSE", "yes")
os.Setenv("ETCD_WEB_URL", "/web")
os.Setenv("ETCD_PEER_ADVERTISED_URL", "127.0.0.1:7002")
os.Setenv("ETCD_PEER_CA_FILE", "/tmp/peer/file.ca")
os.Setenv("ETCD_PEER_CERT_FILE", "/tmp/peer/file.cert")
os.Setenv("ETCD_PEER_KEY_FILE", "/tmp/peer/file.key")
os.Setenv("ETCD_PEER_LISTEN_HOST", "127.0.0.1:7003")
c := NewConfig()
c.LoadEnv()
assert.Equal(t, c.CAFile, "/tmp/file.ca", "")
assert.Equal(t, c.CertFile, "/tmp/file.cert", "")
assert.Equal(t, c.Cors, []string{"localhost:4001", "localhost:4002"}, "")
assert.Equal(t, c.DataDir, "/tmp/data", "")
assert.Equal(t, c.KeyFile, "/tmp/file.key", "")
assert.Equal(t, c.ListenHost, "127.0.0.1:4003", "")
assert.Equal(t, c.Machines, []string{"coreos.com:4001", "coreos.com:4002"}, "")
assert.Equal(t, c.MachinesFile, "/tmp/machines", "")
assert.Equal(t, c.MaxClusterSize, 10, "")
assert.Equal(t, c.MaxResultBuffer, 512, "")
assert.Equal(t, c.MaxRetryAttempts, 5, "")
assert.Equal(t, c.Name, "test-name", "")
assert.Equal(t, c.Snapshot, true, "")
assert.Equal(t, c.Verbose, true, "")
assert.Equal(t, c.VeryVerbose, true, "")
assert.Equal(t, c.WebURL, "/web", "")
assert.Equal(t, c.Peer.AdvertisedUrl, "127.0.0.1:7002", "")
assert.Equal(t, c.Peer.CAFile, "/tmp/peer/file.ca", "")
assert.Equal(t, c.Peer.CertFile, "/tmp/peer/file.cert", "")
assert.Equal(t, c.Peer.KeyFile, "/tmp/peer/file.key", "")
assert.Equal(t, c.Peer.ListenHost, "127.0.0.1:7003", "")
}
// Ensures that a the advertised url can be parsed from the environment.
func TestConfigAdvertisedUrlEnv(t *testing.T) {
withEnv("ETCD_ADVERTISED_URL", "127.0.0.1:4002", func(c *Config) {
assert.Nil(t, c.LoadEnv(), "")
assert.Equal(t, c.AdvertisedUrl, "127.0.0.1:4002", "")
})
}
// Ensures that a the advertised flag can be parsed.
func TestConfigAdvertisedUrlFlag(t *testing.T) {
c := NewConfig()
assert.Nil(t, c.LoadFlags([]string{"-c", "127.0.0.1:4002"}), "")
assert.Equal(t, c.AdvertisedUrl, "127.0.0.1:4002", "")
}
// Ensures that a the CA file can be parsed from the environment.
func TestConfigCAFileEnv(t *testing.T) {
withEnv("ETCD_CA_FILE", "/tmp/file.ca", func(c *Config) {
assert.Nil(t, c.LoadEnv(), "")
assert.Equal(t, c.CAFile, "/tmp/file.ca", "")
})
}
// Ensures that a the CA file flag can be parsed.
func TestConfigCAFileFlag(t *testing.T) {
c := NewConfig()
assert.Nil(t, c.LoadFlags([]string{"-clientCAFile", "/tmp/file.ca"}), "")
assert.Equal(t, c.CAFile, "/tmp/file.ca", "")
}
// Ensures that a the CA file can be parsed from the environment.
func TestConfigCertFileEnv(t *testing.T) {
withEnv("ETCD_CERT_FILE", "/tmp/file.cert", func(c *Config) {
assert.Nil(t, c.LoadEnv(), "")
assert.Equal(t, c.CertFile, "/tmp/file.cert", "")
})
}
// Ensures that a the Cert file flag can be parsed.
func TestConfigCertFileFlag(t *testing.T) {
c := NewConfig()
assert.Nil(t, c.LoadFlags([]string{"-clientCert", "/tmp/file.cert"}), "")
assert.Equal(t, c.CertFile, "/tmp/file.cert", "")
}
// Ensures that a the Key file can be parsed from the environment.
func TestConfigKeyFileEnv(t *testing.T) {
withEnv("ETCD_KEY_FILE", "/tmp/file.key", func(c *Config) {
assert.Nil(t, c.LoadEnv(), "")
assert.Equal(t, c.KeyFile, "/tmp/file.key", "")
})
}
// Ensures that a the Key file flag can be parsed.
func TestConfigKeyFileFlag(t *testing.T) {
c := NewConfig()
assert.Nil(t, c.LoadFlags([]string{"-clientKey", "/tmp/file.key"}), "")
assert.Equal(t, c.KeyFile, "/tmp/file.key", "")
}
// Ensures that a the Listen Host can be parsed from the environment.
func TestConfigListenHostEnv(t *testing.T) {
withEnv("ETCD_LISTEN_HOST", "127.0.0.1:4003", func(c *Config) {
assert.Nil(t, c.LoadEnv(), "")
assert.Equal(t, c.ListenHost, "127.0.0.1:4003", "")
})
}
// Ensures that a the Listen Host file flag can be parsed.
func TestConfigListenHostFlag(t *testing.T) {
c := NewConfig()
assert.Nil(t, c.LoadFlags([]string{"-cl", "127.0.0.1:4003"}), "")
assert.Equal(t, c.ListenHost, "127.0.0.1:4003", "")
}
// Ensures that the Machines can be parsed from the environment.
func TestConfigMachinesEnv(t *testing.T) {
withEnv("ETCD_MACHINES", "coreos.com:4001,coreos.com:4002", func(c *Config) {
assert.Nil(t, c.LoadEnv(), "")
assert.Equal(t, c.Machines, []string{"coreos.com:4001", "coreos.com:4002"}, "")
})
}
// Ensures that a the Machines flag can be parsed.
func TestConfigMachinesFlag(t *testing.T) {
c := NewConfig()
assert.Nil(t, c.LoadFlags([]string{"-C", "coreos.com:4001,coreos.com:4002"}), "")
assert.Equal(t, c.Machines, []string{"coreos.com:4001", "coreos.com:4002"}, "")
}
// Ensures that the Machines File can be parsed from the environment.
func TestConfigMachinesFileEnv(t *testing.T) {
withEnv("ETCD_MACHINES_FILE", "/tmp/machines", func(c *Config) {
assert.Nil(t, c.LoadEnv(), "")
assert.Equal(t, c.MachinesFile, "/tmp/machines", "")
})
}
// Ensures that a the Machines File flag can be parsed.
func TestConfigMachinesFileFlag(t *testing.T) {
c := NewConfig()
assert.Nil(t, c.LoadFlags([]string{"-CF", "/tmp/machines"}), "")
assert.Equal(t, c.MachinesFile, "/tmp/machines", "")
}
// Ensures that the Max Cluster Size can be parsed from the environment.
func TestConfigMaxClusterSizeEnv(t *testing.T) {
withEnv("ETCD_MAX_CLUSTER_SIZE", "5", func(c *Config) {
assert.Nil(t, c.LoadEnv(), "")
assert.Equal(t, c.MaxClusterSize, 5, "")
})
}
// Ensures that a the Max Cluster Size flag can be parsed.
func TestConfigMaxClusterSizeFlag(t *testing.T) {
c := NewConfig()
assert.Nil(t, c.LoadFlags([]string{"-maxsize", "5"}), "")
assert.Equal(t, c.MaxClusterSize, 5, "")
}
// Ensures that the Max Result Buffer can be parsed from the environment.
func TestConfigMaxResultBufferEnv(t *testing.T) {
withEnv("ETCD_MAX_RESULT_BUFFER", "512", func(c *Config) {
assert.Nil(t, c.LoadEnv(), "")
assert.Equal(t, c.MaxResultBuffer, 512, "")
})
}
// Ensures that a the Max Result Buffer flag can be parsed.
func TestConfigMaxResultBufferFlag(t *testing.T) {
c := NewConfig()
assert.Nil(t, c.LoadFlags([]string{"-m", "512"}), "")
assert.Equal(t, c.MaxResultBuffer, 512, "")
}
// Ensures that the Max Retry Attempts can be parsed from the environment.
func TestConfigMaxRetryAttemptsEnv(t *testing.T) {
withEnv("ETCD_MAX_RETRY_ATTEMPTS", "10", func(c *Config) {
assert.Nil(t, c.LoadEnv(), "")
assert.Equal(t, c.MaxRetryAttempts, 10, "")
})
}
// Ensures that a the Max Retry Attempts flag can be parsed.
func TestConfigMaxRetryAttemptsFlag(t *testing.T) {
c := NewConfig()
assert.Nil(t, c.LoadFlags([]string{"-r", "10"}), "")
assert.Equal(t, c.MaxRetryAttempts, 10, "")
}
// Ensures that the Name can be parsed from the environment.
func TestConfigNameEnv(t *testing.T) {
withEnv("ETCD_NAME", "test-name", func(c *Config) {
assert.Nil(t, c.LoadEnv(), "")
assert.Equal(t, c.Name, "test-name", "")
})
}
// Ensures that a the Name flag can be parsed.
func TestConfigNameFlag(t *testing.T) {
c := NewConfig()
assert.Nil(t, c.LoadFlags([]string{"-n", "test-name"}), "")
assert.Equal(t, c.Name, "test-name", "")
}
// Ensures that Snapshot can be parsed from the environment.
func TestConfigSnapshotEnv(t *testing.T) {
withEnv("ETCD_SNAPSHOT", "1", func(c *Config) {
assert.Nil(t, c.LoadEnv(), "")
assert.Equal(t, c.Snapshot, true, "")
})
}
// Ensures that a the Snapshot flag can be parsed.
func TestConfigSnapshotFlag(t *testing.T) {
c := NewConfig()
assert.Nil(t, c.LoadFlags([]string{"-snapshot"}), "")
assert.Equal(t, c.Snapshot, true, "")
}
// Ensures that Verbose can be parsed from the environment.
func TestConfigVerboseEnv(t *testing.T) {
withEnv("ETCD_VERBOSE", "true", func(c *Config) {
assert.Nil(t, c.LoadEnv(), "")
assert.Equal(t, c.Verbose, true, "")
})
}
// Ensures that a the Verbose flag can be parsed.
func TestConfigVerboseFlag(t *testing.T) {
c := NewConfig()
assert.Nil(t, c.LoadFlags([]string{"-v"}), "")
assert.Equal(t, c.Verbose, true, "")
}
// Ensures that Very Verbose can be parsed from the environment.
func TestConfigVeryVerboseEnv(t *testing.T) {
withEnv("ETCD_VERY_VERBOSE", "true", func(c *Config) {
assert.Nil(t, c.LoadEnv(), "")
assert.Equal(t, c.VeryVerbose, true, "")
})
}
// Ensures that a the Very Verbose flag can be parsed.
func TestConfigVeryVerboseFlag(t *testing.T) {
c := NewConfig()
assert.Nil(t, c.LoadFlags([]string{"-vv"}), "")
assert.Equal(t, c.VeryVerbose, true, "")
}
// Ensures that Web URL can be parsed from the environment.
func TestConfigWebURLEnv(t *testing.T) {
withEnv("ETCD_WEB_URL", "/web", func(c *Config) {
assert.Nil(t, c.LoadEnv(), "")
assert.Equal(t, c.WebURL, "/web", "")
})
}
// Ensures that a the Web URL flag can be parsed.
func TestConfigWebURLFlag(t *testing.T) {
c := NewConfig()
assert.Nil(t, c.LoadFlags([]string{"-w", "/web"}), "")
assert.Equal(t, c.WebURL, "/web", "")
}
// Ensures that the Peer Advertised URL can be parsed from the environment.
func TestConfigPeerAdvertisedUrlEnv(t *testing.T) {
withEnv("ETCD_PEER_ADVERTISED_URL", "localhost:7002", func(c *Config) {
assert.Nil(t, c.LoadEnv(), "")
assert.Equal(t, c.Peer.AdvertisedUrl, "localhost:7002", "")
})
}
// Ensures that a the Peer Advertised URL flag can be parsed.
func TestConfigPeerAdvertisedUrlFlag(t *testing.T) {
c := NewConfig()
assert.Nil(t, c.LoadFlags([]string{"-s", "localhost:7002"}), "")
assert.Equal(t, c.Peer.AdvertisedUrl, "localhost:7002", "")
}
// Ensures that the Peer CA File can be parsed from the environment.
func TestConfigPeerCAFileEnv(t *testing.T) {
withEnv("ETCD_PEER_CA_FILE", "/tmp/peer/file.ca", func(c *Config) {
assert.Nil(t, c.LoadEnv(), "")
assert.Equal(t, c.Peer.CAFile, "/tmp/peer/file.ca", "")
})
}
// Ensures that a the Peer CA file flag can be parsed.
func TestConfigPeerCAFileFlag(t *testing.T) {
c := NewConfig()
assert.Nil(t, c.LoadFlags([]string{"-serverCAFile", "/tmp/peer/file.ca"}), "")
assert.Equal(t, c.Peer.CAFile, "/tmp/peer/file.ca", "")
}
// Ensures that the Peer Cert File can be parsed from the environment.
func TestConfigPeerCertFileEnv(t *testing.T) {
withEnv("ETCD_PEER_CERT_FILE", "/tmp/peer/file.cert", func(c *Config) {
assert.Nil(t, c.LoadEnv(), "")
assert.Equal(t, c.Peer.CertFile, "/tmp/peer/file.cert", "")
})
}
// Ensures that a the Cert file flag can be parsed.
func TestConfigPeerCertFileFlag(t *testing.T) {
c := NewConfig()
assert.Nil(t, c.LoadFlags([]string{"-serverCert", "/tmp/peer/file.cert"}), "")
assert.Equal(t, c.Peer.CertFile, "/tmp/peer/file.cert", "")
}
// Ensures that the Peer Key File can be parsed from the environment.
func TestConfigPeerKeyFileEnv(t *testing.T) {
withEnv("ETCD_PEER_KEY_FILE", "/tmp/peer/file.key", func(c *Config) {
assert.Nil(t, c.LoadEnv(), "")
assert.Equal(t, c.Peer.KeyFile, "/tmp/peer/file.key", "")
})
}
// Ensures that a the Peer Key file flag can be parsed.
func TestConfigPeerKeyFileFlag(t *testing.T) {
c := NewConfig()
assert.Nil(t, c.LoadFlags([]string{"-serverKey", "/tmp/peer/file.key"}), "")
assert.Equal(t, c.Peer.KeyFile, "/tmp/peer/file.key", "")
}
// Ensures that the Peer Listen Host can be parsed from the environment.
func TestConfigPeerListenHostEnv(t *testing.T) {
withEnv("ETCD_PEER_LISTEN_HOST", "localhost:7004", func(c *Config) {
assert.Nil(t, c.LoadEnv(), "")
assert.Equal(t, c.Peer.ListenHost, "localhost:7004", "")
})
}
// Ensures that a the Peer Listen Host file flag can be parsed.
func TestConfigPeerListenHostFlag(t *testing.T) {
c := NewConfig()
assert.Nil(t, c.LoadFlags([]string{"-sl", "127.0.0.1:4003"}), "")
assert.Equal(t, c.Peer.ListenHost, "127.0.0.1:4003", "")
}
// Ensures that a system config field is overridden by a custom config field.
func TestConfigCustomConfigOverrideSystemConfig(t *testing.T) {
system := `advertised_url = "127.0.0.1:5000"`
custom := `advertised_url = "127.0.0.1:6000"`
withTempFile(system, func(p1 string) {
withTempFile(custom, func(p2 string) {
c := NewConfig()
c.SystemPath = p1
assert.Nil(t, c.Load([]string{"-config", p2}), "")
assert.Equal(t, c.AdvertisedUrl, "http://127.0.0.1:6000", "")
})
})
}
// Ensures that a custom config field is overridden by an environment variable.
func TestConfigEnvVarOverrideCustomConfig(t *testing.T) {
os.Setenv("ETCD_PEER_ADVERTISED_URL", "127.0.0.1:8000")
defer os.Setenv("ETCD_PEER_ADVERTISED_URL", "")
custom := `[peer]`+"\n"+`advertised_url = "127.0.0.1:9000"`
withTempFile(custom, func(path string) {
c := NewConfig()
c.SystemPath = ""
assert.Nil(t, c.Load([]string{"-config", path}), "")
assert.Equal(t, c.Peer.AdvertisedUrl, "http://127.0.0.1:8000", "")
})
}
// Ensures that an environment variable field is overridden by a command line argument.
func TestConfigCLIArgsOverrideEnvVar(t *testing.T) {
os.Setenv("ETCD_ADVERTISED_URL", "127.0.0.1:1000")
defer os.Setenv("ETCD_ADVERTISED_URL", "")
c := NewConfig()
c.SystemPath = ""
assert.Nil(t, c.Load([]string{"-c", "127.0.0.1:2000"}), "")
assert.Equal(t, c.AdvertisedUrl, "http://127.0.0.1:2000", "")
}
//--------------------------------------
// Helpers
//--------------------------------------
// Sets up the environment with a given environment variable set.
func withEnv(key, value string, f func(c *Config)) {
os.Setenv(key, value)
defer os.Setenv(key, "")
c := NewConfig()
f(c)
}
// Creates a temp file and calls a function with the context.
func withTempFile(content string, fn func(string)) {
f, _ := ioutil.TempFile("", "")
f.WriteString(content)
f.Close()
defer os.Remove(f.Name())
fn(f.Name())
}

19
server/info.go Normal file
View File

@ -0,0 +1,19 @@
package server
// Info describes the non-mutable state of the server upon initialization.
// These fields cannot be changed without deleting the server fields and
// reinitializing.
type Info struct {
Name string `json:"name"`
RaftURL string `json:"raftURL"`
EtcdURL string `json:"etcdURL"`
WebURL string `json:"webURL"`
RaftListenHost string `json:"raftListenHost"`
EtcdListenHost string `json:"etcdListenHost"`
RaftTLS TLSInfo `json:"raftTLS"`
EtcdTLS TLSInfo `json:"etcdTLS"`
}

77
server/join_command.go Normal file
View File

@ -0,0 +1,77 @@
package server
import (
"encoding/binary"
etcdErr "github.com/coreos/etcd/error"
"github.com/coreos/etcd/log"
"github.com/coreos/go-raft"
)
func init() {
raft.RegisterCommand(&JoinCommand{})
}
// The JoinCommand adds a node to the cluster.
type JoinCommand struct {
MinVersion int `json:"minVersion"`
MaxVersion int `json:"maxVersion"`
Name string `json:"name"`
RaftURL string `json:"raftURL"`
EtcdURL string `json:"etcdURL"`
}
func NewJoinCommand(minVersion int, maxVersion int, name, raftUrl, etcdUrl string) *JoinCommand {
return &JoinCommand{
MinVersion: minVersion,
MaxVersion: maxVersion,
Name: name,
RaftURL: raftUrl,
EtcdURL: etcdUrl,
}
}
// The name of the join command in the log
func (c *JoinCommand) CommandName() string {
return "etcd:join"
}
// Join a server to the cluster
func (c *JoinCommand) Apply(server raft.Server) (interface{}, error) {
ps, _ := server.Context().(*PeerServer)
b := make([]byte, 8)
binary.PutUvarint(b, server.CommitIndex())
// Make sure we're not getting a cached value from the registry.
ps.registry.Invalidate(c.Name)
// Check if the join command is from a previous machine, who lost all its previous log.
if _, ok := ps.registry.ClientURL(c.Name); ok {
return b, nil
}
// Check machine number in the cluster
if ps.registry.Count() == ps.MaxClusterSize {
log.Debug("Reject join request from ", c.Name)
return []byte{0}, etcdErr.NewError(etcdErr.EcodeNoMoreMachine, "", server.CommitIndex())
}
// Add to shared machine registry.
ps.registry.Register(c.Name, c.RaftURL, c.EtcdURL)
// Add peer in raft
err := server.AddPeer(c.Name, "")
// Add peer stats
if c.Name != ps.RaftServer().Name() {
ps.followersStats.Followers[c.Name] = &raftFollowerStats{}
ps.followersStats.Followers[c.Name].Latency.Minimum = 1 << 63
}
return b, err
}
func (c *JoinCommand) NodeName() string {
return c.Name
}

25
server/package_stats.go Normal file
View File

@ -0,0 +1,25 @@
package server
import (
"time"
)
// packageStats represent the stats we need for a package.
// It has sending time and the size of the package.
type packageStats struct {
sendingTime time.Time
size int
}
// NewPackageStats creates a pacakgeStats and return the pointer to it.
func NewPackageStats(now time.Time, size int) *packageStats {
return &packageStats{
sendingTime: now,
size: size,
}
}
// Time return the sending time of the package.
func (ps *packageStats) Time() time.Time {
return ps.sendingTime
}

440
server/peer_server.go Normal file
View File

@ -0,0 +1,440 @@
package server
import (
"bytes"
"crypto/tls"
"encoding/binary"
"encoding/json"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/url"
"strconv"
"time"
etcdErr "github.com/coreos/etcd/error"
"github.com/coreos/etcd/log"
"github.com/coreos/etcd/store"
"github.com/coreos/go-raft"
"github.com/gorilla/mux"
)
type PeerServer struct {
raftServer raft.Server
server *Server
httpServer *http.Server
listener net.Listener
joinIndex uint64
name string
url string
listenHost string
tlsConf *TLSConfig
tlsInfo *TLSInfo
followersStats *raftFollowersStats
serverStats *raftServerStats
registry *Registry
store store.Store
snapConf *snapshotConf
MaxClusterSize int
RetryTimes int
}
// TODO: find a good policy to do snapshot
type snapshotConf struct {
// Etcd will check if snapshot is need every checkingInterval
checkingInterval time.Duration
// The number of writes when the last snapshot happened
lastWrites uint64
// If the incremental number of writes since the last snapshot
// exceeds the write Threshold, etcd will do a snapshot
writesThr uint64
}
func NewPeerServer(name string, path string, url string, listenHost string, tlsConf *TLSConfig, tlsInfo *TLSInfo, registry *Registry, store store.Store, snapCount int) *PeerServer {
s := &PeerServer{
name: name,
url: url,
listenHost: listenHost,
tlsConf: tlsConf,
tlsInfo: tlsInfo,
registry: registry,
store: store,
snapConf: &snapshotConf{time.Second * 3, 0, uint64(snapCount)},
followersStats: &raftFollowersStats{
Leader: name,
Followers: make(map[string]*raftFollowerStats),
},
serverStats: &raftServerStats{
StartTime: time.Now(),
sendRateQueue: &statsQueue{
back: -1,
},
recvRateQueue: &statsQueue{
back: -1,
},
},
}
// Create transporter for raft
raftTransporter := newTransporter(tlsConf.Scheme, tlsConf.Client, s)
// Create raft server
raftServer, err := raft.NewServer(name, path, raftTransporter, s.store, s, "")
if err != nil {
log.Fatal(err)
}
s.raftServer = raftServer
return s
}
// Start the raft server
func (s *PeerServer) ListenAndServe(snapshot bool, cluster []string) error {
// LoadSnapshot
if snapshot {
err := s.raftServer.LoadSnapshot()
if err == nil {
log.Debugf("%s finished load snapshot", s.name)
} else {
log.Debug(err)
}
}
s.raftServer.SetElectionTimeout(ElectionTimeout)
s.raftServer.SetHeartbeatTimeout(HeartbeatTimeout)
s.raftServer.Start()
if s.raftServer.IsLogEmpty() {
// start as a leader in a new cluster
if len(cluster) == 0 {
s.startAsLeader()
} else {
s.startAsFollower(cluster)
}
} else {
// Rejoin the previous cluster
cluster = s.registry.PeerURLs(s.raftServer.Leader(), s.name)
for i := 0; i < len(cluster); i++ {
u, err := url.Parse(cluster[i])
if err != nil {
log.Debug("rejoin cannot parse url: ", err)
}
cluster[i] = u.Host
}
ok := s.joinCluster(cluster)
if !ok {
log.Warn("the entire cluster is down! this machine will restart the cluster.")
}
log.Debugf("%s restart as a follower", s.name)
}
go s.monitorSync()
// open the snapshot
if snapshot {
go s.monitorSnapshot()
}
// start to response to raft requests
return s.startTransport(s.tlsConf.Scheme, s.tlsConf.Server)
}
// Overridden version of net/http added so we can manage the listener.
func (s *PeerServer) listenAndServe() error {
addr := s.httpServer.Addr
if addr == "" {
addr = ":http"
}
l, e := net.Listen("tcp", addr)
if e != nil {
return e
}
s.listener = l
return s.httpServer.Serve(l)
}
// Overridden version of net/http added so we can manage the listener.
func (s *PeerServer) listenAndServeTLS(certFile, keyFile string) error {
addr := s.httpServer.Addr
if addr == "" {
addr = ":https"
}
config := &tls.Config{}
if s.httpServer.TLSConfig != nil {
*config = *s.httpServer.TLSConfig
}
if config.NextProtos == nil {
config.NextProtos = []string{"http/1.1"}
}
var err error
config.Certificates = make([]tls.Certificate, 1)
config.Certificates[0], err = tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
return err
}
conn, err := net.Listen("tcp", addr)
if err != nil {
return err
}
tlsListener := tls.NewListener(conn, config)
s.listener = tlsListener
return s.httpServer.Serve(tlsListener)
}
// Stops the server.
func (s *PeerServer) Close() {
if s.listener != nil {
s.listener.Close()
s.listener = nil
}
}
// Retrieves the underlying Raft server.
func (s *PeerServer) RaftServer() raft.Server {
return s.raftServer
}
// Associates the client server with the peer server.
func (s *PeerServer) SetServer(server *Server) {
s.server = server
}
func (s *PeerServer) startAsLeader() {
// leader need to join self as a peer
for {
_, err := s.raftServer.Do(NewJoinCommand(store.MinVersion(), store.MaxVersion(), s.raftServer.Name(), s.url, s.server.URL()))
if err == nil {
break
}
}
log.Debugf("%s start as a leader", s.name)
}
func (s *PeerServer) startAsFollower(cluster []string) {
// start as a follower in a existing cluster
for i := 0; i < s.RetryTimes; i++ {
ok := s.joinCluster(cluster)
if ok {
return
}
log.Warnf("cannot join to cluster via given machines, retry in %d seconds", RetryInterval)
time.Sleep(time.Second * RetryInterval)
}
log.Fatalf("Cannot join the cluster via given machines after %x retries", s.RetryTimes)
}
// Start to listen and response raft command
func (s *PeerServer) startTransport(scheme string, tlsConf tls.Config) error {
log.Infof("raft server [name %s, listen on %s, advertised url %s]", s.name, s.listenHost, s.url)
router := mux.NewRouter()
s.httpServer = &http.Server{
Handler: router,
TLSConfig: &tlsConf,
Addr: s.listenHost,
}
// internal commands
router.HandleFunc("/name", s.NameHttpHandler)
router.HandleFunc("/version", s.VersionHttpHandler)
router.HandleFunc("/version/{version:[0-9]+}/check", s.VersionCheckHttpHandler)
router.HandleFunc("/upgrade", s.UpgradeHttpHandler)
router.HandleFunc("/join", s.JoinHttpHandler)
router.HandleFunc("/remove/{name:.+}", s.RemoveHttpHandler)
router.HandleFunc("/vote", s.VoteHttpHandler)
router.HandleFunc("/log", s.GetLogHttpHandler)
router.HandleFunc("/log/append", s.AppendEntriesHttpHandler)
router.HandleFunc("/snapshot", s.SnapshotHttpHandler)
router.HandleFunc("/snapshotRecovery", s.SnapshotRecoveryHttpHandler)
router.HandleFunc("/etcdURL", s.EtcdURLHttpHandler)
if scheme == "http" {
return s.listenAndServe()
} else {
return s.listenAndServeTLS(s.tlsInfo.CertFile, s.tlsInfo.KeyFile)
}
}
// getVersion fetches the peer version of a cluster.
func getVersion(t *transporter, versionURL url.URL) (int, error) {
resp, req, err := t.Get(versionURL.String())
if err != nil {
return 0, err
}
defer resp.Body.Close()
t.CancelWhenTimeout(req)
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return 0, err
}
// Parse version number.
version, _ := strconv.Atoi(string(body))
return version, nil
}
// Upgradable checks whether all peers in a cluster support an upgrade to the next store version.
func (s *PeerServer) Upgradable() error {
nextVersion := s.store.Version() + 1
for _, peerURL := range s.registry.PeerURLs(s.raftServer.Leader(), s.name) {
u, err := url.Parse(peerURL)
if err != nil {
return fmt.Errorf("PeerServer: Cannot parse URL: '%s' (%s)", peerURL, err)
}
t, _ := s.raftServer.Transporter().(*transporter)
checkURL := (&url.URL{Host: u.Host, Scheme: s.tlsConf.Scheme, Path: fmt.Sprintf("/version/%d/check", nextVersion)}).String()
resp, _, err := t.Get(checkURL)
if err != nil {
return fmt.Errorf("PeerServer: Cannot check version compatibility: %s", u.Host)
}
if resp.StatusCode != 200 {
return fmt.Errorf("PeerServer: Version %d is not compatible with peer: %s", nextVersion, u.Host)
}
}
return nil
}
func (s *PeerServer) joinCluster(cluster []string) bool {
for _, machine := range cluster {
if len(machine) == 0 {
continue
}
err := s.joinByMachine(s.raftServer, machine, s.tlsConf.Scheme)
if err == nil {
log.Debugf("%s success join to the cluster via machine %s", s.name, machine)
return true
} else {
if _, ok := err.(etcdErr.Error); ok {
log.Fatal(err)
}
log.Debugf("cannot join to cluster via machine %s %s", machine, err)
}
}
return false
}
// Send join requests to machine.
func (s *PeerServer) joinByMachine(server raft.Server, machine string, scheme string) error {
var b bytes.Buffer
// t must be ok
t, _ := server.Transporter().(*transporter)
// Our version must match the leaders version
versionURL := url.URL{Host: machine, Scheme: scheme, Path: "/version"}
version, err := getVersion(t, versionURL)
if err != nil {
return fmt.Errorf("Error during join version check: %v", err)
}
if version < store.MinVersion() || version > store.MaxVersion() {
return fmt.Errorf("Unable to join: cluster version is %d; version compatibility is %d - %d", version, store.MinVersion(), store.MaxVersion())
}
json.NewEncoder(&b).Encode(NewJoinCommand(store.MinVersion(), store.MaxVersion(), server.Name(), s.url, s.server.URL()))
joinURL := url.URL{Host: machine, Scheme: scheme, Path: "/join"}
log.Debugf("Send Join Request to %s", joinURL.String())
resp, req, err := t.Post(joinURL.String(), &b)
for {
if err != nil {
return fmt.Errorf("Unable to join: %v", err)
}
if resp != nil {
defer resp.Body.Close()
t.CancelWhenTimeout(req)
if resp.StatusCode == http.StatusOK {
b, _ := ioutil.ReadAll(resp.Body)
s.joinIndex, _ = binary.Uvarint(b)
return nil
}
if resp.StatusCode == http.StatusTemporaryRedirect {
address := resp.Header.Get("Location")
log.Debugf("Send Join Request to %s", address)
json.NewEncoder(&b).Encode(NewJoinCommand(store.MinVersion(), store.MaxVersion(), server.Name(), s.url, s.server.URL()))
resp, req, err = t.Post(address, &b)
} else if resp.StatusCode == http.StatusBadRequest {
log.Debug("Reach max number machines in the cluster")
decoder := json.NewDecoder(resp.Body)
err := &etcdErr.Error{}
decoder.Decode(err)
return *err
} else {
return fmt.Errorf("Unable to join")
}
}
}
}
func (s *PeerServer) Stats() []byte {
s.serverStats.LeaderInfo.Uptime = time.Now().Sub(s.serverStats.LeaderInfo.startTime).String()
queue := s.serverStats.sendRateQueue
s.serverStats.SendingPkgRate, s.serverStats.SendingBandwidthRate = queue.Rate()
queue = s.serverStats.recvRateQueue
s.serverStats.RecvingPkgRate, s.serverStats.RecvingBandwidthRate = queue.Rate()
b, _ := json.Marshal(s.serverStats)
return b
}
func (s *PeerServer) PeerStats() []byte {
if s.raftServer.State() == raft.Leader {
b, _ := json.Marshal(s.followersStats)
return b
}
return nil
}
func (s *PeerServer) monitorSnapshot() {
for {
time.Sleep(s.snapConf.checkingInterval)
currentWrites := s.store.TotalTransactions() - s.snapConf.lastWrites
if uint64(currentWrites) > s.snapConf.writesThr {
s.raftServer.TakeSnapshot()
s.snapConf.lastWrites = s.store.TotalTransactions()
}
}
}
func (s *PeerServer) monitorSync() {
ticker := time.Tick(time.Millisecond * 500)
for {
select {
case now := <-ticker:
if s.raftServer.State() == raft.Leader {
s.raftServer.Do(s.store.CommandFactory().CreateSyncCommand(now))
}
}
}
}

View File

@ -0,0 +1,236 @@
package server
import (
"encoding/json"
"net/http"
"strconv"
etcdErr "github.com/coreos/etcd/error"
"github.com/coreos/etcd/log"
"github.com/coreos/etcd/store"
"github.com/coreos/go-raft"
"github.com/gorilla/mux"
)
// Get all the current logs
func (ps *PeerServer) GetLogHttpHandler(w http.ResponseWriter, req *http.Request) {
log.Debugf("[recv] GET %s/log", ps.url)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(ps.raftServer.LogEntries())
}
// Response to vote request
func (ps *PeerServer) VoteHttpHandler(w http.ResponseWriter, req *http.Request) {
rvreq := &raft.RequestVoteRequest{}
if _, err := rvreq.Decode(req.Body); err != nil {
http.Error(w, "", http.StatusBadRequest)
log.Warnf("[recv] BADREQUEST %s/vote [%v]", ps.url, err)
return
}
log.Debugf("[recv] POST %s/vote [%s]", ps.url, rvreq.CandidateName)
resp := ps.raftServer.RequestVote(rvreq)
if resp == nil {
log.Warn("[vote] Error: nil response")
http.Error(w, "", http.StatusInternalServerError)
return
}
if _, err := resp.Encode(w); err != nil {
log.Warn("[vote] Error: %v", err)
http.Error(w, "", http.StatusInternalServerError)
return
}
}
// Response to append entries request
func (ps *PeerServer) AppendEntriesHttpHandler(w http.ResponseWriter, req *http.Request) {
aereq := &raft.AppendEntriesRequest{}
if _, err := aereq.Decode(req.Body); err != nil {
http.Error(w, "", http.StatusBadRequest)
log.Warnf("[recv] BADREQUEST %s/log/append [%v]", ps.url, err)
return
}
log.Debugf("[recv] POST %s/log/append [%d]", ps.url, len(aereq.Entries))
ps.serverStats.RecvAppendReq(aereq.LeaderName, int(req.ContentLength))
resp := ps.raftServer.AppendEntries(aereq)
if resp == nil {
log.Warn("[ae] Error: nil response")
http.Error(w, "", http.StatusInternalServerError)
return
}
if !resp.Success {
log.Debugf("[Append Entry] Step back")
}
if _, err := resp.Encode(w); err != nil {
log.Warn("[ae] Error: %v", err)
http.Error(w, "", http.StatusInternalServerError)
return
}
}
// Response to recover from snapshot request
func (ps *PeerServer) SnapshotHttpHandler(w http.ResponseWriter, req *http.Request) {
ssreq := &raft.SnapshotRequest{}
if _, err := ssreq.Decode(req.Body); err != nil {
http.Error(w, "", http.StatusBadRequest)
log.Warnf("[recv] BADREQUEST %s/snapshot [%v]", ps.url, err)
return
}
log.Debugf("[recv] POST %s/snapshot", ps.url)
resp := ps.raftServer.RequestSnapshot(ssreq)
if resp == nil {
log.Warn("[ss] Error: nil response")
http.Error(w, "", http.StatusInternalServerError)
return
}
if _, err := resp.Encode(w); err != nil {
log.Warn("[ss] Error: %v", err)
http.Error(w, "", http.StatusInternalServerError)
return
}
}
// Response to recover from snapshot request
func (ps *PeerServer) SnapshotRecoveryHttpHandler(w http.ResponseWriter, req *http.Request) {
ssrreq := &raft.SnapshotRecoveryRequest{}
if _, err := ssrreq.Decode(req.Body); err != nil {
http.Error(w, "", http.StatusBadRequest)
log.Warnf("[recv] BADREQUEST %s/snapshotRecovery [%v]", ps.url, err)
return
}
log.Debugf("[recv] POST %s/snapshotRecovery", ps.url)
resp := ps.raftServer.SnapshotRecoveryRequest(ssrreq)
if resp == nil {
log.Warn("[ssr] Error: nil response")
http.Error(w, "", http.StatusInternalServerError)
return
}
if _, err := resp.Encode(w); err != nil {
log.Warn("[ssr] Error: %v", err)
http.Error(w, "", http.StatusInternalServerError)
return
}
}
// Get the port that listening for etcd connecting of the server
func (ps *PeerServer) EtcdURLHttpHandler(w http.ResponseWriter, req *http.Request) {
log.Debugf("[recv] Get %s/etcdURL/ ", ps.url)
w.WriteHeader(http.StatusOK)
w.Write([]byte(ps.server.URL()))
}
// Response to the join request
func (ps *PeerServer) JoinHttpHandler(w http.ResponseWriter, req *http.Request) {
command := &JoinCommand{}
// Write CORS header.
if ps.server.OriginAllowed("*") {
w.Header().Add("Access-Control-Allow-Origin", "*")
} else if ps.server.OriginAllowed(req.Header.Get("Origin")) {
w.Header().Add("Access-Control-Allow-Origin", req.Header.Get("Origin"))
}
err := decodeJsonRequest(req, command)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
log.Debugf("Receive Join Request from %s", command.Name)
err = ps.server.Dispatch(command, w, req)
// Return status.
if err != nil {
if etcdErr, ok := err.(*etcdErr.Error); ok {
log.Debug("Return error: ", (*etcdErr).Error())
etcdErr.Write(w)
} else {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
}
// Response to remove request
func (ps *PeerServer) RemoveHttpHandler(w http.ResponseWriter, req *http.Request) {
if req.Method != "DELETE" {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
vars := mux.Vars(req)
command := &RemoveCommand{
Name: vars["name"],
}
log.Debugf("[recv] Remove Request [%s]", command.Name)
ps.server.Dispatch(command, w, req)
}
// Response to the name request
func (ps *PeerServer) NameHttpHandler(w http.ResponseWriter, req *http.Request) {
log.Debugf("[recv] Get %s/name/ ", ps.url)
w.WriteHeader(http.StatusOK)
w.Write([]byte(ps.name))
}
// Response to the name request
func (ps *PeerServer) VersionHttpHandler(w http.ResponseWriter, req *http.Request) {
log.Debugf("[recv] Get %s/version/ ", ps.url)
w.WriteHeader(http.StatusOK)
w.Write([]byte(strconv.Itoa(ps.store.Version())))
}
// Checks whether a given version is supported.
func (ps *PeerServer) VersionCheckHttpHandler(w http.ResponseWriter, req *http.Request) {
log.Debugf("[recv] Get %s%s ", ps.url, req.URL.Path)
vars := mux.Vars(req)
version, _ := strconv.Atoi(vars["version"])
if version >= store.MinVersion() && version <= store.MaxVersion() {
w.WriteHeader(http.StatusOK)
} else {
w.WriteHeader(http.StatusForbidden)
}
}
// Upgrades the current store version to the next version.
func (ps *PeerServer) UpgradeHttpHandler(w http.ResponseWriter, req *http.Request) {
log.Debugf("[recv] Get %s/version", ps.url)
// Check if upgrade is possible for all nodes.
if err := ps.Upgradable(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Create an upgrade command from the current version.
c := ps.store.CommandFactory().CreateUpgradeCommand()
if err := ps.server.Dispatch(c, w, req); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}

View File

@ -0,0 +1,56 @@
package server
import (
"math"
"time"
)
type raftFollowersStats struct {
Leader string `json:"leader"`
Followers map[string]*raftFollowerStats `json:"followers"`
}
type raftFollowerStats struct {
Latency struct {
Current float64 `json:"current"`
Average float64 `json:"average"`
averageSquare float64
StandardDeviation float64 `json:"standardDeviation"`
Minimum float64 `json:"minimum"`
Maximum float64 `json:"maximum"`
} `json:"latency"`
Counts struct {
Fail uint64 `json:"fail"`
Success uint64 `json:"success"`
} `json:"counts"`
}
// Succ function update the raftFollowerStats with a successful send
func (ps *raftFollowerStats) Succ(d time.Duration) {
total := float64(ps.Counts.Success) * ps.Latency.Average
totalSquare := float64(ps.Counts.Success) * ps.Latency.averageSquare
ps.Counts.Success++
ps.Latency.Current = float64(d) / (1000000.0)
if ps.Latency.Current > ps.Latency.Maximum {
ps.Latency.Maximum = ps.Latency.Current
}
if ps.Latency.Current < ps.Latency.Minimum {
ps.Latency.Minimum = ps.Latency.Current
}
ps.Latency.Average = (total + ps.Latency.Current) / float64(ps.Counts.Success)
ps.Latency.averageSquare = (totalSquare + ps.Latency.Current*ps.Latency.Current) / float64(ps.Counts.Success)
// sdv = sqrt(avg(x^2) - avg(x)^2)
ps.Latency.StandardDeviation = math.Sqrt(ps.Latency.averageSquare - ps.Latency.Average*ps.Latency.Average)
}
// Fail function update the raftFollowerStats with a unsuccessful send
func (ps *raftFollowerStats) Fail() {
ps.Counts.Fail++
}

View File

@ -0,0 +1,55 @@
package server
import (
"time"
"github.com/coreos/go-raft"
)
type raftServerStats struct {
Name string `json:"name"`
State string `json:"state"`
StartTime time.Time `json:"startTime"`
LeaderInfo struct {
Name string `json:"leader"`
Uptime string `json:"uptime"`
startTime time.Time
} `json:"leaderInfo"`
RecvAppendRequestCnt uint64 `json:"recvAppendRequestCnt,"`
RecvingPkgRate float64 `json:"recvPkgRate,omitempty"`
RecvingBandwidthRate float64 `json:"recvBandwidthRate,omitempty"`
SendAppendRequestCnt uint64 `json:"sendAppendRequestCnt"`
SendingPkgRate float64 `json:"sendPkgRate,omitempty"`
SendingBandwidthRate float64 `json:"sendBandwidthRate,omitempty"`
sendRateQueue *statsQueue
recvRateQueue *statsQueue
}
func (ss *raftServerStats) RecvAppendReq(leaderName string, pkgSize int) {
ss.State = raft.Follower
if leaderName != ss.LeaderInfo.Name {
ss.LeaderInfo.Name = leaderName
ss.LeaderInfo.startTime = time.Now()
}
ss.recvRateQueue.Insert(NewPackageStats(time.Now(), pkgSize))
ss.RecvAppendRequestCnt++
}
func (ss *raftServerStats) SendAppendReq(pkgSize int) {
now := time.Now()
if ss.State != raft.Leader {
ss.State = raft.Leader
ss.LeaderInfo.Name = ss.Name
ss.LeaderInfo.startTime = now
}
ss.sendRateQueue.Insert(NewPackageStats(now, pkgSize))
ss.SendAppendRequestCnt++
}

179
server/registry.go Normal file
View File

@ -0,0 +1,179 @@
package server
import (
"fmt"
"net/url"
"path"
"path/filepath"
"strings"
"sync"
"github.com/coreos/etcd/log"
"github.com/coreos/etcd/store"
)
// The location of the machine URL data.
const RegistryKey = "/_etcd/machines"
// The Registry stores URL information for nodes.
type Registry struct {
sync.Mutex
store store.Store
nodes map[string]*node
}
// The internal storage format of the registry.
type node struct {
peerVersion string
peerURL string
url string
}
// Creates a new Registry.
func NewRegistry(s store.Store) *Registry {
return &Registry{
store: s,
nodes: make(map[string]*node),
}
}
// Adds a node to the registry.
func (r *Registry) Register(name string, peerURL string, url string) error {
r.Lock()
defer r.Unlock()
// Write data to store.
key := path.Join(RegistryKey, name)
value := fmt.Sprintf("raft=%s&etcd=%s", peerURL, url)
_, err := r.store.Create(key, value, false, store.Permanent)
log.Debugf("Register: %s", name)
return err
}
// Removes a node from the registry.
func (r *Registry) Unregister(name string) error {
r.Lock()
defer r.Unlock()
// Remove from cache.
// delete(r.nodes, name)
// Remove the key from the store.
_, err := r.store.Delete(path.Join(RegistryKey, name), false)
log.Debugf("Unregister: %s", name)
return err
}
// Returns the number of nodes in the cluster.
func (r *Registry) Count() int {
e, err := r.store.Get(RegistryKey, false, false)
if err != nil {
return 0
}
return len(e.KVPairs)
}
// Retrieves the client URL for a given node by name.
func (r *Registry) ClientURL(name string) (string, bool) {
r.Lock()
defer r.Unlock()
return r.clientURL(name)
}
func (r *Registry) clientURL(name string) (string, bool) {
if r.nodes[name] == nil {
r.load(name)
}
if node := r.nodes[name]; node != nil {
return node.url, true
}
return "", false
}
// Retrieves the peer URL for a given node by name.
func (r *Registry) PeerURL(name string) (string, bool) {
r.Lock()
defer r.Unlock()
return r.peerURL(name)
}
func (r *Registry) peerURL(name string) (string, bool) {
if r.nodes[name] == nil {
r.load(name)
}
if node := r.nodes[name]; node != nil {
return node.peerURL, true
}
return "", false
}
// Retrieves the Client URLs for all nodes.
func (r *Registry) ClientURLs(leaderName, selfName string) []string {
return r.urls(leaderName, selfName, r.clientURL)
}
// Retrieves the Peer URLs for all nodes.
func (r *Registry) PeerURLs(leaderName, selfName string) []string {
return r.urls(leaderName, selfName, r.peerURL)
}
// Retrieves the URLs for all nodes using url function.
func (r *Registry) urls(leaderName, selfName string, url func(name string) (string, bool)) []string {
r.Lock()
defer r.Unlock()
// Build list including the leader and self.
urls := make([]string, 0)
if url, _ := url(leaderName); len(url) > 0 {
urls = append(urls, url)
}
// Retrieve a list of all nodes.
if e, _ := r.store.Get(RegistryKey, false, false); e != nil {
// Lookup the URL for each one.
for _, pair := range e.KVPairs {
_, name := filepath.Split(pair.Key)
if url, _ := url(name); len(url) > 0 && name != leaderName {
urls = append(urls, url)
}
}
}
log.Infof("URLs: %s / %s (%s)", leaderName, selfName, strings.Join(urls, ","))
return urls
}
// Removes a node from the cache.
func (r *Registry) Invalidate(name string) {
delete(r.nodes, name)
}
// Loads the given node by name from the store into the cache.
func (r *Registry) load(name string) {
if name == "" {
return
}
// Retrieve from store.
e, err := r.store.Get(path.Join(RegistryKey, name), false, false)
if err != nil {
return
}
// Parse as a query string.
m, err := url.ParseQuery(e.Value)
if err != nil {
panic(fmt.Sprintf("Failed to parse machines entry: %s", name))
}
// Create node.
r.nodes[name] = &node{
url: m["etcd"][0],
peerURL: m["raft"][0],
}
}

67
server/remove_command.go Normal file
View File

@ -0,0 +1,67 @@
package server
import (
"encoding/binary"
"os"
"github.com/coreos/etcd/log"
"github.com/coreos/go-raft"
)
func init() {
raft.RegisterCommand(&RemoveCommand{})
}
// The RemoveCommand removes a server from the cluster.
type RemoveCommand struct {
Name string `json:"name"`
}
// The name of the remove command in the log
func (c *RemoveCommand) CommandName() string {
return "etcd:remove"
}
// Remove a server from the cluster
func (c *RemoveCommand) Apply(server raft.Server) (interface{}, error) {
ps, _ := server.Context().(*PeerServer)
// Remove node from the shared registry.
err := ps.registry.Unregister(c.Name)
// Delete from stats
delete(ps.followersStats.Followers, c.Name)
if err != nil {
log.Debugf("Error while unregistering: %s (%v)", c.Name, err)
return []byte{0}, err
}
// Remove peer in raft
err = server.RemovePeer(c.Name)
if err != nil {
log.Debugf("Unable to remove peer: %s (%v)", c.Name, err)
return []byte{0}, err
}
if c.Name == server.Name() {
// the removed node is this node
// if the node is not replaying the previous logs
// and the node has sent out a join request in this
// start. It is sure that this node received a new remove
// command and need to be removed
if server.CommitIndex() > ps.joinIndex && ps.joinIndex != 0 {
log.Debugf("server [%s] is removed", server.Name())
os.Exit(0)
} else {
// else ignore remove
log.Debugf("ignore previous remove command.")
}
}
b := make([]byte, 8)
binary.PutUvarint(b, server.CommitIndex())
return b, err
}

399
server/server.go Normal file
View File

@ -0,0 +1,399 @@
package server
import (
"crypto/tls"
"encoding/json"
"fmt"
"net"
"net/http"
"net/url"
"strings"
"time"
etcdErr "github.com/coreos/etcd/error"
"github.com/coreos/etcd/log"
"github.com/coreos/etcd/mod"
"github.com/coreos/etcd/server/v1"
"github.com/coreos/etcd/server/v2"
"github.com/coreos/etcd/store"
_ "github.com/coreos/etcd/store/v2"
"github.com/coreos/go-raft"
"github.com/gorilla/mux"
)
// This is the default implementation of the Server interface.
type Server struct {
http.Server
peerServer *PeerServer
registry *Registry
listener net.Listener
store store.Store
name string
url string
tlsConf *TLSConfig
tlsInfo *TLSInfo
corsOrigins map[string]bool
}
// Creates a new Server.
func New(name string, urlStr string, listenHost string, tlsConf *TLSConfig, tlsInfo *TLSInfo, peerServer *PeerServer, registry *Registry, store store.Store) *Server {
s := &Server{
Server: http.Server{
Handler: mux.NewRouter(),
TLSConfig: &tlsConf.Server,
Addr: listenHost,
},
name: name,
store: store,
registry: registry,
url: urlStr,
tlsConf: tlsConf,
tlsInfo: tlsInfo,
peerServer: peerServer,
}
// Install the routes.
s.handleFunc("/version", s.GetVersionHandler).Methods("GET")
s.installV1()
s.installV2()
s.installMod()
return s
}
// The current state of the server in the cluster.
func (s *Server) State() string {
return s.peerServer.RaftServer().State()
}
// The node name of the leader in the cluster.
func (s *Server) Leader() string {
return s.peerServer.RaftServer().Leader()
}
// The current Raft committed index.
func (s *Server) CommitIndex() uint64 {
return s.peerServer.RaftServer().CommitIndex()
}
// The current Raft term.
func (s *Server) Term() uint64 {
return s.peerServer.RaftServer().Term()
}
// The server URL.
func (s *Server) URL() string {
return s.url
}
// Retrives the Peer URL for a given node name.
func (s *Server) PeerURL(name string) (string, bool) {
return s.registry.PeerURL(name)
}
// Returns a reference to the Store.
func (s *Server) Store() store.Store {
return s.store
}
func (s *Server) installV1() {
s.handleFuncV1("/v1/keys/{key:.*}", v1.GetKeyHandler).Methods("GET")
s.handleFuncV1("/v1/keys/{key:.*}", v1.SetKeyHandler).Methods("POST", "PUT")
s.handleFuncV1("/v1/keys/{key:.*}", v1.DeleteKeyHandler).Methods("DELETE")
s.handleFuncV1("/v1/watch/{key:.*}", v1.WatchKeyHandler).Methods("GET", "POST")
s.handleFunc("/v1/leader", s.GetLeaderHandler).Methods("GET")
s.handleFunc("/v1/machines", s.GetMachinesHandler).Methods("GET")
s.handleFunc("/v1/stats/self", s.GetStatsHandler).Methods("GET")
s.handleFunc("/v1/stats/leader", s.GetLeaderStatsHandler).Methods("GET")
s.handleFunc("/v1/stats/store", s.GetStoreStatsHandler).Methods("GET")
}
func (s *Server) installV2() {
s.handleFuncV2("/v2/keys/{key:.*}", v2.GetHandler).Methods("GET")
s.handleFuncV2("/v2/keys/{key:.*}", v2.PostHandler).Methods("POST")
s.handleFuncV2("/v2/keys/{key:.*}", v2.PutHandler).Methods("PUT")
s.handleFuncV2("/v2/keys/{key:.*}", v2.DeleteHandler).Methods("DELETE")
s.handleFunc("/v2/leader", s.GetLeaderHandler).Methods("GET")
s.handleFunc("/v2/machines", s.GetMachinesHandler).Methods("GET")
s.handleFunc("/v2/stats/self", s.GetStatsHandler).Methods("GET")
s.handleFunc("/v2/stats/leader", s.GetLeaderStatsHandler).Methods("GET")
s.handleFunc("/v2/stats/store", s.GetStoreStatsHandler).Methods("GET")
s.handleFunc("/v2/speedTest", s.SpeedTestHandler).Methods("GET")
}
func (s *Server) installMod() {
r := s.Handler.(*mux.Router)
r.PathPrefix("/mod").Handler(http.StripPrefix("/mod", mod.HttpHandler()))
}
// Adds a v1 server handler to the router.
func (s *Server) handleFuncV1(path string, f func(http.ResponseWriter, *http.Request, v1.Server) error) *mux.Route {
return s.handleFunc(path, func(w http.ResponseWriter, req *http.Request) error {
return f(w, req, s)
})
}
// Adds a v2 server handler to the router.
func (s *Server) handleFuncV2(path string, f func(http.ResponseWriter, *http.Request, v2.Server) error) *mux.Route {
return s.handleFunc(path, func(w http.ResponseWriter, req *http.Request) error {
return f(w, req, s)
})
}
// Adds a server handler to the router.
func (s *Server) handleFunc(path string, f func(http.ResponseWriter, *http.Request) error) *mux.Route {
r := s.Handler.(*mux.Router)
// Wrap the standard HandleFunc interface to pass in the server reference.
return r.HandleFunc(path, func(w http.ResponseWriter, req *http.Request) {
// Log request.
log.Debugf("[recv] %s %s %s [%s]", req.Method, s.url, req.URL.Path, req.RemoteAddr)
// Write CORS header.
if s.OriginAllowed("*") {
w.Header().Add("Access-Control-Allow-Origin", "*")
} else if origin := req.Header.Get("Origin"); s.OriginAllowed(origin) {
w.Header().Add("Access-Control-Allow-Origin", origin)
}
// Execute handler function and return error if necessary.
if err := f(w, req); err != nil {
if etcdErr, ok := err.(*etcdErr.Error); ok {
log.Debug("Return error: ", (*etcdErr).Error())
etcdErr.Write(w)
} else {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
})
}
// Start to listen and response etcd client command
func (s *Server) ListenAndServe() error {
log.Infof("etcd server [name %s, listen on %s, advertised url %s]", s.name, s.Server.Addr, s.url)
if s.tlsConf.Scheme == "http" {
return s.listenAndServe()
} else {
return s.listenAndServeTLS(s.tlsInfo.CertFile, s.tlsInfo.KeyFile)
}
}
// Overridden version of net/http added so we can manage the listener.
func (s *Server) listenAndServe() error {
addr := s.Server.Addr
if addr == "" {
addr = ":http"
}
l, e := net.Listen("tcp", addr)
if e != nil {
return e
}
s.listener = l
return s.Server.Serve(l)
}
// Overridden version of net/http added so we can manage the listener.
func (s *Server) listenAndServeTLS(certFile, keyFile string) error {
addr := s.Server.Addr
if addr == "" {
addr = ":https"
}
config := &tls.Config{}
if s.Server.TLSConfig != nil {
*config = *s.Server.TLSConfig
}
if config.NextProtos == nil {
config.NextProtos = []string{"http/1.1"}
}
var err error
config.Certificates = make([]tls.Certificate, 1)
config.Certificates[0], err = tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
return err
}
conn, err := net.Listen("tcp", addr)
if err != nil {
return err
}
tlsListener := tls.NewListener(conn, config)
s.listener = tlsListener
return s.Server.Serve(tlsListener)
}
// Stops the server.
func (s *Server) Close() {
if s.listener != nil {
s.listener.Close()
s.listener = nil
}
}
// Dispatch command to the current leader
func (s *Server) Dispatch(c raft.Command, w http.ResponseWriter, req *http.Request) error {
ps := s.peerServer
if ps.raftServer.State() == raft.Leader {
result, err := ps.raftServer.Do(c)
if err != nil {
return err
}
if result == nil {
return etcdErr.NewError(300, "Empty result from raft", s.Store().Index())
}
// response for raft related commands[join/remove]
if b, ok := result.([]byte); ok {
w.WriteHeader(http.StatusOK)
w.Write(b)
return nil
}
var b []byte
if strings.HasPrefix(req.URL.Path, "/v1") {
b, _ = json.Marshal(result.(*store.Event).Response())
w.WriteHeader(http.StatusOK)
} else {
e, _ := result.(*store.Event)
b, _ = json.Marshal(e)
// etcd index should be the same as the event index
// which is also the last modified index of the node
w.Header().Add("X-Etcd-Index", fmt.Sprint(e.Index))
w.Header().Add("X-Raft-Index", fmt.Sprint(s.CommitIndex()))
w.Header().Add("X-Raft-Term", fmt.Sprint(s.Term()))
if e.IsCreated() {
w.WriteHeader(http.StatusCreated)
} else {
w.WriteHeader(http.StatusOK)
}
}
w.Write(b)
return nil
} else {
leader := ps.raftServer.Leader()
// No leader available.
if leader == "" {
return etcdErr.NewError(300, "", s.Store().Index())
}
var url string
switch c.(type) {
case *JoinCommand, *RemoveCommand:
url, _ = ps.registry.PeerURL(leader)
default:
url, _ = ps.registry.ClientURL(leader)
}
redirect(url, w, req)
return nil
}
}
// Sets a comma-delimited list of origins that are allowed.
func (s *Server) AllowOrigins(origins []string) error {
// Construct a lookup of all origins.
m := make(map[string]bool)
for _, v := range origins {
if v != "*" {
if _, err := url.Parse(v); err != nil {
return fmt.Errorf("Invalid CORS origin: %s", err)
}
}
m[v] = true
}
s.corsOrigins = m
return nil
}
// Determines whether the server will allow a given CORS origin.
func (s *Server) OriginAllowed(origin string) bool {
return s.corsOrigins["*"] || s.corsOrigins[origin]
}
// Handler to return the current version of etcd.
func (s *Server) GetVersionHandler(w http.ResponseWriter, req *http.Request) error {
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "etcd %s", ReleaseVersion)
return nil
}
// Handler to return the current leader's raft address
func (s *Server) GetLeaderHandler(w http.ResponseWriter, req *http.Request) error {
leader := s.peerServer.RaftServer().Leader()
if leader == "" {
return etcdErr.NewError(etcdErr.EcodeLeaderElect, "", s.Store().Index())
}
w.WriteHeader(http.StatusOK)
url, _ := s.registry.PeerURL(leader)
w.Write([]byte(url))
return nil
}
// Handler to return all the known machines in the current cluster.
func (s *Server) GetMachinesHandler(w http.ResponseWriter, req *http.Request) error {
machines := s.registry.ClientURLs(s.peerServer.RaftServer().Leader(), s.name)
w.WriteHeader(http.StatusOK)
w.Write([]byte(strings.Join(machines, ", ")))
return nil
}
// Retrieves stats on the Raft server.
func (s *Server) GetStatsHandler(w http.ResponseWriter, req *http.Request) error {
w.Write(s.peerServer.Stats())
return nil
}
// Retrieves stats on the leader.
func (s *Server) GetLeaderStatsHandler(w http.ResponseWriter, req *http.Request) error {
if s.peerServer.RaftServer().State() == raft.Leader {
w.Write(s.peerServer.PeerStats())
return nil
}
leader := s.peerServer.RaftServer().Leader()
if leader == "" {
return etcdErr.NewError(300, "", s.Store().Index())
}
hostname, _ := s.registry.ClientURL(leader)
redirect(hostname, w, req)
return nil
}
// Retrieves stats on the leader.
func (s *Server) GetStoreStatsHandler(w http.ResponseWriter, req *http.Request) error {
w.Write(s.store.JsonStats())
return nil
}
// Executes a speed test to evaluate the performance of update replication.
func (s *Server) SpeedTestHandler(w http.ResponseWriter, req *http.Request) error {
count := 1000
c := make(chan bool, count)
for i := 0; i < count; i++ {
go func() {
for j := 0; j < 10; j++ {
c := s.Store().CommandFactory().CreateSetCommand("foo", "bar", time.Unix(0, 0))
s.peerServer.RaftServer().Do(c)
}
c <- true
}()
}
for i := 0; i < count; i++ {
<-c
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("speed test success"))
return nil
}

89
server/stats_queue.go Normal file
View File

@ -0,0 +1,89 @@
package server
import (
"sync"
"time"
)
const (
queueCapacity = 200
)
type statsQueue struct {
items [queueCapacity]*packageStats
size int
front int
back int
totalPkgSize int
rwl sync.RWMutex
}
func (q *statsQueue) Len() int {
return q.size
}
func (q *statsQueue) PkgSize() int {
return q.totalPkgSize
}
// FrontAndBack gets the front and back elements in the queue
// We must grab front and back together with the protection of the lock
func (q *statsQueue) frontAndBack() (*packageStats, *packageStats) {
q.rwl.RLock()
defer q.rwl.RUnlock()
if q.size != 0 {
return q.items[q.front], q.items[q.back]
}
return nil, nil
}
// Insert function insert a packageStats into the queue and update the records
func (q *statsQueue) Insert(p *packageStats) {
q.rwl.Lock()
defer q.rwl.Unlock()
q.back = (q.back + 1) % queueCapacity
if q.size == queueCapacity { //dequeue
q.totalPkgSize -= q.items[q.front].size
q.front = (q.back + 1) % queueCapacity
} else {
q.size++
}
q.items[q.back] = p
q.totalPkgSize += q.items[q.back].size
}
// Rate function returns the package rate and byte rate
func (q *statsQueue) Rate() (float64, float64) {
front, back := q.frontAndBack()
if front == nil || back == nil {
return 0, 0
}
if time.Now().Sub(back.Time()) > time.Second {
q.Clear()
return 0, 0
}
sampleDuration := back.Time().Sub(front.Time())
pr := float64(q.Len()) / float64(sampleDuration) * float64(time.Second)
br := float64(q.PkgSize()) / float64(sampleDuration) * float64(time.Second)
return pr, br
}
// Clear function clear up the statsQueue
func (q *statsQueue) Clear() {
q.rwl.Lock()
defer q.rwl.Unlock()
q.back = -1
q.front = 0
q.size = 0
q.totalPkgSize = 0
}

15
server/timeout.go Normal file
View File

@ -0,0 +1,15 @@
package server
import (
"time"
)
const (
// The amount of time to elapse without a heartbeat before becoming a candidate.
ElectionTimeout = 200 * time.Millisecond
// The frequency by which heartbeats are sent to followers.
HeartbeatTimeout = 50 * time.Millisecond
RetryInterval = 10
)

12
server/tls_config.go Normal file
View File

@ -0,0 +1,12 @@
package server
import (
"crypto/tls"
)
// TLSConfig holds the TLS configuration.
type TLSConfig struct {
Scheme string
Server tls.Config
Client tls.Config
}

76
server/tls_info.go Normal file
View File

@ -0,0 +1,76 @@
package server
import (
"crypto/tls"
"crypto/x509"
"encoding/pem"
"errors"
"io/ioutil"
)
// TLSInfo holds the SSL certificates paths.
type TLSInfo struct {
CertFile string `json:"CertFile"`
KeyFile string `json:"KeyFile"`
CAFile string `json:"CAFile"`
}
// Generates a TLS configuration from the given files.
func (info TLSInfo) Config() (TLSConfig, error) {
var t TLSConfig
t.Scheme = "http"
// If the user do not specify key file, cert file and CA file, the type will be HTTP
if info.KeyFile == "" && info.CertFile == "" && info.CAFile == "" {
return t, nil
}
// Both the key and cert must be present.
if info.KeyFile == "" || info.CertFile == "" {
return t, errors.New("KeyFile and CertFile must both be present")
}
tlsCert, err := tls.LoadX509KeyPair(info.CertFile, info.KeyFile)
if err != nil {
return t, err
}
t.Scheme = "https"
t.Server.ClientAuth, t.Server.ClientCAs, err = newCertPool(info.CAFile)
if err != nil {
return t, err
}
// The client should trust the RootCA that the Server uses since
// everyone is a peer in the network.
t.Client.Certificates = []tls.Certificate{tlsCert}
t.Client.RootCAs = t.Server.ClientCAs
return t, nil
}
// newCertPool creates x509 certPool and corresponding Auth Type.
// If the given CAfile is valid, add the cert into the pool and verify the clients'
// certs against the cert in the pool.
// If the given CAfile is empty, do not verify the clients' cert.
// If the given CAfile is not valid, fatal.
func newCertPool(CAFile string) (tls.ClientAuthType, *x509.CertPool, error) {
if CAFile == "" {
return tls.NoClientCert, nil, nil
}
pemByte, err := ioutil.ReadFile(CAFile)
if err != nil {
return 0, nil, err
}
block, pemByte := pem.Decode(pemByte)
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return 0, nil, err
}
certPool := x509.NewCertPool()
certPool.AddCert(cert)
return tls.RequireAndVerifyClientCert, certPool, nil
}

View File

@ -1,20 +1,4 @@
/*
Copyright 2013 CoreOS Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
package server
import (
"bytes"
@ -25,6 +9,7 @@ import (
"net/http"
"time"
"github.com/coreos/etcd/log"
"github.com/coreos/go-raft"
)
@ -32,24 +17,27 @@ import (
// This should not exceed 3 * RTT
var dailTimeout = 3 * HeartbeatTimeout
// Timeout for setup internal raft http connection + receive response header
// This should not exceed 3 * RTT + RTT
var responseHeaderTimeout = 4 * HeartbeatTimeout
// Timeout for setup internal raft http connection + receive all post body
// The raft server will not send back response header until it received all the
// post body.
// This should not exceed dailTimeout + electionTimeout
var responseHeaderTimeout = 3*HeartbeatTimeout + ElectionTimeout
// Timeout for receiving the response body from the server
// This should not exceed election timeout
var tranTimeout = ElectionTimeout
// This should not exceed heartbeatTimeout
var tranTimeout = HeartbeatTimeout
// Transporter layer for communication between raft nodes
type transporter struct {
client *http.Client
transport *http.Transport
client *http.Client
transport *http.Transport
peerServer *PeerServer
}
// Create transporter using by raft server
// Create http or https transporter based on
// whether the user give the server cert and key
func newTransporter(scheme string, tlsConf tls.Config) *transporter {
func newTransporter(scheme string, tlsConf tls.Config, peerServer *PeerServer) *transporter {
t := transporter{}
tr := &http.Transport{
@ -64,6 +52,7 @@ func newTransporter(scheme string, tlsConf tls.Config) *transporter {
t.client = &http.Client{Transport: tr}
t.transport = tr
t.peerServer = peerServer
return &t
}
@ -74,28 +63,28 @@ func dialWithTimeout(network, addr string) (net.Conn, error) {
}
// Sends AppendEntries RPCs to a peer when the server is the leader.
func (t *transporter) SendAppendEntriesRequest(server *raft.Server, peer *raft.Peer, req *raft.AppendEntriesRequest) *raft.AppendEntriesResponse {
func (t *transporter) SendAppendEntriesRequest(server raft.Server, peer *raft.Peer, req *raft.AppendEntriesRequest) *raft.AppendEntriesResponse {
var b bytes.Buffer
if _, err := req.Encode(&b); err != nil {
warn("transporter.ae.encoding.error:", err)
log.Warn("transporter.ae.encoding.error:", err)
return nil
}
size := b.Len()
r.serverStats.SendAppendReq(size)
t.peerServer.serverStats.SendAppendReq(size)
u, _ := nameToRaftURL(peer.Name)
u, _ := t.peerServer.registry.PeerURL(peer.Name)
debugf("Send LogEntries to %s ", u)
log.Debugf("Send LogEntries to %s ", u)
thisFollowerStats, ok := r.followersStats.Followers[peer.Name]
thisFollowerStats, ok := t.peerServer.followersStats.Followers[peer.Name]
if !ok { //this is the first time this follower has been seen
thisFollowerStats = &raftFollowerStats{}
thisFollowerStats.Latency.Minimum = 1 << 63
r.followersStats.Followers[peer.Name] = thisFollowerStats
t.peerServer.followersStats.Followers[peer.Name] = thisFollowerStats
}
start := time.Now()
@ -105,7 +94,7 @@ func (t *transporter) SendAppendEntriesRequest(server *raft.Server, peer *raft.P
end := time.Now()
if err != nil {
debugf("Cannot send AppendEntriesRequest to %s: %s", u, err)
log.Debugf("Cannot send AppendEntriesRequest to %s: %s", u, err)
if ok {
thisFollowerStats.Fail()
}
@ -123,7 +112,7 @@ func (t *transporter) SendAppendEntriesRequest(server *raft.Server, peer *raft.P
aeresp := &raft.AppendEntriesResponse{}
if _, err = aeresp.Decode(resp.Body); err != nil && err != io.EOF {
warn("transporter.ae.decoding.error:", err)
log.Warn("transporter.ae.decoding.error:", err)
return nil
}
return aeresp
@ -133,21 +122,21 @@ func (t *transporter) SendAppendEntriesRequest(server *raft.Server, peer *raft.P
}
// Sends RequestVote RPCs to a peer when the server is the candidate.
func (t *transporter) SendVoteRequest(server *raft.Server, peer *raft.Peer, req *raft.RequestVoteRequest) *raft.RequestVoteResponse {
func (t *transporter) SendVoteRequest(server raft.Server, peer *raft.Peer, req *raft.RequestVoteRequest) *raft.RequestVoteResponse {
var b bytes.Buffer
if _, err := req.Encode(&b); err != nil {
warn("transporter.vr.encoding.error:", err)
log.Warn("transporter.vr.encoding.error:", err)
return nil
}
u, _ := nameToRaftURL(peer.Name)
debugf("Send Vote from %s to %s", server.Name(), u)
u, _ := t.peerServer.registry.PeerURL(peer.Name)
log.Debugf("Send Vote from %s to %s", server.Name(), u)
resp, httpRequest, err := t.Post(fmt.Sprintf("%s/vote", u), &b)
if err != nil {
debugf("Cannot send VoteRequest to %s : %s", u, err)
log.Debugf("Cannot send VoteRequest to %s : %s", u, err)
}
if resp != nil {
@ -157,7 +146,7 @@ func (t *transporter) SendVoteRequest(server *raft.Server, peer *raft.Peer, req
rvrsp := &raft.RequestVoteResponse{}
if _, err = rvrsp.Decode(resp.Body); err != nil && err != io.EOF {
warn("transporter.vr.decoding.error:", err)
log.Warn("transporter.vr.decoding.error:", err)
return nil
}
return rvrsp
@ -166,21 +155,21 @@ func (t *transporter) SendVoteRequest(server *raft.Server, peer *raft.Peer, req
}
// Sends SnapshotRequest RPCs to a peer when the server is the candidate.
func (t *transporter) SendSnapshotRequest(server *raft.Server, peer *raft.Peer, req *raft.SnapshotRequest) *raft.SnapshotResponse {
func (t *transporter) SendSnapshotRequest(server raft.Server, peer *raft.Peer, req *raft.SnapshotRequest) *raft.SnapshotResponse {
var b bytes.Buffer
if _, err := req.Encode(&b); err != nil {
warn("transporter.ss.encoding.error:", err)
log.Warn("transporter.ss.encoding.error:", err)
return nil
}
u, _ := nameToRaftURL(peer.Name)
debugf("Send Snapshot Request from %s to %s", server.Name(), u)
u, _ := t.peerServer.registry.PeerURL(peer.Name)
log.Debugf("Send Snapshot Request from %s to %s", server.Name(), u)
resp, httpRequest, err := t.Post(fmt.Sprintf("%s/snapshot", u), &b)
if err != nil {
debugf("Cannot send Snapshot Request to %s : %s", u, err)
log.Debugf("Cannot send Snapshot Request to %s : %s", u, err)
}
if resp != nil {
@ -190,7 +179,7 @@ func (t *transporter) SendSnapshotRequest(server *raft.Server, peer *raft.Peer,
ssrsp := &raft.SnapshotResponse{}
if _, err = ssrsp.Decode(resp.Body); err != nil && err != io.EOF {
warn("transporter.ss.decoding.error:", err)
log.Warn("transporter.ss.decoding.error:", err)
return nil
}
return ssrsp
@ -199,21 +188,21 @@ func (t *transporter) SendSnapshotRequest(server *raft.Server, peer *raft.Peer,
}
// Sends SnapshotRecoveryRequest RPCs to a peer when the server is the candidate.
func (t *transporter) SendSnapshotRecoveryRequest(server *raft.Server, peer *raft.Peer, req *raft.SnapshotRecoveryRequest) *raft.SnapshotRecoveryResponse {
func (t *transporter) SendSnapshotRecoveryRequest(server raft.Server, peer *raft.Peer, req *raft.SnapshotRecoveryRequest) *raft.SnapshotRecoveryResponse {
var b bytes.Buffer
if _, err := req.Encode(&b); err != nil {
warn("transporter.ss.encoding.error:", err)
log.Warn("transporter.ss.encoding.error:", err)
return nil
}
u, _ := nameToRaftURL(peer.Name)
debugf("Send Snapshot Recovery from %s to %s", server.Name(), u)
u, _ := t.peerServer.registry.PeerURL(peer.Name)
log.Debugf("Send Snapshot Recovery from %s to %s", server.Name(), u)
resp, httpRequest, err := t.Post(fmt.Sprintf("%s/snapshotRecovery", u), &b)
if err != nil {
debugf("Cannot send Snapshot Recovery to %s : %s", u, err)
log.Debugf("Cannot send Snapshot Recovery to %s : %s", u, err)
}
if resp != nil {
@ -223,7 +212,7 @@ func (t *transporter) SendSnapshotRecoveryRequest(server *raft.Server, peer *raf
ssrrsp := &raft.SnapshotRecoveryResponse{}
if _, err = ssrrsp.Decode(resp.Body); err != nil && err != io.EOF {
warn("transporter.ssr.decoding.error:", err)
log.Warn("transporter.ssr.decoding.error:", err)
return nil
}
return ssrrsp

View File

@ -14,8 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package main
package server
/*
import (
"crypto/tls"
"fmt"
@ -37,7 +38,7 @@ func TestTransporterTimeout(t *testing.T) {
conf := tls.Config{}
ts := newTransporter("http", conf)
ts := newTransporter("http", conf, nil)
ts.Get("http://google.com")
_, _, err := ts.Get("http://google.com:9999")
@ -75,3 +76,4 @@ func TestTransporterTimeout(t *testing.T) {
}
}
*/

46
server/util.go Normal file
View File

@ -0,0 +1,46 @@
package server
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"github.com/coreos/etcd/log"
)
func decodeJsonRequest(req *http.Request, data interface{}) error {
decoder := json.NewDecoder(req.Body)
if err := decoder.Decode(&data); err != nil && err != io.EOF {
log.Warnf("Malformed json request: %v", err)
return fmt.Errorf("Malformed json request: %v", err)
}
return nil
}
func redirect(hostname string, w http.ResponseWriter, req *http.Request) {
originalURL := req.URL
redirectURL, _ := url.Parse(hostname)
// we need the original path and raw query
redirectURL.Path = originalURL.Path
redirectURL.RawQuery = originalURL.RawQuery
redirectURL.Fragment = originalURL.Fragment
log.Debugf("Redirect to %s", redirectURL.String())
http.Redirect(w, req, redirectURL.String(), http.StatusTemporaryRedirect)
}
// trimsplit slices s into all substrings separated by sep and returns a
// slice of the substrings between the separator with all leading and trailing
// white space removed, as defined by Unicode.
func trimsplit(s, sep string) []string {
raw := strings.Split(s, ",")
trimmed := make([]string, 0)
for _, r := range raw {
trimmed = append(trimmed, strings.TrimSpace(r))
}
return trimmed
}

View File

@ -0,0 +1,14 @@
package v1
import (
"github.com/gorilla/mux"
"net/http"
)
// Removes a key from the store.
func DeleteKeyHandler(w http.ResponseWriter, req *http.Request, s Server) error {
vars := mux.Vars(req)
key := "/" + vars["key"]
c := s.Store().CommandFactory().CreateDeleteCommand(key, false)
return s.Dispatch(c, w, req)
}

View File

@ -0,0 +1,27 @@
package v1
import (
"encoding/json"
"net/http"
"github.com/gorilla/mux"
)
// Retrieves the value for a given key.
func GetKeyHandler(w http.ResponseWriter, req *http.Request, s Server) error {
vars := mux.Vars(req)
key := "/" + vars["key"]
// Retrieve the key from the store.
event, err := s.Store().Get(key, false, false)
if err != nil {
return err
}
// Convert event to a response and write to client.
b, _ := json.Marshal(event.Response())
w.WriteHeader(http.StatusOK)
w.Write(b)
return nil
}

View File

@ -0,0 +1,47 @@
package v1
import (
"net/http"
etcdErr "github.com/coreos/etcd/error"
"github.com/coreos/etcd/store"
"github.com/coreos/go-raft"
"github.com/gorilla/mux"
)
// Sets the value for a given key.
func SetKeyHandler(w http.ResponseWriter, req *http.Request, s Server) error {
vars := mux.Vars(req)
key := "/" + vars["key"]
req.ParseForm()
// Parse non-blank value.
value := req.Form.Get("value")
if len(value) == 0 {
return etcdErr.NewError(200, "Set", s.Store().Index())
}
// Convert time-to-live to an expiration time.
expireTime, err := store.TTL(req.Form.Get("ttl"))
if err != nil {
return etcdErr.NewError(202, "Set", s.Store().Index())
}
// If the "prevValue" is specified then test-and-set. Otherwise create a new key.
var c raft.Command
if prevValueArr, ok := req.Form["prevValue"]; ok {
if len(prevValueArr[0]) > 0 {
// test against previous value
c = s.Store().CommandFactory().CreateCompareAndSwapCommand(key, value, prevValueArr[0], 0, expireTime)
} else {
// test against existence
c = s.Store().CommandFactory().CreateCreateCommand(key, value, expireTime, false)
}
} else {
c = s.Store().CommandFactory().CreateSetCommand(key, value, expireTime)
}
return s.Dispatch(c, w, req)
}

15
server/v1/v1.go Normal file
View File

@ -0,0 +1,15 @@
package v1
import (
"github.com/coreos/etcd/store"
"github.com/coreos/go-raft"
"net/http"
)
// The Server interface provides all the methods required for the v1 API.
type Server interface {
CommitIndex() uint64
Term() uint64
Store() store.Store
Dispatch(raft.Command, http.ResponseWriter, *http.Request) error
}

View File

@ -0,0 +1,39 @@
package v1
import (
"encoding/json"
"net/http"
"strconv"
etcdErr "github.com/coreos/etcd/error"
"github.com/gorilla/mux"
)
// Watches a given key prefix for changes.
func WatchKeyHandler(w http.ResponseWriter, req *http.Request, s Server) error {
var err error
vars := mux.Vars(req)
key := "/" + vars["key"]
// Create a command to watch from a given index (default 0).
var sinceIndex uint64 = 0
if req.Method == "POST" {
sinceIndex, err = strconv.ParseUint(string(req.FormValue("index")), 10, 64)
if err != nil {
return etcdErr.NewError(203, "Watch From Index", s.Store().Index())
}
}
// Start the watcher on the store.
c, err := s.Store().Watch(key, false, sinceIndex)
if err != nil {
return etcdErr.NewError(500, key, s.Store().Index())
}
event := <-c
b, _ := json.Marshal(event.Response())
w.WriteHeader(http.StatusOK)
w.Write(b)
return nil
}

View File

@ -0,0 +1,16 @@
package v2
import (
"net/http"
"github.com/gorilla/mux"
)
func DeleteHandler(w http.ResponseWriter, req *http.Request, s Server) error {
vars := mux.Vars(req)
key := "/" + vars["key"]
recursive := (req.FormValue("recursive") == "true")
c := s.Store().CommandFactory().CreateDeleteCommand(key, recursive)
return s.Dispatch(c, w, req)
}

80
server/v2/get_handler.go Normal file
View File

@ -0,0 +1,80 @@
package v2
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
etcdErr "github.com/coreos/etcd/error"
"github.com/coreos/etcd/log"
"github.com/coreos/etcd/store"
"github.com/coreos/go-raft"
"github.com/gorilla/mux"
)
func GetHandler(w http.ResponseWriter, req *http.Request, s Server) error {
var err error
var event *store.Event
vars := mux.Vars(req)
key := "/" + vars["key"]
// Help client to redirect the request to the current leader
if req.FormValue("consistent") == "true" && s.State() != raft.Leader {
leader := s.Leader()
hostname, _ := s.PeerURL(leader)
url := hostname + req.URL.Path
log.Debugf("Redirect consistent get to %s", url)
http.Redirect(w, req, url, http.StatusTemporaryRedirect)
return nil
}
recursive := (req.FormValue("recursive") == "true")
sorted := (req.FormValue("sorted") == "true")
if req.FormValue("wait") == "true" { // watch
// Create a command to watch from a given index (default 0).
var sinceIndex uint64 = 0
waitIndex := req.FormValue("waitIndex")
if waitIndex != "" {
sinceIndex, err = strconv.ParseUint(string(req.FormValue("waitIndex")), 10, 64)
if err != nil {
return etcdErr.NewError(etcdErr.EcodeIndexNaN, "Watch From Index", s.Store().Index())
}
}
// Start the watcher on the store.
eventChan, err := s.Store().Watch(key, recursive, sinceIndex)
if err != nil {
return etcdErr.NewError(500, key, s.Store().Index())
}
cn, _ := w.(http.CloseNotifier)
closeChan := cn.CloseNotify()
select {
case <-closeChan:
return nil
case event = <-eventChan:
}
} else { //get
// Retrieve the key from the store.
event, err = s.Store().Get(key, recursive, sorted)
if err != nil {
return err
}
}
w.Header().Add("X-Etcd-Index", fmt.Sprint(s.Store().Index()))
w.Header().Add("X-Raft-Index", fmt.Sprint(s.CommitIndex()))
w.Header().Add("X-Raft-Term", fmt.Sprint(s.Term()))
w.WriteHeader(http.StatusOK)
b, _ := json.Marshal(event)
w.Write(b)
return nil
}

23
server/v2/post_handler.go Normal file
View File

@ -0,0 +1,23 @@
package v2
import (
"net/http"
etcdErr "github.com/coreos/etcd/error"
"github.com/coreos/etcd/store"
"github.com/gorilla/mux"
)
func PostHandler(w http.ResponseWriter, req *http.Request, s Server) error {
vars := mux.Vars(req)
key := "/" + vars["key"]
value := req.FormValue("value")
expireTime, err := store.TTL(req.FormValue("ttl"))
if err != nil {
return etcdErr.NewError(etcdErr.EcodeTTLNaN, "Create", s.Store().Index())
}
c := s.Store().CommandFactory().CreateCreateCommand(key, value, expireTime, true)
return s.Dispatch(c, w, req)
}

96
server/v2/put_handler.go Normal file
View File

@ -0,0 +1,96 @@
package v2
import (
"net/http"
"strconv"
"time"
etcdErr "github.com/coreos/etcd/error"
"github.com/coreos/etcd/store"
"github.com/coreos/go-raft"
"github.com/gorilla/mux"
)
func PutHandler(w http.ResponseWriter, req *http.Request, s Server) error {
var c raft.Command
vars := mux.Vars(req)
key := "/" + vars["key"]
req.ParseForm()
value := req.Form.Get("value")
expireTime, err := store.TTL(req.Form.Get("ttl"))
if err != nil {
return etcdErr.NewError(etcdErr.EcodeTTLNaN, "Update", s.Store().Index())
}
_, valueOk := req.Form["prevValue"]
prevValue := req.Form.Get("prevValue")
_, indexOk := req.Form["prevIndex"]
prevIndexStr := req.Form.Get("prevIndex")
_, existOk := req.Form["prevExist"]
prevExist := req.Form.Get("prevExist")
// Set handler: create a new node or replace the old one.
if !valueOk && !indexOk && !existOk {
return SetHandler(w, req, s, key, value, expireTime)
}
// update with test
if existOk {
if prevExist == "false" {
// Create command: create a new node. Fail, if a node already exists
// Ignore prevIndex and prevValue
return CreateHandler(w, req, s, key, value, expireTime)
}
if prevExist == "true" && !indexOk && !valueOk {
return UpdateHandler(w, req, s, key, value, expireTime)
}
}
var prevIndex uint64
if indexOk {
prevIndex, err = strconv.ParseUint(prevIndexStr, 10, 64)
// bad previous index
if err != nil {
return etcdErr.NewError(etcdErr.EcodeIndexNaN, "CompareAndSwap", s.Store().Index())
}
} else {
prevIndex = 0
}
if valueOk {
if prevValue == "" {
return etcdErr.NewError(etcdErr.EcodePrevValueRequired, "CompareAndSwap", s.Store().Index())
}
}
c = s.Store().CommandFactory().CreateCompareAndSwapCommand(key, value, prevValue, prevIndex, expireTime)
return s.Dispatch(c, w, req)
}
func SetHandler(w http.ResponseWriter, req *http.Request, s Server, key, value string, expireTime time.Time) error {
c := s.Store().CommandFactory().CreateSetCommand(key, value, expireTime)
return s.Dispatch(c, w, req)
}
func CreateHandler(w http.ResponseWriter, req *http.Request, s Server, key, value string, expireTime time.Time) error {
c := s.Store().CommandFactory().CreateCreateCommand(key, value, expireTime, false)
return s.Dispatch(c, w, req)
}
func UpdateHandler(w http.ResponseWriter, req *http.Request, s Server, key, value string, expireTime time.Time) error {
// Update should give at least one option
if value == "" && expireTime.Sub(store.Permanent) == 0 {
return etcdErr.NewError(etcdErr.EcodeValueOrTTLRequired, "Update", s.Store().Index())
}
c := s.Store().CommandFactory().CreateUpdateCommand(key, value, expireTime)
return s.Dispatch(c, w, req)
}

View File

@ -0,0 +1,29 @@
package v2
import (
"fmt"
"net/url"
"testing"
"github.com/coreos/etcd/server"
"github.com/coreos/etcd/tests"
"github.com/stretchr/testify/assert"
)
// Ensures that a key is deleted.
//
// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=XXX
// $ curl -X DELETE localhost:4001/v2/keys/foo/bar
//
func TestV2DeleteKey(t *testing.T) {
tests.RunServer(func(s *server.Server) {
v := url.Values{}
v.Set("value", "XXX")
resp, err := tests.PutForm(fmt.Sprintf("http://%s%s", s.URL(), "/v2/keys/foo/bar"), v)
tests.ReadBody(resp)
resp, err = tests.DeleteForm(fmt.Sprintf("http://%s%s", s.URL(), "/v2/keys/foo/bar"), url.Values{})
body := tests.ReadBody(resp)
assert.Nil(t, err, "")
assert.Equal(t, string(body), `{"action":"delete","key":"/foo/bar","prevValue":"XXX","modifiedIndex":2}`, "")
})
}

View File

@ -0,0 +1,169 @@
package v2
import (
"fmt"
"net/url"
"testing"
"time"
"github.com/coreos/etcd/server"
"github.com/coreos/etcd/tests"
"github.com/stretchr/testify/assert"
)
// Ensures that a value can be retrieve for a given key.
//
// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=XXX
// $ curl localhost:4001/v2/keys/foo/bar
//
func TestV2GetKey(t *testing.T) {
tests.RunServer(func(s *server.Server) {
v := url.Values{}
v.Set("value", "XXX")
resp, _ := tests.PutForm(fmt.Sprintf("http://%s%s", s.URL(), "/v2/keys/foo/bar"), v)
tests.ReadBody(resp)
resp, _ = tests.Get(fmt.Sprintf("http://%s%s", s.URL(), "/v2/keys/foo/bar"))
body := tests.ReadBodyJSON(resp)
assert.Equal(t, body["action"], "get", "")
assert.Equal(t, body["key"], "/foo/bar", "")
assert.Equal(t, body["value"], "XXX", "")
assert.Equal(t, body["modifiedIndex"], 1, "")
})
}
// Ensures that a directory of values can be recursively retrieved for a given key.
//
// $ curl -X PUT localhost:4001/v2/keys/foo/x -d value=XXX
// $ curl -X PUT localhost:4001/v2/keys/foo/y/z -d value=YYY
// $ curl localhost:4001/v2/keys/foo -d recursive=true
//
func TestV2GetKeyRecursively(t *testing.T) {
tests.RunServer(func(s *server.Server) {
v := url.Values{}
v.Set("value", "XXX")
v.Set("ttl", "10")
resp, _ := tests.PutForm(fmt.Sprintf("http://%s%s", s.URL(), "/v2/keys/foo/x"), v)
tests.ReadBody(resp)
v.Set("value", "YYY")
resp, _ = tests.PutForm(fmt.Sprintf("http://%s%s", s.URL(), "/v2/keys/foo/y/z"), v)
tests.ReadBody(resp)
resp, _ = tests.Get(fmt.Sprintf("http://%s%s", s.URL(), "/v2/keys/foo?recursive=true"))
body := tests.ReadBodyJSON(resp)
assert.Equal(t, body["action"], "get", "")
assert.Equal(t, body["key"], "/foo", "")
assert.Equal(t, body["dir"], true, "")
assert.Equal(t, body["modifiedIndex"], 1, "")
assert.Equal(t, len(body["kvs"].([]interface{})), 2, "")
kv0 := body["kvs"].([]interface{})[0].(map[string]interface{})
assert.Equal(t, kv0["key"], "/foo/x", "")
assert.Equal(t, kv0["value"], "XXX", "")
assert.Equal(t, kv0["ttl"], 10, "")
kv1 := body["kvs"].([]interface{})[1].(map[string]interface{})
assert.Equal(t, kv1["key"], "/foo/y", "")
assert.Equal(t, kv1["dir"], true, "")
kvs2 := kv1["kvs"].([]interface{})[0].(map[string]interface{})
assert.Equal(t, kvs2["key"], "/foo/y/z", "")
assert.Equal(t, kvs2["value"], "YYY", "")
})
}
// Ensures that a watcher can wait for a value to be set and return it to the client.
//
// $ curl localhost:4001/v2/keys/foo/bar?wait=true
// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=XXX
//
func TestV2WatchKey(t *testing.T) {
tests.RunServer(func(s *server.Server) {
var body map[string]interface{}
c := make(chan bool)
go func() {
resp, _ := tests.Get(fmt.Sprintf("http://%s%s", s.URL(), "/v2/keys/foo/bar?wait=true"))
body = tests.ReadBodyJSON(resp)
c <- true
}()
// Make sure response didn't fire early.
time.Sleep(1 * time.Millisecond)
assert.Nil(t, body, "")
// Set a value.
v := url.Values{}
v.Set("value", "XXX")
resp, _ := tests.PutForm(fmt.Sprintf("http://%s%s", s.URL(), "/v2/keys/foo/bar"), v)
tests.ReadBody(resp)
// A response should follow from the GET above.
time.Sleep(1 * time.Millisecond)
select {
case <-c:
default:
t.Fatal("cannot get watch result")
}
assert.NotNil(t, body, "")
assert.Equal(t, body["action"], "set", "")
assert.Equal(t, body["key"], "/foo/bar", "")
assert.Equal(t, body["value"], "XXX", "")
assert.Equal(t, body["modifiedIndex"], 1, "")
})
}
// Ensures that a watcher can wait for a value to be set after a given index.
//
// $ curl localhost:4001/v2/keys/foo/bar?wait=true&waitIndex=4
// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=XXX
// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=YYY
//
func TestV2WatchKeyWithIndex(t *testing.T) {
tests.RunServer(func(s *server.Server) {
var body map[string]interface{}
c := make(chan bool)
go func() {
resp, _ := tests.Get(fmt.Sprintf("http://%s%s", s.URL(), "/v2/keys/foo/bar?wait=true&waitIndex=2"))
body = tests.ReadBodyJSON(resp)
c <- true
}()
// Make sure response didn't fire early.
time.Sleep(1 * time.Millisecond)
assert.Nil(t, body, "")
// Set a value (before given index).
v := url.Values{}
v.Set("value", "XXX")
resp, _ := tests.PutForm(fmt.Sprintf("http://%s%s", s.URL(), "/v2/keys/foo/bar"), v)
tests.ReadBody(resp)
// Make sure response didn't fire early.
time.Sleep(1 * time.Millisecond)
assert.Nil(t, body, "")
// Set a value (before given index).
v.Set("value", "YYY")
resp, _ = tests.PutForm(fmt.Sprintf("http://%s%s", s.URL(), "/v2/keys/foo/bar"), v)
tests.ReadBody(resp)
// A response should follow from the GET above.
time.Sleep(1 * time.Millisecond)
select {
case <-c:
default:
t.Fatal("cannot get watch result")
}
assert.NotNil(t, body, "")
assert.Equal(t, body["action"], "set", "")
assert.Equal(t, body["key"], "/foo/bar", "")
assert.Equal(t, body["value"], "YYY", "")
assert.Equal(t, body["modifiedIndex"], 2, "")
})
}

View File

@ -0,0 +1,38 @@
package v2
import (
"fmt"
"testing"
"github.com/coreos/etcd/server"
"github.com/coreos/etcd/tests"
"github.com/stretchr/testify/assert"
)
// Ensures a unique value is added to the key's children.
//
// $ curl -X POST localhost:4001/v2/keys/foo/bar
// $ curl -X POST localhost:4001/v2/keys/foo/bar
// $ curl -X POST localhost:4001/v2/keys/foo/baz
//
func TestV2CreateUnique(t *testing.T) {
tests.RunServer(func(s *server.Server) {
// POST should add index to list.
resp, _ := tests.PostForm(fmt.Sprintf("http://%s%s", s.URL(), "/v2/keys/foo/bar"), nil)
body := tests.ReadBodyJSON(resp)
assert.Equal(t, body["action"], "create", "")
assert.Equal(t, body["key"], "/foo/bar/1", "")
assert.Equal(t, body["dir"], true, "")
assert.Equal(t, body["modifiedIndex"], 1, "")
// Second POST should add next index to list.
resp, _ = tests.PostForm(fmt.Sprintf("http://%s%s", s.URL(), "/v2/keys/foo/bar"), nil)
body = tests.ReadBodyJSON(resp)
assert.Equal(t, body["key"], "/foo/bar/2", "")
// POST to a different key should add index to that list.
resp, _ = tests.PostForm(fmt.Sprintf("http://%s%s", s.URL(), "/v2/keys/foo/baz"), nil)
body = tests.ReadBodyJSON(resp)
assert.Equal(t, body["key"], "/foo/baz/3", "")
})
}

View File

@ -0,0 +1,280 @@
package v2
import (
"fmt"
"net/url"
"testing"
"time"
"github.com/coreos/etcd/server"
"github.com/coreos/etcd/tests"
"github.com/stretchr/testify/assert"
)
// Ensures that a key is set to a given value.
//
// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=XXX
//
func TestV2SetKey(t *testing.T) {
tests.RunServer(func(s *server.Server) {
v := url.Values{}
v.Set("value", "XXX")
resp, err := tests.PutForm(fmt.Sprintf("http://%s%s", s.URL(), "/v2/keys/foo/bar"), v)
body := tests.ReadBody(resp)
assert.Nil(t, err, "")
assert.Equal(t, string(body), `{"action":"set","key":"/foo/bar","value":"XXX","modifiedIndex":1}`, "")
})
}
// Ensures that a time-to-live is added to a key.
//
// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=XXX -d ttl=20
//
func TestV2SetKeyWithTTL(t *testing.T) {
tests.RunServer(func(s *server.Server) {
t0 := time.Now()
v := url.Values{}
v.Set("value", "XXX")
v.Set("ttl", "20")
resp, _ := tests.PutForm(fmt.Sprintf("http://%s%s", s.URL(), "/v2/keys/foo/bar"), v)
body := tests.ReadBodyJSON(resp)
assert.Equal(t, body["ttl"], 20, "")
// Make sure the expiration date is correct.
expiration, _ := time.Parse(time.RFC3339Nano, body["expiration"].(string))
assert.Equal(t, expiration.Sub(t0)/time.Second, 20, "")
})
}
// Ensures that an invalid time-to-live is returned as an error.
//
// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=XXX -d ttl=bad_ttl
//
func TestV2SetKeyWithBadTTL(t *testing.T) {
tests.RunServer(func(s *server.Server) {
v := url.Values{}
v.Set("value", "XXX")
v.Set("ttl", "bad_ttl")
resp, _ := tests.PutForm(fmt.Sprintf("http://%s%s", s.URL(), "/v2/keys/foo/bar"), v)
body := tests.ReadBodyJSON(resp)
assert.Equal(t, body["errorCode"], 202, "")
assert.Equal(t, body["message"], "The given TTL in POST form is not a number", "")
assert.Equal(t, body["cause"], "Update", "")
})
}
// Ensures that a key is conditionally set only if it previously did not exist.
//
// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=XXX -d prevExist=false
//
func TestV2CreateKeySuccess(t *testing.T) {
tests.RunServer(func(s *server.Server) {
v := url.Values{}
v.Set("value", "XXX")
v.Set("prevExist", "false")
resp, _ := tests.PutForm(fmt.Sprintf("http://%s%s", s.URL(), "/v2/keys/foo/bar"), v)
body := tests.ReadBodyJSON(resp)
assert.Equal(t, body["value"], "XXX", "")
})
}
// Ensures that a key is not conditionally because it previously existed.
//
// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=XXX
// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=XXX -d prevExist=false
//
func TestV2CreateKeyFail(t *testing.T) {
tests.RunServer(func(s *server.Server) {
v := url.Values{}
v.Set("value", "XXX")
v.Set("prevExist", "false")
resp, _ := tests.PutForm(fmt.Sprintf("http://%s%s", s.URL(), "/v2/keys/foo/bar"), v)
tests.ReadBody(resp)
resp, _ = tests.PutForm(fmt.Sprintf("http://%s%s", s.URL(), "/v2/keys/foo/bar"), v)
body := tests.ReadBodyJSON(resp)
assert.Equal(t, body["errorCode"], 105, "")
assert.Equal(t, body["message"], "Already exists", "")
assert.Equal(t, body["cause"], "/foo/bar", "")
})
}
// Ensures that a key is conditionally set only if it previously did exist.
//
// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=XXX
// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=YYY -d prevExist=true
//
func TestV2UpdateKeySuccess(t *testing.T) {
tests.RunServer(func(s *server.Server) {
v := url.Values{}
v.Set("value", "XXX")
resp, _ := tests.PutForm(fmt.Sprintf("http://%s%s", s.URL(), "/v2/keys/foo/bar"), v)
tests.ReadBody(resp)
v.Set("value", "YYY")
v.Set("prevExist", "true")
resp, _ = tests.PutForm(fmt.Sprintf("http://%s%s", s.URL(), "/v2/keys/foo/bar"), v)
body := tests.ReadBodyJSON(resp)
assert.Equal(t, body["action"], "update", "")
assert.Equal(t, body["prevValue"], "XXX", "")
})
}
// Ensures that a key is not conditionally set if it previously did not exist.
//
// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=XXX -d prevExist=true
//
func TestV2UpdateKeyFailOnValue(t *testing.T) {
tests.RunServer(func(s *server.Server) {
v := url.Values{}
resp, _ := tests.PutForm(fmt.Sprintf("http://%s%s", s.URL(), "/v2/keys/foo"), v)
v.Set("value", "YYY")
v.Set("prevExist", "true")
resp, _ = tests.PutForm(fmt.Sprintf("http://%s%s", s.URL(), "/v2/keys/foo/bar"), v)
body := tests.ReadBodyJSON(resp)
assert.Equal(t, body["errorCode"], 100, "")
assert.Equal(t, body["message"], "Key Not Found", "")
assert.Equal(t, body["cause"], "/foo/bar", "")
})
}
// Ensures that a key is not conditionally set if it previously did not exist.
//
// $ curl -X PUT localhost:4001/v2/keys/foo -d value=XXX -d prevExist=true
// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=XXX -d prevExist=true
//
func TestV2UpdateKeyFailOnMissingDirectory(t *testing.T) {
tests.RunServer(func(s *server.Server) {
v := url.Values{}
v.Set("value", "YYY")
v.Set("prevExist", "true")
resp, _ := tests.PutForm(fmt.Sprintf("http://%s%s", s.URL(), "/v2/keys/foo/bar"), v)
body := tests.ReadBodyJSON(resp)
assert.Equal(t, body["errorCode"], 100, "")
assert.Equal(t, body["message"], "Key Not Found", "")
assert.Equal(t, body["cause"], "/foo", "")
})
}
// Ensures that a key is set only if the previous index matches.
//
// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=XXX
// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=YYY -d prevIndex=1
//
func TestV2SetKeyCASOnIndexSuccess(t *testing.T) {
tests.RunServer(func(s *server.Server) {
v := url.Values{}
v.Set("value", "XXX")
resp, _ := tests.PutForm(fmt.Sprintf("http://%s%s", s.URL(), "/v2/keys/foo/bar"), v)
tests.ReadBody(resp)
v.Set("value", "YYY")
v.Set("prevIndex", "1")
resp, _ = tests.PutForm(fmt.Sprintf("http://%s%s", s.URL(), "/v2/keys/foo/bar"), v)
body := tests.ReadBodyJSON(resp)
assert.Equal(t, body["action"], "compareAndSwap", "")
assert.Equal(t, body["prevValue"], "XXX", "")
assert.Equal(t, body["value"], "YYY", "")
assert.Equal(t, body["modifiedIndex"], 2, "")
})
}
// Ensures that a key is not set if the previous index does not match.
//
// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=XXX
// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=YYY -d prevIndex=10
//
func TestV2SetKeyCASOnIndexFail(t *testing.T) {
tests.RunServer(func(s *server.Server) {
v := url.Values{}
v.Set("value", "XXX")
resp, _ := tests.PutForm(fmt.Sprintf("http://%s%s", s.URL(), "/v2/keys/foo/bar"), v)
tests.ReadBody(resp)
v.Set("value", "YYY")
v.Set("prevIndex", "10")
resp, _ = tests.PutForm(fmt.Sprintf("http://%s%s", s.URL(), "/v2/keys/foo/bar"), v)
body := tests.ReadBodyJSON(resp)
assert.Equal(t, body["errorCode"], 101, "")
assert.Equal(t, body["message"], "Test Failed", "")
assert.Equal(t, body["cause"], "[ != XXX] [10 != 1]", "")
assert.Equal(t, body["index"], 1, "")
})
}
// Ensures that an error is thrown if an invalid previous index is provided.
//
// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=YYY -d prevIndex=bad_index
//
func TestV2SetKeyCASWithInvalidIndex(t *testing.T) {
tests.RunServer(func(s *server.Server) {
v := url.Values{}
v.Set("value", "YYY")
v.Set("prevIndex", "bad_index")
resp, _ := tests.PutForm(fmt.Sprintf("http://%s%s", s.URL(), "/v2/keys/foo/bar"), v)
body := tests.ReadBodyJSON(resp)
assert.Equal(t, body["errorCode"], 203, "")
assert.Equal(t, body["message"], "The given index in POST form is not a number", "")
assert.Equal(t, body["cause"], "CompareAndSwap", "")
})
}
// Ensures that a key is set only if the previous value matches.
//
// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=XXX
// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=YYY -d prevValue=XXX
//
func TestV2SetKeyCASOnValueSuccess(t *testing.T) {
tests.RunServer(func(s *server.Server) {
v := url.Values{}
v.Set("value", "XXX")
resp, _ := tests.PutForm(fmt.Sprintf("http://%s%s", s.URL(), "/v2/keys/foo/bar"), v)
tests.ReadBody(resp)
v.Set("value", "YYY")
v.Set("prevValue", "XXX")
resp, _ = tests.PutForm(fmt.Sprintf("http://%s%s", s.URL(), "/v2/keys/foo/bar"), v)
body := tests.ReadBodyJSON(resp)
assert.Equal(t, body["action"], "compareAndSwap", "")
assert.Equal(t, body["prevValue"], "XXX", "")
assert.Equal(t, body["value"], "YYY", "")
assert.Equal(t, body["modifiedIndex"], 2, "")
})
}
// Ensures that a key is not set if the previous value does not match.
//
// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=XXX
// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=YYY -d prevValue=AAA
//
func TestV2SetKeyCASOnValueFail(t *testing.T) {
tests.RunServer(func(s *server.Server) {
v := url.Values{}
v.Set("value", "XXX")
resp, _ := tests.PutForm(fmt.Sprintf("http://%s%s", s.URL(), "/v2/keys/foo/bar"), v)
tests.ReadBody(resp)
v.Set("value", "YYY")
v.Set("prevValue", "AAA")
resp, _ = tests.PutForm(fmt.Sprintf("http://%s%s", s.URL(), "/v2/keys/foo/bar"), v)
body := tests.ReadBodyJSON(resp)
assert.Equal(t, body["errorCode"], 101, "")
assert.Equal(t, body["message"], "Test Failed", "")
assert.Equal(t, body["cause"], "[AAA != XXX] [0 != 1]", "")
assert.Equal(t, body["index"], 1, "")
})
}
// Ensures that an error is returned if a blank prevValue is set.
//
// $ curl -X PUT localhost:4001/v2/keys/foo/bar -d value=XXX -d prevValue=
//
func TestV2SetKeyCASWithMissingValueFails(t *testing.T) {
tests.RunServer(func(s *server.Server) {
v := url.Values{}
v.Set("value", "XXX")
v.Set("prevValue", "")
resp, _ := tests.PutForm(fmt.Sprintf("http://%s%s", s.URL(), "/v2/keys/foo/bar"), v)
body := tests.ReadBodyJSON(resp)
assert.Equal(t, body["errorCode"], 201, "")
assert.Equal(t, body["message"], "PrevValue is Required in POST form", "")
assert.Equal(t, body["cause"], "CompareAndSwap", "")
})
}

18
server/v2/v2.go Normal file
View File

@ -0,0 +1,18 @@
package v2
import (
"github.com/coreos/etcd/store"
"github.com/coreos/go-raft"
"net/http"
)
// The Server interface provides all the methods required for the v2 API.
type Server interface {
State() string
Leader() string
CommitIndex() uint64
Term() uint64
PeerURL(string) (string, bool)
Store() store.Store
Dispatch(raft.Command, http.ResponseWriter, *http.Request) error
}

3
server/version.go Normal file
View File

@ -0,0 +1,3 @@
package server
const Version = "v2"

View File

@ -1,52 +0,0 @@
/*
Copyright 2013 CoreOS Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"time"
)
// basic conf.
// TODO: find a good policy to do snapshot
type snapshotConf struct {
// Etcd will check if snapshot is need every checkingInterval
checkingInterval time.Duration
// The number of writes when the last snapshot happened
lastWrites uint64
// If the incremental number of writes since the last snapshot
// exceeds the write Threshold, etcd will do a snapshot
writesThr uint64
}
var snapConf *snapshotConf
func newSnapshotConf() *snapshotConf {
// check snapshot every 3 seconds and the threshold is 20K
return &snapshotConf{time.Second * 3, etcdStore.TotalWrites(), 20 * 1000}
}
func monitorSnapshot() {
for {
time.Sleep(snapConf.checkingInterval)
currentWrites := etcdStore.TotalWrites() - snapConf.lastWrites
if currentWrites > snapConf.writesThr {
r.TakeSnapshot()
snapConf.lastWrites = etcdStore.TotalWrites()
}
}
}

59
store/command_factory.go Normal file
View File

@ -0,0 +1,59 @@
package store
import (
"fmt"
"time"
"github.com/coreos/go-raft"
)
// A lookup of factories by version.
var factories = make(map[int]CommandFactory)
var minVersion, maxVersion int
// The CommandFactory provides a way to create different types of commands
// depending on the current version of the store.
type CommandFactory interface {
Version() int
CreateUpgradeCommand() raft.Command
CreateSetCommand(key string, value string, expireTime time.Time) raft.Command
CreateCreateCommand(key string, value string, expireTime time.Time, unique bool) raft.Command
CreateUpdateCommand(key string, value string, expireTime time.Time) raft.Command
CreateDeleteCommand(key string, recursive bool) raft.Command
CreateCompareAndSwapCommand(key string, value string, prevValue string, prevIndex uint64, expireTime time.Time) raft.Command
CreateSyncCommand(now time.Time) raft.Command
}
// RegisterCommandFactory adds a command factory to the global registry.
func RegisterCommandFactory(factory CommandFactory) {
version := factory.Version()
if GetCommandFactory(version) != nil {
panic(fmt.Sprintf("Command factory already registered for version: %d", factory.Version()))
}
factories[version] = factory
// Update compatibility versions.
if minVersion == 0 || version > minVersion {
minVersion = version
}
if maxVersion == 0 || version > maxVersion {
maxVersion = version
}
}
// GetCommandFactory retrieves a command factory for a given command version.
func GetCommandFactory(version int) CommandFactory {
return factories[version]
}
// MinVersion returns the minimum compatible store version.
func MinVersion() int {
return minVersion
}
// MaxVersion returns the maximum compatible store version.
func MaxVersion() int {
return maxVersion
}

91
store/event.go Normal file
View File

@ -0,0 +1,91 @@
package store
import (
"time"
)
const (
Get = "get"
Create = "create"
Set = "set"
Update = "update"
Delete = "delete"
CompareAndSwap = "compareAndSwap"
Expire = "expire"
)
type Event struct {
Action string `json:"action"`
Key string `json:"key, omitempty"`
Dir bool `json:"dir,omitempty"`
PrevValue string `json:"prevValue,omitempty"`
Value string `json:"value,omitempty"`
KVPairs kvPairs `json:"kvs,omitempty"`
Expiration *time.Time `json:"expiration,omitempty"`
TTL int64 `json:"ttl,omitempty"` // Time to live in second
ModifiedIndex uint64 `json:"modifiedIndex"`
}
func newEvent(action string, key string, index uint64) *Event {
return &Event{
Action: action,
Key: key,
ModifiedIndex: index,
}
}
func (e *Event) IsCreated() bool {
if e.Action == Create {
return true
}
if e.Action == Set && e.PrevValue == "" {
return true
}
return false
}
func (e *Event) Index() uint64 {
return e.ModifiedIndex
}
// Converts an event object into a response object.
func (event *Event) Response() interface{} {
if !event.Dir {
response := &Response{
Action: event.Action,
Key: event.Key,
Value: event.Value,
PrevValue: event.PrevValue,
Index: event.ModifiedIndex,
TTL: event.TTL,
Expiration: event.Expiration,
}
if response.Action == Set {
if response.PrevValue == "" {
response.NewKey = true
}
}
if response.Action == CompareAndSwap || response.Action == Create {
response.Action = "testAndSet"
}
return response
} else {
responses := make([]*Response, len(event.KVPairs))
for i, kv := range event.KVPairs {
responses[i] = &Response{
Action: event.Action,
Key: kv.Key,
Value: kv.Value,
Dir: kv.Dir,
Index: event.ModifiedIndex,
}
}
return responses
}
}

97
store/event_history.go Normal file
View File

@ -0,0 +1,97 @@
package store
import (
"fmt"
"strings"
"sync"
etcdErr "github.com/coreos/etcd/error"
)
type EventHistory struct {
Queue eventQueue
StartIndex uint64
LastIndex uint64
rwl sync.RWMutex
}
func newEventHistory(capacity int) *EventHistory {
return &EventHistory{
Queue: eventQueue{
Capacity: capacity,
Events: make([]*Event, capacity),
},
}
}
// addEvent function adds event into the eventHistory
func (eh *EventHistory) addEvent(e *Event) *Event {
eh.rwl.Lock()
defer eh.rwl.Unlock()
eh.Queue.insert(e)
eh.LastIndex = e.Index()
eh.StartIndex = eh.Queue.Events[eh.Queue.Front].ModifiedIndex
return e
}
// scan function is enumerating events from the index in history and
// stops till the first point where the key has identified prefix
func (eh *EventHistory) scan(prefix string, index uint64) (*Event, *etcdErr.Error) {
eh.rwl.RLock()
defer eh.rwl.RUnlock()
// the index should locate after the event history's StartIndex
if index-eh.StartIndex < 0 {
return nil,
etcdErr.NewError(etcdErr.EcodeEventIndexCleared,
fmt.Sprintf("the requested history has been cleared [%v/%v]",
eh.StartIndex, index), 0)
}
// the index should locate before the size of the queue minus the duplicate count
if index > eh.LastIndex { // future index
return nil, nil
}
i := eh.Queue.Front
for {
e := eh.Queue.Events[i]
if strings.HasPrefix(e.Key, prefix) && index <= e.Index() { // make sure we bypass the smaller one
return e, nil
}
i = (i + 1) % eh.Queue.Capacity
if i > eh.Queue.back() {
return nil, nil
}
}
}
// clone will be protected by a stop-world lock
// do not need to obtain internal lock
func (eh *EventHistory) clone() *EventHistory {
clonedQueue := eventQueue{
Capacity: eh.Queue.Capacity,
Events: make([]*Event, eh.Queue.Capacity),
Size: eh.Queue.Size,
Front: eh.Queue.Front,
}
for i, e := range eh.Queue.Events {
clonedQueue.Events[i] = e
}
return &EventHistory{
StartIndex: eh.StartIndex,
Queue: clonedQueue,
LastIndex: eh.LastIndex,
}
}

Some files were not shown because too many files have changed in this diff Show More