/* * 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.core.effector.EffectorBody; import org.apache.brooklyn.core.effector.Effectors; import org.apache.brooklyn.core.effector.ssh.SshEffectorTasks; import org.apache.brooklyn.core.entity.Entities; import org.apache.brooklyn.core.entity.EntityInternal; import org.apache.brooklyn.core.entity.EntityTasks; 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.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.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().displayName("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().displayName("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().displayName("reconfigure").body(new Runnable() { @Override public void run() { DynamicTasks.waitForLast(); ((EntityInternal)entity()).sensors().set(SoftwareProcess.INSTALL_DIR, (String)null); entity().config().set(SoftwareProcess.INSTALL_UNIQUE_LABEL, (String)null); entity().getConfigMap().addToLocalBag(parameters.getAllConfig()); entity().sensors().set(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 // TODO Want better way to append to the config (so can do it all in the spec) String launchParameters = dryRunChild.getConfig(BrooklynNode.EXTRA_LAUNCH_PARAMETERS); if (Strings.isBlank(launchParameters)) launchParameters = ""; else launchParameters += " "; launchParameters += "--highAvailability "+HighAvailabilityMode.HOT_STANDBY; ((EntityInternal)dryRunChild).config().set(BrooklynNode.EXTRA_LAUNCH_PARAMETERS, launchParameters); 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().displayName("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().displayName("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; } }