diff --git a/README.md b/README.md index 6ee7a48..32e27ec 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ This plugin requires three properties that need to be configured for each step: - `SALT_API_END_POINT`: the URL of the salt-api endpoint (e.g. https://localhost:8000) - `Function`: the function to be passed to the salt-api call (excluding the target) --- For example, if you enter `test.ping` for the function value, the resulting salt call will be `salt <​yourHostName>​ test.ping`. The target will always default to the hostname of the Rundeck server. +-- For example, if you enter `test.ping` for the function value, the resulting salt call will be `salt <​yourHostName>​ test.ping`. The target will always default to the hostname of the Rundeck server. You can enter for example `cmd.run_all "ls -l /tmp"` or `state.sls test001` for the function value. - `SALT_API_EAUTH`: the authenticati​on mechanism that should be used by salt-api -- This would be the equivalent to the `-a` parameter being passed on the command line (e.g. `salt -a pam test.ping`) - `SALT_API_VERSION` (optional): The expected version of salt-api. Defaults to latest if left blank. diff --git a/src/main/java/org/rundeck/plugin/salt/output/SaltFullJsonReturnHandler.java b/src/main/java/org/rundeck/plugin/salt/output/SaltFullJsonReturnHandler.java new file mode 100644 index 0000000..91d0dd6 --- /dev/null +++ b/src/main/java/org/rundeck/plugin/salt/output/SaltFullJsonReturnHandler.java @@ -0,0 +1,141 @@ +/** +* Copyright (c) 2013, salesforce.com, inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without modification, are permitted provided +* that the following conditions are met: +* +* Redistributions of source code must retain the above copyright notice, this list of conditions and the +* following disclaimer. +* +* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and +* the following disclaimer in the documentation and/or other materials provided with the distribution. +* +* Neither the name of salesforce.com, inc. nor the names of its contributors may be used to endorse or +* promote products derived from this software without specific prior written permission. +* +* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED +* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +* PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +* TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +* POSSIBILITY OF SUCH DAMAGE. +*/ + +package org.rundeck.plugin.salt.output; + +import java.lang.reflect.Type; +import java.util.Map; +import java.util.ArrayList; +import java.util.List; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.JsonSyntaxException; +import java.util.Collection; +import java.util.HashMap; +import java.util.Set; + +/** +* Handler for generating {@link SaltReturnResponse} from minion json responses. +*/ +public class SaltFullJsonReturnHandler implements SaltReturnHandler { + + protected String exitCodeKey; + protected String standardOutputKey; + protected String standardErrorKey; + + public void setExitCodeKey(String exitCodeKey) { + this.exitCodeKey = exitCodeKey; + } + + public void setStandardOutputKey(String standardOutputKey) { + this.standardOutputKey = standardOutputKey; + } + + public void setStandardErrorKey(String standardErrorKey) { + this.standardErrorKey = standardErrorKey; + } + + @Override + public String toString() { + return "SaltFullJsonReturnHandler [exitCodeKey=" + exitCodeKey + ", standardOutputKey=" + standardOutputKey + + ", standardErrorKey=" + standardErrorKey + "]"; + } + + /** + * Deserializes a {@link SaltReturnResponse} from a salt minion json response using + * the specified exit code, standard output, and standard error keys. + * + * @param rawResponse + * a minion's full json response. + * + * @throws SaltReturnResponseParseException + * if there was an error interpreting the response + */ + @Override + public SaltReturnResponse extractResponse(String rawResponse) throws SaltReturnResponseParseException { + try { + Gson gson = new Gson(); + Object result = gson.fromJson(rawResponse, Object.class); + SaltReturnResponse response = new SaltReturnResponse(); + + if (exitCodeKey != null) { + List results=getValues(result, exitCodeKey); + if (results.size() == 0) { + throw new SaltReturnResponseParseException("No " + exitCodeKey + " attribute in JSON output"); + } + Integer exitCode=new Integer(0); + for (String tmp : results) { + if (!"true".equalsIgnoreCase(tmp)) + exitCode=1; + } + response.setExitCode(exitCode); + } + + if (standardOutputKey != null) { + List results=getValues(result, standardOutputKey); + if (results.size() == 0) { + throw new SaltReturnResponseParseException("No " + standardOutputKey + " attribute in JSON output"); + } + response.addOutput(results.toString()); + } + + if (standardErrorKey != null) { + List results=getValues(result, standardErrorKey); + if (results.size() == 0) { + throw new SaltReturnResponseParseException("No " + standardErrorKey + " attribute in JSON output"); + } + response.addError(results.toString()); + } + + return response; + } catch (JsonSyntaxException e) { + throw new SaltReturnResponseParseException(e); + } + } + + private List getValues(Object object, String attribute) { + ArrayList attributeValues = new ArrayList(); + if (object instanceof Map) { + Map map = (Map) object; + for ( String key : (Set)map.keySet() ) { + if (attribute.equalsIgnoreCase(key)) { + attributeValues.add(map.get(key).toString()); + } + } + Collection values = map.values(); + for (Object value : values) + attributeValues.addAll(getValues(value, attribute)); + } + else if (object instanceof Collection) { + Collection values = (Collection) object; + for (Object value : values) { + attributeValues.addAll(getValues(value, attribute)); + } + } + return attributeValues; + } +} diff --git a/src/main/resources/defaultReturners.yaml b/src/main/resources/defaultReturners.yaml index 2ffeaa6..f1285f0 100644 --- a/src/main/resources/defaultReturners.yaml +++ b/src/main/resources/defaultReturners.yaml @@ -2,9 +2,11 @@ defaultCommandParser: &defaultCommandParser !!org.rundeck.plugin.salt.output.SaltJsonReturnHandler {exitCodeKey: retcode, standardOutputKey: stdout, standardErrorKey: stderr} alwaysSuccessful: &alwaysSuccessful !!org.rundeck.plugin.salt.output.DefaultSaltReturnHandler { exitCode: 0 } +fullJsonCommandParser: &fullJsonCommandParser !!org.rundeck.plugin.salt.output.SaltFullJsonReturnHandler {exitCodeKey: result, standardOutputKey: comment, standardErrorKey: comment} handlerMappings: cmd.run_all: *defaultCommandParser + state.sls : *fullJsonCommandParser file.touch: *alwaysSuccessful file.append: *alwaysSuccessful file.remove: *alwaysSuccessful diff --git a/src/test/java/org/rundeck/plugin/salt/output/SaltFullJsonReturnHandlerTest.java b/src/test/java/org/rundeck/plugin/salt/output/SaltFullJsonReturnHandlerTest.java new file mode 100644 index 0000000..428c091 --- /dev/null +++ b/src/test/java/org/rundeck/plugin/salt/output/SaltFullJsonReturnHandlerTest.java @@ -0,0 +1,112 @@ +/** + * Copyright (c) 2013, salesforce.com, inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions and the + * following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and + * the following disclaimer in the documentation and/or other materials provided with the distribution. + * + * Neither the name of salesforce.com, inc. nor the names of its contributors may be used to endorse or + * promote products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package org.rundeck.plugin.salt.output; + +import java.util.Map; + +import org.junit.Assert; +import org.junit.Test; + +import com.google.common.collect.Maps; + +public class SaltFullJsonReturnHandlerTest { + + protected static final String EXIT_CODE_KEY = "result"; + protected static final String ERR_KEY = "comment"; + protected static final String OUT_KEY = "comment"; + protected static final String SAMPLE_JSON_TEMPLATE = "{\"service_\":{\"" + OUT_KEY + "\":\"%s\",\"start_time\":\"10:03:59.046156\",\"" + EXIT_CODE_KEY + "\":%s,\"duration\":144.388\"}}"; + + @Test + public void testExtractResponse() { + SaltFullJsonReturnHandler handler = new SaltFullJsonReturnHandler(); + handler.setExitCodeKey(EXIT_CODE_KEY); + handler.setStandardOutputKey(OUT_KEY); + handler.setStandardErrorKey(ERR_KEY); + + String result = "true"; + String comment = "some comment"; + String json = String.format(SAMPLE_JSON_TEMPLATE, comment, result, comment, result); + SaltReturnResponse response = handler.extractResponse(json); + Assert.assertEquals("Expected passed in exit code", new Integer(0), response.getExitCode()); + Assert.assertEquals("Expected single stdout line", 1, response.getStandardOutput().size()); + Assert.assertEquals("Expected passed in stdout line", "[" + comment + "]", response.getStandardOutput().get(0)); + Assert.assertEquals("Expected single stderr line", 1, response.getStandardError().size()); + Assert.assertEquals("Expected passed in stderr line", "[" + comment + "]", response.getStandardError().get(0)); + } + + @Test + public void testExtractResponseWithoutKeys() { + SaltFullJsonReturnHandler handler = new SaltFullJsonReturnHandler(); + + String result = "true"; + String comment = "some comment"; + String json = String.format(SAMPLE_JSON_TEMPLATE, comment, result, comment, result); + SaltReturnResponse response = handler.extractResponse(json); + Assert.assertNull("Expected not to parse exit code", response.getExitCode()); + Assert.assertTrue("Expected not to parse stdout", response.getStandardOutput().isEmpty()); + Assert.assertTrue("Expected not to parse stderr", response.getStandardError().isEmpty()); + } + + @Test(expected = SaltReturnResponseParseException.class) + public void testExtractResponseExitCodeKeySpecifiedButNoExitCode() { + SaltFullJsonReturnHandler handler = new SaltFullJsonReturnHandler(); + handler.setExitCodeKey("someotherkey"); + handler.setStandardOutputKey(OUT_KEY); + handler.setStandardErrorKey(ERR_KEY); + + String result = "true"; + String comment = "some comment"; + String json = String.format(SAMPLE_JSON_TEMPLATE, comment, result, comment, result); + handler.extractResponse(json); + } + + @Test(expected = SaltReturnResponseParseException.class) + public void testExtractResponseStandardOutputKeySpecifiedButNoStandardOutput() { + SaltFullJsonReturnHandler handler = new SaltFullJsonReturnHandler(); + handler.setExitCodeKey(EXIT_CODE_KEY); + handler.setStandardOutputKey("someotherkey"); + handler.setStandardErrorKey(ERR_KEY); + + String result = "true"; + String comment = "some comment"; + String json = String.format(SAMPLE_JSON_TEMPLATE, comment, result, comment, result); + handler.extractResponse(json); + } + + @Test(expected = SaltReturnResponseParseException.class) + public void testExtractResponseStandardErrorKeySpecifiedButNoStandardError() { + SaltFullJsonReturnHandler handler = new SaltFullJsonReturnHandler(); + handler.setExitCodeKey(EXIT_CODE_KEY); + handler.setStandardOutputKey(OUT_KEY); + handler.setStandardErrorKey("someotherkey"); + + String result = "true"; + String comment = "some comment"; + String json = String.format(SAMPLE_JSON_TEMPLATE, comment, result, comment, result); + handler.extractResponse(json); + } +}