Compare commits
394 Commits
v0.2.0-rc2
...
v0.3.0
Author | SHA1 | Date | |
---|---|---|---|
f9d27c37aa | |||
ddc40e5b45 | |||
0c2287b201 | |||
102d8e543d | |||
b4d5534c75 | |||
4f72403acd | |||
4c0c256a9d | |||
d0f254a278 | |||
0696e5026f | |||
42363001b4 | |||
ed3d63248a | |||
2a2714a4bf | |||
27cb38f38c | |||
e8be43d4c6 | |||
76da437f29 | |||
b4635b0b80 | |||
147235f8f5 | |||
aa5c8b8ffd | |||
1b3481fe25 | |||
21d7d14178 | |||
14f15c33fe | |||
bfbc539321 | |||
5f124166eb | |||
468a68c96c | |||
da3fe920cb | |||
3d9fc3846c | |||
63fa35c99f | |||
68305181f9 | |||
c844fccf2a | |||
f2452a4a3c | |||
4e21405647 | |||
06f990236c | |||
8a172322ff | |||
1b5f9eb013 | |||
5851cb5b8d | |||
ba98de6ef0 | |||
d2a0f8f2fd | |||
445b584333 | |||
f7dae0de02 | |||
1c6a41dda4 | |||
1c91c167fc | |||
39518b463a | |||
cbdf4a738c | |||
1d4912b22f | |||
bc7297c2d0 | |||
39ddb29e63 | |||
fe35839a77 | |||
297832ff91 | |||
2d75ef0c7a | |||
2822b9c579 | |||
ff6090836c | |||
a8b07b1b48 | |||
8687dd3802 | |||
40021ab72e | |||
72514f8ab2 | |||
40a8542c22 | |||
f56965b1c0 | |||
69922340f6 | |||
0e50d9787a | |||
9e43e726a9 | |||
03cadc543f | |||
b61cf9cb8e | |||
8d2a8e1c7a | |||
72b393ca53 | |||
6398206e4f | |||
226c20c097 | |||
0b9c5c975e | |||
272dc343ef | |||
d7d20d1c3d | |||
2557992b70 | |||
33be0e09fe | |||
13b6c1e684 | |||
ea8a353545 | |||
0566bf2d5d | |||
93a129e55a | |||
58e1f12240 | |||
0fa6d38574 | |||
e1ed380f04 | |||
354a91290e | |||
3ec7004421 | |||
a542a7804b | |||
03ff4c8b76 | |||
7992448f6a | |||
9a0ddb3760 | |||
7ee7e910eb | |||
281b0e7e59 | |||
50e6256058 | |||
1b00c449a5 | |||
9848072d21 | |||
2652e46d66 | |||
d13dd50d51 | |||
8fece992eb | |||
3264b51a74 | |||
0692097a73 | |||
19ef1042d6 | |||
394e651591 | |||
2fe22f1890 | |||
089021ca6d | |||
f158dfcd77 | |||
19980a7033 | |||
a7d9efa900 | |||
0abd860f7e | |||
5c3a3db2d8 | |||
074099a1b2 | |||
a2ee620394 | |||
ffa2b07dc4 | |||
60bbc57aeb | |||
86718167e8 | |||
7bd4d05a38 | |||
d0c4916fe9 | |||
91fc6aabd2 | |||
c0ff8f6026 | |||
a93d60be90 | |||
c47760382e | |||
9c8a23c333 | |||
91f768f9ae | |||
6c48466bfe | |||
5bf667851c | |||
d77e3a203f | |||
c32dfa013d | |||
6b70efbe30 | |||
641edd4e6e | |||
823fdfab12 | |||
0cacb6cba4 | |||
7a948746a8 | |||
139f59f7d1 | |||
80c22a4fb2 | |||
a417782151 | |||
e87f3231a1 | |||
fe39288ebf | |||
97bc5b260d | |||
47f24d1088 | |||
3a75d0a465 | |||
17c8f6d2e8 | |||
7eaad5c8e0 | |||
3e7c2dff96 | |||
14c96306a0 | |||
d122ed3bcd | |||
451e874696 | |||
290ca6bbc7 | |||
a2e5bae951 | |||
823e744ed9 | |||
35c89c7537 | |||
77887e8253 | |||
1e6c0dee24 | |||
e89e42382a | |||
184a5901e6 | |||
6f8b0dc7ef | |||
21f0c6f9d4 | |||
bd2b3793a6 | |||
25caac370f | |||
0c8329a3fb | |||
444b5d329c | |||
7a7f6aea00 | |||
b226b14eb2 | |||
72b165ad4c | |||
bd04905154 | |||
471c40735c | |||
c2d1dc4f51 | |||
000290dc94 | |||
3f3a324108 | |||
77477b3e43 | |||
c2077ed0b6 | |||
ceefa98c76 | |||
d3fb9f0f0f | |||
d9088a5f18 | |||
87113f985f | |||
4da933e4a4 | |||
56909bb6a3 | |||
ecd73acc01 | |||
e2e0853492 | |||
0f97e3528a | |||
89074ffcea | |||
6b14fe7747 | |||
48e36422b5 | |||
32df6f92fc | |||
cde184fdbf | |||
97f1363afa | |||
8b8b8d74fe | |||
5821a1390a | |||
ae2130952b | |||
b0cdf73565 | |||
c64c739fab | |||
feb6f2dad7 | |||
2c42271cea | |||
8597904bc2 | |||
227ea57c37 | |||
5b924dfd4e | |||
181805bb87 | |||
63c06d6c79 | |||
782166dadd | |||
7bfd11679b | |||
f250649a5e | |||
629a827ee2 | |||
c247d807af | |||
22a25a18b3 | |||
6b77b94127 | |||
2bfb8f5e4f | |||
dd6623be2f | |||
fa3b4a7941 | |||
8047cfd48e | |||
53477af1eb | |||
355bd6df9b | |||
8d25dac1ba | |||
b47042634a | |||
e02441acb1 | |||
ddd8e85146 | |||
68546de2cb | |||
7553a92232 | |||
c7e642baa2 | |||
0377ea751d | |||
5efad911d5 | |||
d38a260e00 | |||
0cc2e8887f | |||
3736b7678a | |||
e373c9ac8c | |||
5f2d9b38dc | |||
a7eeb01bc8 | |||
c26215a740 | |||
4e8828c5c4 | |||
2dde2cbb6f | |||
88e0263d08 | |||
28652ed7af | |||
60c2680bfd | |||
9a8bd96ad3 | |||
66271fe986 | |||
1caa3245e6 | |||
04720bd8bf | |||
ecc96df699 | |||
0b47d6888d | |||
e9b85264ab | |||
d397e83a05 | |||
801b642dea | |||
e7527ebb45 | |||
bfde27a99d | |||
c65f8fc7ad | |||
547c05685e | |||
5794d70881 | |||
009b84c5fc | |||
b5426d967e | |||
60827ebd5a | |||
ae10b6226d | |||
d32fd00bbb | |||
7c29d5bf5c | |||
f37d9df118 | |||
7da85d66fd | |||
cf656ccfdd | |||
d7087ed61a | |||
98351b9756 | |||
5260c7c939 | |||
78e2fc1bc8 | |||
c17ad07bdb | |||
5b105ed156 | |||
af3240fa18 | |||
45b4d6d194 | |||
04ad7a91dd | |||
fd0d0813ce | |||
5a4c41be37 | |||
57f94f65a8 | |||
189b98c03f | |||
f46fdbf078 | |||
ed2d7d64cd | |||
cc10b1084d | |||
c49711ae2c | |||
b7be38da12 | |||
6b8b6cda66 | |||
850cbf6018 | |||
dcabc6ae80 | |||
1ab3db18bb | |||
8edc1f30d9 | |||
5266968b61 | |||
9bd36f173e | |||
a9e20aecc6 | |||
e66f6d76e6 | |||
c62603ba73 | |||
4692d6e966 | |||
9049a2afd5 | |||
bbbf8fd574 | |||
d66dc3c1c7 | |||
59ccefee0f | |||
3b485e20bb | |||
44af8ea190 | |||
2504c4ad34 | |||
d447ba52ad | |||
5e499456f0 | |||
e96234382a | |||
78e2a27ee0 | |||
715b4d7bfc | |||
c36f306a1d | |||
bfa7d54b02 | |||
4ff773aaaf | |||
0611c81e88 | |||
6b4f7cf861 | |||
51406a3582 | |||
9ebac0b9fd | |||
4acfc26c5e | |||
5271378fc2 | |||
5ab2f5454c | |||
4748da1e09 | |||
c3b4d10b74 | |||
f026d1c14e | |||
e30bf19684 | |||
d4553714d9 | |||
e1d909eb0e | |||
317b34f4a0 | |||
0937b4d266 | |||
ef988020b7 | |||
70c8c09360 | |||
d89fa131ab | |||
3f85829e87 | |||
3cde996d21 | |||
39fb266776 | |||
557ffbb861 | |||
ddcf3975ed | |||
cc88215b46 | |||
3bd2d0da88 | |||
b7854354ff | |||
80bc68eb49 | |||
8c8ab6a4cb | |||
985db7838c | |||
04c98d0a75 | |||
53436af899 | |||
81a73dbc80 | |||
c4179829d6 | |||
9cf1fcc987 | |||
e2fa89d554 | |||
75c02ed0da | |||
c7536ff5e1 | |||
fd8ce5d11a | |||
838645f1b7 | |||
4fb8e79237 | |||
c352db9acd | |||
60813103e3 | |||
d816db07e3 | |||
296eaf7b34 | |||
2ce587ebc7 | |||
18bf886368 | |||
1de78fef4d | |||
54794d57fe | |||
412f56f971 | |||
e87ee6a86b | |||
418eccb3d7 | |||
61227d7477 | |||
931ae5fec3 | |||
bcf692d775 | |||
7e5aa3137d | |||
e24d2fdb6c | |||
fe80a868a0 | |||
468bfedf34 | |||
5e1fdf554d | |||
d204fa8438 | |||
dba5eb57cf | |||
0399593589 | |||
36dda352d9 | |||
44f050f59a | |||
3caf7745b6 | |||
c1508becd6 | |||
a5bca025b1 | |||
bb64e7b6e5 | |||
f66bd1689d | |||
3e4f8a382e | |||
44b08fea62 | |||
74bd0d95b8 | |||
f83e76eb60 | |||
06473ba6fe | |||
cb9f677cf6 | |||
9ea1ef8916 | |||
dd354c9e22 | |||
59e98fcc62 | |||
4bec461db1 | |||
8442e7a0dc | |||
aabd0faebe | |||
31bc0bb670 | |||
0fb8fc0b8d | |||
a06f5e74af | |||
0762c79e2e | |||
d646d7c16a | |||
e00296960c | |||
c305eda344 | |||
636dad190e | |||
b556252358 | |||
4ba7d85d56 | |||
40d297be66 | |||
0867b33de5 | |||
faab194247 | |||
46f8a354d1 | |||
3d16633a94 | |||
d2d7e37990 | |||
f8985d731f | |||
373199fe46 | |||
171072c736 | |||
90a8f56c96 | |||
5b739f6166 | |||
702cf1cc36 |
7
.gitignore
vendored
7
.gitignore
vendored
@ -1,6 +1,5 @@
|
||||
src/
|
||||
pkg/
|
||||
/etcd
|
||||
/server/release_version.go
|
||||
/pkg
|
||||
/go-bindata
|
||||
/machine*
|
||||
/bin
|
||||
/src
|
||||
|
@ -1,8 +0,0 @@
|
||||
language: go
|
||||
go: 1.1
|
||||
|
||||
install:
|
||||
- echo "Skip install"
|
||||
|
||||
script:
|
||||
- ./test.sh
|
13
CHANGELOG
13
CHANGELOG
@ -1,4 +1,15 @@
|
||||
v0.2
|
||||
v0.3.0
|
||||
* Add Compare-and-Delete support.
|
||||
* Added prevNode to response objects.
|
||||
* Added Discovery API.
|
||||
* Add tracing and debug endpoints (Documentation/debugging.md).
|
||||
* Improved logging of cluster events.
|
||||
* go get github.com/coreos/etcd works.
|
||||
* info file is no longer used.
|
||||
* Snapshots are on by default.
|
||||
* Statistics APIs documented.
|
||||
|
||||
v0.2.0
|
||||
* Support directory creation and removal.
|
||||
* Add Compare-and-Swap (CAS) support.
|
||||
* Support recursive GETs.
|
||||
|
14
Dockerfile
14
Dockerfile
@ -1,10 +1,12 @@
|
||||
FROM ubuntu:12.04
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y python-software-properties git
|
||||
RUN add-apt-repository -y ppa:duh/golang
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y golang
|
||||
# Let's install go just like Docker (from source).
|
||||
RUN apt-get update -q
|
||||
RUN apt-get install -qy build-essential curl git
|
||||
RUN curl -s https://go.googlecode.com/files/go1.2.src.tar.gz | tar -v -C /usr/local -xz
|
||||
RUN cd /usr/local/go/src && ./make.bash --no-clean 2>&1
|
||||
ENV PATH /usr/local/go/bin:$PATH
|
||||
ADD . /opt/etcd
|
||||
RUN cd /opt/etcd && ./build
|
||||
EXPOSE 4001 7001
|
||||
ENTRYPOINT ["/opt/etcd/etcd"]
|
||||
ENTRYPOINT ["/opt/etcd/bin/etcd"]
|
||||
|
||||
|
1062
Documentation/api.md
Normal file
1062
Documentation/api.md
Normal file
File diff suppressed because it is too large
Load Diff
46
Documentation/clients-matrix.md
Normal file
46
Documentation/clients-matrix.md
Normal file
@ -0,0 +1,46 @@
|
||||
# Client libraries support matrix for etcd
|
||||
|
||||
As etcd features support is really uneven between client libraries, a compatibility matrix can be important.
|
||||
We will consider in detail only the features of clients supporting the v2 API. Clients still supporting the v1 API *only* are listed below.
|
||||
|
||||
## v1-only clients
|
||||
|
||||
Clients supporting only the API version 1
|
||||
|
||||
- [justinsb/jetcd](https://github.com/justinsb/jetcd) Java
|
||||
- [transitorykris/etcd-py](https://github.com/transitorykris/etcd-py) Python
|
||||
- [russellhaering/txetcd](https://github.com/russellhaering/txetcd) Python
|
||||
- [iconara/etcd-rb](https://github.com/iconara/etcd-rb) Ruby
|
||||
- [jpfuentes2/etcd-ruby](https://github.com/jpfuentes2/etcd-ruby) Ruby
|
||||
- [aterreno/etcd-clojure](https://github.com/aterreno/etcd-clojure) Clojure
|
||||
- [marshall-lee/etcd.erl](https://github.com/marshall-lee/etcd.erl) Erlang
|
||||
|
||||
|
||||
## v2 clients
|
||||
|
||||
The v2 API has a lot of features, we will categorize them in a few categories:
|
||||
|
||||
- **HTTPS Auth**: Support for SSL-certificate based authentication
|
||||
- **Reconnect**: If the client is able to reconnect automatically to another server if one fails.
|
||||
- **Mod/Lock**: Support for the locking module
|
||||
- **Mod/Leader**: Support for the leader election module
|
||||
- **GET,PUT,POST,DEL Features**: Support for all the modifiers when calling the etcd server with said HTTP method.
|
||||
|
||||
|
||||
### Supported features matrix
|
||||
|
||||
| Client| [go-etcd](https://github.com/coreos/go-etcd) | [jetcd](https://github.com/diwakergupta/jetcd) | [python-etcd](https://github.com/jplana/python-etcd) | [python-etcd-client](https://github.com/dsoprea/PythonEtcdClient) | [node-etcd](https://github.com/stianeikeland/node-etcd) | [nodejs-etcd](https://github.com/lavagetto/nodejs-etcd) | [etcd-ruby](https://github.com/ranjib/etcd-ruby) | [etcd-api](https://github.com/jdarcy/etcd-api) | [cetcd](https://github.com/dwwoelfel/cetcd) | [clj-etcd](https://github.com/rthomas/clj-etcd) | [etcetera](https://github.com/drusellers/etcetera)|
|
||||
| --- | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: |
|
||||
| **HTTPS Auth** | Y | Y | Y | Y | Y | Y | - | - | - | - | - |
|
||||
| **Reconnect** | Y | - | Y | Y | - | - | - | Y | - | - | - |
|
||||
| **Mod/Lock** | Y | - | Y | Y | - | - | - | - | - | - | - |
|
||||
| **Mod/Leader** | Y | - | - | Y | - | - | - | - | - | - | - |
|
||||
| **GET Features** | F | B | F | F | F | F | F | B | F | G | F |
|
||||
| **PUT Features** | F | B | F | F | F | F | F | G | F | G | F |
|
||||
| **POST Features** | F | - | F | F | - | F | F | - | - | - | F |
|
||||
| **DEL Features** | F | B | F | F | F | F | F | B | G | B | F |
|
||||
|
||||
**Legend**
|
||||
|
||||
**F**: Full support **G**: Good support **B**: Basic support
|
||||
**Y**: Feature supported **-**: Feature not supported
|
45
Documentation/cluster-discovery.md
Normal file
45
Documentation/cluster-discovery.md
Normal file
@ -0,0 +1,45 @@
|
||||
# Cluster Discovery
|
||||
|
||||
## Overview
|
||||
|
||||
Starting an etcd cluster can be painful since each node needs to know of another node in the cluster to get started. If you are trying to bring up a cluster all at once, say using a cloud formation, you also need to coordinate who will be the initial cluster leader. The discovery protocol helps you by providing an automated way to discover other existing peers in a cluster.
|
||||
|
||||
## Using discovery.etcd.io
|
||||
|
||||
### Create a Token
|
||||
|
||||
To use the discovery API, you must first create a token for your etcd cluster. Visit [https://discovery.etcd.io/new](https://discovery.etcd.io/new) to create a new token.
|
||||
|
||||
You can inspect the list of peers by viewing `https://discovery.etcd.io/<token>`.
|
||||
|
||||
### Start etcd With the Discovery Flag
|
||||
|
||||
Specify the `-discovery` flag when you start each etcd instance. The list of existing peers in the cluster will be downloaded and configured. If the instance is the first peer, it will start as the leader of the cluster.
|
||||
|
||||
Here's a full example:
|
||||
|
||||
```
|
||||
TOKEN=$(curl https://discovery.etcd.io/new)
|
||||
./etcd -name instance1 -peer-addr 10.1.2.3:7001 -addr 10.1.2.3:4001 -discovery https://discovery.etcd.io/$TOKEN
|
||||
./etcd -name instance2 -peer-addr 10.1.2.4:7002 -addr 10.1.2.4:4002 -discovery https://discovery.etcd.io/$TOKEN
|
||||
```
|
||||
|
||||
## Running Your Own Discovery Endpoint
|
||||
|
||||
The discovery API communicates with a separate etcd cluster to store and retrieve the list of peers. CoreOS provides [https://discovery.etcd.io](https://discovery.etcd.io) as a free service, but you can easily run your own etcd cluster for this purpose. Here's an example using an etcd cluster located at `10.10.10.10:4001`:
|
||||
|
||||
```
|
||||
TOKEN="testcluster"
|
||||
./etcd -name instance1 -peer-addr 10.1.2.3:7001 -addr 10.1.2.3:4001 -discovery http://10.10.10.10:4001/v2/keys/$TOKEN
|
||||
./etcd -name instance2 -peer-addr 10.1.2.4:7002 -addr 10.1.2.4:4002 -discovery http://10.10.10.10:4001/v2/keys/$TOKEN
|
||||
```
|
||||
|
||||
If you're interested in how to discovery API works behind the scenes, read about the [Discovery Protocol](https://github.com/coreos/etcd/blob/master/Documentation/discovery-protocol.md).
|
||||
|
||||
## Setting Peer Addresses Correctly
|
||||
|
||||
The Discovery API submits the `-peer-addr` of each etcd instance to the configured Discovery endpoint. It's important to select an address that *all* peers in the cluster can communicate with. For example, if you're located in two regions of a cloud provider, configuring a private `10.x` address will not work between the two regions, and communication will not be possible between all peers.
|
||||
|
||||
## Stale Peers
|
||||
|
||||
The discovery API will automatically clean up the address of a stale peer that is no longer part of the cluster. The TTL for this process is a week, which should be long enough to handle any extremely long outage you may encounter. There is no harm in having stale peers in the list until they are cleaned up, since an etcd instance only needs to connect to one valid peer in the cluster to join.
|
174
Documentation/clustering.md
Normal file
174
Documentation/clustering.md
Normal file
@ -0,0 +1,174 @@
|
||||
## Clustering
|
||||
|
||||
### Example cluster of three machines
|
||||
|
||||
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.
|
||||
|
||||
Let start by creating 3 new etcd instances.
|
||||
|
||||
We use `-peer-addr` to specify server port and `-addr` to specify client port and `-data-dir` to specify the directory to store the log and info of the machine in the cluster:
|
||||
|
||||
```sh
|
||||
./etcd -peer-addr 127.0.0.1:7001 -addr 127.0.0.1:4001 -data-dir machines/machine1 -name machine1
|
||||
```
|
||||
|
||||
**Note:** If you want to run etcd on an external IP address and still have access locally, you'll need to add `-bind-addr 0.0.0.0` so that it will listen on both external and localhost addresses.
|
||||
A similar argument `-peer-bind-addr` is used to setup the listening address for the server port.
|
||||
|
||||
Let's join two more machines to this cluster using the `-peers` argument. A single connection to any peer will allow a new machine to join, but multiple can be specified for greater resiliency.
|
||||
|
||||
```sh
|
||||
./etcd -peer-addr 127.0.0.1:7002 -addr 127.0.0.1:4002 -peers 127.0.0.1:7001,127.0.0.1:7003 -data-dir machines/machine2 -name machine2
|
||||
./etcd -peer-addr 127.0.0.1:7003 -addr 127.0.0.1:4003 -peers 127.0.0.1:7001,127.0.0.1:7002 -data-dir machines/machine3 -name machine3
|
||||
```
|
||||
|
||||
We can retrieve a list of machines in the cluster using the HTTP API:
|
||||
|
||||
```sh
|
||||
curl -L http://127.0.0.1:4001/v2/machines
|
||||
```
|
||||
|
||||
We should see there are three machines 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 the main key API:
|
||||
|
||||
```sh
|
||||
curl -L http://127.0.0.1:4001/v2/keys/_etcd/machines
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "get",
|
||||
"node": {
|
||||
"createdIndex": 1,
|
||||
"dir": true,
|
||||
"key": "/_etcd/machines",
|
||||
"modifiedIndex": 1,
|
||||
"nodes": [
|
||||
{
|
||||
"createdIndex": 1,
|
||||
"key": "/_etcd/machines/machine1",
|
||||
"modifiedIndex": 1,
|
||||
"value": "raft=http://127.0.0.1:7001&etcd=http://127.0.0.1:4001"
|
||||
},
|
||||
{
|
||||
"createdIndex": 2,
|
||||
"key": "/_etcd/machines/machine2",
|
||||
"modifiedIndex": 2,
|
||||
"value": "raft=http://127.0.0.1:7002&etcd=http://127.0.0.1:4002"
|
||||
},
|
||||
{
|
||||
"createdIndex": 3,
|
||||
"key": "/_etcd/machines/machine3",
|
||||
"modifiedIndex": 3,
|
||||
"value": "raft=http://127.0.0.1:7003&etcd=http://127.0.0.1:4003"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
We can also get the current leader in the cluster:
|
||||
|
||||
```
|
||||
curl -L http://127.0.0.1:4001/v2/leader
|
||||
```
|
||||
|
||||
The first server we set up should still be the leader unless it has died during these commands.
|
||||
|
||||
```
|
||||
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/v2/keys/foo -XPUT -d value=bar
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "set",
|
||||
"node": {
|
||||
"createdIndex": 4,
|
||||
"key": "/foo",
|
||||
"modifiedIndex": 4,
|
||||
"value": "bar"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Killing Nodes in the Cluster
|
||||
|
||||
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/v2/keys/foo
|
||||
```
|
||||
|
||||
We can also see that a new leader has been elected:
|
||||
|
||||
```
|
||||
curl -L http://127.0.0.1:4002/v2/leader
|
||||
```
|
||||
|
||||
```
|
||||
http://127.0.0.1:7002
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```
|
||||
http://127.0.0.1:7003
|
||||
```
|
||||
|
||||
|
||||
### Testing Persistence
|
||||
|
||||
Next we'll kill all the machines to test persistence.
|
||||
Type `CTRL-C` on each terminal and then rerun the same command you used to start each machine.
|
||||
|
||||
Your request for the `foo` key will return the correct value:
|
||||
|
||||
```sh
|
||||
curl -L http://127.0.0.1:4002/v2/keys/foo
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "get",
|
||||
"node": {
|
||||
"createdIndex": 4,
|
||||
"key": "/foo",
|
||||
"modifiedIndex": 4,
|
||||
"value": "bar"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### 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 `-*-file` flags to `-peer-*-file`.
|
||||
|
||||
If you are using SSL for server-to-server communication, you must use it on all instances of etcd.
|
||||
|
||||
|
||||
### What size cluster should I use?
|
||||
|
||||
Every command the client sends to the master is broadcast to all of the followers.
|
||||
The command is not committed until the majority of the cluster peers 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 peers.
|
||||
|
||||
Odd numbers are good because if you have 8 peers the majority will be 5 and if you have 9 peers the majority will still be 5.
|
||||
The result is that an 8 peer cluster can tolerate 3 peer failures and a 9 peer cluster can tolerate 4 machine failures.
|
||||
And in the best case when all 9 peers are responding the cluster will perform at the speed of the fastest 5 machines.
|
@ -19,6 +19,7 @@ configuration files.
|
||||
### Optional
|
||||
|
||||
* `-addr` - The advertised public hostname:port for client communication. Defaults to `127.0.0.1:4001`.
|
||||
* `-discovery` - A URL to use for discovering the peer list. (i.e `"https://discovery.etcd.io/your-unique-key"`).
|
||||
* `-bind-addr` - The listening hostname for client communication. Defaults to advertised ip.
|
||||
* `-peers` - A comma separated list of peers in the cluster (i.e `"203.0.113.101:7001,203.0.113.102:7001"`).
|
||||
* `-peers-file` - The file path containing a comma separated list of peers in the cluster.
|
||||
@ -26,7 +27,7 @@ configuration files.
|
||||
* `-cert-file` - The cert file of the client.
|
||||
* `-key-file` - The key file of the client.
|
||||
* `-config` - The path of the etcd config file. Defaults to `/etc/etcd/etcd.conf`.
|
||||
* `-cors-origins` - A comma separated white list of origins for cross-origin resource sharing.
|
||||
* `-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.
|
||||
* `-data-dir` - The directory to store log and snapshot. Defaults to the current working directory.
|
||||
* `-max-result-buffer` - The max size of result buffer. Defaults to `1024`.
|
||||
@ -37,11 +38,10 @@ configuration files.
|
||||
* `-peer-ca-file` - The path of the CAFile. Enables client/peer cert authentication when present.
|
||||
* `-peer-cert-file` - The cert file of the server.
|
||||
* `-peer-key-file` - The key file of the server.
|
||||
* `-snapshot` - Open or close snapshot. Defaults to `false`.
|
||||
* `-snapshot=false` - Disable log snapshots. Defaults to `true`.
|
||||
* `-v` - Enable verbose logging. Defaults to `false`.
|
||||
* `-vv` - Enable very verbose logging. Defaults to `false`.
|
||||
* `-version` - Print the version and exit.
|
||||
* `-web-url` - The hostname:port of web interface.
|
||||
|
||||
## Configuration File
|
||||
|
||||
@ -53,7 +53,7 @@ addr = "127.0.0.1:4001"
|
||||
bind_addr = "127.0.0.1:4001"
|
||||
ca_file = ""
|
||||
cert_file = ""
|
||||
cors_origins = []
|
||||
cors = []
|
||||
cpu_profile_file = ""
|
||||
data_dir = "."
|
||||
key_file = ""
|
||||
@ -66,7 +66,6 @@ name = "default-name"
|
||||
snapshot = false
|
||||
verbose = false
|
||||
very_verbose = false
|
||||
web_url = ""
|
||||
|
||||
[peer]
|
||||
addr = "127.0.0.1:7001"
|
||||
@ -96,7 +95,6 @@ key_file = ""
|
||||
* `ETCD_SNAPSHOT`
|
||||
* `ETCD_VERBOSE`
|
||||
* `ETCD_VERY_VERBOSE`
|
||||
* `ETCD_WEB_URL`
|
||||
* `ETCD_PEER_ADDR`
|
||||
* `ETCD_PEER_BIND_ADDR`
|
||||
* `ETCD_PEER_CA_FILE`
|
69
Documentation/debugging.md
Normal file
69
Documentation/debugging.md
Normal file
@ -0,0 +1,69 @@
|
||||
# Debugging etcd
|
||||
|
||||
Diagnosing issues in a distributed application is hard.
|
||||
etcd will help as much as it can - just enable these debug features using the CLI flag `-trace=*` or the config option `trace=*`.
|
||||
|
||||
## Logging
|
||||
|
||||
Log verbosity can be increased to the max using either the `-vvv` CLI flag or the `very_very_verbose=true` config option.
|
||||
|
||||
The only supported logging mode is to stdout.
|
||||
|
||||
## Metrics
|
||||
|
||||
etcd itself can generate a set of metrics.
|
||||
These metrics represent many different internal data points that can be helpful when debugging etcd servers.
|
||||
|
||||
#### Metrics reference
|
||||
|
||||
Each individual metric name is prefixed with `etcd.<NAME>`, where \<NAME\> is the configured name of the etcd server.
|
||||
|
||||
* `timer.appendentries.handle`: amount of time a peer takes to process an AppendEntriesRequest from the POV of the peer itself
|
||||
* `timer.peer.<PEER>.heartbeat`: amount of time a peer heartbeat operation takes from the POV of the leader that initiated that operation for peer \<PEER\>
|
||||
* `timer.command.<COMMAND>`: amount of time a given command took to be processed through the local server's raft state machine. This does not include time waiting on locks.
|
||||
|
||||
#### Fetching metrics over HTTP
|
||||
|
||||
Once tracing has been enabled on a given etcd server, all metric data is available at the server's `/debug/metrics` HTTP endpoint (i.e. `http://127.0.0.1:4001/debug/metrics`).
|
||||
Executing a GET HTTP command against the metrics endpoint will yield the current state of all metrics in the etcd server.
|
||||
|
||||
#### Sending metrics to Graphite
|
||||
|
||||
etcd supports [Graphite's Carbon plaintext protocol](https://graphite.readthedocs.org/en/latest/feeding-carbon.html#the-plaintext-protocol) - a TCP wire protocol designed for shipping metric data to an aggregator.
|
||||
To send metrics to a Graphite endpoint using this protocol, use of the `-graphite-host` CLI flag or the `graphite_host` config option (i.e. `graphite_host=172.17.0.19:2003`).
|
||||
|
||||
See an [example graphite deploy script](https://github.com/coreos/etcd/contrib/graphite).
|
||||
|
||||
#### Generating additional metrics with Collectd
|
||||
|
||||
[Collectd](http://collectd.org/documentation.shtml) gathers metrics from the host running etcd.
|
||||
While these aren't metrics generated by etcd itself, it can be invaluable to compare etcd's view of the world to that of a separate process running next to etcd.
|
||||
|
||||
See an [example collectd deploy script](https://github.com/coreos/etcd/contrib/collectd).
|
||||
|
||||
## Profiling
|
||||
|
||||
etcd exposes profiling information from the Go pprof package over HTTP.
|
||||
The basic browseable interface is served by etcd at the `/debug/pprof` HTTP endpoint (i.e. `http://127.0.0.1:4001/debug/pprof`).
|
||||
For more information on using profiling tools, see http://blog.golang.org/profiling-go-programs.
|
||||
|
||||
**NOTE**: In the following examples you need to ensure that the `./bin/etcd` is identical to the `./bin/etcd` that you are targetting (same git hash, arch, platform, etc).
|
||||
|
||||
#### Heap memory profile
|
||||
|
||||
```
|
||||
go tool pprof ./bin/etcd http://127.0.0.1:4001/debug/pprof/heap
|
||||
```
|
||||
|
||||
#### CPU profile
|
||||
|
||||
```
|
||||
go tool pprof ./bin/etcd http://127.0.0.1:4001/debug/pprof/profile
|
||||
```
|
||||
|
||||
#### Blocked goroutine profile
|
||||
|
||||
```
|
||||
go tool pprof ./bin/etcd http://127.0.0.1:4001/debug/pprof/block
|
||||
```
|
||||
|
87
Documentation/discovery-protocol.md
Normal file
87
Documentation/discovery-protocol.md
Normal file
@ -0,0 +1,87 @@
|
||||
# Discovery Protocol
|
||||
|
||||
Starting a new etcd cluster can be painful since each machine needs to know of at least one live machine in the cluster. If you are trying to bring up a new cluster all at once, say using an AWS cloud formation, you also need to coordinate who will be the initial cluster leader. The discovery protocol uses an existing running etcd cluster to start a second etcd cluster.
|
||||
|
||||
To use this feature you add the command line flag `-discovery` to your etcd args. In this example we will use `http://example.com/v2/keys/_etcd/registry` as the URL prefix.
|
||||
|
||||
## The Protocol
|
||||
|
||||
By convention the etcd discovery protocol uses the key prefix `_etcd/registry`. A full URL to the keyspace will be `http://example.com/v2/keys/_etcd/registry`.
|
||||
|
||||
### Creating a New Cluster
|
||||
|
||||
Generate a unique token that will identify the new cluster. This will be used as a key prefix in the following steps. An easy way to do this is to use uuidgen:
|
||||
|
||||
```
|
||||
UUID=$(uuidgen)
|
||||
```
|
||||
|
||||
### Bringing up Machines
|
||||
|
||||
Now that you have your cluster ID you can start bringing up machines. Every machine will follow this protocol internally in etcd if given a `-discovery`.
|
||||
|
||||
### Registering your Machine
|
||||
|
||||
The first thing etcd must do is register your machine. This is done by using the machine name (from the `-name` arg) and posting it with a long TTL to the given key.
|
||||
|
||||
```
|
||||
curl -X PUT "http://example.com/v2/keys/_etcd/registry/${UUID}/${etcd_machine_name}?ttl=604800" -d value=${peer_addr}
|
||||
```
|
||||
|
||||
### Discovering Peers
|
||||
|
||||
Now that this etcd machine is registered it must discover its peers.
|
||||
|
||||
But, the tricky bit of starting a new cluster is that one machine needs to assume the initial role of leader and will have no peers. To figure out if another machine has already started the cluster etcd needs to create the `_state` key and set its value to "started":
|
||||
|
||||
```
|
||||
curl -X PUT "http://example.com/v2/keys/_etcd/registry/${UUID}/_state?prevExist=false" -d value=started
|
||||
```
|
||||
|
||||
If this returns a `200 OK` response then this machine is the initial leader and should start with no peers configured. If, however, this returns a `412 Precondition Failed` then you need to find all of the registered peers:
|
||||
|
||||
```
|
||||
curl -X GET "http://example.com/v2/keys/_etcd/registry/${UUID}?recursive=true"
|
||||
```
|
||||
|
||||
```
|
||||
{
|
||||
"action": "get",
|
||||
"node": {
|
||||
"createdIndex": 11,
|
||||
"dir": true,
|
||||
"key": "/_etcd/registry/9D4258A5-A1D3-4074-8837-31C1E091131D",
|
||||
"modifiedIndex": 11,
|
||||
"nodes": [
|
||||
{
|
||||
"createdIndex": 16,
|
||||
"expiration": "2014-02-03T13:19:57.631253589-08:00",
|
||||
"key": "/_etcd/registry/9D4258A5-A1D3-4074-8837-31C1E091131D/peer1",
|
||||
"modifiedIndex": 16,
|
||||
"ttl": 604765,
|
||||
"value": "127.0.0.1:7001"
|
||||
},
|
||||
{
|
||||
"createdIndex": 17,
|
||||
"expiration": "2014-02-03T13:19:57.631253589-08:00",
|
||||
"key": "/_etcd/registry/9D4258A5-A1D3-4074-8837-31C1E091131D/peer2",
|
||||
"modifiedIndex": 17,
|
||||
"ttl": 604765,
|
||||
"value": "127.0.0.1:7002"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Using this information you can connect to the rest of the peers in the cluster.
|
||||
|
||||
### Heartbeating
|
||||
|
||||
At this point etcd will start heart beating to your registration URL. The
|
||||
protocol uses a heartbeat so permanently deleted nodes get slowly removed from
|
||||
the discovery information cluster.
|
||||
|
||||
The heartbeat interval is about once per day and the TTL is one week. This
|
||||
should give a sufficiently wide window to protect against a discovery service
|
||||
taking a temporary outage yet provide adequate cleanup.
|
@ -5,13 +5,13 @@
|
||||

|
||||
|
||||
## 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:
|
||||
In **etcd**, the **node** is the base from which the filesystem is constructed.
|
||||
**etcd**'s file system is Unix-like with two kinds of nodes: file and directories.
|
||||
|
||||
- **File Node** has data associated with it.
|
||||
- **Directory Node** has children nodes associated with it.
|
||||
- A **file node** has data associated with it.
|
||||
- A **directory node** has child nodes associated with it.
|
||||
|
||||
Besides the file and directory difference, all nodes have common attributes and operations as follows:
|
||||
All nodes, regardless of type, have the following attributes and operations:
|
||||
|
||||
### Attributes:
|
||||
- **Expiration Time** [optional]
|
||||
@ -20,7 +20,7 @@ Besides the file and directory difference, all nodes have common attributes and
|
||||
|
||||
- **ACL**
|
||||
|
||||
The path of access control list of the node.
|
||||
The path to the node's access control list.
|
||||
|
||||
### Operation:
|
||||
- **Get** (path, recursive, sorted)
|
||||
@ -55,7 +55,7 @@ Besides the file and directory difference, all nodes have common attributes and
|
||||
- **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
|
||||
- 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.
|
||||
@ -69,7 +69,7 @@ Besides the file and directory difference, all nodes have common attributes and
|
||||
|
||||
### 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).
|
||||
Each node has various meta-data, including three names of the 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.
|
||||
|
79
Documentation/libraries-and-tools.md
Normal file
79
Documentation/libraries-and-tools.md
Normal file
@ -0,0 +1,79 @@
|
||||
## Libraries and Tools
|
||||
|
||||
**Tools**
|
||||
|
||||
- [etcdctl](https://github.com/coreos/etcdctl) - A command line client for etcd
|
||||
- [etcd-dump](https://npmjs.org/package/etcd-dump) - Command line utility for dumping/restoring etcd.
|
||||
|
||||
**Go libraries**
|
||||
|
||||
- [go-etcd](https://github.com/coreos/go-etcd) - Supports v2
|
||||
|
||||
**Java libraries**
|
||||
|
||||
- [justinsb/jetcd](https://github.com/justinsb/jetcd)
|
||||
- [diwakergupta/jetcd](https://github.com/diwakergupta/jetcd) - Supports v2
|
||||
|
||||
**Python libraries**
|
||||
|
||||
- [transitorykris/etcd-py](https://github.com/transitorykris/etcd-py)
|
||||
- [jplana/python-etcd](https://github.com/jplana/python-etcd) - Supports v2
|
||||
- [russellhaering/txetcd](https://github.com/russellhaering/txetcd) - a Twisted Python library
|
||||
|
||||
**Node libraries**
|
||||
|
||||
- [stianeikeland/node-etcd](https://github.com/stianeikeland/node-etcd) - Supports v2 (w Coffeescript)
|
||||
- [lavagetto/nodejs-etcd](https://github.com/lavagetto/nodejs-etcd) - Supports v2
|
||||
|
||||
**Ruby libraries**
|
||||
|
||||
- [iconara/etcd-rb](https://github.com/iconara/etcd-rb)
|
||||
- [jpfuentes2/etcd-ruby](https://github.com/jpfuentes2/etcd-ruby)
|
||||
- [ranjib/etcd-ruby](https://github.com/ranjib/etcd-ruby) - Supports v2
|
||||
|
||||
**C libraries**
|
||||
|
||||
- [jdarcy/etcd-api](https://github.com/jdarcy/etcd-api) - Supports v2
|
||||
|
||||
**Clojure libraries**
|
||||
|
||||
- [aterreno/etcd-clojure](https://github.com/aterreno/etcd-clojure)
|
||||
- [dwwoelfel/cetcd](https://github.com/dwwoelfel/cetcd) - Supports v2
|
||||
- [rthomas/clj-etcd](https://github.com/rthomas/clj-etcd) - Supports v2
|
||||
|
||||
**Erlang libraries**
|
||||
|
||||
- [marshall-lee/etcd.erl](https://github.com/marshall-lee/etcd.erl)
|
||||
|
||||
**.Net Libraries**
|
||||
-[drusellers/etcetera](https://github.com/drusellers/etcetera)
|
||||
|
||||
A detailed recap of client functionalities can be found in the [clients compatibility matrix][clients-matrix.md].
|
||||
|
||||
[clients-matrix.md]: https://github.com/coreos/etcd/blob/master/Documentation/clients-matrix.md
|
||||
|
||||
**Chef Integration**
|
||||
|
||||
- [coderanger/etcd-chef](https://github.com/coderanger/etcd-chef)
|
||||
|
||||
**Chef Cookbook**
|
||||
|
||||
- [spheromak/etcd-cookbook](https://github.com/spheromak/etcd-cookbook)
|
||||
|
||||
**BOSH Releases**
|
||||
|
||||
- [cloudfoundry-community/etcd-boshrelease](https://github.com/cloudfoundry-community/etcd-boshrelease)
|
||||
- [cloudfoundry/cf-release](https://github.com/cloudfoundry/cf-release/tree/master/jobs/etcd)
|
||||
|
||||
**Projects using etcd**
|
||||
|
||||
- [binocarlos/yoda](https://github.com/binocarlos/yoda) - etcd + ZeroMQ
|
||||
- [calavera/active-proxy](https://github.com/calavera/active-proxy) - HTTP Proxy configured with etcd
|
||||
- [derekchiang/etcdplus](https://github.com/derekchiang/etcdplus) - A set of distributed synchronization primitives built upon etcd
|
||||
- [go-discover](https://github.com/flynn/go-discover) - service discovery in Go
|
||||
- [gleicon/goreman](https://github.com/gleicon/goreman/tree/etcd) - Branch of the Go Foreman clone with etcd support
|
||||
- [garethr/hiera-etcd](https://github.com/garethr/hiera-etcd) - Puppet hiera backend using etcd
|
||||
- [mattn/etcd-vim](https://github.com/mattn/etcd-vim) - SET and GET keys from inside vim
|
||||
- [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
|
||||
- [configdb](https://git.autistici.org/ai/configdb/tree/master) - A REST relational abstraction on top of arbitrary database backends, aimed at storing configs and inventories.
|
105
Documentation/modules.md
Normal file
105
Documentation/modules.md
Normal file
@ -0,0 +1,105 @@
|
||||
## Modules
|
||||
|
||||
etcd has a number of modules that are built on top of the core etcd API.
|
||||
These modules provide things like dashboards, locks and leader election.
|
||||
|
||||
### Dashboard
|
||||
|
||||
An HTML dashboard can be found at `http://127.0.0.1:4001/mod/dashboard/`.
|
||||
This dashboard is compiled into the etcd binary and uses the same API as regular etcd clients.
|
||||
|
||||
Use the `-cors='*'` flag to allow your browser to request information from the current master as it changes.
|
||||
|
||||
### Lock
|
||||
|
||||
The Lock module implements a fair lock that can be used when lots of clients want access to a single resource.
|
||||
A lock can be associated with a value.
|
||||
The value is unique so if a lock tries to request a value that is already queued for a lock then it will find it and watch until that value obtains the lock.
|
||||
If you lock the same value on a key from two separate curl sessions they'll both return at the same time.
|
||||
|
||||
Here's the API:
|
||||
|
||||
**Acquire a lock (with no value) for "customer1"**
|
||||
|
||||
```sh
|
||||
curl -X POST http://127.0.0.1:4001/mod/v2/lock/customer1?ttl=60
|
||||
```
|
||||
|
||||
**Acquire a lock for "customer1" that is associated with the value "bar"**
|
||||
|
||||
```sh
|
||||
curl -X POST http://127.0.0.1:4001/mod/v2/lock/customer1?ttl=60 -d value=bar
|
||||
```
|
||||
|
||||
**Renew the TTL on the "customer1" lock for index 2**
|
||||
|
||||
```sh
|
||||
curl -X PUT http://127.0.0.1:4001/mod/v2/lock/customer1?ttl=60 -d index=2
|
||||
```
|
||||
|
||||
**Renew the TTL on the "customer1" lock for value "customer1"**
|
||||
|
||||
```sh
|
||||
curl -X PUT http://127.0.0.1:4001/mod/v2/lock/customer1?ttl=60 -d value=bar
|
||||
```
|
||||
|
||||
**Retrieve the current value for the "customer1" lock.**
|
||||
|
||||
```sh
|
||||
curl http://127.0.0.1:4001/mod/v2/lock/customer1
|
||||
```
|
||||
|
||||
**Retrieve the current index for the "customer1" lock**
|
||||
|
||||
```sh
|
||||
curl http://127.0.0.1:4001/mod/v2/lock/customer1?field=index
|
||||
```
|
||||
|
||||
**Delete the "customer1" lock with the index 2**
|
||||
|
||||
```sh
|
||||
curl -X DELETE http://127.0.0.1:4001/mod/v2/lock/customer1?index=2
|
||||
```
|
||||
|
||||
**Delete the "customer1" lock with the value "bar"**
|
||||
|
||||
```sh
|
||||
curl -X DELETE http://127.0.0.1:4001/mod/v2/lock/customer1?value=bar
|
||||
```
|
||||
|
||||
|
||||
### Leader Election
|
||||
|
||||
The Leader Election module wraps the Lock module to allow clients to come to consensus on a single value.
|
||||
This is useful when you want one server to process at a time but allow other servers to fail over.
|
||||
The API is similar to the Lock module but is limited to simple strings values.
|
||||
|
||||
Here's the API:
|
||||
|
||||
**Attempt to set a value for the "order_processing" leader key:**
|
||||
|
||||
```sh
|
||||
curl -X PUT http://127.0.0.1:4001/mod/v2/leader/order_processing?ttl=60 -d name=myserver1.foo.com
|
||||
```
|
||||
|
||||
**Retrieve the current value for the "order_processing" leader key:**
|
||||
|
||||
```sh
|
||||
curl http://127.0.0.1:4001/mod/v2/leader/order_processing
|
||||
myserver1.foo.com
|
||||
```
|
||||
|
||||
**Remove a value from the "order_processing" leader key:**
|
||||
|
||||
```sh
|
||||
curl -X DELETE http://127.0.0.1:4001/mod/v2/leader/order_processing?name=myserver1.foo.com
|
||||
```
|
||||
|
||||
If multiple clients attempt to set the value for a key then only one will succeed.
|
||||
The other clients will hang until the current value is removed because of TTL or because of a `DELETE` operation.
|
||||
Multiple clients can submit the same value and will all be notified when that value succeeds.
|
||||
|
||||
To update the TTL of a value simply reissue the same `PUT` command that you used to set the value.
|
||||
|
||||
|
||||
|
30
Documentation/optimal-cluster-size.md
Normal file
30
Documentation/optimal-cluster-size.md
Normal file
@ -0,0 +1,30 @@
|
||||
# Optimal etcd Cluster Size
|
||||
|
||||
etcd's Raft consensus algorithm is most efficient in small clusters between 3 and 9 peers. Let's briefly explore how etcd works internally to understand why.
|
||||
|
||||
## Writing to etcd
|
||||
|
||||
Writes to an etcd peer are always redirected to the leader of the cluster and distributed to all of the peers immediately. A write is only considered successful when a majority of the peers acknowledge the write.
|
||||
|
||||
For example, in a 5 node cluster, a write operation is only as fast as the 3rd fastest machine. This is the main reason for keeping your etcd cluster below 9 nodes. In practice, you only need to worry about write performance in high latency environments such as a cluster spanning multiple data centers.
|
||||
|
||||
## Leader Election
|
||||
|
||||
The leader election process is similar to writing a key — a majority of the cluster must acknowledge the new leader before cluster operations can continue. The longer each node takes to elect a new leader means you have to wait longer before you can write to the cluster again. In low latency environments this process takes milliseconds.
|
||||
|
||||
## Odd Cluster Size
|
||||
|
||||
The other important cluster optimization is to always have an odd cluster size. Adding an odd node to the cluster doesn't change the size of the majority and therefore doesn't increase the total latency of the majority as described above. But you do gain a higher tolerance for peer failure by adding the extra machine. You can see this in practice when comparing two even and odd sized clusters:
|
||||
|
||||
| Cluster Size | Majority | Failure Tolerance |
|
||||
|--------------|------------|-------------------|
|
||||
| 8 machines | 5 machines | 3 machines |
|
||||
| 9 machines | 5 machines | **4 machines** |
|
||||
|
||||
As you can see, adding another node to bring the cluster up to an odd size is always worth it. During a network partition, an odd cluster size also guarantees that there will almost always be a majority of the cluster that can continue to operate and be the source of truth when the partition ends.
|
||||
|
||||
## Cluster Management
|
||||
|
||||
Currently, each CoreOS machine is an etcd peer — if you have 30 CoreOS machines, you have 30 etcd peers and end up with a cluster size that is way too large. If desired, you may manually stop some of these etcd instances to increase cluster performance.
|
||||
|
||||
Functionality is being developed to expose two different types of followers: active and benched followers. Active followers will influence operations within the cluster. Benched followers will not participate, but will transparently proxy etcd traffic to an active follower. This allows every CoreOS machine to expose etcd on port 4001 for ease of use. Benched followers will have the ability to transition into an active follower if needed.
|
62
Documentation/platforms/freebsd.md
Normal file
62
Documentation/platforms/freebsd.md
Normal file
@ -0,0 +1,62 @@
|
||||
# FreeBSD
|
||||
|
||||
Starting with version 0.1.2 both etcd and etcdctl have been ported to FreeBSD and can
|
||||
be installed either via packages or ports system. Their versions have been recently
|
||||
updated to 0.2.0 so now you can enjoy using etcd and etcdctl on FreeBSD 10.0 (RC4 as
|
||||
of now) and 9.x where they have been tested. They might also work when installed from
|
||||
ports on earlier versions of FreeBSD, but your mileage may vary.
|
||||
|
||||
## Installation
|
||||
|
||||
### Using pkgng package system
|
||||
|
||||
1. If you do not have pkgng installed, install it with command `pkg` and answering 'Y'
|
||||
when asked
|
||||
|
||||
2. Update your repository data with `pkg update`
|
||||
|
||||
3. Install etcd with `pkg install coreosetcd coreosetcdctl`
|
||||
|
||||
4. Verify successful installation with `pkg info | grep etcd` and you should get:
|
||||
|
||||
```
|
||||
r@fbsd10:/ # pkg info | grep etcd
|
||||
coreosetcd0.2.0 Highlyavailable key value store and service discovery
|
||||
coreosetcdctl0.2.0 Simple commandline client for etcd
|
||||
r@fbsd10:/ #
|
||||
```
|
||||
|
||||
5. You’re ready to use etcd and etcdctl! For more information about using pkgng, plese
|
||||
see: http://www.freebsd.org/doc/handbook/pkgngintro.html
|
||||
|
||||
### Using ports system
|
||||
|
||||
1. If you do not have ports installed, install with with `portsnap fetch extract` (it
|
||||
may take some time depending on your hardware and network connection)
|
||||
|
||||
2. Build etcd with `cd /usr/ports/devel/etcd && make install clean`, you
|
||||
will get an option to build and install documentation and etcdctl with it.
|
||||
|
||||
3. If you havent install it with etcdctl, and you would like to install it later, you can build it
|
||||
with `cd /usr/ports/devel/etcdctl && make install clean`
|
||||
|
||||
4. Verify successful installation with `pkg info | grep etcd` and you should get:
|
||||
|
||||
|
||||
```
|
||||
r@fbsd10:/ # pkg info | grep etcd
|
||||
coreosetcd0.2.0 Highlyavailable key value store and service discovery
|
||||
coreosetcdctl0.2.0 Simple commandline client for etcd
|
||||
r@fbsd10:/ #
|
||||
```
|
||||
|
||||
5. You’re ready to use etcd and etcdctl! For more information about using ports system,
|
||||
please see: https://www.freebsd.org/doc/handbook/portsusing.html
|
||||
|
||||
## Issues
|
||||
|
||||
If you find any issues with the build/install procedure or you’ve found a problem that
|
||||
you’ve verified is local to FreeBSD version only (for example, by not being able to
|
||||
reproduce it on any other platform, like OSX or Linux), please sent a
|
||||
problem report using this page for more
|
||||
information: http://www.freebsd.org/sendpr.html
|
130
Documentation/security.md
Normal file
130
Documentation/security.md
Normal file
@ -0,0 +1,130 @@
|
||||
# Reading and Writing over HTTPS
|
||||
|
||||
## Transport Security with HTTPS
|
||||
|
||||
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:
|
||||
http://www.g-loaded.eu/2005/11/10/be-your-own-ca/
|
||||
|
||||
For testing you can use the certificates in the `fixtures/ca` directory.
|
||||
|
||||
Let's configure etcd to use this keypair:
|
||||
|
||||
```sh
|
||||
./etcd -f -name machine0 -data-dir machine0 -cert-file=./fixtures/ca/server.crt -key-file=./fixtures/ca/server.key.insecure
|
||||
```
|
||||
|
||||
There are a few new options we're using:
|
||||
|
||||
* `-f` - forces a new machine configuration, even if an existing configuration is found. (WARNING: data loss!)
|
||||
* `-cert-file` and `-key-file` 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/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.
|
||||
|
||||
**OSX 10.9+ Users**: curl 7.30.0 on OSX 10.9+ doesn't understand certificates passed in on the command line.
|
||||
Instead you must import the dummy ca.crt directly into the keychain or add the `-k` flag to curl to ignore errors.
|
||||
If you want to test without the `-k` flag run `open ./fixtures/ca/ca.crt` and follow the prompts.
|
||||
Please remove this certificate after you are done testing!
|
||||
If you know of a workaround let us know.
|
||||
|
||||
```
|
||||
...
|
||||
SSLv3, TLS handshake, Finished (20):
|
||||
...
|
||||
```
|
||||
|
||||
And also the response from the etcd server:
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "set",
|
||||
"key": "/foo",
|
||||
"modifiedIndex": 3,
|
||||
"value": "bar"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 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.
|
||||
|
||||
```sh
|
||||
./etcd -f -name machine0 -data-dir machine0 -ca-file=./fixtures/ca/ca.crt -cert-file=./fixtures/ca/server.crt -key-file=./fixtures/ca/server.key.insecure
|
||||
```
|
||||
|
||||
```-ca-file``` is the path to the CA cert.
|
||||
|
||||
Try the same request to this server:
|
||||
|
||||
```sh
|
||||
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.
|
||||
|
||||
```
|
||||
...
|
||||
routines:SSL3_READ_BYTES:sslv3 alert bad certificate
|
||||
...
|
||||
```
|
||||
|
||||
We need to give the CA signed cert to the server.
|
||||
|
||||
```sh
|
||||
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/v2/keys/foo -XPUT -d value=bar -v
|
||||
```
|
||||
|
||||
You should able to see:
|
||||
|
||||
```
|
||||
...
|
||||
SSLv3, TLS handshake, CERT verify (15):
|
||||
...
|
||||
TLS handshake, Finished (20)
|
||||
```
|
||||
|
||||
And also the response from the server:
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "set",
|
||||
"node": {
|
||||
"createdIndex": 12,
|
||||
"key": "/foo",
|
||||
"modifiedIndex": 12,
|
||||
"value": "bar"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Why SSLv3 alert handshake failure when using SSL client auth?
|
||||
|
||||
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 ]
|
||||
...
|
||||
extendedKeyUsage = clientAuth
|
||||
...
|
||||
```
|
||||
|
||||
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/machine.crt -infiles machine.csr
|
||||
```
|
93
Documentation/tuning.md
Normal file
93
Documentation/tuning.md
Normal file
@ -0,0 +1,93 @@
|
||||
## Tuning
|
||||
|
||||
The default settings in etcd should work well for installations on a local network where the average network latency is low.
|
||||
However, when using etcd across multiple data centers or over networks with high latency you may need to tweak the heartbeat and election timeout settings.
|
||||
|
||||
### Timeouts
|
||||
|
||||
The underlying distributed consensus protocol relies on two separate timeouts to ensure that nodes can handoff leadership if one stalls or goes offline.
|
||||
The first timeout is called the *Heartbeat Timeout*.
|
||||
This is the frequency with which the leader will notify followers that it is still the leader.
|
||||
etcd batches commands together for higher throughput so this heartbeat timeout is also a delay for how long it takes for commands to be committed.
|
||||
By default, etcd uses a `50ms` heartbeat timeout.
|
||||
|
||||
The second timeout is the *Election Timeout*.
|
||||
This timeout is how long a follower node will go without hearing a heartbeat before attempting to become leader itself.
|
||||
By default, etcd uses a `200ms` election timeout.
|
||||
|
||||
Adjusting these values is a trade off.
|
||||
Lowering the heartbeat timeout will cause individual commands to be committed faster but it will lower the overall throughput of etcd.
|
||||
If your etcd instances have low utilization then lowering the heartbeat timeout can improve your command response time.
|
||||
|
||||
The election timeout should be set based on the heartbeat timeout and your network ping time between nodes.
|
||||
Election timeouts should be at least 10 times your ping time so it can account for variance in your network.
|
||||
For example, if the ping time between your nodes is 10ms then you should have at least a 100ms election timeout.
|
||||
|
||||
You should also set your election timeout to at least 4 to 5 times your heartbeat timeout to account for variance in leader replication.
|
||||
For a heartbeat timeout of 50ms you should set your election timeout to at least 200ms - 250ms.
|
||||
|
||||
You can override the default values on the command line:
|
||||
|
||||
```sh
|
||||
# Command line arguments:
|
||||
$ etcd -peer-heartbeat-timeout=100 -peer-election-timeout=500
|
||||
|
||||
# Environment variables:
|
||||
$ ETCD_PEER_HEARTBEAT_TIMEOUT=100 ETCD_PEER_ELECTION_TIMEOUT=500 etcd
|
||||
```
|
||||
|
||||
Or you can set the values within the configuration file:
|
||||
|
||||
```toml
|
||||
[peer]
|
||||
heartbeat_timeout = 100
|
||||
election_timeout = 100
|
||||
```
|
||||
|
||||
The values are specified in milliseconds.
|
||||
|
||||
|
||||
### Snapshots
|
||||
|
||||
etcd appends all key changes to a log file.
|
||||
This log grows forever and is a complete linear history of every change made to the keys.
|
||||
A complete history works well for lightly used clusters but clusters that are heavily used would carry around a large log.
|
||||
|
||||
To avoid having a huge log etcd makes periodic snapshots.
|
||||
These snapshots provide a way for etcd to compact the log by saving the current state of the system and removing old logs.
|
||||
|
||||
### Snapshot Tuning
|
||||
|
||||
Creating snapshots can be expensive so they're only created after a given number of changes to etcd.
|
||||
By default, snapshots will be made after every 10,000 changes.
|
||||
If etcd's memory usage and disk usage are too high, you can lower the snapshot threshold by setting the following on the command line:
|
||||
|
||||
```sh
|
||||
# Command line arguments:
|
||||
$ etcd -snapshot-count=5000
|
||||
|
||||
# Environment variables:
|
||||
$ ETCD_SNAPSHOT_COUNT=5000 etcd
|
||||
```
|
||||
|
||||
Or you can change the setting in the configuration file:
|
||||
|
||||
```toml
|
||||
snapshot_count = 5000
|
||||
```
|
||||
|
||||
You can also disable snapshotting by adding the following to your command line:
|
||||
|
||||
```sh
|
||||
# Command line arguments:
|
||||
$ etcd -snapshot false
|
||||
|
||||
# Environment variables:
|
||||
$ ETCD_SNAPSHOT=false etcd
|
||||
```
|
||||
|
||||
You can also enable snapshotting within the configuration file:
|
||||
|
||||
```toml
|
||||
snapshot = false
|
||||
```
|
890
README.md
890
README.md
@ -2,7 +2,7 @@
|
||||
|
||||
README version 0.2.0
|
||||
|
||||
[](https://travis-ci.org/coreos/etcd)
|
||||
[](https://drone.io/github.com/coreos/etcd/latest)
|
||||
|
||||
A highly-available key value store for shared configuration and service discovery.
|
||||
etcd is inspired by zookeeper and doozer, with a focus on:
|
||||
@ -32,7 +32,7 @@ Or feel free to just use curl, as in the examples below.
|
||||
|
||||
### Getting etcd
|
||||
|
||||
The latest release is available as a binary at [Github][github-release].
|
||||
The latest release and setup instructions are available at [Github][github-release].
|
||||
|
||||
[github-release]: https://github.com/coreos/etcd/releases/
|
||||
|
||||
@ -47,886 +47,63 @@ cd etcd
|
||||
./build
|
||||
```
|
||||
|
||||
This will generate a binary in the base directory called `./etcd`.
|
||||
This will generate a binary called `./bin/etcd`.
|
||||
|
||||
_NOTE_: you need go 1.1+. Please check your installation with
|
||||
_NOTE_: you need go 1.2+. Please check your installation with
|
||||
|
||||
```
|
||||
go version
|
||||
```
|
||||
|
||||
### Running
|
||||
|
||||
### Running a single machine
|
||||
|
||||
These examples will use a single machine cluster to show you the basics of the etcd REST API.
|
||||
Let's start etcd:
|
||||
First start a single machine cluster of etcd:
|
||||
|
||||
```sh
|
||||
./etcd -data-dir machine0 -name machine0
|
||||
./bin/etcd
|
||||
```
|
||||
|
||||
This will bring up etcd listening on port 4001 for client communication and on port 7001 for server-to-server communication.
|
||||
The `-data-dir machine0` argument tells etcd to write machine configuration, logs and snapshots to the `./machine0/` directory.
|
||||
The `-name machine` tells the rest of the cluster that this machine is named machine0.
|
||||
|
||||
Next lets set a single key and then retrieve it:
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
### Setting the value to a key
|
||||
|
||||
Let’s set the first key-value pair to the datastore.
|
||||
In this case the key is `/message` and the value is `Hello world`.
|
||||
|
||||
```sh
|
||||
curl -L http://127.0.0.1:4001/v2/keys/message -X PUT -d value="Hello world"
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "set",
|
||||
"node": {
|
||||
"createdIndex": 2,
|
||||
"key": "/message",
|
||||
"modifiedIndex": 2,
|
||||
"value": "Hello world"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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 `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`.
|
||||
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. 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.
|
||||
|
||||
|
||||
### Get the value of a key
|
||||
|
||||
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/v2/keys/message
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "get",
|
||||
"node": {
|
||||
"createdIndex": 2,
|
||||
"key": "/message",
|
||||
"modifiedIndex": 2,
|
||||
"value": "Hello world"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### 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/v2/keys/message -XPUT -d value="Hello etcd"
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "set",
|
||||
"node": {
|
||||
"createdIndex": 3,
|
||||
"key": "/message",
|
||||
"modifiedIndex": 3,
|
||||
"prevValue": "Hello world",
|
||||
"value": "Hello etcd"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Notice that `node.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.
|
||||
|
||||
|
||||
### Deleting a key
|
||||
|
||||
You can remove the `/message` key with a `DELETE` request:
|
||||
|
||||
```sh
|
||||
curl -L http://127.0.0.1:4001/v2/keys/message -XDELETE
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "delete",
|
||||
"node": {
|
||||
"createdIndex": 3,
|
||||
"key": "/message",
|
||||
"modifiedIndex": 4,
|
||||
"prevValue": "Hello etcd"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Using key TTL
|
||||
|
||||
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/v2/keys/foo -XPUT -d value=bar -d ttl=5
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "set",
|
||||
"node": {
|
||||
"createdIndex": 5,
|
||||
"expiration": "2013-12-04T12:01:21.874888581-08:00",
|
||||
"key": "/foo",
|
||||
"modifiedIndex": 5,
|
||||
"ttl": 5,
|
||||
"value": "bar"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note the two new fields in response:
|
||||
|
||||
1. The `expiration` is the time that this key will expire and be deleted.
|
||||
|
||||
2. The `ttl` is the time to live for the key, in seconds.
|
||||
|
||||
_NOTE_: Keys can only be expired by a cluster leader so if a machine 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/v2/keys/foo
|
||||
```
|
||||
|
||||
If the TTL has expired, the key will be deleted, and you will be returned a 100.
|
||||
|
||||
```json
|
||||
{
|
||||
"cause": "/foo",
|
||||
"errorCode": 100,
|
||||
"index": 6,
|
||||
"message": "Key Not Found"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Waiting for a change
|
||||
|
||||
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/v2/keys/foo?wait=true
|
||||
```
|
||||
|
||||
Now we are waiting for any changes at path `/foo`.
|
||||
|
||||
In another terminal, we set a key `/foo` with value `bar`:
|
||||
|
||||
```sh
|
||||
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",
|
||||
"node": {
|
||||
"createdIndex": 7,
|
||||
"key": "/foo",
|
||||
"modifiedIndex": 7,
|
||||
"value": "bar"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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 7 again:
|
||||
|
||||
```sh
|
||||
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 Compare-and-Swap (CAS)
|
||||
|
||||
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/v2/keys/foo -XPUT -d value=one
|
||||
```
|
||||
|
||||
Let's try some invalid `CompareAndSwap` commands first.
|
||||
|
||||
Trying to set this existing key with `prevExist=false` fails as expected:
|
||||
```sh
|
||||
curl -L http://127.0.0.1:4001/v2/keys/foo?prevExist=false -XPUT -d value=three
|
||||
```
|
||||
|
||||
The error code explains the problem:
|
||||
|
||||
```json
|
||||
{
|
||||
"cause": "/foo",
|
||||
"errorCode": 105,
|
||||
"index": 39776,
|
||||
"message": "Already exists"
|
||||
}
|
||||
```
|
||||
|
||||
Now lets provide a `prevValue` parameter:
|
||||
|
||||
```sh
|
||||
curl -L http://127.0.0.1:4001/v2/keys/foo?prevValue=two -XPUT -d value=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
|
||||
{
|
||||
"cause": "[two != one] [0 != 8]",
|
||||
"errorCode": 101,
|
||||
"index": 8,
|
||||
"message": "Test Failed"
|
||||
}
|
||||
```
|
||||
|
||||
which means `CompareAndSwap` failed.
|
||||
|
||||
Let's try a valid condition:
|
||||
|
||||
```sh
|
||||
curl -L http://127.0.0.1:4001/v2/keys/foo?prevValue=one -XPUT -d value=two
|
||||
```
|
||||
|
||||
The response should be
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "compareAndSwap",
|
||||
"node": {
|
||||
"createdIndex": 8,
|
||||
"key": "/foo",
|
||||
"modifiedIndex": 9,
|
||||
"prevValue": "one",
|
||||
"value": "two"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
We successfully changed the value from "one" to "two" since we gave the correct previous value.
|
||||
|
||||
|
||||
### Listing a directory
|
||||
|
||||
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.
|
||||
|
||||
In this example, let's first create some keys:
|
||||
|
||||
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/v2/keys/foo_dir/foo -XPUT -d value=bar
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "set",
|
||||
"node": {
|
||||
"createdIndex": 2,
|
||||
"key": "/foo_dir/foo",
|
||||
"modifiedIndex": 2,
|
||||
"value": "bar"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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",
|
||||
"node": {
|
||||
"dir": true,
|
||||
"key": "/",
|
||||
"nodes": [
|
||||
{
|
||||
"createdIndex": 2,
|
||||
"dir": true,
|
||||
"key": "/foo_dir",
|
||||
"modifiedIndex": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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",
|
||||
"node": {
|
||||
"dir": true,
|
||||
"key": "/",
|
||||
"nodes": [
|
||||
{
|
||||
"createdIndex": 2,
|
||||
"dir": true,
|
||||
"key": "/foo_dir",
|
||||
"modifiedIndex": 2,
|
||||
"nodes": [
|
||||
{
|
||||
"createdIndex": 2,
|
||||
"key": "/foo_dir/foo",
|
||||
"modifiedIndex": 2,
|
||||
"value": "bar"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### 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",
|
||||
"node": {
|
||||
"createdIndex": 10,
|
||||
"dir": true,
|
||||
"key": "/foo_dir",
|
||||
"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",
|
||||
"node": {
|
||||
"createdIndex": 3,
|
||||
"key": "/_message",
|
||||
"modifiedIndex": 3,
|
||||
"value": "Hello hidden world"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
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",
|
||||
"node": {
|
||||
"createdIndex": 4,
|
||||
"key": "/message",
|
||||
"modifiedIndex": 4,
|
||||
"value": "Hello world"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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",
|
||||
"node": {
|
||||
"dir": true,
|
||||
"key": "/",
|
||||
"nodes": [
|
||||
{
|
||||
"createdIndex": 2,
|
||||
"dir": true,
|
||||
"key": "/foo_dir",
|
||||
"modifiedIndex": 2
|
||||
},
|
||||
{
|
||||
"createdIndex": 4,
|
||||
"key": "/message",
|
||||
"modifiedIndex": 4,
|
||||
"value": "Hello world"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
Let's configure etcd to use this keypair:
|
||||
|
||||
```sh
|
||||
./etcd -f -name machine0 -data-dir machine0 -cert-file=./fixtures/ca/server.crt -key-file=./fixtures/ca/server.key.insecure
|
||||
```
|
||||
|
||||
There are a few new options we're using:
|
||||
|
||||
* `-f` - forces a new machine configuration, even if an existing configuration is found. (WARNING: data loss!)
|
||||
* `-cert-file` and `-key-file` 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/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.
|
||||
|
||||
**OSX 10.9+ Users**: curl 7.30.0 on OSX 10.9+ doesn't understand certificates passed in on the command line.
|
||||
Instead you must import the dummy ca.crt directly into the keychain or add the `-k` flag to curl to ignore errors.
|
||||
If you want to test without the `-k` flag run `open ./fixtures/ca/ca.crt` and follow the prompts.
|
||||
Please remove this certificate after you are done testing!
|
||||
If you know of a workaround let us know.
|
||||
|
||||
```
|
||||
...
|
||||
SSLv3, TLS handshake, Finished (20):
|
||||
...
|
||||
```
|
||||
|
||||
And also the response from the etcd server:
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "set",
|
||||
"key": "/foo",
|
||||
"modifiedIndex": 3,
|
||||
"prevValue": "bar",
|
||||
"value": "bar"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### 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.
|
||||
|
||||
```sh
|
||||
./etcd -f -name machine0 -data-dir machine0 -ca-file=./fixtures/ca/ca.crt -cert-file=./fixtures/ca/server.crt -key-file=./fixtures/ca/server.key.insecure
|
||||
```
|
||||
|
||||
```-ca-file``` is the path to the CA cert.
|
||||
|
||||
Try the same request to this server:
|
||||
|
||||
```sh
|
||||
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.
|
||||
|
||||
```
|
||||
...
|
||||
routines:SSL3_READ_BYTES:sslv3 alert bad certificate
|
||||
...
|
||||
```
|
||||
|
||||
We need to give the CA signed cert to the server.
|
||||
|
||||
```sh
|
||||
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/v2/keys/foo -XPUT -d value=bar -v
|
||||
```
|
||||
|
||||
You should able to see:
|
||||
|
||||
```
|
||||
...
|
||||
SSLv3, TLS handshake, CERT verify (15):
|
||||
...
|
||||
TLS handshake, Finished (20)
|
||||
```
|
||||
|
||||
And also the response from the server:
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "set",
|
||||
"node": {
|
||||
"createdIndex": 12,
|
||||
"key": "/foo",
|
||||
"modifiedIndex": 12,
|
||||
"prevValue": "two",
|
||||
"value": "bar"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Clustering
|
||||
|
||||
### Example cluster of three machines
|
||||
|
||||
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.
|
||||
|
||||
Let start by creating 3 new etcd instances.
|
||||
|
||||
We use `-peer-addr` to specify server port and `-addr` to specify client port and `-data-dir` to specify the directory to store the log and info of the machine in the cluster:
|
||||
|
||||
```sh
|
||||
./etcd -peer-addr 127.0.0.1:7001 -addr 127.0.0.1:4001 -data-dir machines/machine1 -name machine1
|
||||
```
|
||||
|
||||
**Note:** If you want to run etcd on an external IP address and still have access locally, you'll need to add `-bind-addr 0.0.0.0` so that it will listen on both external and localhost addresses.
|
||||
A similar argument `-peer-bind-addr` is used to setup the listening address for the server port.
|
||||
|
||||
Let's join two more machines to this cluster using the `-peers` argument:
|
||||
|
||||
```sh
|
||||
./etcd -peer-addr 127.0.0.1:7002 -addr 127.0.0.1:4002 -peers 127.0.0.1:7001 -data-dir machines/machine2 -name machine2
|
||||
./etcd -peer-addr 127.0.0.1:7003 -addr 127.0.0.1:4003 -peers 127.0.0.1:7001 -data-dir machines/machine3 -name machine3
|
||||
```
|
||||
|
||||
We can retrieve a list of machines in the cluster using the HTTP API:
|
||||
|
||||
```sh
|
||||
curl -L http://127.0.0.1:4001/v2/machines
|
||||
```
|
||||
|
||||
We should see there are three machines 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 the main key API:
|
||||
|
||||
```sh
|
||||
curl -L http://127.0.0.1:4001/v2/keys/_etcd/machines
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "get",
|
||||
"node": {
|
||||
"createdIndex": 1,
|
||||
"dir": true,
|
||||
"key": "/_etcd/machines",
|
||||
"modifiedIndex": 1,
|
||||
"nodes": [
|
||||
{
|
||||
"createdIndex": 1,
|
||||
"key": "/_etcd/machines/machine1",
|
||||
"modifiedIndex": 1,
|
||||
"value": "raft=http://127.0.0.1:7001&etcd=http://127.0.0.1:4001"
|
||||
},
|
||||
{
|
||||
"createdIndex": 2,
|
||||
"key": "/_etcd/machines/machine2",
|
||||
"modifiedIndex": 2,
|
||||
"value": "raft=http://127.0.0.1:7002&etcd=http://127.0.0.1:4002"
|
||||
},
|
||||
{
|
||||
"createdIndex": 3,
|
||||
"key": "/_etcd/machines/machine3",
|
||||
"modifiedIndex": 3,
|
||||
"value": "raft=http://127.0.0.1:7003&etcd=http://127.0.0.1:4003"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
We can also get the current leader in the cluster:
|
||||
|
||||
```
|
||||
curl -L http://127.0.0.1:4001/v2/leader
|
||||
```
|
||||
|
||||
The first server we set up should still be the leader unless it has died during these commands.
|
||||
|
||||
curl -L http://127.0.0.1:4001/v2/keys/mykey -XPUT -d value="this is awesome"
|
||||
curl -L http://127.0.0.1:4001/v2/keys/mykey
|
||||
```
|
||||
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/v2/keys/foo -XPUT -d value=bar
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "set",
|
||||
"node": {
|
||||
"createdIndex": 4,
|
||||
"key": "/foo",
|
||||
"modifiedIndex": 4,
|
||||
"value": "bar"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Killing Nodes in the Cluster
|
||||
|
||||
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/v2/keys/foo
|
||||
```
|
||||
|
||||
We can also see that a new leader has been elected:
|
||||
|
||||
```
|
||||
curl -L http://127.0.0.1:4002/v2/leader
|
||||
```
|
||||
|
||||
```
|
||||
http://127.0.0.1:7002
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```
|
||||
http://127.0.0.1:7003
|
||||
```
|
||||
|
||||
|
||||
### Testing Persistence
|
||||
|
||||
Next we'll kill all the machines to test persistence.
|
||||
Type `CTRL-C` on each terminal and then rerun the same command you used to start each machine.
|
||||
|
||||
Your request for the `foo` key will return the correct value:
|
||||
|
||||
```sh
|
||||
curl -L http://127.0.0.1:4002/v2/keys/foo
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "get",
|
||||
"node": {
|
||||
"createdIndex": 4,
|
||||
"key": "/foo",
|
||||
"modifiedIndex": 4,
|
||||
"value": "bar"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Using HTTPS between servers
|
||||
You have successfully started an etcd on a single machine and written a key to the store. Now it time to dig into the full etcd API and other guides.
|
||||
|
||||
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 `-*-file` flags to `-peer-*-file`.
|
||||
### Next Steps
|
||||
|
||||
If you are using SSL for server-to-server communication, you must use it on all instances of etcd.
|
||||
- Explore the full [API][api.md].
|
||||
- Setup a [multi-machine cluster][clustering.md].
|
||||
- Learn the [config format, env variables and flags][configuration.md].
|
||||
- Find [language bindings and tools][libraries-and-tools.md].
|
||||
- Learn about the dashboard, lock and leader election [modules][modules.md].
|
||||
- Use TLS to [secure an etcd cluster][security.md].
|
||||
- [Tune etcd][tuning.md].
|
||||
|
||||
[api.md]: https://github.com/coreos/etcd/blob/master/Documentation/api.md
|
||||
[clustering.md]: https://github.com/coreos/etcd/blob/master/Documentation/clustering.md
|
||||
[configuration.md]: https://github.com/coreos/etcd/blob/master/Documentation/configuration.md
|
||||
[libraries-and-tools.md]: https://github.com/coreos/etcd/blob/master/Documentation/libraries-and-tools.md
|
||||
[modules.md]: https://github.com/coreos/etcd/blob/master/Documentation/modules.md
|
||||
[security.md]: https://github.com/coreos/etcd/blob/master/Documentation/security.md
|
||||
[tuning.md]: https://github.com/coreos/etcd/blob/master/Documentation/tuning.md
|
||||
|
||||
## 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**
|
||||
|
||||
- [etcdctl](https://github.com/coreos/etcdctl) - A command line client for etcd
|
||||
|
||||
**Go libraries**
|
||||
|
||||
- [go-etcd](https://github.com/coreos/go-etcd)
|
||||
|
||||
**Java libraries**
|
||||
|
||||
- [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)
|
||||
- [jplana/python-etcd](https://github.com/jplana/python-etcd)
|
||||
- [russellhaering/txetcd](https://github.com/russellhaering/txetcd) - a Twisted Python library
|
||||
|
||||
**Node libraries**
|
||||
|
||||
- [stianeikeland/node-etcd](https://github.com/stianeikeland/node-etcd)
|
||||
|
||||
**Ruby libraries**
|
||||
|
||||
- [iconara/etcd-rb](https://github.com/iconara/etcd-rb)
|
||||
- [jpfuentes2/etcd-ruby](https://github.com/jpfuentes2/etcd-ruby)
|
||||
- [ranjib/etcd-ruby](https://github.com/ranjib/etcd-ruby)
|
||||
|
||||
**C libraries**
|
||||
|
||||
- [jdarcy/etcd-api](https://github.com/jdarcy/etcd-api)
|
||||
|
||||
**Clojure libraries**
|
||||
|
||||
- [aterreno/etcd-clojure](https://github.com/aterreno/etcd-clojure)
|
||||
- [rthomas/clj-etcd](https://github.com/rthomas/clj-etcd)
|
||||
|
||||
**Erlang libraries**
|
||||
|
||||
- [marshall-lee/etcd.erl](https://github.com/marshall-lee/etcd.erl)
|
||||
|
||||
**Chef Integration**
|
||||
|
||||
- [coderanger/etcd-chef](https://github.com/coderanger/etcd-chef)
|
||||
|
||||
**Chef Cookbook**
|
||||
|
||||
- [spheromak/etcd-cookbook](https://github.com/spheromak/etcd-cookbook)
|
||||
|
||||
**Projects using etcd**
|
||||
|
||||
- [binocarlos/yoda](https://github.com/binocarlos/yoda) - etcd + ZeroMQ
|
||||
- [calavera/active-proxy](https://github.com/calavera/active-proxy) - HTTP Proxy configured with etcd
|
||||
- [derekchiang/etcdplus](https://github.com/derekchiang/etcdplus) - A set of distributed synchronization primitives built upon etcd
|
||||
- [go-discover](https://github.com/flynn/go-discover) - service discovery in Go
|
||||
- [gleicon/goreman](https://github.com/gleicon/goreman/tree/etcd) - Branch of the Go Foreman clone with etcd support
|
||||
- [garethr/hiera-etcd](https://github.com/garethr/hiera-etcd) - Puppet hiera backend using etcd
|
||||
- [mattn/etcd-vim](https://github.com/mattn/etcd-vim) - SET and GET keys from inside vim
|
||||
- [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.
|
||||
The command is not committed until the majority of the cluster peers 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 peers.
|
||||
|
||||
Odd numbers are good because if you have 8 peers the majority will be 5 and if you have 9 peers the majority will still be 5.
|
||||
The result is that an 8 peer cluster can tolerate 3 peer failures and a 9 peer cluster can tolerate 4 machine failures.
|
||||
And in the best case when all 9 peers are responding the cluster will perform at the speed of the fastest 5 machines.
|
||||
|
||||
|
||||
### Why SSLv3 alert handshake failure when using SSL client auth?
|
||||
|
||||
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 ]
|
||||
...
|
||||
extendedKeyUsage = clientAuth
|
||||
...
|
||||
```
|
||||
|
||||
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/machine.crt -infiles machine.csr
|
||||
```
|
||||
|
||||
|
||||
## Project Details
|
||||
|
||||
### Versioning
|
||||
|
||||
#### Service Versioning
|
||||
|
||||
etcd uses [semantic versioning][semver].
|
||||
New minor versions may add additional features to the API however.
|
||||
New minor versions may add additional features to the API.
|
||||
|
||||
You can get the version of etcd by issuing a request to /version:
|
||||
|
||||
@ -934,10 +111,15 @@ You can get the version of etcd by issuing a request to /version:
|
||||
curl -L http://127.0.0.1:4001/version
|
||||
```
|
||||
|
||||
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/
|
||||
|
||||
#### API Versioning
|
||||
|
||||
Clients are encouraged to use the `v2` API. The `v1` API will not change.
|
||||
|
||||
The `v2` API responses should not change after the 0.2.0 release but new features will be added over time.
|
||||
|
||||
During the pre-v1.0.0 series of releases we may break the API as we fix bugs and get feedback.
|
||||
|
||||
### License
|
||||
|
||||
|
63
bench/bench.go
Normal file
63
bench/bench.go
Normal file
@ -0,0 +1,63 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"strconv"
|
||||
|
||||
"github.com/coreos/etcd/third_party/github.com/coreos/go-etcd/etcd"
|
||||
)
|
||||
|
||||
func write(endpoint string, requests int, end chan int) {
|
||||
client := etcd.NewClient([]string{endpoint})
|
||||
|
||||
for i := 0; i < requests; i++ {
|
||||
key := strconv.Itoa(i)
|
||||
_, err := client.Set(key, key, 0)
|
||||
if err != nil {
|
||||
println(err.Error())
|
||||
}
|
||||
}
|
||||
end <- 1
|
||||
}
|
||||
|
||||
func watch(endpoint string, key string) {
|
||||
client := etcd.NewClient([]string{endpoint})
|
||||
|
||||
receiver := make(chan *etcd.Response)
|
||||
go client.Watch(key, 0, true, receiver, nil)
|
||||
|
||||
log.Printf("watching: %s", key)
|
||||
|
||||
received := 0
|
||||
for {
|
||||
<-receiver
|
||||
received++
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
endpoint := flag.String("endpoint", "http://127.0.0.1:4001", "etcd HTTP endpoint")
|
||||
|
||||
rWrites := flag.Int("write-requests", 50000, "number of writes")
|
||||
cWrites := flag.Int("concurrent-writes", 500, "number of concurrent writes")
|
||||
|
||||
watches := flag.Int("watches", 500, "number of writes")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
for i := 0; i < *watches; i++ {
|
||||
key := strconv.Itoa(i)
|
||||
go watch(*endpoint, key)
|
||||
}
|
||||
|
||||
wChan := make(chan int, *cWrites)
|
||||
for i := 0; i < *cWrites; i++ {
|
||||
go write(*endpoint, (*rWrites / *cWrites), wChan)
|
||||
}
|
||||
|
||||
for i := 0; i < *cWrites; i++ {
|
||||
<-wChan
|
||||
log.Printf("Completed %d writes", (*rWrites / *cWrites))
|
||||
}
|
||||
}
|
30
build
30
build
@ -1,26 +1,12 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
#!/bin/sh -e
|
||||
|
||||
ETCD_PACKAGE=github.com/coreos/etcd
|
||||
export GOPATH="${PWD}"
|
||||
SRC_DIR="$GOPATH/src"
|
||||
ETCD_DIR="$SRC_DIR/$ETCD_PACKAGE"
|
||||
|
||||
ETCD_BASE=$(dirname "${ETCD_DIR}")
|
||||
if [ ! -d "${ETCD_BASE}" ]; then
|
||||
mkdir -p "${ETCD_BASE}"
|
||||
if [ ! -h src/github.com/coreos/etcd ]; then
|
||||
mkdir -p src/github.com/coreos/
|
||||
ln -s ../../.. src/github.com/coreos/etcd
|
||||
fi
|
||||
|
||||
if [ ! -h "${ETCD_DIR}" ]; then
|
||||
ln -s ../../../ "${ETCD_DIR}"
|
||||
fi
|
||||
export GOBIN=${PWD}/bin
|
||||
export GOPATH=${PWD}
|
||||
|
||||
for i in third_party/*; do
|
||||
if [ "$i" = "third_party/src" ]; then
|
||||
continue
|
||||
fi
|
||||
cp -R "$i" src/
|
||||
done
|
||||
|
||||
./scripts/release-version > server/release_version.go
|
||||
go build "${ETCD_PACKAGE}"
|
||||
go install github.com/coreos/etcd
|
||||
go install github.com/coreos/etcd/bench
|
||||
|
24
build.ps1
24
build.ps1
@ -1,24 +0,0 @@
|
||||
|
||||
$ETCD_PACKAGE="github.com/coreos/etcd"
|
||||
$env:GOPATH=$pwd.Path
|
||||
$SRC_DIR="$env:GOPATH/src"
|
||||
$ETCD_DIR="$SRC_DIR/$ETCD_PACKAGE"
|
||||
$env:ETCD_DIR="$SRC_DIR/$ETCD_PACKAGE"
|
||||
|
||||
$ETCD_BASE=(Split-Path $ETCD_DIR -Parent)
|
||||
if(-not(test-path $ETCD_DIR)){
|
||||
mkdir -force "$ETCD_BASE" > $null
|
||||
}
|
||||
|
||||
if(-not(test-path $ETCD_DIR )){
|
||||
cmd /c 'mklink /D "%ETCD_DIR%" ..\..\..\'
|
||||
}
|
||||
|
||||
foreach($i in (ls third_party/*)){
|
||||
if("$i" -eq "third_party/src") {continue}
|
||||
|
||||
cp -Recurse -force "$i" src/
|
||||
}
|
||||
|
||||
./scripts/release-version.ps1 | Out-File -Encoding UTF8 server/release_version.go
|
||||
go build -v "${ETCD_PACKAGE}"
|
@ -1,7 +1,6 @@
|
||||
package server
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
@ -13,8 +12,12 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/etcd/third_party/github.com/BurntSushi/toml"
|
||||
|
||||
"github.com/coreos/etcd/discovery"
|
||||
"github.com/coreos/etcd/log"
|
||||
"github.com/BurntSushi/toml"
|
||||
ustrings "github.com/coreos/etcd/pkg/strings"
|
||||
"github.com/coreos/etcd/server"
|
||||
)
|
||||
|
||||
// The default location for the etcd configuration file.
|
||||
@ -53,6 +56,7 @@ type Config struct {
|
||||
CPUProfileFile string
|
||||
CorsOrigins []string `toml:"cors" env:"ETCD_CORS"`
|
||||
DataDir string `toml:"data_dir" env:"ETCD_DATA_DIR"`
|
||||
Discovery string `toml:"discovery" env:"ETCD_DISCOVERY"`
|
||||
Force bool
|
||||
KeyFile string `toml:"key_file" env:"ETCD_KEY_FILE"`
|
||||
Peers []string `toml:"peers" env:"ETCD_PEERS"`
|
||||
@ -60,6 +64,7 @@ type Config struct {
|
||||
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"`
|
||||
RetryInterval float64 `toml:"retry_interval" env:"ETCD_RETRY_INTERVAL"`
|
||||
Name string `toml:"name" env:"ETCD_NAME"`
|
||||
Snapshot bool `toml:"snapshot" env:"ETCD_SNAPSHOT"`
|
||||
SnapshotCount int `toml:"snapshot_count" env:"ETCD_SNAPSHOTCOUNT"`
|
||||
@ -67,26 +72,34 @@ type Config struct {
|
||||
ShowVersion bool
|
||||
Verbose bool `toml:"verbose" env:"ETCD_VERBOSE"`
|
||||
VeryVerbose bool `toml:"very_verbose" env:"ETCD_VERY_VERBOSE"`
|
||||
|
||||
Peer struct {
|
||||
Addr string `toml:"addr" env:"ETCD_PEER_ADDR"`
|
||||
BindAddr string `toml:"bind_addr" env:"ETCD_PEER_BIND_ADDR"`
|
||||
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"`
|
||||
VeryVeryVerbose bool `toml:"very_very_verbose" env:"ETCD_VERY_VERY_VERBOSE"`
|
||||
Peer struct {
|
||||
Addr string `toml:"addr" env:"ETCD_PEER_ADDR"`
|
||||
BindAddr string `toml:"bind_addr" env:"ETCD_PEER_BIND_ADDR"`
|
||||
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"`
|
||||
HeartbeatTimeout int `toml:"heartbeat_timeout" env:"ETCD_PEER_HEARTBEAT_TIMEOUT"`
|
||||
ElectionTimeout int `toml:"election_timeout" env:"ETCD_PEER_ELECTION_TIMEOUT"`
|
||||
}
|
||||
strTrace string `toml:"trace" env:"ETCD_TRACE"`
|
||||
GraphiteHost string `toml:"graphite_host" env:"ETCD_GRAPHITE_HOST"`
|
||||
}
|
||||
|
||||
// NewConfig returns a Config initialized with default values.
|
||||
func NewConfig() *Config {
|
||||
// New returns a Config initialized with default values.
|
||||
func New() *Config {
|
||||
c := new(Config)
|
||||
c.SystemPath = DefaultSystemConfigPath
|
||||
c.Addr = "127.0.0.1:4001"
|
||||
c.MaxClusterSize = 9
|
||||
c.MaxResultBuffer = 1024
|
||||
c.MaxRetryAttempts = 3
|
||||
c.Peer.Addr = "127.0.0.1:7001"
|
||||
c.RetryInterval = 10.0
|
||||
c.Snapshot = true
|
||||
c.SnapshotCount = 10000
|
||||
c.Peer.Addr = "127.0.0.1:7001"
|
||||
c.Peer.HeartbeatTimeout = defaultHeartbeatTimeout
|
||||
c.Peer.ElectionTimeout = defaultElectionTimeout
|
||||
return c
|
||||
}
|
||||
|
||||
@ -131,6 +144,13 @@ func (c *Config) Load(arguments []string) error {
|
||||
return fmt.Errorf("sanitize: %v", err)
|
||||
}
|
||||
|
||||
// Attempt cluster discovery
|
||||
if c.Discovery != "" {
|
||||
if err := c.handleDiscovery(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Force remove server configuration if specified.
|
||||
if c.Force {
|
||||
c.Reset()
|
||||
@ -189,12 +209,42 @@ func (c *Config) loadEnv(target interface{}) error {
|
||||
case reflect.String:
|
||||
value.Field(i).SetString(v)
|
||||
case reflect.Slice:
|
||||
value.Field(i).Set(reflect.ValueOf(trimsplit(v, ",")))
|
||||
value.Field(i).Set(reflect.ValueOf(ustrings.TrimSplit(v, ",")))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Config) handleDiscovery() error {
|
||||
p, err := discovery.Do(c.Discovery, c.Name, c.Peer.Addr)
|
||||
|
||||
// This is fatal, discovery encountered an unexpected error
|
||||
// and we have no peer list.
|
||||
if err != nil && len(c.Peers) == 0 {
|
||||
log.Fatalf("Discovery failed and a backup peer list wasn't provided: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Warn about errors coming from discovery, this isn't fatal
|
||||
// since the user might have provided a peer list elsewhere.
|
||||
if err != nil {
|
||||
log.Warnf("Discovery encountered an error but a backup peer list (%v) was provided: %v", c.Peers, err)
|
||||
}
|
||||
|
||||
for i := range p {
|
||||
// Strip the scheme off of the peer if it has one
|
||||
// TODO(bp): clean this up!
|
||||
purl, err := url.Parse(p[i])
|
||||
if err == nil {
|
||||
p[i] = purl.Host
|
||||
}
|
||||
}
|
||||
|
||||
c.Peers = p
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Loads configuration from command line flags.
|
||||
func (c *Config) LoadFlags(arguments []string) error {
|
||||
var peers, cors, path string
|
||||
@ -210,13 +260,15 @@ func (c *Config) LoadFlags(arguments []string) error {
|
||||
f.BoolVar(&c.Force, "force", false, "")
|
||||
|
||||
f.BoolVar(&c.Verbose, "v", c.Verbose, "")
|
||||
f.BoolVar(&c.VeryVerbose, "vv", c.Verbose, "")
|
||||
f.BoolVar(&c.VeryVerbose, "vv", c.VeryVerbose, "")
|
||||
f.BoolVar(&c.VeryVeryVerbose, "vvv", c.VeryVeryVerbose, "")
|
||||
|
||||
f.StringVar(&peers, "peers", "", "")
|
||||
f.StringVar(&c.PeersFile, "peers-file", c.PeersFile, "")
|
||||
|
||||
f.StringVar(&c.Name, "name", c.Name, "")
|
||||
f.StringVar(&c.Addr, "addr", c.Addr, "")
|
||||
f.StringVar(&c.Discovery, "discovery", c.Discovery, "")
|
||||
f.StringVar(&c.BindAddr, "bind-addr", c.BindAddr, "")
|
||||
f.StringVar(&c.Peer.Addr, "peer-addr", c.Peer.Addr, "")
|
||||
f.StringVar(&c.Peer.BindAddr, "peer-bind-addr", c.Peer.BindAddr, "")
|
||||
@ -232,13 +284,20 @@ func (c *Config) LoadFlags(arguments []string) error {
|
||||
f.StringVar(&c.DataDir, "data-dir", c.DataDir, "")
|
||||
f.IntVar(&c.MaxResultBuffer, "max-result-buffer", c.MaxResultBuffer, "")
|
||||
f.IntVar(&c.MaxRetryAttempts, "max-retry-attempts", c.MaxRetryAttempts, "")
|
||||
f.Float64Var(&c.RetryInterval, "retry-interval", c.RetryInterval, "")
|
||||
f.IntVar(&c.MaxClusterSize, "max-cluster-size", c.MaxClusterSize, "")
|
||||
f.IntVar(&c.Peer.HeartbeatTimeout, "peer-heartbeat-timeout", c.Peer.HeartbeatTimeout, "")
|
||||
f.IntVar(&c.Peer.ElectionTimeout, "peer-election-timeout", c.Peer.ElectionTimeout, "")
|
||||
|
||||
f.StringVar(&cors, "cors", "", "")
|
||||
|
||||
f.BoolVar(&c.Snapshot, "snapshot", c.Snapshot, "")
|
||||
f.IntVar(&c.SnapshotCount, "snapshot-count", c.SnapshotCount, "")
|
||||
f.StringVar(&c.CPUProfileFile, "cpuprofile", "", "")
|
||||
|
||||
f.StringVar(&c.strTrace, "trace", "", "")
|
||||
f.StringVar(&c.GraphiteHost, "graphite-host", "", "")
|
||||
|
||||
// BEGIN IGNORED FLAGS
|
||||
f.StringVar(&path, "config", "", "")
|
||||
// BEGIN IGNORED FLAGS
|
||||
@ -271,16 +330,16 @@ func (c *Config) LoadFlags(arguments []string) error {
|
||||
// Print deprecation warnings on STDERR.
|
||||
f.Visit(func(f *flag.Flag) {
|
||||
if len(newFlagNameLookup[f.Name]) > 0 {
|
||||
fmt.Fprintf(os.Stderr, "[deprecated] use -%s, not -%s", newFlagNameLookup[f.Name], f.Name)
|
||||
fmt.Fprintf(os.Stderr, "[deprecated] use -%s, not -%s\n", newFlagNameLookup[f.Name], f.Name)
|
||||
}
|
||||
})
|
||||
|
||||
// Convert some parameters to lists.
|
||||
if peers != "" {
|
||||
c.Peers = trimsplit(peers, ",")
|
||||
c.Peers = ustrings.TrimSplit(peers, ",")
|
||||
}
|
||||
if cors != "" {
|
||||
c.CorsOrigins = trimsplit(cors, ",")
|
||||
c.CorsOrigins = ustrings.TrimSplit(cors, ",")
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -296,7 +355,7 @@ func (c *Config) LoadPeersFile() error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("Peers file error: %s", err)
|
||||
}
|
||||
c.Peers = trimsplit(string(b), ",")
|
||||
c.Peers = ustrings.TrimSplit(string(b), ",")
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -322,9 +381,6 @@ func (c *Config) NameFromHostname() {
|
||||
|
||||
// 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
|
||||
}
|
||||
@ -338,66 +394,18 @@ func (c *Config) Reset() error {
|
||||
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.Addr
|
||||
info.EtcdListenHost = c.BindAddr
|
||||
info.RaftURL = c.Peer.Addr
|
||||
info.RaftListenHost = c.Peer.BindAddr
|
||||
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
|
||||
}
|
||||
var err error
|
||||
|
||||
// Sanitize the URLs first.
|
||||
if c.Addr, err = sanitizeURL(c.Addr, tlsConfig.Scheme); err != nil {
|
||||
if c.Addr, err = sanitizeURL(c.Addr, c.EtcdTLSInfo().Scheme()); err != nil {
|
||||
return fmt.Errorf("Advertised URL: %s", err)
|
||||
}
|
||||
if c.BindAddr, err = sanitizeBindAddr(c.BindAddr, c.Addr); err != nil {
|
||||
return fmt.Errorf("Listen Host: %s", err)
|
||||
}
|
||||
if c.Peer.Addr, err = sanitizeURL(c.Peer.Addr, peerTlsConfig.Scheme); err != nil {
|
||||
if c.Peer.Addr, err = sanitizeURL(c.Peer.Addr, c.PeerTLSInfo().Scheme()); err != nil {
|
||||
return fmt.Errorf("Peer Advertised URL: %s", err)
|
||||
}
|
||||
if c.Peer.BindAddr, err = sanitizeBindAddr(c.Peer.BindAddr, c.Peer.Addr); err != nil {
|
||||
@ -410,39 +418,40 @@ func (c *Config) Sanitize() error {
|
||||
c.NameFromHostname()
|
||||
}
|
||||
|
||||
if c.DataDir == "" && c.Name != "" {
|
||||
if c.DataDir == "" && c.Name != "" && !c.ShowVersion && !c.ShowHelp {
|
||||
c.DataDirFromName()
|
||||
}
|
||||
|
||||
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,
|
||||
// EtcdTLSInfo retrieves a TLSInfo object for the etcd server
|
||||
func (c *Config) EtcdTLSInfo() server.TLSInfo {
|
||||
return server.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,
|
||||
// PeerRaftInfo retrieves a TLSInfo object for the peer server.
|
||||
func (c *Config) PeerTLSInfo() server.TLSInfo {
|
||||
return server.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()
|
||||
// MetricsBucketName generates the name that should be used for a
|
||||
// corresponding MetricsBucket object
|
||||
func (c *Config) MetricsBucketName() string {
|
||||
return fmt.Sprintf("etcd.%s", c.Name)
|
||||
}
|
||||
|
||||
// Trace determines if any trace-level information should be emitted
|
||||
func (c *Config) Trace() bool {
|
||||
return c.strTrace == "*"
|
||||
}
|
||||
|
||||
// sanitizeURL will cleanup a host string in the format hostname[:port] and
|
@ -1,12 +1,12 @@
|
||||
package server
|
||||
package config
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/coreos/etcd/third_party/github.com/BurntSushi/toml"
|
||||
"github.com/coreos/etcd/third_party/github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Ensures that a configuration can be deserialized from TOML.
|
||||
@ -18,6 +18,7 @@ func TestConfigTOML(t *testing.T) {
|
||||
cors = ["*"]
|
||||
cpu_profile_file = "XXX"
|
||||
data_dir = "/tmp/data"
|
||||
discovery = "http://example.com/foobar"
|
||||
key_file = "/tmp/file.key"
|
||||
bind_addr = "127.0.0.1:4003"
|
||||
peers = ["coreos.com:4001", "coreos.com:4002"]
|
||||
@ -29,7 +30,6 @@ func TestConfigTOML(t *testing.T) {
|
||||
snapshot = true
|
||||
verbose = true
|
||||
very_verbose = true
|
||||
web_url = "/web"
|
||||
|
||||
[peer]
|
||||
addr = "127.0.0.1:7002"
|
||||
@ -38,7 +38,7 @@ func TestConfigTOML(t *testing.T) {
|
||||
key_file = "/tmp/peer/file.key"
|
||||
bind_addr = "127.0.0.1:7003"
|
||||
`
|
||||
c := NewConfig()
|
||||
c := New()
|
||||
_, err := toml.Decode(content, &c)
|
||||
assert.Nil(t, err, "")
|
||||
assert.Equal(t, c.Addr, "127.0.0.1:4002", "")
|
||||
@ -46,6 +46,7 @@ func TestConfigTOML(t *testing.T) {
|
||||
assert.Equal(t, c.CertFile, "/tmp/file.cert", "")
|
||||
assert.Equal(t, c.CorsOrigins, []string{"*"}, "")
|
||||
assert.Equal(t, c.DataDir, "/tmp/data", "")
|
||||
assert.Equal(t, c.Discovery, "http://example.com/foobar", "")
|
||||
assert.Equal(t, c.KeyFile, "/tmp/file.key", "")
|
||||
assert.Equal(t, c.BindAddr, "127.0.0.1:4003", "")
|
||||
assert.Equal(t, c.Peers, []string{"coreos.com:4001", "coreos.com:4002"}, "")
|
||||
@ -71,6 +72,7 @@ func TestConfigEnv(t *testing.T) {
|
||||
os.Setenv("ETCD_CPU_PROFILE_FILE", "XXX")
|
||||
os.Setenv("ETCD_CORS", "localhost:4001,localhost:4002")
|
||||
os.Setenv("ETCD_DATA_DIR", "/tmp/data")
|
||||
os.Setenv("ETCD_DISCOVERY", "http://example.com/foobar")
|
||||
os.Setenv("ETCD_KEY_FILE", "/tmp/file.key")
|
||||
os.Setenv("ETCD_BIND_ADDR", "127.0.0.1:4003")
|
||||
os.Setenv("ETCD_PEERS", "coreos.com:4001,coreos.com:4002")
|
||||
@ -82,19 +84,19 @@ func TestConfigEnv(t *testing.T) {
|
||||
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_ADDR", "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_BIND_ADDR", "127.0.0.1:7003")
|
||||
|
||||
c := NewConfig()
|
||||
c := New()
|
||||
c.LoadEnv()
|
||||
assert.Equal(t, c.CAFile, "/tmp/file.ca", "")
|
||||
assert.Equal(t, c.CertFile, "/tmp/file.cert", "")
|
||||
assert.Equal(t, c.CorsOrigins, []string{"localhost:4001", "localhost:4002"}, "")
|
||||
assert.Equal(t, c.DataDir, "/tmp/data", "")
|
||||
assert.Equal(t, c.Discovery, "http://example.com/foobar", "")
|
||||
assert.Equal(t, c.KeyFile, "/tmp/file.key", "")
|
||||
assert.Equal(t, c.BindAddr, "127.0.0.1:4003", "")
|
||||
assert.Equal(t, c.Peers, []string{"coreos.com:4001", "coreos.com:4002"}, "")
|
||||
@ -111,39 +113,42 @@ func TestConfigEnv(t *testing.T) {
|
||||
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.BindAddr, "127.0.0.1:7003", "")
|
||||
|
||||
// Clear this as it will mess up other tests
|
||||
os.Setenv("ETCD_DISCOVERY", "")
|
||||
}
|
||||
|
||||
// Ensures that the "help" flag can be parsed.
|
||||
func TestConfigHelpFlag(t *testing.T) {
|
||||
c := NewConfig()
|
||||
c := New()
|
||||
assert.Nil(t, c.LoadFlags([]string{"-help"}), "")
|
||||
assert.True(t, c.ShowHelp)
|
||||
}
|
||||
|
||||
// Ensures that the abbreviated "help" flag can be parsed.
|
||||
func TestConfigAbbreviatedHelpFlag(t *testing.T) {
|
||||
c := NewConfig()
|
||||
c := New()
|
||||
assert.Nil(t, c.LoadFlags([]string{"-h"}), "")
|
||||
assert.True(t, c.ShowHelp)
|
||||
}
|
||||
|
||||
// Ensures that the "version" flag can be parsed.
|
||||
func TestConfigVersionFlag(t *testing.T) {
|
||||
c := NewConfig()
|
||||
c := New()
|
||||
assert.Nil(t, c.LoadFlags([]string{"-version"}), "")
|
||||
assert.True(t, c.ShowVersion)
|
||||
}
|
||||
|
||||
// Ensures that the "force config" flag can be parsed.
|
||||
func TestConfigForceFlag(t *testing.T) {
|
||||
c := NewConfig()
|
||||
c := New()
|
||||
assert.Nil(t, c.LoadFlags([]string{"-force"}), "")
|
||||
assert.True(t, c.Force)
|
||||
}
|
||||
|
||||
// Ensures that the abbreviated "force config" flag can be parsed.
|
||||
func TestConfigAbbreviatedForceFlag(t *testing.T) {
|
||||
c := NewConfig()
|
||||
c := New()
|
||||
assert.Nil(t, c.LoadFlags([]string{"-f"}), "")
|
||||
assert.True(t, c.Force)
|
||||
}
|
||||
@ -158,7 +163,7 @@ func TestConfigAddrEnv(t *testing.T) {
|
||||
|
||||
// Ensures that a the advertised flag can be parsed.
|
||||
func TestConfigAddrFlag(t *testing.T) {
|
||||
c := NewConfig()
|
||||
c := New()
|
||||
assert.Nil(t, c.LoadFlags([]string{"-addr", "127.0.0.1:4002"}), "")
|
||||
assert.Equal(t, c.Addr, "127.0.0.1:4002", "")
|
||||
}
|
||||
@ -173,7 +178,7 @@ func TestConfigCAFileEnv(t *testing.T) {
|
||||
|
||||
// Ensures that a the CA file flag can be parsed.
|
||||
func TestConfigCAFileFlag(t *testing.T) {
|
||||
c := NewConfig()
|
||||
c := New()
|
||||
assert.Nil(t, c.LoadFlags([]string{"-ca-file", "/tmp/file.ca"}), "")
|
||||
assert.Equal(t, c.CAFile, "/tmp/file.ca", "")
|
||||
}
|
||||
@ -188,7 +193,7 @@ func TestConfigCertFileEnv(t *testing.T) {
|
||||
|
||||
// Ensures that a the Cert file flag can be parsed.
|
||||
func TestConfigCertFileFlag(t *testing.T) {
|
||||
c := NewConfig()
|
||||
c := New()
|
||||
assert.Nil(t, c.LoadFlags([]string{"-cert-file", "/tmp/file.cert"}), "")
|
||||
assert.Equal(t, c.CertFile, "/tmp/file.cert", "")
|
||||
}
|
||||
@ -203,7 +208,7 @@ func TestConfigKeyFileEnv(t *testing.T) {
|
||||
|
||||
// Ensures that a the Key file flag can be parsed.
|
||||
func TestConfigKeyFileFlag(t *testing.T) {
|
||||
c := NewConfig()
|
||||
c := New()
|
||||
assert.Nil(t, c.LoadFlags([]string{"-key-file", "/tmp/file.key"}), "")
|
||||
assert.Equal(t, c.KeyFile, "/tmp/file.key", "")
|
||||
}
|
||||
@ -218,14 +223,14 @@ func TestConfigBindAddrEnv(t *testing.T) {
|
||||
|
||||
// Ensures that a the Listen Host file flag can be parsed.
|
||||
func TestConfigBindAddrFlag(t *testing.T) {
|
||||
c := NewConfig()
|
||||
c := New()
|
||||
assert.Nil(t, c.LoadFlags([]string{"-bind-addr", "127.0.0.1:4003"}), "")
|
||||
assert.Equal(t, c.BindAddr, "127.0.0.1:4003", "")
|
||||
}
|
||||
|
||||
// Ensures that a the Listen Host port overrides the advertised port
|
||||
func TestConfigBindAddrOverride(t *testing.T) {
|
||||
c := NewConfig()
|
||||
c := New()
|
||||
assert.Nil(t, c.LoadFlags([]string{"-addr", "127.0.0.1:4009", "-bind-addr", "127.0.0.1:4010"}), "")
|
||||
assert.Nil(t, c.Sanitize())
|
||||
assert.Equal(t, c.BindAddr, "127.0.0.1:4010", "")
|
||||
@ -233,7 +238,7 @@ func TestConfigBindAddrOverride(t *testing.T) {
|
||||
|
||||
// Ensures that a the Listen Host inherits its port from the advertised addr
|
||||
func TestConfigBindAddrInheritPort(t *testing.T) {
|
||||
c := NewConfig()
|
||||
c := New()
|
||||
assert.Nil(t, c.LoadFlags([]string{"-addr", "127.0.0.1:4009", "-bind-addr", "127.0.0.1"}), "")
|
||||
assert.Nil(t, c.Sanitize())
|
||||
assert.Equal(t, c.BindAddr, "127.0.0.1:4009", "")
|
||||
@ -241,7 +246,7 @@ func TestConfigBindAddrInheritPort(t *testing.T) {
|
||||
|
||||
// Ensures that a port only argument errors out
|
||||
func TestConfigBindAddrErrorOnNoHost(t *testing.T) {
|
||||
c := NewConfig()
|
||||
c := New()
|
||||
assert.Nil(t, c.LoadFlags([]string{"-addr", "127.0.0.1:4009", "-bind-addr", ":4010"}), "")
|
||||
assert.Error(t, c.Sanitize())
|
||||
}
|
||||
@ -256,7 +261,7 @@ func TestConfigPeersEnv(t *testing.T) {
|
||||
|
||||
// Ensures that a the Peers flag can be parsed.
|
||||
func TestConfigPeersFlag(t *testing.T) {
|
||||
c := NewConfig()
|
||||
c := New()
|
||||
assert.Nil(t, c.LoadFlags([]string{"-peers", "coreos.com:4001,coreos.com:4002"}), "")
|
||||
assert.Equal(t, c.Peers, []string{"coreos.com:4001", "coreos.com:4002"}, "")
|
||||
}
|
||||
@ -271,7 +276,7 @@ func TestConfigPeersFileEnv(t *testing.T) {
|
||||
|
||||
// Ensures that a the Peers File flag can be parsed.
|
||||
func TestConfigPeersFileFlag(t *testing.T) {
|
||||
c := NewConfig()
|
||||
c := New()
|
||||
assert.Nil(t, c.LoadFlags([]string{"-peers-file", "/tmp/peers"}), "")
|
||||
assert.Equal(t, c.PeersFile, "/tmp/peers", "")
|
||||
}
|
||||
@ -286,7 +291,7 @@ func TestConfigMaxClusterSizeEnv(t *testing.T) {
|
||||
|
||||
// Ensures that a the Max Cluster Size flag can be parsed.
|
||||
func TestConfigMaxClusterSizeFlag(t *testing.T) {
|
||||
c := NewConfig()
|
||||
c := New()
|
||||
assert.Nil(t, c.LoadFlags([]string{"-max-cluster-size", "5"}), "")
|
||||
assert.Equal(t, c.MaxClusterSize, 5, "")
|
||||
}
|
||||
@ -301,7 +306,7 @@ func TestConfigMaxResultBufferEnv(t *testing.T) {
|
||||
|
||||
// Ensures that a the Max Result Buffer flag can be parsed.
|
||||
func TestConfigMaxResultBufferFlag(t *testing.T) {
|
||||
c := NewConfig()
|
||||
c := New()
|
||||
assert.Nil(t, c.LoadFlags([]string{"-max-result-buffer", "512"}), "")
|
||||
assert.Equal(t, c.MaxResultBuffer, 512, "")
|
||||
}
|
||||
@ -316,7 +321,7 @@ func TestConfigMaxRetryAttemptsEnv(t *testing.T) {
|
||||
|
||||
// Ensures that a the Max Retry Attempts flag can be parsed.
|
||||
func TestConfigMaxRetryAttemptsFlag(t *testing.T) {
|
||||
c := NewConfig()
|
||||
c := New()
|
||||
assert.Nil(t, c.LoadFlags([]string{"-max-retry-attempts", "10"}), "")
|
||||
assert.Equal(t, c.MaxRetryAttempts, 10, "")
|
||||
}
|
||||
@ -331,14 +336,14 @@ func TestConfigNameEnv(t *testing.T) {
|
||||
|
||||
// Ensures that a the Name flag can be parsed.
|
||||
func TestConfigNameFlag(t *testing.T) {
|
||||
c := NewConfig()
|
||||
c := New()
|
||||
assert.Nil(t, c.LoadFlags([]string{"-name", "test-name"}), "")
|
||||
assert.Equal(t, c.Name, "test-name", "")
|
||||
}
|
||||
|
||||
// Ensures that a Name gets guessed if not specified
|
||||
func TestConfigNameGuess(t *testing.T) {
|
||||
c := NewConfig()
|
||||
c := New()
|
||||
assert.Nil(t, c.LoadFlags([]string{}), "")
|
||||
assert.Nil(t, c.Sanitize())
|
||||
name, _ := os.Hostname()
|
||||
@ -347,7 +352,7 @@ func TestConfigNameGuess(t *testing.T) {
|
||||
|
||||
// Ensures that a DataDir gets guessed if not specified
|
||||
func TestConfigDataDirGuess(t *testing.T) {
|
||||
c := NewConfig()
|
||||
c := New()
|
||||
assert.Nil(t, c.LoadFlags([]string{}), "")
|
||||
assert.Nil(t, c.Sanitize())
|
||||
name, _ := os.Hostname()
|
||||
@ -364,7 +369,7 @@ func TestConfigSnapshotEnv(t *testing.T) {
|
||||
|
||||
// Ensures that a the Snapshot flag can be parsed.
|
||||
func TestConfigSnapshotFlag(t *testing.T) {
|
||||
c := NewConfig()
|
||||
c := New()
|
||||
assert.Nil(t, c.LoadFlags([]string{"-snapshot"}), "")
|
||||
assert.Equal(t, c.Snapshot, true, "")
|
||||
}
|
||||
@ -379,7 +384,7 @@ func TestConfigVerboseEnv(t *testing.T) {
|
||||
|
||||
// Ensures that a the Verbose flag can be parsed.
|
||||
func TestConfigVerboseFlag(t *testing.T) {
|
||||
c := NewConfig()
|
||||
c := New()
|
||||
assert.Nil(t, c.LoadFlags([]string{"-v"}), "")
|
||||
assert.Equal(t, c.Verbose, true, "")
|
||||
}
|
||||
@ -394,7 +399,7 @@ func TestConfigVeryVerboseEnv(t *testing.T) {
|
||||
|
||||
// Ensures that a the Very Verbose flag can be parsed.
|
||||
func TestConfigVeryVerboseFlag(t *testing.T) {
|
||||
c := NewConfig()
|
||||
c := New()
|
||||
assert.Nil(t, c.LoadFlags([]string{"-vv"}), "")
|
||||
assert.Equal(t, c.VeryVerbose, true, "")
|
||||
}
|
||||
@ -409,7 +414,7 @@ func TestConfigPeerAddrEnv(t *testing.T) {
|
||||
|
||||
// Ensures that a the Peer Advertised URL flag can be parsed.
|
||||
func TestConfigPeerAddrFlag(t *testing.T) {
|
||||
c := NewConfig()
|
||||
c := New()
|
||||
assert.Nil(t, c.LoadFlags([]string{"-peer-addr", "localhost:7002"}), "")
|
||||
assert.Equal(t, c.Peer.Addr, "localhost:7002", "")
|
||||
}
|
||||
@ -424,7 +429,7 @@ func TestConfigPeerCAFileEnv(t *testing.T) {
|
||||
|
||||
// Ensures that a the Peer CA file flag can be parsed.
|
||||
func TestConfigPeerCAFileFlag(t *testing.T) {
|
||||
c := NewConfig()
|
||||
c := New()
|
||||
assert.Nil(t, c.LoadFlags([]string{"-peer-ca-file", "/tmp/peer/file.ca"}), "")
|
||||
assert.Equal(t, c.Peer.CAFile, "/tmp/peer/file.ca", "")
|
||||
}
|
||||
@ -439,7 +444,7 @@ func TestConfigPeerCertFileEnv(t *testing.T) {
|
||||
|
||||
// Ensures that a the Cert file flag can be parsed.
|
||||
func TestConfigPeerCertFileFlag(t *testing.T) {
|
||||
c := NewConfig()
|
||||
c := New()
|
||||
assert.Nil(t, c.LoadFlags([]string{"-peer-cert-file", "/tmp/peer/file.cert"}), "")
|
||||
assert.Equal(t, c.Peer.CertFile, "/tmp/peer/file.cert", "")
|
||||
}
|
||||
@ -454,7 +459,7 @@ func TestConfigPeerKeyFileEnv(t *testing.T) {
|
||||
|
||||
// Ensures that a the Peer Key file flag can be parsed.
|
||||
func TestConfigPeerKeyFileFlag(t *testing.T) {
|
||||
c := NewConfig()
|
||||
c := New()
|
||||
assert.Nil(t, c.LoadFlags([]string{"-peer-key-file", "/tmp/peer/file.key"}), "")
|
||||
assert.Equal(t, c.Peer.KeyFile, "/tmp/peer/file.key", "")
|
||||
}
|
||||
@ -469,7 +474,7 @@ func TestConfigPeerBindAddrEnv(t *testing.T) {
|
||||
|
||||
// Ensures that a bad flag returns an error.
|
||||
func TestConfigBadFlag(t *testing.T) {
|
||||
c := NewConfig()
|
||||
c := New()
|
||||
err := c.LoadFlags([]string{"-no-such-flag"})
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, err.Error(), `flag provided but not defined: -no-such-flag`)
|
||||
@ -477,7 +482,7 @@ func TestConfigBadFlag(t *testing.T) {
|
||||
|
||||
// Ensures that a the Peer Listen Host file flag can be parsed.
|
||||
func TestConfigPeerBindAddrFlag(t *testing.T) {
|
||||
c := NewConfig()
|
||||
c := New()
|
||||
assert.Nil(t, c.LoadFlags([]string{"-peer-bind-addr", "127.0.0.1:4003"}), "")
|
||||
assert.Equal(t, c.Peer.BindAddr, "127.0.0.1:4003", "")
|
||||
}
|
||||
@ -488,7 +493,7 @@ func TestConfigCustomConfigOverrideSystemConfig(t *testing.T) {
|
||||
custom := `addr = "127.0.0.1:6000"`
|
||||
withTempFile(system, func(p1 string) {
|
||||
withTempFile(custom, func(p2 string) {
|
||||
c := NewConfig()
|
||||
c := New()
|
||||
c.SystemPath = p1
|
||||
assert.Nil(t, c.Load([]string{"-config", p2}), "")
|
||||
assert.Equal(t, c.Addr, "http://127.0.0.1:6000", "")
|
||||
@ -503,7 +508,7 @@ func TestConfigEnvVarOverrideCustomConfig(t *testing.T) {
|
||||
|
||||
custom := `[peer]` + "\n" + `advertised_url = "127.0.0.1:9000"`
|
||||
withTempFile(custom, func(path string) {
|
||||
c := NewConfig()
|
||||
c := New()
|
||||
c.SystemPath = ""
|
||||
assert.Nil(t, c.Load([]string{"-config", path}), "")
|
||||
assert.Equal(t, c.Peer.Addr, "http://127.0.0.1:8000", "")
|
||||
@ -515,7 +520,7 @@ func TestConfigCLIArgsOverrideEnvVar(t *testing.T) {
|
||||
os.Setenv("ETCD_ADDR", "127.0.0.1:1000")
|
||||
defer os.Setenv("ETCD_ADDR", "")
|
||||
|
||||
c := NewConfig()
|
||||
c := New()
|
||||
c.SystemPath = ""
|
||||
assert.Nil(t, c.Load([]string{"-addr", "127.0.0.1:2000"}), "")
|
||||
assert.Equal(t, c.Addr, "http://127.0.0.1:2000", "")
|
||||
@ -527,162 +532,162 @@ func TestConfigCLIArgsOverrideEnvVar(t *testing.T) {
|
||||
|
||||
func TestConfigDeprecatedAddrFlag(t *testing.T) {
|
||||
_, stderr := capture(func() {
|
||||
c := NewConfig()
|
||||
c := New()
|
||||
err := c.LoadFlags([]string{"-c", "127.0.0.1:4002"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, c.Addr, "127.0.0.1:4002")
|
||||
})
|
||||
assert.Equal(t, stderr, "[deprecated] use -addr, not -c")
|
||||
assert.Equal(t, stderr, "[deprecated] use -addr, not -c\n")
|
||||
}
|
||||
|
||||
func TestConfigDeprecatedBindAddrFlag(t *testing.T) {
|
||||
_, stderr := capture(func() {
|
||||
c := NewConfig()
|
||||
c := New()
|
||||
err := c.LoadFlags([]string{"-cl", "127.0.0.1:4003"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, c.BindAddr, "127.0.0.1:4003", "")
|
||||
})
|
||||
assert.Equal(t, stderr, "[deprecated] use -bind-addr, not -cl", "")
|
||||
assert.Equal(t, stderr, "[deprecated] use -bind-addr, not -cl\n", "")
|
||||
}
|
||||
|
||||
func TestConfigDeprecatedCAFileFlag(t *testing.T) {
|
||||
_, stderr := capture(func() {
|
||||
c := NewConfig()
|
||||
c := New()
|
||||
err := c.LoadFlags([]string{"-clientCAFile", "/tmp/file.ca"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, c.CAFile, "/tmp/file.ca", "")
|
||||
})
|
||||
assert.Equal(t, stderr, "[deprecated] use -ca-file, not -clientCAFile", "")
|
||||
assert.Equal(t, stderr, "[deprecated] use -ca-file, not -clientCAFile\n", "")
|
||||
}
|
||||
|
||||
func TestConfigDeprecatedCertFileFlag(t *testing.T) {
|
||||
_, stderr := capture(func() {
|
||||
c := NewConfig()
|
||||
c := New()
|
||||
err := c.LoadFlags([]string{"-clientCert", "/tmp/file.cert"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, c.CertFile, "/tmp/file.cert", "")
|
||||
})
|
||||
assert.Equal(t, stderr, "[deprecated] use -cert-file, not -clientCert", "")
|
||||
assert.Equal(t, stderr, "[deprecated] use -cert-file, not -clientCert\n", "")
|
||||
}
|
||||
|
||||
func TestConfigDeprecatedKeyFileFlag(t *testing.T) {
|
||||
_, stderr := capture(func() {
|
||||
c := NewConfig()
|
||||
c := New()
|
||||
err := c.LoadFlags([]string{"-clientKey", "/tmp/file.key"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, c.KeyFile, "/tmp/file.key", "")
|
||||
})
|
||||
assert.Equal(t, stderr, "[deprecated] use -key-file, not -clientKey", "")
|
||||
assert.Equal(t, stderr, "[deprecated] use -key-file, not -clientKey\n", "")
|
||||
}
|
||||
|
||||
func TestConfigDeprecatedPeersFlag(t *testing.T) {
|
||||
_, stderr := capture(func() {
|
||||
c := NewConfig()
|
||||
c := New()
|
||||
err := c.LoadFlags([]string{"-C", "coreos.com:4001,coreos.com:4002"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, c.Peers, []string{"coreos.com:4001", "coreos.com:4002"}, "")
|
||||
})
|
||||
assert.Equal(t, stderr, "[deprecated] use -peers, not -C", "")
|
||||
assert.Equal(t, stderr, "[deprecated] use -peers, not -C\n", "")
|
||||
}
|
||||
|
||||
func TestConfigDeprecatedPeersFileFlag(t *testing.T) {
|
||||
_, stderr := capture(func() {
|
||||
c := NewConfig()
|
||||
c := New()
|
||||
err := c.LoadFlags([]string{"-CF", "/tmp/machines"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, c.PeersFile, "/tmp/machines", "")
|
||||
})
|
||||
assert.Equal(t, stderr, "[deprecated] use -peers-file, not -CF", "")
|
||||
assert.Equal(t, stderr, "[deprecated] use -peers-file, not -CF\n", "")
|
||||
}
|
||||
|
||||
func TestConfigDeprecatedMaxClusterSizeFlag(t *testing.T) {
|
||||
_, stderr := capture(func() {
|
||||
c := NewConfig()
|
||||
c := New()
|
||||
err := c.LoadFlags([]string{"-maxsize", "5"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, c.MaxClusterSize, 5, "")
|
||||
})
|
||||
assert.Equal(t, stderr, "[deprecated] use -max-cluster-size, not -maxsize", "")
|
||||
assert.Equal(t, stderr, "[deprecated] use -max-cluster-size, not -maxsize\n", "")
|
||||
}
|
||||
|
||||
func TestConfigDeprecatedMaxResultBufferFlag(t *testing.T) {
|
||||
_, stderr := capture(func() {
|
||||
c := NewConfig()
|
||||
c := New()
|
||||
err := c.LoadFlags([]string{"-m", "512"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, c.MaxResultBuffer, 512, "")
|
||||
})
|
||||
assert.Equal(t, stderr, "[deprecated] use -max-result-buffer, not -m", "")
|
||||
assert.Equal(t, stderr, "[deprecated] use -max-result-buffer, not -m\n", "")
|
||||
}
|
||||
|
||||
func TestConfigDeprecatedMaxRetryAttemptsFlag(t *testing.T) {
|
||||
_, stderr := capture(func() {
|
||||
c := NewConfig()
|
||||
c := New()
|
||||
err := c.LoadFlags([]string{"-r", "10"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, c.MaxRetryAttempts, 10, "")
|
||||
})
|
||||
assert.Equal(t, stderr, "[deprecated] use -max-retry-attempts, not -r", "")
|
||||
assert.Equal(t, stderr, "[deprecated] use -max-retry-attempts, not -r\n", "")
|
||||
}
|
||||
|
||||
func TestConfigDeprecatedNameFlag(t *testing.T) {
|
||||
_, stderr := capture(func() {
|
||||
c := NewConfig()
|
||||
c := New()
|
||||
err := c.LoadFlags([]string{"-n", "test-name"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, c.Name, "test-name", "")
|
||||
})
|
||||
assert.Equal(t, stderr, "[deprecated] use -name, not -n", "")
|
||||
assert.Equal(t, stderr, "[deprecated] use -name, not -n\n", "")
|
||||
}
|
||||
|
||||
func TestConfigDeprecatedPeerAddrFlag(t *testing.T) {
|
||||
_, stderr := capture(func() {
|
||||
c := NewConfig()
|
||||
c := New()
|
||||
err := c.LoadFlags([]string{"-s", "localhost:7002"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, c.Peer.Addr, "localhost:7002", "")
|
||||
})
|
||||
assert.Equal(t, stderr, "[deprecated] use -peer-addr, not -s", "")
|
||||
assert.Equal(t, stderr, "[deprecated] use -peer-addr, not -s\n", "")
|
||||
}
|
||||
|
||||
func TestConfigDeprecatedPeerBindAddrFlag(t *testing.T) {
|
||||
_, stderr := capture(func() {
|
||||
c := NewConfig()
|
||||
c := New()
|
||||
err := c.LoadFlags([]string{"-sl", "127.0.0.1:4003"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, c.Peer.BindAddr, "127.0.0.1:4003", "")
|
||||
})
|
||||
assert.Equal(t, stderr, "[deprecated] use -peer-bind-addr, not -sl", "")
|
||||
assert.Equal(t, stderr, "[deprecated] use -peer-bind-addr, not -sl\n", "")
|
||||
}
|
||||
|
||||
func TestConfigDeprecatedPeerCAFileFlag(t *testing.T) {
|
||||
_, stderr := capture(func() {
|
||||
c := NewConfig()
|
||||
c := New()
|
||||
err := c.LoadFlags([]string{"-serverCAFile", "/tmp/peer/file.ca"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, c.Peer.CAFile, "/tmp/peer/file.ca", "")
|
||||
})
|
||||
assert.Equal(t, stderr, "[deprecated] use -peer-ca-file, not -serverCAFile", "")
|
||||
assert.Equal(t, stderr, "[deprecated] use -peer-ca-file, not -serverCAFile\n", "")
|
||||
}
|
||||
|
||||
func TestConfigDeprecatedPeerCertFileFlag(t *testing.T) {
|
||||
_, stderr := capture(func() {
|
||||
c := NewConfig()
|
||||
c := New()
|
||||
err := c.LoadFlags([]string{"-serverCert", "/tmp/peer/file.cert"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, c.Peer.CertFile, "/tmp/peer/file.cert", "")
|
||||
})
|
||||
assert.Equal(t, stderr, "[deprecated] use -peer-cert-file, not -serverCert", "")
|
||||
assert.Equal(t, stderr, "[deprecated] use -peer-cert-file, not -serverCert\n", "")
|
||||
}
|
||||
|
||||
func TestConfigDeprecatedPeerKeyFileFlag(t *testing.T) {
|
||||
_, stderr := capture(func() {
|
||||
c := NewConfig()
|
||||
c := New()
|
||||
err := c.LoadFlags([]string{"-serverKey", "/tmp/peer/file.key"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, c.Peer.KeyFile, "/tmp/peer/file.key", "")
|
||||
})
|
||||
assert.Equal(t, stderr, "[deprecated] use -peer-key-file, not -serverKey", "")
|
||||
assert.Equal(t, stderr, "[deprecated] use -peer-key-file, not -serverKey\n", "")
|
||||
}
|
||||
|
||||
//--------------------------------------
|
||||
@ -693,7 +698,7 @@ func TestConfigDeprecatedPeerKeyFileFlag(t *testing.T) {
|
||||
func withEnv(key, value string, f func(c *Config)) {
|
||||
os.Setenv(key, value)
|
||||
defer os.Setenv(key, "")
|
||||
c := NewConfig()
|
||||
c := New()
|
||||
f(c)
|
||||
}
|
||||
|
9
config/timeout.go
Normal file
9
config/timeout.go
Normal file
@ -0,0 +1,9 @@
|
||||
package config
|
||||
|
||||
const (
|
||||
// The amount of time (in ms) to elapse without a heartbeat before becoming a candidate
|
||||
defaultElectionTimeout = 200
|
||||
|
||||
// The frequency (in ms) by which heartbeats are sent to followers.
|
||||
defaultHeartbeatTimeout = 50
|
||||
)
|
9
contrib/collectd/Dockerfile
Normal file
9
contrib/collectd/Dockerfile
Normal file
@ -0,0 +1,9 @@
|
||||
FROM stackbrew/ubuntu:raring
|
||||
|
||||
RUN apt-get update && apt-get install -y collectd
|
||||
RUN adduser --system --group --no-create-home collectd
|
||||
ADD collectd.conf /etc/collectd/collectd.conf.tmpl
|
||||
ADD collectd-wrapper /bin/collectd-wrapper
|
||||
RUN chown -R collectd:collectd /etc/collectd
|
||||
|
||||
CMD ["collectd-wrapper"]
|
20
contrib/collectd/README
Normal file
20
contrib/collectd/README
Normal file
@ -0,0 +1,20 @@
|
||||
We're going to use Docker to build a chroot env that can be run with systemd-nspawn since I cannot figure out how to run
|
||||
a container using docker in the global network namespace.
|
||||
|
||||
1. Build the collectd image using docker
|
||||
docker build -t collectd .
|
||||
|
||||
2. Run the container (since we have to run it to export it...)
|
||||
COLLECTD_CONTAINER=`docker run -name collectd-tmp -d collectd`
|
||||
|
||||
3. Export then kill the container
|
||||
docker export collectd-tmp > /tmp/collectd.tar
|
||||
|
||||
4. Kill the temporary container
|
||||
docker kill $COLLECTD_CONTAINER
|
||||
|
||||
5. Unpack the tar archive
|
||||
mkdir -p /tmp/collectd && tar -xvf /tmp/collectd.tar -C /tmp/collectd/
|
||||
|
||||
6. Run collectd with systemd-nspawn - replace the COLLECTD_* env vars with your parameters!
|
||||
sudo systemd-run --unit collectd systemd-nspawn -D /tmp/collectd /bin/bash -c "COLLECTD_GRAPHITE_HOSTNAME=172.31.13.241 COLLECTD_LOCAL_HOSTNAME=node1 /bin/collectd-wrapper"
|
16
contrib/collectd/collectd-wrapper
Executable file
16
contrib/collectd/collectd-wrapper
Executable file
@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
|
||||
cat /etc/collectd/collectd.conf.tmpl > /etc/collectd/collectd.conf
|
||||
|
||||
cat << EOF >> /etc/collectd/collectd.conf
|
||||
Hostname "${COLLECTD_LOCAL_HOSTNAME}"
|
||||
|
||||
<Plugin write_graphite>
|
||||
<Carbon>
|
||||
Host "${COLLECTD_GRAPHITE_HOSTNAME}"
|
||||
Port "2003"
|
||||
</Carbon>
|
||||
</Plugin>
|
||||
EOF
|
||||
|
||||
collectd -C /etc/collectd/collectd.conf -f
|
898
contrib/collectd/collectd.conf
Normal file
898
contrib/collectd/collectd.conf
Normal file
@ -0,0 +1,898 @@
|
||||
# Config file for collectd(1).
|
||||
#
|
||||
# Some plugins need additional configuration and are disabled by default.
|
||||
# Please read collectd.conf(5) for details.
|
||||
#
|
||||
# You should also read /usr/share/doc/collectd-core/README.Debian.plugins
|
||||
# before enabling any more plugins.
|
||||
|
||||
#Hostname "localhost"
|
||||
#FQDNLookup true
|
||||
#BaseDir "/var/lib/collectd"
|
||||
#PluginDir "/usr/lib/collectd"
|
||||
#TypesDB "/usr/share/collectd/types.db" "/etc/collectd/my_types.db"
|
||||
#Interval 10
|
||||
#Timeout 2
|
||||
#ReadThreads 5
|
||||
|
||||
LoadPlugin logfile
|
||||
#LoadPlugin syslog
|
||||
|
||||
<Plugin logfile>
|
||||
LogLevel "info"
|
||||
File STDOUT
|
||||
Timestamp true
|
||||
PrintSeverity false
|
||||
</Plugin>
|
||||
|
||||
#<Plugin syslog>
|
||||
# LogLevel info
|
||||
#</Plugin>
|
||||
|
||||
#LoadPlugin amqp
|
||||
#LoadPlugin apache
|
||||
#LoadPlugin apcups
|
||||
#LoadPlugin ascent
|
||||
#LoadPlugin battery
|
||||
#LoadPlugin bind
|
||||
#LoadPlugin conntrack
|
||||
#LoadPlugin contextswitch
|
||||
LoadPlugin cpu
|
||||
#LoadPlugin cpufreq
|
||||
#LoadPlugin csv
|
||||
#LoadPlugin curl
|
||||
#LoadPlugin curl_json
|
||||
#LoadPlugin curl_xml
|
||||
#LoadPlugin dbi
|
||||
LoadPlugin df
|
||||
#LoadPlugin disk
|
||||
#LoadPlugin dns
|
||||
#LoadPlugin email
|
||||
#LoadPlugin entropy
|
||||
#LoadPlugin ethstat
|
||||
#LoadPlugin exec
|
||||
#LoadPlugin filecount
|
||||
#LoadPlugin fscache
|
||||
#LoadPlugin gmond
|
||||
#LoadPlugin hddtemp
|
||||
#LoadPlugin interface
|
||||
#LoadPlugin ipmi
|
||||
#LoadPlugin iptables
|
||||
#LoadPlugin ipvs
|
||||
#LoadPlugin irq
|
||||
#LoadPlugin java
|
||||
#LoadPlugin libvirt
|
||||
#LoadPlugin load
|
||||
#LoadPlugin madwifi
|
||||
#LoadPlugin mbmon
|
||||
#LoadPlugin md
|
||||
#LoadPlugin memcachec
|
||||
#LoadPlugin memcached
|
||||
LoadPlugin memory
|
||||
#LoadPlugin multimeter
|
||||
#LoadPlugin mysql
|
||||
#LoadPlugin netlink
|
||||
#LoadPlugin network
|
||||
#LoadPlugin nfs
|
||||
#LoadPlugin nginx
|
||||
#LoadPlugin notify_desktop
|
||||
#LoadPlugin notify_email
|
||||
#LoadPlugin ntpd
|
||||
#LoadPlugin numa
|
||||
#LoadPlugin nut
|
||||
#LoadPlugin olsrd
|
||||
#LoadPlugin openvpn
|
||||
#<LoadPlugin perl>
|
||||
# Globals true
|
||||
#</LoadPlugin>
|
||||
#LoadPlugin pinba
|
||||
#LoadPlugin ping
|
||||
#LoadPlugin postgresql
|
||||
#LoadPlugin powerdns
|
||||
#LoadPlugin processes
|
||||
#LoadPlugin protocols
|
||||
#<LoadPlugin python>
|
||||
# Globals true
|
||||
#</LoadPlugin>
|
||||
#LoadPlugin rrdcached
|
||||
#LoadPlugin rrdtool
|
||||
#LoadPlugin sensors
|
||||
#LoadPlugin serial
|
||||
#LoadPlugin snmp
|
||||
#LoadPlugin swap
|
||||
#LoadPlugin table
|
||||
#LoadPlugin tail
|
||||
LoadPlugin tcpconns
|
||||
#LoadPlugin teamspeak2
|
||||
#LoadPlugin ted
|
||||
#LoadPlugin thermal
|
||||
#LoadPlugin tokyotyrant
|
||||
#LoadPlugin unixsock
|
||||
#LoadPlugin uptime
|
||||
#LoadPlugin users
|
||||
#LoadPlugin uuid
|
||||
#LoadPlugin varnish
|
||||
#LoadPlugin vmem
|
||||
#LoadPlugin vserver
|
||||
#LoadPlugin wireless
|
||||
LoadPlugin write_graphite
|
||||
#LoadPlugin write_http
|
||||
#LoadPlugin write_mongodb
|
||||
|
||||
#<Plugin amqp>
|
||||
# <Publish "name">
|
||||
# Host "localhost"
|
||||
# Port "5672"
|
||||
# VHost "/"
|
||||
# User "guest"
|
||||
# Password "guest"
|
||||
# Exchange "amq.fanout"
|
||||
# RoutingKey "collectd"
|
||||
# Persistent false
|
||||
# StoreRates false
|
||||
# </Publish>
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin apache>
|
||||
# <Instance "foo">
|
||||
# URL "http://localhost/server-status?auto"
|
||||
# User "www-user"
|
||||
# Password "secret"
|
||||
# VerifyPeer false
|
||||
# VerifyHost false
|
||||
# CACert "/etc/ssl/ca.crt"
|
||||
# Server "apache"
|
||||
# </Instance>
|
||||
#
|
||||
# <Instance "bar">
|
||||
# URL "http://some.domain.tld/status?auto"
|
||||
# Host "some.domain.tld"
|
||||
# Server "lighttpd"
|
||||
# </Instance>
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin apcups>
|
||||
# Host "localhost"
|
||||
# Port "3551"
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin ascent>
|
||||
# URL "http://localhost/ascent/status/"
|
||||
# User "www-user"
|
||||
# Password "secret"
|
||||
# VerifyPeer false
|
||||
# VerifyHost false
|
||||
# CACert "/etc/ssl/ca.crt"
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin "bind">
|
||||
# URL "http://localhost:8053/"
|
||||
#
|
||||
# ParseTime false
|
||||
#
|
||||
# OpCodes true
|
||||
# QTypes true
|
||||
# ServerStats true
|
||||
# ZoneMaintStats true
|
||||
# ResolverStats false
|
||||
# MemoryStats true
|
||||
#
|
||||
# <View "_default">
|
||||
# QTypes true
|
||||
# ResolverStats true
|
||||
# CacheRRSets true
|
||||
#
|
||||
# Zone "127.in-addr.arpa/IN"
|
||||
# </View>
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin csv>
|
||||
# DataDir "/var/lib/collectd/csv"
|
||||
# StoreRates false
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin curl>
|
||||
# <Page "stock_quotes">
|
||||
# URL "http://finance.google.com/finance?q=NYSE%3AAMD"
|
||||
# User "foo"
|
||||
# Password "bar"
|
||||
# VerifyPeer false
|
||||
# VerifyHost false
|
||||
# CACert "/etc/ssl/ca.crt"
|
||||
# MeasureResponseTime false
|
||||
# <Match>
|
||||
# Regex "<span +class=\"pr\"[^>]*> *([0-9]*\\.[0-9]+) *</span>"
|
||||
# DSType "GaugeAverage"
|
||||
# Type "stock_value"
|
||||
# Instance "AMD"
|
||||
# </Match>
|
||||
# </Page>
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin curl_json>
|
||||
## See: http://wiki.apache.org/couchdb/Runtime_Statistics
|
||||
# <URL "http://localhost:5984/_stats">
|
||||
# Instance "httpd"
|
||||
# <Key "httpd/requests/count">
|
||||
# Type "http_requests"
|
||||
# </Key>
|
||||
#
|
||||
# <Key "httpd_request_methods/*/count">
|
||||
# Type "http_request_methods"
|
||||
# </Key>
|
||||
#
|
||||
# <Key "httpd_status_codes/*/count">
|
||||
# Type "http_response_codes"
|
||||
# </Key>
|
||||
# </URL>
|
||||
## Database status metrics:
|
||||
# <URL "http://localhost:5984/_all_dbs">
|
||||
# Instance "dbs"
|
||||
# <Key "*/doc_count">
|
||||
# Type "gauge"
|
||||
# </Key>
|
||||
# <Key "*/doc_del_count">
|
||||
# Type "counter"
|
||||
# </Key>
|
||||
# <Key "*/disk_size">
|
||||
# Type "bytes"
|
||||
# </Key>
|
||||
# </URL>
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin "curl_xml">
|
||||
# <URL "http://localhost/stats.xml">
|
||||
# Host "my_host"
|
||||
# Instance "some_instance"
|
||||
# User "collectd"
|
||||
# Password "thaiNg0I"
|
||||
# VerifyPeer true
|
||||
# VerifyHost true
|
||||
# CACert "/path/to/ca.crt"
|
||||
#
|
||||
# <XPath "table[@id=\"magic_level\"]/tr">
|
||||
# Type "magic_level"
|
||||
# InstancePrefix "prefix-"
|
||||
# InstanceFrom "td[1]"
|
||||
# ValuesFrom "td[2]/span[@class=\"level\"]"
|
||||
# </XPath>
|
||||
# </URL>
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin dbi>
|
||||
# <Query "num_of_customers">
|
||||
# Statement "SELECT 'customers' AS c_key, COUNT(*) AS c_value \
|
||||
# FROM customers_tbl"
|
||||
# MinVersion 40102
|
||||
# MaxVersion 50042
|
||||
# <Result>
|
||||
# Type "gauge"
|
||||
# InstancePrefix "customer"
|
||||
# InstancesFrom "c_key"
|
||||
# ValuesFrom "c_value"
|
||||
# </Result>
|
||||
# </Query>
|
||||
#
|
||||
# <Database "customers_db">
|
||||
# Driver "mysql"
|
||||
# DriverOption "host" "localhost"
|
||||
# DriverOption "username" "collectd"
|
||||
# DriverOption "password" "secret"
|
||||
# DriverOption "dbname" "custdb0"
|
||||
# SelectDB "custdb0"
|
||||
# Query "num_of_customers"
|
||||
# Query "..."
|
||||
# </Database>
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin df>
|
||||
# Device "/dev/sda1"
|
||||
# Device "192.168.0.2:/mnt/nfs"
|
||||
# MountPoint "/home"
|
||||
# FSType "ext3"
|
||||
# IgnoreSelected false
|
||||
# ReportByDevice false
|
||||
# ReportReserved false
|
||||
# ReportInodes false
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin disk>
|
||||
# Disk "hda"
|
||||
# Disk "/sda[23]/"
|
||||
# IgnoreSelected false
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin dns>
|
||||
# Interface "eth0"
|
||||
# IgnoreSource "192.168.0.1"
|
||||
# SelectNumericQueryTypes false
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin email>
|
||||
# SocketFile "/var/run/collectd-email"
|
||||
# SocketGroup "collectd"
|
||||
# SocketPerms "0770"
|
||||
# MaxConns 5
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin ethstat>
|
||||
# Interface "eth0"
|
||||
# Map "rx_csum_offload_errors" "if_rx_errors" "checksum_offload"
|
||||
# Map "multicast" "if_multicast"
|
||||
# MappedOnly false
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin exec>
|
||||
# Exec user "/path/to/exec"
|
||||
# Exec "user:group" "/path/to/exec"
|
||||
# NotificationExec user "/path/to/exec"
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin filecount>
|
||||
# <Directory "/path/to/dir">
|
||||
# Instance "foodir"
|
||||
# Name "*.conf"
|
||||
# MTime "-5m"
|
||||
# Size "+10k"
|
||||
# Recursive true
|
||||
# IncludeHidden false
|
||||
# </Directory>
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin gmond>
|
||||
# MCReceiveFrom "239.2.11.71" "8649"
|
||||
#
|
||||
# <Metric "swap_total">
|
||||
# Type "swap"
|
||||
# TypeInstance "total"
|
||||
# DataSource "value"
|
||||
# </Metric>
|
||||
#
|
||||
# <Metric "swap_free">
|
||||
# Type "swap"
|
||||
# TypeInstance "free"
|
||||
# DataSource "value"
|
||||
# </Metric>
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin hddtemp>
|
||||
# Host "127.0.0.1"
|
||||
# Port 7634
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin interface>
|
||||
# Interface "eth0"
|
||||
# IgnoreSelected false
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin ipmi>
|
||||
# Sensor "some_sensor"
|
||||
# Sensor "another_one"
|
||||
# IgnoreSelected false
|
||||
# NotifySensorAdd false
|
||||
# NotifySensorRemove true
|
||||
# NotifySensorNotPresent false
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin iptables>
|
||||
# Chain "table" "chain"
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin irq>
|
||||
# Irq 7
|
||||
# Irq 8
|
||||
# Irq 9
|
||||
# IgnoreSelected true
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin java>
|
||||
# JVMArg "-verbose:jni"
|
||||
# JVMArg "-Djava.class.path=/usr/share/collectd/java/collectd-api.jar"
|
||||
#
|
||||
# LoadPlugin "org.collectd.java.GenericJMX"
|
||||
# <Plugin "GenericJMX">
|
||||
# # See /usr/share/doc/collectd/examples/GenericJMX.conf
|
||||
# # for an example config.
|
||||
# </Plugin>
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin libvirt>
|
||||
# Connection "xen:///"
|
||||
# RefreshInterval 60
|
||||
# Domain "name"
|
||||
# BlockDevice "name:device"
|
||||
# InterfaceDevice "name:device"
|
||||
# IgnoreSelected false
|
||||
# HostnameFormat name
|
||||
# InterfaceFormat name
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin madwifi>
|
||||
# Interface "wlan0"
|
||||
# IgnoreSelected false
|
||||
# Source "SysFS"
|
||||
# WatchSet "None"
|
||||
# WatchAdd "node_octets"
|
||||
# WatchAdd "node_rssi"
|
||||
# WatchAdd "is_rx_acl"
|
||||
# WatchAdd "is_scan_active"
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin mbmon>
|
||||
# Host "127.0.0.1"
|
||||
# Port 411
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin md>
|
||||
# Device "/dev/md0"
|
||||
# IgnoreSelected false
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin memcachec>
|
||||
# <Page "plugin_instance">
|
||||
# Server "localhost"
|
||||
# Key "page_key"
|
||||
# <Match>
|
||||
# Regex "(\\d+) bytes sent"
|
||||
# ExcludeRegex "<lines to be excluded>"
|
||||
# DSType CounterAdd
|
||||
# Type "ipt_octets"
|
||||
# Instance "type_instance"
|
||||
# </Match>
|
||||
# </Page>
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin memcached>
|
||||
# Socket "/var/run/memcached.sock"
|
||||
# or:
|
||||
# Host "127.0.0.1"
|
||||
# Port "11211"
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin mysql>
|
||||
# <Database db_name>
|
||||
# Host "database.serv.er"
|
||||
# Port "3306"
|
||||
# User "db_user"
|
||||
# Password "secret"
|
||||
# Database "db_name"
|
||||
# MasterStats true
|
||||
# </Database>
|
||||
#
|
||||
# <Database db_name2>
|
||||
# Host "localhost"
|
||||
# Socket "/var/run/mysql/mysqld.sock"
|
||||
# SlaveStats true
|
||||
# SlaveNotifications true
|
||||
# </Database>
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin netlink>
|
||||
# Interface "All"
|
||||
# VerboseInterface "All"
|
||||
# QDisc "eth0" "pfifo_fast-1:0"
|
||||
# Class "ppp0" "htb-1:10"
|
||||
# Filter "ppp0" "u32-1:0"
|
||||
# IgnoreSelected false
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin network>
|
||||
# # client setup:
|
||||
# Server "ff18::efc0:4a42" "25826"
|
||||
# <Server "239.192.74.66" "25826">
|
||||
# SecurityLevel Encrypt
|
||||
# Username "user"
|
||||
# Password "secret"
|
||||
# Interface "eth0"
|
||||
# </Server>
|
||||
# TimeToLive "128"
|
||||
#
|
||||
# # server setup:
|
||||
# Listen "0.0.0.0" "25826"
|
||||
# <Listen "239.192.74.66" "25826">
|
||||
# SecurityLevel Sign
|
||||
# AuthFile "/etc/collectd/passwd"
|
||||
# Interface "eth0"
|
||||
# </Listen>
|
||||
# MaxPacketSize 1024
|
||||
#
|
||||
# # proxy setup (client and server as above):
|
||||
# Forward true
|
||||
#
|
||||
# # statistics about the network plugin itself
|
||||
# ReportStats false
|
||||
#
|
||||
# # "garbage collection"
|
||||
# CacheFlush 1800
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin nginx>
|
||||
# URL "http://localhost/status?auto"
|
||||
# User "www-user"
|
||||
# Password "secret"
|
||||
# VerifyPeer false
|
||||
# VerifyHost false
|
||||
# CACert "/etc/ssl/ca.crt"
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin notify_desktop>
|
||||
# OkayTimeout 1000
|
||||
# WarningTimeout 5000
|
||||
# FailureTimeout 0
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin notify_email>
|
||||
# SMTPServer "localhost"
|
||||
# SMTPPort 25
|
||||
# SMTPUser "my-username"
|
||||
# SMTPPassword "my-password"
|
||||
# From "collectd@main0server.com"
|
||||
# # <WARNING/FAILURE/OK> on <hostname>.
|
||||
# # Beware! Do not use not more than two placeholders (%)!
|
||||
# Subject "[collectd] %s on %s!"
|
||||
# Recipient "email1@domain1.net"
|
||||
# Recipient "email2@domain2.com"
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin ntpd>
|
||||
# Host "localhost"
|
||||
# Port 123
|
||||
# ReverseLookups false
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin nut>
|
||||
# UPS "upsname@hostname:port"
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin olsrd>
|
||||
# Host "127.0.0.1"
|
||||
# Port "2006"
|
||||
# CollectLinks "Summary"
|
||||
# CollectRoutes "Summary"
|
||||
# CollectTopology "Summary"
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin openvpn>
|
||||
# StatusFile "/etc/openvpn/openvpn-status.log"
|
||||
# ImprovedNamingSchema false
|
||||
# CollectCompression true
|
||||
# CollectIndividualUsers true
|
||||
# CollectUserCount false
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin perl>
|
||||
# IncludeDir "/my/include/path"
|
||||
# BaseName "Collectd::Plugins"
|
||||
# EnableDebugger ""
|
||||
# LoadPlugin Monitorus
|
||||
# LoadPlugin OpenVZ
|
||||
#
|
||||
# <Plugin foo>
|
||||
# Foo "Bar"
|
||||
# Qux "Baz"
|
||||
# </Plugin>
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin pinba>
|
||||
# Address "::0"
|
||||
# Port "30002"
|
||||
# <View "name">
|
||||
# Host "host name"
|
||||
# Server "server name"
|
||||
# Script "script name"
|
||||
# <View>
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin ping>
|
||||
# Host "host.foo.bar"
|
||||
# Host "host.baz.qux"
|
||||
# Interval 1.0
|
||||
# Timeout 0.9
|
||||
# TTL 255
|
||||
# SourceAddress "1.2.3.4"
|
||||
# Device "eth0"
|
||||
# MaxMissed -1
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin postgresql>
|
||||
# <Query magic>
|
||||
# Statement "SELECT magic FROM wizard WHERE host = $1;"
|
||||
# Param hostname
|
||||
#
|
||||
# <Result>
|
||||
# Type gauge
|
||||
# InstancePrefix "magic"
|
||||
# ValuesFrom "magic"
|
||||
# </Result>
|
||||
# </Query>
|
||||
#
|
||||
# <Query rt36_tickets>
|
||||
# Statement "SELECT COUNT(type) AS count, type \
|
||||
# FROM (SELECT CASE \
|
||||
# WHEN resolved = 'epoch' THEN 'open' \
|
||||
# ELSE 'resolved' END AS type \
|
||||
# FROM tickets) type \
|
||||
# GROUP BY type;"
|
||||
#
|
||||
# <Result>
|
||||
# Type counter
|
||||
# InstancePrefix "rt36_tickets"
|
||||
# InstancesFrom "type"
|
||||
# ValuesFrom "count"
|
||||
# </Result>
|
||||
# </Query>
|
||||
#
|
||||
# <Database foo>
|
||||
# Host "hostname"
|
||||
# Port 5432
|
||||
# User "username"
|
||||
# Password "secret"
|
||||
#
|
||||
# SSLMode "prefer"
|
||||
# KRBSrvName "kerberos_service_name"
|
||||
#
|
||||
# Query magic
|
||||
# </Database>
|
||||
#
|
||||
# <Database bar>
|
||||
# Interval 60
|
||||
# Service "service_name"
|
||||
#
|
||||
# Query backend # predefined
|
||||
# Query rt36_tickets
|
||||
# </Database>
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin powerdns>
|
||||
# <Server "server_name">
|
||||
# Collect "latency"
|
||||
# Collect "udp-answers" "udp-queries"
|
||||
# Socket "/var/run/pdns.controlsocket"
|
||||
# </Server>
|
||||
# <Recursor "recursor_name">
|
||||
# Collect "questions"
|
||||
# Collect "cache-hits" "cache-misses"
|
||||
# Socket "/var/run/pdns_recursor.controlsocket"
|
||||
# </Recursor>
|
||||
# LocalSocket "/opt/collectd/var/run/collectd-powerdns"
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin processes>
|
||||
# Process "name"
|
||||
# ProcessMatch "foobar" "/usr/bin/perl foobar\\.pl.*"
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin protocols>
|
||||
# Value "/^Tcp:/"
|
||||
# IgnoreSelected false
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin python>
|
||||
# ModulePath "/path/to/your/python/modules"
|
||||
# LogTraces true
|
||||
# Interactive true
|
||||
# Import "spam"
|
||||
#
|
||||
# <Module spam>
|
||||
# spam "wonderful" "lovely"
|
||||
# </Module>
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin rrdcached>
|
||||
# DaemonAddress "unix:/var/run/rrdcached.sock"
|
||||
# DataDir "/var/lib/rrdcached/db/collectd"
|
||||
# CreateFiles true
|
||||
# CollectStatistics true
|
||||
#</Plugin>
|
||||
|
||||
<Plugin rrdtool>
|
||||
DataDir "/var/lib/collectd/rrd"
|
||||
# CacheTimeout 120
|
||||
# CacheFlush 900
|
||||
# WritesPerSecond 30
|
||||
# RandomTimeout 0
|
||||
#
|
||||
# The following settings are rather advanced
|
||||
# and should usually not be touched:
|
||||
# StepSize 10
|
||||
# HeartBeat 20
|
||||
# RRARows 1200
|
||||
# RRATimespan 158112000
|
||||
# XFF 0.1
|
||||
</Plugin>
|
||||
|
||||
#<Plugin sensors>
|
||||
# SensorConfigFile "/etc/sensors3.conf"
|
||||
# Sensor "it8712-isa-0290/temperature-temp1"
|
||||
# Sensor "it8712-isa-0290/fanspeed-fan3"
|
||||
# Sensor "it8712-isa-0290/voltage-in8"
|
||||
# IgnoreSelected false
|
||||
#</Plugin>
|
||||
|
||||
# See /usr/share/doc/collectd/examples/snmp-data.conf.gz for a
|
||||
# comprehensive sample configuration.
|
||||
#<Plugin snmp>
|
||||
# <Data "powerplus_voltge_input">
|
||||
# Type "voltage"
|
||||
# Table false
|
||||
# Instance "input_line1"
|
||||
# Scale 0.1
|
||||
# Values "SNMPv2-SMI::enterprises.6050.5.4.1.1.2.1"
|
||||
# </Data>
|
||||
# <Data "hr_users">
|
||||
# Type "users"
|
||||
# Table false
|
||||
# Instance ""
|
||||
# Shift -1
|
||||
# Values "HOST-RESOURCES-MIB::hrSystemNumUsers.0"
|
||||
# </Data>
|
||||
# <Data "std_traffic">
|
||||
# Type "if_octets"
|
||||
# Table true
|
||||
# InstancePrefix "traffic"
|
||||
# Instance "IF-MIB::ifDescr"
|
||||
# Values "IF-MIB::ifInOctets" "IF-MIB::ifOutOctets"
|
||||
# </Data>
|
||||
#
|
||||
# <Host "some.switch.mydomain.org">
|
||||
# Address "192.168.0.2"
|
||||
# Version 1
|
||||
# Community "community_string"
|
||||
# Collect "std_traffic"
|
||||
# Inverval 120
|
||||
# </Host>
|
||||
# <Host "some.server.mydomain.org">
|
||||
# Address "192.168.0.42"
|
||||
# Version 2
|
||||
# Community "another_string"
|
||||
# Collect "std_traffic" "hr_users"
|
||||
# </Host>
|
||||
# <Host "some.ups.mydomain.org">
|
||||
# Address "192.168.0.3"
|
||||
# Version 1
|
||||
# Community "more_communities"
|
||||
# Collect "powerplus_voltge_input"
|
||||
# Interval 300
|
||||
# </Host>
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin swap>
|
||||
# ReportByDevice false
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin table>
|
||||
# <Table "/proc/slabinfo">
|
||||
# Instance "slabinfo"
|
||||
# Separator " "
|
||||
# <Result>
|
||||
# Type gauge
|
||||
# InstancePrefix "active_objs"
|
||||
# InstancesFrom 0
|
||||
# ValuesFrom 1
|
||||
# </Result>
|
||||
# <Result>
|
||||
# Type gauge
|
||||
# InstancePrefix "objperslab"
|
||||
# InstancesFrom 0
|
||||
# ValuesFrom 4
|
||||
# </Result>
|
||||
# </Table>
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin "tail">
|
||||
# <File "/var/log/exim4/mainlog">
|
||||
# Instance "exim"
|
||||
# <Match>
|
||||
# Regex "S=([1-9][0-9]*)"
|
||||
# DSType "CounterAdd"
|
||||
# Type "ipt_bytes"
|
||||
# Instance "total"
|
||||
# </Match>
|
||||
# <Match>
|
||||
# Regex "\\<R=local_user\\>"
|
||||
# ExcludeRegex "\\<R=local_user\\>.*mail_spool defer"
|
||||
# DSType "CounterInc"
|
||||
# Type "counter"
|
||||
# Instance "local_user"
|
||||
# </Match>
|
||||
# </File>
|
||||
#</Plugin>
|
||||
|
||||
<Plugin tcpconns>
|
||||
LocalPort "4001"
|
||||
LocalPort "7001"
|
||||
</Plugin>
|
||||
|
||||
#<Plugin teamspeak2>
|
||||
# Host "127.0.0.1"
|
||||
# Port "51234"
|
||||
# Server "8767"
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin ted>
|
||||
# Device "/dev/ttyUSB0"
|
||||
# Retries 0
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin thermal>
|
||||
# ForceUseProcfs false
|
||||
# Device "THRM"
|
||||
# IgnoreSelected false
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin tokyotyrant>
|
||||
# Host "localhost"
|
||||
# Port "1978"
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin unixsock>
|
||||
# SocketFile "/var/run/collectd-unixsock"
|
||||
# SocketGroup "collectd"
|
||||
# SocketPerms "0660"
|
||||
# DeleteSocket false
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin uuid>
|
||||
# UUIDFile "/etc/uuid"
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin varnish>
|
||||
# <Instance>
|
||||
# CollectCache true
|
||||
# CollectBackend true
|
||||
# CollectConnections true
|
||||
# CollectSHM true
|
||||
# CollectESI false
|
||||
# CollectFetch false
|
||||
# CollectHCB false
|
||||
# CollectSMA false
|
||||
# CollectSMS false
|
||||
# CollectSM false
|
||||
# CollectTotals false
|
||||
# CollectWorkers false
|
||||
# </Instance>
|
||||
#
|
||||
# <Instance "myinstance">
|
||||
# CollectCache true
|
||||
# </Instance>
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin vmem>
|
||||
# Verbose false
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin write_graphite>
|
||||
# <Carbon>
|
||||
# Host "127.0.01"
|
||||
# Port "2003"
|
||||
# Prefix "collectd"
|
||||
# Postfix "collectd"
|
||||
# StoreRates false
|
||||
# AlwaysAppendDS false
|
||||
# EscapeCharacter "_"
|
||||
# </Carbon>
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin write_http>
|
||||
# <URL "http://example.com/collectd-post">
|
||||
# User "collectd"
|
||||
# Password "secret"
|
||||
# VerifyPeer true
|
||||
# VerifyHost true
|
||||
# CACert "/etc/ssl/ca.crt"
|
||||
# Format "Command"
|
||||
# StoreRates false
|
||||
# </URL>
|
||||
#</Plugin>
|
||||
|
||||
#<Plugin write_mongodb>
|
||||
# <Node "example">
|
||||
# Host "localhost"
|
||||
# Port "27017"
|
||||
# Timeout 1000
|
||||
# StoreRates false
|
||||
# <Node>
|
||||
#</Plugin>
|
||||
|
||||
Include "/etc/collectd/filters.conf"
|
||||
Include "/etc/collectd/thresholds.conf"
|
31
contrib/graphite/Dockerfile
Normal file
31
contrib/graphite/Dockerfile
Normal file
@ -0,0 +1,31 @@
|
||||
from stackbrew/ubuntu:precise
|
||||
|
||||
run echo 'deb http://us.archive.ubuntu.com/ubuntu/ precise universe' >> /etc/apt/sources.list
|
||||
run apt-get -y update
|
||||
|
||||
# Install required packages
|
||||
run apt-get -y install python-cairo python-django python-twisted python-django-tagging python-simplejson python-pysqlite2 python-support python-pip gunicorn supervisor nginx-light
|
||||
run pip install whisper
|
||||
run pip install --install-option="--prefix=/var/lib/graphite" --install-option="--install-lib=/var/lib/graphite/lib" carbon
|
||||
run pip install --install-option="--prefix=/var/lib/graphite" --install-option="--install-lib=/var/lib/graphite/webapp" graphite-web
|
||||
|
||||
# Add system service config
|
||||
add ./nginx.conf /etc/nginx/nginx.conf
|
||||
add ./supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||
|
||||
# Add graphite config
|
||||
add ./initial_data.json /var/lib/graphite/webapp/graphite/initial_data.json
|
||||
add ./local_settings.py /var/lib/graphite/webapp/graphite/local_settings.py
|
||||
add ./carbon.conf /var/lib/graphite/conf/carbon.conf
|
||||
add ./storage-schemas.conf /var/lib/graphite/conf/storage-schemas.conf
|
||||
run mkdir -p /var/lib/graphite/storage/whisper
|
||||
run touch /var/lib/graphite/storage/graphite.db /var/lib/graphite/storage/index
|
||||
run chown -R www-data /var/lib/graphite/storage
|
||||
run chmod 0775 /var/lib/graphite/storage /var/lib/graphite/storage/whisper
|
||||
run chmod 0664 /var/lib/graphite/storage/graphite.db
|
||||
run cd /var/lib/graphite/webapp/graphite && python manage.py syncdb --noinput
|
||||
|
||||
expose :80
|
||||
expose :2003
|
||||
|
||||
cmd ["/usr/bin/supervisord"]
|
7
contrib/graphite/README
Normal file
7
contrib/graphite/README
Normal file
@ -0,0 +1,7 @@
|
||||
Running graphite under Docker is straightforward:
|
||||
|
||||
1. Build the graphite image using Docker
|
||||
docker build -t graphite .
|
||||
|
||||
2. Run a graphite container. Be sure to replace the $IP field with the IP address at which you wish to expose your graphite web service.
|
||||
docker run -p $IP:8080:80 -p $IP:2003:2003 -d graphite
|
62
contrib/graphite/carbon.conf
Normal file
62
contrib/graphite/carbon.conf
Normal file
@ -0,0 +1,62 @@
|
||||
[cache]
|
||||
LOCAL_DATA_DIR = /var/lib/graphite/storage/whisper/
|
||||
|
||||
# Specify the user to drop privileges to
|
||||
# If this is blank carbon runs as the user that invokes it
|
||||
# This user must have write access to the local data directory
|
||||
USER =
|
||||
|
||||
# Limit the size of the cache to avoid swapping or becoming CPU bound.
|
||||
# Sorts and serving cache queries gets more expensive as the cache grows.
|
||||
# Use the value "inf" (infinity) for an unlimited cache size.
|
||||
MAX_CACHE_SIZE = inf
|
||||
|
||||
# Limits the number of whisper update_many() calls per second, which effectively
|
||||
# means the number of write requests sent to the disk. This is intended to
|
||||
# prevent over-utilizing the disk and thus starving the rest of the system.
|
||||
# When the rate of required updates exceeds this, then carbon's caching will
|
||||
# take effect and increase the overall throughput accordingly.
|
||||
MAX_UPDATES_PER_SECOND = 1000
|
||||
|
||||
# Softly limits the number of whisper files that get created each minute.
|
||||
# Setting this value low (like at 50) is a good way to ensure your graphite
|
||||
# system will not be adversely impacted when a bunch of new metrics are
|
||||
# sent to it. The trade off is that it will take much longer for those metrics'
|
||||
# database files to all get created and thus longer until the data becomes usable.
|
||||
# Setting this value high (like "inf" for infinity) will cause graphite to create
|
||||
# the files quickly but at the risk of slowing I/O down considerably for a while.
|
||||
MAX_CREATES_PER_MINUTE = inf
|
||||
|
||||
LINE_RECEIVER_INTERFACE = 0.0.0.0
|
||||
LINE_RECEIVER_PORT = 2003
|
||||
|
||||
#PICKLE_RECEIVER_INTERFACE = 0.0.0.0
|
||||
#PICKLE_RECEIVER_PORT = 2004
|
||||
|
||||
#CACHE_QUERY_INTERFACE = 0.0.0.0
|
||||
#CACHE_QUERY_PORT = 7002
|
||||
|
||||
LOG_UPDATES = False
|
||||
|
||||
# Enable AMQP if you want to receve metrics using an amqp broker
|
||||
# ENABLE_AMQP = False
|
||||
|
||||
# Verbose means a line will be logged for every metric received
|
||||
# useful for testing
|
||||
# AMQP_VERBOSE = False
|
||||
|
||||
# AMQP_HOST = localhost
|
||||
# AMQP_PORT = 5672
|
||||
# AMQP_VHOST = /
|
||||
# AMQP_USER = guest
|
||||
# AMQP_PASSWORD = guest
|
||||
# AMQP_EXCHANGE = graphite
|
||||
|
||||
# Patterns for all of the metrics this machine will store. Read more at
|
||||
# http://en.wikipedia.org/wiki/Advanced_Message_Queuing_Protocol#Bindings
|
||||
#
|
||||
# Example: store all sales, linux servers, and utilization metrics
|
||||
# BIND_PATTERNS = sales.#, servers.linux.#, #.utilization
|
||||
#
|
||||
# Example: store everything
|
||||
# BIND_PATTERNS = #
|
20
contrib/graphite/initial_data.json
Normal file
20
contrib/graphite/initial_data.json
Normal file
@ -0,0 +1,20 @@
|
||||
[
|
||||
{
|
||||
"pk": 1,
|
||||
"model": "auth.user",
|
||||
"fields": {
|
||||
"username": "admin",
|
||||
"first_name": "",
|
||||
"last_name": "",
|
||||
"is_active": true,
|
||||
"is_superuser": true,
|
||||
"is_staff": true,
|
||||
"last_login": "2011-09-20 17:02:14",
|
||||
"groups": [],
|
||||
"user_permissions": [],
|
||||
"password": "sha1$1b11b$edeb0a67a9622f1f2cfeabf9188a711f5ac7d236",
|
||||
"email": "root@example.com",
|
||||
"date_joined": "2011-09-20 17:02:14"
|
||||
}
|
||||
}
|
||||
]
|
1
contrib/graphite/local_settings.py
Normal file
1
contrib/graphite/local_settings.py
Normal file
@ -0,0 +1 @@
|
||||
TIME_ZONE = 'UTC'
|
69
contrib/graphite/nginx.conf
Normal file
69
contrib/graphite/nginx.conf
Normal file
@ -0,0 +1,69 @@
|
||||
daemon off;
|
||||
user www-data;
|
||||
worker_processes 1;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
types_hash_max_size 2048;
|
||||
server_tokens off;
|
||||
|
||||
server_names_hash_bucket_size 32;
|
||||
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
access_log /var/log/nginx/access.log;
|
||||
error_log /var/log/nginx/error.log;
|
||||
|
||||
gzip on;
|
||||
gzip_disable "msie6";
|
||||
|
||||
server {
|
||||
listen 80 default_server;
|
||||
server_name _;
|
||||
|
||||
open_log_file_cache max=1000 inactive=20s min_uses=2 valid=1m;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8000;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Server $host;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header Host $host;
|
||||
|
||||
client_max_body_size 10m;
|
||||
client_body_buffer_size 128k;
|
||||
|
||||
proxy_connect_timeout 90;
|
||||
proxy_send_timeout 90;
|
||||
proxy_read_timeout 90;
|
||||
|
||||
proxy_buffer_size 4k;
|
||||
proxy_buffers 4 32k;
|
||||
proxy_busy_buffers_size 64k;
|
||||
proxy_temp_file_write_size 64k;
|
||||
}
|
||||
|
||||
add_header Access-Control-Allow-Origin "*";
|
||||
add_header Access-Control-Allow-Methods "GET, OPTIONS";
|
||||
add_header Access-Control-Allow-Headers "origin, authorization, accept";
|
||||
|
||||
location /content {
|
||||
alias /var/lib/graphite/webapp/content;
|
||||
}
|
||||
|
||||
location /media {
|
||||
alias /usr/share/pyshared/django/contrib/admin/media;
|
||||
}
|
||||
}
|
||||
}
|
7
contrib/graphite/storage-schemas.conf
Normal file
7
contrib/graphite/storage-schemas.conf
Normal file
@ -0,0 +1,7 @@
|
||||
[carbon]
|
||||
pattern = ^carbon\..*
|
||||
retentions = 1m:31d,10m:1y,1h:5y
|
||||
|
||||
[default]
|
||||
pattern = .*
|
||||
retentions = 10s:8d,1m:31d,10m:1y,1h:5y
|
25
contrib/graphite/supervisord.conf
Normal file
25
contrib/graphite/supervisord.conf
Normal file
@ -0,0 +1,25 @@
|
||||
[supervisord]
|
||||
nodaemon = true
|
||||
environment = GRAPHITE_STORAGE_DIR='/var/lib/graphite/storage',GRAPHITE_CONF_DIR='/var/lib/graphite/conf'
|
||||
|
||||
[program:nginx]
|
||||
command = /usr/sbin/nginx
|
||||
stdout_logfile = /var/log/supervisor/%(program_name)s.log
|
||||
stderr_logfile = /var/log/supervisor/%(program_name)s.log
|
||||
autorestart = true
|
||||
|
||||
[program:carbon-cache]
|
||||
user = www-data
|
||||
command = /var/lib/graphite/bin/carbon-cache.py --debug start
|
||||
stdout_logfile = /var/log/supervisor/%(program_name)s.log
|
||||
stderr_logfile = /var/log/supervisor/%(program_name)s.log
|
||||
autorestart = true
|
||||
|
||||
[program:graphite-webapp]
|
||||
user = www-data
|
||||
directory = /var/lib/graphite/webapp
|
||||
environment = PYTHONPATH='/var/lib/graphite/webapp'
|
||||
command = /usr/bin/gunicorn_django -b127.0.0.1:8000 -w2 graphite/settings.py
|
||||
stdout_logfile = /var/log/supervisor/%(program_name)s.log
|
||||
stderr_logfile = /var/log/supervisor/%(program_name)s.log
|
||||
autorestart = true
|
145
discovery/discovery.go
Normal file
145
discovery/discovery.go
Normal file
@ -0,0 +1,145 @@
|
||||
package discovery
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
etcdErr "github.com/coreos/etcd/error"
|
||||
"github.com/coreos/etcd/log"
|
||||
"github.com/coreos/etcd/third_party/github.com/coreos/go-etcd/etcd"
|
||||
)
|
||||
|
||||
const (
|
||||
stateKey = "_state"
|
||||
startedState = "started"
|
||||
defaultTTL = 604800 // One week TTL
|
||||
)
|
||||
|
||||
type Discoverer struct {
|
||||
client *etcd.Client
|
||||
name string
|
||||
peer string
|
||||
prefix string
|
||||
discoveryURL string
|
||||
}
|
||||
|
||||
var defaultDiscoverer *Discoverer
|
||||
|
||||
func init() {
|
||||
defaultDiscoverer = &Discoverer{}
|
||||
}
|
||||
|
||||
func (d *Discoverer) Do(discoveryURL string, name string, peer string) (peers []string, err error) {
|
||||
d.name = name
|
||||
d.peer = peer
|
||||
d.discoveryURL = discoveryURL
|
||||
|
||||
u, err := url.Parse(discoveryURL)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// prefix is prepended to all keys for this discovery
|
||||
d.prefix = strings.TrimPrefix(u.Path, "/v2/keys/")
|
||||
|
||||
// keep the old path in case we need to set the KeyPrefix below
|
||||
oldPath := u.Path
|
||||
u.Path = ""
|
||||
|
||||
// Connect to a scheme://host not a full URL with path
|
||||
log.Infof("Discovery via %s using prefix %s.", u.String(), d.prefix)
|
||||
d.client = etcd.NewClient([]string{u.String()})
|
||||
|
||||
if !strings.HasPrefix(oldPath, "/v2/keys") {
|
||||
d.client.SetKeyPrefix("")
|
||||
}
|
||||
|
||||
// Register this machine first and announce that we are a member of
|
||||
// this cluster
|
||||
err = d.heartbeat()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Start the very slow heartbeat to the cluster now in anticipation
|
||||
// that everything is going to go alright now
|
||||
go d.startHeartbeat()
|
||||
|
||||
// Attempt to take the leadership role, if there is no error we are it!
|
||||
resp, err := d.client.Create(path.Join(d.prefix, stateKey), startedState, 0)
|
||||
|
||||
// Bail out on unexpected errors
|
||||
if err != nil {
|
||||
if clientErr, ok := err.(*etcd.EtcdError); !ok || clientErr.ErrorCode != etcdErr.EcodeNodeExist {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// If we got a response then the CAS was successful, we are leader
|
||||
if resp != nil && resp.Node.Value == startedState {
|
||||
// We are the leader, we have no peers
|
||||
log.Infof("Discovery _state was empty, so this machine is the initial leader.")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Fall through to finding the other discovery peers
|
||||
return d.findPeers()
|
||||
}
|
||||
|
||||
func (d *Discoverer) findPeers() (peers []string, err error) {
|
||||
resp, err := d.client.Get(path.Join(d.prefix), false, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
node := resp.Node
|
||||
|
||||
if node == nil {
|
||||
return nil, fmt.Errorf("%s key doesn't exist.", d.prefix)
|
||||
}
|
||||
|
||||
for _, n := range node.Nodes {
|
||||
// Skip our own entry in the list, there is no point
|
||||
if strings.HasSuffix(n.Key, "/"+d.name) {
|
||||
continue
|
||||
}
|
||||
peers = append(peers, n.Value)
|
||||
}
|
||||
|
||||
if len(peers) == 0 {
|
||||
return nil, errors.New("Discovery found an initialized cluster but no peers are registered.")
|
||||
}
|
||||
|
||||
log.Infof("Discovery found peers %v", peers)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (d *Discoverer) startHeartbeat() {
|
||||
// In case of errors we should attempt to heartbeat fairly frequently
|
||||
heartbeatInterval := defaultTTL / 8
|
||||
ticker := time.Tick(time.Second * time.Duration(heartbeatInterval))
|
||||
for {
|
||||
select {
|
||||
case <-ticker:
|
||||
err := d.heartbeat()
|
||||
if err != nil {
|
||||
log.Warnf("Discovery heartbeat failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Discoverer) heartbeat() error {
|
||||
_, err := d.client.Set(path.Join(d.prefix, d.name), d.peer, defaultTTL)
|
||||
return err
|
||||
}
|
||||
|
||||
func Do(discoveryURL string, name string, peer string) ([]string, error) {
|
||||
return defaultDiscoverer.Do(discoveryURL, name, peer)
|
||||
}
|
@ -33,12 +33,18 @@ const (
|
||||
EcodeNodeExist = 105
|
||||
EcodeKeyIsPreserved = 106
|
||||
EcodeRootROnly = 107
|
||||
EcodeDirNotEmpty = 108
|
||||
|
||||
EcodeValueRequired = 200
|
||||
EcodePrevValueRequired = 201
|
||||
EcodeTTLNaN = 202
|
||||
EcodeIndexNaN = 203
|
||||
EcodeValueOrTTLRequired = 204
|
||||
EcodeValueRequired = 200
|
||||
EcodePrevValueRequired = 201
|
||||
EcodeTTLNaN = 202
|
||||
EcodeIndexNaN = 203
|
||||
EcodeValueOrTTLRequired = 204
|
||||
EcodeTimeoutNaN = 205
|
||||
EcodeNameRequired = 206
|
||||
EcodeIndexOrValueRequired = 207
|
||||
EcodeIndexValueMutex = 208
|
||||
EcodeInvalidField = 209
|
||||
|
||||
EcodeRaftInternal = 300
|
||||
EcodeLeaderElect = 301
|
||||
@ -51,14 +57,15 @@ func init() {
|
||||
errors = make(map[int]string)
|
||||
|
||||
// command related errors
|
||||
errors[EcodeKeyNotFound] = "Key Not Found"
|
||||
errors[EcodeTestFailed] = "Test Failed" //test and set
|
||||
errors[EcodeNotFile] = "Not A File"
|
||||
errors[EcodeKeyNotFound] = "Key not found"
|
||||
errors[EcodeTestFailed] = "Compare failed" //test and set
|
||||
errors[EcodeNotFile] = "Not a file"
|
||||
errors[EcodeNoMorePeer] = "Reached the max number of peers in the cluster"
|
||||
errors[EcodeNotDir] = "Not A Directory"
|
||||
errors[EcodeNodeExist] = "Already exists" // create
|
||||
errors[EcodeNotDir] = "Not a directory"
|
||||
errors[EcodeNodeExist] = "Key already exists" // create
|
||||
errors[EcodeRootROnly] = "Root is read only"
|
||||
errors[EcodeKeyIsPreserved] = "The prefix of given key is a keyword in etcd"
|
||||
errors[EcodeDirNotEmpty] = "Directory not empty"
|
||||
|
||||
// Post form related errors
|
||||
errors[EcodeValueRequired] = "Value is Required in POST form"
|
||||
@ -66,6 +73,11 @@ func init() {
|
||||
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"
|
||||
errors[EcodeTimeoutNaN] = "The given timeout in POST form is not a number"
|
||||
errors[EcodeNameRequired] = "Name is required in POST form"
|
||||
errors[EcodeIndexOrValueRequired] = "Index or value is required"
|
||||
errors[EcodeIndexValueMutex] = "Index and value cannot both be specified"
|
||||
errors[EcodeInvalidField] = "Invalid field"
|
||||
|
||||
// raft related errors
|
||||
errors[EcodeRaftInternal] = "Raft Internal Error"
|
||||
@ -109,10 +121,19 @@ 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)
|
||||
} else {
|
||||
http.Error(w, e.toJsonString(), http.StatusBadRequest)
|
||||
// 3xx is raft internal error
|
||||
status := http.StatusBadRequest
|
||||
switch e.ErrorCode {
|
||||
case EcodeKeyNotFound:
|
||||
status = http.StatusNotFound
|
||||
case EcodeNotFile, EcodeDirNotEmpty:
|
||||
status = http.StatusForbidden
|
||||
case EcodeTestFailed, EcodeNodeExist:
|
||||
status = http.StatusPreconditionFailed
|
||||
default:
|
||||
if e.ErrorCode/100 == 3 {
|
||||
status = http.StatusInternalServerError
|
||||
}
|
||||
}
|
||||
http.Error(w, e.toJsonString(), status)
|
||||
}
|
||||
|
147
etcd.go
147
etcd.go
@ -18,17 +18,26 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/third_party/github.com/coreos/raft"
|
||||
|
||||
"github.com/coreos/etcd/config"
|
||||
ehttp "github.com/coreos/etcd/http"
|
||||
"github.com/coreos/etcd/log"
|
||||
"github.com/coreos/etcd/metrics"
|
||||
"github.com/coreos/etcd/server"
|
||||
"github.com/coreos/etcd/store"
|
||||
"github.com/coreos/raft"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Load configuration.
|
||||
var config = server.NewConfig()
|
||||
var config = config.New()
|
||||
if err := config.Load(os.Args[1:]); err != nil {
|
||||
fmt.Println(server.Usage() + "\n")
|
||||
fmt.Println(err.Error() + "\n")
|
||||
@ -42,7 +51,10 @@ func main() {
|
||||
}
|
||||
|
||||
// Enable options.
|
||||
if config.VeryVerbose {
|
||||
if config.VeryVeryVerbose {
|
||||
log.Verbose = true
|
||||
raft.SetLogLevel(raft.Trace)
|
||||
} else if config.VeryVerbose {
|
||||
log.Verbose = true
|
||||
raft.SetLogLevel(raft.Debug)
|
||||
} else if config.Verbose {
|
||||
@ -61,42 +73,129 @@ func main() {
|
||||
log.Fatalf("Unable to create path: %s", err)
|
||||
}
|
||||
|
||||
// Load info object.
|
||||
info, err := config.Info()
|
||||
if err != nil {
|
||||
log.Fatal("info:", err)
|
||||
// Warn people if they have an info file
|
||||
info := filepath.Join(config.DataDir, "info")
|
||||
if _, err := os.Stat(info); err == nil {
|
||||
log.Warnf("All cached configuration is now ignored. The file %s can be removed.", info)
|
||||
}
|
||||
|
||||
// Retrieve TLS configuration.
|
||||
tlsConfig, err := info.EtcdTLS.Config()
|
||||
if err != nil {
|
||||
log.Fatal("Client TLS:", err)
|
||||
var mbName string
|
||||
if config.Trace() {
|
||||
mbName = config.MetricsBucketName()
|
||||
runtime.SetBlockProfileRate(1)
|
||||
}
|
||||
peerTLSConfig, err := info.RaftTLS.Config()
|
||||
|
||||
mb := metrics.NewBucket(mbName)
|
||||
|
||||
if config.GraphiteHost != "" {
|
||||
err := mb.Publish(config.GraphiteHost)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieve CORS configuration
|
||||
corsInfo, err := ehttp.NewCORSInfo(config.CorsOrigins)
|
||||
if err != nil {
|
||||
log.Fatal("Peer TLS:", err)
|
||||
log.Fatal("CORS:", 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.SnapshotCount)
|
||||
ps.MaxClusterSize = config.MaxClusterSize
|
||||
ps.RetryTimes = config.MaxRetryAttempts
|
||||
// Create stats objects
|
||||
followersStats := server.NewRaftFollowersStats(config.Name)
|
||||
serverStats := server.NewRaftServerStats(config.Name)
|
||||
|
||||
// Create client server.
|
||||
s := server.New(info.Name, info.EtcdURL, info.EtcdListenHost, &tlsConfig, &info.EtcdTLS, ps, registry, store)
|
||||
if err := s.AllowOrigins(config.CorsOrigins); err != nil {
|
||||
panic(err)
|
||||
// Calculate all of our timeouts
|
||||
heartbeatTimeout := time.Duration(config.Peer.HeartbeatTimeout) * time.Millisecond
|
||||
electionTimeout := time.Duration(config.Peer.ElectionTimeout) * time.Millisecond
|
||||
dialTimeout := (3 * heartbeatTimeout) + electionTimeout
|
||||
responseHeaderTimeout := (3 * heartbeatTimeout) + electionTimeout
|
||||
|
||||
// Create peer server
|
||||
psConfig := server.PeerServerConfig{
|
||||
Name: config.Name,
|
||||
Scheme: config.PeerTLSInfo().Scheme(),
|
||||
URL: config.Peer.Addr,
|
||||
SnapshotCount: config.SnapshotCount,
|
||||
MaxClusterSize: config.MaxClusterSize,
|
||||
RetryTimes: config.MaxRetryAttempts,
|
||||
RetryInterval: config.RetryInterval,
|
||||
}
|
||||
ps := server.NewPeerServer(psConfig, registry, store, &mb, followersStats, serverStats)
|
||||
|
||||
var psListener net.Listener
|
||||
if psConfig.Scheme == "https" {
|
||||
peerServerTLSConfig, err := config.PeerTLSInfo().ServerConfig()
|
||||
if err != nil {
|
||||
log.Fatal("peer server TLS error: ", err)
|
||||
}
|
||||
|
||||
psListener, err = server.NewTLSListener(config.Peer.BindAddr, peerServerTLSConfig)
|
||||
if err != nil {
|
||||
log.Fatal("Failed to create peer listener: ", err)
|
||||
}
|
||||
} else {
|
||||
psListener, err = server.NewListener(config.Peer.BindAddr)
|
||||
if err != nil {
|
||||
log.Fatal("Failed to create peer listener: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create raft transporter and server
|
||||
raftTransporter := server.NewTransporter(followersStats, serverStats, registry, heartbeatTimeout, dialTimeout, responseHeaderTimeout)
|
||||
if psConfig.Scheme == "https" {
|
||||
raftClientTLSConfig, err := config.PeerTLSInfo().ClientConfig()
|
||||
if err != nil {
|
||||
log.Fatal("raft client TLS error: ", err)
|
||||
}
|
||||
raftTransporter.SetTLSConfig(*raftClientTLSConfig)
|
||||
}
|
||||
raftServer, err := raft.NewServer(config.Name, config.DataDir, raftTransporter, store, ps, "")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
raftServer.SetElectionTimeout(electionTimeout)
|
||||
raftServer.SetHeartbeatInterval(heartbeatTimeout)
|
||||
ps.SetRaftServer(raftServer)
|
||||
|
||||
// Create etcd server
|
||||
s := server.New(config.Name, config.Addr, ps, registry, store, &mb)
|
||||
|
||||
if config.Trace() {
|
||||
s.EnableTracing()
|
||||
}
|
||||
|
||||
var sListener net.Listener
|
||||
if config.EtcdTLSInfo().Scheme() == "https" {
|
||||
etcdServerTLSConfig, err := config.EtcdTLSInfo().ServerConfig()
|
||||
if err != nil {
|
||||
log.Fatal("etcd TLS error: ", err)
|
||||
}
|
||||
|
||||
sListener, err = server.NewTLSListener(config.BindAddr, etcdServerTLSConfig)
|
||||
if err != nil {
|
||||
log.Fatal("Failed to create TLS etcd listener: ", err)
|
||||
}
|
||||
} else {
|
||||
sListener, err = server.NewListener(config.BindAddr)
|
||||
if err != nil {
|
||||
log.Fatal("Failed to create etcd listener: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
ps.SetServer(s)
|
||||
ps.Start(config.Snapshot, config.Peers)
|
||||
|
||||
// Run peer server in separate thread while the client server blocks.
|
||||
go func() {
|
||||
log.Fatal(ps.ListenAndServe(config.Snapshot, config.Peers))
|
||||
log.Infof("peer server [name %s, listen on %s, advertised url %s]", ps.Config.Name, psListener.Addr(), ps.Config.URL)
|
||||
sHTTP := &ehttp.CORSHandler{ps.HTTPHandler(), corsInfo}
|
||||
log.Fatal(http.Serve(psListener, sHTTP))
|
||||
}()
|
||||
log.Fatal(s.ListenAndServe())
|
||||
|
||||
log.Infof("etcd server [name %s, listen on %s, advertised url %s]", s.Name, sListener.Addr(), s.URL())
|
||||
sHTTP := &ehttp.CORSHandler{s.HTTPHandler(), corsInfo}
|
||||
log.Fatal(http.Serve(sListener, sHTTP))
|
||||
}
|
||||
|
@ -1,3 +1,3 @@
|
||||
// +build !go1.1
|
||||
// +build !go1.2
|
||||
|
||||
"etcd requires go 1.1 or greater to build"
|
||||
"etcd requires go 1.2 or greater to build"
|
||||
|
@ -14,56 +14,55 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package server
|
||||
package http
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
type corsHandler struct {
|
||||
router *mux.Router
|
||||
corsOrigins map[string]bool
|
||||
}
|
||||
type CORSInfo map[string]bool
|
||||
|
||||
// AllowOrigins sets a comma-delimited list of origins that are allowed.
|
||||
func (s *corsHandler) AllowOrigins(origins []string) error {
|
||||
func NewCORSInfo(origins []string) (*CORSInfo, 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)
|
||||
return nil, fmt.Errorf("Invalid CORS origin: %s", err)
|
||||
}
|
||||
}
|
||||
m[v] = true
|
||||
}
|
||||
s.corsOrigins = m
|
||||
|
||||
return nil
|
||||
info := CORSInfo(m)
|
||||
return &info, nil
|
||||
}
|
||||
|
||||
// OriginAllowed determines whether the server will allow a given CORS origin.
|
||||
func (c *corsHandler) OriginAllowed(origin string) bool {
|
||||
return c.corsOrigins["*"] || c.corsOrigins[origin]
|
||||
func (c CORSInfo) OriginAllowed(origin string) bool {
|
||||
return c["*"] || c[origin]
|
||||
}
|
||||
|
||||
type CORSHandler struct {
|
||||
Handler http.Handler
|
||||
Info *CORSInfo
|
||||
}
|
||||
|
||||
// addHeader adds the correct cors headers given an origin
|
||||
func (h *corsHandler) addHeader(w http.ResponseWriter, origin string) {
|
||||
func (h *CORSHandler) addHeader(w http.ResponseWriter, origin string) {
|
||||
w.Header().Add("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
|
||||
w.Header().Add("Access-Control-Allow-Origin", origin)
|
||||
}
|
||||
|
||||
// ServeHTTP adds the correct CORS headers based on the origin and returns immediatly
|
||||
// with a 200 OK if the method is OPTIONS.
|
||||
func (h *corsHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
func (h *CORSHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
// Write CORS header.
|
||||
if h.OriginAllowed("*") {
|
||||
if h.Info.OriginAllowed("*") {
|
||||
h.addHeader(w, "*")
|
||||
} else if origin := req.Header.Get("Origin"); h.OriginAllowed(origin) {
|
||||
} else if origin := req.Header.Get("Origin"); h.Info.OriginAllowed(origin) {
|
||||
h.addHeader(w, origin)
|
||||
}
|
||||
|
||||
@ -72,7 +71,5 @@ func (h *corsHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
h.router.ServeHTTP(w, req)
|
||||
h.Handler.ServeHTTP(w, req)
|
||||
}
|
||||
|
||||
|
36
http/query_params.go
Normal file
36
http/query_params.go
Normal file
@ -0,0 +1,36 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func NewLowerQueryParamsHandler(hdlr http.Handler) *LowerQueryParamsHandler {
|
||||
return &LowerQueryParamsHandler{hdlr}
|
||||
}
|
||||
|
||||
type LowerQueryParamsHandler struct {
|
||||
Handler http.Handler
|
||||
}
|
||||
|
||||
func (h *LowerQueryParamsHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
err := req.ParseForm()
|
||||
if err == nil {
|
||||
lowerBoolQueryParams(req)
|
||||
}
|
||||
h.Handler.ServeHTTP(w, req)
|
||||
}
|
||||
|
||||
func lowerBoolQueryParams(req *http.Request) {
|
||||
form := req.Form
|
||||
for key, vals := range form {
|
||||
for i, val := range vals {
|
||||
lowered := strings.ToLower(val)
|
||||
if lowered == "true" || lowered == "false" {
|
||||
req.Form[key][i] = lowered
|
||||
} else {
|
||||
req.Form[key][i] = val
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
46
http/query_params_test.go
Normal file
46
http/query_params_test.go
Normal file
@ -0,0 +1,46 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type NilResponseWriter struct{}
|
||||
|
||||
func (w NilResponseWriter) Header() http.Header {
|
||||
return http.Header{}
|
||||
}
|
||||
|
||||
func (w NilResponseWriter) Write(data []byte) (int, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (w NilResponseWriter) WriteHeader(code int) {
|
||||
return
|
||||
}
|
||||
|
||||
type FunctionHandler struct {
|
||||
f func(*http.Request)
|
||||
}
|
||||
|
||||
func (h FunctionHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
h.f(r)
|
||||
}
|
||||
|
||||
func TestQueryParamsLowered(t *testing.T) {
|
||||
assertFunc := func(req *http.Request) {
|
||||
if len(req.Form["One"]) != 1 || req.Form["One"][0] != "true" {
|
||||
t.Errorf("Unexpected value for One: %s", req.Form["One"])
|
||||
} else if len(req.Form["TWO"]) != 1 || req.Form["TWO"][0] != "false" {
|
||||
t.Errorf("Unexpected value for TWO")
|
||||
} else if len(req.Form["three"]) != 2 || req.Form["three"][0] != "true" || req.Form["three"][1] != "false" {
|
||||
t.Errorf("Unexpected value for three")
|
||||
}
|
||||
}
|
||||
assertHdlr := FunctionHandler{assertFunc}
|
||||
hdlr := NewLowerQueryParamsHandler(assertHdlr)
|
||||
respWriter := NilResponseWriter{}
|
||||
|
||||
req, _ := http.NewRequest("GET", "http://example.com?One=TRUE&TWO=False&three=true&three=FALSE", nil)
|
||||
hdlr.ServeHTTP(respWriter, req)
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
golog "github.com/coreos/go-log/log"
|
||||
golog "github.com/coreos/etcd/third_party/github.com/coreos/go-log/log"
|
||||
"os"
|
||||
)
|
||||
|
||||
|
42
metrics/metrics.go
Normal file
42
metrics/metrics.go
Normal file
@ -0,0 +1,42 @@
|
||||
// Package metrics provides both a means of generating metrics and the ability
|
||||
// to send metric data to a graphite endpoint.
|
||||
// The usage of this package without providing a graphite_addr when calling
|
||||
// NewBucket results in NOP metric objects. No data will be collected.
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
gometrics "github.com/coreos/etcd/third_party/github.com/rcrowley/go-metrics"
|
||||
)
|
||||
|
||||
type Timer gometrics.Timer
|
||||
type Gauge gometrics.Gauge
|
||||
|
||||
type Bucket interface {
|
||||
// If a timer exists in this Bucket, return it. Otherwise, create
|
||||
// a new timer with the given name and store it in this Bucket.
|
||||
// The returned object will fulfull the Timer interface.
|
||||
Timer(name string) Timer
|
||||
|
||||
// This acts similarly to Timer, but with objects that fufill the
|
||||
// Gauge interface.
|
||||
Gauge(name string) Gauge
|
||||
|
||||
// Write the current state of all Metrics in a human-readable format
|
||||
// to the provide io.Writer.
|
||||
Dump(io.Writer)
|
||||
|
||||
// Instruct the Bucket to periodically push all metric data to the
|
||||
// provided graphite endpoint.
|
||||
Publish(string) error
|
||||
}
|
||||
|
||||
// Create a new Bucket object that periodically
|
||||
func NewBucket(name string) Bucket {
|
||||
if name == "" {
|
||||
return nilBucket{}
|
||||
}
|
||||
|
||||
return newStandardBucket(name)
|
||||
}
|
25
metrics/nil.go
Normal file
25
metrics/nil.go
Normal file
@ -0,0 +1,25 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
gometrics "github.com/coreos/etcd/third_party/github.com/rcrowley/go-metrics"
|
||||
)
|
||||
|
||||
type nilBucket struct{}
|
||||
|
||||
func (nmb nilBucket) Dump(w io.Writer) {
|
||||
return
|
||||
}
|
||||
|
||||
func (nmb nilBucket) Timer(name string) Timer {
|
||||
return gometrics.NilTimer{}
|
||||
}
|
||||
|
||||
func (nmf nilBucket) Gauge(name string) Gauge {
|
||||
return gometrics.NilGauge{}
|
||||
}
|
||||
|
||||
func (nmf nilBucket) Publish(string) error {
|
||||
return nil
|
||||
}
|
86
metrics/standard.go
Normal file
86
metrics/standard.go
Normal file
@ -0,0 +1,86 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
gometrics "github.com/coreos/etcd/third_party/github.com/rcrowley/go-metrics"
|
||||
)
|
||||
|
||||
const (
|
||||
// RuntimeMemStatsSampleInterval is the interval in seconds at which the
|
||||
// Go runtime's memory statistics will be gathered.
|
||||
RuntimeMemStatsSampleInterval = time.Duration(2) * time.Second
|
||||
|
||||
// GraphitePublishInterval is the interval in seconds at which all
|
||||
// gathered statistics will be published to a Graphite endpoint.
|
||||
GraphitePublishInterval = time.Duration(2) * time.Second
|
||||
)
|
||||
|
||||
type standardBucket struct {
|
||||
sync.Mutex
|
||||
name string
|
||||
registry gometrics.Registry
|
||||
timers map[string]Timer
|
||||
gauges map[string]Gauge
|
||||
}
|
||||
|
||||
func newStandardBucket(name string) standardBucket {
|
||||
registry := gometrics.NewRegistry()
|
||||
|
||||
gometrics.RegisterRuntimeMemStats(registry)
|
||||
go gometrics.CaptureRuntimeMemStats(registry, RuntimeMemStatsSampleInterval)
|
||||
|
||||
return standardBucket{
|
||||
name: name,
|
||||
registry: registry,
|
||||
timers: make(map[string]Timer),
|
||||
gauges: make(map[string]Gauge),
|
||||
}
|
||||
}
|
||||
|
||||
func (smb standardBucket) Dump(w io.Writer) {
|
||||
gometrics.WriteOnce(smb.registry, w)
|
||||
return
|
||||
}
|
||||
|
||||
func (smb standardBucket) Timer(name string) Timer {
|
||||
smb.Lock()
|
||||
defer smb.Unlock()
|
||||
|
||||
timer, ok := smb.timers[name]
|
||||
if !ok {
|
||||
timer = gometrics.NewTimer()
|
||||
smb.timers[name] = timer
|
||||
smb.registry.Register(name, timer)
|
||||
}
|
||||
|
||||
return timer
|
||||
}
|
||||
|
||||
func (smb standardBucket) Gauge(name string) Gauge {
|
||||
smb.Lock()
|
||||
defer smb.Unlock()
|
||||
|
||||
gauge, ok := smb.gauges[name]
|
||||
if !ok {
|
||||
gauge = gometrics.NewGauge()
|
||||
smb.gauges[name] = gauge
|
||||
smb.registry.Register(name, gauge)
|
||||
}
|
||||
|
||||
return gauge
|
||||
}
|
||||
|
||||
func (smb standardBucket) Publish(graphite_addr string) error {
|
||||
addr, err := net.ResolveTCPAddr("tcp", graphite_addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go gometrics.Graphite(smb.registry, GraphitePublishInterval, smb.name, addr)
|
||||
|
||||
return nil
|
||||
}
|
@ -174,7 +174,7 @@ module.exports = function (grunt) {
|
||||
options: {
|
||||
dest: '<%= yeoman.dist %>'
|
||||
},
|
||||
html: ['<%= yeoman.app %>/**/*.html']
|
||||
html: ['<%= yeoman.app %>/index.html']
|
||||
},
|
||||
usemin: {
|
||||
options: {
|
||||
@ -240,6 +240,14 @@ module.exports = function (grunt) {
|
||||
}]
|
||||
}
|
||||
},
|
||||
|
||||
ngmin: {
|
||||
dist: {
|
||||
src: '.tmp/concat/scripts/app.js',
|
||||
dest: '.tmp/concat/scripts/app.js'
|
||||
}
|
||||
},
|
||||
|
||||
// Put files not handled in other tasks here
|
||||
copy: {
|
||||
dist: {
|
||||
@ -251,10 +259,10 @@ module.exports = function (grunt) {
|
||||
src: [
|
||||
'*.{ico,png,txt}',
|
||||
'.htaccess',
|
||||
'images/{,*/}*.{webp,gif}',
|
||||
'images/{,*/}*.{webp,gif,svg}',
|
||||
'styles/fonts/{,*/}*.*',
|
||||
'views/*.*',
|
||||
'index.html',
|
||||
//'index.html',
|
||||
'bower_components/sass-bootstrap/fonts/*.*'
|
||||
]
|
||||
}]
|
||||
@ -286,7 +294,7 @@ module.exports = function (grunt) {
|
||||
'copy:styles'
|
||||
],
|
||||
dist: [
|
||||
'compass',
|
||||
//'compass',
|
||||
'copy:styles',
|
||||
'imagemin',
|
||||
'svgmin',
|
||||
@ -327,13 +335,15 @@ module.exports = function (grunt) {
|
||||
|
||||
grunt.registerTask('build', [
|
||||
'clean:dist',
|
||||
'jshint',
|
||||
'useminPrepare',
|
||||
'concurrent:dist',
|
||||
'autoprefixer',
|
||||
'concat',
|
||||
'cssmin',
|
||||
'uglify',
|
||||
'ngmin',
|
||||
'usemin',
|
||||
'uglify',
|
||||
'copy:dist'
|
||||
]);
|
||||
|
||||
|
@ -22,6 +22,8 @@ bower install
|
||||
|
||||
### View in Browser
|
||||
|
||||
run `export ETCD_DASHBOARD_DIR=/absolute/path/to/coreos/etcd/mod/dashboard/app`
|
||||
|
||||
Run etcd like you normally would and afterward browse to:
|
||||
|
||||
http://localhost:4001/mod/dashboard/
|
||||
|
@ -1 +0,0 @@
|
||||
*.coffee
|
@ -1,50 +0,0 @@
|
||||
<!doctype html>
|
||||
<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
|
||||
<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
|
||||
<!--[if IE 8]> <html class="no-js lt-ie9"> <![endif]-->
|
||||
<!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<title>etcd Browser</title>
|
||||
<meta name="description" content="">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<!-- Place favicon.ico and apple-touch-icon.png in the root directory -->
|
||||
|
||||
<!-- build:css(.tmp) styles/main.css -->
|
||||
<link rel="stylesheet" href="styles/etcd-widgets.css">
|
||||
<link href="http://fonts.googleapis.com/css?family=Source+Sans+Pro:200,300,400,400italic,600,700,900" rel="stylesheet" type="text/css">
|
||||
<link href="http://fonts.googleapis.com/css?family=Source+Code+Pro:400,500,600,700" rel="stylesheet" type="text/css">
|
||||
<!-- endbuild -->
|
||||
</head>
|
||||
<body ng-app="etcdBrowser">
|
||||
<!--[if lt IE 7]>
|
||||
<p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
|
||||
<![endif]-->
|
||||
|
||||
<!--[if lt IE 9]>
|
||||
<script src="bower_components/es5-shim/es5-shim.js"></script>
|
||||
<script src="bower_components/json3/lib/json3.min.js"></script>
|
||||
<![endif]-->
|
||||
|
||||
<!-- Add your site or application content here -->
|
||||
<div id="etd_browser" ng-view="etcd">
|
||||
</div>
|
||||
<!-- build:js scripts/browser-modules.js -->
|
||||
<script src="bower_components/jquery/jquery.js"></script>
|
||||
<script src="bower_components/angular/angular.js"></script>
|
||||
<script src="bower_components/angular-resource/angular-resource.js"></script>
|
||||
<script src="bower_components/angular-route/angular-route.js"></script>
|
||||
<script src="bower_components/angular-cookies/angular-cookies.js"></script>
|
||||
<script src="bower_components/angular-sanitize/angular-sanitize.js"></script>
|
||||
<script src="bower_components/underscore/underscore.js"></script>
|
||||
<script src="bower_components/moment/moment.js"></script>
|
||||
<!-- endbuild -->
|
||||
|
||||
<!-- build:js({.tmp,app}) scripts/browser-scripts.js -->
|
||||
<script src="scripts/ng-time-relative.min.js"></script>
|
||||
<script src="scripts/common/services/etcd.js"></script>
|
||||
<script src="scripts/controllers/browser.js"></script>
|
||||
<!-- endbuild -->
|
||||
</body>
|
||||
</html>
|
7
mod/dashboard/app/images/add.svg
Normal file
7
mod/dashboard/app/images/add.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
preserveAspectRatio="xMinYMin" viewBox="0 0 72.556 61" enable-background="new 0 0 72.556 61" xml:space="preserve">
|
||||
<path d="M34.521,8v11.088v23v10.737c0,2.209,1.791,4,4,4c2.209,0,4-1.791,4-4V42.067V19.109V8c0-2.209-1.791-4-4-4
|
||||
C36.312,4,34.521,5.791,34.521,8z"/>
|
||||
<path d="M16.109,34.412h11.088h23h10.737c2.209,0,4-1.791,4-4c0-2.209-1.791-4-4-4H50.175H27.217H16.109c-2.209,0-4,1.791-4,4
|
||||
C12.109,32.621,13.9,34.412,16.109,34.412z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 561 B |
6
mod/dashboard/app/images/back.svg
Normal file
6
mod/dashboard/app/images/back.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
preserveAspectRatio="xMinYMin" viewBox="0 0 73.356 61" enable-background="new 0 0 73.356 61" xml:space="preserve">
|
||||
<path d="M5.27,33.226l22.428,22.428c1.562,1.562,4.095,1.562,5.657,0c1.562-1.562,1.562-4.095,0-5.657L17.77,34.413h48.514
|
||||
c2.209,0,4-1.791,4-4s-1.791-4-4-4H17.749l15.604-15.582c1.563-1.561,1.565-4.094,0.004-5.657C32.576,4.391,31.552,4,30.527,4
|
||||
c-1.023,0-2.046,0.39-2.827,1.169L5.272,27.567c-0.751,0.75-1.173,1.768-1.173,2.829C4.098,31.458,4.52,32.476,5.27,33.226z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 608 B |
7
mod/dashboard/app/images/delete.svg
Normal file
7
mod/dashboard/app/images/delete.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<svg version="1.1" fill="#f00" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
x="0px" y="0px" preserveAspectRatio="xMinYMin" viewBox="0 0 76.143 61" enable-background="new 0 0 76.143 61" xml:space="preserve">
|
||||
<path d="M49.41,13.505l-6.035,6.035L27.112,35.803l-6.035,6.035c-1.562,1.562-1.562,4.095,0,5.657c1.562,1.562,4.095,1.562,5.657,0
|
||||
l6.05-6.05l16.234-16.234l6.05-6.05c1.562-1.562,1.562-4.095,0-5.657C53.505,11.943,50.972,11.943,49.41,13.505z"/>
|
||||
<path d="M21.077,19.162l6.035,6.035L43.375,41.46l6.035,6.035c1.562,1.562,4.095,1.562,5.657,0c1.562-1.562,1.562-4.095,0-5.657
|
||||
l-6.05-6.05L32.783,19.555l-6.05-6.05c-1.562-1.562-4.095-1.562-5.657,0C19.515,15.067,19.515,17.6,21.077,19.162z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 749 B |
46
mod/dashboard/app/images/logo.svg
Normal file
46
mod/dashboard/app/images/logo.svg
Normal file
@ -0,0 +1,46 @@
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
preserveAspectRatio="xMinYMin" viewBox="0 0 792 306" enable-background="new 0 0 792 306" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<path fill="#53A3DA" d="M136.168,45.527C76.898,45.527,28.689,93.739,28.689,153c0,59.265,48.209,107.474,107.479,107.474
|
||||
c59.252,0,107.465-48.209,107.465-107.474C243.633,93.739,195.42,45.527,136.168,45.527z"/>
|
||||
<path fill="#F1606D" d="M136.168,55.389c-17.283,0-31.941,27.645-37.235,66.069c-0.169,1.236-0.333,2.487-0.478,3.746
|
||||
c-0.723,6.047-1.213,12.335-1.458,18.808c-0.117,2.962-0.175,5.956-0.175,8.988c0,3.029,0.058,6.029,0.175,8.985
|
||||
c0.245,6.472,0.735,12.764,1.458,18.811c8.104,1.049,16.769,1.761,25.807,2.099c3.907,0.146,7.872,0.233,11.907,0.233
|
||||
c4.023,0,8-0.088,11.895-0.233c9.049-0.338,17.708-1.05,25.819-2.099c0.892-0.114,1.77-0.239,2.659-0.368
|
||||
c33.754-4.74,57.235-15.232,57.235-27.428C233.776,99.088,190.071,55.389,136.168,55.389z"/>
|
||||
<path fill="#FFFFFF" d="M176.541,125.569c-0.979-1.428-2.029-2.796-3.148-4.11c-8.956-10.557-22.297-17.265-37.224-17.265
|
||||
c-4.839,0-9.148,7.407-11.907,18.909c-1.096,4.586-1.947,9.819-2.495,15.498c-0.432,4.551-0.665,9.391-0.665,14.399
|
||||
s0.233,9.849,0.665,14.396c4.554,0.432,9.387,0.664,14.402,0.664c5.009,0,9.842-0.232,14.396-0.664
|
||||
c10.011-0.95,18.653-2.875,24.775-5.411c6.046-2.501,9.624-5.615,9.624-8.985C184.963,142.832,181.858,133.388,176.541,125.569z"
|
||||
/>
|
||||
</g>
|
||||
<g>
|
||||
<path fill="#231F20" d="M344.891,100.053c12.585,0,22.816,6.138,29.262,13.062l-10.064,11.326
|
||||
c-5.353-5.192-11.175-8.495-19.041-8.495c-16.839,0-28.953,14.16-28.953,37.291c0,23.448,11.169,37.608,28.32,37.608
|
||||
c9.128,0,15.895-3.775,21.717-10.228l10.067,11.169c-8.335,9.598-19.038,14.95-32.099,14.95c-26.119,0-46.731-18.88-46.731-53.025
|
||||
C297.37,120.036,318.454,100.053,344.891,100.053z"/>
|
||||
<path fill="#231F20" d="M416.961,125.701c19.352,0,36.822,14.793,36.822,40.597c0,25.647-17.471,40.439-36.822,40.439
|
||||
c-19.197,0-36.66-14.792-36.66-40.439C380.301,140.494,397.764,125.701,416.961,125.701z M416.961,191.945
|
||||
c11.33,0,18.25-10.228,18.25-25.647c0-15.577-6.92-25.804-18.25-25.804s-18.094,10.227-18.094,25.804
|
||||
C398.867,181.717,405.631,191.945,416.961,191.945z"/>
|
||||
<path fill="#231F20" d="M459.771,127.589h14.943l1.26,13.688h0.629c5.506-10.07,13.691-15.577,21.871-15.577
|
||||
c3.938,0,6.455,0.472,8.811,1.574l-3.148,15.734c-2.67-0.784-4.717-1.257-8.018-1.257c-6.139,0-13.539,4.245-18.256,15.893v47.203
|
||||
h-18.092V127.589z"/>
|
||||
<path fill="#231F20" d="M541.121,125.701c20.928,0,31.941,15.107,31.941,36.667c0,3.458-0.314,6.604-0.787,8.495h-49.09
|
||||
c1.57,14.003,10.379,21.869,22.811,21.869c6.613,0,12.273-2.041,17.941-5.662l6.135,11.326
|
||||
c-7.395,4.878-16.676,8.341-26.432,8.341c-21.404,0-38.08-14.95-38.08-40.439C505.561,141.12,523.023,125.701,541.121,125.701z
|
||||
M557.326,159.376c0-12.277-5.189-19.671-15.732-19.671c-9.125,0-16.996,6.768-18.57,19.671H557.326z"/>
|
||||
<path fill="#F1606D" d="M600.602,152.607c0-32.729,17.785-53.344,42.799-53.344c24.863,0,42.641,20.615,42.641,53.344
|
||||
c0,32.889-17.777,54.13-42.641,54.13C618.387,206.737,600.602,185.496,600.602,152.607z M678.49,152.607
|
||||
c0-28.639-14.158-46.731-35.09-46.731c-21.084,0-35.248,18.093-35.248,46.731c0,28.796,14.164,47.521,35.248,47.521
|
||||
C664.332,200.128,678.49,181.403,678.49,152.607z"/>
|
||||
<path fill="#53A4D9" d="M699.738,186.125c7.557,8.495,18.412,14.003,30.529,14.003c15.732,0,25.807-8.499,25.807-20.767
|
||||
c0-12.904-8.494-17.154-18.723-21.717l-15.736-7.082c-8.969-3.936-20.934-10.385-20.934-25.808
|
||||
c0-14.947,12.904-25.492,30.059-25.492c12.588,0,22.658,5.665,28.949,12.435l-4.244,4.878c-5.982-6.452-14.32-10.7-24.705-10.7
|
||||
c-13.691,0-22.816,7.239-22.816,18.565c0,11.962,10.385,16.521,17.936,19.985l15.738,6.921
|
||||
c11.486,5.195,21.713,11.647,21.713,27.539s-13.061,27.851-33.201,27.851c-15.107,0-26.75-6.451-34.932-15.576L699.738,186.125z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 4.1 KiB |
@ -1,8 +1,6 @@
|
||||
<!doctype html>
|
||||
<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
|
||||
<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
|
||||
<!--[if IE 8]> <html class="no-js lt-ie9"> <![endif]-->
|
||||
<!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]-->
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="no-js" ng-app="etcdControlPanel" ng-controller="RootCtrl">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
@ -11,124 +9,56 @@
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<link href="http://fonts.googleapis.com/css?family=Source+Sans+Pro:200,300,400,400italic,600,700,900" rel="stylesheet" type="text/css">
|
||||
<link href="http://fonts.googleapis.com/css?family=Source+Code+Pro:400,500,600,700" rel="stylesheet" type="text/css">
|
||||
<style>
|
||||
body {
|
||||
padding: 30px;
|
||||
margin: 0px;
|
||||
}
|
||||
h1 {
|
||||
font-family: "Source Sans Pro", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
font-weight: 400;
|
||||
margin: 0px 0px 20px 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
iframe {
|
||||
border: none;
|
||||
}
|
||||
<!-- build:css styles/styles.css -->
|
||||
<link rel="stylesheet" href="styles/bootstrap.css">
|
||||
<link rel="stylesheet" href="styles/main.css">
|
||||
<link rel="stylesheet" href="styles/browser.css">
|
||||
<link rel="stylesheet" href="styles/stats.css">
|
||||
<link rel="stylesheet" href="styles/etcd-widgets.css">
|
||||
<!-- endbuild -->
|
||||
</head>
|
||||
|
||||
a {
|
||||
color: #1e6ec1;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
iframe {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
iframe + iframe {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
#footer {
|
||||
width: 100%;
|
||||
font-family: "Source Sans Pro", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
#coreos-logo {
|
||||
margin: 10px auto 0 auto;
|
||||
height: 30px;
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
#coreos-logo svg {
|
||||
fill: #999;
|
||||
max-width: 100px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#powered-by {
|
||||
font-size: 12px;
|
||||
color: #333;
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
line-height: 190%;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<body>
|
||||
<h1>etcd Dashboard</h1>
|
||||
<iframe src="stats.html" style="width: 100%; height: 400px;"></iframe>
|
||||
<iframe src="browser.html" style="width: 100%; height: 400px;"></iframe>
|
||||
|
||||
<div id="view-container" ng-view></div>
|
||||
|
||||
<div id="footer">
|
||||
<div id="powered-by">Powered by <a href="https://github.com/coreos/etcd">etcd</a></div>
|
||||
<div id="coreos-logo">
|
||||
<a href="http://coreos.com">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
preserveAspectRatio="xMinYMin" viewBox="0 0 792 306" enable-background="new 0 0 792 306" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<path fill="#53A3DA" d="M136.168,45.527C76.898,45.527,28.689,93.739,28.689,153c0,59.265,48.209,107.474,107.479,107.474
|
||||
c59.252,0,107.465-48.209,107.465-107.474C243.633,93.739,195.42,45.527,136.168,45.527z"/>
|
||||
<path fill="#F1606D" d="M136.168,55.389c-17.283,0-31.941,27.645-37.235,66.069c-0.169,1.236-0.333,2.487-0.478,3.746
|
||||
c-0.723,6.047-1.213,12.335-1.458,18.808c-0.117,2.962-0.175,5.956-0.175,8.988c0,3.029,0.058,6.029,0.175,8.985
|
||||
c0.245,6.472,0.735,12.764,1.458,18.811c8.104,1.049,16.769,1.761,25.807,2.099c3.907,0.146,7.872,0.233,11.907,0.233
|
||||
c4.023,0,8-0.088,11.895-0.233c9.049-0.338,17.708-1.05,25.819-2.099c0.892-0.114,1.77-0.239,2.659-0.368
|
||||
c33.754-4.74,57.235-15.232,57.235-27.428C233.776,99.088,190.071,55.389,136.168,55.389z"/>
|
||||
<path fill="#FFFFFF" d="M176.541,125.569c-0.979-1.428-2.029-2.796-3.148-4.11c-8.956-10.557-22.297-17.265-37.224-17.265
|
||||
c-4.839,0-9.148,7.407-11.907,18.909c-1.096,4.586-1.947,9.819-2.495,15.498c-0.432,4.551-0.665,9.391-0.665,14.399
|
||||
s0.233,9.849,0.665,14.396c4.554,0.432,9.387,0.664,14.402,0.664c5.009,0,9.842-0.232,14.396-0.664
|
||||
c10.011-0.95,18.653-2.875,24.775-5.411c6.046-2.501,9.624-5.615,9.624-8.985C184.963,142.832,181.858,133.388,176.541,125.569z"
|
||||
/>
|
||||
</g>
|
||||
<g>
|
||||
<path fill="#231F20" d="M344.891,100.053c12.585,0,22.816,6.138,29.262,13.062l-10.064,11.326
|
||||
c-5.353-5.192-11.175-8.495-19.041-8.495c-16.839,0-28.953,14.16-28.953,37.291c0,23.448,11.169,37.608,28.32,37.608
|
||||
c9.128,0,15.895-3.775,21.717-10.228l10.067,11.169c-8.335,9.598-19.038,14.95-32.099,14.95c-26.119,0-46.731-18.88-46.731-53.025
|
||||
C297.37,120.036,318.454,100.053,344.891,100.053z"/>
|
||||
<path fill="#231F20" d="M416.961,125.701c19.352,0,36.822,14.793,36.822,40.597c0,25.647-17.471,40.439-36.822,40.439
|
||||
c-19.197,0-36.66-14.792-36.66-40.439C380.301,140.494,397.764,125.701,416.961,125.701z M416.961,191.945
|
||||
c11.33,0,18.25-10.228,18.25-25.647c0-15.577-6.92-25.804-18.25-25.804s-18.094,10.227-18.094,25.804
|
||||
C398.867,181.717,405.631,191.945,416.961,191.945z"/>
|
||||
<path fill="#231F20" d="M459.771,127.589h14.943l1.26,13.688h0.629c5.506-10.07,13.691-15.577,21.871-15.577
|
||||
c3.938,0,6.455,0.472,8.811,1.574l-3.148,15.734c-2.67-0.784-4.717-1.257-8.018-1.257c-6.139,0-13.539,4.245-18.256,15.893v47.203
|
||||
h-18.092V127.589z"/>
|
||||
<path fill="#231F20" d="M541.121,125.701c20.928,0,31.941,15.107,31.941,36.667c0,3.458-0.314,6.604-0.787,8.495h-49.09
|
||||
c1.57,14.003,10.379,21.869,22.811,21.869c6.613,0,12.273-2.041,17.941-5.662l6.135,11.326
|
||||
c-7.395,4.878-16.676,8.341-26.432,8.341c-21.404,0-38.08-14.95-38.08-40.439C505.561,141.12,523.023,125.701,541.121,125.701z
|
||||
M557.326,159.376c0-12.277-5.189-19.671-15.732-19.671c-9.125,0-16.996,6.768-18.57,19.671H557.326z"/>
|
||||
<path fill="#F1606D" d="M600.602,152.607c0-32.729,17.785-53.344,42.799-53.344c24.863,0,42.641,20.615,42.641,53.344
|
||||
c0,32.889-17.777,54.13-42.641,54.13C618.387,206.737,600.602,185.496,600.602,152.607z M678.49,152.607
|
||||
c0-28.639-14.158-46.731-35.09-46.731c-21.084,0-35.248,18.093-35.248,46.731c0,28.796,14.164,47.521,35.248,47.521
|
||||
C664.332,200.128,678.49,181.403,678.49,152.607z"/>
|
||||
<path fill="#53A4D9" d="M699.738,186.125c7.557,8.495,18.412,14.003,30.529,14.003c15.732,0,25.807-8.499,25.807-20.767
|
||||
c0-12.904-8.494-17.154-18.723-21.717l-15.736-7.082c-8.969-3.936-20.934-10.385-20.934-25.808
|
||||
c0-14.947,12.904-25.492,30.059-25.492c12.588,0,22.658,5.665,28.949,12.435l-4.244,4.878c-5.982-6.452-14.32-10.7-24.705-10.7
|
||||
c-13.691,0-22.816,7.239-22.816,18.565c0,11.962,10.385,16.521,17.936,19.985l15.738,6.921
|
||||
c11.486,5.195,21.713,11.647,21.713,27.539s-13.061,27.851-33.201,27.851c-15.107,0-26.75-6.451-34.932-15.576L699.738,186.125z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<div id="powered-by" class="text-center">Powered by <a href="https://github.com/coreos/etcd" tabindex="-1">etcd</a></div>
|
||||
<div id="coreos-logo">
|
||||
<a href="http://coreos.com" tabindex="-1"><img src="images/logo.svg"/></a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
<!-- build:js scripts/modules.js -->
|
||||
<script src="bower_components/jquery/jquery.js"></script>
|
||||
<script src="bower_components/angular/angular.js"></script>
|
||||
<script src="bower_components/angular-resource/angular-resource.js"></script>
|
||||
<script src="bower_components/angular-route/angular-route.js"></script>
|
||||
<script src="bower_components/angular-cookies/angular-cookies.js"></script>
|
||||
<script src="bower_components/angular-sanitize/angular-sanitize.js"></script>
|
||||
<script src="bower_components/d3/d3.js"></script>
|
||||
<script src="bower_components/underscore/underscore.js"></script>
|
||||
<script src="bower_components/underscore.string/lib/underscore.string.js"></script>
|
||||
<script src="bower_components/moment/moment.js"></script>
|
||||
<script src="scripts/vega.js"></script>
|
||||
<script src="scripts/ng-time-relative.min.js"></script>
|
||||
<!-- endbuild -->
|
||||
|
||||
<!-- build:js scripts/app.js -->
|
||||
<script src="scripts/app.js"></script>
|
||||
<script src="scripts/controllers/root.js"></script>
|
||||
<script src="scripts/directives.js"></script>
|
||||
<script src="scripts/shims.js"></script>
|
||||
<script src="scripts/controllers/home.js"></script>
|
||||
<script src="scripts/controllers/browser.js"></script>
|
||||
<script src="scripts/common/services/etcd.js"></script>
|
||||
<script src="scripts/common/services/prefix-url.js"></script>
|
||||
<script src="scripts/common/directives/highlight.js"></script>
|
||||
<script src="scripts/common/directives/enter.js"></script>
|
||||
<script src="scripts/common/services/etcd.js"></script>
|
||||
<script src="scripts/controllers/stats.js"></script>
|
||||
<!-- endbuild -->
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
43
mod/dashboard/app/scripts/app.js
Normal file
43
mod/dashboard/app/scripts/app.js
Normal file
@ -0,0 +1,43 @@
|
||||
'use strict';
|
||||
|
||||
var app = angular.module('etcdControlPanel', [
|
||||
'ngRoute',
|
||||
'ngResource',
|
||||
'etcd',
|
||||
'etcdDirectives',
|
||||
'timeRelative',
|
||||
'underscore',
|
||||
'jquery',
|
||||
'moment',
|
||||
'vg'
|
||||
]);
|
||||
|
||||
app.constant('urlPrefix', '/mod/dashboard');
|
||||
app.constant('keyPrefix', '/v2/keys/');
|
||||
|
||||
app.config(function($routeProvider, $locationProvider, urlPrefix) {
|
||||
|
||||
function prefixUrl(url) {
|
||||
return urlPrefix + url;
|
||||
}
|
||||
|
||||
$locationProvider.html5Mode(true);
|
||||
|
||||
$routeProvider
|
||||
.when(prefixUrl('/'), {
|
||||
controller: 'HomeCtrl',
|
||||
templateUrl: prefixUrl('/views/home.html')
|
||||
})
|
||||
.when(prefixUrl('/stats'), {
|
||||
controller: 'StatsCtrl',
|
||||
templateUrl: prefixUrl('/views/stats.html')
|
||||
})
|
||||
.when(prefixUrl('/browser'), {
|
||||
controller: 'BrowserCtrl',
|
||||
templateUrl: prefixUrl('/views/browser.html')
|
||||
})
|
||||
.otherwise({
|
||||
templateUrl: prefixUrl('/404.html')
|
||||
});
|
||||
|
||||
});
|
16
mod/dashboard/app/scripts/common/directives/enter.js
Normal file
16
mod/dashboard/app/scripts/common/directives/enter.js
Normal file
@ -0,0 +1,16 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('etcdControlPanel')
|
||||
.directive('ngEnter', function() {
|
||||
return function(scope, element, attrs) {
|
||||
element.bind('keydown keypress', function(event) {
|
||||
if(event.which === 13) {
|
||||
scope.$apply(function(){
|
||||
scope.$eval(attrs.ngEnter);
|
||||
});
|
||||
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
19
mod/dashboard/app/scripts/common/directives/highlight.js
Normal file
19
mod/dashboard/app/scripts/common/directives/highlight.js
Normal file
@ -0,0 +1,19 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('etcdControlPanel')
|
||||
.directive('highlight', function(keyPrefix) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
highlightBase: '=',
|
||||
highlightCurrent: '='
|
||||
},
|
||||
link: function(scope, element, attrs) {
|
||||
var base = _.str.strRight(scope.highlightBase, keyPrefix),
|
||||
current = _.str.trim(scope.highlightCurrent, '/');
|
||||
if (base === current) {
|
||||
element.parent().parent().addClass('etcd-selected');
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
@ -88,7 +88,9 @@ angular.module('etcd', [])
|
||||
return newStat('leader').get().then(function(response) {
|
||||
return newKey('/_etcd/machines/' + response.data.leader).get().then(function(response) {
|
||||
// TODO: do something better here p.s. I hate javascript
|
||||
var data = JSON.parse('{"' + decodeURI(response.data.value.replace(/&/g, "\",\"").replace(/=/g,"\":\"")) + '"}');
|
||||
var data = decodeURIComponent(response.data.node.value);
|
||||
data = data.replace(/&/g, "\",\"").replace(/=/g,"\":\"");
|
||||
data = JSON.parse('{"' + data + '"}');
|
||||
return data.etcd;
|
||||
});
|
||||
});
|
||||
|
10
mod/dashboard/app/scripts/common/services/prefix-url.js
Normal file
10
mod/dashboard/app/scripts/common/services/prefix-url.js
Normal file
@ -0,0 +1,10 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('etcdControlPanel')
|
||||
.factory('prefixUrl', function(urlPrefix) {
|
||||
|
||||
return function(url) {
|
||||
return urlPrefix + url;
|
||||
}
|
||||
|
||||
});
|
@ -1,33 +1,39 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('etcdBrowser', ['ngRoute', 'etcd', 'timeRelative'])
|
||||
|
||||
.constant('keyPrefix', '/v2/keys/')
|
||||
|
||||
.config(['$routeProvider', 'keyPrefix', function ($routeProvider, keyPrefix) {
|
||||
//read localstorage
|
||||
var previousPath = localStorage.getItem('etcd_path');
|
||||
|
||||
$routeProvider
|
||||
.when('/', {
|
||||
redirectTo: keyPrefix
|
||||
})
|
||||
.otherwise({
|
||||
templateUrl: 'views/browser.html',
|
||||
controller: 'MainCtrl'
|
||||
});
|
||||
}])
|
||||
|
||||
.controller('MainCtrl', ['$scope', '$location', 'EtcdV2', 'keyPrefix', function ($scope, $location, EtcdV2, keyPrefix) {
|
||||
angular.module('etcdControlPanel')
|
||||
.controller('BrowserCtrl', function ($scope, $window, EtcdV2, keyPrefix, $, _, moment) {
|
||||
$scope.save = 'etcd-save-hide';
|
||||
$scope.preview = 'etcd-preview-hide';
|
||||
$scope.enableBack = true;
|
||||
$scope.writingNew = false;
|
||||
$scope.key = null;
|
||||
$scope.list = [];
|
||||
|
||||
// etcdPath is the path to the key that is currenly being looked at.
|
||||
$scope.etcdPath = $location.path();
|
||||
$scope.etcdPath = keyPrefix;
|
||||
$scope.inputPath = keyPrefix;
|
||||
|
||||
$scope.$watch('etcdPath', function() {
|
||||
$scope.resetInputPath = function() {
|
||||
$scope.inputPath = $scope.etcdPath;
|
||||
};
|
||||
|
||||
$scope.setActiveKey = function(key) {
|
||||
$scope.etcdPath = keyPrefix + _.str.trim(key, '/');
|
||||
$scope.resetInputPath();
|
||||
};
|
||||
|
||||
$scope.stripPrefix = function(path) {
|
||||
return _.str.strRight(path, keyPrefix);
|
||||
};
|
||||
|
||||
$scope.onEnter = function() {
|
||||
var path = $scope.stripPrefix($scope.inputPath);
|
||||
if (path !== '') {
|
||||
$scope.setActiveKey(path);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.updateCurrentKey = function() {
|
||||
function etcdPathKey() {
|
||||
return pathKey($scope.etcdPath);
|
||||
}
|
||||
@ -39,17 +45,17 @@ angular.module('etcdBrowser', ['ngRoute', 'etcd', 'timeRelative'])
|
||||
}
|
||||
return parts[1];
|
||||
}
|
||||
|
||||
// Notify everyone of the update
|
||||
localStorage.setItem('etcdPath', $scope.etcdPath);
|
||||
$scope.enableBack = true;
|
||||
//disable back button if at root (/v2/keys/)
|
||||
if($scope.etcdPath === keyPrefix) {
|
||||
if ($scope.etcdPath === keyPrefix) {
|
||||
$scope.enableBack = false;
|
||||
}
|
||||
|
||||
$scope.key = EtcdV2.getKey(etcdPathKey($scope.etcdPath));
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$watch('etcdPath', $scope.updateCurrentKey);
|
||||
|
||||
$scope.$watch('key', function() {
|
||||
if ($scope.writingNew === true) {
|
||||
@ -60,13 +66,13 @@ angular.module('etcdBrowser', ['ngRoute', 'etcd', 'timeRelative'])
|
||||
$('#etcd-browse-error').hide();
|
||||
// Looking at a directory if we got an array
|
||||
if (data.dir === true) {
|
||||
$scope.list = data.kvs;
|
||||
$scope.list = data.node.nodes;
|
||||
$scope.preview = 'etcd-preview-hide';
|
||||
} else {
|
||||
$scope.singleValue = data.value;
|
||||
$scope.singleValue = data.node.value;
|
||||
$scope.preview = 'etcd-preview-reveal';
|
||||
$scope.key.getParent().get().success(function(data) {
|
||||
$scope.list = data.kvs;
|
||||
$scope.list = data.node.nodes;
|
||||
});
|
||||
}
|
||||
$scope.previewMessage = 'No key selected.';
|
||||
@ -79,20 +85,18 @@ angular.module('etcdBrowser', ['ngRoute', 'etcd', 'timeRelative'])
|
||||
//back button click
|
||||
$scope.back = function() {
|
||||
$scope.etcdPath = $scope.key.getParent().path();
|
||||
$scope.syncLocation();
|
||||
$scope.resetInputPath();
|
||||
$scope.preview = 'etcd-preview-hide';
|
||||
$scope.writingNew = false;
|
||||
};
|
||||
|
||||
$scope.syncLocation = function() {
|
||||
$location.path($scope.etcdPath);
|
||||
};
|
||||
|
||||
$scope.showSave = function() {
|
||||
$scope.save = 'etcd-save-reveal';
|
||||
};
|
||||
|
||||
$scope.saveData = function() {
|
||||
$scope.setActiveKey($scope.stripPrefix($scope.inputPath));
|
||||
$scope.updateCurrentKey();
|
||||
// TODO: fixup etcd to allow for empty values
|
||||
$scope.key.set($scope.singleValue || ' ').then(function(response) {
|
||||
$scope.save = 'etcd-save-hide';
|
||||
@ -100,11 +104,13 @@ angular.module('etcdBrowser', ['ngRoute', 'etcd', 'timeRelative'])
|
||||
$scope.back();
|
||||
$scope.writingNew = false;
|
||||
}, function (response) {
|
||||
$scope.showSaveError(data.message);
|
||||
$scope.showSaveError(response.message);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.deleteKey = function() {
|
||||
$scope.deleteKey = function(key) {
|
||||
$scope.setActiveKey(key);
|
||||
$scope.updateCurrentKey();
|
||||
$scope.key.deleteKey().then(function(response) {
|
||||
//TODO: remove loader
|
||||
$scope.save = 'etcd-save-hide';
|
||||
@ -136,56 +142,15 @@ angular.module('etcdBrowser', ['ngRoute', 'etcd', 'timeRelative'])
|
||||
};
|
||||
|
||||
$scope.getHeight = function() {
|
||||
return $(window).height();
|
||||
return $($window).height();
|
||||
};
|
||||
$scope.$watch($scope.getHeight, function() {
|
||||
$('.etcd-body').css('height', $scope.getHeight()-45);
|
||||
});
|
||||
window.onresize = function(){
|
||||
|
||||
//$scope.$watch($scope.getHeight, function() {
|
||||
////$('.etcd-container.etcd-browser etcd-body').css('height', $scope.getHeight()-45);
|
||||
//});
|
||||
|
||||
$window.onresize = function(){
|
||||
$scope.$apply();
|
||||
};
|
||||
|
||||
}])
|
||||
|
||||
.directive('ngEnter', function() {
|
||||
return function(scope, element, attrs) {
|
||||
element.bind('keydown keypress', function(event) {
|
||||
if(event.which === 13) {
|
||||
scope.$apply(function(){
|
||||
scope.$eval(attrs.ngEnter);
|
||||
});
|
||||
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
};
|
||||
})
|
||||
|
||||
.directive('highlight', function() {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function(scope, element, attrs) {
|
||||
if('#' + scope.etcdPath === attrs.href) {
|
||||
element.parent().parent().addClass('etcd-selected');
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
moment.lang('en', {
|
||||
relativeTime : {
|
||||
future: 'Expires in %s',
|
||||
past: 'Expired %s ago',
|
||||
s: 'seconds',
|
||||
m: 'a minute',
|
||||
mm: '%d minutes',
|
||||
h: 'an hour',
|
||||
hh: '%d hours',
|
||||
d: 'a day',
|
||||
dd: '%d days',
|
||||
M: 'a month',
|
||||
MM: '%d months',
|
||||
y: 'a year',
|
||||
yy: '%d years'
|
||||
}
|
||||
});
|
||||
|
3
mod/dashboard/app/scripts/controllers/home.js
Normal file
3
mod/dashboard/app/scripts/controllers/home.js
Normal file
@ -0,0 +1,3 @@
|
||||
angular.module('etcdControlPanel')
|
||||
.controller('HomeCtrl', function($scope) {
|
||||
});
|
9
mod/dashboard/app/scripts/controllers/root.js
Normal file
9
mod/dashboard/app/scripts/controllers/root.js
Normal file
@ -0,0 +1,9 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('etcdControlPanel')
|
||||
.controller('RootCtrl', function($rootScope, prefixUrl) {
|
||||
|
||||
// Expose prefixUrl() function to all.
|
||||
$rootScope.prefixUrl = prefixUrl;
|
||||
|
||||
});
|
@ -1,20 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('etcdStats', ['ngRoute', 'etcd'])
|
||||
|
||||
.config(['$routeProvider', function ($routeProvider) {
|
||||
$routeProvider
|
||||
.when('/', {
|
||||
templateUrl: 'views/stats.html',
|
||||
controller: 'StatsCtrl'
|
||||
})
|
||||
.otherwise({
|
||||
templateUrl: 'views/stats.html',
|
||||
controller: 'StatsCtrl'
|
||||
});
|
||||
}])
|
||||
|
||||
.controller('StatsCtrl', ['$scope', 'EtcdV2', 'statsVega', function ($scope, EtcdV2, statsVega) {
|
||||
angular.module('etcdControlPanel')
|
||||
.controller('StatsCtrl', function ($scope, $rootScope, $interval, EtcdV2, statsVega, vg) {
|
||||
$scope.graphContainer = '#latency';
|
||||
$scope.graphVisibility = 'etcd-graph-show';
|
||||
$scope.tableVisibility = 'etcd-table-hide';
|
||||
@ -42,10 +30,14 @@ angular.module('etcdStats', ['ngRoute', 'etcd'])
|
||||
});
|
||||
//sort array so peers don't jump when output
|
||||
$scope.peers.sort(function(a, b){
|
||||
if(a.name < b.name) return -1;
|
||||
if(a.name > b.name) return 1;
|
||||
if(a.name < b.name) {
|
||||
return -1;
|
||||
}
|
||||
if(a.name > b.name) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
});
|
||||
drawGraph();
|
||||
});
|
||||
}
|
||||
@ -86,10 +78,10 @@ angular.module('etcdStats', ['ngRoute', 'etcd'])
|
||||
$scope.getWidth = function() {
|
||||
return $(window).width();
|
||||
};
|
||||
$scope.$watch($scope.getHeight, function() {
|
||||
$('.etcd-body').css('height', $scope.getHeight()-5);
|
||||
readStats();
|
||||
});
|
||||
//$scope.$watch($scope.getHeight, function() {
|
||||
////$('.etcd-container.etcd-stats .etcd-body').css('height', $scope.getHeight()-5);
|
||||
//readStats();
|
||||
//});
|
||||
$scope.$watch($scope.getWidth, function() {
|
||||
readStats();
|
||||
});
|
||||
@ -97,12 +89,31 @@ angular.module('etcdStats', ['ngRoute', 'etcd'])
|
||||
$scope.$apply();
|
||||
};
|
||||
|
||||
// Update the graphs live
|
||||
setInterval(function() {
|
||||
readStats();
|
||||
$scope.$apply();
|
||||
}, 500);
|
||||
}])
|
||||
$scope.pollPromise = null;
|
||||
|
||||
$scope.startPolling = function() {
|
||||
// Update the graphs live
|
||||
if ($scope.pollPromise) {
|
||||
return;
|
||||
}
|
||||
$scope.pollPromise = $interval(function() {
|
||||
readStats();
|
||||
}, 500);
|
||||
};
|
||||
|
||||
$scope.stopPolling = function() {
|
||||
$interval.cancel($scope.pollPromise);
|
||||
$scope.pollPromise = null;
|
||||
};
|
||||
|
||||
// Stop polling when navigating away from a view with this controller.
|
||||
$rootScope.$on('$routeChangeStart', function () {
|
||||
$scope.stopPolling();
|
||||
});
|
||||
|
||||
$scope.startPolling();
|
||||
|
||||
})
|
||||
|
||||
|
||||
/* statsVega returns the vega configuration for the stats dashboard */
|
||||
|
3
mod/dashboard/app/scripts/directives.js
Normal file
3
mod/dashboard/app/scripts/directives.js
Normal file
@ -0,0 +1,3 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('etcdDirectives', []);
|
36
mod/dashboard/app/scripts/shims.js
Normal file
36
mod/dashboard/app/scripts/shims.js
Normal file
@ -0,0 +1,36 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('underscore', []).factory('_', function($window) {
|
||||
return $window._;
|
||||
});
|
||||
|
||||
angular.module('jquery', []).factory('$', function($window) {
|
||||
return $window.$;
|
||||
});
|
||||
|
||||
angular.module('vg', []).factory('vg', function($window) {
|
||||
return $window.vg;
|
||||
});
|
||||
|
||||
angular.module('moment', []).factory('moment', function($window) {
|
||||
|
||||
$window.moment.lang('en', {
|
||||
relativeTime : {
|
||||
future: 'Expires in %s',
|
||||
past: 'Expired %s ago',
|
||||
s: 'seconds',
|
||||
m: 'a minute',
|
||||
mm: '%d minutes',
|
||||
h: 'an hour',
|
||||
hh: '%d hours',
|
||||
d: 'a day',
|
||||
dd: '%d days',
|
||||
M: 'a month',
|
||||
MM: '%d months',
|
||||
y: 'a year',
|
||||
yy: '%d years'
|
||||
}
|
||||
});
|
||||
|
||||
return $window.moment;
|
||||
});
|
@ -1,50 +0,0 @@
|
||||
<!doctype html>
|
||||
<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
|
||||
<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
|
||||
<!--[if IE 8]> <html class="no-js lt-ie9"> <![endif]-->
|
||||
<!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<title>etcd Browser</title>
|
||||
<meta name="description" content="">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<!-- Place favicon.ico and apple-touch-icon.png in the root directory -->
|
||||
|
||||
<!-- build:css(.tmp) styles/main.css -->
|
||||
<link rel="stylesheet" href="styles/etcd-widgets.css">
|
||||
<link href="http://fonts.googleapis.com/css?family=Source+Sans+Pro:200,300,400,400italic,600,700,900" rel="stylesheet" type="text/css">
|
||||
<link href="http://fonts.googleapis.com/css?family=Source+Code+Pro:400,500,600,700" rel="stylesheet" type="text/css">
|
||||
<!-- endbuild -->
|
||||
</head>
|
||||
<body ng-app="etcdStats">
|
||||
<!--[if lt IE 7]>
|
||||
<p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
|
||||
<![endif]-->
|
||||
|
||||
<!--[if lt IE 9]>
|
||||
<script src="bower_components/es5-shim/es5-shim.js"></script>
|
||||
<script src="bower_components/json3/lib/json3.min.js"></script>
|
||||
<![endif]-->
|
||||
|
||||
<!-- Add your site or application content here -->
|
||||
<div id="etd_stats" ng-view="etcd">
|
||||
</div>
|
||||
<!-- build:js scripts/stats-modules.js -->
|
||||
<script src="bower_components/jquery/jquery.js"></script>
|
||||
<script src="bower_components/angular/angular.js"></script>
|
||||
<script src="bower_components/angular-resource/angular-resource.js"></script>
|
||||
<script src="bower_components/angular-route/angular-route.js"></script>
|
||||
<script src="bower_components/angular-cookies/angular-cookies.js"></script>
|
||||
<script src="bower_components/angular-sanitize/angular-sanitize.js"></script>
|
||||
<script src="bower_components/d3/d3.js"></script>
|
||||
<script src="bower_components/underscore/underscore.js"></script>
|
||||
<!-- endbuild -->
|
||||
|
||||
<!-- build:js({.tmp,app}) scripts/stats-scripts.js -->
|
||||
<script src="scripts/vega.js"></script>
|
||||
<script src="scripts/common/services/etcd.js"></script>
|
||||
<script src="scripts/controllers/stats.js"></script>
|
||||
<!-- endbuild -->
|
||||
</body>
|
||||
</html>
|
186
mod/dashboard/app/styles/browser.css
Normal file
186
mod/dashboard/app/styles/browser.css
Normal file
@ -0,0 +1,186 @@
|
||||
.etcd-container.etcd-browser {
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
}
|
||||
|
||||
.home-container .etcd-container.etcd-browser {
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.etcd-container.etcd-browser .etcd-header {
|
||||
height: 37px;
|
||||
}
|
||||
|
||||
.etcd-container.etcd-browser.etcd-preview-reveal .etcd-back {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.etcd-container.etcd-browser.etcd-preview-hide .etcd-back {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.etcd-container.etcd-browser.etcd-preview-reveal .etcd-add {
|
||||
}
|
||||
|
||||
.etcd-container.etcd-browser.etcd-preview-hide .etcd-add {
|
||||
}
|
||||
|
||||
.etcd-container.etcd-browser .etcd-header .etcd-browser-path {
|
||||
position: absolute;
|
||||
left: 72px;
|
||||
right: 0px;
|
||||
top: 0;
|
||||
margin: 6px 5px 6px 5px;
|
||||
}
|
||||
|
||||
.etcd-container.etcd-browser .etcd-header .etcd-browser-path input {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
-webkit-box-sizing: border-box;
|
||||
}
|
||||
|
||||
.etcd-container.etcd-browser .etcd-header .etcd-save {
|
||||
position: absolute;
|
||||
width: 54px;
|
||||
right: -55px;
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
.etcd-container.etcd-browser.etcd-save-reveal .etcd-header .etcd-save {
|
||||
right: 7px;
|
||||
}
|
||||
|
||||
.etcd-container.etcd-browser.etcd-save-reveal .etcd-header .etcd-browser-path {
|
||||
right: 62px;
|
||||
}
|
||||
|
||||
.etcd-container.etcd-browser.etcd-save-hide .etcd-header .etcd-save {
|
||||
right: -55px;
|
||||
}
|
||||
|
||||
.etcd-container.etcd-browser.etcd-save-hide .etcd-header .etcd-browser-path {
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
.etcd-container.etcd-browser .etcd-preview {
|
||||
position: absolute;
|
||||
left: 100%;
|
||||
min-height: 100%;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
top: 0px;
|
||||
box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
background-color: #fff;
|
||||
width: 100%;
|
||||
border-left: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.etcd-container.etcd-browser .etcd-preview pre, .etcd-container.etcd-browser .etcd-preview textarea {
|
||||
padding: 20px 20px 20px 20px;
|
||||
margin: 0px;
|
||||
font-family: Consolas, "Liberation Mono", Courier, monospace;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
white-space: pre-wrap;
|
||||
position: absolute;
|
||||
font-size: 13px;
|
||||
border: 1px;
|
||||
outline: none;
|
||||
box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
}
|
||||
|
||||
.etcd-container.etcd-browser.etcd-preview-reveal .etcd-preview pre, .etcd-container.etcd-browser.etcd-preview-reveal .etcd-preview textarea {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.etcd-container.etcd-browser .etcd-preview .etcd-empty {
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.etcd-container.etcd-browser.etcd-preview-reveal .etcd-empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.etcd-container.etcd-browser .etcd-preview .etcd-empty-message {
|
||||
margin-top: 25%;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* Single Column Positioning */
|
||||
@media (max-width: 700px) {
|
||||
.etcd-container.etcd-browser .etcd-list {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.etcd-container.etcd-browser.etcd-preview-reveal .etcd-list {
|
||||
left: -100%;
|
||||
transition-property: all;
|
||||
transition-duration: 250ms;
|
||||
transition-timing-function: ease-in-out;
|
||||
}
|
||||
|
||||
.etcd-container.etcd-browser.etcd-preview-hide .etcd-list {
|
||||
left: 0%;
|
||||
transition-property: all;
|
||||
transition-duration: 250ms;
|
||||
transition-timing-function: ease-in-out;
|
||||
}
|
||||
|
||||
.etcd-container.etcd-browser .etcd-preview {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.etcd-container.etcd-browser.etcd-preview-reveal .etcd-preview { left: -1px;
|
||||
transition-property: all;
|
||||
transition-duration: 250ms;
|
||||
transition-timing-function: ease-in-out;
|
||||
}
|
||||
|
||||
.etcd-container.etcd-browser.etcd-preview-hide .etcd-preview {
|
||||
left: 100%;
|
||||
transition-property: all;
|
||||
transition-duration: 250ms;
|
||||
transition-timing-function: ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Double Column Positioning */
|
||||
@media (min-width: 700px) {
|
||||
.etcd-container.etcd-browser .etcd-list {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.etcd-container.etcd-browser .etcd-preview {
|
||||
left: 50%;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.etcd-container.etcd-browser.etcd-preview-reveal .etcd-preview {
|
||||
left: 50%; /* does nothing */
|
||||
}
|
||||
|
||||
.etcd-container.etcd-browser.etcd-preview-reveal .etcd-preview .etcd-empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.etcd-container.etcd-browser.etcd-preview-hide .etcd-preview {
|
||||
left: 50%; /* does nothing */
|
||||
}
|
||||
|
||||
.etcd-container.etcd-browser.etcd-preview-hide .etcd-preview .etcd-empty {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.etcd-container.etcd-browser.etcd-preview-hide .etcd-preview pre, .etcd-container.etcd-browser.etcd-preview-hide .etcd-preview textarea {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,22 +1,56 @@
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #fafafa;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
color: #333;
|
||||
background: #fafafa;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
color: #333;
|
||||
padding: 30px;
|
||||
margin: 0px;
|
||||
height: 100%;
|
||||
}
|
||||
h1 {
|
||||
font-family: "Source Sans Pro", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
font-weight: 400;
|
||||
margin: 0px 0px 20px 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.hero-unit {
|
||||
margin: 50px auto 0 auto;
|
||||
width: 300px;
|
||||
font-size: 18px;
|
||||
font-weight: 200;
|
||||
line-height: 30px;
|
||||
background-color: #eee;
|
||||
border-radius: 6px;
|
||||
padding: 60px;
|
||||
a {
|
||||
color: #1e6ec1;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.hero-unit h1 {
|
||||
font-size: 60px;
|
||||
line-height: 1;
|
||||
letter-spacing: -1px;
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#footer {
|
||||
width: 100%;
|
||||
font-family: "Source Sans Pro", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
#coreos-logo {
|
||||
margin: 10px auto 0 auto;
|
||||
height: 30px;
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
#coreos-logo svg {
|
||||
fill: #999;
|
||||
MAX-WIDTH: 100PX;
|
||||
DISPLAY: INLINE-BLOCK;
|
||||
VERTICAL-ALIGN: MIDDLE;
|
||||
}
|
||||
|
||||
#POWERED-BY {
|
||||
FONT-SIZE: 12PX;
|
||||
COLOR: #333;
|
||||
WIDTH: 100%;
|
||||
DISPLAY: INLINE-BLOCK;
|
||||
VERTICAL-ALIGN: MIDDLE;
|
||||
LINE-HEIGHT: 190%;
|
||||
TEXT-ALIGN: CENTER;
|
||||
}
|
||||
|
144
mod/dashboard/app/styles/stats.css
Normal file
144
mod/dashboard/app/styles/stats.css
Normal file
@ -0,0 +1,144 @@
|
||||
.etcd-container.etcd-stats {
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
}
|
||||
|
||||
.home-container .etcd-container.etcd-stats {
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.etcd-container.etcd-stats h2 {
|
||||
margin-top: -7px;
|
||||
}
|
||||
|
||||
.etcd-container.etcd-stats table .etcd-latency {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.etcd-container.etcd-stats .etcd-list {
|
||||
position: absolute;
|
||||
left: 100%;
|
||||
min-height: 100%;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
top: 0px;
|
||||
box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
background-color: #fff;
|
||||
width: 100%;
|
||||
border-left: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.etcd-container.etcd-stats .etcd-list .etcd-square {
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
display: inline-block;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.etcd-container.etcd-stats .etcd-list .etcd-square-red {
|
||||
background-color: #c40022;
|
||||
}
|
||||
|
||||
.etcd-container.etcd-stats .etcd-list .etcd-square-orange {
|
||||
background-color: #FFC000;
|
||||
}
|
||||
|
||||
.etcd-container.etcd-stats .etcd-list .etcd-square-green {
|
||||
background-color: #00DB24;
|
||||
}
|
||||
|
||||
.etcd-container.etcd-stats .etcd-list .etcd-peer-type {
|
||||
color: #999;
|
||||
padding-left: 3px;
|
||||
font-size: 13px;
|
||||
line-height: 100%;
|
||||
}
|
||||
|
||||
.etcd-container.etcd-stats .etcd-list .etcd-latency-value {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.etcd-container.etcd-stats .etcd-graph {
|
||||
box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.etcd-container.etcd-stats .etcd-graph .etcd-graph-container {
|
||||
position: absolute;
|
||||
top: 60px;
|
||||
bottom: 20px;
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
}
|
||||
|
||||
|
||||
/* Single Column Positioning */
|
||||
@media (max-width: 700px) {
|
||||
.etcd-container.etcd-stats .etcd-list {
|
||||
width: 100%;
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.etcd-container.etcd-stats .etcd-graph {
|
||||
left: 0%;
|
||||
}
|
||||
|
||||
.etcd-container.etcd-stats.etcd-table-reveal .etcd-graph {
|
||||
left: -100%;
|
||||
transition-property: all;
|
||||
transition-duration: 250ms;
|
||||
transition-timing-function: ease-in-out;
|
||||
}
|
||||
.etcd-container.etcd-stats.etcd-table-hide .etcd-graph {
|
||||
left: 0%;
|
||||
transition-property: all;
|
||||
transition-duration: 250ms;
|
||||
transition-timing-function: ease-in-out;
|
||||
|
||||
}
|
||||
.etcd-container.etcd-stats.etcd-table-hide .etcd-format-selector .etcd-selector-graph svg * {
|
||||
fill: #428bca;
|
||||
}
|
||||
|
||||
.etcd-container.etcd-stats.etcd-table-hide .etcd-list {
|
||||
left: 100%;
|
||||
transition-property: all;
|
||||
transition-duration: 250ms;
|
||||
transition-timing-function: ease-in-out;
|
||||
}
|
||||
.etcd-container.etcd-stats.etcd-table-reveal .etcd-list {
|
||||
left: 0%;
|
||||
transition-property: all;
|
||||
transition-duration: 250ms;
|
||||
transition-timing-function: ease-in-out;
|
||||
}
|
||||
.etcd-container.etcd-stats.etcd-table-reveal .etcd-format-selector .etcd-selector-table svg * {
|
||||
fill: #428bca;
|
||||
}
|
||||
}
|
||||
|
||||
/* Double Column Positioning */
|
||||
@media (min-width: 700px) {
|
||||
.etcd-container.etcd-stats .etcd-list {
|
||||
width: 50%;
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
.etcd-container.etcd-stats .etcd-graph {
|
||||
left: 0%;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.etcd-container.etcd-stats .etcd-format-selector {
|
||||
display: none;
|
||||
}
|
||||
}
|
@ -1,99 +1,73 @@
|
||||
<div class="etcd-container etcd-browser {{columns}} {{preview}} {{save}}">
|
||||
<!--
|
||||
<div class="etcd-popover etcd-popover-error">
|
||||
<div class="etcd-popover-notch"></div>
|
||||
<div class="etcd-popover-content">
|
||||
Overwrite this value?
|
||||
</div>
|
||||
<div class="etcd-popover-confirm">
|
||||
<button class="etcd-button etcd-button-small etcd-button-primary etcd-confirm">Overwrite</button>
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
<div class="etcd-popover etcd-popover-error" id="etcd-save-error">
|
||||
<div class="etcd-popover-notch"></div>
|
||||
<div class="etcd-popover-content">
|
||||
Error:
|
||||
</div>
|
||||
</div>
|
||||
<div class="etcd-popover etcd-popover-error" id="etcd-browse-error">
|
||||
<div class="etcd-popover-notch"></div>
|
||||
<div class="etcd-popover-content">
|
||||
Error:
|
||||
</div>
|
||||
</div>
|
||||
<div class="etcd-header solid">
|
||||
<a class="etcd-back" ng-click="back()" ng-class="{false:'etcd-disabled'}[enableBack]">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
preserveAspectRatio="xMinYMin" viewBox="0 0 73.356 61" enable-background="new 0 0 73.356 61" xml:space="preserve">
|
||||
<path d="M5.27,33.226l22.428,22.428c1.562,1.562,4.095,1.562,5.657,0c1.562-1.562,1.562-4.095,0-5.657L17.77,34.413h48.514
|
||||
c2.209,0,4-1.791,4-4s-1.791-4-4-4H17.749l15.604-15.582c1.563-1.561,1.565-4.094,0.004-5.657C32.576,4.391,31.552,4,30.527,4
|
||||
c-1.023,0-2.046,0.39-2.827,1.169L5.272,27.567c-0.751,0.75-1.173,1.768-1.173,2.829C4.098,31.458,4.52,32.476,5.27,33.226z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a class="etcd-add">
|
||||
<div ng-controller="BrowserCtrl" class="etcd-container etcd-browser {{columns}} {{preview}} {{save}}">
|
||||
|
||||
<svg version="1.1" ng-click="add()" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
preserveAspectRatio="xMinYMin" viewBox="0 0 72.556 61" enable-background="new 0 0 72.556 61" xml:space="preserve">
|
||||
<path d="M34.521,8v11.088v23v10.737c0,2.209,1.791,4,4,4c2.209,0,4-1.791,4-4V42.067V19.109V8c0-2.209-1.791-4-4-4
|
||||
C36.312,4,34.521,5.791,34.521,8z"/>
|
||||
<path d="M16.109,34.412h11.088h23h10.737c2.209,0,4-1.791,4-4c0-2.209-1.791-4-4-4H50.175H27.217H16.109c-2.209,0-4,1.791-4,4
|
||||
C12.109,32.621,13.9,34.412,16.109,34.412z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<div class="etcd-browser-path">
|
||||
<input type="text" ng-model="etcdPath" ng-enter="syncLocation()" tabindex="888" />
|
||||
</div>
|
||||
<button class="etcd-button etcd-button-small etcd-button-primary etcd-save" ng-click="saveData()">Save</button>
|
||||
<div class="etcd-popover etcd-popover-error" id="etcd-save-error">
|
||||
<div class="etcd-popover-notch"></div>
|
||||
<div class="etcd-popover-content">Error:</div>
|
||||
</div>
|
||||
|
||||
<div class="etcd-popover etcd-popover-error" id="etcd-browse-error">
|
||||
<div class="etcd-popover-notch"></div>
|
||||
<div class="etcd-popover-content">Error:</div>
|
||||
</div>
|
||||
|
||||
<div class="etcd-header solid">
|
||||
<a class="etcd-back" ng-click="back()" ng-class="{false:'etcd-disabled'}[enableBack]">
|
||||
<img src="images/back.svg"/>
|
||||
</a>
|
||||
<a class="etcd-add" ng-click="add()"><img src="images/add.svg"/></a>
|
||||
<div class="etcd-browser-path">
|
||||
<input type="text" ng-model="inputPath" ng-enter="onEnter()" tabindex="888" />
|
||||
</div>
|
||||
<div class="etcd-body">
|
||||
<div class="etcd-list">
|
||||
<table cellpadding="0" cellspacing="0">
|
||||
<thead>
|
||||
<td class="etcd-name-header">Name</td>
|
||||
<td class="etcd-ttl-header">TTL</td>
|
||||
<td class="etcd-actions-header"> </td>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="key in list | orderBy:'key'">
|
||||
<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>
|
||||
</td>
|
||||
<td>
|
||||
<div class="etcd-actions">
|
||||
<div class="etcd-delete" ng-switch on="!!key.dir">
|
||||
<svg ng-switch-when="false" ng-click="deleteKey()" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
x="0px" y="0px" preserveAspectRatio="xMinYMin" viewBox="0 0 76.143 61" enable-background="new 0 0 76.143 61" xml:space="preserve">
|
||||
<path d="M49.41,13.505l-6.035,6.035L27.112,35.803l-6.035,6.035c-1.562,1.562-1.562,4.095,0,5.657c1.562,1.562,4.095,1.562,5.657,0
|
||||
l6.05-6.05l16.234-16.234l6.05-6.05c1.562-1.562,1.562-4.095,0-5.657C53.505,11.943,50.972,11.943,49.41,13.505z"/>
|
||||
<path d="M21.077,19.162l6.035,6.035L43.375,41.46l6.035,6.035c1.562,1.562,4.095,1.562,5.657,0c1.562-1.562,1.562-4.095,0-5.657
|
||||
l-6.05-6.05L32.783,19.555l-6.05-6.05c-1.562-1.562-4.095-1.562-5.657,0C19.515,15.067,19.515,17.6,21.077,19.162z"/>
|
||||
</svg>
|
||||
<div ng-switch-when="true"></div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="etcd-preview">
|
||||
<textarea placeholder="Enter a key name above and the value here" ng-model="singleValue" tabindex="888" ng-change="showSave()">
|
||||
</textarea>
|
||||
<div class="etcd-empty">
|
||||
<div class="etcd-empty-message">{{preview_message}}</div>
|
||||
<button class="etcd-button etcd-button-small etcd-button-primary etcd-save" ng-click="saveData()">Save</button>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="etcd-body">
|
||||
|
||||
<div class="etcd-list">
|
||||
<table cellpadding="0" cellspacing="0">
|
||||
<thead>
|
||||
<td class="etcd-name-header">Name</td>
|
||||
<td class="etcd-ttl-header">TTL</td>
|
||||
<td class="etcd-actions-header"> </td>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="key in list | orderBy:'key'">
|
||||
<td>
|
||||
<highlight ng-class="{true:'directory'}[key.dir]" ng-click="setActiveKey(key.key)" highlight-base="etcdPath" highlight-current="key.key">{{key.key}}</highlight>
|
||||
</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>
|
||||
</td>
|
||||
<td>
|
||||
<div class="etcd-actions">
|
||||
<div ng-switch on="!!key.dir">
|
||||
<img class="etcd-delete" src="images/delete.svg" ng-switch-when="false" ng-click="deleteKey(key.key)" />
|
||||
<div ng-switch-when="true"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="etcd-dialog">
|
||||
<div class="etcd-dialog-message">
|
||||
Save and replicate this change?
|
||||
</div>
|
||||
<div class="etcd-dialog-buttons">
|
||||
<button class="etcd-button etcd-button-primary">Save Changes</button>
|
||||
<a href="javascript:void(0);">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="etcd-preview">
|
||||
<textarea placeholder="Enter a key name above and the value here" ng-model="singleValue" tabindex="888" ng-change="showSave()"></textarea>
|
||||
<div class="etcd-empty">
|
||||
<div class="etcd-empty-message">{{preview_message}}</div>
|
||||
</div>
|
||||
<div class="etcd-dialog">
|
||||
<div class="etcd-dialog-message">Save and replicate this change?</div>
|
||||
<div class="etcd-dialog-buttons">
|
||||
<button class="etcd-button etcd-button-primary">Save Changes</button>
|
||||
<a href="javascript:void(0);">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
4
mod/dashboard/app/views/home.html
Normal file
4
mod/dashboard/app/views/home.html
Normal file
@ -0,0 +1,4 @@
|
||||
<a href="stats" tabindex="-1">stats only</a>
|
||||
<div ng-include="prefixUrl('/views/stats.html')" class="home-container"></div>
|
||||
<a href="browser" tabindex="-1">browser only</a>
|
||||
<div ng-include="prefixUrl('/views/browser.html')" class="home-container"></div>
|
@ -1,4 +1,4 @@
|
||||
<div class="etcd-container etcd-stats {{columns}} {{tableVisibility}}">
|
||||
<div ng-controller="StatsCtrl" class="etcd-container etcd-stats {{columns}} {{tableVisibility}}">
|
||||
<div class="etcd-body">
|
||||
<div class="etcd-format-selector">
|
||||
<div class="etcd-selector-item etcd-selector-graph" ng-click="showGraph()">
|
||||
|
@ -12,11 +12,11 @@
|
||||
"angular-cookies": "~1.2.0-rc.2",
|
||||
"angular-sanitize": "~1.2.0-rc.2",
|
||||
"d3": "~3.3.6",
|
||||
"moment": "~2.3.0"
|
||||
"moment": "~2.3.0",
|
||||
"underscore.string": "~2.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"angular-mocks": "~1.2.0-rc.2",
|
||||
"angular-scenario": "~1.2.0-rc.2",
|
||||
"underscore": "~1.5.2"
|
||||
}
|
||||
}
|
||||
|
@ -8,10 +8,11 @@ npm install
|
||||
bower install
|
||||
grunt build
|
||||
|
||||
export GOPATH="${DIR}/../../"
|
||||
go get github.com/jteeuwen/go-bindata/...
|
||||
|
||||
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 -nomemcopy -pkg "resources" -toc -out resources/$file.go -prefix dist $i
|
||||
dirs=
|
||||
for i in `find dist -type d`; do
|
||||
dirs="${dirs} ${i}"
|
||||
done
|
||||
|
||||
${GOPATH}/bin/go-bindata -nomemcopy -pkg "resources" -o resources/resources.go -prefix dist ${dirs}
|
||||
|
@ -24,9 +24,9 @@ func memoryFileServer(w http.ResponseWriter, req *http.Request) {
|
||||
file = file + ".html"
|
||||
}
|
||||
upath = path.Join(dir, file)
|
||||
b, ok := resources.File("/" + upath)
|
||||
b, err := resources.Asset(upath)
|
||||
|
||||
if ok == false {
|
||||
if err != nil {
|
||||
http.Error(w, upath+": File not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
@ -11,19 +11,17 @@
|
||||
"grunt-contrib-compass": "~0.5.0",
|
||||
"grunt-contrib-jshint": "~0.6.0",
|
||||
"grunt-contrib-cssmin": "~0.6.0",
|
||||
"grunt-contrib-connect": "~0.3.0",
|
||||
"grunt-contrib-clean": "~0.5.0",
|
||||
"grunt-contrib-htmlmin": "~0.1.3",
|
||||
"grunt-contrib-imagemin": "~0.2.0",
|
||||
"grunt-contrib-watch": "~0.5.2",
|
||||
"grunt-autoprefixer": "~0.2.0",
|
||||
"grunt-usemin": "~0.1.11",
|
||||
"grunt-usemin": "~2.0.2",
|
||||
"grunt-svgmin": "~0.2.0",
|
||||
"grunt-rev": "~0.1.0",
|
||||
"grunt-open": "~0.2.0",
|
||||
"grunt-concurrent": "~0.3.0",
|
||||
"load-grunt-tasks": "~0.1.0",
|
||||
"connect-livereload": "~0.2.0",
|
||||
"grunt-google-cdn": "~0.2.0",
|
||||
"grunt-ngmin": "~0.0.2",
|
||||
"time-grunt": "~0.1.0",
|
||||
|
@ -1,11 +0,0 @@
|
||||
package resources
|
||||
|
||||
func File(name string) ([]byte, bool) {
|
||||
data, ok := go_bindata[name]
|
||||
|
||||
if ok == false {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return data(), true
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
package resources
|
||||
|
||||
// Global Table of Contents map. Generated by go-bindata.
|
||||
// After startup of the program, all generated data files will
|
||||
// put themselves in this map. The key is the full filename, as
|
||||
// supplied to go-bindata.
|
||||
var go_bindata = make(map[string]func() []byte)
|
@ -1,39 +0,0 @@
|
||||
package resources
|
||||
|
||||
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 {
|
||||
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 buf bytes.Buffer
|
||||
io.Copy(&buf, gz)
|
||||
gz.Close()
|
||||
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
|
||||
func init() {
|
||||
go_bindata["/browser.html"] = browser_html
|
||||
}
|
File diff suppressed because one or more lines are too long
161
mod/dashboard/resources/resources.go
Normal file
161
mod/dashboard/resources/resources.go
Normal file
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
@ -1,39 +0,0 @@
|
||||
package resources
|
||||
|
||||
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 {
|
||||
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 buf bytes.Buffer
|
||||
io.Copy(&buf, gz)
|
||||
gz.Close()
|
||||
|
||||
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
@ -1,39 +0,0 @@
|
||||
package resources
|
||||
|
||||
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\x14\x45\x53\x14\x28\x8a\x1e\x68\x8a\xb6\x88\xa5\x29\x95\xa4\xad\x75\x5c\xff\xf7\x0e\x45\xd9\x8d\x64\x79\xbb\x41\xf7\x92\x7d\x80\xe2\x70\xe6\x9b\x8f\xc3\x6f\x28\x2d\x73\x7e\x40\x54\x10\xad\x33\xcc\x0c\xcd\x7d\x5a\x4a\x43\xb8\x64\x0a\x35\x53\x6d\x88\xd1\xe8\x74\xa2\xa5\xd8\xef\xa4\x3e\x9f\xe1\xd9\x90\xb5\x60\xbf\x71\xcd\xd7\x5c\x70\x73\x3c\x9f\xf1\xea\x0d\x82\x9f\x1b\xac\x75\x99\x1f\xdb\xb5\xc1\xf5\x4d\xa9\x76\xc4\xf8\x9a\x09\x46\x4d\xa9\x3e\x73\x1d\x74\xbf\xf8\xf9\xdc\xb0\x1d\xea\x9a\xb6\x8a\x54\x05\x46\x72\xeb\x53\xc1\xe9\x63\x86\x75\x51\xd6\x3f\x58\xe3\xdb\x77\x3d\xdc\x06\x5b\x1f\xb6\xe8\xc0\x94\xe6\xa5\xcc\x70\x14\x44\x18\x3d\xed\x84\x84\x4c\x85\x31\xd5\x62\x3c\xae\xeb\x3a\xa8\x93\xa0\x54\xdb\x71\x1c\x86\xe1\x18\xfc\x5b\x97\xc5\x93\xe0\xf2\x71\xc8\x31\x4a\xd3\x74\xdc\xac\x82\x6b\x86\xc3\xea\x09\xa3\xa3\x1b\x6f\x08\x54\x8a\x69\xa6\x0e\xec\x5b\x5d\xc1\x06\x7e\x21\x86\x97\x19\x7e\xfa\xc0\xe5\xef\xf0\x8f\xd1\x81\xb3\xfa\x7d\x69\x41\x50\x88\x66\xf6\x2f\x08\xc3\x39\x46\x4c\xda\xda\xfb\x6b\x42\x1f\xb7\xaa\xdc\xcb\x3c\xc3\x92\xd5\xa8\xe7\x05\x3c\x17\xba\x22\x94\x65\xf8\x92\x67\xa8\x06\x15\x31\x05\xda\x70\x21\x32\xfc\x4d\xf4\x3d\xfc\xbe\xc7\x08\x00\x3f\x24\xa1\x0f\x40\x69\x44\xfd\x68\x16\xcc\xe2\xc8\x0b\xfd\xc4\x1a\x26\x5e\x94\x04\x93\xf9\xe4\x32\x73\x03\x0d\xbd\xd6\xcd\xad\x7a\x9d\xd5\x76\xb8\x49\xae\x9d\xdd\xef\xc4\xb4\xc0\xdf\xcd\xae\xd9\x92\x87\xc4\x9b\x34\xe8\x8e\x92\x77\xe1\xf6\x09\x01\x4d\x6f\x32\x0f\xe2\x24\xa5\x7e\x1a\x4c\xa3\x14\x68\x46\x76\x3e\xf5\xe7\xc1\x3c\x9a\x5d\x26\x6e\xb8\x21\xf0\x31\x0e\x83\xc9\x03\x90\x8e\x83\xf9\xec\x01\x70\xdb\x27\xda\x62\x79\x2e\xce\x6b\xb0\x2e\x13\x37\x7c\x4c\x9c\x8f\xcb\xee\x5d\x79\x7c\xc2\xe3\x81\x2a\x5b\xe9\xac\xde\x74\x95\x3d\x06\x69\xff\x0f\xb1\x37\x0d\xd8\x17\xfb\xaf\xd6\xf8\xf5\x8b\x7d\x9a\x06\x93\x74\x66\x87\x69\x98\x3c\x2b\xf8\x9e\xe7\x0b\x45\xaf\x80\x41\x43\xd8\xd1\xc5\xfd\x06\xa8\x79\x6e\x8a\x0c\x3b\x70\x8c\x0a\xc6\xb7\x85\x81\xaa\x25\x41\x14\x45\x83\x07\xdc\x45\x8c\xc1\xd1\x06\xbe\x32\x2c\x34\x41\x92\xc6\xaf\x00\xeb\xe4\xf8\x8c\x1a\xfb\xd3\xbe\x30\xdd\x4d\xdb\x83\x28\xe2\xd5\xcf\x0c\xde\x19\x3f\x12\xc3\x24\x3d\x2e\xc7\x60\x78\x5e\xdf\x0d\xcc\xbf\x6f\x1b\x8c\x38\x1c\xad\x70\xe1\x7d\xf4\x2f\x23\x28\xb8\x36\x77\xf9\xc1\xda\x00\xb9\xa6\xa1\x10\x65\x42\x54\x24\xcf\xb9\xdc\x36\x65\xb7\x73\xab\xa8\x76\xde\x8f\x29\x18\xc9\x07\x0a\x6c\xf2\x0e\x1b\x49\x76\xcc\xb7\xae\xb0\x47\xc7\xe1\x27\xb0\x2c\xc7\xe6\x05\xb1\xd7\x72\x5c\xcb\xda\x8f\x02\xcb\x2d\x8d\xa5\xb1\x2f\xde\x21\x78\x65\xef\x0c\xc5\x2a\x46\x40\x24\x95\x25\xc3\x25\xb2\xa3\x1e\x68\x95\x0b\x23\x08\xd1\x35\x37\xb4\x40\xf6\xfa\x38\x19\xb5\x67\x8b\x91\x68\x76\x34\xf2\xd0\x86\x08\xcd\x16\x68\xb4\x29\x85\x28\x6b\x30\x9d\xff\x70\x6b\x76\x9b\x28\xcb\x1a\xf8\xc0\x56\xe1\xcf\x3b\x39\x9a\x3c\xf6\x10\xaf\x89\xfc\xba\x60\x90\x4a\xb4\x55\x3b\x9d\xae\x18\xe7\xf3\x12\x4e\x44\x76\xaa\x64\x17\x7d\x73\xac\xa0\xdd\xdf\xba\x90\x77\xa0\x72\xf0\x5a\x0d\xdc\xb3\xcf\x24\xcd\xd9\x86\xec\x85\xe9\xa5\xbb\x0f\x31\x7c\x86\x6d\xd5\xfe\x23\x6b\xe7\xa6\xff\x6b\x4f\x14\x73\xb7\x39\x58\x17\xe8\x34\xfa\xcc\x0e\x8d\xc2\x98\x1c\x2d\x5c\x1d\x5b\x45\x04\x74\xaf\x14\x93\x06\x2d\x51\x3c\xf5\x50\xc7\xbf\x54\x44\x6e\xd9\xfd\x80\x59\xd8\x0b\x50\x2c\xbf\xe7\xbd\xca\xc0\x1d\x3e\xef\x5e\x52\xc8\x01\xe1\xfa\x07\x22\xf6\xec\x7a\x80\x7d\xf4\xbf\x91\xdc\xef\xd6\x4c\x2d\x22\x04\xdf\x95\x3b\xfd\xc5\xb5\x06\xab\xba\xe9\x87\x5b\xed\x83\xd1\xf6\xf7\xe0\x0d\xd2\x3e\xb6\xc3\x3f\x01\x00\x00\xff\xff\x0a\x0c\x99\x1a\x0e\x0b\x00\x00"
|
||||
|
||||
// views_stats_html returns raw, uncompressed file data.
|
||||
func views_stats_html() []byte {
|
||||
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 buf bytes.Buffer
|
||||
io.Copy(&buf, gz)
|
||||
gz.Close()
|
||||
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
|
||||
func init() {
|
||||
go_bindata["/views/stats.html"] = views_stats_html
|
||||
}
|
45
mod/leader/v2/delete_handler.go
Normal file
45
mod/leader/v2/delete_handler.go
Normal file
@ -0,0 +1,45 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
etcdErr "github.com/coreos/etcd/error"
|
||||
"github.com/coreos/etcd/third_party/github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// deleteHandler remove a given leader.
|
||||
func (h *handler) deleteHandler(w http.ResponseWriter, req *http.Request) error {
|
||||
vars := mux.Vars(req)
|
||||
name := req.FormValue("name")
|
||||
if name == "" {
|
||||
return etcdErr.NewError(etcdErr.EcodeNameRequired, "Delete", 0)
|
||||
}
|
||||
|
||||
// Proxy the request to the the lock service.
|
||||
u, err := url.Parse(fmt.Sprintf("%s/mod/v2/lock/%s", h.addr, vars["key"]))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
q := u.Query()
|
||||
q.Set("value", name)
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
r, err := http.NewRequest("DELETE", u.String(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Read from the leader lock.
|
||||
resp, err := h.client.Do(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
io.Copy(w, resp.Body)
|
||||
|
||||
return nil
|
||||
}
|
26
mod/leader/v2/get_handler.go
Normal file
26
mod/leader/v2/get_handler.go
Normal file
@ -0,0 +1,26 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/coreos/etcd/third_party/github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// getHandler retrieves the current leader.
|
||||
func (h *handler) getHandler(w http.ResponseWriter, req *http.Request) error {
|
||||
vars := mux.Vars(req)
|
||||
|
||||
// Proxy the request to the lock service.
|
||||
url := fmt.Sprintf("%s/mod/v2/lock/%s?field=value", h.addr, vars["key"])
|
||||
resp, err := h.client.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
io.Copy(w, resp.Body)
|
||||
return nil
|
||||
}
|
49
mod/leader/v2/handler.go
Normal file
49
mod/leader/v2/handler.go
Normal file
@ -0,0 +1,49 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
etcdErr "github.com/coreos/etcd/error"
|
||||
"github.com/coreos/etcd/third_party/github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// prefix is appended to the lock's prefix since the leader mod uses the lock mod.
|
||||
const prefix = "/_mod/leader"
|
||||
|
||||
// handler manages the leader HTTP request.
|
||||
type handler struct {
|
||||
*mux.Router
|
||||
client *http.Client
|
||||
transport *http.Transport
|
||||
addr string
|
||||
}
|
||||
|
||||
// NewHandler creates an HTTP handler that can be registered on a router.
|
||||
func NewHandler(addr string) http.Handler {
|
||||
transport := &http.Transport{DisableKeepAlives: false}
|
||||
h := &handler{
|
||||
Router: mux.NewRouter(),
|
||||
client: &http.Client{Transport: transport},
|
||||
transport: transport,
|
||||
addr: addr,
|
||||
}
|
||||
h.StrictSlash(false)
|
||||
h.handleFunc("/{key:.*}", h.getHandler).Methods("GET")
|
||||
h.handleFunc("/{key:.*}", h.setHandler).Methods("PUT")
|
||||
h.handleFunc("/{key:.*}", h.deleteHandler).Methods("DELETE")
|
||||
return h
|
||||
}
|
||||
|
||||
func (h *handler) handleFunc(path string, f func(http.ResponseWriter, *http.Request) error) *mux.Route {
|
||||
return h.Router.HandleFunc(path, func(w http.ResponseWriter, req *http.Request) {
|
||||
if err := f(w, req); err != nil {
|
||||
switch err := err.(type) {
|
||||
case *etcdErr.Error:
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
err.Write(w)
|
||||
default:
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
58
mod/leader/v2/set_handler.go
Normal file
58
mod/leader/v2/set_handler.go
Normal file
@ -0,0 +1,58 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
etcdErr "github.com/coreos/etcd/error"
|
||||
"github.com/coreos/etcd/third_party/github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// setHandler attempts to set the current leader.
|
||||
func (h *handler) setHandler(w http.ResponseWriter, req *http.Request) error {
|
||||
vars := mux.Vars(req)
|
||||
name := req.FormValue("name")
|
||||
if name == "" {
|
||||
return etcdErr.NewError(etcdErr.EcodeNameRequired, "Set", 0)
|
||||
}
|
||||
|
||||
// Proxy the request to the the lock service.
|
||||
u, err := url.Parse(fmt.Sprintf("%s/mod/v2/lock/%s", h.addr, vars["key"]))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
q := u.Query()
|
||||
q.Set("value", name)
|
||||
q.Set("ttl", req.FormValue("ttl"))
|
||||
q.Set("timeout", req.FormValue("timeout"))
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
r, err := http.NewRequest("POST", u.String(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Close request if this connection disconnects.
|
||||
closeNotifier, _ := w.(http.CloseNotifier)
|
||||
stopChan := make(chan bool)
|
||||
defer close(stopChan)
|
||||
go func() {
|
||||
select {
|
||||
case <-closeNotifier.CloseNotify():
|
||||
h.transport.CancelRequest(r)
|
||||
case <-stopChan:
|
||||
}
|
||||
}()
|
||||
|
||||
// Read from the leader lock.
|
||||
resp, err := h.client.Do(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
io.Copy(w, resp.Body)
|
||||
return nil
|
||||
}
|
85
mod/leader/v2/tests/mod_leader_test.go
Normal file
85
mod/leader/v2/tests/mod_leader_test.go
Normal file
@ -0,0 +1,85 @@
|
||||
package leader
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/etcd/server"
|
||||
"github.com/coreos/etcd/tests"
|
||||
"github.com/coreos/etcd/third_party/github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Ensure that a leader can be set and read.
|
||||
func TestModLeaderSet(t *testing.T) {
|
||||
tests.RunServer(func(s *server.Server) {
|
||||
// Set leader.
|
||||
body, status, err := testSetLeader(s, "foo", "xxx", 10)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, status, 200)
|
||||
assert.Equal(t, body, "2")
|
||||
|
||||
// Check that the leader is set.
|
||||
body, status, err = testGetLeader(s, "foo")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, status, 200)
|
||||
assert.Equal(t, body, "xxx")
|
||||
|
||||
// Delete leader.
|
||||
body, status, err = testDeleteLeader(s, "foo", "xxx")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, status, 200)
|
||||
assert.Equal(t, body, "")
|
||||
|
||||
// Check that the leader is removed.
|
||||
body, status, err = testGetLeader(s, "foo")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, status, 200)
|
||||
assert.Equal(t, body, "")
|
||||
})
|
||||
}
|
||||
|
||||
// Ensure that a leader can be renewed.
|
||||
func TestModLeaderRenew(t *testing.T) {
|
||||
tests.RunServer(func(s *server.Server) {
|
||||
// Set leader.
|
||||
body, status, err := testSetLeader(s, "foo", "xxx", 2)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, status, 200)
|
||||
assert.Equal(t, body, "2")
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
// Renew leader.
|
||||
body, status, err = testSetLeader(s, "foo", "xxx", 3)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, status, 200)
|
||||
assert.Equal(t, body, "2")
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// Check that the leader is set.
|
||||
body, status, err = testGetLeader(s, "foo")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, status, 200)
|
||||
assert.Equal(t, body, "xxx")
|
||||
})
|
||||
}
|
||||
|
||||
func testSetLeader(s *server.Server, key string, name string, ttl int) (string, int, error) {
|
||||
resp, err := tests.PutForm(fmt.Sprintf("%s/mod/v2/leader/%s?name=%s&ttl=%d", s.URL(), key, name, ttl), nil)
|
||||
ret := tests.ReadBody(resp)
|
||||
return string(ret), resp.StatusCode, err
|
||||
}
|
||||
|
||||
func testGetLeader(s *server.Server, key string) (string, int, error) {
|
||||
resp, err := tests.Get(fmt.Sprintf("%s/mod/v2/leader/%s", s.URL(), key))
|
||||
ret := tests.ReadBody(resp)
|
||||
return string(ret), resp.StatusCode, err
|
||||
}
|
||||
|
||||
func testDeleteLeader(s *server.Server, key string, name string) (string, int, error) {
|
||||
resp, err := tests.DeleteForm(fmt.Sprintf("%s/mod/v2/leader/%s?name=%s", s.URL(), key, name), nil)
|
||||
ret := tests.ReadBody(resp)
|
||||
return string(ret), resp.StatusCode, err
|
||||
}
|
@ -1,128 +1,181 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-etcd/etcd"
|
||||
"github.com/gorilla/mux"
|
||||
etcdErr "github.com/coreos/etcd/error"
|
||||
"github.com/coreos/etcd/third_party/github.com/coreos/go-etcd/etcd"
|
||||
"github.com/coreos/etcd/third_party/github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// acquireHandler attempts to acquire a lock on the given key.
|
||||
// The "key" parameter specifies the resource to lock.
|
||||
// The "value" parameter specifies a value to associate with the lock.
|
||||
// The "ttl" parameter specifies how long the lock will persist for.
|
||||
// The "timeout" parameter specifies how long the request should wait for the lock.
|
||||
func (h *handler) acquireHandler(w http.ResponseWriter, req *http.Request) {
|
||||
func (h *handler) acquireHandler(w http.ResponseWriter, req *http.Request) error {
|
||||
h.client.SyncCluster()
|
||||
|
||||
// Setup connection watcher.
|
||||
closeNotifier, _ := w.(http.CloseNotifier)
|
||||
closeChan := closeNotifier.CloseNotify()
|
||||
stopChan := make(chan bool)
|
||||
|
||||
// Parse "key" and "ttl" query parameters.
|
||||
// Parse the lock "key".
|
||||
vars := mux.Vars(req)
|
||||
keypath := path.Join(prefix, vars["key"])
|
||||
ttl, err := strconv.Atoi(req.FormValue("ttl"))
|
||||
if err != nil {
|
||||
http.Error(w, "invalid ttl: " + err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
value := req.FormValue("value")
|
||||
|
||||
// Parse "timeout" parameter.
|
||||
var timeout int
|
||||
if len(req.FormValue("timeout")) == 0 {
|
||||
var err error
|
||||
if req.FormValue("timeout") == "" {
|
||||
timeout = -1
|
||||
} else if timeout, err = strconv.Atoi(req.FormValue("timeout")); err != nil {
|
||||
http.Error(w, "invalid timeout: " + err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
return etcdErr.NewError(etcdErr.EcodeTimeoutNaN, "Acquire", 0)
|
||||
}
|
||||
timeout = timeout + 1
|
||||
|
||||
// Create an incrementing id for the lock.
|
||||
resp, err := h.client.AddChild(keypath, "-", uint64(ttl))
|
||||
// Parse TTL.
|
||||
ttl, err := strconv.Atoi(req.FormValue("ttl"))
|
||||
if err != nil {
|
||||
http.Error(w, "add lock index error: " + err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
return etcdErr.NewError(etcdErr.EcodeTTLNaN, "Acquire", 0)
|
||||
}
|
||||
|
||||
// If node exists then just watch it. Otherwise create the node and watch it.
|
||||
node, index, pos := h.findExistingNode(keypath, value)
|
||||
if index > 0 {
|
||||
if pos == 0 {
|
||||
// If lock is already acquired then update the TTL.
|
||||
h.client.Update(node.Key, node.Value, uint64(ttl))
|
||||
} else {
|
||||
// Otherwise watch until it becomes acquired (or errors).
|
||||
err = h.watch(keypath, index, nil)
|
||||
}
|
||||
} else {
|
||||
index, err = h.createNode(keypath, value, ttl, closeChan, stopChan)
|
||||
}
|
||||
|
||||
// Stop all goroutines.
|
||||
close(stopChan)
|
||||
|
||||
// Check for an error.
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write response.
|
||||
w.Write([]byte(strconv.Itoa(index)))
|
||||
return nil
|
||||
}
|
||||
|
||||
// createNode creates a new lock node and watches it until it is acquired or acquisition fails.
|
||||
func (h *handler) createNode(keypath string, value string, ttl int, closeChan <-chan bool, stopChan chan bool) (int, error) {
|
||||
// Default the value to "-" if it is blank.
|
||||
if len(value) == 0 {
|
||||
value = "-"
|
||||
}
|
||||
|
||||
// Create an incrementing id for the lock.
|
||||
resp, err := h.client.AddChild(keypath, value, uint64(ttl))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
indexpath := resp.Node.Key
|
||||
index, _ := strconv.Atoi(path.Base(indexpath))
|
||||
|
||||
// Keep updating TTL to make sure lock request is not expired before acquisition.
|
||||
stop := make(chan bool)
|
||||
go h.ttlKeepAlive(indexpath, ttl, stop)
|
||||
go h.ttlKeepAlive(indexpath, value, ttl, stopChan)
|
||||
|
||||
// Monitor for broken connection.
|
||||
// Watch until we acquire or fail.
|
||||
err = h.watch(keypath, index, closeChan)
|
||||
|
||||
// Check for connection disconnect before we write the lock index.
|
||||
if err != nil {
|
||||
select {
|
||||
case <-closeChan:
|
||||
err = errors.New("user interrupted")
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// Update TTL one last time if acquired. Otherwise delete.
|
||||
if err == nil {
|
||||
h.client.Update(indexpath, value, uint64(ttl))
|
||||
} else {
|
||||
h.client.Delete(indexpath, false)
|
||||
}
|
||||
|
||||
return index, err
|
||||
}
|
||||
|
||||
// findExistingNode search for a node on the lock with the given value.
|
||||
func (h *handler) findExistingNode(keypath string, value string) (*etcd.Node, int, int) {
|
||||
if len(value) > 0 {
|
||||
resp, err := h.client.Get(keypath, true, true)
|
||||
if err == nil {
|
||||
nodes := lockNodes{resp.Node.Nodes}
|
||||
if node, pos := nodes.FindByValue(value); node != nil {
|
||||
index, _ := strconv.Atoi(path.Base(node.Key))
|
||||
return node, index, pos
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, 0, 0
|
||||
}
|
||||
|
||||
// ttlKeepAlive continues to update a key's TTL until the stop channel is closed.
|
||||
func (h *handler) ttlKeepAlive(k string, value string, ttl int, stopChan chan bool) {
|
||||
for {
|
||||
select {
|
||||
case <-time.After(time.Duration(ttl/2) * time.Second):
|
||||
h.client.Update(k, value, uint64(ttl))
|
||||
case <-stopChan:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// watch continuously waits for a given lock index to be acquired or until lock fails.
|
||||
// Returns a boolean indicating success.
|
||||
func (h *handler) watch(keypath string, index int, closeChan <-chan bool) error {
|
||||
// Wrap close chan so we can pass it to Client.Watch().
|
||||
stopWatchChan := make(chan bool)
|
||||
go func() {
|
||||
select {
|
||||
case <-closeChan:
|
||||
stopWatchChan <- true
|
||||
case <-stop:
|
||||
// Stop watching for connection disconnect.
|
||||
case <-stopWatchChan:
|
||||
}
|
||||
}()
|
||||
defer close(stopWatchChan)
|
||||
|
||||
// Extract the lock index.
|
||||
index, _ := strconv.Atoi(path.Base(resp.Node.Key))
|
||||
|
||||
// Wait until we successfully get a lock or we get a failure.
|
||||
var success bool
|
||||
for {
|
||||
// Read all indices.
|
||||
resp, err = h.client.Get(keypath, true, true)
|
||||
// Read all nodes for the lock.
|
||||
resp, err := h.client.Get(keypath, true, true)
|
||||
if err != nil {
|
||||
http.Error(w, "lock children lookup error: " + err.Error(), http.StatusInternalServerError)
|
||||
break
|
||||
return fmt.Errorf("lock watch lookup error: %s", err.Error())
|
||||
}
|
||||
indices := extractResponseIndices(resp)
|
||||
waitIndex := resp.Node.ModifiedIndex
|
||||
prevIndex := findPrevIndex(indices, index)
|
||||
nodes := lockNodes{resp.Node.Nodes}
|
||||
prevIndex := nodes.PrevIndex(index)
|
||||
|
||||
// If there is no previous index then we have the lock.
|
||||
if prevIndex == 0 {
|
||||
success = true
|
||||
break
|
||||
return nil
|
||||
}
|
||||
|
||||
// Otherwise watch previous index until it's gone.
|
||||
// Watch previous index until it's gone.
|
||||
_, err = h.client.Watch(path.Join(keypath, strconv.Itoa(prevIndex)), waitIndex, false, nil, stopWatchChan)
|
||||
if err == etcd.ErrWatchStoppedByUser {
|
||||
break
|
||||
return fmt.Errorf("lock watch closed")
|
||||
} else if err != nil {
|
||||
http.Error(w, "lock watch error: " + err.Error(), http.StatusInternalServerError)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Check for connection disconnect before we write the lock index.
|
||||
select {
|
||||
case <-stopWatchChan:
|
||||
success = false
|
||||
default:
|
||||
}
|
||||
|
||||
// Stop the ttl keep-alive.
|
||||
close(stop)
|
||||
|
||||
if success {
|
||||
// Write lock index to response body if we acquire the lock.
|
||||
h.client.Update(indexpath, "-", uint64(ttl))
|
||||
w.Write([]byte(strconv.Itoa(index)))
|
||||
} else {
|
||||
// Make sure key is deleted if we couldn't acquire.
|
||||
h.client.Delete(indexpath, false)
|
||||
}
|
||||
}
|
||||
|
||||
// ttlKeepAlive continues to update a key's TTL until the stop channel is closed.
|
||||
func (h *handler) ttlKeepAlive(k string, ttl int, stop chan bool) {
|
||||
for {
|
||||
select {
|
||||
case <-time.After(time.Duration(ttl / 2) * time.Second):
|
||||
h.client.Update(k, "-", uint64(ttl))
|
||||
case <-stop:
|
||||
return
|
||||
return fmt.Errorf("lock watch error: %s", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,28 +3,43 @@ package v2
|
||||
import (
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
etcdErr "github.com/coreos/etcd/error"
|
||||
"github.com/coreos/etcd/third_party/github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// getIndexHandler retrieves the current lock index.
|
||||
func (h *handler) getIndexHandler(w http.ResponseWriter, req *http.Request) {
|
||||
// The "field" parameter specifies to read either the lock "index" or lock "value".
|
||||
func (h *handler) getIndexHandler(w http.ResponseWriter, req *http.Request) error {
|
||||
h.client.SyncCluster()
|
||||
|
||||
vars := mux.Vars(req)
|
||||
keypath := path.Join(prefix, vars["key"])
|
||||
field := req.FormValue("field")
|
||||
if len(field) == 0 {
|
||||
field = "value"
|
||||
}
|
||||
|
||||
// Read all indices.
|
||||
resp, err := h.client.Get(keypath, true, true)
|
||||
if err != nil {
|
||||
http.Error(w, "lock children lookup error: " + err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
return err
|
||||
}
|
||||
nodes := lockNodes{resp.Node.Nodes}
|
||||
|
||||
// Write out the requested field.
|
||||
if node := nodes.First(); node != nil {
|
||||
switch field {
|
||||
case "index":
|
||||
w.Write([]byte(path.Base(node.Key)))
|
||||
|
||||
case "value":
|
||||
w.Write([]byte(node.Value))
|
||||
|
||||
default:
|
||||
return etcdErr.NewError(etcdErr.EcodeInvalidField, "Get", 0)
|
||||
}
|
||||
}
|
||||
|
||||
// Write out the index of the last one to the response body.
|
||||
indices := extractResponseIndices(resp)
|
||||
if len(indices) > 0 {
|
||||
w.Write([]byte(strconv.Itoa(indices[0])))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user