You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@brooklyn.apache.org by m4...@apache.org on 2017/09/29 09:47:41 UTC
[4/6] brooklyn-library git commit: Move chef from brooklyn—software-base to own module
Move chef from brooklyn—software-base to own module
Project: http://git-wip-us.apache.org/repos/asf/brooklyn-library/repo
Commit: http://git-wip-us.apache.org/repos/asf/brooklyn-library/commit/737dc8a9
Tree: http://git-wip-us.apache.org/repos/asf/brooklyn-library/tree/737dc8a9
Diff: http://git-wip-us.apache.org/repos/asf/brooklyn-library/diff/737dc8a9
Branch: refs/heads/master
Commit: 737dc8a9710963f91cf25f1c696f8ba8dd4b551d
Parents: 127c969
Author: Aled Sage <al...@gmail.com>
Authored: Fri Sep 15 20:48:28 2017 +0100
Committer: Aled Sage <al...@gmail.com>
Committed: Tue Sep 19 20:59:23 2017 +0100
----------------------------------------------------------------------
karaf/features/src/main/feature/feature.xml | 1 +
software/cm/chef/pom.xml | 84 ++++
.../brooklyn/entity/chef/ChefAttributeFeed.java | 413 +++++++++++++++++++
.../entity/chef/ChefAttributePollConfig.java | 61 +++
.../brooklyn/entity/chef/ChefBashCommands.java | 42 ++
.../apache/brooklyn/entity/chef/ChefConfig.java | 94 +++++
.../brooklyn/entity/chef/ChefConfigs.java | 102 +++++
.../apache/brooklyn/entity/chef/ChefEntity.java | 26 ++
.../brooklyn/entity/chef/ChefEntityImpl.java | 39 ++
.../entity/chef/ChefLifecycleEffectorTasks.java | 364 ++++++++++++++++
.../brooklyn/entity/chef/ChefServerTasks.java | 97 +++++
.../brooklyn/entity/chef/ChefSoloDriver.java | 85 ++++
.../brooklyn/entity/chef/ChefSoloTasks.java | 70 ++++
.../apache/brooklyn/entity/chef/ChefTasks.java | 154 +++++++
.../entity/chef/KnifeConvergeTaskFactory.java | 249 +++++++++++
.../brooklyn/entity/chef/KnifeTaskFactory.java | 241 +++++++++++
.../chef/resolve/ChefEntitySpecResolver.java | 43 ++
...oklyn.core.resolve.entity.EntitySpecResolver | 20 +
.../resources/OSGI-INF/blueprint/blueprint.xml | 32 ++
software/cm/chef/src/main/resources/catalog.bom | 26 ++
.../brooklyn/entity/chef/ChefConfigsTest.java | 40 ++
.../entity/chef/ChefLiveTestSupport.java | 99 +++++
.../chef/ChefServerTasksIntegrationTest.java | 109 +++++
.../AbstractChefToyMySqlEntityLiveTest.java | 41 ++
.../ChefSoloDriverMySqlEntityLiveTest.java | 49 +++
.../mysql/ChefSoloDriverToyMySqlEntity.java | 89 ++++
...micChefAutodetectToyMySqlEntityLiveTest.java | 43 ++
...DynamicChefServerToyMySqlEntityLiveTest.java | 50 +++
.../DynamicChefSoloToyMySqlEntityLiveTest.java | 43 ++
.../chef/mysql/DynamicToyMySqlEntityChef.java | 81 ++++
.../chef/mysql/TypedToyMySqlEntityChef.java | 55 +++
.../brooklyn-tests.pem | 27 ++
.../brooklyn-validator.pem | 27 ++
.../hosted-chef-brooklyn-credentials/knife.rb | 27 ++
software/cm/pom.xml | 3 +-
software/database/pom.xml | 12 +
36 files changed, 3036 insertions(+), 2 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/brooklyn-library/blob/737dc8a9/karaf/features/src/main/feature/feature.xml
----------------------------------------------------------------------
diff --git a/karaf/features/src/main/feature/feature.xml b/karaf/features/src/main/feature/feature.xml
index 5bb9319..8106a7b 100644
--- a/karaf/features/src/main/feature/feature.xml
+++ b/karaf/features/src/main/feature/feature.xml
@@ -27,6 +27,7 @@
</feature>
<feature name="brooklyn-software-cm" version="${project.version}" description="Configuration Management modules">
+ <bundle>mvn:org.apache.brooklyn/brooklyn-software-cm-chef/${project.version}</bundle>
<bundle>mvn:org.apache.brooklyn/brooklyn-software-cm-salt/${project.version}</bundle>
<bundle>mvn:org.apache.brooklyn/brooklyn-software-cm-ansible/${project.version}</bundle>
</feature>
http://git-wip-us.apache.org/repos/asf/brooklyn-library/blob/737dc8a9/software/cm/chef/pom.xml
----------------------------------------------------------------------
diff --git a/software/cm/chef/pom.xml b/software/cm/chef/pom.xml
new file mode 100644
index 0000000..e673281
--- /dev/null
+++ b/software/cm/chef/pom.xml
@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+ <modelVersion>4.0.0</modelVersion>
+ <artifactId>brooklyn-software-cm-chef</artifactId>
+ <packaging>jar</packaging>
+ <name>Brooklyn CM Chef</name>
+ <description>Brooklyn entities for Configuration Management using Chef</description>
+
+
+ <parent>
+ <groupId>org.apache.brooklyn</groupId>
+ <artifactId>brooklyn-library</artifactId>
+ <version>0.12.0-SNAPSHOT</version> <!-- BROOKLYN_VERSION -->
+ <relativePath>../../../pom.xml</relativePath>
+ </parent>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.brooklyn</groupId>
+ <artifactId>brooklyn-api</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.brooklyn</groupId>
+ <artifactId>brooklyn-software-base</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.brooklyn</groupId>
+ <artifactId>brooklyn-camp</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.brooklyn</groupId>
+ <artifactId>brooklyn-test-support</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.brooklyn</groupId>
+ <artifactId>brooklyn-core</artifactId>
+ <version>${project.version}</version>
+ <classifier>tests</classifier>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.brooklyn</groupId>
+ <artifactId>brooklyn-software-base</artifactId>
+ <version>${project.version}</version>
+ <classifier>tests</classifier>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.assertj</groupId>
+ <artifactId>assertj-core</artifactId>
+ <version>${assertj.version}</version>
+ <scope>test</scope>
+ </dependency>
+
+ </dependencies>
+
+</project>
http://git-wip-us.apache.org/repos/asf/brooklyn-library/blob/737dc8a9/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefAttributeFeed.java
----------------------------------------------------------------------
diff --git a/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefAttributeFeed.java b/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefAttributeFeed.java
new file mode 100644
index 0000000..f6d2615
--- /dev/null
+++ b/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefAttributeFeed.java
@@ -0,0 +1,413 @@
+/*
+ * 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.chef;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.EntityLocal;
+import org.apache.brooklyn.api.mgmt.ExecutionContext;
+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.entity.EntityInternal;
+import org.apache.brooklyn.core.feed.AbstractFeed;
+import org.apache.brooklyn.core.feed.PollHandler;
+import org.apache.brooklyn.core.feed.Poller;
+import org.apache.brooklyn.feed.ssh.SshPollValue;
+import org.apache.brooklyn.util.core.flags.TypeCoercions;
+import org.apache.brooklyn.util.core.task.system.ProcessTaskWrapper;
+import org.apache.brooklyn.util.time.Duration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Splitter;
+import com.google.common.base.Strings;
+import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.common.reflect.TypeToken;
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+
+/**
+ * A sensor feed that retrieves attributes from Chef server and converts selected attributes to sensors.
+ *
+ * <p>To use this feed, you must provide the entity, the name of the node as it is known to Chef, and a collection of attribute
+ * sensors. The attribute sensors must follow the naming convention of starting with the string <tt>chef.attribute.</tt>
+ * followed by a period-separated path through the Chef attribute hierarchy. For example, an attribute sensor named
+ * <tt>chef.attribute.sql_server.instance_name</tt> would cause the feed to search for a Chef attribute called
+ * <tt>sql_server</tt>, and within that an attribute <tt>instance_name</tt>, and set the sensor to the value of this
+ * attribute.</p>
+ *
+ * <p>This feed uses the <tt>knife</tt> tool to query all the attributes on a named node. It then iterates over the configured
+ * list of attribute sensors, using the sensor name to locate an equivalent Chef attribute. The sensor is then set to the value
+ * of the Chef attribute.</p>
+ *
+ * <p>Example:</p>
+ *
+ * {@code
+ * @Override
+ * protected void connectSensors() {
+ * nodeAttributesFeed = ChefAttributeFeed.newFeed(this, nodeName, new AttributeSensor[]{
+ * SqlServerNode.CHEF_ATTRIBUTE_NODE_NAME,
+ * SqlServerNode.CHEF_ATTRIBUTE_SQL_SERVER_INSTANCE_NAME,
+ * SqlServerNode.CHEF_ATTRIBUTE_SQL_SERVER_PORT,
+ * SqlServerNode.CHEF_ATTRIBUTE_SQL_SERVER_SA_PASSWORD
+ * });
+ * }
+ * }
+ *
+ * @since 0.6.0
+ * @author richardcloudsoft
+ */
+public class ChefAttributeFeed extends AbstractFeed {
+
+ private static final Logger log = LoggerFactory.getLogger(ChefAttributeFeed.class);
+
+ /**
+ * Prefix for attribute sensor names.
+ */
+ public static final String CHEF_ATTRIBUTE_PREFIX = "chef.attribute.";
+
+ @SuppressWarnings("serial")
+ public static final ConfigKey<Set<ChefAttributePollConfig<?>>> POLLS = ConfigKeys.newConfigKey(
+ new TypeToken<Set<ChefAttributePollConfig<?>>>() {},
+ "polls");
+
+ public static final ConfigKey<String> NODE_NAME = ConfigKeys.newStringConfigKey("nodeName");
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ @SuppressWarnings("rawtypes")
+ public static class Builder {
+ private Entity entity;
+ private boolean onlyIfServiceUp = false;
+ private String nodeName;
+ private Set<ChefAttributePollConfig> polls = Sets.newLinkedHashSet();
+ private Duration period = Duration.of(30, TimeUnit.SECONDS);
+ private String uniqueTag;
+ private volatile boolean built;
+
+ public Builder entity(Entity val) {
+ this.entity = checkNotNull(val, "entity");
+ return this;
+ }
+ public Builder onlyIfServiceUp() { return onlyIfServiceUp(true); }
+ public Builder onlyIfServiceUp(boolean onlyIfServiceUp) {
+ this.onlyIfServiceUp = onlyIfServiceUp;
+ return this;
+ }
+ public Builder nodeName(String nodeName) {
+ this.nodeName = checkNotNull(nodeName, "nodeName");
+ return this;
+ }
+ public Builder addSensor(ChefAttributePollConfig config) {
+ polls.add(config);
+ return this;
+ }
+ @SuppressWarnings("unchecked")
+ public Builder addSensor(String chefAttributePath, AttributeSensor sensor) {
+ return addSensor(new ChefAttributePollConfig(sensor).chefAttributePath(chefAttributePath));
+ }
+ public Builder addSensors(Map<String, AttributeSensor> sensors) {
+ for (Map.Entry<String, AttributeSensor> entry : sensors.entrySet()) {
+ addSensor(entry.getKey(), entry.getValue());
+ }
+ return this;
+ }
+ public Builder addSensors(AttributeSensor[] sensors) {
+ return addSensors(Arrays.asList(checkNotNull(sensors, "sensors")));
+ }
+ public Builder addSensors(Iterable<AttributeSensor> sensors) {
+ for(AttributeSensor sensor : checkNotNull(sensors, "sensors")) {
+ checkNotNull(sensor, "sensors collection contains a null value");
+ checkArgument(sensor.getName().startsWith(CHEF_ATTRIBUTE_PREFIX), "sensor name must be prefixed "+CHEF_ATTRIBUTE_PREFIX+" for autodetection to work");
+ addSensor(sensor.getName().substring(CHEF_ATTRIBUTE_PREFIX.length()), sensor);
+ }
+ return this;
+ }
+ public Builder period(Duration period) {
+ this.period = period;
+ return this;
+ }
+ public Builder period(long millis) {
+ return period(Duration.of(millis, TimeUnit.MILLISECONDS));
+ }
+ public Builder period(long val, TimeUnit units) {
+ return period(Duration.of(val, units));
+ }
+ public Builder uniqueTag(String uniqueTag) {
+ this.uniqueTag = uniqueTag;
+ return this;
+ }
+ public ChefAttributeFeed build() {
+ built = true;
+ ChefAttributeFeed result = new ChefAttributeFeed(this);
+ result.setEntity(checkNotNull((EntityLocal)entity, "entity"));
+ result.start();
+ return result;
+ }
+ @Override
+ protected void finalize() {
+ if (!built) log.warn("SshFeed.Builder created, but build() never called");
+ }
+ }
+
+ private KnifeTaskFactory<String> knifeTaskFactory;
+
+ /**
+ * For rebind; do not call directly; use builder
+ */
+ public ChefAttributeFeed() {
+ }
+
+ protected ChefAttributeFeed(Builder builder) {
+ setConfig(ONLY_IF_SERVICE_UP, builder.onlyIfServiceUp);
+ setConfig(NODE_NAME, checkNotNull(builder.nodeName, "builder.nodeName"));
+
+ Set<ChefAttributePollConfig<?>> polls = Sets.newLinkedHashSet();
+ for (ChefAttributePollConfig<?> config : builder.polls) {
+ if (!config.isEnabled()) continue;
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ ChefAttributePollConfig<?> configCopy = new ChefAttributePollConfig(config);
+ if (configCopy.getPeriod() < 0) configCopy.period(builder.period);
+ polls.add(configCopy);
+ }
+ setConfig(POLLS, polls);
+ initUniqueTag(builder.uniqueTag, polls);
+ }
+
+ @Override
+ protected void preStart() {
+ final String nodeName = getConfig(NODE_NAME);
+ final Set<ChefAttributePollConfig<?>> polls = getConfig(POLLS);
+
+ long minPeriod = Integer.MAX_VALUE;
+ for (ChefAttributePollConfig<?> config : polls) {
+ minPeriod = Math.min(minPeriod, config.getPeriod());
+ }
+
+ knifeTaskFactory = new KnifeNodeAttributeQueryTaskFactory(nodeName);
+
+ final Callable<SshPollValue> getAttributesFromKnife = new Callable<SshPollValue>() {
+ @Override
+ public SshPollValue call() throws Exception {
+ ProcessTaskWrapper<String> taskWrapper = knifeTaskFactory.newTask();
+ final ExecutionContext executionContext = ((EntityInternal) entity).getExecutionContext();
+ log.debug("START: Running knife to query attributes of Chef node {}", nodeName);
+ executionContext.submit(taskWrapper);
+ taskWrapper.block();
+ log.debug("DONE: Running knife to query attributes of Chef node {}", nodeName);
+ return new SshPollValue(null, taskWrapper.getExitCode(), taskWrapper.getStdout(), taskWrapper.getStderr());
+ }
+ };
+
+ getPoller().scheduleAtFixedRate(
+ new CallInEntityExecutionContext<SshPollValue>(entity, getAttributesFromKnife),
+ new SendChefAttributesToSensors(entity, polls),
+ minPeriod);
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ protected Poller<SshPollValue> getPoller() {
+ return (Poller<SshPollValue>) super.getPoller();
+ }
+
+ /**
+ * An implementation of {@link KnifeTaskFactory} that queries for the attributes of a node.
+ */
+ private static class KnifeNodeAttributeQueryTaskFactory extends KnifeTaskFactory<String> {
+ private final String nodeName;
+
+ public KnifeNodeAttributeQueryTaskFactory(String nodeName) {
+ super("retrieve attributes of node " + nodeName);
+ this.nodeName = nodeName;
+ }
+
+ @Override
+ protected List<String> initialKnifeParameters() {
+ return ImmutableList.of("node", "show", "-l", nodeName, "--format", "json");
+ }
+ }
+
+ /**
+ * A {@link Callable} that wraps another {@link Callable}, where the inner {@link Callable} is executed in the context of a
+ * specific entity.
+ *
+ * @param <T> The type of the {@link Callable}.
+ */
+ private static class CallInEntityExecutionContext<T> implements Callable<T> {
+
+ private final Callable<T> job;
+ private Entity entity;
+
+ private CallInEntityExecutionContext(Entity entity, Callable<T> job) {
+ this.job = job;
+ this.entity = entity;
+ }
+
+ @Override
+ public T call() throws Exception {
+ final ExecutionContext executionContext = ((EntityInternal) entity).getExecutionContext();
+ return executionContext.submit(Maps.newHashMap(), job).get();
+ }
+ }
+
+ /**
+ * A poll handler that takes the result of the <tt>knife</tt> invocation and sets the appropriate sensors.
+ */
+ private static class SendChefAttributesToSensors implements PollHandler<SshPollValue> {
+ private static final Iterable<String> PREFIXES = ImmutableList.of("", "automatic", "force_override", "override", "normal", "force_default", "default");
+ private static final Splitter SPLITTER = Splitter.on('.');
+
+ private final Entity entity;
+ private final Map<String, AttributeSensor<?>> chefAttributeSensors;
+
+ public SendChefAttributesToSensors(Entity entity, Set<ChefAttributePollConfig<?>> polls) {
+ this.entity = entity;
+ chefAttributeSensors = Maps.newLinkedHashMap();
+ for (ChefAttributePollConfig<?> config : polls) {
+ chefAttributeSensors.put(config.getChefAttributePath(), config.getSensor());
+ }
+ }
+
+ @Override
+ public boolean checkSuccess(SshPollValue val) {
+ if (val.getExitStatus() != 0) return false;
+ String stderr = val.getStderr();
+ if (stderr == null || stderr.length() != 0) return false;
+ String out = val.getStdout();
+ if (out == null || out.length() == 0) return false;
+ if (!out.contains("{")) return false;
+ return true;
+ }
+
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ @Override
+ public void onSuccess(SshPollValue val) {
+ String stdout = val.getStdout();
+ int jsonStarts = stdout.indexOf('{');
+ if (jsonStarts > 0)
+ stdout = stdout.substring(jsonStarts);
+ JsonElement jsonElement = new Gson().fromJson(stdout, JsonElement.class);
+
+ for (Map.Entry<String, AttributeSensor<?>> attribute : chefAttributeSensors.entrySet()) {
+ String chefAttributeName = attribute.getKey();
+ AttributeSensor<?> sensor = attribute.getValue();
+ log.trace("Finding value for attribute sensor " + sensor.getName());
+
+ Iterable<String> path = SPLITTER.split(chefAttributeName);
+ JsonElement elementForSensor = null;
+ for(String prefix : PREFIXES) {
+ Iterable<String> prefixedPath = !Strings.isNullOrEmpty(prefix)
+ ? Iterables.concat(ImmutableList.of(prefix), path)
+ : path;
+ try {
+ elementForSensor = getElementByPath(jsonElement.getAsJsonObject(), prefixedPath);
+ } catch(IllegalArgumentException e) {
+ log.error("Entity {}: bad Chef attribute {} for sensor {}: {}", new Object[]{
+ entity.getDisplayName(),
+ Joiner.on('.').join(prefixedPath),
+ sensor.getName(),
+ e.getMessage()});
+ throw Throwables.propagate(e);
+ }
+ if (elementForSensor != null) {
+ log.debug("Entity {}: apply Chef attribute {} to sensor {} with value {}", new Object[]{
+ entity.getDisplayName(),
+ Joiner.on('.').join(prefixedPath),
+ sensor.getName(),
+ elementForSensor.getAsString()});
+ break;
+ }
+ }
+ if (elementForSensor != null) {
+ entity.sensors().set((AttributeSensor)sensor, TypeCoercions.coerce(elementForSensor.getAsString(), sensor.getTypeToken()));
+ } else {
+ log.debug("Entity {}: no Chef attribute matching {}; setting sensor {} to null", new Object[]{
+ entity.getDisplayName(),
+ chefAttributeName,
+ sensor.getName()});
+ entity.sensors().set(sensor, null);
+ }
+ }
+ }
+
+ private JsonElement getElementByPath(JsonElement element, Iterable<String> path) {
+ if (Iterables.isEmpty(path)) {
+ return element;
+ } else {
+ String head = Iterables.getFirst(path, null);
+ Preconditions.checkArgument(!Strings.isNullOrEmpty(head), "path must not contain empty or null elements");
+ Iterable<String> tail = Iterables.skip(path, 1);
+ JsonElement child = ((JsonObject) element).get(head);
+ return child != null
+ ? getElementByPath(child, tail)
+ : null;
+ }
+ }
+
+ @Override
+ public void onFailure(SshPollValue val) {
+ log.error("Chef attribute query did not respond as expected. exitcode={} stdout={} stderr={}", new Object[]{val.getExitStatus(), val.getStdout(), val.getStderr()});
+ for (AttributeSensor<?> attribute : chefAttributeSensors.values()) {
+ if (!attribute.getName().startsWith(CHEF_ATTRIBUTE_PREFIX))
+ continue;
+ entity.sensors().set(attribute, null);
+ }
+ }
+
+ @Override
+ public void onException(Exception exception) {
+ log.error("Detected exception while retrieving Chef attributes from entity " + entity.getDisplayName(), exception);
+ for (AttributeSensor<?> attribute : chefAttributeSensors.values()) {
+ if (!attribute.getName().startsWith(CHEF_ATTRIBUTE_PREFIX))
+ continue;
+ entity.sensors().set(attribute, null);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return super.toString()+"["+getDescription()+"]";
+ }
+
+ @Override
+ public String getDescription() {
+ return ""+chefAttributeSensors;
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/brooklyn-library/blob/737dc8a9/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefAttributePollConfig.java
----------------------------------------------------------------------
diff --git a/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefAttributePollConfig.java b/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefAttributePollConfig.java
new file mode 100644
index 0000000..c090f34
--- /dev/null
+++ b/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefAttributePollConfig.java
@@ -0,0 +1,61 @@
+/*
+ * 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.chef;
+
+import com.google.common.base.Function;
+import com.google.common.base.Functions;
+
+import org.apache.brooklyn.api.sensor.AttributeSensor;
+import org.apache.brooklyn.core.feed.PollConfig;
+
+public class ChefAttributePollConfig<T> extends PollConfig<Object, T, ChefAttributePollConfig<T>>{
+
+ private String chefAttributePath;
+
+ public static <T> ChefAttributePollConfig<T> forSensor(AttributeSensor<T> sensor) {
+ return new ChefAttributePollConfig<T>(sensor);
+ }
+
+ public static ChefAttributePollConfig<Void> forMultiple() {
+ return new ChefAttributePollConfig<Void>(PollConfig.NO_SENSOR);
+ }
+
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ public ChefAttributePollConfig(AttributeSensor<T> sensor) {
+ super(sensor);
+ onSuccess((Function)Functions.identity());
+ }
+
+ public ChefAttributePollConfig(ChefAttributePollConfig<T> other) {
+ super(other);
+ this.chefAttributePath = other.chefAttributePath;
+ }
+
+ public String getChefAttributePath() {
+ return chefAttributePath;
+ }
+
+ public ChefAttributePollConfig<T> chefAttributePath(String val) {
+ this.chefAttributePath = val; return this;
+ }
+
+ @Override protected String toStringBaseName() { return "chef"; }
+ @Override protected String toStringPollSource() { return chefAttributePath; }
+
+}
http://git-wip-us.apache.org/repos/asf/brooklyn-library/blob/737dc8a9/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefBashCommands.java
----------------------------------------------------------------------
diff --git a/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefBashCommands.java b/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefBashCommands.java
new file mode 100644
index 0000000..dde58d3
--- /dev/null
+++ b/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefBashCommands.java
@@ -0,0 +1,42 @@
+/*
+ * 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.chef;
+
+import static org.apache.brooklyn.util.ssh.BashCommands.INSTALL_CURL;
+import static org.apache.brooklyn.util.ssh.BashCommands.INSTALL_TAR;
+import static org.apache.brooklyn.util.ssh.BashCommands.INSTALL_UNZIP;
+import static org.apache.brooklyn.util.ssh.BashCommands.downloadToStdout;
+import static org.apache.brooklyn.util.ssh.BashCommands.sudo;
+
+import org.apache.brooklyn.util.ssh.BashCommands;
+
+import com.google.common.annotations.Beta;
+
+/** BASH commands useful for setting up Chef */
+@Beta
+public class ChefBashCommands {
+
+ public static final String INSTALL_FROM_OPSCODE =
+ BashCommands.chain(
+ INSTALL_CURL,
+ INSTALL_TAR,
+ INSTALL_UNZIP,
+ "( "+downloadToStdout("https://www.opscode.com/chef/install.sh") + " | " + sudo("bash")+" )");
+
+}
http://git-wip-us.apache.org/repos/asf/brooklyn-library/blob/737dc8a9/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefConfig.java
----------------------------------------------------------------------
diff --git a/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefConfig.java b/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefConfig.java
new file mode 100644
index 0000000..07b4ce5
--- /dev/null
+++ b/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefConfig.java
@@ -0,0 +1,94 @@
+/*
+ * 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.chef;
+
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.core.config.ConfigKeys;
+import org.apache.brooklyn.core.config.MapConfigKey;
+import org.apache.brooklyn.core.config.SetConfigKey;
+import org.apache.brooklyn.util.core.flags.SetFromFlag;
+
+import com.google.common.annotations.Beta;
+
+/** {@link ConfigKey}s used to configure the chef driver */
+@Beta
+public interface ChefConfig {
+
+ public static final ConfigKey<String> CHEF_COOKBOOK_PRIMARY_NAME = ConfigKeys.newStringConfigKey("brooklyn.chef.cookbook.primary.name",
+ "Namespace to use for passing data to Chef and for finding effectors");
+
+ @SetFromFlag("cookbook_urls")
+ public static final MapConfigKey<String> CHEF_COOKBOOK_URLS = new MapConfigKey<String>(String.class, "brooklyn.chef.cookbooksUrls");
+
+ @SetFromFlag("converge_twice")
+ public static final ConfigKey<Boolean> CHEF_RUN_CONVERGE_TWICE = ConfigKeys.newBooleanConfigKey("brooklyn.chef.converge.twice",
+ "Whether to run converge commands twice if the first one fails; needed in some contexts, e.g. when switching between chef-server and chef-solo mode", false);
+
+ @Deprecated /** @deprecated since 0.7.0 use #CHEF_LAUNCH_RUN_LIST */
+ public static final SetConfigKey<String> CHEF_RUN_LIST = new SetConfigKey<String>(String.class, "brooklyn.chef.runList");
+
+ /** typically set from spec, to customize the launch part of the start effector */
+ @SetFromFlag("launch_run_list")
+ public static final SetConfigKey<String> CHEF_LAUNCH_RUN_LIST = new SetConfigKey<String>(String.class, "brooklyn.chef.launch.runList");
+ /** typically set from spec, to customize the launch part of the start effector */
+ @SetFromFlag("launch_attributes")
+ public static final MapConfigKey<Object> CHEF_LAUNCH_ATTRIBUTES = new MapConfigKey<Object>(Object.class, "brooklyn.chef.launch.attributes");
+
+ public static enum ChefModes {
+ /** Force use of Chef Solo */
+ SOLO,
+ /** Force use of Knife; knife must be installed, and either
+ * {@link ChefConfig#KNIFE_EXECUTABLE} and {@link ChefConfig#KNIFE_CONFIG_FILE} must be set
+ * or knife on the path with valid global config set up */
+ KNIFE,
+ // TODO server via API
+ /** Tries {@link #KNIFE} if valid, else {@link #SOLO} */
+ AUTODETECT
+ };
+
+ @SetFromFlag("chef_mode")
+ public static final ConfigKey<ChefModes> CHEF_MODE = ConfigKeys.newConfigKey(ChefModes.class, "brooklyn.chef.mode",
+ "Whether Chef should run in solo mode, knife mode, or auto-detect", ChefModes.AUTODETECT);
+
+ // TODO server-url for server via API mode
+
+ public static final ConfigKey<String> KNIFE_SETUP_COMMANDS = ConfigKeys.newStringConfigKey("brooklyn.chef.knife.setupCommands",
+ "An optional set of commands to run on localhost before invoking knife; useful if using ruby version manager for example");
+ public static final ConfigKey<String> KNIFE_EXECUTABLE = ConfigKeys.newStringConfigKey("brooklyn.chef.knife.executableFile",
+ "Knife command to run on the Brooklyn machine, including full path; defaults to scanning the path");
+ public static final ConfigKey<String> KNIFE_CONFIG_FILE = ConfigKeys.newStringConfigKey("brooklyn.chef.knife.configFile",
+ "Knife config file (typically knife.rb) to use, including full path; defaults to knife default/global config");
+
+ @SetFromFlag("chef_node_name")
+ public static final ConfigKey<String> CHEF_NODE_NAME = ConfigKeys.newStringConfigKey("brooklyn.chef.node.nodeName",
+ "Node name to register with the chef server for this entity, if using Chef server and a specific node name is desired; "
+ + "if supplied ,this must be unique across the nodes Chef Server manages; if not supplied, one will be created if needed");
+
+ // for providing some simple (ssh-based) lifecycle operations and checks
+ @SetFromFlag("pid_file")
+ public static final ConfigKey<String> PID_FILE = ConfigKeys.newStringConfigKey("brooklyn.chef.lifecycle.pidFile",
+ "Path to PID file on remote machine, for use in checking running and stopping; may contain wildcards");
+ @SetFromFlag("service_name")
+ public static final ConfigKey<String> SERVICE_NAME = ConfigKeys.newStringConfigKey("brooklyn.chef.lifecycle.serviceName",
+ "Name of OS service this will run as, for use in checking running and stopping");
+ @SetFromFlag("windows_service_name")
+ public static final ConfigKey<String> WINDOWS_SERVICE_NAME = ConfigKeys.newStringConfigKey("brooklyn.chef.lifecycle.windowsServiceName",
+ "Name of OS service this will run as on Windows, if different there, for use in checking running and stopping");
+
+}
http://git-wip-us.apache.org/repos/asf/brooklyn-library/blob/737dc8a9/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefConfigs.java
----------------------------------------------------------------------
diff --git a/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefConfigs.java b/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefConfigs.java
new file mode 100644
index 0000000..16d5fa0
--- /dev/null
+++ b/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefConfigs.java
@@ -0,0 +1,102 @@
+/*
+ * 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.chef;
+
+import java.util.Map;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.EntitySpec;
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.core.config.MapConfigKey.MapModifications;
+import org.apache.brooklyn.core.config.SetConfigKey.SetModifications;
+import org.apache.brooklyn.core.entity.EntityInternal;
+import org.apache.brooklyn.util.git.GithubUrls;
+
+import com.google.common.annotations.Beta;
+import com.google.common.base.Preconditions;
+
+/** Conveniences for configuring brooklyn Chef entities
+ * @since 0.6.0 */
+@Beta
+public class ChefConfigs {
+
+ public static void addToLaunchRunList(EntitySpec<?> entity, String ...recipes) {
+ for (String recipe: recipes)
+ entity.configure(ChefConfig.CHEF_LAUNCH_RUN_LIST, SetModifications.addItem(recipe));
+ }
+
+ public static void addToLaunchRunList(EntityInternal entity, String ...recipes) {
+ for (String recipe: recipes)
+ entity.config().set(ChefConfig.CHEF_LAUNCH_RUN_LIST, SetModifications.addItem(recipe));
+ }
+
+ public static void addToCookbooksFromGithub(EntitySpec<?> entity, String ...cookbookNames) {
+ for (String cookbookName: cookbookNames)
+ addToCookbooksFromGithub(entity, cookbookName, getGithubOpscodeRepo(cookbookName));
+ }
+
+ public static void addToCookbooksFromGithub(EntityInternal entity, String ...cookbookNames) {
+ for (String cookbookName: cookbookNames)
+ addToCookbooksFromGithub(entity, cookbookName, getGithubOpscodeRepo(cookbookName));
+ }
+
+ public static String getGithubOpscodeRepo(String cookbookName) {
+ return getGithubOpscodeRepo(cookbookName, "master");
+ }
+ public static String getGithubOpscodeRepo(String cookbookName, String tag) {
+ return GithubUrls.tgz("opscode-cookbooks", cookbookName, tag);
+ }
+
+ public static void addToCookbooksFromGithub(EntitySpec<?> entity, String cookbookName, String cookbookUrl) {
+ entity.configure(ChefConfig.CHEF_COOKBOOK_URLS.subKey(cookbookName), cookbookUrl);
+ }
+
+ public static void addToCookbooksFromGithub(EntityInternal entity, String cookbookName, String cookbookUrl) {
+ entity.config().set(ChefConfig.CHEF_COOKBOOK_URLS.subKey(cookbookName), cookbookUrl);
+ }
+
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ public static void addLaunchAttributes(EntitySpec<?> entity, Map<? extends Object,? extends Object> attributesMap) {
+ entity.configure(ChefConfig.CHEF_LAUNCH_ATTRIBUTES, MapModifications.add((Map)attributesMap));
+ }
+
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ public static void addLaunchAttributes(EntityInternal entity, Map<? extends Object,? extends Object> attributesMap) {
+ entity.config().set(ChefConfig.CHEF_LAUNCH_ATTRIBUTES, MapModifications.add((Map)attributesMap));
+ }
+
+ /** replaces the attributes underneath the rootAttribute parameter with the given value;
+ * see {@link #addLaunchAttributesMap(EntitySpec, Map)} for richer functionality */
+ public static void setLaunchAttribute(EntitySpec<?> entity, String rootAttribute, Object value) {
+ entity.configure(ChefConfig.CHEF_LAUNCH_ATTRIBUTES.subKey(rootAttribute), value);
+ }
+
+ /** replaces the attributes underneath the rootAttribute parameter with the given value;
+ * see {@link #addLaunchAttributesMap(EntitySpec, Map)} for richer functionality */
+ public static void setLaunchAttribute(EntityInternal entity, String rootAttribute, Object value) {
+ entity.config().set(ChefConfig.CHEF_LAUNCH_ATTRIBUTES.subKey(rootAttribute), value);
+ }
+
+ public static <T> T getRequiredConfig(Entity entity, ConfigKey<T> key) {
+ return Preconditions.checkNotNull(
+ Preconditions.checkNotNull(entity, "Entity must be supplied").getConfig(key),
+ "Key "+key+" is required on "+entity);
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/brooklyn-library/blob/737dc8a9/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefEntity.java
----------------------------------------------------------------------
diff --git a/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefEntity.java b/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefEntity.java
new file mode 100644
index 0000000..1b1f866
--- /dev/null
+++ b/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefEntity.java
@@ -0,0 +1,26 @@
+/*
+ * 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.chef;
+
+import org.apache.brooklyn.api.entity.ImplementedBy;
+import org.apache.brooklyn.entity.software.base.SoftwareProcess;
+
+@ImplementedBy(ChefEntityImpl.class)
+public interface ChefEntity extends SoftwareProcess, ChefConfig {
+}
http://git-wip-us.apache.org/repos/asf/brooklyn-library/blob/737dc8a9/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefEntityImpl.java
----------------------------------------------------------------------
diff --git a/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefEntityImpl.java b/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefEntityImpl.java
new file mode 100644
index 0000000..6bc3cfd
--- /dev/null
+++ b/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefEntityImpl.java
@@ -0,0 +1,39 @@
+/*
+ * 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.chef;
+
+import org.apache.brooklyn.entity.stock.EffectorStartableImpl;
+import org.apache.brooklyn.util.text.Strings;
+
+public class ChefEntityImpl extends EffectorStartableImpl implements ChefEntity {
+
+ @Override
+ public void init() {
+ String primaryName = getConfig(CHEF_COOKBOOK_PRIMARY_NAME);
+ if (!Strings.isBlank(primaryName)) setDefaultDisplayName(primaryName+" (chef)");
+
+ super.init();
+ new ChefLifecycleEffectorTasks().attachLifecycleEffectors(this);
+ }
+
+ @Override
+ public void populateServiceNotUpDiagnostics() {
+ // TODO no-op currently; should check ssh'able etc
+ }
+}
http://git-wip-us.apache.org/repos/asf/brooklyn-library/blob/737dc8a9/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefLifecycleEffectorTasks.java
----------------------------------------------------------------------
diff --git a/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefLifecycleEffectorTasks.java b/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefLifecycleEffectorTasks.java
new file mode 100644
index 0000000..8c3dad9
--- /dev/null
+++ b/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefLifecycleEffectorTasks.java
@@ -0,0 +1,364 @@
+/*
+ * 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.chef;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.location.MachineLocation;
+import org.apache.brooklyn.core.effector.ssh.SshEffectorTasks;
+import org.apache.brooklyn.core.entity.Attributes;
+import org.apache.brooklyn.core.entity.lifecycle.Lifecycle;
+import org.apache.brooklyn.core.location.Machines;
+import org.apache.brooklyn.entity.software.base.SoftwareProcess;
+import org.apache.brooklyn.entity.software.base.lifecycle.MachineLifecycleEffectorTasks;
+import org.apache.brooklyn.location.ssh.SshMachineLocation;
+import org.apache.brooklyn.util.collections.Jsonya;
+import org.apache.brooklyn.util.collections.Jsonya.Navigator;
+import org.apache.brooklyn.util.collections.MutableMap;
+import org.apache.brooklyn.util.core.config.ConfigBag;
+import org.apache.brooklyn.util.core.task.DynamicTasks;
+import org.apache.brooklyn.util.core.task.TaskTags;
+import org.apache.brooklyn.util.core.task.Tasks;
+import org.apache.brooklyn.util.core.task.system.ProcessTaskWrapper;
+import org.apache.brooklyn.util.exceptions.Exceptions;
+import org.apache.brooklyn.util.net.Urls;
+import org.apache.brooklyn.util.ssh.BashCommands;
+import org.apache.brooklyn.util.text.Strings;
+import org.apache.brooklyn.util.time.Duration;
+import org.apache.brooklyn.util.time.Time;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.annotations.Beta;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Supplier;
+import com.google.common.collect.ImmutableList;
+
+/**
+ * Creates effectors to start, restart, and stop processes using Chef.
+ * <p>
+ * Instances of this should use the {@link ChefConfig} config attributes to configure startup,
+ * and invoke {@link #usePidFile(String)} or {@link #useService(String)} to determine check-running and stop behaviour.
+ * Alternatively this can be subclassed and {@link #postStartCustom()} and {@link #stopProcessesAtMachine()} overridden.
+ *
+ * @since 0.6.0
+ **/
+@Beta
+public class ChefLifecycleEffectorTasks extends MachineLifecycleEffectorTasks implements ChefConfig {
+
+ private static final Logger log = LoggerFactory.getLogger(ChefLifecycleEffectorTasks.class);
+
+ protected String _pidFile, _serviceName, _windowsServiceName;
+
+ public ChefLifecycleEffectorTasks() {
+ }
+
+ public ChefLifecycleEffectorTasks usePidFile(String pidFile) {
+ this._pidFile = pidFile;
+ return this;
+ }
+ public ChefLifecycleEffectorTasks useService(String serviceName) {
+ this._serviceName = serviceName;
+ return this;
+ }
+ public ChefLifecycleEffectorTasks useWindowsService(String serviceName) {
+ this._windowsServiceName = serviceName;
+ return this;
+ }
+
+ public String getPidFile() {
+ if (_pidFile!=null) return _pidFile;
+ return _pidFile = entity().getConfig(ChefConfig.PID_FILE);
+ }
+
+ public String getServiceName() {
+ if (_serviceName!=null) return _serviceName;
+ return _serviceName = entity().getConfig(ChefConfig.SERVICE_NAME);
+ }
+
+ protected String getNodeName() {
+ // (node name is needed so we can node delete it)
+
+ // TODO would be better if CHEF_NODE_NAME were a freemarker template, could access entity.id, or hostname, etc,
+ // in addition to supporting hard-coded node names (which is all we support so far).
+
+ String nodeName = entity().getConfig(ChefConfig.CHEF_NODE_NAME);
+ if (Strings.isNonBlank(nodeName)) return Strings.makeValidFilename(nodeName);
+ // node name is taken from ID of this entity, if not specified
+ return entity().getId();
+ }
+
+ public String getWindowsServiceName() {
+ if (_windowsServiceName!=null) return _windowsServiceName;
+ return _windowsServiceName = entity().getConfig(ChefConfig.WINDOWS_SERVICE_NAME);
+ }
+
+ @Override
+ public void attachLifecycleEffectors(Entity entity) {
+ if (getPidFile()==null && getServiceName()==null && getClass().equals(ChefLifecycleEffectorTasks.class)) {
+ // warn on incorrect usage
+ log.warn("Uses of "+getClass()+" must define a PID file or a service name (or subclass and override {start,stop} methods as per javadoc) " +
+ "in order for check-running and stop to work");
+ }
+
+ super.attachLifecycleEffectors(entity);
+ }
+
+ public static ChefModes detectChefMode(Entity entity) {
+ ChefModes mode = entity.getConfig(ChefConfig.CHEF_MODE);
+ if (mode == ChefModes.AUTODETECT) {
+ // TODO server via API
+ ProcessTaskWrapper<Boolean> installCheck = DynamicTasks.queue(
+ ChefServerTasks.isKnifeInstalled());
+ mode = installCheck.get() ? ChefModes.KNIFE : ChefModes.SOLO;
+ log.debug("Using Chef in "+mode+" mode due to autodetect exit code "+installCheck.getExitCode());
+ }
+ Preconditions.checkNotNull(mode, "Non-null "+ChefConfig.CHEF_MODE+" required for "+entity);
+ return mode;
+ }
+
+ @Override
+ protected String startProcessesAtMachine(Supplier<MachineLocation> machineS) {
+ ChefModes mode = detectChefMode(entity());
+ switch (mode) {
+ case KNIFE:
+ startWithKnifeAsync();
+ break;
+
+ case SOLO:
+ startWithChefSoloAsync();
+ break;
+
+ default:
+ throw new IllegalStateException("Unknown Chef mode "+mode+" when starting processes for "+entity());
+ }
+
+ return "chef start tasks submitted ("+mode+")";
+ }
+
+ protected String getPrimaryCookbook() {
+ return entity().getConfig(CHEF_COOKBOOK_PRIMARY_NAME);
+ }
+
+ @SuppressWarnings({ "unchecked", "deprecation" })
+ protected void startWithChefSoloAsync() {
+ String baseDir = MachineLifecycleEffectorTasks.resolveOnBoxDir(entity(), Machines.findUniqueMachineLocation(entity().getLocations(), SshMachineLocation.class).get());
+ String installDir = Urls.mergePaths(baseDir, "installs/chef");
+
+ @SuppressWarnings("rawtypes")
+ Map<String, String> cookbooks = (Map)
+ ConfigBag.newInstance( entity().getConfig(CHEF_COOKBOOK_URLS) )
+ .putIfAbsent( entity().getConfig(CHEF_COOKBOOK_URLS) )
+ .getAllConfig();
+ if (cookbooks.isEmpty())
+ log.warn("No cookbook_urls set for "+entity()+"; launch will likely fail subsequently");
+ DynamicTasks.queue(
+ ChefSoloTasks.installChef(installDir, false),
+ ChefSoloTasks.installCookbooks(installDir, cookbooks, false));
+
+ // TODO chef for and run a prestart recipe if necessary
+ // TODO open ports
+
+ String primary = getPrimaryCookbook();
+
+ // put all config under brooklyn/cookbook/config
+ Navigator<MutableMap<Object, Object>> attrs = Jsonya.newInstancePrimitive().at("brooklyn");
+ if (Strings.isNonBlank(primary)) attrs.at(primary);
+ attrs.at("config");
+ attrs.put( entity().config().getBag().getAllConfig() );
+ // and put launch attrs at root
+ try {
+ attrs.root().put((Map<?,?>)Tasks.resolveDeepValue(entity().getConfig(CHEF_LAUNCH_ATTRIBUTES), Object.class, entity().getExecutionContext()));
+ } catch (Exception e) { Exceptions.propagate(e); }
+
+ Collection<? extends String> runList = entity().getConfig(CHEF_LAUNCH_RUN_LIST);
+ if (runList==null) runList = entity().getConfig(CHEF_RUN_LIST);
+ if (runList==null) {
+ if (Strings.isNonBlank(primary)) runList = ImmutableList.of(primary+"::"+"start");
+ else throw new IllegalStateException("Require a primary cookbook or a run_list to effect "+"start"+" on "+entity());
+ }
+
+ String runDir = Urls.mergePaths(baseDir,
+ "apps/"+entity().getApplicationId()+"/chef/entities/"+entity().getEntityType().getSimpleName()+"_"+entity().getId());
+
+ DynamicTasks.queue(ChefSoloTasks.buildChefFile(runDir, installDir, "launch",
+ runList, (Map<String, Object>) attrs.root().get()));
+
+ DynamicTasks.queue(ChefSoloTasks.runChef(runDir, "launch", entity().getConfig(CHEF_RUN_CONVERGE_TWICE)));
+ }
+
+ @SuppressWarnings({ "unchecked", "rawtypes", "deprecation" })
+ protected void startWithKnifeAsync() {
+ // TODO prestart, ports (as above); also, note, some aspects of this are untested as we need a chef server
+
+ String primary = getPrimaryCookbook();
+
+ // put all config under brooklyn/cookbook/config
+ Navigator<MutableMap<Object, Object>> attrs = Jsonya.newInstancePrimitive().at("brooklyn");
+ if (Strings.isNonBlank(primary)) attrs.at(primary);
+ attrs.at("config");
+ attrs.put( entity().config().getBag().getAllConfig() );
+ // and put launch attrs at root
+ try {
+ attrs.root().put((Map<?,?>)Tasks.resolveDeepValue(entity().getConfig(CHEF_LAUNCH_ATTRIBUTES), Object.class, entity().getExecutionContext()));
+ } catch (Exception e) { Exceptions.propagate(e); }
+
+ Collection<? extends String> runList = entity().getConfig(CHEF_LAUNCH_RUN_LIST);
+ if (runList==null) runList = entity().getConfig(CHEF_RUN_LIST);
+ if (runList==null) {
+ if (Strings.isNonBlank(primary)) runList = ImmutableList.of(primary+"::"+"start");
+ else throw new IllegalStateException("Require a primary cookbook or a run_list to effect "+"start"+" on "+entity());
+ }
+
+ DynamicTasks.queue(
+ ChefServerTasks.knifeConvergeTask()
+ .knifeNodeName(getNodeName())
+ .knifeRunList(Strings.join(runList, ","))
+ .knifeAddAttributes((Map) attrs.root().get())
+ .knifeRunTwice(entity().getConfig(CHEF_RUN_CONVERGE_TWICE)) );
+ }
+
+ @Override
+ protected void postStartCustom() {
+ boolean result = false;
+ result |= tryCheckStartPid();
+ result |= tryCheckStartService();
+ result |= tryCheckStartWindowsService();
+ if (!result) {
+ log.warn("No way to check whether "+entity()+" is running; assuming yes");
+ }
+ entity().sensors().set(SoftwareProcess.SERVICE_UP, true);
+ super.postStartCustom();
+ }
+
+ protected boolean tryCheckStartPid() {
+ if (getPidFile()==null) return false;
+
+ // if it's still up after 5s assume we are good (default behaviour)
+ Time.sleep(Duration.FIVE_SECONDS);
+ if (!DynamicTasks.queue(SshEffectorTasks.isPidFromFileRunning(getPidFile()).runAsRoot()).get()) {
+ throw new IllegalStateException("The process for "+entity()+" appears not to be running (pid file "+getPidFile()+")");
+ }
+
+ // and set the PID
+ entity().sensors().set(Attributes.PID,
+ Integer.parseInt(DynamicTasks.queue(SshEffectorTasks.ssh("cat "+getPidFile()).runAsRoot()).block().getStdout().trim()));
+ return true;
+ }
+
+ protected boolean tryCheckStartService() {
+ if (getServiceName()==null) return false;
+
+ // if it's still up after 5s assume we are good (default behaviour)
+ Time.sleep(Duration.FIVE_SECONDS);
+ if (!((Integer)0).equals(DynamicTasks.queue(SshEffectorTasks.ssh("/etc/init.d/"+getServiceName()+" status").runAsRoot()).get())) {
+ throw new IllegalStateException("The process for "+entity()+" appears not to be running (service "+getServiceName()+")");
+ }
+
+ return true;
+ }
+
+ protected boolean tryCheckStartWindowsService() {
+ if (getWindowsServiceName()==null) return false;
+
+ // if it's still up after 5s assume we are good (default behaviour)
+ Time.sleep(Duration.FIVE_SECONDS);
+ if (!((Integer)0).equals(DynamicTasks.queue(SshEffectorTasks.ssh("sc query \""+getWindowsServiceName()+"\" | find \"RUNNING\"").runAsCommand()).get())) {
+ throw new IllegalStateException("The process for "+entity()+" appears not to be running (windowsService "+getWindowsServiceName()+")");
+ }
+
+ return true;
+ }
+
+ @Override
+ protected String stopProcessesAtMachine() {
+ boolean result = false;
+ result |= tryStopService();
+ result |= tryStopWindowsService();
+ result |= tryStopPid();
+ if (!result) {
+ throw new IllegalStateException("The process for "+entity()+" could not be stopped (no impl!)");
+ }
+ return "stopped";
+ }
+
+ @Override
+ protected StopMachineDetails<Integer> stopAnyProvisionedMachines() {
+ if (detectChefMode(entity())==ChefModes.KNIFE) {
+ DynamicTasks.queue(
+ // if this task fails show it as failed but don't block subsequent routines
+ // (ie allow us to actually decommission the machine)
+ // TODO args could be a List<String> config key ?
+ TaskTags.markInessential(
+ new KnifeTaskFactory<String>("delete node and client registration at chef server")
+ .add("knife node delete "+getNodeName()+" -y")
+ .add("knife client delete "+getNodeName()+" -y")
+ .requiringZeroAndReturningStdout()
+ .newTask() ));
+ }
+
+ return super.stopAnyProvisionedMachines();
+ }
+
+ protected boolean tryStopService() {
+ if (getServiceName()==null) return false;
+ int result = DynamicTasks.queue(SshEffectorTasks.ssh("/etc/init.d/"+getServiceName()+" stop").runAsRoot()).get();
+ if (0==result) return true;
+ if (entity().getAttribute(Attributes.SERVICE_STATE_ACTUAL)!=Lifecycle.RUNNING)
+ return true;
+
+ throw new IllegalStateException("The process for "+entity()+" appears could not be stopped (exit code "+result+" to service stop)");
+ }
+
+ protected boolean tryStopWindowsService() {
+ if (getWindowsServiceName()==null) return false;
+ int result = DynamicTasks.queue(SshEffectorTasks.ssh("sc query \""+getWindowsServiceName()+"\"").runAsCommand()).get();
+ if (0==result) return true;
+ if (entity().getAttribute(Attributes.SERVICE_STATE_ACTUAL)!=Lifecycle.RUNNING)
+ return true;
+
+ throw new IllegalStateException("The process for "+entity()+" appears could not be stopped (exit code "+result+" to service stop)");
+ }
+
+ protected boolean tryStopPid() {
+ Integer pid = entity().getAttribute(Attributes.PID);
+ if (pid==null) {
+ if (entity().getAttribute(Attributes.SERVICE_STATE_ACTUAL)==Lifecycle.RUNNING && getPidFile()==null)
+ log.warn("No PID recorded for "+entity()+" when running, with PID file "+getPidFile()+"; skipping kill in "+Tasks.current());
+ else
+ if (log.isDebugEnabled())
+ log.debug("No PID recorded for "+entity()+"; skipping ("+entity().getAttribute(Attributes.SERVICE_STATE_ACTUAL)+" / "+getPidFile()+")");
+ return false;
+ }
+
+ // allow non-zero exit as process may have already been killed
+ DynamicTasks.queue(SshEffectorTasks.ssh(
+ "kill "+pid, "sleep 5", BashCommands.ok("kill -9 "+pid)).allowingNonZeroExitCode().runAsRoot()).block();
+
+ if (DynamicTasks.queue(SshEffectorTasks.isPidRunning(pid).runAsRoot()).get()) {
+ throw new IllegalStateException("Process for "+entity()+" in "+pid+" still running after kill");
+ }
+ entity().sensors().set(Attributes.PID, null);
+ return true;
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/brooklyn-library/blob/737dc8a9/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefServerTasks.java
----------------------------------------------------------------------
diff --git a/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefServerTasks.java b/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefServerTasks.java
new file mode 100644
index 0000000..c81ff9d
--- /dev/null
+++ b/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefServerTasks.java
@@ -0,0 +1,97 @@
+/*
+ * 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.chef;
+
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.security.KeyPair;
+
+import org.apache.brooklyn.location.ssh.SshMachineLocation;
+import org.apache.brooklyn.util.core.crypto.SecureKeys;
+
+import com.google.common.base.Throwables;
+import com.google.common.io.Files;
+
+public class ChefServerTasks {
+
+ private static File chefKeyDir;
+
+ private synchronized static File getExtractedKeysDir() {
+ if (chefKeyDir==null) {
+ chefKeyDir = Files.createTempDir();
+ chefKeyDir.deleteOnExit();
+ }
+ return chefKeyDir;
+ }
+
+ /** extract key to a temp file, but one per machine, scheduled for deletion afterwards;
+ * we extract the key because chef has no way to accept passphrases at present */
+ synchronized static File extractKeyFile(SshMachineLocation machine) {
+ File f = new File(getExtractedKeysDir(), machine.getAddress().getHostName()+".pem");
+ if (f.exists()) return f;
+ KeyPair data = machine.findKeyPair();
+ if (data==null) return null;
+ try {
+ f.deleteOnExit();
+ Files.write(SecureKeys.stringPem(data), f, Charset.defaultCharset());
+ return f;
+ } catch (IOException e) {
+ throw Throwables.propagate(e);
+ }
+ }
+
+ public static KnifeTaskFactory<Boolean> isKnifeInstalled() {
+ return new KnifeTaskFactory<Boolean>("knife install check")
+ .knifeAddParameters("node list")
+ .notThrowingOnCommonKnifeErrors()
+ .returningIsExitCodeZero();
+ }
+
+ /** plain knife converge task - run list must be set, other arguments are optional */
+ public static KnifeConvergeTaskFactory<String> knifeConvergeTask() {
+ return new KnifeConvergeTaskFactory<String>("knife converge")
+ .requiringZeroAndReturningStdout();
+ }
+ /** knife converge task configured for this run list (and sudo) */
+ public static KnifeConvergeTaskFactory<String> knifeConvergeRunList(String runList) {
+ return knifeConvergeTask()
+ .knifeRunList(runList)
+ .knifeSudo(true);
+ }
+
+ /** knife converge task configured for this run list on windows (ssh) */
+ public static KnifeConvergeTaskFactory<String> knifeConvergeRunListWindowsSsh(String runList) {
+ return knifeConvergeTask()
+ .knifeRunList(runList)
+ .knifeSudo(false)
+ .knifeAddExtraBootstrapParameters("windows ssh");
+ }
+
+ /** knife converge task configured for this run list on windows (winrm) */
+ public static KnifeConvergeTaskFactory<String> knifeConvergeRunListWindowsWinrm(String runList) {
+ return knifeConvergeTask()
+ .knifeRunList(runList)
+ .knifeSudo(false)
+ .knifeAddExtraBootstrapParameters("windows winrm")
+ .knifePortUseKnifeDefault();
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/brooklyn-library/blob/737dc8a9/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefSoloDriver.java
----------------------------------------------------------------------
diff --git a/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefSoloDriver.java b/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefSoloDriver.java
new file mode 100644
index 0000000..6ee1786
--- /dev/null
+++ b/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefSoloDriver.java
@@ -0,0 +1,85 @@
+/*
+ * 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.chef;
+
+import org.apache.brooklyn.api.entity.EntityLocal;
+import org.apache.brooklyn.api.mgmt.TaskAdaptable;
+import org.apache.brooklyn.api.mgmt.TaskFactory;
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.core.config.ConfigKeys;
+import org.apache.brooklyn.entity.software.base.AbstractSoftwareProcessSshDriver;
+import org.apache.brooklyn.location.ssh.SshMachineLocation;
+import org.apache.brooklyn.util.core.task.DynamicTasks;
+
+import com.google.common.annotations.Beta;
+import com.google.common.reflect.TypeToken;
+
+/** Driver class to facilitate use of Chef */
+@Beta
+@Deprecated /** @deprecated since 0.7.0 use ChefEntity or ChefLifecycleEffectorTasks */
+public class ChefSoloDriver extends AbstractSoftwareProcessSshDriver implements ChefConfig {
+
+ @SuppressWarnings("serial")
+ public static final ConfigKey<TaskFactory<? extends TaskAdaptable<Boolean>>> IS_RUNNING_TASK = ConfigKeys.newConfigKey(
+ new TypeToken<TaskFactory<? extends TaskAdaptable<Boolean>>>() {},
+ "brooklyn.chef.task.driver.isRunningTask");
+
+ @SuppressWarnings("serial")
+ public static final ConfigKey<TaskFactory<?>> STOP_TASK = ConfigKeys.newConfigKey(
+ new TypeToken<TaskFactory<?>>() {},
+ "brooklyn.chef.task.driver.stopTask");
+
+ public ChefSoloDriver(EntityLocal entity, SshMachineLocation location) {
+ super(entity, location);
+ }
+
+ @Override
+ public void install() {
+ // TODO flag to force reinstallation
+ DynamicTasks.queue(
+ ChefSoloTasks.installChef(getInstallDir(), false),
+ ChefSoloTasks.installCookbooks(getInstallDir(), getRequiredConfig(CHEF_COOKBOOK_URLS), false));
+ }
+
+ @Override
+ public void customize() {
+ DynamicTasks.queue(ChefSoloTasks.buildChefFile(getRunDir(), getInstallDir(), "launch", getRequiredConfig(CHEF_RUN_LIST),
+ getEntity().getConfig(CHEF_LAUNCH_ATTRIBUTES)));
+ }
+
+ @Override
+ public void launch() {
+ DynamicTasks.queue(ChefSoloTasks.runChef(getRunDir(), "launch", getEntity().getConfig(CHEF_RUN_CONVERGE_TWICE)));
+ }
+
+ @Override
+ public boolean isRunning() {
+ return DynamicTasks.queue(getRequiredConfig(IS_RUNNING_TASK)).asTask().getUnchecked();
+ }
+
+ @Override
+ public void stop() {
+ DynamicTasks.queue(getRequiredConfig(STOP_TASK));
+ }
+
+ protected <T> T getRequiredConfig(ConfigKey<T> key) {
+ return ChefConfigs.getRequiredConfig(getEntity(), key);
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/brooklyn-library/blob/737dc8a9/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefSoloTasks.java
----------------------------------------------------------------------
diff --git a/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefSoloTasks.java b/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefSoloTasks.java
new file mode 100644
index 0000000..505f37e
--- /dev/null
+++ b/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefSoloTasks.java
@@ -0,0 +1,70 @@
+/*
+ * 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.chef;
+
+import java.util.Map;
+
+import org.apache.brooklyn.api.mgmt.TaskFactory;
+import org.apache.brooklyn.core.effector.ssh.SshEffectorTasks;
+import org.apache.brooklyn.util.ssh.BashCommands;
+
+import com.google.common.annotations.Beta;
+
+@Beta
+public class ChefSoloTasks {
+
+ public static TaskFactory<?> installChef(String chefDirectory, boolean force) {
+ // TODO check on entity whether it is chef _server_
+ String installCmd = cdAndRun(chefDirectory, ChefBashCommands.INSTALL_FROM_OPSCODE);
+ if (!force) installCmd = BashCommands.alternatives("which chef-solo", installCmd);
+ return SshEffectorTasks.ssh(installCmd).summary("install chef");
+ }
+
+ public static TaskFactory<?> installCookbooks(final String chefDirectory, final Map<String,String> cookbooksAndUrls, final boolean force) {
+ return ChefTasks.installCookbooks(chefDirectory, cookbooksAndUrls, force);
+ }
+
+ public static TaskFactory<?> installCookbook(String chefDirectory, String cookbookName, String cookbookArchiveUrl, boolean force) {
+ return ChefTasks.installCookbook(chefDirectory, cookbookName, cookbookArchiveUrl, force);
+ }
+
+ protected static String cdAndRun(String targetDirectory, String command) {
+ return BashCommands.chain("mkdir -p "+targetDirectory,
+ "cd "+targetDirectory,
+ command);
+ }
+
+ public static TaskFactory<?> buildChefFile(String runDirectory, String chefDirectory, String phase, Iterable<? extends String> runList,
+ Map<String, Object> optionalAttributes) {
+ return ChefTasks.buildChefFile(runDirectory, chefDirectory, phase, runList, optionalAttributes);
+ }
+
+ public static TaskFactory<?> runChef(String runDir, String phase) {
+ return runChef(runDir, phase, false);
+ }
+ /** see {@link ChefConfig#CHEF_RUN_CONVERGE_TWICE} for background on why 'twice' is available */
+ public static TaskFactory<?> runChef(String runDir, String phase, Boolean twice) {
+ String cmd = "sudo chef-solo -c "+phase+".rb -j "+phase+".json -ldebug";
+ if (twice!=null && twice) cmd = BashCommands.alternatives(cmd, cmd);
+
+ return SshEffectorTasks.ssh(cdAndRun(runDir, cmd)).
+ summary("run chef for "+phase).requiringExitCodeZero();
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/brooklyn-library/blob/737dc8a9/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefTasks.java
----------------------------------------------------------------------
diff --git a/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefTasks.java b/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefTasks.java
new file mode 100644
index 0000000..e2edcb2
--- /dev/null
+++ b/software/cm/chef/src/main/java/org/apache/brooklyn/entity/chef/ChefTasks.java
@@ -0,0 +1,154 @@
+/*
+ * 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.chef;
+
+import java.util.Map;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.mgmt.TaskAdaptable;
+import org.apache.brooklyn.api.mgmt.TaskFactory;
+import org.apache.brooklyn.core.effector.EffectorTasks;
+import org.apache.brooklyn.core.effector.ssh.SshEffectorTasks;
+import org.apache.brooklyn.util.collections.MutableMap;
+import org.apache.brooklyn.util.core.file.ArchiveTasks;
+import org.apache.brooklyn.util.core.file.ArchiveUtils.ArchiveType;
+import org.apache.brooklyn.util.core.task.DynamicTasks;
+import org.apache.brooklyn.util.core.task.TaskBuilder;
+import org.apache.brooklyn.util.core.task.Tasks;
+import org.apache.brooklyn.util.net.Urls;
+import org.apache.brooklyn.util.ssh.BashCommands;
+import org.apache.brooklyn.util.text.Identifiers;
+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.collect.ImmutableList;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+@Beta
+public class ChefTasks {
+
+ private static final Logger log = LoggerFactory.getLogger(ChefTasks.class);
+
+ public static TaskFactory<?> installChef(String chefDirectory, boolean force) {
+ // TODO check on entity whether it is chef _server_
+ String installCmd = cdAndRun(chefDirectory, ChefBashCommands.INSTALL_FROM_OPSCODE);
+ if (!force) installCmd = BashCommands.alternatives("which chef-solo", installCmd);
+ return SshEffectorTasks.ssh(installCmd).summary("install chef");
+ }
+
+ public static TaskFactory<?> installCookbooks(final String chefDirectory, final Map<String,String> cookbooksAndUrls, final boolean force) {
+ return Tasks.<Void>builder().displayName("install "+(cookbooksAndUrls==null ? "0" : cookbooksAndUrls.size())+" cookbook"+Strings.s(cookbooksAndUrls)).body(
+ new Runnable() {
+ @Override
+ public void run() {
+ Entity e = EffectorTasks.findEntity();
+ if (cookbooksAndUrls==null)
+ throw new IllegalStateException("No cookbooks defined to install at "+e);
+ for (String cookbook: cookbooksAndUrls.keySet())
+ DynamicTasks.queue(installCookbook(chefDirectory, cookbook, cookbooksAndUrls.get(cookbook), force));
+ }
+ }).buildFactory();
+ }
+
+ public static TaskFactory<?> installCookbook(final String chefDirectory, final String cookbookName, final String cookbookArchiveUrl, final boolean force) {
+ return new TaskFactory<TaskAdaptable<?>>() {
+ @Override
+ public TaskAdaptable<?> newTask() {
+ TaskBuilder<Void> tb = Tasks.<Void>builder().displayName("install cookbook "+cookbookName);
+
+ String cookbookDir = Urls.mergePaths(chefDirectory, cookbookName);
+ String privateTmpDirContainingUnpackedCookbook =
+ Urls.mergePaths(chefDirectory, "tmp-"+Strings.makeValidFilename(cookbookName)+"-"+Identifiers.makeRandomId(4));
+
+ // TODO - skip the install earlier if it exists and isn't forced
+// if (!force) {
+// // in builder.body, check
+// // "ls "+cookbookDir
+// // and stop if it's zero
+// // remove reference to 'force' below
+// }
+
+ String destName = null;
+ if (ArchiveType.of(cookbookArchiveUrl)==ArchiveType.UNKNOWN) {
+ destName = cookbookName + ".tgz";
+ log.debug("Assuming TGZ type for chef cookbook url "+cookbookArchiveUrl+"; it will be downloaded as "+destName);
+ }
+ tb.add(ArchiveTasks.deploy(null, null, cookbookArchiveUrl, EffectorTasks.findSshMachine(), privateTmpDirContainingUnpackedCookbook,
+ false, null, destName).newTask());
+
+ String installCmd = BashCommands.chain(
+ "cd "+privateTmpDirContainingUnpackedCookbook,
+ "COOKBOOK_EXPANDED_DIR=`ls`",
+ BashCommands.requireTest("`ls | wc -w` -eq 1",
+ "The deployed archive "+cookbookArchiveUrl+" must contain exactly one directory"),
+ "mv $COOKBOOK_EXPANDED_DIR '../"+cookbookName+"'",
+ "cd ..",
+ "rm -rf '"+privateTmpDirContainingUnpackedCookbook+"'");
+
+ installCmd = force ? BashCommands.alternatives("rm -rf "+cookbookDir, installCmd) : BashCommands.alternatives("ls "+cookbookDir+" > /dev/null 2> /dev/null", installCmd);
+ tb.add(SshEffectorTasks.ssh(installCmd).summary("renaming cookbook dir").requiringExitCodeZero().newTask());
+
+ return tb.build();
+ }
+ };
+ }
+
+ protected static String cdAndRun(String targetDirectory, String command) {
+ return BashCommands.chain("mkdir -p '"+targetDirectory+"'",
+ "cd '"+targetDirectory+"'",
+ command);
+ }
+
+ public static TaskFactory<?> buildChefFile(String runDirectory, String chefDirectory, String phase, Iterable<? extends String> runList,
+ Map<String, Object> optionalAttributes) {
+ // TODO if it's server, try knife first
+ // TODO configure add'l properties
+ String phaseRb =
+ "root = "
+ + "'"+runDirectory+"'"
+ // recommended alternate to runDir is the following, but it is not available in some rubies
+ //+ File.absolute_path(File.dirname(__FILE__))"+
+ + "\n"+
+ "file_cache_path root\n"+
+// "cookbook_path root + '/cookbooks'\n";
+ "cookbook_path '"+chefDirectory+"'\n";
+
+ Map<String,Object> phaseJsonMap = MutableMap.of();
+ if (optionalAttributes!=null)
+ phaseJsonMap.putAll(optionalAttributes);
+ if (runList!=null)
+ phaseJsonMap.put("run_list", ImmutableList.copyOf(runList));
+ Gson json = new GsonBuilder().create();
+ String phaseJson = json.toJson(phaseJsonMap);
+
+ return Tasks.sequential("build chef files for "+phase,
+ SshEffectorTasks.put(Urls.mergePaths(runDirectory)+"/"+phase+".rb").contents(phaseRb).createDirectory(),
+ SshEffectorTasks.put(Urls.mergePaths(runDirectory)+"/"+phase+".json").contents(phaseJson));
+ }
+
+ public static TaskFactory<?> runChef(String runDir, String phase) {
+ // TODO chef server
+ return SshEffectorTasks.ssh(cdAndRun(runDir, "sudo chef-solo -c "+phase+".rb -j "+phase+".json -ldebug")).
+ summary("run chef for "+phase).requiringExitCodeZero();
+ }
+
+}