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:44 UTC
[26/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/brooklynnode/EntityHttpClientImpl.java
----------------------------------------------------------------------
diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/EntityHttpClientImpl.java b/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/EntityHttpClientImpl.java
new file mode 100644
index 0000000..8d8a396
--- /dev/null
+++ b/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/EntityHttpClientImpl.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.brooklynnode;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.net.URI;
+import java.util.Map;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.sensor.AttributeSensor;
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.core.mgmt.BrooklynTaskTags;
+import org.apache.brooklyn.util.collections.MutableMap;
+import org.apache.brooklyn.util.core.http.HttpTool;
+import org.apache.brooklyn.util.core.http.HttpToolResponse;
+import org.apache.brooklyn.util.core.task.Tasks;
+import org.apache.brooklyn.util.exceptions.Exceptions;
+import org.apache.brooklyn.util.net.Urls;
+import org.apache.brooklyn.util.stream.Streams;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.HttpClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+
+public class EntityHttpClientImpl implements EntityHttpClient {
+ private static final Logger LOG = LoggerFactory.getLogger(EntityHttpClientImpl.class);
+
+ protected static interface HttpCall {
+ public HttpToolResponse call(HttpClient client, URI uri);
+ }
+
+ protected Entity entity;
+ protected AttributeSensor<?> urlSensor;
+ protected ConfigKey<?> urlConfig;
+ protected Predicate<Integer> responseSuccess = ResponseCodePredicates.success();
+
+ protected EntityHttpClientImpl(Entity entity, AttributeSensor<?> urlSensor) {
+ this.entity = entity;
+ this.urlSensor = urlSensor;
+ }
+
+ protected EntityHttpClientImpl(Entity entity, ConfigKey<?> urlConfig) {
+ this.entity = entity;
+ this.urlConfig = urlConfig;
+ }
+
+ @Override
+ public HttpTool.HttpClientBuilder getHttpClientForBrooklynNode() {
+ String baseUrl = getEntityUrl();
+ HttpTool.HttpClientBuilder builder = HttpTool.httpClientBuilder()
+ .trustAll()
+ .laxRedirect(true)
+ .uri(baseUrl);
+ if (entity.getConfig(BrooklynNode.MANAGEMENT_USER) != null) {
+ UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(
+ entity.getConfig(BrooklynNode.MANAGEMENT_USER),
+ entity.getConfig(BrooklynNode.MANAGEMENT_PASSWORD));
+ builder.credentials(credentials);
+ }
+ return builder;
+ }
+
+ @Override
+ public EntityHttpClient responseSuccess(Predicate<Integer> responseSuccess) {
+ this.responseSuccess = checkNotNull(responseSuccess, "responseSuccess");
+ return this;
+ }
+
+ protected HttpToolResponse exec(String path, HttpCall httpCall) {
+ HttpClient client = Preconditions.checkNotNull(getHttpClientForBrooklynNode(), "No address info for "+entity)
+ .build();
+ String baseUri = getEntityUrl();
+ URI uri = URI.create(Urls.mergePaths(baseUri, path));
+
+ HttpToolResponse result;
+ try {
+ result = httpCall.call(client, uri);
+ } catch (Exception e) {
+ Exceptions.propagateIfFatal(e);
+ throw new IllegalStateException("Invalid response invoking " + uri + ": " + e, e);
+ }
+ Tasks.addTagDynamically(BrooklynTaskTags.tagForStream("http_response", Streams.byteArray(result.getContent())));
+ if (!responseSuccess.apply(result.getResponseCode())) {
+ LOG.warn("Invalid response invoking {}: response code {}\n{}: {}",
+ new Object[]{uri, result.getResponseCode(), result, new String(result.getContent())});
+ throw new IllegalStateException("Invalid response invoking " + uri + ": response code " + result.getResponseCode());
+ }
+ return result;
+ }
+
+ @Override
+ public HttpToolResponse get(String path) {
+ return exec(path, new HttpCall() {
+ @Override
+ public HttpToolResponse call(HttpClient client, URI uri) {
+ return HttpTool.httpGet(client, uri, MutableMap.<String, String>of());
+ }
+ });
+ }
+
+ @Override
+ public HttpToolResponse post(String path, final Map<String, String> headers, final byte[] body) {
+ return exec(path, new HttpCall() {
+ @Override
+ public HttpToolResponse call(HttpClient client, URI uri) {
+ return HttpTool.httpPost(client, uri, headers, body);
+ }
+ });
+ }
+
+ @Override
+ public HttpToolResponse post(String path, final Map<String, String> headers, final Map<String, String> formParams) {
+ return exec(path, new HttpCall() {
+ @Override
+ public HttpToolResponse call(HttpClient client, URI uri) {
+ return HttpTool.httpPost(client, uri, headers, formParams);
+ }
+ });
+ }
+
+ protected String getEntityUrl() {
+ Preconditions.checkState(urlSensor == null ^ urlConfig == null, "Exactly one of urlSensor and urlConfig should be non-null for entity " + entity);
+ Object url = null;
+ if (urlSensor != null) {
+ url = entity.getAttribute(urlSensor);
+ } else if (urlConfig != null) {
+ url = entity.getConfig(urlConfig);
+ }
+ Preconditions.checkNotNull(url, "URL sensor " + urlSensor + " for entity " + entity + " is empty");
+ return url.toString();
+ }
+
+ @Override
+ public HttpToolResponse delete(String path, final Map<String, String> headers) {
+ return exec(path, new HttpCall() {
+ @Override
+ public HttpToolResponse call(HttpClient client, URI uri) {
+ return HttpTool.httpDelete(client, uri, headers);
+ }
+ });
+ }
+}
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/LocalBrooklynNode.java
----------------------------------------------------------------------
diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/LocalBrooklynNode.java b/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/LocalBrooklynNode.java
new file mode 100644
index 0000000..38e8cfe
--- /dev/null
+++ b/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/LocalBrooklynNode.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.brooklynnode;
+
+import org.apache.brooklyn.api.entity.ImplementedBy;
+
+/**
+ * A {@link BrooklynNode} entity that represents the local Brooklyn service.
+ * <p>
+ * Management username and password can be specified in the {@code brooklyn.properties} file, as
+ * either specific username and password (useful when the credentials are set with SHA-256 hashes
+ * or via LDAP) or a username with separately configured webconsole plaintext password.
+ * <pre>
+ * brooklyn.entity.brooklynnode.local.user=admin
+ * brooklyn.entity.brooklynnode.local.password=password
+ * brooklyn.webconsole.security.user.admin.password=password
+ * </pre>
+ */
+@ImplementedBy(LocalBrooklynNodeImpl.class)
+public interface LocalBrooklynNode extends BrooklynNode {
+}
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/LocalBrooklynNodeImpl.java
----------------------------------------------------------------------
diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/LocalBrooklynNodeImpl.java b/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/LocalBrooklynNodeImpl.java
new file mode 100644
index 0000000..9f85ebe
--- /dev/null
+++ b/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/LocalBrooklynNodeImpl.java
@@ -0,0 +1,46 @@
+/*
+ * 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.brooklynnode;
+
+import org.apache.brooklyn.core.internal.BrooklynProperties;
+import org.apache.brooklyn.util.text.Strings;
+
+public class LocalBrooklynNodeImpl extends BrooklynNodeImpl implements LocalBrooklynNode {
+
+ private static final String LOCAL_BROOKLYN_NODE_KEY = "brooklyn.entity.brooklynnode.local.%s";
+ private static final String BROOKLYN_WEBCONSOLE_PASSWORD_KEY = "brooklyn.webconsole.security.user.%s.password";
+
+ @Override
+ protected void connectSensors() {
+ // Override management username and password from brooklyn.properties
+ BrooklynProperties properties = (BrooklynProperties) getManagementContext().getConfig();
+ String user = (String) properties.get(String.format(LOCAL_BROOKLYN_NODE_KEY, "user"));
+ String password = (String) properties.get(String.format(LOCAL_BROOKLYN_NODE_KEY, "password"));
+ if (Strings.isBlank(password)) {
+ if (Strings.isBlank(user)) user = "admin";
+ password = (String) properties.get(String.format(BROOKLYN_WEBCONSOLE_PASSWORD_KEY, user));
+ }
+ if (Strings.isNonBlank(user) && Strings.isNonBlank(password)) {
+ setConfig(MANAGEMENT_USER, user);
+ setConfig(MANAGEMENT_PASSWORD, password);
+ }
+ super.connectSensors();
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/RemoteEffectorBuilder.java
----------------------------------------------------------------------
diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/RemoteEffectorBuilder.java b/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/RemoteEffectorBuilder.java
new file mode 100644
index 0000000..a031930
--- /dev/null
+++ b/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/RemoteEffectorBuilder.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.entity.brooklynnode;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Map;
+
+import org.apache.brooklyn.api.effector.Effector;
+import org.apache.brooklyn.effector.core.Effectors;
+import org.apache.brooklyn.effector.core.Effectors.EffectorBuilder;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynEntityMirrorImpl.RemoteEffector;
+import org.apache.brooklyn.util.core.http.HttpToolResponse;
+
+import com.google.common.base.Function;
+
+public class RemoteEffectorBuilder {
+ private static class ResultParser implements Function<HttpToolResponse, String> {
+ @Override
+ public String apply(HttpToolResponse input) {
+ return input.getContentAsString();
+ }
+ }
+
+
+ public static Collection<Effector<String>> of(Collection<?> cfgEffectors) {
+ Collection<Effector<String>> effectors = new ArrayList<Effector<String>>();
+ for (Object objEff : cfgEffectors) {
+ Map<?, ?> cfgEff = (Map<?, ?>)objEff;
+ String effName = (String)cfgEff.get("name");
+ String description = (String)cfgEff.get("description");
+
+ EffectorBuilder<String> eff = Effectors.effector(String.class, effName);
+ Collection<?> params = (Collection<?>)cfgEff.get("parameters");
+
+ /* The *return type* should NOT be included in the signature here.
+ * It might be a type known only at the mirrored brooklyn node
+ * (in which case loading it here would fail); or possibly it could
+ * be a different version of the type here, in which case the signature
+ * would look valid here, but deserializing it would fail.
+ *
+ * Best to just pass the json representation back to the caller.
+ * (They won't be able to tell the difference between that and deserialize-then-serialize!)
+ */
+
+ if (description != null) {
+ eff.description(description);
+ }
+
+ for (Object objParam : params) {
+ buildParam(eff, (Map<?, ?>)objParam);
+ }
+
+ eff.impl(new RemoteEffector<String>(effName, new ResultParser()));
+ effectors.add(eff.build());
+ }
+ return effectors;
+ }
+
+ private static void buildParam(EffectorBuilder<String> eff, Map<?, ?> cfgParam) {
+ String name = (String)cfgParam.get("name");
+ String description = (String)cfgParam.get("description");
+ String defaultValue = (String)cfgParam.get("defaultValue");
+
+ eff.parameter(Object.class, name, description, defaultValue /*TypeCoercions.coerce(defaultValue, paramType)*/);
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/effector/BrooklynClusterUpgradeEffectorBody.java
----------------------------------------------------------------------
diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/effector/BrooklynClusterUpgradeEffectorBody.java b/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/effector/BrooklynClusterUpgradeEffectorBody.java
new file mode 100644
index 0000000..c6ec7b4
--- /dev/null
+++ b/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/effector/BrooklynClusterUpgradeEffectorBody.java
@@ -0,0 +1,206 @@
+/*
+ * 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.brooklynnode.effector;
+
+import java.io.File;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.concurrent.Callable;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.brooklyn.api.effector.Effector;
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.EntitySpec;
+import org.apache.brooklyn.api.entity.Group;
+import org.apache.brooklyn.api.mgmt.TaskAdaptable;
+import org.apache.brooklyn.api.mgmt.ha.HighAvailabilityMode;
+import org.apache.brooklyn.api.mgmt.ha.ManagementNodeState;
+import org.apache.brooklyn.effector.core.EffectorBody;
+import org.apache.brooklyn.effector.core.Effectors;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynCluster;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynNode;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynCluster.SelectMasterEffector;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynCluster.UpgradeClusterEffector;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynNode.SetHighAvailabilityModeEffector;
+import org.apache.brooklyn.entity.core.Attributes;
+import org.apache.brooklyn.entity.core.EntityPredicates;
+import org.apache.brooklyn.entity.core.EntityTasks;
+import org.apache.brooklyn.entity.group.DynamicCluster;
+import org.apache.brooklyn.entity.lifecycle.Lifecycle;
+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.Tasks;
+import org.apache.brooklyn.util.net.Urls;
+import org.apache.brooklyn.util.time.Duration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicates;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.Iterables;
+
+public class BrooklynClusterUpgradeEffectorBody extends EffectorBody<Void> implements UpgradeClusterEffector {
+
+ private static final Logger log = LoggerFactory.getLogger(BrooklynClusterUpgradeEffectorBody.class);
+
+ public static final Effector<Void> UPGRADE_CLUSTER = Effectors.effector(UpgradeClusterEffector.UPGRADE_CLUSTER)
+ .impl(new BrooklynClusterUpgradeEffectorBody()).build();
+
+ private final AtomicBoolean upgradeInProgress = new AtomicBoolean();
+
+ @Override
+ public Void call(ConfigBag parameters) {
+ if (!upgradeInProgress.compareAndSet(false, true)) {
+ throw new IllegalStateException("An upgrade is already in progress.");
+ }
+
+ EntitySpec<?> origMemberSpec = entity().getConfig(BrooklynCluster.MEMBER_SPEC);
+ Preconditions.checkNotNull(origMemberSpec, BrooklynCluster.MEMBER_SPEC.getName() + " is required for " + UpgradeClusterEffector.class.getName());
+
+ log.debug("Upgrading "+entity()+", changing "+BrooklynCluster.MEMBER_SPEC+" from "+origMemberSpec+" / "+origMemberSpec.getConfig());
+
+ boolean success = false;
+ try {
+ String newDownloadUrl = parameters.get(DOWNLOAD_URL);
+
+ EntitySpec<?> newMemberSpec = EntitySpec.create(origMemberSpec);
+
+ ConfigBag newConfig = ConfigBag.newInstance();
+ newConfig.putIfNotNull(DOWNLOAD_URL, newDownloadUrl);
+ newConfig.put(BrooklynNode.DISTRO_UPLOAD_URL, inferUploadUrl(newDownloadUrl));
+ newConfig.putAll(ConfigBag.newInstance(parameters.get(EXTRA_CONFIG)).getAllConfigAsConfigKeyMap());
+ newMemberSpec.configure(newConfig.getAllConfigAsConfigKeyMap());
+
+ entity().setConfig(BrooklynCluster.MEMBER_SPEC, newMemberSpec);
+
+ log.debug("Upgrading "+entity()+", new "+BrooklynCluster.MEMBER_SPEC+": "+newMemberSpec+" / "+newMemberSpec.getConfig()+" (adding: "+newConfig+")");
+
+ upgrade(parameters);
+
+ success = true;
+ } finally {
+ if (!success) {
+ log.debug("Upgrading "+entity()+" failed, will rethrow after restoring "+BrooklynCluster.MEMBER_SPEC+" to: "+origMemberSpec);
+ entity().setConfig(BrooklynCluster.MEMBER_SPEC, origMemberSpec);
+ }
+
+ upgradeInProgress.set(false);
+ }
+ return null;
+ }
+
+ private String inferUploadUrl(String newDownloadUrl) {
+ if (newDownloadUrl==null) return null;
+ boolean isLocal = "file".equals(Urls.getProtocol(newDownloadUrl)) || new File(newDownloadUrl).exists();
+ if (isLocal) {
+ return newDownloadUrl;
+ } else {
+ return null;
+ }
+ }
+
+ protected void upgrade(ConfigBag parameters) {
+ //TODO currently this will fight with auto-scaler policies; they must be turned off for upgrade to work
+
+ Group cluster = (Group)entity();
+ Collection<Entity> initialMembers = cluster.getMembers();
+ int initialClusterSize = initialMembers.size();
+
+ if (!BrooklynNodeUpgradeEffectorBody.isPersistenceModeEnabled(cluster)) {
+ // would could try a `forcePersistNow`, but that's sloppy;
+ // for now, require HA/persistence for upgrading
+ DynamicTasks.queue( Tasks.warning("Check persistence",
+ new IllegalStateException("Persistence does not appear to be enabled at this cluster. "
+ + "Cluster upgrade will not succeed unless a custom launch script enables it.")) );
+ }
+
+ //TODO we'd like to disable these nodes as standby targets, ie in some 'hot standby but not available for failover' mode
+ //currently if failover happens to a new node, assumptions below may fail and the cluster may require manual repair
+
+ //1. Initially create a single node to check if it will launch successfully
+ TaskAdaptable<Collection<Entity>> initialNodeTask = DynamicTasks.queue(newCreateNodesTask(1, "Creating first upgraded version node"));
+
+ //2. If everything is OK with the first node launch the rest as well
+ @SuppressWarnings("unused")
+ TaskAdaptable<Collection<Entity>> remainingNodesTask = DynamicTasks.queue(newCreateNodesTask(initialClusterSize - 1, "Creating remaining upgraded version nodes ("+(initialClusterSize - 1)+")"));
+
+ //3. Once we have all nodes running without errors switch master
+ DynamicTasks.queue(Effectors.invocation(cluster, BrooklynCluster.SELECT_MASTER, MutableMap.of(SelectMasterEffector.NEW_MASTER_ID,
+ Iterables.getOnlyElement(initialNodeTask.asTask().getUnchecked()).getId()))).asTask().getUnchecked();
+
+ //4. Stop the nodes which were running at the start of the upgrade call, but keep them around.
+ // Should we create a quarantine-like zone for old stopped version?
+ // For members that were created meanwhile - they will be using the new version already. If the new version
+ // isn't good then they will fail to start as well, forcing the policies to retry (and succeed once the
+ // URL is reverted).
+
+ //any other nodes created via other means should also be using the new spec, so initialMembers will be all the old version nodes
+ DynamicTasks.queue(Effectors.invocation(BrooklynNode.STOP_NODE_BUT_LEAVE_APPS, Collections.emptyMap(), initialMembers)).asTask().getUnchecked();
+ }
+
+ private TaskAdaptable<Collection<Entity>> newCreateNodesTask(int size, String name) {
+ return Tasks.<Collection<Entity>>builder().name(name).body(new CreateNodesCallable(size)).build();
+ }
+
+ protected class CreateNodesCallable implements Callable<Collection<Entity>> {
+ private final int size;
+ public CreateNodesCallable(int size) {
+ this.size = size;
+ }
+ @Override
+ public Collection<Entity> call() throws Exception {
+ return createNodes(size);
+ }
+ }
+
+ protected Collection<Entity> createNodes(int nodeCnt) {
+ DynamicCluster cluster = (DynamicCluster)entity();
+
+ //1. Create the nodes
+ Collection<Entity> newNodes = cluster.resizeByDelta(nodeCnt);
+
+ //2. Wait for them to be RUNNING (or at least STARTING to have completed)
+ // (should already be the case, because above is synchronous and, we think, it will fail if start does not succeed)
+ DynamicTasks.queue(EntityTasks.requiringAttributeEventually(newNodes, Attributes.SERVICE_STATE_ACTUAL,
+ Predicates.not(Predicates.equalTo(Lifecycle.STARTING)), Duration.minutes(30)));
+
+ //3. Set HOT_STANDBY in case it is not enabled on the command line ...
+ // TODO support via EntitySpec
+ DynamicTasks.queue(Effectors.invocation(
+ BrooklynNode.SET_HIGH_AVAILABILITY_MODE,
+ MutableMap.of(SetHighAvailabilityModeEffector.MODE, HighAvailabilityMode.HOT_STANDBY),
+ newNodes)).asTask().getUnchecked();
+ //... and wait until all of the nodes change state
+ // TODO fail quicker if state changes to FAILED
+ DynamicTasks.queue(EntityTasks.requiringAttributeEventually(newNodes, BrooklynNode.MANAGEMENT_NODE_STATE,
+ Predicates.equalTo(ManagementNodeState.HOT_STANDBY), Duration.FIVE_MINUTES));
+
+ // TODO also check that the nodes created all report the original master, in case persistence changes it
+
+ //5. Just in case check if all of the nodes are SERVICE_UP (which would rule out ON_FIRE as well)
+ Collection<Entity> failedNodes = Collections2.filter(newNodes, EntityPredicates.attributeEqualTo(BrooklynNode.SERVICE_UP, Boolean.FALSE));
+ if (!failedNodes.isEmpty()) {
+ throw new IllegalStateException("Nodes " + failedNodes + " are not " + BrooklynNode.SERVICE_UP + " though successfully in " + ManagementNodeState.HOT_STANDBY);
+ }
+ return newNodes;
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/effector/BrooklynNodeUpgradeEffectorBody.java
----------------------------------------------------------------------
diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/effector/BrooklynNodeUpgradeEffectorBody.java b/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/effector/BrooklynNodeUpgradeEffectorBody.java
new file mode 100644
index 0000000..baf4ae9
--- /dev/null
+++ b/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/effector/BrooklynNodeUpgradeEffectorBody.java
@@ -0,0 +1,229 @@
+/*
+ * 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.brooklynnode.effector;
+
+import java.util.Map;
+
+import org.apache.brooklyn.api.effector.Effector;
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.EntitySpec;
+import org.apache.brooklyn.api.entity.drivers.DriverDependentEntity;
+import org.apache.brooklyn.api.mgmt.ha.HighAvailabilityMode;
+import org.apache.brooklyn.api.mgmt.ha.ManagementNodeState;
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.core.config.ConfigKeys;
+import org.apache.brooklyn.core.config.MapConfigKey;
+import org.apache.brooklyn.effector.core.EffectorBody;
+import org.apache.brooklyn.effector.core.Effectors;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynCluster;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynNode;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynNodeDriver;
+import org.apache.brooklyn.entity.core.Entities;
+import org.apache.brooklyn.entity.core.EntityInternal;
+import org.apache.brooklyn.entity.core.EntityTasks;
+import org.apache.brooklyn.entity.software.base.SoftwareProcess;
+import org.apache.brooklyn.entity.software.base.SoftwareProcess.StopSoftwareParameters;
+import org.apache.brooklyn.entity.software.base.SoftwareProcess.StopSoftwareParameters.StopMode;
+import org.apache.brooklyn.sensor.ssh.SshEffectorTasks;
+import org.apache.brooklyn.util.core.config.ConfigBag;
+import org.apache.brooklyn.util.core.task.DynamicTasks;
+import org.apache.brooklyn.util.core.task.Tasks;
+import org.apache.brooklyn.util.net.Urls;
+import org.apache.brooklyn.util.text.Identifiers;
+import org.apache.brooklyn.util.text.Strings;
+import org.apache.brooklyn.util.time.Duration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.annotations.Beta;
+import com.google.common.base.Predicates;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.reflect.TypeToken;
+
+@SuppressWarnings("serial")
+/** Upgrades a brooklyn node in-place on the box,
+ * by creating a child brooklyn node and ensuring it can rebind in HOT_STANDBY
+ * <p>
+ * Requires the target node to have persistence enabled.
+ */
+public class BrooklynNodeUpgradeEffectorBody extends EffectorBody<Void> {
+
+ private static final Logger log = LoggerFactory.getLogger(BrooklynNodeUpgradeEffectorBody.class);
+
+ public static final ConfigKey<String> DOWNLOAD_URL = BrooklynNode.DOWNLOAD_URL.getConfigKey();
+ public static final ConfigKey<Boolean> DO_DRY_RUN_FIRST = ConfigKeys.newBooleanConfigKey(
+ "doDryRunFirst", "Test rebinding with a temporary instance before stopping the entity for upgrade.", true);
+ public static final ConfigKey<Map<String,Object>> EXTRA_CONFIG = MapConfigKey.builder(new TypeToken<Map<String,Object>>() {})
+ .name("extraConfig")
+ .description("Additional new config to set on the BrooklynNode as part of upgrading")
+ .build();
+
+ public static final Effector<Void> UPGRADE = Effectors.effector(Void.class, "upgrade")
+ .description("Changes the Brooklyn build used to run this node, "
+ + "by spawning a dry-run node then copying the installed files across. "
+ + "This node must be running for persistence for in-place upgrading to work.")
+ .parameter(BrooklynNode.SUGGESTED_VERSION)
+ .parameter(DOWNLOAD_URL)
+ .parameter(DO_DRY_RUN_FIRST)
+ .parameter(EXTRA_CONFIG)
+ .impl(new BrooklynNodeUpgradeEffectorBody()).build();
+
+ @Override
+ public Void call(ConfigBag parametersO) {
+ if (!isPersistenceModeEnabled(entity())) {
+ // would could try a `forcePersistNow`, but that's sloppy;
+ // for now, require HA/persistence for upgrading
+ DynamicTasks.queue( Tasks.warning("Check persistence",
+ new IllegalStateException("Persistence does not appear to be enabled at this cluster. "
+ + "In-place node upgrade will not succeed unless a custom launch script enables it.")) );
+ }
+
+ final ConfigBag parameters = ConfigBag.newInstanceCopying(parametersO);
+
+ /*
+ * all parameters are passed to children, apart from EXTRA_CONFIG
+ * whose value (as a map) is so passed; it provides an easy way to set extra config in the gui.
+ * (IOW a key-value mapping can be passed either inside EXTRA_CONFIG or as a sibling to EXTRA_CONFIG)
+ */
+ if (parameters.containsKey(EXTRA_CONFIG)) {
+ Map<String, Object> extra = parameters.get(EXTRA_CONFIG);
+ parameters.remove(EXTRA_CONFIG);
+ parameters.putAll(extra);
+ }
+ log.debug(this+" upgrading, using "+parameters);
+
+ final String bkName;
+ boolean doDryRunFirst = parameters.get(DO_DRY_RUN_FIRST);
+ if(doDryRunFirst) {
+ bkName = dryRunUpdate(parameters);
+ } else {
+ bkName = "direct-"+Identifiers.makeRandomId(4);
+ }
+
+ // Stop running instance
+ DynamicTasks.queue(Tasks.builder().name("shutdown node")
+ .add(Effectors.invocation(entity(), BrooklynNode.STOP_NODE_BUT_LEAVE_APPS, ImmutableMap.of(StopSoftwareParameters.STOP_MACHINE_MODE, StopMode.NEVER)))
+ .build());
+
+ // backup old files
+ DynamicTasks.queue(Tasks.builder().name("backup old version").body(new Runnable() {
+ @Override
+ public void run() {
+ String runDir = entity().getAttribute(SoftwareProcess.RUN_DIR);
+ String bkDir = Urls.mergePaths(runDir, "..", Urls.getBasename(runDir)+"-backups", bkName);
+ log.debug(this+" storing backup of previous version in "+bkDir);
+ DynamicTasks.queue(SshEffectorTasks.ssh(
+ "cd "+runDir,
+ "mkdir -p "+bkDir,
+ "mv * "+bkDir
+ // By removing the run dir of the entity we force it to go through
+ // the customize step again on start and re-generate local-brooklyn.properties.
+ ).summary("move files"));
+ }
+ }).build());
+
+ // Reconfigure entity
+ DynamicTasks.queue(Tasks.builder().name("reconfigure").body(new Runnable() {
+ @Override
+ public void run() {
+ DynamicTasks.waitForLast();
+ ((EntityInternal)entity()).setAttribute(SoftwareProcess.INSTALL_DIR, (String)null);
+ entity().setConfig(SoftwareProcess.INSTALL_UNIQUE_LABEL, (String)null);
+ entity().getConfigMap().addToLocalBag(parameters.getAllConfig());
+ entity().setAttribute(BrooklynNode.DOWNLOAD_URL, entity().getConfig(DOWNLOAD_URL));
+
+ // Setting SUGGESTED_VERSION will result in an new empty INSTALL_FOLDER, but clear it
+ // just in case the user specified already installed version.
+ ((BrooklynNodeDriver)((DriverDependentEntity<?>)entity()).getDriver()).clearInstallDir();
+ }
+ }).build());
+
+ // Start this entity, running the new version.
+ // This will download and install the new dist (if not already done by the dry run node).
+ DynamicTasks.queue(Effectors.invocation(entity(), BrooklynNode.START, ConfigBag.EMPTY));
+
+ return null;
+ }
+
+ private String dryRunUpdate(ConfigBag parameters) {
+ // TODO require entity() node state master or hot standby AND require persistence enabled, or a new 'force_attempt_upgrade' parameter to be applied
+ // TODO could have a 'skip_dry_run_upgrade' parameter
+ // TODO could support 'dry_run_only' parameter, with optional resumption tasks (eg new dynamic effector)
+
+ // 1 add new brooklyn version entity as child (so uses same machine), with same config apart from things in parameters
+ final Entity dryRunChild = entity().addChild(createDryRunSpec()
+ .displayName("Upgraded Version Dry-Run Node")
+ // install dir and label are recomputed because they are not inherited, and download_url will normally be different
+ .configure(parameters.getAllConfig()));
+
+ //force this to start as hot-standby
+ // TODO alternatively could use REST API as in BrooklynClusterUpgradeEffectorBody
+ String launchParameters = dryRunChild.getConfig(BrooklynNode.EXTRA_LAUNCH_PARAMETERS);
+ if (Strings.isBlank(launchParameters)) launchParameters = "";
+ else launchParameters += " ";
+ launchParameters += "--highAvailability "+HighAvailabilityMode.HOT_STANDBY;
+ ((EntityInternal)dryRunChild).setConfig(BrooklynNode.EXTRA_LAUNCH_PARAMETERS, launchParameters);
+
+ Entities.manage(dryRunChild);
+ final String dryRunNodeUid = dryRunChild.getId();
+ ((EntityInternal)dryRunChild).setDisplayName("Dry-Run Upgraded Brooklyn Node ("+dryRunNodeUid+")");
+
+ DynamicTasks.queue(Effectors.invocation(dryRunChild, BrooklynNode.START, ConfigBag.EMPTY));
+
+ // 2 confirm hot standby status
+ DynamicTasks.queue(EntityTasks.requiringAttributeEventually(dryRunChild, BrooklynNode.MANAGEMENT_NODE_STATE,
+ Predicates.equalTo(ManagementNodeState.HOT_STANDBY), Duration.FIVE_MINUTES));
+
+ // 3 stop new version
+ DynamicTasks.queue(Tasks.builder().name("shutdown transient node")
+ .add(Effectors.invocation(dryRunChild, BrooklynNode.STOP_NODE_BUT_LEAVE_APPS, ImmutableMap.of(StopSoftwareParameters.STOP_MACHINE_MODE, StopMode.NEVER)))
+ .build());
+
+ DynamicTasks.queue(Tasks.<Void>builder().name("remove transient node").body(
+ new Runnable() {
+ @Override
+ public void run() {
+ Entities.unmanage(dryRunChild);
+ }
+ }
+ ).build());
+
+ return dryRunChild.getId();
+ }
+
+ protected EntitySpec<? extends BrooklynNode> createDryRunSpec() {
+ return EntitySpec.create(BrooklynNode.class);
+ }
+
+ @Beta
+ static boolean isPersistenceModeEnabled(Entity entity) {
+ // TODO when there are PERSIST* options in BrooklynNode, look at them here!
+ // or, even better, make a REST call to check persistence
+ String params = null;
+ if (entity instanceof BrooklynCluster) {
+ EntitySpec<?> spec = entity.getConfig(BrooklynCluster.MEMBER_SPEC);
+ params = Strings.toString( spec.getConfig().get(BrooklynNode.EXTRA_LAUNCH_PARAMETERS) );
+ }
+ if (params==null) params = entity.getConfig(BrooklynNode.EXTRA_LAUNCH_PARAMETERS);
+ if (params==null) return false;
+ if (params.indexOf("persist")==0) return false;
+ return true;
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/effector/SelectMasterEffectorBody.java
----------------------------------------------------------------------
diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/effector/SelectMasterEffectorBody.java b/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/effector/SelectMasterEffectorBody.java
new file mode 100644
index 0000000..f9bc79f
--- /dev/null
+++ b/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/effector/SelectMasterEffectorBody.java
@@ -0,0 +1,174 @@
+/*
+ * 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.brooklynnode.effector;
+
+import java.util.NoSuchElementException;
+import java.util.concurrent.Callable;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.brooklyn.api.effector.Effector;
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.Group;
+import org.apache.brooklyn.api.mgmt.ha.HighAvailabilityMode;
+import org.apache.brooklyn.api.mgmt.ha.ManagementNodeState;
+import org.apache.brooklyn.effector.core.EffectorBody;
+import org.apache.brooklyn.effector.core.Effectors;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynCluster;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynNode;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynCluster.SelectMasterEffector;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynNode.SetHighAvailabilityModeEffector;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynNode.SetHighAvailabilityPriorityEffector;
+import org.apache.brooklyn.entity.core.EntityPredicates;
+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.repeat.Repeater;
+import org.apache.brooklyn.util.time.Duration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Objects;
+import com.google.common.collect.Iterables;
+
+public class SelectMasterEffectorBody extends EffectorBody<Void> implements SelectMasterEffector {
+ public static final Effector<Void> SELECT_MASTER = Effectors.effector(SelectMasterEffector.SELECT_MASTER).impl(new SelectMasterEffectorBody()).build();
+
+ private static final Logger LOG = LoggerFactory.getLogger(SelectMasterEffectorBody.class);
+
+ private static final int HA_STANDBY_PRIORITY = 0;
+ private static final int HA_MASTER_PRIORITY = 1;
+
+ private AtomicBoolean selectMasterInProgress = new AtomicBoolean();
+
+ @Override
+ public Void call(ConfigBag parameters) {
+ if (!selectMasterInProgress.compareAndSet(false, true)) {
+ throw new IllegalStateException("A master change is already in progress.");
+ }
+
+ try {
+ selectMaster(parameters);
+ } finally {
+ selectMasterInProgress.set(false);
+ }
+ return null;
+ }
+
+ private void selectMaster(ConfigBag parameters) {
+ String newMasterId = parameters.get(NEW_MASTER_ID);
+ Preconditions.checkNotNull(newMasterId, NEW_MASTER_ID.getName() + " parameter is required");
+
+ final Entity oldMaster = entity().getAttribute(BrooklynCluster.MASTER_NODE);
+ if (oldMaster != null && oldMaster.getId().equals(newMasterId)) {
+ LOG.info(newMasterId + " is already the current master, no change needed.");
+ return;
+ }
+
+ final Entity newMaster = getMember(newMasterId);
+
+ //1. Increase the priority of the node we wish to become master
+ toggleNodePriority(newMaster, HA_MASTER_PRIORITY);
+
+ //2. Demote the existing master so a new election takes place
+ try {
+ // this allows the finally block to run even on failure
+ DynamicTasks.swallowChildrenFailures();
+
+ if (oldMaster != null) {
+ demoteOldMaster(oldMaster, HighAvailabilityMode.HOT_STANDBY);
+ }
+
+ waitMasterHandover(oldMaster, newMaster);
+ } finally {
+ //3. Revert the priority of the node once it has become master
+ toggleNodePriority(newMaster, HA_STANDBY_PRIORITY);
+ }
+
+ checkMasterSelected(newMaster);
+ }
+
+ private void waitMasterHandover(final Entity oldMaster, final Entity newMaster) {
+ boolean masterChanged = Repeater.create()
+ .backoff(Repeater.DEFAULT_REAL_QUICK_PERIOD, 1.5, Duration.FIVE_SECONDS)
+ .limitTimeTo(Duration.ONE_MINUTE)
+ .until(new Callable<Boolean>() {
+ @Override
+ public Boolean call() throws Exception {
+ Entity master = getMasterNode();
+ return !Objects.equal(master, oldMaster) && master != null;
+ }
+ })
+ .run();
+ if (!masterChanged) {
+ LOG.warn("Timeout waiting for node to become master: " + newMaster + ".");
+ }
+ }
+
+ private void demoteOldMaster(Entity oldMaster, HighAvailabilityMode mode) {
+ ManagementNodeState oldState = DynamicTasks.queue(
+ Effectors.invocation(
+ oldMaster,
+ BrooklynNode.SET_HIGH_AVAILABILITY_MODE,
+ MutableMap.of(SetHighAvailabilityModeEffector.MODE, mode))
+ ).asTask().getUnchecked();
+
+ if (oldState != ManagementNodeState.MASTER) {
+ LOG.warn("The previous HA state on node " + oldMaster.getId() + " was " + oldState +
+ ", while the expected value is " + ManagementNodeState.MASTER + ".");
+ }
+ }
+
+ private void toggleNodePriority(Entity node, int newPriority) {
+ Integer oldPriority = DynamicTasks.queue(
+ Effectors.invocation(
+ node,
+ BrooklynNode.SET_HIGH_AVAILABILITY_PRIORITY,
+ MutableMap.of(SetHighAvailabilityPriorityEffector.PRIORITY, newPriority))
+ ).asTask().getUnchecked();
+
+ Integer expectedPriority = (newPriority == HA_MASTER_PRIORITY ? HA_STANDBY_PRIORITY : HA_MASTER_PRIORITY);
+ if (oldPriority != expectedPriority) {
+ LOG.warn("The previous HA priority on node " + node.getId() + " was " + oldPriority +
+ ", while the expected value is " + expectedPriority + " (while setting priority " +
+ newPriority + ").");
+ }
+ }
+
+ private void checkMasterSelected(Entity newMaster) {
+ Entity actualMaster = getMasterNode();
+ if (actualMaster != newMaster) {
+ throw new IllegalStateException("Expected node " + newMaster + " to be master, but found that " +
+ "master is " + actualMaster + " instead.");
+ }
+ }
+
+ private Entity getMember(String memberId) {
+ Group cluster = (Group)entity();
+ try {
+ return Iterables.find(cluster.getMembers(), EntityPredicates.idEqualTo(memberId));
+ } catch (NoSuchElementException e) {
+ throw new IllegalStateException(memberId + " is not an ID of brooklyn node in this cluster");
+ }
+ }
+
+ private Entity getMasterNode() {
+ return entity().getAttribute(BrooklynCluster.MASTER_NODE);
+ }
+}
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/effector/SetHighAvailabilityModeEffectorBody.java
----------------------------------------------------------------------
diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/effector/SetHighAvailabilityModeEffectorBody.java b/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/effector/SetHighAvailabilityModeEffectorBody.java
new file mode 100644
index 0000000..96d3e49
--- /dev/null
+++ b/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/effector/SetHighAvailabilityModeEffectorBody.java
@@ -0,0 +1,63 @@
+/*
+ * 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.brooklynnode.effector;
+
+import org.apache.brooklyn.api.effector.Effector;
+import org.apache.brooklyn.api.mgmt.ha.HighAvailabilityMode;
+import org.apache.brooklyn.api.mgmt.ha.ManagementNodeState;
+import org.apache.brooklyn.effector.core.EffectorBody;
+import org.apache.brooklyn.effector.core.Effectors;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynNode;
+import org.apache.brooklyn.entity.brooklynnode.EntityHttpClient;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynNode.SetHighAvailabilityModeEffector;
+import org.apache.brooklyn.sensor.feed.http.HttpValueFunctions;
+import org.apache.brooklyn.sensor.feed.http.JsonFunctions;
+import org.apache.brooklyn.util.core.config.ConfigBag;
+import org.apache.brooklyn.util.core.http.HttpToolResponse;
+import org.apache.brooklyn.util.guava.Functionals;
+import org.apache.brooklyn.util.javalang.Enums;
+import org.apache.http.HttpStatus;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableMap;
+
+public class SetHighAvailabilityModeEffectorBody extends EffectorBody<ManagementNodeState> implements SetHighAvailabilityModeEffector {
+ public static final Effector<ManagementNodeState> SET_HIGH_AVAILABILITY_MODE = Effectors.effector(SetHighAvailabilityModeEffector.SET_HIGH_AVAILABILITY_MODE).impl(new SetHighAvailabilityModeEffectorBody()).build();
+
+ @Override
+ public ManagementNodeState call(ConfigBag parameters) {
+ HighAvailabilityMode mode = parameters.get(MODE);
+ Preconditions.checkNotNull(mode, MODE.getName() + " parameter is required");
+
+ EntityHttpClient httpClient = ((BrooklynNode)entity()).http();
+ HttpToolResponse resp = httpClient.post("/v1/server/ha/state",
+ ImmutableMap.of("Brooklyn-Allow-Non-Master-Access", "true"),
+ ImmutableMap.of("mode", mode.toString()));
+
+ if (resp.getResponseCode() == HttpStatus.SC_OK) {
+ Function<HttpToolResponse, ManagementNodeState> parseRespone = Functionals.chain(
+ Functionals.chain(HttpValueFunctions.jsonContents(), JsonFunctions.cast(String.class)),
+ Enums.fromStringFunction(ManagementNodeState.class));
+ return parseRespone.apply(resp);
+ } else {
+ throw new IllegalStateException("Unexpected response code: " + resp.getResponseCode() + "\n" + resp.getContentAsString());
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/effector/SetHighAvailabilityPriorityEffectorBody.java
----------------------------------------------------------------------
diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/effector/SetHighAvailabilityPriorityEffectorBody.java b/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/effector/SetHighAvailabilityPriorityEffectorBody.java
new file mode 100644
index 0000000..4fd1aa4
--- /dev/null
+++ b/software/base/src/main/java/org/apache/brooklyn/entity/brooklynnode/effector/SetHighAvailabilityPriorityEffectorBody.java
@@ -0,0 +1,54 @@
+/*
+ * 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.brooklynnode.effector;
+
+import org.apache.brooklyn.api.effector.Effector;
+import org.apache.brooklyn.effector.core.EffectorBody;
+import org.apache.brooklyn.effector.core.Effectors;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynNode;
+import org.apache.brooklyn.entity.brooklynnode.EntityHttpClient;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynNode.SetHighAvailabilityPriorityEffector;
+import org.apache.brooklyn.util.core.config.ConfigBag;
+import org.apache.brooklyn.util.core.http.HttpToolResponse;
+import org.apache.http.HttpStatus;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
+
+public class SetHighAvailabilityPriorityEffectorBody extends EffectorBody<Integer> implements SetHighAvailabilityPriorityEffector {
+ public static final Effector<Integer> SET_HIGH_AVAILABILITY_PRIORITY = Effectors.effector(SetHighAvailabilityPriorityEffector.SET_HIGH_AVAILABILITY_PRIORITY).impl(new SetHighAvailabilityPriorityEffectorBody()).build();
+
+ @Override
+ public Integer call(ConfigBag parameters) {
+ Integer priority = parameters.get(PRIORITY);
+ Preconditions.checkNotNull(priority, PRIORITY.getName() + " parameter is required");
+
+ EntityHttpClient httpClient = ((BrooklynNode)entity()).http();
+ HttpToolResponse resp = httpClient.post("/v1/server/ha/priority",
+ ImmutableMap.of("Brooklyn-Allow-Non-Master-Access", "true"),
+ ImmutableMap.of("priority", priority.toString()));
+
+ if (resp.getResponseCode() == HttpStatus.SC_OK) {
+ return Integer.valueOf(resp.getContentAsString());
+ } else {
+ throw new IllegalStateException("Unexpected response code: " + resp.getResponseCode() + "\n" + resp.getContentAsString());
+ }
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/main/java/org/apache/brooklyn/entity/chef/ChefAttributeFeed.java
----------------------------------------------------------------------
diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/chef/ChefAttributeFeed.java b/software/base/src/main/java/org/apache/brooklyn/entity/chef/ChefAttributeFeed.java
new file mode 100644
index 0000000..6dcd9a9
--- /dev/null
+++ b/software/base/src/main/java/org/apache/brooklyn/entity/chef/ChefAttributeFeed.java
@@ -0,0 +1,410 @@
+/*
+ * 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.internal.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.entity.core.EntityInternal;
+import org.apache.brooklyn.sensor.feed.AbstractFeed;
+import org.apache.brooklyn.sensor.feed.PollHandler;
+import org.apache.brooklyn.sensor.feed.Poller;
+import org.apache.brooklyn.sensor.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 EntityLocal 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(EntityLocal 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(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>() {
+ public SshPollValue call() throws Exception {
+ ProcessTaskWrapper<String> taskWrapper = knifeTaskFactory.newTask();
+ final ExecutionContext executionContext = ((EntityInternal) entity).getManagementSupport().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);
+ }
+
+ @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 EntityLocal entity;
+
+ private CallInEntityExecutionContext(EntityLocal entity, Callable<T> job) {
+ this.job = job;
+ this.entity = entity;
+ }
+
+ @Override
+ public T call() throws Exception {
+ final ExecutionContext executionContext = ((EntityInternal) entity).getManagementSupport().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 EntityLocal entity;
+ private final Map<String, AttributeSensor<?>> chefAttributeSensors;
+
+ public SendChefAttributesToSensors(EntityLocal 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.setAttribute((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.setAttribute(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.setAttribute(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.setAttribute(attribute, null);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return super.toString()+"["+getDescription()+"]";
+ }
+
+ @Override
+ public String getDescription() {
+ return ""+chefAttributeSensors;
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/64c2b2e5/software/base/src/main/java/org/apache/brooklyn/entity/chef/ChefAttributePollConfig.java
----------------------------------------------------------------------
diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/chef/ChefAttributePollConfig.java b/software/base/src/main/java/org/apache/brooklyn/entity/chef/ChefAttributePollConfig.java
new file mode 100644
index 0000000..c6e1c6b
--- /dev/null
+++ b/software/base/src/main/java/org/apache/brooklyn/entity/chef/ChefAttributePollConfig.java
@@ -0,0 +1,53 @@
+/*
+ * 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.sensor.AttributeSensor;
+import org.apache.brooklyn.sensor.feed.PollConfig;
+
+import com.google.common.base.Function;
+import com.google.common.base.Functions;
+
+public class ChefAttributePollConfig<T> extends PollConfig<Object, T, ChefAttributePollConfig<T>>{
+
+ private String chefAttributePath;
+
+ @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/incubator-brooklyn/blob/64c2b2e5/software/base/src/main/java/org/apache/brooklyn/entity/chef/ChefBashCommands.java
----------------------------------------------------------------------
diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/chef/ChefBashCommands.java b/software/base/src/main/java/org/apache/brooklyn/entity/chef/ChefBashCommands.java
new file mode 100644
index 0000000..dde58d3
--- /dev/null
+++ b/software/base/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/incubator-brooklyn/blob/64c2b2e5/software/base/src/main/java/org/apache/brooklyn/entity/chef/ChefConfig.java
----------------------------------------------------------------------
diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/chef/ChefConfig.java b/software/base/src/main/java/org/apache/brooklyn/entity/chef/ChefConfig.java
new file mode 100644
index 0000000..974149e
--- /dev/null
+++ b/software/base/src/main/java/org/apache/brooklyn/entity/chef/ChefConfig.java
@@ -0,0 +1,98 @@
+/*
+ * 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");
+
+ @Deprecated /** @deprecatd since 0.7.0 use #CHEF_COOKBOOK_URLS */
+ @SetFromFlag("cookbooks")
+ public static final MapConfigKey<String> CHEF_COOKBOOKS = new MapConfigKey<String>(String.class, "brooklyn.chef.cookbooksUrls");
+
+ @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");
+
+}