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"); + +}
