/*
* The MIT License
*
* Copyright (c) 2010, Dominik Bartholdi
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.jvnet.hudson.plugins.scriptler;
import hudson.Extension;
import hudson.Util;
import hudson.model.Computer;
import hudson.model.ComputerSet;
import hudson.model.Hudson;
import hudson.model.Hudson.MasterComputer;
import hudson.model.ManagementLink;
import hudson.security.Permission;
import hudson.util.RemotingDiagnostics;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.ServletException;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.jvnet.hudson.plugins.scriptler.config.Script;
import org.jvnet.hudson.plugins.scriptler.config.ScriptlerConfiguration;
import org.jvnet.hudson.plugins.scriptler.share.Catalog;
import org.jvnet.hudson.plugins.scriptler.share.CatalogEntry;
import org.jvnet.hudson.plugins.scriptler.share.CatalogInfo;
import org.jvnet.hudson.plugins.scriptler.share.CatalogManager;
import org.kohsuke.stapler.HttpRedirect;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
/**
* Creates the link on the "manage hudson" page and handles all the web
* requests.
*
* @author domi
*
*/
@Extension
public class ScriptlerManagment extends ManagementLink {
private final static Logger LOGGER = Logger.getLogger(ScriptlerManagment.class.getName());
// always retrieve via getter
private ScriptlerConfiguration cfg = null;
/*
* (non-Javadoc)
*
* @see hudson.model.ManagementLink#getIconFileName()
*/
@Override
public String getIconFileName() {
return "notepad.gif";
}
/*
* (non-Javadoc)
*
* @see hudson.model.ManagementLink#getUrlName()
*/
@Override
public String getUrlName() {
return "scriptler";
}
/*
* (non-Javadoc)
*
* @see hudson.model.Action#getDisplayName()
*/
public String getDisplayName() {
return Messages.display_name();
}
@Override
public String getDescription() {
return Messages.description();
}
public ScriptlerManagment getScriptler() {
return this;
}
public ScriptlerConfiguration getConfiguration() {
if (cfg == null) {
try {
cfg = ScriptlerConfiguration.load();
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Failed to load scriptler configuration", e);
}
}
return cfg;
}
/**
* Downloads a script from a catalog and imports it to the local system.
*
* @param res
* request
* @param rsp
* response
* @param name
* the name of the file to be downloaded
* @param catalogName
* the catalog to download the file from
* @return same forward as from <code>doScriptAdd</code>
* @throws IOException
*/
public HttpResponse doDownloadScript(StaplerRequest res, StaplerResponse rsp, @QueryParameter("name") String name,
@QueryParameter("catalog") String catalogName) throws IOException {
checkPermission(Hudson.ADMINISTER);
CatalogInfo catInfo = getConfiguration().getCatalogInfo(catalogName);
CatalogManager catalogManager = new CatalogManager(catInfo);
Catalog catalog = catalogManager.loadCatalog();
CatalogEntry entry = catalog.getEntryByName(name);
String source = catalogManager.downloadScript(name);
return doScriptAdd(res, rsp, name, entry.comment, source);
}
/**
* Saves a script snipplet as file to the system.
*
* @param res
* response
* @param rsp
* request
* @param name
* the name for the file
* @param comment
* a comment
* @param script
* script code
* @return forward to 'index'
* @throws IOException
*/
public HttpResponse doScriptAdd(StaplerRequest res, StaplerResponse rsp, @QueryParameter("name") String name, @QueryParameter("comment") String comment,
@QueryParameter("script") String script) throws IOException {
checkPermission(Hudson.ADMINISTER);
if (StringUtils.isEmpty(script) || StringUtils.isEmpty(name)) {
throw new IllegalArgumentException("name and script must not be empty");
}
name = fixFileName(name);
// save (overwrite) the file/script
File newScriptFile = new File(getScriptDirectory(), name);
Writer writer = new FileWriter(newScriptFile);
writer.write(script);
writer.close();
// save (overwrite) the meta information
Script newScript = new Script(name, comment);
ScriptlerConfiguration cfg = getConfiguration();
cfg.addOrReplace(newScript);
cfg.save();
return new HttpRedirect("index");
}
/**
* Removes a script from the config and filesystem.
*
* @param res
* response
* @param rsp
* request
* @param name
* the name of the file to be removed
* @return forward to 'index'
* @throws IOException
*/
public HttpResponse doRemoveScript(StaplerRequest res, StaplerResponse rsp, @QueryParameter("name") String name) throws IOException {
checkPermission(Hudson.ADMINISTER);
// remove the file
File oldScript = new File(getScriptDirectory(), name);
oldScript.delete();
// remove the meta information
ScriptlerConfiguration cfg = getConfiguration();
cfg.removeScript(name);
cfg.save();
return new HttpRedirect("index");
}
/**
* Uploads a script and stores it with the given filename to the
* configuration. It will be stored on the filessytem.
*
* @param req
* request
* @return forward to index page.
* @throws IOException
* @throws ServletException
*/
public HttpResponse doUploadScript(StaplerRequest req) throws IOException, ServletException {
checkPermission(Hudson.ADMINISTER);
try {
File rootDir = getScriptDirectory();
ServletFileUpload upload = new ServletFileUpload(new DiskFileItemFactory());
FileItem fileItem = (FileItem) upload.parseRequest(req).get(0);
String fileName = Util.getFileName(fileItem.getName());
if (StringUtils.isEmpty(fileName)) {
return new HttpRedirect(".");
}
fileName = fixFileName(fileName);
fileItem.write(new File(rootDir, fileName));
Script script = getScript(fileName, false);
if (script == null) {
script = new Script(fileName, "uploaded");
}
ScriptlerConfiguration config = getConfiguration();
config.addOrReplace(script);
return new HttpRedirect("index");
} catch (IOException e) {
throw e;
} catch (Exception e) {
throw new ServletException(e);
}
}
/**
* Loads the script information.
*
* @param scriptName
* the name of the script
* @param withSrc
* should the script sources be loaded too?
* @return the script
*/
protected Script getScript(String scriptName, boolean withSrc) {
Script s = getConfiguration().getScriptByName(scriptName);
File scriptSrc = new File(getScriptDirectory(), scriptName);
if (withSrc) {
try {
Reader reader = new FileReader(scriptSrc);
String src = IOUtils.toString(reader);
s.setScript(src);
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "not able to load sources for script [" + scriptName + "]", e);
}
}
return s;
}
/**
* Display the screen to trigger a script. The source of the script get
* loaded from the filesystem and placed in the request to display it on the
* page before execution.
*
* @param req
* request
* @param rsp
* response
* @param scriptName
* the name of the script to be executed
* @throws IOException
* @throws ServletException
*/
public void doRunScript(StaplerRequest req, StaplerResponse rsp, @QueryParameter("name") String scriptName) throws IOException, ServletException {
checkPermission(Hudson.ADMINISTER);
Script script = getScript(scriptName, true);
req.setAttribute("script", script);
// set default selection
req.setAttribute("currentNode", "(master)");
req.getView(this, "runscript.jelly").forward(req, rsp);
}
/**
* Trigger/run/execute the script on a slave and show the result/output. The
* request then gets forward to <code>runscript.jelly</code> (This is
* usually also where the request came from). The script passed to this
* method gets restored in the request again (and not loaded from the
* system). This way one is able to modify the script before execution and
* reuse the modified version for further executions.
*
* @param req
* request
* @param rsp
* response
* @param scriptName
* the name of the script
* @param script
* the script code (groovy)
* @param node
* the node, to execute the code on.
* @throws IOException
* @throws ServletException
*/
public void doTriggerScript(StaplerRequest req, StaplerResponse rsp, @QueryParameter("scriptName") String scriptName,
@QueryParameter("script") String script, @QueryParameter("node") String node) throws IOException, ServletException {
checkPermission(Hudson.ADMINISTER);
// set the script info back to the request, to display it together with
// the output.
Script tempScript = getScript(scriptName, false);
tempScript.setScript(script);
req.setAttribute("script", tempScript);
req.setAttribute("currentNode", node);
String output = doScript(node, script);
req.setAttribute("output", output);
req.getView(this, "runscript.jelly").forward(req, rsp);
}
/**
* Runs the execution on a given slave.
*
* @param node
* where to run the script.
* @param scriptTxt
* the script (groovy) to be executed.
* @return the output
* @throws IOException
* @throws ServletException
*/
private String doScript(String node, String scriptTxt) throws IOException, ServletException {
String output = "[no output]";
if (node != null && scriptTxt != null) {
try {
Computer comp = Hudson.getInstance().getComputer(node);
if (comp == null && "(master)".equals(node)) {
output = RemotingDiagnostics.executeGroovy(scriptTxt, MasterComputer.localChannel);
} else if (comp == null) {
output = Messages.node_not_found(node);
} else {
if (comp.getChannel() == null) {
output = Messages.node_not_online(node);
}
else {
output = RemotingDiagnostics.executeGroovy(scriptTxt, comp.getChannel());
}
}
} catch (InterruptedException e) {
throw new ServletException(e);
}
}
return output;
}
/**
* Loads the script by its name and forwards the request to "edit.jelly".
*
* @param req
* request
* @param rsp
* response
* @param scriptName
* the name of the script to be loaded in to the edit view.
* @throws IOException
* @throws ServletException
*/
public void doEditScript(StaplerRequest req, StaplerResponse rsp, @QueryParameter("name") String scriptName) throws IOException, ServletException {
checkPermission(Hudson.ADMINISTER);
Script script = getScript(scriptName, true);
req.setAttribute("script", script);
req.getView(this, "edit.jelly").forward(req, rsp);
}
/**
* Gets the names of all configured slaves, regardless whether they are
* online.
*
* @return list with all slave names
*/
@SuppressWarnings("deprecation")
public List<String> getSlaveNames() {
ComputerSet computers = Hudson.getInstance().getComputer();
List<String> slaveNames = computers.get_slaveNames();
// slaveNames is unmodifiable, therefore create a new list
List<String> test = new ArrayList<String>();
test.addAll(slaveNames);
// add 'magic' name for master, so all nodes can be handled the same way
if (!test.contains("(master)")) {
test.add("(master)");
}
return test;
}
/**
* Gets the remote catalogs containing the available scripts for download.
*
* @return the catalog
*/
public List<Catalog> getCatalogs() {
List<Catalog> catalogs = new ArrayList<Catalog>();
List<CatalogInfo> catalogInfos = getConfiguration().getCatalogInfos();
for (CatalogInfo catalogInfo : catalogInfos) {
CatalogManager mgr = new CatalogManager(catalogInfo);
Catalog catalog = mgr.loadCatalog();
// as catalogInfo is marked as transient, we have to set it
catalog.setInfo(catalogInfo);
if (catalog != null) {
catalogs.add(catalog);
}
}
return catalogs;
}
/**
* returns the directory where the script files get stored
*
* @return the script directory
*/
public static File getScriptDirectory() {
return new File(getScriptlerHomeDirectory(), "scripts");
}
public static File getScriptlerHomeDirectory() {
return new File(Hudson.getInstance().getRootDir(), "scriptler");
}
private void checkPermission(Permission permission) {
Hudson.getInstance().checkPermission(permission);
}
private String fixFileName(String name) {
if (!name.endsWith(".groovy")) {
name += ".groovy";
}
// make sure we don't have spaces in the filename
name = name.replace(" ", "_").trim();
return name;
}
}