/*
* RHQ Management Platform
* Copyright (C) 2010 Red Hat, Inc.
* All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 2 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
package org.rhq.bundle.ant.type;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.TreeSet;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.taskdefs.Chmod;
import org.apache.tools.ant.taskdefs.Copy;
import org.apache.tools.ant.taskdefs.Execute;
import org.apache.tools.ant.taskdefs.optional.unix.Symlink;
/**
* An Ant task that installs a system startup/shutdown service. Currently only Red Hat Linux versions are supported.
*
* @author Ian Springer
*/
public class SystemServiceType extends AbstractBundleType {
private static final String OS_NAME = System.getProperty("os.name");
private static final File REDHAT_RELEASE_FILE = new File("/etc/redhat-release");
private static final Set<Character> REDHAT_RUN_LEVELS = new HashSet<Character>();
static {
for (char c = '0'; c <= '6'; c++) {
REDHAT_RUN_LEVELS.add(c);
}
// TODO: Add 's' and/or 'S' depending on the flavor of UNIX.
}
private static final File INIT_DIR = new File("/etc/init.d");
private static final File SYSCONFIG_DIR = new File("/etc/sysconfig");
private static final File DEFAULT_ROOT = new File("/");
private String name;
private File scriptFile;
private File configFile;
private String scriptFileName;
private String configFileName;
private boolean overwriteScript;
private boolean overwriteConfig;
private boolean overwriteLinks = true;
private File root = DEFAULT_ROOT;
private String startLevels;
/**
* An integer from 0-99 indicating the service's start order - services with a lower priority number are started
* before services with a higher priority number.
*/
private Byte startPriority;
/**
* An integer from 0-99 indicating the service's stop order - services with a lower priority number are stopped
* before services with a higher priority number.
*/
private Byte stopPriority;
private Set<Character> startLevelChars;
private Set<Character> stopLevelChars;
private File scriptDestFile;
private File configDestFile;
public void validate() throws BuildException {
validateAttributes();
this.scriptDestFile = new File(getInitDir(), this.name);
this.configDestFile = new File(getSysConfigDir(), this.name);
}
public void init() throws BuildException {
if (!"Linux".equals(OS_NAME) || !REDHAT_RELEASE_FILE.exists()) {
throw new BuildException("The system-service element is only supported on Red Hat Linux systems.");
}
if (!this.scriptFile.exists() || this.scriptFile.isDirectory()) {
throw new BuildException("The 'scriptFile' attribute must be set to the path of an existing regular file.");
}
if (this.configFile != null && (!this.configFile.exists() || this.configFile.isDirectory())) {
throw new BuildException("The 'configFile' attribute must be set to the path of an existing regular file.");
}
}
public void install() throws BuildException {
// Install the config file if one was provided (e.g. /etc/sysconfig/named).
if (this.configFile != null) {
File sysconfigDir = getSysConfigDir();
if (!sysconfigDir.exists()) {
sysconfigDir.mkdirs();
}
if (!sysconfigDir.canWrite()) {
throw new BuildException(sysconfigDir + " directory is not writeable.");
}
// Don't copy the file ourselves - let our parent DeploymentUnitType handle it, so the deployment metadata
// (i.e. MD5) can be calculated and saved.
//copyFile(this.configFile, this.configDestFile, this.overwriteConfig);
setPermissions(this.configDestFile, "644");
}
// Install the script itself (e.g. /etc/init.d/named).
File initDir = getInitDir();
if (!initDir.exists()) {
initDir.mkdirs();
}
if (!initDir.canWrite()) {
throw new BuildException(initDir + " directory is not writeable.");
}
getProject().log("Installing service script " + this.scriptDestFile + "...");
// Don't copy the file ourselves - let our parent DeploymentUnitType handle it, so the deployment metadata
// (i.e. MD5) can be calculated and saved.
//copyFile(this.scriptFile, scriptDestFile, this.overwriteScript);
setPermissions(this.scriptDestFile, "755");
// Create the symlinks in the rcX.d dirs (e.g. /etc/rc3.d/S24named -> ../init.d/named)
createScriptSymlinks(this.scriptDestFile, this.startPriority, this.startLevelChars, 'S');
createScriptSymlinks(this.scriptDestFile, this.stopPriority, this.stopLevelChars, 'K');
}
private File getInitDir() {
return new File(this.root, INIT_DIR.getPath().substring(1));
}
public File getScriptDestFile() {
return this.scriptDestFile;
}
private File getSysConfigDir() {
return new File(this.root, SYSCONFIG_DIR.getPath().substring(1));
}
public File getConfigDestFile() {
return this.configDestFile;
}
public void start() throws BuildException {
File scriptFile = getScriptDestFile();
String[] commandLine = { scriptFile.getAbsolutePath(), "start" };
try {
executeCommand(commandLine);
} catch (IOException e) {
throw new BuildException("Failed to start " + this.name + " system service via command ["
+ Arrays.toString(commandLine) + "].", e);
}
}
public void stop() throws BuildException {
File scriptFile = getScriptDestFile();
String[] commandLine = { scriptFile.getAbsolutePath(), "stop" };
try {
executeCommand(commandLine);
} catch (IOException e) {
throw new BuildException("Failed to stop " + this.name + " system service via command ["
+ Arrays.toString(commandLine) + "].", e);
}
}
public void uninstall() throws BuildException {
// TODO
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getScriptFileName() {
return scriptFileName;
}
public File getScriptFile() {
return scriptFile;
}
public void setScriptFile(String scriptFile) {
File file = new File(scriptFile);
if (file.isAbsolute()) {
throw new BuildException("Path specified by 'scriptFile' attribute (" + scriptFile
+ ") is not relative - it must be a relative path, relative to the Ant basedir.");
}
this.scriptFileName = scriptFile;
this.scriptFile = getProject().resolveFile(scriptFile);
}
public String getConfigFileName() {
return configFileName;
}
public File getConfigFile() {
return configFile;
}
public void setConfigFile(String configFile) {
File file = new File(configFile);
if (file.isAbsolute()) {
throw new BuildException("Path specified by 'configFile' attribute (" + configFile
+ ") is not relative - it must be a relative path, relative to the Ant basedir.");
}
this.configFileName = configFile;
this.configFile = getProject().resolveFile(configFile);
}
public boolean isOverwriteScript() {
return overwriteScript;
}
public void setOverwriteScript(boolean overwriteScript) {
this.overwriteScript = overwriteScript;
}
public boolean isOverwriteConfig() {
return overwriteConfig;
}
public void setOverwriteConfig(boolean overwriteConfig) {
this.overwriteConfig = overwriteConfig;
}
public boolean isOverwriteLinks() {
return overwriteLinks;
}
public void setOverwriteLinks(boolean overwriteLinks) {
this.overwriteLinks = overwriteLinks;
}
public String getStartLevels() {
return startLevels;
}
public void setStartLevels(String startLevels) {
this.startLevels = startLevels;
}
public byte getStartPriority() {
return startPriority;
}
public void setStartPriority(byte startPriority) {
this.startPriority = startPriority;
}
public byte getStopPriority() {
return stopPriority;
}
public void setStopPriority(byte stopPriority) {
this.stopPriority = stopPriority;
}
public File getRoot() {
return root;
}
public void setRoot(File root) {
this.root = root;
}
/**
* Ensure we have a consistent and legal set of attributes, and set
* any internal flags necessary based on different combinations
* of attributes.
*
* @throws BuildException if an error occurs
*/
protected void validateAttributes() throws BuildException {
if (this.name == null) {
throw new BuildException("The 'name' attribute is required.");
}
if (this.name.length() == 0) {
throw new BuildException("The 'name' attribute must have a non-empty value.");
}
if (this.scriptFile == null) {
throw new BuildException("The 'scriptFile' attribute is required.");
}
if (this.startLevels == null) {
throw new BuildException("The 'startLevels' attribute is required.");
}
if (this.startLevels.length() == 0) {
throw new BuildException("The 'startLevels' attribute must have a non-empty value.");
}
this.startLevelChars = parseLevels(this.startLevels);
this.stopLevelChars = new TreeSet<Character>();
for (char level : REDHAT_RUN_LEVELS) {
if (!this.startLevelChars.contains(level)) {
this.stopLevelChars.add(level);
}
}
if (this.startPriority == null) {
throw new BuildException("The 'startPriority' attribute is required.");
}
if (this.startPriority < 0 || this.startPriority > 99) {
throw new BuildException("The 'startPriority' attribute must be >=0 and <= 99.");
}
if (this.stopPriority == null) {
throw new BuildException("The 'stopPriority' attribute is required.");
}
if (this.stopPriority < 0 || this.stopPriority > 99) {
throw new BuildException("The 'startPriority' attribute must be >=0 and <= 99.");
}
if (!this.root.exists()) {
this.root.mkdirs();
if (!this.root.exists()) {
throw new BuildException("Failed to create root directory " + this.root
+ " as specified by 'root' attribute.");
}
}
if (!this.root.isDirectory()) {
throw new BuildException("The 'root' attribute must be set to the path of a directory.");
}
if (!this.root.equals(DEFAULT_ROOT)) {
getProject().log("Using root " + this.root + ".");
}
}
private static Set<Character> parseLevels(String levels) {
Set<Character> levelChars = new TreeSet<Character>();
String[] tokens = levels.split("[ ]*,[ ]*");
for (String token : tokens) {
if (!token.equals("")) {
Character level;
try {
if (token.length() != 1) {
throw new Exception();
}
level = token.charAt(0);
if (!REDHAT_RUN_LEVELS.contains(level)) {
throw new Exception();
}
} catch (Exception e) {
throw new BuildException(
"Invalid run level: "
+ token
+ " - the 'startLevels' attribute must be a comma-separated list of run levels - the valid levels are "
+ REDHAT_RUN_LEVELS + ".");
}
if (levelChars.contains(level)) {
throw new BuildException("The 'startLevels' attribute defines run level " + level
+ " more than once.");
}
levelChars.add(level);
}
}
return levelChars;
}
private void createScriptSymlinks(File scriptFile, byte priority, Set<Character> levels, char fileNamePrefix) {
String priorityString = String.format("%02d", priority);
for (char level : levels) {
File rcDir = new File(this.root, "etc/rc" + level + ".d");
if (!rcDir.exists()) {
rcDir.mkdirs();
}
if (!rcDir.exists()) {
throw new BuildException(rcDir + " does not exist.");
}
if (!rcDir.isDirectory()) {
throw new BuildException(rcDir + " exists but is not a directory.");
}
if (!rcDir.isDirectory()) {
throw new BuildException(rcDir + " directory is not writeable.");
}
File link = new File(rcDir, fileNamePrefix + priorityString + this.name);
getProject().log("Creating symbolic link " + link + " referencing " + scriptFile + "...");
createSymlink(scriptFile, link, this.overwriteLinks);
}
}
private void copyFile(File sourceFile, File destFile, boolean overwrite) {
Copy copyTask = new Copy();
copyTask.setProject(getProject());
copyTask.init();
copyTask.setFile(sourceFile);
copyTask.setTofile(destFile);
copyTask.setOverwrite(overwrite);
copyTask.execute();
}
private void createSymlink(File targetFile, File linkFile, boolean overwrite) {
Symlink symlinkTask = new Symlink();
symlinkTask.setProject(getProject());
symlinkTask.init();
symlinkTask.setResource(targetFile.getAbsolutePath());
symlinkTask.setLink(linkFile.getAbsolutePath());
symlinkTask.setOverwrite(overwrite);
symlinkTask.execute();
}
private int executeCommand(String[] commandLine) throws IOException {
Execute executeTask = new Execute();
executeTask.setCommandline(commandLine);
return executeTask.execute();
}
private void setPermissions(File file, String perms) {
Chmod chmodTask = new Chmod();
chmodTask.setProject(getProject());
chmodTask.init();
chmodTask.setFile(file);
chmodTask.setPerm(perms);
chmodTask.execute();
}
}