You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@brooklyn.apache.org by he...@apache.org on 2015/08/19 13:09:39 UTC

[21/72] [abbrv] incubator-brooklyn git commit: BROOKLYN-162 - apply org.apache package prefix to software-base, tidying package names, and moving a few sensory things to core

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/main/java/org/apache/brooklyn/entity/software/base/SoftwareProcess.java
----------------------------------------------------------------------
diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/software/base/SoftwareProcess.java b/software/base/src/main/java/org/apache/brooklyn/entity/software/base/SoftwareProcess.java
new file mode 100644
index 0000000..573bc00
--- /dev/null
+++ b/software/base/src/main/java/org/apache/brooklyn/entity/software/base/SoftwareProcess.java
@@ -0,0 +1,362 @@
+/*
+ * 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.brooklyn.entity.software.base;
+
+import java.util.Collection;
+import java.util.Map;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.location.MachineProvisioningLocation;
+import org.apache.brooklyn.api.sensor.AttributeSensor;
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.core.config.ConfigKeys;
+import org.apache.brooklyn.core.config.MapConfigKey;
+import org.apache.brooklyn.entity.annotation.Effector;
+import org.apache.brooklyn.entity.core.Attributes;
+import org.apache.brooklyn.entity.core.BrooklynConfigKeys;
+import org.apache.brooklyn.entity.lifecycle.Lifecycle;
+import org.apache.brooklyn.entity.lifecycle.Lifecycle.Transition;
+import org.apache.brooklyn.entity.trait.Startable;
+import org.apache.brooklyn.sensor.core.AttributeSensorAndConfigKey;
+import org.apache.brooklyn.sensor.core.Sensors;
+import org.apache.brooklyn.util.collections.MutableMap;
+import org.apache.brooklyn.util.core.flags.SetFromFlag;
+import org.apache.brooklyn.util.time.Duration;
+
+import com.google.common.annotations.Beta;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.reflect.TypeToken;
+
+public interface SoftwareProcess extends Entity, Startable {
+
+    AttributeSensor<String> HOSTNAME = Attributes.HOSTNAME;
+    AttributeSensor<String> ADDRESS = Attributes.ADDRESS;
+    AttributeSensor<String> SUBNET_HOSTNAME = Attributes.SUBNET_HOSTNAME;
+    AttributeSensor<String> SUBNET_ADDRESS = Attributes.SUBNET_ADDRESS;
+
+    @SuppressWarnings("serial")
+    ConfigKey<Collection<Integer>> REQUIRED_OPEN_LOGIN_PORTS = ConfigKeys.newConfigKey(
+            new TypeToken<Collection<Integer>>() {},
+            "requiredOpenLoginPorts",
+            "The port(s) to be opened, to allow login",
+            ImmutableSet.of(22));
+
+    @SetFromFlag("startTimeout")
+    ConfigKey<Duration> START_TIMEOUT = BrooklynConfigKeys.START_TIMEOUT;
+
+    @SetFromFlag("startLatch")
+    ConfigKey<Boolean> START_LATCH = BrooklynConfigKeys.START_LATCH;
+
+    @SetFromFlag("setupLatch")
+    ConfigKey<Boolean> SETUP_LATCH = BrooklynConfigKeys.SETUP_LATCH;
+
+    @SetFromFlag("installResourcesLatch")
+    ConfigKey<Boolean> INSTALL_RESOURCES_LATCH = BrooklynConfigKeys.INSTALL_RESOURCES_LATCH;
+
+    @SetFromFlag("installLatch")
+    ConfigKey<Boolean> INSTALL_LATCH = BrooklynConfigKeys.INSTALL_LATCH;
+
+    @SetFromFlag("runtimeResourcesLatch")
+    ConfigKey<Boolean> RUNTIME_RESOURCES_LATCH = BrooklynConfigKeys.RUNTIME_RESOURCES_LATCH;
+
+    @SetFromFlag("customizeLatch")
+    ConfigKey<Boolean> CUSTOMIZE_LATCH = BrooklynConfigKeys.CUSTOMIZE_LATCH;
+
+    @SetFromFlag("launchLatch")
+    ConfigKey<Boolean> LAUNCH_LATCH = BrooklynConfigKeys.LAUNCH_LATCH;
+
+    @SetFromFlag("skipStart")
+    ConfigKey<Boolean> ENTITY_STARTED = BrooklynConfigKeys.SKIP_ENTITY_START;
+
+    @SetFromFlag("skipStartIfRunning")
+    ConfigKey<Boolean> SKIP_ENTITY_START_IF_RUNNING = BrooklynConfigKeys.SKIP_ENTITY_START_IF_RUNNING;
+
+    @SetFromFlag("skipInstall")
+    ConfigKey<Boolean> SKIP_INSTALLATION = BrooklynConfigKeys.SKIP_ENTITY_INSTALLATION;
+
+    @SetFromFlag("preInstallCommand")
+    ConfigKey<String> PRE_INSTALL_COMMAND = BrooklynConfigKeys.PRE_INSTALL_COMMAND;
+
+    @SetFromFlag("postInstallCommand")
+    ConfigKey<String> POST_INSTALL_COMMAND = BrooklynConfigKeys.POST_INSTALL_COMMAND;
+
+    @SetFromFlag("preLaunchCommand")
+    ConfigKey<String> PRE_LAUNCH_COMMAND = BrooklynConfigKeys.PRE_LAUNCH_COMMAND;
+
+    @SetFromFlag("postLaunchCommand")
+    ConfigKey<String> POST_LAUNCH_COMMAND = BrooklynConfigKeys.POST_LAUNCH_COMMAND;
+
+    @SetFromFlag("version")
+    ConfigKey<String> SUGGESTED_VERSION = BrooklynConfigKeys.SUGGESTED_VERSION;
+
+    @SetFromFlag("downloadUrl")
+    AttributeSensorAndConfigKey<String,String> DOWNLOAD_URL = Attributes.DOWNLOAD_URL;
+
+    @SetFromFlag("downloadAddonUrls")
+    AttributeSensorAndConfigKey<Map<String,String>,Map<String,String>> DOWNLOAD_ADDON_URLS = Attributes.DOWNLOAD_ADDON_URLS;
+
+    @SetFromFlag("installLabel")
+    ConfigKey<String> INSTALL_UNIQUE_LABEL = BrooklynConfigKeys.INSTALL_UNIQUE_LABEL;
+
+    @SetFromFlag("expandedInstallDir")
+    AttributeSensorAndConfigKey<String,String> EXPANDED_INSTALL_DIR = BrooklynConfigKeys.EXPANDED_INSTALL_DIR;
+
+    @SetFromFlag("installDir")
+    AttributeSensorAndConfigKey<String,String> INSTALL_DIR = BrooklynConfigKeys.INSTALL_DIR;
+    @Deprecated
+    ConfigKey<String> SUGGESTED_INSTALL_DIR = BrooklynConfigKeys.SUGGESTED_INSTALL_DIR;
+
+    @SetFromFlag("runDir")
+    AttributeSensorAndConfigKey<String,String> RUN_DIR = BrooklynConfigKeys.RUN_DIR;
+    @Deprecated
+    ConfigKey<String> SUGGESTED_RUN_DIR = BrooklynConfigKeys.SUGGESTED_RUN_DIR;
+
+    public static final ConfigKey<Boolean> OPEN_IPTABLES = ConfigKeys.newBooleanConfigKey("openIptables", 
+            "Whether to open the INBOUND_PORTS via iptables rules; " +
+            "if true then ssh in to run iptables commands, as part of machine provisioning", false);
+
+    public static final ConfigKey<Boolean> STOP_IPTABLES = ConfigKeys.newBooleanConfigKey("stopIptables", 
+            "Whether to stop iptables entirely; " +
+            "if true then ssh in to stop the iptables service, as part of machine provisioning", false);
+
+    public static final ConfigKey<Boolean> DONT_REQUIRE_TTY_FOR_SUDO = ConfigKeys.newBooleanConfigKey("dontRequireTtyForSudo", 
+            "Whether to explicitly set /etc/sudoers, so don't need tty (will leave unchanged if 'false'); " +
+            "some machines require a tty for sudo; brooklyn by default does not use a tty " +
+            "(so that it can get separate error+stdout streams); you can enable a tty as an " +
+            "option to every ssh command, or you can do it once and " +
+            "modify the machine so that a tty is not subsequently required.",
+            false);
+    
+    /**
+     * Files to be copied to the server before pre-install.
+     * <p>
+     * Map of {@code classpath://foo/file.txt} (or other url) source to destination path,
+     * as {@code subdir/file} relative to installation directory or {@code /absolute/path/to/file}.
+     *
+     * @see #PRE_INSTALL_TEMPLATES
+     */
+    @Beta
+    @SuppressWarnings("serial")
+    @SetFromFlag("preInstallFiles")
+    ConfigKey<Map<String, String>> PRE_INSTALL_FILES = ConfigKeys.newConfigKey(new TypeToken<Map<String, String>>() { },
+            "files.preinstall", "Mapping of files, to be copied before install, to destination name relative to installDir");
+
+    /**
+     * Templates to be filled in and then copied to the server before install.
+     *
+     * @see #PRE_INSTALL_FILES
+     */
+    @Beta
+    @SuppressWarnings("serial")
+    @SetFromFlag("preInstallTemplates")
+    ConfigKey<Map<String, String>> PRE_INSTALL_TEMPLATES = ConfigKeys.newConfigKey(new TypeToken<Map<String, String>>() { },
+            "templates.preinstall", "Mapping of templates, to be filled in and copied before pre-install, to destination name relative to installDir");
+
+    /**
+     * Files to be copied to the server before install.
+     * <p>
+     * Map of {@code classpath://foo/file.txt} (or other url) source to destination path,
+     * as {@code subdir/file} relative to installation directory or {@code /absolute/path/to/file}.
+     *
+     * @see #INSTALL_TEMPLATES
+     */
+    @Beta
+    @SuppressWarnings("serial")
+    @SetFromFlag("installFiles")
+    ConfigKey<Map<String, String>> INSTALL_FILES = ConfigKeys.newConfigKey(new TypeToken<Map<String, String>>() { },
+            "files.install", "Mapping of files, to be copied before install, to destination name relative to installDir");
+
+    /**
+     * Templates to be filled in and then copied to the server before install.
+     *
+     * @see #INSTALL_FILES
+     */
+    @Beta
+    @SuppressWarnings("serial")
+    @SetFromFlag("installTemplates")
+    ConfigKey<Map<String, String>> INSTALL_TEMPLATES = ConfigKeys.newConfigKey(new TypeToken<Map<String, String>>() { },
+            "templates.install", "Mapping of templates, to be filled in and copied before install, to destination name relative to installDir");
+
+    /**
+     * Files to be copied to the server after customisation.
+     * <p>
+     * Map of {@code classpath://foo/file.txt} (or other url) source to destination path,
+     * as {@code subdir/file} relative to runtime directory or {@code /absolute/path/to/file}.
+     *
+     * @see #RUNTIME_TEMPLATES
+     */
+    @Beta
+    @SuppressWarnings("serial")
+    @SetFromFlag("runtimeFiles")
+    ConfigKey<Map<String, String>> RUNTIME_FILES = ConfigKeys.newConfigKey(new TypeToken<Map<String, String>>() { },
+            "files.runtime", "Mapping of files, to be copied before customisation, to destination name relative to runDir");
+
+    /**
+     * Templates to be filled in and then copied to the server after customisation.
+     *
+     * @see #RUNTIME_FILES
+     */
+    @Beta
+    @SuppressWarnings("serial")
+    @SetFromFlag("runtimeTemplates")
+    ConfigKey<Map<String, String>> RUNTIME_TEMPLATES = ConfigKeys.newConfigKey(new TypeToken<Map<String, String>>() { },
+            "templates.runtime", "Mapping of templates, to be filled in and copied before customisation, to destination name relative to runDir");
+
+    @SetFromFlag("env")
+    MapConfigKey<Object> SHELL_ENVIRONMENT = new MapConfigKey<Object>(Object.class,
+            "shell.env", "Map of environment variables to pass to the runtime shell", MutableMap.<String,Object>of());
+
+    @SetFromFlag("provisioningProperties")
+    MapConfigKey<Object> PROVISIONING_PROPERTIES = new MapConfigKey<Object>(Object.class,
+            "provisioning.properties", "Custom properties to be passed in when provisioning a new machine", MutableMap.<String,Object>of());
+
+    @SetFromFlag("maxRebindSensorsDelay")
+    ConfigKey<Duration> MAXIMUM_REBIND_SENSOR_CONNECT_DELAY = ConfigKeys.newConfigKey(Duration.class,
+            "softwareProcess.maxSensorRebindDelay",
+            "The maximum delay to apply when reconnecting sensors when rebinding to this entity. " +
+                    "Brooklyn will wait a random amount of time, up to the value of this config key, to " +
+                    "avoid a thundering herd problem when the entity shares its machine with " +
+                    "several others. Set to null or to 0 to disable any delay.",
+            Duration.TEN_SECONDS);
+
+    /**
+     * Sets the object that manages the sequence of calls of the entity's driver.
+     */
+    @Beta
+    @SetFromFlag("lifecycleEffectorTasks")
+    ConfigKey<SoftwareProcessDriverLifecycleEffectorTasks> LIFECYCLE_EFFECTOR_TASKS = ConfigKeys.newConfigKey(SoftwareProcessDriverLifecycleEffectorTasks.class,
+            "softwareProcess.lifecycleTasks", "An object that handles lifecycle of an entity's associated machine.",
+            new SoftwareProcessDriverLifecycleEffectorTasks());
+
+    ConfigKey<Boolean> RETRIEVE_USAGE_METRICS = ConfigKeys.newBooleanConfigKey(
+            "metrics.usage.retrieve",
+            "Whether to retrieve the usage (e.g. performance) metrics",
+            true);
+
+    /** Controls the behavior when starting (stop, restart) {@link Startable} children as part of the start (stop, restart) effector on this entity
+     * <p>
+     * (NB: restarts are currently not propagated to children in the default {@link SoftwareProcess}
+     * due to the various semantics which may be desired; this may change, but if entities have specific requirements for restart,
+     * developers should either subclass the {@link SoftwareProcessDriverLifecycleEffectorTasks} and/or lean on sensors from the parent */
+    enum ChildStartableMode {
+        /** do nothing with {@link Startable} children */
+        NONE(true, false, false),
+        /** start (stop) {@link Startable} children concurrent with *driver* start (stop),
+         * in foreground, so invoking entity will wait for children to complete.
+         * <p>
+         * if the child requires the parent to reach a particular state before acting,
+         * when running in foreground the parent should communicate its state using sensors
+         * which the child listens for.
+         * note that often sensors at the parent are not activated until it is started,
+         * so the usual sensors connected at an entity may not be available when running in this mode */
+        FOREGROUND(false, false, false),
+        /** as {@link #FOREGROUND} but {@link ChildStartableMode#isLate} */
+        FOREGROUND_LATE(false, false, true),
+        /** start {@link Startable} children concurrent with *driver* start (stop, restart),
+         * but in background, ie disassociated from the effector task at this entity
+         * (so that this entity can complete start/stop independent of children) */
+        BACKGROUND(false, true, false),
+        /** as {@link #BACKGROUND} but {@link ChildStartableMode#isLate} */
+        BACKGROUND_LATE(false, true, true);
+
+        /** whether starting (stopping, restarting) children is disabled */
+        public final boolean isDisabled;
+        /** whether starting (stopping, restarting) children is backgrounded, so parent should not wait on them */
+        public final boolean isBackground;
+        /** whether starting (stopping, restarting) children should be nested, so start occurs after the driver is started,
+         * and stop before the driver is stopped (if false the children operations are concurrent with the parent),
+         * (with restart always being done in parallel though this behaviour may change) */
+        public final boolean isLate;
+
+        private ChildStartableMode(boolean isDisabled, boolean isBackground, boolean isLate) {
+            this.isDisabled = isDisabled;
+            this.isBackground = isBackground;
+            this.isLate = isLate;
+        }
+
+    }
+
+    @SetFromFlag("childStartMode")
+    ConfigKey<ChildStartableMode> CHILDREN_STARTABLE_MODE = ConfigKeys.newConfigKey(ChildStartableMode.class,
+            "children.startable.mode", "Controls behaviour when starting Startable children as part of this entity's lifecycle.",
+            ChildStartableMode.NONE);
+
+    @SuppressWarnings("rawtypes")
+    AttributeSensor<MachineProvisioningLocation> PROVISIONING_LOCATION = Sensors.newSensor(
+            MachineProvisioningLocation.class, "softwareservice.provisioningLocation", "Location used to provision a machine where this is running");
+
+    AttributeSensor<Boolean> SERVICE_PROCESS_IS_RUNNING = Sensors.newBooleanSensor("service.process.isRunning", 
+            "Whether the process for the service is confirmed as running");
+    
+    AttributeSensor<Lifecycle> SERVICE_STATE_ACTUAL = Attributes.SERVICE_STATE_ACTUAL;
+    AttributeSensor<Transition> SERVICE_STATE_EXPECTED = Attributes.SERVICE_STATE_EXPECTED;
+ 
+    AttributeSensor<String> PID_FILE = Sensors.newStringSensor("softwareprocess.pid.file", "PID file");
+
+    @Beta
+    public static class RestartSoftwareParameters {
+        @Beta /** @since 0.7.0 semantics of parameters to restart being explored */
+        public static final ConfigKey<Boolean> RESTART_CHILDREN = ConfigKeys.newConfigKey(Boolean.class, "restartChildren",
+            "Whether to restart children; default false", false);
+
+        @Beta /** @since 0.7.0 semantics of parameters to restart being explored */
+        public static final ConfigKey<Object> RESTART_MACHINE = ConfigKeys.newConfigKey(Object.class, "restartMachine",
+            "Whether to restart/replace the machine provisioned for this entity:  'true', 'false', or 'auto' are supported, "
+            + "with the default being 'auto' which means to restart or reprovision the machine if there is no simpler way known to restart the entity "
+            + "(for example, if the machine is unhealthy, it would not be possible to restart the process, not even via a stop-then-start sequence); "
+            + "if the machine was not provisioned for this entity, this parameter has no effect", 
+            RestartMachineMode.AUTO.toString().toLowerCase());
+        
+        // we supply a typed variant for retrieval; we want the untyped (above) to use lower case as the default in the GUI
+        // (very hard if using enum, since enum takes the name, and RendererHints do not apply to parameters) 
+        @Beta /** @since 0.7.0 semantics of parameters to restart being explored */
+        public static final ConfigKey<RestartMachineMode> RESTART_MACHINE_TYPED = ConfigKeys.newConfigKey(RestartMachineMode.class, "restartMachine");
+            
+        public enum RestartMachineMode { TRUE, FALSE, AUTO }
+    }
+
+    @Beta
+    public static class StopSoftwareParameters {
+        //IF_NOT_STOPPED includes STARTING, STOPPING, RUNNING
+        public enum StopMode { ALWAYS, IF_NOT_STOPPED, NEVER };
+
+        @Beta /** @since 0.7.0 semantics of parameters to restart being explored */
+        public static final ConfigKey<StopMode> STOP_PROCESS_MODE = ConfigKeys.newConfigKey(StopMode.class, "stopProcessMode",
+                "When to stop the process with regard to the entity state. " +
+                "ALWAYS will try to stop the process even if the entity is marked as stopped, " +
+                "IF_NOT_STOPPED stops the process only if the entity is not marked as stopped, " +
+                "NEVER doesn't stop the process.", StopMode.IF_NOT_STOPPED);
+
+        @Beta /** @since 0.7.0 semantics of parameters to restart being explored */
+        public static final ConfigKey<StopMode> STOP_MACHINE_MODE = ConfigKeys.newConfigKey(StopMode.class, "stopMachineMode",
+                "When to stop the machine with regard to the entity state. " +
+                "ALWAYS will try to stop the machine even if the entity is marked as stopped, " +
+                "IF_NOT_STOPPED stops the machine only if the entity is not marked as stopped, " +
+                "NEVER doesn't stop the machine.", StopMode.IF_NOT_STOPPED);
+    }
+    
+    // NB: the START, STOP, and RESTART effectors themselves are (re)defined by MachineLifecycleEffectorTasks
+
+    /**
+     * @since 0.8.0
+     */
+    @Effector(description="Populates the attribute service.notUp.diagnostics, with any available health indicators")
+    @Beta
+    void populateServiceNotUpDiagnostics();
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/main/java/org/apache/brooklyn/entity/software/base/SoftwareProcessDriver.java
----------------------------------------------------------------------
diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/software/base/SoftwareProcessDriver.java b/software/base/src/main/java/org/apache/brooklyn/entity/software/base/SoftwareProcessDriver.java
new file mode 100644
index 0000000..8221282
--- /dev/null
+++ b/software/base/src/main/java/org/apache/brooklyn/entity/software/base/SoftwareProcessDriver.java
@@ -0,0 +1,75 @@
+/*
+ * 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.brooklyn.entity.software.base;
+
+import org.apache.brooklyn.api.entity.drivers.EntityDriver;
+import org.apache.brooklyn.api.internal.EntityLocal;
+import org.apache.brooklyn.entity.trait.Startable;
+
+/**
+ * The {@link EntityDriver} for a {@link SoftwareProcess}.
+ *
+ * <p/>
+ * In many cases it is cleaner to store entity lifecycle effectors (and sometimes other implementations) in a class to
+ * which the entity delegates.  Classes implementing this interface provide this delegate, often inheriting utilities
+ * specific to a particular transport (e.g. ssh) shared among many different entities.
+ * <p/>
+ * In this way, it is also possible for entities to cleanly support multiple mechanisms for start/stop and other methods.
+ */
+public interface SoftwareProcessDriver extends EntityDriver {
+
+    /**
+     * The entity whose components we are controlling.
+     */
+    EntityLocal getEntity();
+
+    /**
+     * Whether the entity components have started.
+     */
+    boolean isRunning();
+
+    /**
+     * Rebinds the driver to a pre-existing software process.
+     */
+    void rebind();
+
+    /**
+     * Performs software start (or queues tasks to do this)
+     */
+    void start();
+
+    /**
+     * Performs software restart (or queues tasks to do this).
+     * Unlike stop/start implementations here are expected to update SERVICE_STATE for STOPPING and STARTING
+     * as appropriate (but framework will set RUNNING afterwards, after detecting it is running).
+     * @see Startable#restart()
+     */
+    void restart();
+
+    /**
+     * Performs software stop (or queues tasks to do this) 
+     * @see Startable#stop()
+     */
+    void stop();
+
+    /**
+     * Kills the process, ungracefully and immediately where possible (e.g. with `kill -9`).
+     */
+    void kill();
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/main/java/org/apache/brooklyn/entity/software/base/SoftwareProcessDriverLifecycleEffectorTasks.java
----------------------------------------------------------------------
diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/software/base/SoftwareProcessDriverLifecycleEffectorTasks.java b/software/base/src/main/java/org/apache/brooklyn/entity/software/base/SoftwareProcessDriverLifecycleEffectorTasks.java
new file mode 100644
index 0000000..56dfcb2
--- /dev/null
+++ b/software/base/src/main/java/org/apache/brooklyn/entity/software/base/SoftwareProcessDriverLifecycleEffectorTasks.java
@@ -0,0 +1,261 @@
+/*
+ * 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.brooklyn.entity.software.base;
+
+import java.util.Map;
+
+import org.apache.brooklyn.api.location.MachineLocation;
+import org.apache.brooklyn.api.location.MachineProvisioningLocation;
+import org.apache.brooklyn.api.mgmt.TaskAdaptable;
+import org.apache.brooklyn.entity.core.Attributes;
+import org.apache.brooklyn.entity.core.Entities;
+import org.apache.brooklyn.entity.lifecycle.Lifecycle;
+import org.apache.brooklyn.entity.lifecycle.ServiceStateLogic;
+import org.apache.brooklyn.entity.software.base.SoftwareProcess.ChildStartableMode;
+import org.apache.brooklyn.entity.software.base.SoftwareProcess.RestartSoftwareParameters;
+import org.apache.brooklyn.entity.software.base.SoftwareProcess.RestartSoftwareParameters.RestartMachineMode;
+import org.apache.brooklyn.entity.software.base.lifecycle.MachineLifecycleEffectorTasks;
+import org.apache.brooklyn.entity.trait.StartableMethods;
+import org.apache.brooklyn.util.core.config.ConfigBag;
+import org.apache.brooklyn.util.core.task.DynamicTasks;
+import org.apache.brooklyn.util.text.Strings;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.annotations.Beta;
+import com.google.common.base.Supplier;
+
+/** Thin shim delegating to driver to do start/stop/restart, wrapping as tasks,
+ * with common code pulled up to {@link MachineLifecycleEffectorTasks} for non-driver usage 
+ * @since 0.6.0 */
+@Beta
+public class SoftwareProcessDriverLifecycleEffectorTasks extends MachineLifecycleEffectorTasks {
+    
+    private static final Logger log = LoggerFactory.getLogger(SoftwareProcessDriverLifecycleEffectorTasks.class);
+    
+    public void restart(ConfigBag parameters) {
+        RestartMachineMode isRestartMachine = parameters.get(RestartSoftwareParameters.RESTART_MACHINE_TYPED);
+        if (isRestartMachine==null) 
+            isRestartMachine=RestartMachineMode.AUTO;
+        if (isRestartMachine==RestartMachineMode.AUTO) 
+            isRestartMachine = getDefaultRestartStopsMachine() ? RestartMachineMode.TRUE : RestartMachineMode.FALSE; 
+
+        if (isRestartMachine==RestartMachineMode.TRUE) {
+            log.debug("restart of "+entity()+" requested be applied at machine level");
+            super.restart(parameters);
+            return;
+        }
+        
+        DynamicTasks.queue("pre-restart", new PreRestartTask());
+
+        log.debug("restart of "+entity()+" appears to have driver and hostname - doing driver-level restart");
+        entity().getDriver().restart();
+        
+        restartChildren(parameters);
+        
+        DynamicTasks.queue("post-restart", new PostRestartTask());
+    }
+
+    private class PreRestartTask implements Runnable {
+        @Override
+        public void run() {
+            preRestartCustom();
+        }
+    }
+
+    private class PostRestartTask implements Runnable {
+        @Override
+        public void run() {
+            postStartCustom();
+            postRestartCustom();
+            ServiceStateLogic.setExpectedState(entity(), Lifecycle.RUNNING);
+        }
+    }
+    
+    @Override
+    protected boolean getDefaultRestartStopsMachine() {
+        if (entity().getDriver() == null) {
+            log.debug("restart of "+entity()+" has no driver - doing machine-level restart");
+            return true;
+        }
+
+        if (Strings.isEmpty(entity().getAttribute(Attributes.HOSTNAME))) {
+            log.debug("restart of "+entity()+" has no hostname - doing machine-level restart");
+            return true;
+        }
+        
+        return false;
+    }
+    
+    @Override
+    protected SoftwareProcessImpl entity() {
+        return (SoftwareProcessImpl) super.entity();
+    }
+    
+    @Override
+    protected Map<String, Object> obtainProvisioningFlags(final MachineProvisioningLocation<?> location) {
+        return entity().obtainProvisioningFlags(location);
+    }
+     
+    @Override
+    protected void preStartCustom(MachineLocation machine) {
+        entity().initDriver(machine);
+
+        // Note: must only apply config-sensors after adding to locations and creating driver; 
+        // otherwise can't do things like acquire free port from location, or allowing driver to set up ports
+        super.preStartCustom(machine);
+        
+        entity().preStart();
+    }
+
+    /** returns how children startables should be handled (reporting none for efficiency if there are no children) */
+    protected ChildStartableMode getChildrenStartableModeEffective() {
+        if (entity().getChildren().isEmpty()) return ChildStartableMode.NONE;
+        ChildStartableMode result = entity().getConfig(SoftwareProcess.CHILDREN_STARTABLE_MODE);
+        if (result!=null) return result;
+        return ChildStartableMode.NONE;
+    }
+
+    @Override
+    protected String startProcessesAtMachine(final Supplier<MachineLocation> machineS) {
+        ChildStartableMode mode = getChildrenStartableModeEffective();
+        TaskAdaptable<?> children = null;
+        if (!mode.isDisabled) {
+            children = StartableMethods.startingChildren(entity(), machineS.get());
+            // submit rather than queue so it runs in parallel
+            // (could also wrap as parallel task with driver.start() -
+            // but only benefit is that child starts show as child task,
+            // rather than bg task, so not worth the code complexity)
+            if (!mode.isLate) Entities.submit(entity(), children);
+        }
+        
+        entity().getDriver().start();
+        String result = "Started with driver "+entity().getDriver();
+        
+        if (!mode.isDisabled) {
+            if (mode.isLate) {
+                DynamicTasks.waitForLast();
+                if (mode.isBackground) {
+                    Entities.submit(entity(), children);
+                } else {
+                    // when running foreground late, there is no harm here in queueing
+                    DynamicTasks.queue(children);
+                }
+            }
+            if (!mode.isBackground) children.asTask().getUnchecked();
+            result += "; children started "+mode;
+        }
+        return result;
+    }
+
+    @Override
+    protected void postStartCustom() {
+        entity().postDriverStart();
+        if (entity().connectedSensors) {
+            // many impls aren't idempotent - though they should be!
+            log.debug("skipping connecting sensors for "+entity()+" in driver-tasks postStartCustom because already connected (e.g. restarting)");
+        } else {
+            log.debug("connecting sensors for "+entity()+" in driver-tasks postStartCustom because already connected (e.g. restarting)");
+            entity().connectSensors();
+        }
+        entity().waitForServiceUp();
+        entity().postStart();
+    }
+    
+    @Override
+    protected void preStopConfirmCustom() {
+        super.preStopConfirmCustom();
+        
+        entity().preStopConfirmCustom();
+    }
+    
+    @Override
+    protected void preStopCustom() {
+        super.preStopCustom();
+        
+        entity().preStop();
+    }
+
+    @Override
+    protected void preRestartCustom() {
+        super.preRestartCustom();
+        
+        entity().preRestart();
+    }
+
+    @Override
+    protected void postRestartCustom() {
+        super.postRestartCustom();
+        
+        entity().postRestart();
+    }
+
+    @Override
+    protected String stopProcessesAtMachine() {
+        String result;
+        
+        ChildStartableMode mode = getChildrenStartableModeEffective();
+        TaskAdaptable<?> children = null;
+        Exception childException = null;
+        
+        if (!mode.isDisabled) {
+            children = StartableMethods.stoppingChildren(entity());
+            
+            if (mode.isBackground || !mode.isLate) Entities.submit(entity(), children);
+            else {
+                DynamicTasks.queue(children);
+                try {
+                    DynamicTasks.waitForLast();
+                } catch (Exception e) {
+                    childException = e;
+                }
+            }
+        }
+        
+        if (entity().getDriver() != null) { 
+            entity().getDriver().stop();
+            result = "Driver stop completed";
+        } else {
+            result = "No driver (nothing to do here)";
+        }
+
+        if (!mode.isDisabled && !mode.isBackground) {
+            try {
+                children.asTask().get();
+            } catch (Exception e) {
+                childException = e;
+                log.debug("Error stopping children; continuing and will rethrow if no other errors", e);
+            }            
+        }
+        
+        if (childException!=null)
+            throw new IllegalStateException(result+"; but error stopping child: "+childException, childException);
+
+        return result;
+    }
+    
+    @Override
+    protected void postStopCustom() {
+        super.postStopCustom();
+        
+        entity().postStop();
+    }
+
+}
+

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/main/java/org/apache/brooklyn/entity/software/base/SoftwareProcessImpl.java
----------------------------------------------------------------------
diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/software/base/SoftwareProcessImpl.java b/software/base/src/main/java/org/apache/brooklyn/entity/software/base/SoftwareProcessImpl.java
new file mode 100644
index 0000000..a3e8bcd
--- /dev/null
+++ b/software/base/src/main/java/org/apache/brooklyn/entity/software/base/SoftwareProcessImpl.java
@@ -0,0 +1,651 @@
+/*
+ * 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.brooklyn.entity.software.base;
+
+import groovy.time.TimeDuration;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.concurrent.Callable;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.drivers.DriverDependentEntity;
+import org.apache.brooklyn.api.entity.drivers.EntityDriverManager;
+import org.apache.brooklyn.api.internal.EntityLocal;
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.api.location.MachineLocation;
+import org.apache.brooklyn.api.location.MachineProvisioningLocation;
+import org.apache.brooklyn.api.location.PortRange;
+import org.apache.brooklyn.api.mgmt.Task;
+import org.apache.brooklyn.api.sensor.EnricherSpec;
+import org.apache.brooklyn.api.sensor.SensorEvent;
+import org.apache.brooklyn.api.sensor.SensorEventListener;
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.entity.core.AbstractEntity;
+import org.apache.brooklyn.entity.core.Attributes;
+import org.apache.brooklyn.entity.core.BrooklynConfigKeys;
+import org.apache.brooklyn.entity.core.Entities;
+import org.apache.brooklyn.entity.lifecycle.Lifecycle;
+import org.apache.brooklyn.entity.lifecycle.ServiceStateLogic;
+import org.apache.brooklyn.entity.lifecycle.Lifecycle.Transition;
+import org.apache.brooklyn.entity.lifecycle.ServiceStateLogic.ServiceNotUpLogic;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.apache.brooklyn.location.basic.LocationConfigKeys;
+import org.apache.brooklyn.location.basic.SshMachineLocation;
+import org.apache.brooklyn.location.cloud.CloudLocationConfig;
+import org.apache.brooklyn.sensor.enricher.AbstractEnricher;
+import org.apache.brooklyn.sensor.feed.function.FunctionFeed;
+import org.apache.brooklyn.sensor.feed.function.FunctionPollConfig;
+import org.apache.brooklyn.util.collections.MutableMap;
+import org.apache.brooklyn.util.collections.MutableSet;
+import org.apache.brooklyn.util.core.config.ConfigBag;
+import org.apache.brooklyn.util.core.flags.TypeCoercions;
+import org.apache.brooklyn.util.core.task.DynamicTasks;
+import org.apache.brooklyn.util.core.task.Tasks;
+import org.apache.brooklyn.util.exceptions.Exceptions;
+import org.apache.brooklyn.util.guava.Maybe;
+import org.apache.brooklyn.util.time.CountdownTimer;
+import org.apache.brooklyn.util.time.Duration;
+import org.apache.brooklyn.util.time.Time;
+
+import com.google.common.base.Functions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+import com.google.common.reflect.TypeToken;
+
+/**
+ * An {@link Entity} representing a piece of software which can be installed, run, and controlled.
+ * A single such entity can only run on a single {@link MachineLocation} at a time (you can have multiple on the machine). 
+ * It typically takes config keys for suggested versions, filesystem locations to use, and environment variables to set.
+ * <p>
+ * It exposes sensors for service state (Lifecycle) and status (String), and for host info, log file location.
+ */
+public abstract class SoftwareProcessImpl extends AbstractEntity implements SoftwareProcess, DriverDependentEntity {
+    private static final Logger log = LoggerFactory.getLogger(SoftwareProcessImpl.class);
+    
+    private transient SoftwareProcessDriver driver;
+
+    /** @see #connectServiceUpIsRunning() */
+    private volatile FunctionFeed serviceProcessIsRunning;
+
+    protected boolean connectedSensors = false;
+    
+    public SoftwareProcessImpl() {
+        super(MutableMap.of(), null);
+    }
+    public SoftwareProcessImpl(Entity parent) {
+        this(MutableMap.of(), parent);
+    }
+    public SoftwareProcessImpl(Map properties) {
+        this(properties, null);
+    }
+    public SoftwareProcessImpl(Map properties, Entity parent) {
+        super(properties, parent);
+    }
+
+    protected void setProvisioningLocation(MachineProvisioningLocation val) {
+        if (getAttribute(PROVISIONING_LOCATION) != null) throw new IllegalStateException("Cannot change provisioning location: existing="+getAttribute(PROVISIONING_LOCATION)+"; new="+val);
+        setAttribute(PROVISIONING_LOCATION, val);
+    }
+    
+    protected MachineProvisioningLocation getProvisioningLocation() {
+        return getAttribute(PROVISIONING_LOCATION);
+    }
+    
+    @Override
+    public SoftwareProcessDriver getDriver() {
+        return driver;
+    }
+
+    protected SoftwareProcessDriver newDriver(MachineLocation loc){
+        EntityDriverManager entityDriverManager = getManagementContext().getEntityDriverManager();
+        return (SoftwareProcessDriver)entityDriverManager.build(this, loc);
+    }
+
+    protected MachineLocation getMachineOrNull() {
+        return Iterables.get(Iterables.filter(getLocations(), MachineLocation.class), 0, null);
+    }
+
+    @Override
+    public void init() {
+        super.init();
+        getLifecycleEffectorTasks().attachLifecycleEffectors(this);
+    }
+    
+    @Override
+    protected void initEnrichers() {
+        super.initEnrichers();
+        ServiceNotUpLogic.updateNotUpIndicator(this, SERVICE_PROCESS_IS_RUNNING, "No information yet on whether this service is running");
+        // add an indicator above so that if is_running comes through, the map is cleared and an update is guaranteed
+        addEnricher(EnricherSpec.create(UpdatingNotUpFromServiceProcessIsRunning.class).uniqueTag("service-process-is-running-updating-not-up"));
+        addEnricher(EnricherSpec.create(ServiceNotUpDiagnosticsCollector.class).uniqueTag("service-not-up-diagnostics-collector"));
+    }
+    
+    /**
+     * @since 0.8.0
+     */
+    protected static class ServiceNotUpDiagnosticsCollector extends AbstractEnricher implements SensorEventListener<Object> {
+        public ServiceNotUpDiagnosticsCollector() {
+        }
+        
+        @Override
+        public void setEntity(EntityLocal entity) {
+            super.setEntity(entity);
+            if (!(entity instanceof SoftwareProcess)) {
+                throw new IllegalArgumentException("Expected SoftwareProcess, but got entity "+entity);
+            }
+            subscribe(entity, Attributes.SERVICE_UP, this);
+            onUpdated();
+        }
+
+        @Override
+        public void onEvent(SensorEvent<Object> event) {
+            onUpdated();
+        }
+
+        protected void onUpdated() {
+            Boolean up = entity.getAttribute(SERVICE_UP);
+            if (up == null || up) {
+                entity.setAttribute(ServiceStateLogic.SERVICE_NOT_UP_DIAGNOSTICS, ImmutableMap.<String, Object>of());
+            } else {
+                ((SoftwareProcess)entity).populateServiceNotUpDiagnostics();
+            }
+        }
+    }
+    
+    @Override
+    public void populateServiceNotUpDiagnostics() {
+        if (getDriver() == null) {
+            ServiceStateLogic.updateMapSensorEntry(this, ServiceStateLogic.SERVICE_NOT_UP_DIAGNOSTICS, "driver", "No driver");
+            return;
+        }
+
+        Location loc = getDriver().getLocation();
+        if (loc instanceof SshMachineLocation) {
+            if (!((SshMachineLocation)loc).isSshable()) {
+                ServiceStateLogic.updateMapSensorEntry(
+                        this, 
+                        ServiceStateLogic.SERVICE_NOT_UP_DIAGNOSTICS, 
+                        "sshable", 
+                        "The machine for this entity does not appear to be sshable");
+            }
+            return;
+        }
+
+        boolean processIsRunning = getDriver().isRunning();
+        if (!processIsRunning) {
+            ServiceStateLogic.updateMapSensorEntry(
+                    this, 
+                    ServiceStateLogic.SERVICE_NOT_UP_DIAGNOSTICS, 
+                    SERVICE_PROCESS_IS_RUNNING.getName(), 
+                    "The software process for this entity does not appear to be running");
+        }
+    }
+
+    /** subscribes to SERVICE_PROCESS_IS_RUNNING and SERVICE_UP; the latter has no effect if the former is set,
+     * but to support entities which set SERVICE_UP directly we want to make sure that the absence of 
+     * SERVICE_PROCESS_IS_RUNNING does not trigger any not-up indicators */
+    protected static class UpdatingNotUpFromServiceProcessIsRunning extends AbstractEnricher implements SensorEventListener<Object> {
+        public UpdatingNotUpFromServiceProcessIsRunning() {}
+        
+        @Override
+        public void setEntity(EntityLocal entity) {
+            super.setEntity(entity);
+            subscribe(entity, SERVICE_PROCESS_IS_RUNNING, this);
+            subscribe(entity, Attributes.SERVICE_UP, this);
+            onUpdated();
+        }
+
+        @Override
+        public void onEvent(SensorEvent<Object> event) {
+            onUpdated();
+        }
+
+        protected void onUpdated() {
+            Boolean isRunning = entity.getAttribute(SERVICE_PROCESS_IS_RUNNING);
+            if (Boolean.FALSE.equals(isRunning)) {
+                ServiceNotUpLogic.updateNotUpIndicator(entity, SERVICE_PROCESS_IS_RUNNING, "The software process for this entity does not appear to be running");
+                return;
+            }
+            if (Boolean.TRUE.equals(isRunning)) {
+                ServiceNotUpLogic.clearNotUpIndicator(entity, SERVICE_PROCESS_IS_RUNNING);
+                return;
+            }
+            // no info on "isRunning"
+            Boolean isUp = entity.getAttribute(Attributes.SERVICE_UP);
+            if (Boolean.TRUE.equals(isUp)) {
+                // if service explicitly set up, then don't apply our rule
+                ServiceNotUpLogic.clearNotUpIndicator(entity, SERVICE_PROCESS_IS_RUNNING);
+                return;
+            }
+            // service not up, or no info
+            ServiceNotUpLogic.updateNotUpIndicator(entity, SERVICE_PROCESS_IS_RUNNING, "No information on whether this service is running");
+        }
+    }
+    
+    /**
+     * Called before driver.start; guarantees the driver will exist, and locations will have been set.
+     */
+    protected void preStart() {
+    }
+    
+    /**
+     * Called after driver.start(). Default implementation is to wait to confirm the driver 
+     * definitely started the process.
+     */
+    protected void postDriverStart() {
+        waitForEntityStart();
+    }
+
+    /**
+     * For binding to the running app (e.g. connecting sensors to registry). Will be called
+     * on start() and on rebind().
+     * <p>
+     * Implementations should be idempotent (ie tell whether sensors already connected),
+     * though the framework is pretty good about not calling when already connected. 
+     * TODO improve the framework's feed system to detect duplicate additions
+     */
+    protected void connectSensors() {
+        connectedSensors = true;
+    }
+
+    /**
+     * For connecting the {@link #SERVICE_UP} sensor to the value of the {@code getDriver().isRunning()} expression.
+     * <p>
+     * Should be called inside {@link #connectSensors()}.
+     *
+     * @see #disconnectServiceUpIsRunning()
+     */
+    protected void connectServiceUpIsRunning() {
+        serviceProcessIsRunning = FunctionFeed.builder()
+                .entity(this)
+                .period(Duration.FIVE_SECONDS)
+                .poll(new FunctionPollConfig<Boolean, Boolean>(SERVICE_PROCESS_IS_RUNNING)
+                        .suppressDuplicates(true)
+                        .onException(Functions.constant(Boolean.FALSE))
+                        .callable(new Callable<Boolean>() {
+                            public Boolean call() {
+                                return getDriver().isRunning();
+                            }
+                        }))
+                .build();
+    }
+
+    /**
+     * For disconnecting the {@link #SERVICE_UP} feed.
+     * <p>
+     * Should be called from {@link #disconnectSensors()}.
+     *
+     * @see #connectServiceUpIsRunning()
+     */
+    protected void disconnectServiceUpIsRunning() {
+        if (serviceProcessIsRunning != null) serviceProcessIsRunning.stop();
+        // set null so the SERVICE_UP enricher runs (possibly removing it), then remove so everything is removed
+        // TODO race because the is-running check may be mid-task
+        setAttribute(SERVICE_PROCESS_IS_RUNNING, null);
+        removeAttribute(SERVICE_PROCESS_IS_RUNNING);
+    }
+
+    /**
+     * Called after the rest of start has completed (after {@link #connectSensors()} and {@link #waitForServiceUp()})
+     */
+    protected void postStart() {
+    }
+    
+    protected void preStopConfirmCustom() {
+    }
+    
+    protected void preStop() {
+        // note asymmetry that disconnectSensors is done in the entity not the driver
+        // whereas on start the *driver* calls connectSensors, before calling postStart,
+        // ie waiting for the entity truly to be started before calling postStart;
+        // TODO feels like that confusion could be eliminated with a single place for pre/post logic!)
+        log.debug("disconnecting sensors for "+this+" in entity.preStop");
+        disconnectSensors();
+        
+        // Must set the serviceProcessIsRunning explicitly to false - we've disconnected the sensors
+        // so nothing else will.
+        // Otherwise, if restarted, there will be no change to serviceProcessIsRunning, so the
+        // serviceUpIndicators will not change, so serviceUp will not be reset.
+        // TODO Is there a race where disconnectSensors could leave a task of the feeds still running
+        // which could set serviceProcessIsRunning to true again before the task completes and the feed
+        // is fully terminated?
+        setAttribute(SoftwareProcess.SERVICE_PROCESS_IS_RUNNING, false);
+    }
+
+    /**
+     * Called after the rest of stop has completed (after VM deprovisioned, but before state set to STOPPED)
+     */
+    protected void postStop() {
+    }
+
+    /**
+     * Called before driver.restart; guarantees the driver will exist, and locations will have been set.
+     */
+    protected void preRestart() {
+    }
+
+    protected void postRestart() {
+    }
+
+    /**
+     * For disconnecting from the running app. Will be called on stop.
+     */
+    protected void disconnectSensors() {
+        connectedSensors = false;
+    }
+
+    /**
+     * Called after this entity is fully rebound (i.e. it is fully managed).
+     */
+    protected void postRebind() {
+    }
+    
+    protected void callRebindHooks() {
+        Duration configuredMaxDelay = getConfig(MAXIMUM_REBIND_SENSOR_CONNECT_DELAY);
+        if (configuredMaxDelay == null || Duration.ZERO.equals(configuredMaxDelay)) {
+            connectSensors();
+        } else {
+            long delay = (long) (Math.random() * configuredMaxDelay.toMilliseconds());
+            log.debug("Scheduled reconnection of sensors on {} in {}ms", this, delay);
+            Timer timer = new Timer();
+            timer.schedule(new TimerTask() {
+                @Override public void run() {
+                    try {
+                        if (getManagementSupport().isNoLongerManaged()) {
+                            log.debug("Entity {} no longer managed; ignoring scheduled connect sensors on rebind", SoftwareProcessImpl.this);
+                            return;
+                        }
+                        connectSensors();
+                    } catch (Throwable e) {
+                        log.warn("Problem connecting sensors on rebind of "+SoftwareProcessImpl.this, e);
+                        Exceptions.propagateIfFatal(e);
+                    }
+                }
+            }, delay);
+        }
+        // don't wait here - it may be long-running, e.g. if remote entity has died, and we don't want to block rebind waiting or cause it to fail
+        // the service will subsequently show service not up and thus failure
+//        waitForServiceUp();
+    }
+
+    @Override 
+    public void onManagementStarting() {
+        super.onManagementStarting();
+        
+        Lifecycle state = getAttribute(SERVICE_STATE_ACTUAL);
+        if (state == null || state == Lifecycle.CREATED) {
+            // Expect this is a normal start() sequence (i.e. start() will subsequently be called)
+            setAttribute(SERVICE_UP, false);
+            ServiceStateLogic.setExpectedState(this, Lifecycle.CREATED);
+            // force actual to be created because this is expected subsequently
+            setAttribute(SERVICE_STATE_ACTUAL, Lifecycle.CREATED);
+        }
+    }
+    
+    @Override 
+    public void onManagementStarted() {
+        super.onManagementStarted();
+        
+        Lifecycle state = getAttribute(SERVICE_STATE_ACTUAL);
+        if (state != null && state != Lifecycle.CREATED) {
+            postRebind();
+        }
+    }
+    
+    @Override
+    public void rebind() {
+        //SERVICE_STATE_ACTUAL might be ON_FIRE due to a temporary condition (problems map non-empty)
+        //Only if the expected state is ON_FIRE then the entity has permanently failed.
+        Transition expectedState = getAttribute(SERVICE_STATE_EXPECTED);
+        if (expectedState == null || expectedState.getState() != Lifecycle.RUNNING) {
+            log.warn("On rebind of {}, not calling software process rebind hooks because expected state is {}", this, expectedState);
+            return;
+        }
+
+        Lifecycle actualState = getAttribute(SERVICE_STATE_ACTUAL);
+        if (actualState == null || actualState != Lifecycle.RUNNING) {
+            log.warn("Rebinding entity {}, even though actual state is {}. Expected state is {}", new Object[] {this, actualState, expectedState});
+        }
+
+        // e.g. rebinding to a running instance
+        // FIXME For rebind, what to do about things in STARTING or STOPPING state?
+        // FIXME What if location not set?
+        log.info("Rebind {} connecting to pre-running service", this);
+        
+        MachineLocation machine = getMachineOrNull();
+        if (machine != null) {
+            initDriver(machine);
+            driver.rebind();
+            if (log.isDebugEnabled()) log.debug("On rebind of {}, re-created driver {}", this, driver);
+        } else {
+            log.info("On rebind of {}, no MachineLocation found (with locations {}) so not generating driver",
+                    this, getLocations());
+        }
+        
+        callRebindHooks();
+    }
+    
+    public void waitForServiceUp() {
+        Duration timeout = getConfig(BrooklynConfigKeys.START_TIMEOUT);
+        waitForServiceUp(timeout);
+    }
+    public void waitForServiceUp(Duration duration) {
+        Entities.waitForServiceUp(this, duration);
+    }
+    public void waitForServiceUp(TimeDuration duration) {
+        waitForServiceUp(duration.toMilliseconds(), TimeUnit.MILLISECONDS);
+    }
+    public void waitForServiceUp(long duration, TimeUnit units) {
+        Entities.waitForServiceUp(this, Duration.of(duration, units));
+    }
+
+    protected Map<String,Object> obtainProvisioningFlags(MachineProvisioningLocation location) {
+        ConfigBag result = ConfigBag.newInstance(location.getProvisioningFlags(ImmutableList.of(getClass().getName())));
+        result.putAll(getConfig(PROVISIONING_PROPERTIES));
+        if (result.get(CloudLocationConfig.INBOUND_PORTS) == null) {
+            Collection<Integer> ports = getRequiredOpenPorts();
+            Object requiredPorts = result.get(CloudLocationConfig.ADDITIONAL_INBOUND_PORTS);
+            if (requiredPorts instanceof Integer) {
+                ports.add((Integer) requiredPorts);
+            } else if (requiredPorts instanceof Iterable) {
+                for (Object o : (Iterable<?>) requiredPorts) {
+                    if (o instanceof Integer) ports.add((Integer) o);
+                }
+            }
+            if (ports != null && ports.size() > 0) result.put(CloudLocationConfig.INBOUND_PORTS, ports);
+        }
+        result.put(LocationConfigKeys.CALLER_CONTEXT, this);
+        return result.getAllConfigMutable();
+    }
+
+    /** returns the ports that this entity wants to use;
+     * default implementation returns {@link SoftwareProcess#REQUIRED_OPEN_LOGIN_PORTS} plus first value 
+     * for each {@link org.apache.brooklyn.sensor.core.PortAttributeSensorAndConfigKey} config key {@link PortRange}
+     * plus any ports defined with a config keys ending in {@code .port}.
+     */
+    protected Collection<Integer> getRequiredOpenPorts() {
+        Set<Integer> ports = MutableSet.copyOf(getConfig(REQUIRED_OPEN_LOGIN_PORTS));
+        Map<ConfigKey<?>, ?> allConfig = config().getBag().getAllConfigAsConfigKeyMap();
+        Set<ConfigKey<?>> configKeys = Sets.newHashSet(allConfig.keySet());
+        configKeys.addAll(getEntityType().getConfigKeys());
+
+        /* TODO: This won't work if there's a port collision, which will cause the corresponding port attribute
+           to be incremented until a free port is found. In that case the entity will use the free port, but the
+           firewall will open the initial port instead. Mostly a problem for SameServerEntity, localhost location.
+         */
+        for (ConfigKey<?> k: configKeys) {
+            Object value;
+            if (PortRange.class.isAssignableFrom(k.getType()) || k.getName().matches(".*\\.port")) {
+                value = config().get(k);
+            } else {
+                // config().get() will cause this to block until all config has been resolved
+                // using config().getRaw(k) means that we won't be able to use e.g. 'http.port: $brooklyn:component("x").attributeWhenReady("foo")'
+                // but that's unlikely to be used
+                Maybe<Object> maybeValue = config().getRaw(k);
+                value = maybeValue.isPresent() ? maybeValue.get() : null;
+            }
+
+            Maybe<PortRange> maybePortRange = TypeCoercions.tryCoerce(value, new TypeToken<PortRange>() {});
+
+            if (maybePortRange.isPresentAndNonNull()) {
+                PortRange p = maybePortRange.get();
+                if (p != null && !p.isEmpty()) ports.add(p.iterator().next());
+            }
+        }        
+        
+        log.debug("getRequiredOpenPorts detected default {} for {}", ports, this);
+        return ports;
+    }
+
+    protected void initDriver(MachineLocation machine) {
+        SoftwareProcessDriver newDriver = doInitDriver(machine);
+        if (newDriver == null) {
+            throw new UnsupportedOperationException("cannot start "+this+" on "+machine+": no driver available");
+        }
+        driver = newDriver;
+    }
+
+    /**
+     * Creates the driver (if does not already exist or needs replaced for some reason). Returns either the existing driver
+     * or a new driver. Must not return null.
+     */
+    protected SoftwareProcessDriver doInitDriver(MachineLocation machine) {
+        if (driver!=null) {
+            if ((driver instanceof AbstractSoftwareProcessDriver) && machine.equals(((AbstractSoftwareProcessDriver)driver).getLocation())) {
+                return driver; //just reuse
+            } else {
+                log.warn("driver/location change is untested for {} at {}; changing driver and continuing", this, machine);
+                return newDriver(machine);
+            }
+        } else {
+            return newDriver(machine);
+        }
+    }
+    
+    // TODO Find a better way to detect early death of process.
+    public void waitForEntityStart() {
+        if (log.isDebugEnabled()) log.debug("waiting to ensure {} doesn't abort prematurely", this);
+        Duration startTimeout = getConfig(START_TIMEOUT);
+        CountdownTimer timer = startTimeout.countdownTimer();
+        boolean isRunningResult = false;
+        long delay = 100;
+        Exception firstFailure = null;
+        while (!isRunningResult && !timer.isExpired()) {
+            Time.sleep(delay);
+            try {
+                isRunningResult = driver.isRunning();
+                if (log.isDebugEnabled()) log.debug("checked {}, 'is running' returned: {}", this, isRunningResult);
+            } catch (Exception  e) {
+                Exceptions.propagateIfFatal(e);
+
+                isRunningResult = false;
+                if (driver != null) {
+                    String msg = "checked " + this + ", 'is running' threw an exception; logging subsequent exceptions at debug level";
+                    if (firstFailure == null) {
+                        log.error(msg, e);
+                    } else {
+                        log.debug(msg, e);
+                    }
+                } else {
+                    // provide extra context info, as we're seeing this happen in strange circumstances
+                    log.error(this+" concurrent start and shutdown detected", e);
+                }
+                if (firstFailure == null) {
+                    firstFailure = e;
+                }
+            }
+            // slow exponential delay -- 1.1^N means after 40 tries and 50s elapsed, it reaches the max of 5s intervals
+            // TODO use Repeater 
+            delay = Math.min(delay*11/10, 5000);
+        }
+        if (!isRunningResult) {
+            String msg = "Software process entity "+this+" did not pass is-running check within "+
+                    "the required "+startTimeout+" limit ("+timer.getDurationElapsed().toStringRounded()+" elapsed)";
+            if (firstFailure != null) {
+                msg += "; check failed at least once with exception: " + firstFailure.getMessage() + ", see logs for details";
+            }
+            log.warn(msg+" (throwing)");
+            ServiceStateLogic.setExpectedState(this, Lifecycle.RUNNING);
+            throw new IllegalStateException(msg, firstFailure);
+        }
+    }
+
+    /**
+     * If custom behaviour is required by sub-classes, consider overriding {@link #preStart()} or {@link #postStart()})}.
+     * Also consider adding additional work via tasks, executed using {@link DynamicTasks#queue(String, Callable)}.
+     */
+    @Override
+    public final void start(final Collection<? extends Location> locations) {
+        if (DynamicTasks.getTaskQueuingContext() != null) {
+            getLifecycleEffectorTasks().start(locations);
+        } else {
+            Task<?> task = Tasks.builder().name("start (sequential)").body(new Runnable() { public void run() { getLifecycleEffectorTasks().start(locations); } }).build();
+            Entities.submit(this, task).getUnchecked();
+        }
+    }
+
+    /**
+     * If custom behaviour is required by sub-classes, consider overriding  {@link #preStop()} or {@link #postStop()}.
+     * Also consider adding additional work via tasks, executed using {@link DynamicTasks#queue(String, Callable)}.
+     */
+    @Override
+    public final void stop() {
+        // TODO There is a race where we set SERVICE_UP=false while sensor-adapter threads may still be polling.
+        // The other thread might reset SERVICE_UP to true immediately after we set it to false here.
+        // Deactivating adapters before setting SERVICE_UP reduces the race, and it is reduced further by setting
+        // SERVICE_UP to false at the end of stop as well.
+        
+        // Perhaps we should wait until all feeds have completed here, 
+        // or do a SERVICE_STATE check before setting SERVICE_UP to true in a feed (?).
+
+        if (DynamicTasks.getTaskQueuingContext() != null) {
+            getLifecycleEffectorTasks().stop(ConfigBag.EMPTY);
+        } else {
+            Task<?> task = Tasks.builder().name("stop").body(new Runnable() { public void run() { getLifecycleEffectorTasks().stop(ConfigBag.EMPTY); } }).build();
+            Entities.submit(this, task).getUnchecked();
+        }
+    }
+
+    /**
+     * If custom behaviour is required by sub-classes, consider overriding {@link #preRestart()} or {@link #postRestart()}.
+     * Also consider adding additional work via tasks, executed using {@link DynamicTasks#queue(String, Callable)}.
+     */
+    @Override
+    public final void restart() {
+        if (DynamicTasks.getTaskQueuingContext() != null) {
+            getLifecycleEffectorTasks().restart(ConfigBag.EMPTY);
+        } else {
+            Task<?> task = Tasks.builder().name("restart").body(new Runnable() { public void run() { getLifecycleEffectorTasks().restart(ConfigBag.EMPTY); } }).build();
+            Entities.submit(this, task).getUnchecked();
+        }
+    }
+    
+    protected SoftwareProcessDriverLifecycleEffectorTasks getLifecycleEffectorTasks() {
+        return getConfig(LIFECYCLE_EFFECTOR_TASKS);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/main/java/org/apache/brooklyn/entity/software/base/VanillaSoftwareProcess.java
----------------------------------------------------------------------
diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/software/base/VanillaSoftwareProcess.java b/software/base/src/main/java/org/apache/brooklyn/entity/software/base/VanillaSoftwareProcess.java
new file mode 100644
index 0000000..1c55ddd
--- /dev/null
+++ b/software/base/src/main/java/org/apache/brooklyn/entity/software/base/VanillaSoftwareProcess.java
@@ -0,0 +1,62 @@
+/*
+ * 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.brooklyn.entity.software.base;
+
+import org.apache.brooklyn.api.catalog.Catalog;
+import org.apache.brooklyn.api.entity.ImplementedBy;
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.core.config.ConfigKeys;
+
+/** 
+ * A {@link SoftwareProcess} entity that runs commands from an archive.
+ * <p>
+ * Downloads and unpacks the archive indicated (optionally) then runs the management commands (scripts) indicated
+ * (relative to the root of the archive if supplied, otherwise in a tmp working dir) to manage. Uses config keys
+ * to identify the files or commands to use.
+ * <p>
+ * In the simplest mode, simply provide either:
+ * <ul>
+ * <li> an archive in {@link #DOWNLOAD_URL} containing a <code>./start.sh</code>
+ * <li> a start command to invoke in {@link #LAUNCH_COMMAND}
+ * </ul>
+ * The only constraint is that the start command must write the PID into the file pointed to by the injected environment
+ * variable {@code PID_FILE} unless one of the options below is supported.
+ * <p>
+ * The start command can be a complex bash command, downloading and unpacking files, and handling the {@code PID_FILE} requirement.
+ * For example {@code export MY_PID_FILE=$PID_FILE ; ./my_start.sh} or {@code nohup ./start.sh & ; echo $! > $PID_FILE ; sleep 5}.
+ * </pre>
+ * You can supply both {@link #DOWNLOAD_URL} and {@link #LAUNCH_COMMAND} configuration as well..
+ * <p>
+ * In addition, you can supply an {@link #INSTALL_COMMAND} and / or a {@link #CUSTOMIZE_COMMAND} to reduce the complexity
+ * of the {@link #LAUNCH_COMMAND}, and to avoid repeating actions that are unnecessary in subsequent launches.
+ * <p>
+ * By default the PID is used to stop the process using {@code kill} followed by {@code kill -9} if needed and restart
+ * is implemented by stopping the process and then running {@link VanillaSoftwareProcessSshDriver#launch()}, but it is
+ * possible to override this behavior through config keys:
+ * <ul>
+ * <li> A custom {@link #CHECK_RUNNING_COMMAND}
+ * <li> A custom {@link #STOP_COMMAND}
+ * <li> A different {@link SoftwareProcess#PID_FILE} to use
+ * <li>
+ */
+@Catalog(name="Vanilla Software Process", description="A software process configured with scripts, e.g. for launch, check-running and stop")
+@ImplementedBy(VanillaSoftwareProcessImpl.class)
+public interface VanillaSoftwareProcess extends AbstractVanillaProcess {
+    ConfigKey<String> LAUNCH_COMMAND = ConfigKeys.newConfigKeyWithDefault(AbstractVanillaProcess.LAUNCH_COMMAND, "./start.sh");
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/main/java/org/apache/brooklyn/entity/software/base/VanillaSoftwareProcessDriver.java
----------------------------------------------------------------------
diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/software/base/VanillaSoftwareProcessDriver.java b/software/base/src/main/java/org/apache/brooklyn/entity/software/base/VanillaSoftwareProcessDriver.java
new file mode 100644
index 0000000..6fee1d2
--- /dev/null
+++ b/software/base/src/main/java/org/apache/brooklyn/entity/software/base/VanillaSoftwareProcessDriver.java
@@ -0,0 +1,23 @@
+/*
+ * 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.brooklyn.entity.software.base;
+
+public interface VanillaSoftwareProcessDriver extends SoftwareProcessDriver {
+
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/main/java/org/apache/brooklyn/entity/software/base/VanillaSoftwareProcessImpl.java
----------------------------------------------------------------------
diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/software/base/VanillaSoftwareProcessImpl.java b/software/base/src/main/java/org/apache/brooklyn/entity/software/base/VanillaSoftwareProcessImpl.java
new file mode 100644
index 0000000..6f1aec0
--- /dev/null
+++ b/software/base/src/main/java/org/apache/brooklyn/entity/software/base/VanillaSoftwareProcessImpl.java
@@ -0,0 +1,37 @@
+/*
+ * 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.brooklyn.entity.software.base;
+
+
+public class VanillaSoftwareProcessImpl extends SoftwareProcessImpl implements VanillaSoftwareProcess {
+    @Override
+    public Class<?> getDriverInterface() {
+        return VanillaSoftwareProcessDriver.class;
+    }
+    @Override
+    protected void connectSensors() {
+        super.connectSensors();
+        connectServiceUpIsRunning();
+    }
+    @Override
+    protected void disconnectSensors() {
+        disconnectServiceUpIsRunning();
+        super.disconnectSensors();
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/main/java/org/apache/brooklyn/entity/software/base/VanillaSoftwareProcessSshDriver.java
----------------------------------------------------------------------
diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/software/base/VanillaSoftwareProcessSshDriver.java b/software/base/src/main/java/org/apache/brooklyn/entity/software/base/VanillaSoftwareProcessSshDriver.java
new file mode 100644
index 0000000..7edb047
--- /dev/null
+++ b/software/base/src/main/java/org/apache/brooklyn/entity/software/base/VanillaSoftwareProcessSshDriver.java
@@ -0,0 +1,162 @@
+/*
+ * 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.brooklyn.entity.software.base;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.brooklyn.api.entity.drivers.downloads.DownloadResolver;
+import org.apache.brooklyn.api.internal.EntityLocal;
+import org.apache.brooklyn.entity.core.Entities;
+import org.apache.brooklyn.entity.software.base.lifecycle.ScriptHelper;
+import org.apache.brooklyn.location.basic.SshMachineLocation;
+import org.apache.brooklyn.util.collections.MutableMap;
+import org.apache.brooklyn.util.core.file.ArchiveUtils;
+import org.apache.brooklyn.util.guava.Maybe;
+import org.apache.brooklyn.util.net.Urls;
+import org.apache.brooklyn.util.os.Os;
+import org.apache.brooklyn.util.ssh.BashCommands;
+import org.apache.brooklyn.util.text.Identifiers;
+import org.apache.brooklyn.util.text.Strings;
+
+public class VanillaSoftwareProcessSshDriver extends AbstractSoftwareProcessSshDriver implements VanillaSoftwareProcessDriver {
+
+    public VanillaSoftwareProcessSshDriver(EntityLocal entity, SshMachineLocation machine) {
+        super(entity, machine);
+    }
+
+    String downloadedFilename = null;
+
+    /** needed because the download url might be different! */
+    @Override
+    protected String getInstallLabelExtraSalt() {
+        Maybe<Object> url = getEntity().getConfigRaw(SoftwareProcess.DOWNLOAD_URL, true);
+        if (url.isAbsent()) return null;
+        // TODO a user-friendly hash would be nice, but tricky since we don't want it to be too long or contain path chars
+        return Identifiers.makeIdFromHash( url.get().hashCode() );
+    }
+    
+    @Override
+    public void install() {
+        Maybe<Object> url = getEntity().getConfigRaw(SoftwareProcess.DOWNLOAD_URL, true);
+        if (url.isPresentAndNonNull()) {
+            DownloadResolver resolver = Entities.newDownloader(this);
+            List<String> urls = resolver.getTargets();
+            downloadedFilename = resolver.getFilename();
+
+            List<String> commands = new LinkedList<String>();
+            commands.addAll(BashCommands.commandsToDownloadUrlsAs(urls, downloadedFilename));
+            commands.addAll(ArchiveUtils.installCommands(downloadedFilename));
+
+            int result = newScript(INSTALLING)
+                    .failOnNonZeroResultCode(false)
+                    .body.append(commands)
+                    .execute();
+            
+            if (result!=0) {
+                // could not install at remote machine; try resolving URL here and copying across
+                for (String urlI: urls) {
+                    result = ArchiveUtils.install(getMachine(), urlI, Urls.mergePaths(getInstallDir(), downloadedFilename));
+                    if (result==0) 
+                        break;
+                }
+                if (result != 0) 
+                    throw new IllegalStateException("Error installing archive: " + downloadedFilename);
+            }
+        }
+        
+        String installCommand = getEntity().getConfig(VanillaSoftwareProcess.INSTALL_COMMAND);
+        
+        if (Strings.isNonBlank(installCommand)) {
+            newScript(INSTALLING)
+                .failOnNonZeroResultCode()
+                .environmentVariablesReset(getShellEnvironment())
+                .body.append(installCommand)
+                .execute();
+        }
+    }
+
+    @Override
+    public void customize() {
+        if (downloadedFilename != null) {
+            newScript(CUSTOMIZING)
+                    .failOnNonZeroResultCode()
+                    // don't set vars yet -- it resolves dependencies (e.g. DB) which we don't want until we start
+                    .environmentVariablesReset()
+                    .body.append(ArchiveUtils.extractCommands(downloadedFilename, getInstallDir()))
+                    .execute();
+        }
+        
+        String customizeCommand = getEntity().getConfig(VanillaSoftwareProcess.CUSTOMIZE_COMMAND);
+        
+        if (Strings.isNonBlank(customizeCommand)) {
+            newScript(CUSTOMIZING)
+                .failOnNonZeroResultCode()
+                .body.append(customizeCommand)
+                .execute();
+        }
+    }
+
+    @Override
+    public Map<String, String> getShellEnvironment() {
+        return MutableMap.copyOf(super.getShellEnvironment()).add("PID_FILE", getPidFile());
+    }
+
+    public String getPidFile() {
+        // TODO see note in VanillaSoftwareProcess about PID_FILE as a config key
+        // if (getEntity().getConfigRaw(PID_FILE, includeInherited)) ...
+        return Os.mergePathsUnix(getRunDir(), PID_FILENAME);
+    }
+
+    @Override
+    public void launch() {
+        newScript(LAUNCHING)
+            .failOnNonZeroResultCode()
+            .body.append(getEntity().getConfig(VanillaSoftwareProcess.LAUNCH_COMMAND))
+            .execute();
+    }
+
+    @Override
+    public boolean isRunning() {
+        String customCommand = getEntity().getConfig(VanillaSoftwareProcess.CHECK_RUNNING_COMMAND);
+        ScriptHelper script = null;
+        if (customCommand == null) {
+            script = newScript(MutableMap.of(USE_PID_FILE, getPidFile()), CHECK_RUNNING);
+        } else {
+            // TODO: template substitutions?
+            script = newScript(CHECK_RUNNING).body.append(customCommand);
+        }
+        return script.execute() == 0;
+    }
+
+    @Override
+    public void stop() {
+        String customCommand = getEntity().getConfig(VanillaSoftwareProcess.STOP_COMMAND);
+        ScriptHelper script = null;
+        if (customCommand == null) {
+            script = newScript(MutableMap.of(USE_PID_FILE, getPidFile()), STOPPING);
+        } else {
+            // TODO: template substitutions?
+            script = newScript(STOPPING).body.append(customCommand);
+        }
+        script.execute();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/main/java/org/apache/brooklyn/entity/software/base/VanillaWindowsProcess.java
----------------------------------------------------------------------
diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/software/base/VanillaWindowsProcess.java b/software/base/src/main/java/org/apache/brooklyn/entity/software/base/VanillaWindowsProcess.java
new file mode 100644
index 0000000..1354b03
--- /dev/null
+++ b/software/base/src/main/java/org/apache/brooklyn/entity/software/base/VanillaWindowsProcess.java
@@ -0,0 +1,64 @@
+/*
+ * 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.brooklyn.entity.software.base;
+
+import java.util.Collection;
+
+import org.apache.brooklyn.api.entity.ImplementedBy;
+import org.apache.brooklyn.api.sensor.AttributeSensor;
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.core.config.ConfigKeys;
+import org.apache.brooklyn.sensor.core.Sensors;
+import org.apache.brooklyn.util.time.Duration;
+
+import com.google.common.collect.ImmutableSet;
+
+@ImplementedBy(VanillaWindowsProcessImpl.class)
+public interface VanillaWindowsProcess extends AbstractVanillaProcess {
+    // 3389 is RDP; 5985 is WinRM (3389 isn't used by Brooklyn, but useful for the end-user subsequently)
+    ConfigKey<Collection<Integer>> REQUIRED_OPEN_LOGIN_PORTS = ConfigKeys.newConfigKeyWithDefault(
+            SoftwareProcess.REQUIRED_OPEN_LOGIN_PORTS,
+            ImmutableSet.of(5985, 3389));
+    ConfigKey<String> PRE_INSTALL_POWERSHELL_COMMAND = ConfigKeys.newStringConfigKey("pre.install.powershell.command",
+            "powershell command to run during the pre-install phase");
+    ConfigKey<Boolean> PRE_INSTALL_REBOOT_REQUIRED = ConfigKeys.newBooleanConfigKey("pre.install.reboot.required",
+            "indicates that a reboot should be performed after the pre-install command is run", false);
+    ConfigKey<Boolean> INSTALL_REBOOT_REQUIRED = ConfigKeys.newBooleanConfigKey("install.reboot.required",
+            "indicates that a reboot should be performed after the install command is run", false);
+    ConfigKey<Boolean> CUSTOMIZE_REBOOT_REQUIRED = ConfigKeys.newBooleanConfigKey("customize.reboot.required",
+            "indicates that a reboot should be performed after the customize command is run", false);
+    ConfigKey<String> LAUNCH_POWERSHELL_COMMAND = ConfigKeys.newStringConfigKey("launch.powershell.command",
+            "command to run to launch the process");
+    ConfigKey<String> CHECK_RUNNING_POWERSHELL_COMMAND = ConfigKeys.newStringConfigKey("checkRunning.powershell.command",
+            "command to determine whether the process is running");
+    ConfigKey<String> STOP_POWERSHELL_COMMAND = ConfigKeys.newStringConfigKey("stop.powershell.command",
+            "command to run to stop the process");
+    ConfigKey<String> CUSTOMIZE_POWERSHELL_COMMAND = ConfigKeys.newStringConfigKey("customize.powershell.command",
+            "powershell command to run during the customization phase");
+    ConfigKey<String> INSTALL_POWERSHELL_COMMAND = ConfigKeys.newStringConfigKey("install.powershell.command",
+            "powershell command to run during the install phase");
+    ConfigKey<Duration> REBOOT_BEGUN_TIMEOUT = ConfigKeys.newDurationConfigKey("reboot.begun.timeout",
+            "duration to wait whilst waiting for a machine to begin rebooting, and thus become unavailable", Duration.TWO_MINUTES);
+    // TODO If automatic updates are enabled and there are updates waiting to be installed, thirty minutes may not be sufficient...
+    ConfigKey<Duration> REBOOT_COMPLETED_TIMEOUT = ConfigKeys.newDurationConfigKey("reboot.completed.timeout",
+            "duration to wait whilst waiting for a machine to finish rebooting, and thus to become available again", Duration.minutes(30));
+    
+    AttributeSensor<Integer> RDP_PORT = Sensors.newIntegerSensor("rdpPort");
+    AttributeSensor<Integer> WINRM_PORT = Sensors.newIntegerSensor("winrmPort");
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/main/java/org/apache/brooklyn/entity/software/base/VanillaWindowsProcessDriver.java
----------------------------------------------------------------------
diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/software/base/VanillaWindowsProcessDriver.java b/software/base/src/main/java/org/apache/brooklyn/entity/software/base/VanillaWindowsProcessDriver.java
new file mode 100644
index 0000000..a6083ff
--- /dev/null
+++ b/software/base/src/main/java/org/apache/brooklyn/entity/software/base/VanillaWindowsProcessDriver.java
@@ -0,0 +1,23 @@
+/*
+ * 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.brooklyn.entity.software.base;
+
+public interface VanillaWindowsProcessDriver extends SoftwareProcessDriver {
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/main/java/org/apache/brooklyn/entity/software/base/VanillaWindowsProcessImpl.java
----------------------------------------------------------------------
diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/software/base/VanillaWindowsProcessImpl.java b/software/base/src/main/java/org/apache/brooklyn/entity/software/base/VanillaWindowsProcessImpl.java
new file mode 100644
index 0000000..b3b9f83
--- /dev/null
+++ b/software/base/src/main/java/org/apache/brooklyn/entity/software/base/VanillaWindowsProcessImpl.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.apache.brooklyn.entity.software.base;
+
+
+public class VanillaWindowsProcessImpl extends SoftwareProcessImpl implements VanillaWindowsProcess {
+    @Override
+    public Class getDriverInterface() {
+        return VanillaWindowsProcessDriver.class;
+    }
+
+    @Override
+    protected void preStart() {
+        super.preStart();
+        setAttribute(RDP_PORT, 3389);
+        setAttribute(WINRM_PORT, 5985);
+    }
+    
+    @Override
+    protected void connectSensors() {
+        super.connectSensors();
+        connectServiceUpIsRunning();
+    }
+
+    @Override
+    protected void disconnectSensors() {
+        disconnectServiceUpIsRunning();
+        super.disconnectSensors();
+    }
+
+}