From 8d9e717828d97237754a3fce252566ad7e8a554e Mon Sep 17 00:00:00 2001 From: Pierre Rioux Date: Thu, 27 Sep 2018 09:38:31 -0400 Subject: adding dynamic err message support for violations Change-Id: I80c1a0cade46ef623fce91921449642d8eafb2f6 Issue-ID: LOG-683 Signed-off-by: Pierre Rioux --- .gitignore | 1 + .../etc/rules/poa-event/default-rules.groovy | 168 --------------------- .../onap/aai/validation/config/TopicConfig.java | 11 +- .../validation/ruledriven/RuleDrivenValidator.java | 30 ++-- .../aai/validation/ruledriven/rule/GroovyRule.java | 13 +- .../onap/aai/validation/ruledriven/rule/Rule.java | 92 +++++------ .../aai/validation/ruledriven/rule/RuleResult.java | 98 ++++++++++++ .../aai/validation/ruledriven/rule/RuleHelper.java | 13 +- .../ruledriven/rule/TestRuleExecution.java | 67 +++++++- 9 files changed, 247 insertions(+), 246 deletions(-) delete mode 100644 bundleconfig/etc/rules/poa-event/default-rules.groovy create mode 100644 src/main/java/org/onap/aai/validation/ruledriven/rule/RuleResult.java diff --git a/.gitignore b/.gitignore index 59551a6..73b0166 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,6 @@ bundleconfig-local/etc/auth/tomcat_keystore .classpath .project .settings/ +staticContent/ target/ src/main/java/META-INF/ diff --git a/bundleconfig/etc/rules/poa-event/default-rules.groovy b/bundleconfig/etc/rules/poa-event/default-rules.groovy deleted file mode 100644 index 8397abd..0000000 --- a/bundleconfig/etc/rules/poa-event/default-rules.groovy +++ /dev/null @@ -1,168 +0,0 @@ -/* - * ============LICENSE_START=================================================== - * Copyright (c) 2018 Amdocs - * ============================================================================ - * 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. - * ============LICENSE_END===================================================== - */ - -entity { - name 'POA-EVENT' - indexing { - indices 'default-rules' - } - validation { - useRule { - name 'Verify AAI nf-naming-code' - attributes 'context-list.aai.vf-list[*]' - } - useRule { - name 'port-mirroring-AAI-has-valid-vnfc' - attributes 'context-list.sdc.vf-list[*]', 'context-list.aai.vf-list[*]' - } - useRule { - name 'port-mirroring-SDC-vnfc-types-missing' - attributes 'context-list.sdc.vf-list[*]', 'context-list.aai.vf-list[*]' - } - useRule { - name 'port-mirroring-AAI-vnfc-type-exists-in-SDC-SUCCESS' - attributes 'context-list.sdc.vf-list[*]', 'context-list.aai.vf-list[*]' - } - } -} - -rule { - name 'Verify AAI nf-naming-code' - category 'INVALID_VALUE' - description 'Validate that nf-naming-code exists and is populated in AAI VNF instance' - errorText 'The nf-naming-code is not populated in AAI VNF instance' - severity 'CRITICAL' - attributes 'vfList' - validate ''' - def parsed = new groovy.json.JsonSlurper().parseText(vfList.toString()) - for (vf in parsed) { - String nfNamingCode = vf."nf-naming-code" - if (nfNamingCode == null || nfNamingCode.equals("")) { - return false - } - } - return true - ''' -} - -rule { - name 'port-mirroring-AAI-has-valid-vnfc' - category 'INVALID_VALUE' - description 'Validate that each VNFC instance in AAI conforms to a VNFC type defined in SDC model' - errorText 'AAI VNFC instance includes non-specified type in design SDC model' - severity 'ERROR' - attributes 'sdcVfList', 'aaiVfList' - validate ''' - def slurper = new groovy.json.JsonSlurper() - def parsedSdc = slurper.parseText(sdcVfList.toString()) - def parsedAai = slurper.parseText(aaiVfList.toString()) - - // gather all SDC nfc-naming-codes - List sdcNfcNamingCodeList = new ArrayList<>() - parsedSdc.each { - for(sdcVnfc in it.vnfc) { - String sdcNfcNamingCode = sdcVnfc."nfc-naming-code" - if(sdcNfcNamingCode != null) { - sdcNfcNamingCodeList.add(sdcNfcNamingCode) - } - } - } - - // check that all SDC nfc-naming-codes exist in AAI - parsedAai.each { - for(aaiVnfc in it.vnfc) { - String aaiNfcNamingCode = aaiVnfc."nfc-naming-code" - if(aaiNfcNamingCode != null) { - if(!sdcNfcNamingCodeList.contains(aaiNfcNamingCode)) { - return false - } - } - } - } - return true - ''' -} - - -rule { - name 'port-mirroring-SDC-vnfc-types-missing' - category 'INVALID_VALUE' - description 'Validate that each VNFC type specified in SDC model exists in AAI' - errorText 'Design has specified types but not all of them exist in AAI' - severity 'WARNING' - attributes 'sdcVfList', 'aaiVfList' - validate ''' - def getNfcNamingCodeSet = { parsedEntity -> - Set namingCodeSet = new HashSet<>() - parsedEntity.each { - for(vnfcItem in it."vnfc") { - println "vnfc: " + vnfcItem - String namingCode = vnfcItem."nfc-naming-code" - if(namingCode != null) { - namingCodeSet.add(namingCode) - } - } - } - return namingCodeSet - } - - // gather all unique nfc-naming-codes from AAI and SDC - def slurper = new groovy.json.JsonSlurper() - def aaiNfcNamingCodeSet = getNfcNamingCodeSet(slurper.parseText(aaiVfList.toString())) as java.util.HashSet - def sdcNfcNamingCodeSet = getNfcNamingCodeSet(slurper.parseText(sdcVfList.toString())) as java.util.HashSet - - println "AAI: " + aaiNfcNamingCodeSet - println "SDC: " + sdcNfcNamingCodeSet - - // check that all nfc-naming-codes in SDC exist in AAI - return aaiNfcNamingCodeSet.containsAll(sdcNfcNamingCodeSet) - ''' -} - - -rule { - name 'port-mirroring-AAI-vnfc-type-exists-in-SDC-SUCCESS' - category 'SUCCESS' - description 'Verify that every vnfc in sdc has been created in AAI' - errorText 'Every vnfc type specified in sdc has been created in AAI' - severity 'INFO' - attributes 'sdcVfList', 'aaiVfList' - validate ''' - def getNfcNamingCodeSet = { parsedEntity -> - Set namingCodeSet = new HashSet<>() - parsedEntity.each { - for(vnfcItem in it."vnfc") { - String namingCode = vnfcItem."nfc-naming-code" - if(namingCode != null) { - namingCodeSet.add(namingCode) - } - } - } - return namingCodeSet - } - - // gather all unique nfc-naming-codes from AAI and SDC - def slurper = new groovy.json.JsonSlurper() - def aaiNfcNamingCodeSet = getNfcNamingCodeSet(slurper.parseText(aaiVfList.toString())) as java.util.HashSet - def sdcNfcNamingCodeSet = getNfcNamingCodeSet(slurper.parseText(sdcVfList.toString())) as java.util.HashSet - - // check that all nfc-naming-codes in SDC exist in AAI - // return false if all SDC naming codes exist in AAI to trigger an INFO violation - return !aaiNfcNamingCodeSet.containsAll(sdcNfcNamingCodeSet) - ''' -} diff --git a/src/main/java/org/onap/aai/validation/config/TopicConfig.java b/src/main/java/org/onap/aai/validation/config/TopicConfig.java index 99742ac..ccf5d51 100644 --- a/src/main/java/org/onap/aai/validation/config/TopicConfig.java +++ b/src/main/java/org/onap/aai/validation/config/TopicConfig.java @@ -46,13 +46,12 @@ public class TopicConfig { List publisherTopics = new ArrayList<>(); @Autowired - public TopicConfig (@Value("${consumer.topic.names}") final String consumerNames, @Value("${publisher.topic.names}") final String publisherNames){ - - consumerTopicNames = Arrays.asList(consumerNames.split(","));; - publisherTopicNames = Arrays.asList(publisherNames.split(","));; - - + public TopicConfig (@Value("${consumer.topic.names}") final String consumerNames, + @Value("${publisher.topic.names}") final String publisherNames) { + consumerTopicNames = Arrays.asList(consumerNames.split(",")); + publisherTopicNames = Arrays.asList(publisherNames.split(",")); } + /** * Gets the configuration of topics for consumption. * diff --git a/src/main/java/org/onap/aai/validation/ruledriven/RuleDrivenValidator.java b/src/main/java/org/onap/aai/validation/ruledriven/RuleDrivenValidator.java index 476c098..2bc2d90 100644 --- a/src/main/java/org/onap/aai/validation/ruledriven/RuleDrivenValidator.java +++ b/src/main/java/org/onap/aai/validation/ruledriven/RuleDrivenValidator.java @@ -21,6 +21,7 @@ import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -49,6 +50,7 @@ import org.onap.aai.validation.ruledriven.configuration.EntitySection; import org.onap.aai.validation.ruledriven.configuration.GroovyConfigurationException; import org.onap.aai.validation.ruledriven.configuration.RulesConfigurationLoader; import org.onap.aai.validation.ruledriven.rule.Rule; +import org.onap.aai.validation.ruledriven.rule.RuleResult; /** * Validator using explicit rules @@ -159,28 +161,30 @@ public class RuleDrivenValidator implements Validator { AttributeValues attributeValues = entity.getAttributeValues(rule.getAttributePaths()); // Execute the rule for this particular set of attribute values. - boolean valid = false; + RuleResult result = null; try { - valid = rule.execute(attributeValues); + result = rule.execute(attributeValues); } catch (IllegalArgumentException e) { throw new ValidationServiceException(ValidationServiceError.RULE_EXECUTION_ERROR, e, rule, attributeValues); } applicationLogger.debug(String.format("%s|%s|\"%s\"|%s", entity.getType(), entity.getIds(), rule.getName(), - valid ? "pass" : "fail")); + result.getSuccess() ? "pass" : "fail")); + + if (!result.getSuccess()) { + String errorMessage = MessageFormat.format(rule.getErrorMessage(), result.getErrorArguments().toArray()); - if (!valid) { //@formatter:off - Violation violation = builder - .category(rule.getErrorCategory()) - .severity(rule.getSeverity()) - .violationType(ViolationType.RULE) - .validationRule(rule.getName()) - .violationDetails(attributeValues.generateReport()) - .errorMessage(rule.getErrorMessage()) - .build(); - //@formatter:on + Violation violation = builder + .category(rule.getErrorCategory()) + .severity(rule.getSeverity()) + .violationType(ViolationType.RULE) + .validationRule(rule.getName()) + .violationDetails(attributeValues.generateReport()) + .errorMessage(errorMessage) + .build(); + //@formatter:on validationResult.addViolation(violation); } diff --git a/src/main/java/org/onap/aai/validation/ruledriven/rule/GroovyRule.java b/src/main/java/org/onap/aai/validation/ruledriven/rule/GroovyRule.java index b151f6b..df15791 100644 --- a/src/main/java/org/onap/aai/validation/ruledriven/rule/GroovyRule.java +++ b/src/main/java/org/onap/aai/validation/ruledriven/rule/GroovyRule.java @@ -125,7 +125,7 @@ public class GroovyRule implements Rule { * @return */ @Override - public Boolean execute(AttributeValues attributeValues) { + public RuleResult execute(AttributeValues attributeValues) { // Obtain the values of each of the attributes to pass into the rule List valueList = new ArrayList<>(); for (String attrName : this.attributePaths) { @@ -141,10 +141,10 @@ public class GroovyRule implements Rule { * @param values * * @param groovyObject an instance/object of a Groovy class that implements one or more rule methods - * @return the Boolean result of evaluating the expression + * @return the result of evaluating the expression */ @Override - public Boolean execute(Object... values) { + public RuleResult execute(Object... values) { Object result = null; try { result = groovyObject.invokeMethod(getRuleMethod(), values); @@ -153,12 +153,7 @@ public class GroovyRule implements Rule { } catch (NullPointerException e) { throw new IllegalArgumentException("Argument is null", e); } - - if (result instanceof Number) { - return !result.equals(0); - } else { - return (Boolean) result; - } + return new RuleResult(result); } @Override diff --git a/src/main/java/org/onap/aai/validation/ruledriven/rule/Rule.java b/src/main/java/org/onap/aai/validation/ruledriven/rule/Rule.java index 1196db0..60705ea 100644 --- a/src/main/java/org/onap/aai/validation/ruledriven/rule/Rule.java +++ b/src/main/java/org/onap/aai/validation/ruledriven/rule/Rule.java @@ -25,57 +25,57 @@ import org.onap.aai.validation.reader.data.AttributeValues; */ public interface Rule { - /** - * Gets the name of the rule - * - * @return the name - */ - String getName(); + /** + * Gets the name of the rule + * + * @return the name + */ + String getName(); - /** - * Gets the error message. - * - * @return the error message - */ - String getErrorMessage(); + /** + * Gets the error message. + * + * @return the error message + */ + String getErrorMessage(); - /** - * Gets the error category. - * - * @return the error category - */ - String getErrorCategory(); + /** + * Gets the error category. + * + * @return the error category + */ + String getErrorCategory(); - /** - * Gets the severity. - * - * @return the severity - */ - String getSeverity(); + /** + * Gets the severity. + * + * @return the severity + */ + String getSeverity(); - /** - * Gets the paths to the attributes to pass to the rule - * - * @return the attribute paths - */ - List getAttributePaths(); + /** + * Gets the paths to the attributes to pass to the rule + * + * @return the attribute paths + */ + List getAttributePaths(); - /** - * Execute the rule. - * - * @param values - * the attribute values to pass to the rule - * @return a boolean representing the rule evaluation (meaning success/failure) - */ - Boolean execute(AttributeValues values); + /** + * Execute the rule. + * + * @param values + * the attribute values to pass to the rule + * @return a RuleResult instance representing the rule evaluation (meaning success/failure) + */ + RuleResult execute(AttributeValues values); - /** - * Execute the rule. - * - * @param values - * the attribute values to pass to the rule - * @return a boolean representing the rule evaluation (meaning success/failure) - */ - Boolean execute(Object... values); + /** + * Execute the rule. + * + * @param values + * the attribute values to pass to the rule + * @return a RuleResult instance representing the rule evaluation (meaning success/failure) + */ + RuleResult execute(Object... values); } diff --git a/src/main/java/org/onap/aai/validation/ruledriven/rule/RuleResult.java b/src/main/java/org/onap/aai/validation/ruledriven/rule/RuleResult.java new file mode 100644 index 0000000..19f77f7 --- /dev/null +++ b/src/main/java/org/onap/aai/validation/ruledriven/rule/RuleResult.java @@ -0,0 +1,98 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * 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. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.ruledriven.rule; + +import groovy.lang.Tuple2; +import java.util.Collections; +import java.util.List; + +/** + * Encapsulates the results of rule execution + * + */ +public class RuleResult { + + private Boolean success = true; + private List errorArguments = Collections.emptyList(); + + /** + * Creates an instance of this class using the groovy object returned by rule execution. + * + * Valid object types: + * Boolean: true = success; false = fail + * Number: 0 = success; non-zero = fail + * Tuple2: contains rule result and argument list + * - tuple's "first" contains a boolean representing the results of rule execution + * - tuple's "second" contains a list of strings used to expand rule error text + * + * @param groovyResult + */ + public RuleResult(Object groovyResult) { + if (groovyResult instanceof Number) { + success = !((Number)groovyResult).equals(0); + } else if (groovyResult instanceof Tuple2) { + handleTuple(groovyResult); + } else { + success = (Boolean)groovyResult; + } + } + + @SuppressWarnings("unused") + private RuleResult() { + // intentionally empty + } + + /** + * Returns the results of rule execution, i.e. success or fail + * @return + */ + public Boolean getSuccess() { + return success; + } + + /** + * Returns the list of arguments used to expand rule error text. + * + * For example, this errorText in a rule definition: + * 'Error found with "{0}" in "{1}"; value "{2}" is not a valid MAC address' + * + * used with the following runtime argument list: + * ["macaddr", "tenants.tenant.vservers.vserver.l-interfaces.l-interface", "02:fd:59:3"] + * + * would display: + * Error found with "macaddr" in "tenants.tenant.vservers.vserver.l-interfaces.l-interface"; value "02:fd:59:3" is not a valid MAC address + * + * @return a list of strings; will not return null + */ + public List getErrorArguments() { + return errorArguments; + } + + /** + * Handles a Tuple2 object returned by a groovy rule. + * The tuple's "first" contains a boolean representing the results of rule execution. + * The tuple's "second" contains a list of strings used to expand rule error text. + * @param tupleObject + */ + private void handleTuple(Object tupleObject) { + @SuppressWarnings("unchecked") + Tuple2> tuple = (Tuple2>)tupleObject; + success = tuple.getFirst(); + errorArguments = (tuple.getSecond() == null) ? Collections.emptyList() : tuple.getSecond(); + } +} diff --git a/src/test/java/org/onap/aai/validation/ruledriven/rule/RuleHelper.java b/src/test/java/org/onap/aai/validation/ruledriven/rule/RuleHelper.java index 456d011..c9d6284 100644 --- a/src/test/java/org/onap/aai/validation/ruledriven/rule/RuleHelper.java +++ b/src/test/java/org/onap/aai/validation/ruledriven/rule/RuleHelper.java @@ -21,16 +21,23 @@ import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; +import java.text.MessageFormat; import org.onap.aai.validation.reader.data.AttributeValues; -import org.onap.aai.validation.ruledriven.rule.Rule; public class RuleHelper { static void assertRuleResult(Rule rule, AttributeValues values, Boolean expectedResult) { - assertThat(rule + " failed for values [" + values + "]", rule.execute(values), is(equalTo(expectedResult))); + assertThat(rule + " failed for values [" + values + "]", rule.execute(values).getSuccess(), is(equalTo(expectedResult))); } static void assertRuleResult(Rule rule, Object value, Boolean expectedResult) { - assertThat(rule + " failed for value [" + value + "]", rule.execute(value), is(equalTo(expectedResult))); + assertThat(rule + " failed for value [" + value + "]", rule.execute(value).getSuccess(), is(equalTo(expectedResult))); + } + + static void assertRuleErrorMessage(Rule rule, Object value, String expectedErrorMessage) { + RuleResult result = rule.execute(value); + String errorMessage = MessageFormat.format(rule.getErrorMessage(), result.getErrorArguments().toArray()); + assertThat(rule + " failed to validate error message [" + expectedErrorMessage + "]", + errorMessage, is(equalTo(expectedErrorMessage))); } } diff --git a/src/test/java/org/onap/aai/validation/ruledriven/rule/TestRuleExecution.java b/src/test/java/org/onap/aai/validation/ruledriven/rule/TestRuleExecution.java index 15e958d..426bbb4 100644 --- a/src/test/java/org/onap/aai/validation/ruledriven/rule/TestRuleExecution.java +++ b/src/test/java/org/onap/aai/validation/ruledriven/rule/TestRuleExecution.java @@ -34,7 +34,6 @@ import org.junit.rules.ExpectedException; import org.onap.aai.validation.reader.data.AttributeValues; import org.onap.aai.validation.ruledriven.configuration.GroovyConfigurationException; import org.onap.aai.validation.ruledriven.configuration.RuleSection; -import org.onap.aai.validation.ruledriven.rule.GroovyRule; /** * Tests for creating an AuditRule object and then executing the rule expression against fixed attribute values @@ -81,6 +80,51 @@ public class TestRuleExecution { assertRuleResult(rule, Arrays.asList(5, 44), false); } + /** + * Simple example of a rule using error message expansion + * @throws Exception + */ + @Test + public void testRuleErrorTextWithArguments() throws Exception { + + final String errorMessage = "Error message with arguments: {0}, {1}"; + final String expectedErrorMessage = "Error message with arguments: arg1, arg2"; + + String expression = "return new groovy.lang.Tuple2(true, java.util.Arrays.asList(\"arg1\", \"arg2\"))"; + GroovyRule rule = buildRuleWithErrorMessage("i", expression, errorMessage); + assertRuleResult(rule, 1, expectedErrorMessage); + + String expressionOneArgumentTooMany = "return new groovy.lang.Tuple2(true, java.util.Arrays.asList(\"arg1\", \"arg2\", \"arg3\"))"; + GroovyRule rule3 = buildRuleWithErrorMessage("i", expressionOneArgumentTooMany, errorMessage); + assertRuleResult(rule3, 1, expectedErrorMessage); + + String expressionOneLessArgument = "return new groovy.lang.Tuple2(true, java.util.Arrays.asList(\"arg1\"))"; + GroovyRule rule2 = buildRuleWithErrorMessage("i", expressionOneLessArgument, errorMessage); + assertRuleResult(rule2, 1, "Error message with arguments: arg1, {1}"); + } + + /** + * Simple example of a rule using error message expansion, without arguments + * @throws Exception + */ + @Test + public void testRuleErrorTextWithoutArguments() throws Exception { + + final String errorMessage = "Error message without arguments"; + + String expressionWithArgs = "return new groovy.lang.Tuple2(true, java.util.Arrays.asList(\"arg1\", \"arg2\"))"; + GroovyRule rule = buildRuleWithErrorMessage("i", expressionWithArgs, errorMessage); + assertRuleResult(rule, 1, errorMessage); + + String expressionWithoutArgs = "return new groovy.lang.Tuple2(true, java.util.Collections.emptyList())"; + GroovyRule rule2 = buildRuleWithErrorMessage("i", expressionWithoutArgs, errorMessage); + assertRuleResult(rule2, 1, errorMessage); + + String expressionWithNullAsArgs = "return new groovy.lang.Tuple2(true, null)"; + GroovyRule rule3 = buildRuleWithErrorMessage("i", expressionWithNullAsArgs, errorMessage); + assertRuleResult(rule3, 1, errorMessage); + } + /** * vserver is related to vpe and vserver-name contains me6 */ @@ -468,6 +512,18 @@ public class TestRuleExecution { return new GroovyRule(ruleConfig); } + private GroovyRule buildRuleWithErrorMessage(String name, String attribute, String expression, String errorMessage) + throws IOException, InstantiationException, IllegalAccessException, GroovyConfigurationException { + RuleSection ruleConfig = new RuleSection(); + ruleConfig.setName(name); + ruleConfig.setAttributes(Collections.singletonList(attribute)); + ruleConfig.setExpression(expression); + if(errorMessage != null) { + ruleConfig.setErrorMessage(errorMessage); + } + return new GroovyRule(ruleConfig); + } + /** * Build a simple rule (with a default name) using a RuleConfiguration object * @@ -487,8 +543,17 @@ public class TestRuleExecution { return buildRule("testRule", attributes, expression); } + private GroovyRule buildRuleWithErrorMessage(String attribute, String expression, String errorText) + throws InstantiationException, IllegalAccessException, IOException, GroovyConfigurationException { + return buildRuleWithErrorMessage("testRule", attribute, expression, errorText); + } + private void assertRuleResult(GroovyRule rule, Object value, boolean expectedResult) { RuleHelper.assertRuleResult(rule, value, expectedResult); } + private void assertRuleResult(GroovyRule rule, Object value, String expectedErrorMessage) { + RuleHelper.assertRuleErrorMessage(rule, value, expectedErrorMessage); + } + } -- cgit 1.2.3-korg