You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jclouds.apache.org by na...@apache.org on 2015/09/15 23:55:42 UTC

jclouds-labs git commit: JCLOUDS-985: Add members API

Repository: jclouds-labs
Updated Branches:
  refs/heads/master a058e9fc9 -> 8b11430ac


JCLOUDS-985: Add members API


Project: http://git-wip-us.apache.org/repos/asf/jclouds-labs/repo
Commit: http://git-wip-us.apache.org/repos/asf/jclouds-labs/commit/8b11430a
Tree: http://git-wip-us.apache.org/repos/asf/jclouds-labs/tree/8b11430a
Diff: http://git-wip-us.apache.org/repos/asf/jclouds-labs/diff/8b11430a

Branch: refs/heads/master
Commit: 8b11430acdd4088309ff12bcebfb06fc6eb8e672
Parents: a058e9f
Author: Christopher Dancy <da...@pega.com>
Authored: Fri Aug 7 12:08:05 2015 -0400
Committer: Ignasi Barrera <na...@apache.org>
Committed: Tue Sep 15 23:50:00 2015 +0200

----------------------------------------------------------------------
 .../src/main/java/org/jclouds/etcd/EtcdApi.java |   6 +-
 .../java/org/jclouds/etcd/EtcdApiMetadata.java  |  18 +-
 .../jclouds/etcd/config/EtcdHttpApiModule.java  |  10 +-
 .../etcd/domain/members/CreateMember.java       |  47 ++++++
 .../org/jclouds/etcd/domain/members/Member.java |  49 ++++++
 .../jclouds/etcd/domain/statistics/Counts.java  |   2 +-
 .../etcd/domain/statistics/Follower.java        |   2 +-
 .../jclouds/etcd/domain/statistics/Latency.java |   2 +-
 .../jclouds/etcd/domain/statistics/Leader.java  |   2 +-
 .../etcd/domain/statistics/LeaderInfo.java      |   2 +-
 .../jclouds/etcd/domain/statistics/Self.java    |   2 +-
 .../jclouds/etcd/domain/statistics/Store.java   |   2 +-
 .../jclouds/etcd/fallbacks/EtcdFallbacks.java   |  38 +++++
 .../org/jclouds/etcd/features/MembersApi.java   |  74 ++++++++
 .../jclouds/etcd/features/MiscellaneousApi.java |   3 +
 .../jclouds/etcd/features/StatisticsApi.java    |  16 +-
 .../jclouds/etcd/handlers/EtcdErrorHandler.java |  87 ++++++++++
 .../etcd/features/MembersApiLiveTest.java       | 104 ++++++++++++
 .../etcd/features/MembersApiMockTest.java       | 168 +++++++++++++++++++
 .../etcd/features/MiscellaneousApiMockTest.java |  17 ++
 .../etcd/features/StatisticsApiLiveTest.java    |  45 ++++-
 etcd/src/test/resources/health-bad.json         |   1 +
 .../test/resources/members-add-existent.json    |   1 +
 .../resources/members-add-illegal-format.json   |   1 +
 .../resources/members-add-malformed-url.json    |   1 +
 etcd/src/test/resources/members-added.json      |   9 +
 .../resources/members-delete-nonexistent.json   |   1 +
 etcd/src/test/resources/members.json            |  24 +++
 28 files changed, 704 insertions(+), 30 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/8b11430a/etcd/src/main/java/org/jclouds/etcd/EtcdApi.java
----------------------------------------------------------------------
diff --git a/etcd/src/main/java/org/jclouds/etcd/EtcdApi.java b/etcd/src/main/java/org/jclouds/etcd/EtcdApi.java
index dc3e0cb..6573435 100644
--- a/etcd/src/main/java/org/jclouds/etcd/EtcdApi.java
+++ b/etcd/src/main/java/org/jclouds/etcd/EtcdApi.java
@@ -20,14 +20,18 @@ package org.jclouds.etcd;
 import java.io.Closeable;
 
 import org.jclouds.etcd.features.StatisticsApi;
+import org.jclouds.etcd.features.MembersApi;
 import org.jclouds.etcd.features.MiscellaneousApi;
 import org.jclouds.rest.annotations.Delegate;
 
 public interface EtcdApi extends Closeable {
 
    @Delegate
-   StatisticsApi statisticsApi();
+   MembersApi membersApi();
 
    @Delegate
    MiscellaneousApi miscellaneousApi();
+
+   @Delegate
+   StatisticsApi statisticsApi();
 }

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/8b11430a/etcd/src/main/java/org/jclouds/etcd/EtcdApiMetadata.java
----------------------------------------------------------------------
diff --git a/etcd/src/main/java/org/jclouds/etcd/EtcdApiMetadata.java b/etcd/src/main/java/org/jclouds/etcd/EtcdApiMetadata.java
index acec91a..7b30457 100644
--- a/etcd/src/main/java/org/jclouds/etcd/EtcdApiMetadata.java
+++ b/etcd/src/main/java/org/jclouds/etcd/EtcdApiMetadata.java
@@ -56,18 +56,12 @@ public class EtcdApiMetadata extends BaseHttpApiMetadata<EtcdApi> {
 
       protected Builder() {
          super(EtcdApi.class);
-         id("etcd").
-         name("Etcd API").
-         identityName("Optional Username").
-         credentialName("Optional Password").
-         defaultIdentity("N/A").
-         defaultCredential("N/A").
-         documentation(URI.create("https://github.com/coreos/etcd/blob/master/Documentation/api.md")).
-         version(API_VERSION).
-         buildVersion(BUILD_VERSION).
-         defaultEndpoint("http://127.0.0.1:2379").
-         defaultProperties(EtcdApiMetadata.defaultProperties()).
-         defaultModules(ImmutableSet.<Class<? extends Module>> of(EtcdHttpApiModule.class));
+         id("etcd").name("Etcd API").identityName("Optional Username").credentialName("Optional Password")
+               .defaultIdentity("N/A").defaultCredential("N/A")
+               .documentation(URI.create("https://github.com/coreos/etcd/blob/master/Documentation/api.md"))
+               .version(API_VERSION).buildVersion(BUILD_VERSION).defaultEndpoint("http://127.0.0.1:2379")
+               .defaultProperties(EtcdApiMetadata.defaultProperties())
+               .defaultModules(ImmutableSet.<Class<? extends Module>> of(EtcdHttpApiModule.class));
       }
 
       @Override

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/8b11430a/etcd/src/main/java/org/jclouds/etcd/config/EtcdHttpApiModule.java
----------------------------------------------------------------------
diff --git a/etcd/src/main/java/org/jclouds/etcd/config/EtcdHttpApiModule.java b/etcd/src/main/java/org/jclouds/etcd/config/EtcdHttpApiModule.java
index db492b4..f635387 100644
--- a/etcd/src/main/java/org/jclouds/etcd/config/EtcdHttpApiModule.java
+++ b/etcd/src/main/java/org/jclouds/etcd/config/EtcdHttpApiModule.java
@@ -18,15 +18,23 @@
 package org.jclouds.etcd.config;
 
 import org.jclouds.etcd.EtcdApi;
+import org.jclouds.http.HttpErrorHandler;
+import org.jclouds.http.annotation.ClientError;
+import org.jclouds.http.annotation.Redirection;
+import org.jclouds.http.annotation.ServerError;
 import org.jclouds.rest.ConfiguresHttpApi;
 import org.jclouds.rest.config.HttpApiModule;
 
+import org.jclouds.etcd.handlers.EtcdErrorHandler;
+
 @ConfiguresHttpApi
 public class EtcdHttpApiModule extends HttpApiModule<EtcdApi> {
 
    @Override
    protected void bindErrorHandlers() {
-
+      bind(HttpErrorHandler.class).annotatedWith(Redirection.class).to(EtcdErrorHandler.class);
+      bind(HttpErrorHandler.class).annotatedWith(ClientError.class).to(EtcdErrorHandler.class);
+      bind(HttpErrorHandler.class).annotatedWith(ServerError.class).to(EtcdErrorHandler.class);
    }
 
    protected void configure() {

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/8b11430a/etcd/src/main/java/org/jclouds/etcd/domain/members/CreateMember.java
----------------------------------------------------------------------
diff --git a/etcd/src/main/java/org/jclouds/etcd/domain/members/CreateMember.java b/etcd/src/main/java/org/jclouds/etcd/domain/members/CreateMember.java
new file mode 100644
index 0000000..025e8bd
--- /dev/null
+++ b/etcd/src/main/java/org/jclouds/etcd/domain/members/CreateMember.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.jclouds.etcd.domain.members;
+
+import java.util.List;
+
+import org.jclouds.javax.annotation.Nullable;
+import org.jclouds.json.SerializedNames;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+
+@AutoValue
+public abstract class CreateMember {
+
+   @Nullable
+   public abstract String name();
+
+   public abstract List<String> peerURLs();
+
+   public abstract List<String> clientURLs();
+
+   CreateMember() {
+   }
+
+   @SerializedNames({ "name", "peerURLs", "clientURLs" })
+   public static CreateMember create(String name, List<String> peerURLs, List<String> clientURLs) {
+      if (clientURLs == null)
+         clientURLs = ImmutableList.of();
+      return new AutoValue_CreateMember(name, ImmutableList.copyOf(peerURLs), ImmutableList.copyOf(clientURLs));
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/8b11430a/etcd/src/main/java/org/jclouds/etcd/domain/members/Member.java
----------------------------------------------------------------------
diff --git a/etcd/src/main/java/org/jclouds/etcd/domain/members/Member.java b/etcd/src/main/java/org/jclouds/etcd/domain/members/Member.java
new file mode 100644
index 0000000..cfeae6c
--- /dev/null
+++ b/etcd/src/main/java/org/jclouds/etcd/domain/members/Member.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.jclouds.etcd.domain.members;
+
+import java.util.List;
+
+import org.jclouds.javax.annotation.Nullable;
+import org.jclouds.json.SerializedNames;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+
+@AutoValue
+public abstract class Member {
+
+   public abstract String id();
+
+   @Nullable
+   public abstract String name();
+
+   public abstract List<String> peerURLs();
+
+   public abstract List<String> clientURLs();
+
+   Member() {
+   }
+
+   @SerializedNames({ "id", "name", "peerURLs", "clientURLs" })
+   private static Member create(String id, String name, List<String> peerURLs, List<String> clientURLs) {
+      if (clientURLs == null)
+         clientURLs = ImmutableList.of();
+      return new AutoValue_Member(id, name, ImmutableList.copyOf(peerURLs), ImmutableList.copyOf(clientURLs));
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/8b11430a/etcd/src/main/java/org/jclouds/etcd/domain/statistics/Counts.java
----------------------------------------------------------------------
diff --git a/etcd/src/main/java/org/jclouds/etcd/domain/statistics/Counts.java b/etcd/src/main/java/org/jclouds/etcd/domain/statistics/Counts.java
index e1d4fda..eabc6a9 100644
--- a/etcd/src/main/java/org/jclouds/etcd/domain/statistics/Counts.java
+++ b/etcd/src/main/java/org/jclouds/etcd/domain/statistics/Counts.java
@@ -32,7 +32,7 @@ public abstract class Counts {
    }
 
    @SerializedNames({ "fail", "success" })
-   public static Counts create(int fail, int success) {
+   private static Counts create(int fail, int success) {
       return new AutoValue_Counts(fail, success);
    }
 }

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/8b11430a/etcd/src/main/java/org/jclouds/etcd/domain/statistics/Follower.java
----------------------------------------------------------------------
diff --git a/etcd/src/main/java/org/jclouds/etcd/domain/statistics/Follower.java b/etcd/src/main/java/org/jclouds/etcd/domain/statistics/Follower.java
index a7204f2..05c333e 100644
--- a/etcd/src/main/java/org/jclouds/etcd/domain/statistics/Follower.java
+++ b/etcd/src/main/java/org/jclouds/etcd/domain/statistics/Follower.java
@@ -32,7 +32,7 @@ public abstract class Follower {
    }
 
    @SerializedNames({ "counts", "latency" })
-   public static Follower create(Counts counts, Latency latency) {
+   private static Follower create(Counts counts, Latency latency) {
       return new AutoValue_Follower(counts, latency);
    }
 }

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/8b11430a/etcd/src/main/java/org/jclouds/etcd/domain/statistics/Latency.java
----------------------------------------------------------------------
diff --git a/etcd/src/main/java/org/jclouds/etcd/domain/statistics/Latency.java b/etcd/src/main/java/org/jclouds/etcd/domain/statistics/Latency.java
index 20797b0..1002170 100644
--- a/etcd/src/main/java/org/jclouds/etcd/domain/statistics/Latency.java
+++ b/etcd/src/main/java/org/jclouds/etcd/domain/statistics/Latency.java
@@ -38,7 +38,7 @@ public abstract class Latency {
    }
 
    @SerializedNames({ "average", "current", "maximum", "minimum", "standardDeviation" })
-   public static Latency create(double average, double current, double maximum, double minimum,
+   private static Latency create(double average, double current, double maximum, double minimum,
          double standardDeviation) {
       return new AutoValue_Latency(average, current, maximum, minimum, standardDeviation);
    }

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/8b11430a/etcd/src/main/java/org/jclouds/etcd/domain/statistics/Leader.java
----------------------------------------------------------------------
diff --git a/etcd/src/main/java/org/jclouds/etcd/domain/statistics/Leader.java b/etcd/src/main/java/org/jclouds/etcd/domain/statistics/Leader.java
index b537c53..e3af2b0 100644
--- a/etcd/src/main/java/org/jclouds/etcd/domain/statistics/Leader.java
+++ b/etcd/src/main/java/org/jclouds/etcd/domain/statistics/Leader.java
@@ -35,7 +35,7 @@ public abstract class Leader {
    }
 
    @SerializedNames({ "leader", "followers" })
-   public static Leader create(String leader, Map<String, Follower> followers) {
+   private static Leader create(String leader, Map<String, Follower> followers) {
       return new AutoValue_Leader(leader, ImmutableMap.copyOf(followers));
    }
 }

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/8b11430a/etcd/src/main/java/org/jclouds/etcd/domain/statistics/LeaderInfo.java
----------------------------------------------------------------------
diff --git a/etcd/src/main/java/org/jclouds/etcd/domain/statistics/LeaderInfo.java b/etcd/src/main/java/org/jclouds/etcd/domain/statistics/LeaderInfo.java
index c220463..ec0aa67 100644
--- a/etcd/src/main/java/org/jclouds/etcd/domain/statistics/LeaderInfo.java
+++ b/etcd/src/main/java/org/jclouds/etcd/domain/statistics/LeaderInfo.java
@@ -34,7 +34,7 @@ public abstract class LeaderInfo {
    }
 
    @SerializedNames({ "leader", "startTime", "uptime" })
-   public static LeaderInfo create(String leader, String startTime, String uptime) {
+   private static LeaderInfo create(String leader, String startTime, String uptime) {
       return new AutoValue_LeaderInfo(leader, startTime, uptime);
    }
 }

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/8b11430a/etcd/src/main/java/org/jclouds/etcd/domain/statistics/Self.java
----------------------------------------------------------------------
diff --git a/etcd/src/main/java/org/jclouds/etcd/domain/statistics/Self.java b/etcd/src/main/java/org/jclouds/etcd/domain/statistics/Self.java
index bfe9bc2..a9d7e22 100644
--- a/etcd/src/main/java/org/jclouds/etcd/domain/statistics/Self.java
+++ b/etcd/src/main/java/org/jclouds/etcd/domain/statistics/Self.java
@@ -47,7 +47,7 @@ public abstract class Self {
 
    @SerializedNames({ "id", "leaderInfo", "name", "recvAppendRequestCnt", "sendAppendRequestCnt", "sendBandwidthRate",
          "sendPkgRate", "startTime", "state" })
-   public static Self create(String id, LeaderInfo leaderInfo, String name, double recvAppendRequestCnt,
+   private static Self create(String id, LeaderInfo leaderInfo, String name, double recvAppendRequestCnt,
          double sendAppendRequestCnt, double sendBandwidthRate, double sendPkgRate, String startTime, String state) {
       return new AutoValue_Self(id, leaderInfo, name, recvAppendRequestCnt, sendAppendRequestCnt, sendBandwidthRate,
             sendPkgRate, startTime, state);

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/8b11430a/etcd/src/main/java/org/jclouds/etcd/domain/statistics/Store.java
----------------------------------------------------------------------
diff --git a/etcd/src/main/java/org/jclouds/etcd/domain/statistics/Store.java b/etcd/src/main/java/org/jclouds/etcd/domain/statistics/Store.java
index 7bb78a3..7c468d1 100644
--- a/etcd/src/main/java/org/jclouds/etcd/domain/statistics/Store.java
+++ b/etcd/src/main/java/org/jclouds/etcd/domain/statistics/Store.java
@@ -58,7 +58,7 @@ public abstract class Store {
    @SerializedNames({ "compareAndSwapFail", "compareAndSwapSuccess", "createFail", "createSuccess", "deleteFail",
          "deleteSuccess", "expireCount", "getsFail", "getsSuccess", "setsFail", "setsSuccess", "updateFail",
          "updateSuccess", "watchers" })
-   public static Store create(int compareAndSwapFail, int compareAndSwapSuccess, int createFail, int createSuccess,
+   private static Store create(int compareAndSwapFail, int compareAndSwapSuccess, int createFail, int createSuccess,
          int deleteFail, int deleteSuccess, int expireCount, int getsFail, int getsSuccess, int setsFail,
          int setsSuccess, int updateFail, int updateSuccess, int watchers) {
       return new AutoValue_Store(compareAndSwapFail, compareAndSwapSuccess, createFail, createSuccess, deleteFail,

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/8b11430a/etcd/src/main/java/org/jclouds/etcd/fallbacks/EtcdFallbacks.java
----------------------------------------------------------------------
diff --git a/etcd/src/main/java/org/jclouds/etcd/fallbacks/EtcdFallbacks.java b/etcd/src/main/java/org/jclouds/etcd/fallbacks/EtcdFallbacks.java
new file mode 100644
index 0000000..95152a7
--- /dev/null
+++ b/etcd/src/main/java/org/jclouds/etcd/fallbacks/EtcdFallbacks.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jclouds.etcd.fallbacks;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Predicates.equalTo;
+import static com.google.common.base.Throwables.propagate;
+
+import static org.jclouds.http.HttpUtils.returnValueOnCodeOrNull;
+
+import org.jclouds.Fallback;
+
+public final class EtcdFallbacks {
+
+   public static final class FalseOn503 implements Fallback<Boolean> {
+      public Boolean createOrPropagate(Throwable t) throws Exception {
+         if (checkNotNull(t, "throwable") != null && t.getMessage().contains("{\"health\": \"false\"}")
+               && returnValueOnCodeOrNull(t, true, equalTo(503)) != null) {
+            return Boolean.FALSE;
+         }
+         throw propagate(t);
+      }
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/8b11430a/etcd/src/main/java/org/jclouds/etcd/features/MembersApi.java
----------------------------------------------------------------------
diff --git a/etcd/src/main/java/org/jclouds/etcd/features/MembersApi.java b/etcd/src/main/java/org/jclouds/etcd/features/MembersApi.java
new file mode 100644
index 0000000..655b748
--- /dev/null
+++ b/etcd/src/main/java/org/jclouds/etcd/features/MembersApi.java
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.jclouds.etcd.features;
+
+import java.util.List;
+
+import javax.inject.Named;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.core.MediaType;
+
+import org.jclouds.Fallbacks.FalseOnNotFoundOr404;
+import org.jclouds.etcd.domain.members.CreateMember;
+import org.jclouds.etcd.domain.members.Member;
+import org.jclouds.rest.ResourceAlreadyExistsException;
+import org.jclouds.rest.annotations.BinderParam;
+import org.jclouds.rest.annotations.Fallback;
+import org.jclouds.rest.annotations.SelectJson;
+import org.jclouds.rest.binders.BindToJsonPayload;
+
+@Consumes(MediaType.APPLICATION_JSON)
+@Path("/{jclouds.api-version}/members")
+public interface MembersApi {
+
+   /**
+    * @return list of members within cluster
+    */
+   @Named("members:list")
+   @SelectJson("members")
+   @GET
+   List<Member> list();
+
+   /**
+    * @param member
+    *           non-existing member to add to cluster
+    * @return newly created member
+    * @throws ResourceAlreadyExistsException
+    *            if member with peerURLs was already present
+    */
+   @Named("members:add")
+   @POST
+   Member add(@BinderParam(BindToJsonPayload.class) CreateMember memberToCreate);
+
+   /**
+    * @param memberID
+    *           id of previously existing member
+    * @return true if member was deleted or false if id did not match an
+    *         existing member
+    */
+   @Named("members:delete")
+   @Path("/{id}")
+   @Fallback(FalseOnNotFoundOr404.class)
+   @DELETE
+   boolean delete(@PathParam("id") String memberID);
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/8b11430a/etcd/src/main/java/org/jclouds/etcd/features/MiscellaneousApi.java
----------------------------------------------------------------------
diff --git a/etcd/src/main/java/org/jclouds/etcd/features/MiscellaneousApi.java b/etcd/src/main/java/org/jclouds/etcd/features/MiscellaneousApi.java
index ba8d898..73d61ea 100644
--- a/etcd/src/main/java/org/jclouds/etcd/features/MiscellaneousApi.java
+++ b/etcd/src/main/java/org/jclouds/etcd/features/MiscellaneousApi.java
@@ -24,6 +24,8 @@ import javax.ws.rs.Path;
 import javax.ws.rs.core.MediaType;
 
 import org.jclouds.etcd.domain.miscellaneous.Version;
+import org.jclouds.etcd.fallbacks.EtcdFallbacks.FalseOn503;
+import org.jclouds.rest.annotations.Fallback;
 import org.jclouds.rest.annotations.SelectJson;
 
 public interface MiscellaneousApi {
@@ -38,6 +40,7 @@ public interface MiscellaneousApi {
    @Consumes(MediaType.APPLICATION_JSON)
    @Path("/health")
    @SelectJson("health")
+   @Fallback(FalseOn503.class)
    @GET
    boolean health();
 

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/8b11430a/etcd/src/main/java/org/jclouds/etcd/features/StatisticsApi.java
----------------------------------------------------------------------
diff --git a/etcd/src/main/java/org/jclouds/etcd/features/StatisticsApi.java b/etcd/src/main/java/org/jclouds/etcd/features/StatisticsApi.java
index 859fac1..d469cf6 100644
--- a/etcd/src/main/java/org/jclouds/etcd/features/StatisticsApi.java
+++ b/etcd/src/main/java/org/jclouds/etcd/features/StatisticsApi.java
@@ -31,18 +31,28 @@ import org.jclouds.etcd.domain.statistics.Store;
 @Path("/{jclouds.api-version}/stats")
 public interface StatisticsApi {
 
+   /**
+    * @return information on leader and entire cluster but only if WE are the
+    *         leader
+    */
    @Named("statistics:leader")
-   @GET
    @Path("/leader")
+   @GET
    Leader leader();
 
+   /**
+    * @return information on node we are currently pointing at
+    */
    @Named("statistics:self")
-   @GET
    @Path("/self")
+   @GET
    Self self();
 
+   /**
+    * @return information about operations this node has handled
+    */
    @Named("statistics:store")
-   @GET
    @Path("/store")
+   @GET
    Store store();
 }

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/8b11430a/etcd/src/main/java/org/jclouds/etcd/handlers/EtcdErrorHandler.java
----------------------------------------------------------------------
diff --git a/etcd/src/main/java/org/jclouds/etcd/handlers/EtcdErrorHandler.java b/etcd/src/main/java/org/jclouds/etcd/handlers/EtcdErrorHandler.java
new file mode 100644
index 0000000..4784e44
--- /dev/null
+++ b/etcd/src/main/java/org/jclouds/etcd/handlers/EtcdErrorHandler.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jclouds.etcd.handlers;
+
+import static org.jclouds.util.Closeables2.closeQuietly;
+
+import java.io.IOException;
+
+import javax.annotation.Resource;
+
+import org.jclouds.http.HttpCommand;
+import org.jclouds.http.HttpErrorHandler;
+import org.jclouds.http.HttpResponse;
+import org.jclouds.http.HttpResponseException;
+import org.jclouds.logging.Logger;
+import org.jclouds.rest.ResourceAlreadyExistsException;
+import org.jclouds.rest.ResourceNotFoundException;
+import org.jclouds.util.Strings2;
+
+import com.google.common.base.Throwables;
+
+/**
+ * Handle errors and propagate exception
+ */
+public class EtcdErrorHandler implements HttpErrorHandler {
+   @Resource
+   protected Logger logger = Logger.NULL;
+
+   public void handleError(HttpCommand command, HttpResponse response) {
+
+      String message = parseMessage(response);
+      Exception exception = null;
+      try {
+
+         message = message != null ? message
+               : String.format("%s -> %s", command.getCurrentRequest().getRequestLine(), response.getStatusLine());
+
+         switch (response.getStatusCode()) {
+            case 400:
+               exception = new IllegalArgumentException(message);
+               break;
+            case 404:
+               exception = new ResourceNotFoundException(message);
+               break;
+            case 409:
+               exception = new ResourceAlreadyExistsException(message);
+               break;
+            default:
+               exception = new HttpResponseException(message, command, response);
+               break;
+         }
+      } catch (Exception e) {
+         exception = new HttpResponseException(command, response, e);
+      } finally {
+         if (exception == null) {
+            exception = message != null ? new HttpResponseException(command, response, message)
+                  : new HttpResponseException(command, response);
+         }
+         closeQuietly(response.getPayload());
+         command.setException(exception);
+      }
+   }
+
+   private String parseMessage(HttpResponse response) {
+      if (response.getPayload() == null)
+         return null;
+      try {
+         return Strings2.toStringAndClose(response.getPayload().openStream());
+      } catch (IOException e) {
+         throw Throwables.propagate(e);
+      }
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/8b11430a/etcd/src/test/java/org/jclouds/etcd/features/MembersApiLiveTest.java
----------------------------------------------------------------------
diff --git a/etcd/src/test/java/org/jclouds/etcd/features/MembersApiLiveTest.java b/etcd/src/test/java/org/jclouds/etcd/features/MembersApiLiveTest.java
new file mode 100644
index 0000000..4935a60
--- /dev/null
+++ b/etcd/src/test/java/org/jclouds/etcd/features/MembersApiLiveTest.java
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jclouds.etcd.features;
+
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.assertFalse;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.jclouds.etcd.BaseEtcdApiLiveTest;
+import org.jclouds.etcd.domain.members.CreateMember;
+import org.jclouds.etcd.domain.members.Member;
+import org.jclouds.rest.ResourceAlreadyExistsException;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.ImmutableList;
+
+@Test(groups = "live", testName = "MembersApiLiveTest", singleThreaded = true)
+public class MembersApiLiveTest extends BaseEtcdApiLiveTest {
+
+   private String selfID;
+   private Member nonSelfMember;
+   private Member addedMember;
+
+   @BeforeClass
+   protected void init() {
+      selfID = api.statisticsApi().self().id();
+      assertNotNull(selfID);
+   }
+
+   public void testListMembers() {
+      List<Member> members = api().list();
+      assertNotNull(members);
+      assertTrue(members.size() > 0);
+      for (Member member : members) {
+         if (!member.id().equals(selfID)) {
+            this.nonSelfMember = member;
+            return;
+         }
+      }
+      throw new RuntimeException("Could not find another member in cluster with different id");
+   }
+
+   @Test(dependsOnMethods = "testListMembers")
+   public void testDeleteMember() {
+      boolean successful = api().delete(nonSelfMember.id());
+      assertTrue(successful);
+   }
+
+   @Test(dependsOnMethods = "testDeleteMember")
+   public void testAddMember() {
+      assertNotNull(nonSelfMember);
+
+      addedMember = api().add(CreateMember.create(null, nonSelfMember.peerURLs(), null));
+      assertNotNull(addedMember);
+      assertTrue(addedMember.peerURLs().containsAll(nonSelfMember.peerURLs()));
+   }
+
+   @Test(dependsOnMethods = "testAddMember", expectedExceptions = ResourceAlreadyExistsException.class)
+   public void testAddExistingMember() {
+      assertNotNull(addedMember);
+
+      Member existingMember = api().add(CreateMember.create(null, addedMember.peerURLs(), addedMember.clientURLs()));
+      assertNull(existingMember);
+   }
+
+   @Test(dependsOnMethods = "testAddExistingMember", expectedExceptions = IllegalArgumentException.class)
+   public void testAddMemberWithMalformedURL() {
+      api().add(CreateMember.create(null, ImmutableList.of("htp:/hello/world:11bye"), null));
+   }
+
+   @Test(dependsOnMethods = "testAddMemberWithMalformedURL", expectedExceptions = IllegalArgumentException.class)
+   public void testAddMemberWithIllegalFormat() {
+      api().add(CreateMember.create(null, ImmutableList.of("http://www.google.com"), null));
+   }
+
+   @Test(dependsOnMethods = "testAddMemberWithIllegalFormat")
+   public void testDeleteMemberNonExistentMember() {
+      boolean successful = api().delete(UUID.randomUUID().toString().replaceAll("-", ""));
+      assertFalse(successful);
+   }
+
+   private MembersApi api() {
+      return api.membersApi();
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/8b11430a/etcd/src/test/java/org/jclouds/etcd/features/MembersApiMockTest.java
----------------------------------------------------------------------
diff --git a/etcd/src/test/java/org/jclouds/etcd/features/MembersApiMockTest.java b/etcd/src/test/java/org/jclouds/etcd/features/MembersApiMockTest.java
new file mode 100644
index 0000000..863e131
--- /dev/null
+++ b/etcd/src/test/java/org/jclouds/etcd/features/MembersApiMockTest.java
@@ -0,0 +1,168 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jclouds.etcd.features;
+
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.assertFalse;
+
+import java.util.List;
+
+import org.jclouds.etcd.EtcdApi;
+import org.jclouds.etcd.EtcdApiMetadata;
+import org.jclouds.etcd.domain.members.CreateMember;
+import org.jclouds.etcd.domain.members.Member;
+import org.jclouds.etcd.internal.BaseEtcdMockTest;
+import org.jclouds.rest.ResourceAlreadyExistsException;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.ImmutableList;
+import com.squareup.okhttp.mockwebserver.MockResponse;
+import com.squareup.okhttp.mockwebserver.MockWebServer;
+
+/**
+ * Mock tests for the {@link org.jclouds.etcd.features.MembersApi} class.
+ */
+@Test(groups = "unit", testName = "MembersApiMockTest")
+public class MembersApiMockTest extends BaseEtcdMockTest {
+
+   public void testListMembers() throws Exception {
+      MockWebServer server = mockEtcdJavaWebServer();
+
+      server.enqueue(new MockResponse().setBody(payloadFromResource("/members.json")).setResponseCode(200));
+      EtcdApi etcdApi = api(server.getUrl("/"));
+      MembersApi api = etcdApi.membersApi();
+      try {
+         List<Member> members = api.list();
+         assertNotNull(members);
+         assertTrue(members.size() == 2);
+         assertSent(server, "GET", "/" + EtcdApiMetadata.API_VERSION + "/members");
+      } finally {
+         etcdApi.close();
+         server.shutdown();
+      }
+   }
+
+   public void testAddMember() throws Exception {
+      MockWebServer server = mockEtcdJavaWebServer();
+
+      server.enqueue(new MockResponse().setBody(payloadFromResource("/members-added.json")).setResponseCode(201));
+      EtcdApi etcdApi = api(server.getUrl("/"));
+      MembersApi api = etcdApi.membersApi();
+      try {
+         String peerURL = "http://10.0.0.10:2380";
+         String clientURL = "http://10.0.0.10:2381";
+         Member member = api.add(CreateMember.create(null, ImmutableList.of(peerURL), ImmutableList.of(clientURL)));
+         assertNotNull(member);
+         assertTrue(member.peerURLs().contains(peerURL));
+         assertTrue(member.clientURLs().contains(clientURL));
+         assertSent(server, "POST", "/" + EtcdApiMetadata.API_VERSION + "/members");
+      } finally {
+         etcdApi.close();
+         server.shutdown();
+      }
+   }
+
+   @Test(expectedExceptions = IllegalArgumentException.class)
+   public void testAddMemberWithMalformedURL() throws Exception {
+      MockWebServer server = mockEtcdJavaWebServer();
+
+      server.enqueue(
+            new MockResponse().setBody(payloadFromResource("/members-add-malformed-url.json")).setResponseCode(400));
+      EtcdApi etcdApi = api(server.getUrl("/"));
+      MembersApi api = etcdApi.membersApi();
+      try {
+         String peerURL = "htp:/hello/world:11bye";
+         api.add(CreateMember.create(null, ImmutableList.of(peerURL), null));
+      } finally {
+         etcdApi.close();
+         server.shutdown();
+      }
+   }
+
+   @Test(expectedExceptions = IllegalArgumentException.class)
+   public void testAddMemberWithIllegalFormat() throws Exception {
+      MockWebServer server = mockEtcdJavaWebServer();
+
+      server.enqueue(
+            new MockResponse().setBody(payloadFromResource("/members-add-illegal-format.json")).setResponseCode(400));
+      EtcdApi etcdApi = api(server.getUrl("/"));
+      MembersApi api = etcdApi.membersApi();
+      try {
+         String peerURL = "http://www.google.com";
+         api.add(CreateMember.create(null, ImmutableList.of(peerURL), null));
+      } finally {
+         etcdApi.close();
+         server.shutdown();
+      }
+   }
+
+   @Test(expectedExceptions = ResourceAlreadyExistsException.class)
+   public void testAddExistingMember() throws Exception {
+      MockWebServer server = mockEtcdJavaWebServer();
+
+      server.enqueue(
+            new MockResponse().setBody(payloadFromResource("/members-add-existent.json")).setResponseCode(409));
+      EtcdApi etcdApi = api(server.getUrl("/"));
+      MembersApi api = etcdApi.membersApi();
+      try {
+         String peerURL = "http://10.0.0.10:2380";
+         Member member = api.add(CreateMember.create(null, ImmutableList.of(peerURL), null));
+         assertNull(member);
+         assertSent(server, "POST", "/" + EtcdApiMetadata.API_VERSION + "/members");
+      } finally {
+         etcdApi.close();
+         server.shutdown();
+      }
+   }
+
+   public void testDeleteMember() throws Exception {
+      MockWebServer server = mockEtcdJavaWebServer();
+
+      server.enqueue(new MockResponse().setBody("").setResponseCode(204));
+      EtcdApi etcdApi = api(server.getUrl("/"));
+      MembersApi api = etcdApi.membersApi();
+      try {
+         String memberID = "123456789";
+         boolean deleted = api.delete(memberID);
+         assertTrue(deleted);
+         assertSent(server, "DELETE", "/" + EtcdApiMetadata.API_VERSION + "/members/" + memberID);
+      } finally {
+         etcdApi.close();
+         server.shutdown();
+      }
+   }
+
+   public void testDeleteNonExistentMember() throws Exception {
+      MockWebServer server = mockEtcdJavaWebServer();
+
+      server.enqueue(
+            new MockResponse().setBody(payloadFromResource("/members-delete-nonexistent.json")).setResponseCode(404));
+      EtcdApi etcdApi = api(server.getUrl("/"));
+      MembersApi api = etcdApi.membersApi();
+      try {
+         String memberID = "1234567890";
+         boolean deleted = api.delete(memberID);
+         assertFalse(deleted);
+         assertSent(server, "DELETE", "/" + EtcdApiMetadata.API_VERSION + "/members/" + memberID);
+      } finally {
+         etcdApi.close();
+         server.shutdown();
+      }
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/8b11430a/etcd/src/test/java/org/jclouds/etcd/features/MiscellaneousApiMockTest.java
----------------------------------------------------------------------
diff --git a/etcd/src/test/java/org/jclouds/etcd/features/MiscellaneousApiMockTest.java b/etcd/src/test/java/org/jclouds/etcd/features/MiscellaneousApiMockTest.java
index abd239d..ad5d8a1 100644
--- a/etcd/src/test/java/org/jclouds/etcd/features/MiscellaneousApiMockTest.java
+++ b/etcd/src/test/java/org/jclouds/etcd/features/MiscellaneousApiMockTest.java
@@ -18,6 +18,7 @@ package org.jclouds.etcd.features;
 
 import static org.testng.Assert.assertNotNull;
 import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.assertFalse;
 
 import org.jclouds.etcd.EtcdApi;
 import org.jclouds.etcd.domain.miscellaneous.Version;
@@ -69,6 +70,22 @@ public class MiscellaneousApiMockTest extends BaseEtcdMockTest {
       }
    }
 
+   public void testGetBadHealth() throws Exception {
+      MockWebServer server = mockEtcdJavaWebServer();
+
+      server.enqueue(new MockResponse().setBody(payloadFromResource("/health-bad.json")).setResponseCode(503));
+      EtcdApi etcdJavaApi = api(server.getUrl("/"));
+      MiscellaneousApi api = etcdJavaApi.miscellaneousApi();
+      try {
+         boolean health = api.health();
+         assertFalse(health);
+         assertSent(server, "GET", "/health");
+      } finally {
+         etcdJavaApi.close();
+         server.shutdown();
+      }
+   }
+
    public void testGetMetrics() throws Exception {
       MockWebServer server = mockEtcdJavaWebServer();
 

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/8b11430a/etcd/src/test/java/org/jclouds/etcd/features/StatisticsApiLiveTest.java
----------------------------------------------------------------------
diff --git a/etcd/src/test/java/org/jclouds/etcd/features/StatisticsApiLiveTest.java b/etcd/src/test/java/org/jclouds/etcd/features/StatisticsApiLiveTest.java
index b02370d..97cd1ac 100644
--- a/etcd/src/test/java/org/jclouds/etcd/features/StatisticsApiLiveTest.java
+++ b/etcd/src/test/java/org/jclouds/etcd/features/StatisticsApiLiveTest.java
@@ -17,24 +17,57 @@
 package org.jclouds.etcd.features;
 
 import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.fail;
+
+import java.util.Properties;
+
+import org.jclouds.Constants;
 import org.jclouds.etcd.BaseEtcdApiLiveTest;
+import org.jclouds.etcd.EtcdApi;
+import org.jclouds.etcd.domain.members.Member;
+import org.jclouds.etcd.domain.statistics.Self;
 import org.testng.annotations.Test;
 
+import com.google.inject.Module;
+
 @Test(groups = "live", testName = "StatisticsApiLiveTest")
 public class StatisticsApiLiveTest extends BaseEtcdApiLiveTest {
 
+   private Self self;
+
    @Test
-   public void testGetLeader() throws Exception {
-      assertNotNull(api().leader());
+   public void testGetSelf() {
+      self = api().self();
+      assertNotNull(self);
    }
 
-   @Test
-   public void testGetSelf() throws Exception {
-      assertNotNull(api().self());
+   @Test(dependsOnMethods = "testGetSelf")
+   public void testGetLeader() {
+
+      /*
+       * It's possible the default end-point is not the cluster leader. If true
+       * we will iterate through all members to find the leader and execute the
+       * 'leader' endpoint against its client URL.
+       */
+      if (self.state().equals("StateLeader")) {
+         assertNotNull(api().leader());
+      } else {
+         for (Member possibleLeader : api.membersApi().list()) {
+            if (possibleLeader.id().equals(self.leaderInfo().leader())) {
+               Properties properties = new Properties();
+               properties.setProperty(Constants.PROPERTY_ENDPOINT, possibleLeader.clientURLs().get(0));
+               Iterable<Module> modules = setupModules();
+               EtcdApi etcdApi = super.create(properties, modules);
+               assertNotNull(etcdApi.statisticsApi().leader());
+               return;
+            }
+         }
+         fail("Could not find a leader within cluster");
+      }
    }
 
    @Test
-   public void testGetStore() throws Exception {
+   public void testGetStore() {
       assertNotNull(api().store());
    }
 

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/8b11430a/etcd/src/test/resources/health-bad.json
----------------------------------------------------------------------
diff --git a/etcd/src/test/resources/health-bad.json b/etcd/src/test/resources/health-bad.json
new file mode 100644
index 0000000..6b68ef1
--- /dev/null
+++ b/etcd/src/test/resources/health-bad.json
@@ -0,0 +1 @@
+{"health": "false"}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/8b11430a/etcd/src/test/resources/members-add-existent.json
----------------------------------------------------------------------
diff --git a/etcd/src/test/resources/members-add-existent.json b/etcd/src/test/resources/members-add-existent.json
new file mode 100644
index 0000000..363618c
--- /dev/null
+++ b/etcd/src/test/resources/members-add-existent.json
@@ -0,0 +1 @@
+{"message":"etcdserver: ID exists"}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/8b11430a/etcd/src/test/resources/members-add-illegal-format.json
----------------------------------------------------------------------
diff --git a/etcd/src/test/resources/members-add-illegal-format.json b/etcd/src/test/resources/members-add-illegal-format.json
new file mode 100644
index 0000000..c344f28
--- /dev/null
+++ b/etcd/src/test/resources/members-add-illegal-format.json
@@ -0,0 +1 @@
+{"message":"URL address does not have the form \"host:port\": http://www.google.com"}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/8b11430a/etcd/src/test/resources/members-add-malformed-url.json
----------------------------------------------------------------------
diff --git a/etcd/src/test/resources/members-add-malformed-url.json b/etcd/src/test/resources/members-add-malformed-url.json
new file mode 100644
index 0000000..ffa9a2e
--- /dev/null
+++ b/etcd/src/test/resources/members-add-malformed-url.json
@@ -0,0 +1 @@
+{"message":"URL scheme must be http or https: 1234567890"}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/8b11430a/etcd/src/test/resources/members-added.json
----------------------------------------------------------------------
diff --git a/etcd/src/test/resources/members-added.json b/etcd/src/test/resources/members-added.json
new file mode 100644
index 0000000..904d739
--- /dev/null
+++ b/etcd/src/test/resources/members-added.json
@@ -0,0 +1,9 @@
+{
+    "id": "3777296169",
+    "peerURLs": [
+        "http://10.0.0.10:2380"
+    ],
+    "clientURLs": [
+        "http://10.0.0.10:2381"
+    ]
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/8b11430a/etcd/src/test/resources/members-delete-nonexistent.json
----------------------------------------------------------------------
diff --git a/etcd/src/test/resources/members-delete-nonexistent.json b/etcd/src/test/resources/members-delete-nonexistent.json
new file mode 100644
index 0000000..52302e6
--- /dev/null
+++ b/etcd/src/test/resources/members-delete-nonexistent.json
@@ -0,0 +1 @@
+{"message":"No such member: 1234567890"}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/8b11430a/etcd/src/test/resources/members.json
----------------------------------------------------------------------
diff --git a/etcd/src/test/resources/members.json b/etcd/src/test/resources/members.json
new file mode 100644
index 0000000..cdfcbb5
--- /dev/null
+++ b/etcd/src/test/resources/members.json
@@ -0,0 +1,24 @@
+{
+    "members": [
+        {
+            "id": "272e204152",
+            "name": "infra1",
+            "peerURLs": [
+                "http://10.0.0.10:2380"
+            ],
+            "clientURLs": [
+                "http://10.0.0.10:2379"
+            ]
+        },
+        {
+            "id": "2225373f43",
+            "name": "infra2",
+            "peerURLs": [
+                "http://10.0.0.11:2380"
+            ],
+            "clientURLs": [
+                "http://10.0.0.11:2379"
+            ]
+        },
+    ]
+}
\ No newline at end of file