You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@whirr.apache.org by as...@apache.org on 2011/05/05 16:02:10 UTC

svn commit: r1099810 - in /incubator/whirr/trunk: ./ core/src/main/java/org/apache/whirr/ core/src/main/java/org/apache/whirr/service/ core/src/main/resources/ core/src/test/java/org/apache/whirr/service/integration/

Author: asavu
Date: Thu May  5 14:02:10 2011
New Revision: 1099810

URL: http://svn.apache.org/viewvc?rev=1099810&view=rev
Log:
WHIRR-288. Add blob store persistence for cluster state (asavu)

Added:
    incubator/whirr/trunk/core/src/main/java/org/apache/whirr/service/BlobClusterStateStore.java
    incubator/whirr/trunk/core/src/test/java/org/apache/whirr/service/integration/
    incubator/whirr/trunk/core/src/test/java/org/apache/whirr/service/integration/BlobClusterStateStoreTest.java
Modified:
    incubator/whirr/trunk/CHANGES.txt
    incubator/whirr/trunk/core/src/main/java/org/apache/whirr/ClusterController.java
    incubator/whirr/trunk/core/src/main/java/org/apache/whirr/ClusterSpec.java
    incubator/whirr/trunk/core/src/main/java/org/apache/whirr/service/ClusterStateStore.java
    incubator/whirr/trunk/core/src/main/java/org/apache/whirr/service/ClusterStateStoreFactory.java
    incubator/whirr/trunk/core/src/main/java/org/apache/whirr/service/FileClusterStateStore.java
    incubator/whirr/trunk/core/src/main/resources/whirr-default.properties

Modified: incubator/whirr/trunk/CHANGES.txt
URL: http://svn.apache.org/viewvc/incubator/whirr/trunk/CHANGES.txt?rev=1099810&r1=1099809&r2=1099810&view=diff
==============================================================================
--- incubator/whirr/trunk/CHANGES.txt (original)
+++ incubator/whirr/trunk/CHANGES.txt Thu May  5 14:02:10 2011
@@ -59,6 +59,8 @@ Trunk (unreleased changes)
     WHIRR-297. Separate ZooKeeper and ElasticSearch install and configuration 
     scripts into more generic functions (asavu)
 
+    WHIRR-288. Add blob store persistence for cluster state (asavu)
+
   BUG FIXES
 
     WHIRR-253. ZooKeeper service should only authorize ingress to ZooKeeper 

Modified: incubator/whirr/trunk/core/src/main/java/org/apache/whirr/ClusterController.java
URL: http://svn.apache.org/viewvc/incubator/whirr/trunk/core/src/main/java/org/apache/whirr/ClusterController.java?rev=1099810&r1=1099809&r2=1099810&view=diff
==============================================================================
--- incubator/whirr/trunk/core/src/main/java/org/apache/whirr/ClusterController.java (original)
+++ incubator/whirr/trunk/core/src/main/java/org/apache/whirr/ClusterController.java Thu May  5 14:02:10 2011
@@ -164,9 +164,9 @@ public class ClusterController {
   public Set<Cluster.Instance> getInstances(ClusterSpec spec, ClusterStateStore stateStore)
       throws IOException, InterruptedException {
     Set<Cluster.Instance> instances = Sets.newLinkedHashSet();
-    if (stateStore != null) {
+    Cluster cluster = (stateStore != null) ? stateStore.load() : null;
+    if (cluster != null) {
       /* enrich the instance information with node metadata */
-      Cluster cluster = stateStore.load();
 
       for(NodeMetadata node : getNodes(spec)) {
         Cluster.Instance instance = cluster.getInstanceMatching(withIds(node.getId()));
@@ -176,6 +176,7 @@ public class ClusterController {
       }
     } else {
       /* return a list of instances with no roles attached */
+
       Credentials credentials = new Credentials(spec.getClusterUser(), spec.getPrivateKey());
       for(NodeMetadata node : getNodes(spec)) {
         instances.add(new Cluster.Instance(credentials, Sets.<String>newHashSet(),

Modified: incubator/whirr/trunk/core/src/main/java/org/apache/whirr/ClusterSpec.java
URL: http://svn.apache.org/viewvc/incubator/whirr/trunk/core/src/main/java/org/apache/whirr/ClusterSpec.java?rev=1099810&r1=1099809&r2=1099810&view=diff
==============================================================================
--- incubator/whirr/trunk/core/src/main/java/org/apache/whirr/ClusterSpec.java (original)
+++ incubator/whirr/trunk/core/src/main/java/org/apache/whirr/ClusterSpec.java Thu May  5 14:02:10 2011
@@ -47,6 +47,10 @@ import org.apache.commons.configuration.
 import org.apache.commons.configuration.interpol.ConfigurationInterpolator;
 import org.apache.commons.io.IOUtils;
 import org.apache.commons.lang.text.StrLookup;
+import org.jclouds.predicates.Validator;
+import org.jclouds.predicates.validators.DnsNameValidator;
+import org.jclouds.rest.annotations.ParamValidators;
+import org.jclouds.s3.predicates.validators.BucketNameValidator;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -125,6 +129,15 @@ public class ClusterSpec {
     BLOBSTORE_CREDENTIAL(String.class, false, "The blob store credential"),
 
     BLOBSTORE_LOCATION_ID(String.class, false, "The blob store location ID"),
+
+    STATE_STORE(String.class, false, "What kind of store to use for state " +
+      "(local, blob or none). Defaults to local."),
+
+    STATE_STORE_CONTAINER(String.class, false, "Container where to store state. " +
+      "Valid only for the blob state store."),
+
+    STATE_STORE_BLOB(String.class, false, "Blob name for state storage. " +
+      "Valid only for the blob state store. Defaults to whirr-<cluster-name>"),
       
     IMAGE_ID(String.class, false, "The ID of the image to use for " + 
       "instances. If not specified then a vanilla Linux image is " + 
@@ -237,6 +250,10 @@ public class ClusterSpec {
   private String blobStoreIdentity;
   private String blobStoreCredential;
 
+  private String stateStore;
+  private String stateStoreContainer;
+  private String stateStoreBlob;
+
   private String privateKey;
   private File privateKeyFile;
   private String publicKey;
@@ -292,6 +309,10 @@ public class ClusterSpec {
     setBlobStoreIdentity(getString(Property.BLOBSTORE_IDENTITY));
     setBlobStoreCredential(getString(Property.BLOBSTORE_CREDENTIAL));
 
+    setStateStore(getString(Property.STATE_STORE));
+    setStateStoreContainer(getString(Property.STATE_STORE_CONTAINER));
+    setStateStoreBlob(getString(Property.STATE_STORE_BLOB));
+
     checkAndSetKeyPair();
 
     setImageId(getString(Property.IMAGE_ID));
@@ -307,7 +328,7 @@ public class ClusterSpec {
   }
 
   private String getString(Property key) {
-    return config.getString(key.getConfigName());
+    return config.getString(key.getConfigName(), null);
   }
 
   private int getInt(Property key, int defaultValue) {
@@ -447,6 +468,24 @@ public class ClusterSpec {
     return blobStoreLocationId;
   }
 
+  public String getStateStore() {
+    if (stateStore == null) {
+      return "local";
+    }
+    return stateStore;
+  }
+
+  public String getStateStoreContainer() {
+    return stateStoreContainer;
+  }
+
+  public String getStateStoreBlob() {
+    if (stateStoreBlob == null && "blob".equals(stateStore)) {
+      return "whirr-" + getClusterName();
+    }
+    return stateStoreBlob;
+  }
+
   public String getServiceName() {
     return serviceName;
   }
@@ -535,6 +574,29 @@ public class ClusterSpec {
     blobStoreLocationId = locationId;
   }
 
+  public void setStateStore(String type) {
+    if (type != null) {
+      checkArgument(Sets.newHashSet("local", "blob", "none").contains(type),
+        "Invalid state store. Valid values are local, blob or none.");
+    }
+    this.stateStore = type;
+  }
+
+  public void setStateStoreContainer(String container) {
+    checkContainerName(container);
+    this.stateStoreContainer = container;
+  }
+
+  private void checkContainerName(String name) {
+    if (name != null) {
+      checkArgument((new DnsNameValidator(3, 63){}).apply(name));
+    }
+  }
+
+  public void setStateStoreBlob(String blob) {
+    this.stateStoreBlob = blob;
+  }
+
   public void setClusterName(String clusterName) {
     this.clusterName = clusterName;
   }

Added: incubator/whirr/trunk/core/src/main/java/org/apache/whirr/service/BlobClusterStateStore.java
URL: http://svn.apache.org/viewvc/incubator/whirr/trunk/core/src/main/java/org/apache/whirr/service/BlobClusterStateStore.java?rev=1099810&view=auto
==============================================================================
--- incubator/whirr/trunk/core/src/main/java/org/apache/whirr/service/BlobClusterStateStore.java (added)
+++ incubator/whirr/trunk/core/src/main/java/org/apache/whirr/service/BlobClusterStateStore.java Thu May  5 14:02:10 2011
@@ -0,0 +1,84 @@
+/**
+ * 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.apache.whirr.service;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.whirr.Cluster;
+import org.apache.whirr.ClusterSpec;
+import org.jclouds.blobstore.BlobStore;
+import org.jclouds.blobstore.BlobStoreContext;
+import org.jclouds.blobstore.domain.Blob;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+public class BlobClusterStateStore extends ClusterStateStore {
+
+  private static final Logger LOG = LoggerFactory
+    .getLogger(FileClusterStateStore.class);
+
+  private ClusterSpec spec;
+  private BlobStoreContext context;
+
+  private String container;
+  private String blobName;
+
+  public BlobClusterStateStore(ClusterSpec spec) {
+    this.spec = spec;
+    this.context = BlobStoreContextBuilder.build(spec);
+
+    this.container = checkNotNull(spec.getStateStoreContainer());
+    this.blobName = checkNotNull(spec.getStateStoreBlob());
+
+    /* create container if it does not already exists */
+    if (!context.getBlobStore().containerExists(container)) {
+      context.getBlobStore().createContainerInLocation(null, container);
+    }
+  }
+
+  @Override
+  public Cluster load() throws IOException {
+    Blob blob = context.getBlobStore().getBlob(container, blobName);
+    if (blob != null) {
+      return unserialize(spec,
+        IOUtils.toString(blob.getPayload().getInput(), "utf-8"));
+    }
+    return null;
+  }
+
+  @Override
+  public void save(Cluster cluster) throws IOException {
+    BlobStore store = context.getBlobStore();
+
+    Blob blob = store.newBlob(blobName);
+    blob.setPayload(serialize(cluster));
+    store.putBlob(container, blob);
+
+    LOG.info("Saved cluster state to '{}' ", context.getSigner()
+      .signGetBlob(container, blobName).getEndpoint().toString());
+  }
+
+  @Override
+  public void destroy() throws IOException {
+    context.getBlobStore().removeBlob(container, blobName);
+  }
+}

Modified: incubator/whirr/trunk/core/src/main/java/org/apache/whirr/service/ClusterStateStore.java
URL: http://svn.apache.org/viewvc/incubator/whirr/trunk/core/src/main/java/org/apache/whirr/service/ClusterStateStore.java?rev=1099810&r1=1099809&r2=1099810&view=diff
==============================================================================
--- incubator/whirr/trunk/core/src/main/java/org/apache/whirr/service/ClusterStateStore.java (original)
+++ incubator/whirr/trunk/core/src/main/java/org/apache/whirr/service/ClusterStateStore.java Thu May  5 14:02:10 2011
@@ -19,8 +19,20 @@
 package org.apache.whirr.service;
 
 import java.io.IOException;
-
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Iterator;
+import java.util.Set;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Splitter;
+import com.google.common.collect.Sets;
 import org.apache.whirr.Cluster;
+import org.apache.whirr.ClusterSpec;
+import org.apache.whirr.util.DnsUtil;
+import org.jclouds.domain.Credentials;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * Interface for cluster state storage facilities.
@@ -51,4 +63,61 @@ public abstract class ClusterStateStore 
    */
   public abstract void destroy() throws IOException;
 
+
+  /**
+   * Create parser friendly string representation for a {@link Cluster}
+   *
+   * @param cluster
+   * @return String representation
+   * @throws IOException
+   */
+  protected String serialize(Cluster cluster) throws IOException {
+    StringBuilder sb = new StringBuilder();
+
+    for (Cluster.Instance instance : cluster.getInstances()) {
+      String id = instance.getId();
+      String roles = Joiner.on(',').join(instance.getRoles());
+
+      String publicAddress = DnsUtil.resolveAddress(instance.getPublicAddress()
+        .getHostAddress());
+      String privateAddress = instance.getPrivateAddress().getHostAddress();
+
+      sb.append(id).append("\t");
+      sb.append(roles).append("\t");
+      sb.append(publicAddress).append("\t");
+      sb.append(privateAddress).append("\n");
+    }
+
+    return sb.toString();
+  }
+
+  /**
+   * Rebuild the {@link Cluster} instance by using the string representation
+   *
+   * @param spec
+   * @param content
+   * @return
+   * @throws UnknownHostException
+   */
+  protected Cluster unserialize(ClusterSpec spec, String content) throws UnknownHostException {
+    Credentials credentials = new Credentials(spec.getClusterUser(), spec.getPrivateKey());
+    Set<Cluster.Instance> instances = Sets.newLinkedHashSet();
+
+    for(String line : Splitter.on("\n").split(content)) {
+      if (line.trim().equals("")) continue; /* ignore empty lines */
+      Iterator<String> fields = Splitter.on("\t").split(line).iterator();
+
+      String id = fields.next();
+      Set<String> roles = Sets.newLinkedHashSet(Splitter.on(",").split(fields.next()));
+      String publicAddress = fields.next();
+      String privateAddress = fields.next();
+
+      instances.add(new Cluster.Instance(credentials, roles,
+        InetAddress.getByName(publicAddress).getHostAddress(),
+        privateAddress, id, null));
+    }
+
+    return new Cluster(instances);
+  }
+
 }

Modified: incubator/whirr/trunk/core/src/main/java/org/apache/whirr/service/ClusterStateStoreFactory.java
URL: http://svn.apache.org/viewvc/incubator/whirr/trunk/core/src/main/java/org/apache/whirr/service/ClusterStateStoreFactory.java?rev=1099810&r1=1099809&r2=1099810&view=diff
==============================================================================
--- incubator/whirr/trunk/core/src/main/java/org/apache/whirr/service/ClusterStateStoreFactory.java (original)
+++ incubator/whirr/trunk/core/src/main/java/org/apache/whirr/service/ClusterStateStoreFactory.java Thu May  5 14:02:10 2011
@@ -18,9 +18,16 @@
 
 package org.apache.whirr.service;
 
+import com.google.common.collect.Maps;
 import org.apache.commons.configuration.Configuration;
 import org.apache.commons.configuration.PropertiesConfiguration;
+import org.apache.whirr.Cluster;
 import org.apache.whirr.ClusterSpec;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.Map;
 
 /**
  * A factory for ClusterStateStores.
@@ -28,12 +35,40 @@ import org.apache.whirr.ClusterSpec;
  */
 public class ClusterStateStoreFactory {
 
+  private static final Logger LOG = LoggerFactory
+    .getLogger(ClusterStateStoreFactory.class);
+
+  private class NoopClusterStateStore extends ClusterStateStore {
+    public NoopClusterStateStore() {
+      LOG.warn("No cluster state is going to be persisted. There is no easy " +
+        "way to retrieve instance roles after launch.");
+    }
+    @Override
+    public Cluster load() throws IOException {
+      return null;
+    }
+    @Override
+    public void save(Cluster cluster) throws IOException {
+    }
+    @Override
+    public void destroy() throws IOException {
+    }
+  }
+
   public ClusterStateStore create(ClusterSpec spec) {
     return create(spec, new PropertiesConfiguration());
   }
 
   public ClusterStateStore create(ClusterSpec spec, Configuration conf) {
-    return new FileClusterStateStore(spec);
+    if ("local".equals(spec.getStateStore())) {
+      return new FileClusterStateStore(spec);
+
+    } else if("blob".equals(spec.getStateStore())) {
+      return new BlobClusterStateStore(spec);
+
+    } else {
+      return new NoopClusterStateStore();
+    }
   }
 
 }

Modified: incubator/whirr/trunk/core/src/main/java/org/apache/whirr/service/FileClusterStateStore.java
URL: http://svn.apache.org/viewvc/incubator/whirr/trunk/core/src/main/java/org/apache/whirr/service/FileClusterStateStore.java?rev=1099810&r1=1099809&r2=1099810&view=diff
==============================================================================
--- incubator/whirr/trunk/core/src/main/java/org/apache/whirr/service/FileClusterStateStore.java (original)
+++ incubator/whirr/trunk/core/src/main/java/org/apache/whirr/service/FileClusterStateStore.java Thu May  5 14:02:10 2011
@@ -19,23 +19,15 @@ package org.apache.whirr.service;
 
 import java.io.File;
 import java.io.IOException;
-import java.net.InetAddress;
-import java.util.Iterator;
-import java.util.Set;
 
 import org.apache.whirr.Cluster;
 import org.apache.whirr.ClusterSpec;
-import org.apache.whirr.util.DnsUtil;
-import org.jclouds.domain.Credentials;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import com.google.common.base.Charsets;
 import com.google.common.base.Joiner;
-import com.google.common.base.Splitter;
-import com.google.common.collect.Sets;
 import com.google.common.io.Files;
-import com.google.common.io.LineProcessor;
 
 import static com.google.common.base.Preconditions.checkNotNull;
 
@@ -50,43 +42,17 @@ public class FileClusterStateStore exten
       .getLogger(FileClusterStateStore.class);
 
   private ClusterSpec spec;
-  private Credentials credentials;
 
   public FileClusterStateStore(ClusterSpec spec) {
     checkNotNull(spec,"clusterSpec");
-
     this.spec = spec;
-    this.credentials = new Credentials(spec.getClusterUser(), spec.getPrivateKey());
   }
 
   @Override
   public Cluster load() throws IOException {
     File instancesFile = new File(spec.getClusterDirectory(), "instances");
-    Set<Cluster.Instance> instances = Files.readLines(instancesFile, Charsets.UTF_8,
-        new LineProcessor<Set<Cluster.Instance>>() {
-
-          Set<Cluster.Instance> instances = Sets.newLinkedHashSet();
-
-          @Override
-          public boolean processLine(String line) throws IOException {
-            Iterator<String> fields = Splitter.on("\t").split(line).iterator();
-            String id = fields.next();
-            Set<String> roles = Sets.newLinkedHashSet(Splitter.on(",").split(
-                fields.next()));
-            String publicAddress = fields.next();
-            String privateAddress = fields.next();
-            instances.add(new Cluster.Instance(credentials, roles,
-              InetAddress.getByName(publicAddress).getHostAddress(),
-              privateAddress, id, null));
-            return true;
-          }
-
-          @Override
-          public Set<Cluster.Instance> getResult() {
-            return instances;
-          }
-        });
-    return new Cluster(instances);
+    return unserialize(spec,
+      Joiner.on("\n").join(Files.readLines(instancesFile, Charsets.UTF_8)));
   }
 
   @Override
@@ -96,27 +62,12 @@ public class FileClusterStateStore exten
 
   @Override
   public void save(Cluster cluster) throws IOException {
-    File clusterDir = spec.getClusterDirectory();
-    File instancesFile = new File(clusterDir, "instances");
-
-    StringBuilder sb = new StringBuilder();
-    for (Cluster.Instance instance : cluster.getInstances()) {
-      String id = instance.getId();
-      String roles = Joiner.on(',').join(instance.getRoles());
-
-      String publicAddress = DnsUtil.resolveAddress(instance.getPublicAddress()
-        .getHostAddress());
-      String privateAddress = instance.getPrivateAddress().getHostAddress();
-
-      sb.append(id).append("\t");
-      sb.append(roles).append("\t");
-      sb.append(publicAddress).append("\t");
-      sb.append(privateAddress).append("\n");
-    }
+    File instancesFile = new File(spec.getClusterDirectory(), "instances");
 
     try {
-      Files.write(sb.toString(), instancesFile, Charsets.UTF_8);
+      Files.write(serialize(cluster).toString(), instancesFile, Charsets.UTF_8);
       LOG.info("Wrote instances file {}", instancesFile);
+
     } catch (IOException e) {
       LOG.error("Problem writing instances file {}", instancesFile, e);
     }

Modified: incubator/whirr/trunk/core/src/main/resources/whirr-default.properties
URL: http://svn.apache.org/viewvc/incubator/whirr/trunk/core/src/main/resources/whirr-default.properties?rev=1099810&r1=1099809&r2=1099810&view=diff
==============================================================================
--- incubator/whirr/trunk/core/src/main/resources/whirr-default.properties (original)
+++ incubator/whirr/trunk/core/src/main/resources/whirr-default.properties Thu May  5 14:02:10 2011
@@ -21,3 +21,5 @@ whirr.max-startup-retries=1
 
 whirr.cluster-user=${sys:user.name}
 
+whirr.state-store=local
+

Added: incubator/whirr/trunk/core/src/test/java/org/apache/whirr/service/integration/BlobClusterStateStoreTest.java
URL: http://svn.apache.org/viewvc/incubator/whirr/trunk/core/src/test/java/org/apache/whirr/service/integration/BlobClusterStateStoreTest.java?rev=1099810&view=auto
==============================================================================
--- incubator/whirr/trunk/core/src/test/java/org/apache/whirr/service/integration/BlobClusterStateStoreTest.java (added)
+++ incubator/whirr/trunk/core/src/test/java/org/apache/whirr/service/integration/BlobClusterStateStoreTest.java Thu May  5 14:02:10 2011
@@ -0,0 +1,126 @@
+/**
+ * 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.apache.whirr.service.integration;
+
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+import org.apache.commons.configuration.Configuration;
+import org.apache.commons.configuration.ConfigurationException;
+import org.apache.commons.configuration.PropertiesConfiguration;
+import org.apache.commons.lang.RandomStringUtils;
+import org.apache.whirr.Cluster;
+import org.apache.whirr.ClusterSpec;
+import org.apache.whirr.service.BlobClusterStateStore;
+import org.apache.whirr.service.BlobStoreContextBuilder;
+import org.jclouds.blobstore.BlobStoreContext;
+import org.jclouds.domain.Credentials;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Set;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertNull;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+public class BlobClusterStateStoreTest {
+
+  private static final Logger LOG = LoggerFactory
+      .getLogger(BlobClusterStateStoreTest.class);
+
+  private Configuration getTestConfiguration() throws ConfigurationException {
+    return new PropertiesConfiguration("whirr-core-test.properties");
+  }
+
+  private ClusterSpec getTestClusterSpec() throws Exception {
+    return ClusterSpec.withTemporaryKeys(getTestConfiguration());
+  }
+
+  @Test
+  public void testStoreAndLoadState() throws Exception {
+    ClusterSpec spec = getTestClusterSpec();
+
+    BlobStoreContext context = BlobStoreContextBuilder.build(spec);
+    String container = generateRandomContainerName(context);
+    try {
+      spec.setStateStore("blob");
+      spec.setStateStoreContainer(container);
+
+      Cluster expected = createTestCluster(new String[]{"region/id1", "region/id2"},
+          new String[]{"role1", "role2"});
+
+      BlobClusterStateStore store = new BlobClusterStateStore(spec);
+      store.save(expected);
+
+      /* load and check the stored state */
+      Cluster stored = store.load();
+      Cluster.Instance first = Iterables.getFirst(stored.getInstances(), null);
+      assertNotNull(first);
+
+      assertThat(first.getId(), is("region/id1"));
+      assertThat(first.getRoles().contains("role1"), is(true));
+      assertThat(stored.getInstances().size(), is(2));
+
+      /* destroy stored state and check it no longer exists */
+      store.destroy();
+      expected = store.load();
+      assertNull(expected);
+
+    } finally {
+      LOG.info("Removing temporary container '{}'", container);
+      context.getBlobStore().deleteContainer(container);
+    }
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testInvalidContainerName() throws Exception {
+    ClusterSpec spec = getTestClusterSpec();
+
+    /* underscores are not allowed and it should throw exception */
+    spec.setStateStoreContainer("whirr_test");
+  }
+
+  private String generateRandomContainerName(BlobStoreContext context) {
+    String candidate;
+    do {
+      candidate = RandomStringUtils.randomAlphanumeric(12).toLowerCase();
+    } while(!context.getBlobStore().createContainerInLocation(null, candidate));
+    LOG.info("Created temporary container '{}'", candidate);
+    return candidate;
+  }
+
+  private Cluster createTestCluster(String[] ids, String[] roles) {
+    checkArgument(ids.length == roles.length, "each ID should have a role");
+
+    Credentials credentials = new Credentials("dummy", "dummy");
+    Set<Cluster.Instance> instances = Sets.newLinkedHashSet();
+
+    for(int i = 0; i < ids.length; i++) {
+      String ip = "127.0.0." + (i + 1);
+      instances.add(new Cluster.Instance(credentials,
+        Sets.newHashSet(roles[i]), ip, ip, ids[i], null));
+    }
+
+    return new Cluster(instances);
+  }
+
+}