blob: a142de4303f2498a40c0446bde01d9e8b2282d28 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.brooklyn.entity.chef;
import java.util.Collection;
import java.util.Map;
import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.location.MachineLocation;
import org.apache.brooklyn.core.effector.ssh.SshEffectorTasks;
import org.apache.brooklyn.core.entity.Attributes;
import org.apache.brooklyn.core.entity.lifecycle.Lifecycle;
import org.apache.brooklyn.core.location.Machines;
import org.apache.brooklyn.entity.software.base.SoftwareProcess;
import org.apache.brooklyn.entity.software.base.lifecycle.MachineLifecycleEffectorTasks;
import org.apache.brooklyn.location.ssh.SshMachineLocation;
import org.apache.brooklyn.util.collections.Jsonya;
import org.apache.brooklyn.util.collections.Jsonya.Navigator;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.core.config.ConfigBag;
import org.apache.brooklyn.util.core.task.DynamicTasks;
import org.apache.brooklyn.util.core.task.TaskTags;
import org.apache.brooklyn.util.core.task.Tasks;
import org.apache.brooklyn.util.core.task.system.ProcessTaskWrapper;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.net.Urls;
import org.apache.brooklyn.util.ssh.BashCommands;
import org.apache.brooklyn.util.text.Strings;
import org.apache.brooklyn.util.time.Duration;
import org.apache.brooklyn.util.time.Time;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.annotations.Beta;
import com.google.common.base.Preconditions;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableList;
/**
* Creates effectors to start, restart, and stop processes using Chef.
* <p>
* Instances of this should use the {@link ChefConfig} config attributes to configure startup,
* and invoke {@link #usePidFile(String)} or {@link #useService(String)} to determine check-running and stop behaviour.
* Alternatively this can be subclassed and {@link #postStartCustom()} and {@link #stopProcessesAtMachine()} overridden.
*
* @since 0.6.0
**/
@Beta
public class ChefLifecycleEffectorTasks extends MachineLifecycleEffectorTasks implements ChefConfig {
private static final Logger log = LoggerFactory.getLogger(ChefLifecycleEffectorTasks.class);
protected String _pidFile, _serviceName, _windowsServiceName;
public ChefLifecycleEffectorTasks() {
}
public ChefLifecycleEffectorTasks usePidFile(String pidFile) {
this._pidFile = pidFile;
return this;
}
public ChefLifecycleEffectorTasks useService(String serviceName) {
this._serviceName = serviceName;
return this;
}
public ChefLifecycleEffectorTasks useWindowsService(String serviceName) {
this._windowsServiceName = serviceName;
return this;
}
public String getPidFile() {
if (_pidFile!=null) return _pidFile;
return _pidFile = entity().getConfig(ChefConfig.PID_FILE);
}
public String getServiceName() {
if (_serviceName!=null) return _serviceName;
return _serviceName = entity().getConfig(ChefConfig.SERVICE_NAME);
}
protected String getNodeName() {
// (node name is needed so we can node delete it)
// TODO would be better if CHEF_NODE_NAME were a freemarker template, could access entity.id, or hostname, etc,
// in addition to supporting hard-coded node names (which is all we support so far).
String nodeName = entity().getConfig(ChefConfig.CHEF_NODE_NAME);
if (Strings.isNonBlank(nodeName)) return Strings.makeValidFilename(nodeName);
// node name is taken from ID of this entity, if not specified
return entity().getId();
}
public String getWindowsServiceName() {
if (_windowsServiceName!=null) return _windowsServiceName;
return _windowsServiceName = entity().getConfig(ChefConfig.WINDOWS_SERVICE_NAME);
}
@Override
public void attachLifecycleEffectors(Entity entity) {
if (getPidFile()==null && getServiceName()==null && getClass().equals(ChefLifecycleEffectorTasks.class)) {
// warn on incorrect usage
log.warn("Uses of "+getClass()+" must define a PID file or a service name (or subclass and override {start,stop} methods as per javadoc) " +
"in order for check-running and stop to work");
}
super.attachLifecycleEffectors(entity);
}
public static ChefModes detectChefMode(Entity entity) {
ChefModes mode = entity.getConfig(ChefConfig.CHEF_MODE);
if (mode == ChefModes.AUTODETECT) {
// TODO server via API
ProcessTaskWrapper<Boolean> installCheck = DynamicTasks.queue(
ChefServerTasks.isKnifeInstalled());
mode = installCheck.get() ? ChefModes.KNIFE : ChefModes.SOLO;
log.debug("Using Chef in "+mode+" mode due to autodetect exit code "+installCheck.getExitCode());
}
Preconditions.checkNotNull(mode, "Non-null "+ChefConfig.CHEF_MODE+" required for "+entity);
return mode;
}
@Override
protected String startProcessesAtMachine(Supplier<MachineLocation> machineS) {
ChefModes mode = detectChefMode(entity());
switch (mode) {
case KNIFE:
startWithKnifeAsync();
break;
case SOLO:
startWithChefSoloAsync();
break;
default:
throw new IllegalStateException("Unknown Chef mode "+mode+" when starting processes for "+entity());
}
return "chef start tasks submitted ("+mode+")";
}
protected String getPrimaryCookbook() {
return entity().getConfig(CHEF_COOKBOOK_PRIMARY_NAME);
}
@SuppressWarnings({ "unchecked", "deprecation" })
protected void startWithChefSoloAsync() {
String baseDir = MachineLifecycleEffectorTasks.resolveOnBoxDir(entity(), Machines.findUniqueMachineLocation(entity().getLocations(), SshMachineLocation.class).get());
String installDir = Urls.mergePaths(baseDir, "installs/chef");
@SuppressWarnings("rawtypes")
Map<String, String> cookbooks = (Map)
ConfigBag.newInstance( entity().getConfig(CHEF_COOKBOOK_URLS) )
.putIfAbsent( entity().getConfig(CHEF_COOKBOOK_URLS) )
.getAllConfig();
if (cookbooks.isEmpty())
log.warn("No cookbook_urls set for "+entity()+"; launch will likely fail subsequently");
DynamicTasks.queue(
ChefSoloTasks.installChef(installDir, false),
ChefSoloTasks.installCookbooks(installDir, cookbooks, false));
// TODO chef for and run a prestart recipe if necessary
// TODO open ports
String primary = getPrimaryCookbook();
// put all config under brooklyn/cookbook/config
Navigator<MutableMap<Object, Object>> attrs = Jsonya.newInstancePrimitive().at("brooklyn");
if (Strings.isNonBlank(primary)) attrs.at(primary);
attrs.at("config");
attrs.put( entity().config().getBag().getAllConfig() );
// and put launch attrs at root
try {
attrs.root().put((Map<?,?>)Tasks.resolveDeepValue(entity().getConfig(CHEF_LAUNCH_ATTRIBUTES), Object.class, entity().getExecutionContext()));
} catch (Exception e) { Exceptions.propagate(e); }
Collection<? extends String> runList = entity().getConfig(CHEF_LAUNCH_RUN_LIST);
if (runList==null) runList = entity().getConfig(CHEF_RUN_LIST);
if (runList==null) {
if (Strings.isNonBlank(primary)) runList = ImmutableList.of(primary+"::"+"start");
else throw new IllegalStateException("Require a primary cookbook or a run_list to effect "+"start"+" on "+entity());
}
String runDir = Urls.mergePaths(baseDir,
"apps/"+entity().getApplicationId()+"/chef/entities/"+entity().getEntityType().getSimpleName()+"_"+entity().getId());
DynamicTasks.queue(ChefSoloTasks.buildChefFile(runDir, installDir, "launch",
runList, (Map<String, Object>) attrs.root().get()));
DynamicTasks.queue(ChefSoloTasks.runChef(runDir, "launch", entity().getConfig(CHEF_RUN_CONVERGE_TWICE)));
}
@SuppressWarnings({ "unchecked", "rawtypes", "deprecation" })
protected void startWithKnifeAsync() {
// TODO prestart, ports (as above); also, note, some aspects of this are untested as we need a chef server
String primary = getPrimaryCookbook();
// put all config under brooklyn/cookbook/config
Navigator<MutableMap<Object, Object>> attrs = Jsonya.newInstancePrimitive().at("brooklyn");
if (Strings.isNonBlank(primary)) attrs.at(primary);
attrs.at("config");
attrs.put( entity().config().getBag().getAllConfig() );
// and put launch attrs at root
try {
attrs.root().put((Map<?,?>)Tasks.resolveDeepValue(entity().getConfig(CHEF_LAUNCH_ATTRIBUTES), Object.class, entity().getExecutionContext()));
} catch (Exception e) { Exceptions.propagate(e); }
Collection<? extends String> runList = entity().getConfig(CHEF_LAUNCH_RUN_LIST);
if (runList==null) runList = entity().getConfig(CHEF_RUN_LIST);
if (runList==null) {
if (Strings.isNonBlank(primary)) runList = ImmutableList.of(primary+"::"+"start");
else throw new IllegalStateException("Require a primary cookbook or a run_list to effect "+"start"+" on "+entity());
}
DynamicTasks.queue(
ChefServerTasks.knifeConvergeTask()
.knifeNodeName(getNodeName())
.knifeRunList(Strings.join(runList, ","))
.knifeAddAttributes((Map) attrs.root().get())
.knifeRunTwice(entity().getConfig(CHEF_RUN_CONVERGE_TWICE)) );
}
@Override
protected void postStartCustom() {
boolean result = false;
result |= tryCheckStartPid();
result |= tryCheckStartService();
result |= tryCheckStartWindowsService();
if (!result) {
log.warn("No way to check whether "+entity()+" is running; assuming yes");
}
entity().sensors().set(SoftwareProcess.SERVICE_UP, true);
super.postStartCustom();
}
protected boolean tryCheckStartPid() {
if (getPidFile()==null) return false;
// if it's still up after 5s assume we are good (default behaviour)
Time.sleep(Duration.FIVE_SECONDS);
if (!DynamicTasks.queue(SshEffectorTasks.isPidFromFileRunning(getPidFile()).runAsRoot()).get()) {
throw new IllegalStateException("The process for "+entity()+" appears not to be running (pid file "+getPidFile()+")");
}
// and set the PID
entity().sensors().set(Attributes.PID,
Integer.parseInt(DynamicTasks.queue(SshEffectorTasks.ssh("cat "+getPidFile()).runAsRoot()).block().getStdout().trim()));
return true;
}
protected boolean tryCheckStartService() {
if (getServiceName()==null) return false;
// if it's still up after 5s assume we are good (default behaviour)
Time.sleep(Duration.FIVE_SECONDS);
if (!((Integer)0).equals(DynamicTasks.queue(SshEffectorTasks.ssh("/etc/init.d/"+getServiceName()+" status").runAsRoot()).get())) {
throw new IllegalStateException("The process for "+entity()+" appears not to be running (service "+getServiceName()+")");
}
return true;
}
protected boolean tryCheckStartWindowsService() {
if (getWindowsServiceName()==null) return false;
// if it's still up after 5s assume we are good (default behaviour)
Time.sleep(Duration.FIVE_SECONDS);
if (!((Integer)0).equals(DynamicTasks.queue(SshEffectorTasks.ssh("sc query \""+getWindowsServiceName()+"\" | find \"RUNNING\"").runAsCommand()).get())) {
throw new IllegalStateException("The process for "+entity()+" appears not to be running (windowsService "+getWindowsServiceName()+")");
}
return true;
}
@Override
protected String stopProcessesAtMachine() {
boolean result = false;
result |= tryStopService();
result |= tryStopWindowsService();
result |= tryStopPid();
if (!result) {
throw new IllegalStateException("The process for "+entity()+" could not be stopped (no impl!)");
}
return "stopped";
}
@Override
protected StopMachineDetails<Integer> stopAnyProvisionedMachines() {
if (detectChefMode(entity())==ChefModes.KNIFE) {
DynamicTasks.queue(
// if this task fails show it as failed but don't block subsequent routines
// (ie allow us to actually decommission the machine)
// TODO args could be a List<String> config key ?
TaskTags.markInessential(
new KnifeTaskFactory<String>("delete node and client registration at chef server")
.add("knife node delete "+getNodeName()+" -y")
.add("knife client delete "+getNodeName()+" -y")
.requiringZeroAndReturningStdout()
.newTask() ));
}
return super.stopAnyProvisionedMachines();
}
protected boolean tryStopService() {
if (getServiceName()==null) return false;
int result = DynamicTasks.queue(SshEffectorTasks.ssh("/etc/init.d/"+getServiceName()+" stop").runAsRoot()).get();
if (0==result) return true;
if (entity().getAttribute(Attributes.SERVICE_STATE_ACTUAL)!=Lifecycle.RUNNING)
return true;
throw new IllegalStateException("The process for "+entity()+" appears could not be stopped (exit code "+result+" to service stop)");
}
protected boolean tryStopWindowsService() {
if (getWindowsServiceName()==null) return false;
int result = DynamicTasks.queue(SshEffectorTasks.ssh("sc query \""+getWindowsServiceName()+"\"").runAsCommand()).get();
if (0==result) return true;
if (entity().getAttribute(Attributes.SERVICE_STATE_ACTUAL)!=Lifecycle.RUNNING)
return true;
throw new IllegalStateException("The process for "+entity()+" appears could not be stopped (exit code "+result+" to service stop)");
}
protected boolean tryStopPid() {
Integer pid = entity().getAttribute(Attributes.PID);
if (pid==null) {
if (entity().getAttribute(Attributes.SERVICE_STATE_ACTUAL)==Lifecycle.RUNNING && getPidFile()==null)
log.warn("No PID recorded for "+entity()+" when running, with PID file "+getPidFile()+"; skipping kill in "+Tasks.current());
else
if (log.isDebugEnabled())
log.debug("No PID recorded for "+entity()+"; skipping ("+entity().getAttribute(Attributes.SERVICE_STATE_ACTUAL)+" / "+getPidFile()+")");
return false;
}
// allow non-zero exit as process may have already been killed
DynamicTasks.queue(SshEffectorTasks.ssh(
"kill "+pid, "sleep 5", BashCommands.ok("kill -9 "+pid)).allowingNonZeroExitCode().runAsRoot()).block();
if (DynamicTasks.queue(SshEffectorTasks.isPidRunning(pid).runAsRoot()).get()) {
throw new IllegalStateException("Process for "+entity()+" in "+pid+" still running after kill");
}
entity().sensors().set(Attributes.PID, null);
return true;
}
}