mirror of
https://github.com/etcd-io/etcd.git
synced 2024-09-27 06:25:44 +00:00
v0.2.0-rc1
This commit is contained in:
commit
aa047b124d
6
.gitignore
vendored
6
.gitignore
vendored
@ -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
33
CHANGELOG
Normal 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)
|
58
Documentation/errorcode.md
Normal file
58
Documentation/errorcode.md
Normal 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"
|
101
Documentation/etcd-file-system.md
Normal file
101
Documentation/etcd-file-system.md
Normal file
@ -0,0 +1,101 @@
|
||||
#Etcd File System
|
||||
|
||||
## Structure
|
||||
[TODO]
|
||||

|
||||
|
||||
## 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]
|
104
Documentation/external-documentation.md
Normal file
104
Documentation/external-documentation.md
Normal 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`
|
BIN
Documentation/img/etcd_fs_structure.jpg
Normal file
BIN
Documentation/img/etcd_fs_structure.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 26 KiB |
400
README.md
400
README.md
@ -1,26 +1,26 @@
|
||||
# etcd
|
||||
README version 0.1.0
|
||||
|
||||
README version 0.2.0
|
||||
|
||||
[](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
|
||||
|
||||
Let’s set the first key-value pair to the node. In this case the key is `/message` and the value is `Hello world`.
|
||||
Let’s 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
31
Vagrantfile
vendored
@ -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
3
build
@ -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}"
|
||||
|
@ -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}"
|
||||
|
254
command.go
254
command.go
@ -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
156
config.go
@ -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
|
||||
}
|
@ -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
321
etcd.go
@ -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)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
338
etcd_handlers.go
338
etcd_handlers.go
@ -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)
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
619
etcd_test.go
619
etcd_test.go
@ -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)
|
||||
}
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
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
|
||||
-----END CERTIFICATE-----
|
||||
|
@ -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-----
|
||||
|
@ -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-----
|
||||
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
|
||||
-----END CERTIFICATE-----
|
@ -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-----
|
||||
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
|
||||
-----END CERTIFICATE-----
|
63
fixtures/ca/server-chain.pem
Normal file
63
fixtures/ca/server-chain.pem
Normal file
@ -0,0 +1,63 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
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
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
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
|
||||
ePQ4BNEN9wt3SsPp8ig39g==
|
||||
-----END CERTIFICATE-----
|
@ -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
|
||||
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
|
||||
ePQ4BNEN9wt3SsPp8ig39g==
|
||||
-----END CERTIFICATE-----
|
||||
|
@ -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-----
|
||||
|
@ -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-----
|
||||
|
@ -1,51 +1,51 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
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
|
||||
-----END RSA PRIVATE KEY-----
|
||||
|
@ -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-----
|
@ -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.
@ -1,31 +1,31 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
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
|
||||
-----END CERTIFICATE-----
|
||||
|
@ -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-----
|
||||
|
@ -1,54 +1,54 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
Proc-Type: 4,ENCRYPTED
|
||||
DEK-Info: DES-EDE3-CBC,6357944A31F1FA4D
|
||||
DEK-Info: DES-EDE3-CBC,F332544C4A2552FC
|
||||
|
||||
8JSh0AAkpFI4wbTIxc461YY18lytg4AQHXDj3DU2lmkOcdS5vPQOqqWvqOW7DrmU
|
||||
FyNkQRke/SyL6ZkcEPAe0wCaRV4KlU49HZOfxMGlJLeRbrSbcVsE/Kk1dJo3VB+x
|
||||
Dm496BZMqVLrEuXx05kLiy3TUEsMNN3NSyFT/F76ZiV9HJ/ruVv/P5CS1Rwxzju7
|
||||
lZM4nEEsFCnGTPjdB/Es9HCjPX4HZmSdTwcU/vDBctS5aj91aE1EBSMdJfSYq2k4
|
||||
Ct6tAmOfBoi4qj+6Wr+gmaDyGvJyIscUbaBNXdM530CckkaFbZ00zXIqwnV33oiZ
|
||||
1CZ6uEd0Uk7Hu0n8lnBClqLJmXVMyxnO2lv2MPAPkBeLK5OEFj8aX9ekZDrkIL7M
|
||||
VgNgA5UCzmvtiEHNvow3BcEXhRFO1oT3O6AEyHpDUW3Ev7jn3SGg8Dg6suLs6GBc
|
||||
h9sFynnKO0cPT0tJDBbPNqZwcgktnpIiWHPJutMl6kQxAGDijK3WrGOialvuK99x
|
||||
rP/IIQm8bzi8eRvCulLtGw1al+yEH5Kwbu804cEgt2GFNXXvTo4M1RDFyxYiD9wz
|
||||
g+Gs0ZCkwpjasM2iNnURDX/xgZo4o2G/R53221Y47/xVu94DVSTD/AQWvEbgWPga
|
||||
zV7AQhUHGyoHMt5IyStcx1tobR2cmro4yCq3AN1aQwnUJm8LDY+KJpCAL+t4P61j
|
||||
hUCrJ6ZyUsQAPnvJmYJ4gkm8e8uNKfB57SONkNw35aE1W0AK8sKY2ax+dcTuEtnd
|
||||
6Pdh7WtucjMS8BEb2FvO1hOaidZ7aHfkU2QDJRlRsfKM0dfwVRN7BWmHGNPfGt1p
|
||||
f13Odpsa2OHS5htrcQ1PsxykXdHxrCLKlwz/dA1lu84L2F/Ious0B7rq5+Jf+R6U
|
||||
/xrzg24X63TYz0ysUfily4h0L1HyDQsseaSHCl5yYVKbjkBmapQwS4DmrlcTUUcF
|
||||
KA6myKCyRuHsMr8sXo7qUmKhuUIjrgStbHIzueIlan69HdL0c8mhNHlIXP2tWQkI
|
||||
v4pJXwtHp8sRMtF2Kbf/ZYCa5HKowu2JndFU45O+msWEq0xL0N+r+L16y74CEpuF
|
||||
6N3aI0UnLdhzVG4ZGCEQ+YYvFySIzP/veGhrJj/UaJyRkf15YEKKs8sULOhVGmdR
|
||||
LSHBe8yo7i1Vqec5bS6ZrttCNdaNWriylsEPjIZOkrlAAZGYqmwP2PR7NBvnWLX0
|
||||
QGIQoPQJuiibM9L9o+DEDDldzxFkWYdjsAO/ROs2bLng6uVU5oNsoTxZZOT2S+KN
|
||||
eM/TkQasJn27JHAIRb64GFayY6ianh2RJHFOGrVNnZ8aU8SM/eP7r07vYxXgNAA0
|
||||
r748yVacwLk7nhq1IXrrcRSNugTXUVgQSQw7VL5+ftxpHIHC5ZNj+NWYB8dAB2m+
|
||||
e0+BJhxoEFzNa3PypvLPqhOPs4FshVyzcejrULjEZDJZ59pLHJENu4IS9MV0/spo
|
||||
fjytvXK07Lk+UpAEbtUOONl9WIV06EAbJjH1Ow6cBGvJVypYqgWfhMH5vLvpID6k
|
||||
RAKVGU3/J1Sq4J2OcgRQ6g2oQQV4GXwOEL9o+6mesbcGHFiu8YLGriKGul5HxE3k
|
||||
5sSJUNKna1s6PxL+0vJQLljf9qrNG/bYS6p9jGtd1Wxma7vAEbpUvB644kSEEo1h
|
||||
1GleOr/8bz7BvMgoLg7pikogqOCRxHBN7AsGXzasF7bQXITIQLv/b46G5auvNECn
|
||||
mLXQayPx1okwSkYeOkDsoHATiFqmjyE6z5JGVF21z31K85ksxEzB+agchfD28Lme
|
||||
7GmlvfiEq7eSK/MUhkW+msnuURY6WJpKg+9LOm83sp9OVXDWLWdkurfQTh4iXdPX
|
||||
zLrQFhiHqmsZtLUUAQE9Hr34lvUwctDm7+LaL8IZgydlNAZks0xUuZhrmHHbbiRj
|
||||
iuqCHvvVFrlhJPAUsL6ICw1ygUJVo/jXdsky7K5zeeLuPFr2IlrlUiGKgt4ew/xa
|
||||
x3aFOw7zO5wVFSBjv7AfiImeM6/ke4fR7Jry75KWE2RLDQ8Gcnc8R8bIYUMZJjnV
|
||||
JCjAb+PJbyfTqth5/epjYw2c5XulxGgQnab+P3gZL3W0GiHggPOviNVx8LPjW9Os
|
||||
PQsRFGTB7S4lA1GCNRjwUgKdx5SKjNTcmGRltEju95Jdk2mOnI6cqTLiaXix9+fH
|
||||
WWb9s9TiL6FvaDCEbU4npb0vpkJNtHHudy0x7FdbQhLMtPeVH4qLlDAIHyrrt7g4
|
||||
Mzu6LUlQCvvcrB2jfWym+hYCIkIn+MbMK2jaknh0jPS3F72i2wHRd3ykIg5Rdv7T
|
||||
5OjApmDgMXibTBf2NmQAix/O6PYZp+jFZ/P3KNh7XSDC+Ne6qa8O3cdwaQsNLblt
|
||||
7e+fTGHJeWnQT5Kk0fY5Q1Lrje0lQ3iZgnUrsNX1wzYwOJtCbKXYC7wgBQmz6/IQ
|
||||
WSvlSZANwuutp9XdR44J3wbBm9Ux9cTgyL43PHSPTirPdu2QjQKE4A4ryqg+v64G
|
||||
q4Mal0kMhfSJ1OP7t7dAkMCNjBrsmi9QsBlmSX18BOP3B4TzqiAWIH2NGxnH5MVg
|
||||
TN/yQiIMyD24Zia7Z/CpMulQWyYKw9PkP7bdfyG65aJ0+LnYRu5MYS9wO694dcUG
|
||||
kZDyBq866y8GIDVvzWHNIh+iEbvnGvAgIFOT9i983Y/3SqTJA5EJ6iK/1AAsnERd
|
||||
14gQ/2SybyHZVB6RyPIw3E/QHu8ywNVOHg+pQDecFD0nQWV+l7fhRxn7+w4oLfx9
|
||||
PUIA/VFHaxx4pr14oHX1xUMOxuovhEjdXS1JkU6Niib5EqfrL3MDBnoMcH4EiAuT
|
||||
XfNy5K+uxD2uW0bxq6yjRGTB1yec8kcrcZyArQkAI7mfTOAywSiQyD6O038hQ7Rl
|
||||
hfMBSNjJiEhEvr4teNK6YPacQvsUEaAK411sHpb+enMR7l75PUazNdLHkVr/4rYo
|
||||
Z4eL6J3TAVmCN2QKvPDEgz9lr5pl4ogTN5mysPzCc8YtT+B7o096TcD8U+8bstFa
|
||||
GcTfsEGcxggX321Xc7kbfBPXKS7yq8b9wqwB3zbDTWgf3H/fd2aHozPBFeYWi8iK
|
||||
6qIM1wiFYYV20So/jF3U8a6EdOjy47JdXXdUONj9Qe+fE+8iNA2x0FzWUJIXy0dI
|
||||
0qzUXmrtDC8Uj4pnudhKPIHLErzRRgqS5qY6+Q+LNET41cltRat3t1/txN7WVPYa
|
||||
3AKo1SRb6VyAthcbih4pEPC2rQpjPcPrhuXNP3HeiDxQ9CHcNxniN2HmVMYxf7p9
|
||||
WaKSYgfTLP/La3YHlicimDIpmLvPjCzVvEx7bY2zrRU24mmSopG2gsLpnbLUDL2e
|
||||
U1R5lXKVULaO47hdtwcLQkmGo+b3uW+TAAmn6wTrJbOHKjq2+kFwZuilnmspXPcp
|
||||
t64OrBbE2jmY2TiBurDhT8HnbRhpeYWn7LqRdYOV9Fsl/3ftXIr3pItUDitiGxkH
|
||||
aNdFTm7XM2Sw25sS3TKxvksZW4sOMkIOUu/U/LrwQqNjvXMJMFJWRpMR9yDx62uc
|
||||
yElcz53zAo5yg0q7vNOu4yap0xQdOc5fu7fWDWZO+cfEYFtaSZV1du9QPxTpnSy+
|
||||
UQIqFmv4sNcJOl3gdVu2rClKi//jutnjyPkxhoPGpbSxA1WbltcO0c2J1eSJJnd2
|
||||
2jWbHFXFpIIp4WLI1TKKMM2ARVL6ITi9L/EL7HNAXh+bw5F1j3KkdsYKDMepgk2b
|
||||
ArJVPqvbLu8r6AdTtooOXfweJIWIGQNitDx4Ghj2d+XH6StImGBatYoofgKBH4q3
|
||||
nvxQRj+lsCX2ChdOD2rB1kyv8ak/6h880qtv3XTlaXzKUZfQ6Okd8/z+//eQcpRe
|
||||
WY8uXfXWyc/EZ+IHkAp9FCwjlSe2kQpGDmTbXz55gT2fKy/WtJOX7YDEpoUcPjI7
|
||||
+efoQYjo62e73/drH2SafQBkoDTfB66+IVO7osGJx7wb5iLENjkZ7mnpLR4oydsr
|
||||
miF9Iaik4eKzbX2EEXCvVX3/oCG51cdXB3Tun9bnLNZmHCsRFeOjNPTydMA9V729
|
||||
Pw07cBCKDMYK5D0QYkRRFWqpCsLiq5GZlEjLoJEjh7FJyQwCYrilfSSanCNk7llk
|
||||
onCKwh11OrC6Amdq7NvLfhuM0pHkLf+LZsEQ1Z6MbMN6Sm/NGAMFl0SeCQrlZNub
|
||||
rtOSQ3M6LhzFxWddfxt53MiK3adXRWdqeGBQtQLddj5PPCCJ7SSxFzIhAP6/jo5E
|
||||
1Tym9BkqoaZhnK7IwGvc3qBlziyvs952OfwkS57YkM5t5DFdJwlVCNUiVvVNJdju
|
||||
Z0PnRgfxZDUU+IB2h0jplSFpgsHPgZ3nPMYYprPMomj/qH2NTNQFLwf5Wv2eT33+
|
||||
AWjdnY4rUf5Up2rRVoPFgx2mVY3t54pLhX7UjcEwXMyKn8/jy/BhtHCRVdN9gHba
|
||||
sHvicd4tOGepTjZYORDWq1pEJv0uB2qdHoJlA4PoH5HT32b5MW2hVssHyVt0DpLO
|
||||
JP03k4ZxzA7JFCzjXQJvQfJ5uWTz2d/pHXQN7D+h2JxADhd+NeLvP7hwLZnXCF9z
|
||||
uz3AmYbFQVWGnUaWuvwPfizqTRuKmbkkuEcVPhoOsrqXSj+K5ulMTOzPmAAku3Ig
|
||||
UU2yAzCb6Jh/BlqhjreX0DU+ugvUZ0dYk+sGKv61VKA1Ifu315S6RtkowZn5cL/g
|
||||
AQDc94OQ1kQUe0etnDjTUjW+MswAYrjsHGUa2KCYUwz/X/u4xfbaX/cPRiyM09+U
|
||||
VbACr1ido9Fb/B71CHyYMi2nx0FYM0LpIfC/336+ONpuidfUwWD3Thia2krEaUQW
|
||||
0/47ALXa96BVhJ1XWItII+1uW243YnN1W/QKB+fB4IKFqX6qCf7XddabFMWBG4OS
|
||||
I8Ms2LdDcybIORs7G1iP95+UMljMNo9TFHH7vxEH1RcTRPejjmLk7/AxQzB63hML
|
||||
+Cf24pD8H2CaHKBet97CEfP5AYk2Y3KcSPm7rmc9bwllFEbIotjz4QEPAOL8A81l
|
||||
DUPuIaBNUgTzyjFhuzh5CCnseiJBrn4BnKJGeS27AzL2OEzsB6irmbrztAYA5AK8
|
||||
D7UrjdoT7f1MSmqf0gyXrxtrOJszdHG1U0HW3pZfJhgiqDVtUaE2ekspu9ji8vbk
|
||||
s9wtIRWXM9+EXPVGtp2nbJftJjxj+Bj4GignDw3taLfn5nl5pfUbW9EQGBQ10KlU
|
||||
CNl25Hwl5qcdvQeyfom/lXrUgY4nyGEwv6fqjjklz6N3yQpYYpJdW5kdTue56QDV
|
||||
MkKeogf8ZgyFqMaEtaMhHNwSW032K3hc2B2GXoaF17KPdb5snEKGN+TXG+2tuV8A
|
||||
ZBMT7Is+29Kyf750cUUmDlHS21BN9OjJcdZV9vQLbivRUtggfsMKImC0DYcqL4O4
|
||||
ToZB2VHiywlZjPLkiJSbx5u91mYCv10x2IPd3N4O15MWVZtp1L9KZEO8Dkn/OMAd
|
||||
mw6mtFYHCfCnHC8hjvU/cWfvlia1M0gLDweokd3auiN61gQ/1S9THkUMPP/RA7qd
|
||||
Zpj5UeqoTpLDjcVbYFl6NzkJM2KSbsdIewpqgsZW7PHiwO3c0iiXux8Ueo8nyDc9
|
||||
U3+mbYcBHpkd8F2hdyJPi5t9qW4ytI5P5LCA8Cgoj2CvkOLYh7qQFTMay2MEwsm6
|
||||
qwwcK8S3Ul4kZKx5KxoZOux4jDM36fR4pf0z+GIrDzfbnonJsv+oqHXGQYs4adwv
|
||||
4LT71pJ9mein1LGDrRq6Kgvp/Eu4Sr/S3oWjkFKxa/zd2LucKrjt5n9M5dL4NVZK
|
||||
0xyuoWTWY2lUol0hi53W0otz2hLQPFJllaHKUGISclmVSbHLi/6/Cc9XGALLM1+N
|
||||
hGyrMDsEJLsgsMNZFm1QDphrGgHo/HdBsQoghiUTxuLS2gIE/845maJYW9F+1YZh
|
||||
Q3uZKjFpL8MHyLTbP7RXjUFKUkM2lU5JJ9ydr3+W1OkVJENnmLbpalQctkO0W1It
|
||||
vXU0U8bB4nCtZUO75299m4mUh/lw2Ej/D651+Pbk+iHcO/+p1hjiVfhoLF701JNo
|
||||
WmEZ2/1ZGBqtO+ON7mlJLWOeYmiLnE/GheVPnFWv7ZFZCLVp8uEG69iK4pm9EH0B
|
||||
QaqUA42etpo7gHOMfTiOkwvmQDWsiXl4+MSnPo+MBaCFlWor8ZHj5OkDU79bEn1x
|
||||
dzzjhb3HTEJ9a//RC1vZbpRJuA4eueTrVkAJYcH1JMtnftKFz8qAaxYxsk8lVbnK
|
||||
TIkxA1CjP9Btoq5a0XNkSRzzTAXo1blYAA/xy6HLbODuwNY4Mym+ZIPR9B6QlYZQ
|
||||
-----END RSA PRIVATE KEY-----
|
||||
|
@ -1,51 +1,51 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
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
|
||||
-----END RSA PRIVATE KEY-----
|
||||
|
@ -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
44
log/log.go
Normal 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...)
|
||||
}
|
58
machines.go
58
machines.go
@ -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
|
||||
}
|
@ -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
|
||||
|
@ -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/
|
||||
|
@ -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'}
|
||||
});
|
||||
};
|
||||
|
@ -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.';
|
||||
|
@ -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 = [];
|
||||
|
@ -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">—</div>
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
}
|
||||
|
||||
|
@ -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
@ -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
@ -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
|
||||
}
|
||||
|
11
mod/mod.go
11
mod/mod.go
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
203
raft_handlers.go
203
raft_handlers.go
@ -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))
|
||||
}
|
341
raft_server.go
341
raft_server.go
@ -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{})
|
||||
}
|
225
raft_stats.go
225
raft_stats.go
@ -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
2
release_version.go
Normal file
@ -0,0 +1,2 @@
|
||||
package main
|
||||
const releaseVersion = "v0.1.2-33-g1a2a9d6"
|
@ -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
405
server/config.go
Normal 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
479
server/config_test.go
Normal 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
19
server/info.go
Normal 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
77
server/join_command.go
Normal 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
25
server/package_stats.go
Normal 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
440
server/peer_server.go
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
236
server/peer_server_handlers.go
Normal file
236
server/peer_server_handlers.go
Normal 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)
|
||||
}
|
56
server/raft_follower_stats.go
Normal file
56
server/raft_follower_stats.go
Normal 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++
|
||||
}
|
55
server/raft_server_stats.go
Normal file
55
server/raft_server_stats.go
Normal 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
179
server/registry.go
Normal 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
67
server/remove_command.go
Normal 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
399
server/server.go
Normal 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
89
server/stats_queue.go
Normal 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
15
server/timeout.go
Normal 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
12
server/tls_config.go
Normal 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
76
server/tls_info.go
Normal 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
|
||||
}
|
@ -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
|
@ -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
46
server/util.go
Normal 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
|
||||
}
|
14
server/v1/delete_key_handler.go
Normal file
14
server/v1/delete_key_handler.go
Normal 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)
|
||||
}
|
27
server/v1/get_key_handler.go
Normal file
27
server/v1/get_key_handler.go
Normal 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
|
||||
}
|
47
server/v1/set_key_handler.go
Normal file
47
server/v1/set_key_handler.go
Normal 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
15
server/v1/v1.go
Normal 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
|
||||
}
|
39
server/v1/watch_key_handler.go
Normal file
39
server/v1/watch_key_handler.go
Normal 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
|
||||
}
|
16
server/v2/delete_handler.go
Normal file
16
server/v2/delete_handler.go
Normal 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
80
server/v2/get_handler.go
Normal 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
23
server/v2/post_handler.go
Normal 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
96
server/v2/put_handler.go
Normal 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)
|
||||
}
|
29
server/v2/tests/delete_handler_test.go
Normal file
29
server/v2/tests/delete_handler_test.go
Normal 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}`, "")
|
||||
})
|
||||
}
|
169
server/v2/tests/get_handler_test.go
Normal file
169
server/v2/tests/get_handler_test.go
Normal 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, "")
|
||||
})
|
||||
}
|
38
server/v2/tests/post_handler_test.go
Normal file
38
server/v2/tests/post_handler_test.go
Normal 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", "")
|
||||
})
|
||||
}
|
280
server/v2/tests/put_handler_test.go
Normal file
280
server/v2/tests/put_handler_test.go
Normal 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
18
server/v2/v2.go
Normal 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
3
server/version.go
Normal file
@ -0,0 +1,3 @@
|
||||
package server
|
||||
|
||||
const Version = "v2"
|
52
snapshot.go
52
snapshot.go
@ -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
59
store/command_factory.go
Normal 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
91
store/event.go
Normal 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
97
store/event_history.go
Normal 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
Loading…
x
Reference in New Issue
Block a user