Skip to content

Commit

Permalink
first pass at jetty script
Browse files Browse the repository at this point in the history
exec throws a new ShellExecException when failure
added interrupt action to agent cli
  • Loading branch information
ypujante committed Jan 7, 2011
1 parent c87bbb7 commit 7f572d4
Show file tree
Hide file tree
Showing 11 changed files with 426 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,13 @@ public interface Agent
String executeAction(args) throws AgentException

/**
* Interrupts the action.
* Interrupts the action provided or the current one if neither <code>action</code> nor
* <code>actionId</code> is provided. If you provide one, you should provide only one of
* <code>action</code> or <code>actionId</code>.
*
* @param args.mountPoint same mount point provided during {@link #installScript(Object)}
* @param args.action the lifecycle method you want to interrupt on (for double checking)
* (optional: if not provided will cancel any action currently running on the mountpoint)
* @param args.action the lifecycle method you want to interrupt
* @param args.actionId the id of the action to interrupt
* @return <code>true</code> if the action was interrupted properly or <code>false</code> if
* there was no such action or already completed
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright (c) 2010-2011 LinkedIn, Inc
*
* Licensed 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.linkedin.glu.agent.api

/**
* @author yan@pongasoft.com */
public class ShellExecException extends ScriptException
{
private static final long serialVersionUID = 1L;

int res
byte[] output
byte[] error

ShellExecException()
{
}

ShellExecException(s)
{
super(s)
}

ShellExecException(s, throwable)
{
super(s, throwable)
}

String getStringOutput()
{
return toStringOutput(output)
}

String getStringError()
{
return toStringOutput(error)
}

/**
* Converts the output into a string. Assumes that the encoding is UTF-8. Replaces all line feeds
* by '\n' and remove the last line feed.
*/
private String toStringOutput(byte[] output)
{
if(output == null)
return null

return new String(output, "UTF-8").readLines().join('\n')
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ class ClientMain implements Startable
}

def agentActions =
['installScript', 'installScriptClassname', 'clearError', 'install', 'executeAction', 'waitForState', 'start', 'uninstallScript', 'forceUninstallScript', 'uninstall'].findAll {
['interruptAction', 'installScript', 'installScriptClassname', 'clearError', 'install', 'executeAction', 'waitForState', 'start', 'uninstallScript', 'forceUninstallScript', 'uninstall'].findAll {
Config.getOptionalString(config, it, null)
}

Expand Down Expand Up @@ -220,6 +220,23 @@ class ClientMain implements Startable
actionArgs: extractArgs(config))
}

// interruptAction
def interruptAction = { agent, mountPoint ->
def state = agent.getState(mountPoint: mountPoint)

if(state.transitionAction)
{
if(agent.interruptAction(mountPoint: mountPoint, action: state.transitionAction))
println "Interrupted action ${state.transitionAction}"
else
println "Action ${state.transitionAction} completed."
}
else
{
println "Not currently executing an action"
}
}

// waitForState
def waitForState = { agent, mountPoint ->
if(agent.waitForState(mountPoint: mountPoint,
Expand Down Expand Up @@ -263,7 +280,7 @@ class ClientMain implements Startable
'[-w state] [-t timeout] [-p parentMountPoint] [-m mountPoint]')
cli.a(longOpt: 'args', 'arguments of the script or action (ex: [a:\'12\'])', args:1, required: false)
cli.c(longOpt: 'installScriptClassname', 'install the script given a class name)', args:1, required: false)
cli.C(longOpt: 'fileContent', 'retrieves the file content)', args:1, required: false)
cli.C(longOpt: 'fileContent', 'retrieves the file content', args:1, required: false)
cli.e(longOpt: 'executeAction', 'executes the provided action (ex: install)', args:1, required: false)
cli.f(longOpt: 'clientConfigFile', 'the client config file', args: 1, required: false)
cli.F(longOpt: 'forceUninstallScript', 'force uninstall script', args: 0, required: false)
Expand All @@ -284,6 +301,7 @@ class ClientMain implements Startable
cli.v(longOpt: 'verbose', 'verbose', args: 0, required: false)
cli.w(longOpt: 'waitForState', 'wait for the provided state', args:1, required: false)
cli.x(longOpt: 'clearError', 'arguments of the script or action (ex: [a:\'12\'])', args:0, required: false)
cli.X(longOpt: 'interruptAction', 'interrupts the current action', args:0, required: false)
cli.y(longOpt: 'sync', 'sync', args: 0, required: false)

def options = cli.parse(args)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import org.linkedin.groovy.util.net.GroovyNetUtils
import org.linkedin.groovy.util.encryption.EncryptionUtils
import org.linkedin.groovy.util.io.GroovyIOUtils
import org.linkedin.util.lang.MemorySize
import org.linkedin.glu.agent.api.ShellExecException

/**
* contains the utility methods for the shell
Expand Down Expand Up @@ -675,6 +676,9 @@ def class ShellImpl implements Shell
*/
private String toStringCommandLine(commandLine)
{
if(commandLine instanceof GString)
commandLine = commandLine.toString()

if(!(commandLine instanceof String))
commandLine = commandLine.collect { it.toString() }.join(' ')

Expand Down Expand Up @@ -703,7 +707,11 @@ def class ShellImpl implements Shell
log.debug("output=${toStringOutput(map.output)}")
log.debug("error=${toStringOutput(map.error)}")
}
throw new ScriptException("Error while executing command ${commandLine}: res=${map.res} - output=${toLimitedStringOutput(map.output, 512)} - error=${toLimitedStringOutput(map.error, 512)}".toString())

ShellExecException exception =
new ShellExecException("Error while executing command ${commandLine}: res=${map.res} - output=${toLimitedStringOutput(map.output, 512)} - error=${toLimitedStringOutput(map.error, 512)}".toString())
map.each { k,v -> exception."${k}" = v}
throw exception
}

return toStringOutput(map.output)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def class FromLocationScriptFactory implements ScriptFactory, Serializable
{
if(!_script)
{
if(!_scriptFile)
if(!_scriptFile?.exists())
_scriptFile = scriptConfig.shell.fetch(_location)

Class scriptClass = new GroovyClassLoader(getClass().classLoader).parseClass(_scriptFile.file)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -300,12 +300,12 @@ def class ScriptNode implements Shutdownable
return future.cancel(true)
}
else
return false
return interruptCurrentExecution()
}

private FutureExecution findFutureByNameOrId(args)
{
def future
FutureExecution future = null
if(args.action)
{
future = _scriptExecution.findFutureActionByName(args.action)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2010-2010 LinkedIn, Inc
* Copyright (c) 2010-2011 LinkedIn, Inc
*
* Licensed 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
Expand Down Expand Up @@ -27,6 +27,7 @@ import org.linkedin.groovy.util.ivy.IvyURLHandler
import org.linkedin.groovy.util.io.GroovyIOUtils
import org.linkedin.groovy.util.collections.GroovyCollectionsUtils
import org.linkedin.groovy.util.net.GroovyNetUtils
import org.linkedin.glu.agent.api.ShellExecException

/**
* Test for various capabilities.
Expand Down Expand Up @@ -102,8 +103,21 @@ class TestCapabilities extends GroovyTestCase
def shell = new ShellImpl(fileSystem: fs)

// non existent script
assertEquals('Error while executing command /non/existent/666: res=127 - output= - error=bash: /non/existent/666: No such file or directory',
shouldFail(ScriptException) { shell.exec("/non/existent/666") })
try
{
shell.exec("/non/existent/666")
fail("should fail")
}
catch(ShellExecException e)
{
assertEquals(127, e.res)
assertEquals('', e.stringOutput)
String shellScriptError = 'bash: /non/existent/666: No such file or directory'
assertEquals(shellScriptError, e.stringError)
assertEquals('Error while executing command /non/existent/666: res=127 - output= - error='
+ shellScriptError,
e.message)
}

def shellScript = shell.fetch("./src/test/resources/shellScriptTestCapabilities.sh")

Expand All @@ -128,7 +142,22 @@ class TestCapabilities extends GroovyTestCase

// we make the shell script non executable
fs.chmod(shellScript, '-x')
assertTrue(shouldFail(ScriptException) { shell.exec("${shellScript} a b c") }.endsWith('Permission denied'))
try
{
shell.exec("${shellScript} a b c")
fail("should fail")
}
catch(ShellExecException e)
{
assertEquals(126, e.res)
assertEquals('', e.stringOutput)
String shellScriptError =
"bash: ${shellScript}: Permission denied".toString()
assertEquals(shellScriptError, e.stringError)
assertEquals("Error while executing command ${shellScript} a b c: " +
"res=126 - output= - error=${shellScriptError}",
e.message)
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion project-spec.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
spec = [
name: 'glu',
group: 'org.linkedin',
version: '1.5.1',
version: '1.6.0',

versions: [
grails: '1.3.5',
Expand Down
56 changes: 56 additions & 0 deletions scripts/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright (c) 2010-2011 LinkedIn, Inc
*
* Licensed 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.
*/

/**
* Configuring for all scripts
*/

import org.linkedin.gradle.tasks.SingleArtifactTask

def gluScriptsTestProjectName = 'org.linkedin.glu.scripts.tests'

configure(subprojects.findAll {it.name != gluScriptsTestProjectName}) {
apply plugin: 'groovy'
apply plugin: 'org.linkedin.release'

release {
releaseConfigurations << 'script'
}

dependencies {
compile spec.external.linkedinUtilsGroovy
compile project(':agent:org.linkedin.glu.agent-api')
groovy spec.external.groovy

//testCompile project(gluScriptsTestProjectName)
testCompile spec.external.junit
}

def scripts = fileTree(dir: 'src/main/groovy', include: '**/*.groovy')

scripts.each { File scriptFile ->
def baseName = scriptFile.name - '.groovy'
task([type: SingleArtifactTask], baseName) {
artifactFile = scriptFile
artifactReleaseInfo =
[
name: baseName,
extension: 'groovy',
configurations: ['script']
]
}
}
}
Loading

0 comments on commit 7f572d4

Please sign in to comment.