/*
* 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.salt;
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.server.BrooklynServerConfig;
import org.apache.brooklyn.entity.software.base.lifecycle.MachineLifecycleEffectorTasks;
import org.apache.brooklyn.util.core.task.DynamicTasks;
import org.apache.brooklyn.util.core.task.Tasks;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.brooklyn.util.net.Urls;
import org.apache.brooklyn.util.ssh.BashCommands;
import org.apache.brooklyn.util.time.Duration;
import org.apache.brooklyn.util.time.Time;
import com.google.common.annotations.Beta;
import com.google.common.base.Supplier;
/**
* Creates effectors to start, restart, and stop processes using SaltStack.
* <p>
* Instances of this should use the {@link SaltConfig} 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 SaltLifecycleEffectorTasks extends MachineLifecycleEffectorTasks implements SaltConfig {
private static final Logger log = LoggerFactory.getLogger(SaltLifecycleEffectorTasks.class);
protected SaltStackMaster master = null;
protected String pidFile, serviceName, windowsServiceName;
public SaltLifecycleEffectorTasks() {
}
public SaltLifecycleEffectorTasks usePidFile(String pidFile) {
this.pidFile = pidFile;
return this;
}
public SaltLifecycleEffectorTasks useService(String serviceName) {
this.serviceName = serviceName;
return this;
}
public SaltLifecycleEffectorTasks useWindowsService(String serviceName) {
this.windowsServiceName = serviceName;
return this;
}
public SaltLifecycleEffectorTasks master(SaltStackMaster master) {
this.master = master;
return this;
}
@Override
public void attachLifecycleEffectors(Entity entity) {
if (pidFile==null && serviceName==null && getClass().equals(SaltLifecycleEffectorTasks.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);
}
@Override
protected String startProcessesAtMachine(Supplier<MachineLocation> machineS) {
startMinionAsync();
return "salt start tasks submitted";
}
protected void startMinionAsync() {
// TODO make directories more configurable (both for ssh-drivers and for this)
String installDir = Urls.mergePaths(BrooklynServerConfig.getMgmtBaseDir(entity().getManagementContext()), "salt-install");
String runDir = Urls.mergePaths(BrooklynServerConfig.getMgmtBaseDir(entity().getManagementContext()),
"apps/"+entity().getApplicationId()+"/salt-entities/"+entity().getId());
Boolean masterless = entity().getConfig(SaltConfig.MASTERLESS_MODE);
if (masterless) {
DynamicTasks.queue(
SaltTasks.installFormulas(installDir, SaltConfigs.getRequiredConfig(entity(), SALT_FORMULAS), false),
SaltTasks.buildSaltFile(runDir,
SaltConfigs.getRequiredConfig(entity(), SALT_RUN_LIST),
entity().getConfig(SALT_LAUNCH_ATTRIBUTES)),
SaltTasks.installSaltMinion(entity(), runDir, installDir, false),
SaltTasks.runSalt(runDir));
} else {
throw new UnsupportedOperationException("Salt master mode not yet supported for minions");
}
}
@Override
protected void postStartCustom() {
boolean result = false;
result |= tryCheckStartPid();
result |= tryCheckStartService();
result |= tryCheckStartWindowsService();
if (!result) {
throw new IllegalStateException("The process for "+entity()+" appears not to be running (no way to check!)");
}
}
protected boolean tryCheckStartPid() {
if (pidFile==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(pidFile).runAsRoot()).get()) {
throw new IllegalStateException("The process for "+entity()+" appears not to be running (pid file "+pidFile+")");
}
// and set the PID
entity().sensors().set(Attributes.PID,
Integer.parseInt(DynamicTasks.queue(SshEffectorTasks.ssh("cat "+pidFile).runAsRoot()).block().getStdout().trim()));
return true;
}
protected boolean tryCheckStartService() {
if (serviceName==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/"+serviceName+" status").runAsRoot()).get())) {
throw new IllegalStateException("The process for "+entity()+" appears not to be running (service "+serviceName+")");
}
return true;
}
protected boolean tryCheckStartWindowsService() {
if (windowsServiceName==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 \""+serviceName+"\" | find \"RUNNING\"").runAsCommand()).get())) {
throw new IllegalStateException("The process for "+entity()+" appears not to be running (windowsService "+windowsServiceName+")");
}
return true;
}
@Override
protected String stopProcessesAtMachine() {
boolean result = false;
result |= tryStopService();
result |= tryStopPid();
if (!result) {
throw new IllegalStateException("The process for "+entity()+" appears could not be stopped (no impl!)");
}
return "stopped";
}
protected boolean tryStopService() {
if (serviceName==null) return false;
int result = DynamicTasks.queue(SshEffectorTasks.ssh("/etc/init.d/"+serviceName+" stop").runAsRoot()).get();
if (0==result) return true;
if (entity().getAttribute(Attributes.SERVICE_STATE)!=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)==Lifecycle.RUNNING && pidFile==null)
log.warn("No PID recorded for "+entity()+" when running, with PID file "+pidFile+"; skipping kill in "+Tasks.current());
else
if (log.isDebugEnabled())
log.debug("No PID recorded for "+entity()+"; skipping ("+entity().getAttribute(Attributes.SERVICE_STATE)+" / "+pidFile+")");
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;
}
/**
* {@inheritDoc}
*
* @return the Salt master entity if it exists.
* @see #master(SaltStackMaster)
* @see SaltConfig#MASTERLESS_MODE
*/
@Override
public SaltStackMaster getMaster() {
return master;
}
}