You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@brooklyn.apache.org by al...@apache.org on 2016/09/30 09:00:28 UTC

[2/9] brooklyn-server git commit: Add tests to SoftwareProcessRebindNotRunningEntityTest

Add tests to SoftwareProcessRebindNotRunningEntityTest


Project: http://git-wip-us.apache.org/repos/asf/brooklyn-server/repo
Commit: http://git-wip-us.apache.org/repos/asf/brooklyn-server/commit/5d252d7e
Tree: http://git-wip-us.apache.org/repos/asf/brooklyn-server/tree/5d252d7e
Diff: http://git-wip-us.apache.org/repos/asf/brooklyn-server/diff/5d252d7e

Branch: refs/heads/master
Commit: 5d252d7ef1ec3f35ce874c05db95c9db2d9e2774
Parents: 001b02c
Author: Aled Sage <al...@gmail.com>
Authored: Mon Sep 26 22:13:38 2016 +0100
Committer: Aled Sage <al...@gmail.com>
Committed: Tue Sep 27 10:36:58 2016 +0100

----------------------------------------------------------------------
 .../core/mgmt/rebind/RebindTestFixture.java     |   4 +
 .../core/internal/ssh/RecordingSshTool.java     |   9 +-
 ...ftwareProcessRebindNotRunningEntityTest.java | 341 ++++++++++++++++---
 3 files changed, 310 insertions(+), 44 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/5d252d7e/core/src/test/java/org/apache/brooklyn/core/mgmt/rebind/RebindTestFixture.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/core/mgmt/rebind/RebindTestFixture.java b/core/src/test/java/org/apache/brooklyn/core/mgmt/rebind/RebindTestFixture.java
index 819b8f0..9a81cea 100644
--- a/core/src/test/java/org/apache/brooklyn/core/mgmt/rebind/RebindTestFixture.java
+++ b/core/src/test/java/org/apache/brooklyn/core/mgmt/rebind/RebindTestFixture.java
@@ -147,6 +147,10 @@ public abstract class RebindTestFixture<T extends StartableApplication> {
         return (newManagementContext != null) ? newManagementContext : origManagementContext;
     }
     
+    protected T app() {
+        return (newApp != null) ? newApp : origApp;
+    }
+    
     public static void waitForTaskCountToBecome(final ManagementContext mgmt, final int allowedMax) {
         waitForTaskCountToBecome(mgmt, allowedMax, false);
     }

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/5d252d7e/core/src/test/java/org/apache/brooklyn/util/core/internal/ssh/RecordingSshTool.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/brooklyn/util/core/internal/ssh/RecordingSshTool.java b/core/src/test/java/org/apache/brooklyn/util/core/internal/ssh/RecordingSshTool.java
index 2ac8acf..04e78cf 100644
--- a/core/src/test/java/org/apache/brooklyn/util/core/internal/ssh/RecordingSshTool.java
+++ b/core/src/test/java/org/apache/brooklyn/util/core/internal/ssh/RecordingSshTool.java
@@ -72,7 +72,7 @@ public class RecordingSshTool implements SshTool {
     }
 
     public interface CustomResponseGenerator {
-        public CustomResponse generate(ExecParams execParams);
+        public CustomResponse generate(ExecParams execParams) throws Exception;
     }
 
     public static class CustomResponse {
@@ -225,7 +225,12 @@ public class RecordingSshTool implements SshTool {
             for (Entry<String, CustomResponseGenerator> entry : customResponses.entrySet()) {
                 if (cmd.matches(entry.getKey())) {
                     CustomResponseGenerator responseGenerator = entry.getValue();
-                    CustomResponse response = responseGenerator.generate(new ExecParams(props, commands, env));
+                    CustomResponse response;
+                    try {
+                        response = responseGenerator.generate(new ExecParams(props, commands, env));
+                    } catch (Exception e) {
+                        throw Exceptions.propagate(e);
+                    }
                     writeCustomResponseStreams(props, response);
                     return response.exitCode;
                 }

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/5d252d7e/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SoftwareProcessRebindNotRunningEntityTest.java
----------------------------------------------------------------------
diff --git a/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SoftwareProcessRebindNotRunningEntityTest.java b/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SoftwareProcessRebindNotRunningEntityTest.java
index 3fb2188..f318d0e 100644
--- a/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SoftwareProcessRebindNotRunningEntityTest.java
+++ b/software/base/src/test/java/org/apache/brooklyn/entity/software/base/SoftwareProcessRebindNotRunningEntityTest.java
@@ -16,83 +16,340 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-
 package org.apache.brooklyn.entity.software.base;
 
-import com.google.common.collect.ImmutableList;
-import org.apache.brooklyn.api.entity.Entity;
+import static org.testng.Assert.assertTrue;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
 import org.apache.brooklyn.api.entity.EntitySpec;
-import org.apache.brooklyn.api.entity.ImplementedBy;
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.api.location.LocationSpec;
+import org.apache.brooklyn.api.location.MachineLocation;
+import org.apache.brooklyn.api.location.MachineProvisioningLocation;
+import org.apache.brooklyn.api.location.NoMachinesAvailableException;
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.core.config.ConfigKeys;
+import org.apache.brooklyn.core.entity.Attributes;
 import org.apache.brooklyn.core.entity.EntityAsserts;
 import org.apache.brooklyn.core.entity.lifecycle.Lifecycle;
-import org.apache.brooklyn.core.test.BrooklynAppUnitTestSupport;
-import org.apache.brooklyn.location.localhost.LocalhostMachineProvisioningLocation;
+import org.apache.brooklyn.core.entity.trait.Startable;
+import org.apache.brooklyn.core.location.AbstractLocation;
+import org.apache.brooklyn.core.mgmt.rebind.RebindTestFixtureWithApp;
+import org.apache.brooklyn.core.test.entity.TestApplication;
+import org.apache.brooklyn.location.byon.FixedListMachineProvisioningLocation;
+import org.apache.brooklyn.location.ssh.SshMachineLocation;
 import org.apache.brooklyn.test.Asserts;
+import org.apache.brooklyn.util.core.internal.ssh.RecordingSshTool;
+import org.apache.brooklyn.util.core.internal.ssh.RecordingSshTool.CustomResponse;
+import org.apache.brooklyn.util.core.internal.ssh.RecordingSshTool.CustomResponseGenerator;
+import org.apache.brooklyn.util.core.internal.ssh.RecordingSshTool.ExecParams;
+import org.apache.brooklyn.util.exceptions.Exceptions;
+import org.apache.brooklyn.util.time.Duration;
+import org.testng.annotations.AfterMethod;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
-import java.util.Date;
-import java.util.List;
+import com.google.common.base.Predicates;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.reflect.TypeToken;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
 
-public class SoftwareProcessRebindNotRunningEntityTest extends BrooklynAppUnitTestSupport {
+public class SoftwareProcessRebindNotRunningEntityTest extends RebindTestFixtureWithApp {
 
-    private List<LocalhostMachineProvisioningLocation> locations;
+    // TODO We'd like to record the fact that we were starting or stopping, rather than just say "on-fire".
+    // For example, we can use the Attributes.SERVICE_NOT_UP_INDICATORS to say what went wrong.
+    
+    // TODO If we fail during provisioningLocation.obtain() or provisioningLocation.release(), then we
+    // should tell the user that a VM might have started being provisioned but been forgotten about; or
+    // that termination of the VM may or may not have completed.
+    // We could use the Attributes.SERVICE_NOT_UP_INDICATORS to achieve that.
+    
+    // TODO Parent app should go on-fire after restart, but it doesn't - it continues saying "starting".
+    // We probably need to set the "service.state.expected" to on-fire on the parent as well (i.e. for 
+    // more things than just SoftwareProcess).
+    //    Sensor: service.notUp.indicators (java.util.Map) = {service.state=Application starting, service-lifecycle-indicators-from-children-and-members=VanillaSoftwareProcessImpl{id=nyijf1980z} is not up}
+    //    Sensor: entity.id (java.lang.String) = nz6qobkstx
+    //    Sensor: application.id (java.lang.String) = nz6qobkstx
+    //    Sensor: catalog.id (java.lang.String) = null
+    //    Sensor: service.isUp (java.lang.Boolean) = false
+    //    Sensor: service.problems (java.util.Map) = {service-lifecycle-indicators-from-children-and-members=Required entity not healthy: VanillaSoftwareProcessImpl{id=nyijf1980z}}
+    //    Sensor: service.state (org.apache.brooklyn.core.entity.lifecycle.Lifecycle) = starting
+    //    Sensor: service.state.expected (org.apache.brooklyn.core.entity.lifecycle.Lifecycle$Transition) = starting @ 1474967564852 / Tue Sep 27 10:12:44 BST 2016
+    //
+    // In each test, we should add at the end:
+    //   EntityAsserts.assertAttributeEqualsEventually(newApp, Attributes.SERVICE_STATE_ACTUAL, Lifecycle.ON_FIRE);
+    //   EntityAsserts.assertAttributeEqualsEventually(newApp, Attributes.SERVICE_UP, false);
 
+    private ListeningExecutorService executor;
+    private LocationSpec<SshMachineLocation> machineSpec;
+    private FixedListMachineProvisioningLocation<?> locationProvisioner;
+    
+    // We track the latches, so we can countDown() them all to unblock them. Otherwise they can
+    // interfere with tearDown by blocking threads.
+    // TODO Longer term, we should investigate/fix that so tearDown finishes promptly no matter what!
+    private List<CountDownLatch> latches;
+    
     @BeforeMethod(alwaysRun=true)
+    @Override
     public void setUp() throws Exception {
         super.setUp();
-        locations =  ImmutableList.of(app.newLocalhostProvisioningLocation());
+
+        latches = Lists.newCopyOnWriteArrayList();
+        
+        machineSpec = LocationSpec.create(SshMachineLocation.class)
+                .configure("address", "1.2.3.4")
+                .configure(SshMachineLocation.SSH_TOOL_CLASS, RecordingSshTool.class.getName());
+        
+        locationProvisioner = app().getManagementContext().getLocationManager().createLocation(LocationSpec.create(FixedListMachineProvisioningLocation.class)
+                .configure(FixedListMachineProvisioningLocation.MACHINE_SPECS, ImmutableList.<LocationSpec<? extends MachineLocation>>of(
+                        machineSpec)));
+
+        executor = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
+        
+        RecordingSshTool.clear();
     }
 
-    //TODO cover more cases and all entity states
+    @AfterMethod(alwaysRun=true)
+    @Override
+    public void tearDown() throws Exception {
+        try {
+            for (CountDownLatch latch : latches) {
+                while (latch.getCount() > 0) {
+                    latch.countDown();
+                }
+            }
+            super.tearDown();
+            if (executor != null) executor.shutdownNow();
+        } finally {
+            RecordingSshTool.clear();
+        }
+    }
+
+    @Override
+    protected TestApplication createApp() {
+        return mgmt().getEntityManager().createEntity(EntitySpec.create(TestApplication.class));
+    }
+
+    @Test
+    public void testRebindWhileWaitingForCheckRunning() throws Exception {
+        final CountDownLatch checkRunningCalledLatch = newLatch(1);
+        RecordingSshTool.setCustomResponse(".*myCheckRunning.*", new CustomResponseGenerator() {
+            @Override
+            public CustomResponse generate(ExecParams execParams) {
+                checkRunningCalledLatch.countDown();
+                return new CustomResponse(1, "", "");
+            }});
+        
+        VanillaSoftwareProcess entity = app().createAndManageChild(EntitySpec.create(VanillaSoftwareProcess.class)
+                .configure(VanillaSoftwareProcess.LAUNCH_COMMAND, "myLaunch")
+                .configure(VanillaSoftwareProcess.CHECK_RUNNING_COMMAND, "myCheckRunning"));
+        
+        startAsync(app(), ImmutableList.of(locationProvisioner));
+        awaitOrFail(checkRunningCalledLatch, Asserts.DEFAULT_LONG_TIMEOUT);
+
+        EntityAsserts.assertAttributeEquals(entity, Attributes.SERVICE_STATE_ACTUAL, Lifecycle.STARTING);
+
+        TestApplication newApp = rebind();
+        final VanillaSoftwareProcess newEntity = (VanillaSoftwareProcess) Iterables.find(newApp.getChildren(), Predicates.instanceOf(VanillaSoftwareProcess.class));
+
+        EntityAsserts.assertAttributeEqualsEventually(newEntity, Attributes.SERVICE_STATE_ACTUAL, Lifecycle.ON_FIRE);
+        EntityAsserts.assertAttributeEqualsEventually(newEntity, Attributes.SERVICE_UP, false);
+    }
+
+    @Test
+    public void testRebindWhileLaunching() throws Exception {
+        final CountDownLatch launchCalledLatch = newLatch(1);
+        final CountDownLatch launchBlockedLatch = newLatch(1);
+        RecordingSshTool.setCustomResponse(".*myLaunch.*", new CustomResponseGenerator() {
+            @Override
+            public CustomResponse generate(ExecParams execParams) throws Exception {
+                launchCalledLatch.countDown();
+                launchBlockedLatch.await();
+                return new CustomResponse(0, "", "");
+            }});
+        
+        VanillaSoftwareProcess entity = app().createAndManageChild(EntitySpec.create(VanillaSoftwareProcess.class)
+                .configure(VanillaSoftwareProcess.LAUNCH_COMMAND, "myLaunch")
+                .configure(VanillaSoftwareProcess.CHECK_RUNNING_COMMAND, "myCheckRunning"));
+        
+        startAsync(app(), ImmutableList.of(locationProvisioner));
+        awaitOrFail(launchCalledLatch, Asserts.DEFAULT_LONG_TIMEOUT);
+
+        EntityAsserts.assertAttributeEquals(entity, Attributes.SERVICE_STATE_ACTUAL, Lifecycle.STARTING);
+
+        TestApplication newApp = rebind();
+        final VanillaSoftwareProcess newEntity = (VanillaSoftwareProcess) Iterables.find(newApp.getChildren(), Predicates.instanceOf(VanillaSoftwareProcess.class));
+
+        EntityAsserts.assertAttributeEqualsEventually(newEntity, Attributes.SERVICE_STATE_ACTUAL, Lifecycle.ON_FIRE);
+        EntityAsserts.assertAttributeEqualsEventually(newEntity, Attributes.SERVICE_UP, false);
+    }
 
     @Test
-    public void testRebindAfterStarting() {
-        MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class));
-        entity.start(locations); //FIXME - NPE
-        //TODO find better solution to rebind starting app than only setting its attributes
-        ((MyServiceImpl) entity).setAttribute(entity.SERVICE_STATE_EXPECTED, new Lifecycle.Transition(Lifecycle.ON_FIRE, new Date()));
-        ((MyServiceImpl) entity).setAttribute(entity.SERVICE_STATE_ACTUAL, Lifecycle.STARTING);
-        ((MyServiceImpl) entity).rebind();
-
-        assertEntityIsOnFire(entity);
+    public void testRebindWhileStoppingProcess() throws Exception {
+        final CountDownLatch stopCalledLatch = newLatch(1);
+        final CountDownLatch stopBlockedLatch = newLatch(1);
+        RecordingSshTool.setCustomResponse(".*myStop.*", new CustomResponseGenerator() {
+            @Override
+            public CustomResponse generate(ExecParams execParams) throws Exception {
+                stopCalledLatch.countDown();
+                stopBlockedLatch.await();
+                return new CustomResponse(0, "", "");
+            }});
+        
+        VanillaSoftwareProcess entity = app().createAndManageChild(EntitySpec.create(VanillaSoftwareProcess.class)
+                .configure(VanillaSoftwareProcess.LAUNCH_COMMAND, "myLaunch")
+                .configure(VanillaSoftwareProcess.STOP_COMMAND, "myStop")
+                .configure(VanillaSoftwareProcess.CHECK_RUNNING_COMMAND, "myCheckRunning"));
+        app().start(ImmutableList.of(locationProvisioner));
+        
+        stopAsync(entity);
+        awaitOrFail(stopCalledLatch, Asserts.DEFAULT_LONG_TIMEOUT);
+
+        EntityAsserts.assertAttributeEquals(entity, Attributes.SERVICE_STATE_ACTUAL, Lifecycle.STOPPING);
+
+        TestApplication newApp = rebind();
+        final VanillaSoftwareProcess newEntity = (VanillaSoftwareProcess) Iterables.find(newApp.getChildren(), Predicates.instanceOf(VanillaSoftwareProcess.class));
+
+        EntityAsserts.assertAttributeEqualsEventually(newEntity, Attributes.SERVICE_STATE_ACTUAL, Lifecycle.ON_FIRE);
+        EntityAsserts.assertAttributeEqualsEventually(newEntity, Attributes.SERVICE_UP, false);
     }
 
     @Test
-    public void testRebindAfterStopping() {
+    public void testRebindWhileProvisioning() throws Exception {
+        final CountDownLatch obtainCalledLatch = newLatch(1);
+        final CountDownLatch obtainBlockedLatch = newLatch(1);
+        MyProvisioningLocation blockingProvisioner = mgmt().getLocationManager().createLocation(LocationSpec.create(MyProvisioningLocation.class)
+                .configure(MyProvisioningLocation.OBTAIN_CALLED_LATCH, obtainCalledLatch)
+                .configure(MyProvisioningLocation.OBTAIN_BLOCKED_LATCH, obtainBlockedLatch)
+                .configure(MyProvisioningLocation.MACHINE_SPEC, machineSpec));
+        
+        VanillaSoftwareProcess entity = app().createAndManageChild(EntitySpec.create(VanillaSoftwareProcess.class)
+                .configure(VanillaSoftwareProcess.LAUNCH_COMMAND, "myLaunch")
+                .configure(VanillaSoftwareProcess.CHECK_RUNNING_COMMAND, "myCheckRunning"));
+        
+        startAsync(app(), ImmutableList.of(blockingProvisioner));
+        awaitOrFail(obtainCalledLatch, Asserts.DEFAULT_LONG_TIMEOUT);
+
+        EntityAsserts.assertAttributeEquals(entity, Attributes.SERVICE_STATE_ACTUAL, Lifecycle.STARTING);
+
+        TestApplication newApp = rebind();
+        final VanillaSoftwareProcess newEntity = (VanillaSoftwareProcess) Iterables.find(newApp.getChildren(), Predicates.instanceOf(VanillaSoftwareProcess.class));
 
+        EntityAsserts.assertAttributeEqualsEventually(newEntity, Attributes.SERVICE_STATE_ACTUAL, Lifecycle.ON_FIRE);
+        EntityAsserts.assertAttributeEqualsEventually(newEntity, Attributes.SERVICE_UP, false);
     }
 
     @Test
-    public void testRebindRunningEntity() {
-        MyService entity = app.createAndManageChild(EntitySpec.create(MyService.class));
-        entity.start(locations);
-        ((MyServiceImpl) entity).rebind();
-        assertEntityIsRunning(entity);
+    public void testRebindWhileTerminatingVm() throws Exception {
+        final CountDownLatch releaseCalledLatch = newLatch(1);
+        final CountDownLatch obtainBlockedLatch = newLatch(1);
+        MyProvisioningLocation blockingProvisioner = mgmt().getLocationManager().createLocation(LocationSpec.create(MyProvisioningLocation.class)
+                .configure(MyProvisioningLocation.RELEASE_CALLED_LATCH, releaseCalledLatch)
+                .configure(MyProvisioningLocation.RELEASE_BLOCKED_LATCH, obtainBlockedLatch)
+                .configure(MyProvisioningLocation.MACHINE_SPEC, machineSpec));
+        
+        VanillaSoftwareProcess entity = app().createAndManageChild(EntitySpec.create(VanillaSoftwareProcess.class)
+                .configure(VanillaSoftwareProcess.LAUNCH_COMMAND, "myLaunch")
+                .configure(VanillaSoftwareProcess.CHECK_RUNNING_COMMAND, "myCheckRunning"));
+        
+        app().start(ImmutableList.of(blockingProvisioner));
+        
+        stopAsync(entity);
+        awaitOrFail(releaseCalledLatch, Asserts.DEFAULT_LONG_TIMEOUT);
+
+        EntityAsserts.assertAttributeEquals(entity, Attributes.SERVICE_STATE_ACTUAL, Lifecycle.STOPPING);
+
+        TestApplication newApp = rebind();
+        final VanillaSoftwareProcess newEntity = (VanillaSoftwareProcess) Iterables.find(newApp.getChildren(), Predicates.instanceOf(VanillaSoftwareProcess.class));
+
+        EntityAsserts.assertAttributeEqualsEventually(newEntity, Attributes.SERVICE_STATE_ACTUAL, Lifecycle.ON_FIRE);
+        EntityAsserts.assertAttributeEqualsEventually(newEntity, Attributes.SERVICE_UP, false);
     }
 
-    //TODO - more precise assert?
-    protected void assertEntityIsOnFire(MyService entity) {
-        EntityAsserts.assertAttributeEquals(entity, entity.SERVICE_STATE_ACTUAL, Lifecycle.ON_FIRE);
-        //TODO assert SERVICE_STATE_EXPECTED
+    protected ListenableFuture<Void> startAsync(final Startable entity, final Collection<? extends Location> locs) {
+        return executor.submit(new Callable<Void>() {
+            @Override public Void call() throws Exception {
+                entity.start(locs);
+                return null;
+            }});
     }
 
-    protected void assertEntityIsRunning(MyService entity) {
-        Asserts.assertTrue(((MyServiceImpl) entity).getDriver().isRunning());
+    protected ListenableFuture<Void> stopAsync(final Startable entity) {
+        return executor.submit(new Callable<Void>() {
+            @Override public Void call() throws Exception {
+                entity.stop();
+                return null;
+            }});
     }
 
-    //TODO
-    @ImplementedBy(MyServiceImpl.class)
-    public interface MyService extends SoftwareProcess {
+    protected void awaitOrFail(CountDownLatch latch, Duration timeout) throws Exception {
+        boolean success = latch.await(timeout.toMilliseconds(), TimeUnit.MILLISECONDS);
+        assertTrue(success, "latch "+latch+" not satisfied in "+timeout);
+    }
+    
+    protected CountDownLatch newLatch(int count) {
+        CountDownLatch result = new CountDownLatch(count);
+        latches.add(result);
+        return result;
     }
 
-    public static class MyServiceImpl extends SoftwareProcessImpl implements MyService {
-        public MyServiceImpl() {}
-        public MyServiceImpl(Entity parent) { super(parent); }
+    public static class MyProvisioningLocation extends AbstractLocation implements MachineProvisioningLocation<SshMachineLocation> {
+        public static final ConfigKey<CountDownLatch> OBTAIN_CALLED_LATCH = ConfigKeys.newConfigKey(CountDownLatch.class, "obtainCalledLatch");
+        public static final ConfigKey<CountDownLatch> OBTAIN_BLOCKED_LATCH = ConfigKeys.newConfigKey(CountDownLatch.class, "obtainBlockedLatch");
+        public static final ConfigKey<CountDownLatch> RELEASE_CALLED_LATCH = ConfigKeys.newConfigKey(CountDownLatch.class, "releaseCalledLatch");
+        public static final ConfigKey<CountDownLatch> RELEASE_BLOCKED_LATCH = ConfigKeys.newConfigKey(CountDownLatch.class, "releaseBlockedLatch");
+        public static final ConfigKey<LocationSpec<SshMachineLocation>> MACHINE_SPEC = ConfigKeys.newConfigKey(
+                new TypeToken<LocationSpec<SshMachineLocation>>() {},
+                "machineSpec");
+
+        @Override
+        public MachineProvisioningLocation<SshMachineLocation> newSubLocation(Map<?, ?> newFlags) {
+            throw new UnsupportedOperationException();
+        }
+        
+        @Override
+        public SshMachineLocation obtain(Map<?,?> flags) throws NoMachinesAvailableException {
+            CountDownLatch calledLatch = config().get(OBTAIN_CALLED_LATCH);
+            CountDownLatch blockedLatch = config().get(OBTAIN_BLOCKED_LATCH);
+            LocationSpec<SshMachineLocation> machineSpec = config().get(MACHINE_SPEC);
+            
+            if (calledLatch != null) calledLatch.countDown();
+            try {
+                if (blockedLatch != null) blockedLatch.await();
+            } catch (InterruptedException e) {
+                throw Exceptions.propagate(e);
+            }
+            return getManagementContext().getLocationManager().createLocation(machineSpec);
+        }
+
+        @Override
+        public void release(SshMachineLocation machine) {
+            CountDownLatch calledLatch = config().get(RELEASE_CALLED_LATCH);
+            CountDownLatch blockedLatch = config().get(RELEASE_BLOCKED_LATCH);
+            
+            if (calledLatch != null) calledLatch.countDown();
+            try {
+                if (blockedLatch != null) blockedLatch.await();
+            } catch (InterruptedException e) {
+                throw Exceptions.propagate(e);
+            }
+        }
 
         @Override
-        public Class getDriverInterface() {
-            return null;
+        public Map getProvisioningFlags(Collection<String> tags) {
+            return Collections.emptyMap();
         }
     }
-}
\ No newline at end of file
+}