From 09dff3351afb63786d3803acc663fa6eaa425bc7 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Wed, 28 Nov 2018 09:38:58 -0800 Subject: [PATCH 001/115] [DOCS] Adds TLS warning to rolling upgrades (#35841) --- docs/reference/setup/bootstrap-checks-xes.asciidoc | 1 + docs/reference/upgrade/rolling_upgrade.asciidoc | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/docs/reference/setup/bootstrap-checks-xes.asciidoc b/docs/reference/setup/bootstrap-checks-xes.asciidoc index 7d021a04cc109..a935e84c4b08a 100644 --- a/docs/reference/setup/bootstrap-checks-xes.asciidoc +++ b/docs/reference/setup/bootstrap-checks-xes.asciidoc @@ -49,6 +49,7 @@ valid. The Distinguished Names (DNs) that are listed in the role mappings files must also be valid. [float] +[[bootstrap-checks-tls]] === SSL/TLS check //See TLSLicenseBootstrapCheck.java diff --git a/docs/reference/upgrade/rolling_upgrade.asciidoc b/docs/reference/upgrade/rolling_upgrade.asciidoc index fc136f25499ad..86a21627a8901 100644 --- a/docs/reference/upgrade/rolling_upgrade.asciidoc +++ b/docs/reference/upgrade/rolling_upgrade.asciidoc @@ -13,6 +13,11 @@ Upgrading from earlier 5.x versions requires a <>. You must <> from versions prior to 5.x. +WARNING: If the {es} {security-features} are enabled on your 5.x cluster, before +you can do a rolling upgrade you must encrypt the internode-communication with +SSL/TLS, which requires a full cluster restart. For more information about this +requirement and the associated bootstrap check, see <>. + To perform a rolling upgrade: . *Disable shard allocation*. From dfb969f6972a11221a196ee02ebd59f95f661902 Mon Sep 17 00:00:00 2001 From: Benjamin Trent Date: Wed, 28 Nov 2018 11:43:57 -0600 Subject: [PATCH 002/115] ML: Adding XContentObjectTransformer class (#35957) * ML: Adding XContentObjectTransformer class * adding license headers * Adding custom deprecation handler, and test for checking parsing failures * forwarding deprection logs to LoggingDeprecationHandler --- ...LoggingDeprecationAccumulationHandler.java | 51 ++++++++ .../ml/utils/XContentObjectTransformer.java | 83 +++++++++++++ .../utils/XContentObjectTransformerTests.java | 113 ++++++++++++++++++ 3 files changed, 247 insertions(+) create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/utils/LoggingDeprecationAccumulationHandler.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/utils/XContentObjectTransformer.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/utils/XContentObjectTransformerTests.java diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/utils/LoggingDeprecationAccumulationHandler.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/utils/LoggingDeprecationAccumulationHandler.java new file mode 100644 index 0000000000000..1ab443b12de01 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/utils/LoggingDeprecationAccumulationHandler.java @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.core.ml.utils; + +import org.elasticsearch.common.logging.LoggerMessageFormat; +import org.elasticsearch.common.xcontent.DeprecationHandler; +import org.elasticsearch.common.xcontent.LoggingDeprecationHandler; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Very similar to {@link org.elasticsearch.common.xcontent.LoggingDeprecationHandler} main differences are: + * 1. Is not a Singleton + * 2. Accumulates all deprecation warnings into a list that can be retrieved + * with {@link LoggingDeprecationAccumulationHandler#getDeprecations()} + * + * NOTE: The accumulation is NOT THREAD SAFE + */ +public class LoggingDeprecationAccumulationHandler implements DeprecationHandler { + + private final List deprecations = new ArrayList<>(); + + @Override + public void usedDeprecatedName(String usedName, String modernName) { + LoggingDeprecationHandler.INSTANCE.usedDeprecatedName(usedName, modernName); + deprecations.add(LoggerMessageFormat.format("Deprecated field [{}] used, expected [{}] instead", + usedName, + modernName)); + } + + @Override + public void usedDeprecatedField(String usedName, String replacedWith) { + LoggingDeprecationHandler.INSTANCE.usedDeprecatedField(usedName, replacedWith); + deprecations.add(LoggerMessageFormat.format("Deprecated field [{}] used, replaced by [{}]", + usedName, + replacedWith)); + } + + /** + * The collected deprecation warnings + */ + public List getDeprecations() { + return Collections.unmodifiableList(deprecations); + } + +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/utils/XContentObjectTransformer.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/utils/XContentObjectTransformer.java new file mode 100644 index 0000000000000..00453d3680fe9 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/utils/XContentObjectTransformer.java @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.core.ml.utils; + +import org.elasticsearch.common.CheckedFunction; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.index.query.AbstractQueryBuilder; +import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.search.SearchModule; +import org.elasticsearch.search.aggregations.AggregatorFactories; + +import java.io.IOException; +import java.util.Collections; +import java.util.Map; + +/** + * This is a utility class that allows simple one-to-one transformations between an ToXContentObject type + * to and from other supported objects. + * + * @param The type of the object that we will be transforming to/from + */ +public class XContentObjectTransformer { + + private final NamedXContentRegistry registry; + private final CheckedFunction parserFunction; + + // We need this registry for parsing out Aggregations and Searches + private static NamedXContentRegistry searchRegistry; + static { + SearchModule searchModule = new SearchModule(Settings.EMPTY, false, Collections.emptyList()); + searchRegistry = new NamedXContentRegistry(searchModule.getNamedXContents()); + } + + public static XContentObjectTransformer aggregatorTransformer() { + return new XContentObjectTransformer<>(searchRegistry, (p) -> { + // Serializing a map creates an object, need to skip the start object for the aggregation parser + assert(XContentParser.Token.START_OBJECT.equals(p.nextToken())); + return AggregatorFactories.parseAggregators(p); + }); + } + + public static XContentObjectTransformer queryBuilderTransformer() { + return new XContentObjectTransformer<>(searchRegistry, AbstractQueryBuilder::parseInnerQueryBuilder); + } + + XContentObjectTransformer(NamedXContentRegistry registry, CheckedFunction parserFunction) { + this.parserFunction = parserFunction; + this.registry = registry; + } + + public T fromMap(Map stringObjectMap) throws IOException { + LoggingDeprecationAccumulationHandler deprecationLogger = new LoggingDeprecationAccumulationHandler(); + try(XContentBuilder xContentBuilder = XContentFactory.jsonBuilder().map(stringObjectMap); + XContentParser parser = XContentType.JSON + .xContent() + .createParser(registry, + deprecationLogger, + BytesReference.bytes(xContentBuilder).streamInput())) { + //TODO do something with the accumulated deprecation warnings + return parserFunction.apply(parser); + } + } + + public Map toMap(T object) throws IOException { + try(XContentBuilder xContentBuilder = XContentFactory.jsonBuilder()) { + XContentBuilder content = object.toXContent(xContentBuilder, ToXContent.EMPTY_PARAMS); + return XContentHelper.convertToMap(BytesReference.bytes(content), true, XContentType.JSON).v2(); + } + } + +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/utils/XContentObjectTransformerTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/utils/XContentObjectTransformerTests.java new file mode 100644 index 0000000000000..47fc7566b206a --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/utils/XContentObjectTransformerTests.java @@ -0,0 +1,113 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.core.ml.utils; + +import org.elasticsearch.common.ParsingException; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.common.xcontent.XContentParseException; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.index.query.QueryBuilders; +import org.elasticsearch.search.aggregations.AggregationBuilders; +import org.elasticsearch.search.aggregations.AggregatorFactories; +import org.elasticsearch.search.aggregations.metrics.max.MaxAggregationBuilder; +import org.elasticsearch.test.ESTestCase; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static org.elasticsearch.common.xcontent.ToXContent.EMPTY_PARAMS; +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertToXContentEquivalent; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; + +public class XContentObjectTransformerTests extends ESTestCase { + + public void testFromMap() throws IOException { + Map aggMap = Collections.singletonMap("fieldName", + Collections.singletonMap("max", + Collections.singletonMap("field", "fieldName"))); + + XContentObjectTransformer aggTransformer = XContentObjectTransformer.aggregatorTransformer(); + assertXContentAreEqual(aggTransformer.fromMap(aggMap), aggMap); + assertXContentAreEqual(aggTransformer.fromMap(aggMap), aggTransformer.toMap(aggTransformer.fromMap(aggMap))); + + Map queryMap = Collections.singletonMap("match", + Collections.singletonMap("fieldName", new HashMap(){{ + // Add all the default fields so they are not added dynamically when the object is parsed + put("query","fieldValue"); + put("operator","OR"); + put("prefix_length",0); + put("max_expansions",50); + put("fuzzy_transpositions",true); + put("lenient",false); + put("zero_terms_query","NONE"); + put("auto_generate_synonyms_phrase_query",true); + put("boost",1.0); + }})); + + XContentObjectTransformer queryBuilderTransformer = XContentObjectTransformer.queryBuilderTransformer(); + assertXContentAreEqual(queryBuilderTransformer.fromMap(queryMap), queryMap); + assertXContentAreEqual(queryBuilderTransformer.fromMap(queryMap), + queryBuilderTransformer.toMap(queryBuilderTransformer.fromMap(queryMap))); + } + + public void testFromMapWithBadMaps() { + Map queryMap = Collections.singletonMap("match", + Collections.singletonMap("airline", new HashMap() {{ + put("query", "notSupported"); + put("type", "phrase"); //phrase stopped being supported for match in 6.x + }})); + + XContentObjectTransformer queryBuilderTransformer = XContentObjectTransformer.queryBuilderTransformer(); + ParsingException exception = expectThrows(ParsingException.class, + () -> queryBuilderTransformer.fromMap(queryMap)); + + assertThat(exception.getMessage(), equalTo("[match] query does not support [type]")); + + Map aggMap = Collections.singletonMap("badTerms", + Collections.singletonMap("terms", new HashMap() {{ + put("size", 0); //size being 0 in terms agg stopped being supported in 6.x + put("field", "myField"); + }})); + + XContentObjectTransformer aggTransformer = XContentObjectTransformer.aggregatorTransformer(); + XContentParseException xContentParseException = expectThrows(XContentParseException.class, () -> aggTransformer.fromMap(aggMap)); + assertThat(xContentParseException.getMessage(), containsString("[terms] failed to parse field [size]")); + } + + public void testToMap() throws IOException { + XContentObjectTransformer aggTransformer = XContentObjectTransformer.aggregatorTransformer(); + XContentObjectTransformer queryBuilderTransformer = XContentObjectTransformer.queryBuilderTransformer(); + + AggregatorFactories.Builder aggs = new AggregatorFactories.Builder(); + long aggHistogramInterval = randomNonNegativeLong(); + MaxAggregationBuilder maxTime = AggregationBuilders.max("time").field("time"); + aggs.addAggregator(AggregationBuilders.dateHistogram("buckets") + .interval(aggHistogramInterval).subAggregation(maxTime).field("time")); + + assertXContentAreEqual(aggs, aggTransformer.toMap(aggs)); + assertXContentAreEqual(aggTransformer.fromMap(aggTransformer.toMap(aggs)), aggTransformer.toMap(aggs)); + + QueryBuilder queryBuilder = QueryBuilders.termQuery(randomAlphaOfLength(10), randomAlphaOfLength(10)); + + assertXContentAreEqual(queryBuilder, queryBuilderTransformer.toMap(queryBuilder)); + assertXContentAreEqual(queryBuilderTransformer.fromMap(queryBuilderTransformer.toMap(queryBuilder)), + queryBuilderTransformer.toMap(queryBuilder)); + } + + private void assertXContentAreEqual(ToXContentObject object, Map map) throws IOException { + XContentType xContentType = XContentType.JSON; + BytesReference objectReference = XContentHelper.toXContent(object, xContentType, EMPTY_PARAMS, false); + BytesReference mapReference = BytesReference.bytes(XContentFactory.jsonBuilder().map(map)); + assertToXContentEquivalent(objectReference, mapReference, xContentType); + } +} From fd25484a0b2b9087d86912ebb995813006b18730 Mon Sep 17 00:00:00 2001 From: Jim Ferenczi Date: Wed, 28 Nov 2018 20:15:27 +0100 Subject: [PATCH 003/115] Remove custom QueryBuilder#analyzeGraphPhrase (#35983) Now that https://issues.apache.org/jira/browse/LUCENE-8479 is fixed we can remove the custom implementation of QueryBuilder#analyzeGraphPhrase in the match QueryBuilder. --- .../index/search/MatchQuery.java | 92 +------------------ .../query/QueryStringQueryBuilderTests.java | 24 +++-- .../query/SimpleQueryStringBuilderTests.java | 24 +++-- 3 files changed, 36 insertions(+), 104 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/search/MatchQuery.java b/server/src/main/java/org/elasticsearch/index/search/MatchQuery.java index 98f3a16868e6b..7260212a80d7a 100644 --- a/server/src/main/java/org/elasticsearch/index/search/MatchQuery.java +++ b/server/src/main/java/org/elasticsearch/index/search/MatchQuery.java @@ -45,7 +45,6 @@ import org.apache.lucene.search.spans.SpanTermQuery; import org.apache.lucene.util.BytesRef; import org.apache.lucene.util.QueryBuilder; -import org.apache.lucene.util.graph.GraphTokenStreamFiniteStrings; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; @@ -61,9 +60,6 @@ import org.elasticsearch.index.query.support.QueryParsers; import java.io.IOException; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; import static org.elasticsearch.common.lucene.search.Queries.newLenientFieldQuery; import static org.elasticsearch.common.lucene.search.Queries.newUnmappedFieldQuery; @@ -363,14 +359,12 @@ protected Query analyzePhrase(String field, TokenStream stream, int slop) throws return blendPhraseQuery((PhraseQuery) query, mapper); } return query; - } - catch (IllegalStateException e) { + } catch (IllegalStateException e) { if (lenient) { return newLenientFieldQuery(field, e); } throw e; - } - catch (IllegalArgumentException e) { + } catch (IllegalArgumentException e) { if (lenient == false) { DEPRECATION_LOGGER.deprecated(e.getMessage()); } @@ -383,14 +377,12 @@ protected Query analyzeMultiPhrase(String field, TokenStream stream, int slop) t try { checkForPositions(field); return mapper.multiPhraseQuery(field, stream, slop, enablePositionIncrements); - } - catch (IllegalStateException e) { + } catch (IllegalStateException e) { if (lenient) { return newLenientFieldQuery(field, e); } throw e; - } - catch (IllegalArgumentException e) { + } catch (IllegalArgumentException e) { if (lenient == false) { DEPRECATION_LOGGER.deprecated(e.getMessage()); } @@ -525,82 +517,6 @@ private Query boolToExtendedCommonTermsQuery(BooleanQuery bq, Occur highFreqOccu } return query; } - - /** - * Overrides {@link QueryBuilder#analyzeGraphPhrase(TokenStream, String, int)} to add - * a limit (see {@link BooleanQuery#getMaxClauseCount()}) to the number of {@link SpanQuery} - * that this method can create. - * - * TODO Remove when https://issues.apache.org/jira/browse/LUCENE-8479 is fixed. - */ - @Override - protected SpanQuery analyzeGraphPhrase(TokenStream source, String field, int phraseSlop) throws IOException { - source.reset(); - GraphTokenStreamFiniteStrings graph = new GraphTokenStreamFiniteStrings(source); - List clauses = new ArrayList<>(); - int[] articulationPoints = graph.articulationPoints(); - int lastState = 0; - int maxBooleanClause = BooleanQuery.getMaxClauseCount(); - for (int i = 0; i <= articulationPoints.length; i++) { - int start = lastState; - int end = -1; - if (i < articulationPoints.length) { - end = articulationPoints[i]; - } - lastState = end; - final SpanQuery queryPos; - if (graph.hasSidePath(start)) { - List queries = new ArrayList<>(); - Iterator it = graph.getFiniteStrings(start, end); - while (it.hasNext()) { - TokenStream ts = it.next(); - SpanQuery q = createSpanQuery(ts, field); - if (q != null) { - if (queries.size() >= maxBooleanClause) { - throw new BooleanQuery.TooManyClauses(); - } - queries.add(q); - } - } - if (queries.size() > 0) { - queryPos = new SpanOrQuery(queries.toArray(new SpanQuery[0])); - } else { - queryPos = null; - } - } else { - Term[] terms = graph.getTerms(field, start); - assert terms.length > 0; - if (terms.length >= maxBooleanClause) { - throw new BooleanQuery.TooManyClauses(); - } - if (terms.length == 1) { - queryPos = new SpanTermQuery(terms[0]); - } else { - SpanTermQuery[] orClauses = new SpanTermQuery[terms.length]; - for (int idx = 0; idx < terms.length; idx++) { - orClauses[idx] = new SpanTermQuery(terms[idx]); - } - - queryPos = new SpanOrQuery(orClauses); - } - } - - if (queryPos != null) { - if (clauses.size() >= maxBooleanClause) { - throw new BooleanQuery.TooManyClauses(); - } - clauses.add(queryPos); - } - } - - if (clauses.isEmpty()) { - return null; - } else if (clauses.size() == 1) { - return clauses.get(0); - } else { - return new SpanNearQuery(clauses.toArray(new SpanQuery[0]), phraseSlop, true); - } - } } /** diff --git a/server/src/test/java/org/elasticsearch/index/query/QueryStringQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/QueryStringQueryBuilderTests.java index fd85a2b568dcd..1d062492d63e9 100644 --- a/server/src/test/java/org/elasticsearch/index/query/QueryStringQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/QueryStringQueryBuilderTests.java @@ -683,17 +683,23 @@ public void testToQueryWithGraph() throws Exception { // span query with slop query = queryParser.parse("\"that guinea pig smells\"~2"); - expectedQuery = new SpanNearQuery.Builder(STRING_FIELD_NAME, true) - .addClause(new SpanTermQuery(new Term(STRING_FIELD_NAME, "that"))) - .addClause( - new SpanOrQuery( - new SpanNearQuery.Builder(STRING_FIELD_NAME, true) - .addClause(new SpanTermQuery(new Term(STRING_FIELD_NAME, "guinea"))) - .addClause(new SpanTermQuery(new Term(STRING_FIELD_NAME, "pig"))).build(), - new SpanTermQuery(new Term(STRING_FIELD_NAME, "cavy")))) - .addClause(new SpanTermQuery(new Term(STRING_FIELD_NAME, "smells"))) + PhraseQuery pq1 = new PhraseQuery.Builder() + .add(new Term(STRING_FIELD_NAME, "that")) + .add(new Term(STRING_FIELD_NAME, "guinea")) + .add(new Term(STRING_FIELD_NAME, "pig")) + .add(new Term(STRING_FIELD_NAME, "smells")) + .setSlop(2) + .build(); + PhraseQuery pq2 = new PhraseQuery.Builder() + .add(new Term(STRING_FIELD_NAME, "that")) + .add(new Term(STRING_FIELD_NAME, "cavy")) + .add(new Term(STRING_FIELD_NAME, "smells")) .setSlop(2) .build(); + expectedQuery = new BooleanQuery.Builder() + .add(pq1, Occur.SHOULD) + .add(pq2, Occur.SHOULD) + .build(); assertThat(query, Matchers.equalTo(expectedQuery)); } } diff --git a/server/src/test/java/org/elasticsearch/index/query/SimpleQueryStringBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/SimpleQueryStringBuilderTests.java index 10b603f702ddf..162cd7a1aad33 100644 --- a/server/src/test/java/org/elasticsearch/index/query/SimpleQueryStringBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/SimpleQueryStringBuilderTests.java @@ -29,6 +29,7 @@ import org.apache.lucene.search.FuzzyQuery; import org.apache.lucene.search.MatchAllDocsQuery; import org.apache.lucene.search.MatchNoDocsQuery; +import org.apache.lucene.search.PhraseQuery; import org.apache.lucene.search.PrefixQuery; import org.apache.lucene.search.Query; import org.apache.lucene.search.SynonymQuery; @@ -547,16 +548,25 @@ public void testAnalyzerWithGraph() { // phrase with slop query = parser.parse("big \"tiny guinea pig\"~2"); + PhraseQuery pq1 = new PhraseQuery.Builder() + .add(new Term(STRING_FIELD_NAME, "tiny")) + .add(new Term(STRING_FIELD_NAME, "guinea")) + .add(new Term(STRING_FIELD_NAME, "pig")) + .setSlop(2) + .build(); + PhraseQuery pq2 = new PhraseQuery.Builder() + .add(new Term(STRING_FIELD_NAME, "tiny")) + .add(new Term(STRING_FIELD_NAME, "cavy")) + .setSlop(2) + .build(); expectedQuery = new BooleanQuery.Builder() .add(new TermQuery(new Term(STRING_FIELD_NAME, "big")), defaultOp) - .add(new SpanNearQuery(new SpanQuery[] { - new SpanTermQuery(new Term(STRING_FIELD_NAME, "tiny")), - new SpanOrQuery( - new SpanNearQuery(new SpanQuery[] { span1, span2 }, 0, true), - new SpanTermQuery(new Term(STRING_FIELD_NAME, "cavy")) - ) - }, 2, true), defaultOp) + .add(new BooleanQuery.Builder() + .add(pq1, BooleanClause.Occur.SHOULD) + .add(pq2, BooleanClause.Occur.SHOULD) + .build(), + defaultOp) .build(); assertThat(query, equalTo(expectedQuery)); } From 1842b1e2670463356be54208ab0759585218b560 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Wed, 28 Nov 2018 12:36:29 -0800 Subject: [PATCH 004/115] [DOCS] Adds higher-level monitoring configuration page (#35926) --- .../collecting-monitoring-data.asciidoc | 216 ++++++++++++++++++ .../configuring-metricbeat.asciidoc | 7 +- .../configuring-monitoring.asciidoc | 202 +--------------- 3 files changed, 226 insertions(+), 199 deletions(-) create mode 100644 docs/reference/monitoring/collecting-monitoring-data.asciidoc diff --git a/docs/reference/monitoring/collecting-monitoring-data.asciidoc b/docs/reference/monitoring/collecting-monitoring-data.asciidoc new file mode 100644 index 0000000000000..61b08801eebb8 --- /dev/null +++ b/docs/reference/monitoring/collecting-monitoring-data.asciidoc @@ -0,0 +1,216 @@ +[role="xpack"] +[testenv="gold"] +[[collecting-monitoring-data]] +=== Collecting monitoring data +++++ +Collecting monitoring data +++++ + +If you enable the Elastic {monitor-features} in your cluster, you can +optionally collect metrics about {es}. By default, monitoring is enabled but +data collection is disabled. + +This method involves sending the metrics to the monitoring cluster by using +exporters. For an alternative method, see <>. + +NOTE: If you want to collect monitoring data from sources such as Beats and {ls} +and route it to a monitoring cluster, you must follow this method. You cannot +use {metricbeat} to ship the monitoring data for those products yet. + +Advanced monitoring settings enable you to control how frequently data is +collected, configure timeouts, and set the retention period for locally-stored +monitoring indices. You can also adjust how monitoring data is displayed. + +To learn about monitoring in general, see +{stack-ov}/xpack-monitoring.html[Monitoring the {stack}]. + +. Configure your cluster to collect monitoring data: + +.. Verify that the `xpack.monitoring.enabled` setting is `true`, which is its +default value, on each node in the cluster. For more information, see +<>. + +.. Verify that the `xpack.monitoring.elasticsearch.collection.enabled` setting +is `true`, which is its default value, on each node in the cluster. ++ +-- +NOTE: You can specify this setting in either the `elasticsearch.yml` on each +node or across the cluster as a dynamic cluster setting. If {es} +{security-features} are enabled, you must have `monitor` cluster privileges to +view the cluster settings and `manage` cluster privileges to change them. + +For more information, see <> and <>. +-- + +.. Set the `xpack.monitoring.collection.enabled` setting to `true` on each +node in the cluster. By default, it is is disabled (`false`). ++ +-- +NOTE: You can specify this setting in either the `elasticsearch.yml` on each +node or across the cluster as a dynamic cluster setting. If {es} +{security-features} are enabled, you must have `monitor` cluster privileges to +view the cluster settings and `manage` cluster privileges to change them. + +For example, use the following APIs to review and change this setting: + +[source,js] +---------------------------------- +GET _cluster/settings + +PUT _cluster/settings +{ + "persistent": { + "xpack.monitoring.collection.enabled": true + } +} +---------------------------------- +// CONSOLE + +Alternatively, you can enable this setting in {kib}. In the side navigation, +click *Monitoring*. If data collection is disabled, you are prompted to turn it +on. + +For more +information, see <> and <>. +-- + +.. Optional: Specify which indices you want to monitor. ++ +-- +By default, the monitoring agent collects data from all {es} indices. +To collect data from particular indices, configure the +`xpack.monitoring.collection.indices` setting. You can specify multiple indices +as a comma-separated list or use an index pattern to match multiple indices. For +example: + +[source,yaml] +---------------------------------- +xpack.monitoring.collection.indices: logstash-*, index1, test2 +---------------------------------- + +You can prepend `+` or `-` to explicitly include or exclude index names or +patterns. For example, to include all indices that start with `test` except +`test3`, you could specify `+test*,-test3`. +-- + +.. Optional: Specify how often to collect monitoring data. The default value for +the `xpack.monitoring.collection.interval` setting 10 seconds. See +<>. + +. Identify where to store monitoring data. ++ +-- +By default, the data is stored on the same cluster by using a +<>. + +Alternatively, you can use an <> to send data to +a separate _monitoring cluster_. + +For more information about typical monitoring architectures, +see {stack-ov}/how-monitoring-works.html[How Monitoring Works]. +-- + +. If you choose to use an `http` exporter: + +.. On the cluster that you want to monitor (often called the _production cluster_), +configure each node to send metrics to your monitoring cluster. Configure an +HTTP exporter in the `xpack.monitoring.exporters` settings in the +`elasticsearch.yml` file. For example: ++ +-- +[source,yaml] +-------------------------------------------------- +xpack.monitoring.exporters: + id1: + type: http + host: ["http://es-mon-1:9200", "http://es-mon2:9200"] +-------------------------------------------------- +-- + +.. If the Elastic {security-features} are enabled on the monitoring cluster, you +must provide appropriate credentials when data is shipped to the monitoring cluster: + +... Create a user on the monitoring cluster that has the +{stack-ov}/built-in-roles.html[`remote_monitoring_agent` built-in role]. +Alternatively, use the +{stack-ov}/built-in-users.html[`remote_monitoring_user` built-in user]. + +... Add the user ID and password settings to the HTTP exporter settings in the +`elasticsearch.yml` file on each node. + ++ +-- +For example: + +[source,yaml] +-------------------------------------------------- +xpack.monitoring.exporters: + id1: + type: http + host: ["http://es-mon-1:9200", "http://es-mon2:9200"] + auth.username: remote_monitoring_user + auth.password: YOUR_PASSWORD +-------------------------------------------------- +-- + +.. If you configured the monitoring cluster to use +<>, you must use the HTTPS protocol in +the `host` setting. You must also specify the trusted CA certificates that will +be used to verify the identity of the nodes in the monitoring cluster. + +*** To add a CA certificate to an {es} node's trusted certificates, you can +specify the location of the PEM encoded certificate with the +`certificate_authorities` setting. For example: ++ +-- +[source,yaml] +-------------------------------------------------- +xpack.monitoring.exporters: + id1: + type: http + host: ["https://es-mon1:9200", "https://es-mon2:9200"] + auth: + username: remote_monitoring_user + password: YOUR_PASSWORD + ssl: + certificate_authorities: [ "/path/to/ca.crt" ] +-------------------------------------------------- +-- + +*** Alternatively, you can configure trusted certificates using a truststore +(a Java Keystore file that contains the certificates). For example: ++ +-- +[source,yaml] +-------------------------------------------------- +xpack.monitoring.exporters: + id1: + type: http + host: ["https://es-mon1:9200", "https://es-mon2:9200"] + auth: + username: remote_monitoring_user + password: YOUR_PASSWORD + ssl: + truststore.path: /path/to/file + truststore.password: password +-------------------------------------------------- +-- + +. Configure your cluster to route monitoring data from sources such as {kib}, +Beats, and {ls} to the monitoring cluster. For information about configuring +each product to collect and send monitoring data, see +{stack-ov}/xpack-monitoring.html[Monitoring the {stack}]. + +. If you updated settings in the `elasticsearch.yml` files on your production +cluster, restart {es}. See <> and <>. ++ +-- +TIP: You may want to temporarily {ref}/modules-cluster.html[disable shard +allocation] before you restart your nodes to avoid unnecessary shard +reallocation during the install process. + +-- + +. Optional: +<>. + +. {kibana-ref}/monitoring-data.html[View the monitoring data in {kib}]. diff --git a/docs/reference/monitoring/configuring-metricbeat.asciidoc b/docs/reference/monitoring/configuring-metricbeat.asciidoc index d7bd58eb33f29..6098336538bf9 100644 --- a/docs/reference/monitoring/configuring-metricbeat.asciidoc +++ b/docs/reference/monitoring/configuring-metricbeat.asciidoc @@ -1,13 +1,16 @@ [role="xpack"] [testenv="gold"] [[configuring-metricbeat]] -=== Monitoring {es} with {metricbeat} +=== Collecting {es} monitoring data with {metricbeat} +++++ +Collecting monitoring data with {metricbeat} +++++ beta[] In 6.5 and later, you can use {metricbeat} to collect data about {es} and ship it to the monitoring cluster, rather than routing it through exporters -as described in <>. +as described in <>. image::monitoring/images/metricbeat.png[Example monitoring architecture] diff --git a/docs/reference/monitoring/configuring-monitoring.asciidoc b/docs/reference/monitoring/configuring-monitoring.asciidoc index 4bce7a0295c09..7e49562e8717a 100644 --- a/docs/reference/monitoring/configuring-monitoring.asciidoc +++ b/docs/reference/monitoring/configuring-monitoring.asciidoc @@ -6,208 +6,16 @@ Configuring monitoring ++++ -If you enable the Elastic {monitor-features} in your cluster, you can -optionally collect metrics about {es}. By default, monitoring is enabled but -data collection is disabled. +If you enable the Elastic {monitor-features} in your cluster, there are two +methods to collect metrics about {es}: -The following method involves sending the metrics to the monitoring cluster by -using exporters. For an alternative method, see <>. - -Advanced monitoring settings enable you to control how frequently data is -collected, configure timeouts, and set the retention period for locally-stored -monitoring indices. You can also adjust how monitoring data is displayed. +* <> +* <> To learn about monitoring in general, see {stack-ov}/xpack-monitoring.html[Monitoring the {stack}]. -. Configure your cluster to collect monitoring data: - -.. Verify that the `xpack.monitoring.enabled` setting is `true`, which is its -default value, on each node in the cluster. For more information, see -<>. - -.. Verify that the `xpack.monitoring.elasticsearch.collection.enabled` setting -is `true`, which is its default value, on each node in the cluster. -+ --- -NOTE: You can specify this setting in either the `elasticsearch.yml` on each -node or across the cluster as a dynamic cluster setting. If {es} -{security-features} are enabled, you must have `monitor` cluster privileges to -view the cluster settings and `manage` cluster privileges to change them. - -For more information, see <> and <>. --- - -.. Set the `xpack.monitoring.collection.enabled` setting to `true` on each -node in the cluster. By default, it is is disabled (`false`). -+ --- -NOTE: You can specify this setting in either the `elasticsearch.yml` on each -node or across the cluster as a dynamic cluster setting. If {es} -{security-features} are enabled, you must have `monitor` cluster privileges to -view the cluster settings and `manage` cluster privileges to change them. - -For example, use the following APIs to review and change this setting: - -[source,js] ----------------------------------- -GET _cluster/settings - -PUT _cluster/settings -{ - "persistent": { - "xpack.monitoring.collection.enabled": true - } -} ----------------------------------- -// CONSOLE - -For more -information, see <> and <>. --- - -.. Optional: Specify which indices you want to monitor. -+ --- -By default, the monitoring agent collects data from all {es} indices. -To collect data from particular indices, configure the -`xpack.monitoring.collection.indices` setting. You can specify multiple indices -as a comma-separated list or use an index pattern to match multiple indices. For -example: - -[source,yaml] ----------------------------------- -xpack.monitoring.collection.indices: logstash-*, index1, test2 ----------------------------------- - -You can prepend `+` or `-` to explicitly include or exclude index names or -patterns. For example, to include all indices that start with `test` except -`test3`, you could specify `+test*,-test3`. --- - -.. Optional: Specify how often to collect monitoring data. The default value for -the `xpack.monitoring.collection.interval` setting 10 seconds. See -<>. - -. Identify where to store monitoring data. -+ --- -By default, the data is stored on the same cluster by using a -<>. - -Alternatively, you can use an <> to send data to -a separate _monitoring cluster_. - -For more information about typical monitoring architectures, -see {stack-ov}/how-monitoring-works.html[How Monitoring Works]. --- - -. If you choose to use an `http` exporter: - -.. On the cluster that you want to monitor (often called the _production cluster_), -configure each node to send metrics to your monitoring cluster. Configure an -HTTP exporter in the `xpack.monitoring.exporters` settings in the -`elasticsearch.yml` file. For example: -+ --- -[source,yaml] --------------------------------------------------- -xpack.monitoring.exporters: - id1: - type: http - host: ["http://es-mon-1:9200", "http://es-mon2:9200"] --------------------------------------------------- --- - -.. If the Elastic {security-features} are enabled on the monitoring cluster, you -must provide appropriate credentials when data is shipped to the monitoring cluster: - -... Create a user on the monitoring cluster that has the -{stack-ov}/built-in-roles.html[`remote_monitoring_agent` built-in role]. -Alternatively, use the -{stack-ov}/built-in-users.html[`remote_monitoring_user` built-in user]. - -... Add the user ID and password settings to the HTTP exporter settings in the -`elasticsearch.yml` file on each node. + -+ --- -For example: - -[source,yaml] --------------------------------------------------- -xpack.monitoring.exporters: - id1: - type: http - host: ["http://es-mon-1:9200", "http://es-mon2:9200"] - auth.username: remote_monitoring_user - auth.password: YOUR_PASSWORD --------------------------------------------------- --- - -.. If you configured the monitoring cluster to use -<>, you must use the HTTPS protocol in -the `host` setting. You must also specify the trusted CA certificates that will -be used to verify the identity of the nodes in the monitoring cluster. - -*** To add a CA certificate to an {es} node's trusted certificates, you can -specify the location of the PEM encoded certificate with the -`certificate_authorities` setting. For example: -+ --- -[source,yaml] --------------------------------------------------- -xpack.monitoring.exporters: - id1: - type: http - host: ["https://es-mon1:9200", "https://es-mon2:9200"] - auth: - username: remote_monitoring_user - password: YOUR_PASSWORD - ssl: - certificate_authorities: [ "/path/to/ca.crt" ] --------------------------------------------------- --- - -*** Alternatively, you can configure trusted certificates using a truststore -(a Java Keystore file that contains the certificates). For example: -+ --- -[source,yaml] --------------------------------------------------- -xpack.monitoring.exporters: - id1: - type: http - host: ["https://es-mon1:9200", "https://es-mon2:9200"] - auth: - username: remote_monitoring_user - password: YOUR_PASSWORD - ssl: - truststore.path: /path/to/file - truststore.password: password --------------------------------------------------- --- - -. Configure your cluster to route monitoring data from sources such as {kib}, -Beats, and {ls} to the monitoring cluster. The -`xpack.monitoring.collection.enabled` setting must be `true` on each node in the -cluster. For information about configuring each product to collect and send -monitoring data, see {stack-ov}/xpack-monitoring.html[Monitoring the {stack}]. - -. If you updated settings in the `elasticsearch.yml` files on your production -cluster, restart {es}. See <> and <>. -+ --- -TIP: You may want to temporarily {ref}/modules-cluster.html[disable shard -allocation] before you restart your nodes to avoid unnecessary shard -reallocation during the install process. - --- - -. Optional: -<>. - -. {kibana-ref}/monitoring-data.html[View the monitoring data in {kib}]. - +include::collecting-monitoring-data.asciidoc[] include::configuring-metricbeat.asciidoc[] include::indices.asciidoc[] include::tribe.asciidoc[] From 15028931e61f977e94aa6b10813bc508707d2057 Mon Sep 17 00:00:00 2001 From: Nik Everett Date: Wed, 28 Nov 2018 09:28:27 -0500 Subject: [PATCH 005/115] Tasks: Only require task permissions (#35667) Right now using the `GET /_tasks/` API and causing a task to opt in to saving its result after being completed requires permissions on the `.tasks` index. When we built this we thought that that was fine, but we've since moved towards not leaking details like "persisting task results after the task is completed is done by saving them into an index named `.tasks`." A more modern way of doing this would be to save the tasks into the index "under the hood" and to have APIs to manage the saved tasks. This is the first step down that road: it drops the requirement to have permissions to interact with the `.tasks` index when fetching task statuses and when persisting statuses beyond the lifetime of the task. In particular, this moves the concept of the "origin" of an action into a more prominent place in the Elasticsearch server. The origin of an action is ignored by the server, but the security plugin uses the origin to make requests on behalf of a user in such a way that the user need not have permissions to perform these actions. It *can* be made to be fairly precise. More specifically, we can create an internal user just for the tasks API that just has permission to interact with the `.tasks` index. This change doesn't do that, instead, it uses the ubiquitus "xpack" user which has most permissions because it is simpler. Adding the tasks user is something I'd like to get to in a follow up change. Instead, the majority of this change is about moving the "origin" concept from the security portion of x-pack into the server. This should allow any code to use the origin. To keep the change managable I've also opted to deprecate rather than remove the "origin" helpers in the security code. Removing them is almost entirely mechanical and I'd like to that in a follow up as well. Relates to #35573 --- .../cluster/node/tasks/get/GetTaskAction.java | 1 + .../tasks/get/TransportGetTaskAction.java | 5 +- .../client/OriginSettingClient.java | 61 +++++++++++++ .../common/util/concurrent/ThreadContext.java | 35 ++++++- .../persistent/PersistentTasksService.java | 30 +----- .../tasks/TaskResultsService.java | 7 +- .../admin/cluster/node/tasks/TasksIT.java | 64 +++++-------- .../cluster/node/tasks/TestTaskPlugin.java | 91 ++++++++++++++++++- .../client/OriginSettingClientTests.java | 79 ++++++++++++++++ .../util/concurrent/ThreadContextTests.java | 25 +++++ .../PersistentTasksNodeServiceTests.java | 5 +- .../xpack/core/ClientHelper.java | 40 +++----- .../authz/store/ReservedRolesStore.java | 4 +- .../xpack/core/ClientHelperTests.java | 26 ------ .../authz/store/ReservedRolesStoreTests.java | 14 --- .../security/authz/AuthorizationUtils.java | 2 + .../authz/AuthorizationUtilsTests.java | 54 ++++------- .../reindex-tests-with-security/build.gradle | 1 + .../qa/reindex-tests-with-security/roles.yml | 20 ++++ .../rest-api-spec/test/10_reindex.yml | 49 +++++++++- 20 files changed, 437 insertions(+), 176 deletions(-) create mode 100644 server/src/main/java/org/elasticsearch/client/OriginSettingClient.java create mode 100644 server/src/test/java/org/elasticsearch/client/OriginSettingClientTests.java diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/node/tasks/get/GetTaskAction.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/node/tasks/get/GetTaskAction.java index c30f5e4091b75..94c3b7f53a104 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/node/tasks/get/GetTaskAction.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/node/tasks/get/GetTaskAction.java @@ -26,6 +26,7 @@ * Action for retrieving a list of currently running tasks */ public class GetTaskAction extends Action { + public static final String TASKS_ORIGIN = "tasks"; public static final GetTaskAction INSTANCE = new GetTaskAction(); public static final String NAME = "cluster:monitor/task/get"; diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/node/tasks/get/TransportGetTaskAction.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/node/tasks/get/TransportGetTaskAction.java index 39648abdc01bd..ba13a904a1b75 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/node/tasks/get/TransportGetTaskAction.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/node/tasks/get/TransportGetTaskAction.java @@ -29,6 +29,7 @@ import org.elasticsearch.action.support.HandledTransportAction; import org.elasticsearch.client.Client; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.client.OriginSettingClient; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.inject.Inject; @@ -53,6 +54,7 @@ import java.io.IOException; +import static org.elasticsearch.action.admin.cluster.node.tasks.get.GetTaskAction.TASKS_ORIGIN; import static org.elasticsearch.action.admin.cluster.node.tasks.list.TransportListTasksAction.waitForCompletionTimeout; /** @@ -78,7 +80,7 @@ public TransportGetTaskAction(Settings settings, ThreadPool threadPool, Transpor super(settings, GetTaskAction.NAME, threadPool, transportService, actionFilters, indexNameExpressionResolver, GetTaskRequest::new); this.clusterService = clusterService; this.transportService = transportService; - this.client = client; + this.client = new OriginSettingClient(client, TASKS_ORIGIN); this.xContentRegistry = xContentRegistry; } @@ -216,6 +218,7 @@ void getFinishedTaskFromIndex(Task thisTask, GetTaskRequest request, ActionListe GetRequest get = new GetRequest(TaskResultsService.TASK_INDEX, TaskResultsService.TASK_TYPE, request.getTaskId().toString()); get.setParentTask(clusterService.localNode().getId(), thisTask.getId()); + client.get(get, new ActionListener() { @Override public void onResponse(GetResponse getResponse) { diff --git a/server/src/main/java/org/elasticsearch/client/OriginSettingClient.java b/server/src/main/java/org/elasticsearch/client/OriginSettingClient.java new file mode 100644 index 0000000000000..49ec79ba07290 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/client/OriginSettingClient.java @@ -0,0 +1,61 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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.elasticsearch.client; + +import org.elasticsearch.action.Action; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestBuilder; +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.action.support.ContextPreservingActionListener; +import org.elasticsearch.common.util.concurrent.ThreadContext; + +import java.util.function.Supplier; + +/** + * A {@linkplain Client} that sends requests with the + * {@link ThreadContext#stashWithOrigin origin} set to a particular + * value and calls its {@linkplain ActionListener} in its original + * {@link ThreadContext}. + */ +public final class OriginSettingClient extends FilterClient { + + private final String origin; + + public OriginSettingClient(Client in, String origin) { + super(in); + this.origin = origin; + } + + @Override + protected < + Request extends ActionRequest, + Response extends ActionResponse, + RequestBuilder extends ActionRequestBuilder> + void doExecute( + Action action, + Request request, + ActionListener listener) { + final Supplier supplier = in().threadPool().getThreadContext().newRestorableContext(false); + try (ThreadContext.StoredContext ignore = in().threadPool().getThreadContext().stashWithOrigin(origin)) { + super.doExecute(action, request, new ContextPreservingActionListener<>(supplier, listener)); + } + } +} diff --git a/server/src/main/java/org/elasticsearch/common/util/concurrent/ThreadContext.java b/server/src/main/java/org/elasticsearch/common/util/concurrent/ThreadContext.java index 9664811149567..79d7c3510c2d1 100644 --- a/server/src/main/java/org/elasticsearch/common/util/concurrent/ThreadContext.java +++ b/server/src/main/java/org/elasticsearch/common/util/concurrent/ThreadContext.java @@ -22,6 +22,8 @@ import org.apache.logging.log4j.LogManager; import org.apache.lucene.util.CloseableThreadLocal; import org.elasticsearch.ExceptionsHelper; +import org.elasticsearch.action.support.ContextPreservingActionListener; +import org.elasticsearch.client.OriginSettingClient; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; @@ -85,6 +87,12 @@ public final class ThreadContext implements Closeable, Writeable { public static final String PREFIX = "request.headers"; public static final Setting DEFAULT_HEADERS_SETTING = Setting.groupSetting(PREFIX + ".", Property.NodeScope); + + /** + * Name for the {@link #stashWithOrigin origin} attribute. + */ + public static final String ACTION_ORIGIN_TRANSIENT_NAME = "action.origin"; + private static final Logger logger = LogManager.getLogger(ThreadContext.class); private static final ThreadContextStruct DEFAULT_CONTEXT = new ThreadContextStruct(); private final Map defaultHeader; @@ -119,7 +127,7 @@ public void close() throws IOException { /** * Removes the current context and resets a default context. The removed context can be - * restored when closing the returned {@link StoredContext} + * restored by closing the returned {@link StoredContext}. */ public StoredContext stashContext() { final ThreadContextStruct context = threadLocal.get(); @@ -127,6 +135,31 @@ public StoredContext stashContext() { return () -> threadLocal.set(context); } + /** + * Removes the current context and resets a default context marked with as + * originating from the supplied string. The removed context can be + * restored by closing the returned {@link StoredContext}. Callers should + * be careful to save the current context before calling this method and + * restore it any listeners, likely with + * {@link ContextPreservingActionListener}. Use {@link OriginSettingClient} + * which can be used to do this automatically. + *

+ * Without security the origin is ignored, but security uses it to authorize + * actions that are made up of many sub-actions. These actions call + * {@link #stashWithOrigin} before performing on behalf of a user that + * should be allowed even if the user doesn't have permission to perform + * those actions on their own. + *

+ * For example, a user might not have permission to GET from the tasks index + * but the tasks API will perform a get on their behalf using this method + * if it can't find the task in memory. + */ + public StoredContext stashWithOrigin(String origin) { + final ThreadContext.StoredContext storedContext = stashContext(); + putTransient(ACTION_ORIGIN_TRANSIENT_NAME, origin); + return storedContext; + } + /** * Removes the current context and resets a new context that contains a merge of the current headers and the given headers. * The removed context can be restored when closing the returned {@link StoredContext}. The merge strategy is that headers diff --git a/server/src/main/java/org/elasticsearch/persistent/PersistentTasksService.java b/server/src/main/java/org/elasticsearch/persistent/PersistentTasksService.java index 145435badc091..01579e9907dc2 100644 --- a/server/src/main/java/org/elasticsearch/persistent/PersistentTasksService.java +++ b/server/src/main/java/org/elasticsearch/persistent/PersistentTasksService.java @@ -26,21 +26,19 @@ import org.elasticsearch.action.ActionRequestBuilder; import org.elasticsearch.action.admin.cluster.node.tasks.cancel.CancelTasksRequest; import org.elasticsearch.action.admin.cluster.node.tasks.cancel.CancelTasksResponse; -import org.elasticsearch.action.support.ContextPreservingActionListener; import org.elasticsearch.client.Client; +import org.elasticsearch.client.OriginSettingClient; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.ClusterStateObserver; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.unit.TimeValue; -import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.node.NodeClosedException; import org.elasticsearch.persistent.PersistentTasksCustomMetaData.PersistentTask; import org.elasticsearch.tasks.TaskId; import org.elasticsearch.threadpool.ThreadPool; import java.util.function.Predicate; -import java.util.function.Supplier; /** * This service is used by persistent tasks and allocated persistent tasks to communicate changes @@ -51,7 +49,6 @@ public class PersistentTasksService { private static final Logger logger = LogManager.getLogger(PersistentTasksService.class); - private static final String ACTION_ORIGIN_TRANSIENT_NAME = "action.origin"; private static final String PERSISTENT_TASK_ORIGIN = "persistent_tasks"; private final Client client; @@ -59,7 +56,7 @@ public class PersistentTasksService { private final ThreadPool threadPool; public PersistentTasksService(ClusterService clusterService, ThreadPool threadPool, Client client) { - this.client = client; + this.client = new OriginSettingClient(client, PERSISTENT_TASK_ORIGIN); this.clusterService = clusterService; this.threadPool = threadPool; } @@ -99,12 +96,7 @@ void sendCancelRequest(final long taskId, final String reason, final ActionListe request.setTaskId(new TaskId(clusterService.localNode().getId(), taskId)); request.setReason(reason); try { - final ThreadContext threadContext = client.threadPool().getThreadContext(); - final Supplier supplier = threadContext.newRestorableContext(false); - - try (ThreadContext.StoredContext ignore = stashWithOrigin(threadContext, PERSISTENT_TASK_ORIGIN)) { - client.admin().cluster().cancelTasks(request, new ContextPreservingActionListener<>(supplier, listener)); - } + client.admin().cluster().cancelTasks(request, listener); } catch (Exception e) { listener.onFailure(e); } @@ -141,14 +133,8 @@ public void sendRemoveRequest(final String taskId, final ActionListener> void execute(final Req request, final Action action, final ActionListener> listener) { try { - final ThreadContext threadContext = client.threadPool().getThreadContext(); - final Supplier supplier = threadContext.newRestorableContext(false); - - try (ThreadContext.StoredContext ignore = stashWithOrigin(threadContext, PERSISTENT_TASK_ORIGIN)) { - client.execute(action, request, - new ContextPreservingActionListener<>(supplier, - ActionListener.wrap(r -> listener.onResponse(r.getTask()), listener::onFailure))); - } + client.execute(action, request, + ActionListener.wrap(r -> listener.onResponse(r.getTask()), listener::onFailure)); } catch (Exception e) { listener.onFailure(e); } @@ -234,10 +220,4 @@ default void onTimeout(TimeValue timeout) { onFailure(new IllegalStateException("Timed out when waiting for persistent task after " + timeout)); } } - - public static ThreadContext.StoredContext stashWithOrigin(ThreadContext threadContext, String origin) { - final ThreadContext.StoredContext storedContext = threadContext.stashContext(); - threadContext.putTransient(ACTION_ORIGIN_TRANSIENT_NAME, origin); - return storedContext; - } } diff --git a/server/src/main/java/org/elasticsearch/tasks/TaskResultsService.java b/server/src/main/java/org/elasticsearch/tasks/TaskResultsService.java index 136a551ab7dea..b968d17d7e94a 100644 --- a/server/src/main/java/org/elasticsearch/tasks/TaskResultsService.java +++ b/server/src/main/java/org/elasticsearch/tasks/TaskResultsService.java @@ -32,6 +32,7 @@ import org.elasticsearch.action.index.IndexResponse; import org.elasticsearch.action.support.master.AcknowledgedResponse; import org.elasticsearch.client.Client; +import org.elasticsearch.client.OriginSettingClient; import org.elasticsearch.client.Requests; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.metadata.IndexMetaData; @@ -51,6 +52,8 @@ import java.nio.charset.StandardCharsets; import java.util.Map; +import static org.elasticsearch.action.admin.cluster.node.tasks.get.GetTaskAction.TASKS_ORIGIN; + /** * Service that can store task results. */ @@ -77,7 +80,7 @@ public class TaskResultsService { @Inject public TaskResultsService(Client client, ClusterService clusterService, TransportCreateIndexAction createIndexAction) { - this.client = client; + this.client = new OriginSettingClient(client, TASKS_ORIGIN); this.clusterService = clusterService; this.createIndexAction = createIndexAction; } @@ -93,7 +96,7 @@ public void storeResult(TaskResult taskResult, ActionListener listener) { createIndexRequest.mapping(TASK_TYPE, taskResultIndexMapping(), XContentType.JSON); createIndexRequest.cause("auto(task api)"); - createIndexAction.execute(null, createIndexRequest, new ActionListener() { + client.admin().indices().create(createIndexRequest, new ActionListener() { @Override public void onResponse(CreateIndexResponse result) { doStoreResult(taskResult, listener); diff --git a/server/src/test/java/org/elasticsearch/action/admin/cluster/node/tasks/TasksIT.java b/server/src/test/java/org/elasticsearch/action/admin/cluster/node/tasks/TasksIT.java index ee4c6979eccc0..1ca09cb19c001 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/cluster/node/tasks/TasksIT.java +++ b/server/src/test/java/org/elasticsearch/action/admin/cluster/node/tasks/TasksIT.java @@ -27,6 +27,7 @@ import org.elasticsearch.action.TaskOperationFailure; import org.elasticsearch.action.admin.cluster.health.ClusterHealthAction; import org.elasticsearch.action.admin.cluster.node.tasks.cancel.CancelTasksResponse; +import org.elasticsearch.action.admin.cluster.node.tasks.get.GetTaskRequest; import org.elasticsearch.action.admin.cluster.node.tasks.get.GetTaskResponse; import org.elasticsearch.action.admin.cluster.node.tasks.list.ListTasksAction; import org.elasticsearch.action.admin.cluster.node.tasks.list.ListTasksResponse; @@ -34,7 +35,6 @@ import org.elasticsearch.action.admin.indices.upgrade.post.UpgradeAction; import org.elasticsearch.action.admin.indices.validate.query.ValidateQueryAction; import org.elasticsearch.action.bulk.BulkAction; -import org.elasticsearch.action.get.GetResponse; import org.elasticsearch.action.index.IndexAction; import org.elasticsearch.action.index.IndexResponse; import org.elasticsearch.action.search.SearchAction; @@ -85,7 +85,6 @@ import static org.elasticsearch.common.unit.TimeValue.timeValueMillis; import static org.elasticsearch.common.unit.TimeValue.timeValueSeconds; import static org.elasticsearch.http.HttpTransportSettings.SETTING_HTTP_MAX_HEADER_SIZE; -import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertNoFailures; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSearchResponse; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertThrows; @@ -722,12 +721,6 @@ public void testTasksWaitForAllTask() throws Exception { } public void testTaskStoringSuccesfulResult() throws Exception { - // Randomly create an empty index to make sure the type is created automatically - if (randomBoolean()) { - logger.info("creating an empty results index with custom settings"); - assertAcked(client().admin().indices().prepareCreate(TaskResultsService.TASK_INDEX)); - } - registerTaskManageListeners(TestTaskPlugin.TestTaskAction.NAME); // we need this to get task id of the process // Start non-blocking test task @@ -739,23 +732,20 @@ public void testTaskStoringSuccesfulResult() throws Exception { TaskInfo taskInfo = events.get(0); TaskId taskId = taskInfo.getTaskId(); - GetResponse resultDoc = client() - .prepareGet(TaskResultsService.TASK_INDEX, TaskResultsService.TASK_TYPE, taskId.toString()).get(); - assertTrue(resultDoc.isExists()); - - Map source = resultDoc.getSource(); - @SuppressWarnings("unchecked") - Map task = (Map) source.get("task"); - assertEquals(taskInfo.getTaskId().getNodeId(), task.get("node")); - assertEquals(taskInfo.getAction(), task.get("action")); - assertEquals(Long.toString(taskInfo.getId()), task.get("id").toString()); - - @SuppressWarnings("unchecked") - Map result = (Map) source.get("response"); + TaskResult taskResult = client().admin().cluster() + .getTask(new GetTaskRequest().setTaskId(taskId)).get().getTask(); + assertTrue(taskResult.isCompleted()); + assertNull(taskResult.getError()); + + assertEquals(taskInfo.getTaskId(), taskResult.getTask().getTaskId()); + assertEquals(taskInfo.getType(), taskResult.getTask().getType()); + assertEquals(taskInfo.getAction(), taskResult.getTask().getAction()); + assertEquals(taskInfo.getDescription(), taskResult.getTask().getDescription()); + assertEquals(taskInfo.getStartTime(), taskResult.getTask().getStartTime()); + assertEquals(taskInfo.getHeaders(), taskResult.getTask().getHeaders()); + Map result = taskResult.getResponseAsMap(); assertEquals("0", result.get("failure_count").toString()); - assertNull(source.get("failure")); - assertNoFailures(client().admin().indices().prepareRefresh(TaskResultsService.TASK_INDEX).get()); SearchResponse searchResponse = client().prepareSearch(TaskResultsService.TASK_INDEX) @@ -793,25 +783,21 @@ public void testTaskStoringFailureResult() throws Exception { TaskInfo failedTaskInfo = events.get(0); TaskId failedTaskId = failedTaskInfo.getTaskId(); - GetResponse failedResultDoc = client() - .prepareGet(TaskResultsService.TASK_INDEX, TaskResultsService.TASK_TYPE, failedTaskId.toString()) - .get(); - assertTrue(failedResultDoc.isExists()); - - Map source = failedResultDoc.getSource(); - @SuppressWarnings("unchecked") - Map task = (Map) source.get("task"); - assertEquals(failedTaskInfo.getTaskId().getNodeId(), task.get("node")); - assertEquals(failedTaskInfo.getAction(), task.get("action")); - assertEquals(Long.toString(failedTaskInfo.getId()), task.get("id").toString()); - - @SuppressWarnings("unchecked") - Map error = (Map) source.get("error"); + TaskResult taskResult = client().admin().cluster() + .getTask(new GetTaskRequest().setTaskId(failedTaskId)).get().getTask(); + assertTrue(taskResult.isCompleted()); + assertNull(taskResult.getResponse()); + + assertEquals(failedTaskInfo.getTaskId(), taskResult.getTask().getTaskId()); + assertEquals(failedTaskInfo.getType(), taskResult.getTask().getType()); + assertEquals(failedTaskInfo.getAction(), taskResult.getTask().getAction()); + assertEquals(failedTaskInfo.getDescription(), taskResult.getTask().getDescription()); + assertEquals(failedTaskInfo.getStartTime(), taskResult.getTask().getStartTime()); + assertEquals(failedTaskInfo.getHeaders(), taskResult.getTask().getHeaders()); + Map error = (Map) taskResult.getErrorAsMap(); assertEquals("Simulating operation failure", error.get("reason")); assertEquals("illegal_state_exception", error.get("type")); - assertNull(source.get("result")); - GetTaskResponse getResponse = expectFinishedTask(failedTaskId); assertNull(getResponse.getTask().getResponse()); assertEquals(error, getResponse.getTask().getErrorAsMap()); diff --git a/server/src/test/java/org/elasticsearch/action/admin/cluster/node/tasks/TestTaskPlugin.java b/server/src/test/java/org/elasticsearch/action/admin/cluster/node/tasks/TestTaskPlugin.java index 5bf000a17bac7..db2711e767be6 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/cluster/node/tasks/TestTaskPlugin.java +++ b/server/src/test/java/org/elasticsearch/action/admin/cluster/node/tasks/TestTaskPlugin.java @@ -23,6 +23,7 @@ import org.elasticsearch.action.ActionRequest; import org.elasticsearch.action.ActionRequestBuilder; import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.action.IndicesRequest; import org.elasticsearch.action.FailedNodeException; import org.elasticsearch.action.TaskOperationFailure; import org.elasticsearch.action.support.ActionFilters; @@ -41,20 +42,30 @@ import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.ToXContent.Params; +import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.common.xcontent.ToXContentFragment; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.plugins.ActionPlugin; import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.plugins.NetworkPlugin; import org.elasticsearch.tasks.CancellableTask; import org.elasticsearch.tasks.Task; import org.elasticsearch.tasks.TaskId; import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.Transport; +import org.elasticsearch.transport.TransportException; +import org.elasticsearch.transport.TransportInterceptor; +import org.elasticsearch.transport.TransportRequest; import org.elasticsearch.transport.TransportService; +import org.elasticsearch.transport.TransportRequestOptions; +import org.elasticsearch.transport.TransportResponse; +import org.elasticsearch.transport.TransportResponseHandler; import java.io.IOException; import java.util.ArrayList; @@ -64,13 +75,15 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; +import static org.elasticsearch.action.admin.cluster.node.tasks.get.GetTaskAction.TASKS_ORIGIN; import static org.elasticsearch.test.ESTestCase.awaitBusy; /** * A plugin that adds a cancellable blocking test task of integration testing of the task manager. */ -public class TestTaskPlugin extends Plugin implements ActionPlugin { +public class TestTaskPlugin extends Plugin implements ActionPlugin, NetworkPlugin { @Override public List> getActions() { @@ -83,6 +96,16 @@ public Collection getTaskHeaders() { return Collections.singleton("Custom-Task-Header"); } + /** + * Intercept transport requests to verify that all of the ones that should + * have the origin set do have the origin set and the ones + * that should not have the origin set do not have it set. + */ + @Override + public List getTransportInterceptors(NamedWriteableRegistry namedWriteableRegistry, ThreadContext threadContext) { + return Collections.singletonList(new OriginAssertingInterceptor(threadContext)); + } + static class TestTask extends CancellableTask { private volatile boolean blocked = true; @@ -489,4 +512,70 @@ protected UnblockTestTasksRequestBuilder(ElasticsearchClient client, Action void sendRequest( + Transport.Connection connection, String action, TransportRequest request, + TransportRequestOptions options, TransportResponseHandler handler) { + if (action.startsWith("indices:data/write/bulk[s]")) { + /* + * We can't reason about these requests because + * *sometimes* they should have the origin, if they are + * running on the node that stores the task. But + * sometimes they won't be and in that case they don't + * need the origin. Either way, the interesting work is + * done by checking that the main bulk request + * (without the [s] part) has the origin. + */ + sender.sendRequest(connection, action, request, options, handler); + return; + } + String expectedOrigin = shouldHaveOrigin(action, request) ? TASKS_ORIGIN : null; + String actualOrigin = threadContext.getTransient(ThreadContext.ACTION_ORIGIN_TRANSIENT_NAME); + if (Objects.equals(expectedOrigin, actualOrigin)) { + sender.sendRequest(connection, action, request, options, handler); + return; + } + handler.handleException(new TransportException("should have origin of [" + + expectedOrigin + "] but was [" + actualOrigin + "] action was [" + + action + "][" + request + "]")); + } + }; + } + + private boolean shouldHaveOrigin(String action, TransportRequest request) { + if (false == action.startsWith("indices:")) { + /* + * The Tasks API never uses origin with non-indices actions. + */ + return false; + } + if ( action.startsWith("indices:admin/refresh") + || action.startsWith("indices:data/read/search")) { + /* + * The test refreshes and searches to count the number of tasks + * in the index and the Tasks API never does either. + */ + return false; + } + if (false == (request instanceof IndicesRequest)) { + return false; + } + IndicesRequest ir = (IndicesRequest) request; + /* + * When the API Tasks API makes an indices request it only every + * targets the .tasks index. Other requests come from the tests. + */ + return Arrays.equals(new String[] {".tasks"}, ir.indices()); + } + } } diff --git a/server/src/test/java/org/elasticsearch/client/OriginSettingClientTests.java b/server/src/test/java/org/elasticsearch/client/OriginSettingClientTests.java new file mode 100644 index 0000000000000..a2af68b51e3f9 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/client/OriginSettingClientTests.java @@ -0,0 +1,79 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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.elasticsearch.client; + +import org.elasticsearch.action.Action; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestBuilder; +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.action.bulk.BulkRequest; +import org.elasticsearch.action.search.ClearScrollRequest; +import org.elasticsearch.action.search.SearchRequest; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.client.NoOpClient; + +public class OriginSettingClientTests extends ESTestCase { + public void testSetsParentId() { + String origin = randomAlphaOfLength(7); + + /* + * This mock will do nothing but verify that origin is set in the + * thread context before executing the action. + */ + NoOpClient mock = new NoOpClient(getTestName()) { + @Override + protected < + Request extends ActionRequest, + Response extends ActionResponse, + RequestBuilder extends ActionRequestBuilder> + void doExecute( + Action action, + Request request, + ActionListener listener) { + assertEquals(origin, threadPool().getThreadContext().getTransient(ThreadContext.ACTION_ORIGIN_TRANSIENT_NAME)); + super.doExecute(action, request, listener); + } + }; + + try (OriginSettingClient client = new OriginSettingClient(mock, origin)) { + // All of these should have the origin set + client.bulk(new BulkRequest()); + client.search(new SearchRequest()); + client.clearScroll(new ClearScrollRequest()); + + ThreadContext threadContext = client.threadPool().getThreadContext(); + client.bulk(new BulkRequest(), listenerThatAssertsOriginNotSet(threadContext)); + client.search(new SearchRequest(), listenerThatAssertsOriginNotSet(threadContext)); + client.clearScroll(new ClearScrollRequest(), listenerThatAssertsOriginNotSet(threadContext)); + } + } + + private ActionListener listenerThatAssertsOriginNotSet(ThreadContext threadContext) { + return ActionListener.wrap( + r -> { + assertNull(threadContext.getTransient(ThreadContext.ACTION_ORIGIN_TRANSIENT_NAME)); + }, + e -> { + fail("didn't expect to fail but: " + e); + }); + } +} diff --git a/server/src/test/java/org/elasticsearch/common/util/concurrent/ThreadContextTests.java b/server/src/test/java/org/elasticsearch/common/util/concurrent/ThreadContextTests.java index a0a92cad7a851..9729aca294184 100644 --- a/server/src/test/java/org/elasticsearch/common/util/concurrent/ThreadContextTests.java +++ b/server/src/test/java/org/elasticsearch/common/util/concurrent/ThreadContextTests.java @@ -55,6 +55,31 @@ public void testStashContext() { assertEquals("1", threadContext.getHeader("default")); } + public void testStashWithOrigin() { + final String origin = randomAlphaOfLengthBetween(4, 16); + final ThreadContext threadContext = new ThreadContext(Settings.EMPTY); + + final boolean setOtherValues = randomBoolean(); + if (setOtherValues) { + threadContext.putTransient("foo", "bar"); + threadContext.putHeader("foo", "bar"); + } + + assertNull(threadContext.getTransient(ThreadContext.ACTION_ORIGIN_TRANSIENT_NAME)); + try (ThreadContext.StoredContext storedContext = threadContext.stashWithOrigin(origin)) { + assertEquals(origin, threadContext.getTransient(ThreadContext.ACTION_ORIGIN_TRANSIENT_NAME)); + assertNull(threadContext.getTransient("foo")); + assertNull(threadContext.getTransient("bar")); + } + + assertNull(threadContext.getTransient(ThreadContext.ACTION_ORIGIN_TRANSIENT_NAME)); + + if (setOtherValues) { + assertEquals("bar", threadContext.getTransient("foo")); + assertEquals("bar", threadContext.getHeader("foo")); + } + } + public void testStashAndMerge() { Settings build = Settings.builder().put("request.headers.default", "1").build(); ThreadContext threadContext = new ThreadContext(build); diff --git a/server/src/test/java/org/elasticsearch/persistent/PersistentTasksNodeServiceTests.java b/server/src/test/java/org/elasticsearch/persistent/PersistentTasksNodeServiceTests.java index 4ddee816e7f71..7c522db238c84 100644 --- a/server/src/test/java/org/elasticsearch/persistent/PersistentTasksNodeServiceTests.java +++ b/server/src/test/java/org/elasticsearch/persistent/PersistentTasksNodeServiceTests.java @@ -22,6 +22,7 @@ import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.admin.cluster.node.tasks.cancel.CancelTasksResponse; +import org.elasticsearch.client.Client; import org.elasticsearch.cluster.ClusterChangedEvent; import org.elasticsearch.cluster.ClusterName; import org.elasticsearch.cluster.ClusterState; @@ -231,7 +232,9 @@ public void testParamsStatusAndNodeTaskAreDelegated() throws Exception { public void testTaskCancellation() { AtomicLong capturedTaskId = new AtomicLong(); AtomicReference> capturedListener = new AtomicReference<>(); - PersistentTasksService persistentTasksService = new PersistentTasksService(null, null, null) { + Client client = mock(Client.class); + when(client.settings()).thenReturn(Settings.EMPTY); + PersistentTasksService persistentTasksService = new PersistentTasksService(null, null, client) { @Override void sendCancelRequest(final long taskId, final String reason, final ActionListener listener) { capturedTaskId.set(taskId); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ClientHelper.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ClientHelper.java index 7dae8856921e8..687db62994e4c 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ClientHelper.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ClientHelper.java @@ -12,7 +12,7 @@ import org.elasticsearch.action.ActionResponse; import org.elasticsearch.action.support.ContextPreservingActionListener; import org.elasticsearch.client.Client; -import org.elasticsearch.client.FilterClient; +import org.elasticsearch.client.OriginSettingClient; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.xpack.core.security.authc.AuthenticationField; @@ -36,7 +36,12 @@ public final class ClientHelper { public static final Set SECURITY_HEADER_FILTERS = Sets.newHashSet(AuthenticationServiceField.RUN_AS_USER_HEADER, AuthenticationField.AUTHENTICATION_KEY); - public static final String ACTION_ORIGIN_TRANSIENT_NAME = "action.origin"; + /** + * . + * @deprecated use ThreadContext.ACTION_ORIGIN_TRANSIENT_NAME + */ + @Deprecated + public static final String ACTION_ORIGIN_TRANSIENT_NAME = ThreadContext.ACTION_ORIGIN_TRANSIENT_NAME; public static final String SECURITY_ORIGIN = "security"; public static final String WATCHER_ORIGIN = "watcher"; public static final String ML_ORIGIN = "ml"; @@ -50,18 +55,20 @@ private ClientHelper() {} /** * Stashes the current context and sets the origin in the current context. The original context is returned as a stored context + * @deprecated use ThreadContext.stashWithOrigin */ + @Deprecated public static ThreadContext.StoredContext stashWithOrigin(ThreadContext threadContext, String origin) { - final ThreadContext.StoredContext storedContext = threadContext.stashContext(); - threadContext.putTransient(ACTION_ORIGIN_TRANSIENT_NAME, origin); - return storedContext; + return threadContext.stashWithOrigin(origin); } /** * Returns a client that will always set the appropriate origin and ensure the proper context is restored by listeners + * @deprecated use {@link OriginSettingClient} instead */ + @Deprecated public static Client clientWithOrigin(Client client, String origin) { - return new ClientWithOrigin(client, origin); + return new OriginSettingClient(client, origin); } /** @@ -166,25 +173,4 @@ private static ThreadContext.StoredContext stashWithHeaders(ThreadContext thread threadContext.copyHeaders(headers.entrySet()); return storedContext; } - - private static final class ClientWithOrigin extends FilterClient { - - private final String origin; - - private ClientWithOrigin(Client in, String origin) { - super(in); - this.origin = origin; - } - - @Override - protected > void doExecute( - Action action, Request request, ActionListener listener) { - final Supplier supplier = in().threadPool().getThreadContext().newRestorableContext(false); - try (ThreadContext.StoredContext ignore = in().threadPool().getThreadContext().stashContext()) { - in().threadPool().getThreadContext().putTransient(ACTION_ORIGIN_TRANSIENT_NAME, origin); - super.doExecute(action, request, new ContextPreservingActionListener<>(supplier, listener)); - } - } - } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java index 214e80a1ac017..046cf59254957 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java @@ -118,9 +118,7 @@ private static Map initializeReservedRoles() { RoleDescriptor.IndicesPrivileges.builder() .indices(".monitoring-*").privileges("read", "read_cross_cluster").build(), RoleDescriptor.IndicesPrivileges.builder() - .indices(".management-beats").privileges("create_index", "read", "write").build(), - RoleDescriptor.IndicesPrivileges.builder() - .indices(".tasks").privileges("create_index", "read", "create").build() + .indices(".management-beats").privileges("create_index", "read", "write").build() }, null, new ConditionalClusterPrivilege[] { new ManageApplicationPrivileges(Collections.singleton("kibana-*")) }, diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ClientHelperTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ClientHelperTests.java index 95361dbff42b0..1a0a8e764124d 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ClientHelperTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ClientHelperTests.java @@ -42,32 +42,6 @@ import static org.mockito.Mockito.when; public class ClientHelperTests extends ESTestCase { - - public void testStashContext() { - final String origin = randomAlphaOfLengthBetween(4, 16); - final ThreadContext threadContext = new ThreadContext(Settings.EMPTY); - - final boolean setOtherValues = randomBoolean(); - if (setOtherValues) { - threadContext.putTransient("foo", "bar"); - threadContext.putHeader("foo", "bar"); - } - - assertNull(threadContext.getTransient(ClientHelper.ACTION_ORIGIN_TRANSIENT_NAME)); - ThreadContext.StoredContext storedContext = ClientHelper.stashWithOrigin(threadContext, origin); - assertEquals(origin, threadContext.getTransient(ClientHelper.ACTION_ORIGIN_TRANSIENT_NAME)); - assertNull(threadContext.getTransient("foo")); - assertNull(threadContext.getTransient("bar")); - - storedContext.close(); - assertNull(threadContext.getTransient(ClientHelper.ACTION_ORIGIN_TRANSIENT_NAME)); - - if (setOtherValues) { - assertEquals("bar", threadContext.getTransient("foo")); - assertEquals("bar", threadContext.getHeader("foo")); - } - } - public void testExecuteAsyncWrapsListener() throws Exception { final ThreadContext threadContext = new ThreadContext(Settings.EMPTY); final String headerName = randomAlphaOfLengthBetween(4, 16); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java index 10286c45f38ad..f73c6c52957f8 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java @@ -11,11 +11,9 @@ import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsAction; import org.elasticsearch.action.admin.cluster.state.ClusterStateAction; import org.elasticsearch.action.admin.cluster.stats.ClusterStatsAction; -import org.elasticsearch.action.admin.indices.close.CloseIndexAction; import org.elasticsearch.action.admin.indices.create.CreateIndexAction; import org.elasticsearch.action.admin.indices.delete.DeleteIndexAction; import org.elasticsearch.action.admin.indices.get.GetIndexAction; -import org.elasticsearch.action.admin.indices.mapping.put.PutMappingAction; import org.elasticsearch.action.admin.indices.recovery.RecoveryAction; import org.elasticsearch.action.admin.indices.settings.put.UpdateSettingsAction; import org.elasticsearch.action.admin.indices.template.delete.DeleteIndexTemplateAction; @@ -279,18 +277,6 @@ public void testKibanaSystemRole() { assertThat(kibanaRole.indices().allowedIndicesMatcher(MultiSearchAction.NAME).test(index), is(true)); assertThat(kibanaRole.indices().allowedIndicesMatcher(GetAction.NAME).test(index), is(true)); assertThat(kibanaRole.indices().allowedIndicesMatcher(READ_CROSS_CLUSTER_NAME).test(index), is(false)); - - // Tasks index - final String taskIndex = org.elasticsearch.tasks.TaskResultsService.TASK_INDEX; - // Things that kibana_system *should* be able to do - assertThat(kibanaRole.indices().allowedIndicesMatcher(CreateIndexAction.NAME).test(taskIndex), is(true)); - assertThat(kibanaRole.indices().allowedIndicesMatcher(PutMappingAction.NAME).test(taskIndex), is(true)); - assertThat(kibanaRole.indices().allowedIndicesMatcher(IndexAction.NAME).test(taskIndex), is(true)); - assertThat(kibanaRole.indices().allowedIndicesMatcher(GetAction.NAME).test(taskIndex), is(true)); - // Things that kibana_system *should not* be able to do - assertThat(kibanaRole.indices().allowedIndicesMatcher(DeleteIndexAction.NAME).test(taskIndex), is(false)); - assertThat(kibanaRole.indices().allowedIndicesMatcher(DeleteAction.NAME).test(taskIndex), is(false)); - assertThat(kibanaRole.indices().allowedIndicesMatcher(CloseIndexAction.NAME).test(taskIndex), is(false)); } public void testKibanaUserRole() { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationUtils.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationUtils.java index 075a9b889b041..8173aed8f5764 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationUtils.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationUtils.java @@ -23,6 +23,7 @@ import java.util.function.Consumer; import java.util.function.Predicate; +import static org.elasticsearch.action.admin.cluster.node.tasks.get.GetTaskAction.TASKS_ORIGIN; import static org.elasticsearch.xpack.core.ClientHelper.DEPRECATION_ORIGIN; import static org.elasticsearch.xpack.core.ClientHelper.INDEX_LIFECYCLE_ORIGIN; import static org.elasticsearch.xpack.core.ClientHelper.ML_ORIGIN; @@ -113,6 +114,7 @@ public static void switchUserBasedOnActionOriginAndExecute(ThreadContext threadC case PERSISTENT_TASK_ORIGIN: case ROLLUP_ORIGIN: case INDEX_LIFECYCLE_ORIGIN: + case TASKS_ORIGIN: // TODO use a more limited user for tasks securityContext.executeAsUser(XPackUser.INSTANCE, consumer, Version.CURRENT); break; default: diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationUtilsTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationUtilsTests.java index 66b1e9d9c2ad4..f1767b11c784c 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationUtilsTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationUtilsTests.java @@ -23,6 +23,7 @@ import java.util.concurrent.CountDownLatch; import java.util.function.Consumer; +import static org.elasticsearch.action.admin.cluster.node.tasks.get.GetTaskAction.TASKS_ORIGIN; import static org.hamcrest.Matchers.is; /** @@ -90,60 +91,45 @@ public void testShouldSetUser() { } public void testSwitchAndExecuteXpackSecurityUser() throws Exception { - SecurityContext securityContext = new SecurityContext(Settings.EMPTY, threadContext); - final String headerName = randomAlphaOfLengthBetween(4, 16); - final String headerValue = randomAlphaOfLengthBetween(4, 16); - final CountDownLatch latch = new CountDownLatch(2); - - final ActionListener listener = ActionListener.wrap(v -> { - assertNull(threadContext.getTransient(ClientHelper.ACTION_ORIGIN_TRANSIENT_NAME)); - assertNull(threadContext.getHeader(headerName)); - assertEquals(XPackSecurityUser.INSTANCE, securityContext.getAuthentication().getUser()); - latch.countDown(); - }, e -> fail(e.getMessage())); - - final Consumer consumer = original -> { - assertNull(threadContext.getTransient(ClientHelper.ACTION_ORIGIN_TRANSIENT_NAME)); - assertNull(threadContext.getHeader(headerName)); - assertEquals(XPackSecurityUser.INSTANCE, securityContext.getAuthentication().getUser()); - latch.countDown(); - listener.onResponse(null); - }; - threadContext.putHeader(headerName, headerValue); - threadContext.putTransient(ClientHelper.ACTION_ORIGIN_TRANSIENT_NAME, ClientHelper.SECURITY_ORIGIN); + assertSwitchBasedOnOriginAndExecute(ClientHelper.SECURITY_ORIGIN, XPackSecurityUser.INSTANCE); + } - AuthorizationUtils.switchUserBasedOnActionOriginAndExecute(threadContext, securityContext, consumer); + public void testSwitchAndExecuteXpackUser() throws Exception { + String origin = randomFrom(ClientHelper.ML_ORIGIN, ClientHelper.WATCHER_ORIGIN, ClientHelper.DEPRECATION_ORIGIN, + ClientHelper.MONITORING_ORIGIN, ClientHelper.PERSISTENT_TASK_ORIGIN, ClientHelper.INDEX_LIFECYCLE_ORIGIN); + assertSwitchBasedOnOriginAndExecute(origin, XPackUser.INSTANCE); + } - latch.await(); + public void testSwitchWithTaskOrigin() throws Exception { + assertSwitchBasedOnOriginAndExecute(TASKS_ORIGIN, XPackUser.INSTANCE); } - public void testSwitchAndExecuteXpackUser() throws Exception { + private void assertSwitchBasedOnOriginAndExecute(String origin, User user) throws Exception { SecurityContext securityContext = new SecurityContext(Settings.EMPTY, threadContext); final String headerName = randomAlphaOfLengthBetween(4, 16); final String headerValue = randomAlphaOfLengthBetween(4, 16); final CountDownLatch latch = new CountDownLatch(2); final ActionListener listener = ActionListener.wrap(v -> { - assertNull(threadContext.getTransient(ClientHelper.ACTION_ORIGIN_TRANSIENT_NAME)); + assertNull(threadContext.getTransient(ThreadContext.ACTION_ORIGIN_TRANSIENT_NAME)); assertNull(threadContext.getHeader(headerName)); - assertEquals(XPackUser.INSTANCE, securityContext.getAuthentication().getUser()); + assertEquals(user, securityContext.getAuthentication().getUser()); latch.countDown(); }, e -> fail(e.getMessage())); final Consumer consumer = original -> { - assertNull(threadContext.getTransient(ClientHelper.ACTION_ORIGIN_TRANSIENT_NAME)); + assertNull(threadContext.getTransient(ThreadContext.ACTION_ORIGIN_TRANSIENT_NAME)); assertNull(threadContext.getHeader(headerName)); - assertEquals(XPackUser.INSTANCE, securityContext.getAuthentication().getUser()); + assertEquals(user, securityContext.getAuthentication().getUser()); latch.countDown(); listener.onResponse(null); }; threadContext.putHeader(headerName, headerValue); - threadContext.putTransient(ClientHelper.ACTION_ORIGIN_TRANSIENT_NAME, - randomFrom(ClientHelper.ML_ORIGIN, ClientHelper.WATCHER_ORIGIN, ClientHelper.DEPRECATION_ORIGIN, - ClientHelper.MONITORING_ORIGIN, ClientHelper.PERSISTENT_TASK_ORIGIN, ClientHelper.INDEX_LIFECYCLE_ORIGIN)); + try (ThreadContext.StoredContext ignored = threadContext.stashWithOrigin(origin)) { + AuthorizationUtils.switchUserBasedOnActionOriginAndExecute(threadContext, securityContext, consumer); - AuthorizationUtils.switchUserBasedOnActionOriginAndExecute(threadContext, securityContext, consumer); - - latch.await(); + latch.await(); + } } + } diff --git a/x-pack/qa/reindex-tests-with-security/build.gradle b/x-pack/qa/reindex-tests-with-security/build.gradle index ea2b7d6990622..0bd51f483eaad 100644 --- a/x-pack/qa/reindex-tests-with-security/build.gradle +++ b/x-pack/qa/reindex-tests-with-security/build.gradle @@ -21,6 +21,7 @@ integTestCluster { test_admin: 'superuser', powerful_user: 'superuser', minimal_user: 'minimal', + minimal_with_task_user: 'minimal_with_task', readonly_user: 'readonly', dest_only_user: 'dest_only', can_not_see_hidden_docs_user: 'can_not_see_hidden_docs', diff --git a/x-pack/qa/reindex-tests-with-security/roles.yml b/x-pack/qa/reindex-tests-with-security/roles.yml index ce45f980670fd..f91fefaca1bcb 100644 --- a/x-pack/qa/reindex-tests-with-security/roles.yml +++ b/x-pack/qa/reindex-tests-with-security/roles.yml @@ -29,6 +29,26 @@ minimal: - create_index - indices:admin/refresh +# Search and write on both source and destination indices. It should work if you could just search on the source and +# write to the destination but that isn't how security works. +minimal_with_task: + cluster: + - cluster:monitor/main + - cluster:monitor/task/get + indices: + - names: source + privileges: + - read + - write + - create_index + - indices:admin/refresh + - names: dest + privileges: + - read + - write + - create_index + - indices:admin/refresh + # Read only operations on indices readonly: cluster: diff --git a/x-pack/qa/reindex-tests-with-security/src/test/resources/rest-api-spec/test/10_reindex.yml b/x-pack/qa/reindex-tests-with-security/src/test/resources/rest-api-spec/test/10_reindex.yml index a5779ff94d06d..ab9f56652e3b6 100644 --- a/x-pack/qa/reindex-tests-with-security/src/test/resources/rest-api-spec/test/10_reindex.yml +++ b/x-pack/qa/reindex-tests-with-security/src/test/resources/rest-api-spec/test/10_reindex.yml @@ -54,7 +54,6 @@ setup: text: test - match: { hits.total: 1 } - --- "Reindex with runas user with minimal privileges works": @@ -87,7 +86,6 @@ setup: text: test - match: { hits.total: 1 } - --- "Reindex as readonly user is forbidden": @@ -316,3 +314,50 @@ setup: index: dest dest: index: source + +--- +"Reindex wait_for_completion=false as minimal with task API": + + - do: + index: + index: source + type: foo + id: 1 + body: { "text": "test" } + - do: + indices.refresh: {} + + - do: + headers: {es-security-runas-user: minimal_with_task_user} + reindex: + refresh: true + wait_for_completion: false + body: + source: + index: source + dest: + index: dest + - set: {task: task} + + - do: + headers: {es-security-runas-user: minimal_with_task_user} + tasks.get: + wait_for_completion: true + task_id: $task + - match: {response.created: 1} + + - do: + search: + index: dest + body: + query: + match: + text: test + - match: { hits.total: 1 } + + # the minimal user doesn't have permission to read the tasks API + - do: + headers: {es-security-runas-user: minimal_user} + catch: forbidden + tasks.get: + task_id: $task From 3bc3493f7b97f15a7c4816745b4c400efcdfd08e Mon Sep 17 00:00:00 2001 From: ik Date: Thu, 29 Nov 2018 06:29:05 +0900 Subject: [PATCH 006/115] LLREST: Add PreferHasAttributeNodeSelector (#36005) `PreferHasAttributeNodeSelector` works like exactly like `HasAttributeNodeSelector` but if not nodes match the attribute then it will not filter the list of nodes. --- .../PreferHasAttributeNodeSelector.java | 105 ++++++++++++++++++ .../PreferHasAttributeNodeSelectorTests.java | 69 ++++++++++++ 2 files changed, 174 insertions(+) create mode 100644 client/rest/src/main/java/org/elasticsearch/client/PreferHasAttributeNodeSelector.java create mode 100644 client/rest/src/test/java/org/elasticsearch/client/PreferHasAttributeNodeSelectorTests.java diff --git a/client/rest/src/main/java/org/elasticsearch/client/PreferHasAttributeNodeSelector.java b/client/rest/src/main/java/org/elasticsearch/client/PreferHasAttributeNodeSelector.java new file mode 100644 index 0000000000000..e33a6936745bb --- /dev/null +++ b/client/rest/src/main/java/org/elasticsearch/client/PreferHasAttributeNodeSelector.java @@ -0,0 +1,105 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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.elasticsearch.client; + +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * Both {@link PreferHasAttributeNodeSelector} and {@link HasAttributeNodeSelector} will work the same + * if there is a {@link Node} with particular attribute in the attributes, + * but {@link PreferHasAttributeNodeSelector} will select another {@link Node}s even if there is no {@link Node} + * with particular attribute in the attributes. + */ +public final class PreferHasAttributeNodeSelector implements NodeSelector { + private final String key; + private final String value; + + public PreferHasAttributeNodeSelector(String key, String value) { + this.key = key; + this.value = value; + } + + @Override + public void select(Iterable nodes) { + boolean foundAtLeastOne = false; + + for (Node node : nodes) { + Map> attributes = node.getAttributes(); + + if (attributes == null) { + continue; + } + + List values = attributes.get(key); + + if (values == null) { + continue; + } + + if (values.contains(value)) { + foundAtLeastOne = true; + break; + } + } + + if (foundAtLeastOne) { + Iterator nodeIterator = nodes.iterator(); + while (nodeIterator.hasNext()) { + Map> attributes = nodeIterator.next().getAttributes(); + + if (attributes == null) { + continue; + } + + List values = attributes.get(key); + + if (values == null || !values.contains(value)) { + nodeIterator.remove(); + } + } + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PreferHasAttributeNodeSelector that = (PreferHasAttributeNodeSelector) o; + return Objects.equals(key, that.key) && + Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(key, value); + } + + @Override + public String toString() { + return key + "=" + value; + } +} diff --git a/client/rest/src/test/java/org/elasticsearch/client/PreferHasAttributeNodeSelectorTests.java b/client/rest/src/test/java/org/elasticsearch/client/PreferHasAttributeNodeSelectorTests.java new file mode 100644 index 0000000000000..07056b627dd98 --- /dev/null +++ b/client/rest/src/test/java/org/elasticsearch/client/PreferHasAttributeNodeSelectorTests.java @@ -0,0 +1,69 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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.elasticsearch.client; + +import org.apache.http.HttpHost; +import org.elasticsearch.client.Node.Roles; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +import static org.junit.Assert.assertEquals; + +public class PreferHasAttributeNodeSelectorTests extends RestClientTestCase { + public void testFoundPreferHasAttribute() { + Node hasAttributeValue = dummyNode(singletonMap("attr", singletonList("val"))); + Node hasAttributeButNotValue = dummyNode(singletonMap("attr", singletonList("notval"))); + Node hasAttributeValueInList = dummyNode(singletonMap("attr", Arrays.asList("val", "notval"))); + Node notHasAttribute = dummyNode(singletonMap("notattr", singletonList("val"))); + List nodes = new ArrayList<>(); + nodes.add(hasAttributeValue); + nodes.add(hasAttributeButNotValue); + nodes.add(hasAttributeValueInList); + nodes.add(notHasAttribute); + List expected = new ArrayList<>(); + expected.add(hasAttributeValue); + expected.add(hasAttributeValueInList); + new PreferHasAttributeNodeSelector("attr", "val").select(nodes); + assertEquals(expected, nodes); + } + + public void testNotFoundPreferHasAttribute() { + Node notHasAttribute = dummyNode(singletonMap("notattr", singletonList("val"))); + List nodes = new ArrayList<>(); + nodes.add(notHasAttribute); + List expected = new ArrayList<>(); + expected.add(notHasAttribute); + new PreferHasAttributeNodeSelector("attr", "val").select(nodes); + assertEquals(expected, nodes); + } + + private static Node dummyNode(Map> attributes) { + return new Node(new HttpHost("dummy"), Collections.emptySet(), + randomAsciiAlphanumOfLength(5), randomAsciiAlphanumOfLength(5), + new Roles(randomBoolean(), randomBoolean(), randomBoolean()), + attributes); + } +} From ca08f053d3fffed6aced5d4417cc1944704d2ee0 Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Wed, 28 Nov 2018 16:55:59 -0500 Subject: [PATCH 007/115] Move creation of temporary directory to Java (#36002) In the long run we want to move all of startup to a Java program. This will simplify our startup scripts and make maintenance of startup less dependent on the underlying platform that we run on. This commit moves the creation of the temporary directory off of system-dependent commands and onto a simple Java program. --- distribution/src/bin/elasticsearch-env | 10 +--- distribution/src/bin/elasticsearch-env.bat | 5 +- .../tools/launchers/Launchers.java | 10 ++++ .../tools/launchers/TempDirectory.java | 59 +++++++++++++++++++ 4 files changed, 71 insertions(+), 13 deletions(-) create mode 100644 distribution/tools/launchers/src/main/java/org/elasticsearch/tools/launchers/TempDirectory.java diff --git a/distribution/src/bin/elasticsearch-env b/distribution/src/bin/elasticsearch-env index cc16f710345e4..fc8b4a809fec4 100644 --- a/distribution/src/bin/elasticsearch-env +++ b/distribution/src/bin/elasticsearch-env @@ -81,13 +81,5 @@ ES_DISTRIBUTION_FLAVOR=${es.distribution.flavor} ES_DISTRIBUTION_TYPE=${es.distribution.type} if [ -z "$ES_TMPDIR" ]; then - set +e - mktemp --version 2>&1 | grep coreutils > /dev/null - mktemp_coreutils=$? - set -e - if [ $mktemp_coreutils -eq 0 ]; then - ES_TMPDIR=`mktemp -d --tmpdir "elasticsearch.XXXXXXXX"` - else - ES_TMPDIR=`mktemp -d -t elasticsearch` - fi + ES_TMPDIR=`"$JAVA" -cp "$ES_CLASSPATH" org.elasticsearch.tools.launchers.TempDirectory` fi diff --git a/distribution/src/bin/elasticsearch-env.bat b/distribution/src/bin/elasticsearch-env.bat index b990767092092..7c4b8dc49f47c 100644 --- a/distribution/src/bin/elasticsearch-env.bat +++ b/distribution/src/bin/elasticsearch-env.bat @@ -57,8 +57,5 @@ set ES_DISTRIBUTION_FLAVOR=${es.distribution.flavor} set ES_DISTRIBUTION_TYPE=${es.distribution.type} if not defined ES_TMPDIR ( - set ES_TMPDIR=!TMP!\elasticsearch - if not exist "!ES_TMPDIR!" ( - mkdir "!ES_TMPDIR!" - ) + for /f "tokens=* usebackq" %%a in (`"%JAVA% -cp "!ES_CLASSPATH!" "org.elasticsearch.tools.launchers.TempDirectory""`) do set ES_TMPDIR=%%a ) diff --git a/distribution/tools/launchers/src/main/java/org/elasticsearch/tools/launchers/Launchers.java b/distribution/tools/launchers/src/main/java/org/elasticsearch/tools/launchers/Launchers.java index 6c9a1ef9473c3..bb82b1a09a856 100644 --- a/distribution/tools/launchers/src/main/java/org/elasticsearch/tools/launchers/Launchers.java +++ b/distribution/tools/launchers/src/main/java/org/elasticsearch/tools/launchers/Launchers.java @@ -21,6 +21,11 @@ import org.elasticsearch.tools.java_version_checker.SuppressForbidden; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.FileAttribute; + /** * Utility methods for launchers. */ @@ -56,4 +61,9 @@ static void exit(final int status) { System.exit(status); } + @SuppressForbidden(reason = "Files#createTempDirectory(String, FileAttribute...)") + static Path createTempDirectory(final String prefix, final FileAttribute... attrs) throws IOException { + return Files.createTempDirectory(prefix, attrs); + } + } diff --git a/distribution/tools/launchers/src/main/java/org/elasticsearch/tools/launchers/TempDirectory.java b/distribution/tools/launchers/src/main/java/org/elasticsearch/tools/launchers/TempDirectory.java new file mode 100644 index 0000000000000..55f54a4e1da46 --- /dev/null +++ b/distribution/tools/launchers/src/main/java/org/elasticsearch/tools/launchers/TempDirectory.java @@ -0,0 +1,59 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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.elasticsearch.tools.launchers; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; + +/** + * Provides a path for a temporary directory. On non-Windows OS, this will be created as a sub-directory of the default temporary directory. + * Note that this causes the created temporary directory to be a private temporary directory. + */ +final class TempDirectory { + + /** + * The main entry point. The exit code is 0 if we successfully created a temporary directory as a sub-directory of the default + * temporary directory and printed the resulting path to the console. + * + * @param args the args to the program which should be empty + * @throws IOException if an I/O exception occurred while creating the temporary directory + */ + public static void main(final String[] args) throws IOException { + if (args.length != 0) { + throw new IllegalArgumentException("expected zero arguments but was " + Arrays.toString(args)); + } + /* + * On Windows, we avoid creating a unique temporary directory per invocation lest we pollute the temporary directory. On other + * operating systems, temporary directories will be cleaned automatically via various mechanisms (e.g., systemd, or restarts). + */ + final Path path; + if (System.getProperty("os.name").startsWith("Windows")) { + path = Paths.get(System.getProperty("java.io.tmpdir"), "elasticsearch"); + Files.createDirectories(path); + } else { + path = Launchers.createTempDirectory("elasticsearch-"); + } + Launchers.outPrintln(path.toString()); + } + +} From 794d5b9948de17778002a231b890b09f38e05525 Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Tue, 27 Nov 2018 15:08:53 +1100 Subject: [PATCH 008/115] Allow noop PutUser updates (#35843) When assertions are enabled, a Put User action that have no effect (a noop update) would trigger an assertion failure and shutdown the node. This change accepts "noop" as an update result, and adds more diagnostics to the assertion failure message. --- .../core/security/action/user/PutUserRequest.java | 15 +++++++++++++++ .../security/authc/esnative/NativeUsersStore.java | 4 +++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/PutUserRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/PutUserRequest.java index f39c56825168c..d09b168e9669b 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/PutUserRequest.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/PutUserRequest.java @@ -17,6 +17,7 @@ import org.elasticsearch.common.io.stream.StreamOutput; import java.io.IOException; +import java.util.Arrays; import java.util.Map; import static org.elasticsearch.action.ValidateActions.addValidationError; @@ -180,4 +181,18 @@ private static void writeCharArrayToStream(StreamOutput out, char[] chars) throw } out.writeBytesReference(charBytesRef); } + + @Override + public String toString() { + return "PutUserRequest{" + + "username='" + username + '\'' + + ", roles=" + Arrays.toString(roles) + + ", fullName='" + fullName + '\'' + + ", email='" + email + '\'' + + ", metadata=" + metadata + + ", passwordHash=" + (passwordHash == null ? "" : "") + + ", enabled=" + enabled + + ", refreshPolicy=" + refreshPolicy + + '}'; + } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStore.java index b30a588f82a34..2b76f43671b23 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStore.java @@ -351,7 +351,9 @@ private void updateUserWithoutPassword(final PutUserRequest putUserRequest, fina new ActionListener() { @Override public void onResponse(UpdateResponse updateResponse) { - assert updateResponse.getResult() == DocWriteResponse.Result.UPDATED; + assert updateResponse.getResult() == DocWriteResponse.Result.UPDATED + || updateResponse.getResult() == DocWriteResponse.Result.NOOP + : "Expected 'UPDATED' or 'NOOP' result [" + updateResponse + "] for request [" + putUserRequest + "]"; clearRealmCache(putUserRequest.username(), listener, false); } From f57e429efddaf952fc216a528fbc606f7fc15bf9 Mon Sep 17 00:00:00 2001 From: Ignacio Vera Date: Thu, 29 Nov 2018 07:52:56 +0100 Subject: [PATCH 009/115] HLRC: Add delete user action (#35294) * HLRC: Add delete user action It adds delete user action to the high level rest client. Relates #29827 --- .../elasticsearch/client/SecurityClient.java | 29 +++++++ .../client/SecurityRequestConverters.java | 12 +++ .../client/security/DeleteUserRequest.java | 71 +++++++++++++++++ .../client/security/DeleteUserResponse.java | 50 ++++++++++++ .../org/elasticsearch/client/SecurityIT.java | 16 +++- .../SecurityRequestConvertersTests.java | 13 ++++ .../SecurityDocumentationIT.java | 63 +++++++++++++++ .../security/DeleteRoleResponseTests.java | 1 + .../security/DeleteUserRequestTests.java | 78 +++++++++++++++++++ .../security/DeleteUserResponseTests.java | 43 ++++++++++ .../high-level/security/delete-user.asciidoc | 32 ++++++++ .../high-level/supported-apis.asciidoc | 2 + 12 files changed, 407 insertions(+), 3 deletions(-) create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/security/DeleteUserRequest.java create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/security/DeleteUserResponse.java create mode 100644 client/rest-high-level/src/test/java/org/elasticsearch/client/security/DeleteUserRequestTests.java create mode 100644 client/rest-high-level/src/test/java/org/elasticsearch/client/security/DeleteUserResponseTests.java create mode 100644 docs/java-rest/high-level/security/delete-user.asciidoc diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityClient.java index ab7b3e33905b7..a033ee61f79dc 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityClient.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityClient.java @@ -35,6 +35,8 @@ import org.elasticsearch.client.security.DeleteRoleMappingResponse; import org.elasticsearch.client.security.DeleteRoleRequest; import org.elasticsearch.client.security.DeleteRoleResponse; +import org.elasticsearch.client.security.DeleteUserRequest; +import org.elasticsearch.client.security.DeleteUserResponse; import org.elasticsearch.client.security.DisableUserRequest; import org.elasticsearch.client.security.EmptyResponse; import org.elasticsearch.client.security.EnableUserRequest; @@ -102,6 +104,33 @@ public void putUserAsync(PutUserRequest request, RequestOptions options, ActionL PutUserResponse::fromXContent, listener, emptySet()); } + /** + * Removes user from the native realm synchronously. + * See + * the docs for more. + * @param request the request with the user to delete + * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @return the response from the delete user call + * @throws IOException in case there is a problem sending the request or parsing back the response + */ + public DeleteUserResponse deleteUser(DeleteUserRequest request, RequestOptions options) throws IOException { + return restHighLevelClient.performRequestAndParseEntity(request, SecurityRequestConverters::deleteUser, options, + DeleteUserResponse::fromXContent, singleton(404)); + } + + /** + * Asynchronously deletes a user in the native realm. + * See + * the docs for more. + * @param request the request with the user to delete + * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @param listener the listener to be notified upon request completion + */ + public void deleteUserAsync(DeleteUserRequest request, RequestOptions options, ActionListener listener) { + restHighLevelClient.performRequestAsyncAndParseEntity(request, SecurityRequestConverters::deleteUser, options, + DeleteUserResponse::fromXContent, listener, singleton(404)); + } + /** * Create/Update a role mapping. * See diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityRequestConverters.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityRequestConverters.java index a1123de725110..6485899acf947 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityRequestConverters.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityRequestConverters.java @@ -31,6 +31,7 @@ import org.elasticsearch.client.security.GetPrivilegesRequest; import org.elasticsearch.client.security.DeleteRoleMappingRequest; import org.elasticsearch.client.security.DeleteRoleRequest; +import org.elasticsearch.client.security.DeleteUserRequest; import org.elasticsearch.client.security.InvalidateTokenRequest; import org.elasticsearch.client.security.GetRolesRequest; import org.elasticsearch.client.security.PutRoleMappingRequest; @@ -76,6 +77,17 @@ static Request putUser(PutUserRequest putUserRequest) throws IOException { return request; } + static Request deleteUser(DeleteUserRequest deleteUserRequest) { + String endpoint = new RequestConverters.EndpointBuilder() + .addPathPartAsIs("_xpack","security", "user") + .addPathPart(deleteUserRequest.getName()) + .build(); + Request request = new Request(HttpDelete.METHOD_NAME, endpoint); + RequestConverters.Params params = new RequestConverters.Params(request); + params.withRefreshPolicy(deleteUserRequest.getRefreshPolicy()); + return request; + } + static Request putRoleMapping(final PutRoleMappingRequest putRoleMappingRequest) throws IOException { final String endpoint = new RequestConverters.EndpointBuilder() .addPathPartAsIs("_xpack/security/role_mapping") diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/DeleteUserRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/DeleteUserRequest.java new file mode 100644 index 0000000000000..6995cfdfbca5e --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/DeleteUserRequest.java @@ -0,0 +1,71 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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.elasticsearch.client.security; + +import org.elasticsearch.client.Validatable; + +import java.util.Objects; + +/** + * A request to delete a user from the native realm. + */ +public final class DeleteUserRequest implements Validatable { + + private final String name; + private final RefreshPolicy refreshPolicy; + + public DeleteUserRequest(String name) { + this(name, RefreshPolicy.IMMEDIATE); + } + + public DeleteUserRequest(String name, RefreshPolicy refreshPolicy) { + this.name = Objects.requireNonNull(name, "user name is required"); + this.refreshPolicy = Objects.requireNonNull(refreshPolicy, "refresh policy is required"); + } + + public String getName() { + return name; + } + + public RefreshPolicy getRefreshPolicy() { + return refreshPolicy; + } + + @Override + public int hashCode() { + return Objects.hash(name, refreshPolicy); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final DeleteUserRequest other = (DeleteUserRequest) obj; + + return (refreshPolicy == other.refreshPolicy) && Objects.equals(name, other.name); + } +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/DeleteUserResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/DeleteUserResponse.java new file mode 100644 index 0000000000000..31306dd7478be --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/DeleteUserResponse.java @@ -0,0 +1,50 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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.elasticsearch.client.security; + +import org.elasticsearch.client.core.AcknowledgedResponse; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.XContentParser; + +import java.io.IOException; + +/** + * Response for a user being deleted from the native realm + */ +public final class DeleteUserResponse extends AcknowledgedResponse { + + private static final String PARSE_FIELD_NAME = "found"; + + private static final ConstructingObjectParser PARSER = AcknowledgedResponse + .generateParser("delete_user_response", DeleteUserResponse::new, PARSE_FIELD_NAME); + + public DeleteUserResponse(boolean acknowledged) { + super(acknowledged); + } + + public static DeleteUserResponse fromXContent(final XContentParser parser) throws IOException { + return PARSER.parse(parser, null); + } + + @Override + protected String getFieldName() { + return PARSE_FIELD_NAME; + } +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityIT.java index 5a5091fe7586d..27b1d31e6d7d5 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityIT.java @@ -22,6 +22,8 @@ import org.apache.http.client.methods.HttpDelete; import org.elasticsearch.ElasticsearchStatusException; import org.elasticsearch.client.security.AuthenticateResponse; +import org.elasticsearch.client.security.DeleteUserRequest; +import org.elasticsearch.client.security.DeleteUserResponse; import org.elasticsearch.client.security.PutUserRequest; import org.elasticsearch.client.security.PutUserResponse; import org.elasticsearch.client.security.RefreshPolicy; @@ -74,14 +76,22 @@ public void testAuthenticate() throws Exception { assertThat(authenticateResponse.enabled(), is(true)); // delete user - final Request deleteUserRequest = new Request(HttpDelete.METHOD_NAME, - "/_xpack/security/user/" + putUserRequest.getUser().getUsername()); - highLevelClient().getLowLevelClient().performRequest(deleteUserRequest); + final DeleteUserRequest deleteUserRequest = + new DeleteUserRequest(putUserRequest.getUser().getUsername(), putUserRequest.getRefreshPolicy()); + + final DeleteUserResponse deleteUserResponse = + execute(deleteUserRequest, securityClient::deleteUser, securityClient::deleteUserAsync); + assertThat(deleteUserResponse.isAcknowledged(), is(true)); // authentication no longer works ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, () -> execute(securityClient::authenticate, securityClient::authenticateAsync, authorizationRequestOptions(basicAuthHeader))); assertThat(e.getMessage(), containsString("unable to authenticate user [" + putUserRequest.getUser().getUsername() + "]")); + + // delete non-existing user + final DeleteUserResponse deleteUserResponse2 = + execute(deleteUserRequest, securityClient::deleteUser, securityClient::deleteUserAsync); + assertThat(deleteUserResponse2.isAcknowledged(), is(false)); } private static User randomUser() { diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityRequestConvertersTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityRequestConvertersTests.java index dcfa9210094de..110e0cc56c986 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityRequestConvertersTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityRequestConvertersTests.java @@ -27,6 +27,7 @@ import org.elasticsearch.client.security.DeletePrivilegesRequest; import org.elasticsearch.client.security.DeleteRoleMappingRequest; import org.elasticsearch.client.security.DeleteRoleRequest; +import org.elasticsearch.client.security.DeleteUserRequest; import org.elasticsearch.client.security.DisableUserRequest; import org.elasticsearch.client.security.EnableUserRequest; import org.elasticsearch.client.security.GetPrivilegesRequest; @@ -80,6 +81,18 @@ public void testPutUser() throws IOException { assertToXContentBody(putUserRequest, request.getEntity()); } + public void testDeleteUser() { + final String name = randomAlphaOfLengthBetween(4, 12); + final RefreshPolicy refreshPolicy = randomFrom(RefreshPolicy.values()); + final Map expectedParams = getExpectedParamsFromRefreshPolicy(refreshPolicy); + DeleteUserRequest deleteUserRequest = new DeleteUserRequest(name, refreshPolicy); + Request request = SecurityRequestConverters.deleteUser(deleteUserRequest); + assertEquals(HttpDelete.METHOD_NAME, request.getMethod()); + assertEquals("/_xpack/security/user/" + name, request.getEndpoint()); + assertEquals(expectedParams, request.getParameters()); + assertNull(request.getEntity()); + } + public void testPutRoleMapping() throws IOException { final String username = randomAlphaOfLengthBetween(4, 7); final String rolename = randomAlphaOfLengthBetween(4, 7); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java index 8d36381eeaa06..8d390f9e052f7 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java @@ -44,6 +44,8 @@ import org.elasticsearch.client.security.DeleteRoleMappingResponse; import org.elasticsearch.client.security.DeleteRoleRequest; import org.elasticsearch.client.security.DeleteRoleResponse; +import org.elasticsearch.client.security.DeleteUserRequest; +import org.elasticsearch.client.security.DeleteUserResponse; import org.elasticsearch.client.security.DisableUserRequest; import org.elasticsearch.client.security.EmptyResponse; import org.elasticsearch.client.security.EnableUserRequest; @@ -150,6 +152,67 @@ public void onFailure(Exception e) { } } + public void testDeleteUser() throws Exception { + RestHighLevelClient client = highLevelClient(); + addUser(client, "testUser", "testPassword"); + + { + // tag::delete-user-request + DeleteUserRequest deleteUserRequest = new DeleteUserRequest( + "testUser"); // <1> + // end::delete-user-request + + // tag::delete-user-execute + DeleteUserResponse deleteUserResponse = client.security().deleteUser(deleteUserRequest, RequestOptions.DEFAULT); + // end::delete-user-execute + + // tag::delete-user-response + boolean found = deleteUserResponse.isAcknowledged(); // <1> + // end::delete-user-response + assertTrue(found); + + // check if deleting the already deleted user again will give us a different response + deleteUserResponse = client.security().deleteUser(deleteUserRequest, RequestOptions.DEFAULT); + assertFalse(deleteUserResponse.isAcknowledged()); + } + + { + DeleteUserRequest deleteUserRequest = new DeleteUserRequest("testUser", RefreshPolicy.IMMEDIATE); + + ActionListener listener; + //tag::delete-user-execute-listener + listener = new ActionListener() { + @Override + public void onResponse(DeleteUserResponse deleteUserResponse) { + // <1> + } + + @Override + public void onFailure(Exception e) { + // <2> + } + }; + //end::delete-user-execute-listener + + // Replace the empty listener by a blocking listener in test + final CountDownLatch latch = new CountDownLatch(1); + listener = new LatchedActionListener<>(listener, latch); + + //tag::delete-user-execute-async + client.security().deleteUserAsync(deleteUserRequest, RequestOptions.DEFAULT, listener); // <1> + //end::delete-user-execute-async + + assertTrue(latch.await(30L, TimeUnit.SECONDS)); + } + } + + private void addUser(RestHighLevelClient client, String userName, String password) throws IOException { + User user = new User(userName, Collections.singletonList(userName)); + PutUserRequest request = new PutUserRequest(user, password.toCharArray(), true, RefreshPolicy.NONE); + PutUserResponse response = client.security().putUser(request, RequestOptions.DEFAULT); + assertTrue(response.isCreated()); + } + public void testPutRoleMapping() throws Exception { final RestHighLevelClient client = highLevelClient(); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/DeleteRoleResponseTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/DeleteRoleResponseTests.java index a2c08fcf8814e..ea16c9fad330a 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/DeleteRoleResponseTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/DeleteRoleResponseTests.java @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ + package org.elasticsearch.client.security; import org.elasticsearch.common.bytes.BytesReference; diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/DeleteUserRequestTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/DeleteUserRequestTests.java new file mode 100644 index 0000000000000..a317d41f21b69 --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/DeleteUserRequestTests.java @@ -0,0 +1,78 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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.elasticsearch.client.security; + +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.EqualsHashCodeTestUtils; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import static org.hamcrest.Matchers.equalTo; + +public class DeleteUserRequestTests extends ESTestCase { + + public void testDeleteUserRequest() { + final String name = randomAlphaOfLength(10); + final RefreshPolicy refreshPolicy = randomFrom(RefreshPolicy.values()); + final DeleteUserRequest deleteUserRequest = new DeleteUserRequest(name, refreshPolicy); + assertThat(deleteUserRequest.getName(), equalTo(name)); + assertThat(deleteUserRequest.getRefreshPolicy(), equalTo(refreshPolicy)); + } + + public void testDeleteUserRequestThrowsExceptionForNullName() { + final NullPointerException ile = + expectThrows(NullPointerException.class, () -> new DeleteUserRequest(null, randomFrom(RefreshPolicy.values()))); + assertThat(ile.getMessage(), equalTo("user name is required")); + } + + public void testDeleteUserRequestThrowsExceptionForNullRefreshPolicy() { + final NullPointerException ile = + expectThrows(NullPointerException.class, () -> new DeleteUserRequest(randomAlphaOfLength(10), null)); + assertThat(ile.getMessage(), equalTo("refresh policy is required")); + } + + public void testEqualsHashCode() { + final String name = randomAlphaOfLength(10); + final RefreshPolicy refreshPolicy = randomFrom(RefreshPolicy.values()); + final DeleteUserRequest deleteUserRequest = new DeleteUserRequest(name, refreshPolicy); + assertNotNull(deleteUserRequest); + + EqualsHashCodeTestUtils.checkEqualsAndHashCode(deleteUserRequest, (original) -> { + return new DeleteUserRequest(original.getName(), original.getRefreshPolicy()); + }); + + EqualsHashCodeTestUtils.checkEqualsAndHashCode(deleteUserRequest, (original) -> { + return new DeleteUserRequest(original.getName(), original.getRefreshPolicy()); + }, DeleteUserRequestTests::mutateTestItem); + + } + + private static DeleteUserRequest mutateTestItem(DeleteUserRequest original) { + if (randomBoolean()) { + return new DeleteUserRequest(randomAlphaOfLength(10), original.getRefreshPolicy()); + } else { + List values = Arrays.stream(RefreshPolicy.values()).filter(rp -> rp != original.getRefreshPolicy()).collect( + Collectors.toList()); + return new DeleteUserRequest(original.getName(), randomFrom(values)); + } + } +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/DeleteUserResponseTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/DeleteUserResponseTests.java new file mode 100644 index 0000000000000..e27ebad6d01a4 --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/DeleteUserResponseTests.java @@ -0,0 +1,43 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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.elasticsearch.client.security; + +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.test.ESTestCase; + +import java.io.IOException; + +public class DeleteUserResponseTests extends ESTestCase { + + public void testParsingWithMissingField() throws IOException { + XContentType contentType = randomFrom(XContentType.values()); + XContentBuilder builder = XContentFactory.contentBuilder(contentType).startObject().endObject(); + BytesReference bytes = BytesReference.bytes(builder); + XContentParser parser = XContentFactory.xContent(contentType) + .createParser(NamedXContentRegistry.EMPTY, null, bytes.streamInput()); + parser.nextToken(); + expectThrows(IllegalArgumentException.class, () -> DeleteUserResponse.fromXContent(parser)); + } +} diff --git a/docs/java-rest/high-level/security/delete-user.asciidoc b/docs/java-rest/high-level/security/delete-user.asciidoc new file mode 100644 index 0000000000000..52573bb29c74e --- /dev/null +++ b/docs/java-rest/high-level/security/delete-user.asciidoc @@ -0,0 +1,32 @@ +-- +:api: delete-user +:request: DeleteUserRequest +:response: DeleteUserResponse +-- + +[id="{upid}-{api}"] +=== Delete User API + +[id="{upid}-{api}-request"] +==== Delete User Request + +A user can be deleted as follows: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[{api}-request] +-------------------------------------------------- + +[id="{upid}-{api}-response"] +==== Delete Response + +The returned +{response}+ allows to retrieve information about the executed + operation as follows: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[{api}-response] +-------------------------------------------------- +<1> whether the given user was found + +include::../execution.asciidoc[] \ No newline at end of file diff --git a/docs/java-rest/high-level/supported-apis.asciidoc b/docs/java-rest/high-level/supported-apis.asciidoc index 0acc2bb6d5203..4798e9281cdac 100644 --- a/docs/java-rest/high-level/supported-apis.asciidoc +++ b/docs/java-rest/high-level/supported-apis.asciidoc @@ -372,6 +372,7 @@ include::rollup/get_rollup_index_caps.asciidoc[] The Java High Level REST Client supports the following Security APIs: * <> +* <<{upid}-delete-user>> * <> * <> * <> @@ -391,6 +392,7 @@ The Java High Level REST Client supports the following Security APIs: * <<{upid}-delete-privileges>> include::security/put-user.asciidoc[] +include::security/delete-user.asciidoc[] include::security/enable-user.asciidoc[] include::security/disable-user.asciidoc[] include::security/change-password.asciidoc[] From 300c61d58d3f7deaa4b19f988752e5b70b4f9df6 Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Thu, 29 Nov 2018 17:59:04 +1100 Subject: [PATCH 010/115] Add "request.id" to file audit logs (#35536) This generates a synthesized "id" for each incoming request that is included in the audit logs (file only). This id can be used to correlate events for the same request (e.g. authentication success with access granted). This request.id is specific to the audit logs and is not used for any other purpose The request.id is consistent across nodes if a single request requires execution on multiple nodes (e.g. search across multiple shards). --- .../core/src/main/config/log4j2.properties | 2 + .../IndicesAliasesRequestInterceptor.java | 4 +- .../interceptor/ResizeRequestInterceptor.java | 3 +- .../xpack/security/audit/AuditTrail.java | 43 ++- .../security/audit/AuditTrailService.java | 72 ++-- .../xpack/security/audit/AuditUtil.java | 35 ++ .../security/audit/index/IndexAuditTrail.java | 40 +-- .../logfile/DeprecatedLoggingAuditTrail.java | 36 +- .../audit/logfile/LoggingAuditTrail.java | 68 ++-- .../security/authc/AuthenticationService.java | 42 ++- .../security/authz/AuthorizationService.java | 184 +++++----- .../SecuritySearchOperationListener.java | 7 +- .../audit/AuditTrailServiceTests.java | 55 +-- .../index/IndexAuditTrailMutedTests.java | 38 +- .../audit/index/IndexAuditTrailTests.java | 37 +- .../DeprecatedLoggingAuditTrailTests.java | 97 ++--- .../logfile/LoggingAuditTrailFilterTests.java | 331 +++++++++--------- .../audit/logfile/LoggingAuditTrailTests.java | 176 ++++++---- .../authc/AuthenticationServiceTests.java | 90 +++-- .../authz/AuthorizationServiceTests.java | 172 ++++++--- .../SecuritySearchOperationListenerTests.java | 30 +- 21 files changed, 921 insertions(+), 641 deletions(-) diff --git a/x-pack/plugin/core/src/main/config/log4j2.properties b/x-pack/plugin/core/src/main/config/log4j2.properties index d7b0181f8f971..8054038a1ebb7 100644 --- a/x-pack/plugin/core/src/main/config/log4j2.properties +++ b/x-pack/plugin/core/src/main/config/log4j2.properties @@ -23,6 +23,7 @@ appender.audit_rolling.layout.pattern = {\ %varsNotEmpty{, "url.path":"%enc{%map{url.path}}{JSON}"}\ %varsNotEmpty{, "url.query":"%enc{%map{url.query}}{JSON}"}\ %varsNotEmpty{, "request.body":"%enc{%map{request.body}}{JSON}"}\ + %varsNotEmpty{, "request.id":"%enc{%map{request.id}}{JSON}"}\ %varsNotEmpty{, "action":"%enc{%map{action}}{JSON}"}\ %varsNotEmpty{, "request.name":"%enc{%map{request.name}}{JSON}"}\ %varsNotEmpty{, "indices":%map{indices}}\ @@ -50,6 +51,7 @@ appender.audit_rolling.layout.pattern = {\ # "url.path" the URI component between the port and the query string; it is percent (URL) encoded # "url.query" the URI component after the path and before the fragment; it is percent (URL) encoded # "request.body" the content of the request body entity, JSON escaped +# "request.id" a synthentic identifier for the incoming request, this is unique per incoming request, and consistent across all audit events generated by that request # "action" an action is the most granular operation that is authorized and this identifies it in a namespaced way (internal) # "request.name" if the event is in connection to a transport message this is the name of the request class, similar to how rest requests are identified by the url path (internal) # "indices" the array of indices that the "action" is acting upon diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/interceptor/IndicesAliasesRequestInterceptor.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/interceptor/IndicesAliasesRequestInterceptor.java index 88793e1d51442..3a1234d4525a7 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/interceptor/IndicesAliasesRequestInterceptor.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/interceptor/IndicesAliasesRequestInterceptor.java @@ -19,6 +19,7 @@ import org.elasticsearch.xpack.core.security.authz.permission.Role; import org.elasticsearch.xpack.core.security.support.Exceptions; import org.elasticsearch.xpack.security.audit.AuditTrailService; +import org.elasticsearch.xpack.security.audit.AuditUtil; import java.util.HashMap; import java.util.Map; @@ -70,7 +71,8 @@ public void intercept(IndicesAliasesRequest request, Authentication authenticati permissionsMap.computeIfAbsent(alias, userPermissions.indices()::allowedActionsMatcher); if (Operations.subsetOf(aliasPermissions, indexPermissions) == false) { // TODO we've already audited a access granted event so this is going to look ugly - auditTrailService.accessDenied(authentication, action, request, userPermissions.names()); + auditTrailService.accessDenied(AuditUtil.extractRequestId(threadContext), authentication, action, request, + userPermissions.names()); throw Exceptions.authorizationError("Adding an alias is not allowed when the alias " + "has more permissions than any of the indices"); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/interceptor/ResizeRequestInterceptor.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/interceptor/ResizeRequestInterceptor.java index b54d9d4335a10..183b648e8ac48 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/interceptor/ResizeRequestInterceptor.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/interceptor/ResizeRequestInterceptor.java @@ -21,6 +21,7 @@ import org.elasticsearch.xpack.core.security.authz.permission.Role; import org.elasticsearch.xpack.core.security.support.Exceptions; import org.elasticsearch.xpack.security.audit.AuditTrailService; +import static org.elasticsearch.xpack.security.audit.AuditUtil.extractRequestId; public final class ResizeRequestInterceptor extends AbstractComponent implements RequestInterceptor { @@ -60,7 +61,7 @@ public void intercept(ResizeRequest request, Authentication authentication, Role userPermissions.indices().allowedActionsMatcher(request.getTargetIndexRequest().index()); if (Operations.subsetOf(targetIndexPermissions, sourceIndexPermissions) == false) { // TODO we've already audited a access granted event so this is going to look ugly - auditTrailService.accessDenied(authentication, action, request, userPermissions.names()); + auditTrailService.accessDenied(extractRequestId(threadContext), authentication, action, request, userPermissions.names()); throw Exceptions.authorizationError("Resizing an index is not allowed when the target index " + "has more permissions than the source index"); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/AuditTrail.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/AuditTrail.java index 3f19d28192583..d85d12718862f 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/AuditTrail.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/AuditTrail.java @@ -18,43 +18,50 @@ public interface AuditTrail { String name(); - void authenticationSuccess(String realm, User user, RestRequest request); + void authenticationSuccess(String requestId, String realm, User user, RestRequest request); - void authenticationSuccess(String realm, User user, String action, TransportMessage message); + void authenticationSuccess(String requestId, String realm, User user, String action, TransportMessage message); - void anonymousAccessDenied(String action, TransportMessage message); + void anonymousAccessDenied(String requestId, String action, TransportMessage message); - void anonymousAccessDenied(RestRequest request); + void anonymousAccessDenied(String requestId, RestRequest request); - void authenticationFailed(RestRequest request); + void authenticationFailed(String requestId, RestRequest request); - void authenticationFailed(String action, TransportMessage message); + void authenticationFailed(String requestId, String action, TransportMessage message); - void authenticationFailed(AuthenticationToken token, String action, TransportMessage message); + void authenticationFailed(String requestId, AuthenticationToken token, String action, TransportMessage message); - void authenticationFailed(AuthenticationToken token, RestRequest request); + void authenticationFailed(String requestId, AuthenticationToken token, RestRequest request); - void authenticationFailed(String realm, AuthenticationToken token, String action, TransportMessage message); + void authenticationFailed(String requestId, String realm, AuthenticationToken token, String action, TransportMessage message); - void authenticationFailed(String realm, AuthenticationToken token, RestRequest request); + void authenticationFailed(String requestId, String realm, AuthenticationToken token, RestRequest request); - void accessGranted(Authentication authentication, String action, TransportMessage message, String[] roleNames); + void accessGranted(String requestId, Authentication authentication, String action, TransportMessage message, String[] roleNames); - void accessDenied(Authentication authentication, String action, TransportMessage message, String[] roleNames); + void accessDenied(String requestId, Authentication authentication, String action, TransportMessage message, String[] roleNames); - void tamperedRequest(RestRequest request); + void tamperedRequest(String requestId, RestRequest request); - void tamperedRequest(String action, TransportMessage message); + void tamperedRequest(String requestId, String action, TransportMessage message); - void tamperedRequest(User user, String action, TransportMessage request); + void tamperedRequest(String requestId, User user, String action, TransportMessage request); + /** + * The {@link #connectionGranted(InetAddress, String, SecurityIpFilterRule)} and + * {@link #connectionDenied(InetAddress, String, SecurityIpFilterRule)} methods do not have a requestId because they related to a + * potentially long-lived TCP connection, not a single request. For both Transport and Rest connections, a single connection + * granted/denied event is generated even if that connection is used for multiple Elasticsearch actions (potentially as different users) + */ void connectionGranted(InetAddress inetAddress, String profile, SecurityIpFilterRule rule); void connectionDenied(InetAddress inetAddress, String profile, SecurityIpFilterRule rule); - void runAsGranted(Authentication authentication, String action, TransportMessage message, String[] roleNames); + void runAsGranted(String requestId, Authentication authentication, String action, TransportMessage message, String[] roleNames); - void runAsDenied(Authentication authentication, String action, TransportMessage message, String[] roleNames); + void runAsDenied(String requestId, Authentication authentication, String action, TransportMessage message, String[] roleNames); + + void runAsDenied(String requestId, Authentication authentication, RestRequest request, String[] roleNames); - void runAsDenied(Authentication authentication, RestRequest request, String[] roleNames); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/AuditTrailService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/AuditTrailService.java index aa861ce68576a..60e096b895be2 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/AuditTrailService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/AuditTrailService.java @@ -39,136 +39,136 @@ public List getAuditTrails() { } @Override - public void authenticationSuccess(String realm, User user, RestRequest request) { + public void authenticationSuccess(String requestId, String realm, User user, RestRequest request) { if (licenseState.isAuditingAllowed()) { for (AuditTrail auditTrail : auditTrails) { - auditTrail.authenticationSuccess(realm, user, request); + auditTrail.authenticationSuccess(requestId, realm, user, request); } } } @Override - public void authenticationSuccess(String realm, User user, String action, TransportMessage message) { + public void authenticationSuccess(String requestId, String realm, User user, String action, TransportMessage message) { if (licenseState.isAuditingAllowed()) { for (AuditTrail auditTrail : auditTrails) { - auditTrail.authenticationSuccess(realm, user, action, message); + auditTrail.authenticationSuccess(requestId, realm, user, action, message); } } } @Override - public void anonymousAccessDenied(String action, TransportMessage message) { + public void anonymousAccessDenied(String requestId, String action, TransportMessage message) { if (licenseState.isAuditingAllowed()) { for (AuditTrail auditTrail : auditTrails) { - auditTrail.anonymousAccessDenied(action, message); + auditTrail.anonymousAccessDenied(requestId, action, message); } } } @Override - public void anonymousAccessDenied(RestRequest request) { + public void anonymousAccessDenied(String requestId, RestRequest request) { if (licenseState.isAuditingAllowed()) { for (AuditTrail auditTrail : auditTrails) { - auditTrail.anonymousAccessDenied(request); + auditTrail.anonymousAccessDenied(requestId, request); } } } @Override - public void authenticationFailed(RestRequest request) { + public void authenticationFailed(String requestId, RestRequest request) { if (licenseState.isAuditingAllowed()) { for (AuditTrail auditTrail : auditTrails) { - auditTrail.authenticationFailed(request); + auditTrail.authenticationFailed(requestId, request); } } } @Override - public void authenticationFailed(String action, TransportMessage message) { + public void authenticationFailed(String requestId, String action, TransportMessage message) { if (licenseState.isAuditingAllowed()) { for (AuditTrail auditTrail : auditTrails) { - auditTrail.authenticationFailed(action, message); + auditTrail.authenticationFailed(requestId, action, message); } } } @Override - public void authenticationFailed(AuthenticationToken token, String action, TransportMessage message) { + public void authenticationFailed(String requestId, AuthenticationToken token, String action, TransportMessage message) { if (licenseState.isAuditingAllowed()) { for (AuditTrail auditTrail : auditTrails) { - auditTrail.authenticationFailed(token, action, message); + auditTrail.authenticationFailed(requestId, token, action, message); } } } @Override - public void authenticationFailed(String realm, AuthenticationToken token, String action, TransportMessage message) { + public void authenticationFailed(String requestId, String realm, AuthenticationToken token, String action, TransportMessage message) { if (licenseState.isAuditingAllowed()) { for (AuditTrail auditTrail : auditTrails) { - auditTrail.authenticationFailed(realm, token, action, message); + auditTrail.authenticationFailed(requestId, realm, token, action, message); } } } @Override - public void authenticationFailed(AuthenticationToken token, RestRequest request) { + public void authenticationFailed(String requestId, AuthenticationToken token, RestRequest request) { if (licenseState.isAuditingAllowed()) { for (AuditTrail auditTrail : auditTrails) { - auditTrail.authenticationFailed(token, request); + auditTrail.authenticationFailed(requestId, token, request); } } } @Override - public void authenticationFailed(String realm, AuthenticationToken token, RestRequest request) { + public void authenticationFailed(String requestId, String realm, AuthenticationToken token, RestRequest request) { if (licenseState.isAuditingAllowed()) { for (AuditTrail auditTrail : auditTrails) { - auditTrail.authenticationFailed(realm, token, request); + auditTrail.authenticationFailed(requestId, realm, token, request); } } } @Override - public void accessGranted(Authentication authentication, String action, TransportMessage message, String[] roleNames) { + public void accessGranted(String requestId, Authentication authentication, String action, TransportMessage msg, String[] roleNames) { if (licenseState.isAuditingAllowed()) { for (AuditTrail auditTrail : auditTrails) { - auditTrail.accessGranted(authentication, action, message, roleNames); + auditTrail.accessGranted(requestId, authentication, action, msg, roleNames); } } } @Override - public void accessDenied(Authentication authentication, String action, TransportMessage message, String[] roleNames) { + public void accessDenied(String requestId, Authentication authentication, String action, TransportMessage message, String[] roleNames) { if (licenseState.isAuditingAllowed()) { for (AuditTrail auditTrail : auditTrails) { - auditTrail.accessDenied(authentication, action, message, roleNames); + auditTrail.accessDenied(requestId, authentication, action, message, roleNames); } } } @Override - public void tamperedRequest(RestRequest request) { + public void tamperedRequest(String requestId, RestRequest request) { if (licenseState.isAuditingAllowed()) { for (AuditTrail auditTrail : auditTrails) { - auditTrail.tamperedRequest(request); + auditTrail.tamperedRequest(requestId, request); } } } @Override - public void tamperedRequest(String action, TransportMessage message) { + public void tamperedRequest(String requestId, String action, TransportMessage message) { if (licenseState.isAuditingAllowed()) { for (AuditTrail auditTrail : auditTrails) { - auditTrail.tamperedRequest(action, message); + auditTrail.tamperedRequest(requestId, action, message); } } } @Override - public void tamperedRequest(User user, String action, TransportMessage request) { + public void tamperedRequest(String requestId, User user, String action, TransportMessage request) { if (licenseState.isAuditingAllowed()) { for (AuditTrail auditTrail : auditTrails) { - auditTrail.tamperedRequest(user, action, request); + auditTrail.tamperedRequest(requestId, user, action, request); } } } @@ -192,28 +192,28 @@ public void connectionDenied(InetAddress inetAddress, String profile, SecurityIp } @Override - public void runAsGranted(Authentication authentication, String action, TransportMessage message, String[] roleNames) { + public void runAsGranted(String requestId, Authentication authentication, String action, TransportMessage message, String[] roleNames) { if (licenseState.isAuditingAllowed()) { for (AuditTrail auditTrail : auditTrails) { - auditTrail.runAsGranted(authentication, action, message, roleNames); + auditTrail.runAsGranted(requestId, authentication, action, message, roleNames); } } } @Override - public void runAsDenied(Authentication authentication, String action, TransportMessage message, String[] roleNames) { + public void runAsDenied(String requestId, Authentication authentication, String action, TransportMessage message, String[] roleNames) { if (licenseState.isAuditingAllowed()) { for (AuditTrail auditTrail : auditTrails) { - auditTrail.runAsDenied(authentication, action, message, roleNames); + auditTrail.runAsDenied(requestId, authentication, action, message, roleNames); } } } @Override - public void runAsDenied(Authentication authentication, RestRequest request, String[] roleNames) { + public void runAsDenied(String requestId, Authentication authentication, RestRequest request, String[] roleNames) { if (licenseState.isAuditingAllowed()) { for (AuditTrail auditTrail : auditTrails) { - auditTrail.runAsDenied(authentication, request, roleNames); + auditTrail.runAsDenied(requestId, authentication, request, roleNames); } } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/AuditUtil.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/AuditUtil.java index 0d2cbc24ee12c..089972781b050 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/AuditUtil.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/AuditUtil.java @@ -6,6 +6,9 @@ package org.elasticsearch.xpack.security.audit; import org.elasticsearch.action.IndicesRequest; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.UUIDs; +import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.rest.RestRequest; import org.elasticsearch.transport.TransportMessage; @@ -17,6 +20,8 @@ public class AuditUtil { + private static final String AUDIT_REQUEST_ID = "_xpack_audit_request_id"; + public static String restRequestContent(RestRequest request) { if (request.hasContent()) { try { @@ -38,4 +43,34 @@ public static Set indices(TransportMessage message) { private static Set arrayToSetOrNull(String[] indices) { return indices == null ? null : new HashSet<>(Arrays.asList(indices)); } + + public static String generateRequestId(ThreadContext threadContext) { + return generateRequestId(threadContext, true); + } + + public static String getOrGenerateRequestId(ThreadContext threadContext) { + final String requestId = extractRequestId(threadContext); + if (Strings.isEmpty(requestId)) { + return generateRequestId(threadContext, false); + } + return requestId; + } + + private static String generateRequestId(ThreadContext threadContext, boolean checkExisting) { + if (checkExisting) { + final String existing = extractRequestId(threadContext); + if (existing != null) { + throw new IllegalStateException("Cannot generate a new audit request id - existing id [" + + existing + "] already registered"); + } + } + final String requestId = UUIDs.randomBase64UUID(); + // Store as a header (not transient) so that it is passed over the network if this request requires execution on other nodes + threadContext.putHeader(AUDIT_REQUEST_ID, requestId); + return requestId; + } + + public static String extractRequestId(ThreadContext threadContext) { + return threadContext.getHeader(AUDIT_REQUEST_ID); + } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrail.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrail.java index 6fa9ddb2b8412..689510e83aaeb 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrail.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrail.java @@ -470,7 +470,7 @@ public synchronized void stop() { } @Override - public void authenticationSuccess(String realm, User user, RestRequest request) { + public void authenticationSuccess(String requestId, String realm, User user, RestRequest request) { if (events.contains(AUTHENTICATION_SUCCESS)) { try { enqueue(message("authentication_success", new Tuple<>(realm, realm), user, null, request), "authentication_success"); @@ -481,7 +481,7 @@ public void authenticationSuccess(String realm, User user, RestRequest request) } @Override - public void authenticationSuccess(String realm, User user, String action, TransportMessage message) { + public void authenticationSuccess(String requestId, String realm, User user, String action, TransportMessage message) { if (events.contains(AUTHENTICATION_SUCCESS)) { try { enqueue(message("authentication_success", action, user, null, new Tuple<>(realm, realm), null, message), @@ -493,7 +493,7 @@ public void authenticationSuccess(String realm, User user, String action, Transp } @Override - public void anonymousAccessDenied(String action, TransportMessage message) { + public void anonymousAccessDenied(String requestId, String action, TransportMessage message) { if (events.contains(ANONYMOUS_ACCESS_DENIED)) { try { enqueue(message("anonymous_access_denied", action, (User) null, null, null, indices(message), message), @@ -505,7 +505,7 @@ public void anonymousAccessDenied(String action, TransportMessage message) { } @Override - public void anonymousAccessDenied(RestRequest request) { + public void anonymousAccessDenied(String requestId, RestRequest request) { if (events.contains(ANONYMOUS_ACCESS_DENIED)) { try { enqueue(message("anonymous_access_denied", null, null, null, null, request), "anonymous_access_denied"); @@ -516,7 +516,7 @@ public void anonymousAccessDenied(RestRequest request) { } @Override - public void authenticationFailed(String action, TransportMessage message) { + public void authenticationFailed(String requestId, String action, TransportMessage message) { if (events.contains(AUTHENTICATION_FAILED)) { try { enqueue(message("authentication_failed", action, (User) null, null, null, indices(message), message), @@ -528,7 +528,7 @@ public void authenticationFailed(String action, TransportMessage message) { } @Override - public void authenticationFailed(RestRequest request) { + public void authenticationFailed(String requestId, RestRequest request) { if (events.contains(AUTHENTICATION_FAILED)) { try { enqueue(message("authentication_failed", null, null, null, null, request), "authentication_failed"); @@ -539,7 +539,7 @@ public void authenticationFailed(RestRequest request) { } @Override - public void authenticationFailed(AuthenticationToken token, String action, TransportMessage message) { + public void authenticationFailed(String requestId, AuthenticationToken token, String action, TransportMessage message) { if (events.contains(AUTHENTICATION_FAILED)) { if (XPackUser.is(token.principal()) == false) { try { @@ -552,7 +552,7 @@ public void authenticationFailed(AuthenticationToken token, String action, Trans } @Override - public void authenticationFailed(AuthenticationToken token, RestRequest request) { + public void authenticationFailed(String requestId, AuthenticationToken token, RestRequest request) { if (events.contains(AUTHENTICATION_FAILED)) { if (XPackUser.is(token.principal()) == false) { try { @@ -565,7 +565,7 @@ public void authenticationFailed(AuthenticationToken token, RestRequest request) } @Override - public void authenticationFailed(String realm, AuthenticationToken token, String action, TransportMessage message) { + public void authenticationFailed(String requestId, String realm, AuthenticationToken token, String action, TransportMessage message) { if (events.contains(REALM_AUTHENTICATION_FAILED)) { if (XPackUser.is(token.principal()) == false) { try { @@ -579,7 +579,7 @@ public void authenticationFailed(String realm, AuthenticationToken token, String } @Override - public void authenticationFailed(String realm, AuthenticationToken token, RestRequest request) { + public void authenticationFailed(String requestId, String realm, AuthenticationToken token, RestRequest request) { if (events.contains(REALM_AUTHENTICATION_FAILED)) { if (XPackUser.is(token.principal()) == false) { try { @@ -592,7 +592,7 @@ public void authenticationFailed(String realm, AuthenticationToken token, RestRe } @Override - public void accessGranted(Authentication authentication, String action, TransportMessage message, String[] roleNames) { + public void accessGranted(String requestId, Authentication authentication, String action, TransportMessage msg, String[] roleNames) { final User user = authentication.getUser(); final boolean isSystem = SystemUser.is(user) || XPackUser.is(user); final boolean logSystemAccessGranted = isSystem && events.contains(SYSTEM_ACCESS_GRANTED); @@ -602,8 +602,8 @@ public void accessGranted(Authentication authentication, String action, Transpor assert authentication.getAuthenticatedBy() != null; final String authRealmName = authentication.getAuthenticatedBy().getName(); final String lookRealmName = authentication.getLookedUpBy() == null ? null : authentication.getLookedUpBy().getName(); - enqueue(message("access_granted", action, user, roleNames, new Tuple(authRealmName, lookRealmName), indices(message), - message), "access_granted"); + enqueue(message("access_granted", action, user, roleNames, new Tuple(authRealmName, lookRealmName), indices(msg), + msg), "access_granted"); } catch (final Exception e) { logger.warn("failed to index audit event: [access_granted]", e); } @@ -611,7 +611,7 @@ public void accessGranted(Authentication authentication, String action, Transpor } @Override - public void accessDenied(Authentication authentication, String action, TransportMessage message, String[] roleNames) { + public void accessDenied(String requestId, Authentication authentication, String action, TransportMessage message, String[] roleNames) { if (events.contains(ACCESS_DENIED) && (XPackUser.is(authentication.getUser()) == false)) { try { assert authentication.getAuthenticatedBy() != null; @@ -626,7 +626,7 @@ public void accessDenied(Authentication authentication, String action, Transport } @Override - public void tamperedRequest(RestRequest request) { + public void tamperedRequest(String requestId, RestRequest request) { if (events.contains(TAMPERED_REQUEST)) { try { enqueue(message("tampered_request", null, null, null, null, request), "tampered_request"); @@ -637,7 +637,7 @@ public void tamperedRequest(RestRequest request) { } @Override - public void tamperedRequest(String action, TransportMessage message) { + public void tamperedRequest(String requestId, String action, TransportMessage message) { if (events.contains(TAMPERED_REQUEST)) { try { enqueue(message("tampered_request", action, (User) null, null, null, indices(message), message), "tampered_request"); @@ -648,7 +648,7 @@ public void tamperedRequest(String action, TransportMessage message) { } @Override - public void tamperedRequest(User user, String action, TransportMessage request) { + public void tamperedRequest(String requestId, User user, String action, TransportMessage request) { if (events.contains(TAMPERED_REQUEST) && XPackUser.is(user) == false) { try { enqueue(message("tampered_request", action, user, null, null, indices(request), request), "tampered_request"); @@ -681,7 +681,7 @@ public void connectionDenied(InetAddress inetAddress, String profile, SecurityIp } @Override - public void runAsGranted(Authentication authentication, String action, TransportMessage message, String[] roleNames) { + public void runAsGranted(String requestId, Authentication authentication, String action, TransportMessage message, String[] roleNames) { if (events.contains(RUN_AS_GRANTED)) { try { assert authentication.getAuthenticatedBy() != null; @@ -696,7 +696,7 @@ public void runAsGranted(Authentication authentication, String action, Transport } @Override - public void runAsDenied(Authentication authentication, String action, TransportMessage message, String[] roleNames) { + public void runAsDenied(String requestId, Authentication authentication, String action, TransportMessage message, String[] roleNames) { if (events.contains(RUN_AS_DENIED)) { try { assert authentication.getAuthenticatedBy() != null; @@ -711,7 +711,7 @@ public void runAsDenied(Authentication authentication, String action, TransportM } @Override - public void runAsDenied(Authentication authentication, RestRequest request, String[] roleNames) { + public void runAsDenied(String requestId, Authentication authentication, RestRequest request, String[] roleNames) { if (events.contains(RUN_AS_DENIED)) { try { assert authentication.getAuthenticatedBy() != null; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/logfile/DeprecatedLoggingAuditTrail.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/logfile/DeprecatedLoggingAuditTrail.java index 0880f10bd3789..c7a4d1964d35d 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/logfile/DeprecatedLoggingAuditTrail.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/logfile/DeprecatedLoggingAuditTrail.java @@ -132,7 +132,7 @@ public DeprecatedLoggingAuditTrail(Settings settings, ClusterService clusterServ } @Override - public void authenticationSuccess(String realm, User user, RestRequest request) { + public void authenticationSuccess(String requestId, String realm, User user, RestRequest request) { if (events.contains(AUTHENTICATION_SUCCESS) && (eventFilterPolicyRegistry.ignorePredicate() .test(new AuditEventMetaInfo(Optional.of(user), Optional.of(realm), Optional.empty(), Optional.empty())) == false)) { if (includeRequestBody) { @@ -147,7 +147,7 @@ localNodeInfo.prefix, principal(user), realm, request.uri(), request.params(), o } @Override - public void authenticationSuccess(String realm, User user, String action, TransportMessage message) { + public void authenticationSuccess(String requestId, String realm, User user, String action, TransportMessage message) { if (events.contains(AUTHENTICATION_SUCCESS)) { final Optional indices = indices(message); if (eventFilterPolicyRegistry.ignorePredicate() @@ -167,7 +167,7 @@ localNodeInfo.prefix, originAttributes(threadContext, message, localNodeInfo), p } @Override - public void anonymousAccessDenied(String action, TransportMessage message) { + public void anonymousAccessDenied(String requestId, String action, TransportMessage message) { if (events.contains(ANONYMOUS_ACCESS_DENIED)) { final Optional indices = indices(message); if (eventFilterPolicyRegistry.ignorePredicate() @@ -187,7 +187,7 @@ localNodeInfo.prefix, originAttributes(threadContext, message, localNodeInfo), a } @Override - public void anonymousAccessDenied(RestRequest request) { + public void anonymousAccessDenied(String requestId, RestRequest request) { if (events.contains(ANONYMOUS_ACCESS_DENIED) && (eventFilterPolicyRegistry.ignorePredicate().test(AuditEventMetaInfo.EMPTY) == false)) { if (includeRequestBody) { @@ -201,7 +201,7 @@ public void anonymousAccessDenied(RestRequest request) { } @Override - public void authenticationFailed(AuthenticationToken token, String action, TransportMessage message) { + public void authenticationFailed(String requestId, AuthenticationToken token, String action, TransportMessage message) { if (events.contains(AUTHENTICATION_FAILED)) { final Optional indices = indices(message); if (eventFilterPolicyRegistry.ignorePredicate() @@ -221,7 +221,7 @@ localNodeInfo.prefix, originAttributes(threadContext, message, localNodeInfo), t } @Override - public void authenticationFailed(RestRequest request) { + public void authenticationFailed(String requestId, RestRequest request) { if (events.contains(AUTHENTICATION_FAILED) && (eventFilterPolicyRegistry.ignorePredicate().test(AuditEventMetaInfo.EMPTY) == false)) { if (includeRequestBody) { @@ -235,7 +235,7 @@ public void authenticationFailed(RestRequest request) { } @Override - public void authenticationFailed(String action, TransportMessage message) { + public void authenticationFailed(String requestId, String action, TransportMessage message) { if (events.contains(AUTHENTICATION_FAILED)) { final Optional indices = indices(message); if (eventFilterPolicyRegistry.ignorePredicate() @@ -255,7 +255,7 @@ localNodeInfo.prefix, originAttributes(threadContext, message, localNodeInfo), a } @Override - public void authenticationFailed(AuthenticationToken token, RestRequest request) { + public void authenticationFailed(String requestId, AuthenticationToken token, RestRequest request) { if (events.contains(AUTHENTICATION_FAILED) && (eventFilterPolicyRegistry.ignorePredicate() .test(new AuditEventMetaInfo(Optional.of(token), Optional.empty(), Optional.empty())) == false)) { if (includeRequestBody) { @@ -269,7 +269,7 @@ public void authenticationFailed(AuthenticationToken token, RestRequest request) } @Override - public void authenticationFailed(String realm, AuthenticationToken token, String action, TransportMessage message) { + public void authenticationFailed(String requestId, String realm, AuthenticationToken token, String action, TransportMessage message) { if (events.contains(REALM_AUTHENTICATION_FAILED)) { final Optional indices = indices(message); if (eventFilterPolicyRegistry.ignorePredicate() @@ -291,7 +291,7 @@ localNodeInfo.prefix, realm, originAttributes(threadContext, message, localNodeI } @Override - public void authenticationFailed(String realm, AuthenticationToken token, RestRequest request) { + public void authenticationFailed(String requestId, String realm, AuthenticationToken token, RestRequest request) { if (events.contains(REALM_AUTHENTICATION_FAILED) && (eventFilterPolicyRegistry.ignorePredicate() .test(new AuditEventMetaInfo(Optional.of(token), Optional.of(realm), Optional.empty())) == false)) { if (includeRequestBody) { @@ -306,7 +306,7 @@ localNodeInfo.prefix, realm, hostAttributes(request), token.principal(), request } @Override - public void accessGranted(Authentication authentication, String action, TransportMessage message, String[] roleNames) { + public void accessGranted(String reqId, Authentication authentication, String action, TransportMessage message, String[] roleNames) { final User user = authentication.getUser(); final boolean isSystem = SystemUser.is(user) || XPackUser.is(user); if ((isSystem && events.contains(SYSTEM_ACCESS_GRANTED)) || ((isSystem == false) && events.contains(ACCESS_GRANTED))) { @@ -329,7 +329,7 @@ localNodeInfo.prefix, originAttributes(threadContext, message, localNodeInfo), s } @Override - public void accessDenied(Authentication authentication, String action, TransportMessage message, String[] roleNames) { + public void accessDenied(String requestId, Authentication authentication, String action, TransportMessage message, String[] roleNames) { if (events.contains(ACCESS_DENIED)) { final Optional indices = indices(message); if (eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.of(authentication.getUser()), @@ -350,7 +350,7 @@ localNodeInfo.prefix, originAttributes(threadContext, message, localNodeInfo), s } @Override - public void tamperedRequest(RestRequest request) { + public void tamperedRequest(String requestId, RestRequest request) { if (events.contains(TAMPERED_REQUEST) && (eventFilterPolicyRegistry.ignorePredicate().test(AuditEventMetaInfo.EMPTY) == false)) { if (includeRequestBody) { logger.info("{}[rest] [tampered_request]\t{}, uri=[{}]{}, request_body=[{}]", localNodeInfo.prefix, hostAttributes(request), @@ -363,7 +363,7 @@ public void tamperedRequest(RestRequest request) { } @Override - public void tamperedRequest(String action, TransportMessage message) { + public void tamperedRequest(String requestId, String action, TransportMessage message) { if (events.contains(TAMPERED_REQUEST)) { final Optional indices = indices(message); if (eventFilterPolicyRegistry.ignorePredicate() @@ -383,7 +383,7 @@ public void tamperedRequest(String action, TransportMessage message) { } @Override - public void tamperedRequest(User user, String action, TransportMessage request) { + public void tamperedRequest(String requestId, User user, String action, TransportMessage request) { if (events.contains(TAMPERED_REQUEST)) { final Optional indices = indices(request); if (eventFilterPolicyRegistry.ignorePredicate() @@ -419,7 +419,7 @@ public void connectionDenied(InetAddress inetAddress, String profile, SecurityIp } @Override - public void runAsGranted(Authentication authentication, String action, TransportMessage message, String[] roleNames) { + public void runAsGranted(String requestId, Authentication authentication, String action, TransportMessage message, String[] roleNames) { if (events.contains(RUN_AS_GRANTED)) { final Optional indices = indices(message); if (eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.of(authentication.getUser()), @@ -440,7 +440,7 @@ localNodeInfo.prefix, originAttributes(threadContext, message, localNodeInfo), r } @Override - public void runAsDenied(Authentication authentication, String action, TransportMessage message, String[] roleNames) { + public void runAsDenied(String requestId, Authentication authentication, String action, TransportMessage message, String[] roleNames) { if (events.contains(RUN_AS_DENIED)) { final Optional indices = indices(message); if (eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.of(authentication.getUser()), @@ -461,7 +461,7 @@ localNodeInfo.prefix, originAttributes(threadContext, message, localNodeInfo), r } @Override - public void runAsDenied(Authentication authentication, RestRequest request, String[] roleNames) { + public void runAsDenied(String requestId, Authentication authentication, RestRequest request, String[] roleNames) { if (events.contains(RUN_AS_DENIED) && (eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.of(authentication.getUser()), Optional.of(effectiveRealmName(authentication)), Optional.of(roleNames), Optional.empty())) == false)) { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrail.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrail.java index 6de4e34f9264d..d8862dcf17daa 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrail.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrail.java @@ -102,6 +102,7 @@ public class LoggingAuditTrail extends AbstractComponent implements AuditTrail, public static final String URL_PATH_FIELD_NAME = "url.path"; public static final String URL_QUERY_FIELD_NAME = "url.query"; public static final String REQUEST_BODY_FIELD_NAME = "request.body"; + public static final String REQUEST_ID_FIELD_NAME = "request.id"; public static final String ACTION_FIELD_NAME = "action"; public static final String INDICES_FIELD_NAME = "indices"; public static final String REQUEST_NAME_FIELD_NAME = "request.name"; @@ -209,7 +210,7 @@ public LoggingAuditTrail(Settings settings, ClusterService clusterService, Threa } @Override - public void authenticationSuccess(String realm, User user, RestRequest request) { + public void authenticationSuccess(String requestId, String realm, User user, RestRequest request) { if (events.contains(AUTHENTICATION_SUCCESS) && eventFilterPolicyRegistry.ignorePredicate() .test(new AuditEventMetaInfo(Optional.of(user), Optional.of(realm), Optional.empty(), Optional.empty())) == false) { final StringMapMessage logEntry = new LogEntryBuilder() @@ -217,6 +218,7 @@ public void authenticationSuccess(String realm, User user, RestRequest request) .with(EVENT_ACTION_FIELD_NAME, "authentication_success") .with(REALM_FIELD_NAME, realm) .withRestUri(request) + .withRequestId(requestId) .withPrincipal(user) .withRestOrigin(request) .withRequestBody(request) @@ -227,7 +229,7 @@ public void authenticationSuccess(String realm, User user, RestRequest request) } @Override - public void authenticationSuccess(String realm, User user, String action, TransportMessage message) { + public void authenticationSuccess(String requestId, String realm, User user, String action, TransportMessage message) { if (events.contains(AUTHENTICATION_SUCCESS)) { final Optional indices = indices(message); if (eventFilterPolicyRegistry.ignorePredicate() @@ -238,6 +240,7 @@ public void authenticationSuccess(String realm, User user, String action, Transp .with(REALM_FIELD_NAME, realm) .with(ACTION_FIELD_NAME, action) .with(REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()) + .withRequestId(requestId) .withPrincipal(user) .withRestOrTransportOrigin(message, threadContext) .with(INDICES_FIELD_NAME, indices.orElse(null)) @@ -249,7 +252,7 @@ public void authenticationSuccess(String realm, User user, String action, Transp } @Override - public void anonymousAccessDenied(String action, TransportMessage message) { + public void anonymousAccessDenied(String requestId, String action, TransportMessage message) { if (events.contains(ANONYMOUS_ACCESS_DENIED)) { final Optional indices = indices(message); if (eventFilterPolicyRegistry.ignorePredicate() @@ -259,6 +262,7 @@ public void anonymousAccessDenied(String action, TransportMessage message) { .with(EVENT_ACTION_FIELD_NAME, "anonymous_access_denied") .with(ACTION_FIELD_NAME, action) .with(REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()) + .withRequestId(requestId) .withRestOrTransportOrigin(message, threadContext) .with(INDICES_FIELD_NAME, indices.orElse(null)) .withOpaqueId(threadContext) @@ -269,7 +273,7 @@ public void anonymousAccessDenied(String action, TransportMessage message) { } @Override - public void anonymousAccessDenied(RestRequest request) { + public void anonymousAccessDenied(String requestId, RestRequest request) { if (events.contains(ANONYMOUS_ACCESS_DENIED) && eventFilterPolicyRegistry.ignorePredicate().test(AuditEventMetaInfo.EMPTY) == false) { final StringMapMessage logEntry = new LogEntryBuilder() @@ -278,6 +282,7 @@ public void anonymousAccessDenied(RestRequest request) { .withRestUri(request) .withRestOrigin(request) .withRequestBody(request) + .withRequestId(requestId) .withOpaqueId(threadContext) .build(); logger.info(logEntry); @@ -285,7 +290,7 @@ public void anonymousAccessDenied(RestRequest request) { } @Override - public void authenticationFailed(AuthenticationToken token, String action, TransportMessage message) { + public void authenticationFailed(String requestId, AuthenticationToken token, String action, TransportMessage message) { if (events.contains(AUTHENTICATION_FAILED)) { final Optional indices = indices(message); if (eventFilterPolicyRegistry.ignorePredicate() @@ -296,6 +301,7 @@ public void authenticationFailed(AuthenticationToken token, String action, Trans .with(ACTION_FIELD_NAME, action) .with(PRINCIPAL_FIELD_NAME, token.principal()) .with(REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()) + .withRequestId(requestId) .withRestOrTransportOrigin(message, threadContext) .with(INDICES_FIELD_NAME, indices.orElse(null)) .withOpaqueId(threadContext) @@ -306,7 +312,7 @@ public void authenticationFailed(AuthenticationToken token, String action, Trans } @Override - public void authenticationFailed(RestRequest request) { + public void authenticationFailed(String requestId, RestRequest request) { if (events.contains(AUTHENTICATION_FAILED) && eventFilterPolicyRegistry.ignorePredicate().test(AuditEventMetaInfo.EMPTY) == false) { final StringMapMessage logEntry = new LogEntryBuilder() .with(EVENT_TYPE_FIELD_NAME, REST_ORIGIN_FIELD_VALUE) @@ -314,6 +320,7 @@ public void authenticationFailed(RestRequest request) { .withRestUri(request) .withRestOrigin(request) .withRequestBody(request) + .withRequestId(requestId) .withOpaqueId(threadContext) .build(); logger.info(logEntry); @@ -321,7 +328,7 @@ public void authenticationFailed(RestRequest request) { } @Override - public void authenticationFailed(String action, TransportMessage message) { + public void authenticationFailed(String requestId, String action, TransportMessage message) { if (events.contains(AUTHENTICATION_FAILED)) { final Optional indices = indices(message); if (eventFilterPolicyRegistry.ignorePredicate() @@ -331,6 +338,7 @@ public void authenticationFailed(String action, TransportMessage message) { .with(EVENT_ACTION_FIELD_NAME, "authentication_failed") .with(ACTION_FIELD_NAME, action) .with(REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()) + .withRequestId(requestId) .withRestOrTransportOrigin(message, threadContext) .with(INDICES_FIELD_NAME, indices.orElse(null)) .withOpaqueId(threadContext) @@ -341,7 +349,7 @@ public void authenticationFailed(String action, TransportMessage message) { } @Override - public void authenticationFailed(AuthenticationToken token, RestRequest request) { + public void authenticationFailed(String requestId, AuthenticationToken token, RestRequest request) { if (events.contains(AUTHENTICATION_FAILED) && eventFilterPolicyRegistry.ignorePredicate() .test(new AuditEventMetaInfo(Optional.of(token), Optional.empty(), Optional.empty())) == false) { final StringMapMessage logEntry = new LogEntryBuilder() @@ -351,6 +359,7 @@ public void authenticationFailed(AuthenticationToken token, RestRequest request) .withRestUri(request) .withRestOrigin(request) .withRequestBody(request) + .withRequestId(requestId) .withOpaqueId(threadContext) .build(); logger.info(logEntry); @@ -358,7 +367,7 @@ public void authenticationFailed(AuthenticationToken token, RestRequest request) } @Override - public void authenticationFailed(String realm, AuthenticationToken token, String action, TransportMessage message) { + public void authenticationFailed(String requestId, String realm, AuthenticationToken token, String action, TransportMessage message) { if (events.contains(REALM_AUTHENTICATION_FAILED)) { final Optional indices = indices(message); if (eventFilterPolicyRegistry.ignorePredicate() @@ -370,6 +379,7 @@ public void authenticationFailed(String realm, AuthenticationToken token, String .with(PRINCIPAL_FIELD_NAME, token.principal()) .with(ACTION_FIELD_NAME, action) .with(REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()) + .withRequestId(requestId) .withRestOrTransportOrigin(message, threadContext) .with(INDICES_FIELD_NAME, indices.orElse(null)) .withOpaqueId(threadContext) @@ -380,7 +390,7 @@ public void authenticationFailed(String realm, AuthenticationToken token, String } @Override - public void authenticationFailed(String realm, AuthenticationToken token, RestRequest request) { + public void authenticationFailed(String requestId, String realm, AuthenticationToken token, RestRequest request) { if (events.contains(REALM_AUTHENTICATION_FAILED) && eventFilterPolicyRegistry.ignorePredicate() .test(new AuditEventMetaInfo(Optional.of(token), Optional.of(realm), Optional.empty())) == false) { final StringMapMessage logEntry = new LogEntryBuilder() @@ -391,6 +401,7 @@ public void authenticationFailed(String realm, AuthenticationToken token, RestRe .withRestUri(request) .withRestOrigin(request) .withRequestBody(request) + .withRequestId(requestId) .withOpaqueId(threadContext) .build(); logger.info(logEntry); @@ -398,20 +409,21 @@ public void authenticationFailed(String realm, AuthenticationToken token, RestRe } @Override - public void accessGranted(Authentication authentication, String action, TransportMessage message, String[] roleNames) { + public void accessGranted(String requestId, Authentication authentication, String action, TransportMessage msg, String[] roleNames) { final User user = authentication.getUser(); final boolean isSystem = SystemUser.is(user) || XPackUser.is(user); if ((isSystem && events.contains(SYSTEM_ACCESS_GRANTED)) || ((isSystem == false) && events.contains(ACCESS_GRANTED))) { - final Optional indices = indices(message); + final Optional indices = indices(msg); if (eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.of(user), Optional.of(effectiveRealmName(authentication)), Optional.of(roleNames), indices)) == false) { final StringMapMessage logEntry = new LogEntryBuilder() .with(EVENT_TYPE_FIELD_NAME, TRANSPORT_ORIGIN_FIELD_VALUE) .with(EVENT_ACTION_FIELD_NAME, "access_granted") .with(ACTION_FIELD_NAME, action) - .with(REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()) + .with(REQUEST_NAME_FIELD_NAME, msg.getClass().getSimpleName()) + .withRequestId(requestId) .withSubject(authentication) - .withRestOrTransportOrigin(message, threadContext) + .withRestOrTransportOrigin(msg, threadContext) .with(INDICES_FIELD_NAME, indices.orElse(null)) .with(PRINCIPAL_ROLES_FIELD_NAME, roleNames) .withOpaqueId(threadContext) @@ -422,7 +434,7 @@ public void accessGranted(Authentication authentication, String action, Transpor } @Override - public void accessDenied(Authentication authentication, String action, TransportMessage message, String[] roleNames) { + public void accessDenied(String requestId, Authentication authentication, String action, TransportMessage message, String[] roleNames) { if (events.contains(ACCESS_DENIED)) { final Optional indices = indices(message); if (eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.of(authentication.getUser()), @@ -432,6 +444,7 @@ public void accessDenied(Authentication authentication, String action, Transport .with(EVENT_ACTION_FIELD_NAME, "access_denied") .with(ACTION_FIELD_NAME, action) .with(REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()) + .withRequestId(requestId) .withSubject(authentication) .withRestOrTransportOrigin(message, threadContext) .with(INDICES_FIELD_NAME, indices.orElse(null)) @@ -444,7 +457,7 @@ public void accessDenied(Authentication authentication, String action, Transport } @Override - public void tamperedRequest(RestRequest request) { + public void tamperedRequest(String requestId, RestRequest request) { if (events.contains(TAMPERED_REQUEST) && eventFilterPolicyRegistry.ignorePredicate().test(AuditEventMetaInfo.EMPTY) == false) { final StringMapMessage logEntry = new LogEntryBuilder() .with(EVENT_TYPE_FIELD_NAME, REST_ORIGIN_FIELD_VALUE) @@ -452,6 +465,7 @@ public void tamperedRequest(RestRequest request) { .withRestUri(request) .withRestOrigin(request) .withRequestBody(request) + .withRequestId(requestId) .withOpaqueId(threadContext) .build(); logger.info(logEntry); @@ -459,7 +473,7 @@ public void tamperedRequest(RestRequest request) { } @Override - public void tamperedRequest(String action, TransportMessage message) { + public void tamperedRequest(String requestId, String action, TransportMessage message) { if (events.contains(TAMPERED_REQUEST)) { final Optional indices = indices(message); if (eventFilterPolicyRegistry.ignorePredicate() @@ -469,6 +483,7 @@ public void tamperedRequest(String action, TransportMessage message) { .with(EVENT_ACTION_FIELD_NAME, "tampered_request") .with(ACTION_FIELD_NAME, action) .with(REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()) + .withRequestId(requestId) .withRestOrTransportOrigin(message, threadContext) .with(INDICES_FIELD_NAME, indices.orElse(null)) .withOpaqueId(threadContext) @@ -479,7 +494,7 @@ public void tamperedRequest(String action, TransportMessage message) { } @Override - public void tamperedRequest(User user, String action, TransportMessage message) { + public void tamperedRequest(String requestId, User user, String action, TransportMessage message) { if (events.contains(TAMPERED_REQUEST)) { final Optional indices = indices(message); if (eventFilterPolicyRegistry.ignorePredicate() @@ -489,6 +504,7 @@ public void tamperedRequest(User user, String action, TransportMessage message) .with(EVENT_ACTION_FIELD_NAME, "tampered_request") .with(ACTION_FIELD_NAME, action) .with(REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()) + .withRequestId(requestId) .withRestOrTransportOrigin(message, threadContext) .withPrincipal(user) .with(INDICES_FIELD_NAME, indices.orElse(null)) @@ -532,7 +548,7 @@ public void connectionDenied(InetAddress inetAddress, String profile, SecurityIp } @Override - public void runAsGranted(Authentication authentication, String action, TransportMessage message, String[] roleNames) { + public void runAsGranted(String requestId, Authentication authentication, String action, TransportMessage message, String[] roleNames) { if (events.contains(RUN_AS_GRANTED)) { final Optional indices = indices(message); if (eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.of(authentication.getUser()), @@ -542,6 +558,7 @@ public void runAsGranted(Authentication authentication, String action, Transport .with(EVENT_ACTION_FIELD_NAME, "run_as_granted") .with(ACTION_FIELD_NAME, action) .with(REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()) + .withRequestId(requestId) .withRunAsSubject(authentication) .withRestOrTransportOrigin(message, threadContext) .with(INDICES_FIELD_NAME, indices.orElse(null)) @@ -554,7 +571,7 @@ public void runAsGranted(Authentication authentication, String action, Transport } @Override - public void runAsDenied(Authentication authentication, String action, TransportMessage message, String[] roleNames) { + public void runAsDenied(String requestId, Authentication authentication, String action, TransportMessage message, String[] roleNames) { if (events.contains(RUN_AS_DENIED)) { final Optional indices = indices(message); if (eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.of(authentication.getUser()), @@ -564,6 +581,7 @@ public void runAsDenied(Authentication authentication, String action, TransportM .with(EVENT_ACTION_FIELD_NAME, "run_as_denied") .with(ACTION_FIELD_NAME, action) .with(REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()) + .withRequestId(requestId) .withRunAsSubject(authentication) .withRestOrTransportOrigin(message, threadContext) .with(INDICES_FIELD_NAME, indices.orElse(null)) @@ -576,7 +594,7 @@ public void runAsDenied(Authentication authentication, String action, TransportM } @Override - public void runAsDenied(Authentication authentication, RestRequest request, String[] roleNames) { + public void runAsDenied(String requestId, Authentication authentication, RestRequest request, String[] roleNames) { if (events.contains(RUN_AS_DENIED) && eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.of(authentication.getUser()), Optional.of(effectiveRealmName(authentication)), Optional.of(roleNames), Optional.empty())) == false) { @@ -588,6 +606,7 @@ public void runAsDenied(Authentication authentication, RestRequest request, Stri .withRunAsSubject(authentication) .withRestOrigin(request) .withRequestBody(request) + .withRequestId(requestId) .withOpaqueId(threadContext) .build(); logger.info(logEntry); @@ -673,6 +692,13 @@ LogEntryBuilder withRequestBody(RestRequest request) { return this; } + LogEntryBuilder withRequestId(String requestId) { + if (requestId != null) { + logEntry.with(REQUEST_ID_FIELD_NAME, requestId); + } + return this; + } + LogEntryBuilder withOpaqueId(ThreadContext threadContext) { final String opaqueId = threadContext.getHeader(Task.X_OPAQUE_ID); if (opaqueId != null) { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java index b9c8d542a6004..e9756925ce6de 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java @@ -35,6 +35,7 @@ import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.audit.AuditTrail; import org.elasticsearch.xpack.security.audit.AuditTrailService; +import org.elasticsearch.xpack.security.audit.AuditUtil; import org.elasticsearch.xpack.security.authc.support.RealmUserLookup; import java.util.LinkedHashMap; @@ -146,7 +147,8 @@ class Authenticator { } Authenticator(String action, TransportMessage message, User fallbackUser, ActionListener listener) { - this(new AuditableTransportRequest(auditTrail, failureHandler, threadContext, action, message), fallbackUser, listener); + this(new AuditableTransportRequest(auditTrail, failureHandler, threadContext, action, message + ), fallbackUser, listener); } private Authenticator(AuditableRequest auditableRequest, User fallbackUser, ActionListener listener) { @@ -470,55 +472,58 @@ static class AuditableTransportRequest extends AuditableRequest { private final String action; private final TransportMessage message; + private final String requestId; AuditableTransportRequest(AuditTrail auditTrail, AuthenticationFailureHandler failureHandler, ThreadContext threadContext, String action, TransportMessage message) { super(auditTrail, failureHandler, threadContext); this.action = action; this.message = message; + // There might be an existing audit-id (e.g. generated by the rest request) but there might not be (e.g. an internal action) + this.requestId = AuditUtil.getOrGenerateRequestId(threadContext); } @Override void authenticationSuccess(String realm, User user) { - auditTrail.authenticationSuccess(realm, user, action, message); + auditTrail.authenticationSuccess(requestId, realm, user, action, message); } @Override void realmAuthenticationFailed(AuthenticationToken token, String realm) { - auditTrail.authenticationFailed(realm, token, action, message); + auditTrail.authenticationFailed(requestId, realm, token, action, message); } @Override ElasticsearchSecurityException tamperedRequest() { - auditTrail.tamperedRequest(action, message); + auditTrail.tamperedRequest(requestId, action, message); return new ElasticsearchSecurityException("failed to verify signed authentication information"); } @Override ElasticsearchSecurityException exceptionProcessingRequest(Exception e, @Nullable AuthenticationToken token) { if (token != null) { - auditTrail.authenticationFailed(token, action, message); + auditTrail.authenticationFailed(requestId, token, action, message); } else { - auditTrail.authenticationFailed(action, message); + auditTrail.authenticationFailed(requestId, action, message); } return failureHandler.exceptionProcessingRequest(message, action, e, threadContext); } @Override ElasticsearchSecurityException authenticationFailed(AuthenticationToken token) { - auditTrail.authenticationFailed(token, action, message); + auditTrail.authenticationFailed(requestId, token, action, message); return failureHandler.failedAuthentication(message, token, action, threadContext); } @Override ElasticsearchSecurityException anonymousAccessDenied() { - auditTrail.anonymousAccessDenied(action, message); + auditTrail.anonymousAccessDenied(requestId, action, message); return failureHandler.missingToken(message, action, threadContext); } @Override ElasticsearchSecurityException runAsDenied(Authentication authentication, AuthenticationToken token) { - auditTrail.runAsDenied(authentication, action, message, Role.EMPTY.names()); + auditTrail.runAsDenied(requestId, authentication, action, message, Role.EMPTY.names()); return failureHandler.failedAuthentication(message, token, action, threadContext); } @@ -532,55 +537,58 @@ public String toString() { static class AuditableRestRequest extends AuditableRequest { private final RestRequest request; + private final String requestId; @SuppressWarnings("unchecked") AuditableRestRequest(AuditTrail auditTrail, AuthenticationFailureHandler failureHandler, ThreadContext threadContext, RestRequest request) { super(auditTrail, failureHandler, threadContext); this.request = request; + // There should never be an existing audit-id when processing a rest request. + this.requestId = AuditUtil.generateRequestId(threadContext); } @Override void authenticationSuccess(String realm, User user) { - auditTrail.authenticationSuccess(realm, user, request); + auditTrail.authenticationSuccess(requestId, realm, user, request); } @Override void realmAuthenticationFailed(AuthenticationToken token, String realm) { - auditTrail.authenticationFailed(realm, token, request); + auditTrail.authenticationFailed(requestId, realm, token, request); } @Override ElasticsearchSecurityException tamperedRequest() { - auditTrail.tamperedRequest(request); + auditTrail.tamperedRequest(requestId, request); return new ElasticsearchSecurityException("rest request attempted to inject a user"); } @Override ElasticsearchSecurityException exceptionProcessingRequest(Exception e, @Nullable AuthenticationToken token) { if (token != null) { - auditTrail.authenticationFailed(token, request); + auditTrail.authenticationFailed(requestId, token, request); } else { - auditTrail.authenticationFailed(request); + auditTrail.authenticationFailed(requestId, request); } return failureHandler.exceptionProcessingRequest(request, e, threadContext); } @Override ElasticsearchSecurityException authenticationFailed(AuthenticationToken token) { - auditTrail.authenticationFailed(token, request); + auditTrail.authenticationFailed(requestId, token, request); return failureHandler.failedAuthentication(request, token, threadContext); } @Override ElasticsearchSecurityException anonymousAccessDenied() { - auditTrail.anonymousAccessDenied(request); + auditTrail.anonymousAccessDenied(requestId, request); return failureHandler.missingToken(request, threadContext); } @Override ElasticsearchSecurityException runAsDenied(Authentication authentication, AuthenticationToken token) { - auditTrail.runAsDenied(authentication, request, Role.EMPTY.names()); + auditTrail.runAsDenied(requestId, authentication, request, Role.EMPTY.names()); return failureHandler.failedAuthentication(request, token, threadContext); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java index cd4265b18f287..b159fe414c625 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java @@ -62,6 +62,7 @@ import org.elasticsearch.xpack.core.security.user.XPackSecurityUser; import org.elasticsearch.xpack.core.security.user.XPackUser; import org.elasticsearch.xpack.security.audit.AuditTrailService; +import org.elasticsearch.xpack.security.audit.AuditUtil; import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm; import org.elasticsearch.xpack.security.authz.IndicesAndAliasesResolver.ResolvedIndices; import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore; @@ -81,13 +82,13 @@ public class AuthorizationService extends AbstractComponent { public static final Setting ANONYMOUS_AUTHORIZATION_EXCEPTION_SETTING = - Setting.boolSetting(setting("authc.anonymous.authz_exception"), true, Property.NodeScope); + Setting.boolSetting(setting("authc.anonymous.authz_exception"), true, Property.NodeScope); public static final String ORIGINATING_ACTION_KEY = "_originating_action_name"; public static final String ROLE_NAMES_KEY = "_effective_role_names"; private static final Predicate MONITOR_INDEX_PREDICATE = IndexPrivilege.MONITOR.predicate(); private static final Predicate SAME_USER_PRIVILEGE = Automatons.predicate( - ChangePasswordAction.NAME, AuthenticateAction.NAME, HasPrivilegesAction.NAME, GetUserPrivilegesAction.NAME); + ChangePasswordAction.NAME, AuthenticateAction.NAME, HasPrivilegesAction.NAME, GetUserPrivilegesAction.NAME); private static final String INDEX_SUB_REQUEST_PRIMARY = IndexAction.NAME + "[p]"; private static final String INDEX_SUB_REQUEST_REPLICA = IndexAction.NAME + "[r]"; @@ -133,6 +134,22 @@ public AuthorizationService(Settings settings, CompositeRolesStore rolesStore, C public void authorize(Authentication authentication, String action, TransportRequest request, Role userRole, Role runAsRole) throws ElasticsearchSecurityException { final TransportRequest originalRequest = request; + + String auditId = AuditUtil.extractRequestId(threadContext); + if (auditId == null) { + // We would like to assert that there is an existing request-id, but if this is a system action, then that might not be + // true because the request-id is generated during authentication + if (isInternalUser(authentication.getUser()) != false) { + auditId = AuditUtil.getOrGenerateRequestId(threadContext); + } else { + auditTrail.tamperedRequest(null, authentication.getUser(), action, request); + final String message = "Attempt to authorize action [" + action + "] for [" + authentication.getUser().principal() + + "] without an existing request-id"; + assert false : message; + throw new ElasticsearchSecurityException(message); + } + } + if (request instanceof ConcreteShardRequest) { request = ((ConcreteShardRequest) request).getRequest(); assert TransportActionProxy.isProxyRequest(request) == false : "expected non-proxy request for action: " + action; @@ -140,7 +157,7 @@ public void authorize(Authentication authentication, String action, TransportReq request = TransportActionProxy.unwrapRequest(request); if (TransportActionProxy.isProxyRequest(originalRequest) && TransportActionProxy.isProxyAction(action) == false) { throw new IllegalStateException("originalRequest is a proxy request for: [" + request + "] but action: [" - + action + "] isn't"); + + action + "] isn't"); } } // prior to doing any authorization lets set the originating action in the context only @@ -151,10 +168,10 @@ public void authorize(Authentication authentication, String action, TransportReq if (SystemUser.isAuthorized(action)) { putTransientIfNonExisting(AuthorizationServiceField.INDICES_PERMISSIONS_KEY, IndicesAccessControl.ALLOW_ALL); putTransientIfNonExisting(ROLE_NAMES_KEY, new String[] { SystemUser.ROLE_NAME }); - auditTrail.accessGranted(authentication, action, request, new String[] { SystemUser.ROLE_NAME }); + auditTrail.accessGranted(auditId, authentication, action, request, new String[] { SystemUser.ROLE_NAME }); return; } - throw denial(authentication, action, request, new String[] { SystemUser.ROLE_NAME }); + throw denial(auditId, authentication, action, request, new String[] { SystemUser.ROLE_NAME }); } // get the roles of the authenticated user, which may be different than the effective @@ -166,12 +183,12 @@ public void authorize(Authentication authentication, String action, TransportReq // if we are running as a user we looked up then the authentication must contain a lookedUpBy. If it doesn't then this user // doesn't really exist but the authc service allowed it through to avoid leaking users that exist in the system if (authentication.getLookedUpBy() == null) { - throw denyRunAs(authentication, action, request, permission.names()); + throw denyRunAs(auditId, authentication, action, request, permission.names()); } else if (permission.runAs().check(authentication.getUser().principal())) { - auditTrail.runAsGranted(authentication, action, request, permission.names()); + auditTrail.runAsGranted(auditId, authentication, action, request, permission.names()); permission = runAsRole; } else { - throw denyRunAs(authentication, action, request, permission.names()); + throw denyRunAs(auditId, authentication, action, request, permission.names()); } } putTransientIfNonExisting(ROLE_NAMES_KEY, permission.names()); @@ -179,55 +196,55 @@ public void authorize(Authentication authentication, String action, TransportReq // first, we'll check if the action is a cluster action. If it is, we'll only check it against the cluster permissions if (ClusterPrivilege.ACTION_MATCHER.test(action)) { final ClusterPermission cluster = permission.cluster(); - if (cluster.check(action, request) || checkSameUserPermissions(action, request, authentication) ) { + if (cluster.check(action, request) || checkSameUserPermissions(action, request, authentication)) { putTransientIfNonExisting(AuthorizationServiceField.INDICES_PERMISSIONS_KEY, IndicesAccessControl.ALLOW_ALL); - auditTrail.accessGranted(authentication, action, request, permission.names()); + auditTrail.accessGranted(auditId, authentication, action, request, permission.names()); return; } - throw denial(authentication, action, request, permission.names()); + throw denial(auditId, authentication, action, request, permission.names()); } // ok... this is not a cluster action, let's verify it's an indices action if (!IndexPrivilege.ACTION_MATCHER.test(action)) { - throw denial(authentication, action, request, permission.names()); + throw denial(auditId, authentication, action, request, permission.names()); } //composite actions are explicitly listed and will be authorized at the sub-request / shard level if (isCompositeAction(action)) { if (request instanceof CompositeIndicesRequest == false) { throw new IllegalStateException("Composite actions must implement " + CompositeIndicesRequest.class.getSimpleName() - + ", " + request.getClass().getSimpleName() + " doesn't"); + + ", " + request.getClass().getSimpleName() + " doesn't"); } // we check if the user can execute the action, without looking at indices, which will be authorized at the shard level if (permission.indices().check(action)) { - auditTrail.accessGranted(authentication, action, request, permission.names()); + auditTrail.accessGranted(auditId, authentication, action, request, permission.names()); return; } - throw denial(authentication, action, request, permission.names()); + throw denial(auditId, authentication, action, request, permission.names()); } else if (isTranslatedToBulkAction(action)) { if (request instanceof CompositeIndicesRequest == false) { throw new IllegalStateException("Bulk translated actions must implement " + CompositeIndicesRequest.class.getSimpleName() - + ", " + request.getClass().getSimpleName() + " doesn't"); + + ", " + request.getClass().getSimpleName() + " doesn't"); } // we check if the user can execute the action, without looking at indices, which will be authorized at the shard level if (permission.indices().check(action)) { - auditTrail.accessGranted(authentication, action, request, permission.names()); + auditTrail.accessGranted(auditId, authentication, action, request, permission.names()); return; } - throw denial(authentication, action, request, permission.names()); + throw denial(auditId, authentication, action, request, permission.names()); } else if (TransportActionProxy.isProxyAction(action)) { // we authorize proxied actions once they are "unwrapped" on the next node if (TransportActionProxy.isProxyRequest(originalRequest) == false) { throw new IllegalStateException("originalRequest is not a proxy request: [" + originalRequest + "] but action: [" - + action + "] is a proxy action"); + + action + "] is a proxy action"); } if (permission.indices().check(action)) { - auditTrail.accessGranted(authentication, action, request, permission.names()); + auditTrail.accessGranted(auditId, authentication, action, request, permission.names()); return; } else { // we do this here in addition to the denial below since we might run into an assertion on scroll request below if we // don't have permission to read cross cluster but wrap a scroll request. - throw denial(authentication, action, request, permission.names()); + throw denial(auditId, authentication, action, request, permission.names()); } } @@ -246,63 +263,63 @@ public void authorize(Authentication authentication, String action, TransportReq // index and if they cannot, we can fail the request early before we allow the execution of the action and in // turn the shard actions if (SearchScrollAction.NAME.equals(action) && permission.indices().check(action) == false) { - throw denial(authentication, action, request, permission.names()); + throw denial(auditId, authentication, action, request, permission.names()); } else { // we store the request as a transient in the ThreadContext in case of a authorization failure at the shard // level. If authorization fails we will audit a access_denied message and will use the request to retrieve // information such as the index and the incoming address of the request - auditTrail.accessGranted(authentication, action, request, permission.names()); + auditTrail.accessGranted(auditId, authentication, action, request, permission.names()); return; } } else { assert false : - "only scroll related requests are known indices api that don't support retrieving the indices they relate to"; - throw denial(authentication, action, request, permission.names()); + "only scroll related requests are known indices api that don't support retrieving the indices they relate to"; + throw denial(auditId, authentication, action, request, permission.names()); } } final boolean allowsRemoteIndices = request instanceof IndicesRequest - && IndicesAndAliasesResolver.allowsRemoteIndices((IndicesRequest) request); + && IndicesAndAliasesResolver.allowsRemoteIndices((IndicesRequest) request); // If this request does not allow remote indices // then the user must have permission to perform this action on at least 1 local index if (allowsRemoteIndices == false && permission.indices().check(action) == false) { - throw denial(authentication, action, request, permission.names()); + throw denial(auditId, authentication, action, request, permission.names()); } final MetaData metaData = clusterService.state().metaData(); final AuthorizedIndices authorizedIndices = new AuthorizedIndices(authentication.getUser(), permission, action, metaData); - final ResolvedIndices resolvedIndices = resolveIndexNames(authentication, action, request, - metaData, authorizedIndices, permission); + final ResolvedIndices resolvedIndices = resolveIndexNames(auditId, authentication, action, request, metaData, + authorizedIndices, permission); assert !resolvedIndices.isEmpty() - : "every indices request needs to have its indices set thus the resolved indices must not be empty"; + : "every indices request needs to have its indices set thus the resolved indices must not be empty"; // If this request does reference any remote indices // then the user must have permission to perform this action on at least 1 local index if (resolvedIndices.getRemote().isEmpty() && permission.indices().check(action) == false) { - throw denial(authentication, action, request, permission.names()); + throw denial(auditId, authentication, action, request, permission.names()); } //all wildcard expressions have been resolved and only the security plugin could have set '-*' here. //'-*' matches no indices so we allow the request to go through, which will yield an empty response if (resolvedIndices.isNoIndicesPlaceholder()) { putTransientIfNonExisting(AuthorizationServiceField.INDICES_PERMISSIONS_KEY, IndicesAccessControl.ALLOW_NO_INDICES); - auditTrail.accessGranted(authentication, action, request, permission.names()); + auditTrail.accessGranted(auditId, authentication, action, request, permission.names()); return; } final Set localIndices = new HashSet<>(resolvedIndices.getLocal()); IndicesAccessControl indicesAccessControl = permission.authorize(action, localIndices, metaData, fieldPermissionsCache); if (!indicesAccessControl.isGranted()) { - throw denial(authentication, action, request, permission.names()); + throw denial(auditId, authentication, action, request, permission.names()); } else if (hasSecurityIndexAccess(indicesAccessControl) - && MONITOR_INDEX_PREDICATE.test(action) == false - && isSuperuser(authentication.getUser()) == false) { + && MONITOR_INDEX_PREDICATE.test(action) == false + && isSuperuser(authentication.getUser()) == false) { // only the XPackUser is allowed to work with this index, but we should allow indices monitoring actions through for debugging // purposes. These monitor requests also sometimes resolve indices concretely and then requests them logger.debug("user [{}] attempted to directly perform [{}] against the security index [{}]", - authentication.getUser().principal(), action, SecurityIndexManager.SECURITY_INDEX_NAME); - throw denial(authentication, action, request, permission.names()); + authentication.getUser().principal(), action, SecurityIndexManager.SECURITY_INDEX_NAME); + throw denial(auditId, authentication, action, request, permission.names()); } else { putTransientIfNonExisting(AuthorizationServiceField.INDICES_PERMISSIONS_KEY, indicesAccessControl); } @@ -318,7 +335,7 @@ && isSuperuser(authentication.getUser()) == false) { } indicesAccessControl = permission.authorize("indices:admin/aliases", aliasesAndIndices, metaData, fieldPermissionsCache); if (!indicesAccessControl.isGranted()) { - throw denial(authentication, "indices:admin/aliases", request, permission.names()); + throw denial(auditId, authentication, "indices:admin/aliases", request, permission.names()); } // no need to re-add the indicesAccessControl in the context, // because the create index call doesn't do anything FLS or DLS @@ -328,12 +345,16 @@ && isSuperuser(authentication.getUser()) == false) { if (action.equals(TransportShardBulkAction.ACTION_NAME)) { // is this is performing multiple actions on the index, then check each of those actions. assert request instanceof BulkShardRequest - : "Action " + action + " requires " + BulkShardRequest.class + " but was " + request.getClass(); + : "Action " + action + " requires " + BulkShardRequest.class + " but was " + request.getClass(); - authorizeBulkItems(authentication, (BulkShardRequest) request, permission, metaData, localIndices, authorizedIndices); + authorizeBulkItems(auditId, authentication, (BulkShardRequest) request, permission, metaData, localIndices, authorizedIndices); } - auditTrail.accessGranted(authentication, action, request, permission.names()); + auditTrail.accessGranted(auditId, authentication, action, request, permission.names()); + } + + private boolean isInternalUser(User user) { + return SystemUser.is(user) || XPackUser.is(user) || XPackSecurityUser.is(user); } private boolean hasSecurityIndexAccess(IndicesAccessControl indicesAccessControl) { @@ -353,14 +374,14 @@ private boolean hasSecurityIndexAccess(IndicesAccessControl indicesAccessControl * and then checks whether that action is allowed on the targeted index. Items * that fail this checks are {@link BulkItemRequest#abort(String, Exception) * aborted}, with an - * {@link #denial(Authentication, String, TransportRequest, String[]) access + * {@link #denial(String, Authentication, String, TransportRequest, String[]) access * denied} exception. Because a shard level request is for exactly 1 index, and * there are a small number of possible item {@link DocWriteRequest.OpType * types}, the number of distinct authorization checks that need to be performed * is very small, but the results must be cached, to avoid adding a high * overhead to each bulk request. */ - private void authorizeBulkItems(Authentication authentication, BulkShardRequest request, Role permission, + private void authorizeBulkItems(String auditRequestId, Authentication authentication, BulkShardRequest request, Role permission, MetaData metaData, Set indices, AuthorizedIndices authorizedIndices) { // Maps original-index -> expanded-index-name (expands date-math, but not aliases) final Map resolvedIndexNames = new HashMap<>(); @@ -369,14 +390,14 @@ private void authorizeBulkItems(Authentication authentication, BulkShardRequest for (BulkItemRequest item : request.items()) { String resolvedIndex = resolvedIndexNames.computeIfAbsent(item.index(), key -> { final ResolvedIndices resolvedIndices = indicesAndAliasesResolver.resolveIndicesAndAliases(item.request(), metaData, - authorizedIndices); + authorizedIndices); if (resolvedIndices.getRemote().size() != 0) { throw illegalArgument("Bulk item should not write to remote indices, but request writes to " - + String.join(",", resolvedIndices.getRemote())); + + String.join(",", resolvedIndices.getRemote())); } if (resolvedIndices.getLocal().size() != 1) { throw illegalArgument("Bulk item should write to exactly 1 index, but request writes to " - + String.join(",", resolvedIndices.getLocal())); + + String.join(",", resolvedIndices.getLocal())); } final String resolved = resolvedIndices.getLocal().get(0); if (indices.contains(resolved) == false) { @@ -388,11 +409,11 @@ private void authorizeBulkItems(Authentication authentication, BulkShardRequest final Tuple indexAndAction = new Tuple<>(resolvedIndex, itemAction); final boolean granted = indexActionAuthority.computeIfAbsent(indexAndAction, key -> { final IndicesAccessControl itemAccessControl = permission.authorize(itemAction, Collections.singleton(resolvedIndex), - metaData, fieldPermissionsCache); + metaData, fieldPermissionsCache); return itemAccessControl.isGranted(); }); if (granted == false) { - item.abort(resolvedIndex, denial(authentication, itemAction, request, permission.names())); + item.abort(resolvedIndex, denial(auditRequestId, authentication, itemAction, request, permission.names())); } } } @@ -416,12 +437,12 @@ private static String getAction(BulkItemRequest item) { throw new IllegalArgumentException("No equivalent action for opType [" + docWriteRequest.opType() + "]"); } - private ResolvedIndices resolveIndexNames(Authentication authentication, String action, TransportRequest request, + private ResolvedIndices resolveIndexNames(String auditRequestId, Authentication authentication, String action, TransportRequest request, MetaData metaData, AuthorizedIndices authorizedIndices, Role permission) { try { return indicesAndAliasesResolver.resolve(request, metaData, authorizedIndices); } catch (Exception e) { - auditTrail.accessDenied(authentication, action, request, permission.names()); + auditTrail.accessDenied(auditRequestId, authentication, action, request, permission.names()); throw e; } } @@ -440,7 +461,7 @@ public void roles(User user, ActionListener roleActionListener) { // passed into this method. The XPackUser has the Superuser role and we can simply return that if (SystemUser.is(user)) { throw new IllegalArgumentException("the user [" + user.principal() + "] is the system user and we should never try to get its" + - " roles"); + " roles"); } if (XPackUser.is(user)) { assert XPackUser.INSTANCE.roles().length == 1; @@ -472,35 +493,35 @@ public void roles(User user, ActionListener roleActionListener) { private static boolean isCompositeAction(String action) { return action.equals(BulkAction.NAME) || - action.equals(MultiGetAction.NAME) || - action.equals(MultiTermVectorsAction.NAME) || - action.equals(MultiSearchAction.NAME) || - action.equals("indices:data/read/mpercolate") || - action.equals("indices:data/read/msearch/template") || - action.equals("indices:data/read/search/template") || - action.equals("indices:data/write/reindex") || - action.equals("indices:data/read/sql") || - action.equals("indices:data/read/sql/translate"); + action.equals(MultiGetAction.NAME) || + action.equals(MultiTermVectorsAction.NAME) || + action.equals(MultiSearchAction.NAME) || + action.equals("indices:data/read/mpercolate") || + action.equals("indices:data/read/msearch/template") || + action.equals("indices:data/read/search/template") || + action.equals("indices:data/write/reindex") || + action.equals("indices:data/read/sql") || + action.equals("indices:data/read/sql/translate"); } private static boolean isTranslatedToBulkAction(String action) { return action.equals(IndexAction.NAME) || - action.equals(DeleteAction.NAME) || - action.equals(INDEX_SUB_REQUEST_PRIMARY) || - action.equals(INDEX_SUB_REQUEST_REPLICA) || - action.equals(DELETE_SUB_REQUEST_PRIMARY) || - action.equals(DELETE_SUB_REQUEST_REPLICA); + action.equals(DeleteAction.NAME) || + action.equals(INDEX_SUB_REQUEST_PRIMARY) || + action.equals(INDEX_SUB_REQUEST_REPLICA) || + action.equals(DELETE_SUB_REQUEST_PRIMARY) || + action.equals(DELETE_SUB_REQUEST_REPLICA); } private static boolean isScrollRelatedAction(String action) { return action.equals(SearchScrollAction.NAME) || - action.equals(SearchTransportService.FETCH_ID_SCROLL_ACTION_NAME) || - action.equals(SearchTransportService.QUERY_FETCH_SCROLL_ACTION_NAME) || - action.equals(SearchTransportService.QUERY_SCROLL_ACTION_NAME) || - action.equals(SearchTransportService.FREE_CONTEXT_SCROLL_ACTION_NAME) || - action.equals(ClearScrollAction.NAME) || - action.equals("indices:data/read/sql/close_cursor") || - action.equals(SearchTransportService.CLEAR_SCROLL_CONTEXTS_ACTION_NAME); + action.equals(SearchTransportService.FETCH_ID_SCROLL_ACTION_NAME) || + action.equals(SearchTransportService.QUERY_FETCH_SCROLL_ACTION_NAME) || + action.equals(SearchTransportService.QUERY_SCROLL_ACTION_NAME) || + action.equals(SearchTransportService.FREE_CONTEXT_SCROLL_ACTION_NAME) || + action.equals(ClearScrollAction.NAME) || + action.equals("indices:data/read/sql/close_cursor") || + action.equals(SearchTransportService.CLEAR_SCROLL_CONTEXTS_ACTION_NAME); } static boolean checkSameUserPermissions(String action, TransportRequest request, Authentication authentication) { @@ -524,7 +545,7 @@ static boolean checkSameUserPermissions(String action, TransportRequest request, assert AuthenticateAction.NAME.equals(action) || HasPrivilegesAction.NAME.equals(action) || GetUserPrivilegesAction.NAME.equals(action) || sameUsername == false - : "Action '" + action + "' should not be possible when sameUsername=" + sameUsername; + : "Action '" + action + "' should not be possible when sameUsername=" + sameUsername; return sameUsername; } return false; @@ -548,14 +569,15 @@ private static boolean checkChangePasswordAction(Authentication authentication) return ReservedRealm.TYPE.equals(realmType) || NativeRealmSettings.TYPE.equals(realmType); } - ElasticsearchSecurityException denial(Authentication authentication, String action, TransportRequest request, String[] roleNames) { - auditTrail.accessDenied(authentication, action, request, roleNames); + ElasticsearchSecurityException denial(String auditRequestId, Authentication authentication, String action, TransportRequest request, + String[] roleNames) { + auditTrail.accessDenied(auditRequestId, authentication, action, request, roleNames); return denialException(authentication, action); } - private ElasticsearchSecurityException denyRunAs(Authentication authentication, String action, TransportRequest request, - String[] roleNames) { - auditTrail.runAsDenied(authentication, action, request, roleNames); + private ElasticsearchSecurityException denyRunAs(String auditRequestId, Authentication authentication, String action, + TransportRequest request, String[] roleNames) { + auditTrail.runAsDenied(auditRequestId, authentication, action, request, roleNames); return denialException(authentication, action); } @@ -570,9 +592,9 @@ private ElasticsearchSecurityException denialException(Authentication authentica // check for run as if (authentication.getUser().isRunAs()) { logger.debug("action [{}] is unauthorized for user [{}] run as [{}]", action, authUser.principal(), - authentication.getUser().principal()); + authentication.getUser().principal()); return authorizationError("action [{}] is unauthorized for user [{}] run as [{}]", action, authUser.principal(), - authentication.getUser().principal()); + authentication.getUser().principal()); } logger.debug("action [{}] is unauthorized for user [{}]", action, authUser.principal()); return authorizationError("action [{}] is unauthorized for user [{}]", action, authUser.principal()); @@ -580,7 +602,7 @@ private ElasticsearchSecurityException denialException(Authentication authentica static boolean isSuperuser(User user) { return Arrays.stream(user.roles()) - .anyMatch(ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR.getName()::equals); + .anyMatch(ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR.getName()::equals); } public static void addSettings(List> settings) { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/SecuritySearchOperationListener.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/SecuritySearchOperationListener.java index e3121c9512d46..044552d9d7710 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/SecuritySearchOperationListener.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/SecuritySearchOperationListener.java @@ -15,6 +15,7 @@ import org.elasticsearch.xpack.security.audit.AuditTrailService; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.AuthenticationField; +import org.elasticsearch.xpack.security.audit.AuditUtil; import static org.elasticsearch.xpack.security.authz.AuthorizationService.ORIGINATING_ACTION_KEY; import static org.elasticsearch.xpack.security.authz.AuthorizationService.ROLE_NAMES_KEY; @@ -63,7 +64,7 @@ public void validateSearchContext(SearchContext searchContext, TransportRequest final Authentication current = Authentication.getAuthentication(threadContext); final String action = threadContext.getTransient(ORIGINATING_ACTION_KEY); ensureAuthenticatedUserIsSame(originalAuth, current, auditTrailService, searchContext.id(), action, request, - threadContext.getTransient(ROLE_NAMES_KEY)); + AuditUtil.extractRequestId(threadContext), threadContext.getTransient(ROLE_NAMES_KEY)); } } } @@ -75,7 +76,7 @@ public void validateSearchContext(SearchContext searchContext, TransportRequest * (or lookup) realm. To work around this we compare the username and the originating realm type. */ static void ensureAuthenticatedUserIsSame(Authentication original, Authentication current, AuditTrailService auditTrailService, - long id, String action, TransportRequest request, String[] roleNames) { + long id, String action, TransportRequest request, String requestId, String[] roleNames) { // this is really a best effort attempt since we cannot guarantee principal uniqueness // and realm names can change between nodes. final boolean samePrincipal = original.getUser().principal().equals(current.getUser().principal()); @@ -94,7 +95,7 @@ static void ensureAuthenticatedUserIsSame(Authentication original, Authenticatio final boolean sameUser = samePrincipal && sameRealmType; if (sameUser == false) { - auditTrailService.accessDenied(current, action, request, roleNames); + auditTrailService.accessDenied(requestId, current, action, request, roleNames); throw new SearchContextMissingException(id); } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/AuditTrailServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/AuditTrailServiceTests.java index 6ce5ad0ccfab2..d4289080a9b30 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/AuditTrailServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/AuditTrailServiceTests.java @@ -54,11 +54,12 @@ public void init() throws Exception { } public void testAuthenticationFailed() throws Exception { - service.authenticationFailed(token, "_action", message); + final String requestId = randomAlphaOfLengthBetween(6, 12); + service.authenticationFailed(requestId, token, "_action", message); verify(licenseState).isAuditingAllowed(); if (isAuditingAllowed) { for (AuditTrail auditTrail : auditTrails) { - verify(auditTrail).authenticationFailed(token, "_action", message); + verify(auditTrail).authenticationFailed(requestId, token, "_action", message); } } else { verifyZeroInteractions(auditTrails.toArray((Object[]) new AuditTrail[auditTrails.size()])); @@ -66,11 +67,12 @@ public void testAuthenticationFailed() throws Exception { } public void testAuthenticationFailedNoToken() throws Exception { - service.authenticationFailed("_action", message); + final String requestId = randomAlphaOfLengthBetween(6, 12); + service.authenticationFailed(requestId, "_action", message); verify(licenseState).isAuditingAllowed(); if (isAuditingAllowed) { for (AuditTrail auditTrail : auditTrails) { - verify(auditTrail).authenticationFailed("_action", message); + verify(auditTrail).authenticationFailed(requestId, "_action", message); } } else { verifyZeroInteractions(auditTrails.toArray((Object[]) new AuditTrail[auditTrails.size()])); @@ -78,11 +80,12 @@ public void testAuthenticationFailedNoToken() throws Exception { } public void testAuthenticationFailedRestNoToken() throws Exception { - service.authenticationFailed(restRequest); + final String requestId = randomAlphaOfLengthBetween(6, 12); + service.authenticationFailed(requestId, restRequest); verify(licenseState).isAuditingAllowed(); if (isAuditingAllowed) { for (AuditTrail auditTrail : auditTrails) { - verify(auditTrail).authenticationFailed(restRequest); + verify(auditTrail).authenticationFailed(requestId, restRequest); } } else { verifyZeroInteractions(auditTrails.toArray((Object[]) new AuditTrail[auditTrails.size()])); @@ -90,11 +93,12 @@ public void testAuthenticationFailedRestNoToken() throws Exception { } public void testAuthenticationFailedRest() throws Exception { - service.authenticationFailed(token, restRequest); + final String requestId = randomAlphaOfLengthBetween(6, 12); + service.authenticationFailed(requestId, token, restRequest); verify(licenseState).isAuditingAllowed(); if (isAuditingAllowed) { for (AuditTrail auditTrail : auditTrails) { - verify(auditTrail).authenticationFailed(token, restRequest); + verify(auditTrail).authenticationFailed(requestId, token, restRequest); } } else { verifyZeroInteractions(auditTrails.toArray((Object[]) new AuditTrail[auditTrails.size()])); @@ -102,11 +106,12 @@ public void testAuthenticationFailedRest() throws Exception { } public void testAuthenticationFailedRealm() throws Exception { - service.authenticationFailed("_realm", token, "_action", message); + final String requestId = randomAlphaOfLengthBetween(6, 12); + service.authenticationFailed(requestId, "_realm", token, "_action", message); verify(licenseState).isAuditingAllowed(); if (isAuditingAllowed) { for (AuditTrail auditTrail : auditTrails) { - verify(auditTrail).authenticationFailed("_realm", token, "_action", message); + verify(auditTrail).authenticationFailed(requestId, "_realm", token, "_action", message); } } else { verifyZeroInteractions(auditTrails.toArray((Object[]) new AuditTrail[auditTrails.size()])); @@ -114,11 +119,12 @@ public void testAuthenticationFailedRealm() throws Exception { } public void testAuthenticationFailedRestRealm() throws Exception { - service.authenticationFailed("_realm", token, restRequest); + final String requestId = randomAlphaOfLengthBetween(6, 12); + service.authenticationFailed(requestId, "_realm", token, restRequest); verify(licenseState).isAuditingAllowed(); if (isAuditingAllowed) { for (AuditTrail auditTrail : auditTrails) { - verify(auditTrail).authenticationFailed("_realm", token, restRequest); + verify(auditTrail).authenticationFailed(requestId, "_realm", token, restRequest); } } else { verifyZeroInteractions(auditTrails.toArray((Object[]) new AuditTrail[auditTrails.size()])); @@ -126,11 +132,12 @@ public void testAuthenticationFailedRestRealm() throws Exception { } public void testAnonymousAccess() throws Exception { - service.anonymousAccessDenied("_action", message); + final String requestId = randomAlphaOfLengthBetween(6, 12); + service.anonymousAccessDenied(requestId, "_action", message); verify(licenseState).isAuditingAllowed(); if (isAuditingAllowed) { for (AuditTrail auditTrail : auditTrails) { - verify(auditTrail).anonymousAccessDenied("_action", message); + verify(auditTrail).anonymousAccessDenied(requestId, "_action", message); } } else { verifyZeroInteractions(auditTrails.toArray((Object[]) new AuditTrail[auditTrails.size()])); @@ -141,11 +148,12 @@ public void testAccessGranted() throws Exception { Authentication authentication =new Authentication(new User("_username", "r1"), new RealmRef(null, null, null), new RealmRef(null, null, null)); String[] roles = new String[] { randomAlphaOfLengthBetween(1, 6) }; - service.accessGranted(authentication, "_action", message, roles); + final String requestId = randomAlphaOfLengthBetween(6, 12); + service.accessGranted(requestId, authentication, "_action", message, roles); verify(licenseState).isAuditingAllowed(); if (isAuditingAllowed) { for (AuditTrail auditTrail : auditTrails) { - verify(auditTrail).accessGranted(authentication, "_action", message, roles); + verify(auditTrail).accessGranted(requestId, authentication, "_action", message, roles); } } else { verifyZeroInteractions(auditTrails.toArray((Object[]) new AuditTrail[auditTrails.size()])); @@ -156,11 +164,12 @@ public void testAccessDenied() throws Exception { Authentication authentication = new Authentication(new User("_username", "r1"), new RealmRef(null, null, null), new RealmRef(null, null, null)); String[] roles = new String[] { randomAlphaOfLengthBetween(1, 6) }; - service.accessDenied(authentication, "_action", message, roles); + final String requestId = randomAlphaOfLengthBetween(6, 12); + service.accessDenied(requestId, authentication, "_action", message, roles); verify(licenseState).isAuditingAllowed(); if (isAuditingAllowed) { for (AuditTrail auditTrail : auditTrails) { - verify(auditTrail).accessDenied(authentication, "_action", message, roles); + verify(auditTrail).accessDenied(requestId, authentication, "_action", message, roles); } } else { verifyZeroInteractions(auditTrails.toArray((Object[]) new AuditTrail[auditTrails.size()])); @@ -198,11 +207,12 @@ public void testConnectionDenied() throws Exception { public void testAuthenticationSuccessRest() throws Exception { User user = new User("_username", "r1"); String realm = "_realm"; - service.authenticationSuccess(realm, user, restRequest); + final String requestId = randomAlphaOfLengthBetween(6, 12); + service.authenticationSuccess(requestId, realm, user, restRequest); verify(licenseState).isAuditingAllowed(); if (isAuditingAllowed) { for (AuditTrail auditTrail : auditTrails) { - verify(auditTrail).authenticationSuccess(realm, user, restRequest); + verify(auditTrail).authenticationSuccess(requestId, realm, user, restRequest); } } else { verifyZeroInteractions(auditTrails.toArray((Object[]) new AuditTrail[auditTrails.size()])); @@ -212,11 +222,12 @@ public void testAuthenticationSuccessRest() throws Exception { public void testAuthenticationSuccessTransport() throws Exception { User user = new User("_username", "r1"); String realm = "_realm"; - service.authenticationSuccess(realm, user, "_action", message); + final String requestId = randomAlphaOfLengthBetween(6, 12); + service.authenticationSuccess(requestId, realm, user, "_action", message); verify(licenseState).isAuditingAllowed(); if (isAuditingAllowed) { for (AuditTrail auditTrail : auditTrails) { - verify(auditTrail).authenticationSuccess(realm, user, "_action", message); + verify(auditTrail).authenticationSuccess(requestId, realm, user, "_action", message); } } else { verifyZeroInteractions(auditTrails.toArray((Object[]) new AuditTrail[auditTrails.size()])); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrailMutedTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrailMutedTests.java index 33ba5741e087e..0a795281da674 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrailMutedTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrailMutedTests.java @@ -94,7 +94,7 @@ public void stop() { public void testAnonymousAccessDeniedMutedTransport() { createAuditTrail(new String[] { "anonymous_access_denied" }); TransportMessage message = mock(TransportMessage.class); - auditTrail.anonymousAccessDenied("_action", message); + auditTrail.anonymousAccessDenied(randomAlphaOfLengthBetween(6, 12), "_action", message); assertThat(messageEnqueued.get(), is(false)); assertThat(clientCalled.get(), is(false)); @@ -104,7 +104,7 @@ public void testAnonymousAccessDeniedMutedTransport() { public void testAnonymousAccessDeniedMutedRest() { createAuditTrail(new String[] { "anonymous_access_denied" }); RestRequest restRequest = mock(RestRequest.class); - auditTrail.anonymousAccessDenied(restRequest); + auditTrail.anonymousAccessDenied(randomAlphaOfLengthBetween(6, 12), restRequest); assertThat(messageEnqueued.get(), is(false)); assertThat(clientCalled.get(), is(false)); @@ -117,12 +117,12 @@ public void testAuthenticationFailedMutedTransport() { AuthenticationToken token = mock(AuthenticationToken.class); // without realm - auditTrail.authenticationFailed(token, "_action", message); + auditTrail.authenticationFailed(randomAlphaOfLengthBetween(6, 12), token, "_action", message); assertThat(messageEnqueued.get(), is(false)); assertThat(clientCalled.get(), is(false)); // without the token - auditTrail.authenticationFailed("_action", message); + auditTrail.authenticationFailed(randomAlphaOfLengthBetween(6, 12), "_action", message); assertThat(messageEnqueued.get(), is(false)); assertThat(clientCalled.get(), is(false)); @@ -135,12 +135,12 @@ public void testAuthenticationFailedMutedRest() { AuthenticationToken token = mock(AuthenticationToken.class); // without the realm - auditTrail.authenticationFailed(token, restRequest); + auditTrail.authenticationFailed(randomAlphaOfLengthBetween(6, 12), token, restRequest); assertThat(messageEnqueued.get(), is(false)); assertThat(clientCalled.get(), is(false)); // without the token - auditTrail.authenticationFailed(restRequest); + auditTrail.authenticationFailed(randomAlphaOfLengthBetween(6, 12), restRequest); assertThat(messageEnqueued.get(), is(false)); assertThat(clientCalled.get(), is(false)); @@ -153,7 +153,7 @@ public void testAuthenticationFailedRealmMutedTransport() { AuthenticationToken token = mock(AuthenticationToken.class); // with realm - auditTrail.authenticationFailed(randomAlphaOfLengthBetween(2, 10), token, "_action", message); + auditTrail.authenticationFailed(randomAlphaOfLengthBetween(6, 12), randomAlphaOfLengthBetween(2, 10), token, "_action", message); assertThat(messageEnqueued.get(), is(false)); assertThat(clientCalled.get(), is(false)); @@ -166,7 +166,7 @@ public void testAuthenticationFailedRealmMutedRest() { AuthenticationToken token = mock(AuthenticationToken.class); // with realm - auditTrail.authenticationFailed(randomAlphaOfLengthBetween(2, 10), token, restRequest); + auditTrail.authenticationFailed(randomAlphaOfLengthBetween(6, 12), randomAlphaOfLengthBetween(2, 10), token, restRequest); assertThat(messageEnqueued.get(), is(false)); assertThat(clientCalled.get(), is(false)); verifyZeroInteractions(token, restRequest); @@ -176,7 +176,8 @@ public void testAccessGrantedMuted() { createAuditTrail(new String[] { "access_granted" }); final TransportMessage message = mock(TransportMessage.class); final Authentication authentication = mock(Authentication.class); - auditTrail.accessGranted(authentication, randomAlphaOfLengthBetween(6, 40), message, new String[] { "role" }); + auditTrail.accessGranted(randomAlphaOfLengthBetween(6, 12), authentication, randomAlphaOfLengthBetween(6, 40), message, + new String[] { "role" }); assertThat(messageEnqueued.get(), is(false)); assertThat(clientCalled.get(), is(false)); verifyZeroInteractions(message); @@ -186,7 +187,7 @@ public void testSystemAccessGrantedMuted() { createAuditTrail(randomFrom(new String[] { "access_granted" }, null)); final TransportMessage message = mock(TransportMessage.class); final Authentication authentication = new Authentication(SystemUser.INSTANCE, new RealmRef(null, null, null), null); - auditTrail.accessGranted(authentication, "internal:foo", message, new String[] { "role" }); + auditTrail.accessGranted(randomAlphaOfLengthBetween(6, 12), authentication, "internal:foo", message, new String[] { "role" }); assertThat(messageEnqueued.get(), is(false)); assertThat(clientCalled.get(), is(false)); @@ -197,7 +198,8 @@ public void testAccessDeniedMuted() { createAuditTrail(new String[] { "access_denied" }); final TransportMessage message = mock(TransportMessage.class); final Authentication authentication = mock(Authentication.class); - auditTrail.accessDenied(authentication, randomAlphaOfLengthBetween(6, 40), message, new String[] { "role" }); + auditTrail.accessDenied(randomAlphaOfLengthBetween(6, 12), authentication, randomAlphaOfLengthBetween(6, 40), message, + new String[] { "role" }); assertThat(messageEnqueued.get(), is(false)); assertThat(clientCalled.get(), is(false)); @@ -210,12 +212,12 @@ public void testTamperedRequestMuted() { User user = mock(User.class); // with user - auditTrail.tamperedRequest(user, randomAlphaOfLengthBetween(6, 40), message); + auditTrail.tamperedRequest(randomAlphaOfLengthBetween(6, 12), user, randomAlphaOfLengthBetween(6, 40), message); assertThat(messageEnqueued.get(), is(false)); assertThat(clientCalled.get(), is(false)); // without user - auditTrail.tamperedRequest(randomAlphaOfLengthBetween(6, 40), message); + auditTrail.tamperedRequest(randomAlphaOfLengthBetween(6, 12), randomAlphaOfLengthBetween(6, 40), message); assertThat(messageEnqueued.get(), is(false)); assertThat(clientCalled.get(), is(false)); @@ -251,7 +253,8 @@ public void testRunAsGrantedMuted() { TransportMessage message = mock(TransportMessage.class); Authentication authentication = mock(Authentication.class); - auditTrail.runAsGranted(authentication, randomAlphaOfLengthBetween(6, 40), message, new String[] { "role" }); + auditTrail.runAsGranted(randomAlphaOfLengthBetween(6, 12), authentication, randomAlphaOfLengthBetween(6, 40), message, + new String[] { "role" }); assertThat(messageEnqueued.get(), is(false)); assertThat(clientCalled.get(), is(false)); @@ -263,7 +266,8 @@ public void testRunAsDeniedMuted() { TransportMessage message = mock(TransportMessage.class); Authentication authentication = mock(Authentication.class); - auditTrail.runAsDenied(authentication, randomAlphaOfLengthBetween(6, 40), message, new String[] { "role" }); + auditTrail.runAsDenied(randomAlphaOfLengthBetween(6, 12), authentication, randomAlphaOfLengthBetween(6, 40), message, + new String[] { "role" }); assertThat(messageEnqueued.get(), is(false)); assertThat(clientCalled.get(), is(false)); @@ -276,7 +280,7 @@ public void testAuthenticationSuccessRest() { User user = mock(User.class); String realm = "_realm"; - auditTrail.authenticationSuccess(realm, user, restRequest); + auditTrail.authenticationSuccess(randomAlphaOfLengthBetween(6, 12), realm, user, restRequest); assertThat(messageEnqueued.get(), is(false)); assertThat(clientCalled.get(), is(false)); @@ -288,7 +292,7 @@ public void testAuthenticationSuccessTransport() { TransportMessage message = mock(TransportMessage.class); User user = mock(User.class); String realm = "_realm"; - auditTrail.authenticationSuccess(realm, user, randomAlphaOfLengthBetween(6, 40), message); + auditTrail.authenticationSuccess(randomAlphaOfLengthBetween(6, 12), realm, user, randomAlphaOfLengthBetween(6, 40), message); assertThat(messageEnqueued.get(), is(false)); assertThat(clientCalled.get(), is(false)); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrailTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrailTests.java index 9673a14f36933..32a2be9d5a711 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrailTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrailTests.java @@ -427,7 +427,7 @@ public void testProcessorsSetting() { public void testAnonymousAccessDeniedTransport() throws Exception { initialize(); TransportMessage message = randomFrom(new RemoteHostMockMessage(), new LocalHostMockMessage(), new MockIndicesTransportMessage()); - auditor.anonymousAccessDenied("_action", message); + auditor.anonymousAccessDenied(randomAlphaOfLengthBetween(6, 12), "_action", message); SearchHit hit = getIndexedAuditMessage(enqueuedMessage.get()); assertAuditMessage(hit, "transport", "anonymous_access_denied"); @@ -450,7 +450,7 @@ public void testAnonymousAccessDeniedTransport() throws Exception { public void testAnonymousAccessDeniedRest() throws Exception { initialize(); RestRequest request = mockRestRequest(); - auditor.anonymousAccessDenied(request); + auditor.anonymousAccessDenied(randomAlphaOfLengthBetween(6, 12), request); SearchHit hit = getIndexedAuditMessage(enqueuedMessage.get()); assertAuditMessage(hit, "rest", "anonymous_access_denied"); @@ -464,7 +464,7 @@ public void testAnonymousAccessDeniedRest() throws Exception { public void testAuthenticationFailedTransport() throws Exception { initialize(); TransportMessage message = randomBoolean() ? new RemoteHostMockMessage() : new LocalHostMockMessage(); - auditor.authenticationFailed(new MockToken(), "_action", message); + auditor.authenticationFailed(randomAlphaOfLengthBetween(6, 12), new MockToken(), "_action", message); SearchHit hit = getIndexedAuditMessage(enqueuedMessage.get()); Map sourceMap = hit.getSourceAsMap(); assertAuditMessage(hit, "transport", "authentication_failed"); @@ -484,7 +484,7 @@ public void testAuthenticationFailedTransport() throws Exception { public void testAuthenticationFailedTransportNoToken() throws Exception { initialize(); TransportMessage message = randomFrom(new RemoteHostMockMessage(), new LocalHostMockMessage(), new MockIndicesTransportMessage()); - auditor.authenticationFailed("_action", message); + auditor.authenticationFailed(randomAlphaOfLengthBetween(6, 12), "_action", message); SearchHit hit = getIndexedAuditMessage(enqueuedMessage.get()); assertAuditMessage(hit, "transport", "authentication_failed"); @@ -508,7 +508,7 @@ public void testAuthenticationFailedTransportNoToken() throws Exception { public void testAuthenticationFailedRest() throws Exception { initialize(); RestRequest request = mockRestRequest(); - auditor.authenticationFailed(new MockToken(), request); + auditor.authenticationFailed(randomAlphaOfLengthBetween(6, 12), new MockToken(), request); SearchHit hit = getIndexedAuditMessage(enqueuedMessage.get()); assertAuditMessage(hit, "rest", "authentication_failed"); @@ -523,7 +523,7 @@ public void testAuthenticationFailedRest() throws Exception { public void testAuthenticationFailedRestNoToken() throws Exception { initialize(); RestRequest request = mockRestRequest(); - auditor.authenticationFailed(request); + auditor.authenticationFailed(randomAlphaOfLengthBetween(6, 12), request); SearchHit hit = getIndexedAuditMessage(enqueuedMessage.get()); assertAuditMessage(hit, "rest", "authentication_failed"); @@ -538,7 +538,7 @@ public void testAuthenticationFailedRestNoToken() throws Exception { public void testAuthenticationFailedTransportRealm() throws Exception { initialize(); TransportMessage message = randomFrom(new RemoteHostMockMessage(), new LocalHostMockMessage(), new MockIndicesTransportMessage()); - auditor.authenticationFailed("_realm", new MockToken(), "_action", message); + auditor.authenticationFailed(randomAlphaOfLengthBetween(6, 12), "_realm", new MockToken(), "_action", message); SearchHit hit = getIndexedAuditMessage(enqueuedMessage.get()); assertAuditMessage(hit, "transport", "realm_authentication_failed"); @@ -564,7 +564,7 @@ public void testAuthenticationFailedTransportRealm() throws Exception { public void testAuthenticationFailedRestRealm() throws Exception { initialize(); RestRequest request = mockRestRequest(); - auditor.authenticationFailed("_realm", new MockToken(), request); + auditor.authenticationFailed(randomAlphaOfLengthBetween(6, 12), "_realm", new MockToken(), request); SearchHit hit = getIndexedAuditMessage(enqueuedMessage.get()); assertAuditMessage(hit, "rest", "realm_authentication_failed"); @@ -587,7 +587,7 @@ public void testAccessGranted() throws Exception { user = new User("_username", new String[]{"r1"}); } String role = randomAlphaOfLengthBetween(1, 6); - auditor.accessGranted(createAuthentication(user), "_action", message, new String[] { role }); + auditor.accessGranted(randomAlphaOfLengthBetween(6, 12), createAuthentication(user), "_action", message, new String[] { role }); SearchHit hit = getIndexedAuditMessage(enqueuedMessage.get()); assertAuditMessage(hit, "transport", "access_granted"); @@ -615,7 +615,8 @@ public void testSystemAccessGranted() throws Exception { initialize(new String[] { "system_access_granted" }, null); TransportMessage message = randomBoolean() ? new RemoteHostMockMessage() : new LocalHostMockMessage(); String role = randomAlphaOfLengthBetween(1, 6); - auditor.accessGranted(createAuthentication(SystemUser.INSTANCE), "internal:_action", message, new String[] { role }); + auditor.accessGranted(randomAlphaOfLength(8), createAuthentication(SystemUser.INSTANCE), "internal:_action", message, + new String[] { role }); SearchHit hit = getIndexedAuditMessage(enqueuedMessage.get()); assertAuditMessage(hit, "transport", "access_granted"); @@ -639,7 +640,7 @@ public void testAccessDenied() throws Exception { user = new User("_username", new String[]{"r1"}); } String role = randomAlphaOfLengthBetween(1, 6); - auditor.accessDenied(createAuthentication(user), "_action", message, new String[] { role }); + auditor.accessDenied(randomAlphaOfLengthBetween(6, 12), createAuthentication(user), "_action", message, new String[] { role }); SearchHit hit = getIndexedAuditMessage(enqueuedMessage.get()); Map sourceMap = hit.getSourceAsMap(); @@ -666,7 +667,7 @@ public void testAccessDenied() throws Exception { public void testTamperedRequestRest() throws Exception { initialize(); RestRequest request = mockRestRequest(); - auditor.tamperedRequest(request); + auditor.tamperedRequest(randomAlphaOfLengthBetween(6, 12), request); SearchHit hit = getIndexedAuditMessage(enqueuedMessage.get()); assertAuditMessage(hit, "rest", "tampered_request"); @@ -681,7 +682,7 @@ public void testTamperedRequestRest() throws Exception { public void testTamperedRequest() throws Exception { initialize(); TransportRequest message = new RemoteHostMockTransportRequest(); - auditor.tamperedRequest("_action", message); + auditor.tamperedRequest(randomAlphaOfLengthBetween(6, 12), "_action", message); SearchHit hit = getIndexedAuditMessage(enqueuedMessage.get()); Map sourceMap = hit.getSourceAsMap(); @@ -702,7 +703,7 @@ public void testTamperedRequestWithUser() throws Exception { } else { user = new User("_username", new String[]{"r1"}); } - auditor.tamperedRequest(user, "_action", message); + auditor.tamperedRequest(randomAlphaOfLengthBetween(6, 12), user, "_action", message); SearchHit hit = getIndexedAuditMessage(enqueuedMessage.get()); @@ -752,7 +753,7 @@ public void testRunAsGranted() throws Exception { TransportMessage message = randomFrom(new RemoteHostMockMessage(), new LocalHostMockMessage(), new MockIndicesTransportMessage()); User user = new User("running as", new String[]{"r2"}, new User("_username", new String[] {"r1"})); String role = randomAlphaOfLengthBetween(1, 6); - auditor.runAsGranted(createAuthentication(user), "_action", message, new String[] { role }); + auditor.runAsGranted(randomAlphaOfLengthBetween(6, 12), createAuthentication(user), "_action", message, new String[] { role }); SearchHit hit = getIndexedAuditMessage(enqueuedMessage.get()); assertAuditMessage(hit, "transport", "run_as_granted"); @@ -771,7 +772,7 @@ public void testRunAsDenied() throws Exception { initialize(); TransportMessage message = randomFrom(new RemoteHostMockMessage(), new LocalHostMockMessage(), new MockIndicesTransportMessage()); User user = new User("running as", new String[]{"r2"}, new User("_username", new String[] {"r1"})); - auditor.runAsDenied(createAuthentication(user), "_action", message, new String[] { "r1" }); + auditor.runAsDenied(randomAlphaOfLengthBetween(6, 12), createAuthentication(user), "_action", message, new String[] { "r1" }); SearchHit hit = getIndexedAuditMessage(enqueuedMessage.get()); assertAuditMessage(hit, "transport", "run_as_denied"); @@ -796,7 +797,7 @@ public void testAuthenticationSuccessRest() throws Exception { user = new User("_username", new String[] { "r1" }); } String realm = "_realm"; - auditor.authenticationSuccess(realm, user, request); + auditor.authenticationSuccess(randomAlphaOfLengthBetween(6, 12), realm, user, request); SearchHit hit = getIndexedAuditMessage(enqueuedMessage.get()); assertAuditMessage(hit, "rest", "authentication_success"); @@ -823,7 +824,7 @@ public void testAuthenticationSuccessTransport() throws Exception { user = new User("_username", new String[] { "r1" }); } String realm = "_realm"; - auditor.authenticationSuccess(realm, user, "_action", message); + auditor.authenticationSuccess(randomAlphaOfLengthBetween(6, 12), realm, user, "_action", message); SearchHit hit = getIndexedAuditMessage(enqueuedMessage.get()); Map sourceMap = hit.getSourceAsMap(); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/DeprecatedLoggingAuditTrailTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/DeprecatedLoggingAuditTrailTests.java index 238dab033443b..087718888982e 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/DeprecatedLoggingAuditTrailTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/DeprecatedLoggingAuditTrailTests.java @@ -53,10 +53,10 @@ import java.util.Locale; import java.util.Map; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; -import static org.hamcrest.Matchers.containsString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -161,7 +161,7 @@ public void testAnonymousAccessDeniedTransport() throws Exception { DeprecatedLoggingAuditTrail auditTrail = new DeprecatedLoggingAuditTrail(settings, clusterService, logger, threadContext); final TransportMessage message = randomBoolean() ? new MockMessage(threadContext) : new MockIndicesRequest(threadContext); final String origins = DeprecatedLoggingAuditTrail.originAttributes(threadContext, message, auditTrail.localNodeInfo); - auditTrail.anonymousAccessDenied("_action", message); + auditTrail.anonymousAccessDenied(randomAlphaOfLength(12), "_action", message); if (message instanceof IndicesRequest) { assertMsg(logger, Level.INFO, prefix + "[transport] [anonymous_access_denied]\t" + origins + ", action=[_action], indices=[" + indices(message) + "], request=[MockIndicesRequest]" + opaqueId); @@ -174,7 +174,7 @@ public void testAnonymousAccessDeniedTransport() throws Exception { CapturingLogger.output(logger.getName(), Level.INFO).clear(); settings = Settings.builder().put(settings).put("xpack.security.audit.logfile.events.exclude", "anonymous_access_denied").build(); auditTrail = new DeprecatedLoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.anonymousAccessDenied("_action", message); + auditTrail.anonymousAccessDenied(randomAlphaOfLength(12), "_action", message); assertEmptyLog(logger); } @@ -185,7 +185,7 @@ public void testAnonymousAccessDeniedRest() throws Exception { final RestRequest request = tuple.v2(); final Logger logger = CapturingLogger.newCapturingLogger(Level.INFO, null); DeprecatedLoggingAuditTrail auditTrail = new DeprecatedLoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.anonymousAccessDenied(request); + auditTrail.anonymousAccessDenied(randomAlphaOfLength(12), request); if (includeRequestBody) { assertMsg(logger, Level.INFO, prefix + "[rest] [anonymous_access_denied]\torigin_address=[" + NetworkAddress.format(address) + "], uri=[_uri]" + opaqueId + ", request_body=[" + expectedMessage + "]"); @@ -198,7 +198,7 @@ public void testAnonymousAccessDeniedRest() throws Exception { CapturingLogger.output(logger.getName(), Level.INFO).clear(); settings = Settings.builder().put(settings).put("xpack.security.audit.logfile.events.exclude", "anonymous_access_denied").build(); auditTrail = new DeprecatedLoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.anonymousAccessDenied(request); + auditTrail.anonymousAccessDenied(randomAlphaOfLength(12), request); assertEmptyLog(logger); } @@ -207,7 +207,7 @@ public void testAuthenticationFailed() throws Exception { DeprecatedLoggingAuditTrail auditTrail = new DeprecatedLoggingAuditTrail(settings, clusterService, logger, threadContext); final TransportMessage message = randomBoolean() ? new MockMessage(threadContext) : new MockIndicesRequest(threadContext); final String origins = DeprecatedLoggingAuditTrail.originAttributes(threadContext, message, auditTrail.localNodeInfo); - auditTrail.authenticationFailed(new MockToken(), "_action", message); + auditTrail.authenticationFailed(randomAlphaOfLength(12), new MockToken(), "_action", message); if (message instanceof IndicesRequest) { assertMsg(logger, Level.INFO, prefix + "[transport] [authentication_failed]\t" + origins + ", principal=[_principal], action=[_action], indices=[" + indices(message) + @@ -221,7 +221,7 @@ public void testAuthenticationFailed() throws Exception { CapturingLogger.output(logger.getName(), Level.INFO).clear(); settings = Settings.builder().put(settings).put("xpack.security.audit.logfile.events.exclude", "authentication_failed").build(); auditTrail = new DeprecatedLoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.authenticationFailed(new MockToken(), "_action", message); + auditTrail.authenticationFailed(randomAlphaOfLength(12), new MockToken(), "_action", message); assertEmptyLog(logger); } @@ -230,7 +230,7 @@ public void testAuthenticationFailedNoToken() throws Exception { DeprecatedLoggingAuditTrail auditTrail = new DeprecatedLoggingAuditTrail(settings, clusterService, logger, threadContext); final TransportMessage message = randomBoolean() ? new MockMessage(threadContext) : new MockIndicesRequest(threadContext); final String origins = DeprecatedLoggingAuditTrail.originAttributes(threadContext, message, auditTrail.localNodeInfo); - auditTrail.authenticationFailed("_action", message); + auditTrail.authenticationFailed(randomAlphaOfLength(12), "_action", message); if (message instanceof IndicesRequest) { assertMsg(logger, Level.INFO, prefix + "[transport] [authentication_failed]\t" + origins + ", action=[_action], indices=[" + indices(message) + "], request=[MockIndicesRequest]" + opaqueId); @@ -243,7 +243,7 @@ public void testAuthenticationFailedNoToken() throws Exception { CapturingLogger.output(logger.getName(), Level.INFO).clear(); settings = Settings.builder().put(settings).put("xpack.security.audit.logfile.events.exclude", "authentication_failed").build(); auditTrail = new DeprecatedLoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.authenticationFailed("_action", message); + auditTrail.authenticationFailed(randomAlphaOfLength(12), "_action", message); assertEmptyLog(logger); } @@ -254,7 +254,7 @@ public void testAuthenticationFailedRest() throws Exception { final RestRequest request = tuple.v2(); final Logger logger = CapturingLogger.newCapturingLogger(Level.INFO, null); DeprecatedLoggingAuditTrail auditTrail = new DeprecatedLoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.authenticationFailed(new MockToken(), request); + auditTrail.authenticationFailed(randomAlphaOfLength(12), new MockToken(), request); if (includeRequestBody) { assertMsg(logger, Level.INFO, prefix + "[rest] [authentication_failed]\torigin_address=[" + NetworkAddress.format(address) + "], principal=[_principal], uri=[_uri]" + opaqueId + ", request_body=[" + @@ -268,7 +268,7 @@ public void testAuthenticationFailedRest() throws Exception { CapturingLogger.output(logger.getName(), Level.INFO).clear(); settings = Settings.builder().put(settings).put("xpack.security.audit.logfile.events.exclude", "authentication_failed").build(); auditTrail = new DeprecatedLoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.authenticationFailed(new MockToken(), request); + auditTrail.authenticationFailed(randomAlphaOfLength(12), new MockToken(), request); assertEmptyLog(logger); } @@ -279,7 +279,7 @@ public void testAuthenticationFailedRestNoToken() throws Exception { final RestRequest request = tuple.v2(); final Logger logger = CapturingLogger.newCapturingLogger(Level.INFO, null); DeprecatedLoggingAuditTrail auditTrail = new DeprecatedLoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.authenticationFailed(request); + auditTrail.authenticationFailed(randomAlphaOfLength(12), request); if (includeRequestBody) { assertMsg(logger, Level.INFO, prefix + "[rest] [authentication_failed]\torigin_address=[" + NetworkAddress.format(address) + "], uri=[_uri]" + opaqueId + ", request_body=[" + expectedMessage + "]"); @@ -292,7 +292,7 @@ public void testAuthenticationFailedRestNoToken() throws Exception { CapturingLogger.output(logger.getName(), Level.INFO).clear(); settings = Settings.builder().put(settings).put("xpack.security.audit.logfile.events.exclude", "authentication_failed").build(); auditTrail = new DeprecatedLoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.authenticationFailed(request); + auditTrail.authenticationFailed(randomAlphaOfLength(12), request); assertEmptyLog(logger); } @@ -300,7 +300,7 @@ public void testAuthenticationFailedRealm() throws Exception { final Logger logger = CapturingLogger.newCapturingLogger(Level.INFO, null); DeprecatedLoggingAuditTrail auditTrail = new DeprecatedLoggingAuditTrail(settings, clusterService, logger, threadContext); final TransportMessage message = randomBoolean() ? new MockMessage(threadContext) : new MockIndicesRequest(threadContext); - auditTrail.authenticationFailed("_realm", new MockToken(), "_action", message); + auditTrail.authenticationFailed(randomAlphaOfLength(12), "_realm", new MockToken(), "_action", message); assertEmptyLog(logger); // test enabled @@ -308,7 +308,7 @@ public void testAuthenticationFailedRealm() throws Exception { Settings.builder().put(settings).put("xpack.security.audit.logfile.events.include", "realm_authentication_failed").build(); auditTrail = new DeprecatedLoggingAuditTrail(settings, clusterService, logger, threadContext); final String origins = DeprecatedLoggingAuditTrail.originAttributes(threadContext, message, auditTrail.localNodeInfo); - auditTrail.authenticationFailed("_realm", new MockToken(), "_action", message); + auditTrail.authenticationFailed(randomAlphaOfLength(12), "_realm", new MockToken(), "_action", message); if (message instanceof IndicesRequest) { assertMsg(logger, Level.INFO, prefix + "[transport] [realm_authentication_failed]\trealm=[_realm], " + origins + ", principal=[_principal], action=[_action], indices=[" + indices(message) + "], " + @@ -326,14 +326,14 @@ public void testAuthenticationFailedRealmRest() throws Exception { final RestRequest request = tuple.v2(); final Logger logger = CapturingLogger.newCapturingLogger(Level.INFO, null); DeprecatedLoggingAuditTrail auditTrail = new DeprecatedLoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.authenticationFailed("_realm", new MockToken(), request); + auditTrail.authenticationFailed(randomAlphaOfLength(12), "_realm", new MockToken(), request); assertEmptyLog(logger); // test enabled settings = Settings.builder().put(settings).put("xpack.security.audit.logfile.events.include", "realm_authentication_failed").build(); auditTrail = new DeprecatedLoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.authenticationFailed("_realm", new MockToken(), request); + auditTrail.authenticationFailed(randomAlphaOfLength(12), "_realm", new MockToken(), request); if (includeRequestBody) { assertMsg(logger, Level.INFO, prefix + "[rest] [realm_authentication_failed]\trealm=[_realm], origin_address=[" + NetworkAddress.format(address) + "], principal=[_principal], uri=[_uri]" + opaqueId + ", request_body=[" + @@ -357,7 +357,7 @@ public void testAccessGranted() throws Exception { user = new User("_username", new String[]{"r1"}); } final String role = randomAlphaOfLengthBetween(1, 6); - auditTrail.accessGranted(createAuthentication(user), "_action", message, new String[] { role }); + auditTrail.accessGranted(randomAlphaOfLength(12), createAuthentication(user), "_action", message, new String[] { role }); final String userInfo = (runAs ? "principal=[running as], realm=[lookRealm], run_by_principal=[_username], run_by_realm=[authRealm]" : "principal=[_username], realm=[authRealm]") + ", roles=[" + role + "]"; if (message instanceof IndicesRequest) { @@ -372,7 +372,7 @@ public void testAccessGranted() throws Exception { CapturingLogger.output(logger.getName(), Level.INFO).clear(); settings = Settings.builder().put(settings).put("xpack.security.audit.logfile.events.exclude", "access_granted").build(); auditTrail = new DeprecatedLoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.accessGranted(createAuthentication(user), "_action", message, new String[] { role }); + auditTrail.accessGranted(randomAlphaOfLength(12), createAuthentication(user), "_action", message, new String[] { role }); assertEmptyLog(logger); } @@ -381,14 +381,14 @@ public void testAccessGrantedInternalSystemAction() throws Exception { DeprecatedLoggingAuditTrail auditTrail = new DeprecatedLoggingAuditTrail(settings, clusterService, logger, threadContext); final TransportMessage message = randomBoolean() ? new MockMessage(threadContext) : new MockIndicesRequest(threadContext); final String role = randomAlphaOfLengthBetween(1, 6); - auditTrail.accessGranted(createAuthentication(SystemUser.INSTANCE), "internal:_action", message, new String[] { role }); + auditTrail.accessGranted(null, createAuthentication(SystemUser.INSTANCE), "internal:_action", message, new String[] { role }); assertEmptyLog(logger); // test enabled settings = Settings.builder().put(settings).put("xpack.security.audit.logfile.events.include", "system_access_granted").build(); auditTrail = new DeprecatedLoggingAuditTrail(settings, clusterService, logger, threadContext); final String origins = DeprecatedLoggingAuditTrail.originAttributes(threadContext, message, auditTrail.localNodeInfo); - auditTrail.accessGranted(createAuthentication(SystemUser.INSTANCE), "internal:_action", message, new String[] { role }); + auditTrail.accessGranted(null, createAuthentication(SystemUser.INSTANCE), "internal:_action", message, new String[] { role }); if (message instanceof IndicesRequest) { assertMsg(logger, Level.INFO, prefix + "[transport] [access_granted]\t" + origins + ", principal=[" + SystemUser.INSTANCE.principal() @@ -414,7 +414,7 @@ public void testAccessGrantedInternalSystemActionNonSystemUser() throws Exceptio user = new User("_username", new String[]{"r1"}); } final String role = randomAlphaOfLengthBetween(1, 6); - auditTrail.accessGranted(createAuthentication(user), "internal:_action", message, new String[] { role }); + auditTrail.accessGranted(randomAlphaOfLength(12), createAuthentication(user), "internal:_action", message, new String[] { role }); final String userInfo = (runAs ? "principal=[running as], realm=[lookRealm], run_by_principal=[_username], run_by_realm=[authRealm]" : "principal=[_username], realm=[authRealm]") + ", roles=[" + role + "]"; if (message instanceof IndicesRequest) { @@ -429,7 +429,7 @@ public void testAccessGrantedInternalSystemActionNonSystemUser() throws Exceptio CapturingLogger.output(logger.getName(), Level.INFO).clear(); settings = Settings.builder().put(settings).put("xpack.security.audit.logfile.events.exclude", "access_granted").build(); auditTrail = new DeprecatedLoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.accessGranted(createAuthentication(user), "internal:_action", message, new String[] { role }); + auditTrail.accessGranted(randomAlphaOfLength(12), createAuthentication(user), "internal:_action", message, new String[] { role }); assertEmptyLog(logger); } @@ -446,7 +446,7 @@ public void testAccessDenied() throws Exception { user = new User("_username", new String[]{"r1"}); } final String role = randomAlphaOfLengthBetween(1, 6); - auditTrail.accessDenied(createAuthentication(user), "_action", message, new String[] { role }); + auditTrail.accessDenied(randomAlphaOfLength(12), createAuthentication(user), "_action", message, new String[] { role }); final String userInfo = (runAs ? "principal=[running as], realm=[lookRealm], run_by_principal=[_username], run_by_realm=[authRealm]" : "principal=[_username], realm=[authRealm]") + ", roles=[" + role + "]"; if (message instanceof IndicesRequest) { @@ -461,7 +461,7 @@ public void testAccessDenied() throws Exception { CapturingLogger.output(logger.getName(), Level.INFO).clear(); settings = Settings.builder().put(settings).put("xpack.security.audit.logfile.events.exclude", "access_denied").build(); auditTrail = new DeprecatedLoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.accessDenied(createAuthentication(user), "_action", message, new String[] { role }); + auditTrail.accessDenied(randomAlphaOfLength(12), createAuthentication(user), "_action", message, new String[] { role }); assertEmptyLog(logger); } @@ -472,7 +472,7 @@ public void testTamperedRequestRest() throws Exception { final RestRequest request = tuple.v2(); final Logger logger = CapturingLogger.newCapturingLogger(Level.INFO, null); DeprecatedLoggingAuditTrail auditTrail = new DeprecatedLoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.tamperedRequest(request); + auditTrail.tamperedRequest(randomAlphaOfLength(12), request); if (includeRequestBody) { assertMsg(logger, Level.INFO, prefix + "[rest] [tampered_request]\torigin_address=[" + NetworkAddress.format(address) + "], uri=[_uri]" + opaqueId + ", request_body=[" + expectedMessage + "]"); @@ -485,7 +485,7 @@ public void testTamperedRequestRest() throws Exception { CapturingLogger.output(logger.getName(), Level.INFO).clear(); settings = Settings.builder().put(settings).put("xpack.security.audit.logfile.events.exclude", "tampered_request").build(); auditTrail = new DeprecatedLoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.tamperedRequest(request); + auditTrail.tamperedRequest(randomAlphaOfLength(12), request); assertEmptyLog(logger); } @@ -495,7 +495,7 @@ public void testTamperedRequest() throws Exception { final Logger logger = CapturingLogger.newCapturingLogger(Level.INFO, null); final DeprecatedLoggingAuditTrail auditTrail = new DeprecatedLoggingAuditTrail(settings, clusterService, logger, threadContext); final String origins = DeprecatedLoggingAuditTrail.originAttributes(threadContext, message, auditTrail.localNodeInfo); - auditTrail.tamperedRequest(action, message); + auditTrail.tamperedRequest(randomAlphaOfLength(12), action, message); if (message instanceof IndicesRequest) { assertMsg(logger, Level.INFO, prefix + "[transport] [tampered_request]\t" + origins + ", action=[_action], indices=[" + indices(message) + "], request=[MockIndicesRequest]" + opaqueId); @@ -522,7 +522,7 @@ public void testTamperedRequestWithUser() throws Exception { final Logger logger = CapturingLogger.newCapturingLogger(Level.INFO, null); DeprecatedLoggingAuditTrail auditTrail = new DeprecatedLoggingAuditTrail(settings, clusterService, logger, threadContext); final String origins = DeprecatedLoggingAuditTrail.originAttributes(threadContext, message, auditTrail.localNodeInfo); - auditTrail.tamperedRequest(user, action, message); + auditTrail.tamperedRequest(randomAlphaOfLength(12), user, action, message); if (message instanceof IndicesRequest) { assertMsg(logger, Level.INFO, prefix + "[transport] [tampered_request]\t" + origins + ", " + userInfo + ", action=[_action], indices=[" + indices(message) + "], request=[MockIndicesRequest]" + opaqueId); @@ -535,7 +535,7 @@ public void testTamperedRequestWithUser() throws Exception { CapturingLogger.output(logger.getName(), Level.INFO).clear(); settings = Settings.builder().put(settings).put("xpack.security.audit.logfile.events.exclude", "tampered_request").build(); auditTrail = new DeprecatedLoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.tamperedRequest(user, action, message); + auditTrail.tamperedRequest(randomAlphaOfLength(12), user, action, message); assertEmptyLog(logger); } @@ -582,7 +582,7 @@ public void testRunAsGranted() throws Exception { final String origins = DeprecatedLoggingAuditTrail.originAttributes(threadContext, message, auditTrail.localNodeInfo); final User user = new User("running as", new String[]{"r2"}, new User("_username", new String[] {"r1"})); final String role = randomAlphaOfLengthBetween(1, 6); - auditTrail.runAsGranted(createAuthentication(user), "_action", message, new String[] { role }); + auditTrail.runAsGranted(randomAlphaOfLength(12), createAuthentication(user), "_action", message, new String[] { role }); if (message instanceof IndicesRequest) { assertMsg(logger, Level.INFO, prefix + "[transport] [run_as_granted]\t" + origins @@ -599,7 +599,7 @@ public void testRunAsGranted() throws Exception { CapturingLogger.output(logger.getName(), Level.INFO).clear(); settings = Settings.builder().put(settings).put("xpack.security.audit.logfile.events.exclude", "run_as_granted").build(); auditTrail = new DeprecatedLoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.runAsGranted(createAuthentication(user), "_action", message, new String[] { role }); + auditTrail.runAsGranted(randomAlphaOfLength(12), createAuthentication(user), "_action", message, new String[] { role }); assertEmptyLog(logger); } @@ -610,7 +610,7 @@ public void testRunAsDenied() throws Exception { final String origins = DeprecatedLoggingAuditTrail.originAttributes(threadContext, message, auditTrail.localNodeInfo); final User user = new User("running as", new String[]{"r2"}, new User("_username", new String[] {"r1"})); final String role = randomAlphaOfLengthBetween(1, 6); - auditTrail.runAsDenied(createAuthentication(user), "_action", message, new String[] { role }); + auditTrail.runAsDenied(randomAlphaOfLength(12), createAuthentication(user), "_action", message, new String[] { role }); if (message instanceof IndicesRequest) { assertMsg(logger, Level.INFO, prefix + "[transport] [run_as_denied]\t" + origins @@ -627,7 +627,7 @@ public void testRunAsDenied() throws Exception { CapturingLogger.output(logger.getName(), Level.INFO).clear(); settings = Settings.builder().put(settings).put("xpack.security.audit.logfile.events.exclude", "run_as_denied").build(); auditTrail = new DeprecatedLoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.runAsDenied(createAuthentication(user), "_action", message, new String[] { role }); + auditTrail.runAsDenied(randomAlphaOfLength(12), createAuthentication(user), "_action", message, new String[] { role }); assertEmptyLog(logger); } @@ -673,7 +673,7 @@ public void testAuthenticationSuccessRest() throws Exception { .build(); final Logger logger = CapturingLogger.newCapturingLogger(Level.INFO, null); DeprecatedLoggingAuditTrail auditTrail = new DeprecatedLoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.authenticationSuccess(realm, user, request); + auditTrail.authenticationSuccess(randomAlphaOfLength(12), realm, user, request); if (includeRequestBody) { assertMsg(logger, Level.INFO, prefix + "[rest] [authentication_success]\t" + userInfo + ", realm=[_realm], uri=[_uri], params=[" + params @@ -689,7 +689,7 @@ public void testAuthenticationSuccessRest() throws Exception { settings = Settings.builder().put(this.settings).put("xpack.security.audit.logfile.events.exclude", "authentication_success") .build(); auditTrail = new DeprecatedLoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.authenticationSuccess(realm, user, request); + auditTrail.authenticationSuccess(randomAlphaOfLength(12), realm, user, request); assertEmptyLog(logger); } @@ -709,7 +709,7 @@ public void testAuthenticationSuccessTransport() throws Exception { } final String userInfo = runAs ? "principal=[running as], run_by_principal=[_username]" : "principal=[_username]"; final String realm = "_realm"; - auditTrail.authenticationSuccess(realm, user, "_action", message); + auditTrail.authenticationSuccess(randomAlphaOfLength(12), realm, user, "_action", message); if (message instanceof IndicesRequest) { assertMsg(logger, Level.INFO, prefix + "[transport] [authentication_success]\t" + origins + ", " + userInfo + ", realm=[_realm], action=[_action], indices=[" + indices(message) + "], request=[MockIndicesRequest]" + opaqueId); @@ -725,7 +725,7 @@ public void testAuthenticationSuccessTransport() throws Exception { .put("xpack.security.audit.logfile.events.exclude", "authentication_success") .build(); auditTrail = new DeprecatedLoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.authenticationSuccess(realm, user, "_action", message); + auditTrail.authenticationSuccess(randomAlphaOfLength(12), realm, user, "_action", message); assertEmptyLog(logger); } @@ -747,37 +747,38 @@ public void testRequestsWithoutIndices() throws Exception { final List output = CapturingLogger.output(logger.getName(), Level.INFO); int logEntriesCount = 1; for (final TransportMessage message : messages) { - auditTrail.anonymousAccessDenied("_action", message); + final String requestId = randomAlphaOfLength(12); + auditTrail.anonymousAccessDenied(requestId, "_action", message); assertThat(output.size(), is(logEntriesCount++)); assertThat(output.get(logEntriesCount - 2), not(containsString("indices=["))); - auditTrail.authenticationFailed(new MockToken(), "_action", message); + auditTrail.authenticationFailed(requestId, new MockToken(), "_action", message); assertThat(output.size(), is(logEntriesCount++)); assertThat(output.get(logEntriesCount - 2), not(containsString("indices=["))); - auditTrail.authenticationFailed("_action", message); + auditTrail.authenticationFailed(requestId, "_action", message); assertThat(output.size(), is(logEntriesCount++)); assertThat(output.get(logEntriesCount - 2), not(containsString("indices=["))); auditTrail.authenticationFailed(realm, new MockToken(), "_action", message); assertThat(output.size(), is(logEntriesCount++)); assertThat(output.get(logEntriesCount - 2), not(containsString("indices=["))); - auditTrail.accessGranted(createAuthentication(user), "_action", message, new String[] { role }); + auditTrail.accessGranted(requestId, createAuthentication(user), "_action", message, new String[] { role }); assertThat(output.size(), is(logEntriesCount++)); assertThat(output.get(logEntriesCount - 2), not(containsString("indices=["))); - auditTrail.accessDenied(createAuthentication(user), "_action", message, new String[] { role }); + auditTrail.accessDenied(requestId, createAuthentication(user), "_action", message, new String[] { role }); assertThat(output.size(), is(logEntriesCount++)); assertThat(output.get(logEntriesCount - 2), not(containsString("indices=["))); - auditTrail.tamperedRequest("_action", message); + auditTrail.tamperedRequest(requestId, "_action", message); assertThat(output.size(), is(logEntriesCount++)); assertThat(output.get(logEntriesCount - 2), not(containsString("indices=["))); - auditTrail.tamperedRequest(user, "_action", message); + auditTrail.tamperedRequest(requestId, user, "_action", message); assertThat(output.size(), is(logEntriesCount++)); assertThat(output.get(logEntriesCount - 2), not(containsString("indices=["))); - auditTrail.runAsGranted(createAuthentication(user), "_action", message, new String[] { role }); + auditTrail.runAsGranted(requestId, createAuthentication(user), "_action", message, new String[] { role }); assertThat(output.size(), is(logEntriesCount++)); assertThat(output.get(logEntriesCount - 2), not(containsString("indices=["))); - auditTrail.runAsDenied(createAuthentication(user), "_action", message, new String[] { role }); + auditTrail.runAsDenied(requestId, createAuthentication(user), "_action", message, new String[] { role }); assertThat(output.size(), is(logEntriesCount++)); assertThat(output.get(logEntriesCount - 2), not(containsString("indices=["))); - auditTrail.authenticationSuccess(realm, user, "_action", message); + auditTrail.authenticationSuccess(requestId, realm, user, "_action", message); assertThat(output.size(), is(logEntriesCount++)); assertThat(output.get(logEntriesCount - 2), not(containsString("indices=["))); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailFilterTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailFilterTests.java index 8060ba2fe2437..8cb3dbf01b247 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailFilterTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailFilterTests.java @@ -391,7 +391,7 @@ public void testUsersFilter() throws Exception { final LoggingAuditTrail auditTrail = new LoggingAuditTrail(settingsBuilder.build(), clusterService, logger, threadContext); final List logOutput = CapturingLogger.output(logger.getName(), Level.INFO); // anonymous accessDenied - auditTrail.anonymousAccessDenied("_action", message); + auditTrail.anonymousAccessDenied(randomAlphaOfLength(8), "_action", message); if (filterMissingUser) { assertThat("Anonymous message: not filtered out by the missing user filter", logOutput.size(), is(0)); } else { @@ -400,7 +400,7 @@ public void testUsersFilter() throws Exception { logOutput.clear(); threadContext.stashContext(); - auditTrail.anonymousAccessDenied(getRestRequest()); + auditTrail.anonymousAccessDenied(randomAlphaOfLength(8), getRestRequest()); if (filterMissingUser) { assertThat("Anonymous rest request: not filtered out by the missing user filter", logOutput.size(), is(0)); } else { @@ -410,7 +410,7 @@ public void testUsersFilter() throws Exception { threadContext.stashContext(); // authenticationFailed - auditTrail.authenticationFailed(getRestRequest()); + auditTrail.authenticationFailed(randomAlphaOfLength(8), getRestRequest()); if (filterMissingUser) { assertThat("AuthenticationFailed no token rest request: not filtered out by the missing user filter", logOutput.size(), is(0)); } else { @@ -419,17 +419,17 @@ public void testUsersFilter() throws Exception { logOutput.clear(); threadContext.stashContext(); - auditTrail.authenticationFailed(unfilteredToken, "_action", message); + auditTrail.authenticationFailed(randomAlphaOfLength(8), unfilteredToken, "_action", message); assertThat("AuthenticationFailed token request: unfiltered user is filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.authenticationFailed(filteredToken, "_action", message); + auditTrail.authenticationFailed(randomAlphaOfLength(8), filteredToken, "_action", message); assertThat("AuthenticationFailed token request: filtered user is not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); - auditTrail.authenticationFailed("_action", message); + auditTrail.authenticationFailed(randomAlphaOfLength(8), "_action", message); if (filterMissingUser) { assertThat("AuthenticationFailed no token message: not filtered out by the missing user filter", logOutput.size(), is(0)); } else { @@ -438,92 +438,92 @@ public void testUsersFilter() throws Exception { logOutput.clear(); threadContext.stashContext(); - auditTrail.authenticationFailed(unfilteredToken, getRestRequest()); + auditTrail.authenticationFailed(randomAlphaOfLength(8), unfilteredToken, getRestRequest()); assertThat("AuthenticationFailed rest request: unfiltered user is filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.authenticationFailed(filteredToken, getRestRequest()); + auditTrail.authenticationFailed(randomAlphaOfLength(8), filteredToken, getRestRequest()); assertThat("AuthenticationFailed rest request: filtered user is not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); - auditTrail.authenticationFailed("_realm", unfilteredToken, "_action", message); + auditTrail.authenticationFailed(randomAlphaOfLength(8), "_realm", unfilteredToken, "_action", message); assertThat("AuthenticationFailed realm message: unfiltered user is filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.authenticationFailed("_realm", filteredToken, "_action", message); + auditTrail.authenticationFailed(randomAlphaOfLength(8), "_realm", filteredToken, "_action", message); assertThat("AuthenticationFailed realm message: filtered user is not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); - auditTrail.authenticationFailed("_realm", unfilteredToken, getRestRequest()); + auditTrail.authenticationFailed(randomAlphaOfLength(8), "_realm", unfilteredToken, getRestRequest()); assertThat("AuthenticationFailed realm rest request: unfiltered user is filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.authenticationFailed("_realm", filteredToken, getRestRequest()); + auditTrail.authenticationFailed(randomAlphaOfLength(8), "_realm", filteredToken, getRestRequest()); assertThat("AuthenticationFailed realm rest request: filtered user is not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); // accessGranted - auditTrail.accessGranted(unfilteredAuthentication, "_action", message, new String[] { "role1" }); + auditTrail.accessGranted(randomAlphaOfLength(8), unfilteredAuthentication, "_action", message, new String[] { "role1" }); assertThat("AccessGranted message: unfiltered user is filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.accessGranted(filteredAuthentication, "_action", message, new String[] { "role1" }); + auditTrail.accessGranted(randomAlphaOfLength(8), filteredAuthentication, "_action", message, new String[] { "role1" }); assertThat("AccessGranted message: filtered user is not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); - auditTrail.accessGranted(createAuthentication(SystemUser.INSTANCE, "effectiveRealmName"), "internal:_action", message, - new String[] { "role1" }); + auditTrail.accessGranted(randomAlphaOfLength(8), createAuthentication(SystemUser.INSTANCE, "effectiveRealmName"), + "internal:_action", message, new String[] { "role1" }); assertThat("AccessGranted internal message: system user is filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.accessGranted(unfilteredAuthentication, "internal:_action", message, new String[] { "role1" }); + auditTrail.accessGranted(randomAlphaOfLength(8), unfilteredAuthentication, "internal:_action", message, new String[] { "role1" }); assertThat("AccessGranted internal message: unfiltered user is filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.accessGranted(filteredAuthentication, "internal:_action", message, new String[] { "role1" }); + auditTrail.accessGranted(randomAlphaOfLength(8), filteredAuthentication, "internal:_action", message, new String[] { "role1" }); assertThat("AccessGranted internal message: filtered user is not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); // accessDenied - auditTrail.accessDenied(unfilteredAuthentication, "_action", message, new String[] { "role1" }); + auditTrail.accessDenied(randomAlphaOfLength(8), unfilteredAuthentication, "_action", message, new String[] { "role1" }); assertThat("AccessDenied message: unfiltered user is filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.accessDenied(filteredAuthentication, "_action", message, new String[] { "role1" }); + auditTrail.accessDenied(randomAlphaOfLength(8), filteredAuthentication, "_action", message, new String[] { "role1" }); assertThat("AccessDenied message: filtered user is not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); - auditTrail.accessDenied(createAuthentication(SystemUser.INSTANCE, "effectiveRealmName"), "internal:_action", message, - new String[] { "role1" }); + auditTrail.accessDenied(randomAlphaOfLength(8), createAuthentication(SystemUser.INSTANCE, "effectiveRealmName"), "internal:_action", + message, new String[] { "role1" }); assertThat("AccessDenied internal message: system user is filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.accessDenied(unfilteredAuthentication, "internal:_action", message, new String[] { "role1" }); + auditTrail.accessDenied(randomAlphaOfLength(8), unfilteredAuthentication, "internal:_action", message, new String[] { "role1" }); assertThat("AccessDenied internal message: unfiltered user is filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.accessDenied(filteredAuthentication, "internal:_action", message, new String[] { "role1" }); + auditTrail.accessDenied(randomAlphaOfLength(8), filteredAuthentication, "internal:_action", message, new String[] { "role1" }); assertThat("AccessDenied internal message: filtered user is not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); // tamperedRequest - auditTrail.tamperedRequest(getRestRequest()); + auditTrail.tamperedRequest(randomAlphaOfLength(8), getRestRequest()); if (filterMissingUser) { assertThat("Tampered rest: is not filtered out by the missing user filter", logOutput.size(), is(0)); } else { @@ -532,7 +532,7 @@ public void testUsersFilter() throws Exception { logOutput.clear(); threadContext.stashContext(); - auditTrail.tamperedRequest("_action", message); + auditTrail.tamperedRequest(randomAlphaOfLength(8), "_action", message); if (filterMissingUser) { assertThat("Tampered message: is not filtered out by the missing user filter", logOutput.size(), is(0)); } else { @@ -541,12 +541,12 @@ public void testUsersFilter() throws Exception { logOutput.clear(); threadContext.stashContext(); - auditTrail.tamperedRequest(unfilteredAuthentication.getUser(), "_action", message); + auditTrail.tamperedRequest(randomAlphaOfLength(8), unfilteredAuthentication.getUser(), "_action", message); assertThat("Tampered message: unfiltered user is filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.tamperedRequest(filteredAuthentication.getUser(), "_action", message); + auditTrail.tamperedRequest(randomAlphaOfLength(8), filteredAuthentication.getUser(), "_action", message); assertThat("Tampered message: filtered user is not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); @@ -572,54 +572,58 @@ public void testUsersFilter() throws Exception { threadContext.stashContext(); // runAsGranted - auditTrail.runAsGranted(unfilteredAuthentication, "_action", new MockMessage(threadContext), new String[] { "role1" }); + auditTrail.runAsGranted(randomAlphaOfLength(8), unfilteredAuthentication, "_action", new MockMessage(threadContext), + new String[] { "role1" }); assertThat("RunAsGranted message: unfiltered user is filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.runAsGranted(filteredAuthentication, "_action", new MockMessage(threadContext), new String[] { "role1" }); + auditTrail.runAsGranted(randomAlphaOfLength(8), filteredAuthentication, "_action", new MockMessage(threadContext), + new String[] { "role1" }); assertThat("RunAsGranted message: filtered user is not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); // runAsDenied - auditTrail.runAsDenied(unfilteredAuthentication, "_action", new MockMessage(threadContext), new String[] { "role1" }); + auditTrail.runAsDenied(randomAlphaOfLength(8), unfilteredAuthentication, "_action", new MockMessage(threadContext), + new String[] { "role1" }); assertThat("RunAsDenied message: unfiltered user is filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.runAsDenied(filteredAuthentication, "_action", new MockMessage(threadContext), new String[] { "role1" }); + auditTrail.runAsDenied(randomAlphaOfLength(8), filteredAuthentication, "_action", new MockMessage(threadContext), + new String[] { "role1" }); assertThat("RunAsDenied message: filtered user is not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); - auditTrail.runAsDenied(unfilteredAuthentication, getRestRequest(), new String[] { "role1" }); + auditTrail.runAsDenied(randomAlphaOfLength(8), unfilteredAuthentication, getRestRequest(), new String[] { "role1" }); assertThat("RunAsDenied rest request: unfiltered user is filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.runAsDenied(filteredAuthentication, getRestRequest(), new String[] { "role1" }); + auditTrail.runAsDenied(randomAlphaOfLength(8), filteredAuthentication, getRestRequest(), new String[] { "role1" }); assertThat("RunAsDenied rest request: filtered user is not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); // authentication Success - auditTrail.authenticationSuccess("_realm", unfilteredAuthentication.getUser(), getRestRequest()); + auditTrail.authenticationSuccess(randomAlphaOfLength(8), "_realm", unfilteredAuthentication.getUser(), getRestRequest()); assertThat("AuthenticationSuccess rest request: unfiltered user is filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.authenticationSuccess("_realm", filteredAuthentication.getUser(), getRestRequest()); + auditTrail.authenticationSuccess(randomAlphaOfLength(8), "_realm", filteredAuthentication.getUser(), getRestRequest()); assertThat("AuthenticationSuccess rest request: filtered user is not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); - auditTrail.authenticationSuccess("_realm", unfilteredAuthentication.getUser(), "_action", message); + auditTrail.authenticationSuccess(randomAlphaOfLength(8), "_realm", unfilteredAuthentication.getUser(), "_action", message); assertThat("AuthenticationSuccess message: unfiltered user is filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.authenticationSuccess("_realm", filteredAuthentication.getUser(), "_action", message); + auditTrail.authenticationSuccess(randomAlphaOfLength(8), "_realm", filteredAuthentication.getUser(), "_action", message); assertThat("AuthenticationSuccess message: filtered user is not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); @@ -666,7 +670,7 @@ public void testRealmsFilter() throws Exception { final LoggingAuditTrail auditTrail = new LoggingAuditTrail(settingsBuilder.build(), clusterService, logger, threadContext); final List logOutput = CapturingLogger.output(logger.getName(), Level.INFO); // anonymous accessDenied - auditTrail.anonymousAccessDenied("_action", message); + auditTrail.anonymousAccessDenied(randomAlphaOfLength(8), "_action", message); if (filterMissingRealm) { assertThat("Anonymous message: not filtered out by the missing realm filter", logOutput.size(), is(0)); } else { @@ -675,7 +679,7 @@ public void testRealmsFilter() throws Exception { logOutput.clear(); threadContext.stashContext(); - auditTrail.anonymousAccessDenied(getRestRequest()); + auditTrail.anonymousAccessDenied(randomAlphaOfLength(8), getRestRequest()); if (filterMissingRealm) { assertThat("Anonymous rest request: not filtered out by the missing realm filter", logOutput.size(), is(0)); } else { @@ -685,7 +689,7 @@ public void testRealmsFilter() throws Exception { threadContext.stashContext(); // authenticationFailed - auditTrail.authenticationFailed(getRestRequest()); + auditTrail.authenticationFailed(randomAlphaOfLength(8), getRestRequest()); if (filterMissingRealm) { assertThat("AuthenticationFailed no token rest request: not filtered out by the missing realm filter", logOutput.size(), is(0)); } else { @@ -694,7 +698,7 @@ public void testRealmsFilter() throws Exception { logOutput.clear(); threadContext.stashContext(); - auditTrail.authenticationFailed(authToken, "_action", message); + auditTrail.authenticationFailed(randomAlphaOfLength(8), authToken, "_action", message); if (filterMissingRealm) { assertThat("AuthenticationFailed token request: not filtered out by the missing realm filter", logOutput.size(), is(0)); } else { @@ -703,7 +707,7 @@ public void testRealmsFilter() throws Exception { logOutput.clear(); threadContext.stashContext(); - auditTrail.authenticationFailed("_action", message); + auditTrail.authenticationFailed(randomAlphaOfLength(8), "_action", message); if (filterMissingRealm) { assertThat("AuthenticationFailed no token message: not filtered out by the missing realm filter", logOutput.size(), is(0)); } else { @@ -712,7 +716,7 @@ public void testRealmsFilter() throws Exception { logOutput.clear(); threadContext.stashContext(); - auditTrail.authenticationFailed(authToken, getRestRequest()); + auditTrail.authenticationFailed(randomAlphaOfLength(8), authToken, getRestRequest()); if (filterMissingRealm) { assertThat("AuthenticationFailed rest request: not filtered out by the missing realm filter", logOutput.size(), is(0)); } else { @@ -721,94 +725,102 @@ public void testRealmsFilter() throws Exception { logOutput.clear(); threadContext.stashContext(); - auditTrail.authenticationFailed(unfilteredRealm, authToken, "_action", message); + auditTrail.authenticationFailed(randomAlphaOfLength(8), unfilteredRealm, authToken, "_action", message); assertThat("AuthenticationFailed realm message: unfiltered realm is filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.authenticationFailed(filteredRealm, authToken, "_action", message); + auditTrail.authenticationFailed(randomAlphaOfLength(8), filteredRealm, authToken, "_action", message); assertThat("AuthenticationFailed realm message: filtered realm is not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); - auditTrail.authenticationFailed(unfilteredRealm, authToken, getRestRequest()); + auditTrail.authenticationFailed(randomAlphaOfLength(8), unfilteredRealm, authToken, getRestRequest()); assertThat("AuthenticationFailed realm rest request: unfiltered realm is filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.authenticationFailed(filteredRealm, authToken, getRestRequest()); + auditTrail.authenticationFailed(randomAlphaOfLength(8), filteredRealm, authToken, getRestRequest()); assertThat("AuthenticationFailed realm rest request: filtered realm is not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); // accessGranted - auditTrail.accessGranted(createAuthentication(user, filteredRealm), "_action", message, new String[] { "role1" }); + auditTrail.accessGranted(randomAlphaOfLength(8), createAuthentication(user, filteredRealm), "_action", message, + new String[] { "role1" }); assertThat("AccessGranted message: filtered realm is not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); - auditTrail.accessGranted(createAuthentication(user, unfilteredRealm), "_action", message, new String[] { "role1" }); + auditTrail.accessGranted(randomAlphaOfLength(8), createAuthentication(user, unfilteredRealm), "_action", message, + new String[] { "role1" }); assertThat("AccessGranted message: unfiltered realm is filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.accessGranted(createAuthentication(SystemUser.INSTANCE, filteredRealm), "internal:_action", message, - new String[] { "role1" }); + auditTrail.accessGranted(randomAlphaOfLength(8), createAuthentication(SystemUser.INSTANCE, filteredRealm), "internal:_action", + message, new String[] { "role1" }); assertThat("AccessGranted internal message system user: filtered realm is not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); - auditTrail.accessGranted(createAuthentication(SystemUser.INSTANCE, unfilteredRealm), "internal:_action", message, - new String[] { "role1" }); + auditTrail.accessGranted(randomAlphaOfLength(8), createAuthentication(SystemUser.INSTANCE, unfilteredRealm), "internal:_action", + message, new String[] { "role1" }); assertThat("AccessGranted internal message system user: unfiltered realm is filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.accessGranted(createAuthentication(user, filteredRealm), "internal:_action", message, new String[] { "role1" }); + auditTrail.accessGranted(randomAlphaOfLength(8), createAuthentication(user, filteredRealm), "internal:_action", message, + new String[] { "role1" }); assertThat("AccessGranted internal message: filtered realm is not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); - auditTrail.accessGranted(createAuthentication(user, unfilteredRealm), "internal:_action", message, new String[] { "role1" }); + auditTrail.accessGranted(randomAlphaOfLength(8), createAuthentication(user, unfilteredRealm), "internal:_action", message, + new String[] { "role1" }); assertThat("AccessGranted internal message: unfiltered realm is filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); // accessDenied - auditTrail.accessDenied(createAuthentication(user, filteredRealm), "_action", message, new String[] { "role1" }); + auditTrail.accessDenied(randomAlphaOfLength(8), createAuthentication(user, filteredRealm), "_action", message, + new String[] { "role1" }); assertThat("AccessDenied message: filtered realm is not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); - auditTrail.accessDenied(createAuthentication(user, unfilteredRealm), "_action", message, new String[] { "role1" }); + auditTrail.accessDenied(randomAlphaOfLength(8), createAuthentication(user, unfilteredRealm), "_action", message, + new String[] { "role1" }); assertThat("AccessDenied message: unfiltered realm is filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.accessDenied(createAuthentication(SystemUser.INSTANCE, filteredRealm), "internal:_action", message, - new String[] { "role1" }); + auditTrail.accessDenied(randomAlphaOfLength(8), createAuthentication(SystemUser.INSTANCE, filteredRealm), "internal:_action", + message, new String[] { "role1" }); assertThat("AccessDenied internal message system user: filtered realm is not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); - auditTrail.accessDenied(createAuthentication(SystemUser.INSTANCE, unfilteredRealm), "internal:_action", message, - new String[] { "role1" }); + auditTrail.accessDenied(randomAlphaOfLength(8), createAuthentication(SystemUser.INSTANCE, unfilteredRealm), "internal:_action", + message, new String[] { "role1" }); assertThat("AccessDenied internal message system user: unfiltered realm is filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.accessDenied(createAuthentication(user, filteredRealm), "internal:_action", message, new String[] { "role1" }); + auditTrail.accessDenied(randomAlphaOfLength(8), createAuthentication(user, filteredRealm), "internal:_action", message, + new String[] { "role1" }); assertThat("AccessGranted internal message: filtered realm is not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); - auditTrail.accessDenied(createAuthentication(user, unfilteredRealm), "internal:_action", message, new String[] { "role1" }); + auditTrail.accessDenied(randomAlphaOfLength(8), createAuthentication(user, unfilteredRealm), "internal:_action", message, + new String[] { "role1" }); assertThat("AccessGranted internal message: unfiltered realm is filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); // tamperedRequest - auditTrail.tamperedRequest(getRestRequest()); + auditTrail.tamperedRequest(randomAlphaOfLength(8), getRestRequest()); if (filterMissingRealm) { assertThat("Tampered rest: is not filtered out by the missing realm filter", logOutput.size(), is(0)); } else { @@ -817,7 +829,7 @@ public void testRealmsFilter() throws Exception { logOutput.clear(); threadContext.stashContext(); - auditTrail.tamperedRequest("_action", message); + auditTrail.tamperedRequest(randomAlphaOfLength(8), "_action", message); if (filterMissingRealm) { assertThat("Tampered message: is not filtered out by the missing realm filter", logOutput.size(), is(0)); } else { @@ -826,7 +838,7 @@ public void testRealmsFilter() throws Exception { logOutput.clear(); threadContext.stashContext(); - auditTrail.tamperedRequest(user, "_action", message); + auditTrail.tamperedRequest(randomAlphaOfLength(8), user, "_action", message); if (filterMissingRealm) { assertThat("Tampered message: is not filtered out by the missing realm filter", logOutput.size(), is(0)); } else { @@ -856,58 +868,60 @@ public void testRealmsFilter() throws Exception { threadContext.stashContext(); // runAsGranted - auditTrail.runAsGranted(createAuthentication(user, filteredRealm), "_action", new MockMessage(threadContext), - new String[] { "role1" }); + auditTrail.runAsGranted(randomAlphaOfLength(8), createAuthentication(user, filteredRealm), "_action", + new MockMessage(threadContext), new String[] { "role1" }); assertThat("RunAsGranted message: filtered realm is not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); - auditTrail.runAsGranted(createAuthentication(user, unfilteredRealm), "_action", new MockMessage(threadContext), - new String[] { "role1" }); + auditTrail.runAsGranted(randomAlphaOfLength(8), createAuthentication(user, unfilteredRealm), "_action", + new MockMessage(threadContext), new String[] { "role1" }); assertThat("RunAsGranted message: unfiltered realm is filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); // runAsDenied - auditTrail.runAsDenied(createAuthentication(user, filteredRealm), "_action", new MockMessage(threadContext), + auditTrail.runAsDenied(randomAlphaOfLength(8), createAuthentication(user, filteredRealm), "_action", new MockMessage(threadContext), new String[] { "role1" }); assertThat("RunAsDenied message: filtered realm is not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); - auditTrail.runAsDenied(createAuthentication(user, unfilteredRealm), "_action", new MockMessage(threadContext), - new String[] { "role1" }); + auditTrail.runAsDenied(randomAlphaOfLength(8), createAuthentication(user, unfilteredRealm), "_action", + new MockMessage(threadContext), new String[] { "role1" }); assertThat("RunAsDenied message: unfiltered realm is filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.runAsDenied(createAuthentication(user, filteredRealm), getRestRequest(), new String[] { "role1" }); + auditTrail.runAsDenied(randomAlphaOfLength(8), createAuthentication(user, filteredRealm), getRestRequest(), + new String[] { "role1" }); assertThat("RunAsDenied rest request: filtered realm is not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); - auditTrail.runAsDenied(createAuthentication(user, unfilteredRealm), getRestRequest(), new String[] { "role1" }); + auditTrail.runAsDenied(randomAlphaOfLength(8), createAuthentication(user, unfilteredRealm), getRestRequest(), + new String[] { "role1" }); assertThat("RunAsDenied rest request: unfiltered realm is filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); // authentication Success - auditTrail.authenticationSuccess(unfilteredRealm, user, getRestRequest()); + auditTrail.authenticationSuccess(randomAlphaOfLength(8), unfilteredRealm, user, getRestRequest()); assertThat("AuthenticationSuccess rest request: unfiltered realm is filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.authenticationSuccess(filteredRealm, user, getRestRequest()); + auditTrail.authenticationSuccess(randomAlphaOfLength(8), filteredRealm, user, getRestRequest()); assertThat("AuthenticationSuccess rest request: filtered realm is not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); - auditTrail.authenticationSuccess(unfilteredRealm, user, "_action", message); + auditTrail.authenticationSuccess(randomAlphaOfLength(8), unfilteredRealm, user, "_action", message); assertThat("AuthenticationSuccess message: unfiltered realm is filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.authenticationSuccess(filteredRealm, user, "_action", message); + auditTrail.authenticationSuccess(randomAlphaOfLength(8), filteredRealm, user, "_action", message); assertThat("AuthenticationSuccess message: filtered realm is not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); @@ -976,7 +990,7 @@ public void testRolesFilter() throws Exception { final LoggingAuditTrail auditTrail = new LoggingAuditTrail(settingsBuilder.build(), clusterService, logger, threadContext); final List logOutput = CapturingLogger.output(logger.getName(), Level.INFO); // anonymous accessDenied - auditTrail.anonymousAccessDenied("_action", message); + auditTrail.anonymousAccessDenied(randomAlphaOfLength(8), "_action", message); if (filterMissingRoles) { assertThat("Anonymous message: not filtered out by the missing roles filter", logOutput.size(), is(0)); } else { @@ -985,7 +999,7 @@ public void testRolesFilter() throws Exception { logOutput.clear(); threadContext.stashContext(); - auditTrail.anonymousAccessDenied(getRestRequest()); + auditTrail.anonymousAccessDenied(randomAlphaOfLength(8), getRestRequest()); if (filterMissingRoles) { assertThat("Anonymous rest request: not filtered out by the missing roles filter", logOutput.size(), is(0)); } else { @@ -995,7 +1009,7 @@ public void testRolesFilter() throws Exception { threadContext.stashContext(); // authenticationFailed - auditTrail.authenticationFailed(getRestRequest()); + auditTrail.authenticationFailed(randomAlphaOfLength(8), getRestRequest()); if (filterMissingRoles) { assertThat("AuthenticationFailed no token rest request: not filtered out by the missing roles filter", logOutput.size(), is(0)); } else { @@ -1004,7 +1018,7 @@ public void testRolesFilter() throws Exception { logOutput.clear(); threadContext.stashContext(); - auditTrail.authenticationFailed(authToken, "_action", message); + auditTrail.authenticationFailed(randomAlphaOfLength(8), authToken, "_action", message); if (filterMissingRoles) { assertThat("AuthenticationFailed token request: not filtered out by the missing roles filter", logOutput.size(), is(0)); } else { @@ -1013,7 +1027,7 @@ public void testRolesFilter() throws Exception { logOutput.clear(); threadContext.stashContext(); - auditTrail.authenticationFailed("_action", message); + auditTrail.authenticationFailed(randomAlphaOfLength(8), "_action", message); if (filterMissingRoles) { assertThat("AuthenticationFailed no token message: not filtered out by the missing roles filter", logOutput.size(), is(0)); } else { @@ -1022,7 +1036,7 @@ public void testRolesFilter() throws Exception { logOutput.clear(); threadContext.stashContext(); - auditTrail.authenticationFailed(authToken, getRestRequest()); + auditTrail.authenticationFailed(randomAlphaOfLength(8), authToken, getRestRequest()); if (filterMissingRoles) { assertThat("AuthenticationFailed rest request: not filtered out by the missing roles filter", logOutput.size(), is(0)); } else { @@ -1031,7 +1045,7 @@ public void testRolesFilter() throws Exception { logOutput.clear(); threadContext.stashContext(); - auditTrail.authenticationFailed("_realm", authToken, "_action", message); + auditTrail.authenticationFailed(randomAlphaOfLength(8), "_realm", authToken, "_action", message); if (filterMissingRoles) { assertThat("AuthenticationFailed realm message: not filtered out by the missing roles filter", logOutput.size(), is(0)); } else { @@ -1040,7 +1054,7 @@ public void testRolesFilter() throws Exception { logOutput.clear(); threadContext.stashContext(); - auditTrail.authenticationFailed("_realm", authToken, getRestRequest()); + auditTrail.authenticationFailed(randomAlphaOfLength(8), "_realm", authToken, getRestRequest()); if (filterMissingRoles) { assertThat("AuthenticationFailed realm rest request: not filtered out by the missing roles filter", logOutput.size(), is(0)); } else { @@ -1050,67 +1064,67 @@ public void testRolesFilter() throws Exception { threadContext.stashContext(); // accessGranted - auditTrail.accessGranted(authentication, "_action", message, unfilteredRoles); + auditTrail.accessGranted(randomAlphaOfLength(8), authentication, "_action", message, unfilteredRoles); assertThat("AccessGranted message: unfiltered roles filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.accessGranted(authentication, "_action", message, filteredRoles); + auditTrail.accessGranted(randomAlphaOfLength(8), authentication, "_action", message, filteredRoles); assertThat("AccessGranted message: filtered roles not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); - auditTrail.accessGranted(createAuthentication(SystemUser.INSTANCE, "effectiveRealmName"), "internal:_action", message, - unfilteredRoles); + auditTrail.accessGranted(randomAlphaOfLength(8), createAuthentication(SystemUser.INSTANCE, "effectiveRealmName"), + "internal:_action", message, unfilteredRoles); assertThat("AccessGranted internal message system user: unfiltered roles filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.accessGranted(createAuthentication(SystemUser.INSTANCE, "effectiveRealmName"), "internal:_action", message, - filteredRoles); + auditTrail.accessGranted(randomAlphaOfLength(8), createAuthentication(SystemUser.INSTANCE, "effectiveRealmName"), + "internal:_action", message, filteredRoles); assertThat("AccessGranted internal message system user: filtered roles not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); - auditTrail.accessGranted(authentication, "internal:_action", message, unfilteredRoles); + auditTrail.accessGranted(randomAlphaOfLength(8), authentication, "internal:_action", message, unfilteredRoles); assertThat("AccessGranted internal message: unfiltered roles filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.accessGranted(authentication, "internal:_action", message, filteredRoles); + auditTrail.accessGranted(randomAlphaOfLength(8), authentication, "internal:_action", message, filteredRoles); assertThat("AccessGranted internal message: filtered roles not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); // accessDenied - auditTrail.accessDenied(authentication, "_action", message, unfilteredRoles); + auditTrail.accessDenied(randomAlphaOfLength(8), authentication, "_action", message, unfilteredRoles); assertThat("AccessDenied message: unfiltered roles filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.accessDenied(authentication, "_action", message, filteredRoles); + auditTrail.accessDenied(randomAlphaOfLength(8), authentication, "_action", message, filteredRoles); assertThat("AccessDenied message: filtered roles not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); - auditTrail.accessDenied(createAuthentication(SystemUser.INSTANCE, "effectiveRealmName"), "internal:_action", message, - unfilteredRoles); + auditTrail.accessDenied(randomAlphaOfLength(8), createAuthentication(SystemUser.INSTANCE, "effectiveRealmName"), "internal:_action", + message, unfilteredRoles); assertThat("AccessDenied internal message system user: unfiltered roles filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.accessDenied(createAuthentication(SystemUser.INSTANCE, "effectiveRealmName"), "internal:_action", message, - filteredRoles); + auditTrail.accessDenied(randomAlphaOfLength(8), createAuthentication(SystemUser.INSTANCE, "effectiveRealmName"), "internal:_action", + message, filteredRoles); assertThat("AccessDenied internal message system user: filtered roles not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); - auditTrail.accessDenied(authentication, "internal:_action", message, unfilteredRoles); + auditTrail.accessDenied(randomAlphaOfLength(8), authentication, "internal:_action", message, unfilteredRoles); assertThat("AccessDenied internal message: unfiltered roles filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.accessDenied(authentication, "internal:_action", message, filteredRoles); + auditTrail.accessDenied(randomAlphaOfLength(8), authentication, "internal:_action", message, filteredRoles); assertThat("AccessDenied internal message: filtered roles not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); @@ -1136,39 +1150,39 @@ public void testRolesFilter() throws Exception { threadContext.stashContext(); // runAsGranted - auditTrail.runAsGranted(authentication, "_action", new MockMessage(threadContext), unfilteredRoles); + auditTrail.runAsGranted(randomAlphaOfLength(8), authentication, "_action", new MockMessage(threadContext), unfilteredRoles); assertThat("RunAsGranted message: unfiltered roles filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.runAsGranted(authentication, "_action", new MockMessage(threadContext), filteredRoles); + auditTrail.runAsGranted(randomAlphaOfLength(8), authentication, "_action", new MockMessage(threadContext), filteredRoles); assertThat("RunAsGranted message: filtered roles not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); // runAsDenied - auditTrail.runAsDenied(authentication, "_action", new MockMessage(threadContext), unfilteredRoles); + auditTrail.runAsDenied(randomAlphaOfLength(8), authentication, "_action", new MockMessage(threadContext), unfilteredRoles); assertThat("RunAsDenied message: unfiltered roles filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.runAsDenied(authentication, "_action", new MockMessage(threadContext), filteredRoles); + auditTrail.runAsDenied(randomAlphaOfLength(8), authentication, "_action", new MockMessage(threadContext), filteredRoles); assertThat("RunAsDenied message: filtered roles not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); - auditTrail.runAsDenied(authentication, getRestRequest(), unfilteredRoles); + auditTrail.runAsDenied(randomAlphaOfLength(8), authentication, getRestRequest(), unfilteredRoles); assertThat("RunAsDenied rest request: unfiltered roles filtered out", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.runAsDenied(authentication, getRestRequest(), filteredRoles); + auditTrail.runAsDenied(randomAlphaOfLength(8), authentication, getRestRequest(), filteredRoles); assertThat("RunAsDenied rest request: filtered roles not filtered out", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); // authentication Success - auditTrail.authenticationSuccess("_realm", authentication.getUser(), getRestRequest()); + auditTrail.authenticationSuccess(randomAlphaOfLength(8), "_realm", authentication.getUser(), getRestRequest()); if (filterMissingRoles) { assertThat("AuthenticationSuccess rest request: is not filtered out by the missing roles filter", logOutput.size(), is(0)); } else { @@ -1177,7 +1191,7 @@ public void testRolesFilter() throws Exception { logOutput.clear(); threadContext.stashContext(); - auditTrail.authenticationSuccess("_realm", authentication.getUser(), "_action", message); + auditTrail.authenticationSuccess(randomAlphaOfLength(8), "_realm", authentication.getUser(), "_action", message); if (filterMissingRoles) { assertThat("AuthenticationSuccess message: is not filtered out by the missing roles filter", logOutput.size(), is(0)); } else { @@ -1249,7 +1263,7 @@ public void testIndicesFilter() throws Exception { final LoggingAuditTrail auditTrail = new LoggingAuditTrail(settingsBuilder.build(), clusterService, logger, threadContext); final List logOutput = CapturingLogger.output(logger.getName(), Level.INFO); // anonymous accessDenied - auditTrail.anonymousAccessDenied("_action", noIndexMessage); + auditTrail.anonymousAccessDenied(randomAlphaOfLength(8), "_action", noIndexMessage); if (filterMissingIndices) { assertThat("Anonymous message no index: not filtered out by the missing indices filter", logOutput.size(), is(0)); } else { @@ -1258,17 +1272,17 @@ public void testIndicesFilter() throws Exception { logOutput.clear(); threadContext.stashContext(); - auditTrail.anonymousAccessDenied("_action", new MockIndicesRequest(threadContext, unfilteredIndices)); + auditTrail.anonymousAccessDenied(randomAlphaOfLength(8), "_action", new MockIndicesRequest(threadContext, unfilteredIndices)); assertThat("Anonymous message unfiltered indices: filtered out by indices filters", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.anonymousAccessDenied("_action", new MockIndicesRequest(threadContext, filteredIndices)); + auditTrail.anonymousAccessDenied(randomAlphaOfLength(8), "_action", new MockIndicesRequest(threadContext, filteredIndices)); assertThat("Anonymous message filtered indices: not filtered out by indices filters", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); - auditTrail.anonymousAccessDenied(getRestRequest()); + auditTrail.anonymousAccessDenied(randomAlphaOfLength(8), getRestRequest()); if (filterMissingIndices) { assertThat("Anonymous rest request: not filtered out by the missing indices filter", logOutput.size(), is(0)); } else { @@ -1278,7 +1292,7 @@ public void testIndicesFilter() throws Exception { threadContext.stashContext(); // authenticationFailed - auditTrail.authenticationFailed(getRestRequest()); + auditTrail.authenticationFailed(randomAlphaOfLength(8), getRestRequest()); if (filterMissingIndices) { assertThat("AuthenticationFailed no token rest request: not filtered out by the missing indices filter", logOutput.size(), is(0)); @@ -1288,7 +1302,7 @@ public void testIndicesFilter() throws Exception { logOutput.clear(); threadContext.stashContext(); - auditTrail.authenticationFailed(authToken, "_action", noIndexMessage); + auditTrail.authenticationFailed(randomAlphaOfLength(8), authToken, "_action", noIndexMessage); if (filterMissingIndices) { assertThat("AuthenticationFailed token request no index: not filtered out by the missing indices filter", logOutput.size(), is(0)); @@ -1298,17 +1312,19 @@ public void testIndicesFilter() throws Exception { logOutput.clear(); threadContext.stashContext(); - auditTrail.authenticationFailed(authToken, "_action", new MockIndicesRequest(threadContext, unfilteredIndices)); + auditTrail.authenticationFailed(randomAlphaOfLength(8), authToken, "_action", + new MockIndicesRequest(threadContext, unfilteredIndices)); assertThat("AuthenticationFailed token request unfiltered indices: filtered out by indices filter", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.authenticationFailed(authToken, "_action", new MockIndicesRequest(threadContext, filteredIndices)); + auditTrail.authenticationFailed(randomAlphaOfLength(8), authToken, "_action", + new MockIndicesRequest(threadContext, filteredIndices)); assertThat("AuthenticationFailed token request filtered indices: not filtered out by indices filter", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); - auditTrail.authenticationFailed("_action", noIndexMessage); + auditTrail.authenticationFailed(randomAlphaOfLength(8), "_action", noIndexMessage); if (filterMissingIndices) { assertThat("AuthenticationFailed no token message no index: not filtered out by the missing indices filter", logOutput.size(), is(0)); @@ -1318,17 +1334,17 @@ public void testIndicesFilter() throws Exception { logOutput.clear(); threadContext.stashContext(); - auditTrail.authenticationFailed("_action", new MockIndicesRequest(threadContext, unfilteredIndices)); + auditTrail.authenticationFailed(randomAlphaOfLength(8), "_action", new MockIndicesRequest(threadContext, unfilteredIndices)); assertThat("AuthenticationFailed no token request unfiltered indices: filtered out by indices filter", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.authenticationFailed("_action", new MockIndicesRequest(threadContext, filteredIndices)); + auditTrail.authenticationFailed(randomAlphaOfLength(8), "_action", new MockIndicesRequest(threadContext, filteredIndices)); assertThat("AuthenticationFailed no token request filtered indices: not filtered out by indices filter", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); - auditTrail.authenticationFailed(authToken, getRestRequest()); + auditTrail.authenticationFailed(randomAlphaOfLength(8), authToken, getRestRequest()); if (filterMissingIndices) { assertThat("AuthenticationFailed rest request: not filtered out by the missing indices filter", logOutput.size(), is(0)); } else { @@ -1337,7 +1353,7 @@ public void testIndicesFilter() throws Exception { logOutput.clear(); threadContext.stashContext(); - auditTrail.authenticationFailed("_realm", authToken, "_action", noIndexMessage); + auditTrail.authenticationFailed(randomAlphaOfLength(8), "_realm", authToken, "_action", noIndexMessage); if (filterMissingIndices) { assertThat("AuthenticationFailed realm message no index: not filtered out by the missing indices filter", logOutput.size(), is(0)); @@ -1347,17 +1363,19 @@ public void testIndicesFilter() throws Exception { logOutput.clear(); threadContext.stashContext(); - auditTrail.authenticationFailed("_realm", authToken, "_action", new MockIndicesRequest(threadContext, unfilteredIndices)); + auditTrail.authenticationFailed(randomAlphaOfLength(8), "_realm", authToken, "_action", + new MockIndicesRequest(threadContext, unfilteredIndices)); assertThat("AuthenticationFailed realm message unfiltered indices: filtered out by indices filter", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.authenticationFailed("_realm", authToken, "_action", new MockIndicesRequest(threadContext, filteredIndices)); + auditTrail.authenticationFailed(randomAlphaOfLength(8), "_realm", authToken, "_action", + new MockIndicesRequest(threadContext, filteredIndices)); assertThat("AuthenticationFailed realm message filtered indices: not filtered out by indices filter", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); - auditTrail.authenticationFailed("_realm", authToken, getRestRequest()); + auditTrail.authenticationFailed(randomAlphaOfLength(8), "_realm", authToken, getRestRequest()); if (filterMissingIndices) { assertThat("AuthenticationFailed realm rest request: not filtered out by the missing indices filter", logOutput.size(), is(0)); } else { @@ -1367,7 +1385,7 @@ public void testIndicesFilter() throws Exception { threadContext.stashContext(); // accessGranted - auditTrail.accessGranted(authentication, "_action", noIndexMessage, new String[] { "role1" }); + auditTrail.accessGranted(randomAlphaOfLength(8), authentication, "_action", noIndexMessage, new String[] { "role1" }); if (filterMissingIndices) { assertThat("AccessGranted message no index: not filtered out by the missing indices filter", logOutput.size(), is(0)); } else { @@ -1376,20 +1394,21 @@ public void testIndicesFilter() throws Exception { logOutput.clear(); threadContext.stashContext(); - auditTrail.accessGranted(authentication, "_action", new MockIndicesRequest(threadContext, unfilteredIndices), + auditTrail.accessGranted(randomAlphaOfLength(8), authentication, "_action", + new MockIndicesRequest(threadContext, unfilteredIndices), new String[] { "role1" }); assertThat("AccessGranted message unfiltered indices: filtered out by indices filter", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.accessGranted(authentication, "_action", new MockIndicesRequest(threadContext, filteredIndices), + auditTrail.accessGranted(randomAlphaOfLength(8), authentication, "_action", new MockIndicesRequest(threadContext, filteredIndices), new String[] { "role1" }); assertThat("AccessGranted message filtered indices: not filtered out by indices filter", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); - auditTrail.accessGranted(createAuthentication(SystemUser.INSTANCE, "effectiveRealmName"), "internal:_action", noIndexMessage, - new String[] { "role1" }); + auditTrail.accessGranted(randomAlphaOfLength(8), createAuthentication(SystemUser.INSTANCE, "effectiveRealmName"), + "internal:_action", noIndexMessage, new String[] { "role1" }); if (filterMissingIndices) { assertThat("AccessGranted message system user no index: not filtered out by the missing indices filter", logOutput.size(), is(0)); @@ -1399,22 +1418,20 @@ public void testIndicesFilter() throws Exception { logOutput.clear(); threadContext.stashContext(); - auditTrail.accessGranted(createAuthentication(SystemUser.INSTANCE, "effectiveRealmName"), "internal:_action", - new MockIndicesRequest(threadContext, unfilteredIndices), - new String[] { "role1" }); + auditTrail.accessGranted(randomAlphaOfLength(8), createAuthentication(SystemUser.INSTANCE, "effectiveRealmName"), + "internal:_action", new MockIndicesRequest(threadContext, unfilteredIndices), new String[] { "role1" }); assertThat("AccessGranted message system user unfiltered indices: filtered out by indices filter", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.accessGranted(createAuthentication(SystemUser.INSTANCE, "effectiveRealmName"), "internal:_action", - new MockIndicesRequest(threadContext, filteredIndices), - new String[] { "role1" }); + auditTrail.accessGranted(randomAlphaOfLength(8), createAuthentication(SystemUser.INSTANCE, "effectiveRealmName"), + "internal:_action", new MockIndicesRequest(threadContext, filteredIndices), new String[] { "role1" }); assertThat("AccessGranted message system user filtered indices: not filtered out by indices filter", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); // accessDenied - auditTrail.accessDenied(authentication, "_action", noIndexMessage, new String[] { "role1" }); + auditTrail.accessDenied(randomAlphaOfLength(8), authentication, "_action", noIndexMessage, new String[] { "role1" }); if (filterMissingIndices) { assertThat("AccessDenied message no index: not filtered out by the missing indices filter", logOutput.size(), is(0)); } else { @@ -1423,20 +1440,20 @@ public void testIndicesFilter() throws Exception { logOutput.clear(); threadContext.stashContext(); - auditTrail.accessDenied(authentication, "_action", new MockIndicesRequest(threadContext, unfilteredIndices), + auditTrail.accessDenied(randomAlphaOfLength(8), authentication, "_action", new MockIndicesRequest(threadContext, unfilteredIndices), new String[] { "role1" }); assertThat("AccessDenied message unfiltered indices: filtered out by indices filter", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.accessDenied(authentication, "_action", new MockIndicesRequest(threadContext, filteredIndices), + auditTrail.accessDenied(randomAlphaOfLength(8), authentication, "_action", new MockIndicesRequest(threadContext, filteredIndices), new String[] { "role1" }); assertThat("AccessDenied message filtered indices: not filtered out by indices filter", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); - auditTrail.accessDenied(createAuthentication(SystemUser.INSTANCE, "effectiveRealmName"), "internal:_action", noIndexMessage, - new String[] { "role1" }); + auditTrail.accessDenied(randomAlphaOfLength(8), createAuthentication(SystemUser.INSTANCE, "effectiveRealmName"), "internal:_action", + noIndexMessage, new String[] { "role1" }); if (filterMissingIndices) { assertThat("AccessDenied message system user no index: not filtered out by the missing indices filter", logOutput.size(), is(0)); @@ -1446,14 +1463,14 @@ public void testIndicesFilter() throws Exception { logOutput.clear(); threadContext.stashContext(); - auditTrail.accessDenied(createAuthentication(SystemUser.INSTANCE, "effectiveRealmName"), "internal:_action", + auditTrail.accessDenied(randomAlphaOfLength(8), createAuthentication(SystemUser.INSTANCE, "effectiveRealmName"), "internal:_action", new MockIndicesRequest(threadContext, unfilteredIndices), new String[] { "role1" }); assertThat("AccessDenied message system user unfiltered indices: filtered out by indices filter", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.accessDenied(createAuthentication(SystemUser.INSTANCE, "effectiveRealmName"), "internal:_action", + auditTrail.accessDenied(randomAlphaOfLength(8), createAuthentication(SystemUser.INSTANCE, "effectiveRealmName"), "internal:_action", new MockIndicesRequest(threadContext, filteredIndices), new String[] { "role1" }); assertThat("AccessGranted message system user filtered indices: not filtered out by indices filter", logOutput.size(), is(0)); @@ -1481,7 +1498,7 @@ public void testIndicesFilter() throws Exception { threadContext.stashContext(); // runAsGranted - auditTrail.runAsGranted(authentication, "_action", noIndexMessage, new String[] { "role1" }); + auditTrail.runAsGranted(randomAlphaOfLength(8), authentication, "_action", noIndexMessage, new String[] { "role1" }); if (filterMissingIndices) { assertThat("RunAsGranted message no index: not filtered out by missing indices filter", logOutput.size(), is(0)); } else { @@ -1490,20 +1507,20 @@ public void testIndicesFilter() throws Exception { logOutput.clear(); threadContext.stashContext(); - auditTrail.runAsGranted(authentication, "_action", new MockIndicesRequest(threadContext, unfilteredIndices), + auditTrail.runAsGranted(randomAlphaOfLength(8), authentication, "_action", new MockIndicesRequest(threadContext, unfilteredIndices), new String[] { "role1" }); assertThat("RunAsGranted message unfiltered indices: filtered out by indices filter", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.runAsGranted(authentication, "_action", new MockIndicesRequest(threadContext, filteredIndices), + auditTrail.runAsGranted(randomAlphaOfLength(8), authentication, "_action", new MockIndicesRequest(threadContext, filteredIndices), new String[] { "role1" }); assertThat("RunAsGranted message filtered indices: not filtered out by indices filter", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); // runAsDenied - auditTrail.runAsDenied(authentication, "_action", noIndexMessage, new String[] { "role1" }); + auditTrail.runAsDenied(randomAlphaOfLength(8), authentication, "_action", noIndexMessage, new String[] { "role1" }); if (filterMissingIndices) { assertThat("RunAsDenied message no index: not filtered out by missing indices filter", logOutput.size(), is(0)); } else { @@ -1512,19 +1529,19 @@ public void testIndicesFilter() throws Exception { logOutput.clear(); threadContext.stashContext(); - auditTrail.runAsDenied(authentication, "_action", new MockIndicesRequest(threadContext, unfilteredIndices), + auditTrail.runAsDenied(randomAlphaOfLength(8), authentication, "_action", new MockIndicesRequest(threadContext, unfilteredIndices), new String[] { "role1" }); assertThat("RunAsDenied message unfiltered indices: filtered out by indices filter", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.runAsDenied(authentication, "_action", new MockIndicesRequest(threadContext, filteredIndices), + auditTrail.runAsDenied(randomAlphaOfLength(8), authentication, "_action", new MockIndicesRequest(threadContext, filteredIndices), new String[] { "role1" }); assertThat("RunAsDenied message filtered indices: not filtered out by indices filter", logOutput.size(), is(0)); logOutput.clear(); threadContext.stashContext(); - auditTrail.runAsDenied(authentication, getRestRequest(), new String[] { "role1" }); + auditTrail.runAsDenied(randomAlphaOfLength(8), authentication, getRestRequest(), new String[] { "role1" }); if (filterMissingIndices) { assertThat("RunAsDenied rest request: not filtered out by missing indices filter", logOutput.size(), is(0)); } else { @@ -1534,7 +1551,7 @@ public void testIndicesFilter() throws Exception { threadContext.stashContext(); // authentication Success - auditTrail.authenticationSuccess("_realm", authentication.getUser(), getRestRequest()); + auditTrail.authenticationSuccess(randomAlphaOfLength(8), "_realm", authentication.getUser(), getRestRequest()); if (filterMissingIndices) { assertThat("AuthenticationSuccess rest request: is not filtered out by the missing indices filter", logOutput.size(), is(0)); } else { @@ -1543,7 +1560,7 @@ public void testIndicesFilter() throws Exception { logOutput.clear(); threadContext.stashContext(); - auditTrail.authenticationSuccess("_realm", authentication.getUser(), "_action", noIndexMessage); + auditTrail.authenticationSuccess(randomAlphaOfLength(8), "_realm", authentication.getUser(), "_action", noIndexMessage); if (filterMissingIndices) { assertThat("AuthenticationSuccess message no index: not filtered out by missing indices filter", logOutput.size(), is(0)); } else { @@ -1552,13 +1569,13 @@ public void testIndicesFilter() throws Exception { logOutput.clear(); threadContext.stashContext(); - auditTrail.authenticationSuccess("_realm", authentication.getUser(), "_action", + auditTrail.authenticationSuccess(randomAlphaOfLength(8), "_realm", authentication.getUser(), "_action", new MockIndicesRequest(threadContext, unfilteredIndices)); assertThat("AuthenticationSuccess message unfiltered indices: filtered out by indices filter", logOutput.size(), is(1)); logOutput.clear(); threadContext.stashContext(); - auditTrail.authenticationSuccess("_realm", authentication.getUser(), "_action", + auditTrail.authenticationSuccess(randomAlphaOfLength(8), "_realm", authentication.getUser(), "_action", new MockIndicesRequest(threadContext, filteredIndices)); assertThat("AuthenticationSuccess message filtered indices: not filtered out by indices filter", logOutput.size(), is(0)); logOutput.clear(); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailTests.java index 7f0bf6703c7e7..3273e97b95fbe 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailTests.java @@ -38,6 +38,7 @@ import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; import org.elasticsearch.xpack.core.security.user.SystemUser; import org.elasticsearch.xpack.core.security.user.User; +import org.elasticsearch.xpack.security.audit.AuditUtil; import org.elasticsearch.xpack.security.rest.RemoteHostHeader; import org.elasticsearch.xpack.security.transport.filter.IPFilter; import org.elasticsearch.xpack.security.transport.filter.SecurityIpFilterRule; @@ -200,12 +201,14 @@ public void clearLog() throws Exception { public void testAnonymousAccessDeniedTransport() throws Exception { final TransportMessage message = randomBoolean() ? new MockMessage(threadContext) : new MockIndicesRequest(threadContext); - auditTrail.anonymousAccessDenied("_action", message); + final String requestId = randomRequestId(); + auditTrail.anonymousAccessDenied(requestId, "_action", message); final MapBuilder checkedFields = new MapBuilder<>(commonFields); final MapBuilder checkedArrayFields = new MapBuilder<>(); checkedFields.put(LoggingAuditTrail.EVENT_TYPE_FIELD_NAME, LoggingAuditTrail.TRANSPORT_ORIGIN_FIELD_VALUE) - .put(LoggingAuditTrail.EVENT_ACTION_FIELD_NAME, "anonymous_access_denied") - .put(LoggingAuditTrail.ACTION_FIELD_NAME, "_action"); + .put(LoggingAuditTrail.EVENT_ACTION_FIELD_NAME, "anonymous_access_denied") + .put(LoggingAuditTrail.ACTION_FIELD_NAME, "_action") + .put(LoggingAuditTrail.REQUEST_ID_FIELD_NAME, requestId); indicesRequest(message, checkedFields, checkedArrayFields); restOrTransportOrigin(message, threadContext, checkedFields); opaqueId(threadContext, checkedFields); @@ -218,7 +221,7 @@ public void testAnonymousAccessDeniedTransport() throws Exception { .put("xpack.security.audit.logfile.events.exclude", "anonymous_access_denied") .build(); auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.anonymousAccessDenied("_action", message); + auditTrail.anonymousAccessDenied(requestId, "_action", message); assertEmptyLog(logger); } @@ -229,7 +232,8 @@ public void testAnonymousAccessDeniedRest() throws Exception { final String expectedMessage = tuple.v1().expectedMessage(); final RestRequest request = tuple.v2(); - auditTrail.anonymousAccessDenied(request); + final String requestId = randomRequestId(); + auditTrail.anonymousAccessDenied(requestId, request); final MapBuilder checkedFields = new MapBuilder<>(commonFields); checkedFields.put(LoggingAuditTrail.EVENT_TYPE_FIELD_NAME, LoggingAuditTrail.REST_ORIGIN_FIELD_VALUE) .put(LoggingAuditTrail.EVENT_ACTION_FIELD_NAME, "anonymous_access_denied") @@ -237,6 +241,7 @@ public void testAnonymousAccessDeniedRest() throws Exception { .put(LoggingAuditTrail.ORIGIN_ADDRESS_FIELD_NAME, NetworkAddress.format(address)) .put(LoggingAuditTrail.REQUEST_BODY_FIELD_NAME, includeRequestBody && Strings.hasLength(expectedMessage) ? expectedMessage : null) + .put(LoggingAuditTrail.REQUEST_ID_FIELD_NAME, requestId) .put(LoggingAuditTrail.URL_PATH_FIELD_NAME, "_uri") .put(LoggingAuditTrail.URL_QUERY_FIELD_NAME, null); opaqueId(threadContext, checkedFields); @@ -249,7 +254,7 @@ public void testAnonymousAccessDeniedRest() throws Exception { .put("xpack.security.audit.logfile.events.exclude", "anonymous_access_denied") .build(); auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.anonymousAccessDenied(request); + auditTrail.anonymousAccessDenied(requestId, request); assertEmptyLog(logger); } @@ -257,14 +262,16 @@ public void testAuthenticationFailed() throws Exception { final AuthenticationToken mockToken = new MockToken(); final TransportMessage message = randomBoolean() ? new MockMessage(threadContext) : new MockIndicesRequest(threadContext); - auditTrail.authenticationFailed(mockToken, "_action", message); + final String requestId = randomRequestId(); + auditTrail.authenticationFailed(requestId, mockToken, "_action", message); final MapBuilder checkedArrayFields = new MapBuilder<>(); final MapBuilder checkedFields = new MapBuilder<>(commonFields); checkedFields.put(LoggingAuditTrail.EVENT_TYPE_FIELD_NAME, LoggingAuditTrail.TRANSPORT_ORIGIN_FIELD_VALUE) .put(LoggingAuditTrail.EVENT_ACTION_FIELD_NAME, "authentication_failed") .put(LoggingAuditTrail.ACTION_FIELD_NAME, "_action") .put(LoggingAuditTrail.PRINCIPAL_FIELD_NAME, mockToken.principal()) - .put(LoggingAuditTrail.REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()); + .put(LoggingAuditTrail.REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()) + .put(LoggingAuditTrail.REQUEST_ID_FIELD_NAME, requestId); restOrTransportOrigin(message, threadContext, checkedFields); indicesRequest(message, checkedFields, checkedArrayFields); opaqueId(threadContext, checkedFields); @@ -277,20 +284,22 @@ public void testAuthenticationFailed() throws Exception { .put("xpack.security.audit.logfile.events.exclude", "authentication_failed") .build(); auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.authenticationFailed(new MockToken(), "_action", message); + auditTrail.authenticationFailed(requestId, new MockToken(), "_action", message); assertEmptyLog(logger); } public void testAuthenticationFailedNoToken() throws Exception { final TransportMessage message = randomBoolean() ? new MockMessage(threadContext) : new MockIndicesRequest(threadContext); - auditTrail.authenticationFailed("_action", message); + final String requestId = randomRequestId(); + auditTrail.authenticationFailed(requestId, "_action", message); final MapBuilder checkedFields = new MapBuilder<>(commonFields); final MapBuilder checkedArrayFields = new MapBuilder<>(); checkedFields.put(LoggingAuditTrail.EVENT_TYPE_FIELD_NAME, LoggingAuditTrail.TRANSPORT_ORIGIN_FIELD_VALUE) .put(LoggingAuditTrail.EVENT_ACTION_FIELD_NAME, "authentication_failed") .put(LoggingAuditTrail.ACTION_FIELD_NAME, "_action") - .put(LoggingAuditTrail.REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()); + .put(LoggingAuditTrail.REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()) + .put(LoggingAuditTrail.REQUEST_ID_FIELD_NAME, requestId); restOrTransportOrigin(message, threadContext, checkedFields); indicesRequest(message, checkedFields, checkedArrayFields); opaqueId(threadContext, checkedFields); @@ -303,7 +312,7 @@ public void testAuthenticationFailedNoToken() throws Exception { .put("xpack.security.audit.logfile.events.exclude", "authentication_failed") .build(); auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.authenticationFailed("_action", message); + auditTrail.authenticationFailed(requestId, "_action", message); assertEmptyLog(logger); } @@ -319,7 +328,8 @@ public void testAuthenticationFailedRest() throws Exception { final RestRequest request = tuple.v2(); final AuthenticationToken mockToken = new MockToken(); - auditTrail.authenticationFailed(mockToken, request); + final String requestId = randomRequestId(); + auditTrail.authenticationFailed(requestId, mockToken, request); final MapBuilder checkedFields = new MapBuilder<>(commonFields); checkedFields.put(LoggingAuditTrail.EVENT_TYPE_FIELD_NAME, LoggingAuditTrail.REST_ORIGIN_FIELD_VALUE) .put(LoggingAuditTrail.EVENT_ACTION_FIELD_NAME, "authentication_failed") @@ -329,6 +339,7 @@ public void testAuthenticationFailedRest() throws Exception { .put(LoggingAuditTrail.ORIGIN_ADDRESS_FIELD_NAME, NetworkAddress.format(address)) .put(LoggingAuditTrail.REQUEST_BODY_FIELD_NAME, includeRequestBody && Strings.hasLength(expectedMessage) ? expectedMessage : null) + .put(LoggingAuditTrail.REQUEST_ID_FIELD_NAME, requestId) .put(LoggingAuditTrail.URL_PATH_FIELD_NAME, "_uri") .put(LoggingAuditTrail.URL_QUERY_FIELD_NAME, params.isEmpty() ? null : "foo=bar"); opaqueId(threadContext, checkedFields); @@ -341,7 +352,7 @@ public void testAuthenticationFailedRest() throws Exception { .put("xpack.security.audit.logfile.events.exclude", "authentication_failed") .build(); auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.authenticationFailed(new MockToken(), request); + auditTrail.authenticationFailed(requestId, new MockToken(), request); assertEmptyLog(logger); } @@ -356,7 +367,8 @@ public void testAuthenticationFailedRestNoToken() throws Exception { final String expectedMessage = tuple.v1().expectedMessage(); final RestRequest request = tuple.v2(); - auditTrail.authenticationFailed(request); + final String requestId = randomRequestId(); + auditTrail.authenticationFailed(requestId, request); final MapBuilder checkedFields = new MapBuilder<>(commonFields); checkedFields.put(LoggingAuditTrail.EVENT_TYPE_FIELD_NAME, LoggingAuditTrail.REST_ORIGIN_FIELD_VALUE) .put(LoggingAuditTrail.EVENT_ACTION_FIELD_NAME, "authentication_failed") @@ -366,6 +378,7 @@ public void testAuthenticationFailedRestNoToken() throws Exception { .put(LoggingAuditTrail.ORIGIN_ADDRESS_FIELD_NAME, NetworkAddress.format(address)) .put(LoggingAuditTrail.REQUEST_BODY_FIELD_NAME, includeRequestBody && Strings.hasLength(expectedMessage) ? expectedMessage : null) + .put(LoggingAuditTrail.REQUEST_ID_FIELD_NAME, requestId) .put(LoggingAuditTrail.URL_PATH_FIELD_NAME, "_uri") .put(LoggingAuditTrail.URL_QUERY_FIELD_NAME, params.isEmpty() ? null : "bar=baz"); opaqueId(threadContext, checkedFields); @@ -378,7 +391,7 @@ public void testAuthenticationFailedRestNoToken() throws Exception { .put("xpack.security.audit.logfile.events.exclude", "authentication_failed") .build(); auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.authenticationFailed(request); + auditTrail.authenticationFailed(requestId, request); assertEmptyLog(logger); } @@ -386,7 +399,8 @@ public void testAuthenticationFailedRealm() throws Exception { final AuthenticationToken mockToken = new MockToken(); final TransportMessage message = randomBoolean() ? new MockMessage(threadContext) : new MockIndicesRequest(threadContext); final String realm = randomAlphaOfLengthBetween(1, 6); - auditTrail.authenticationFailed(realm, mockToken, "_action", message); + final String requestId = randomRequestId(); + auditTrail.authenticationFailed(requestId, realm, mockToken, "_action", message); assertEmptyLog(logger); // test enabled @@ -395,7 +409,7 @@ public void testAuthenticationFailedRealm() throws Exception { .put("xpack.security.audit.logfile.events.include", "realm_authentication_failed") .build(); auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.authenticationFailed(realm, mockToken, "_action", message); + auditTrail.authenticationFailed(requestId, realm, mockToken, "_action", message); final MapBuilder checkedFields = new MapBuilder<>(commonFields); final MapBuilder checkedArrayFields = new MapBuilder<>(); checkedFields.put(LoggingAuditTrail.EVENT_TYPE_FIELD_NAME, LoggingAuditTrail.TRANSPORT_ORIGIN_FIELD_VALUE) @@ -403,7 +417,8 @@ public void testAuthenticationFailedRealm() throws Exception { .put(LoggingAuditTrail.REALM_FIELD_NAME, realm) .put(LoggingAuditTrail.PRINCIPAL_FIELD_NAME, mockToken.principal()) .put(LoggingAuditTrail.ACTION_FIELD_NAME, "_action") - .put(LoggingAuditTrail.REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()); + .put(LoggingAuditTrail.REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()) + .put(LoggingAuditTrail.REQUEST_ID_FIELD_NAME, requestId); restOrTransportOrigin(message, threadContext, checkedFields); indicesRequest(message, checkedFields, checkedArrayFields); opaqueId(threadContext, checkedFields); @@ -422,7 +437,8 @@ public void testAuthenticationFailedRealmRest() throws Exception { final RestRequest request = tuple.v2(); final AuthenticationToken mockToken = new MockToken(); final String realm = randomAlphaOfLengthBetween(1, 6); - auditTrail.authenticationFailed(realm, mockToken, request); + final String requestId = randomRequestId(); + auditTrail.authenticationFailed(requestId, realm, mockToken, request); assertEmptyLog(logger); // test enabled @@ -431,7 +447,7 @@ public void testAuthenticationFailedRealmRest() throws Exception { .put("xpack.security.audit.logfile.events.include", "realm_authentication_failed") .build(); auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.authenticationFailed(realm, mockToken, request); + auditTrail.authenticationFailed(requestId, realm, mockToken, request); final MapBuilder checkedFields = new MapBuilder<>(commonFields); checkedFields.put(LoggingAuditTrail.EVENT_TYPE_FIELD_NAME, LoggingAuditTrail.REST_ORIGIN_FIELD_VALUE) .put(LoggingAuditTrail.EVENT_ACTION_FIELD_NAME, "realm_authentication_failed") @@ -442,6 +458,7 @@ public void testAuthenticationFailedRealmRest() throws Exception { .put(LoggingAuditTrail.ACTION_FIELD_NAME, null) .put(LoggingAuditTrail.REQUEST_BODY_FIELD_NAME, includeRequestBody && Strings.hasLength(expectedMessage) ? expectedMessage : null) + .put(LoggingAuditTrail.REQUEST_ID_FIELD_NAME, requestId) .put(LoggingAuditTrail.URL_PATH_FIELD_NAME, "_uri") .put(LoggingAuditTrail.URL_QUERY_FIELD_NAME, params.isEmpty() ? null : "_param=baz"); opaqueId(threadContext, checkedFields); @@ -452,14 +469,16 @@ public void testAccessGranted() throws Exception { final TransportMessage message = randomBoolean() ? new MockMessage(threadContext) : new MockIndicesRequest(threadContext); final String[] roles = randomArray(0, 4, String[]::new, () -> randomBoolean() ? null : randomAlphaOfLengthBetween(1, 4)); final Authentication authentication = createAuthentication(); + final String requestId = randomRequestId(); - auditTrail.accessGranted(authentication, "_action", message, roles); + auditTrail.accessGranted(requestId, authentication, "_action", message, roles); final MapBuilder checkedFields = new MapBuilder<>(commonFields); final MapBuilder checkedArrayFields = new MapBuilder<>(); checkedFields.put(LoggingAuditTrail.EVENT_TYPE_FIELD_NAME, LoggingAuditTrail.TRANSPORT_ORIGIN_FIELD_VALUE) .put(LoggingAuditTrail.EVENT_ACTION_FIELD_NAME, "access_granted") .put(LoggingAuditTrail.ACTION_FIELD_NAME, "_action") - .put(LoggingAuditTrail.REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()); + .put(LoggingAuditTrail.REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()) + .put(LoggingAuditTrail.REQUEST_ID_FIELD_NAME, requestId); checkedArrayFields.put(LoggingAuditTrail.PRINCIPAL_ROLES_FIELD_NAME, roles); subject(authentication, checkedFields); restOrTransportOrigin(message, threadContext, checkedFields); @@ -474,7 +493,7 @@ public void testAccessGranted() throws Exception { .put("xpack.security.audit.logfile.events.exclude", "access_granted") .build(); auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.accessGranted(authentication, "_action", message, roles); + auditTrail.accessGranted(requestId, authentication, "_action", message, roles); assertEmptyLog(logger); } @@ -482,7 +501,8 @@ public void testAccessGrantedInternalSystemAction() throws Exception { final TransportMessage message = randomBoolean() ? new MockMessage(threadContext) : new MockIndicesRequest(threadContext); final String[] roles = randomArray(0, 4, String[]::new, () -> randomBoolean() ? null : randomAlphaOfLengthBetween(1, 4)); final Authentication authentication = new Authentication(SystemUser.INSTANCE, new RealmRef("_reserved", "test", "foo"), null); - auditTrail.accessGranted(authentication, "internal:_action", message, roles); + final String requestId = randomRequestId(); + auditTrail.accessGranted(requestId, authentication, "internal:_action", message, roles); assertEmptyLog(logger); // test enabled @@ -491,7 +511,7 @@ public void testAccessGrantedInternalSystemAction() throws Exception { .put("xpack.security.audit.logfile.events.include", "system_access_granted") .build(); auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.accessGranted(authentication, "internal:_action", message, roles); + auditTrail.accessGranted(requestId, authentication, "internal:_action", message, roles); final MapBuilder checkedFields = new MapBuilder<>(commonFields); final MapBuilder checkedArrayFields = new MapBuilder<>(); checkedFields.put(LoggingAuditTrail.EVENT_TYPE_FIELD_NAME, LoggingAuditTrail.TRANSPORT_ORIGIN_FIELD_VALUE) @@ -499,7 +519,8 @@ public void testAccessGrantedInternalSystemAction() throws Exception { .put(LoggingAuditTrail.PRINCIPAL_FIELD_NAME, SystemUser.INSTANCE.principal()) .put(LoggingAuditTrail.PRINCIPAL_REALM_FIELD_NAME, "_reserved") .put(LoggingAuditTrail.ACTION_FIELD_NAME, "internal:_action") - .put(LoggingAuditTrail.REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()); + .put(LoggingAuditTrail.REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()) + .put(LoggingAuditTrail.REQUEST_ID_FIELD_NAME, requestId); checkedArrayFields.put(LoggingAuditTrail.PRINCIPAL_ROLES_FIELD_NAME, roles); restOrTransportOrigin(message, threadContext, checkedFields); indicesRequest(message, checkedFields, checkedArrayFields); @@ -511,14 +532,16 @@ public void testAccessGrantedInternalSystemActionNonSystemUser() throws Exceptio final TransportMessage message = randomBoolean() ? new MockMessage(threadContext) : new MockIndicesRequest(threadContext); final String[] roles = randomArray(0, 4, String[]::new, () -> randomBoolean() ? null : randomAlphaOfLengthBetween(1, 4)); final Authentication authentication = createAuthentication(); + final String requestId = randomRequestId(); - auditTrail.accessGranted(authentication, "internal:_action", message, roles); + auditTrail.accessGranted(requestId, authentication, "internal:_action", message, roles); final MapBuilder checkedFields = new MapBuilder<>(commonFields); final MapBuilder checkedArrayFields = new MapBuilder<>(); checkedFields.put(LoggingAuditTrail.EVENT_TYPE_FIELD_NAME, LoggingAuditTrail.TRANSPORT_ORIGIN_FIELD_VALUE) - .put(LoggingAuditTrail.EVENT_ACTION_FIELD_NAME, "access_granted") - .put(LoggingAuditTrail.ACTION_FIELD_NAME, "internal:_action") - .put(LoggingAuditTrail.REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()); + .put(LoggingAuditTrail.EVENT_ACTION_FIELD_NAME, "access_granted") + .put(LoggingAuditTrail.ACTION_FIELD_NAME, "internal:_action") + .put(LoggingAuditTrail.REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()) + .put(LoggingAuditTrail.REQUEST_ID_FIELD_NAME, requestId); checkedArrayFields.put(LoggingAuditTrail.PRINCIPAL_ROLES_FIELD_NAME, roles); subject(authentication, checkedFields); restOrTransportOrigin(message, threadContext, checkedFields); @@ -533,7 +556,7 @@ public void testAccessGrantedInternalSystemActionNonSystemUser() throws Exceptio .put("xpack.security.audit.logfile.events.exclude", "access_granted") .build(); auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.accessGranted(authentication, "internal:_action", message, roles); + auditTrail.accessGranted(requestId, authentication, "internal:_action", message, roles); assertEmptyLog(logger); } @@ -541,14 +564,16 @@ public void testAccessDenied() throws Exception { final TransportMessage message = randomBoolean() ? new MockMessage(threadContext) : new MockIndicesRequest(threadContext); final String[] roles = randomArray(0, 4, String[]::new, () -> randomBoolean() ? null : randomAlphaOfLengthBetween(1, 4)); final Authentication authentication = createAuthentication(); + final String requestId = randomRequestId(); - auditTrail.accessDenied(authentication, "_action/bar", message, roles); + auditTrail.accessDenied(requestId, authentication, "_action/bar", message, roles); final MapBuilder checkedFields = new MapBuilder<>(commonFields); final MapBuilder checkedArrayFields = new MapBuilder<>(); checkedFields.put(LoggingAuditTrail.EVENT_TYPE_FIELD_NAME, LoggingAuditTrail.TRANSPORT_ORIGIN_FIELD_VALUE) .put(LoggingAuditTrail.EVENT_ACTION_FIELD_NAME, "access_denied") .put(LoggingAuditTrail.ACTION_FIELD_NAME, "_action/bar") - .put(LoggingAuditTrail.REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()); + .put(LoggingAuditTrail.REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()) + .put(LoggingAuditTrail.REQUEST_ID_FIELD_NAME, requestId); checkedArrayFields.put(LoggingAuditTrail.PRINCIPAL_ROLES_FIELD_NAME, roles); subject(authentication, checkedFields); restOrTransportOrigin(message, threadContext, checkedFields); @@ -563,7 +588,7 @@ public void testAccessDenied() throws Exception { .put("xpack.security.audit.logfile.events.exclude", "access_denied") .build(); auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.accessDenied(authentication, "_action", message, roles); + auditTrail.accessDenied(requestId, authentication, "_action", message, roles); assertEmptyLog(logger); } @@ -577,7 +602,8 @@ public void testTamperedRequestRest() throws Exception { final Tuple tuple = prepareRestContent("_uri", address, params); final String expectedMessage = tuple.v1().expectedMessage(); final RestRequest request = tuple.v2(); - auditTrail.tamperedRequest(request); + final String requestId = randomRequestId(); + auditTrail.tamperedRequest(requestId, request); final MapBuilder checkedFields = new MapBuilder<>(commonFields); checkedFields.put(LoggingAuditTrail.EVENT_TYPE_FIELD_NAME, LoggingAuditTrail.REST_ORIGIN_FIELD_VALUE) .put(LoggingAuditTrail.EVENT_ACTION_FIELD_NAME, "tampered_request") @@ -585,6 +611,7 @@ public void testTamperedRequestRest() throws Exception { .put(LoggingAuditTrail.ORIGIN_ADDRESS_FIELD_NAME, NetworkAddress.format(address)) .put(LoggingAuditTrail.REQUEST_BODY_FIELD_NAME, includeRequestBody && Strings.hasLength(expectedMessage) ? expectedMessage : null) + .put(LoggingAuditTrail.REQUEST_ID_FIELD_NAME, requestId) .put(LoggingAuditTrail.URL_PATH_FIELD_NAME, "_uri") .put(LoggingAuditTrail.URL_QUERY_FIELD_NAME, params.isEmpty() ? null : "_param=baz"); opaqueId(threadContext, checkedFields); @@ -597,20 +624,22 @@ public void testTamperedRequestRest() throws Exception { .put("xpack.security.audit.logfile.events.exclude", "tampered_request") .build(); auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.tamperedRequest(request); + auditTrail.tamperedRequest(requestId, request); assertEmptyLog(logger); } public void testTamperedRequest() throws Exception { final TransportMessage message = randomBoolean() ? new MockMessage(threadContext) : new MockIndicesRequest(threadContext); - auditTrail.tamperedRequest("_action", message); + final String requestId = randomRequestId(); + auditTrail.tamperedRequest(requestId, "_action", message); final MapBuilder checkedFields = new MapBuilder<>(commonFields); final MapBuilder checkedArrayFields = new MapBuilder<>(); checkedFields.put(LoggingAuditTrail.EVENT_TYPE_FIELD_NAME, LoggingAuditTrail.TRANSPORT_ORIGIN_FIELD_VALUE) .put(LoggingAuditTrail.EVENT_ACTION_FIELD_NAME, "tampered_request") .put(LoggingAuditTrail.ACTION_FIELD_NAME, "_action") - .put(LoggingAuditTrail.REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()); + .put(LoggingAuditTrail.REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()) + .put(LoggingAuditTrail.REQUEST_ID_FIELD_NAME, requestId); restOrTransportOrigin(message, threadContext, checkedFields); indicesRequest(message, checkedFields, checkedArrayFields); opaqueId(threadContext, checkedFields); @@ -623,7 +652,7 @@ public void testTamperedRequest() throws Exception { .put("xpack.security.audit.logfile.events.exclude", "tampered_request") .build(); auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.tamperedRequest("_action", message); + auditTrail.tamperedRequest(requestId, "_action", message); assertEmptyLog(logger); } @@ -637,13 +666,15 @@ public void testTamperedRequestWithUser() throws Exception { user = new User("_username", new String[] { "r1" }); } - auditTrail.tamperedRequest(user, "_action", message); + final String requestId = randomRequestId(); + auditTrail.tamperedRequest(requestId, user, "_action", message); final MapBuilder checkedFields = new MapBuilder<>(commonFields); final MapBuilder checkedArrayFields = new MapBuilder<>(); checkedFields.put(LoggingAuditTrail.EVENT_TYPE_FIELD_NAME, LoggingAuditTrail.TRANSPORT_ORIGIN_FIELD_VALUE) .put(LoggingAuditTrail.EVENT_ACTION_FIELD_NAME, "tampered_request") .put(LoggingAuditTrail.ACTION_FIELD_NAME, "_action") - .put(LoggingAuditTrail.REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()); + .put(LoggingAuditTrail.REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()) + .put(LoggingAuditTrail.REQUEST_ID_FIELD_NAME, requestId); if (runAs) { checkedFields.put(LoggingAuditTrail.PRINCIPAL_FIELD_NAME, "running_as"); checkedFields.put(LoggingAuditTrail.PRINCIPAL_RUN_BY_FIELD_NAME, "_username"); @@ -662,7 +693,7 @@ public void testTamperedRequestWithUser() throws Exception { .put("xpack.security.audit.logfile.events.exclude", "tampered_request") .build(); auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.tamperedRequest(user, "_action", message); + auditTrail.tamperedRequest(requestId, user, "_action", message); assertEmptyLog(logger); } @@ -726,8 +757,9 @@ public void testRunAsGranted() throws Exception { new User("running as", new String[] { "r2" }, new User("_username", new String[] { "r1" })), new RealmRef("authRealm", "test", "foo"), new RealmRef("lookRealm", "up", "by")); + final String requestId = randomRequestId(); - auditTrail.runAsGranted(authentication, "_action", message, roles); + auditTrail.runAsGranted(requestId, authentication, "_action", message, roles); final MapBuilder checkedFields = new MapBuilder<>(commonFields); final MapBuilder checkedArrayFields = new MapBuilder<>(); checkedFields.put(LoggingAuditTrail.EVENT_TYPE_FIELD_NAME, LoggingAuditTrail.TRANSPORT_ORIGIN_FIELD_VALUE) @@ -737,7 +769,8 @@ public void testRunAsGranted() throws Exception { .put(LoggingAuditTrail.PRINCIPAL_RUN_AS_FIELD_NAME, "running as") .put(LoggingAuditTrail.PRINCIPAL_RUN_AS_REALM_FIELD_NAME, "lookRealm") .put(LoggingAuditTrail.ACTION_FIELD_NAME, "_action") - .put(LoggingAuditTrail.REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()); + .put(LoggingAuditTrail.REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()) + .put(LoggingAuditTrail.REQUEST_ID_FIELD_NAME, requestId); checkedArrayFields.put(LoggingAuditTrail.PRINCIPAL_ROLES_FIELD_NAME, roles); restOrTransportOrigin(message, threadContext, checkedFields); indicesRequest(message, checkedFields, checkedArrayFields); @@ -751,7 +784,7 @@ public void testRunAsGranted() throws Exception { .put("xpack.security.audit.logfile.events.exclude", "run_as_granted") .build(); auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.runAsGranted(authentication, "_action", message, roles); + auditTrail.runAsGranted(requestId, authentication, "_action", message, roles); assertEmptyLog(logger); } @@ -762,8 +795,9 @@ public void testRunAsDenied() throws Exception { new User("running as", new String[] { "r2" }, new User("_username", new String[] { "r1" })), new RealmRef("authRealm", "test", "foo"), new RealmRef("lookRealm", "up", "by")); + final String requestId = randomRequestId(); - auditTrail.runAsDenied(authentication, "_action", message, roles); + auditTrail.runAsDenied(requestId, authentication, "_action", message, roles); final MapBuilder checkedFields = new MapBuilder<>(commonFields); final MapBuilder checkedArrayFields = new MapBuilder<>(); checkedFields.put(LoggingAuditTrail.EVENT_TYPE_FIELD_NAME, LoggingAuditTrail.TRANSPORT_ORIGIN_FIELD_VALUE) @@ -773,7 +807,8 @@ public void testRunAsDenied() throws Exception { .put(LoggingAuditTrail.PRINCIPAL_RUN_AS_FIELD_NAME, "running as") .put(LoggingAuditTrail.PRINCIPAL_RUN_AS_REALM_FIELD_NAME, "lookRealm") .put(LoggingAuditTrail.ACTION_FIELD_NAME, "_action") - .put(LoggingAuditTrail.REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()); + .put(LoggingAuditTrail.REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()) + .put(LoggingAuditTrail.REQUEST_ID_FIELD_NAME, requestId); checkedArrayFields.put(LoggingAuditTrail.PRINCIPAL_ROLES_FIELD_NAME, roles); restOrTransportOrigin(message, threadContext, checkedFields); indicesRequest(message, checkedFields, checkedArrayFields); @@ -787,7 +822,7 @@ public void testRunAsDenied() throws Exception { .put("xpack.security.audit.logfile.events.exclude", "run_as_denied") .build(); auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.runAsDenied(authentication, "_action", message, roles); + auditTrail.runAsDenied(requestId, authentication, "_action", message, roles); assertEmptyLog(logger); } @@ -809,9 +844,10 @@ public void testAuthenticationSuccessRest() throws Exception { } else { user = new User("_username", new String[] { "r1" }); } + final String requestId = randomRequestId(); // event by default disabled - auditTrail.authenticationSuccess(realm, user, request); + auditTrail.authenticationSuccess(requestId, realm, user, request); assertEmptyLog(logger); settings = Settings.builder() @@ -819,7 +855,7 @@ public void testAuthenticationSuccessRest() throws Exception { .put("xpack.security.audit.logfile.events.include", "authentication_success") .build(); auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.authenticationSuccess(realm, user, request); + auditTrail.authenticationSuccess(requestId, realm, user, request); final MapBuilder checkedFields = new MapBuilder<>(commonFields); checkedFields.put(LoggingAuditTrail.EVENT_TYPE_FIELD_NAME, LoggingAuditTrail.REST_ORIGIN_FIELD_VALUE) .put(LoggingAuditTrail.EVENT_ACTION_FIELD_NAME, "authentication_success") @@ -828,6 +864,7 @@ public void testAuthenticationSuccessRest() throws Exception { .put(LoggingAuditTrail.ORIGIN_ADDRESS_FIELD_NAME, NetworkAddress.format(address)) .put(LoggingAuditTrail.REQUEST_BODY_FIELD_NAME, includeRequestBody && Strings.hasLength(expectedMessage) ? expectedMessage : null) + .put(LoggingAuditTrail.REQUEST_ID_FIELD_NAME, requestId) .put(LoggingAuditTrail.URL_PATH_FIELD_NAME, "_uri") .put(LoggingAuditTrail.URL_QUERY_FIELD_NAME, params.isEmpty() ? null : "foo=bar&evac=true"); if (user.isRunAs()) { @@ -849,9 +886,10 @@ public void testAuthenticationSuccessTransport() throws Exception { user = new User("_username", new String[] { "r1" }); } final String realm = randomAlphaOfLengthBetween(1, 6); + final String requestId = randomRequestId(); // event by default disabled - auditTrail.authenticationSuccess(realm, user, "_action", message); + auditTrail.authenticationSuccess(requestId, realm, user, "_action", message); assertEmptyLog(logger); settings = Settings.builder() @@ -859,14 +897,15 @@ public void testAuthenticationSuccessTransport() throws Exception { .put("xpack.security.audit.logfile.events.include", "authentication_success") .build(); auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.authenticationSuccess(realm, user, "_action", message); + auditTrail.authenticationSuccess(requestId, realm, user, "_action", message); final MapBuilder checkedFields = new MapBuilder<>(commonFields); final MapBuilder checkedArrayFields = new MapBuilder<>(); checkedFields.put(LoggingAuditTrail.EVENT_TYPE_FIELD_NAME, LoggingAuditTrail.TRANSPORT_ORIGIN_FIELD_VALUE) .put(LoggingAuditTrail.EVENT_ACTION_FIELD_NAME, "authentication_success") .put(LoggingAuditTrail.ACTION_FIELD_NAME, "_action") .put(LoggingAuditTrail.REALM_FIELD_NAME, realm) - .put(LoggingAuditTrail.REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()); + .put(LoggingAuditTrail.REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()) + .put(LoggingAuditTrail.REQUEST_ID_FIELD_NAME, requestId); if (user.isRunAs()) { checkedFields.put(LoggingAuditTrail.PRINCIPAL_FIELD_NAME, "running as"); checkedFields.put(LoggingAuditTrail.PRINCIPAL_RUN_BY_FIELD_NAME, "_username"); @@ -895,37 +934,37 @@ public void testRequestsWithoutIndices() throws Exception { final List output = CapturingLogger.output(logger.getName(), Level.INFO); int logEntriesCount = 1; for (final TransportMessage message : messages) { - auditTrail.anonymousAccessDenied("_action", message); + auditTrail.anonymousAccessDenied("_req_id", "_action", message); assertThat(output.size(), is(logEntriesCount++)); assertThat(output.get(logEntriesCount - 2), not(containsString("indices="))); - auditTrail.authenticationFailed(new MockToken(), "_action", message); + auditTrail.authenticationFailed("_req_id", new MockToken(), "_action", message); assertThat(output.size(), is(logEntriesCount++)); assertThat(output.get(logEntriesCount - 2), not(containsString("indices="))); - auditTrail.authenticationFailed("_action", message); + auditTrail.authenticationFailed("_req_id", "_action", message); assertThat(output.size(), is(logEntriesCount++)); assertThat(output.get(logEntriesCount - 2), not(containsString("indices="))); - auditTrail.authenticationFailed(realm, new MockToken(), "_action", message); + auditTrail.authenticationFailed("_req_id", realm, new MockToken(), "_action", message); assertThat(output.size(), is(logEntriesCount++)); assertThat(output.get(logEntriesCount - 2), not(containsString("indices="))); - auditTrail.accessGranted(createAuthentication(), "_action", message, new String[] { role }); + auditTrail.accessGranted("_req_id", createAuthentication(), "_action", message, new String[]{role}); assertThat(output.size(), is(logEntriesCount++)); assertThat(output.get(logEntriesCount - 2), not(containsString("indices="))); - auditTrail.accessDenied(createAuthentication(), "_action", message, new String[] { role }); + auditTrail.accessDenied("_req_id", createAuthentication(), "_action", message, new String[]{role}); assertThat(output.size(), is(logEntriesCount++)); assertThat(output.get(logEntriesCount - 2), not(containsString("indices="))); - auditTrail.tamperedRequest("_action", message); + auditTrail.tamperedRequest("_req_id", "_action", message); assertThat(output.size(), is(logEntriesCount++)); assertThat(output.get(logEntriesCount - 2), not(containsString("indices="))); - auditTrail.tamperedRequest(user, "_action", message); + auditTrail.tamperedRequest("_req_id", user, "_action", message); assertThat(output.size(), is(logEntriesCount++)); assertThat(output.get(logEntriesCount - 2), not(containsString("indices="))); - auditTrail.runAsGranted(createAuthentication(), "_action", message, new String[] { role }); + auditTrail.runAsGranted("_req_id", createAuthentication(), "_action", message, new String[]{role}); assertThat(output.size(), is(logEntriesCount++)); assertThat(output.get(logEntriesCount - 2), not(containsString("indices="))); - auditTrail.runAsDenied(createAuthentication(), "_action", message, new String[] { role }); + auditTrail.runAsDenied("_req_id", createAuthentication(), "_action", message, new String[]{role}); assertThat(output.size(), is(logEntriesCount++)); assertThat(output.get(logEntriesCount - 2), not(containsString("indices="))); - auditTrail.authenticationSuccess(realm, user, "_action", message); + auditTrail.authenticationSuccess("_req_id", realm, user, "_action", message); assertThat(output.size(), is(logEntriesCount++)); assertThat(output.get(logEntriesCount - 2), not(containsString("indices="))); } @@ -1092,6 +1131,13 @@ public void clearCredentials() { } } + /** + * @return A tuple of ( id-to-pass-to-audit-trail, id-to-check-in-audit-message ) + */ + private String randomRequestId() { + return randomBoolean() ? randomAlphaOfLengthBetween(8, 24) : AuditUtil.generateRequestId(threadContext); + } + private static void restOrTransportOrigin(TransportMessage message, ThreadContext threadContext, MapBuilder checkedFields) { final InetSocketAddress restAddress = RemoteHostHeader.restRemoteAddress(threadContext); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java index 1c5bf44fd46bc..becd6069a2526 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java @@ -66,6 +66,7 @@ import org.elasticsearch.xpack.core.security.user.SystemUser; import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.audit.AuditTrailService; +import org.elasticsearch.xpack.security.audit.AuditUtil; import org.elasticsearch.xpack.security.authc.AuthenticationService.Authenticator; import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm; import org.elasticsearch.xpack.security.support.SecurityIndexManager; @@ -94,12 +95,15 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.isEmptyOrNullString; +import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.sameInstance; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; +import static org.mockito.Matchers.same; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; @@ -221,6 +225,7 @@ public void testTokenFirstMissingSecondFound() throws Exception { } public void testTokenMissing() throws Exception { + final String reqId = AuditUtil.getOrGenerateRequestId(threadContext); PlainActionFuture future = new PlainActionFuture<>(); Authenticator authenticator = service.createAuthenticator("_action", message, null, future); authenticator.extractToken((token) -> { @@ -230,7 +235,7 @@ public void testTokenMissing() throws Exception { ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, () -> future.actionGet()); assertThat(e.getMessage(), containsString("missing authentication token")); - verify(auditTrail).anonymousAccessDenied("_action", message); + verify(auditTrail).anonymousAccessDenied(reqId, "_action", message); verifyNoMoreInteractions(auditTrail); } @@ -246,6 +251,7 @@ public void testAuthenticateBothSupportSecondSucceeds() throws Exception { } else { when(secondRealm.token(threadContext)).thenReturn(token); } + final String reqId = AuditUtil.getOrGenerateRequestId(threadContext); final AtomicBoolean completed = new AtomicBoolean(false); service.authenticate("_action", message, (User)null, ActionListener.wrap(result -> { @@ -257,7 +263,7 @@ public void testAuthenticateBothSupportSecondSucceeds() throws Exception { setCompletedToTrue(completed); }, this::logAndFail)); assertTrue(completed.get()); - verify(auditTrail).authenticationFailed(firstRealm.name(), token, "_action", message); + verify(auditTrail).authenticationFailed(reqId, firstRealm.name(), token, "_action", message); } public void testAuthenticateFirstNotSupportingSecondSucceeds() throws Exception { @@ -266,6 +272,7 @@ public void testAuthenticateFirstNotSupportingSecondSucceeds() throws Exception when(secondRealm.supports(token)).thenReturn(true); mockAuthenticate(secondRealm, token, user); when(secondRealm.token(threadContext)).thenReturn(token); + final String reqId = AuditUtil.getOrGenerateRequestId(threadContext); final AtomicBoolean completed = new AtomicBoolean(false); service.authenticate("_action", message, (User)null, ActionListener.wrap(result -> { @@ -274,7 +281,7 @@ public void testAuthenticateFirstNotSupportingSecondSucceeds() throws Exception assertThreadContextContainsAuthentication(result); setCompletedToTrue(completed); }, this::logAndFail)); - verify(auditTrail).authenticationSuccess(secondRealm.name(), user, "_action", message); + verify(auditTrail).authenticationSuccess(reqId, secondRealm.name(), user, "_action", message); verifyNoMoreInteractions(auditTrail); verify(firstRealm, never()).authenticate(eq(token), any(ActionListener.class)); assertTrue(completed.get()); @@ -336,6 +343,7 @@ public void authenticationInContextAndHeader() throws Exception { public void testAuthenticateTransportAnonymous() throws Exception { when(firstRealm.token(threadContext)).thenReturn(null); when(secondRealm.token(threadContext)).thenReturn(null); + final String reqId = AuditUtil.getOrGenerateRequestId(threadContext); try { authenticateBlocking("_action", message, null); fail("expected an authentication exception when trying to authenticate an anonymous message"); @@ -343,7 +351,7 @@ public void testAuthenticateTransportAnonymous() throws Exception { // expected assertAuthenticationException(e); } - verify(auditTrail).anonymousAccessDenied("_action", message); + verify(auditTrail).anonymousAccessDenied(reqId, "_action", message); } public void testAuthenticateRestAnonymous() throws Exception { @@ -356,7 +364,8 @@ public void testAuthenticateRestAnonymous() throws Exception { // expected assertAuthenticationException(e); } - verify(auditTrail).anonymousAccessDenied(restRequest); + String reqId = expectAuditRequestId(); + verify(auditTrail).anonymousAccessDenied(reqId, restRequest); } public void testAuthenticateTransportFallback() throws Exception { @@ -371,6 +380,7 @@ public void testAuthenticateTransportFallback() throws Exception { } public void testAuthenticateTransportDisabledUser() throws Exception { + final String reqId = AuditUtil.getOrGenerateRequestId(threadContext); User user = new User("username", new String[] { "r1", "r2" }, null, null, null, false); User fallback = randomBoolean() ? SystemUser.INSTANCE : null; when(firstRealm.token(threadContext)).thenReturn(token); @@ -379,7 +389,7 @@ public void testAuthenticateTransportDisabledUser() throws Exception { ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, () -> authenticateBlocking("_action", message, fallback)); - verify(auditTrail).authenticationFailed(token, "_action", message); + verify(auditTrail).authenticationFailed(reqId, token, "_action", message); verifyNoMoreInteractions(auditTrail); assertAuthenticationException(e); } @@ -392,12 +402,14 @@ public void testAuthenticateRestDisabledUser() throws Exception { ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, () -> authenticateBlocking(restRequest)); - verify(auditTrail).authenticationFailed(token, restRequest); + String reqId = expectAuditRequestId(); + verify(auditTrail).authenticationFailed(reqId, token, restRequest); verifyNoMoreInteractions(auditTrail); assertAuthenticationException(e); } public void testAuthenticateTransportSuccess() throws Exception { + final String reqId = AuditUtil.getOrGenerateRequestId(threadContext); User user = new User("username", "r1", "r2"); User fallback = randomBoolean() ? SystemUser.INSTANCE : null; when(firstRealm.token(threadContext)).thenReturn(token); @@ -412,7 +424,7 @@ public void testAuthenticateTransportSuccess() throws Exception { setCompletedToTrue(completed); }, this::logAndFail)); - verify(auditTrail).authenticationSuccess(firstRealm.name(), user, "_action", message); + verify(auditTrail).authenticationSuccess(reqId, firstRealm.name(), user, "_action", message); verifyNoMoreInteractions(auditTrail); assertTrue(completed.get()); } @@ -430,7 +442,8 @@ public void testAuthenticateRestSuccess() throws Exception { assertThreadContextContainsAuthentication(authentication); setCompletedToTrue(completed); }, this::logAndFail)); - verify(auditTrail).authenticationSuccess(firstRealm.name(), user1, restRequest); + String reqId = expectAuditRequestId(); + verify(auditTrail).authenticationSuccess(reqId, firstRealm.name(), user1, restRequest); verifyNoMoreInteractions(auditTrail); assertTrue(completed.get()); } @@ -516,12 +529,13 @@ public void testAutheticateTransportContextAndHeader() throws Exception { public void testAuthenticateTamperedUser() throws Exception { InternalMessage message = new InternalMessage(); threadContext.putHeader(AuthenticationField.AUTHENTICATION_KEY, "_signed_auth"); + final String reqId = AuditUtil.getOrGenerateRequestId(threadContext); try { authenticateBlocking("_action", message, randomBoolean() ? SystemUser.INSTANCE : null); } catch (Exception e) { //expected - verify(auditTrail).tamperedRequest("_action", message); + verify(auditTrail).tamperedRequest(reqId, "_action", message); verifyNoMoreInteractions(auditTrail); } } @@ -544,7 +558,8 @@ public void testAnonymousUserRest() throws Exception { assertThat(result, notNullValue()); assertThat(result.getUser(), sameInstance((Object) anonymousUser)); assertThreadContextContainsAuthentication(result); - verify(auditTrail).authenticationSuccess("__anonymous", new AnonymousUser(settings), request); + String reqId = expectAuditRequestId(); + verify(auditTrail).authenticationSuccess(reqId, "__anonymous", new AnonymousUser(settings), request); verifyNoMoreInteractions(auditTrail); } @@ -580,13 +595,14 @@ public void testAnonymousUserTransportWithDefaultUser() throws Exception { } public void testRealmTokenThrowingException() throws Exception { + final String reqId = AuditUtil.getOrGenerateRequestId(threadContext); when(firstRealm.token(threadContext)).thenThrow(authenticationError("realm doesn't like tokens")); try { authenticateBlocking("_action", message, null); fail("exception should bubble out"); } catch (ElasticsearchException e) { assertThat(e.getMessage(), is("realm doesn't like tokens")); - verify(auditTrail).authenticationFailed("_action", message); + verify(auditTrail).authenticationFailed(reqId, "_action", message); } } @@ -597,7 +613,8 @@ public void testRealmTokenThrowingExceptionRest() throws Exception { fail("exception should bubble out"); } catch (ElasticsearchException e) { assertThat(e.getMessage(), is("realm doesn't like tokens")); - verify(auditTrail).authenticationFailed(restRequest); + String reqId = expectAuditRequestId(); + verify(auditTrail).authenticationFailed(reqId, restRequest); } } @@ -605,12 +622,13 @@ public void testRealmSupportsMethodThrowingException() throws Exception { AuthenticationToken token = mock(AuthenticationToken.class); when(secondRealm.token(threadContext)).thenReturn(token); when(secondRealm.supports(token)).thenThrow(authenticationError("realm doesn't like supports")); + final String reqId = AuditUtil.getOrGenerateRequestId(threadContext); try { authenticateBlocking("_action", message, null); fail("exception should bubble out"); } catch (ElasticsearchException e) { assertThat(e.getMessage(), is("realm doesn't like supports")); - verify(auditTrail).authenticationFailed(token, "_action", message); + verify(auditTrail).authenticationFailed(reqId, token, "_action", message); } } @@ -623,11 +641,13 @@ public void testRealmSupportsMethodThrowingExceptionRest() throws Exception { fail("exception should bubble out"); } catch (ElasticsearchException e) { assertThat(e.getMessage(), is("realm doesn't like supports")); - verify(auditTrail).authenticationFailed(token, restRequest); + String reqId = expectAuditRequestId(); + verify(auditTrail).authenticationFailed(reqId, token, restRequest); } } public void testRealmAuthenticateTerminatingAuthenticationProcess() throws Exception { + final String reqId = AuditUtil.getOrGenerateRequestId(threadContext); final AuthenticationToken token = mock(AuthenticationToken.class); when(secondRealm.token(threadContext)).thenReturn(token); when(secondRealm.supports(token)).thenReturn(true); @@ -663,8 +683,8 @@ public void testRealmAuthenticateTerminatingAuthenticationProcess() throws Excep assertThat(e.getHeader("WWW-Authenticate"), contains(basicScheme)); } } - verify(auditTrail).authenticationFailed(secondRealm.name(), token, "_action", message); - verify(auditTrail).authenticationFailed(token, "_action", message); + verify(auditTrail).authenticationFailed(reqId, secondRealm.name(), token, "_action", message); + verify(auditTrail).authenticationFailed(reqId, token, "_action", message); verifyNoMoreInteractions(auditTrail); } @@ -674,12 +694,13 @@ public void testRealmAuthenticateThrowingException() throws Exception { when(secondRealm.supports(token)).thenReturn(true); doThrow(authenticationError("realm doesn't like authenticate")) .when(secondRealm).authenticate(eq(token), any(ActionListener.class)); + final String reqId = AuditUtil.getOrGenerateRequestId(threadContext); try { authenticateBlocking("_action", message, null); fail("exception should bubble out"); } catch (ElasticsearchException e) { assertThat(e.getMessage(), is("realm doesn't like authenticate")); - verify(auditTrail).authenticationFailed(token, "_action", message); + verify(auditTrail).authenticationFailed(reqId, token, "_action", message); } } @@ -694,7 +715,8 @@ public void testRealmAuthenticateThrowingExceptionRest() throws Exception { fail("exception should bubble out"); } catch (ElasticsearchSecurityException e) { assertThat(e.getMessage(), is("realm doesn't like authenticate")); - verify(auditTrail).authenticationFailed(token, restRequest); + String reqId = expectAuditRequestId(); + verify(auditTrail).authenticationFailed(reqId, token, restRequest); } } @@ -707,13 +729,14 @@ public void testRealmLookupThrowingException() throws Exception { mockRealmLookupReturnsNull(firstRealm, "run_as"); doThrow(authenticationError("realm doesn't want to lookup")) .when(secondRealm).lookupUser(eq("run_as"), any(ActionListener.class)); + final String reqId = AuditUtil.getOrGenerateRequestId(threadContext); try { authenticateBlocking("_action", message, null); fail("exception should bubble out"); } catch (ElasticsearchException e) { assertThat(e.getMessage(), is("realm doesn't want to lookup")); - verify(auditTrail).authenticationFailed(token, "_action", message); + verify(auditTrail).authenticationFailed(reqId, token, "_action", message); } } @@ -726,13 +749,13 @@ public void testRealmLookupThrowingExceptionRest() throws Exception { mockRealmLookupReturnsNull(firstRealm, "run_as"); doThrow(authenticationError("realm doesn't want to lookup")) .when(secondRealm).lookupUser(eq("run_as"), any(ActionListener.class)); - try { authenticateBlocking(restRequest); fail("exception should bubble out"); } catch (ElasticsearchException e) { assertThat(e.getMessage(), is("realm doesn't want to lookup")); - verify(auditTrail).authenticationFailed(token, restRequest); + String reqId = expectAuditRequestId(); + verify(auditTrail).authenticationFailed(reqId, token, restRequest); } } @@ -831,7 +854,8 @@ public void testRunAsWithEmptyRunAsUsernameRest() throws Exception { authenticateBlocking(restRequest); fail("exception should be thrown"); } catch (ElasticsearchException e) { - verify(auditTrail).runAsDenied(any(Authentication.class), eq(restRequest), eq(Role.EMPTY.names())); + String reqId = expectAuditRequestId(); + verify(auditTrail).runAsDenied(eq(reqId), any(Authentication.class), eq(restRequest), eq(Role.EMPTY.names())); verifyNoMoreInteractions(auditTrail); } } @@ -840,6 +864,7 @@ public void testRunAsWithEmptyRunAsUsername() throws Exception { AuthenticationToken token = mock(AuthenticationToken.class); User user = new User("lookup user", new String[]{"user"}); threadContext.putHeader(AuthenticationServiceField.RUN_AS_USER_HEADER, ""); + final String reqId = AuditUtil.getOrGenerateRequestId(threadContext); when(secondRealm.token(threadContext)).thenReturn(token); when(secondRealm.supports(token)).thenReturn(true); mockAuthenticate(secondRealm, token, user); @@ -848,7 +873,7 @@ public void testRunAsWithEmptyRunAsUsername() throws Exception { authenticateBlocking("_action", message, null); fail("exception should be thrown"); } catch (ElasticsearchException e) { - verify(auditTrail).runAsDenied(any(Authentication.class), eq("_action"), eq(message), eq(Role.EMPTY.names())); + verify(auditTrail).runAsDenied(eq(reqId), any(Authentication.class), eq("_action"), eq(message), eq(Role.EMPTY.names())); verifyNoMoreInteractions(auditTrail); } } @@ -856,6 +881,7 @@ public void testRunAsWithEmptyRunAsUsername() throws Exception { public void testAuthenticateTransportDisabledRunAsUser() throws Exception { AuthenticationToken token = mock(AuthenticationToken.class); threadContext.putHeader(AuthenticationServiceField.RUN_AS_USER_HEADER, "run_as"); + final String reqId = AuditUtil.getOrGenerateRequestId(threadContext); when(secondRealm.token(threadContext)).thenReturn(token); when(secondRealm.supports(token)).thenReturn(true); mockAuthenticate(secondRealm, token, new User("lookup user", new String[]{"user"})); @@ -868,7 +894,7 @@ public void testAuthenticateTransportDisabledRunAsUser() throws Exception { User fallback = randomBoolean() ? SystemUser.INSTANCE : null; ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, () -> authenticateBlocking("_action", message, fallback)); - verify(auditTrail).authenticationFailed(token, "_action", message); + verify(auditTrail).authenticationFailed(reqId, token, "_action", message); verifyNoMoreInteractions(auditTrail); assertAuthenticationException(e); } @@ -888,7 +914,8 @@ public void testAuthenticateRestDisabledRunAsUser() throws Exception { ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, () -> authenticateBlocking(restRequest)); - verify(auditTrail).authenticationFailed(token, restRequest); + String reqId = expectAuditRequestId(); + verify(auditTrail).authenticationFailed(reqId, token, restRequest); verifyNoMoreInteractions(auditTrail); assertAuthenticationException(e); } @@ -920,7 +947,7 @@ public void testAuthenticateWithToken() throws Exception { }, this::logAndFail)); } assertTrue(completed.get()); - verify(auditTrail).authenticationSuccess("realm", user, "_action", message); + verify(auditTrail).authenticationSuccess(anyString(), eq("realm"), eq(user), eq("_action"), same(message)); verifyNoMoreInteractions(auditTrail); } @@ -970,7 +997,8 @@ public void testInvalidToken() throws Exception { // we need to use a latch here because the key computation goes async on another thread! latch.await(); if (success.get()) { - verify(auditTrail).authenticationSuccess(firstRealm.name(), user, "_action", message); + final String realmName = firstRealm.name(); + verify(auditTrail).authenticationSuccess(anyString(), eq(realmName), eq(user), eq("_action"), same(message)); } verifyNoMoreInteractions(auditTrail); } @@ -1083,6 +1111,12 @@ private Authentication authenticateBlocking(String action, TransportMessage mess return future.actionGet(); } + private String expectAuditRequestId() { + String reqId = AuditUtil.extractRequestId(threadContext); + assertThat(reqId, not(isEmptyOrNullString())); + return reqId; + } + private static void mockRealmLookupReturnsNull(Realm realm, String username) { doAnswer((i) -> { ActionListener listener = (ActionListener) i.getArguments()[1]; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java index cdb2be1e08cef..589d039927869 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java @@ -125,6 +125,7 @@ import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.security.user.XPackUser; import org.elasticsearch.xpack.security.audit.AuditTrailService; +import org.elasticsearch.xpack.security.audit.AuditUtil; import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm; import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore; import org.elasticsearch.xpack.security.authz.store.NativePrivilegeStore; @@ -241,6 +242,7 @@ private void authorize(Authentication authentication, String action, TransportRe public void testActionsForSystemUserIsAuthorized() { final TransportRequest request = mock(TransportRequest.class); + final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); // A failure would throw an exception final Authentication authentication = createAuthentication(SystemUser.INSTANCE); @@ -249,7 +251,7 @@ public void testActionsForSystemUserIsAuthorized() { "indices:admin/settings/update" }; for (String action : actions) { authorize(authentication, action, request); - verify(auditTrail).accessGranted(authentication, action, request, new String[] { SystemUser.ROLE_NAME }); + verify(auditTrail).accessGranted(requestId, authentication, action, request, new String[] { SystemUser.ROLE_NAME }); } verifyNoMoreInteractions(auditTrail); @@ -258,30 +260,35 @@ public void testActionsForSystemUserIsAuthorized() { public void testIndicesActionsForSystemUserWhichAreNotAuthorized() { final TransportRequest request = mock(TransportRequest.class); final Authentication authentication = createAuthentication(SystemUser.INSTANCE); + final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); assertThrowsAuthorizationException( () -> authorize(authentication, "indices:", request), "indices:", SystemUser.INSTANCE.principal()); - verify(auditTrail).accessDenied(authentication, "indices:", request, new String[]{SystemUser.ROLE_NAME}); + verify(auditTrail).accessDenied(requestId, authentication, "indices:", request, new String[]{SystemUser.ROLE_NAME}); verifyNoMoreInteractions(auditTrail); } public void testClusterAdminActionsForSystemUserWhichAreNotAuthorized() { final TransportRequest request = mock(TransportRequest.class); final Authentication authentication = createAuthentication(SystemUser.INSTANCE); + final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); assertThrowsAuthorizationException( () -> authorize(authentication, "cluster:admin/whatever", request), "cluster:admin/whatever", SystemUser.INSTANCE.principal()); - verify(auditTrail).accessDenied(authentication, "cluster:admin/whatever", request, new String[] { SystemUser.ROLE_NAME }); + verify(auditTrail).accessDenied(requestId, authentication, "cluster:admin/whatever", request, + new String[] { SystemUser.ROLE_NAME }); verifyNoMoreInteractions(auditTrail); } public void testClusterAdminSnapshotStatusActionForSystemUserWhichIsNotAuthorized() { final TransportRequest request = mock(TransportRequest.class); final Authentication authentication = createAuthentication(SystemUser.INSTANCE); + final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); assertThrowsAuthorizationException( () -> authorize(authentication, "cluster:admin/snapshot/status", request), "cluster:admin/snapshot/status", SystemUser.INSTANCE.principal()); - verify(auditTrail).accessDenied(authentication, "cluster:admin/snapshot/status", request, new String[] { SystemUser.ROLE_NAME }); + verify(auditTrail).accessDenied(requestId, authentication, "cluster:admin/snapshot/status", request, + new String[] { SystemUser.ROLE_NAME }); verifyNoMoreInteractions(auditTrail); } @@ -296,11 +303,12 @@ public void testAuthorizeUsingConditionalPrivileges() { final ConditionalClusterPrivilege[] conditionalClusterPrivileges = new ConditionalClusterPrivilege[] { conditionalClusterPrivilege }; + final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); RoleDescriptor role = new RoleDescriptor("role1", null, null, null, conditionalClusterPrivileges, null, null ,null); roleMap.put("role1", role); authorize(authentication, DeletePrivilegesAction.NAME, request); - verify(auditTrail).accessGranted(authentication, DeletePrivilegesAction.NAME, request, new String[]{role.getName()}); + verify(auditTrail).accessGranted(requestId, authentication, DeletePrivilegesAction.NAME, request, new String[]{role.getName()}); verifyNoMoreInteractions(auditTrail); } @@ -315,13 +323,14 @@ public void testAuthorizationDeniedWhenConditionalPrivilegesDoNotMatch() { final ConditionalClusterPrivilege[] conditionalClusterPrivileges = new ConditionalClusterPrivilege[] { conditionalClusterPrivilege }; + final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); RoleDescriptor role = new RoleDescriptor("role1", null, null, null, conditionalClusterPrivileges, null, null ,null); roleMap.put("role1", role); assertThrowsAuthorizationException( () -> authorize(authentication, DeletePrivilegesAction.NAME, request), DeletePrivilegesAction.NAME, "user1"); - verify(auditTrail).accessDenied(authentication, DeletePrivilegesAction.NAME, request, new String[]{role.getName()}); + verify(auditTrail).accessDenied(requestId, authentication, DeletePrivilegesAction.NAME, request, new String[]{role.getName()}); verifyNoMoreInteractions(auditTrail); } @@ -329,10 +338,11 @@ public void testNoRolesCausesDenial() { final TransportRequest request = new SearchRequest(); final Authentication authentication = createAuthentication(new User("test user")); mockEmptyMetaData(); + final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); assertThrowsAuthorizationException( () -> authorize(authentication, "indices:a", request), "indices:a", "test user"); - verify(auditTrail).accessDenied(authentication, "indices:a", request, Role.EMPTY.names()); + verify(auditTrail).accessDenied(requestId, authentication, "indices:a", request, Role.EMPTY.names()); verifyNoMoreInteractions(auditTrail); } @@ -341,8 +351,9 @@ public void testUserWithNoRolesCanPerformRemoteSearch() { request.indices("other_cluster:index1", "*_cluster:index2", "other_cluster:other_*"); final Authentication authentication = createAuthentication(new User("test user")); mockEmptyMetaData(); + final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); authorize(authentication, SearchAction.NAME, request); - verify(auditTrail).accessGranted(authentication, SearchAction.NAME, request, Role.EMPTY.names()); + verify(auditTrail).accessGranted(requestId, authentication, SearchAction.NAME, request, Role.EMPTY.names()); verifyNoMoreInteractions(auditTrail); } @@ -356,10 +367,11 @@ public void testUserWithNoRolesCannotPerformLocalSearch() { request.indices("no_such_cluster:index"); final Authentication authentication = createAuthentication(new User("test user")); mockEmptyMetaData(); + final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); assertThrowsAuthorizationException( () -> authorize(authentication, SearchAction.NAME, request), SearchAction.NAME, "test user"); - verify(auditTrail).accessDenied(authentication, SearchAction.NAME, request, Role.EMPTY.names()); + verify(auditTrail).accessDenied(requestId, authentication, SearchAction.NAME, request, Role.EMPTY.names()); verifyNoMoreInteractions(auditTrail); } @@ -372,10 +384,11 @@ public void testUserWithNoRolesCanPerformMultiClusterSearch() { request.indices("local_index", "wildcard_*", "other_cluster:remote_index", "*:foo?"); final Authentication authentication = createAuthentication(new User("test user")); mockEmptyMetaData(); + final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); assertThrowsAuthorizationException( () -> authorize(authentication, SearchAction.NAME, request), SearchAction.NAME, "test user"); - verify(auditTrail).accessDenied(authentication, SearchAction.NAME, request, Role.EMPTY.names()); + verify(auditTrail).accessDenied(requestId, authentication, SearchAction.NAME, request, Role.EMPTY.names()); verifyNoMoreInteractions(auditTrail); } @@ -383,10 +396,11 @@ public void testUserWithNoRolesCannotSql() { TransportRequest request = new SqlQueryRequest(); Authentication authentication = createAuthentication(new User("test user")); mockEmptyMetaData(); + final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); assertThrowsAuthorizationException( () -> authorize(authentication, SqlQueryAction.NAME, request), SqlQueryAction.NAME, "test user"); - verify(auditTrail).accessDenied(authentication, SqlQueryAction.NAME, request, Role.EMPTY.names()); + verify(auditTrail).accessDenied(requestId, authentication, SqlQueryAction.NAME, request, Role.EMPTY.names()); verifyNoMoreInteractions(auditTrail); } /** @@ -398,10 +412,11 @@ public void testRemoteIndicesOnlyWorkWithApplicableRequestTypes() { request.indices("other_cluster:index1", "other_cluster:index2"); final Authentication authentication = createAuthentication(new User("test user")); mockEmptyMetaData(); + final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); assertThrowsAuthorizationException( () -> authorize(authentication, DeleteIndexAction.NAME, request), DeleteIndexAction.NAME, "test user"); - verify(auditTrail).accessDenied(authentication, DeleteIndexAction.NAME, request, Role.EMPTY.names()); + verify(auditTrail).accessDenied(requestId, authentication, DeleteIndexAction.NAME, request, Role.EMPTY.names()); verifyNoMoreInteractions(auditTrail); } @@ -413,16 +428,18 @@ public void testUnknownRoleCausesDenial() { String action = tuple.v1(); TransportRequest request = tuple.v2(); final Authentication authentication = createAuthentication(new User("test user", "non-existent-role")); + final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); mockEmptyMetaData(); assertThrowsAuthorizationException( () -> authorize(authentication, action, request), action, "test user"); - verify(auditTrail).accessDenied(authentication, action, request, Role.EMPTY.names()); + verify(auditTrail).accessDenied(requestId, authentication, action, request, Role.EMPTY.names()); verifyNoMoreInteractions(auditTrail); } public void testThatNonIndicesAndNonClusterActionIsDenied() { final TransportRequest request = mock(TransportRequest.class); + final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); final Authentication authentication = createAuthentication(new User("test user", "a_all")); final RoleDescriptor role = new RoleDescriptor("a_role", null, new IndicesPrivileges[]{IndicesPrivileges.builder().indices("a").privileges("all").build()}, null); @@ -431,7 +448,7 @@ public void testThatNonIndicesAndNonClusterActionIsDenied() { assertThrowsAuthorizationException( () -> authorize(authentication, "whatever", request), "whatever", "test user"); - verify(auditTrail).accessDenied(authentication, "whatever", request, new String[]{role.getName()}); + verify(auditTrail).accessDenied(requestId, authentication, "whatever", request, new String[]{role.getName()}); verifyNoMoreInteractions(auditTrail); } @@ -443,6 +460,7 @@ public void testThatRoleWithNoIndicesIsDenied() { new Tuple<>(SqlQueryAction.NAME, new SqlQueryRequest())); String action = tuple.v1(); TransportRequest request = tuple.v2(); + final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); final Authentication authentication = createAuthentication(new User("test user", "no_indices")); RoleDescriptor role = new RoleDescriptor("a_role", null, null, null); roleMap.put("no_indices", role); @@ -451,22 +469,24 @@ public void testThatRoleWithNoIndicesIsDenied() { assertThrowsAuthorizationException( () -> authorize(authentication, action, request), action, "test user"); - verify(auditTrail).accessDenied(authentication, action, request, new String[]{role.getName()}); + verify(auditTrail).accessDenied(requestId, authentication, action, request, new String[]{role.getName()}); verifyNoMoreInteractions(auditTrail); } public void testElasticUserAuthorizedForNonChangePasswordRequestsWhenNotInSetupMode() { final Authentication authentication = createAuthentication(new ElasticUser(true)); + final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); final Tuple request = randomCompositeRequest(); authorize(authentication, request.v1(), request.v2()); - verify(auditTrail).accessGranted(authentication, request.v1(), request.v2(), new String[]{ElasticUser.ROLE_NAME}); + verify(auditTrail).accessGranted(requestId, authentication, request.v1(), request.v2(), new String[]{ElasticUser.ROLE_NAME}); } public void testSearchAgainstEmptyCluster() { RoleDescriptor role = new RoleDescriptor("a_role", null, new IndicesPrivileges[]{IndicesPrivileges.builder().indices("a").privileges("all").build()}, null); final Authentication authentication = createAuthentication(new User("test user", "a_all")); + final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); roleMap.put("a_all", role); mockEmptyMetaData(); @@ -479,7 +499,7 @@ public void testSearchAgainstEmptyCluster() { assertThrowsAuthorizationException( () -> authorize(authentication, SearchAction.NAME, searchRequest), SearchAction.NAME, "test user"); - verify(auditTrail).accessDenied(authentication, SearchAction.NAME, searchRequest, new String[]{role.getName()}); + verify(auditTrail).accessDenied(requestId, authentication, SearchAction.NAME, searchRequest, new String[]{role.getName()}); verifyNoMoreInteractions(auditTrail); } @@ -488,7 +508,7 @@ public void testSearchAgainstEmptyCluster() { SearchRequest searchRequest = new SearchRequest("does_not_exist") .indicesOptions(IndicesOptions.fromOptions(true, true, true, false)); authorize(authentication, SearchAction.NAME, searchRequest); - verify(auditTrail).accessGranted(authentication, SearchAction.NAME, searchRequest, new String[]{role.getName()}); + verify(auditTrail).accessGranted(requestId, authentication, SearchAction.NAME, searchRequest, new String[]{role.getName()}); final IndicesAccessControl indicesAccessControl = threadContext.getTransient(AuthorizationServiceField.INDICES_PERMISSIONS_KEY); final IndicesAccessControl.IndexAccessControl indexAccessControl = indicesAccessControl.getIndexPermissions(IndicesAndAliasesResolverField.NO_INDEX_PLACEHOLDER); @@ -503,35 +523,38 @@ public void testScrollRelatedRequestsAllowed() { final Authentication authentication = createAuthentication(new User("test user", "a_all")); roleMap.put("a_all", role); mockEmptyMetaData(); + final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); final ClearScrollRequest clearScrollRequest = new ClearScrollRequest(); authorize(authentication, ClearScrollAction.NAME, clearScrollRequest); - verify(auditTrail).accessGranted(authentication, ClearScrollAction.NAME, clearScrollRequest, new String[]{role.getName()}); + verify(auditTrail).accessGranted(requestId, authentication, ClearScrollAction.NAME, clearScrollRequest, + new String[]{role.getName()}); final SearchScrollRequest searchScrollRequest = new SearchScrollRequest(); authorize(authentication, SearchScrollAction.NAME, searchScrollRequest); - verify(auditTrail).accessGranted(authentication, SearchScrollAction.NAME, searchScrollRequest, new String[]{role.getName()}); + verify(auditTrail).accessGranted(requestId, authentication, SearchScrollAction.NAME, searchScrollRequest, + new String[]{role.getName()}); // We have to use a mock request for other Scroll actions as the actual requests are package private to SearchTransportService final TransportRequest request = mock(TransportRequest.class); authorize(authentication, SearchTransportService.CLEAR_SCROLL_CONTEXTS_ACTION_NAME, request); - verify(auditTrail).accessGranted(authentication, SearchTransportService.CLEAR_SCROLL_CONTEXTS_ACTION_NAME, request, + verify(auditTrail).accessGranted(requestId, authentication, SearchTransportService.CLEAR_SCROLL_CONTEXTS_ACTION_NAME, request, new String[]{role.getName()}); authorize(authentication, SearchTransportService.FETCH_ID_SCROLL_ACTION_NAME, request); - verify(auditTrail).accessGranted(authentication, SearchTransportService.FETCH_ID_SCROLL_ACTION_NAME, request, + verify(auditTrail).accessGranted(requestId, authentication, SearchTransportService.FETCH_ID_SCROLL_ACTION_NAME, request, new String[]{role.getName()}); authorize(authentication, SearchTransportService.QUERY_FETCH_SCROLL_ACTION_NAME, request); - verify(auditTrail).accessGranted(authentication, SearchTransportService.QUERY_FETCH_SCROLL_ACTION_NAME, request, + verify(auditTrail).accessGranted(requestId, authentication, SearchTransportService.QUERY_FETCH_SCROLL_ACTION_NAME, request, new String[]{role.getName()}); authorize(authentication, SearchTransportService.QUERY_SCROLL_ACTION_NAME, request); - verify(auditTrail).accessGranted(authentication, SearchTransportService.QUERY_SCROLL_ACTION_NAME, request, + verify(auditTrail).accessGranted(requestId, authentication, SearchTransportService.QUERY_SCROLL_ACTION_NAME, request, new String[]{role.getName()}); authorize(authentication, SearchTransportService.FREE_CONTEXT_SCROLL_ACTION_NAME, request); - verify(auditTrail).accessGranted(authentication, SearchTransportService.FREE_CONTEXT_SCROLL_ACTION_NAME, request, + verify(auditTrail).accessGranted(requestId, authentication, SearchTransportService.FREE_CONTEXT_SCROLL_ACTION_NAME, request, new String[]{role.getName()}); verifyNoMoreInteractions(auditTrail); } @@ -543,11 +566,12 @@ public void testAuthorizeIndicesFailures() { new IndicesPrivileges[]{IndicesPrivileges.builder().indices("a").privileges("all").build()}, null); final Authentication authentication = createAuthentication(new User("test user", "a_all")); roleMap.put("a_all", role); + final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); assertThrowsAuthorizationException( () -> authorize(authentication, "indices:a", request), "indices:a", "test user"); - verify(auditTrail).accessDenied(authentication, "indices:a", request, new String[]{role.getName()}); + verify(auditTrail).accessDenied(requestId, authentication, "indices:a", request, new String[]{role.getName()}); verifyNoMoreInteractions(auditTrail); verify(clusterService, times(1)).state(); verify(state, times(1)).metaData(); @@ -561,11 +585,12 @@ public void testCreateIndexWithAliasWithoutPermissions() { new IndicesPrivileges[]{IndicesPrivileges.builder().indices("a").privileges("all").build()}, null); final Authentication authentication = createAuthentication(new User("test user", "a_all")); roleMap.put("a_all", role); + final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); assertThrowsAuthorizationException( () -> authorize(authentication, CreateIndexAction.NAME, request), IndicesAliasesAction.NAME, "test user"); - verify(auditTrail).accessDenied(authentication, IndicesAliasesAction.NAME, request, new String[]{role.getName()}); + verify(auditTrail).accessDenied(requestId, authentication, IndicesAliasesAction.NAME, request, new String[]{role.getName()}); verifyNoMoreInteractions(auditTrail); verify(clusterService).state(); verify(state, times(1)).metaData(); @@ -579,10 +604,11 @@ public void testCreateIndexWithAlias() { new IndicesPrivileges[]{IndicesPrivileges.builder().indices("a", "a2").privileges("all").build()}, null); final Authentication authentication = createAuthentication(new User("test user", "a_all")); roleMap.put("a_all", role); + final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); authorize(authentication, CreateIndexAction.NAME, request); - verify(auditTrail).accessGranted(authentication, CreateIndexAction.NAME, request, new String[]{role.getName()}); + verify(auditTrail).accessGranted(requestId, authentication, CreateIndexAction.NAME, request, new String[]{role.getName()}); verifyNoMoreInteractions(auditTrail); verify(clusterService).state(); verify(state, times(1)).metaData(); @@ -599,12 +625,13 @@ public void testDenialForAnonymousUser() { RoleDescriptor role = new RoleDescriptor("a_all", null, new IndicesPrivileges[] { IndicesPrivileges.builder().indices("a").privileges("all").build() }, null); roleMap.put("a_all", role); + final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); final Authentication authentication = createAuthentication(anonymousUser); assertThrowsAuthorizationException( () -> authorize(authentication, "indices:a", request), "indices:a", anonymousUser.principal()); - verify(auditTrail).accessDenied(authentication, "indices:a", request, new String[]{role.getName()}); + verify(auditTrail).accessDenied(requestId, authentication, "indices:a", request, new String[]{role.getName()}); verifyNoMoreInteractions(auditTrail); verify(clusterService, times(1)).state(); verify(state, times(1)).metaData(); @@ -624,11 +651,12 @@ public void testDenialForAnonymousUserAuthorizationExceptionDisabled() { RoleDescriptor role = new RoleDescriptor("a_all", null, new IndicesPrivileges[]{IndicesPrivileges.builder().indices("a").privileges("all").build()}, null); roleMap.put("a_all", role); + final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); final ElasticsearchSecurityException securityException = expectThrows(ElasticsearchSecurityException.class, () -> authorize(authentication, "indices:a", request)); assertAuthenticationException(securityException, containsString("action [indices:a] requires authentication")); - verify(auditTrail).accessDenied(authentication, "indices:a", request, new String[]{role.getName()}); + verify(auditTrail).accessDenied(requestId, authentication, "indices:a", request, new String[]{role.getName()}); verifyNoMoreInteractions(auditTrail); verify(clusterService, times(1)).state(); verify(state, times(1)).metaData(); @@ -642,13 +670,14 @@ public void testAuditTrailIsRecordedWhenIndexWildcardThrowsError() { new IndicesPrivileges[]{IndicesPrivileges.builder().indices("a").privileges("all").build()}, null); final Authentication authentication = createAuthentication(new User("test user", "a_all")); roleMap.put("a_all", role); + final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); final IndexNotFoundException nfe = expectThrows( IndexNotFoundException.class, () -> authorize(authentication, GetIndexAction.NAME, request)); assertThat(nfe.getIndex(), is(notNullValue())); assertThat(nfe.getIndex().getName(), is("not-an-index-*")); - verify(auditTrail).accessDenied(authentication, GetIndexAction.NAME, request, new String[]{role.getName()}); + verify(auditTrail).accessDenied(requestId, authentication, GetIndexAction.NAME, request, new String[]{role.getName()}); verifyNoMoreInteractions(auditTrail); verify(clusterService).state(); verify(state, times(1)).metaData(); @@ -656,17 +685,19 @@ public void testAuditTrailIsRecordedWhenIndexWildcardThrowsError() { public void testRunAsRequestWithNoRolesUser() { final TransportRequest request = mock(TransportRequest.class); + final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); final Authentication authentication = createAuthentication(new User("run as me", null, new User("test user", "admin"))); final User user = new User("run as me", null, new User("test user", "admin")); assertNotEquals(authentication.getUser().authenticatedUser(), authentication); assertThrowsAuthorizationExceptionRunAs( () -> authorize(authentication, "indices:a", request), "indices:a", "test user", "run as me"); // run as [run as me] - verify(auditTrail).runAsDenied(authentication, "indices:a", request, Role.EMPTY.names()); + verify(auditTrail).runAsDenied(requestId, authentication, "indices:a", request, Role.EMPTY.names()); verifyNoMoreInteractions(auditTrail); } public void testRunAsRequestWithoutLookedUpBy() { + final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); AuthenticateRequest request = new AuthenticateRequest("run as me"); roleMap.put("can run as", ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR); User user = new User("run as me", Strings.EMPTY_ARRAY, new User("test user", new String[]{"can run as"})); @@ -675,8 +706,8 @@ public void testRunAsRequestWithoutLookedUpBy() { assertThrowsAuthorizationExceptionRunAs( () -> authorize(authentication, AuthenticateAction.NAME, request), AuthenticateAction.NAME, "test user", "run as me"); // run as [run as me] - verify(auditTrail).runAsDenied(authentication, AuthenticateAction.NAME, request, - new String[]{ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR.getName()}); + verify(auditTrail).runAsDenied(requestId, authentication, AuthenticateAction.NAME, request, + new String[] { ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR.getName() }); verifyNoMoreInteractions(auditTrail); } @@ -689,11 +720,12 @@ public void testRunAsRequestRunningAsUnAllowedUser() { new IndicesPrivileges[]{IndicesPrivileges.builder().indices("a").privileges("all").build()}, new String[]{"not the right user"}); roleMap.put("can run as", role); + final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); assertThrowsAuthorizationExceptionRunAs( () -> authorize(authentication, "indices:a", request), "indices:a", "test user", "run as me"); - verify(auditTrail).runAsDenied(authentication, "indices:a", request, new String[]{role.getName()}); + verify(auditTrail).runAsDenied(requestId, authentication, "indices:a", request, new String[]{role.getName()}); verifyNoMoreInteractions(auditTrail); } @@ -723,15 +755,16 @@ public void testRunAsRequestWithRunAsUserWithoutPermission() { } else { mockEmptyMetaData(); } + final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); assertThrowsAuthorizationExceptionRunAs( () -> authorize(authentication, "indices:a", request), "indices:a", "test user", "run as me"); - verify(auditTrail).runAsGranted(authentication, "indices:a", request, new String[]{runAsRole.getName()}); + verify(auditTrail).runAsGranted(requestId, authentication, "indices:a", request, new String[]{runAsRole.getName()}); if (indexExists) { - verify(auditTrail).accessDenied(authentication, "indices:a", request, new String[]{bRole.getName()}); + verify(auditTrail).accessDenied(requestId, authentication, "indices:a", request, new String[]{bRole.getName()}); } else { - verify(auditTrail).accessDenied(authentication, "indices:a", request, Role.EMPTY.names()); + verify(auditTrail).accessDenied(requestId, authentication, "indices:a", request, Role.EMPTY.names()); } verifyNoMoreInteractions(auditTrail); } @@ -756,10 +789,11 @@ public void testRunAsRequestWithValidPermissions() { RoleDescriptor bRole = new RoleDescriptor("b", null, new IndicesPrivileges[]{IndicesPrivileges.builder().indices("b").privileges("all").build()}, null); roleMap.put("b", bRole); + final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); authorize(authentication, "indices:a", request); - verify(auditTrail).runAsGranted(authentication, "indices:a", request, new String[]{runAsRole.getName()}); - verify(auditTrail).accessGranted(authentication, "indices:a", request, new String[]{bRole.getName()}); + verify(auditTrail).runAsGranted(requestId, authentication, "indices:a", request, new String[]{runAsRole.getName()}); + verify(auditTrail).accessGranted(requestId, authentication, "indices:a", request, new String[]{bRole.getName()}); verifyNoMoreInteractions(auditTrail); } @@ -775,6 +809,7 @@ public void testNonXPackUserCannotExecuteOperationAgainstSecurityIndex() { .settings(Settings.builder().put("index.version.created", Version.CURRENT).build()) .numberOfShards(1).numberOfReplicas(0).build(), true) .build()); + final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); List> requests = new ArrayList<>(); requests.add(new Tuple<>(BulkAction.NAME + "[s]", @@ -800,19 +835,19 @@ public void testNonXPackUserCannotExecuteOperationAgainstSecurityIndex() { assertThrowsAuthorizationException( () -> authorize(authentication, action, request), action, "all_access_user"); - verify(auditTrail).accessDenied(authentication, action, request, new String[]{role.getName()}); + verify(auditTrail).accessDenied(requestId, authentication, action, request, new String[]{role.getName()}); verifyNoMoreInteractions(auditTrail); } // we should allow waiting for the health of the index or any index if the user has this permission ClusterHealthRequest request = new ClusterHealthRequest(SECURITY_INDEX_NAME); authorize(authentication, ClusterHealthAction.NAME, request); - verify(auditTrail).accessGranted(authentication, ClusterHealthAction.NAME, request, new String[]{role.getName()}); + verify(auditTrail).accessGranted(requestId, authentication, ClusterHealthAction.NAME, request, new String[]{role.getName()}); // multiple indices request = new ClusterHealthRequest(SECURITY_INDEX_NAME, "foo", "bar"); authorize(authentication, ClusterHealthAction.NAME, request); - verify(auditTrail).accessGranted(authentication, ClusterHealthAction.NAME, request, new String[]{role.getName()}); + verify(auditTrail).accessGranted(requestId, authentication, ClusterHealthAction.NAME, request, new String[]{role.getName()}); verifyNoMoreInteractions(auditTrail); final SearchRequest searchRequest = new SearchRequest("_all"); @@ -833,6 +868,7 @@ public void testGrantedNonXPackUserCanExecuteMonitoringOperationsAgainstSecurity .settings(Settings.builder().put("index.version.created", Version.CURRENT).build()) .numberOfShards(1).numberOfReplicas(0).build(), true) .build()); + final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); List> requests = new ArrayList<>(); requests.add(new Tuple<>(IndicesStatsAction.NAME, new IndicesStatsRequest().indices(SECURITY_INDEX_NAME))); @@ -848,7 +884,7 @@ public void testGrantedNonXPackUserCanExecuteMonitoringOperationsAgainstSecurity final String action = requestTuple.v1(); final TransportRequest request = requestTuple.v2(); authorize(authentication, action, request); - verify(auditTrail).accessGranted(authentication, action, request, new String[]{role.getName()}); + verify(auditTrail).accessGranted(requestId, authentication, action, request, new String[]{role.getName()}); } } @@ -862,6 +898,7 @@ public void testSuperusersCanExecuteOperationAgainstSecurityIndex() { .settings(Settings.builder().put("index.version.created", Version.CURRENT).build()) .numberOfShards(1).numberOfReplicas(0).build(), true) .build()); + final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); List> requests = new ArrayList<>(); requests.add(new Tuple<>(DeleteAction.NAME, @@ -891,7 +928,7 @@ public void testSuperusersCanExecuteOperationAgainstSecurityIndex() { final TransportRequest request = requestTuple.v2(); final Authentication authentication = createAuthentication(superuser); authorize(authentication, action, request); - verify(auditTrail).accessGranted(authentication, action, request, superuser.roles()); + verify(auditTrail).accessGranted(requestId, authentication, action, request, superuser.roles()); } } @@ -906,11 +943,12 @@ public void testSuperusersCanExecuteOperationAgainstSecurityIndexWithWildcard() .settings(Settings.builder().put("index.version.created", Version.CURRENT).build()) .numberOfShards(1).numberOfReplicas(0).build(), true) .build()); + final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); String action = SearchAction.NAME; SearchRequest request = new SearchRequest("_all"); authorize(createAuthentication(superuser), action, request); - verify(auditTrail).accessGranted(authentication, action, request, superuser.roles()); + verify(auditTrail).accessGranted(requestId, authentication, action, request, superuser.roles()); assertThat(request.indices(), arrayContaining(".security")); } @@ -923,6 +961,7 @@ public void testAnonymousRolesAreAppliedToOtherUsers() { roleMap.put("anonymous_user_role", new RoleDescriptor("anonymous_user_role", new String[]{"all"}, new IndicesPrivileges[]{IndicesPrivileges.builder().indices("a").privileges("all").build()}, null)); mockEmptyMetaData(); + AuditUtil.getOrGenerateRequestId(threadContext); // sanity check the anonymous user authorize(createAuthentication(anonymousUser), ClusterHealthAction.NAME, request); @@ -963,9 +1002,11 @@ public void testCompositeActionsAreImmediatelyRejected() { final Authentication authentication = createAuthentication(new User("test user", "no_indices")); final RoleDescriptor role = new RoleDescriptor("no_indices", null, null, null); roleMap.put("no_indices", role); + final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); + assertThrowsAuthorizationException( () -> authorize(authentication, action, request), action, "test user"); - verify(auditTrail).accessDenied(authentication, action, request, new String[]{role.getName()}); + verify(auditTrail).accessDenied(requestId, authentication, action, request, new String[]{role.getName()}); verifyNoMoreInteractions(auditTrail); } @@ -979,14 +1020,17 @@ public void testCompositeActionsIndicesAreNotChecked() { new IndicesPrivileges[]{IndicesPrivileges.builder().indices(randomBoolean() ? "a" : "index").privileges("all").build()}, null); roleMap.put("role", role); + final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); + authorize(authentication, action, request); - verify(auditTrail).accessGranted(authentication, action, request, new String[]{role.getName()}); + verify(auditTrail).accessGranted(requestId, authentication, action, request, new String[]{role.getName()}); verifyNoMoreInteractions(auditTrail); } public void testCompositeActionsMustImplementCompositeIndicesRequest() { String action = randomCompositeRequest().v1(); TransportRequest request = mock(TransportRequest.class); + final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); User user = new User("test user", "role"); roleMap.put("role", new RoleDescriptor("role", null, new IndicesPrivileges[]{IndicesPrivileges.builder().indices(randomBoolean() ? "a" : "index").privileges("all").build()}, @@ -1033,6 +1077,7 @@ public void testCompositeActionsIndicesAreCheckedAtTheShardLevel() { User userDenied = new User("userDenied", "roleDenied"); roleMap.put("roleDenied", new RoleDescriptor("roleDenied", null, new IndicesPrivileges[]{IndicesPrivileges.builder().indices("a").privileges("all").build()}, null)); + AuditUtil.getOrGenerateRequestId(threadContext); mockEmptyMetaData(); authorize(createAuthentication(userAllowed), action, request); assertThrowsAuthorizationException( @@ -1061,11 +1106,15 @@ public void testAuthorizationOfIndividualBulkItems() { roleMap.put("my-role", role); mockEmptyMetaData(); + final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); authorize(authentication, action, request); - verify(auditTrail).accessDenied(authentication, DeleteAction.NAME, request, new String[]{role.getName()}); // alias-1 delete - verify(auditTrail).accessDenied(authentication, IndexAction.NAME, request, new String[]{role.getName()}); // alias-2 index - verify(auditTrail).accessGranted(authentication, action, request, new String[]{role.getName()}); // bulk request is allowed + verify(auditTrail).accessDenied(requestId, authentication, DeleteAction.NAME, request, + new String[] { role.getName() }); // alias-1 delete + verify(auditTrail).accessDenied(requestId, authentication, IndexAction.NAME, request, + new String[] { role.getName() }); // alias-2 index + verify(auditTrail).accessGranted(requestId, authentication, action, request, + new String[] { role.getName() }); // bulk request is allowed verifyNoMoreInteractions(auditTrail); } @@ -1086,15 +1135,16 @@ public void testAuthorizationOfIndividualBulkItemsWithDateMath() { final RoleDescriptor role = new RoleDescriptor("my-role", null, new IndicesPrivileges[]{IndicesPrivileges.builder().indices("datemath-*").privileges("index").build()}, null); roleMap.put("my-role", role); + final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); mockEmptyMetaData(); authorize(authentication, action, request); // both deletes should fail - verify(auditTrail, Mockito.times(2)).accessDenied(authentication, DeleteAction.NAME, request, + verify(auditTrail, Mockito.times(2)).accessDenied(requestId, authentication, DeleteAction.NAME, request, new String[]{role.getName()}); // bulk request is allowed - verify(auditTrail).accessGranted(authentication, action, request, new String[]{role.getName()}); + verify(auditTrail).accessGranted(requestId, authentication, action, request, new String[]{role.getName()}); verifyNoMoreInteractions(auditTrail); } @@ -1302,6 +1352,7 @@ public void testProxyRequestFailsOnNonProxyAction() { TransportRequest request = TransportRequest.Empty.INSTANCE; DiscoveryNode node = new DiscoveryNode("foo", buildNewFakeTransportAddress(), Version.CURRENT); TransportRequest transportRequest = TransportActionProxy.wrapRequest(node, request); + final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); User user = new User("test user", "role"); IllegalStateException illegalStateException = expectThrows(IllegalStateException.class, () -> authorize(createAuthentication(user), "indices:some/action", transportRequest)); @@ -1313,6 +1364,7 @@ public void testProxyRequestFailsOnNonProxyAction() { public void testProxyRequestFailsOnNonProxyRequest() { TransportRequest request = TransportRequest.Empty.INSTANCE; User user = new User("test user", "role"); + AuditUtil.getOrGenerateRequestId(threadContext); IllegalStateException illegalStateException = expectThrows(IllegalStateException.class, () -> authorize(createAuthentication(user), TransportActionProxy.getProxyAction("indices:some/action"), request)); assertThat(illegalStateException.getMessage(), @@ -1329,9 +1381,11 @@ public void testProxyRequestAuthenticationDenied() { final Authentication authentication = createAuthentication(new User("test user", "no_indices")); final RoleDescriptor role = new RoleDescriptor("no_indices", null, null, null); roleMap.put("no_indices", role); + final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); + assertThrowsAuthorizationException( () -> authorize(authentication, action, transportRequest), action, "test user"); - verify(auditTrail).accessDenied(authentication, action, proxiedRequest, new String[]{role.getName()}); + verify(auditTrail).accessDenied(requestId, authentication, action, proxiedRequest, new String[]{role.getName()}); verifyNoMoreInteractions(auditTrail); } @@ -1340,6 +1394,8 @@ public void testProxyRequestAuthenticationGrantedWithAllPrivileges() { new IndicesPrivileges[]{IndicesPrivileges.builder().indices("a").privileges("all").build()}, null); final Authentication authentication = createAuthentication(new User("test user", "a_all")); roleMap.put("a_all", role); + final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); + mockEmptyMetaData(); DiscoveryNode node = new DiscoveryNode("foo", buildNewFakeTransportAddress(), Version.CURRENT); @@ -1347,7 +1403,7 @@ public void testProxyRequestAuthenticationGrantedWithAllPrivileges() { final TransportRequest transportRequest = TransportActionProxy.wrapRequest(node, clearScrollRequest); final String action = TransportActionProxy.getProxyAction(SearchTransportService.CLEAR_SCROLL_CONTEXTS_ACTION_NAME); authorize(authentication, action, transportRequest); - verify(auditTrail).accessGranted(authentication, action, clearScrollRequest, new String[]{role.getName()}); + verify(auditTrail).accessGranted(requestId, authentication, action, clearScrollRequest, new String[]{role.getName()}); } public void testProxyRequestAuthenticationGranted() { @@ -1357,12 +1413,13 @@ public void testProxyRequestAuthenticationGranted() { roleMap.put("a_all", role); mockEmptyMetaData(); DiscoveryNode node = new DiscoveryNode("foo", buildNewFakeTransportAddress(), Version.CURRENT); + final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); final ClearScrollRequest clearScrollRequest = new ClearScrollRequest(); final TransportRequest transportRequest = TransportActionProxy.wrapRequest(node, clearScrollRequest); final String action = TransportActionProxy.getProxyAction(SearchTransportService.CLEAR_SCROLL_CONTEXTS_ACTION_NAME); authorize(authentication, action, transportRequest); - verify(auditTrail).accessGranted(authentication, action, clearScrollRequest, new String[]{role.getName()}); + verify(auditTrail).accessGranted(requestId, authentication, action, clearScrollRequest, new String[]{role.getName()}); } public void testProxyRequestAuthenticationDeniedWithReadPrivileges() { @@ -1370,6 +1427,7 @@ public void testProxyRequestAuthenticationDeniedWithReadPrivileges() { final RoleDescriptor role = new RoleDescriptor("a_role", null, new IndicesPrivileges[]{IndicesPrivileges.builder().indices("a").privileges("read").build()}, null); roleMap.put("a_all", role); + final String requestId = AuditUtil.getOrGenerateRequestId(threadContext); mockEmptyMetaData(); DiscoveryNode node = new DiscoveryNode("foo", buildNewFakeTransportAddress(), Version.CURRENT); ClearScrollRequest clearScrollRequest = new ClearScrollRequest(); @@ -1377,6 +1435,6 @@ public void testProxyRequestAuthenticationDeniedWithReadPrivileges() { String action = TransportActionProxy.getProxyAction(SearchTransportService.CLEAR_SCROLL_CONTEXTS_ACTION_NAME); assertThrowsAuthorizationException( () -> authorize(authentication, action, transportRequest), action, "test user"); - verify(auditTrail).accessDenied(authentication, action, clearScrollRequest, new String[]{role.getName()}); + verify(auditTrail).accessDenied(requestId, authentication, action, clearScrollRequest, new String[]{role.getName()}); } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/SecuritySearchOperationListenerTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/SecuritySearchOperationListenerTests.java index 91d61e1ca5c37..b6fe4346e62a1 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/SecuritySearchOperationListenerTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/SecuritySearchOperationListenerTests.java @@ -118,7 +118,7 @@ public void testValidateSearchContext() throws Exception { expectThrows(SearchContextMissingException.class, () -> listener.validateSearchContext(testSearchContext, request)); assertEquals(testSearchContext.id(), expected.id()); verify(licenseState, times(3)).isAuthAllowed(); - verify(auditTrailService).accessDenied(authentication, "action", request, authentication.getUser().roles()); + verify(auditTrailService).accessDenied(null, authentication, "action", request, authentication.getUser().roles()); } // another user running as the original user @@ -152,7 +152,7 @@ public void testValidateSearchContext() throws Exception { expectThrows(SearchContextMissingException.class, () -> listener.validateSearchContext(testSearchContext, request)); assertEquals(testSearchContext.id(), expected.id()); verify(licenseState, times(5)).isAuthAllowed(); - verify(auditTrailService).accessDenied(authentication, "action", request, authentication.getUser().roles()); + verify(auditTrailService).accessDenied(null, authentication, "action", request, authentication.getUser().roles()); } } @@ -165,55 +165,59 @@ public void testEnsuredAuthenticatedUserIsSame() { TransportRequest request = Empty.INSTANCE; AuditTrailService auditTrail = mock(AuditTrailService.class); - ensureAuthenticatedUserIsSame(original, current, auditTrail, id, action, request, original.getUser().roles()); + final String auditId = randomAlphaOfLengthBetween(8, 20); + ensureAuthenticatedUserIsSame(original, current, auditTrail, id, action, request, auditId, original.getUser().roles()); verifyZeroInteractions(auditTrail); // original user being run as User user = new User(new User("test", "role"), new User("authenticated", "runas")); current = new Authentication(user, new RealmRef("realm", "file", "node"), new RealmRef(randomAlphaOfLengthBetween(1, 16), "file", "node")); - ensureAuthenticatedUserIsSame(original, current, auditTrail, id, action, request, original.getUser().roles()); + ensureAuthenticatedUserIsSame(original, current, auditTrail, id, action, request, auditId, original.getUser().roles()); verifyZeroInteractions(auditTrail); // both user are run as current = new Authentication(user, new RealmRef("realm", "file", "node"), new RealmRef(randomAlphaOfLengthBetween(1, 16), "file", "node")); Authentication runAs = current; - ensureAuthenticatedUserIsSame(runAs, current, auditTrail, id, action, request, original.getUser().roles()); + ensureAuthenticatedUserIsSame(runAs, current, auditTrail, id, action, request, auditId, original.getUser().roles()); verifyZeroInteractions(auditTrail); // different authenticated by type Authentication differentRealmType = new Authentication(new User("test", "role"), new RealmRef("realm", randomAlphaOfLength(5), "node"), null); SearchContextMissingException e = expectThrows(SearchContextMissingException.class, - () -> ensureAuthenticatedUserIsSame(original, differentRealmType, auditTrail, id, action, request, + () -> ensureAuthenticatedUserIsSame(original, differentRealmType, auditTrail, id, action, request, auditId, original.getUser().roles())); assertEquals(id, e.id()); - verify(auditTrail).accessDenied(differentRealmType, action, request, original.getUser().roles()); + verify(auditTrail).accessDenied(auditId, differentRealmType, action, request, original.getUser().roles()); // wrong user Authentication differentUser = new Authentication(new User("test2", "role"), new RealmRef("realm", "realm", "node"), null); e = expectThrows(SearchContextMissingException.class, - () -> ensureAuthenticatedUserIsSame(original, differentUser, auditTrail, id, action, request, original.getUser().roles())); + () -> ensureAuthenticatedUserIsSame(original, differentUser, auditTrail, id, action, request, auditId, + original.getUser().roles())); assertEquals(id, e.id()); - verify(auditTrail).accessDenied(differentUser, action, request, original.getUser().roles()); + verify(auditTrail).accessDenied(auditId, differentUser, action, request, original.getUser().roles()); // run as different user Authentication diffRunAs = new Authentication(new User(new User("test2", "role"), new User("authenticated", "runas")), new RealmRef("realm", "file", "node1"), new RealmRef("realm", "file", "node1")); e = expectThrows(SearchContextMissingException.class, - () -> ensureAuthenticatedUserIsSame(original, diffRunAs, auditTrail, id, action, request, original.getUser().roles())); + () -> ensureAuthenticatedUserIsSame(original, diffRunAs, auditTrail, id, action, request, auditId, + original.getUser().roles())); assertEquals(id, e.id()); - verify(auditTrail).accessDenied(diffRunAs, action, request, original.getUser().roles()); + verify(auditTrail).accessDenied(auditId, diffRunAs, action, request, original.getUser().roles()); // run as different looked up by type Authentication runAsDiffType = new Authentication(user, new RealmRef("realm", "file", "node"), new RealmRef(randomAlphaOfLengthBetween(1, 16), randomAlphaOfLengthBetween(5, 12), "node")); e = expectThrows(SearchContextMissingException.class, - () -> ensureAuthenticatedUserIsSame(runAs, runAsDiffType, auditTrail, id, action, request, original.getUser().roles())); + () -> ensureAuthenticatedUserIsSame(runAs, runAsDiffType, auditTrail, id, action, request, auditId, + original.getUser().roles())); assertEquals(id, e.id()); - verify(auditTrail).accessDenied(runAsDiffType, action, request, original.getUser().roles()); + verify(auditTrail).accessDenied(auditId, runAsDiffType, action, request, original.getUser().roles()); } static class TestScrollSearchContext extends TestSearchContext { From 6e418b65e3e08eded596956a8b0f706290ed811f Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Thu, 29 Nov 2018 08:06:31 +0100 Subject: [PATCH 011/115] [HLRC] Added support for CCR Delete Auto Follow Pattern API (#35981) This change also adds documentation for the Delete Auto Follow Pattern API. Relates to #33824 --- .../org/elasticsearch/client/CcrClient.java | 51 +++++++++++++ .../client/CcrRequestConverters.java | 10 +++ .../ccr/DeleteAutoFollowPatternRequest.java | 37 +++++++++ .../java/org/elasticsearch/client/CCRIT.java | 9 ++- .../documentation/CCRDocumentationIT.java | 75 +++++++++++++++++-- .../ccr/delete_auto_follow_pattern.asciidoc | 32 ++++++++ .../high-level/supported-apis.asciidoc | 2 + 7 files changed, 204 insertions(+), 12 deletions(-) create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/ccr/DeleteAutoFollowPatternRequest.java create mode 100644 docs/java-rest/high-level/ccr/delete_auto_follow_pattern.asciidoc diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/CcrClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/CcrClient.java index dfb516ae08395..86710ffdf8d04 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/CcrClient.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/CcrClient.java @@ -20,6 +20,7 @@ package org.elasticsearch.client; import org.elasticsearch.action.ActionListener; +import org.elasticsearch.client.ccr.DeleteAutoFollowPatternRequest; import org.elasticsearch.client.ccr.PauseFollowRequest; import org.elasticsearch.client.ccr.PutAutoFollowPatternRequest; import org.elasticsearch.client.ccr.PutFollowRequest; @@ -77,6 +78,7 @@ public PutFollowResponse putFollow(PutFollowRequest request, RequestOptions opti * * @param request the request * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @param listener the listener to be notified upon request completion */ public void putFollowAsync(PutFollowRequest request, RequestOptions options, @@ -120,6 +122,7 @@ public AcknowledgedResponse pauseFollow(PauseFollowRequest request, RequestOptio * * @param request the request * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @param listener the listener to be notified upon request completion */ public void pauseFollowAsync(PauseFollowRequest request, RequestOptions options, @@ -162,6 +165,7 @@ public AcknowledgedResponse resumeFollow(ResumeFollowRequest request, RequestOpt * * @param request the request * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @param listener the listener to be notified upon request completion */ public void resumeFollowAsync(ResumeFollowRequest request, RequestOptions options, @@ -206,6 +210,7 @@ public AcknowledgedResponse unfollow(UnfollowRequest request, RequestOptions opt * * @param request the request * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @param listener the listener to be notified upon request completion */ public void unfollowAsync(UnfollowRequest request, RequestOptions options, @@ -249,6 +254,7 @@ public AcknowledgedResponse putAutoFollowPattern(PutAutoFollowPatternRequest req * * @param request the request * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @param listener the listener to be notified upon request completion */ public void putAutoFollowPatternAsync(PutAutoFollowPatternRequest request, RequestOptions options, @@ -262,4 +268,49 @@ public void putAutoFollowPatternAsync(PutAutoFollowPatternRequest request, Collections.emptySet()); } + /** + * Deletes an auto follow pattern. + * + * See + * the docs for more. + * + * @param request the request + * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @return the response + * @throws IOException in case there is a problem sending the request or parsing back the response + */ + public AcknowledgedResponse deleteAutoFollowPattern(DeleteAutoFollowPatternRequest request, + RequestOptions options) throws IOException { + return restHighLevelClient.performRequestAndParseEntity( + request, + CcrRequestConverters::deleteAutoFollowPattern, + options, + AcknowledgedResponse::fromXContent, + Collections.emptySet() + ); + } + + /** + * Deletes an auto follow pattern. + * + * See + * the docs for more. + * + * @param request the request + * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @param listener the listener to be notified upon request completion + */ + public void deleteAutoFollowPatternAsync(DeleteAutoFollowPatternRequest request, + RequestOptions options, + ActionListener listener) { + restHighLevelClient.performRequestAsyncAndParseEntity( + request, + CcrRequestConverters::deleteAutoFollowPattern, + options, + AcknowledgedResponse::fromXContent, + listener, + Collections.emptySet() + ); + } + } diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/CcrRequestConverters.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/CcrRequestConverters.java index 47344ff6c329b..8963919bcd154 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/CcrRequestConverters.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/CcrRequestConverters.java @@ -19,8 +19,10 @@ package org.elasticsearch.client; +import org.apache.http.client.methods.HttpDelete; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; +import org.elasticsearch.client.ccr.DeleteAutoFollowPatternRequest; import org.elasticsearch.client.ccr.PauseFollowRequest; import org.elasticsearch.client.ccr.PutAutoFollowPatternRequest; import org.elasticsearch.client.ccr.PutFollowRequest; @@ -80,4 +82,12 @@ static Request putAutoFollowPattern(PutAutoFollowPatternRequest putAutoFollowPat return request; } + static Request deleteAutoFollowPattern(DeleteAutoFollowPatternRequest deleteAutoFollowPatternRequest) { + String endpoint = new RequestConverters.EndpointBuilder() + .addPathPartAsIs("_ccr", "auto_follow") + .addPathPart(deleteAutoFollowPatternRequest.getName()) + .build(); + return new Request(HttpDelete.METHOD_NAME, endpoint); + } + } diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ccr/DeleteAutoFollowPatternRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ccr/DeleteAutoFollowPatternRequest.java new file mode 100644 index 0000000000000..9293cea964741 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ccr/DeleteAutoFollowPatternRequest.java @@ -0,0 +1,37 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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.elasticsearch.client.ccr; + +import org.elasticsearch.client.Validatable; + +import java.util.Objects; + +public final class DeleteAutoFollowPatternRequest implements Validatable { + + private final String name; + + public DeleteAutoFollowPatternRequest(String name) { + this.name = Objects.requireNonNull(name); + } + + public String getName() { + return name; + } +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/CCRIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/CCRIT.java index 55ee556476f23..00b2d26abaf57 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/CCRIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/CCRIT.java @@ -29,6 +29,7 @@ import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.action.support.WriteRequest; +import org.elasticsearch.client.ccr.DeleteAutoFollowPatternRequest; import org.elasticsearch.client.ccr.PauseFollowRequest; import org.elasticsearch.client.ccr.PutAutoFollowPatternRequest; import org.elasticsearch.client.ccr.PutFollowRequest; @@ -148,10 +149,10 @@ public void testAutoFollowing() throws Exception { }); // Cleanup: - // TODO: replace with hlrc delete auto follow pattern when it is available: - final Request deleteAutoFollowPatternRequest = new Request("DELETE", "/_ccr/auto_follow/pattern1"); - Map deleteAutoFollowPatternResponse = toMap(client().performRequest(deleteAutoFollowPatternRequest)); - assertThat(deleteAutoFollowPatternResponse.get("acknowledged"), is(true)); + final DeleteAutoFollowPatternRequest deleteAutoFollowPatternRequest = new DeleteAutoFollowPatternRequest("pattern1"); + AcknowledgedResponse deleteAutoFollowPatternResponse = + execute(deleteAutoFollowPatternRequest, ccrClient::deleteAutoFollowPattern, ccrClient::deleteAutoFollowPatternAsync); + assertThat(deleteAutoFollowPatternResponse.isAcknowledged(), is(true)); PauseFollowRequest pauseFollowRequest = new PauseFollowRequest("copy-logs-20200101"); AcknowledgedResponse pauseFollowResponse = ccrClient.pauseFollow(pauseFollowRequest, RequestOptions.DEFAULT); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/CCRDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/CCRDocumentationIT.java index bc9375b6d6c9d..1d1aef514cab9 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/CCRDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/CCRDocumentationIT.java @@ -33,6 +33,7 @@ import org.elasticsearch.client.RequestOptions; import org.elasticsearch.client.Response; import org.elasticsearch.client.RestHighLevelClient; +import org.elasticsearch.client.ccr.DeleteAutoFollowPatternRequest; import org.elasticsearch.client.ccr.PauseFollowRequest; import org.elasticsearch.client.ccr.PutAutoFollowPatternRequest; import org.elasticsearch.client.ccr.PutFollowRequest; @@ -401,10 +402,9 @@ public void testPutAutoFollowPattern() throws Exception { // Delete auto follow pattern, so that we can store it again: { - // TODO: replace with hlrc delete auto follow pattern when it is available: - final Request deleteRequest = new Request("DELETE", "/_ccr/auto_follow/my_pattern"); - Map deleteAutoFollowPatternResponse = toMap(client().performRequest(deleteRequest)); - assertThat(deleteAutoFollowPatternResponse.get("acknowledged"), is(true)); + final DeleteAutoFollowPatternRequest deleteRequest = new DeleteAutoFollowPatternRequest("my_pattern"); + AcknowledgedResponse deleteResponse = client.ccr().deleteAutoFollowPattern(deleteRequest, RequestOptions.DEFAULT); + assertThat(deleteResponse.isAcknowledged(), is(true)); } // tag::ccr-put-auto-follow-pattern-execute-listener @@ -435,13 +435,72 @@ public void onFailure(Exception e) { // Cleanup: { - // TODO: replace with hlrc delete auto follow pattern when it is available: - final Request deleteRequest = new Request("DELETE", "/_ccr/auto_follow/my_pattern"); - Map deleteAutoFollowPatternResponse = toMap(client().performRequest(deleteRequest)); - assertThat(deleteAutoFollowPatternResponse.get("acknowledged"), is(true)); + final DeleteAutoFollowPatternRequest deleteRequest = new DeleteAutoFollowPatternRequest("my_pattern"); + AcknowledgedResponse deleteResponse = client.ccr().deleteAutoFollowPattern(deleteRequest, RequestOptions.DEFAULT); + assertThat(deleteResponse.isAcknowledged(), is(true)); } } + public void testDeleteAutoFollowPattern() throws Exception { + RestHighLevelClient client = highLevelClient(); + + // Put auto follow pattern, so that we can delete it: + { + final PutAutoFollowPatternRequest putRequest = + new PutAutoFollowPatternRequest("my_pattern", "local", Collections.singletonList("logs-*")); + AcknowledgedResponse putResponse = client.ccr().putAutoFollowPattern(putRequest, RequestOptions.DEFAULT); + assertThat(putResponse.isAcknowledged(), is(true)); + } + + // tag::ccr-delete-auto-follow-pattern-request + DeleteAutoFollowPatternRequest request = + new DeleteAutoFollowPatternRequest("my_pattern"); // <1> + // end::ccr-delete-auto-follow-pattern-request + + // tag::ccr-delete-auto-follow-pattern-execute + AcknowledgedResponse response = client.ccr() + .deleteAutoFollowPattern(request, RequestOptions.DEFAULT); + // end::ccr-delete-auto-follow-pattern-execute + + // tag::ccr-delete-auto-follow-pattern-response + boolean acknowledged = response.isAcknowledged(); // <1> + // end::ccr-delete-auto-follow-pattern-response + + // Put auto follow pattern, so that we can delete it again: + { + final PutAutoFollowPatternRequest putRequest = + new PutAutoFollowPatternRequest("my_pattern", "local", Collections.singletonList("logs-*")); + AcknowledgedResponse putResponse = client.ccr().putAutoFollowPattern(putRequest, RequestOptions.DEFAULT); + assertThat(putResponse.isAcknowledged(), is(true)); + } + + // tag::ccr-delete-auto-follow-pattern-execute-listener + ActionListener listener = + new ActionListener() { + @Override + public void onResponse(AcknowledgedResponse response) { // <1> + boolean acknowledged = response.isAcknowledged(); + } + + @Override + public void onFailure(Exception e) { + // <2> + } + }; + // end::ccr-delete-auto-follow-pattern-execute-listener + + // Replace the empty listener by a blocking listener in test + final CountDownLatch latch = new CountDownLatch(1); + listener = new LatchedActionListener<>(listener, latch); + + // tag::ccr-delete-auto-follow-pattern-execute-async + client.ccr().deleteAutoFollowPatternAsync(request, + RequestOptions.DEFAULT, listener); // <1> + // end::ccr-delete-auto-follow-pattern-execute-async + + assertTrue(latch.await(30L, TimeUnit.SECONDS)); + } + static Map toMap(Response response) throws IOException { return XContentHelper.convertToMap(JsonXContent.jsonXContent, EntityUtils.toString(response.getEntity()), false); } diff --git a/docs/java-rest/high-level/ccr/delete_auto_follow_pattern.asciidoc b/docs/java-rest/high-level/ccr/delete_auto_follow_pattern.asciidoc new file mode 100644 index 0000000000000..f79dbd5d39de3 --- /dev/null +++ b/docs/java-rest/high-level/ccr/delete_auto_follow_pattern.asciidoc @@ -0,0 +1,32 @@ +-- +:api: ccr-delete-auto-follow-pattern +:request: DeleteAutoFollowPatternRequest +:response: AcknowledgedResponse +-- + +[id="{upid}-{api}"] +=== Delete Auto Follow Pattern API + +[id="{upid}-{api}-request"] +==== Request + +The Delete Auto Follow Pattern API allows you to delete an auto follow pattern. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[{api}-request] +-------------------------------------------------- +<1> The name of the auto follow pattern to delete. + +[id="{upid}-{api}-response"] +==== Response + +The returned +{response}+ indicates if the delete auto follow pattern request was received. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[{api}-response] +-------------------------------------------------- +<1> Whether or not the delete auto follow pattern request was acknowledged. + +include::../execution.asciidoc[] diff --git a/docs/java-rest/high-level/supported-apis.asciidoc b/docs/java-rest/high-level/supported-apis.asciidoc index 4798e9281cdac..026fa32e4c4ba 100644 --- a/docs/java-rest/high-level/supported-apis.asciidoc +++ b/docs/java-rest/high-level/supported-apis.asciidoc @@ -468,12 +468,14 @@ The Java High Level REST Client supports the following CCR APIs: * <<{upid}-ccr-resume-follow>> * <<{upid}-ccr-unfollow>> * <<{upid}-ccr-put-auto-follow-pattern>> +* <<{upid}-ccr-delete-auto-follow-pattern>> include::ccr/put_follow.asciidoc[] include::ccr/pause_follow.asciidoc[] include::ccr/resume_follow.asciidoc[] include::ccr/unfollow.asciidoc[] include::ccr/put_auto_follow_pattern.asciidoc[] +include::ccr/delete_auto_follow_pattern.asciidoc[] == Index Lifecycle Management APIs From 49da679856bb024a4408101522f493a30eb6cb03 Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Tue, 27 Nov 2018 15:07:24 +1100 Subject: [PATCH 012/115] HLRC: Add ability to put user with a password hash (#35844) Update PutUserRequest to support password_hash (see: #35242) This also updates the documentation to bring it in line with our more recent approach to HLRC docs. --- .../client/security/PutUserRequest.java | 81 ++++++++++++-- .../SecurityDocumentationIT.java | 45 +++++++- .../client/security/PutUserRequestTests.java | 101 ++++++++++++++++++ .../high-level/security/put-user.asciidoc | 62 ++++++----- 4 files changed, 250 insertions(+), 39 deletions(-) create mode 100644 client/rest-high-level/src/test/java/org/elasticsearch/client/security/PutUserRequestTests.java diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/PutUserRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/PutUserRequest.java index 66af9fca31cb2..f7bf87da00275 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/PutUserRequest.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/PutUserRequest.java @@ -39,9 +39,46 @@ public final class PutUserRequest implements Validatable, ToXContentObject { private final User user; private final @Nullable char[] password; + private final @Nullable char[] passwordHash; private final boolean enabled; private final RefreshPolicy refreshPolicy; + /** + * Create or update a user in the native realm, with the user's new or updated password specified in plaintext. + * @param user the user to be created or updated + * @param password the password of the user. The password array is not modified by this class. + * It is the responsibility of the caller to clear the password after receiving + * a response. + * @param enabled true if the user is enabled and allowed to access elasticsearch + * @param refreshPolicy the refresh policy for the request. + */ + public static PutUserRequest withPassword(User user, char[] password, boolean enabled, RefreshPolicy refreshPolicy) { + return new PutUserRequest(user, password, null, enabled, refreshPolicy); + } + + /** + * Create or update a user in the native realm, with the user's new or updated password specified as a cryptographic hash. + * @param user the user to be created or updated + * @param passwordHash the hash of the password of the user. It must be in the correct format for the password hashing algorithm in + * use on this elasticsearch cluster. The array is not modified by this class. + * It is the responsibility of the caller to clear the hash after receiving a response. + * @param enabled true if the user is enabled and allowed to access elasticsearch + * @param refreshPolicy the refresh policy for the request. + */ + public static PutUserRequest withPasswordHash(User user, char[] passwordHash, boolean enabled, RefreshPolicy refreshPolicy) { + return new PutUserRequest(user, null, passwordHash, enabled, refreshPolicy); + } + + /** + * Update an existing user in the native realm without modifying their password. + * @param user the user to be created or updated + * @param enabled true if the user is enabled and allowed to access elasticsearch + * @param refreshPolicy the refresh policy for the request. + */ + public static PutUserRequest updateUser(User user, boolean enabled, RefreshPolicy refreshPolicy) { + return new PutUserRequest(user, null, null, enabled, refreshPolicy); + } + /** * Creates a new request that is used to create or update a user in the native realm. * @@ -51,10 +88,33 @@ public final class PutUserRequest implements Validatable, ToXContentObject { * a response. * @param enabled true if the user is enabled and allowed to access elasticsearch * @param refreshPolicy the refresh policy for the request. + * @deprecated Use {@link #withPassword(User, char[], boolean, RefreshPolicy)} or + * {@link #updateUser(User, boolean, RefreshPolicy)} instead. */ + @Deprecated public PutUserRequest(User user, @Nullable char[] password, boolean enabled, @Nullable RefreshPolicy refreshPolicy) { + this(user, password, null, enabled, refreshPolicy); + } + + /** + * Creates a new request that is used to create or update a user in the native realm. + * @param user the user to be created or updated + * @param password the password of the user. The password array is not modified by this class. + * It is the responsibility of the caller to clear the password after receiving + * a response. + * @param passwordHash the hash of the password. Only one of "password" or "passwordHash" may be populated. + * The other parameter must be {@code null}. + * @param enabled true if the user is enabled and allowed to access elasticsearch + * @param refreshPolicy the refresh policy for the request. + */ + private PutUserRequest(User user, @Nullable char[] password, @Nullable char[] passwordHash, boolean enabled, + RefreshPolicy refreshPolicy) { this.user = Objects.requireNonNull(user, "user is required, cannot be null"); + if (password != null && passwordHash != null) { + throw new IllegalArgumentException("cannot specify both password and passwordHash"); + } this.password = password; + this.passwordHash = passwordHash; this.enabled = enabled; this.refreshPolicy = refreshPolicy == null ? RefreshPolicy.getDefault() : refreshPolicy; } @@ -82,6 +142,7 @@ public boolean equals(Object o) { final PutUserRequest that = (PutUserRequest) o; return Objects.equals(user, that.user) && Arrays.equals(password, that.password) + && Arrays.equals(passwordHash, that.passwordHash) && enabled == that.enabled && refreshPolicy == that.refreshPolicy; } @@ -90,6 +151,7 @@ public boolean equals(Object o) { public int hashCode() { int result = Objects.hash(user, enabled, refreshPolicy); result = 31 * result + Arrays.hashCode(password); + result = 31 * result + Arrays.hashCode(passwordHash); return result; } @@ -108,12 +170,10 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws builder.startObject(); builder.field("username", user.getUsername()); if (password != null) { - byte[] charBytes = CharArrays.toUtf8Bytes(password); - try { - builder.field("password").utf8Value(charBytes, 0, charBytes.length); - } finally { - Arrays.fill(charBytes, (byte) 0); - } + charField(builder, "password", password); + } + if (passwordHash != null) { + charField(builder, "password_hash", passwordHash); } builder.field("roles", user.getRoles()); if (user.getFullName() != null) { @@ -126,4 +186,13 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws builder.field("enabled", enabled); return builder.endObject(); } + + private void charField(XContentBuilder builder, String fieldName, char[] chars) throws IOException { + byte[] charBytes = CharArrays.toUtf8Bytes(chars); + try { + builder.field(fieldName).utf8Value(charBytes, 0, charBytes.length); + } finally { + Arrays.fill(charBytes, (byte) 0); + } + } } diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java index 8d390f9e052f7..87e1324786378 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java @@ -22,6 +22,7 @@ import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.ContentType; import org.apache.http.nio.entity.NStringEntity; +import org.elasticsearch.ElasticsearchStatusException; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.LatchedActionListener; import org.elasticsearch.action.support.PlainActionFuture; @@ -80,7 +81,11 @@ import org.elasticsearch.rest.RestStatus; import org.hamcrest.Matchers; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; import java.io.IOException; +import java.security.SecureRandom; +import java.util.Base64; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -93,6 +98,7 @@ import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.emptyIterable; @@ -108,10 +114,13 @@ public void testPutUser() throws Exception { RestHighLevelClient client = highLevelClient(); { - //tag::put-user-execute + //tag::put-user-password-request char[] password = new char[]{'p', 'a', 's', 's', 'w', 'o', 'r', 'd'}; User user = new User("example", Collections.singletonList("superuser")); - PutUserRequest request = new PutUserRequest(user, password, true, RefreshPolicy.NONE); + PutUserRequest request = PutUserRequest.withPassword(user, password, true, RefreshPolicy.NONE); + //end::put-user-password-request + + //tag::put-user-execute PutUserResponse response = client.security().putUser(request, RequestOptions.DEFAULT); //end::put-user-execute @@ -121,11 +130,37 @@ public void testPutUser() throws Exception { assertTrue(isCreated); } - { + byte[] salt = new byte[32]; + SecureRandom.getInstanceStrong().nextBytes(salt); char[] password = new char[]{'p', 'a', 's', 's', 'w', 'o', 'r', 'd'}; - User user2 = new User("example2", Collections.singletonList("superuser")); - PutUserRequest request = new PutUserRequest(user2, password, true, RefreshPolicy.NONE); + User user = new User("example2", Collections.singletonList("superuser")); + + //tag::put-user-hash-request + SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2withHMACSHA512"); + PBEKeySpec keySpec = new PBEKeySpec(password, salt, 10000, 256); + final byte[] pbkdfEncoded = secretKeyFactory.generateSecret(keySpec).getEncoded(); + char[] passwordHash = ("{PBKDF2}10000$" + Base64.getEncoder().encodeToString(salt) + + "$" + Base64.getEncoder().encodeToString(pbkdfEncoded)).toCharArray(); + + PutUserRequest request = PutUserRequest.withPasswordHash(user, passwordHash, true, RefreshPolicy.NONE); + //end::put-user-hash-request + + try { + client.security().putUser(request, RequestOptions.DEFAULT); + } catch (ElasticsearchStatusException e) { + // This is expected to fail as the server will not be using PBKDF2, but that's easiest hasher to support + // in a standard JVM without introducing additional libraries. + assertThat(e.getDetailedMessage(), containsString("PBKDF2")); + } + } + + { + User user = new User("example", Arrays.asList("superuser", "another-role")); + //tag::put-user-update-request + PutUserRequest request = PutUserRequest.updateUser(user, true, RefreshPolicy.NONE); + //end::put-user-update-request + // tag::put-user-execute-listener ActionListener listener = new ActionListener() { @Override diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/PutUserRequestTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/PutUserRequestTests.java new file mode 100644 index 0000000000000..76d3b283b0d90 --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/PutUserRequestTests.java @@ -0,0 +1,101 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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.elasticsearch.client.security; + +import org.elasticsearch.client.security.user.User; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.test.ESTestCase; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; + +public class PutUserRequestTests extends ESTestCase { + + public void testBuildRequestWithPassword() throws Exception { + final User user = new User("hawkeye", Arrays.asList("kibana_user", "avengers"), + Collections.singletonMap("status", "active"), "Clinton Barton", null); + final char[] password = "f@rmb0y".toCharArray(); + final PutUserRequest request = PutUserRequest.withPassword(user, password, true, RefreshPolicy.IMMEDIATE); + String json = Strings.toString(request); + final Map requestAsMap = XContentHelper.convertToMap(XContentType.JSON.xContent(), json, false); + assertThat(requestAsMap.get("username"), is("hawkeye")); + assertThat(requestAsMap.get("roles"), instanceOf(List.class)); + assertThat((List) requestAsMap.get("roles"), containsInAnyOrder("kibana_user", "avengers")); + assertThat(requestAsMap.get("password"), is("f@rmb0y")); + assertThat(requestAsMap.containsKey("password_hash"), is(false)); + assertThat(requestAsMap.get("full_name"), is("Clinton Barton")); + assertThat(requestAsMap.containsKey("email"), is(false)); + assertThat(requestAsMap.get("enabled"), is(true)); + assertThat(requestAsMap.get("metadata"), instanceOf(Map.class)); + final Map metadata = (Map) requestAsMap.get("metadata"); + assertThat(metadata.size(), is(1)); + assertThat(metadata.get("status"), is("active")); + } + + public void testBuildRequestWithPasswordHash() throws Exception { + final User user = new User("hawkeye", Arrays.asList("kibana_user", "avengers"), + Collections.singletonMap("status", "active"), "Clinton Barton", null); + final char[] passwordHash = "$2a$04$iu1G4x3ZKVDNi6egZIjkFuIPja6elQXiBF1LdRVauV4TGog6FYOpi".toCharArray(); + final PutUserRequest request = PutUserRequest.withPasswordHash(user, passwordHash, true, RefreshPolicy.IMMEDIATE); + String json = Strings.toString(request); + final Map requestAsMap = XContentHelper.convertToMap(XContentType.JSON.xContent(), json, false); + assertThat(requestAsMap.get("username"), is("hawkeye")); + assertThat(requestAsMap.get("roles"), instanceOf(List.class)); + assertThat((List) requestAsMap.get("roles"), containsInAnyOrder("kibana_user", "avengers")); + assertThat(requestAsMap.get("password_hash"), is("$2a$04$iu1G4x3ZKVDNi6egZIjkFuIPja6elQXiBF1LdRVauV4TGog6FYOpi")); + assertThat(requestAsMap.containsKey("password"), is(false)); + assertThat(requestAsMap.get("full_name"), is("Clinton Barton")); + assertThat(requestAsMap.containsKey("email"), is(false)); + assertThat(requestAsMap.get("enabled"), is(true)); + assertThat(requestAsMap.get("metadata"), instanceOf(Map.class)); + final Map metadata = (Map) requestAsMap.get("metadata"); + assertThat(metadata.size(), is(1)); + assertThat(metadata.get("status"), is("active")); + } + + public void testBuildRequestForUpdateOnly() throws Exception { + final User user = new User("hawkeye", Arrays.asList("kibana_user", "avengers"), + Collections.singletonMap("status", "active"), "Clinton Barton", null); + final char[] passwordHash = "$2a$04$iu1G4x3ZKVDNi6egZIjkFuIPja6elQXiBF1LdRVauV4TGog6FYOpi".toCharArray(); + final PutUserRequest request = PutUserRequest.updateUser(user, true, RefreshPolicy.IMMEDIATE); + String json = Strings.toString(request); + final Map requestAsMap = XContentHelper.convertToMap(XContentType.JSON.xContent(), json, false); + assertThat(requestAsMap.get("username"), is("hawkeye")); + assertThat(requestAsMap.get("roles"), instanceOf(List.class)); + assertThat((List) requestAsMap.get("roles"), containsInAnyOrder("kibana_user", "avengers")); + assertThat(requestAsMap.containsKey("password"), is(false)); + assertThat(requestAsMap.containsKey("password_hash"), is(false)); + assertThat(requestAsMap.get("full_name"), is("Clinton Barton")); + assertThat(requestAsMap.containsKey("email"), is(false)); + assertThat(requestAsMap.get("enabled"), is(true)); + assertThat(requestAsMap.get("metadata"), instanceOf(Map.class)); + final Map metadata = (Map) requestAsMap.get("metadata"); + assertThat(metadata.size(), is(1)); + assertThat(metadata.get("status"), is("active")); + } + +} diff --git a/docs/java-rest/high-level/security/put-user.asciidoc b/docs/java-rest/high-level/security/put-user.asciidoc index aca69b8182842..714dd61e1193d 100644 --- a/docs/java-rest/high-level/security/put-user.asciidoc +++ b/docs/java-rest/high-level/security/put-user.asciidoc @@ -1,52 +1,58 @@ -[[java-rest-high-security-put-user]] +-- +:api: put-user +:request: PutUserRequest +:response: PutUserResponse +-- + +[id="{upid}-{api}"] === Put User API -[[java-rest-high-security-put-user-execution]] -==== Execution +[id="{upid}-{api}-request"] +==== Put User Request Request + +The +{request}+ class is used to create or update a user in the Native Realm. +There are 3 different factory methods for creating a request. -Creating and updating a user can be performed using the `security().putUser()` -method: +===== Create or Update User with a Password + +If you wish to create a new user (or update an existing user) and directly specifying the user's new password, use the +`withPassword` method as shown below: ["source","java",subs="attributes,callouts,macros"] -------------------------------------------------- -include-tagged::{doc-tests}/SecurityDocumentationIT.java[put-user-execute] +include-tagged::{doc-tests-file}[{api}-password-request] -------------------------------------------------- -[[java-rest-high-security-put-user-response]] -==== Response +===== Create or Update User with a Hashed Password -The returned `PutUserResponse` contains a single field, `created`. This field -serves as an indication if a user was created or if an existing entry was updated. +If you wish to create a new user (or update an existing user) and perform password hashing on the client, +then use the `withPasswordHash` method: ["source","java",subs="attributes,callouts,macros"] -------------------------------------------------- -include-tagged::{doc-tests}/SecurityDocumentationIT.java[put-user-response] --------------------------------------------------- -<1> `created` is a boolean indicating whether the user was created or updated +include-tagged::{doc-tests-file}[{api}-hash-request] -[[java-rest-high-security-put-user-async]] -==== Asynchronous Execution +-------------------------------------------------- +===== Update a User without changing their password -This request can be executed asynchronously: +If you wish to update an existing user, and do not wish to change the user's password, +then use the `updateUserProperties` method: ["source","java",subs="attributes,callouts,macros"] -------------------------------------------------- -include-tagged::{doc-tests}/SecurityDocumentationIT.java[put-user-execute-async] +include-tagged::{doc-tests-file}[{api}-update-request] -------------------------------------------------- -<1> The `PutUserRequest` to execute and the `ActionListener` to use when -the execution completes. -The asynchronous method does not block and returns immediately. Once the request -has completed the `ActionListener` is called back using the `onResponse` method -if the execution successfully completed or using the `onFailure` method if -it failed. +include::../execution.asciidoc[] -A typical listener for a `PutUserResponse` looks like: +[id="{upid}-{api}-response"] +==== Put User Response + +The returned `PutUserResponse` contains a single field, `created`. This field +serves as an indication if a user was created or if an existing entry was updated. ["source","java",subs="attributes,callouts,macros"] -------------------------------------------------- -include-tagged::{doc-tests}/SecurityDocumentationIT.java[put-user-execute-listener] +include-tagged::{doc-tests}/SecurityDocumentationIT.java[put-user-response] -------------------------------------------------- -<1> Called when the execution is successfully completed. The response is -provided as an argument. -<2> Called in case of failure. The raised exception is provided as an argument. +<1> `created` is a boolean indicating whether the user was created or updated From 5c281477ee4ab0b552e4c8b726302ca93738e432 Mon Sep 17 00:00:00 2001 From: Alpar Torok Date: Thu, 29 Nov 2018 09:43:16 +0200 Subject: [PATCH 013/115] Replace fixtures with docker-compose (#35651) Creates a new plugin to manage docker-compose based test fixtures. Convert the smb-fixture as a first example. --- .ci/packer_cache.sh | 2 +- TESTING.asciidoc | 3 + buildSrc/build.gradle | 1 + .../testfixtures/TestFixtureExtension.java | 48 +++++++ .../testfixtures/TestFixturesPlugin.java | 133 ++++++++++++++++++ .../elasticsearch.test.fixtures.properties | 1 + .../third-party/active-directory/build.gradle | 22 +-- .../ADLdapUserSearchSessionFactoryTests.java | 1 + .../ldap/AbstractActiveDirectoryTestCase.java | 20 ++- .../ActiveDirectoryGroupsResolverTests.java | 1 + .../authc/ldap/ActiveDirectoryRunAsIT.java | 1 + .../ActiveDirectorySessionFactoryTests.java | 2 + .../security/authc/ldap/GroupMappingIT.java | 4 + .../authc/ldap/MultiGroupMappingIT.java | 1 + .../authc/ldap/MultipleAdRealmIT.java | 1 + .../UserAttributeGroupsResolverTests.java | 2 + x-pack/test/smb-fixture/Dockerfile | 12 ++ x-pack/test/smb-fixture/Vagrantfile | 20 --- x-pack/test/smb-fixture/build.gradle | 43 +----- x-pack/test/smb-fixture/docker-compose.yml | 11 ++ .../main/resources/provision/installsmb.sh | 17 +-- 21 files changed, 244 insertions(+), 102 deletions(-) create mode 100644 buildSrc/src/main/java/org/elasticsearch/gradle/testfixtures/TestFixtureExtension.java create mode 100644 buildSrc/src/main/java/org/elasticsearch/gradle/testfixtures/TestFixturesPlugin.java create mode 100644 buildSrc/src/main/resources/META-INF/gradle-plugins/elasticsearch.test.fixtures.properties create mode 100644 x-pack/test/smb-fixture/Dockerfile delete mode 100644 x-pack/test/smb-fixture/Vagrantfile create mode 100644 x-pack/test/smb-fixture/docker-compose.yml diff --git a/.ci/packer_cache.sh b/.ci/packer_cache.sh index 906b2dd642074..6a7e2e69faebf 100755 --- a/.ci/packer_cache.sh +++ b/.ci/packer_cache.sh @@ -16,4 +16,4 @@ while [ -h "$SCRIPT" ] ; do done source $(dirname "${SCRIPT}")/java-versions.properties -JAVA_HOME="${HOME}"/.java/${ES_BUILD_JAVA} ./gradlew resolveAllDependencies --parallel +JAVA_HOME="${HOME}"/.java/${ES_BUILD_JAVA} ./gradlew --parallel resolveAllDependencies composePull diff --git a/TESTING.asciidoc b/TESTING.asciidoc index 225d3c8eade3e..9d52f471d1d05 100644 --- a/TESTING.asciidoc +++ b/TESTING.asciidoc @@ -251,6 +251,9 @@ If you want to just run the precommit checks: ./gradlew precommit --------------------------------------------------------------------------- +Some of these checks will require `docker-compose` installed for bringing up +test fixtures. If it's not present those checks will be skipped automatically. + == Testing the REST layer The available integration tests make use of the java API to communicate with diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 03c56529b8dad..1e3ba1e88ff4b 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -126,6 +126,7 @@ dependencies { compile "org.elasticsearch:jna:4.5.1" compile 'com.github.jengelman.gradle.plugins:shadow:2.0.4' compile 'de.thetaphi:forbiddenapis:2.6' + compile 'com.avast.gradle:docker-compose-gradle-plugin:0.4.5' testCompile "junit:junit:${props.getProperty('junit')}" } diff --git a/buildSrc/src/main/java/org/elasticsearch/gradle/testfixtures/TestFixtureExtension.java b/buildSrc/src/main/java/org/elasticsearch/gradle/testfixtures/TestFixtureExtension.java new file mode 100644 index 0000000000000..edc0fd09f1677 --- /dev/null +++ b/buildSrc/src/main/java/org/elasticsearch/gradle/testfixtures/TestFixtureExtension.java @@ -0,0 +1,48 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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.elasticsearch.gradle.testfixtures; + +import org.gradle.api.NamedDomainObjectContainer; +import org.gradle.api.Project; + +public class TestFixtureExtension { + + private final Project project; + final NamedDomainObjectContainer fixtures; + + public TestFixtureExtension(Project project) { + this.project = project; + this.fixtures = project.container(Project.class); + } + + public void useFixture(String path) { + Project fixtureProject = this.project.findProject(path); + if (fixtureProject == null) { + throw new IllegalArgumentException("Could not find test fixture " + fixtureProject); + } + if (fixtureProject.file(TestFixturesPlugin.DOCKER_COMPOSE_YML).exists() == false) { + throw new IllegalArgumentException( + "Project " + path + " is not a valid test fixture: missing " + TestFixturesPlugin.DOCKER_COMPOSE_YML + ); + } + fixtures.add(fixtureProject); + } + + +} diff --git a/buildSrc/src/main/java/org/elasticsearch/gradle/testfixtures/TestFixturesPlugin.java b/buildSrc/src/main/java/org/elasticsearch/gradle/testfixtures/TestFixturesPlugin.java new file mode 100644 index 0000000000000..2b1c150ee9f43 --- /dev/null +++ b/buildSrc/src/main/java/org/elasticsearch/gradle/testfixtures/TestFixturesPlugin.java @@ -0,0 +1,133 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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.elasticsearch.gradle.testfixtures; + +import com.avast.gradle.dockercompose.ComposeExtension; +import com.avast.gradle.dockercompose.DockerComposePlugin; +import org.elasticsearch.gradle.precommit.JarHellTask; +import org.elasticsearch.gradle.precommit.ThirdPartyAuditTask; +import org.gradle.api.DefaultTask; +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.Task; +import org.gradle.api.plugins.BasePlugin; +import org.gradle.api.tasks.TaskContainer; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Collections; + +public class TestFixturesPlugin implements Plugin { + + static final String DOCKER_COMPOSE_YML = "docker-compose.yml"; + + @Override + public void apply(Project project) { + TaskContainer tasks = project.getTasks(); + + TestFixtureExtension extension = project.getExtensions().create( + "testFixtures", TestFixtureExtension.class, project + ); + + // Don't look for docker-compose on the PATH yet that would pick up on Windows as well + if (project.file("/usr/local/bin/docker-compose").exists() == false && + project.file("/usr/bin/docker-compose").exists() == false + ) { + project.getLogger().warn( + "Tests require docker-compose at /usr/local/bin/docker-compose or /usr/bin/docker-compose " + + "but none could not be found so these will be skipped" + ); + tasks.withType(getTaskClass("com.carrotsearch.gradle.junit4.RandomizedTestingTask"), task -> + task.setEnabled(false) + ); + return; + } + + if (project.file(DOCKER_COMPOSE_YML).exists()) { + project.apply(spec -> spec.plugin(BasePlugin.class)); + project.apply(spec -> spec.plugin(DockerComposePlugin.class)); + ComposeExtension composeExtension = project.getExtensions().getByType(ComposeExtension.class); + composeExtension.setUseComposeFiles(Collections.singletonList(DOCKER_COMPOSE_YML)); + composeExtension.setRemoveContainers(true); + composeExtension.setExecutable( + project.file("/usr/local/bin/docker-compose").exists() ? + "/usr/local/bin/docker-compose" : "/usr/bin/docker-compose" + ); + + project.getTasks().getByName("clean").dependsOn("composeDown"); + + // convenience boilerplate with build plugin + project.getPluginManager().withPlugin("elasticsearch.build", (appliedPlugin) -> { + // Can't reference tasks that are implemented in Groovy, use reflection instead + disableTaskByType(tasks, getTaskClass("org.elasticsearch.gradle.precommit.LicenseHeadersTask")); + disableTaskByType(tasks, getTaskClass("com.carrotsearch.gradle.junit4.RandomizedTestingTask")); + disableTaskByType(tasks, ThirdPartyAuditTask.class); + disableTaskByType(tasks, JarHellTask.class); + }); + } else { + tasks.withType(getTaskClass("com.carrotsearch.gradle.junit4.RandomizedTestingTask"), task -> + extension.fixtures.all(fixtureProject -> { + task.dependsOn(fixtureProject.getTasks().getByName("composeUp")); + task.finalizedBy(fixtureProject.getTasks().getByName("composeDown")); + // Configure ports for the tests as system properties. + // We only know these at execution time so we need to do it in doFirst + task.doFirst(it -> + fixtureProject.getExtensions().getByType(ComposeExtension.class).getServicesInfos() + .forEach((service, infos) -> + infos.getPorts() + .forEach((container, host) -> setSystemProperty( + it, + "test.fixtures." + fixtureProject.getName() + "." + service + "." + container, + host + )) + )); + })); + } + } + + private void setSystemProperty(Task task, String name, Object value) { + try { + Method systemProperty = task.getClass().getMethod("systemProperty", String.class, Object.class); + systemProperty.invoke(task, name, value); + } catch (NoSuchMethodException e) { + throw new IllegalArgumentException("Could not find systemProperty method on RandomizedTestingTask", e); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new IllegalArgumentException("Could not call systemProperty method on RandomizedTestingTask", e); + } + } + + private void disableTaskByType(TaskContainer tasks, Class type) { + tasks.withType(type, task -> task.setEnabled(false)); + } + + @SuppressWarnings("unchecked") + private Class getTaskClass(String type) { + Class aClass; + try { + aClass = Class.forName(type); + if (DefaultTask.class.isAssignableFrom(aClass) == false) { + throw new IllegalArgumentException("Not a task type: " + type); + } + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException("No such task: " + type); + } + return (Class) aClass; + } + +} diff --git a/buildSrc/src/main/resources/META-INF/gradle-plugins/elasticsearch.test.fixtures.properties b/buildSrc/src/main/resources/META-INF/gradle-plugins/elasticsearch.test.fixtures.properties new file mode 100644 index 0000000000000..aac84c21ee637 --- /dev/null +++ b/buildSrc/src/main/resources/META-INF/gradle-plugins/elasticsearch.test.fixtures.properties @@ -0,0 +1 @@ +implementation-class=org.elasticsearch.gradle.testfixtures.TestFixturesPlugin \ No newline at end of file diff --git a/x-pack/qa/third-party/active-directory/build.gradle b/x-pack/qa/third-party/active-directory/build.gradle index c9fa55652108d..4acebbda9a5b6 100644 --- a/x-pack/qa/third-party/active-directory/build.gradle +++ b/x-pack/qa/third-party/active-directory/build.gradle @@ -1,14 +1,13 @@ -Project smbFixtureProject = xpackProject("test:smb-fixture") -evaluationDependsOn(smbFixtureProject.path) - -apply plugin: 'elasticsearch.vagrantsupport' apply plugin: 'elasticsearch.standalone-test' +apply plugin: 'elasticsearch.test.fixtures' dependencies { testCompile project(xpackModule('security')) testCompile project(path: xpackModule('security'), configuration: 'testArtifacts') } +testFixtures.useFixture ":x-pack:test:smb-fixture" + // add test resources from security, so tests can use example certs sourceSets.test.resources.srcDirs(project(xpackModule('security')).sourceSets.test.resources.srcDirs) compileTestJava.options.compilerArgs << "-Xlint:-deprecation,-rawtypes,-serial,-try,-unchecked" @@ -31,17 +30,4 @@ test { } // these are just tests, no need to audit -thirdPartyAudit.enabled = false - -task smbFixture { - dependsOn "vagrantCheckVersion", "virtualboxCheckVersion", smbFixtureProject.up -} - -if (project.rootProject.vagrantSupported) { - if (project.hasProperty('useExternalAD') == false) { - test.dependsOn smbFixture - test.finalizedBy smbFixtureProject.halt - } -} else { - test.enabled = false -} +thirdPartyAudit.enabled = false \ No newline at end of file diff --git a/x-pack/qa/third-party/active-directory/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ADLdapUserSearchSessionFactoryTests.java b/x-pack/qa/third-party/active-directory/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ADLdapUserSearchSessionFactoryTests.java index 9f97ebc6d03f6..bb35a32a0c020 100644 --- a/x-pack/qa/third-party/active-directory/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ADLdapUserSearchSessionFactoryTests.java +++ b/x-pack/qa/third-party/active-directory/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ADLdapUserSearchSessionFactoryTests.java @@ -67,6 +67,7 @@ private MockSecureSettings newSecureSettings(String key, String value) { return secureSettings; } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/35738") public void testUserSearchWithActiveDirectory() throws Exception { String groupSearchBase = "DC=ad,DC=test,DC=elasticsearch,DC=com"; String userSearchBase = "CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com"; diff --git a/x-pack/qa/third-party/active-directory/src/test/java/org/elasticsearch/xpack/security/authc/ldap/AbstractActiveDirectoryTestCase.java b/x-pack/qa/third-party/active-directory/src/test/java/org/elasticsearch/xpack/security/authc/ldap/AbstractActiveDirectoryTestCase.java index dd84fed94c67b..7be52e927143d 100644 --- a/x-pack/qa/third-party/active-directory/src/test/java/org/elasticsearch/xpack/security/authc/ldap/AbstractActiveDirectoryTestCase.java +++ b/x-pack/qa/third-party/active-directory/src/test/java/org/elasticsearch/xpack/security/authc/ldap/AbstractActiveDirectoryTestCase.java @@ -42,13 +42,14 @@ public abstract class AbstractActiveDirectoryTestCase extends ESTestCase { // as we cannot control the URL of the referral which may contain a non-resolvable DNS name as // this name would be served by the samba4 instance public static final Boolean FOLLOW_REFERRALS = Booleans.parseBoolean(getFromEnv("TESTS_AD_FOLLOW_REFERRALS", "false")); - public static final String AD_LDAP_URL = getFromEnv("TESTS_AD_LDAP_URL", "ldaps://localhost:61636"); - public static final String AD_LDAP_GC_URL = getFromEnv("TESTS_AD_LDAP_GC_URL", "ldaps://localhost:63269"); + public static final String AD_LDAP_URL = getFromEnv("TESTS_AD_LDAP_URL", "ldaps://localhost:" + getFromProperty("636")); + public static final String AD_LDAP_GC_URL = getFromEnv("TESTS_AD_LDAP_GC_URL", "ldaps://localhost:" + getFromProperty("3269")); public static final String PASSWORD = getFromEnv("TESTS_AD_USER_PASSWORD", "Passw0rd"); - public static final String AD_LDAP_PORT = getFromEnv("TESTS_AD_LDAP_PORT", "61389"); - public static final String AD_LDAPS_PORT = getFromEnv("TESTS_AD_LDAPS_PORT", "61636"); - public static final String AD_GC_LDAP_PORT = getFromEnv("TESTS_AD_GC_LDAP_PORT", "63268"); - public static final String AD_GC_LDAPS_PORT = getFromEnv("TESTS_AD_GC_LDAPS_PORT", "63269"); + public static final String AD_LDAP_PORT = getFromEnv("TESTS_AD_LDAP_PORT", getFromProperty("389")); + + public static final String AD_LDAPS_PORT = getFromEnv("TESTS_AD_LDAPS_PORT", getFromProperty("636")); + public static final String AD_GC_LDAP_PORT = getFromEnv("TESTS_AD_GC_LDAP_PORT", getFromProperty("3268")); + public static final String AD_GC_LDAPS_PORT = getFromEnv("TESTS_AD_GC_LDAPS_PORT", getFromProperty("3269")); public static final String AD_DOMAIN = "ad.test.elasticsearch.com"; protected SSLService sslService; @@ -144,4 +145,11 @@ private static String getFromEnv(String envVar, String defaultValue) { final String value = System.getenv(envVar); return value == null ? defaultValue : value; } + + private static String getFromProperty(String port) { + String key = "test.fixtures.smb-fixture.fixture." + port; + final String value = System.getProperty(key); + assertNotNull("Expected the actual value for " + port + " to be in system property " + key, value); + return value; + } } diff --git a/x-pack/qa/third-party/active-directory/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectoryGroupsResolverTests.java b/x-pack/qa/third-party/active-directory/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectoryGroupsResolverTests.java index 330ec6b9a758c..d34b9e3d37185 100644 --- a/x-pack/qa/third-party/active-directory/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectoryGroupsResolverTests.java +++ b/x-pack/qa/third-party/active-directory/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectoryGroupsResolverTests.java @@ -31,6 +31,7 @@ public void setReferralFollowing() { ldapConnection.getConnectionOptions().setFollowReferrals(AbstractActiveDirectoryTestCase.FOLLOW_REFERRALS); } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/35738") @SuppressWarnings("unchecked") public void testResolveSubTree() throws Exception { Settings settings = Settings.builder() diff --git a/x-pack/qa/third-party/active-directory/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectoryRunAsIT.java b/x-pack/qa/third-party/active-directory/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectoryRunAsIT.java index 2aabe2a464b94..3cd4711750f85 100644 --- a/x-pack/qa/third-party/active-directory/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectoryRunAsIT.java +++ b/x-pack/qa/third-party/active-directory/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectoryRunAsIT.java @@ -60,6 +60,7 @@ protected Settings nodeSettings(int nodeOrdinal) { return builder.build(); } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/35738") public void testRunAs() throws Exception { String avenger = realmConfig.loginWithCommonName ? "Natasha Romanoff" : "blackwidow"; final AuthenticateRequest request = new AuthenticateRequest(avenger); diff --git a/x-pack/qa/third-party/active-directory/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectorySessionFactoryTests.java b/x-pack/qa/third-party/active-directory/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectorySessionFactoryTests.java index bd416512f564b..447b00856827e 100644 --- a/x-pack/qa/third-party/active-directory/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectorySessionFactoryTests.java +++ b/x-pack/qa/third-party/active-directory/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectorySessionFactoryTests.java @@ -7,6 +7,7 @@ import com.unboundid.ldap.sdk.LDAPException; import com.unboundid.ldap.sdk.ResultCode; +import org.apache.lucene.util.LuceneTestCase; import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; @@ -38,6 +39,7 @@ import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; +@LuceneTestCase.AwaitsFix(bugUrl = "ActiveDirectorySessionFactoryTests") public class ActiveDirectorySessionFactoryTests extends AbstractActiveDirectoryTestCase { private final SecureString SECURED_PASSWORD = new SecureString(PASSWORD); diff --git a/x-pack/qa/third-party/active-directory/src/test/java/org/elasticsearch/xpack/security/authc/ldap/GroupMappingIT.java b/x-pack/qa/third-party/active-directory/src/test/java/org/elasticsearch/xpack/security/authc/ldap/GroupMappingIT.java index a56e1fefcba63..22d3734742085 100644 --- a/x-pack/qa/third-party/active-directory/src/test/java/org/elasticsearch/xpack/security/authc/ldap/GroupMappingIT.java +++ b/x-pack/qa/third-party/active-directory/src/test/java/org/elasticsearch/xpack/security/authc/ldap/GroupMappingIT.java @@ -5,13 +5,17 @@ */ package org.elasticsearch.xpack.security.authc.ldap; +import org.apache.lucene.util.LuceneTestCase; + import java.io.IOException; /** * This tests the group to role mappings from LDAP sources provided by the super class - available from super.realmConfig. * The super class will provide appropriate group mappings via configGroupMappings() */ +@LuceneTestCase.AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/35738") public class GroupMappingIT extends AbstractAdLdapRealmTestCase { + public void testAuthcAuthz() throws IOException { String avenger = realmConfig.loginWithCommonName ? "Natasha Romanoff" : "blackwidow"; assertAccessAllowed(avenger, "avengers"); diff --git a/x-pack/qa/third-party/active-directory/src/test/java/org/elasticsearch/xpack/security/authc/ldap/MultiGroupMappingIT.java b/x-pack/qa/third-party/active-directory/src/test/java/org/elasticsearch/xpack/security/authc/ldap/MultiGroupMappingIT.java index 5e0dda8fe2da5..120ed26b714f8 100644 --- a/x-pack/qa/third-party/active-directory/src/test/java/org/elasticsearch/xpack/security/authc/ldap/MultiGroupMappingIT.java +++ b/x-pack/qa/third-party/active-directory/src/test/java/org/elasticsearch/xpack/security/authc/ldap/MultiGroupMappingIT.java @@ -41,6 +41,7 @@ protected String configRoles() { " privileges: [ all ]\n"; } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/35738") public void testGroupMapping() throws IOException { String asgardian = "odin"; String securityPhilanthropist = realmConfig.loginWithCommonName ? "Bruce Banner" : "hulk"; diff --git a/x-pack/qa/third-party/active-directory/src/test/java/org/elasticsearch/xpack/security/authc/ldap/MultipleAdRealmIT.java b/x-pack/qa/third-party/active-directory/src/test/java/org/elasticsearch/xpack/security/authc/ldap/MultipleAdRealmIT.java index 2231d23296a4c..90a64d5794d31 100644 --- a/x-pack/qa/third-party/active-directory/src/test/java/org/elasticsearch/xpack/security/authc/ldap/MultipleAdRealmIT.java +++ b/x-pack/qa/third-party/active-directory/src/test/java/org/elasticsearch/xpack/security/authc/ldap/MultipleAdRealmIT.java @@ -62,6 +62,7 @@ protected Settings nodeSettings(int nodeOrdinal) { * Because one realm is using "common name" (cn) for login, and the other uses the "userid" (sAMAccountName) [see * {@link #setupSecondaryRealm()}], this is simply a matter of checking that we can authenticate with both identifiers. */ + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/35738") public void testCanAuthenticateAgainstBothRealms() throws IOException { assertAccessAllowed("Natasha Romanoff", "avengers"); assertAccessAllowed("blackwidow", "avengers"); diff --git a/x-pack/qa/third-party/active-directory/src/test/java/org/elasticsearch/xpack/security/authc/ldap/UserAttributeGroupsResolverTests.java b/x-pack/qa/third-party/active-directory/src/test/java/org/elasticsearch/xpack/security/authc/ldap/UserAttributeGroupsResolverTests.java index d6fc22a5cf579..0ea8936e3818e 100644 --- a/x-pack/qa/third-party/active-directory/src/test/java/org/elasticsearch/xpack/security/authc/ldap/UserAttributeGroupsResolverTests.java +++ b/x-pack/qa/third-party/active-directory/src/test/java/org/elasticsearch/xpack/security/authc/ldap/UserAttributeGroupsResolverTests.java @@ -8,6 +8,7 @@ import com.unboundid.ldap.sdk.Attribute; import com.unboundid.ldap.sdk.SearchRequest; import com.unboundid.ldap.sdk.SearchScope; +import org.apache.lucene.util.LuceneTestCase; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.xpack.core.security.support.NoOpLogger; @@ -21,6 +22,7 @@ import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.hasItems; +@LuceneTestCase.AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/35738") public class UserAttributeGroupsResolverTests extends GroupsResolverTestCase { public static final String BRUCE_BANNER_DN = "cn=Bruce Banner,CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com"; diff --git a/x-pack/test/smb-fixture/Dockerfile b/x-pack/test/smb-fixture/Dockerfile new file mode 100644 index 0000000000000..bcd74758ff496 --- /dev/null +++ b/x-pack/test/smb-fixture/Dockerfile @@ -0,0 +1,12 @@ +FROM ubuntu:16.04 +RUN apt-get update -qqy && apt-get install -qqy samba ldap-utils +ADD . /fixture +RUN chmod +x /fixture/src/main/resources/provision/installsmb.sh +RUN /fixture/src/main/resources/provision/installsmb.sh + +EXPOSE 389 +EXPOSE 636 +EXPOSE 3268 +EXPOSE 3269 + +CMD service samba-ad-dc restart && sleep infinity diff --git a/x-pack/test/smb-fixture/Vagrantfile b/x-pack/test/smb-fixture/Vagrantfile deleted file mode 100644 index e3c8d807e2761..0000000000000 --- a/x-pack/test/smb-fixture/Vagrantfile +++ /dev/null @@ -1,20 +0,0 @@ -Vagrant.configure("2") do |config| - - config.vm.define "test.ad.elastic.local" do |config| - config.vm.box = "elastic/ubuntu-16.04-x86_64" - end - - config.vm.hostname = "ad.test.elastic.local" - - if Vagrant.has_plugin?("vagrant-cachier") - config.cache.scope = :box - end - - config.vm.network "forwarded_port", guest: 389, host: 61389, protocol: "tcp" - config.vm.network "forwarded_port", guest: 636, host: 61636, protocol: "tcp" - config.vm.network "forwarded_port", guest: 3268, host: 63268, protocol: "tcp" - config.vm.network "forwarded_port", guest: 3269, host: 63269, protocol: "tcp" - - config.vm.provision "shell", path: "src/main/resources/provision/installsmb.sh" - -end diff --git a/x-pack/test/smb-fixture/build.gradle b/x-pack/test/smb-fixture/build.gradle index 233b289b295db..c361737e22c6d 100644 --- a/x-pack/test/smb-fixture/build.gradle +++ b/x-pack/test/smb-fixture/build.gradle @@ -1,43 +1,2 @@ apply plugin: 'elasticsearch.build' - -Map vagrantEnvVars = [ - 'VAGRANT_CWD' : "${project.projectDir.absolutePath}", - 'VAGRANT_VAGRANTFILE' : 'Vagrantfile', - 'VAGRANT_PROJECT_DIR' : "${project.projectDir.absolutePath}" -] - -String box = "test.ad.elastic.local" - -task update(type: org.elasticsearch.gradle.vagrant.VagrantCommandTask) { - command 'box' - subcommand 'update' - boxName box - environmentVars vagrantEnvVars -} - -task up(type: org.elasticsearch.gradle.vagrant.VagrantCommandTask) { - command 'up' - args '--provision', '--provider', 'virtualbox' - boxName box - environmentVars vagrantEnvVars - dependsOn update -} - -task halt(type: org.elasticsearch.gradle.vagrant.VagrantCommandTask) { - command 'halt' - boxName box - environmentVars vagrantEnvVars -} - -task destroy(type: org.elasticsearch.gradle.vagrant.VagrantCommandTask) { - command 'destroy' - args '-f' - boxName box - environmentVars vagrantEnvVars - dependsOn halt -} - -thirdPartyAudit.enabled = false -licenseHeaders.enabled = false -test.enabled = false -jarHell.enabled = false +apply plugin: 'elasticsearch.test.fixtures' diff --git a/x-pack/test/smb-fixture/docker-compose.yml b/x-pack/test/smb-fixture/docker-compose.yml new file mode 100644 index 0000000000000..a2c11cb50123e --- /dev/null +++ b/x-pack/test/smb-fixture/docker-compose.yml @@ -0,0 +1,11 @@ +version: '3' +services: + fixture: + build: + context: . + dockerfile: Dockerfile + ports: + - "389" + - "636" + - "3268" + - "3269" diff --git a/x-pack/test/smb-fixture/src/main/resources/provision/installsmb.sh b/x-pack/test/smb-fixture/src/main/resources/provision/installsmb.sh index 6c7425da3c0b4..47f70b2b0c29c 100644 --- a/x-pack/test/smb-fixture/src/main/resources/provision/installsmb.sh +++ b/x-pack/test/smb-fixture/src/main/resources/provision/installsmb.sh @@ -6,29 +6,17 @@ set -ex -MARKER_FILE=/etc/marker - -if [ -f $MARKER_FILE ]; then - echo "Already provisioned..." - exit 0; -fi - -VDIR=/vagrant +VDIR=/fixture RESOURCES=$VDIR/src/main/resources CERTS_DIR=$RESOURCES/certs SSL_DIR=/var/lib/samba/private/tls -# Update package manager -apt-get update -qqy - -# Install krb5 packages -apt-get install -qqy samba ldap-utils - # install ssl certs mkdir -p $SSL_DIR cp $CERTS_DIR/*.pem $SSL_DIR chmod 600 $SSL_DIR/key.pem +mkdir -p /etc/ssl/certs/ cat $SSL_DIR/ca.pem >> /etc/ssl/certs/ca-certificates.crt mv /etc/samba/smb.conf /etc/samba/smb.conf.orig @@ -92,4 +80,3 @@ EOL ldapmodify -D Administrator@ad.test.elasticsearch.com -w Passw0rd -H ldaps://127.0.0.1:636 -f /tmp/entrymods -v -touch $MARKER_FILE From 5c17b2f502c09cbeb2680c0d0298d82ca13c5662 Mon Sep 17 00:00:00 2001 From: Tanguy Leroux Date: Thu, 29 Nov 2018 09:08:30 +0100 Subject: [PATCH 014/115] ActiveShardCount should not fail when closing the index (#35936) The ActiveShardCount is used by cluster state observers to wait for a given number of shards to be active before returning to the caller. The current implementation does not work when an index is closed while an observer is waiting on shards to be active. In this case, a NPE is thrown and the observer is never notified that the shards won't become active. This commit fixes the ActiveShardCount.enoughShardsActive() so that it does not fail when an index is closed, similarly to what is done when an index is deleted. --- .../action/support/ActiveShardCount.java | 6 +++++ .../action/support/ActiveShardCountTests.java | 24 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/server/src/main/java/org/elasticsearch/action/support/ActiveShardCount.java b/server/src/main/java/org/elasticsearch/action/support/ActiveShardCount.java index cdd895ff8cd2c..7c6293a662a5a 100644 --- a/server/src/main/java/org/elasticsearch/action/support/ActiveShardCount.java +++ b/server/src/main/java/org/elasticsearch/action/support/ActiveShardCount.java @@ -155,6 +155,12 @@ public boolean enoughShardsActive(final ClusterState clusterState, final String. continue; } final IndexRoutingTable indexRoutingTable = clusterState.routingTable().index(indexName); + if (indexRoutingTable == null && indexMetaData.getState() == IndexMetaData.State.CLOSE) { + // its possible the index was closed while waiting for active shard copies, + // in this case, we'll just consider it that we have enough active shard copies + // and we can stop waiting + continue; + } assert indexRoutingTable != null; if (indexRoutingTable.allPrimaryShardsActive() == false) { // all primary shards aren't active yet diff --git a/server/src/test/java/org/elasticsearch/action/support/ActiveShardCountTests.java b/server/src/test/java/org/elasticsearch/action/support/ActiveShardCountTests.java index 4fb03bf393b3c..69f28557d9c6a 100644 --- a/server/src/test/java/org/elasticsearch/action/support/ActiveShardCountTests.java +++ b/server/src/test/java/org/elasticsearch/action/support/ActiveShardCountTests.java @@ -36,6 +36,7 @@ import java.io.IOException; import java.nio.ByteBuffer; +import java.util.Arrays; /** * Tests for the {@link ActiveShardCount} class @@ -165,6 +166,17 @@ public void testEnoughShardsActiveValueBased() { assertEquals("activeShardCount cannot be negative", e.getMessage()); } + public void testEnoughShardsActiveWithClosedIndex() { + final String indexName = "test-idx"; + final int numberOfShards = randomIntBetween(1, 5); + final int numberOfReplicas = randomIntBetween(4, 7); + + final ClusterState clusterState = initializeWithClosedIndex(indexName, numberOfShards, numberOfReplicas); + for (ActiveShardCount waitForActiveShards : Arrays.asList(ActiveShardCount.DEFAULT, ActiveShardCount.ALL, ActiveShardCount.ONE)) { + assertTrue(waitForActiveShards.enoughShardsActive(clusterState, indexName)); + } + } + private void runTestForOneActiveShard(final ActiveShardCount activeShardCount) { final String indexName = "test-idx"; final int numberOfShards = randomIntBetween(1, 5); @@ -192,6 +204,18 @@ private ClusterState initializeWithNewIndex(final String indexName, final int nu return ClusterState.builder(new ClusterName("test_cluster")).metaData(metaData).routingTable(routingTable).build(); } + private ClusterState initializeWithClosedIndex(final String indexName, final int numShards, final int numReplicas) { + final IndexMetaData indexMetaData = IndexMetaData.builder(indexName) + .settings(settings(Version.CURRENT) + .put(IndexMetaData.SETTING_INDEX_UUID, UUIDs.randomBase64UUID())) + .numberOfShards(numShards) + .numberOfReplicas(numReplicas) + .state(IndexMetaData.State.CLOSE) + .build(); + final MetaData metaData = MetaData.builder().put(indexMetaData, true).build(); + return ClusterState.builder(new ClusterName("test_cluster")).metaData(metaData).build(); + } + private ClusterState startPrimaries(final ClusterState clusterState, final String indexName) { RoutingTable routingTable = clusterState.routingTable(); IndexRoutingTable indexRoutingTable = routingTable.index(indexName); From ca4f4d199123dcc098a79ebb9415135097b14d9c Mon Sep 17 00:00:00 2001 From: Ryan Ernst Date: Thu, 29 Nov 2018 00:22:10 -0800 Subject: [PATCH 015/115] Docs: Add note about oss repositories for deb/rpm (#35973) This commit adds a note about configring the yum/apt repositories for oss only packages. closes #35960 --- docs/reference/setup/install/deb.asciidoc | 30 +++++++++++++++++++++++ docs/reference/setup/install/rpm.asciidoc | 30 +++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/docs/reference/setup/install/deb.asciidoc b/docs/reference/setup/install/deb.asciidoc index 629abe37afe62..3bbda0a579cc4 100644 --- a/docs/reference/setup/install/deb.asciidoc +++ b/docs/reference/setup/install/deb.asciidoc @@ -105,6 +105,36 @@ endif::[] include::skip-set-kernel-parameters.asciidoc[] +ifeval::["{release-state}"=="released"] + +[NOTE] +================================================== + +An alternative package which contains only features that are available under the +Apache 2.0 license is also available. To install it, use the following sources list: + +ifeval::["{release-state}"=="prerelease"] + +["source","sh",subs="attributes,callouts"] +-------------------------------------------------- +echo "deb https://artifacts.elastic.co/packages/oss-{major-version}-prerelease/apt stable main" | sudo tee -a /etc/apt/sources.list.d/elastic-{major-version}.list +-------------------------------------------------- + +endif::[] + +ifeval::["{release-state}"!="prerelease"] + +["source","sh",subs="attributes,callouts"] +-------------------------------------------------- +echo "deb https://artifacts.elastic.co/packages/oss-{major-version}/apt stable main" | sudo tee -a /etc/apt/sources.list.d/elastic-{major-version}.list +-------------------------------------------------- + +endif::[] + +================================================== + +endif::[] + [[install-deb]] ==== Download and install the Debian package manually diff --git a/docs/reference/setup/install/rpm.asciidoc b/docs/reference/setup/install/rpm.asciidoc index a6f106497e9d2..02e69a35d9c0c 100644 --- a/docs/reference/setup/install/rpm.asciidoc +++ b/docs/reference/setup/install/rpm.asciidoc @@ -90,6 +90,36 @@ sudo zypper install elasticsearch <3> endif::[] +ifeval::["{release-state}"=="released"] + +[NOTE] +================================================== + +An alternative package which contains only features that are available under the +Apache 2.0 license is also available. To install it, use the following `baseurl` in your `elasticsearch.repo` file: + +ifeval::["{release-state}"=="prerelease"] + +["source","sh",subs="attributes,callouts"] +-------------------------------------------------- +baseurl=https://artifacts.elastic.co/packages/oss-{major-version}-prerelease/yum +-------------------------------------------------- + +endif::[] + +ifeval::["{release-state}"!="prerelease"] + +["source","sh",subs="attributes,callouts"] +-------------------------------------------------- +baseurl=https://artifacts.elastic.co/packages/oss-{major-version}/yum +-------------------------------------------------- + +endif::[] + +================================================== + +endif::[] + [[install-rpm]] ==== Download and install the RPM manually From 3c2984106f3cb68ce3ea7f5ce1dc58691aa95695 Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Thu, 29 Nov 2018 09:46:09 +0100 Subject: [PATCH 016/115] [CCR] Only auto follow indices when all primary shards have started (#35814) This change adds an extra check that verifies that all primary shards have been started of an index that is about to be auto followed. If not all primary shards have been started for an index then the next auto follow run will try to follow to auto follow this index again. Closes #35480 --- .../ccr/action/AutoFollowCoordinator.java | 11 +- .../action/AutoFollowCoordinatorTests.java | 108 ++++++++++++++---- 2 files changed, 97 insertions(+), 22 deletions(-) diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/AutoFollowCoordinator.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/AutoFollowCoordinator.java index 6323fb7f103db..0e86aa157adfc 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/AutoFollowCoordinator.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/AutoFollowCoordinator.java @@ -19,6 +19,7 @@ import org.elasticsearch.cluster.ClusterStateUpdateTask; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.cluster.metadata.MetaData; +import org.elasticsearch.cluster.routing.IndexRoutingTable; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.settings.Settings; @@ -164,6 +165,7 @@ void getLeaderClusterState(final String remoteCluster, final ClusterStateRequest request = new ClusterStateRequest(); request.clear(); request.metaData(true); + request.routingTable(true); // TODO: set non-compliant status on auto-follow coordination that can be viewed via a stats API ccrLicenseChecker.checkRemoteClusterLicenseAndFetchClusterState( client, @@ -367,7 +369,14 @@ static List getLeaderIndicesToFollow(String remoteCluster, List leaderIndicesToFollow = new ArrayList<>(); for (IndexMetaData leaderIndexMetaData : leaderClusterState.getMetaData()) { if (autoFollowPattern.match(leaderIndexMetaData.getIndex().getName())) { - if (followedIndexUUIDs.contains(leaderIndexMetaData.getIndex().getUUID()) == false) { + IndexRoutingTable indexRoutingTable = leaderClusterState.routingTable().index(leaderIndexMetaData.getIndex()); + if (indexRoutingTable != null && + // Leader indices can be in the cluster state, but not all primary shards may be ready yet. + // This checks ensures all primary shards have started, so that index following does not fail. + // If not all primary shards are ready, then the next time the auto follow coordinator runs + // this index will be auto followed. + indexRoutingTable.allPrimaryShardsActive() && + followedIndexUUIDs.contains(leaderIndexMetaData.getIndex().getUUID()) == false) { // TODO: iterate over the indices in the followerClusterState and check whether a IndexMetaData // has a leader index uuid custom metadata entry that matches with uuid of leaderIndexMetaData variable // If so then handle it differently: not follow it, but just add an entry to diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/AutoFollowCoordinatorTests.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/AutoFollowCoordinatorTests.java index 1da58cc2703db..4624a3622b992 100644 --- a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/AutoFollowCoordinatorTests.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/AutoFollowCoordinatorTests.java @@ -11,6 +11,11 @@ import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.cluster.metadata.MetaData; +import org.elasticsearch.cluster.routing.IndexRoutingTable; +import org.elasticsearch.cluster.routing.RoutingTable; +import org.elasticsearch.cluster.routing.ShardRouting; +import org.elasticsearch.cluster.routing.ShardRoutingState; +import org.elasticsearch.cluster.routing.TestShardRouting; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.settings.Settings; @@ -49,12 +54,7 @@ public void testAutoFollower() { Client client = mock(Client.class); when(client.getRemoteClusterClient(anyString())).thenReturn(client); - ClusterState leaderState = ClusterState.builder(new ClusterName("remote")) - .metaData(MetaData.builder().put(IndexMetaData.builder("logs-20190101") - .settings(settings(Version.CURRENT).put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true)) - .numberOfShards(1) - .numberOfReplicas(0))) - .build(); + ClusterState leaderState = createRemoteClusterState("logs-20190101"); AutoFollowPattern autoFollowPattern = new AutoFollowPattern("remote", Collections.singletonList("logs-*"), null, null, null, null, null, null, null, null, null, null, null); @@ -168,13 +168,7 @@ void updateAutoFollowMetadata(Function updateFunctio public void testAutoFollowerUpdateClusterStateFailure() { Client client = mock(Client.class); when(client.getRemoteClusterClient(anyString())).thenReturn(client); - - ClusterState leaderState = ClusterState.builder(new ClusterName("remote")) - .metaData(MetaData.builder().put(IndexMetaData.builder("logs-20190101") - .settings(settings(Version.CURRENT).put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true)) - .numberOfShards(1) - .numberOfReplicas(0))) - .build(); + ClusterState leaderState = createRemoteClusterState("logs-20190101"); AutoFollowPattern autoFollowPattern = new AutoFollowPattern("remote", Collections.singletonList("logs-*"), null, null, null, null, null, null, null, null, null, null, null); @@ -230,13 +224,7 @@ void updateAutoFollowMetadata(Function updateFunctio public void testAutoFollowerCreateAndFollowApiCallFailure() { Client client = mock(Client.class); when(client.getRemoteClusterClient(anyString())).thenReturn(client); - - ClusterState leaderState = ClusterState.builder(new ClusterName("remote")) - .metaData(MetaData.builder().put(IndexMetaData.builder("logs-20190101") - .settings(settings(Version.CURRENT).put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true)) - .numberOfShards(1) - .numberOfReplicas(0))) - .build(); + ClusterState leaderState = createRemoteClusterState("logs-20190101"); AutoFollowPattern autoFollowPattern = new AutoFollowPattern("remote", Collections.singletonList("logs-*"), null, null, null, null, null, null, null, null, null, null, null); @@ -299,24 +287,39 @@ public void testGetLeaderIndicesToFollow() { new AutoFollowMetadata(Collections.singletonMap("remote", autoFollowPattern), Collections.emptyMap(), headers))) .build(); + RoutingTable.Builder routingTableBuilder = RoutingTable.builder(); MetaData.Builder imdBuilder = MetaData.builder(); for (int i = 0; i < 5; i++) { + String indexName = "metrics-" + i; Settings.Builder builder = Settings.builder() .put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT) - .put(IndexMetaData.SETTING_INDEX_UUID, "metrics-" + i) + .put(IndexMetaData.SETTING_INDEX_UUID, indexName) .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), i % 2 == 0); imdBuilder.put(IndexMetaData.builder("metrics-" + i) .settings(builder) .numberOfShards(1) .numberOfReplicas(0)); + + ShardRouting shardRouting = + TestShardRouting.newShardRouting(indexName, 0, "1", true, ShardRoutingState.INITIALIZING).moveToStarted(); + IndexRoutingTable indexRoutingTable = IndexRoutingTable.builder(imdBuilder.get(indexName).getIndex()) + .addShard(shardRouting) + .build(); + routingTableBuilder.add(indexRoutingTable); } + imdBuilder.put(IndexMetaData.builder("logs-0") .settings(settings(Version.CURRENT)) .numberOfShards(1) .numberOfReplicas(0)); + ShardRouting shardRouting = + TestShardRouting.newShardRouting("logs-0", 0, "1", true, ShardRoutingState.INITIALIZING).moveToStarted(); + IndexRoutingTable indexRoutingTable = IndexRoutingTable.builder(imdBuilder.get("logs-0").getIndex()).addShard(shardRouting).build(); + routingTableBuilder.add(indexRoutingTable); ClusterState leaderState = ClusterState.builder(new ClusterName("remote")) .metaData(imdBuilder) + .routingTable(routingTableBuilder.build()) .build(); List result = AutoFollower.getLeaderIndicesToFollow("remote", autoFollowPattern, leaderState, followerState, @@ -335,6 +338,52 @@ public void testGetLeaderIndicesToFollow() { assertThat(result.get(1).getName(), equalTo("metrics-4")); } + public void testGetLeaderIndicesToFollow_shardsNotStarted() { + AutoFollowPattern autoFollowPattern = new AutoFollowPattern("remote", Collections.singletonList("*"), + null, null, null, null, null, null, null, null, null, null, null); + Map> headers = new HashMap<>(); + ClusterState followerState = ClusterState.builder(new ClusterName("remote")) + .metaData(MetaData.builder().putCustom(AutoFollowMetadata.TYPE, + new AutoFollowMetadata(Collections.singletonMap("remote", autoFollowPattern), Collections.emptyMap(), headers))) + .build(); + + // 1 shard started and another not started: + ClusterState leaderState = createRemoteClusterState("index1"); + MetaData.Builder mBuilder= MetaData.builder(leaderState.metaData()); + mBuilder.put(IndexMetaData.builder("index2") + .settings(settings(Version.CURRENT).put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true)) + .numberOfShards(1) + .numberOfReplicas(0)); + ShardRouting shardRouting = + TestShardRouting.newShardRouting("index2", 0, "1", true, ShardRoutingState.INITIALIZING); + IndexRoutingTable indexRoutingTable = IndexRoutingTable.builder(mBuilder.get("index2").getIndex() + ).addShard(shardRouting).build(); + leaderState = ClusterState.builder(leaderState.getClusterName()) + .metaData(mBuilder) + .routingTable(RoutingTable.builder(leaderState.routingTable()).add(indexRoutingTable).build()) + .build(); + + List result = AutoFollower.getLeaderIndicesToFollow("remote", autoFollowPattern, leaderState, followerState, + Collections.emptyList()); + assertThat(result.size(), equalTo(1)); + assertThat(result.get(0).getName(), equalTo("index1")); + + // Start second shard: + shardRouting = shardRouting.moveToStarted(); + indexRoutingTable = IndexRoutingTable.builder(leaderState.metaData().indices().get("index2").getIndex()) + .addShard(shardRouting).build(); + leaderState = ClusterState.builder(leaderState.getClusterName()) + .metaData(leaderState.metaData()) + .routingTable(RoutingTable.builder(leaderState.routingTable()).add(indexRoutingTable).build()) + .build(); + + result = AutoFollower.getLeaderIndicesToFollow("remote", autoFollowPattern, leaderState, followerState, Collections.emptyList()); + assertThat(result.size(), equalTo(2)); + result.sort(Comparator.comparing(Index::getName)); + assertThat(result.get(0).getName(), equalTo("index1")); + assertThat(result.get(1).getName(), equalTo("index2")); + } + public void testGetFollowerIndexName() { AutoFollowPattern autoFollowPattern = new AutoFollowPattern("remote", Collections.singletonList("metrics-*"), null, null, null, null, null, null, null, null, null, null, null); @@ -408,4 +457,21 @@ public void testStats() { assertThat(autoFollowStats.getRecentAutoFollowErrors().get("_alias2:index2").getCause().getMessage(), equalTo("error")); } + private static ClusterState createRemoteClusterState(String indexName) { + IndexMetaData indexMetaData = IndexMetaData.builder(indexName) + .settings(settings(Version.CURRENT).put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true)) + .numberOfShards(1) + .numberOfReplicas(0) + .build(); + ClusterState.Builder csBuilder = ClusterState.builder(new ClusterName("remote")) + .metaData(MetaData.builder().put(indexMetaData, true)); + + ShardRouting shardRouting = + TestShardRouting.newShardRouting(indexName, 0, "1", true, ShardRoutingState.INITIALIZING).moveToStarted(); + IndexRoutingTable indexRoutingTable = IndexRoutingTable.builder(indexMetaData.getIndex()).addShard(shardRouting).build(); + csBuilder.routingTable(RoutingTable.builder().add(indexRoutingTable).build()).build(); + + return csBuilder.build(); + } + } From f56393d184b59c615ecb56fdc8f8f6b8659045f0 Mon Sep 17 00:00:00 2001 From: Alpar Torok Date: Thu, 29 Nov 2018 11:14:44 +0200 Subject: [PATCH 017/115] Prevent random build failures Looks like some odd race condition causes failed builds by attempting to run the task that should be disabled. Disable the task explicitly untill we figure it out. --- x-pack/test/smb-fixture/build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/x-pack/test/smb-fixture/build.gradle b/x-pack/test/smb-fixture/build.gradle index c361737e22c6d..b543dad95cd3b 100644 --- a/x-pack/test/smb-fixture/build.gradle +++ b/x-pack/test/smb-fixture/build.gradle @@ -1,2 +1,5 @@ apply plugin: 'elasticsearch.build' apply plugin: 'elasticsearch.test.fixtures' + +// TODO: elasticsearch.test.fixtures should disable this but it sometimes doesn't +jarHell.enabled = false From bf61173c852854d4673ae8115ccacaf4a94b3a1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20B=C3=BCscher?= Date: Thu, 29 Nov 2018 11:43:18 +0100 Subject: [PATCH 018/115] Muting ClusterClientITT#testClusterHealthYellowClusterLevel --- .../src/test/java/org/elasticsearch/client/ClusterClientIT.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ClusterClientIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ClusterClientIT.java index 76af65cde2352..3936f6eebe2cc 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/ClusterClientIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ClusterClientIT.java @@ -35,7 +35,6 @@ import org.elasticsearch.common.unit.ByteSizeUnit; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.common.xcontent.support.XContentMapValues; -import org.elasticsearch.client.Request; import org.elasticsearch.indices.recovery.RecoverySettings; import org.elasticsearch.rest.RestStatus; @@ -172,6 +171,7 @@ public void testClusterHealthGreen() throws IOException { assertNoIndices(response); } + @AwaitsFix(bugUrl="https://github.com/elastic/elasticsearch/issues/35450") public void testClusterHealthYellowClusterLevel() throws IOException { createIndex("index", Settings.EMPTY); createIndex("index2", Settings.EMPTY); From c847953f8aa166c7e1bd0b1580ff56bbbc1c5ff4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20B=C3=BCscher?= Date: Thu, 29 Nov 2018 12:01:33 +0100 Subject: [PATCH 019/115] Muting WatchBackwardsCompatibilityIT#testWatcherRestart in qa:rolling-upgrade:without-system-key --- .../elasticsearch/upgrades/WatchBackwardsCompatibilityIT.java | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/WatchBackwardsCompatibilityIT.java b/x-pack/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/WatchBackwardsCompatibilityIT.java index be3b0525d06bf..e2a2250318008 100644 --- a/x-pack/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/WatchBackwardsCompatibilityIT.java +++ b/x-pack/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/WatchBackwardsCompatibilityIT.java @@ -177,6 +177,7 @@ public void testWatcherStats() throws Exception { ); } + @AwaitsFix(bugUrl="https://github.com/elastic/elasticsearch/issues/33753") public void testWatcherRestart() throws Exception { executeUpgradeIfNeeded(); From b515ec7c9b9074dfa2f5fd28bac68fd8a482209e Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Tue, 27 Nov 2018 23:35:42 +0200 Subject: [PATCH 020/115] Add realm information for Authenticate API (#35648) - Add the authentication realm and lookup realm name and type in the response for the _authenticate API - The authentication realm is set as the lookup realm too (instead of setting the lookup realm to null or empty ) when no lookup realm is used. --- .../client/security/AuthenticateResponse.java | 82 ++++++++++++++++--- .../SecurityDocumentationIT.java | 8 ++ .../security/AuthenticateResponseTests.java | 48 +++++++++-- .../high-level/security/authenticate.asciidoc | 14 +++- .../rest-api/security/authenticate.asciidoc | 14 +++- .../action/user/AuthenticateResponse.java | 27 ++++-- .../core/security/authc/Authentication.java | 29 ++++++- .../xpack/core/security/user/User.java | 4 + .../user/TransportAuthenticateAction.java | 6 +- .../rest/action/RestAuthenticateAction.java | 2 +- .../TransportAuthenticateActionTests.java | 12 ++- .../security/authc/TokenAuthIntegTests.java | 8 +- .../authc/esnative/NativeRealmIntegTests.java | 6 +- .../action/RestAuthenticateActionTests.java | 7 +- .../authc/ldap/ActiveDirectoryRunAsIT.java | 2 +- 15 files changed, 222 insertions(+), 47 deletions(-) diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/AuthenticateResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/AuthenticateResponse.java index 62f1cc0955bd1..b3b8fc2c23591 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/AuthenticateResponse.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/AuthenticateResponse.java @@ -46,27 +46,43 @@ public final class AuthenticateResponse { static final ParseField FULL_NAME = new ParseField("full_name"); static final ParseField EMAIL = new ParseField("email"); static final ParseField ENABLED = new ParseField("enabled"); + static final ParseField AUTHENTICATION_REALM = new ParseField("authentication_realm"); + static final ParseField LOOKUP_REALM = new ParseField("lookup_realm"); + static final ParseField REALM_NAME = new ParseField("name"); + static final ParseField REALM_TYPE = new ParseField("type"); @SuppressWarnings("unchecked") private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( "client_security_authenticate_response", a -> new AuthenticateResponse(new User((String) a[0], ((List) a[1]), (Map) a[2], - (String) a[3], (String) a[4]), (Boolean) a[5])); + (String) a[3], (String) a[4]), (Boolean) a[5], (RealmInfo) a[6], (RealmInfo) a[7])); static { + final ConstructingObjectParser realmInfoParser = new ConstructingObjectParser<>("realm_info", + a -> new RealmInfo((String) a[0], (String) a[1])); + realmInfoParser.declareString(constructorArg(), REALM_NAME); + realmInfoParser.declareString(constructorArg(), REALM_TYPE); PARSER.declareString(constructorArg(), USERNAME); PARSER.declareStringArray(constructorArg(), ROLES); PARSER.>declareObject(constructorArg(), (parser, c) -> parser.map(), METADATA); PARSER.declareStringOrNull(optionalConstructorArg(), FULL_NAME); PARSER.declareStringOrNull(optionalConstructorArg(), EMAIL); PARSER.declareBoolean(constructorArg(), ENABLED); + PARSER.declareObject(constructorArg(), realmInfoParser, AUTHENTICATION_REALM); + PARSER.declareObject(constructorArg(), realmInfoParser, LOOKUP_REALM); } private final User user; private final boolean enabled; + private final RealmInfo authenticationRealm; + private final RealmInfo lookupRealm; - public AuthenticateResponse(User user, boolean enabled) { + + public AuthenticateResponse(User user, boolean enabled, RealmInfo authenticationRealm, + RealmInfo lookupRealm) { this.user = user; this.enabled = enabled; + this.authenticationRealm = authenticationRealm; + this.lookupRealm = lookupRealm; } /** @@ -85,25 +101,69 @@ public boolean enabled() { return enabled; } + /** + * @return the realm that authenticated the user + */ + public RealmInfo getAuthenticationRealm() { + return authenticationRealm; + } + + /** + * @return the realm where the user information was looked up + */ + public RealmInfo getLookupRealm() { + return lookupRealm; + } + @Override public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - final AuthenticateResponse that = (AuthenticateResponse) o; - return user.equals(that.user) && enabled == that.enabled; + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + AuthenticateResponse that = (AuthenticateResponse) o; + return enabled == that.enabled && + Objects.equals(user, that.user) && + Objects.equals(authenticationRealm, that.authenticationRealm) && + Objects.equals(lookupRealm, that.lookupRealm); } @Override public int hashCode() { - return Objects.hash(user, enabled); + return Objects.hash(user, enabled, authenticationRealm, lookupRealm); } public static AuthenticateResponse fromXContent(XContentParser parser) throws IOException { return PARSER.parse(parser, null); } + public static class RealmInfo { + private String name; + private String type; + + RealmInfo(String name, String type) { + this.name = name; + this.type = type; + } + + public String getName() { + return name; + } + + public String getType() { + return type; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RealmInfo realmInfo = (RealmInfo) o; + return Objects.equals(name, realmInfo.name) && + Objects.equals(type, realmInfo.type); + } + + @Override + public int hashCode() { + return Objects.hash(name, type); + } + } } diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java index 87e1324786378..79258b314510c 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java @@ -595,6 +595,10 @@ public void testAuthenticate() throws Exception { //tag::authenticate-response User user = response.getUser(); // <1> boolean enabled = response.enabled(); // <2> + final String authenticationRealmName = response.getAuthenticationRealm().getName(); // <3> + final String authenticationRealmType = response.getAuthenticationRealm().getType(); // <4> + final String lookupRealmName = response.getLookupRealm().getName(); // <5> + final String lookupRealmType = response.getLookupRealm().getType(); // <6> //end::authenticate-response assertThat(user.getUsername(), is("test_user")); @@ -603,6 +607,10 @@ public void testAuthenticate() throws Exception { assertThat(user.getEmail(), nullValue()); assertThat(user.getMetadata().isEmpty(), is(true)); assertThat(enabled, is(true)); + assertThat(authenticationRealmName, is("default_file")); + assertThat(authenticationRealmType, is("file")); + assertThat(lookupRealmName, is("default_file")); + assertThat(lookupRealmType, is("file")); } { diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/AuthenticateResponseTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/AuthenticateResponseTests.java index 1931ce3f69883..f09340fa09ffd 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/AuthenticateResponseTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/AuthenticateResponseTests.java @@ -70,7 +70,14 @@ protected AuthenticateResponse createTestInstance() { final String fullName = randomFrom(random(), null, randomAlphaOfLengthBetween(0, 4)); final String email = randomFrom(random(), null, randomAlphaOfLengthBetween(0, 4)); final boolean enabled = randomBoolean(); - return new AuthenticateResponse(new User(username, roles, metadata, fullName, email), enabled); + final String authenticationRealmName = randomAlphaOfLength(5); + final String authenticationRealmType = randomFrom("file", "native", "ldap", "active_directory", "saml", "kerberos"); + final String lookupRealmName = randomAlphaOfLength(5); + final String lookupRealmType = randomFrom("file", "native", "ldap", "active_directory", "saml", "kerberos"); + return new AuthenticateResponse( + new User(username, roles, metadata, fullName, email), enabled, + new AuthenticateResponse.RealmInfo(authenticationRealmName, authenticationRealmType), + new AuthenticateResponse.RealmInfo(lookupRealmName, lookupRealmType)); } private void toXContent(AuthenticateResponse response, XContentBuilder builder) throws IOException { @@ -87,6 +94,14 @@ private void toXContent(AuthenticateResponse response, XContentBuilder builder) builder.field(AuthenticateResponse.EMAIL.getPreferredName(), user.getEmail()); } builder.field(AuthenticateResponse.ENABLED.getPreferredName(), enabled); + builder.startObject(AuthenticateResponse.AUTHENTICATION_REALM.getPreferredName()); + builder.field(AuthenticateResponse.REALM_NAME.getPreferredName(), response.getAuthenticationRealm().getName()); + builder.field(AuthenticateResponse.REALM_TYPE.getPreferredName(), response.getAuthenticationRealm().getType()); + builder.endObject(); + builder.startObject(AuthenticateResponse.LOOKUP_REALM.getPreferredName()); + builder.field(AuthenticateResponse.REALM_NAME.getPreferredName(), response.getLookupRealm().getName()); + builder.field(AuthenticateResponse.REALM_TYPE.getPreferredName(), response.getLookupRealm().getType()); + builder.endObject(); builder.endObject(); } @@ -94,34 +109,49 @@ private AuthenticateResponse copy(AuthenticateResponse response) { final User originalUser = response.getUser(); final User copyUser = new User(originalUser.getUsername(), originalUser.getRoles(), originalUser.getMetadata(), originalUser.getFullName(), originalUser.getEmail()); - return new AuthenticateResponse(copyUser, response.enabled()); + return new AuthenticateResponse(copyUser, response.enabled(), response.getAuthenticationRealm(), + response.getLookupRealm()); } private AuthenticateResponse mutate(AuthenticateResponse response) { final User originalUser = response.getUser(); - switch (randomIntBetween(1, 6)) { + switch (randomIntBetween(1, 8)) { case 1: return new AuthenticateResponse(new User(originalUser.getUsername() + "wrong", originalUser.getRoles(), - originalUser.getMetadata(), originalUser.getFullName(), originalUser.getEmail()), response.enabled()); + originalUser.getMetadata(), originalUser.getFullName(), originalUser.getEmail()), response.enabled(), + response.getAuthenticationRealm(), response.getLookupRealm()); case 2: final Collection wrongRoles = new ArrayList<>(originalUser.getRoles()); wrongRoles.add(randomAlphaOfLengthBetween(1, 4)); return new AuthenticateResponse(new User(originalUser.getUsername(), wrongRoles, originalUser.getMetadata(), - originalUser.getFullName(), originalUser.getEmail()), response.enabled()); + originalUser.getFullName(), originalUser.getEmail()), response.enabled(), response.getAuthenticationRealm(), + response.getLookupRealm()); case 3: final Map wrongMetadata = new HashMap<>(originalUser.getMetadata()); wrongMetadata.put("wrong_string", randomAlphaOfLengthBetween(0, 4)); return new AuthenticateResponse(new User(originalUser.getUsername(), originalUser.getRoles(), wrongMetadata, - originalUser.getFullName(), originalUser.getEmail()), response.enabled()); + originalUser.getFullName(), originalUser.getEmail()), response.enabled(), response.getAuthenticationRealm(), + response.getLookupRealm()); case 4: return new AuthenticateResponse(new User(originalUser.getUsername(), originalUser.getRoles(), originalUser.getMetadata(), - originalUser.getFullName() + "wrong", originalUser.getEmail()), response.enabled()); + originalUser.getFullName() + "wrong", originalUser.getEmail()), response.enabled(), + response.getAuthenticationRealm(), response.getLookupRealm()); case 5: return new AuthenticateResponse(new User(originalUser.getUsername(), originalUser.getRoles(), originalUser.getMetadata(), - originalUser.getFullName(), originalUser.getEmail() + "wrong"), response.enabled()); + originalUser.getFullName(), originalUser.getEmail() + "wrong"), response.enabled(), + response.getAuthenticationRealm(), response.getLookupRealm()); case 6: return new AuthenticateResponse(new User(originalUser.getUsername(), originalUser.getRoles(), originalUser.getMetadata(), - originalUser.getFullName(), originalUser.getEmail()), !response.enabled()); + originalUser.getFullName(), originalUser.getEmail()), !response.enabled(), response.getAuthenticationRealm(), + response.getLookupRealm()); + case 7: + return new AuthenticateResponse(new User(originalUser.getUsername(), originalUser.getRoles(), originalUser.getMetadata(), + originalUser.getFullName(), originalUser.getEmail()), response.enabled(), response.getAuthenticationRealm(), + new AuthenticateResponse.RealmInfo(randomAlphaOfLength(5), randomAlphaOfLength(5))); + case 8: + return new AuthenticateResponse(new User(originalUser.getUsername(), originalUser.getRoles(), originalUser.getMetadata(), + originalUser.getFullName(), originalUser.getEmail()), response.enabled(), + new AuthenticateResponse.RealmInfo(randomAlphaOfLength(5), randomAlphaOfLength(5)), response.getLookupRealm()); } throw new IllegalStateException("Bad random number"); } diff --git a/docs/java-rest/high-level/security/authenticate.asciidoc b/docs/java-rest/high-level/security/authenticate.asciidoc index e50c64bf9d0f5..4d4467a03b4d2 100644 --- a/docs/java-rest/high-level/security/authenticate.asciidoc +++ b/docs/java-rest/high-level/security/authenticate.asciidoc @@ -24,10 +24,14 @@ This method does not require a request object. The client waits for the [id="{upid}-{api}-response"] ==== Response -The returned +{response}+ contains two fields. Firstly, the `user` field +The returned +{response}+ contains four fields. The `user` field , accessed with `getUser`, contains all the information about this -authenticated user. The other field, `enabled`, tells if this user is actually -usable or has been temporalily deactivated. +authenticated user. The field `enabled`, tells if this user is actually +usable or has been temporarily deactivated. The field `authentication_realm`, +accessed with `getAuthenticationRealm` contains the name and type of the +Realm that has authenticated the user and the field `lookup_realm`, +accessed with `getLookupRealm` contains the name and type of the Realm where +the user information were retrieved from. ["source","java",subs="attributes,callouts,macros"] -------------------------------------------------- @@ -36,6 +40,10 @@ include-tagged::{doc-tests-file}[{api}-response] <1> `getUser` retrieves the `User` instance containing the information, see {javadoc-client}/security/user/User.html. <2> `enabled` tells if this user is usable or is deactivated. +<3> `getAuthenticationRealm().getName()` retrieves the name of the realm that authenticated the user. +<4> `getAuthenticationRealm().getType()` retrieves the type of the realm that authenticated the user. +<5> `getLookupRealm().getName()` retrieves the name of the realm from where the user information is looked up. +<6> `getLookupRealm().getType()` retrieves the type of the realm from where the user information is looked up. [id="{upid}-{api}-async"] ==== Asynchronous Execution diff --git a/x-pack/docs/en/rest-api/security/authenticate.asciidoc b/x-pack/docs/en/rest-api/security/authenticate.asciidoc index ab259762332f9..1975a9dde790b 100644 --- a/x-pack/docs/en/rest-api/security/authenticate.asciidoc +++ b/x-pack/docs/en/rest-api/security/authenticate.asciidoc @@ -13,8 +13,8 @@ authenticate a user and retrieve information about the authenticated user. ==== Description -A successful call returns a JSON structure that shows what roles are assigned -to the user as well as any assigned metadata. +A successful call returns a JSON structure that shows user information such as their username, the roles that are +assigned to the user, any assigned metadata, and information about the realms that authenticated and authorized the user. If the user cannot be authenticated, this API returns a 401 status code. @@ -41,7 +41,15 @@ The following example output provides information about the "rdeniro" user: "full_name": null, "email": null, "metadata": { }, - "enabled": true + "enabled": true, + "authentication_realm": { + "name" : "default_file", + "type" : "file" + }, + "lookup_realm": { + "name" : "default_file", + "type" : "file" + } } -------------------------------------------------- // TESTRESPONSE[s/"rdeniro"/"$body.username"/] diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/AuthenticateResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/AuthenticateResponse.java index 0cf7ace1103d0..06a4df019c326 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/AuthenticateResponse.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/AuthenticateResponse.java @@ -5,36 +5,49 @@ */ package org.elasticsearch.xpack.core.security.action.user; +import org.elasticsearch.Version; import org.elasticsearch.action.ActionResponse; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.user.User; import java.io.IOException; public class AuthenticateResponse extends ActionResponse { - private User user; + private Authentication authentication; public AuthenticateResponse() {} - public AuthenticateResponse(User user) { - this.user = user; + public AuthenticateResponse(Authentication authentication){ + this.authentication = authentication; } - public User user() { - return user; + public Authentication authentication() { + return authentication; } @Override public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); - User.writeTo(user, out); + if (out.getVersion().before(Version.V_6_6_0)) { + User.writeTo(authentication.getUser(), out); + } else { + authentication.writeTo(out); + } } @Override public void readFrom(StreamInput in) throws IOException { super.readFrom(in); - user = User.readFrom(in); + if (in.getVersion().before(Version.V_6_6_0)) { + final User user = User.readFrom(in); + final Authentication.RealmRef unknownRealm = new Authentication.RealmRef("__unknown", "__unknown", "__unknown"); + authentication = new Authentication(user, unknownRealm, unknownRealm); + } else { + authentication = new Authentication(in); + } } + } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java index 161d9d449990f..b9dbe0a948ff2 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java @@ -11,6 +11,8 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.xpack.core.security.user.InternalUserSerializationHelper; import org.elasticsearch.xpack.core.security.user.User; @@ -20,7 +22,7 @@ // TODO(hub-cap) Clean this up after moving User over - This class can re-inherit its field AUTHENTICATION_KEY in AuthenticationField. // That interface can be removed -public class Authentication { +public class Authentication implements ToXContentObject { private final User user; private final RealmRef authenticatedBy; @@ -163,6 +165,31 @@ public int hashCode() { return result; } + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(User.Fields.USERNAME.getPreferredName(), user.principal()); + builder.array(User.Fields.ROLES.getPreferredName(), user.roles()); + builder.field(User.Fields.FULL_NAME.getPreferredName(), user.fullName()); + builder.field(User.Fields.EMAIL.getPreferredName(), user.email()); + builder.field(User.Fields.METADATA.getPreferredName(), user.metadata()); + builder.field(User.Fields.ENABLED.getPreferredName(), user.enabled()); + builder.startObject(User.Fields.AUTHENTICATION_REALM.getPreferredName()); + builder.field(User.Fields.REALM_NAME.getPreferredName(), getAuthenticatedBy().getName()); + builder.field(User.Fields.REALM_TYPE.getPreferredName(), getAuthenticatedBy().getType()); + builder.endObject(); + builder.startObject(User.Fields.LOOKUP_REALM.getPreferredName()); + if (getLookedUpBy() != null) { + builder.field(User.Fields.REALM_NAME.getPreferredName(), getLookedUpBy().getName()); + builder.field(User.Fields.REALM_TYPE.getPreferredName(), getLookedUpBy().getType()); + } else { + builder.field(User.Fields.REALM_NAME.getPreferredName(), getAuthenticatedBy().getName()); + builder.field(User.Fields.REALM_TYPE.getPreferredName(), getAuthenticatedBy().getType()); + } + builder.endObject(); + return builder.endObject(); + } + public static class RealmRef { private final String nodeName; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/User.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/User.java index 3fc1ed7254f67..660480513bb29 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/User.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/User.java @@ -241,6 +241,10 @@ public interface Fields { ParseField METADATA = new ParseField("metadata"); ParseField ENABLED = new ParseField("enabled"); ParseField TYPE = new ParseField("type"); + ParseField AUTHENTICATION_REALM = new ParseField("authentication_realm"); + ParseField LOOKUP_REALM = new ParseField("lookup_realm"); + ParseField REALM_TYPE = new ParseField("type"); + ParseField REALM_NAME = new ParseField("name"); } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportAuthenticateAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportAuthenticateAction.java index 6386917a1e98f..67c4811a92e68 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportAuthenticateAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportAuthenticateAction.java @@ -18,6 +18,7 @@ import org.elasticsearch.xpack.core.security.action.user.AuthenticateAction; import org.elasticsearch.xpack.core.security.action.user.AuthenticateRequest; import org.elasticsearch.xpack.core.security.action.user.AuthenticateResponse; +import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.user.SystemUser; import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.security.user.XPackUser; @@ -37,7 +38,8 @@ public TransportAuthenticateAction(Settings settings, ThreadPool threadPool, Tra @Override protected void doExecute(AuthenticateRequest request, ActionListener listener) { - final User runAsUser = securityContext.getUser(); + final Authentication authentication = securityContext.getAuthentication(); + final User runAsUser = authentication == null ? null : authentication.getUser(); final User authUser = runAsUser == null ? null : runAsUser.authenticatedUser(); if (authUser == null) { listener.onFailure(new ElasticsearchSecurityException("did not find an authenticated user")); @@ -46,7 +48,7 @@ protected void doExecute(AuthenticateRequest request, ActionListener(channel) { @Override public RestResponse buildResponse(AuthenticateResponse authenticateResponse, XContentBuilder builder) throws Exception { - authenticateResponse.user().toXContent(builder, ToXContent.EMPTY_PARAMS); + authenticateResponse.authentication().toXContent(builder, ToXContent.EMPTY_PARAMS); return new BytesRestResponse(RestStatus.OK, builder); } }); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportAuthenticateActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportAuthenticateActionTests.java index d09c135836a64..2dc5262a5e744 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportAuthenticateActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/TransportAuthenticateActionTests.java @@ -17,6 +17,7 @@ import org.elasticsearch.xpack.core.security.SecurityContext; import org.elasticsearch.xpack.core.security.action.user.AuthenticateRequest; import org.elasticsearch.xpack.core.security.action.user.AuthenticateResponse; +import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.user.ElasticUser; import org.elasticsearch.xpack.core.security.user.KibanaUser; import org.elasticsearch.xpack.core.security.user.SystemUser; @@ -38,7 +39,9 @@ public class TransportAuthenticateActionTests extends ESTestCase { public void testInternalUser() { SecurityContext securityContext = mock(SecurityContext.class); - when(securityContext.getUser()).thenReturn(randomFrom(SystemUser.INSTANCE, XPackUser.INSTANCE)); + final Authentication authentication = new Authentication(randomFrom(SystemUser.INSTANCE, XPackUser.INSTANCE), + new Authentication.RealmRef("native", "default_native", "node1"), null); + when(securityContext.getAuthentication()).thenReturn(authentication); TransportService transportService = new TransportService(Settings.EMPTY, mock(Transport.class), null, TransportService.NOOP_TRANSPORT_INTERCEPTOR, x -> null, null, Collections.emptySet()); TransportAuthenticateAction action = new TransportAuthenticateAction(Settings.EMPTY, mock(ThreadPool.class), transportService, @@ -89,9 +92,12 @@ public void onFailure(Exception e) { assertThat(throwableRef.get().getMessage(), containsString("did not find an authenticated user")); } - public void testValidUser() { + public void testValidAuthentication(){ final User user = randomFrom(new ElasticUser(true), new KibanaUser(true), new User("joe")); + final Authentication authentication = new Authentication(user, new Authentication.RealmRef("native_realm", "native", "node1"), + null); SecurityContext securityContext = mock(SecurityContext.class); + when(securityContext.getAuthentication()).thenReturn(authentication); when(securityContext.getUser()).thenReturn(user); TransportService transportService = new TransportService(Settings.EMPTY, mock(Transport.class), null, TransportService.NOOP_TRANSPORT_INTERCEPTOR, x -> null, null, Collections.emptySet()); @@ -113,7 +119,7 @@ public void onFailure(Exception e) { }); assertThat(responseRef.get(), notNullValue()); - assertThat(responseRef.get().user(), sameInstance(user)); + assertThat(responseRef.get().authentication(), sameInstance(authentication)); assertThat(throwableRef.get(), nullValue()); } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenAuthIntegTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenAuthIntegTests.java index aa9510fd9a994..c9a00d39dc075 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenAuthIntegTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenAuthIntegTests.java @@ -321,7 +321,7 @@ public void testCreateThenRefreshAsDifferentUser() { request.username(SecuritySettingsSource.TEST_SUPERUSER); client.execute(AuthenticateAction.INSTANCE, request, authFuture); AuthenticateResponse response = authFuture.actionGet(); - assertEquals(SecuritySettingsSource.TEST_SUPERUSER, response.user().principal()); + assertEquals(SecuritySettingsSource.TEST_SUPERUSER, response.authentication().getUser().principal()); authFuture = new PlainActionFuture<>(); request = new AuthenticateRequest(); @@ -329,7 +329,7 @@ public void testCreateThenRefreshAsDifferentUser() { client.filterWithHeader(Collections.singletonMap("Authorization", "Bearer " + createTokenResponse.getTokenString())) .execute(AuthenticateAction.INSTANCE, request, authFuture); response = authFuture.actionGet(); - assertEquals(SecuritySettingsSource.TEST_USER_NAME, response.user().principal()); + assertEquals(SecuritySettingsSource.TEST_USER_NAME, response.authentication().getUser().principal()); authFuture = new PlainActionFuture<>(); request = new AuthenticateRequest(); @@ -337,7 +337,7 @@ public void testCreateThenRefreshAsDifferentUser() { client.filterWithHeader(Collections.singletonMap("Authorization", "Bearer " + refreshResponse.getTokenString())) .execute(AuthenticateAction.INSTANCE, request, authFuture); response = authFuture.actionGet(); - assertEquals(SecuritySettingsSource.TEST_USER_NAME, response.user().principal()); + assertEquals(SecuritySettingsSource.TEST_USER_NAME, response.authentication().getUser().principal()); } public void testClientCredentialsGrant() throws Exception { @@ -356,7 +356,7 @@ public void testClientCredentialsGrant() throws Exception { client.filterWithHeader(Collections.singletonMap("Authorization", "Bearer " + createTokenResponse.getTokenString())) .execute(AuthenticateAction.INSTANCE, request, authFuture); AuthenticateResponse response = authFuture.get(); - assertEquals(SecuritySettingsSource.TEST_SUPERUSER, response.user().principal()); + assertEquals(SecuritySettingsSource.TEST_SUPERUSER, response.authentication().getUser().principal()); // invalidate PlainActionFuture invalidateResponseFuture = new PlainActionFuture<>(); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/NativeRealmIntegTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/NativeRealmIntegTests.java index a0311d228319d..46f1874fbee95 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/NativeRealmIntegTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/NativeRealmIntegTests.java @@ -64,6 +64,7 @@ import static org.elasticsearch.xpack.security.support.SecurityIndexManager.INTERNAL_SECURITY_INDEX; import static org.hamcrest.Matchers.arrayContaining; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; import static org.mockito.Mockito.mock; @@ -571,7 +572,10 @@ public void testOperationsOnReservedUsers() throws Exception { basicAuthHeaderValue(username, getReservedPassword()))) .execute(AuthenticateAction.INSTANCE, new AuthenticateRequest(username)) .get(); - assertThat(authenticateResponse.user().principal(), is(username)); + assertThat(authenticateResponse.authentication().getUser().principal(), is(username)); + assertThat(authenticateResponse.authentication().getAuthenticatedBy().getName(), equalTo("reserved")); + assertThat(authenticateResponse.authentication().getAuthenticatedBy().getType(), equalTo("reserved")); + assertNull(authenticateResponse.authentication().getLookedUpBy()); } public void testOperationsOnReservedRoles() throws Exception { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/RestAuthenticateActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/RestAuthenticateActionTests.java index c3b91089d243e..767d9f6d5b36a 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/RestAuthenticateActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/RestAuthenticateActionTests.java @@ -56,8 +56,13 @@ public void testAuthenticateApi() throws Exception { options.addHeader("Authorization", basicAuthHeaderValue(SecuritySettingsSource.TEST_USER_NAME, new SecureString(SecuritySettingsSourceField.TEST_PASSWORD.toCharArray()))); request.setOptions(options); - ObjectPath objectPath = ObjectPath.createFromResponse(getRestClient().performRequest(request)); + Response a = getRestClient().performRequest(request); + ObjectPath objectPath = ObjectPath.createFromResponse(a); assertThat(objectPath.evaluate("username").toString(), equalTo(SecuritySettingsSource.TEST_USER_NAME)); + assertThat(objectPath.evaluate("authentication_realm.name").toString(), equalTo("file")); + assertThat(objectPath.evaluate("authentication_realm.type").toString(), equalTo("file")); + assertThat(objectPath.evaluate("lookup_realm.name").toString(), equalTo("file")); + assertThat(objectPath.evaluate("lookup_realm.type").toString(), equalTo("file")); @SuppressWarnings("unchecked") List roles = objectPath.evaluate("roles"); assertThat(roles.size(), is(1)); diff --git a/x-pack/qa/third-party/active-directory/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectoryRunAsIT.java b/x-pack/qa/third-party/active-directory/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectoryRunAsIT.java index 3cd4711750f85..68b8ee4bb5765 100644 --- a/x-pack/qa/third-party/active-directory/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectoryRunAsIT.java +++ b/x-pack/qa/third-party/active-directory/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectoryRunAsIT.java @@ -66,7 +66,7 @@ public void testRunAs() throws Exception { final AuthenticateRequest request = new AuthenticateRequest(avenger); final ActionFuture future = runAsClient(avenger).execute(AuthenticateAction.INSTANCE, request); final AuthenticateResponse response = future.get(30, TimeUnit.SECONDS); - assertThat(response.user().principal(), Matchers.equalTo(avenger)); + assertThat(response.authentication().getUser().principal(), Matchers.equalTo(avenger)); } protected Client runAsClient(String user) { From 7b2811b5015d336f92cd16df3f82e5275ec370c4 Mon Sep 17 00:00:00 2001 From: Alpar Torok Date: Thu, 29 Nov 2018 15:19:29 +0200 Subject: [PATCH 021/115] Remove files from incorrect backport There's no transport-nio in 6.x no need for these files from #35877 --- .../transport-nio/licenses/netty-buffer-4.1.31.Final.jar.sha1 | 1 - plugins/transport-nio/licenses/netty-codec-4.1.31.Final.jar.sha1 | 1 - .../licenses/netty-codec-http-4.1.31.Final.jar.sha1 | 1 - .../transport-nio/licenses/netty-common-4.1.31.Final.jar.sha1 | 1 - .../transport-nio/licenses/netty-handler-4.1.31.Final.jar.sha1 | 1 - .../transport-nio/licenses/netty-resolver-4.1.31.Final.jar.sha1 | 1 - .../transport-nio/licenses/netty-transport-4.1.31.Final.jar.sha1 | 1 - 7 files changed, 7 deletions(-) delete mode 100644 plugins/transport-nio/licenses/netty-buffer-4.1.31.Final.jar.sha1 delete mode 100644 plugins/transport-nio/licenses/netty-codec-4.1.31.Final.jar.sha1 delete mode 100644 plugins/transport-nio/licenses/netty-codec-http-4.1.31.Final.jar.sha1 delete mode 100644 plugins/transport-nio/licenses/netty-common-4.1.31.Final.jar.sha1 delete mode 100644 plugins/transport-nio/licenses/netty-handler-4.1.31.Final.jar.sha1 delete mode 100644 plugins/transport-nio/licenses/netty-resolver-4.1.31.Final.jar.sha1 delete mode 100644 plugins/transport-nio/licenses/netty-transport-4.1.31.Final.jar.sha1 diff --git a/plugins/transport-nio/licenses/netty-buffer-4.1.31.Final.jar.sha1 b/plugins/transport-nio/licenses/netty-buffer-4.1.31.Final.jar.sha1 deleted file mode 100644 index 22b58c5241485..0000000000000 --- a/plugins/transport-nio/licenses/netty-buffer-4.1.31.Final.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -e086523d6bb01fcab1d8dd370eecfcd606311b92 \ No newline at end of file diff --git a/plugins/transport-nio/licenses/netty-codec-4.1.31.Final.jar.sha1 b/plugins/transport-nio/licenses/netty-codec-4.1.31.Final.jar.sha1 deleted file mode 100644 index 83e6eab1261c2..0000000000000 --- a/plugins/transport-nio/licenses/netty-codec-4.1.31.Final.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -cfa60b136f5ea57787e910eee37e240bb45402a7 \ No newline at end of file diff --git a/plugins/transport-nio/licenses/netty-codec-http-4.1.31.Final.jar.sha1 b/plugins/transport-nio/licenses/netty-codec-http-4.1.31.Final.jar.sha1 deleted file mode 100644 index b6d43d380ac17..0000000000000 --- a/plugins/transport-nio/licenses/netty-codec-http-4.1.31.Final.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -bf6321b3f10ea3aefc1970b30bb8928e833f236c \ No newline at end of file diff --git a/plugins/transport-nio/licenses/netty-common-4.1.31.Final.jar.sha1 b/plugins/transport-nio/licenses/netty-common-4.1.31.Final.jar.sha1 deleted file mode 100644 index 1c0c67721e22d..0000000000000 --- a/plugins/transport-nio/licenses/netty-common-4.1.31.Final.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -39ddfa47808c8393a343513571e404fef02f45f0 \ No newline at end of file diff --git a/plugins/transport-nio/licenses/netty-handler-4.1.31.Final.jar.sha1 b/plugins/transport-nio/licenses/netty-handler-4.1.31.Final.jar.sha1 deleted file mode 100644 index c344af3b70a7e..0000000000000 --- a/plugins/transport-nio/licenses/netty-handler-4.1.31.Final.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -7703c0696f2f34ec7c223c6a5750366a5f4dfb6f \ No newline at end of file diff --git a/plugins/transport-nio/licenses/netty-resolver-4.1.31.Final.jar.sha1 b/plugins/transport-nio/licenses/netty-resolver-4.1.31.Final.jar.sha1 deleted file mode 100644 index f6d72804412c2..0000000000000 --- a/plugins/transport-nio/licenses/netty-resolver-4.1.31.Final.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -8ea7a47400beedd5bb901b96a0730eea8b7b6f2a \ No newline at end of file diff --git a/plugins/transport-nio/licenses/netty-transport-4.1.31.Final.jar.sha1 b/plugins/transport-nio/licenses/netty-transport-4.1.31.Final.jar.sha1 deleted file mode 100644 index e44515c5e8f65..0000000000000 --- a/plugins/transport-nio/licenses/netty-transport-4.1.31.Final.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -e3396bd65e9c76accac11c29dca035da1cc39cb1 \ No newline at end of file From dc50d8bf295aa2f12cc4d03695bb9981f205cd69 Mon Sep 17 00:00:00 2001 From: Alpar Torok Date: Thu, 29 Nov 2018 15:54:34 +0200 Subject: [PATCH 022/115] Don't disable tasks based on the plugin (#36045) Some times the test fixtures plugin did not correctly disable tasks from the build plugin as it should. The plugin manager and tasks both use domain name collections so the previus conde should have worked. I did not have trime to track it down, but suspect there's some race condition in Gradle causing this. The plugin manager is still incubating. Since the tasks are on the cp even if the plugin is not applyed, we don't really need to involve the plugin at all. Closes #36041 --- .../testfixtures/TestFixturesPlugin.java | 54 +++++++++++-------- x-pack/test/smb-fixture/build.gradle | 3 -- 2 files changed, 31 insertions(+), 26 deletions(-) diff --git a/buildSrc/src/main/java/org/elasticsearch/gradle/testfixtures/TestFixturesPlugin.java b/buildSrc/src/main/java/org/elasticsearch/gradle/testfixtures/TestFixturesPlugin.java index 2b1c150ee9f43..ed185e22a6cbb 100644 --- a/buildSrc/src/main/java/org/elasticsearch/gradle/testfixtures/TestFixturesPlugin.java +++ b/buildSrc/src/main/java/org/elasticsearch/gradle/testfixtures/TestFixturesPlugin.java @@ -27,6 +27,7 @@ import org.gradle.api.Project; import org.gradle.api.Task; import org.gradle.api.plugins.BasePlugin; +import org.gradle.api.tasks.Input; import org.gradle.api.tasks.TaskContainer; import java.lang.reflect.InvocationTargetException; @@ -45,21 +46,18 @@ public void apply(Project project) { "testFixtures", TestFixtureExtension.class, project ); - // Don't look for docker-compose on the PATH yet that would pick up on Windows as well - if (project.file("/usr/local/bin/docker-compose").exists() == false && - project.file("/usr/bin/docker-compose").exists() == false - ) { - project.getLogger().warn( - "Tests require docker-compose at /usr/local/bin/docker-compose or /usr/bin/docker-compose " + - "but none could not be found so these will be skipped" - ); - tasks.withType(getTaskClass("com.carrotsearch.gradle.junit4.RandomizedTestingTask"), task -> - task.setEnabled(false) - ); - return; - } - if (project.file(DOCKER_COMPOSE_YML).exists()) { + // convenience boilerplate with build plugin + // Can't reference tasks that are implemented in Groovy, use reflection instead + disableTaskByType(tasks, getTaskClass("org.elasticsearch.gradle.precommit.LicenseHeadersTask")); + disableTaskByType(tasks, getTaskClass("com.carrotsearch.gradle.junit4.RandomizedTestingTask")); + disableTaskByType(tasks, ThirdPartyAuditTask.class); + disableTaskByType(tasks, JarHellTask.class); + + if (dockerComposeSupported(project) == false) { + return; + } + project.apply(spec -> spec.plugin(BasePlugin.class)); project.apply(spec -> spec.plugin(DockerComposePlugin.class)); ComposeExtension composeExtension = project.getExtensions().getByType(ComposeExtension.class); @@ -71,16 +69,17 @@ public void apply(Project project) { ); project.getTasks().getByName("clean").dependsOn("composeDown"); - - // convenience boilerplate with build plugin - project.getPluginManager().withPlugin("elasticsearch.build", (appliedPlugin) -> { - // Can't reference tasks that are implemented in Groovy, use reflection instead - disableTaskByType(tasks, getTaskClass("org.elasticsearch.gradle.precommit.LicenseHeadersTask")); - disableTaskByType(tasks, getTaskClass("com.carrotsearch.gradle.junit4.RandomizedTestingTask")); - disableTaskByType(tasks, ThirdPartyAuditTask.class); - disableTaskByType(tasks, JarHellTask.class); - }); } else { + if (dockerComposeSupported(project) == false) { + project.getLogger().warn( + "Tests for {} require docker-compose at /usr/local/bin/docker-compose or /usr/bin/docker-compose " + + "but none could not be found so these will be skipped", project.getPath() + ); + tasks.withType(getTaskClass("com.carrotsearch.gradle.junit4.RandomizedTestingTask"), task -> + task.setEnabled(false) + ); + return; + } tasks.withType(getTaskClass("com.carrotsearch.gradle.junit4.RandomizedTestingTask"), task -> extension.fixtures.all(fixtureProject -> { task.dependsOn(fixtureProject.getTasks().getByName("composeUp")); @@ -101,6 +100,15 @@ public void apply(Project project) { } } + @Input + public boolean dockerComposeSupported(Project project) { + // Don't look for docker-compose on the PATH yet that would pick up on Windows as well + return + project.file("/usr/local/bin/docker-compose").exists() == false && + project.file("/usr/bin/docker-compose").exists() == false && + Boolean.parseBoolean(System.getProperty("tests.fixture.enabled", "true")) == false; + } + private void setSystemProperty(Task task, String name, Object value) { try { Method systemProperty = task.getClass().getMethod("systemProperty", String.class, Object.class); diff --git a/x-pack/test/smb-fixture/build.gradle b/x-pack/test/smb-fixture/build.gradle index b543dad95cd3b..c361737e22c6d 100644 --- a/x-pack/test/smb-fixture/build.gradle +++ b/x-pack/test/smb-fixture/build.gradle @@ -1,5 +1,2 @@ apply plugin: 'elasticsearch.build' apply plugin: 'elasticsearch.test.fixtures' - -// TODO: elasticsearch.test.fixtures should disable this but it sometimes doesn't -jarHell.enabled = false From e592bd1ddcd2fcd5b5a66e541fbbc6d1f2aca22f Mon Sep 17 00:00:00 2001 From: Alan Woodward Date: Thu, 29 Nov 2018 10:35:38 +0000 Subject: [PATCH 023/115] Ensure TokenFilters only produce single tokens when parsing synonyms (#34331) A number of tokenfilters can produce multiple tokens at the same position. This is a problem when using token chains to parse synonym files, as the SynonymMap requires that there are no stacked tokens in its input. This commit ensures that when used to parse synonyms, these tokenfilters either produce a single version of their input token, or that they throw an error when mappings are generated. In indexes created in elasticsearch 6.x deprecation warnings are emitted in place of the error. * asciifolding and cjk_bigram produce only the folded or bigrammed token * decompounders, synonyms and keyword_repeat are skipped * n-grams, word-delimiter-filter, multiplexer, fingerprint and phonetic throw errors Fixes #34298 --- .../synonym-graph-tokenfilter.asciidoc | 12 ++ .../tokenfilters/synonym-tokenfilter.asciidoc | 13 ++ .../ASCIIFoldingTokenFilterFactory.java | 10 +- ...bstractCompoundWordTokenFilterFactory.java | 6 + .../common/CJKBigramFilterFactory.java | 14 ++ .../analysis/common/CommonAnalysisPlugin.java | 2 +- .../common/CommonGramsTokenFilterFactory.java | 13 ++ .../common/EdgeNGramTokenFilterFactory.java | 13 ++ .../common/FingerprintTokenFilterFactory.java | 13 ++ .../common/MultiplexerTokenFilterFactory.java | 24 ++- .../common/NGramTokenFilterFactory.java | 15 +- .../WordDelimiterGraphTokenFilterFactory.java | 13 ++ .../WordDelimiterTokenFilterFactory.java | 13 ++ .../common/SynonymsAnalysisTests.java | 156 +++++++++++++++++- .../analysis/PhoneticTokenFilterFactory.java | 13 ++ .../AnalysisPhoneticFactoryTests.java | 23 +++ .../analysis/PreConfiguredTokenFilter.java | 37 ++++- .../analysis/ShingleTokenFilterFactory.java | 15 +- .../analysis/SynonymTokenFilterFactory.java | 15 +- .../index/analysis/TokenFilterFactory.java | 3 +- 20 files changed, 407 insertions(+), 16 deletions(-) diff --git a/docs/reference/analysis/tokenfilters/synonym-graph-tokenfilter.asciidoc b/docs/reference/analysis/tokenfilters/synonym-graph-tokenfilter.asciidoc index 8be5647e10f27..2a555d7d044da 100644 --- a/docs/reference/analysis/tokenfilters/synonym-graph-tokenfilter.asciidoc +++ b/docs/reference/analysis/tokenfilters/synonym-graph-tokenfilter.asciidoc @@ -175,3 +175,15 @@ PUT /test_index Using `synonyms_path` to define WordNet synonyms in a file is supported as well. + +=== Parsing synonym files + +Elasticsearch will use the token filters preceding the synonym filter +in a tokenizer chain to parse the entries in a synonym file. So, for example, if a +synonym filter is placed after a stemmer, then the stemmer will also be applied +to the synonym entries. Because entries in the synonym map cannot have stacked +positions, some token filters may cause issues here. Token filters that produce +multiple versions of a token may choose which version of the token to emit when +parsing synonyms, e.g. `asciifolding` will only produce the folded version of the +token. Others, e.g. `multiplexer`, `word_delimiter_graph` or `ngram` will throw an +error. diff --git a/docs/reference/analysis/tokenfilters/synonym-tokenfilter.asciidoc b/docs/reference/analysis/tokenfilters/synonym-tokenfilter.asciidoc index 2d8fa93147ae7..d0659f3425d3e 100644 --- a/docs/reference/analysis/tokenfilters/synonym-tokenfilter.asciidoc +++ b/docs/reference/analysis/tokenfilters/synonym-tokenfilter.asciidoc @@ -163,3 +163,16 @@ PUT /test_index Using `synonyms_path` to define WordNet synonyms in a file is supported as well. + + +=== Parsing synonym files + +Elasticsearch will use the token filters preceding the synonym filter +in a tokenizer chain to parse the entries in a synonym file. So, for example, if a +synonym filter is placed after a stemmer, then the stemmer will also be applied +to the synonym entries. Because entries in the synonym map cannot have stacked +positions, some token filters may cause issues here. Token filters that produce +multiple versions of a token may choose which version of the token to emit when +parsing synonyms, e.g. `asciifolding` will only produce the folded version of the +token. Others, e.g. `multiplexer`, `word_delimiter_graph` or `ngram` will throw an +error. \ No newline at end of file diff --git a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/ASCIIFoldingTokenFilterFactory.java b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/ASCIIFoldingTokenFilterFactory.java index f8e0c7383a09b..068cc761538d8 100644 --- a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/ASCIIFoldingTokenFilterFactory.java +++ b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/ASCIIFoldingTokenFilterFactory.java @@ -33,7 +33,8 @@ * Factory for ASCIIFoldingFilter. */ public class ASCIIFoldingTokenFilterFactory extends AbstractTokenFilterFactory - implements MultiTermAwareComponent { + implements MultiTermAwareComponent { + public static final ParseField PRESERVE_ORIGINAL = new ParseField("preserve_original"); public static final boolean DEFAULT_PRESERVE_ORIGINAL = false; @@ -53,7 +54,7 @@ public TokenStream create(TokenStream tokenStream) { } @Override - public Object getMultiTermComponent() { + public TokenFilterFactory getSynonymFilter() { if (preserveOriginal == false) { return this; } else { @@ -70,4 +71,9 @@ public TokenStream create(TokenStream tokenStream) { }; } } + + @Override + public Object getMultiTermComponent() { + return getSynonymFilter(); + } } diff --git a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/AbstractCompoundWordTokenFilterFactory.java b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/AbstractCompoundWordTokenFilterFactory.java index b59cc166f09a5..f8b1791e61f98 100644 --- a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/AbstractCompoundWordTokenFilterFactory.java +++ b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/AbstractCompoundWordTokenFilterFactory.java @@ -26,6 +26,7 @@ import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.analysis.AbstractTokenFilterFactory; import org.elasticsearch.index.analysis.Analysis; +import org.elasticsearch.index.analysis.TokenFilterFactory; /** * Contains the common configuration settings between subclasses of this class. @@ -51,4 +52,9 @@ protected AbstractCompoundWordTokenFilterFactory(IndexSettings indexSettings, En throw new IllegalArgumentException("word_list must be provided for [" + name + "], either as a path to a file, or directly"); } } + + @Override + public TokenFilterFactory getSynonymFilter() { + return IDENTITY_FILTER; // don't decompound synonym file + } } diff --git a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/CJKBigramFilterFactory.java b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/CJKBigramFilterFactory.java index 01ca2125faebd..0101dd2a6b8e6 100644 --- a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/CJKBigramFilterFactory.java +++ b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/CJKBigramFilterFactory.java @@ -19,13 +19,16 @@ package org.elasticsearch.analysis.common; +import org.apache.logging.log4j.LogManager; import org.apache.lucene.analysis.TokenStream; import org.apache.lucene.analysis.cjk.CJKBigramFilter; import org.apache.lucene.analysis.miscellaneous.DisableGraphAttribute; +import org.elasticsearch.common.logging.DeprecationLogger; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.env.Environment; import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.analysis.AbstractTokenFilterFactory; +import org.elasticsearch.index.analysis.TokenFilterFactory; import java.util.Arrays; import java.util.HashSet; @@ -48,6 +51,9 @@ */ public final class CJKBigramFilterFactory extends AbstractTokenFilterFactory { + private static final DeprecationLogger DEPRECATION_LOGGER + = new DeprecationLogger(LogManager.getLogger(CJKBigramFilterFactory.class)); + private final int flags; private final boolean outputUnigrams; @@ -90,4 +96,12 @@ public TokenStream create(TokenStream tokenStream) { return filter; } + @Override + public TokenFilterFactory getSynonymFilter() { + if (outputUnigrams) { + DEPRECATION_LOGGER.deprecatedAndMaybeLog("synonym_tokenfilters", "Token filter [" + name() + + "] will not be usable to parse synonyms after v7.0"); + } + return this; + } } diff --git a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/CommonAnalysisPlugin.java b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/CommonAnalysisPlugin.java index 27f36e05afaaf..af74d9998c189 100644 --- a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/CommonAnalysisPlugin.java +++ b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/CommonAnalysisPlugin.java @@ -425,7 +425,7 @@ public List getPreConfiguredTokenFilters() { filters.add(PreConfiguredTokenFilter.singleton("german_stem", false, GermanStemFilter::new)); filters.add(PreConfiguredTokenFilter.singleton("hindi_normalization", true, HindiNormalizationFilter::new)); filters.add(PreConfiguredTokenFilter.singleton("indic_normalization", true, IndicNormalizationFilter::new)); - filters.add(PreConfiguredTokenFilter.singleton("keyword_repeat", false, KeywordRepeatFilter::new)); + filters.add(PreConfiguredTokenFilter.singleton("keyword_repeat", false, false, KeywordRepeatFilter::new)); filters.add(PreConfiguredTokenFilter.singleton("kstem", false, KStemFilter::new)); filters.add(PreConfiguredTokenFilter.singleton("length", false, input -> new LengthFilter(input, 0, Integer.MAX_VALUE))); // TODO this one seems useless diff --git a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/CommonGramsTokenFilterFactory.java b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/CommonGramsTokenFilterFactory.java index a6e9baeab8d81..9818c08443259 100644 --- a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/CommonGramsTokenFilterFactory.java +++ b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/CommonGramsTokenFilterFactory.java @@ -19,18 +19,24 @@ package org.elasticsearch.analysis.common; +import org.apache.logging.log4j.LogManager; import org.apache.lucene.analysis.CharArraySet; import org.apache.lucene.analysis.TokenStream; import org.apache.lucene.analysis.commongrams.CommonGramsFilter; import org.apache.lucene.analysis.commongrams.CommonGramsQueryFilter; +import org.elasticsearch.common.logging.DeprecationLogger; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.env.Environment; import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.analysis.AbstractTokenFilterFactory; import org.elasticsearch.index.analysis.Analysis; +import org.elasticsearch.index.analysis.TokenFilterFactory; public class CommonGramsTokenFilterFactory extends AbstractTokenFilterFactory { + private static final DeprecationLogger DEPRECATION_LOGGER + = new DeprecationLogger(LogManager.getLogger(CommonGramsTokenFilterFactory.class)); + private final CharArraySet words; private final boolean ignoreCase; @@ -60,5 +66,12 @@ public TokenStream create(TokenStream tokenStream) { return filter; } } + + @Override + public TokenFilterFactory getSynonymFilter() { + DEPRECATION_LOGGER.deprecatedAndMaybeLog("synonym_tokenfilters", "Token filter [" + name() + + "] will not be usable to parse synonyms after v7.0"); + return this; + } } diff --git a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/EdgeNGramTokenFilterFactory.java b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/EdgeNGramTokenFilterFactory.java index af6d30a035476..9e8ba329d4beb 100644 --- a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/EdgeNGramTokenFilterFactory.java +++ b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/EdgeNGramTokenFilterFactory.java @@ -19,18 +19,24 @@ package org.elasticsearch.analysis.common; +import org.apache.logging.log4j.LogManager; import org.apache.lucene.analysis.TokenStream; import org.apache.lucene.analysis.ngram.EdgeNGramTokenFilter; import org.apache.lucene.analysis.ngram.NGramTokenFilter; import org.apache.lucene.analysis.reverse.ReverseStringFilter; +import org.elasticsearch.common.logging.DeprecationLogger; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.env.Environment; import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.analysis.AbstractTokenFilterFactory; +import org.elasticsearch.index.analysis.TokenFilterFactory; public class EdgeNGramTokenFilterFactory extends AbstractTokenFilterFactory { + private static final DeprecationLogger DEPRECATION_LOGGER + = new DeprecationLogger(LogManager.getLogger(EdgeNGramTokenFilterFactory.class)); + private final int minGram; private final int maxGram; @@ -77,4 +83,11 @@ public TokenStream create(TokenStream tokenStream) { public boolean breaksFastVectorHighlighter() { return true; } + + @Override + public TokenFilterFactory getSynonymFilter() { + DEPRECATION_LOGGER.deprecatedAndMaybeLog("synonym_tokenfilters", "Token filter [" + name() + + "] will not be usable to parse synonyms after v7.0"); + return this; + } } diff --git a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/FingerprintTokenFilterFactory.java b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/FingerprintTokenFilterFactory.java index f41fb1207c636..2567f99702037 100644 --- a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/FingerprintTokenFilterFactory.java +++ b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/FingerprintTokenFilterFactory.java @@ -19,18 +19,24 @@ package org.elasticsearch.analysis.common; +import org.apache.logging.log4j.LogManager; import org.apache.lucene.analysis.TokenStream; import org.apache.lucene.analysis.miscellaneous.FingerprintFilter; +import org.elasticsearch.common.logging.DeprecationLogger; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.env.Environment; import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.analysis.AbstractTokenFilterFactory; +import org.elasticsearch.index.analysis.TokenFilterFactory; import static org.elasticsearch.analysis.common.FingerprintAnalyzerProvider.DEFAULT_MAX_OUTPUT_SIZE; import static org.elasticsearch.analysis.common.FingerprintAnalyzerProvider.MAX_OUTPUT_SIZE; public class FingerprintTokenFilterFactory extends AbstractTokenFilterFactory { + private static final DeprecationLogger DEPRECATION_LOGGER + = new DeprecationLogger(LogManager.getLogger(FingerprintTokenFilterFactory.class)); + private final char separator; private final int maxOutputSize; @@ -47,4 +53,11 @@ public TokenStream create(TokenStream tokenStream) { return result; } + @Override + public TokenFilterFactory getSynonymFilter() { + DEPRECATION_LOGGER.deprecatedAndMaybeLog("synonym_tokenfilters", "Token filter [" + name() + + "] will not be usable to parse synonyms after v7.0"); + return this; + } + } diff --git a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/MultiplexerTokenFilterFactory.java b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/MultiplexerTokenFilterFactory.java index c3e0d5133c368..f25db9daded08 100644 --- a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/MultiplexerTokenFilterFactory.java +++ b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/MultiplexerTokenFilterFactory.java @@ -19,12 +19,14 @@ package org.elasticsearch.analysis.common; +import org.apache.logging.log4j.LogManager; import org.apache.lucene.analysis.TokenFilter; import org.apache.lucene.analysis.TokenStream; import org.apache.lucene.analysis.miscellaneous.ConditionalTokenFilter; import org.apache.lucene.analysis.miscellaneous.RemoveDuplicatesTokenFilter; import org.apache.lucene.analysis.tokenattributes.PositionIncrementAttribute; import org.elasticsearch.common.Strings; +import org.elasticsearch.common.logging.DeprecationLogger; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.env.Environment; import org.elasticsearch.index.IndexSettings; @@ -40,6 +42,9 @@ public class MultiplexerTokenFilterFactory extends AbstractTokenFilterFactory { + private static final DeprecationLogger DEPRECATION_LOGGER + = new DeprecationLogger(LogManager.getLogger(MultiplexerTokenFilterFactory.class)); + private List filterNames; private final boolean preserveOriginal; @@ -54,6 +59,17 @@ public TokenStream create(TokenStream tokenStream) { throw new UnsupportedOperationException("TokenFilterFactory.getChainAwareTokenFilterFactory() must be called first"); } + @Override + public TokenFilterFactory getSynonymFilter() { + if (preserveOriginal) { + DEPRECATION_LOGGER.deprecatedAndMaybeLog("synonym_tokenfilters", "Token filter [" + name() + + "] will not be usable to parse synonyms after v7.0"); + return IDENTITY_FILTER; + } + throw new IllegalArgumentException("Token filter [" + name() + + "] cannot be used to parse synonyms unless [preserve_original] is [true]"); + } + @Override public TokenFilterFactory getChainAwareTokenFilterFactory(TokenizerFactory tokenizer, List charFilters, List previousTokenFilters, @@ -98,7 +114,13 @@ public TokenStream create(TokenStream tokenStream) { @Override public TokenFilterFactory getSynonymFilter() { - return IDENTITY_FILTER; + if (preserveOriginal) { + DEPRECATION_LOGGER.deprecatedAndMaybeLog("synonym_tokenfilters", "Token filter [" + name() + + "] will not be usable to parse synonyms after v7.0"); + return IDENTITY_FILTER; + } + throw new IllegalArgumentException("Token filter [" + name() + + "] cannot be used to parse synonyms unless [preserve_original] is [true]"); } }; } diff --git a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/NGramTokenFilterFactory.java b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/NGramTokenFilterFactory.java index ae2dd687193d8..16f06826237d3 100644 --- a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/NGramTokenFilterFactory.java +++ b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/NGramTokenFilterFactory.java @@ -19,22 +19,26 @@ package org.elasticsearch.analysis.common; +import org.apache.logging.log4j.LogManager; import org.apache.lucene.analysis.TokenStream; import org.apache.lucene.analysis.ngram.NGramTokenFilter; +import org.elasticsearch.common.logging.DeprecationLogger; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.env.Environment; import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.analysis.AbstractTokenFilterFactory; - +import org.elasticsearch.index.analysis.TokenFilterFactory; public class NGramTokenFilterFactory extends AbstractTokenFilterFactory { + private static final DeprecationLogger DEPRECATION_LOGGER + = new DeprecationLogger(LogManager.getLogger(NGramTokenFilterFactory.class)); + private final int minGram; private final int maxGram; - NGramTokenFilterFactory(IndexSettings indexSettings, Environment environment, String name, Settings settings) { super(indexSettings, name, settings); int maxAllowedNgramDiff = indexSettings.getMaxNgramDiff(); @@ -51,4 +55,11 @@ public class NGramTokenFilterFactory extends AbstractTokenFilterFactory { public TokenStream create(TokenStream tokenStream) { return new NGramTokenFilter(tokenStream, minGram, maxGram); } + + @Override + public TokenFilterFactory getSynonymFilter() { + DEPRECATION_LOGGER.deprecatedAndMaybeLog("synonym_tokenfilters", "Token filter [" + name() + + "] will not be usable to parse synonyms after v7.0"); + return this; + } } diff --git a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/WordDelimiterGraphTokenFilterFactory.java b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/WordDelimiterGraphTokenFilterFactory.java index 1613339853117..824935a352136 100644 --- a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/WordDelimiterGraphTokenFilterFactory.java +++ b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/WordDelimiterGraphTokenFilterFactory.java @@ -19,15 +19,18 @@ package org.elasticsearch.analysis.common; +import org.apache.logging.log4j.LogManager; import org.apache.lucene.analysis.CharArraySet; import org.apache.lucene.analysis.TokenStream; import org.apache.lucene.analysis.miscellaneous.WordDelimiterGraphFilter; import org.apache.lucene.analysis.miscellaneous.WordDelimiterIterator; +import org.elasticsearch.common.logging.DeprecationLogger; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.env.Environment; import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.analysis.AbstractTokenFilterFactory; import org.elasticsearch.index.analysis.Analysis; +import org.elasticsearch.index.analysis.TokenFilterFactory; import java.util.List; import java.util.Set; @@ -45,6 +48,9 @@ public class WordDelimiterGraphTokenFilterFactory extends AbstractTokenFilterFactory { + private static final DeprecationLogger DEPRECATION_LOGGER = + new DeprecationLogger(LogManager.getLogger(WordDelimiterGraphTokenFilterFactory.class)); + private final byte[] charTypeTable; private final int flags; private final CharArraySet protoWords; @@ -96,6 +102,13 @@ public TokenStream create(TokenStream tokenStream) { return new WordDelimiterGraphFilter(tokenStream, charTypeTable, flags, protoWords); } + @Override + public TokenFilterFactory getSynonymFilter() { + DEPRECATION_LOGGER.deprecatedAndMaybeLog("synonym_tokenfilters", "Token filter [" + name() + + "] will not be usable to parse synonyms after v7.0"); + return this; + } + private int getFlag(int flag, Settings settings, String key, boolean defaultValue) { if (settings.getAsBoolean(key, defaultValue)) { return flag; diff --git a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/WordDelimiterTokenFilterFactory.java b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/WordDelimiterTokenFilterFactory.java index 8c38beb8f8b7b..6e492b371a901 100644 --- a/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/WordDelimiterTokenFilterFactory.java +++ b/modules/analysis-common/src/main/java/org/elasticsearch/analysis/common/WordDelimiterTokenFilterFactory.java @@ -19,15 +19,18 @@ package org.elasticsearch.analysis.common; +import org.apache.logging.log4j.LogManager; import org.apache.lucene.analysis.CharArraySet; import org.apache.lucene.analysis.TokenStream; import org.apache.lucene.analysis.miscellaneous.WordDelimiterFilter; import org.apache.lucene.analysis.miscellaneous.WordDelimiterIterator; +import org.elasticsearch.common.logging.DeprecationLogger; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.env.Environment; import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.analysis.AbstractTokenFilterFactory; import org.elasticsearch.index.analysis.Analysis; +import org.elasticsearch.index.analysis.TokenFilterFactory; import java.util.Collection; import java.util.List; @@ -50,6 +53,9 @@ public class WordDelimiterTokenFilterFactory extends AbstractTokenFilterFactory { + private static final DeprecationLogger DEPRECATION_LOGGER = + new DeprecationLogger(LogManager.getLogger(WordDelimiterTokenFilterFactory.class)); + private final byte[] charTypeTable; private final int flags; private final CharArraySet protoWords; @@ -104,6 +110,13 @@ public TokenStream create(TokenStream tokenStream) { protoWords); } + @Override + public TokenFilterFactory getSynonymFilter() { + DEPRECATION_LOGGER.deprecatedAndMaybeLog("synonym_tokenfilters", "Token filter [" + name() + + "] will not be usable to parse synonyms after v7.0"); + return this; + } + public int getFlag(int flag, Settings settings, String key, boolean defaultValue) { if (settings.getAsBooleanLenientForPreEs6Indices(indexSettings.getIndexVersionCreated(), key, defaultValue, deprecationLogger)) { diff --git a/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/SynonymsAnalysisTests.java b/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/SynonymsAnalysisTests.java index 9aec69eaca40d..8583088749386 100644 --- a/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/SynonymsAnalysisTests.java +++ b/modules/analysis-common/src/test/java/org/elasticsearch/analysis/common/SynonymsAnalysisTests.java @@ -22,6 +22,7 @@ import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.BaseTokenStreamTestCase; import org.apache.lucene.analysis.TokenStream; +import org.apache.lucene.analysis.core.KeywordTokenizer; import org.apache.lucene.analysis.tokenattributes.CharTermAttribute; import org.elasticsearch.Version; import org.elasticsearch.cluster.metadata.IndexMetaData; @@ -30,6 +31,9 @@ import org.elasticsearch.env.Environment; import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.analysis.IndexAnalyzers; +import org.elasticsearch.index.analysis.SynonymTokenFilterFactory; +import org.elasticsearch.index.analysis.TokenFilterFactory; +import org.elasticsearch.index.analysis.TokenizerFactory; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.IndexSettingsModule; import org.hamcrest.MatcherAssert; @@ -38,6 +42,9 @@ import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; @@ -119,7 +126,7 @@ public void testExpandSynonymWordDeleteByAnalyzer() throws IOException { } } - public void testSynonymsWithMultiplexer() throws IOException { + public void testSynonymsWrappedByMultiplexer() throws IOException { Settings settings = Settings.builder() .put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT) .put("path.home", createTempDir().toString()) @@ -140,6 +147,153 @@ public void testSynonymsWithMultiplexer() throws IOException { new int[]{ 1, 1, 0, 0, 1, 1 }); } + public void testAsciiFoldingFilterForSynonyms() throws IOException { + Settings settings = Settings.builder() + .put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT) + .put("path.home", createTempDir().toString()) + .put("index.analysis.filter.synonyms.type", "synonym") + .putList("index.analysis.filter.synonyms.synonyms", "hoj, height") + .put("index.analysis.analyzer.synonymAnalyzer.tokenizer", "standard") + .putList("index.analysis.analyzer.synonymAnalyzer.filter", "lowercase", "asciifolding", "synonyms") + .build(); + IndexSettings idxSettings = IndexSettingsModule.newIndexSettings("index", settings); + indexAnalyzers = createTestAnalysis(idxSettings, settings, new CommonAnalysisPlugin()).indexAnalyzers; + + BaseTokenStreamTestCase.assertAnalyzesTo(indexAnalyzers.get("synonymAnalyzer"), "høj", + new String[]{ "hoj", "height" }, + new int[]{ 1, 0 }); + } + + public void testKeywordRepeatAndSynonyms() throws IOException { + Settings settings = Settings.builder() + .put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT) + .put("path.home", createTempDir().toString()) + .put("index.analysis.filter.synonyms.type", "synonym") + .putList("index.analysis.filter.synonyms.synonyms", "programmer, developer") + .put("index.analysis.filter.my_english.type", "stemmer") + .put("index.analysis.filter.my_english.language", "porter2") + .put("index.analysis.analyzer.synonymAnalyzer.tokenizer", "standard") + .putList("index.analysis.analyzer.synonymAnalyzer.filter", "lowercase", "keyword_repeat", "my_english", "synonyms") + .build(); + IndexSettings idxSettings = IndexSettingsModule.newIndexSettings("index", settings); + indexAnalyzers = createTestAnalysis(idxSettings, settings, new CommonAnalysisPlugin()).indexAnalyzers; + + BaseTokenStreamTestCase.assertAnalyzesTo(indexAnalyzers.get("synonymAnalyzer"), "programmers", + new String[]{ "programmers", "programm", "develop" }, + new int[]{ 1, 0, 0 }); + } + + public void testChainedSynonymFilters() throws IOException { + Settings settings = Settings.builder() + .put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT) + .put("path.home", createTempDir().toString()) + .put("index.analysis.filter.synonyms1.type", "synonym") + .putList("index.analysis.filter.synonyms1.synonyms", "term1, term2") + .put("index.analysis.filter.synonyms2.type", "synonym") + .putList("index.analysis.filter.synonyms2.synonyms", "term1, term3") + .put("index.analysis.analyzer.syn.tokenizer", "standard") + .putList("index.analysis.analyzer.syn.filter", "lowercase", "synonyms1", "synonyms2") + .build(); + IndexSettings idxSettings = IndexSettingsModule.newIndexSettings("index", settings); + indexAnalyzers = createTestAnalysis(idxSettings, settings, new CommonAnalysisPlugin()).indexAnalyzers; + + BaseTokenStreamTestCase.assertAnalyzesTo(indexAnalyzers.get("syn"), "term1", + new String[]{ "term1", "term3", "term2" }, new int[]{ 1, 0, 0 }); + } + + public void testShingleFilters() throws IOException { + + Settings settings = Settings.builder() + .put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT) + .put("path.home", createTempDir().toString()) + .put("index.analysis.filter.synonyms.type", "synonym") + .putList("index.analysis.filter.synonyms.synonyms", "programmer, developer") + .put("index.analysis.filter.my_shingle.type", "shingle") + .put("index.analysis.analyzer.my_analyzer.tokenizer", "standard") + .putList("index.analysis.analyzer.my_analyzer.filter", "my_shingle", "synonyms") + .build(); + IndexSettings idxSettings = IndexSettingsModule.newIndexSettings("index", settings); + + indexAnalyzers = createTestAnalysis(idxSettings, settings, new CommonAnalysisPlugin()).indexAnalyzers; + + assertWarnings("Token filter [my_shingle] will not be usable to parse synonyms after v7.0"); + } + + public void testTokenFiltersBypassSynonymAnalysis() throws IOException { + + Settings settings = Settings.builder() + .put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT) + .put("path.home", createTempDir().toString()) + .putList("word_list", "a") + .put("hyphenation_patterns_path", "foo") + .build(); + IndexSettings idxSettings = IndexSettingsModule.newIndexSettings("index", settings); + + String[] bypassingFactories = new String[]{ + "dictionary_decompounder" + }; + + CommonAnalysisPlugin plugin = new CommonAnalysisPlugin(); + for (String factory : bypassingFactories) { + TokenFilterFactory tff = plugin.getTokenFilters().get(factory).get(idxSettings, null, factory, settings); + TokenizerFactory tok = new KeywordTokenizerFactory(idxSettings, null, "keyword", settings); + SynonymTokenFilterFactory stff = new SynonymTokenFilterFactory(idxSettings, null, null, "synonym", settings); + Analyzer analyzer = stff.buildSynonymAnalyzer(tok, Collections.emptyList(), Collections.singletonList(tff)); + + try (TokenStream ts = analyzer.tokenStream("field", "text")) { + assertThat(ts, instanceOf(KeywordTokenizer.class)); + } + } + + } + + public void testDisallowedTokenFilters() throws IOException { + + Settings settings = Settings.builder() + .put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT) + .put("path.home", createTempDir().toString()) + .putList("common_words", "a", "b") + .put("output_unigrams", "true") + .build(); + IndexSettings idxSettings = IndexSettingsModule.newIndexSettings("index", settings); + CommonAnalysisPlugin plugin = new CommonAnalysisPlugin(); + + String[] disallowedFactories = new String[]{ + "multiplexer", "cjk_bigram", "common_grams", "ngram", "edge_ngram", + "word_delimiter", "word_delimiter_graph", "fingerprint" + }; + + List expectedWarnings = new ArrayList<>(); + for (String factory : disallowedFactories) { + TokenFilterFactory tff = plugin.getTokenFilters().get(factory).get(idxSettings, null, factory, settings); + TokenizerFactory tok = new KeywordTokenizerFactory(idxSettings, null, "keyword", settings); + SynonymTokenFilterFactory stff = new SynonymTokenFilterFactory(idxSettings, null, null, "synonym", settings); + + stff.buildSynonymAnalyzer(tok, Collections.emptyList(), Collections.singletonList(tff)); + expectedWarnings.add("Token filter [" + factory + + "] will not be usable to parse synonyms after v7.0"); + } + + assertWarnings(expectedWarnings.toArray(new String[0])); + + settings = Settings.builder() + .put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT) + .put("path.home", createTempDir().toString()) + .put("preserve_original", "false") + .build(); + idxSettings = IndexSettingsModule.newIndexSettings("index", settings); + TokenFilterFactory tff = plugin.getTokenFilters().get("multiplexer").get(idxSettings, null, "multiplexer", settings); + TokenizerFactory tok = new KeywordTokenizerFactory(idxSettings, null, "keyword", settings); + SynonymTokenFilterFactory stff = new SynonymTokenFilterFactory(idxSettings, null, null, "synonym", settings); + + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, + () -> stff.buildSynonymAnalyzer(tok, Collections.emptyList(), Collections.singletonList(tff))); + + assertEquals("Token filter [multiplexer] cannot be used to parse synonyms unless [preserve_original] is [true]", + e.getMessage()); + + } + private void match(String analyzerName, String source, String target) throws IOException { Analyzer analyzer = indexAnalyzers.get(analyzerName).analyzer(); diff --git a/plugins/analysis-phonetic/src/main/java/org/elasticsearch/index/analysis/PhoneticTokenFilterFactory.java b/plugins/analysis-phonetic/src/main/java/org/elasticsearch/index/analysis/PhoneticTokenFilterFactory.java index 1a2f3551bb883..3a5d592561eee 100644 --- a/plugins/analysis-phonetic/src/main/java/org/elasticsearch/index/analysis/PhoneticTokenFilterFactory.java +++ b/plugins/analysis-phonetic/src/main/java/org/elasticsearch/index/analysis/PhoneticTokenFilterFactory.java @@ -30,11 +30,13 @@ import org.apache.commons.codec.language.bm.NameType; import org.apache.commons.codec.language.bm.PhoneticEngine; import org.apache.commons.codec.language.bm.RuleType; +import org.apache.logging.log4j.LogManager; import org.apache.lucene.analysis.TokenStream; import org.apache.lucene.analysis.phonetic.BeiderMorseFilter; import org.apache.lucene.analysis.phonetic.DaitchMokotoffSoundexFilter; import org.apache.lucene.analysis.phonetic.DoubleMetaphoneFilter; import org.apache.lucene.analysis.phonetic.PhoneticFilter; +import org.elasticsearch.common.logging.DeprecationLogger; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.env.Environment; import org.elasticsearch.index.IndexSettings; @@ -47,6 +49,10 @@ public class PhoneticTokenFilterFactory extends AbstractTokenFilterFactory { + + private static final DeprecationLogger DEPRECATION_LOGGER + = new DeprecationLogger(LogManager.getLogger(PhoneticTokenFilterFactory.class)); + private final Encoder encoder; private final boolean replace; private int maxcodelength; @@ -139,4 +145,11 @@ public TokenStream create(TokenStream tokenStream) { } throw new IllegalArgumentException("encoder error"); } + + @Override + public TokenFilterFactory getSynonymFilter() { + DEPRECATION_LOGGER.deprecatedAndMaybeLog("synonym_tokenfilters", "Token filter [" + name() + + "] will not be usable to parse synonyms after v7.0"); + return this; + } } diff --git a/plugins/analysis-phonetic/src/test/java/org/elasticsearch/index/analysis/AnalysisPhoneticFactoryTests.java b/plugins/analysis-phonetic/src/test/java/org/elasticsearch/index/analysis/AnalysisPhoneticFactoryTests.java index 8c551aee9190e..b9909704680a3 100644 --- a/plugins/analysis-phonetic/src/test/java/org/elasticsearch/index/analysis/AnalysisPhoneticFactoryTests.java +++ b/plugins/analysis-phonetic/src/test/java/org/elasticsearch/index/analysis/AnalysisPhoneticFactoryTests.java @@ -19,9 +19,15 @@ package org.elasticsearch.index.analysis; +import org.elasticsearch.Version; +import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.index.IndexSettings; import org.elasticsearch.indices.analysis.AnalysisFactoryTestCase; import org.elasticsearch.plugin.analysis.AnalysisPhoneticPlugin; +import org.elasticsearch.test.IndexSettingsModule; +import java.io.IOException; import java.util.HashMap; import java.util.Map; @@ -38,4 +44,21 @@ protected Map> getTokenFilters() { filters.put("phonetic", PhoneticTokenFilterFactory.class); return filters; } + + public void testDisallowedWithSynonyms() throws IOException { + + AnalysisPhoneticPlugin plugin = new AnalysisPhoneticPlugin(); + + Settings settings = Settings.builder() + .put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT) + .put("path.home", createTempDir().toString()) + .build(); + IndexSettings idxSettings = IndexSettingsModule.newIndexSettings("index", settings); + + TokenFilterFactory tff = plugin.getTokenFilters().get("phonetic").get(idxSettings, null, "phonetic", settings); + tff.getSynonymFilter(); + + assertWarnings("Token filter [phonetic] will not be usable to parse synonyms after v7.0"); + } + } diff --git a/server/src/main/java/org/elasticsearch/index/analysis/PreConfiguredTokenFilter.java b/server/src/main/java/org/elasticsearch/index/analysis/PreConfiguredTokenFilter.java index 12130e856f32a..2f4e2d2125db8 100644 --- a/server/src/main/java/org/elasticsearch/index/analysis/PreConfiguredTokenFilter.java +++ b/server/src/main/java/org/elasticsearch/index/analysis/PreConfiguredTokenFilter.java @@ -37,16 +37,26 @@ public final class PreConfiguredTokenFilter extends PreConfiguredAnalysisCompone */ public static PreConfiguredTokenFilter singleton(String name, boolean useFilterForMultitermQueries, Function create) { - return new PreConfiguredTokenFilter(name, useFilterForMultitermQueries, CachingStrategy.ONE, + return new PreConfiguredTokenFilter(name, useFilterForMultitermQueries, false, CachingStrategy.ONE, (tokenStream, version) -> create.apply(tokenStream)); } + /** + * Create a pre-configured token filter that may not vary at all. + */ + public static PreConfiguredTokenFilter singleton(String name, boolean useFilterForMultitermQueries, + boolean useFilterForParsingSynonyms, + Function create) { + return new PreConfiguredTokenFilter(name, useFilterForMultitermQueries, useFilterForParsingSynonyms, CachingStrategy.ONE, + (tokenStream, version) -> create.apply(tokenStream)); + } + /** * Create a pre-configured token filter that may not vary at all. */ public static PreConfiguredTokenFilter singletonWithVersion(String name, boolean useFilterForMultitermQueries, BiFunction create) { - return new PreConfiguredTokenFilter(name, useFilterForMultitermQueries, CachingStrategy.ONE, + return new PreConfiguredTokenFilter(name, useFilterForMultitermQueries, false, CachingStrategy.ONE, (tokenStream, version) -> create.apply(tokenStream, version)); } @@ -55,7 +65,7 @@ public static PreConfiguredTokenFilter singletonWithVersion(String name, boolean */ public static PreConfiguredTokenFilter luceneVersion(String name, boolean useFilterForMultitermQueries, BiFunction create) { - return new PreConfiguredTokenFilter(name, useFilterForMultitermQueries, CachingStrategy.LUCENE, + return new PreConfiguredTokenFilter(name, useFilterForMultitermQueries, false, CachingStrategy.LUCENE, (tokenStream, version) -> create.apply(tokenStream, version.luceneVersion)); } @@ -64,16 +74,18 @@ public static PreConfiguredTokenFilter luceneVersion(String name, boolean useFil */ public static PreConfiguredTokenFilter elasticsearchVersion(String name, boolean useFilterForMultitermQueries, BiFunction create) { - return new PreConfiguredTokenFilter(name, useFilterForMultitermQueries, CachingStrategy.ELASTICSEARCH, create); + return new PreConfiguredTokenFilter(name, useFilterForMultitermQueries, false, CachingStrategy.ELASTICSEARCH, create); } private final boolean useFilterForMultitermQueries; + private final boolean useFilterForParsingSynonyms; private final BiFunction create; - private PreConfiguredTokenFilter(String name, boolean useFilterForMultitermQueries, + private PreConfiguredTokenFilter(String name, boolean useFilterForMultitermQueries, boolean useFilterForParsingSynonyms, PreBuiltCacheFactory.CachingStrategy cache, BiFunction create) { super(name, cache); this.useFilterForMultitermQueries = useFilterForMultitermQueries; + this.useFilterForParsingSynonyms = useFilterForParsingSynonyms; this.create = create; } @@ -104,6 +116,13 @@ public TokenStream create(TokenStream tokenStream) { public Object getMultiTermComponent() { return this; } + + public TokenFilterFactory getSynonymFilter() { + if (useFilterForParsingSynonyms) { + return this; + } + return IDENTITY_FILTER; + } }; } return new TokenFilterFactory() { @@ -116,6 +135,14 @@ public String name() { public TokenStream create(TokenStream tokenStream) { return create.apply(tokenStream, version); } + + @Override + public TokenFilterFactory getSynonymFilter() { + if (useFilterForParsingSynonyms) { + return this; + } + return IDENTITY_FILTER; + } }; } } diff --git a/server/src/main/java/org/elasticsearch/index/analysis/ShingleTokenFilterFactory.java b/server/src/main/java/org/elasticsearch/index/analysis/ShingleTokenFilterFactory.java index ed8045bc15b9f..6cf2abd87cffb 100644 --- a/server/src/main/java/org/elasticsearch/index/analysis/ShingleTokenFilterFactory.java +++ b/server/src/main/java/org/elasticsearch/index/analysis/ShingleTokenFilterFactory.java @@ -19,15 +19,20 @@ package org.elasticsearch.index.analysis; +import org.apache.logging.log4j.LogManager; import org.apache.lucene.analysis.TokenStream; import org.apache.lucene.analysis.miscellaneous.DisableGraphAttribute; import org.apache.lucene.analysis.shingle.ShingleFilter; +import org.elasticsearch.common.logging.DeprecationLogger; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.env.Environment; import org.elasticsearch.index.IndexSettings; public class ShingleTokenFilterFactory extends AbstractTokenFilterFactory { + private static final DeprecationLogger DEPRECATION_LOGGER = + new DeprecationLogger(LogManager.getLogger(ShingleTokenFilterFactory.class)); + private final Factory factory; public ShingleTokenFilterFactory(IndexSettings indexSettings, Environment environment, String name, Settings settings) { @@ -47,8 +52,8 @@ public ShingleTokenFilterFactory(IndexSettings indexSettings, Environment enviro } String tokenSeparator = settings.get("token_separator", ShingleFilter.DEFAULT_TOKEN_SEPARATOR); String fillerToken = settings.get("filler_token", ShingleFilter.DEFAULT_FILLER_TOKEN); - factory = new Factory("shingle", minShingleSize, maxShingleSize, outputUnigrams, outputUnigramsIfNoShingles, - tokenSeparator, fillerToken); + factory = new Factory("shingle", minShingleSize, maxShingleSize, + outputUnigrams, outputUnigramsIfNoShingles, tokenSeparator, fillerToken); } @@ -57,6 +62,12 @@ public TokenStream create(TokenStream tokenStream) { return factory.create(tokenStream); } + @Override + public TokenFilterFactory getSynonymFilter() { + DEPRECATION_LOGGER.deprecatedAndMaybeLog("synonym_tokenfilters", "Token filter [" + name() + + "] will not be usable to parse synonyms after v7.0"); + return this; + } public Factory getInnerFactory() { return this.factory; diff --git a/server/src/main/java/org/elasticsearch/index/analysis/SynonymTokenFilterFactory.java b/server/src/main/java/org/elasticsearch/index/analysis/SynonymTokenFilterFactory.java index 393bba8fd4518..70ef94813c8f4 100644 --- a/server/src/main/java/org/elasticsearch/index/analysis/SynonymTokenFilterFactory.java +++ b/server/src/main/java/org/elasticsearch/index/analysis/SynonymTokenFilterFactory.java @@ -19,6 +19,7 @@ package org.elasticsearch.index.analysis; +import org.apache.logging.log4j.LogManager; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.LowerCaseFilter; import org.apache.lucene.analysis.TokenStream; @@ -26,6 +27,7 @@ import org.apache.lucene.analysis.synonym.SynonymFilter; import org.apache.lucene.analysis.synonym.SynonymMap; import org.elasticsearch.Version; +import org.elasticsearch.common.logging.DeprecationLogger; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.env.Environment; import org.elasticsearch.index.IndexSettings; @@ -39,6 +41,9 @@ public class SynonymTokenFilterFactory extends AbstractTokenFilterFactory { + private static final DeprecationLogger deprecationLogger + = new DeprecationLogger(LogManager.getLogger(SynonymTokenFilterFactory.class)); + /** * @deprecated this property only works with tokenizer property */ @@ -109,10 +114,18 @@ public String name() { public TokenStream create(TokenStream tokenStream) { return synonyms.fst == null ? tokenStream : new SynonymFilter(tokenStream, synonyms, false); } + + @Override + public TokenFilterFactory getSynonymFilter() { + // In order to allow chained synonym filters, we return IDENTITY here to + // ensure that synonyms don't get applied to the synonym map itself, + // which doesn't support stacked input tokens + return IDENTITY_FILTER; + } }; } - protected Analyzer buildSynonymAnalyzer(TokenizerFactory tokenizer, List charFilters, + public Analyzer buildSynonymAnalyzer(TokenizerFactory tokenizer, List charFilters, List tokenFilters) { if (tokenizerFactory != null) { return new Analyzer() { diff --git a/server/src/main/java/org/elasticsearch/index/analysis/TokenFilterFactory.java b/server/src/main/java/org/elasticsearch/index/analysis/TokenFilterFactory.java index 9d9a48c3a332e..cf17d14501529 100644 --- a/server/src/main/java/org/elasticsearch/index/analysis/TokenFilterFactory.java +++ b/server/src/main/java/org/elasticsearch/index/analysis/TokenFilterFactory.java @@ -58,7 +58,8 @@ default TokenFilterFactory getChainAwareTokenFilterFactory(TokenizerFactory toke * Return a version of this TokenFilterFactory appropriate for synonym parsing * * Filters that should not be applied to synonyms (for example, those that produce - * multiple tokens) can return {@link #IDENTITY_FILTER} + * multiple tokens) should throw an exception + * */ default TokenFilterFactory getSynonymFilter() { return this; From 06adb1964ccc358bb9d4a0a67d991642f825900c Mon Sep 17 00:00:00 2001 From: Albert Zaharovits Date: Thu, 29 Nov 2018 16:12:25 +0200 Subject: [PATCH 024/115] Fix IndexAuditTrail rolling restart on rollover edge. (#35988) This fixes two independent bugs , both tripping integ test failures. They are both facilitated by the rolling nature of the audit index. Moreover, they will both manifest only during a rolling upgrade executed while the audit index rolls over. --- .../security/audit/index/IndexAuditTrail.java | 15 ++++++++++++--- .../upgrades/IndexAuditUpgradeIT.java | 1 - 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrail.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrail.java index 689510e83aaeb..a524ab1bcb6b0 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrail.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrail.java @@ -277,7 +277,7 @@ private boolean canStart(ClusterState clusterState) { } if (TemplateUtils.checkTemplateExistsAndVersionMatches(INDEX_TEMPLATE_NAME, SECURITY_VERSION_STRING, - clusterState, logger, Version.CURRENT::onOrAfter) == false) { + clusterState, logger, Version.CURRENT::onOrBefore) == false) { logger.debug("security audit index template [{}] is not up to date", INDEX_TEMPLATE_NAME); return false; } @@ -308,6 +308,15 @@ private String getIndexName() { return index; } + private boolean hasStaleMessage() { + final Message first = peek(); + if (first == null) { + return false; + } + return false == IndexNameResolver.resolve(first.timestamp, rollover) + .equals(IndexNameResolver.resolve(DateTime.now(DateTimeZone.UTC), rollover)); + } + /** * Starts the service. The state is moved to {@link org.elasticsearch.xpack.security.audit.index.IndexAuditTrail.State#STARTING} * at the beginning of the method. The service's components are initialized and if the current node is the master, the index @@ -381,7 +390,7 @@ void updateCurrentIndexMappingsIfNecessary(ClusterState state) { IndexMetaData indexMetaData = indices.get(0); MappingMetaData docMapping = indexMetaData.mapping("doc"); if (docMapping == null) { - if (indexToRemoteCluster || state.nodes().isLocalNodeElectedMaster()) { + if (indexToRemoteCluster || state.nodes().isLocalNodeElectedMaster() || hasStaleMessage()) { putAuditIndexMappingsAndStart(index); } else { logger.trace("audit index [{}] is missing mapping for type [{}]", index, DOC_TYPE); @@ -399,7 +408,7 @@ void updateCurrentIndexMappingsIfNecessary(ClusterState state) { if (versionString != null && Version.fromString(versionString).onOrAfter(Version.CURRENT)) { innerStart(); } else { - if (indexToRemoteCluster || state.nodes().isLocalNodeElectedMaster()) { + if (indexToRemoteCluster || state.nodes().isLocalNodeElectedMaster() || hasStaleMessage()) { putAuditIndexMappingsAndStart(index); } else if (versionString == null) { logger.trace("audit index [{}] mapping is missing meta field [{}]", index, SECURITY_VERSION_STRING); diff --git a/x-pack/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/IndexAuditUpgradeIT.java b/x-pack/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/IndexAuditUpgradeIT.java index 4c4bd00a5de8a..9b3005a34b06e 100644 --- a/x-pack/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/IndexAuditUpgradeIT.java +++ b/x-pack/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/IndexAuditUpgradeIT.java @@ -62,7 +62,6 @@ public void findMinVersionInCluster() throws IOException { } } - @AwaitsFix(bugUrl="https://github.com/elastic/elasticsearch/issues/35867") public void testAuditLogs() throws Exception { assertBusy(() -> { assertAuditDocsExist(); From 8d5833455d7e3adde7d260941370c5fa1ebf7e22 Mon Sep 17 00:00:00 2001 From: Jim Ferenczi Date: Thu, 29 Nov 2018 14:35:25 +0100 Subject: [PATCH 025/115] Cache the score of the parent document in the nested agg (#36019) The nested agg can defer the collection of children if it is nested under another aggregation. In such case accessing the score in the children aggregation throws an error because the scorer has already advanced to the next parent. This change fixes this error by caching the score of the parent in the nested aggregation. Children aggregations that work on nested documents will be able to access the _score. Also note that the _score in this case is always the parent's score, there is no way to retrieve the score of a nested docs in aggregations. Closes #35985 Closes #34555 --- .../bucket/nested/NestedAggregator.java | 34 +++++++++++++ .../bucket/terms/TermsAggregatorTests.java | 49 +++++++++++++------ 2 files changed, 68 insertions(+), 15 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/nested/NestedAggregator.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/nested/NestedAggregator.java index a85225e846372..4bc39546de0de 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/nested/NestedAggregator.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/nested/NestedAggregator.java @@ -140,7 +140,9 @@ class BufferingNestedLeafBucketCollector extends LeafBucketCollectorBase { final DocIdSetIterator childDocs; final LongArrayList bucketBuffer = new LongArrayList(); + Scorer scorer; int currentParentDoc = -1; + final CachedScorer cachedScorer = new CachedScorer(); BufferingNestedLeafBucketCollector(LeafBucketCollector sub, BitSet parentDocs, DocIdSetIterator childDocs) { super(sub, null); @@ -149,6 +151,12 @@ class BufferingNestedLeafBucketCollector extends LeafBucketCollectorBase { this.childDocs = childDocs; } + @Override + public void setScorer(Scorer scorer) throws IOException { + this.scorer = scorer; + super.setScorer(cachedScorer); + } + @Override public void collect(int parentDoc, long bucket) throws IOException { // if parentDoc is 0 then this means that this parent doesn't have child docs (b/c these appear always before the parent @@ -159,7 +167,12 @@ public void collect(int parentDoc, long bucket) throws IOException { if (currentParentDoc != parentDoc) { processBufferedChildBuckets(); + if (needsScores()) { + // cache the score of the current parent + cachedScorer.score = scorer.score(); + } currentParentDoc = parentDoc; + } bucketBuffer.add(bucket); } @@ -177,6 +190,7 @@ void processBufferedChildBuckets() throws IOException { } for (; childDocId < currentParentDoc; childDocId = childDocs.nextDoc()) { + cachedScorer.doc = childDocId; final long[] buffer = bucketBuffer.buffer; final int size = bucketBuffer.size(); for (int i = 0; i < size; i++) { @@ -185,6 +199,26 @@ void processBufferedChildBuckets() throws IOException { } bucketBuffer.clear(); } + } + + private static class CachedScorer extends Scorer { + int doc; + float score; + + private CachedScorer() { super(null); } + + @Override + public DocIdSetIterator iterator() { + throw new UnsupportedOperationException(); + } + + @Override + public final float score() { return score; } + + @Override + public int docID() { + return doc; + } } diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/terms/TermsAggregatorTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/terms/TermsAggregatorTests.java index 7c8a0b9e06145..d07a9030886d6 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/terms/TermsAggregatorTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/terms/TermsAggregatorTests.java @@ -47,6 +47,7 @@ import org.elasticsearch.index.mapper.SeqNoFieldMapper; import org.elasticsearch.index.mapper.TypeFieldMapper; import org.elasticsearch.index.mapper.UidFieldMapper; +import org.elasticsearch.index.query.MatchAllQueryBuilder; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.indices.breaker.NoneCircuitBreakerService; import org.elasticsearch.search.SearchHit; @@ -60,6 +61,7 @@ import org.elasticsearch.search.aggregations.bucket.MultiBucketsAggregation; import org.elasticsearch.search.aggregations.bucket.filter.Filter; import org.elasticsearch.search.aggregations.bucket.filter.FilterAggregationBuilder; +import org.elasticsearch.search.aggregations.bucket.filter.InternalFilter; import org.elasticsearch.search.aggregations.bucket.global.GlobalAggregationBuilder; import org.elasticsearch.search.aggregations.bucket.global.InternalGlobal; import org.elasticsearch.search.aggregations.bucket.nested.InternalNested; @@ -1042,21 +1044,23 @@ public void testWithNestedAggregations() throws IOException { fieldType.setHasDocValues(true); fieldType.setName("nested_value"); try (IndexReader indexReader = wrap(DirectoryReader.open(directory))) { - InternalNested result = search(newSearcher(indexReader, false, true), - // match root document only - new DocValuesFieldExistsQuery(PRIMARY_TERM_NAME), nested, fieldType); - InternalMultiBucketAggregation terms = result.getAggregations().get("terms"); - assertThat(terms.getBuckets().size(), equalTo(9)); - int ptr = 9; - for (MultiBucketsAggregation.Bucket bucket : terms.getBuckets()) { - InternalTopHits topHits = bucket.getAggregations().get("top_hits"); - assertThat(topHits.getHits().totalHits, equalTo((long) ptr)); - if (withScore) { - assertThat(topHits.getHits().getMaxScore(), equalTo(1f)); - } else { - assertThat(topHits.getHits().getMaxScore(), equalTo(Float.NaN)); - } - --ptr; + { + InternalNested result = search(newSearcher(indexReader, false, true), + // match root document only + new DocValuesFieldExistsQuery(PRIMARY_TERM_NAME), nested, fieldType); + InternalMultiBucketAggregation terms = result.getAggregations().get("terms"); + assertNestedTopHitsScore(terms, withScore); + } + + { + FilterAggregationBuilder filter = new FilterAggregationBuilder("filter", new MatchAllQueryBuilder()) + .subAggregation(nested); + InternalFilter result = search(newSearcher(indexReader, false, true), + // match root document only + new DocValuesFieldExistsQuery(PRIMARY_TERM_NAME), filter, fieldType); + InternalNested nestedResult = result.getAggregations().get("nested"); + InternalMultiBucketAggregation terms = nestedResult.getAggregations().get("terms"); + assertNestedTopHitsScore(terms, withScore); } } } @@ -1065,6 +1069,21 @@ public void testWithNestedAggregations() throws IOException { } } + private void assertNestedTopHitsScore(InternalMultiBucketAggregation terms, boolean withScore) { + assertThat(terms.getBuckets().size(), equalTo(9)); + int ptr = 9; + for (MultiBucketsAggregation.Bucket bucket : terms.getBuckets()) { + InternalTopHits topHits = bucket.getAggregations().get("top_hits"); + assertThat(topHits.getHits().totalHits, equalTo((long) ptr)); + if (withScore) { + assertThat(topHits.getHits().getMaxScore(), equalTo(1f)); + } else { + assertThat(topHits.getHits().getMaxScore(), equalTo(Float.NaN)); + } + --ptr; + } + } + private final SeqNoFieldMapper.SequenceIDFields sequenceIDFields = SeqNoFieldMapper.SequenceIDFields.emptySeqID(); private List generateDocsWithNested(String id, int value, int[] nestedValues) { List documents = new ArrayList<>(); From eaa2c3ea725e6b53e6e98eb0419b04df552a4778 Mon Sep 17 00:00:00 2001 From: Costin Leau Date: Thu, 29 Nov 2018 18:52:47 +0200 Subject: [PATCH 026/115] SQL: Make INTERVAL millis optional (#36043) Fractions of the second are not mandatory anymore inside INTERVAL declarations Fix #36032 (cherry picked from commit 1d458e3f60899e59f0a6b9272401287c69ed6d0f) --- .../sql/expression/literal/Intervals.java | 36 ++++++++++++---- .../expression/literal/IntervalsTests.java | 42 ++++++++++++++++--- 2 files changed, 64 insertions(+), 14 deletions(-) diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/literal/Intervals.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/literal/Intervals.java index ad3b345e9c560..a64535e83b729 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/literal/Intervals.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/literal/Intervals.java @@ -6,6 +6,7 @@ package org.elasticsearch.xpack.sql.expression.literal; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.io.stream.NamedWriteableRegistry.Entry; import org.elasticsearch.xpack.sql.SqlIllegalArgumentException; @@ -124,6 +125,7 @@ private static class ParserBuilder { private final List units; private final List tokens; private final String name; + private boolean optional = false; ParserBuilder(DataType dataType) { units = new ArrayList<>(10); @@ -138,12 +140,17 @@ ParserBuilder unit(TimeUnit unit) { ParserBuilder unit(TimeUnit unit, int maxValue) { units.add(unit); - tokens.add(new Token((char) 0, maxValue)); + tokens.add(new Token((char) 0, maxValue, optional)); return this; } ParserBuilder separator(char ch) { - tokens.add(new Token(ch, 0)); + tokens.add(new Token(ch, 0, optional)); + return this; + } + + ParserBuilder optional() { + optional = true; return this; } @@ -155,15 +162,17 @@ Parser build() { private static class Token { private final char ch; private final int maxValue; + private final boolean optional; - Token(char ch, int maxValue) { + Token(char ch, int maxValue, boolean optional) { this.ch = ch; this.maxValue = maxValue; + this.optional = optional; } @Override public String toString() { - return ch > 0 ? String.valueOf(ch) : "[numeric" + (maxValue > 0 ? " < " + maxValue + " " : "") + "]"; + return ch > 0 ? String.valueOf(ch) : "[numeric]"; } } @@ -203,6 +212,15 @@ TemporalAmount parse(Location source, String string) { for (Token token : tokens) { endToken = startToken; + if (startToken >= string.length()) { + // consumed the string, bail out + if (token.optional) { + break; + } + throw new ParsingException(source, invalidIntervalMessage(string) + ": incorrect format, expecting {}", + Strings.collectionToDelimitedString(tokens, "")); + } + // char token if (token.ch != 0) { char found = string.charAt(startToken); @@ -309,8 +327,8 @@ public static TemporalAmount negate(TemporalAmount interval) { PARSERS.put(DataType.INTERVAL_MINUTE, new ParserBuilder(DataType.INTERVAL_MINUTE).unit(TimeUnit.MINUTE).build()); PARSERS.put(DataType.INTERVAL_SECOND, new ParserBuilder(DataType.INTERVAL_SECOND) .unit(TimeUnit.SECOND) - .separator(DOT) - .unit(TimeUnit.MILLISECOND, MAX_MILLI) + .optional() + .separator(DOT).unit(TimeUnit.MILLISECOND, MAX_MILLI) .build()); // patterns @@ -342,6 +360,7 @@ public static TemporalAmount negate(TemporalAmount interval) { .unit(TimeUnit.MINUTE, MAX_MINUTE) .separator(COLON) .unit(TimeUnit.SECOND, MAX_SECOND) + .optional() .separator(DOT).unit(TimeUnit.MILLISECOND, MAX_MILLI) .build()); @@ -357,6 +376,7 @@ public static TemporalAmount negate(TemporalAmount interval) { .unit(TimeUnit.MINUTE, MAX_MINUTE) .separator(COLON) .unit(TimeUnit.SECOND, MAX_SECOND) + .optional() .separator(DOT).unit(TimeUnit.MILLISECOND, MAX_MILLI) .build()); @@ -364,8 +384,8 @@ public static TemporalAmount negate(TemporalAmount interval) { .unit(TimeUnit.MINUTE) .separator(COLON) .unit(TimeUnit.SECOND, MAX_SECOND) - .separator(DOT) - .unit(TimeUnit.MILLISECOND, MAX_MILLI) + .optional() + .separator(DOT).unit(TimeUnit.MILLISECOND, MAX_MILLI) .build()); } diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/literal/IntervalsTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/literal/IntervalsTests.java index 521f7cb26e18f..bc84f3837ecca 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/literal/IntervalsTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/literal/IntervalsTests.java @@ -89,6 +89,13 @@ public void testSecondInterval() throws Exception { assertEquals(maybeNegate(sign, Duration.ofSeconds(randomSeconds).plusMillis(randomMillis)), amount); } + public void testSecondNoMillisInterval() throws Exception { + int randomSeconds = randomNonNegativeInt(); + String value = format(Locale.ROOT, "%s%d", sign, randomSeconds); + TemporalAmount amount = parseInterval(EMPTY, value, INTERVAL_SECOND); + assertEquals(maybeNegate(sign, Duration.ofSeconds(randomSeconds)), amount); + } + public void testYearToMonth() throws Exception { int randomYear = randomNonNegativeInt(); int randomMonth = randomInt(11); @@ -119,9 +126,12 @@ public void testDayToSecond() throws Exception { int randomHour = randomInt(23); int randomMinute = randomInt(59); int randomSecond = randomInt(59); - int randomMilli = randomInt(999999999); - String value = format(Locale.ROOT, "%s%d %d:%d:%d.%d", sign, randomDay, randomHour, randomMinute, randomSecond, randomMilli); + boolean withMillis = randomBoolean(); + int randomMilli = withMillis ? randomInt(999999999) : 0; + String millisString = withMillis ? "." + randomMilli : ""; + + String value = format(Locale.ROOT, "%s%d %d:%d:%d%s", sign, randomDay, randomHour, randomMinute, randomSecond, millisString); TemporalAmount amount = parseInterval(EMPTY, value, INTERVAL_DAY_TO_SECOND); assertEquals(maybeNegate(sign, Duration.ofDays(randomDay).plusHours(randomHour).plusMinutes(randomMinute) .plusSeconds(randomSecond).plusMillis(randomMilli)), amount); @@ -139,9 +149,12 @@ public void testHourToSecond() throws Exception { int randomHour = randomNonNegativeInt(); int randomMinute = randomInt(59); int randomSecond = randomInt(59); - int randomMilli = randomInt(999999999); - String value = format(Locale.ROOT, "%s%d:%d:%d.%d", sign, randomHour, randomMinute, randomSecond, randomMilli); + boolean withMillis = randomBoolean(); + int randomMilli = withMillis ? randomInt(999999999) : 0; + String millisString = withMillis ? "." + randomMilli : ""; + + String value = format(Locale.ROOT, "%s%d:%d:%d%s", sign, randomHour, randomMinute, randomSecond, millisString); TemporalAmount amount = parseInterval(EMPTY, value, INTERVAL_HOUR_TO_SECOND); assertEquals(maybeNegate(sign, Duration.ofHours(randomHour).plusMinutes(randomMinute).plusSeconds(randomSecond).plusMillis(randomMilli)), amount); @@ -150,9 +163,12 @@ public void testHourToSecond() throws Exception { public void testMinuteToSecond() throws Exception { int randomMinute = randomNonNegativeInt(); int randomSecond = randomInt(59); - int randomMilli = randomInt(999999999); - String value = format(Locale.ROOT, "%s%d:%d.%d", sign, randomMinute, randomSecond, randomMilli); + boolean withMillis = randomBoolean(); + int randomMilli = withMillis ? randomInt(999999999) : 0; + String millisString = withMillis ? "." + randomMilli : ""; + + String value = format(Locale.ROOT, "%s%d:%d%s", sign, randomMinute, randomSecond, millisString); TemporalAmount amount = parseInterval(EMPTY, value, INTERVAL_MINUTE_TO_SECOND); assertEquals(maybeNegate(sign, Duration.ofMinutes(randomMinute).plusSeconds(randomSecond).plusMillis(randomMilli)), amount); } @@ -187,6 +203,20 @@ public void testDayToMinuteTooBig() throws Exception { + "], expected a positive number up to [23]", pe.getMessage()); } + public void testIncompleteYearToMonthInterval() throws Exception { + String value = "123-"; + ParsingException pe = expectThrows(ParsingException.class, () -> parseInterval(EMPTY, value, INTERVAL_YEAR_TO_MONTH)); + assertEquals("line -1:0: Invalid [INTERVAL YEAR TO MONTH] value [123-]: incorrect format, expecting [numeric]-[numeric]", + pe.getMessage()); + } + + public void testIncompleteDayToHourInterval() throws Exception { + String value = "123 23:"; + ParsingException pe = expectThrows(ParsingException.class, () -> parseInterval(EMPTY, value, INTERVAL_DAY_TO_HOUR)); + assertEquals("line -1:0: Invalid [INTERVAL DAY TO HOUR] value [123 23:]: unexpected trailing characters found [:]", + pe.getMessage()); + } + public void testExtraCharLeading() throws Exception { String value = "a123"; ParsingException pe = expectThrows(ParsingException.class, () -> parseInterval(EMPTY, value, INTERVAL_YEAR)); From c0c478522179183c98c25bcf202fc95d0a633d31 Mon Sep 17 00:00:00 2001 From: Ryan Ernst Date: Thu, 29 Nov 2018 09:39:17 -0800 Subject: [PATCH 027/115] Build: Fix jdbc jar pom to not include deps (#36036) This commit adds back the exclusion of dependencies from the pom file for the jdbc jar, which was accidentally lost in #32014 --- x-pack/plugin/sql/jdbc/build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/x-pack/plugin/sql/jdbc/build.gradle b/x-pack/plugin/sql/jdbc/build.gradle index 1a4d2313022f7..19c16e553d8fa 100644 --- a/x-pack/plugin/sql/jdbc/build.gradle +++ b/x-pack/plugin/sql/jdbc/build.gradle @@ -67,6 +67,10 @@ publishing { publications { nebula { artifactId = archivesBaseName + pom.withXml { + // Nebula is mistakenly including all dependencies that are already shadowed into the shadow jar + asNode().remove(asNode().dependencies) + } } } } From 29dc7d1f4430f6afad4988ffc0873d64f993790f Mon Sep 17 00:00:00 2001 From: Toby McLaughlin Date: Fri, 30 Nov 2018 04:33:08 +1100 Subject: [PATCH 028/115] [DOCS] Remove "platinum" references for Docker TLS (#35890) --- .../configuring-tls-docker.asciidoc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/reference/security/securing-communications/configuring-tls-docker.asciidoc b/docs/reference/security/securing-communications/configuring-tls-docker.asciidoc index 49913382482bd..e7e1a00208adc 100644 --- a/docs/reference/security/securing-communications/configuring-tls-docker.asciidoc +++ b/docs/reference/security/securing-communications/configuring-tls-docker.asciidoc @@ -66,7 +66,7 @@ version: '2.2' services: create_certs: container_name: create_certs - image: docker.elastic.co/elasticsearch/elasticsearch-platinum:{version} + image: {docker-image} command: > bash -c ' if [[ ! -d config/certificates/certs ]]; then @@ -103,7 +103,7 @@ version: '2.2' services: es01: container_name: es01 - image: docker.elastic.co/elasticsearch/elasticsearch-platinum:{version} + image: {docker-image} environment: - node.name=es01 - discovery.zen.minimum_master_nodes=2 @@ -128,7 +128,7 @@ services: es02: container_name: es02 - image: docker.elastic.co/elasticsearch/elasticsearch:{version} + image: {docker-image} environment: - node.name=es02 - discovery.zen.minimum_master_nodes=2 @@ -146,7 +146,7 @@ services: volumes: ['esdata_02:/usr/share/elasticsearch/data', './certs:$CERTS_DIR'] wait_until_ready: - image: docker.elastic.co/elasticsearch/elasticsearch:{version} + image: {docker-image} command: /usr/bin/true depends_on: {"es01": {"condition": "service_healthy"}} From 77913fcb1b05b001c773631552c3fda36d587e9a Mon Sep 17 00:00:00 2001 From: Nik Everett Date: Thu, 29 Nov 2018 08:38:09 -0500 Subject: [PATCH 029/115] Build: Fix reproduce info for methods with ( or ) (#35712) Our `REPRODUCE WITH` line wasn't working for tests with `(` or `)` in the method name. This isn't super common, but happens sometimes for our yml based rest tests. This is because randomized runner's globbing mechanism dind't escape `(` or `)`. I filed this upstream at https://github.com/randomizedtesting/randomizedtesting/issues/271 and Dawid kindly fixed the issue. This upgrades to pick up the fix. Closes #35692 --- buildSrc/version.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/version.properties b/buildSrc/version.properties index 594cb6507707c..94f2470666c65 100644 --- a/buildSrc/version.properties +++ b/buildSrc/version.properties @@ -19,7 +19,7 @@ netty = 4.1.31.Final joda = 2.10.1 # test dependencies -randomizedrunner = 2.7.0 +randomizedrunner = 2.7.1 junit = 4.12 httpclient = 4.5.2 # When updating httpcore, please also update server/src/main/resources/org/elasticsearch/bootstrap/test-framework.policy From 89a0d110e29824073b57ea25b77b613ba3b916e6 Mon Sep 17 00:00:00 2001 From: Tim Brooks Date: Thu, 29 Nov 2018 11:05:58 -0700 Subject: [PATCH 030/115] Make keepalive pings bidirectional and optimizable (#35441) (#36063) This is related to #34405 and a follow-up to #34753. It makes a number of changes to our current keepalive pings. The ping interval configuration is moved to the ConnectionProfile. The server channel now responds to pings. This makes the keepalive pings bidirectional. On the client-side, the pings can now be optimized away. What this means is that if the channel has received a message or sent a message since the last pinging round, the ping is not sent for this round. --- .../transport/netty4/Netty4TcpChannel.java | 15 +- .../transport/netty4/Netty4Transport.java | 13 +- .../netty4/Netty4ScheduledPingTests.java | 139 ----------- .../elasticsearch/common/AsyncBiFunction.java | 29 +++ .../transport/ConnectionManager.java | 58 +---- .../transport/ConnectionProfile.java | 55 ++++- .../transport/RemoteClusterConnection.java | 63 ++--- .../transport/RemoteClusterService.java | 7 +- .../elasticsearch/transport/TcpChannel.java | 28 +++ .../elasticsearch/transport/TcpTransport.java | 134 +++++------ .../elasticsearch/transport/Transport.java | 4 - .../transport/TransportKeepAlive.java | 210 +++++++++++++++++ .../transport/ConnectionManagerTests.java | 4 +- .../transport/ConnectionProfileTests.java | 19 +- .../RemoteClusterConnectionTests.java | 194 +++++++-------- .../transport/RemoteClusterServiceTests.java | 12 +- .../transport/TcpTransportTests.java | 30 +-- ...sts.java => TransportHandshakerTests.java} | 2 +- .../transport/TransportKeepAliveTests.java | 220 ++++++++++++++++++ .../test/transport/StubbableTransport.java | 5 - .../AbstractSimpleTransportTestCase.java | 21 +- .../transport/FakeTcpChannel.java | 104 +++++++++ .../transport/MockTcpTransport.java | 26 ++- 23 files changed, 918 insertions(+), 474 deletions(-) delete mode 100644 modules/transport-netty4/src/test/java/org/elasticsearch/transport/netty4/Netty4ScheduledPingTests.java create mode 100644 server/src/main/java/org/elasticsearch/common/AsyncBiFunction.java create mode 100644 server/src/main/java/org/elasticsearch/transport/TransportKeepAlive.java rename server/src/test/java/org/elasticsearch/transport/{TcpTransportHandshakerTests.java => TransportHandshakerTests.java} (99%) create mode 100644 server/src/test/java/org/elasticsearch/transport/TransportKeepAliveTests.java create mode 100644 test/framework/src/main/java/org/elasticsearch/transport/FakeTcpChannel.java diff --git a/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/Netty4TcpChannel.java b/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/Netty4TcpChannel.java index 30a83eeed7efd..4cb0f89adedac 100644 --- a/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/Netty4TcpChannel.java +++ b/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/Netty4TcpChannel.java @@ -35,12 +35,15 @@ public class Netty4TcpChannel implements TcpChannel { private final Channel channel; + private final boolean isServer; private final String profile; private final CompletableContext connectContext; private final CompletableContext closeContext = new CompletableContext<>(); + private final ChannelStats stats = new ChannelStats(); - Netty4TcpChannel(Channel channel, String profile, @Nullable ChannelFuture connectFuture) { + Netty4TcpChannel(Channel channel, boolean isServer, String profile, @Nullable ChannelFuture connectFuture) { this.channel = channel; + this.isServer = isServer; this.profile = profile; this.connectContext = new CompletableContext<>(); this.channel.closeFuture().addListener(f -> { @@ -77,6 +80,11 @@ public void close() { channel.close(); } + @Override + public boolean isServerChannel() { + return isServer; + } + @Override public String getProfile() { return profile; @@ -92,6 +100,11 @@ public void addConnectListener(ActionListener listener) { connectContext.addListener(ActionListener.toBiConsumer(listener)); } + @Override + public ChannelStats getChannelStats() { + return stats; + } + @Override public boolean isOpen() { return channel.isOpen(); diff --git a/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/Netty4Transport.java b/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/Netty4Transport.java index f26e55d7ac647..63f5982d3ed43 100644 --- a/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/Netty4Transport.java +++ b/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/Netty4Transport.java @@ -232,7 +232,7 @@ protected Netty4TcpChannel initiateChannel(DiscoveryNode node) throws IOExceptio } addClosedExceptionLogger(channel); - Netty4TcpChannel nettyChannel = new Netty4TcpChannel(channel, "default", connectFuture); + Netty4TcpChannel nettyChannel = new Netty4TcpChannel(channel, false, "default", connectFuture); channel.attr(CHANNEL_KEY).set(nettyChannel); return nettyChannel; @@ -246,14 +246,6 @@ protected Netty4TcpServerChannel bind(String name, InetSocketAddress address) { return esChannel; } - long successfulPingCount() { - return successfulPings.count(); - } - - long failedPingCount() { - return failedPings.count(); - } - @Override @SuppressForbidden(reason = "debug") protected void stopInternal() { @@ -297,8 +289,7 @@ protected ServerChannelInitializer(String name) { @Override protected void initChannel(Channel ch) throws Exception { addClosedExceptionLogger(ch); - Netty4TcpChannel nettyTcpChannel = new Netty4TcpChannel(ch, name, ch.newSucceededFuture()); - + Netty4TcpChannel nettyTcpChannel = new Netty4TcpChannel(ch, true, name, ch.newSucceededFuture()); ch.attr(CHANNEL_KEY).set(nettyTcpChannel); serverAcceptedChannel(nettyTcpChannel); ch.pipeline().addLast("logging", new ESLoggingHandler()); diff --git a/modules/transport-netty4/src/test/java/org/elasticsearch/transport/netty4/Netty4ScheduledPingTests.java b/modules/transport-netty4/src/test/java/org/elasticsearch/transport/netty4/Netty4ScheduledPingTests.java deleted file mode 100644 index 688e87bba8181..0000000000000 --- a/modules/transport-netty4/src/test/java/org/elasticsearch/transport/netty4/Netty4ScheduledPingTests.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Licensed to Elasticsearch under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch licenses this file to you 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.elasticsearch.transport.netty4; - -import org.elasticsearch.Version; -import org.elasticsearch.cluster.node.DiscoveryNode; -import org.elasticsearch.common.io.stream.NamedWriteableRegistry; -import org.elasticsearch.common.io.stream.StreamInput; -import org.elasticsearch.common.lease.Releasables; -import org.elasticsearch.common.network.NetworkService; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.util.BigArrays; -import org.elasticsearch.indices.breaker.CircuitBreakerService; -import org.elasticsearch.indices.breaker.NoneCircuitBreakerService; -import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.test.transport.MockTransportService; -import org.elasticsearch.threadpool.TestThreadPool; -import org.elasticsearch.threadpool.ThreadPool; -import org.elasticsearch.transport.TcpTransport; -import org.elasticsearch.transport.TransportException; -import org.elasticsearch.transport.TransportRequest; -import org.elasticsearch.transport.TransportRequestOptions; -import org.elasticsearch.transport.TransportResponse; -import org.elasticsearch.transport.TransportResponseHandler; -import org.elasticsearch.transport.TransportService; - -import java.io.IOException; -import java.util.Collections; - -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.greaterThan; - -public class Netty4ScheduledPingTests extends ESTestCase { - - public void testScheduledPing() throws Exception { - ThreadPool threadPool = new TestThreadPool(getClass().getName()); - - Settings settings = Settings.builder() - .put(TcpTransport.PING_SCHEDULE.getKey(), "5ms") - .put(TcpTransport.PORT.getKey(), 0) - .put("cluster.name", "test") - .build(); - - CircuitBreakerService circuitBreakerService = new NoneCircuitBreakerService(); - - NamedWriteableRegistry registry = new NamedWriteableRegistry(Collections.emptyList()); - final Netty4Transport nettyA = new Netty4Transport(settings, Version.CURRENT, threadPool, - new NetworkService(Collections.emptyList()), BigArrays.NON_RECYCLING_INSTANCE, registry, circuitBreakerService); - MockTransportService serviceA = new MockTransportService(settings, nettyA, threadPool, TransportService.NOOP_TRANSPORT_INTERCEPTOR, - null); - serviceA.start(); - serviceA.acceptIncomingRequests(); - - final Netty4Transport nettyB = new Netty4Transport(settings, Version.CURRENT, threadPool, - new NetworkService(Collections.emptyList()), BigArrays.NON_RECYCLING_INSTANCE, registry, circuitBreakerService); - MockTransportService serviceB = new MockTransportService(settings, nettyB, threadPool, TransportService.NOOP_TRANSPORT_INTERCEPTOR, - null); - - serviceB.start(); - serviceB.acceptIncomingRequests(); - - DiscoveryNode nodeA = serviceA.getLocalDiscoNode(); - DiscoveryNode nodeB = serviceB.getLocalDiscoNode(); - - serviceA.connectToNode(nodeB); - serviceB.connectToNode(nodeA); - - assertBusy(() -> { - assertThat(nettyA.successfulPingCount(), greaterThan(100L)); - assertThat(nettyB.successfulPingCount(), greaterThan(100L)); - }); - assertThat(nettyA.failedPingCount(), equalTo(0L)); - assertThat(nettyB.failedPingCount(), equalTo(0L)); - - serviceA.registerRequestHandler("internal:sayHello", TransportRequest.Empty::new, ThreadPool.Names.GENERIC, - (request, channel) -> { - try { - channel.sendResponse(TransportResponse.Empty.INSTANCE); - } catch (IOException e) { - logger.error("Unexpected failure", e); - fail(e.getMessage()); - } - }); - - int rounds = scaledRandomIntBetween(100, 5000); - for (int i = 0; i < rounds; i++) { - serviceB.submitRequest(nodeA, "internal:sayHello", - TransportRequest.Empty.INSTANCE, TransportRequestOptions.builder().withCompress(randomBoolean()).build(), - new TransportResponseHandler() { - @Override - public TransportResponse.Empty read(StreamInput in) { - return TransportResponse.Empty.INSTANCE; - } - - @Override - public String executor() { - return ThreadPool.Names.GENERIC; - } - - @Override - public void handleResponse(TransportResponse.Empty response) { - } - - @Override - public void handleException(TransportException exp) { - logger.error("Unexpected failure", exp); - fail("got exception instead of a response: " + exp.getMessage()); - } - }).txGet(); - } - - assertBusy(() -> { - assertThat(nettyA.successfulPingCount(), greaterThan(200L)); - assertThat(nettyB.successfulPingCount(), greaterThan(200L)); - }); - assertThat(nettyA.failedPingCount(), equalTo(0L)); - assertThat(nettyB.failedPingCount(), equalTo(0L)); - - Releasables.close(serviceA, serviceB); - terminate(threadPool); - } - -} diff --git a/server/src/main/java/org/elasticsearch/common/AsyncBiFunction.java b/server/src/main/java/org/elasticsearch/common/AsyncBiFunction.java new file mode 100644 index 0000000000000..d5bf7b7504347 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/common/AsyncBiFunction.java @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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.elasticsearch.common; + +import org.elasticsearch.action.ActionListener; + +/** + * A {@link java.util.function.BiFunction}-like interface designed to be used with asynchronous executions. + */ +public interface AsyncBiFunction { + + void apply(T t, U u, ActionListener listener); +} diff --git a/server/src/main/java/org/elasticsearch/transport/ConnectionManager.java b/server/src/main/java/org/elasticsearch/transport/ConnectionManager.java index 114bb8c986a07..d5c576784fc00 100644 --- a/server/src/main/java/org/elasticsearch/transport/ConnectionManager.java +++ b/server/src/main/java/org/elasticsearch/transport/ConnectionManager.java @@ -27,10 +27,7 @@ import org.elasticsearch.common.component.Lifecycle; import org.elasticsearch.common.lease.Releasable; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.unit.TimeValue; -import org.elasticsearch.common.util.concurrent.AbstractLifecycleRunnable; import org.elasticsearch.common.util.concurrent.ConcurrentCollections; -import org.elasticsearch.common.util.concurrent.EsRejectedExecutionException; import org.elasticsearch.common.util.concurrent.KeyedLock; import org.elasticsearch.core.internal.io.IOUtils; import org.elasticsearch.threadpool.ThreadPool; @@ -53,13 +50,13 @@ * the connection when the connection manager is closed. */ public class ConnectionManager implements Closeable { + private static final Logger logger = LogManager.getLogger(ConnectionManager.class); private final ConcurrentMap connectedNodes = ConcurrentCollections.newConcurrentMap(); private final KeyedLock connectionLock = new KeyedLock<>(); private final Transport transport; private final ThreadPool threadPool; - private final TimeValue pingSchedule; private final ConnectionProfile defaultProfile; private final Lifecycle lifecycle = new Lifecycle(); private final AtomicBoolean closed = new AtomicBoolean(false); @@ -67,18 +64,14 @@ public class ConnectionManager implements Closeable { private final DelegatingNodeConnectionListener connectionListener = new DelegatingNodeConnectionListener(); public ConnectionManager(Settings settings, Transport transport, ThreadPool threadPool) { - this(settings, transport, threadPool, TcpTransport.PING_SCHEDULE.get(settings)); + this(ConnectionProfile.buildDefaultConnectionProfile(settings), transport, threadPool); } - public ConnectionManager(Settings settings, Transport transport, ThreadPool threadPool, TimeValue pingSchedule) { + public ConnectionManager(ConnectionProfile connectionProfile, Transport transport, ThreadPool threadPool) { this.transport = transport; this.threadPool = threadPool; - this.pingSchedule = pingSchedule; - this.defaultProfile = ConnectionProfile.buildDefaultConnectionProfile(settings); + this.defaultProfile = connectionProfile; this.lifecycle.moveToStarted(); - if (pingSchedule.millis() > 0) { - threadPool.schedule(pingSchedule, ThreadPool.Names.GENERIC, new ScheduledPing()); - } } public void addListener(TransportConnectionListener listener) { @@ -251,47 +244,8 @@ private void ensureOpen() { } } - TimeValue getPingSchedule() { - return pingSchedule; - } - - private class ScheduledPing extends AbstractLifecycleRunnable { - - private ScheduledPing() { - super(lifecycle, logger); - } - - @Override - protected void doRunInLifecycle() { - for (Map.Entry entry : connectedNodes.entrySet()) { - Transport.Connection connection = entry.getValue(); - if (connection.sendPing() == false) { - logger.warn("attempted to send ping to connection without support for pings [{}]", connection); - } - } - } - - @Override - protected void onAfterInLifecycle() { - try { - threadPool.schedule(pingSchedule, ThreadPool.Names.GENERIC, this); - } catch (EsRejectedExecutionException ex) { - if (ex.isExecutorShutdown()) { - logger.debug("couldn't schedule new ping execution, executor is shutting down", ex); - } else { - throw ex; - } - } - } - - @Override - public void onFailure(Exception e) { - if (lifecycle.stoppedOrClosed()) { - logger.trace("failed to send ping transport message", e); - } else { - logger.warn("failed to send ping transport message", e); - } - } + ConnectionProfile getConnectionProfile() { + return defaultProfile; } private static final class DelegatingNodeConnectionListener implements TransportConnectionListener { diff --git a/server/src/main/java/org/elasticsearch/transport/ConnectionProfile.java b/server/src/main/java/org/elasticsearch/transport/ConnectionProfile.java index 4fd03d86d9587..bcab23c1fbdd6 100644 --- a/server/src/main/java/org/elasticsearch/transport/ConnectionProfile.java +++ b/server/src/main/java/org/elasticsearch/transport/ConnectionProfile.java @@ -46,7 +46,7 @@ public static ConnectionProfile resolveConnectionProfile(@Nullable ConnectionPro if (profile == null) { return fallbackProfile; } else if (profile.getConnectTimeout() != null && profile.getHandshakeTimeout() != null - && profile.getCompressionEnabled() != null) { + && profile.getPingInterval() != null && profile.getCompressionEnabled() != null) { return profile; } else { ConnectionProfile.Builder builder = new ConnectionProfile.Builder(profile); @@ -56,6 +56,9 @@ public static ConnectionProfile resolveConnectionProfile(@Nullable ConnectionPro if (profile.getHandshakeTimeout() == null) { builder.setHandshakeTimeout(fallbackProfile.getHandshakeTimeout()); } + if (profile.getPingInterval() == null) { + builder.setPingInterval(fallbackProfile.getPingInterval()); + } if (profile.getCompressionEnabled() == null) { builder.setCompressionEnabled(fallbackProfile.getCompressionEnabled()); } @@ -78,6 +81,7 @@ public static ConnectionProfile buildDefaultConnectionProfile(Settings settings) Builder builder = new Builder(); builder.setConnectTimeout(TransportService.TCP_CONNECT_TIMEOUT.get(settings)); builder.setHandshakeTimeout(TransportService.TCP_CONNECT_TIMEOUT.get(settings)); + builder.setPingInterval(TcpTransport.PING_SCHEDULE.get(settings)); builder.setCompressionEnabled(Transport.TRANSPORT_TCP_COMPRESS.get(settings)); builder.addConnections(connectionsPerNodeBulk, TransportRequestOptions.Type.BULK); builder.addConnections(connectionsPerNodePing, TransportRequestOptions.Type.PING); @@ -94,7 +98,7 @@ public static ConnectionProfile buildDefaultConnectionProfile(Settings settings) * when opening single use connections */ public static ConnectionProfile buildSingleChannelProfile(TransportRequestOptions.Type channelType) { - return buildSingleChannelProfile(channelType, null, null, null); + return buildSingleChannelProfile(channelType, null, null, null, null); } /** @@ -102,7 +106,7 @@ public static ConnectionProfile buildSingleChannelProfile(TransportRequestOption * settings. */ public static ConnectionProfile buildSingleChannelProfile(TransportRequestOptions.Type channelType, boolean compressionEnabled) { - return buildSingleChannelProfile(channelType, null, null, compressionEnabled); + return buildSingleChannelProfile(channelType, null, null, null, compressionEnabled); } /** @@ -111,7 +115,7 @@ public static ConnectionProfile buildSingleChannelProfile(TransportRequestOption */ public static ConnectionProfile buildSingleChannelProfile(TransportRequestOptions.Type channelType, @Nullable TimeValue connectTimeout, @Nullable TimeValue handshakeTimeout) { - return buildSingleChannelProfile(channelType, connectTimeout, handshakeTimeout, null); + return buildSingleChannelProfile(channelType, connectTimeout, handshakeTimeout, null, null); } /** @@ -119,7 +123,8 @@ public static ConnectionProfile buildSingleChannelProfile(TransportRequestOption * handshake timeouts and compression settings. */ public static ConnectionProfile buildSingleChannelProfile(TransportRequestOptions.Type channelType, @Nullable TimeValue connectTimeout, - @Nullable TimeValue handshakeTimeout, @Nullable Boolean compressionEnabled) { + @Nullable TimeValue handshakeTimeout, @Nullable TimeValue pingInterval, + @Nullable Boolean compressionEnabled) { Builder builder = new Builder(); builder.addConnections(1, channelType); final EnumSet otherTypes = EnumSet.allOf(TransportRequestOptions.Type.class); @@ -131,6 +136,9 @@ public static ConnectionProfile buildSingleChannelProfile(TransportRequestOption if (handshakeTimeout != null) { builder.setHandshakeTimeout(handshakeTimeout); } + if (pingInterval != null) { + builder.setPingInterval(pingInterval); + } if (compressionEnabled != null) { builder.setCompressionEnabled(compressionEnabled); } @@ -141,14 +149,16 @@ public static ConnectionProfile buildSingleChannelProfile(TransportRequestOption private final int numConnections; private final TimeValue connectTimeout; private final TimeValue handshakeTimeout; + private final TimeValue pingInterval; private final Boolean compressionEnabled; private ConnectionProfile(List handles, int numConnections, TimeValue connectTimeout, - TimeValue handshakeTimeout, Boolean compressionEnabled) { + TimeValue handshakeTimeout, TimeValue pingInterval, Boolean compressionEnabled) { this.handles = handles; this.numConnections = numConnections; this.connectTimeout = connectTimeout; this.handshakeTimeout = handshakeTimeout; + this.pingInterval = pingInterval; this.compressionEnabled = compressionEnabled; } @@ -159,9 +169,10 @@ public static class Builder { private final List handles = new ArrayList<>(); private final Set addedTypes = EnumSet.noneOf(TransportRequestOptions.Type.class); private int numConnections = 0; - private Boolean compressionEnabled; private TimeValue connectTimeout; private TimeValue handshakeTimeout; + private Boolean compressionEnabled; + private TimeValue pingInterval; /** create an empty builder */ public Builder() { @@ -175,32 +186,44 @@ public Builder(ConnectionProfile source) { connectTimeout = source.getConnectTimeout(); handshakeTimeout = source.getHandshakeTimeout(); compressionEnabled = source.getCompressionEnabled(); + pingInterval = source.getPingInterval(); } /** * Sets a connect timeout for this connection profile */ - public void setConnectTimeout(TimeValue connectTimeout) { + public Builder setConnectTimeout(TimeValue connectTimeout) { if (connectTimeout.millis() < 0) { throw new IllegalArgumentException("connectTimeout must be non-negative but was: " + connectTimeout); } this.connectTimeout = connectTimeout; + return this; } /** * Sets a handshake timeout for this connection profile */ - public void setHandshakeTimeout(TimeValue handshakeTimeout) { + public Builder setHandshakeTimeout(TimeValue handshakeTimeout) { if (handshakeTimeout.millis() < 0) { throw new IllegalArgumentException("handshakeTimeout must be non-negative but was: " + handshakeTimeout); } this.handshakeTimeout = handshakeTimeout; + return this; + } + + /** + * Sets a ping interval for this connection profile + */ + public Builder setPingInterval(TimeValue pingInterval) { + this.pingInterval = pingInterval; + return this; } /** * Sets compression enabled for this connection profile */ - public void setCompressionEnabled(boolean compressionEnabled) { + public Builder setCompressionEnabled(boolean compressionEnabled) { this.compressionEnabled = compressionEnabled; + return this; } /** @@ -208,7 +231,7 @@ public void setCompressionEnabled(boolean compressionEnabled) { * @param numConnections the number of connections to use in the pool for the given connection types * @param types a set of types that should share the given number of connections */ - public void addConnections(int numConnections, TransportRequestOptions.Type... types) { + public Builder addConnections(int numConnections, TransportRequestOptions.Type... types) { if (types == null || types.length == 0) { throw new IllegalArgumentException("types must not be null"); } @@ -220,6 +243,7 @@ public void addConnections(int numConnections, TransportRequestOptions.Type... t addedTypes.addAll(Arrays.asList(types)); handles.add(new ConnectionTypeHandle(this.numConnections, numConnections, EnumSet.copyOf(Arrays.asList(types)))); this.numConnections += numConnections; + return this; } /** @@ -233,7 +257,7 @@ public ConnectionProfile build() { throw new IllegalStateException("not all types are added for this connection profile - missing types: " + types); } return new ConnectionProfile(Collections.unmodifiableList(handles), numConnections, connectTimeout, handshakeTimeout, - compressionEnabled); + pingInterval, compressionEnabled); } } @@ -252,6 +276,13 @@ public TimeValue getHandshakeTimeout() { return handshakeTimeout; } + /** + * Returns the ping interval or null if no explicit ping interval is set on this profile. + */ + public TimeValue getPingInterval() { + return pingInterval; + } + /** * Returns boolean indicating if compression is enabled or null if no explicit compression * is set on this profile. diff --git a/server/src/main/java/org/elasticsearch/transport/RemoteClusterConnection.java b/server/src/main/java/org/elasticsearch/transport/RemoteClusterConnection.java index 69000ade292ea..39b732059885d 100644 --- a/server/src/main/java/org/elasticsearch/transport/RemoteClusterConnection.java +++ b/server/src/main/java/org/elasticsearch/transport/RemoteClusterConnection.java @@ -73,6 +73,7 @@ import java.util.stream.Collectors; import static org.elasticsearch.transport.RemoteClusterService.REMOTE_CLUSTER_COMPRESS; +import static org.elasticsearch.transport.RemoteClusterService.REMOTE_CLUSTER_PING_SCHEDULE; /** * Represents a connection to a single remote cluster. In contrast to a local cluster a remote cluster is not joined such that the @@ -93,10 +94,8 @@ final class RemoteClusterConnection implements TransportConnectionListener, Clos private final TransportService transportService; private final ConnectionManager connectionManager; - private final ConnectionProfile remoteProfile; private final ConnectedNodes connectedNodes; private final String clusterAlias; - private final boolean compress; private final int maxNumRemoteConnections; private final Predicate nodePredicate; private final ThreadPool threadPool; @@ -113,36 +112,32 @@ final class RemoteClusterConnection implements TransportConnectionListener, Clos * @param clusterAlias the configured alias of the cluster to connect to * @param seedNodes a list of seed nodes to discover eligible nodes from * @param transportService the local nodes transport service - * @param connectionManager the connection manager to use for this remote connection * @param maxNumRemoteConnections the maximum number of connections to the remote cluster * @param nodePredicate a predicate to filter eligible remote nodes to connect to * @param proxyAddress the proxy address */ RemoteClusterConnection(Settings settings, String clusterAlias, List> seedNodes, - TransportService transportService, ConnectionManager connectionManager, int maxNumRemoteConnections, - Predicate nodePredicate, String proxyAddress) { + TransportService transportService, int maxNumRemoteConnections, Predicate nodePredicate, + String proxyAddress) { + this(settings, clusterAlias, seedNodes, transportService, maxNumRemoteConnections, nodePredicate, proxyAddress, + createConnectionManager(settings, clusterAlias, transportService)); + } + + // Public for tests to pass a StubbableConnectionManager + RemoteClusterConnection(Settings settings, String clusterAlias, List> seedNodes, + TransportService transportService, int maxNumRemoteConnections, Predicate nodePredicate, + String proxyAddress, ConnectionManager connectionManager) { this.transportService = transportService; this.maxNumRemoteConnections = maxNumRemoteConnections; this.nodePredicate = nodePredicate; this.clusterAlias = clusterAlias; - this.compress = REMOTE_CLUSTER_COMPRESS.getConcreteSettingForNamespace(clusterAlias).get(settings); - ConnectionProfile.Builder builder = new ConnectionProfile.Builder(); - builder.setConnectTimeout(TransportService.TCP_CONNECT_TIMEOUT.get(settings)); - builder.setHandshakeTimeout(TransportService.TCP_CONNECT_TIMEOUT.get(settings)); - builder.addConnections(6, TransportRequestOptions.Type.REG, TransportRequestOptions.Type.PING); // TODO make this configurable? - builder.addConnections(0, // we don't want this to be used for anything else but search - TransportRequestOptions.Type.BULK, - TransportRequestOptions.Type.STATE, - TransportRequestOptions.Type.RECOVERY); - builder.setCompressionEnabled(compress); - remoteProfile = builder.build(); - connectedNodes = new ConnectedNodes(clusterAlias); + this.connectionManager = connectionManager; + this.connectedNodes = new ConnectedNodes(clusterAlias); this.seedNodes = Collections.unmodifiableList(seedNodes); this.skipUnavailable = RemoteClusterService.REMOTE_CLUSTER_SKIP_UNAVAILABLE - .getConcreteSettingForNamespace(clusterAlias).get(settings); + .getConcreteSettingForNamespace(clusterAlias).get(settings); this.connectHandler = new ConnectHandler(); this.threadPool = transportService.threadPool; - this.connectionManager = connectionManager; connectionManager.addListener(this); // we register the transport service here as a listener to make sure we notify handlers on disconnect etc. connectionManager.addListener(transportService); @@ -150,6 +145,7 @@ final class RemoteClusterConnection implements TransportConnectionListener, Clos initialConnectionTimeout = RemoteClusterService.REMOTE_INITIAL_CONNECTION_TIMEOUT_SETTING.get(settings); } + private static DiscoveryNode maybeAddProxyAddress(String proxyAddress, DiscoveryNode node) { if (proxyAddress == null || proxyAddress.isEmpty()) { return node; @@ -333,11 +329,6 @@ public void sendRequest(long requestId, String action, TransportRequest request, TransportActionProxy.wrapRequest(targetNode, request), options); } - @Override - public boolean sendPing() { - return proxyConnection.sendPing(); - } - @Override public void close() { assert false: "proxy connections must not be closed"; @@ -496,13 +487,13 @@ private void collectRemoteNodes(Iterator> seedNodes, fin logger.debug("[{}] opening connection to seed node: [{}] proxy address: [{}]", clusterAlias, seedNode, proxyAddress); final TransportService.HandshakeResponse handshakeResponse; - ConnectionProfile profile = ConnectionProfile.buildSingleChannelProfile(TransportRequestOptions.Type.REG, - compress); + ConnectionProfile profile = ConnectionProfile.buildSingleChannelProfile(TransportRequestOptions.Type.REG); Transport.Connection connection = manager.openConnection(seedNode, profile); boolean success = false; try { try { - handshakeResponse = transportService.handshake(connection, remoteProfile.getHandshakeTimeout().millis(), + ConnectionProfile connectionProfile = connectionManager.getConnectionProfile(); + handshakeResponse = transportService.handshake(connection, connectionProfile.getHandshakeTimeout().millis(), (c) -> remoteClusterName.get() == null ? true : c.equals(remoteClusterName.get())); } catch (IllegalStateException ex) { logger.warn(() -> new ParameterizedMessage("seed node {} cluster name mismatch expected " + @@ -512,7 +503,7 @@ private void collectRemoteNodes(Iterator> seedNodes, fin final DiscoveryNode handshakeNode = maybeAddProxyAddress(proxyAddress, handshakeResponse.getDiscoveryNode()); if (nodePredicate.test(handshakeNode) && connectedNodes.size() < maxNumRemoteConnections) { - manager.connectToNode(handshakeNode, remoteProfile, transportService.connectionValidator(handshakeNode)); + manager.connectToNode(handshakeNode, null, transportService.connectionValidator(handshakeNode)); if (remoteClusterName.get() == null) { assert handshakeResponse.getClusterName().value() != null; remoteClusterName.set(handshakeResponse.getClusterName()); @@ -626,7 +617,7 @@ public void handleResponse(ClusterStateResponse response) { DiscoveryNode node = maybeAddProxyAddress(proxyAddress, n); if (nodePredicate.test(node) && connectedNodes.size() < maxNumRemoteConnections) { try { - connectionManager.connectToNode(node, remoteProfile, + connectionManager.connectToNode(node, null, transportService.connectionValidator(node)); // noop if node is connected connectedNodes.add(node); } catch (ConnectTransportException | IllegalStateException ex) { @@ -822,6 +813,20 @@ private synchronized void ensureIteratorAvailable() { } } + private static ConnectionManager createConnectionManager(Settings settings, String clusterAlias, TransportService transportService) { + ConnectionProfile.Builder builder = new ConnectionProfile.Builder() + .setConnectTimeout(TransportService.TCP_CONNECT_TIMEOUT.get(settings)) + .setHandshakeTimeout(TransportService.TCP_CONNECT_TIMEOUT.get(settings)) + .addConnections(6, TransportRequestOptions.Type.REG, TransportRequestOptions.Type.PING) // TODO make this configurable? + // we don't want this to be used for anything else but search + .addConnections(0, TransportRequestOptions.Type.BULK, + TransportRequestOptions.Type.STATE, + TransportRequestOptions.Type.RECOVERY) + .setCompressionEnabled(REMOTE_CLUSTER_COMPRESS.getConcreteSettingForNamespace(clusterAlias).get(settings)) + .setPingInterval(REMOTE_CLUSTER_PING_SCHEDULE.getConcreteSettingForNamespace(clusterAlias).get(settings)); + return new ConnectionManager(builder.build(), transportService.transport, transportService.threadPool); + } + ConnectionManager getConnectionManager() { return connectionManager; } diff --git a/server/src/main/java/org/elasticsearch/transport/RemoteClusterService.java b/server/src/main/java/org/elasticsearch/transport/RemoteClusterService.java index c7310241aeb8b..a68c225409dcb 100644 --- a/server/src/main/java/org/elasticsearch/transport/RemoteClusterService.java +++ b/server/src/main/java/org/elasticsearch/transport/RemoteClusterService.java @@ -223,11 +223,8 @@ private synchronized void updateRemoteClusters(Map listener); + + /** + * Returns stats about this channel + */ + ChannelStats getChannelStats(); + + class ChannelStats { + + private volatile long lastAccessedTime; + + public ChannelStats() { + lastAccessedTime = TimeValue.nsecToMSec(System.nanoTime()); + } + + void markAccessed(long relativeMillisTime) { + lastAccessedTime = relativeMillisTime; + } + + long lastAccessedTime() { + return lastAccessedTime; + } + } } diff --git a/server/src/main/java/org/elasticsearch/transport/TcpTransport.java b/server/src/main/java/org/elasticsearch/transport/TcpTransport.java index ef83498e7e5aa..04b3d79352f28 100644 --- a/server/src/main/java/org/elasticsearch/transport/TcpTransport.java +++ b/server/src/main/java/org/elasticsearch/transport/TcpTransport.java @@ -46,7 +46,6 @@ import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.io.stream.ReleasableBytesStreamOutput; import org.elasticsearch.common.io.stream.StreamInput; -import org.elasticsearch.common.metrics.CounterMetric; import org.elasticsearch.common.metrics.MeanMetric; import org.elasticsearch.common.network.CloseableChannel; import org.elasticsearch.common.network.NetworkAddress; @@ -61,6 +60,7 @@ import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.util.BigArrays; import org.elasticsearch.common.util.concurrent.AbstractRunnable; +import org.elasticsearch.common.util.concurrent.ConcurrentCollections; import org.elasticsearch.common.util.concurrent.CountDown; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.core.internal.io.IOUtils; @@ -91,7 +91,6 @@ import java.util.Objects; import java.util.Set; import java.util.TreeSet; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; @@ -168,9 +167,6 @@ public abstract class TcpTransport extends AbstractLifecycleComponent implements // This is the number of bytes necessary to read the message size private static final int BYTES_NEEDED_FOR_MESSAGE_SIZE = TcpHeader.MARKER_BYTES_SIZE + TcpHeader.MESSAGE_LENGTH_SIZE; - private static final int PING_DATA_SIZE = -1; - protected final CounterMetric successfulPings = new CounterMetric(); - protected final CounterMetric failedPings = new CounterMetric(); private static final long NINETY_PER_HEAP_SIZE = (long) (JvmInfo.jvmInfo().getMem().getHeapMax().getBytes() * 0.9); private static final BytesReference EMPTY_BYTES_REFERENCE = new BytesArray(new byte[0]); @@ -190,14 +186,14 @@ public abstract class TcpTransport extends AbstractLifecycleComponent implements private final ConcurrentMap profileBoundAddresses = newConcurrentMap(); private final Map> serverChannels = newConcurrentMap(); - private final Set acceptedChannels = Collections.newSetFromMap(new ConcurrentHashMap<>()); + private final Set acceptedChannels = ConcurrentCollections.newConcurrentSet(); private final NamedWriteableRegistry namedWriteableRegistry; // this lock is here to make sure we close this transport and disconnect all the client nodes // connections while no connect operations is going on private final ReadWriteLock closeLock = new ReentrantReadWriteLock(); - protected final boolean compressResponses; + private final boolean compressResponses; private volatile BoundTransportAddress boundAddress; private final String transportName; @@ -205,8 +201,9 @@ public abstract class TcpTransport extends AbstractLifecycleComponent implements private final MeanMetric transmittedBytesMetric = new MeanMetric(); private volatile Map> requestHandlers = Collections.emptyMap(); private final ResponseHandlers responseHandlers = new ResponseHandlers(); + private final TcpTransportHandshaker handshaker; - private final BytesReference pingMessage; + private final TransportKeepAlive keepAlive; private final String nodeName; public TcpTransport(String transportName, Settings settings, Version version, ThreadPool threadPool, BigArrays bigArrays, @@ -229,6 +226,7 @@ public TcpTransport(String transportName, Settings settings, Version version, T TransportStatus.setHandshake((byte) 0)), (v, features, channel, response, requestId) -> sendResponse(v, features, channel, response, requestId, TcpTransportHandshaker.HANDSHAKE_ACTION_NAME, TransportResponseOptions.EMPTY, TransportStatus.setHandshake((byte) 0))); + this.keepAlive = new TransportKeepAlive(threadPool, this::internalSendMessage); this.nodeName = Node.NODE_NAME_SETTING.get(settings); final Settings defaultFeatures = DEFAULT_FEATURES_SETTING.get(settings); @@ -243,15 +241,6 @@ public TcpTransport(String transportName, Settings settings, Version version, T // use a sorted set to present the features in a consistent order this.features = new TreeSet<>(defaultFeatures.names()).toArray(new String[defaultFeatures.names().size()]); } - - try (BytesStreamOutput out = new BytesStreamOutput()) { - out.writeByte((byte) 'E'); - out.writeByte((byte) 'S'); - out.writeInt(TcpTransport.PING_DATA_SIZE); - pingMessage = out.bytes(); - } catch (IOException e) { - throw new AssertionError(e.getMessage(), e); // won't happen - } } @Override @@ -319,31 +308,6 @@ public TcpChannel channel(TransportRequestOptions.Type type) { return connectionTypeHandle.getChannel(channels); } - @Override - public boolean sendPing() { - for (TcpChannel channel : channels) { - internalSendMessage(channel, pingMessage, new SendMetricListener(pingMessage.length()) { - @Override - protected void innerInnerOnResponse(Void v) { - successfulPings.inc(); - } - - @Override - protected void innerOnFailure(Exception e) { - if (channel.isOpen()) { - logger.debug(() -> new ParameterizedMessage("[{}] failed to send ping transport message", node), e); - failedPings.inc(); - } else { - logger.trace(() -> - new ParameterizedMessage("[{}] failed to send ping transport message (channel closed)", node), e); - } - - } - }); - } - return true; - } - @Override public void close() { if (isClosing.compareAndSet(false, true)) { @@ -498,7 +462,7 @@ protected void bindServer(ProfileSettings profileSettings) { } } - protected InetSocketAddress bindToPort(final String name, final InetAddress hostAddress, String port) { + private InetSocketAddress bindToPort(final String name, final InetAddress hostAddress, String port) { PortsRange portsRange = new PortsRange(port); final AtomicReference lastException = new AtomicReference<>(); final AtomicReference boundSocket = new AtomicReference<>(); @@ -670,6 +634,8 @@ protected final void doStop() { threadPool.generic().execute(() -> { closeLock.writeLock().lock(); try { + keepAlive.close(); + // first stop to accept any incoming connections so nobody can connect to this transport for (Map.Entry> entry : serverChannels.entrySet()) { String profile = entry.getKey(); @@ -729,14 +695,14 @@ public void onException(TcpChannel channel, Exception e) { // in case we are able to return data, serialize the exception content and sent it back to the client if (channel.isOpen()) { BytesArray message = new BytesArray(e.getMessage().getBytes(StandardCharsets.UTF_8)); - final SendMetricListener listener = new SendMetricListener(message.length()) { + ActionListener listener = new ActionListener() { @Override - protected void innerInnerOnResponse(Void v) { + public void onResponse(Void aVoid) { CloseableChannel.closeChannel(channel); } @Override - protected void innerOnFailure(Exception e) { + public void onFailure(Exception e) { logger.debug("failed to send message to httpOnTransport channel", e); CloseableChannel.closeChannel(channel); } @@ -745,7 +711,7 @@ protected void innerOnFailure(Exception e) { // elasticsearch binary message. We are just serializing an exception here. Not formatting it // as an elasticsearch transport message. try { - channel.sendMessage(message, listener); + channel.sendMessage(message, new SendListener(channel, message.length(), listener)); } catch (Exception ex) { listener.onFailure(ex); } @@ -774,6 +740,8 @@ protected void onNonChannelException(Exception exception) { protected void serverAcceptedChannel(TcpChannel channel) { boolean addedOnThisCall = acceptedChannels.add(channel); assert addedOnThisCall : "Channel should only be added to accepted channel set once"; + // Mark the channel init time + channel.getChannelStats().markAccessed(threadPool.relativeTimeInMillis()); channel.addCloseListener(ActionListener.wrap(() -> acceptedChannels.remove(channel))); logger.trace(() -> new ParameterizedMessage("Tcp transport channel accepted: {}", channel)); } @@ -835,9 +803,9 @@ private void sendRequestToChannel(final DiscoveryNode node, final TcpChannel cha BytesReference message = buildMessage(requestId, status, node.getVersion(), request, stream); final TransportRequestOptions finalOptions = options; // this might be called in a different thread - SendListener onRequestSent = new SendListener(channel, stream, - () -> messageListener.onRequestSent(node, requestId, action, request, finalOptions), message.length()); - internalSendMessage(channel, message, onRequestSent); + ReleaseListener releaseListener = new ReleaseListener(stream, + () -> messageListener.onRequestSent(node, requestId, action, request, finalOptions)); + internalSendMessage(channel, message, releaseListener); addedReleaseListener = true; } finally { if (!addedReleaseListener) { @@ -849,9 +817,10 @@ private void sendRequestToChannel(final DiscoveryNode node, final TcpChannel cha /** * sends a message to the given channel, using the given callbacks. */ - private void internalSendMessage(TcpChannel channel, BytesReference message, SendMetricListener listener) { + private void internalSendMessage(TcpChannel channel, BytesReference message, ActionListener listener) { + channel.getChannelStats().markAccessed(threadPool.relativeTimeInMillis()); try { - channel.sendMessage(message, listener); + channel.sendMessage(message, new SendListener(channel, message.length(), listener)); } catch (Exception ex) { // call listener to ensure that any resources are released listener.onFailure(ex); @@ -889,9 +858,9 @@ public void sendErrorResponse( final BytesReference bytes = stream.bytes(); final BytesReference header = buildHeader(requestId, status, nodeVersion, bytes.length()); CompositeBytesReference message = new CompositeBytesReference(header, bytes); - SendListener onResponseSent = new SendListener(channel, null, - () -> messageListener.onResponseSent(requestId, action, error), message.length()); - internalSendMessage(channel, message, onResponseSent); + ReleaseListener releaseListener = new ReleaseListener(null, + () -> messageListener.onResponseSent(requestId, action, error)); + internalSendMessage(channel, message, releaseListener); } } @@ -939,9 +908,9 @@ private void sendResponse( final TransportResponseOptions finalOptions = options; // this might be called in a different thread - SendListener listener = new SendListener(channel, stream, - () -> messageListener.onResponseSent(requestId, action, response, finalOptions), message.length()); - internalSendMessage(channel, message, listener); + ReleaseListener releaseListener = new ReleaseListener(stream, + () -> messageListener.onResponseSent(requestId, action, response, finalOptions)); + internalSendMessage(channel, message, releaseListener); addedReleaseListener = true; } finally { if (!addedReleaseListener) { @@ -1003,9 +972,12 @@ private BytesReference buildMessage(long requestId, byte status, Version nodeVer */ public void inboundMessage(TcpChannel channel, BytesReference message) { try { + channel.getChannelStats().markAccessed(threadPool.relativeTimeInMillis()); // Message length of 0 is a ping if (message.length() != 0) { messageReceived(message, channel); + } else { + keepAlive.receiveKeepAlive(channel); } } catch (Exception e) { onException(channel, e); @@ -1099,7 +1071,7 @@ private static int readHeaderBuffer(BytesReference headerBuffer) throws IOExcept messageLength = input.readInt(); } - if (messageLength == TcpTransport.PING_DATA_SIZE) { + if (messageLength == TransportKeepAlive.PING_DATA_SIZE) { // This is a ping return 0; } @@ -1405,6 +1377,10 @@ public void executeHandshake(DiscoveryNode node, TcpChannel channel, ConnectionP handshaker.sendHandshake(responseHandlers.newRequestId(), node, channel, profile.getHandshakeTimeout(), listener); } + final TransportKeepAlive getKeepAlive() { + return keepAlive; + } + final int getNumPendingHandshakes() { return handshaker.getNumPendingHandshakes(); } @@ -1427,42 +1403,48 @@ protected final void ensureOpen() { /** * This listener increments the transmitted bytes metric on success. */ - private abstract class SendMetricListener extends NotifyOnceListener { + private class SendListener extends NotifyOnceListener { + + private final TcpChannel channel; private final long messageSize; + private final ActionListener delegateListener; - private SendMetricListener(long messageSize) { + private SendListener(TcpChannel channel, long messageSize, ActionListener delegateListener) { + this.channel = channel; this.messageSize = messageSize; + this.delegateListener = delegateListener; } @Override - protected final void innerOnResponse(Void object) { + protected void innerOnResponse(Void v) { transmittedBytesMetric.inc(messageSize); - innerInnerOnResponse(object); + delegateListener.onResponse(v); } - protected abstract void innerInnerOnResponse(Void object); + @Override + protected void innerOnFailure(Exception e) { + logger.warn(() -> new ParameterizedMessage("send message failed [channel: {}]", channel), e); + delegateListener.onFailure(e); + } } - private final class SendListener extends SendMetricListener { - private final TcpChannel channel; + private class ReleaseListener implements ActionListener { + private final Closeable optionalCloseable; private final Runnable transportAdaptorCallback; - private SendListener(TcpChannel channel, Closeable optionalCloseable, Runnable transportAdaptorCallback, long messageLength) { - super(messageLength); - this.channel = channel; + private ReleaseListener(Closeable optionalCloseable, Runnable transportAdaptorCallback) { this.optionalCloseable = optionalCloseable; this.transportAdaptorCallback = transportAdaptorCallback; } @Override - protected void innerInnerOnResponse(Void v) { + public void onResponse(Void aVoid) { closeAndCallback(null); } @Override - protected void innerOnFailure(Exception e) { - logger.warn(() -> new ParameterizedMessage("send message failed [channel: {}]", channel), e); + public void onFailure(Exception e) { closeAndCallback(e); } @@ -1618,7 +1600,13 @@ public void onResponse(Void v) { @Override public void onResponse(Version version) { NodeChannels nodeChannels = new NodeChannels(node, channels, connectionProfile, version); - nodeChannels.channels.forEach(ch -> ch.addCloseListener(ActionListener.wrap(nodeChannels::close))); + long relativeMillisTime = threadPool.relativeTimeInMillis(); + nodeChannels.channels.forEach(ch -> { + // Mark the channel init time + ch.getChannelStats().markAccessed(relativeMillisTime); + ch.addCloseListener(ActionListener.wrap(nodeChannels::close)); + }); + keepAlive.registerNodeConnection(nodeChannels.channels, connectionProfile); listener.onResponse(nodeChannels); } diff --git a/server/src/main/java/org/elasticsearch/transport/Transport.java b/server/src/main/java/org/elasticsearch/transport/Transport.java index e13213dca066a..011c3214dfbef 100644 --- a/server/src/main/java/org/elasticsearch/transport/Transport.java +++ b/server/src/main/java/org/elasticsearch/transport/Transport.java @@ -115,10 +115,6 @@ interface Connection extends Closeable { void sendRequest(long requestId, String action, TransportRequest request, TransportRequestOptions options) throws IOException, TransportException; - default boolean sendPing() { - return false; - } - /** * The listener's {@link ActionListener#onResponse(Object)} method will be called when this * connection is closed. No implementations currently throw an exception during close, so diff --git a/server/src/main/java/org/elasticsearch/transport/TransportKeepAlive.java b/server/src/main/java/org/elasticsearch/transport/TransportKeepAlive.java new file mode 100644 index 0000000000000..b8d06e7e1174e --- /dev/null +++ b/server/src/main/java/org/elasticsearch/transport/TransportKeepAlive.java @@ -0,0 +1,210 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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.elasticsearch.transport; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.message.ParameterizedMessage; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.common.AsyncBiFunction; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.component.Lifecycle; +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.common.metrics.CounterMetric; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.util.concurrent.AbstractLifecycleRunnable; +import org.elasticsearch.common.util.concurrent.ConcurrentCollections; +import org.elasticsearch.common.util.concurrent.EsRejectedExecutionException; +import org.elasticsearch.threadpool.ThreadPool; + +import java.io.Closeable; +import java.io.IOException; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Implements the scheduling and sending of keep alive pings. Client channels send keep alive pings to the + * server and server channels respond. Pings are only sent at the scheduled time if the channel did not send + * and receive a message since the last ping. + */ +final class TransportKeepAlive implements Closeable { + + static final int PING_DATA_SIZE = -1; + + private final Logger logger = LogManager.getLogger(TransportKeepAlive.class); + private final CounterMetric successfulPings = new CounterMetric(); + private final CounterMetric failedPings = new CounterMetric(); + private final ConcurrentMap pingIntervals = ConcurrentCollections.newConcurrentMap(); + private final Lifecycle lifecycle = new Lifecycle(); + private final ThreadPool threadPool; + private final AsyncBiFunction pingSender; + private final BytesReference pingMessage; + + TransportKeepAlive(ThreadPool threadPool, AsyncBiFunction pingSender) { + this.threadPool = threadPool; + this.pingSender = pingSender; + + try (BytesStreamOutput out = new BytesStreamOutput()) { + out.writeByte((byte) 'E'); + out.writeByte((byte) 'S'); + out.writeInt(PING_DATA_SIZE); + pingMessage = out.bytes(); + } catch (IOException e) { + throw new AssertionError(e.getMessage(), e); // won't happen + } + + this.lifecycle.moveToStarted(); + } + + void registerNodeConnection(List nodeChannels, ConnectionProfile connectionProfile) { + TimeValue pingInterval = connectionProfile.getPingInterval(); + if (pingInterval.millis() < 0) { + return; + } + + final ScheduledPing scheduledPing = pingIntervals.computeIfAbsent(pingInterval, ScheduledPing::new); + scheduledPing.ensureStarted(); + + for (TcpChannel channel : nodeChannels) { + scheduledPing.addChannel(channel); + + channel.addCloseListener(ActionListener.wrap(() -> { + scheduledPing.removeChannel(channel); + })); + } + } + + /** + * Called when a keep alive ping is received. If the channel that received the keep alive ping is a + * server channel, a ping is sent back. If the channel that received the keep alive is a client channel, + * this method does nothing as the client initiated the ping in the first place. + * + * @param channel that received the keep alive ping + */ + void receiveKeepAlive(TcpChannel channel) { + // The client-side initiates pings and the server-side responds. So if this is a client channel, this + // method is a no-op. + if (channel.isServerChannel()) { + sendPing(channel); + } + } + + long successfulPingCount() { + return successfulPings.count(); + } + + long failedPingCount() { + return failedPings.count(); + } + + private void sendPing(TcpChannel channel) { + pingSender.apply(channel, pingMessage, new ActionListener() { + + @Override + public void onResponse(Void v) { + successfulPings.inc(); + } + + @Override + public void onFailure(Exception e) { + if (channel.isOpen()) { + logger.debug(() -> new ParameterizedMessage("[{}] failed to send transport ping", channel), e); + failedPings.inc(); + } else { + logger.trace(() -> new ParameterizedMessage("[{}] failed to send transport ping (channel closed)", channel), e); + } + } + }); + } + + @Override + public void close() { + lifecycle.moveToStopped(); + lifecycle.moveToClosed(); + } + + private class ScheduledPing extends AbstractLifecycleRunnable { + + private final TimeValue pingInterval; + + private final Set channels = ConcurrentCollections.newConcurrentSet(); + + private final AtomicBoolean isStarted = new AtomicBoolean(false); + private volatile long lastPingRelativeMillis; + + private ScheduledPing(TimeValue pingInterval) { + super(lifecycle, logger); + this.pingInterval = pingInterval; + this.lastPingRelativeMillis = threadPool.relativeTimeInMillis(); + } + + void ensureStarted() { + if (isStarted.get() == false && isStarted.compareAndSet(false, true)) { + threadPool.schedule(pingInterval, ThreadPool.Names.GENERIC, this); + } + } + + void addChannel(TcpChannel channel) { + channels.add(channel); + } + + void removeChannel(TcpChannel channel) { + channels.remove(channel); + } + + @Override + protected void doRunInLifecycle() { + for (TcpChannel channel : channels) { + // In the future it is possible that we may want to kill a channel if we have not read from + // the channel since the last ping. However, this will need to be backwards compatible with + // pre-6.6 nodes that DO NOT respond to pings + if (needsKeepAlivePing(channel)) { + sendPing(channel); + } + } + this.lastPingRelativeMillis = threadPool.relativeTimeInMillis(); + } + + @Override + protected void onAfterInLifecycle() { + try { + threadPool.schedule(pingInterval, ThreadPool.Names.GENERIC, this); + } catch (EsRejectedExecutionException ex) { + if (ex.isExecutorShutdown()) { + logger.debug("couldn't schedule new ping execution, executor is shutting down", ex); + } else { + throw ex; + } + } + } + + @Override + public void onFailure(Exception e) { + logger.warn("failed to send ping transport message", e); + } + + private boolean needsKeepAlivePing(TcpChannel channel) { + TcpChannel.ChannelStats stats = channel.getChannelStats(); + long accessedDelta = stats.lastAccessedTime() - lastPingRelativeMillis; + return accessedDelta <= 0; + } + } +} diff --git a/server/src/test/java/org/elasticsearch/transport/ConnectionManagerTests.java b/server/src/test/java/org/elasticsearch/transport/ConnectionManagerTests.java index 03ecf65737d58..976f0e905c050 100644 --- a/server/src/test/java/org/elasticsearch/transport/ConnectionManagerTests.java +++ b/server/src/test/java/org/elasticsearch/transport/ConnectionManagerTests.java @@ -54,7 +54,9 @@ public void createConnectionManager() { transport = mock(Transport.class); connectionManager = new ConnectionManager(settings, transport, threadPool); TimeValue oneSecond = new TimeValue(1000); - connectionProfile = ConnectionProfile.buildSingleChannelProfile(TransportRequestOptions.Type.REG, oneSecond, oneSecond, false); + TimeValue oneMinute = TimeValue.timeValueMinutes(1); + connectionProfile = ConnectionProfile.buildSingleChannelProfile(TransportRequestOptions.Type.REG, oneSecond, oneSecond, + oneMinute, false); } @After diff --git a/server/src/test/java/org/elasticsearch/transport/ConnectionProfileTests.java b/server/src/test/java/org/elasticsearch/transport/ConnectionProfileTests.java index 8d053f7ade630..4f380de08ed1c 100644 --- a/server/src/test/java/org/elasticsearch/transport/ConnectionProfileTests.java +++ b/server/src/test/java/org/elasticsearch/transport/ConnectionProfileTests.java @@ -36,6 +36,7 @@ public void testBuildConnectionProfile() { ConnectionProfile.Builder builder = new ConnectionProfile.Builder(); TimeValue connectTimeout = TimeValue.timeValueMillis(randomIntBetween(1, 10)); TimeValue handshakeTimeout = TimeValue.timeValueMillis(randomIntBetween(1, 10)); + TimeValue pingInterval = TimeValue.timeValueMillis(randomIntBetween(1, 10)); boolean compressionEnabled = randomBoolean(); final boolean setConnectTimeout = randomBoolean(); if (setConnectTimeout) { @@ -49,6 +50,10 @@ public void testBuildConnectionProfile() { if (setCompress) { builder.setCompressionEnabled(compressionEnabled); } + final boolean setPingInterval = randomBoolean(); + if (setPingInterval) { + builder.setPingInterval(pingInterval); + } builder.addConnections(1, TransportRequestOptions.Type.BULK); builder.addConnections(2, TransportRequestOptions.Type.STATE, TransportRequestOptions.Type.RECOVERY); builder.addConnections(3, TransportRequestOptions.Type.PING); @@ -82,6 +87,12 @@ public void testBuildConnectionProfile() { assertNull(build.getCompressionEnabled()); } + if (setPingInterval) { + assertEquals(pingInterval, build.getPingInterval()); + } else { + assertNull(build.getPingInterval()); + } + List list = new ArrayList<>(10); for (int i = 0; i < 10; i++) { list.add(i); @@ -160,7 +171,10 @@ public void testConnectionProfileResolve() { if (connectionHandshakeSet) { builder.setHandshakeTimeout(TimeValue.timeValueMillis(randomNonNegativeLong())); } - + final boolean pingIntervalSet = randomBoolean(); + if (pingIntervalSet) { + builder.setPingInterval(TimeValue.timeValueMillis(randomNonNegativeLong())); + } final boolean connectionCompressSet = randomBoolean(); if (connectionCompressSet) { builder.setCompressionEnabled(randomBoolean()); @@ -176,6 +190,8 @@ public void testConnectionProfileResolve() { equalTo(connectionTimeoutSet ? profile.getConnectTimeout() : defaultProfile.getConnectTimeout())); assertThat(resolved.getHandshakeTimeout(), equalTo(connectionHandshakeSet ? profile.getHandshakeTimeout() : defaultProfile.getHandshakeTimeout())); + assertThat(resolved.getPingInterval(), + equalTo(pingIntervalSet ? profile.getPingInterval() : defaultProfile.getPingInterval())); assertThat(resolved.getCompressionEnabled(), equalTo(connectionCompressSet ? profile.getCompressionEnabled() : defaultProfile.getCompressionEnabled())); } @@ -191,6 +207,7 @@ public void testDefaultConnectionProfile() { assertEquals(TransportService.TCP_CONNECT_TIMEOUT.get(Settings.EMPTY), profile.getConnectTimeout()); assertEquals(TransportService.TCP_CONNECT_TIMEOUT.get(Settings.EMPTY), profile.getHandshakeTimeout()); assertEquals(Transport.TRANSPORT_TCP_COMPRESS.get(Settings.EMPTY), profile.getCompressionEnabled()); + assertEquals(TcpTransport.PING_SCHEDULE.get(Settings.EMPTY), profile.getPingInterval()); profile = ConnectionProfile.buildDefaultConnectionProfile(Settings.builder().put("node.master", false).build()); assertEquals(12, profile.getNumConnections()); diff --git a/server/src/test/java/org/elasticsearch/transport/RemoteClusterConnectionTests.java b/server/src/test/java/org/elasticsearch/transport/RemoteClusterConnectionTests.java index 66ab81121072e..a4f81a659c60c 100644 --- a/server/src/test/java/org/elasticsearch/transport/RemoteClusterConnectionTests.java +++ b/server/src/test/java/org/elasticsearch/transport/RemoteClusterConnectionTests.java @@ -59,6 +59,7 @@ import org.elasticsearch.test.VersionUtils; import org.elasticsearch.test.junit.annotations.TestLogging; import org.elasticsearch.test.transport.MockTransportService; +import org.elasticsearch.test.transport.StubbableConnectionManager; import org.elasticsearch.test.transport.StubbableTransport; import org.elasticsearch.threadpool.TestThreadPool; import org.elasticsearch.threadpool.ThreadPool; @@ -170,10 +171,11 @@ public void testRemoteProfileIsUsedForLocalCluster() throws Exception { service.start(); service.acceptIncomingRequests(); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - Arrays.asList(() -> seedNode), service, service.getConnectionManager(), Integer.MAX_VALUE, n -> true, null)) { + Arrays.asList(() -> seedNode), service, Integer.MAX_VALUE, n -> true, null)) { + ConnectionManager connectionManager = connection.getConnectionManager(); updateSeedNodes(connection, Arrays.asList(() -> seedNode)); - assertTrue(service.nodeConnected(seedNode)); - assertTrue(service.nodeConnected(discoverableNode)); + assertTrue(connectionManager.nodeConnected(seedNode)); + assertTrue(connectionManager.nodeConnected(discoverableNode)); assertTrue(connection.assertNoRunningConnections()); PlainTransportFuture futureHandler = new PlainTransportFuture<>( new FutureTransportResponseHandler() { @@ -185,7 +187,7 @@ public ClusterSearchShardsResponse read(StreamInput in) throws IOException { TransportRequestOptions options = TransportRequestOptions.builder().withType(TransportRequestOptions.Type.BULK) .build(); IllegalStateException ise = (IllegalStateException) expectThrows(SendRequestTransportException.class, () -> { - service.sendRequest(discoverableNode, + service.sendRequest(connectionManager.getConnection(discoverableNode), ClusterSearchShardsAction.NAME, new ClusterSearchShardsRequest(), options, futureHandler); futureHandler.txGet(); }).getCause(); @@ -211,10 +213,11 @@ public void testRemoteProfileIsUsedForRemoteCluster() throws Exception { service.start(); service.acceptIncomingRequests(); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - Arrays.asList(() -> seedNode), service, service.getConnectionManager(), Integer.MAX_VALUE, n -> true, null)) { + Arrays.asList(() -> seedNode), service, Integer.MAX_VALUE, n -> true, null)) { + ConnectionManager connectionManager = connection.getConnectionManager(); updateSeedNodes(connection, Arrays.asList(() -> seedNode)); - assertTrue(service.nodeConnected(seedNode)); - assertTrue(service.nodeConnected(discoverableNode)); + assertTrue(connectionManager.nodeConnected(seedNode)); + assertTrue(connectionManager.nodeConnected(discoverableNode)); assertTrue(connection.assertNoRunningConnections()); PlainTransportFuture futureHandler = new PlainTransportFuture<>( new FutureTransportResponseHandler() { @@ -226,7 +229,7 @@ public ClusterSearchShardsResponse read(StreamInput in) throws IOException { TransportRequestOptions options = TransportRequestOptions.builder().withType(TransportRequestOptions.Type.BULK) .build(); IllegalStateException ise = (IllegalStateException) expectThrows(SendRequestTransportException.class, () -> { - service.sendRequest(discoverableNode, + service.sendRequest(connectionManager.getConnection(discoverableNode), ClusterSearchShardsAction.NAME, new ClusterSearchShardsRequest(), options, futureHandler); futureHandler.txGet(); }).getCause(); @@ -263,10 +266,11 @@ public void testDiscoverSingleNode() throws Exception { service.start(); service.acceptIncomingRequests(); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - Arrays.asList(() -> seedNode), service, service.getConnectionManager(), Integer.MAX_VALUE, n -> true, null)) { + Arrays.asList(() -> seedNode), service, Integer.MAX_VALUE, n -> true, null)) { + ConnectionManager connectionManager = connection.getConnectionManager(); updateSeedNodes(connection, Arrays.asList(() -> seedNode)); - assertTrue(service.nodeConnected(seedNode)); - assertTrue(service.nodeConnected(discoverableNode)); + assertTrue(connectionManager.nodeConnected(seedNode)); + assertTrue(connectionManager.nodeConnected(discoverableNode)); assertTrue(connection.assertNoRunningConnections()); } } @@ -292,11 +296,12 @@ public void testDiscoverSingleNodeWithIncompatibleSeed() throws Exception { service.start(); service.acceptIncomingRequests(); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - seedNodes, service, service.getConnectionManager(), Integer.MAX_VALUE, n -> true, null)) { + seedNodes, service, Integer.MAX_VALUE, n -> true, null)) { + ConnectionManager connectionManager = connection.getConnectionManager(); updateSeedNodes(connection, seedNodes); - assertTrue(service.nodeConnected(seedNode)); - assertTrue(service.nodeConnected(discoverableNode)); - assertFalse(service.nodeConnected(incompatibleSeedNode)); + assertTrue(connectionManager.nodeConnected(seedNode)); + assertTrue(connectionManager.nodeConnected(discoverableNode)); + assertFalse(connectionManager.nodeConnected(incompatibleSeedNode)); assertTrue(connection.assertNoRunningConnections()); } } @@ -319,15 +324,16 @@ public void testNodeDisconnected() throws Exception { service.start(); service.acceptIncomingRequests(); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - Arrays.asList(() -> seedNode), service, service.getConnectionManager(), Integer.MAX_VALUE, n -> true, null)) { + Arrays.asList(() -> seedNode), service, Integer.MAX_VALUE, n -> true, null)) { + ConnectionManager connectionManager = connection.getConnectionManager(); updateSeedNodes(connection, Arrays.asList(() -> seedNode)); - assertTrue(service.nodeConnected(seedNode)); - assertTrue(service.nodeConnected(discoverableNode)); - assertFalse(service.nodeConnected(spareNode)); + assertTrue(connectionManager.nodeConnected(seedNode)); + assertTrue(connectionManager.nodeConnected(discoverableNode)); + assertFalse(connectionManager.nodeConnected(spareNode)); knownNodes.add(spareNode); CountDownLatch latchDisconnect = new CountDownLatch(1); CountDownLatch latchConnected = new CountDownLatch(1); - service.addConnectionListener(new TransportConnectionListener() { + connectionManager.addListener(new TransportConnectionListener() { @Override public void onNodeDisconnected(DiscoveryNode node) { if (node.equals(discoverableNode)) { @@ -347,7 +353,7 @@ public void onNodeConnected(DiscoveryNode node) { // now make sure we try to connect again to other nodes once we got disconnected assertTrue(latchDisconnect.await(10, TimeUnit.SECONDS)); assertTrue(latchConnected.await(10, TimeUnit.SECONDS)); - assertTrue(service.nodeConnected(spareNode)); + assertTrue(connectionManager.nodeConnected(spareNode)); } } } @@ -368,15 +374,15 @@ public void testFilterDiscoveredNodes() throws Exception { service.start(); service.acceptIncomingRequests(); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - Arrays.asList(() -> seedNode), service, service.getConnectionManager(), Integer.MAX_VALUE, - n -> n.equals(rejectedNode) == false, null)) { + Arrays.asList(() -> seedNode), service, Integer.MAX_VALUE, n -> n.equals(rejectedNode) == false, null)) { + ConnectionManager connectionManager = connection.getConnectionManager(); updateSeedNodes(connection, Arrays.asList(() -> seedNode)); if (rejectedNode.equals(seedNode)) { - assertFalse(service.nodeConnected(seedNode)); - assertTrue(service.nodeConnected(discoverableNode)); + assertFalse(connectionManager.nodeConnected(seedNode)); + assertTrue(connectionManager.nodeConnected(discoverableNode)); } else { - assertTrue(service.nodeConnected(seedNode)); - assertFalse(service.nodeConnected(discoverableNode)); + assertTrue(connectionManager.nodeConnected(seedNode)); + assertFalse(connectionManager.nodeConnected(discoverableNode)); } assertTrue(connection.assertNoRunningConnections()); } @@ -412,9 +418,10 @@ public void testConnectWithIncompatibleTransports() throws Exception { service.start(); service.acceptIncomingRequests(); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - Arrays.asList(() -> seedNode), service, service.getConnectionManager(), Integer.MAX_VALUE, n -> true, null)) { + Arrays.asList(() -> seedNode), service, Integer.MAX_VALUE, n -> true, null)) { + ConnectionManager connectionManager = connection.getConnectionManager(); expectThrows(Exception.class, () -> updateSeedNodes(connection, Arrays.asList(() -> seedNode))); - assertFalse(service.nodeConnected(seedNode)); + assertFalse(connectionManager.nodeConnected(seedNode)); assertTrue(connection.assertNoRunningConnections()); } } @@ -437,7 +444,7 @@ public void testRemoteConnectionVersionMatchesTransportConnectionVersion() throw assertThat(seedNode.getVersion(), not(equalTo(oldVersionNode.getVersion()))); try (MockTransportService service = MockTransportService.createNewService(Settings.EMPTY, Version.CURRENT, threadPool, null)) { - final Transport.Connection seedConnection = new Transport.Connection() { + final Transport.Connection seedConnection = new CloseableConnection() { @Override public DiscoveryNode getNode() { return seedNode; @@ -448,34 +455,23 @@ public void sendRequest(long requestId, String action, TransportRequest request, throws IOException, TransportException { // no-op } - - @Override - public void addCloseListener(ActionListener listener) { - // no-op - } - - @Override - public boolean isClosed() { - return false; - } - - @Override - public void close() { - // no-op - } }; - service.addGetConnectionBehavior(seedNode.getAddress(), (connectionManager, discoveryNode) -> { + ConnectionManager delegate = new ConnectionManager(Settings.EMPTY, service.transport, threadPool); + StubbableConnectionManager connectionManager = new StubbableConnectionManager(delegate, Settings.EMPTY, service.transport, + threadPool); + + connectionManager.addConnectBehavior(seedNode.getAddress(), (cm, discoveryNode) -> { if (discoveryNode == seedNode) { return seedConnection; } - return connectionManager.getConnection(discoveryNode); + return cm.getConnection(discoveryNode); }); service.start(); service.acceptIncomingRequests(); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - Arrays.asList(() -> seedNode), service, service.getConnectionManager(), Integer.MAX_VALUE, n -> true, null)) { + Arrays.asList(() -> seedNode), service, Integer.MAX_VALUE, n -> true, null, connectionManager)) { connection.addConnectedNode(seedNode); for (DiscoveryNode node : knownNodes) { final Transport.Connection transportConnection = connection.getConnection(node); @@ -488,7 +484,7 @@ public void close() { } @SuppressForbidden(reason = "calls getLocalHost here but it's fine in this case") - public void testSlowNodeCanBeCanceled() throws IOException, InterruptedException { + public void testSlowNodeCanBeCancelled() throws IOException, InterruptedException { try (ServerSocket socket = new MockServerSocket()) { socket.bind(new InetSocketAddress(InetAddress.getLocalHost(), 0), 1); socket.setReuseAddress(true); @@ -518,7 +514,7 @@ public void run() { CountDownLatch listenerCalled = new CountDownLatch(1); AtomicReference exceptionReference = new AtomicReference<>(); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - Arrays.asList(() -> seedNode), service, service.getConnectionManager(), Integer.MAX_VALUE, n -> true, null)) { + Arrays.asList(() -> seedNode), service, Integer.MAX_VALUE, n -> true, null)) { ActionListener listener = ActionListener.wrap(x -> { listenerCalled.countDown(); fail("expected exception"); @@ -555,7 +551,7 @@ public void testFetchShards() throws Exception { service.acceptIncomingRequests(); List> nodes = Collections.singletonList(() -> seedNode); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - nodes, service, service.getConnectionManager(), Integer.MAX_VALUE, n -> true, null)) { + nodes, service, Integer.MAX_VALUE, n -> true, null)) { if (randomBoolean()) { updateSeedNodes(connection, nodes); } @@ -595,7 +591,7 @@ public void testFetchShardsThreadContextHeader() throws Exception { service.acceptIncomingRequests(); List> nodes = Collections.singletonList(() -> seedNode); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - nodes, service, service.getConnectionManager(), Integer.MAX_VALUE, n -> true, null)) { + nodes, service, Integer.MAX_VALUE, n -> true, null)) { SearchRequest request = new SearchRequest("test-index"); Thread[] threads = new Thread[10]; for (int i = 0; i < threads.length; i++) { @@ -649,8 +645,8 @@ public void testFetchShardsSkipUnavailable() throws Exception { service.start(); service.acceptIncomingRequests(); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - Collections.singletonList(() -> seedNode), service, service.getConnectionManager(), Integer.MAX_VALUE, - n -> true, null)) { + Collections.singletonList(() -> seedNode), service, Integer.MAX_VALUE, n -> true, null)) { + ConnectionManager connectionManager = connection.getConnectionManager(); SearchRequest request = new SearchRequest("test-index"); ClusterSearchShardsRequest searchShardsRequest = new ClusterSearchShardsRequest("test-index") @@ -671,7 +667,7 @@ public void testFetchShardsSkipUnavailable() throws Exception { } CountDownLatch disconnectedLatch = new CountDownLatch(1); - service.addConnectionListener(new TransportConnectionListener() { + connectionManager.addListener(new TransportConnectionListener() { @Override public void onNodeDisconnected(DiscoveryNode node) { if (node.equals(seedNode)) { @@ -760,7 +756,8 @@ public void testTriggerUpdatesConcurrently() throws IOException, InterruptedExce service.start(); service.acceptIncomingRequests(); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - seedNodes, service, service.getConnectionManager(), Integer.MAX_VALUE, n -> true, null)) { + seedNodes, service, Integer.MAX_VALUE, n -> true, null)) { + ConnectionManager connectionManager = connection.getConnectionManager(); int numThreads = randomIntBetween(4, 10); Thread[] threads = new Thread[numThreads]; CyclicBarrier barrier = new CyclicBarrier(numThreads); @@ -811,9 +808,9 @@ public void run() { for (int i = 0; i < threads.length; i++) { threads[i].join(); } - assertTrue(service.nodeConnected(seedNode)); - assertTrue(service.nodeConnected(discoverableNode)); - assertTrue(service.nodeConnected(seedNode1)); + assertTrue(connectionManager.nodeConnected(seedNode)); + assertTrue(connectionManager.nodeConnected(discoverableNode)); + assertTrue(connectionManager.nodeConnected(seedNode1)); assertTrue(connection.assertNoRunningConnections()); } } @@ -839,7 +836,7 @@ public void testCloseWhileConcurrentlyConnecting() throws IOException, Interrupt service.start(); service.acceptIncomingRequests(); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - seedNodes, service, service.getConnectionManager(), Integer.MAX_VALUE, n -> true, null)) { + seedNodes, service, Integer.MAX_VALUE, n -> true, null)) { int numThreads = randomIntBetween(4, 10); Thread[] threads = new Thread[numThreads]; CyclicBarrier barrier = new CyclicBarrier(numThreads + 1); @@ -902,7 +899,6 @@ public void run() { threads[i].start(); } barrier.await(); - connection.close(); } } } @@ -944,7 +940,7 @@ public void testGetConnectionInfo() throws Exception { service.acceptIncomingRequests(); int maxNumConnections = randomIntBetween(1, 5); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - seedNodes, service, service.connectionManager(), maxNumConnections, n -> true, null)) { + seedNodes, service, maxNumConnections, n -> true, null)) { // test no nodes connected RemoteConnectionInfo remoteConnectionInfo = assertSerialization(getRemoteConnectionInfo(connection)); assertNotNull(remoteConnectionInfo); @@ -1139,9 +1135,10 @@ public void testEnsureConnected() throws IOException, InterruptedException { service.start(); service.acceptIncomingRequests(); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - Arrays.asList(() -> seedNode), service, service.getConnectionManager(), Integer.MAX_VALUE, n -> true, null)) { - assertFalse(service.nodeConnected(seedNode)); - assertFalse(service.nodeConnected(discoverableNode)); + Arrays.asList(() -> seedNode), service, Integer.MAX_VALUE, n -> true, null)) { + ConnectionManager connectionManager = connection.getConnectionManager(); + assertFalse(connectionManager.nodeConnected(seedNode)); + assertFalse(connectionManager.nodeConnected(discoverableNode)); assertTrue(connection.assertNoRunningConnections()); CountDownLatch latch = new CountDownLatch(1); connection.ensureConnected(new LatchedActionListener<>(new ActionListener() { @@ -1155,8 +1152,8 @@ public void onFailure(Exception e) { } }, latch)); latch.await(); - assertTrue(service.nodeConnected(seedNode)); - assertTrue(service.nodeConnected(discoverableNode)); + assertTrue(connectionManager.nodeConnected(seedNode)); + assertTrue(connectionManager.nodeConnected(discoverableNode)); assertTrue(connection.assertNoRunningConnections()); // exec again we are already connected @@ -1171,8 +1168,8 @@ public void onFailure(Exception e) { } }, latch)); latch.await(); - assertTrue(service.nodeConnected(seedNode)); - assertTrue(service.nodeConnected(discoverableNode)); + assertTrue(connectionManager.nodeConnected(seedNode)); + assertTrue(connectionManager.nodeConnected(discoverableNode)); assertTrue(connection.assertNoRunningConnections()); } } @@ -1188,7 +1185,7 @@ public void testCollectNodes() throws Exception { service.start(); service.acceptIncomingRequests(); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - Arrays.asList(() -> seedNode), service, service.getConnectionManager(), Integer.MAX_VALUE, n -> true, null)) { + Arrays.asList(() -> seedNode), service, Integer.MAX_VALUE, n -> true, null)) { if (randomBoolean()) { updateSeedNodes(connection, Arrays.asList(() -> seedNode)); } @@ -1236,7 +1233,7 @@ public void testConnectedNodesConcurrentAccess() throws IOException, Interrupted service.start(); service.acceptIncomingRequests(); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - seedNodes, service, service.getConnectionManager(), Integer.MAX_VALUE, n -> true, null)) { + seedNodes, service, Integer.MAX_VALUE, n -> true, null)) { final int numGetThreads = randomIntBetween(4, 10); final Thread[] getThreads = new Thread[numGetThreads]; final int numModifyingThreads = randomIntBetween(4, 10); @@ -1326,21 +1323,22 @@ public void testClusterNameIsChecked() throws Exception { service.start(); service.acceptIncomingRequests(); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - Arrays.asList( () -> seedNode), service, service.getConnectionManager(), Integer.MAX_VALUE, n -> true, null)) { - updateSeedNodes(connection, Arrays.asList(() -> seedNode)); - assertTrue(service.nodeConnected(seedNode)); - assertTrue(service.nodeConnected(discoverableNode)); + Arrays.asList( () -> seedNode), service, Integer.MAX_VALUE, n -> true, null)) { + ConnectionManager connectionManager = connection.getConnectionManager(); + updateSeedNodes(connection, Collections.singletonList(() -> seedNode)); + assertTrue(connectionManager.nodeConnected(seedNode)); + assertTrue(connectionManager.nodeConnected(discoverableNode)); assertTrue(connection.assertNoRunningConnections()); List> discoveryNodes = - Arrays.asList(() -> otherClusterTransport.getLocalDiscoNode(), () -> seedNode); + Arrays.asList(otherClusterTransport::getLocalDiscoNode, () -> seedNode); Collections.shuffle(discoveryNodes, random()); updateSeedNodes(connection, discoveryNodes); - assertTrue(service.nodeConnected(seedNode)); + assertTrue(connectionManager.nodeConnected(seedNode)); for (DiscoveryNode otherClusterNode : otherClusterKnownNodes) { - assertFalse(service.nodeConnected(otherClusterNode)); + assertFalse(connectionManager.nodeConnected(otherClusterNode)); } - assertFalse(service.nodeConnected(otherClusterTransport.getLocalDiscoNode())); - assertTrue(service.nodeConnected(discoverableNode)); + assertFalse(connectionManager.nodeConnected(otherClusterTransport.getLocalDiscoNode())); + assertTrue(connectionManager.nodeConnected(discoverableNode)); assertTrue(connection.assertNoRunningConnections()); IllegalStateException illegalStateException = expectThrows(IllegalStateException.class, () -> updateSeedNodes(connection, Arrays.asList(() -> otherClusterTransport.getLocalDiscoNode()))); @@ -1366,7 +1364,7 @@ public void testGetConnection() throws Exception { knownNodes.add(disconnectedNode); try (MockTransportService service = MockTransportService.createNewService(Settings.EMPTY, Version.CURRENT, threadPool, null)) { - Transport.Connection seedConnection = new Transport.Connection() { + Transport.Connection seedConnection = new CloseableConnection() { @Override public DiscoveryNode getNode() { return connectedNode; @@ -1377,37 +1375,25 @@ public void sendRequest(long requestId, String action, TransportRequest request, throws TransportException { // no-op } - - @Override - public void addCloseListener(ActionListener listener) { - // no-op - } - - @Override - public boolean isClosed() { - return false; - } - - @Override - public void close() { - // no-op - } }; - service.addNodeConnectedBehavior(connectedNode.getAddress(), (connectionManager, discoveryNode) + ConnectionManager delegate = new ConnectionManager(Settings.EMPTY, service.transport, threadPool); + StubbableConnectionManager connectionManager = new StubbableConnectionManager(delegate, Settings.EMPTY, service.transport, + threadPool); + + connectionManager.addNodeConnectedBehavior(connectedNode.getAddress(), (cm, discoveryNode) -> discoveryNode.equals(connectedNode)); - service.addGetConnectionBehavior(connectedNode.getAddress(), (connectionManager, discoveryNode) -> { + connectionManager.addConnectBehavior(connectedNode.getAddress(), (cm, discoveryNode) -> { if (discoveryNode == connectedNode) { return seedConnection; } - return connectionManager.getConnection(discoveryNode); + return cm.getConnection(discoveryNode); }); service.start(); service.acceptIncomingRequests(); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - Collections.singletonList(() -> connectedNode), service, service.getConnectionManager(), - Integer.MAX_VALUE, n -> true, null)) { + Collections.singletonList(() -> connectedNode), service, Integer.MAX_VALUE, n -> true, null, connectionManager)) { connection.addConnectedNode(connectedNode); for (int i = 0; i < 10; i++) { //always a direct connection as the remote node is already connected @@ -1449,7 +1435,7 @@ public void testLazyResolveTransportAddress() throws Exception { return seedNode; }; try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - Arrays.asList(seedSupplier), service, service.getConnectionManager(), Integer.MAX_VALUE, n -> true, null)) { + Arrays.asList(seedSupplier), service, Integer.MAX_VALUE, n -> true, null)) { updateSeedNodes(connection, Arrays.asList(seedSupplier)); // Closing connections leads to RemoteClusterConnection.ConnectHandler.collectRemoteNodes // being called again so we try to resolve the same seed node's host twice @@ -1481,7 +1467,7 @@ public void testProxyMode() throws Exception { RemoteClusterAware.buildSeedNode("some-remote-cluster", "node_0:" + randomIntBetween(1, 10000), true); assertEquals("node_0", seedSupplier.get().getAttributes().get("server_name")); try (RemoteClusterConnection connection = new RemoteClusterConnection(Settings.EMPTY, "test-cluster", - Arrays.asList(seedSupplier), service, service.getConnectionManager(), Integer.MAX_VALUE, n -> true, proxyAddress)) { + Arrays.asList(seedSupplier), service, Integer.MAX_VALUE, n -> true, proxyAddress)) { updateSeedNodes(connection, Arrays.asList(seedSupplier), proxyAddress); assertEquals(2, connection.getNumNodesConnected()); assertNotNull(connection.getConnection(discoverableTransport.getLocalDiscoNode())); @@ -1490,7 +1476,7 @@ public void testProxyMode() throws Exception { .getNode().getAddress().toString()); assertEquals(proxyAddress, connection.getConnection(discoverableTransport.getLocalDiscoNode()) .getNode().getAddress().toString()); - service.getConnectionManager().disconnectFromNode(knownNodes.get(0)); + connection.getConnectionManager().disconnectFromNode(knownNodes.get(0)); // ensure we reconnect assertBusy(() -> { assertEquals(2, connection.getNumNodesConnected()); diff --git a/server/src/test/java/org/elasticsearch/transport/RemoteClusterServiceTests.java b/server/src/test/java/org/elasticsearch/transport/RemoteClusterServiceTests.java index 0a02b66b50a29..f03b202deec37 100644 --- a/server/src/test/java/org/elasticsearch/transport/RemoteClusterServiceTests.java +++ b/server/src/test/java/org/elasticsearch/transport/RemoteClusterServiceTests.java @@ -366,7 +366,7 @@ public void testDefaultPingSchedule() throws IOException { assertTrue(service.isCrossClusterSearchEnabled()); assertTrue(service.isRemoteClusterRegistered("cluster_1")); RemoteClusterConnection remoteClusterConnection = service.getRemoteClusterConnection("cluster_1"); - assertEquals(pingSchedule, remoteClusterConnection.getConnectionManager().getPingSchedule()); + assertEquals(pingSchedule, remoteClusterConnection.getConnectionManager().getConnectionProfile().getPingInterval()); } } } @@ -394,9 +394,11 @@ public void testCustomPingSchedule() throws IOException { Settings.Builder builder = Settings.builder(); builder.putList("cluster.remote.cluster_1.seeds", cluster1Seed.getAddress().toString()); builder.putList("cluster.remote.cluster_2.seeds", cluster2Seed.getAddress().toString()); - TimeValue pingSchedule1 = randomBoolean() ? TimeValue.MINUS_ONE : TimeValue.timeValueSeconds(randomIntBetween(1, 10)); + TimeValue pingSchedule1 = // randomBoolean() ? TimeValue.MINUS_ONE : + TimeValue.timeValueSeconds(randomIntBetween(1, 10)); builder.put("cluster.remote.cluster_1.transport.ping_schedule", pingSchedule1); - TimeValue pingSchedule2 = randomBoolean() ? TimeValue.MINUS_ONE : TimeValue.timeValueSeconds(randomIntBetween(1, 10)); + TimeValue pingSchedule2 = //randomBoolean() ? TimeValue.MINUS_ONE : + TimeValue.timeValueSeconds(randomIntBetween(1, 10)); builder.put("cluster.remote.cluster_2.transport.ping_schedule", pingSchedule2); try (RemoteClusterService service = new RemoteClusterService(builder.build(), transportService)) { assertFalse(service.isCrossClusterSearchEnabled()); @@ -406,9 +408,9 @@ public void testCustomPingSchedule() throws IOException { assertTrue(service.isCrossClusterSearchEnabled()); assertTrue(service.isRemoteClusterRegistered("cluster_1")); RemoteClusterConnection remoteClusterConnection1 = service.getRemoteClusterConnection("cluster_1"); - assertEquals(pingSchedule1, remoteClusterConnection1.getConnectionManager().getPingSchedule()); + assertEquals(pingSchedule1, remoteClusterConnection1.getConnectionManager().getConnectionProfile().getPingInterval()); RemoteClusterConnection remoteClusterConnection2 = service.getRemoteClusterConnection("cluster_2"); - assertEquals(pingSchedule2, remoteClusterConnection2.getConnectionManager().getPingSchedule()); + assertEquals(pingSchedule2, remoteClusterConnection2.getConnectionManager().getConnectionProfile().getPingInterval()); } } } diff --git a/server/src/test/java/org/elasticsearch/transport/TcpTransportTests.java b/server/src/test/java/org/elasticsearch/transport/TcpTransportTests.java index 7ec5ebd10a54e..199cf42546d12 100644 --- a/server/src/test/java/org/elasticsearch/transport/TcpTransportTests.java +++ b/server/src/test/java/org/elasticsearch/transport/TcpTransportTests.java @@ -182,13 +182,13 @@ public void testCompressRequest() throws IOException { new BigArrays(new PageCacheRecycler(Settings.EMPTY), null), null, null, null) { @Override - protected FakeChannel bind(String name, InetSocketAddress address) throws IOException { + protected FakeServerChannel bind(String name, InetSocketAddress address) throws IOException { return null; } @Override - protected FakeChannel initiateChannel(DiscoveryNode node) throws IOException { - return new FakeChannel(messageCaptor); + protected FakeTcpChannel initiateChannel(DiscoveryNode node) throws IOException { + return new FakeTcpChannel(true, messageCaptor); } @Override @@ -203,7 +203,7 @@ public NodeChannels openConnection(DiscoveryNode node, ConnectionProfile connect int numConnections = connectionProfile.getNumConnections(); ArrayList fakeChannels = new ArrayList<>(numConnections); for (int i = 0; i < numConnections; ++i) { - fakeChannels.add(new FakeChannel(messageCaptor)); + fakeChannels.add(new FakeTcpChannel(false, messageCaptor)); } return new NodeChannels(node, fakeChannels, connectionProfile, Version.CURRENT); } @@ -248,13 +248,7 @@ public NodeChannels openConnection(DiscoveryNode node, ConnectionProfile connect } } - private static final class FakeChannel implements TcpChannel, TcpServerChannel { - - private final AtomicReference messageCaptor; - - FakeChannel(AtomicReference messageCaptor) { - this.messageCaptor = messageCaptor; - } + private static final class FakeServerChannel implements TcpServerChannel { @Override public void close() { @@ -269,10 +263,6 @@ public String getProfile() { public void addCloseListener(ActionListener listener) { } - @Override - public void addConnectListener(ActionListener listener) { - } - @Override public boolean isOpen() { return false; @@ -282,16 +272,6 @@ public boolean isOpen() { public InetSocketAddress getLocalAddress() { return null; } - - @Override - public InetSocketAddress getRemoteAddress() { - return null; - } - - @Override - public void sendMessage(BytesReference reference, ActionListener listener) { - messageCaptor.set(reference); - } } private static final class Req extends TransportRequest { diff --git a/server/src/test/java/org/elasticsearch/transport/TcpTransportHandshakerTests.java b/server/src/test/java/org/elasticsearch/transport/TransportHandshakerTests.java similarity index 99% rename from server/src/test/java/org/elasticsearch/transport/TcpTransportHandshakerTests.java rename to server/src/test/java/org/elasticsearch/transport/TransportHandshakerTests.java index 23e3870842e20..ec6860f6adddf 100644 --- a/server/src/test/java/org/elasticsearch/transport/TcpTransportHandshakerTests.java +++ b/server/src/test/java/org/elasticsearch/transport/TransportHandshakerTests.java @@ -36,7 +36,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; -public class TcpTransportHandshakerTests extends ESTestCase { +public class TransportHandshakerTests extends ESTestCase { private TcpTransportHandshaker handshaker; private DiscoveryNode node; diff --git a/server/src/test/java/org/elasticsearch/transport/TransportKeepAliveTests.java b/server/src/test/java/org/elasticsearch/transport/TransportKeepAliveTests.java new file mode 100644 index 0000000000000..a56db579cec80 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/transport/TransportKeepAliveTests.java @@ -0,0 +1,220 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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.elasticsearch.transport; + +import org.elasticsearch.common.AsyncBiFunction; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.threadpool.TestThreadPool; + +import java.io.IOException; +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.Collections; +import java.util.Deque; +import java.util.concurrent.ScheduledFuture; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Matchers.same; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +public class TransportKeepAliveTests extends ESTestCase { + + private final ConnectionProfile defaultProfile = ConnectionProfile.buildDefaultConnectionProfile(Settings.EMPTY); + private BytesReference expectedPingMessage; + private AsyncBiFunction pingSender; + private TransportKeepAlive keepAlive; + private CapturingThreadPool threadPool; + + @Override + @SuppressWarnings("unchecked") + public void setUp() throws Exception { + super.setUp(); + pingSender = mock(AsyncBiFunction.class); + threadPool = new CapturingThreadPool(); + keepAlive = new TransportKeepAlive(threadPool, pingSender); + + try (BytesStreamOutput out = new BytesStreamOutput()) { + out.writeByte((byte) 'E'); + out.writeByte((byte) 'S'); + out.writeInt(-1); + expectedPingMessage = out.bytes(); + } catch (IOException e) { + throw new AssertionError(e.getMessage(), e); // won't happen + } + } + + @Override + public void tearDown() throws Exception { + threadPool.shutdown(); + super.tearDown(); + } + + public void testRegisterNodeConnectionSchedulesKeepAlive() { + TimeValue pingInterval = TimeValue.timeValueSeconds(randomLongBetween(1, 60)); + ConnectionProfile connectionProfile = new ConnectionProfile.Builder(defaultProfile) + .setPingInterval(pingInterval) + .build(); + + assertEquals(0, threadPool.scheduledTasks.size()); + + TcpChannel channel1 = new FakeTcpChannel(); + TcpChannel channel2 = new FakeTcpChannel(); + channel1.getChannelStats().markAccessed(threadPool.relativeTimeInMillis()); + channel2.getChannelStats().markAccessed(threadPool.relativeTimeInMillis()); + keepAlive.registerNodeConnection(Arrays.asList(channel1, channel2), connectionProfile); + + assertEquals(1, threadPool.scheduledTasks.size()); + Tuple taskTuple = threadPool.scheduledTasks.poll(); + assertEquals(pingInterval, taskTuple.v1()); + Runnable keepAliveTask = taskTuple.v2(); + assertEquals(0, threadPool.scheduledTasks.size()); + keepAliveTask.run(); + + verify(pingSender, times(1)).apply(same(channel1), eq(expectedPingMessage), any()); + verify(pingSender, times(1)).apply(same(channel2), eq(expectedPingMessage), any()); + + // Test that the task has rescheduled itself + assertEquals(1, threadPool.scheduledTasks.size()); + Tuple rescheduledTask = threadPool.scheduledTasks.poll(); + assertEquals(pingInterval, rescheduledTask.v1()); + } + + public void testRegisterMultipleKeepAliveIntervals() { + TimeValue pingInterval1 = TimeValue.timeValueSeconds(randomLongBetween(1, 30)); + ConnectionProfile connectionProfile1 = new ConnectionProfile.Builder(defaultProfile) + .setPingInterval(pingInterval1) + .build(); + + TimeValue pingInterval2 = TimeValue.timeValueSeconds(randomLongBetween(31, 60)); + ConnectionProfile connectionProfile2 = new ConnectionProfile.Builder(defaultProfile) + .setPingInterval(pingInterval2) + .build(); + + assertEquals(0, threadPool.scheduledTasks.size()); + + TcpChannel channel1 = new FakeTcpChannel(); + TcpChannel channel2 = new FakeTcpChannel(); + channel1.getChannelStats().markAccessed(threadPool.relativeTimeInMillis()); + channel2.getChannelStats().markAccessed(threadPool.relativeTimeInMillis()); + keepAlive.registerNodeConnection(Collections.singletonList(channel1), connectionProfile1); + keepAlive.registerNodeConnection(Collections.singletonList(channel2), connectionProfile2); + + assertEquals(2, threadPool.scheduledTasks.size()); + Tuple taskTuple1 = threadPool.scheduledTasks.poll(); + Tuple taskTuple2 = threadPool.scheduledTasks.poll(); + assertEquals(pingInterval1, taskTuple1.v1()); + assertEquals(pingInterval2, taskTuple2.v1()); + Runnable keepAliveTask1 = taskTuple1.v2(); + Runnable keepAliveTask2 = taskTuple1.v2(); + + assertEquals(0, threadPool.scheduledTasks.size()); + keepAliveTask1.run(); + assertEquals(1, threadPool.scheduledTasks.size()); + keepAliveTask2.run(); + assertEquals(2, threadPool.scheduledTasks.size()); + } + + public void testClosingChannelUnregistersItFromKeepAlive() { + TimeValue pingInterval1 = TimeValue.timeValueSeconds(randomLongBetween(1, 30)); + ConnectionProfile connectionProfile = new ConnectionProfile.Builder(defaultProfile) + .setPingInterval(pingInterval1) + .build(); + + TcpChannel channel1 = new FakeTcpChannel(); + TcpChannel channel2 = new FakeTcpChannel(); + channel1.getChannelStats().markAccessed(threadPool.relativeTimeInMillis()); + channel2.getChannelStats().markAccessed(threadPool.relativeTimeInMillis()); + keepAlive.registerNodeConnection(Collections.singletonList(channel1), connectionProfile); + keepAlive.registerNodeConnection(Collections.singletonList(channel2), connectionProfile); + + channel1.close(); + + Runnable task = threadPool.scheduledTasks.poll().v2(); + task.run(); + + verify(pingSender, times(0)).apply(same(channel1), eq(expectedPingMessage), any()); + verify(pingSender, times(1)).apply(same(channel2), eq(expectedPingMessage), any()); + } + + public void testKeepAliveResponseIfServer() { + TcpChannel channel = new FakeTcpChannel(true); + channel.getChannelStats().markAccessed(threadPool.relativeTimeInMillis()); + + keepAlive.receiveKeepAlive(channel); + + verify(pingSender, times(1)).apply(same(channel), eq(expectedPingMessage), any()); + } + + public void testNoKeepAliveResponseIfClient() { + TcpChannel channel = new FakeTcpChannel(false); + channel.getChannelStats().markAccessed(threadPool.relativeTimeInMillis()); + + keepAlive.receiveKeepAlive(channel); + + verify(pingSender, times(0)).apply(same(channel), eq(expectedPingMessage), any()); + } + + public void testOnlySendPingIfWeHaveNotWrittenAndReadSinceLastPing() { + TimeValue pingInterval = TimeValue.timeValueSeconds(15); + ConnectionProfile connectionProfile = new ConnectionProfile.Builder(defaultProfile) + .setPingInterval(pingInterval) + .build(); + + TcpChannel channel1 = new FakeTcpChannel(); + TcpChannel channel2 = new FakeTcpChannel(); + channel1.getChannelStats().markAccessed(threadPool.relativeTimeInMillis()); + channel2.getChannelStats().markAccessed(threadPool.relativeTimeInMillis()); + keepAlive.registerNodeConnection(Arrays.asList(channel1, channel2), connectionProfile); + + Tuple taskTuple = threadPool.scheduledTasks.poll(); + taskTuple.v2().run(); + + TcpChannel.ChannelStats stats = channel1.getChannelStats(); + stats.markAccessed(threadPool.relativeTimeInMillis() + (pingInterval.millis() / 2)); + + taskTuple = threadPool.scheduledTasks.poll(); + taskTuple.v2().run(); + + verify(pingSender, times(1)).apply(same(channel1), eq(expectedPingMessage), any()); + verify(pingSender, times(2)).apply(same(channel2), eq(expectedPingMessage), any()); + } + + private class CapturingThreadPool extends TestThreadPool { + + private final Deque> scheduledTasks = new ArrayDeque<>(); + + private CapturingThreadPool() { + super(getTestName()); + } + + @Override + public ScheduledFuture schedule(TimeValue delay, String executor, Runnable task) { + scheduledTasks.add(new Tuple<>(delay, task)); + return null; + } + } +} diff --git a/test/framework/src/main/java/org/elasticsearch/test/transport/StubbableTransport.java b/test/framework/src/main/java/org/elasticsearch/test/transport/StubbableTransport.java index d35fe609c0855..ce0e38a83f88d 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/transport/StubbableTransport.java +++ b/test/framework/src/main/java/org/elasticsearch/test/transport/StubbableTransport.java @@ -210,11 +210,6 @@ public void sendRequest(long requestId, String action, TransportRequest request, } } - @Override - public boolean sendPing() { - return connection.sendPing(); - } - @Override public void addCloseListener(ActionListener listener) { connection.addCloseListener(listener); diff --git a/test/framework/src/main/java/org/elasticsearch/transport/AbstractSimpleTransportTestCase.java b/test/framework/src/main/java/org/elasticsearch/transport/AbstractSimpleTransportTestCase.java index 3741be92b8da0..aa8da669cdbbc 100644 --- a/test/framework/src/main/java/org/elasticsearch/transport/AbstractSimpleTransportTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/transport/AbstractSimpleTransportTestCase.java @@ -2013,7 +2013,26 @@ public void testHandshakeUpdatesVersion() throws IOException { } } - public void testTcpHandshake() throws IOException, InterruptedException { + public void testKeepAlivePings() throws Exception { + assumeTrue("only tcp transport has keep alive pings", serviceA.getOriginalTransport() instanceof TcpTransport); + TcpTransport originalTransport = (TcpTransport) serviceA.getOriginalTransport(); + + ConnectionProfile defaultProfile = ConnectionProfile.buildDefaultConnectionProfile(Settings.EMPTY); + ConnectionProfile connectionProfile = new ConnectionProfile.Builder(defaultProfile) + .setPingInterval(TimeValue.timeValueMillis(50)) + .build(); + try (TransportService service = buildService("TS_TPC", Version.CURRENT, null); + TcpTransport.NodeChannels connection = originalTransport.openConnection( + new DiscoveryNode("TS_TPC", "TS_TPC", service.boundAddress().publishAddress(), emptyMap(), emptySet(), version0), + connectionProfile)) { + assertBusy(() -> { + assertTrue(originalTransport.getKeepAlive().successfulPingCount() > 30); + }); + assertEquals(0, originalTransport.getKeepAlive().failedPingCount()); + } + } + + public void testTcpHandshake() { assumeTrue("only tcp transport has a handshake method", serviceA.getOriginalTransport() instanceof TcpTransport); TcpTransport originalTransport = (TcpTransport) serviceA.getOriginalTransport(); NamedWriteableRegistry namedWriteableRegistry = new NamedWriteableRegistry(Collections.emptyList()); diff --git a/test/framework/src/main/java/org/elasticsearch/transport/FakeTcpChannel.java b/test/framework/src/main/java/org/elasticsearch/transport/FakeTcpChannel.java new file mode 100644 index 0000000000000..cd598a6ca3106 --- /dev/null +++ b/test/framework/src/main/java/org/elasticsearch/transport/FakeTcpChannel.java @@ -0,0 +1,104 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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.elasticsearch.transport; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.concurrent.CompletableContext; + +import java.net.InetSocketAddress; +import java.util.concurrent.atomic.AtomicReference; + +public class FakeTcpChannel implements TcpChannel { + + private final boolean isServer; + private final String profile; + private final AtomicReference messageCaptor; + private final ChannelStats stats = new ChannelStats(); + private final CompletableContext closeContext = new CompletableContext<>(); + + public FakeTcpChannel() { + this(false, "profile", new AtomicReference<>()); + } + + public FakeTcpChannel(boolean isServer) { + this(isServer, "profile", new AtomicReference<>()); + } + + public FakeTcpChannel(boolean isServer, AtomicReference messageCaptor) { + this(isServer, "profile", messageCaptor); + } + + + public FakeTcpChannel(boolean isServer, String profile, AtomicReference messageCaptor) { + this.isServer = isServer; + this.profile = profile; + this.messageCaptor = messageCaptor; + } + + @Override + public boolean isServerChannel() { + return isServer; + } + + @Override + public String getProfile() { + return profile; + } + + @Override + public InetSocketAddress getLocalAddress() { + return null; + } + + @Override + public InetSocketAddress getRemoteAddress() { + return null; + } + + @Override + public void sendMessage(BytesReference reference, ActionListener listener) { + messageCaptor.set(reference); + } + + @Override + public void addConnectListener(ActionListener listener) { + + } + + @Override + public void close() { + closeContext.complete(null); + } + + @Override + public void addCloseListener(ActionListener listener) { + closeContext.addListener(ActionListener.toBiConsumer(listener)); + } + + @Override + public boolean isOpen() { + return closeContext.isDone() == false; + } + + @Override + public ChannelStats getChannelStats() { + return stats; + } +} diff --git a/test/framework/src/main/java/org/elasticsearch/transport/MockTcpTransport.java b/test/framework/src/main/java/org/elasticsearch/transport/MockTcpTransport.java index 48358960c3f3a..b5cdbeae2c960 100644 --- a/test/framework/src/main/java/org/elasticsearch/transport/MockTcpTransport.java +++ b/test/framework/src/main/java/org/elasticsearch/transport/MockTcpTransport.java @@ -78,7 +78,7 @@ public class MockTcpTransport extends TcpTransport { * A pre-built light connection profile that shares a single connection across all * types. */ - public static final ConnectionProfile LIGHT_PROFILE; + static final ConnectionProfile LIGHT_PROFILE; private final Set openChannels = new HashSet<>(); @@ -173,7 +173,7 @@ private void readMessage(MockChannel mockChannel, StreamInput input) throws IOEx protected MockChannel initiateChannel(DiscoveryNode node) throws IOException { InetSocketAddress address = node.getAddress().address(); final MockSocket socket = new MockSocket(); - final MockChannel channel = new MockChannel(socket, address, "none"); + final MockChannel channel = new MockChannel(socket, address, false, "none"); boolean success = false; try { @@ -219,6 +219,7 @@ protected ConnectionProfile maybeOverrideConnectionProfile(ConnectionProfile con } builder.setHandshakeTimeout(connectionProfile.getHandshakeTimeout()); builder.setConnectTimeout(connectionProfile.getConnectTimeout()); + builder.setPingInterval(connectionProfile.getPingInterval()); builder.setCompressionEnabled(connectionProfile.getCompressionEnabled()); return builder.build(); } @@ -242,10 +243,12 @@ public final class MockChannel implements Closeable, TcpChannel, TcpServerChanne private final ServerSocket serverSocket; private final Set workerChannels = Collections.newSetFromMap(new ConcurrentHashMap<>()); private final Socket activeChannel; + private final boolean isServer; private final String profile; private final CancellableThreads cancellableThreads = new CancellableThreads(); private final CompletableContext closeFuture = new CompletableContext<>(); private final CompletableContext connectFuture = new CompletableContext<>(); + private final ChannelStats stats = new ChannelStats(); /** * Constructs a new MockChannel instance intended for handling the actual incoming / outgoing traffic. @@ -254,9 +257,10 @@ public final class MockChannel implements Closeable, TcpChannel, TcpServerChanne * @param localAddress Address associated with the corresponding local server socket. Must not be null. * @param profile The associated profile name. */ - public MockChannel(Socket socket, InetSocketAddress localAddress, String profile) { + MockChannel(Socket socket, InetSocketAddress localAddress, boolean isServer, String profile) { this.localAddress = localAddress; this.activeChannel = socket; + this.isServer = isServer; this.serverSocket = null; this.profile = profile; synchronized (openChannels) { @@ -274,6 +278,7 @@ public MockChannel(Socket socket, InetSocketAddress localAddress, String profile this.localAddress = (InetSocketAddress) serverSocket.getLocalSocketAddress(); this.serverSocket = serverSocket; this.profile = profile; + this.isServer = false; this.activeChannel = null; synchronized (openChannels) { openChannels.add(this); @@ -288,8 +293,9 @@ public void accept(Executor executor) throws IOException { configureSocket(incomingSocket); synchronized (this) { if (isOpen.get()) { - incomingChannel = new MockChannel(incomingSocket, - new InetSocketAddress(incomingSocket.getLocalAddress(), incomingSocket.getPort()), profile); + InetSocketAddress localAddress = new InetSocketAddress(incomingSocket.getLocalAddress(), + incomingSocket.getPort()); + incomingChannel = new MockChannel(incomingSocket, localAddress, true, profile); MockChannel finalIncomingChannel = incomingChannel; incomingChannel.addCloseListener(new ActionListener() { @Override @@ -389,6 +395,11 @@ public String getProfile() { return profile; } + @Override + public boolean isServerChannel() { + return isServer; + } + @Override public void addCloseListener(ActionListener listener) { closeFuture.addListener(ActionListener.toBiConsumer(listener)); @@ -399,6 +410,11 @@ public void addConnectListener(ActionListener listener) { connectFuture.addListener(ActionListener.toBiConsumer(listener)); } + @Override + public ChannelStats getChannelStats() { + return stats; + } + @Override public boolean isOpen() { return isOpen.get(); From f15c57430ee9c9f5719fc26e8e05f128ee8924be Mon Sep 17 00:00:00 2001 From: Jay Modi Date: Thu, 29 Nov 2018 11:37:06 -0700 Subject: [PATCH 031/115] Security: improve exact index matching performance (#36017) This commit improves the efficiency of exact index name matching by separating exact matches from those that include wildcards or regular expressions. Internally, exact matching is done using a HashSet instead of adding the exact matches to the automata. For the wildcard and regular expression matches, the underlying implementation has not changed. --- .../authz/permission/IndicesPermission.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java index f0e2f2b7e6217..4936071ee8445 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java @@ -64,6 +64,36 @@ public IndicesPermission(Group... groups) { } static Predicate indexMatcher(List indices) { + Set exactMatch = new HashSet<>(); + List nonExactMatch = new ArrayList<>(); + for (String indexPattern : indices) { + if (indexPattern.startsWith("/") || indexPattern.contains("*") || indexPattern.contains("?")) { + nonExactMatch.add(indexPattern); + } else { + exactMatch.add(indexPattern); + } + } + + if (exactMatch.isEmpty() && nonExactMatch.isEmpty()) { + return s -> false; + } else if (exactMatch.isEmpty()) { + return buildAutomataPredicate(nonExactMatch); + } else if (nonExactMatch.isEmpty()) { + return buildExactMatchPredicate(exactMatch); + } else { + return buildExactMatchPredicate(exactMatch).or(buildAutomataPredicate(nonExactMatch)); + } + } + + private static Predicate buildExactMatchPredicate(Set indices) { + if (indices.size() == 1) { + final String singleValue = indices.iterator().next(); + return singleValue::equals; + } + return indices::contains; + } + + private static Predicate buildAutomataPredicate(List indices) { try { return Automatons.predicate(indices); } catch (TooComplexToDeterminizeException e) { From f44bd9fae11b0efe770a2e426662080345db920f Mon Sep 17 00:00:00 2001 From: Tal Levy Date: Thu, 29 Nov 2018 11:11:20 -0800 Subject: [PATCH 032/115] [ILM] make alias swapping atomic (#35972) In the current implementation, there is a time between the ShrinkStep and the ShrinkSetAliasStep that both the source and target indices will be present with the same aliases. This means that queries to during this time will query both and return duplicates. This fixes that scenario by moving the alias inheritance to the same aliases update request as is done in ShrinkSetAliasStep --- .../indexlifecycle/ShrinkSetAliasStep.java | 18 ++++++++++--- .../xpack/core/indexlifecycle/ShrinkStep.java | 5 ---- .../ShrinkSetAliasStepTests.java | 25 ++++++++++++++++--- .../core/indexlifecycle/ShrinkStepTests.java | 3 +-- 4 files changed, 37 insertions(+), 14 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/ShrinkSetAliasStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/ShrinkSetAliasStep.java index 52fc955a2327a..da59b16ded99b 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/ShrinkSetAliasStep.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/ShrinkSetAliasStep.java @@ -9,6 +9,7 @@ import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest; import org.elasticsearch.client.Client; import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.metadata.AliasMetaData; import org.elasticsearch.cluster.metadata.IndexMetaData; import java.util.Objects; @@ -36,11 +37,20 @@ public void performAction(IndexMetaData indexMetaData, ClusterState currentState String index = indexMetaData.getIndex().getName(); // get target shrink index String targetIndexName = shrunkIndexPrefix + index; - IndicesAliasesRequest aliasesRequest = new IndicesAliasesRequest() .addAliasAction(IndicesAliasesRequest.AliasActions.removeIndex().index(index)) .addAliasAction(IndicesAliasesRequest.AliasActions.add().index(targetIndexName).alias(index)); - + // copy over other aliases from original index + indexMetaData.getAliases().values().spliterator().forEachRemaining(aliasMetaDataObjectCursor -> { + AliasMetaData aliasMetaDataToAdd = aliasMetaDataObjectCursor.value; + // inherit all alias properties except `is_write_index` + aliasesRequest.addAliasAction(IndicesAliasesRequest.AliasActions.add() + .index(targetIndexName).alias(aliasMetaDataToAdd.alias()) + .indexRouting(aliasMetaDataToAdd.indexRouting()) + .searchRouting(aliasMetaDataToAdd.searchRouting()) + .filter(aliasMetaDataToAdd.filter() == null ? null : aliasMetaDataToAdd.filter().string()) + .writeIndex(null)); + }); getClient().admin().indices().aliases(aliasesRequest, ActionListener.wrap(response -> listener.onResponse(true), listener::onFailure)); } @@ -54,7 +64,7 @@ public boolean indexSurvives() { public int hashCode() { return Objects.hash(super.hashCode(), shrunkIndexPrefix); } - + @Override public boolean equals(Object obj) { if (obj == null) { @@ -64,7 +74,7 @@ public boolean equals(Object obj) { return false; } ShrinkSetAliasStep other = (ShrinkSetAliasStep) obj; - return super.equals(obj) && + return super.equals(obj) && Objects.equals(shrunkIndexPrefix, other.shrunkIndexPrefix); } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/ShrinkStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/ShrinkStep.java index e6ff141103c84..c4ad13d676f4c 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/ShrinkStep.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/indexlifecycle/ShrinkStep.java @@ -6,7 +6,6 @@ package org.elasticsearch.xpack.core.indexlifecycle; import org.elasticsearch.action.ActionListener; -import org.elasticsearch.action.admin.indices.alias.Alias; import org.elasticsearch.action.admin.indices.shrink.ResizeRequest; import org.elasticsearch.client.Client; import org.elasticsearch.cluster.ClusterState; @@ -59,13 +58,9 @@ public void performAction(IndexMetaData indexMetaData, ClusterState currentState String shrunkenIndexName = shrunkIndexPrefix + indexMetaData.getIndex().getName(); ResizeRequest resizeRequest = new ResizeRequest(shrunkenIndexName, indexMetaData.getIndex().getName()); resizeRequest.setCopySettings(true); - indexMetaData.getAliases().values().spliterator().forEachRemaining(aliasMetaDataObjectCursor -> { - resizeRequest.getTargetIndexRequest().alias(new Alias(aliasMetaDataObjectCursor.value.alias())); - }); resizeRequest.getTargetIndexRequest().settings(relevantTargetSettings); getClient().admin().indices().resizeIndex(resizeRequest, ActionListener.wrap(response -> { - // TODO(talevy): when is this not acknowledged? listener.onResponse(response.isAcknowledged()); }, listener::onFailure)); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/indexlifecycle/ShrinkSetAliasStepTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/indexlifecycle/ShrinkSetAliasStepTests.java index 5fcfcdeea38f0..a5c0e4d7146bc 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/indexlifecycle/ShrinkSetAliasStepTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/indexlifecycle/ShrinkSetAliasStepTests.java @@ -14,6 +14,7 @@ import org.elasticsearch.client.AdminClient; import org.elasticsearch.client.Client; import org.elasticsearch.client.IndicesAdminClient; +import org.elasticsearch.cluster.metadata.AliasMetaData; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.xpack.core.indexlifecycle.AsyncActionStep.Listener; import org.elasticsearch.xpack.core.indexlifecycle.Step.StepKey; @@ -71,15 +72,33 @@ public ShrinkSetAliasStep copyInstance(ShrinkSetAliasStep instance) { } public void testPerformAction() { - IndexMetaData indexMetaData = IndexMetaData.builder(randomAlphaOfLength(10)).settings(settings(Version.CURRENT)) - .numberOfShards(randomIntBetween(1, 5)).numberOfReplicas(randomIntBetween(0, 5)).build(); + IndexMetaData.Builder indexMetaDataBuilder = IndexMetaData.builder(randomAlphaOfLength(10)).settings(settings(Version.CURRENT)) + .numberOfShards(randomIntBetween(1, 5)).numberOfReplicas(randomIntBetween(0, 5)); + AliasMetaData.Builder aliasBuilder = AliasMetaData.builder(randomAlphaOfLengthBetween(3, 10)); + if (randomBoolean()) { + aliasBuilder.routing(randomAlphaOfLengthBetween(3, 10)); + } + if (randomBoolean()) { + aliasBuilder.searchRouting(randomAlphaOfLengthBetween(3, 10)); + } + if (randomBoolean()) { + aliasBuilder.indexRouting(randomAlphaOfLengthBetween(3, 10)); + } + String aliasMetaDataFilter = randomBoolean() ? null : "{\"term\":{\"year\":2016}}"; + aliasBuilder.filter(aliasMetaDataFilter); + aliasBuilder.writeIndex(randomBoolean()); + AliasMetaData aliasMetaData = aliasBuilder.build(); + IndexMetaData indexMetaData = indexMetaDataBuilder.putAlias(aliasMetaData).build(); ShrinkSetAliasStep step = createRandomInstance(); String sourceIndex = indexMetaData.getIndex().getName(); String shrunkenIndex = step.getShrunkIndexPrefix() + sourceIndex; List expectedAliasActions = Arrays.asList( IndicesAliasesRequest.AliasActions.removeIndex().index(sourceIndex), - IndicesAliasesRequest.AliasActions.add().index(shrunkenIndex).alias(sourceIndex)); + IndicesAliasesRequest.AliasActions.add().index(shrunkenIndex).alias(sourceIndex), + IndicesAliasesRequest.AliasActions.add().index(shrunkenIndex).alias(aliasMetaData.alias()) + .searchRouting(aliasMetaData.searchRouting()).indexRouting(aliasMetaData.indexRouting()) + .filter(aliasMetaDataFilter).writeIndex(null)); AdminClient adminClient = Mockito.mock(AdminClient.class); IndicesAdminClient indicesClient = Mockito.mock(IndicesAdminClient.class); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/indexlifecycle/ShrinkStepTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/indexlifecycle/ShrinkStepTests.java index 472c22025e195..0cd655cb9d60f 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/indexlifecycle/ShrinkStepTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/indexlifecycle/ShrinkStepTests.java @@ -8,7 +8,6 @@ import org.apache.lucene.util.SetOnce; import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; -import org.elasticsearch.action.admin.indices.alias.Alias; import org.elasticsearch.action.admin.indices.rollover.RolloverResponse; import org.elasticsearch.action.admin.indices.shrink.ResizeRequest; import org.elasticsearch.action.admin.indices.shrink.ResizeResponse; @@ -112,7 +111,7 @@ public Void answer(InvocationOnMock invocation) throws Throwable { @SuppressWarnings("unchecked") ActionListener listener = (ActionListener) invocation.getArguments()[1]; assertThat(request.getSourceIndex(), equalTo(sourceIndexMetaData.getIndex().getName())); - assertThat(request.getTargetIndexRequest().aliases(), equalTo(Collections.singleton(new Alias("my_alias")))); + assertThat(request.getTargetIndexRequest().aliases(), equalTo(Collections.emptySet())); assertThat(request.getTargetIndexRequest().settings(), equalTo(Settings.builder() .put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, step.getNumberOfShards()) .put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, sourceIndexMetaData.getNumberOfReplicas()) From 6595ded06067986abf8530ecacb41ae79907df7f Mon Sep 17 00:00:00 2001 From: Gordon Brown Date: Thu, 29 Nov 2018 14:09:51 -0700 Subject: [PATCH 033/115] Recommend index templates when using ILM Rollover (#35922) Because rollover does not propogate the ILM policy in use, add a note to the docs recommending the use of index templates when using rollover. --- docs/reference/ilm/set-up-lifecycle-policy.asciidoc | 1 + docs/reference/ilm/using-policies-rollover.asciidoc | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/docs/reference/ilm/set-up-lifecycle-policy.asciidoc b/docs/reference/ilm/set-up-lifecycle-policy.asciidoc index 11643bdfc0674..5997e618bf64c 100644 --- a/docs/reference/ilm/set-up-lifecycle-policy.asciidoc +++ b/docs/reference/ilm/set-up-lifecycle-policy.asciidoc @@ -45,6 +45,7 @@ To set the policy for an index there are two options: 1. Apply the policy to an index template and bootstrap creating the first index 2. Apply the policy to a new index in a create index request +[[applying-policy-to-template]] === Applying a policy to an index template beta[] diff --git a/docs/reference/ilm/using-policies-rollover.asciidoc b/docs/reference/ilm/using-policies-rollover.asciidoc index ec3eb01fb64d1..3a51fcecd790b 100644 --- a/docs/reference/ilm/using-policies-rollover.asciidoc +++ b/docs/reference/ilm/using-policies-rollover.asciidoc @@ -21,6 +21,13 @@ met. Because the criteria are checked periodically, the index might grow slightly beyond the specified threshold. To control how often the critera are checked, specify the `indices.lifecycle.poll_interval` cluster setting. +IMPORTANT: New indices created via rollover will not automatically inherit the +policy used by the old index, and will not use any policy by default. Therefore, +it is highly recommended to apply the policy via +<>, including a Rollover alias +setting, for your indices which specifies the policy you wish to use for each +new index. + The rollover action takes the following parameters: .`rollover` Action Parameters From ef0180a4448510c91379b63d8dad0acd59fdedf1 Mon Sep 17 00:00:00 2001 From: Mayya Sharipova Date: Thu, 29 Nov 2018 16:27:17 -0500 Subject: [PATCH 034/115] Disallow boosts on inner span queries (#35967) --- docs/reference/migration/migrate_6_6.asciidoc | 5 + .../reference/query-dsl/span-queries.asciidoc | 8 +- .../query/SpanContainingQueryBuilder.java | 4 + .../index/query/SpanFirstQueryBuilder.java | 9 +- .../index/query/SpanNearQueryBuilder.java | 8 +- .../index/query/SpanNotQueryBuilder.java | 14 ++- .../index/query/SpanOrQueryBuilder.java | 10 +- .../index/query/SpanQueryBuilder.java | 30 +++++- .../index/query/SpanWithinQueryBuilder.java | 4 + .../SpanContainingQueryBuilderTests.java | 91 +++++++++++++++- .../query/SpanFirstQueryBuilderTests.java | 28 ++++- .../query/SpanNearQueryBuilderTests.java | 39 ++++++- .../index/query/SpanNotQueryBuilderTests.java | 100 +++++++++++++++++- .../index/query/SpanOrQueryBuilderTests.java | 25 ++++- .../query/SpanTermQueryBuilderTests.java | 9 +- .../query/SpanWithinQueryBuilderTests.java | 91 +++++++++++++++- .../index/query/WrapperQueryBuilderTests.java | 7 +- .../test/AbstractQueryTestCase.java | 40 ++++--- 18 files changed, 475 insertions(+), 47 deletions(-) diff --git a/docs/reference/migration/migrate_6_6.asciidoc b/docs/reference/migration/migrate_6_6.asciidoc index 32cb1b744806b..2cfba69c249d9 100644 --- a/docs/reference/migration/migrate_6_6.asciidoc +++ b/docs/reference/migration/migrate_6_6.asciidoc @@ -33,6 +33,11 @@ rest of Elasticsearc's APIs and Elasticsearch will raise a deprecation warning if those are used on any APIs. We plan to drop support for `_source_exclude` and `_source_include` in 7.0. +[float] +==== Boosts on inner span queries are not allowed. + +Attempts to set `boost` on inner span queries will now throw a parsing exception. + [float] ==== Deprecate `.values` and `.getValues()` on doc values in scripts diff --git a/docs/reference/query-dsl/span-queries.asciidoc b/docs/reference/query-dsl/span-queries.asciidoc index 4a1a019574e92..7dc65433432ec 100644 --- a/docs/reference/query-dsl/span-queries.asciidoc +++ b/docs/reference/query-dsl/span-queries.asciidoc @@ -5,6 +5,12 @@ Span queries are low-level positional queries which provide expert control over the order and proximity of the specified terms. These are typically used to implement very specific queries on legal documents or patents. +It is only allowed to set boost on an outer span query. Compound span queries, +like span_near, only use the list of matching spans of inner span queries in +order to find their own spans, which they then use to produce a score. Scores +are never computed on inner span queries, which is the reason why boosts are not +allowed: they only influence the way scores are computed, not spans. + Span queries cannot be mixed with non-span queries (with the exception of the `span_multi` query). The queries in this group are: @@ -67,4 +73,4 @@ include::span-containing-query.asciidoc[] include::span-within-query.asciidoc[] -include::span-field-masking-query.asciidoc[] \ No newline at end of file +include::span-field-masking-query.asciidoc[] diff --git a/server/src/main/java/org/elasticsearch/index/query/SpanContainingQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/SpanContainingQueryBuilder.java index 2842b84fa1ce1..164a5809f6e39 100644 --- a/server/src/main/java/org/elasticsearch/index/query/SpanContainingQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/SpanContainingQueryBuilder.java @@ -32,6 +32,8 @@ import java.io.IOException; import java.util.Objects; +import static org.elasticsearch.index.query.SpanQueryBuilder.SpanQueryBuilderUtil.checkNoBoost; + /** * Builder for {@link org.apache.lucene.search.spans.SpanContainingQuery}. */ @@ -117,12 +119,14 @@ public static SpanContainingQueryBuilder fromXContent(XContentParser parser) thr throw new ParsingException(parser.getTokenLocation(), "span_containing [big] must be of type span query"); } big = (SpanQueryBuilder) query; + checkNoBoost(NAME, currentFieldName, parser, big); } else if (LITTLE_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { QueryBuilder query = parseInnerQueryBuilder(parser); if (query instanceof SpanQueryBuilder == false) { throw new ParsingException(parser.getTokenLocation(), "span_containing [little] must be of type span query"); } little = (SpanQueryBuilder) query; + checkNoBoost(NAME, currentFieldName, parser, little); } else { throw new ParsingException(parser.getTokenLocation(), "[span_containing] query does not support [" + currentFieldName + "]"); diff --git a/server/src/main/java/org/elasticsearch/index/query/SpanFirstQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/SpanFirstQueryBuilder.java index 376e87424da59..dfd13f9f9fe2b 100644 --- a/server/src/main/java/org/elasticsearch/index/query/SpanFirstQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/SpanFirstQueryBuilder.java @@ -32,6 +32,8 @@ import java.io.IOException; import java.util.Objects; +import static org.elasticsearch.index.query.SpanQueryBuilder.SpanQueryBuilderUtil.checkNoBoost; + public class SpanFirstQueryBuilder extends AbstractQueryBuilder implements SpanQueryBuilder { public static final String NAME = "span_first"; @@ -115,9 +117,10 @@ public static SpanFirstQueryBuilder fromXContent(XContentParser parser) throws I if (MATCH_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { QueryBuilder query = parseInnerQueryBuilder(parser); if (query instanceof SpanQueryBuilder == false) { - throw new ParsingException(parser.getTokenLocation(), "spanFirst [match] must be of type span query"); + throw new ParsingException(parser.getTokenLocation(), "span_first [match] must be of type span query"); } match = (SpanQueryBuilder) query; + checkNoBoost(NAME, currentFieldName, parser, match); } else { throw new ParsingException(parser.getTokenLocation(), "[span_first] query does not support [" + currentFieldName + "]"); } @@ -134,10 +137,10 @@ public static SpanFirstQueryBuilder fromXContent(XContentParser parser) throws I } } if (match == null) { - throw new ParsingException(parser.getTokenLocation(), "spanFirst must have [match] span query clause"); + throw new ParsingException(parser.getTokenLocation(), "span_first must have [match] span query clause"); } if (end == null) { - throw new ParsingException(parser.getTokenLocation(), "spanFirst must have [end] set for it"); + throw new ParsingException(parser.getTokenLocation(), "span_first must have [end] set for it"); } SpanFirstQueryBuilder queryBuilder = new SpanFirstQueryBuilder(match, end); queryBuilder.boost(boost).queryName(queryName); diff --git a/server/src/main/java/org/elasticsearch/index/query/SpanNearQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/SpanNearQueryBuilder.java index ceeef6112ae46..d43c8120fe0c5 100644 --- a/server/src/main/java/org/elasticsearch/index/query/SpanNearQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/SpanNearQueryBuilder.java @@ -38,6 +38,8 @@ import java.util.List; import java.util.Objects; +import static org.elasticsearch.index.query.SpanQueryBuilder.SpanQueryBuilderUtil.checkNoBoost; + /** * Matches spans which are near one another. One can specify slop, the maximum number * of intervening unmatched positions, as well as whether matches are required to be in-order. @@ -166,9 +168,11 @@ public static SpanNearQueryBuilder fromXContent(XContentParser parser) throws IO while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) { QueryBuilder query = parseInnerQueryBuilder(parser); if (query instanceof SpanQueryBuilder == false) { - throw new ParsingException(parser.getTokenLocation(), "spanNear [clauses] must be of type span query"); + throw new ParsingException(parser.getTokenLocation(), "span_near [clauses] must be of type span query"); } - clauses.add((SpanQueryBuilder) query); + final SpanQueryBuilder clause = (SpanQueryBuilder) query; + checkNoBoost(NAME, currentFieldName, parser, clause); + clauses.add(clause); } } else { throw new ParsingException(parser.getTokenLocation(), "[span_near] query does not support [" + currentFieldName + "]"); diff --git a/server/src/main/java/org/elasticsearch/index/query/SpanNotQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/SpanNotQueryBuilder.java index e65310d84a4c7..41e632b68f40c 100644 --- a/server/src/main/java/org/elasticsearch/index/query/SpanNotQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/SpanNotQueryBuilder.java @@ -32,6 +32,8 @@ import java.io.IOException; import java.util.Objects; +import static org.elasticsearch.index.query.SpanQueryBuilder.SpanQueryBuilderUtil.checkNoBoost; + public class SpanNotQueryBuilder extends AbstractQueryBuilder implements SpanQueryBuilder { public static final String NAME = "span_not"; @@ -181,15 +183,17 @@ public static SpanNotQueryBuilder fromXContent(XContentParser parser) throws IOE if (INCLUDE_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { QueryBuilder query = parseInnerQueryBuilder(parser); if (query instanceof SpanQueryBuilder == false) { - throw new ParsingException(parser.getTokenLocation(), "spanNot [include] must be of type span query"); + throw new ParsingException(parser.getTokenLocation(), "span_not [include] must be of type span query"); } include = (SpanQueryBuilder) query; + checkNoBoost(NAME, currentFieldName, parser, include); } else if (EXCLUDE_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { QueryBuilder query = parseInnerQueryBuilder(parser); if (query instanceof SpanQueryBuilder == false) { - throw new ParsingException(parser.getTokenLocation(), "spanNot [exclude] must be of type span query"); + throw new ParsingException(parser.getTokenLocation(), "span_not [exclude] must be of type span query"); } exclude = (SpanQueryBuilder) query; + checkNoBoost(NAME, currentFieldName, parser, exclude); } else { throw new ParsingException(parser.getTokenLocation(), "[span_not] query does not support [" + currentFieldName + "]"); } @@ -210,13 +214,13 @@ public static SpanNotQueryBuilder fromXContent(XContentParser parser) throws IOE } } if (include == null) { - throw new ParsingException(parser.getTokenLocation(), "spanNot must have [include] span query clause"); + throw new ParsingException(parser.getTokenLocation(), "span_not must have [include] span query clause"); } if (exclude == null) { - throw new ParsingException(parser.getTokenLocation(), "spanNot must have [exclude] span query clause"); + throw new ParsingException(parser.getTokenLocation(), "span_not must have [exclude] span query clause"); } if (dist != null && (pre != null || post != null)) { - throw new ParsingException(parser.getTokenLocation(), "spanNot can either use [dist] or [pre] & [post] (or none)"); + throw new ParsingException(parser.getTokenLocation(), "span_not can either use [dist] or [pre] & [post] (or none)"); } SpanNotQueryBuilder spanNotQuery = new SpanNotQueryBuilder(include, exclude); diff --git a/server/src/main/java/org/elasticsearch/index/query/SpanOrQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/SpanOrQueryBuilder.java index 3a44a8d2c1598..d9b2d9cf4be47 100644 --- a/server/src/main/java/org/elasticsearch/index/query/SpanOrQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/SpanOrQueryBuilder.java @@ -35,6 +35,8 @@ import java.util.List; import java.util.Objects; +import static org.elasticsearch.index.query.SpanQueryBuilder.SpanQueryBuilderUtil.checkNoBoost; + /** * Span query that matches the union of its clauses. Maps to {@link SpanOrQuery}. */ @@ -113,9 +115,11 @@ public static SpanOrQueryBuilder fromXContent(XContentParser parser) throws IOEx while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) { QueryBuilder query = parseInnerQueryBuilder(parser); if (query instanceof SpanQueryBuilder == false) { - throw new ParsingException(parser.getTokenLocation(), "spanOr [clauses] must be of type span query"); + throw new ParsingException(parser.getTokenLocation(), "span_or [clauses] must be of type span query"); } - clauses.add((SpanQueryBuilder) query); + final SpanQueryBuilder clause = (SpanQueryBuilder) query; + checkNoBoost(NAME, currentFieldName, parser, clause); + clauses.add(clause); } } else { throw new ParsingException(parser.getTokenLocation(), "[span_or] query does not support [" + currentFieldName + "]"); @@ -132,7 +136,7 @@ public static SpanOrQueryBuilder fromXContent(XContentParser parser) throws IOEx } if (clauses.isEmpty()) { - throw new ParsingException(parser.getTokenLocation(), "spanOr must include [clauses]"); + throw new ParsingException(parser.getTokenLocation(), "span_or must include [clauses]"); } SpanOrQueryBuilder queryBuilder = new SpanOrQueryBuilder(clauses.get(0)); diff --git a/server/src/main/java/org/elasticsearch/index/query/SpanQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/SpanQueryBuilder.java index fec1cac2696dd..f7bf784d6cf99 100644 --- a/server/src/main/java/org/elasticsearch/index/query/SpanQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/SpanQueryBuilder.java @@ -19,9 +19,37 @@ package org.elasticsearch.index.query; +import org.elasticsearch.common.ParsingException; +import org.elasticsearch.common.xcontent.XContentParser; + /** - * Marker interface for a specific type of {@link QueryBuilder} that allows to build span queries + * Marker interface for a specific type of {@link QueryBuilder} that allows to build span queries. */ public interface SpanQueryBuilder extends QueryBuilder { + class SpanQueryBuilderUtil { + private SpanQueryBuilderUtil() { + // utility class + } + + /** + * Checks boost value of a nested span clause is equal to {@link AbstractQueryBuilder#DEFAULT_BOOST}. + * + * @param queryName a query name + * @param fieldName a field name + * @param parser a parser + * @param clause a span query builder + * @throws ParsingException if query boost value isn't equal to {@link AbstractQueryBuilder#DEFAULT_BOOST} + */ + static void checkNoBoost(String queryName, String fieldName, XContentParser parser, SpanQueryBuilder clause) { + try { + if (clause.boost() != AbstractQueryBuilder.DEFAULT_BOOST) { + throw new ParsingException(parser.getTokenLocation(), queryName + " [" + fieldName + "] " + + "as a nested span clause can't have non-default boost value [" + clause.boost() + "]"); + } + } catch (UnsupportedOperationException ignored) { + // if boost is unsupported it can't have been set + } + } + } } diff --git a/server/src/main/java/org/elasticsearch/index/query/SpanWithinQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/SpanWithinQueryBuilder.java index a454dd0fb521b..8f970fc25c165 100644 --- a/server/src/main/java/org/elasticsearch/index/query/SpanWithinQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/SpanWithinQueryBuilder.java @@ -32,6 +32,8 @@ import java.io.IOException; import java.util.Objects; +import static org.elasticsearch.index.query.SpanQueryBuilder.SpanQueryBuilderUtil.checkNoBoost; + /** * Builder for {@link org.apache.lucene.search.spans.SpanWithinQuery}. */ @@ -122,12 +124,14 @@ public static SpanWithinQueryBuilder fromXContent(XContentParser parser) throws throw new ParsingException(parser.getTokenLocation(), "span_within [big] must be of type span query"); } big = (SpanQueryBuilder) query; + checkNoBoost(NAME, currentFieldName, parser, big); } else if (LITTLE_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { QueryBuilder query = parseInnerQueryBuilder(parser); if (query instanceof SpanQueryBuilder == false) { throw new ParsingException(parser.getTokenLocation(), "span_within [little] must be of type span query"); } little = (SpanQueryBuilder) query; + checkNoBoost(NAME, currentFieldName, parser, little); } else { throw new ParsingException(parser.getTokenLocation(), "[span_within] query does not support [" + currentFieldName + "]"); diff --git a/server/src/test/java/org/elasticsearch/index/query/SpanContainingQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/SpanContainingQueryBuilderTests.java index 153958b9af3e0..e6e62d2909a2a 100644 --- a/server/src/test/java/org/elasticsearch/index/query/SpanContainingQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/SpanContainingQueryBuilderTests.java @@ -21,11 +21,13 @@ import org.apache.lucene.search.Query; import org.apache.lucene.search.spans.SpanContainingQuery; +import org.elasticsearch.common.ParsingException; import org.elasticsearch.search.internal.SearchContext; import org.elasticsearch.test.AbstractQueryTestCase; import java.io.IOException; +import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.instanceOf; public class SpanContainingQueryBuilderTests extends AbstractQueryTestCase { @@ -80,7 +82,7 @@ public void testFromJson() throws IOException { " }\n" + " }\n" + " },\n" + - " \"boost\" : 1.0\n" + + " \"boost\" : 2.0\n" + " }\n" + "}"; @@ -89,5 +91,92 @@ public void testFromJson() throws IOException { assertEquals(json, 2, ((SpanNearQueryBuilder) parsed.bigQuery()).clauses().size()); assertEquals(json, "foo", ((SpanTermQueryBuilder) parsed.littleQuery()).value()); + assertEquals(json, 2.0, parsed.boost(), 0.0); + } + + public void testFromJsoWithNonDefaultBoostInBigQuery() { + String json = + "{\n" + + " \"span_containing\" : {\n" + + " \"big\" : {\n" + + " \"span_near\" : {\n" + + " \"clauses\" : [ {\n" + + " \"span_term\" : {\n" + + " \"field1\" : {\n" + + " \"value\" : \"bar\",\n" + + " \"boost\" : 1.0\n" + + " }\n" + + " }\n" + + " }, {\n" + + " \"span_term\" : {\n" + + " \"field1\" : {\n" + + " \"value\" : \"baz\",\n" + + " \"boost\" : 1.0\n" + + " }\n" + + " }\n" + + " } ],\n" + + " \"slop\" : 5,\n" + + " \"in_order\" : true,\n" + + " \"boost\" : 2.0\n" + + " }\n" + + " },\n" + + " \"little\" : {\n" + + " \"span_term\" : {\n" + + " \"field1\" : {\n" + + " \"value\" : \"foo\",\n" + + " \"boost\" : 1.0\n" + + " }\n" + + " }\n" + + " },\n" + + " \"boost\" : 1.0\n" + + " }\n" + + "}"; + + Exception exception = expectThrows(ParsingException.class, () -> parseQuery(json)); + assertThat(exception.getMessage(), + equalTo("span_containing [big] as a nested span clause can't have non-default boost value [2.0]")); + } + + public void testFromJsonWithNonDefaultBoostInLittleQuery() { + String json = + "{\n" + + " \"span_containing\" : {\n" + + " \"little\" : {\n" + + " \"span_near\" : {\n" + + " \"clauses\" : [ {\n" + + " \"span_term\" : {\n" + + " \"field1\" : {\n" + + " \"value\" : \"bar\",\n" + + " \"boost\" : 1.0\n" + + " }\n" + + " }\n" + + " }, {\n" + + " \"span_term\" : {\n" + + " \"field1\" : {\n" + + " \"value\" : \"baz\",\n" + + " \"boost\" : 1.0\n" + + " }\n" + + " }\n" + + " } ],\n" + + " \"slop\" : 5,\n" + + " \"in_order\" : true,\n" + + " \"boost\" : 2.0\n" + + " }\n" + + " },\n" + + " \"big\" : {\n" + + " \"span_term\" : {\n" + + " \"field1\" : {\n" + + " \"value\" : \"foo\",\n" + + " \"boost\" : 1.0\n" + + " }\n" + + " }\n" + + " },\n" + + " \"boost\" : 1.0\n" + + " }\n" + + "}"; + + Exception exception = expectThrows(ParsingException.class, () -> parseQuery(json)); + assertThat(exception.getMessage(), + equalTo("span_containing [little] as a nested span clause can't have non-default boost value [2.0]")); } } diff --git a/server/src/test/java/org/elasticsearch/index/query/SpanFirstQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/SpanFirstQueryBuilderTests.java index 71539939c8c22..2ac3610f2d670 100644 --- a/server/src/test/java/org/elasticsearch/index/query/SpanFirstQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/SpanFirstQueryBuilderTests.java @@ -31,6 +31,7 @@ import java.io.IOException; import static org.elasticsearch.index.query.QueryBuilders.spanTermQuery; +import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.instanceOf; public class SpanFirstQueryBuilderTests extends AbstractQueryTestCase { @@ -59,7 +60,7 @@ public void testParseEnd() throws IOException { builder.endObject(); ParsingException e = expectThrows(ParsingException.class, () -> parseQuery(Strings.toString(builder))); - assertTrue(e.getMessage().contains("spanFirst must have [end] set")); + assertTrue(e.getMessage().contains("span_first must have [end] set")); } { XContentBuilder builder = XContentFactory.jsonBuilder(); @@ -70,7 +71,7 @@ public void testParseEnd() throws IOException { builder.endObject(); ParsingException e = expectThrows(ParsingException.class, () -> parseQuery(Strings.toString(builder))); - assertTrue(e.getMessage().contains("spanFirst must have [match] span query clause")); + assertTrue(e.getMessage().contains("span_first must have [match] span query clause")); } } @@ -97,4 +98,27 @@ public void testFromJson() throws IOException { assertEquals(json, 3, parsed.end()); assertEquals(json, "kimchy", ((SpanTermQueryBuilder) parsed.innerQuery()).value()); } + + + public void testFromJsonWithNonDefaultBoostInMatchQuery() { + String json = + "{\n" + + " \"span_first\" : {\n" + + " \"match\" : {\n" + + " \"span_term\" : {\n" + + " \"user\" : {\n" + + " \"value\" : \"kimchy\",\n" + + " \"boost\" : 2.0\n" + + " }\n" + + " }\n" + + " },\n" + + " \"end\" : 3,\n" + + " \"boost\" : 1.0\n" + + " }\n" + + "}"; + + Exception exception = expectThrows(ParsingException.class, () -> parseQuery(json)); + assertThat(exception.getMessage(), + equalTo("span_first [match] as a nested span clause can't have non-default boost value [2.0]")); + } } diff --git a/server/src/test/java/org/elasticsearch/index/query/SpanNearQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/SpanNearQueryBuilderTests.java index 0e22f33db77c7..cde83fb6f7424 100644 --- a/server/src/test/java/org/elasticsearch/index/query/SpanNearQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/SpanNearQueryBuilderTests.java @@ -114,7 +114,7 @@ public void testFromJson() throws IOException { " } ],\n" + " \"slop\" : 12,\n" + " \"in_order\" : false,\n" + - " \"boost\" : 1.0\n" + + " \"boost\" : 2.0\n" + " }\n" + "}"; @@ -124,6 +124,7 @@ public void testFromJson() throws IOException { assertEquals(json, 3, parsed.clauses().size()); assertEquals(json, 12, parsed.slop()); assertEquals(json, false, parsed.inOrder()); + assertEquals(json, 2.0, parsed.boost(), 0.0); } public void testParsingSlopDefault() throws IOException { @@ -187,4 +188,40 @@ public void testCollectPayloadsNoLongerSupported() throws Exception { assertThat(e.getMessage(), containsString("[span_near] query does not support [collect_payloads]")); } + public void testFromJsonWithNonDefaultBoostInInnerQuery() { + String json = + "{\n" + + " \"span_near\" : {\n" + + " \"clauses\" : [ {\n" + + " \"span_term\" : {\n" + + " \"field\" : {\n" + + " \"value\" : \"value1\",\n" + + " \"boost\" : 2.0\n" + + " }\n" + + " }\n" + + " }, {\n" + + " \"span_term\" : {\n" + + " \"field\" : {\n" + + " \"value\" : \"value2\",\n" + + " \"boost\" : 1.0\n" + + " }\n" + + " }\n" + + " }, {\n" + + " \"span_term\" : {\n" + + " \"field\" : {\n" + + " \"value\" : \"value3\",\n" + + " \"boost\" : 1.0\n" + + " }\n" + + " }\n" + + " } ],\n" + + " \"slop\" : 12,\n" + + " \"in_order\" : false,\n" + + " \"boost\" : 1.0\n" + + " }\n" + + "}"; + + Exception exception = expectThrows(ParsingException.class, () -> parseQuery(json)); + assertThat(exception.getMessage(), + equalTo("span_near [clauses] as a nested span clause can't have non-default boost value [2.0]")); + } } diff --git a/server/src/test/java/org/elasticsearch/index/query/SpanNotQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/SpanNotQueryBuilderTests.java index 1ffa85de0ecdf..7df58553e2768 100644 --- a/server/src/test/java/org/elasticsearch/index/query/SpanNotQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/SpanNotQueryBuilderTests.java @@ -132,7 +132,7 @@ public void testParserExceptions() throws IOException { builder.endObject(); ParsingException e = expectThrows(ParsingException.class, () -> parseQuery(Strings.toString(builder))); - assertThat(e.getDetailedMessage(), containsString("spanNot must have [include]")); + assertThat(e.getDetailedMessage(), containsString("span_not must have [include]")); } { XContentBuilder builder = XContentFactory.jsonBuilder(); @@ -146,7 +146,7 @@ public void testParserExceptions() throws IOException { builder.endObject(); ParsingException e = expectThrows(ParsingException.class, () -> parseQuery(Strings.toString(builder))); - assertThat(e.getDetailedMessage(), containsString("spanNot must have [exclude]")); + assertThat(e.getDetailedMessage(), containsString("span_not must have [exclude]")); } { XContentBuilder builder = XContentFactory.jsonBuilder(); @@ -163,7 +163,7 @@ public void testParserExceptions() throws IOException { builder.endObject(); ParsingException e = expectThrows(ParsingException.class, () -> parseQuery(Strings.toString(builder))); - assertThat(e.getDetailedMessage(), containsString("spanNot can either use [dist] or [pre] & [post] (or none)")); + assertThat(e.getDetailedMessage(), containsString("span_not can either use [dist] or [pre] & [post] (or none)")); } } @@ -203,7 +203,7 @@ public void testFromJson() throws IOException { " },\n" + " \"pre\" : 0,\n" + " \"post\" : 0,\n" + - " \"boost\" : 1.0\n" + + " \"boost\" : 2.0\n" + " }\n" + "}"; @@ -212,5 +212,97 @@ public void testFromJson() throws IOException { assertEquals(json, "hoya", ((SpanTermQueryBuilder) parsed.includeQuery()).value()); assertEquals(json, 2, ((SpanNearQueryBuilder) parsed.excludeQuery()).clauses().size()); + assertEquals(json, 2.0, parsed.boost(), 0.0); + } + + public void testFromJsonWithNonDefaultBoostInIncludeQuery() { + String json = + "{\n" + + " \"span_not\" : {\n" + + " \"exclude\" : {\n" + + " \"span_term\" : {\n" + + " \"field1\" : {\n" + + " \"value\" : \"hoya\",\n" + + " \"boost\" : 1.0\n" + + " }\n" + + " }\n" + + " },\n" + + " \"include\" : {\n" + + " \"span_near\" : {\n" + + " \"clauses\" : [ {\n" + + " \"span_term\" : {\n" + + " \"field1\" : {\n" + + " \"value\" : \"la\",\n" + + " \"boost\" : 1.0\n" + + " }\n" + + " }\n" + + " }, {\n" + + " \"span_term\" : {\n" + + " \"field1\" : {\n" + + " \"value\" : \"hoya\",\n" + + " \"boost\" : 1.0\n" + + " }\n" + + " }\n" + + " } ],\n" + + " \"slop\" : 0,\n" + + " \"in_order\" : true,\n" + + " \"boost\" : 2.0\n" + + " }\n" + + " },\n" + + " \"pre\" : 0,\n" + + " \"post\" : 0,\n" + + " \"boost\" : 1.0\n" + + " }\n" + + "}"; + + Exception exception = expectThrows(ParsingException.class, () -> parseQuery(json)); + assertThat(exception.getMessage(), + equalTo("span_not [include] as a nested span clause can't have non-default boost value [2.0]")); + } + + + public void testFromJsonWithNonDefaultBoostInExcludeQuery() { + String json = + "{\n" + + " \"span_not\" : {\n" + + " \"include\" : {\n" + + " \"span_term\" : {\n" + + " \"field1\" : {\n" + + " \"value\" : \"hoya\",\n" + + " \"boost\" : 1.0\n" + + " }\n" + + " }\n" + + " },\n" + + " \"exclude\" : {\n" + + " \"span_near\" : {\n" + + " \"clauses\" : [ {\n" + + " \"span_term\" : {\n" + + " \"field1\" : {\n" + + " \"value\" : \"la\",\n" + + " \"boost\" : 1.0\n" + + " }\n" + + " }\n" + + " }, {\n" + + " \"span_term\" : {\n" + + " \"field1\" : {\n" + + " \"value\" : \"hoya\",\n" + + " \"boost\" : 1.0\n" + + " }\n" + + " }\n" + + " } ],\n" + + " \"slop\" : 0,\n" + + " \"in_order\" : true,\n" + + " \"boost\" : 2.0\n" + + " }\n" + + " },\n" + + " \"pre\" : 0,\n" + + " \"post\" : 0,\n" + + " \"boost\" : 1.0\n" + + " }\n" + + "}"; + + Exception exception = expectThrows(ParsingException.class, () -> parseQuery(json)); + assertThat(exception.getMessage(), + equalTo("span_not [exclude] as a nested span clause can't have non-default boost value [2.0]")); } } diff --git a/server/src/test/java/org/elasticsearch/index/query/SpanOrQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/SpanOrQueryBuilderTests.java index a480681a5f456..9497cebc4ce2f 100644 --- a/server/src/test/java/org/elasticsearch/index/query/SpanOrQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/SpanOrQueryBuilderTests.java @@ -22,6 +22,7 @@ import org.apache.lucene.search.Query; import org.apache.lucene.search.spans.SpanOrQuery; import org.apache.lucene.search.spans.SpanQuery; +import org.elasticsearch.common.ParsingException; import org.elasticsearch.search.internal.SearchContext; import org.elasticsearch.test.AbstractQueryTestCase; @@ -94,7 +95,7 @@ public void testFromJson() throws IOException { " }\n" + " }\n" + " } ],\n" + - " \"boost\" : 1.0\n" + + " \"boost\" : 2.0\n" + " }\n" + "}"; @@ -102,5 +103,27 @@ public void testFromJson() throws IOException { checkGeneratedJson(json, parsed); assertEquals(json, 3, parsed.clauses().size()); + assertEquals(json, 2.0, parsed.boost(), 0.0); + } + + public void testFromJsonWithNonDefaultBoostInInnerQuery() { + String json = + "{\n" + + " \"span_or\" : {\n" + + " \"clauses\" : [ {\n" + + " \"span_term\" : {\n" + + " \"field\" : {\n" + + " \"value\" : \"value1\",\n" + + " \"boost\" : 2.0\n" + + " }\n" + + " }\n" + + " } ],\n" + + " \"boost\" : 1.0\n" + + " }\n" + + "}"; + + Exception exception = expectThrows(ParsingException.class, () -> parseQuery(json)); + assertThat(exception.getMessage(), + equalTo("span_or [clauses] as a nested span clause can't have non-default boost value [2.0]")); } } diff --git a/server/src/test/java/org/elasticsearch/index/query/SpanTermQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/SpanTermQueryBuilderTests.java index 34dc08d29fb9f..a5ef596e02558 100644 --- a/server/src/test/java/org/elasticsearch/index/query/SpanTermQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/SpanTermQueryBuilderTests.java @@ -76,19 +76,16 @@ protected void doAssertLuceneQuery(SpanTermQueryBuilder queryBuilder, Query quer } /** - * @param amount the number of clauses that will be returned - * @return an array of random {@link SpanTermQueryBuilder} with same field name + * @param amount a number of clauses that will be returned + * @return the array of random {@link SpanTermQueryBuilder} with same field name */ public SpanTermQueryBuilder[] createSpanTermQueryBuilders(int amount) { SpanTermQueryBuilder[] clauses = new SpanTermQueryBuilder[amount]; - SpanTermQueryBuilder first = createTestQueryBuilder(); + SpanTermQueryBuilder first = createTestQueryBuilder(false, true); clauses[0] = first; for (int i = 1; i < amount; i++) { // we need same field name in all clauses, so we only randomize value SpanTermQueryBuilder spanTermQuery = new SpanTermQueryBuilder(first.fieldName(), getRandomValueForFieldName(first.fieldName())); - if (randomBoolean()) { - spanTermQuery.boost(2.0f / randomIntBetween(1, 20)); - } if (randomBoolean()) { spanTermQuery.queryName(randomAlphaOfLengthBetween(1, 10)); } diff --git a/server/src/test/java/org/elasticsearch/index/query/SpanWithinQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/SpanWithinQueryBuilderTests.java index c897b5ee6a05a..a288e2430235a 100644 --- a/server/src/test/java/org/elasticsearch/index/query/SpanWithinQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/SpanWithinQueryBuilderTests.java @@ -21,11 +21,13 @@ import org.apache.lucene.search.Query; import org.apache.lucene.search.spans.SpanWithinQuery; +import org.elasticsearch.common.ParsingException; import org.elasticsearch.search.internal.SearchContext; import org.elasticsearch.test.AbstractQueryTestCase; import java.io.IOException; +import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.instanceOf; public class SpanWithinQueryBuilderTests extends AbstractQueryTestCase { @@ -80,7 +82,7 @@ public void testFromJson() throws IOException { " }\n" + " }\n" + " },\n" + - " \"boost\" : 1.0\n" + + " \"boost\" : 2.0\n" + " }\n" + "}"; @@ -89,5 +91,92 @@ public void testFromJson() throws IOException { assertEquals(json, "foo", ((SpanTermQueryBuilder) parsed.littleQuery()).value()); assertEquals(json, 2, ((SpanNearQueryBuilder) parsed.bigQuery()).clauses().size()); + assertEquals(json, 2.0, parsed.boost(), 0.0); + } + + public void testFromJsonWithNonDefaultBoostInBigQuery() { + String json = + "{\n" + + " \"span_within\" : {\n" + + " \"big\" : {\n" + + " \"span_near\" : {\n" + + " \"clauses\" : [ {\n" + + " \"span_term\" : {\n" + + " \"field1\" : {\n" + + " \"value\" : \"bar\",\n" + + " \"boost\" : 1.0\n" + + " }\n" + + " }\n" + + " }, {\n" + + " \"span_term\" : {\n" + + " \"field1\" : {\n" + + " \"value\" : \"baz\",\n" + + " \"boost\" : 1.0\n" + + " }\n" + + " }\n" + + " } ],\n" + + " \"slop\" : 5,\n" + + " \"in_order\" : true,\n" + + " \"boost\" : 2.0\n" + + " }\n" + + " },\n" + + " \"little\" : {\n" + + " \"span_term\" : {\n" + + " \"field1\" : {\n" + + " \"value\" : \"foo\",\n" + + " \"boost\" : 1.0\n" + + " }\n" + + " }\n" + + " },\n" + + " \"boost\" : 1.0\n" + + " }\n" + + "}"; + + Exception exception = expectThrows(ParsingException.class, () -> parseQuery(json)); + assertThat(exception.getMessage(), + equalTo("span_within [big] as a nested span clause can't have non-default boost value [2.0]")); + } + + public void testFromJsonWithNonDefaultBoostInLittleQuery() { + String json = + "{\n" + + " \"span_within\" : {\n" + + " \"little\" : {\n" + + " \"span_near\" : {\n" + + " \"clauses\" : [ {\n" + + " \"span_term\" : {\n" + + " \"field1\" : {\n" + + " \"value\" : \"bar\",\n" + + " \"boost\" : 1.0\n" + + " }\n" + + " }\n" + + " }, {\n" + + " \"span_term\" : {\n" + + " \"field1\" : {\n" + + " \"value\" : \"baz\",\n" + + " \"boost\" : 1.0\n" + + " }\n" + + " }\n" + + " } ],\n" + + " \"slop\" : 5,\n" + + " \"in_order\" : true,\n" + + " \"boost\" : 2.0\n" + + " }\n" + + " },\n" + + " \"big\" : {\n" + + " \"span_term\" : {\n" + + " \"field1\" : {\n" + + " \"value\" : \"foo\",\n" + + " \"boost\" : 1.0\n" + + " }\n" + + " }\n" + + " },\n" + + " \"boost\" : 1.0\n" + + " }\n" + + "}"; + + Exception exception = expectThrows(ParsingException.class, () -> parseQuery(json)); + assertThat(exception.getMessage(), + equalTo("span_within [little] as a nested span clause can't have non-default boost value [2.0]")); } } diff --git a/server/src/test/java/org/elasticsearch/index/query/WrapperQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/WrapperQueryBuilderTests.java index d9c6b89e8df16..7c5aa31722b34 100644 --- a/server/src/test/java/org/elasticsearch/index/query/WrapperQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/WrapperQueryBuilderTests.java @@ -39,7 +39,12 @@ public class WrapperQueryBuilderTests extends AbstractQueryTestCase { @Override - protected boolean supportsBoostAndQueryName() { + protected boolean supportsBoost() { + return false; + } + + @Override + protected boolean supportsQueryName() { return false; } diff --git a/test/framework/src/main/java/org/elasticsearch/test/AbstractQueryTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/AbstractQueryTestCase.java index 3393ed51fec50..027152deedba6 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/AbstractQueryTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/AbstractQueryTestCase.java @@ -20,7 +20,6 @@ package org.elasticsearch.test; import com.fasterxml.jackson.core.io.JsonStringEncoder; - import org.apache.lucene.search.BoostQuery; import org.apache.lucene.search.Query; import org.apache.lucene.search.TermQuery; @@ -82,15 +81,16 @@ public abstract class AbstractQueryTestCase> private static final int NUMBER_OF_TESTQUERIES = 20; public final QB createTestQueryBuilder() { + return createTestQueryBuilder(supportsBoost(), supportsQueryName()); + } + + public final QB createTestQueryBuilder(boolean supportsBoost, boolean supportsQueryName) { QB query = doCreateTestQueryBuilder(); - //we should not set boost and query name for queries that don't parse it - if (supportsBoostAndQueryName()) { - if (randomBoolean()) { - query.boost(2.0f / randomIntBetween(1, 20)); - } - if (randomBoolean()) { - query.queryName(createUniqueRandomName()); - } + if (supportsBoost && randomBoolean()) { + query.boost(2.0f / randomIntBetween(1, 20)); + } + if (supportsQueryName && randomBoolean()) { + query.queryName(createUniqueRandomName()); } return query; } @@ -459,7 +459,7 @@ public void testToQuery() throws IOException { rewrite(secondLuceneQuery), rewrite(firstLuceneQuery)); } - if (supportsBoostAndQueryName()) { + if (supportsBoost()) { secondQuery.boost(firstQuery.boost() + 1f + randomFloat()); Query thirdLuceneQuery = rewriteQuery(secondQuery, context).toQuery(context); assertNotEquals("modifying the boost doesn't affect the corresponding lucene query", rewrite(firstLuceneQuery), @@ -486,12 +486,22 @@ protected boolean isCachable(QB queryBuilder) { } /** - * Few queries allow you to set the boost and queryName on the java api, although the corresponding parser - * doesn't parse them as they are not supported. This method allows to disable boost and queryName related tests for those queries. - * Those queries are easy to identify: their parsers don't parse `boost` and `_name` as they don't apply to the specific query: - * wrapper query and match_none + * Few queries allow you to set the boost on the Java API, although the corresponding parser + * doesn't parse it as it isn't supported. This method allows to disable boost related tests for those queries. + * Those queries are easy to identify: their parsers don't parse {@code boost} as they don't apply to the specific query: + * wrapper query and {@code match_none}. + */ + protected boolean supportsBoost() { + return true; + } + + /** + * Few queries allow you to set the query name on the Java API, although the corresponding parser + * doesn't parse it as it isn't supported. This method allows to disable query name related tests for those queries. + * Those queries are easy to identify: their parsers don't parse {@code _name} as they don't apply to the specific query: + * wrapper query and {@code match_none}. */ - protected boolean supportsBoostAndQueryName() { + protected boolean supportsQueryName() { return true; } From a2b78e0eff7540b818dbcbd22b9b9e143457d1e6 Mon Sep 17 00:00:00 2001 From: Jake Landis Date: Thu, 29 Nov 2018 11:45:45 -0600 Subject: [PATCH 035/115] Support content type `application/x-ndjson` in DeprecationRestHandler (#36025) org.elasticsearch.rest.RestController#hasContentType checks to see if the RestHandler supports the `application/x-ndjson` Content-Type. DeprecationRestHandler is a wrapper around the real RestHandler, and prior to this change would always return `false` due to the interface's default supportsContentStream(). This prevents API's that use multi-line JSON from properly being deprecated resulting in an HTTP 406 error. This change ensures that the DeprecationRestHandler honors the supportsContentStream() of the wrapped RestHandler. Relates to #35958 --- .../rest/DeprecationRestHandler.java | 5 +++++ .../rest/DeprecationRestHandlerTests.java | 22 +++++++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/rest/DeprecationRestHandler.java b/server/src/main/java/org/elasticsearch/rest/DeprecationRestHandler.java index 1a78a4c8c0452..2408f83d2b54e 100644 --- a/server/src/main/java/org/elasticsearch/rest/DeprecationRestHandler.java +++ b/server/src/main/java/org/elasticsearch/rest/DeprecationRestHandler.java @@ -62,6 +62,11 @@ public void handleRequest(RestRequest request, RestChannel channel, NodeClient c handler.handleRequest(request, channel, client); } + @Override + public boolean supportsContentStream() { + return handler.supportsContentStream(); + } + /** * This does a very basic pass at validating that a header's value contains only expected characters according to RFC-5987, and those * that it references. diff --git a/server/src/test/java/org/elasticsearch/rest/DeprecationRestHandlerTests.java b/server/src/test/java/org/elasticsearch/rest/DeprecationRestHandlerTests.java index 0a8ace71d8860..0dc108ff587cd 100644 --- a/server/src/test/java/org/elasticsearch/rest/DeprecationRestHandlerTests.java +++ b/server/src/test/java/org/elasticsearch/rest/DeprecationRestHandlerTests.java @@ -24,22 +24,30 @@ import org.elasticsearch.common.logging.DeprecationLogger; import org.elasticsearch.test.ESTestCase; +import org.junit.Before; import org.mockito.InOrder; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; /** * Tests {@link DeprecationRestHandler}. */ public class DeprecationRestHandlerTests extends ESTestCase { - private final RestHandler handler = mock(RestHandler.class); + private RestHandler handler; /** * Note: Headers should only use US ASCII (and this inevitably becomes one!). */ private final String deprecationMessage = randomAlphaOfLengthBetween(1, 30); - private final DeprecationLogger deprecationLogger = mock(DeprecationLogger.class); + private DeprecationLogger deprecationLogger; + + @Before + public void setup() { + handler = mock(RestHandler.class); + deprecationLogger = mock(DeprecationLogger.class); + } public void testNullHandler() { expectThrows(NullPointerException.class, () -> new DeprecationRestHandler(null, deprecationMessage, deprecationLogger)); @@ -114,6 +122,16 @@ public void testInvalidHeaderValueEmpty() { expectThrows(IllegalArgumentException.class, () -> DeprecationRestHandler.requireValidHeader(blank)); } + public void testSupportsContentStreamTrue() { + when(handler.supportsContentStream()).thenReturn(true); + assertTrue(new DeprecationRestHandler(handler, deprecationMessage, deprecationLogger).supportsContentStream()); + } + + public void testSupportsContentStreamFalse() { + when(handler.supportsContentStream()).thenReturn(false); + assertFalse(new DeprecationRestHandler(handler, deprecationMessage, deprecationLogger).supportsContentStream()); + } + /** * {@code ASCIIHeaderGenerator} only uses characters expected to be valid in headers (simplified US-ASCII). */ From 7749b34aeb8f07990ac3010b036d1cf525a6cbce Mon Sep 17 00:00:00 2001 From: Tal Levy Date: Thu, 29 Nov 2018 14:11:51 -0800 Subject: [PATCH 036/115] [ILM] rest-spec for remove-policy had wrong link (#36083) --- .../src/test/resources/rest-api-spec/api/ilm.remove_policy.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/api/ilm.remove_policy.json b/x-pack/plugin/src/test/resources/rest-api-spec/api/ilm.remove_policy.json index 41f4883b8e793..de3591d60269e 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/api/ilm.remove_policy.json +++ b/x-pack/plugin/src/test/resources/rest-api-spec/api/ilm.remove_policy.json @@ -1,6 +1,6 @@ { "ilm.remove_policy": { - "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/ilm-delete-policy.html", + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/ilm-remove-policy.html", "methods": [ "POST" ], "url": { "path": "/{index}/_ilm/remove", From f016a9d8e821bcdb949ec599d97d803ed4b05ba2 Mon Sep 17 00:00:00 2001 From: Tim Brooks Date: Thu, 29 Nov 2018 18:18:05 -0700 Subject: [PATCH 037/115] Introduce `TransportLogger` for common logging (#36091) Historically we have had a ESLoggingHandler in the netty module that logs low-level connection operations. This class just extends the netty logging handler with some (broken) message deserialization. This commit fixes this message serialization and moves the class to server. This new logger logs inbound and outbound messages. Eventually, we should move other event logging to this class (connect, close, flush). That way we will have consistent logging regards of which transport is loaded. --- .../transport/netty4/ESLoggingHandler.java | 6 + .../transport/netty4/Netty4Utils.java | 1 - .../transport/nio/NioTransportLoggingIT.java | 79 ++++++++++++ .../elasticsearch/transport/TcpTransport.java | 7 +- .../transport/TransportLogger.java | 118 ++++++++++++++++++ .../transport/TransportLoggerTests.java | 117 +++++++++++++++++ 6 files changed, 325 insertions(+), 3 deletions(-) create mode 100644 plugins/transport-nio/src/test/java/org/elasticsearch/transport/nio/NioTransportLoggingIT.java create mode 100644 server/src/main/java/org/elasticsearch/transport/TransportLogger.java create mode 100644 server/src/test/java/org/elasticsearch/transport/TransportLoggerTests.java diff --git a/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/ESLoggingHandler.java b/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/ESLoggingHandler.java index 1717797b4289e..3f4eb0695fac2 100644 --- a/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/ESLoggingHandler.java +++ b/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/ESLoggingHandler.java @@ -19,6 +19,7 @@ package org.elasticsearch.transport.netty4; +import io.netty.channel.ChannelHandlerContext; import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LoggingHandler; @@ -28,4 +29,9 @@ final class ESLoggingHandler extends LoggingHandler { super(LogLevel.TRACE); } + @Override + public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { + // We do not want to log read complete events because we log inbound messages in the TcpTransport. + ctx.fireChannelReadComplete(); + } } diff --git a/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/Netty4Utils.java b/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/Netty4Utils.java index 655dafdd28981..76d7864c71692 100644 --- a/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/Netty4Utils.java +++ b/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/Netty4Utils.java @@ -156,5 +156,4 @@ public static void closeChannels(final Collection channels) throws IOEx throw closingExceptions; } } - } diff --git a/plugins/transport-nio/src/test/java/org/elasticsearch/transport/nio/NioTransportLoggingIT.java b/plugins/transport-nio/src/test/java/org/elasticsearch/transport/nio/NioTransportLoggingIT.java new file mode 100644 index 0000000000000..b29df77cae1bb --- /dev/null +++ b/plugins/transport-nio/src/test/java/org/elasticsearch/transport/nio/NioTransportLoggingIT.java @@ -0,0 +1,79 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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.elasticsearch.transport.nio; + +import org.apache.logging.log4j.Level; +import org.elasticsearch.NioIntegTestCase; +import org.elasticsearch.action.admin.cluster.node.hotthreads.NodesHotThreadsRequest; +import org.elasticsearch.common.logging.Loggers; +import org.elasticsearch.test.ESIntegTestCase; +import org.elasticsearch.test.MockLogAppender; +import org.elasticsearch.test.junit.annotations.TestLogging; +import org.elasticsearch.transport.TransportLogger; + +@ESIntegTestCase.ClusterScope(numDataNodes = 2) +@TestLogging(value = "org.elasticsearch.transport.TransportLogger:trace") +public class NioTransportLoggingIT extends NioIntegTestCase { + + private MockLogAppender appender; + + public void setUp() throws Exception { + super.setUp(); + appender = new MockLogAppender(); + Loggers.addAppender(Loggers.getLogger(TransportLogger.class), appender); + appender.start(); + } + + public void tearDown() throws Exception { + Loggers.removeAppender(Loggers.getLogger(TransportLogger.class), appender); + appender.stop(); + super.tearDown(); + } + + public void testLoggingHandler() throws IllegalAccessException { + final String writePattern = + ".*\\[length: \\d+" + + ", request id: \\d+" + + ", type: request" + + ", version: .*" + + ", action: cluster:monitor/nodes/hot_threads\\[n\\]\\]" + + " WRITE: \\d+B"; + final MockLogAppender.LoggingExpectation writeExpectation = + new MockLogAppender.PatternSeenEventExcpectation( + "hot threads request", TransportLogger.class.getCanonicalName(), Level.TRACE, writePattern); + + final String readPattern = + ".*\\[length: \\d+" + + ", request id: \\d+" + + ", type: request" + + ", version: .*" + + ", action: cluster:monitor/nodes/hot_threads\\[n\\]\\]" + + " READ: \\d+B"; + + final MockLogAppender.LoggingExpectation readExpectation = + new MockLogAppender.PatternSeenEventExcpectation( + "hot threads request", TransportLogger.class.getCanonicalName(), Level.TRACE, readPattern); + + appender.addExpectation(writeExpectation); + appender.addExpectation(readExpectation); + client().admin().cluster().nodesHotThreads(new NodesHotThreadsRequest()).actionGet(); + appender.assertAllExpectationsMatched(); + } +} diff --git a/server/src/main/java/org/elasticsearch/transport/TcpTransport.java b/server/src/main/java/org/elasticsearch/transport/TcpTransport.java index 04b3d79352f28..72d3715e8defe 100644 --- a/server/src/main/java/org/elasticsearch/transport/TcpTransport.java +++ b/server/src/main/java/org/elasticsearch/transport/TcpTransport.java @@ -201,7 +201,7 @@ public abstract class TcpTransport extends AbstractLifecycleComponent implements private final MeanMetric transmittedBytesMetric = new MeanMetric(); private volatile Map> requestHandlers = Collections.emptyMap(); private final ResponseHandlers responseHandlers = new ResponseHandlers(); - + private final TransportLogger transportLogger; private final TcpTransportHandshaker handshaker; private final TransportKeepAlive keepAlive; private final String nodeName; @@ -220,6 +220,7 @@ public TcpTransport(String transportName, Settings settings, Version version, T this.compressResponses = Transport.TRANSPORT_TCP_COMPRESS.get(settings); this.networkService = networkService; this.transportName = transportName; + this.transportLogger = new TransportLogger(); this.handshaker = new TcpTransportHandshaker(version, threadPool, (node, channel, requestId, v) -> sendRequestToChannel(node, channel, requestId, TcpTransportHandshaker.HANDSHAKE_ACTION_NAME, TransportRequest.Empty.INSTANCE, TransportRequestOptions.EMPTY, v, @@ -819,6 +820,7 @@ private void sendRequestToChannel(final DiscoveryNode node, final TcpChannel cha */ private void internalSendMessage(TcpChannel channel, BytesReference message, ActionListener listener) { channel.getChannelStats().markAccessed(threadPool.relativeTimeInMillis()); + transportLogger.logOutboundMessage(channel, message); try { channel.sendMessage(message, new SendListener(channel, message.length(), listener)); } catch (Exception ex) { @@ -928,7 +930,7 @@ private void sendResponse( * @param length the payload length in bytes * @see TcpHeader */ - final BytesReference buildHeader(long requestId, byte status, Version protocolVersion, int length) throws IOException { + private BytesReference buildHeader(long requestId, byte status, Version protocolVersion, int length) throws IOException { try (BytesStreamOutput headerOutput = new BytesStreamOutput(TcpHeader.HEADER_SIZE)) { headerOutput.setVersion(protocolVersion); TcpHeader.writeHeader(headerOutput, requestId, status, protocolVersion, length); @@ -973,6 +975,7 @@ private BytesReference buildMessage(long requestId, byte status, Version nodeVer public void inboundMessage(TcpChannel channel, BytesReference message) { try { channel.getChannelStats().markAccessed(threadPool.relativeTimeInMillis()); + transportLogger.logInboundMessage(channel, message); // Message length of 0 is a ping if (message.length() != 0) { messageReceived(message, channel); diff --git a/server/src/main/java/org/elasticsearch/transport/TransportLogger.java b/server/src/main/java/org/elasticsearch/transport/TransportLogger.java new file mode 100644 index 0000000000000..ea01cc4ddbfa6 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/transport/TransportLogger.java @@ -0,0 +1,118 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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.elasticsearch.transport; + +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.LogManager; +import org.elasticsearch.Version; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.compress.Compressor; +import org.elasticsearch.common.compress.CompressorFactory; +import org.elasticsearch.common.compress.NotCompressedException; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.core.internal.io.IOUtils; + +import java.io.IOException; + +public final class TransportLogger { + + private static final Logger logger = LogManager.getLogger(TransportLogger.class); + private static final int HEADER_SIZE = TcpHeader.MARKER_BYTES_SIZE + TcpHeader.MESSAGE_LENGTH_SIZE; + + void logInboundMessage(TcpChannel channel, BytesReference message) { + if (logger.isTraceEnabled()) { + try { + String logMessage = format(channel, message, "READ"); + logger.trace(logMessage); + } catch (IOException e) { + logger.trace("an exception occurred formatting a READ trace message", e); + } + } + } + + void logOutboundMessage(TcpChannel channel, BytesReference message) { + if (logger.isTraceEnabled()) { + try { + BytesReference withoutHeader = message.slice(HEADER_SIZE, message.length() - HEADER_SIZE); + String logMessage = format(channel, withoutHeader, "WRITE"); + logger.trace(logMessage); + } catch (IOException e) { + logger.trace("an exception occurred formatting a WRITE trace message", e); + } + } + } + + private String format(TcpChannel channel, BytesReference message, String event) throws IOException { + final StringBuilder sb = new StringBuilder(); + sb.append(channel); + int messageLengthWithHeader = HEADER_SIZE + message.length(); + // This is a ping + if (message.length() == 0) { + sb.append(" [ping]").append(' ').append(event).append(": ").append(messageLengthWithHeader).append('B'); + } else { + boolean success = false; + StreamInput streamInput = message.streamInput(); + try { + final long requestId = streamInput.readLong(); + final byte status = streamInput.readByte(); + final boolean isRequest = TransportStatus.isRequest(status); + final String type = isRequest ? "request" : "response"; + final String version = Version.fromId(streamInput.readInt()).toString(); + sb.append(" [length: ").append(messageLengthWithHeader); + sb.append(", request id: ").append(requestId); + sb.append(", type: ").append(type); + sb.append(", version: ").append(version); + + if (isRequest) { + if (TransportStatus.isCompress(status)) { + Compressor compressor; + try { + final int bytesConsumed = TcpHeader.REQUEST_ID_SIZE + TcpHeader.STATUS_SIZE + TcpHeader.VERSION_ID_SIZE; + compressor = CompressorFactory.compressor(message.slice(bytesConsumed, message.length() - bytesConsumed)); + } catch (NotCompressedException ex) { + throw new IllegalStateException(ex); + } + streamInput = compressor.streamInput(streamInput); + } + + try (ThreadContext context = new ThreadContext(Settings.EMPTY)) { + context.readHeaders(streamInput); + } + // now we decode the features + if (streamInput.getVersion().onOrAfter(Version.V_6_3_0)) { + streamInput.readStringArray(); + } + sb.append(", action: ").append(streamInput.readString()); + } + sb.append(']'); + sb.append(' ').append(event).append(": ").append(messageLengthWithHeader).append('B'); + success = true; + } finally { + if (success) { + IOUtils.close(streamInput); + } else { + IOUtils.closeWhileHandlingException(streamInput); + } + } + } + return sb.toString(); + } +} diff --git a/server/src/test/java/org/elasticsearch/transport/TransportLoggerTests.java b/server/src/test/java/org/elasticsearch/transport/TransportLoggerTests.java new file mode 100644 index 0000000000000..ac58b0e25b91e --- /dev/null +++ b/server/src/test/java/org/elasticsearch/transport/TransportLoggerTests.java @@ -0,0 +1,117 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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.elasticsearch.transport; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.elasticsearch.Version; +import org.elasticsearch.action.admin.cluster.stats.ClusterStatsAction; +import org.elasticsearch.action.admin.cluster.stats.ClusterStatsRequest; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.bytes.CompositeBytesReference; +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.common.logging.Loggers; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.MockLogAppender; +import org.elasticsearch.test.junit.annotations.TestLogging; + +import java.io.IOException; + +import static org.mockito.Mockito.mock; + +@TestLogging(value = "org.elasticsearch.transport.TransportLogger:trace") +public class TransportLoggerTests extends ESTestCase { + + private MockLogAppender appender; + + public void setUp() throws Exception { + super.setUp(); + appender = new MockLogAppender(); + Loggers.addAppender(LogManager.getLogger(TransportLogger.class), appender); + appender.start(); + } + + public void tearDown() throws Exception { + Loggers.removeAppender(LogManager.getLogger(TransportLogger.class), appender); + appender.stop(); + super.tearDown(); + } + + public void testLoggingHandler() throws IOException { + TransportLogger transportLogger = new TransportLogger(); + + final String writePattern = + ".*\\[length: \\d+" + + ", request id: \\d+" + + ", type: request" + + ", version: .*" + + ", action: cluster:monitor/stats]" + + " WRITE: \\d+B"; + final MockLogAppender.LoggingExpectation writeExpectation = + new MockLogAppender.PatternSeenEventExcpectation( + "hot threads request", TransportLogger.class.getCanonicalName(), Level.TRACE, writePattern); + + final String readPattern = + ".*\\[length: \\d+" + + ", request id: \\d+" + + ", type: request" + + ", version: .*" + + ", action: cluster:monitor/stats]" + + " READ: \\d+B"; + + final MockLogAppender.LoggingExpectation readExpectation = + new MockLogAppender.PatternSeenEventExcpectation( + "cluster monitor request", TransportLogger.class.getCanonicalName(), Level.TRACE, readPattern); + + appender.addExpectation(writeExpectation); + appender.addExpectation(readExpectation); + BytesReference bytesReference = buildRequest(); + transportLogger.logInboundMessage(mock(TcpChannel.class), bytesReference.slice(6, bytesReference.length() - 6)); + transportLogger.logOutboundMessage(mock(TcpChannel.class), bytesReference); + appender.assertAllExpectationsMatched(); + } + + private BytesReference buildRequest() throws IOException { + try (BytesStreamOutput messageOutput = new BytesStreamOutput()) { + messageOutput.setVersion(Version.CURRENT); + try (ThreadContext context = new ThreadContext(Settings.EMPTY)) { + context.writeTo(messageOutput); + } + messageOutput.writeStringArray(new String[0]); + messageOutput.writeString(ClusterStatsAction.NAME); + new ClusterStatsRequest().writeTo(messageOutput); + BytesReference messageBody = messageOutput.bytes(); + final BytesReference header = buildHeader(randomInt(30), messageBody.length()); + return new CompositeBytesReference(header, messageBody); + } + } + + private BytesReference buildHeader(long requestId, int length) throws IOException { + try (BytesStreamOutput headerOutput = new BytesStreamOutput(TcpHeader.HEADER_SIZE)) { + headerOutput.setVersion(Version.CURRENT); + TcpHeader.writeHeader(headerOutput, requestId, TransportStatus.setRequest((byte) 0), Version.CURRENT, length); + final BytesReference bytes = headerOutput.bytes(); + assert bytes.length() == TcpHeader.HEADER_SIZE : "header size mismatch expected: " + TcpHeader.HEADER_SIZE + " but was: " + + bytes.length(); + return bytes; + } + } +} From b88bd44eb02efbae6f67693408ac0926454bc7b1 Mon Sep 17 00:00:00 2001 From: Ryan Ernst Date: Thu, 29 Nov 2018 18:58:00 -0800 Subject: [PATCH 038/115] Build: Remove padding from java version in build info (#36088) This commit removes padding from the java version in the output of compiler/runtime/gradle java versions. This value is only ever 1 or 2 digits, and the later information (hotspot/jvm vendor info) is separated by the java home path from the other versions, so there isn't a visual reason for needing exact vertical alignment. --- .../org/elasticsearch/gradle/BuildPlugin.groovy | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy b/buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy index 7c4dc958ae77b..8838aa6261e4f 100644 --- a/buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy +++ b/buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy @@ -155,14 +155,14 @@ class BuildPlugin implements Plugin { println " Gradle Version : ${project.gradle.gradleVersion}" println " OS Info : ${System.getProperty('os.name')} ${System.getProperty('os.version')} (${System.getProperty('os.arch')})" if (gradleJavaVersionDetails != compilerJavaVersionDetails || gradleJavaVersionDetails != runtimeJavaVersionDetails) { - println " Compiler JDK Version : ${getPaddedMajorVersion(compilerJavaVersionEnum)} (${compilerJavaVersionDetails})" + println " Compiler JDK Version : ${compilerJavaVersionEnum} (${compilerJavaVersionDetails})" println " Compiler java.home : ${compilerJavaHome}" - println " Runtime JDK Version : ${getPaddedMajorVersion(runtimeJavaVersionEnum)} (${runtimeJavaVersionDetails})" + println " Runtime JDK Version : ${runtimeJavaVersionEnum} (${runtimeJavaVersionDetails})" println " Runtime java.home : ${runtimeJavaHome}" - println " Gradle JDK Version : ${getPaddedMajorVersion(JavaVersion.toVersion(gradleJavaVersion))} (${gradleJavaVersionDetails})" + println " Gradle JDK Version : ${JavaVersion.toVersion(gradleJavaVersion)} (${gradleJavaVersionDetails})" println " Gradle java.home : ${gradleJavaHome}" } else { - println " JDK Version : ${getPaddedMajorVersion(JavaVersion.toVersion(gradleJavaVersion))} (${gradleJavaVersionDetails})" + println " JDK Version : ${JavaVersion.toVersion(gradleJavaVersion)} (${gradleJavaVersionDetails})" println " JAVA_HOME : ${gradleJavaHome}" } println " Random Testing Seed : ${project.testSeed}" @@ -232,10 +232,6 @@ class BuildPlugin implements Plugin { project.ext.java9Home = project.rootProject.ext.java9Home } - private static String getPaddedMajorVersion(JavaVersion compilerJavaVersionEnum) { - compilerJavaVersionEnum.getMajorVersion().toString().padLeft(2) - } - private static String findCompilerJavaHome() { final String compilerJavaHome = System.getenv('JAVA_HOME') final String compilerJavaProperty = System.getProperty('compiler.java') From 8a8ecdbb48be24f97ddfa18f438c8f9e9cf9fcde Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Fri, 30 Nov 2018 07:41:44 +0100 Subject: [PATCH 039/115] Fix serialization bug in painless execute api request (#36075) The `xContentType` was incorrectly serialized: `xContentType.mediaTypeWithoutParameters()` was used to serialize, but `xContentType.mediaType()` was used to de-serialize. Also the serialization test class did not test `ContextSetup` well, this was due to limitation in serialization test base class. Changed the test class to manually test xcontent serialization, so that for both binary and xcontent serialization tests, the `ContextSetup` is properly tested. Closes #36050 --- .../painless/PainlessExecuteAction.java | 9 ++- .../painless/PainlessExecuteRequestTests.java | 69 +++++++++++++------ 2 files changed, 54 insertions(+), 24 deletions(-) diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessExecuteAction.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessExecuteAction.java index 22f6cec453abb..0caa05cf60d83 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessExecuteAction.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessExecuteAction.java @@ -225,19 +225,20 @@ public boolean equals(Object o) { ContextSetup that = (ContextSetup) o; return Objects.equals(index, that.index) && Objects.equals(document, that.document) && - Objects.equals(query, that.query); + Objects.equals(query, that.query) && + Objects.equals(xContentType, that.xContentType); } @Override public int hashCode() { - return Objects.hash(index, document, query); + return Objects.hash(index, document, query, xContentType); } @Override public void writeTo(StreamOutput out) throws IOException { out.writeOptionalString(index); out.writeOptionalBytesReference(document); - out.writeOptionalString(xContentType != null ? xContentType.mediaType(): null); + out.writeOptionalString(xContentType != null ? xContentType.mediaTypeWithoutParameters(): null); out.writeOptionalNamedWriteable(query); } @@ -354,11 +355,13 @@ public void writeTo(StreamOutput out) throws IOException { // For testing only: @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); builder.field(SCRIPT_FIELD.getPreferredName(), script); builder.field(CONTEXT_FIELD.getPreferredName(), context.name); if (contextSetup != null) { builder.field(CONTEXT_SETUP_FIELD.getPreferredName(), contextSetup); } + builder.endObject(); return builder; } diff --git a/modules/lang-painless/src/test/java/org/elasticsearch/painless/PainlessExecuteRequestTests.java b/modules/lang-painless/src/test/java/org/elasticsearch/painless/PainlessExecuteRequestTests.java index e70d728091fab..4a7a5c77e1cfb 100644 --- a/modules/lang-painless/src/test/java/org/elasticsearch/painless/PainlessExecuteRequestTests.java +++ b/modules/lang-painless/src/test/java/org/elasticsearch/painless/PainlessExecuteRequestTests.java @@ -20,9 +20,14 @@ import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.LoggingDeprecationHandler; import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.XContent; +import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.query.MatchAllQueryBuilder; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.painless.PainlessExecuteAction.Request.ContextSetup; @@ -30,12 +35,40 @@ import org.elasticsearch.script.ScriptContext; import org.elasticsearch.script.ScriptType; import org.elasticsearch.search.SearchModule; -import org.elasticsearch.test.AbstractStreamableXContentTestCase; +import org.elasticsearch.test.AbstractStreamableTestCase; import java.io.IOException; +import java.io.UncheckedIOException; import java.util.Collections; -public class PainlessExecuteRequestTests extends AbstractStreamableXContentTestCase { +import static org.hamcrest.Matchers.equalTo; + +public class PainlessExecuteRequestTests extends AbstractStreamableTestCase { + + // Testing XContent serialization manually here, because the xContentType field in ContextSetup determines + // how the request needs to parse and the xcontent serialization framework randomizes that. The XContentType + // is not known and accessable when the test request instance is created in the xcontent serialization framework. + // Changing that is a big change. Writing a custom xcontent test here is the best option for now, because as far + // as I know this request class is the only case where this is a problem. + public final void testFromXContent() throws Exception { + for (int i = 0; i < 20; i++) { + PainlessExecuteAction.Request testInstance = createTestInstance(); + ContextSetup contextSetup = testInstance.getContextSetup(); + XContent xContent = randomFrom(XContentType.values()).xContent(); + if (contextSetup != null && contextSetup.getXContentType() != null) { + xContent = contextSetup.getXContentType().xContent(); + } + + try (XContentBuilder builder = XContentBuilder.builder(xContent)) { + builder.value(testInstance); + StreamInput instanceInput = BytesReference.bytes(builder).streamInput(); + try (XContentParser parser = xContent.createParser(xContentRegistry(), LoggingDeprecationHandler.INSTANCE, instanceInput)) { + PainlessExecuteAction.Request result = PainlessExecuteAction.Request.parse(parser); + assertThat(result, equalTo(testInstance)); + } + } + } + } @Override protected NamedWriteableRegistry getNamedWriteableRegistry() { @@ -60,16 +93,6 @@ protected PainlessExecuteAction.Request createBlankInstance() { return new PainlessExecuteAction.Request(); } - @Override - protected PainlessExecuteAction.Request doParseInstance(XContentParser parser) throws IOException { - return PainlessExecuteAction.Request.parse(parser); - } - - @Override - protected boolean supportsUnknownFields() { - return false; - } - public void testValidate() { Script script = new Script(ScriptType.STORED, null, randomAlphaOfLength(10), Collections.emptyMap()); PainlessExecuteAction.Request request = new PainlessExecuteAction.Request(script, null, null); @@ -78,20 +101,24 @@ public void testValidate() { assertEquals("Validation Failed: 1: only inline scripts are supported;", e.getMessage()); } - private static ContextSetup randomContextSetup() { + private static ContextSetup randomContextSetup() { String index = randomBoolean() ? randomAlphaOfLength(4) : null; QueryBuilder query = randomBoolean() ? new MatchAllQueryBuilder() : null; - // TODO: pass down XContextType to createTestInstance() method. - // otherwise the document itself is different causing test failures. - // This should be done in a separate change as the test instance is created before xcontent type is randomly picked and - // all the createTestInstance() methods need to be changed, which will make this a big chnage -// BytesReference doc = randomBoolean() ? new BytesArray("{}") : null; BytesReference doc = null; + XContentType xContentType = randomFrom(XContentType.values()); + if (randomBoolean()) { + try { + XContentBuilder xContentBuilder = XContentBuilder.builder(xContentType.xContent()); + xContentBuilder.startObject(); + xContentBuilder.endObject(); + doc = BytesReference.bytes(xContentBuilder); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } ContextSetup contextSetup = new ContextSetup(index, doc, query); -// if (doc != null) { -// contextSetup.setXContentType(XContentType.JSON); -// } + contextSetup.setXContentType(xContentType); return contextSetup; } } From 0b939cb1f11ba29f640ba36c82401a804c8737b2 Mon Sep 17 00:00:00 2001 From: Alpar Torok Date: Fri, 30 Nov 2018 09:34:53 +0200 Subject: [PATCH 040/115] Introducing testing conventions task (#35861) --- .../gradle/precommit/PrecommitTasks.groovy | 7 +- .../gradle/precommit/FilePermissionsTask.java | 6 +- .../precommit/TestingConventionsTasks.java | 267 ++++++++++++++++++ .../gradle/tool/Boilerplate.java | 31 ++ 4 files changed, 306 insertions(+), 5 deletions(-) create mode 100644 buildSrc/src/main/java/org/elasticsearch/gradle/precommit/TestingConventionsTasks.java create mode 100644 buildSrc/src/main/java/org/elasticsearch/gradle/tool/Boilerplate.java diff --git a/buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/PrecommitTasks.groovy b/buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/PrecommitTasks.groovy index b5476ea96621b..7032b05ed9064 100644 --- a/buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/PrecommitTasks.groovy +++ b/buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/PrecommitTasks.groovy @@ -48,7 +48,8 @@ class PrecommitTasks { project.tasks.create('licenseHeaders', LicenseHeadersTask.class), project.tasks.create('filepermissions', FilePermissionsTask.class), configureJarHell(project), - configureThirdPartyAudit(project) + configureThirdPartyAudit(project), + configureTestingConventions(project) ] // tasks with just tests don't need dependency licenses, so this flag makes adding @@ -89,6 +90,10 @@ class PrecommitTasks { ]) } + static Task configureTestingConventions(Project project) { + project.getTasks().create("testingConventions", TestingConventionsTasks.class) + } + private static Task configureJarHell(Project project) { Task task = project.tasks.create('jarHell', JarHellTask.class) task.classpath = project.sourceSets.test.runtimeClasspath diff --git a/buildSrc/src/main/java/org/elasticsearch/gradle/precommit/FilePermissionsTask.java b/buildSrc/src/main/java/org/elasticsearch/gradle/precommit/FilePermissionsTask.java index 100c3a22700ad..4b079384be5c5 100644 --- a/buildSrc/src/main/java/org/elasticsearch/gradle/precommit/FilePermissionsTask.java +++ b/buildSrc/src/main/java/org/elasticsearch/gradle/precommit/FilePermissionsTask.java @@ -28,15 +28,14 @@ import java.util.stream.Collectors; import org.apache.tools.ant.taskdefs.condition.Os; +import org.elasticsearch.gradle.tool.Boilerplate; import org.gradle.api.DefaultTask; import org.gradle.api.GradleException; import org.gradle.api.file.FileCollection; import org.gradle.api.file.FileTree; -import org.gradle.api.plugins.JavaPluginConvention; import org.gradle.api.tasks.InputFiles; import org.gradle.api.tasks.OutputFile; import org.gradle.api.tasks.SkipWhenEmpty; -import org.gradle.api.tasks.SourceSetContainer; import org.gradle.api.tasks.StopExecutionException; import org.gradle.api.tasks.TaskAction; import org.gradle.api.tasks.util.PatternFilterable; @@ -81,8 +80,7 @@ private static boolean isExecutableFile(File file) { @InputFiles @SkipWhenEmpty public FileCollection getFiles() { - SourceSetContainer sourceSets = getProject().getConvention().getPlugin(JavaPluginConvention.class).getSourceSets(); - return sourceSets.stream() + return Boilerplate.getJavaSourceSets(getProject()).stream() .map(sourceSet -> sourceSet.getAllSource().matching(filesFilter)) .reduce(FileTree::plus) .orElse(getProject().files().getAsFileTree()); diff --git a/buildSrc/src/main/java/org/elasticsearch/gradle/precommit/TestingConventionsTasks.java b/buildSrc/src/main/java/org/elasticsearch/gradle/precommit/TestingConventionsTasks.java new file mode 100644 index 0000000000000..1e73fa7cd0f24 --- /dev/null +++ b/buildSrc/src/main/java/org/elasticsearch/gradle/precommit/TestingConventionsTasks.java @@ -0,0 +1,267 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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.elasticsearch.gradle.precommit; + +import org.elasticsearch.gradle.tool.Boilerplate; +import org.gradle.api.DefaultTask; +import org.gradle.api.file.FileCollection; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.SkipWhenEmpty; +import org.gradle.api.tasks.TaskAction; + +import java.io.File; +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.FileVisitResult; +import java.nio.file.FileVisitor; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class TestingConventionsTasks extends DefaultTask { + + private static final String TEST_CLASS_SUFIX = "Tests"; + private static final String INTEG_TEST_CLASS_SUFIX = "IT"; + private static final String TEST_METHOD_PREFIX = "test"; + + /** + * Are there tests to execute ? Accounts for @Ignore and @AwaitsFix + */ + private Boolean activeTestsExists; + + private List testClassNames; + + public TestingConventionsTasks() { + setDescription("Tests various testing conventions"); + // Run only after everything is compiled + Boilerplate.getJavaSourceSets(getProject()).all(sourceSet -> dependsOn(sourceSet.getClassesTaskName())); + } + + @TaskAction + public void doCheck() throws IOException { + activeTestsExists = false; + final List problems; + + try (URLClassLoader isolatedClassLoader = new URLClassLoader( + getTestsClassPath().getFiles().stream().map(this::fileToUrl).toArray(URL[]::new) + )) { + List> classes = getTestClassNames().stream() + .map(name -> loadClassWithoutInitializing(name, isolatedClassLoader)) + .collect(Collectors.toList()); + + Predicate> isStaticClass = clazz -> Modifier.isStatic(clazz.getModifiers()); + Predicate> isPublicClass = clazz -> Modifier.isPublic(clazz.getModifiers()); + Predicate> implementsNamingConvention = clazz -> clazz.getName().endsWith(TEST_CLASS_SUFIX) || + clazz.getName().endsWith(INTEG_TEST_CLASS_SUFIX); + + problems = Stream.concat( + checkNoneExists( + "Test classes implemented by inner classes will not run", + classes.stream() + .filter(isStaticClass) + .filter(implementsNamingConvention.or(this::seemsLikeATest)) + ).stream(), + checkNoneExists( + "Seem like test classes but don't match naming convention", + classes.stream() + .filter(isStaticClass.negate()) + .filter(isPublicClass) + .filter(this::seemsLikeATest) + .filter(implementsNamingConvention.negate()) + ).stream() + ).collect(Collectors.toList()); + } + + if (problems.isEmpty()) { + getSuccessMarker().getParentFile().mkdirs(); + Files.write(getSuccessMarker().toPath(), new byte[]{}, StandardOpenOption.CREATE); + } else { + problems.forEach(getProject().getLogger()::error); + throw new IllegalStateException("Testing conventions are not honored"); + } + } + + @Input + @SkipWhenEmpty + public List getTestClassNames() { + if (testClassNames == null) { + testClassNames = Boilerplate.getJavaSourceSets(getProject()).getByName("test").getOutput().getClassesDirs() + .getFiles().stream() + .filter(File::exists) + .flatMap(testRoot -> walkPathAndLoadClasses(testRoot).stream()) + .collect(Collectors.toList()); + } + return testClassNames; + } + + @OutputFile + public File getSuccessMarker() { + return new File(getProject().getBuildDir(), "markers/" + getName()); + } + + private List checkNoneExists(String message, Stream> stream) { + List problems = new ArrayList<>(); + List> entries = stream.collect(Collectors.toList()); + if (entries.isEmpty() == false) { + problems.add(message + ":"); + entries.stream() + .map(each -> " * " + each.getName()) + .forEach(problems::add); + } + return problems; + } + + private boolean seemsLikeATest(Class clazz) { + try { + ClassLoader classLoader = clazz.getClassLoader(); + Class junitTest; + try { + junitTest = classLoader.loadClass("junit.framework.Test"); + } catch (ClassNotFoundException e) { + throw new IllegalStateException("Could not load junit.framework.Test. It's expected that this class is " + + "available on the tests classpath"); + } + if (junitTest.isAssignableFrom(clazz)) { + getLogger().info("{} is a test because it extends junit.framework.Test", clazz.getName()); + return true; + } + for (Method method : clazz.getMethods()) { + if (matchesTestMethodNamingConvention(clazz, method)) return true; + if (isAnnotated(clazz, method, junitTest)) return true; + } + return false; + } catch (NoClassDefFoundError e) { + throw new IllegalStateException("Failed to inspect class " + clazz.getName(), e); + } + } + + private boolean matchesTestMethodNamingConvention(Class clazz, Method method) { + if (method.getName().startsWith(TEST_METHOD_PREFIX) && + Modifier.isStatic(method.getModifiers()) == false && + method.getReturnType().equals(Void.class) + ) { + getLogger().info("{} is a test because it has method: {}", clazz.getName(), method.getName()); + return true; + } + return false; + } + + private boolean isAnnotated(Class clazz, Method method, Class annotation) { + for (Annotation presentAnnotation : method.getAnnotations()) { + if (annotation.isAssignableFrom(presentAnnotation.getClass())) { + getLogger().info("{} is a test because {} is annotated with junit.framework.Test", + clazz.getName(), method.getName() + ); + return true; + } + } + return false; + } + + private FileCollection getTestsClassPath() { + // This is doesn't need to be annotated with @Classpath because we only really care about the test source set + return getProject().files( + getProject().getConfigurations().getByName("testCompile").resolve(), + Boilerplate.getJavaSourceSets(getProject()) + .stream() + .flatMap(sourceSet -> sourceSet.getOutput().getClassesDirs().getFiles().stream()) + .collect(Collectors.toList()) + ); + } + + private List walkPathAndLoadClasses(File testRoot) { + List classes = new ArrayList<>(); + try { + Files.walkFileTree(testRoot.toPath(), new FileVisitor() { + private String packageName; + + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { + // First we visit the root directory + if (packageName == null) { + // And it package is empty string regardless of the directory name + packageName = ""; + } else { + packageName += dir.getFileName() + "."; + } + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + // Go up one package by jumping back to the second to last '.' + packageName = packageName.substring(0, 1 + packageName.lastIndexOf('.', packageName.length() - 2)); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + String filename = file.getFileName().toString(); + if (filename.endsWith(".class")) { + String className = filename.substring(0, filename.length() - ".class".length()); + classes.add(packageName + className); + } + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException { + throw new IOException("Failed to visit " + file, exc); + } + }); + } catch (IOException e) { + throw new IllegalStateException(e); + } + return classes; + } + + private Class loadClassWithoutInitializing(String name, ClassLoader isolatedClassLoader) { + try { + return Class.forName(name, + // Don't initialize the class to save time. Not needed for this test and this doesn't share a VM with any other tests. + false, + isolatedClassLoader + ); + } catch (ClassNotFoundException e) { + // Will not get here as the exception will be loaded by isolatedClassLoader + throw new RuntimeException("Failed to load class " + name, e); + } + } + + private URL fileToUrl(File file) { + try { + return file.toURI().toURL(); + } catch (MalformedURLException e) { + throw new IllegalStateException(e); + } + } + +} diff --git a/buildSrc/src/main/java/org/elasticsearch/gradle/tool/Boilerplate.java b/buildSrc/src/main/java/org/elasticsearch/gradle/tool/Boilerplate.java new file mode 100644 index 0000000000000..67fc7473a4cfd --- /dev/null +++ b/buildSrc/src/main/java/org/elasticsearch/gradle/tool/Boilerplate.java @@ -0,0 +1,31 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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.elasticsearch.gradle.tool; + +import org.gradle.api.Project; +import org.gradle.api.plugins.JavaPluginConvention; +import org.gradle.api.tasks.SourceSetContainer; + +public abstract class Boilerplate { + + public static SourceSetContainer getJavaSourceSets(Project project) { + return project.getConvention().getPlugin(JavaPluginConvention.class).getSourceSets(); + } + +} From 0f166915a344a6692ae9b89fc2090bc07654941f Mon Sep 17 00:00:00 2001 From: Albert Zaharovits Date: Thu, 29 Nov 2018 21:34:16 +0200 Subject: [PATCH 041/115] Fix Watcher NotificationService's secure settings (#35610) The NotificationService (base class for SlackService, HipchatService ...) has both dynamic cluster settings and SecureSettings and builds the clients (Account) that are used to comm with external services. This commit fixes an important bug about updating/reloading any of these settings (both Secure and dynamic cluster). Briefly the bug is due to the fact that both the secure settings as well as the dynamic node scoped ones can be updated independently, but when constructing the clients some of the settings might not be visible. --- .../notification/NotificationService.java | 176 +++++++++++++++--- .../notification/email/EmailService.java | 20 +- .../notification/hipchat/HipChatService.java | 20 +- .../notification/jira/JiraService.java | 19 +- .../pagerduty/PagerDutyService.java | 18 +- .../notification/slack/SlackService.java | 21 ++- .../NotificationServiceTests.java | 4 +- 7 files changed, 224 insertions(+), 54 deletions(-) diff --git a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/notification/NotificationService.java b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/notification/NotificationService.java index 0b545c7942821..c34b1ab97d6a2 100644 --- a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/notification/NotificationService.java +++ b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/notification/NotificationService.java @@ -5,17 +5,23 @@ */ package org.elasticsearch.xpack.watcher.notification; -import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.component.AbstractComponent; +import org.elasticsearch.common.Nullable; import org.elasticsearch.common.settings.ClusterSettings; +import org.elasticsearch.common.settings.SecureSettings; +import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.SettingsException; +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.function.BiFunction; /** @@ -24,25 +30,68 @@ public abstract class NotificationService extends AbstractComponent { private final String type; - // both are guarded by this - private Map accounts; - private Account defaultAccount; - - public NotificationService(String type, - ClusterSettings clusterSettings, List> pluginSettings) { - this(type); - clusterSettings.addSettingsUpdateConsumer(this::reload, pluginSettings); + private final Settings bootSettings; + private final List> pluginSecureSettings; + // all are guarded by this + private volatile Map accounts; + private volatile Account defaultAccount; + // cached cluster setting, required when recreating the notification clients + // using the new "reloaded" secure settings + private volatile Settings cachedClusterSettings; + // cached secure settings, required when recreating the notification clients + // using the new updated cluster settings + private volatile SecureSettings cachedSecureSettings; + + public NotificationService(String type, Settings settings, ClusterSettings clusterSettings, List> pluginDynamicSettings, + List> pluginSecureSettings) { + this(type, settings, pluginSecureSettings); + // register a grand updater for the whole group, as settings are usable together + clusterSettings.addSettingsUpdateConsumer(this::clusterSettingsConsumer, pluginDynamicSettings); } // Used for testing only - NotificationService(String type) { + NotificationService(String type, Settings settings, List> pluginSecureSettings) { this.type = type; + this.bootSettings = settings; + this.pluginSecureSettings = pluginSecureSettings; + } + + private synchronized void clusterSettingsConsumer(Settings settings) { + // update cached cluster settings + this.cachedClusterSettings = settings; + // use these new dynamic cluster settings together with the previously cached + // secure settings + buildAccounts(); } public synchronized void reload(Settings settings) { - Tuple, Account> accounts = buildAccounts(settings, this::createAccount); - this.accounts = Collections.unmodifiableMap(accounts.v1()); - this.defaultAccount = accounts.v2(); + // `SecureSettings` are available here! cache them as they will be needed + // whenever dynamic cluster settings change and we have to rebuild the accounts + try { + this.cachedSecureSettings = extractSecureSettings(settings, pluginSecureSettings); + } catch (GeneralSecurityException e) { + logger.error("Keystore exception while reloading watcher notification service", e); + return; + } + // use these new secure settings together with the previously cached dynamic + // cluster settings + buildAccounts(); + } + + private void buildAccounts() { + // build complete settings combining cluster and secure settings + final Settings.Builder completeSettingsBuilder = Settings.builder().put(bootSettings, false); + if (this.cachedClusterSettings != null) { + completeSettingsBuilder.put(this.cachedClusterSettings, false); + } + if (this.cachedSecureSettings != null) { + completeSettingsBuilder.setSecureSettings(this.cachedSecureSettings); + } + final Settings completeSettings = completeSettingsBuilder.build(); + // obtain account names and create accounts + final Set accountNames = getAccountNames(completeSettings); + this.accounts = createAccounts(completeSettings, accountNames, this::createAccount); + this.defaultAccount = findDefaultAccountOrNull(completeSettings, this.accounts); } protected abstract Account createAccount(String name, Settings accountSettings); @@ -67,31 +116,100 @@ public Account getAccount(String name) { return theAccount; } - private Tuple, A> buildAccounts(Settings settings, BiFunction accountFactory) { - Settings accountsSettings = settings.getByPrefix("xpack.notification." + type + ".").getAsSettings("account"); - Map accounts = new HashMap<>(); - for (String name : accountsSettings.names()) { - Settings accountSettings = accountsSettings.getAsSettings(name); - A account = accountFactory.apply(name, accountSettings); - accounts.put(name, account); + private String getNotificationsAccountPrefix() { + return "xpack.notification." + type + ".account."; + } + + private Set getAccountNames(Settings settings) { + // secure settings are not responsible for the client names + final Settings noSecureSettings = Settings.builder().put(settings, false).build(); + return noSecureSettings.getByPrefix(getNotificationsAccountPrefix()).names(); + } + + private @Nullable String getDefaultAccountName(Settings settings) { + return settings.get("xpack.notification." + type + ".default_account"); + } + + private Map createAccounts(Settings settings, Set accountNames, + BiFunction accountFactory) { + final Map accounts = new HashMap<>(); + for (final String accountName : accountNames) { + final Settings accountSettings = settings.getAsSettings(getNotificationsAccountPrefix() + accountName); + final Account account = accountFactory.apply(accountName, accountSettings); + accounts.put(accountName, account); } + return Collections.unmodifiableMap(accounts); + } - final String defaultAccountName = settings.get("xpack.notification." + type + ".default_account"); - A defaultAccount; + private @Nullable Account findDefaultAccountOrNull(Settings settings, Map accounts) { + final String defaultAccountName = getDefaultAccountName(settings); if (defaultAccountName == null) { if (accounts.isEmpty()) { - defaultAccount = null; + return null; } else { - A account = accounts.values().iterator().next(); - defaultAccount = account; - + return accounts.values().iterator().next(); } } else { - defaultAccount = accounts.get(defaultAccountName); - if (defaultAccount == null) { + final Account account = accounts.get(defaultAccountName); + if (account == null) { throw new SettingsException("could not find default account [" + defaultAccountName + "]"); } + return account; + } + } + + /** + * Extracts the {@link SecureSettings}` out of the passed in {@link Settings} object. The {@code Setting} argument has to have the + * {@code SecureSettings} open/available. Normally {@code SecureSettings} are available only under specific callstacks (eg. during node + * initialization or during a `reload` call). The returned copy can be reused freely as it will never be closed (this is a bit of + * cheating, but it is necessary in this specific circumstance). Only works for secure settings of type string (not file). + * + * @param source + * A {@code Settings} object with its {@code SecureSettings} open/available. + * @param securePluginSettings + * The list of settings to copy. + * @return A copy of the {@code SecureSettings} of the passed in {@code Settings} argument. + */ + private static SecureSettings extractSecureSettings(Settings source, List> securePluginSettings) + throws GeneralSecurityException { + // get the secure settings out + final SecureSettings sourceSecureSettings = Settings.builder().put(source, true).getSecureSettings(); + // filter and cache them... + final Map cache = new HashMap<>(); + if (sourceSecureSettings != null && securePluginSettings != null) { + for (final String settingKey : sourceSecureSettings.getSettingNames()) { + for (final Setting secureSetting : securePluginSettings) { + if (secureSetting.match(settingKey)) { + cache.put(settingKey, sourceSecureSettings.getString(settingKey)); + } + } + } } - return new Tuple<>(accounts, defaultAccount); + return new SecureSettings() { + + @Override + public boolean isLoaded() { + return true; + } + + @Override + public SecureString getString(String setting) throws GeneralSecurityException { + return cache.get(setting); + } + + @Override + public Set getSettingNames() { + return cache.keySet(); + } + + @Override + public InputStream getFile(String setting) throws GeneralSecurityException { + throw new IllegalStateException("A NotificationService setting cannot be File."); + } + + @Override + public void close() throws IOException { + } + }; } } diff --git a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/notification/email/EmailService.java b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/notification/email/EmailService.java index 70922e57bd078..cf897a68957b9 100644 --- a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/notification/email/EmailService.java +++ b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/notification/email/EmailService.java @@ -17,6 +17,8 @@ import org.elasticsearch.xpack.watcher.notification.NotificationService; import javax.mail.MessagingException; + +import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -104,7 +106,7 @@ public class EmailService extends NotificationService { private final CryptoService cryptoService; public EmailService(Settings settings, @Nullable CryptoService cryptoService, ClusterSettings clusterSettings) { - super("email", clusterSettings, EmailService.getSettings()); + super("email", settings, clusterSettings, EmailService.getDynamicSettings(), EmailService.getSecureSettings()); this.cryptoService = cryptoService; // ensure logging of setting changes clusterSettings.addSettingsUpdateConsumer(SETTING_DEFAULT_ACCOUNT, (s) -> {}); @@ -117,7 +119,6 @@ public EmailService(Settings settings, @Nullable CryptoService cryptoService, Cl clusterSettings.addAffixUpdateConsumer(SETTING_SMTP_PORT, (s, o) -> {}, (s, o) -> {}); clusterSettings.addAffixUpdateConsumer(SETTING_SMTP_USER, (s, o) -> {}, (s, o) -> {}); clusterSettings.addAffixUpdateConsumer(SETTING_SMTP_PASSWORD, (s, o) -> {}, (s, o) -> {}); - clusterSettings.addAffixUpdateConsumer(SETTING_SECURE_PASSWORD, (s, o) -> {}, (s, o) -> {}); clusterSettings.addAffixUpdateConsumer(SETTING_SMTP_TIMEOUT, (s, o) -> {}, (s, o) -> {}); clusterSettings.addAffixUpdateConsumer(SETTING_SMTP_CONNECTION_TIMEOUT, (s, o) -> {}, (s, o) -> {}); clusterSettings.addAffixUpdateConsumer(SETTING_SMTP_WRITE_TIMEOUT, (s, o) -> {}, (s, o) -> {}); @@ -175,12 +176,21 @@ public Email email() { } } - public static List> getSettings() { + private static List> getDynamicSettings() { return Arrays.asList(SETTING_DEFAULT_ACCOUNT, SETTING_PROFILE, SETTING_EMAIL_DEFAULTS, SETTING_SMTP_AUTH, SETTING_SMTP_HOST, SETTING_SMTP_PASSWORD, SETTING_SMTP_PORT, SETTING_SMTP_STARTTLS_ENABLE, SETTING_SMTP_USER, SETTING_SMTP_STARTTLS_REQUIRED, SETTING_SMTP_TIMEOUT, SETTING_SMTP_CONNECTION_TIMEOUT, SETTING_SMTP_WRITE_TIMEOUT, SETTING_SMTP_LOCAL_ADDRESS, - SETTING_SMTP_LOCAL_PORT, SETTING_SMTP_SEND_PARTIAL, SETTING_SMTP_WAIT_ON_QUIT, SETTING_SMTP_SSL_TRUST_ADDRESS, - SETTING_SECURE_PASSWORD); + SETTING_SMTP_LOCAL_PORT, SETTING_SMTP_SEND_PARTIAL, SETTING_SMTP_WAIT_ON_QUIT, SETTING_SMTP_SSL_TRUST_ADDRESS); + } + + private static List> getSecureSettings() { + return Arrays.asList(SETTING_SECURE_PASSWORD); + } + + public static List> getSettings() { + List> allSettings = new ArrayList>(EmailService.getDynamicSettings()); + allSettings.addAll(EmailService.getSecureSettings()); + return allSettings; } } diff --git a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/notification/hipchat/HipChatService.java b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/notification/hipchat/HipChatService.java index 57e9394a0e3e7..b7032d7f25967 100644 --- a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/notification/hipchat/HipChatService.java +++ b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/notification/hipchat/HipChatService.java @@ -14,6 +14,7 @@ import org.elasticsearch.xpack.watcher.common.http.HttpClient; import org.elasticsearch.xpack.watcher.notification.NotificationService; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -64,20 +65,19 @@ public class HipChatService extends NotificationService { private HipChatServer defaultServer; public HipChatService(Settings settings, HttpClient httpClient, ClusterSettings clusterSettings) { - super("hipchat", clusterSettings, HipChatService.getSettings()); + super("hipchat", settings, clusterSettings, HipChatService.getDynamicSettings(), HipChatService.getSecureSettings()); this.httpClient = httpClient; // ensure logging of setting changes clusterSettings.addSettingsUpdateConsumer(SETTING_DEFAULT_ACCOUNT, (s) -> {}); clusterSettings.addSettingsUpdateConsumer(SETTING_DEFAULT_HOST, (s) -> {}); clusterSettings.addSettingsUpdateConsumer(SETTING_DEFAULT_PORT, (s) -> {}); clusterSettings.addAffixUpdateConsumer(SETTING_AUTH_TOKEN, (s, o) -> {}, (s, o) -> {}); - clusterSettings.addAffixUpdateConsumer(SETTING_AUTH_TOKEN_SECURE, (s, o) -> {}, (s, o) -> {}); clusterSettings.addAffixUpdateConsumer(SETTING_PROFILE, (s, o) -> {}, (s, o) -> {}); clusterSettings.addAffixUpdateConsumer(SETTING_ROOM, (s, o) -> {}, (s, o) -> {}); clusterSettings.addAffixUpdateConsumer(SETTING_HOST, (s, o) -> {}, (s, o) -> {}); clusterSettings.addAffixUpdateConsumer(SETTING_PORT, (s, o) -> {}, (s, o) -> {}); clusterSettings.addAffixUpdateConsumer(SETTING_MESSAGE_DEFAULTS, (s, o) -> {}, (s, o) -> {}); - + // do an initial load reload(settings); } @@ -96,8 +96,18 @@ protected HipChatAccount createAccount(String name, Settings accountSettings) { return profile.createAccount(name, accountSettings, defaultServer, httpClient, logger); } + private static List> getDynamicSettings() { + return Arrays.asList(SETTING_DEFAULT_ACCOUNT, SETTING_AUTH_TOKEN, SETTING_PROFILE, SETTING_ROOM, SETTING_MESSAGE_DEFAULTS, + SETTING_DEFAULT_HOST, SETTING_DEFAULT_PORT, SETTING_HOST, SETTING_PORT); + } + + private static List> getSecureSettings() { + return Arrays.asList(SETTING_AUTH_TOKEN_SECURE); + } + public static List> getSettings() { - return Arrays.asList(SETTING_DEFAULT_ACCOUNT, SETTING_AUTH_TOKEN, SETTING_AUTH_TOKEN_SECURE, SETTING_PROFILE, SETTING_ROOM, - SETTING_MESSAGE_DEFAULTS, SETTING_DEFAULT_HOST, SETTING_DEFAULT_PORT, SETTING_HOST, SETTING_PORT); + List> allSettings = new ArrayList>(getDynamicSettings()); + allSettings.addAll(getSecureSettings()); + return allSettings; } } diff --git a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/notification/jira/JiraService.java b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/notification/jira/JiraService.java index b89aafd2ab2c0..efc41fe216364 100644 --- a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/notification/jira/JiraService.java +++ b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/notification/jira/JiraService.java @@ -14,6 +14,7 @@ import org.elasticsearch.xpack.watcher.common.http.HttpClient; import org.elasticsearch.xpack.watcher.notification.NotificationService; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -62,7 +63,7 @@ public class JiraService extends NotificationService { private final HttpClient httpClient; public JiraService(Settings settings, HttpClient httpClient, ClusterSettings clusterSettings) { - super("jira", clusterSettings, JiraService.getSettings()); + super("jira", settings, clusterSettings, JiraService.getDynamicSettings(), JiraService.getSecureSettings()); this.httpClient = httpClient; // ensure logging of setting changes clusterSettings.addSettingsUpdateConsumer(SETTING_DEFAULT_ACCOUNT, (s) -> {}); @@ -70,9 +71,6 @@ public JiraService(Settings settings, HttpClient httpClient, ClusterSettings clu clusterSettings.addAffixUpdateConsumer(SETTING_URL, (s, o) -> {}, (s, o) -> {}); clusterSettings.addAffixUpdateConsumer(SETTING_USER, (s, o) -> {}, (s, o) -> {}); clusterSettings.addAffixUpdateConsumer(SETTING_PASSWORD, (s, o) -> {}, (s, o) -> {}); - clusterSettings.addAffixUpdateConsumer(SETTING_SECURE_USER, (s, o) -> {}, (s, o) -> {}); - clusterSettings.addAffixUpdateConsumer(SETTING_SECURE_URL, (s, o) -> {}, (s, o) -> {}); - clusterSettings.addAffixUpdateConsumer(SETTING_SECURE_PASSWORD, (s, o) -> {}, (s, o) -> {}); clusterSettings.addAffixUpdateConsumer(SETTING_DEFAULTS, (s, o) -> {}, (s, o) -> {}); // do an initial load reload(settings); @@ -83,8 +81,17 @@ protected JiraAccount createAccount(String name, Settings settings) { return new JiraAccount(name, settings, httpClient); } + private static List> getDynamicSettings() { + return Arrays.asList(SETTING_DEFAULT_ACCOUNT, SETTING_ALLOW_HTTP, SETTING_URL, SETTING_USER, SETTING_PASSWORD, SETTING_DEFAULTS); + } + + private static List> getSecureSettings() { + return Arrays.asList(SETTING_SECURE_USER, SETTING_SECURE_PASSWORD, SETTING_SECURE_URL); + } + public static List> getSettings() { - return Arrays.asList(SETTING_ALLOW_HTTP, SETTING_URL, SETTING_USER, SETTING_PASSWORD, SETTING_SECURE_USER, - SETTING_SECURE_PASSWORD, SETTING_SECURE_URL, SETTING_DEFAULTS, SETTING_DEFAULT_ACCOUNT); + List> allSettings = new ArrayList>(getDynamicSettings()); + allSettings.addAll(getSecureSettings()); + return allSettings; } } diff --git a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/notification/pagerduty/PagerDutyService.java b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/notification/pagerduty/PagerDutyService.java index d646d62e8750e..ff46d0d6184e6 100644 --- a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/notification/pagerduty/PagerDutyService.java +++ b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/notification/pagerduty/PagerDutyService.java @@ -14,6 +14,7 @@ import org.elasticsearch.xpack.watcher.common.http.HttpClient; import org.elasticsearch.xpack.watcher.notification.NotificationService; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -40,12 +41,13 @@ public class PagerDutyService extends NotificationService { private final HttpClient httpClient; public PagerDutyService(Settings settings, HttpClient httpClient, ClusterSettings clusterSettings) { - super("pagerduty", clusterSettings, PagerDutyService.getSettings()); + super("pagerduty", settings, clusterSettings, PagerDutyService.getDynamicSettings(), PagerDutyService.getSecureSettings()); this.httpClient = httpClient; + // ensure logging of setting changes clusterSettings.addSettingsUpdateConsumer(SETTING_DEFAULT_ACCOUNT, (s) -> {}); clusterSettings.addAffixUpdateConsumer(SETTING_SERVICE_API_KEY, (s, o) -> {}, (s, o) -> {}); - clusterSettings.addAffixUpdateConsumer(SETTING_SECURE_SERVICE_API_KEY, (s, o) -> {}, (s, o) -> {}); clusterSettings.addAffixUpdateConsumer(SETTING_DEFAULTS, (s, o) -> {}, (s, o) -> {}); + // do an initial load reload(settings); } @@ -54,7 +56,17 @@ protected PagerDutyAccount createAccount(String name, Settings accountSettings) return new PagerDutyAccount(name, accountSettings, accountSettings, httpClient, logger); } + private static List> getDynamicSettings() { + return Arrays.asList(SETTING_SERVICE_API_KEY, SETTING_DEFAULTS, SETTING_DEFAULT_ACCOUNT); + } + + private static List> getSecureSettings() { + return Arrays.asList(SETTING_SECURE_SERVICE_API_KEY); + } + public static List> getSettings() { - return Arrays.asList(SETTING_SERVICE_API_KEY, SETTING_SECURE_SERVICE_API_KEY, SETTING_DEFAULTS, SETTING_DEFAULT_ACCOUNT); + List> allSettings = new ArrayList>(getDynamicSettings()); + allSettings.addAll(getSecureSettings()); + return allSettings; } } diff --git a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/notification/slack/SlackService.java b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/notification/slack/SlackService.java index 23190adb37d38..9a347e50c5298 100644 --- a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/notification/slack/SlackService.java +++ b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/notification/slack/SlackService.java @@ -14,6 +14,7 @@ import org.elasticsearch.xpack.watcher.common.http.HttpClient; import org.elasticsearch.xpack.watcher.notification.NotificationService; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -30,8 +31,7 @@ public class SlackService extends NotificationService { (key) -> Setting.simpleString(key, Property.Dynamic, Property.NodeScope, Property.Filtered)); private static final Setting.AffixSetting SETTING_URL_SECURE = - Setting.affixKeySetting("xpack.notification.slack.account.", "secure_url", - (key) -> SecureSetting.secureString(key, null)); + Setting.affixKeySetting("xpack.notification.slack.account.", "secure_url", (key) -> SecureSetting.secureString(key, null)); private static final Setting.AffixSetting SETTING_DEFAULTS = Setting.affixKeySetting("xpack.notification.slack.account.", "message_defaults", @@ -40,12 +40,13 @@ public class SlackService extends NotificationService { private final HttpClient httpClient; public SlackService(Settings settings, HttpClient httpClient, ClusterSettings clusterSettings) { - super("slack", clusterSettings, SlackService.getSettings()); + super("slack", settings, clusterSettings, SlackService.getDynamicSettings(), SlackService.getSecureSettings()); this.httpClient = httpClient; + // ensure logging of setting changes clusterSettings.addSettingsUpdateConsumer(SETTING_DEFAULT_ACCOUNT, (s) -> {}); clusterSettings.addAffixUpdateConsumer(SETTING_URL, (s, o) -> {}, (s, o) -> {}); - clusterSettings.addAffixUpdateConsumer(SETTING_URL_SECURE, (s, o) -> {}, (s, o) -> {}); clusterSettings.addAffixUpdateConsumer(SETTING_DEFAULTS, (s, o) -> {}, (s, o) -> {}); + // do an initial load reload(settings); } @@ -54,7 +55,17 @@ protected SlackAccount createAccount(String name, Settings accountSettings) { return new SlackAccount(name, accountSettings, accountSettings, httpClient, logger); } + private static List> getDynamicSettings() { + return Arrays.asList(SETTING_URL, SETTING_DEFAULT_ACCOUNT, SETTING_DEFAULTS); + } + + private static List> getSecureSettings() { + return Arrays.asList(SETTING_URL_SECURE); + } + public static List> getSettings() { - return Arrays.asList(SETTING_URL, SETTING_URL_SECURE, SETTING_DEFAULT_ACCOUNT, SETTING_DEFAULTS); + List> allSettings = new ArrayList>(getDynamicSettings()); + allSettings.addAll(getSecureSettings()); + return allSettings; } } diff --git a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/notification/NotificationServiceTests.java b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/notification/NotificationServiceTests.java index ddf45de816367..184ff56c21300 100644 --- a/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/notification/NotificationServiceTests.java +++ b/x-pack/plugin/watcher/src/test/java/org/elasticsearch/xpack/watcher/notification/NotificationServiceTests.java @@ -10,6 +10,8 @@ import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.watcher.notification.NotificationService; +import java.util.Collections; + import static org.hamcrest.Matchers.anyOf; import static org.hamcrest.Matchers.is; @@ -81,7 +83,7 @@ public void testAccountDoesNotExist() throws Exception{ private static class TestNotificationService extends NotificationService { TestNotificationService(Settings settings) { - super("test"); + super("test", settings, Collections.emptyList()); reload(settings); } From 51dbe5aff7483b0558e6e537cb9ac1293c7080f7 Mon Sep 17 00:00:00 2001 From: Dimitris Athanasiou Date: Fri, 30 Nov 2018 09:25:35 +0000 Subject: [PATCH 042/115] [ML] Refactor control message writer to allow reuse for other processes (#36070) --- .../autodetect/NativeAutodetectProcess.java | 8 +-- ...r.java => AutodetectControlMsgWriter.java} | 51 ++------------- .../writer/AbstractControlMsgWriter.java | 62 +++++++++++++++++++ .../NativeAutodetectProcessTests.java | 12 ++-- ...a => AutodetectControlMsgWriterTests.java} | 34 +++++----- 5 files changed, 95 insertions(+), 72 deletions(-) rename x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/autodetect/writer/{ControlMsgToProcessWriter.java => AutodetectControlMsgWriter.java} (83%) create mode 100644 x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/process/writer/AbstractControlMsgWriter.java rename x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/autodetect/writer/{ControlMsgToProcessWriterTests.java => AutodetectControlMsgWriterTests.java} (88%) diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/autodetect/NativeAutodetectProcess.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/autodetect/NativeAutodetectProcess.java index 112805b2f7414..3dc72cce1570c 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/autodetect/NativeAutodetectProcess.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/autodetect/NativeAutodetectProcess.java @@ -18,7 +18,7 @@ import org.elasticsearch.xpack.ml.job.process.autodetect.params.DataLoadParams; import org.elasticsearch.xpack.ml.job.process.autodetect.params.FlushJobParams; import org.elasticsearch.xpack.ml.job.process.autodetect.params.ForecastParams; -import org.elasticsearch.xpack.ml.job.process.autodetect.writer.ControlMsgToProcessWriter; +import org.elasticsearch.xpack.ml.job.process.autodetect.writer.AutodetectControlMsgWriter; import org.elasticsearch.xpack.ml.job.results.AutodetectResult; import org.elasticsearch.xpack.ml.process.AbstractNativeProcess; @@ -94,7 +94,7 @@ public void writeUpdateScheduledEventsMessage(List events, TimeV @Override public String flushJob(FlushJobParams params) throws IOException { - ControlMsgToProcessWriter writer = newMessageWriter(); + AutodetectControlMsgWriter writer = newMessageWriter(); writer.writeFlushControlMessage(params); return writer.writeFlushMessage(); } @@ -114,7 +114,7 @@ public Iterator readAutodetectResults() { return resultsParser.parseResults(processOutStream()); } - private ControlMsgToProcessWriter newMessageWriter() { - return new ControlMsgToProcessWriter(recordWriter(), numberOfFields()); + private AutodetectControlMsgWriter newMessageWriter() { + return new AutodetectControlMsgWriter(recordWriter(), numberOfFields()); } } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/autodetect/writer/ControlMsgToProcessWriter.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/autodetect/writer/AutodetectControlMsgWriter.java similarity index 83% rename from x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/autodetect/writer/ControlMsgToProcessWriter.java rename to x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/autodetect/writer/AutodetectControlMsgWriter.java index fc98990d8d61f..e0ed7458b1d6a 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/autodetect/writer/ControlMsgToProcessWriter.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/process/autodetect/writer/AutodetectControlMsgWriter.java @@ -18,26 +18,20 @@ import org.elasticsearch.xpack.ml.job.process.autodetect.params.DataLoadParams; import org.elasticsearch.xpack.ml.job.process.autodetect.params.FlushJobParams; import org.elasticsearch.xpack.ml.job.process.autodetect.params.ForecastParams; +import org.elasticsearch.xpack.ml.process.writer.AbstractControlMsgWriter; import org.elasticsearch.xpack.ml.process.writer.LengthEncodedWriter; import java.io.IOException; import java.io.OutputStream; import java.io.StringWriter; -import java.util.Arrays; import java.util.List; -import java.util.Objects; import java.util.concurrent.atomic.AtomicLong; /** * A writer for sending control messages to the C++ autodetect process. * The data written to outputIndex is length encoded. */ -public class ControlMsgToProcessWriter { - - /** - * This should be the same size as the buffer in the C++ autodetect process. - */ - public static final int FLUSH_SPACES_LENGTH = 8192; +public class AutodetectControlMsgWriter extends AbstractControlMsgWriter { /** * This must match the code defined in the api::CAnomalyJob C++ class. @@ -85,18 +79,14 @@ public class ControlMsgToProcessWriter { */ private static AtomicLong ms_FlushNumber = new AtomicLong(1); - private final LengthEncodedWriter lengthEncodedWriter; - private final int numberOfFields; - /** * Construct the control message writer with a LengthEncodedWriter * * @param lengthEncodedWriter The writer * @param numberOfFields The number of fields the process expects in each record */ - public ControlMsgToProcessWriter(LengthEncodedWriter lengthEncodedWriter, int numberOfFields) { - this.lengthEncodedWriter = Objects.requireNonNull(lengthEncodedWriter); - this.numberOfFields = numberOfFields; + public AutodetectControlMsgWriter(LengthEncodedWriter lengthEncodedWriter, int numberOfFields) { + super(lengthEncodedWriter, numberOfFields); } /** @@ -106,8 +96,8 @@ public ControlMsgToProcessWriter(LengthEncodedWriter lengthEncodedWriter, int nu * @param os The output stream * @param numberOfFields The number of fields the process expects in each record */ - public static ControlMsgToProcessWriter create(OutputStream os, int numberOfFields) { - return new ControlMsgToProcessWriter(new LengthEncodedWriter(os), numberOfFields); + public static AutodetectControlMsgWriter create(OutputStream os, int numberOfFields) { + return new AutodetectControlMsgWriter(new LengthEncodedWriter(os), numberOfFields); } /** @@ -175,14 +165,6 @@ public void writeForecastMessage(ForecastParams params) throws IOException { lengthEncodedWriter.flush(); } - // todo(hendrikm): workaround, see - // https://github.com/elastic/machine-learning-cpp/issues/123 - private void fillCommandBuffer() throws IOException { - char[] spaces = new char[FLUSH_SPACES_LENGTH]; - Arrays.fill(spaces, ' '); - writeMessage(new String(spaces)); - } - public void writeResetBucketsMessage(DataLoadParams params) throws IOException { writeControlCodeFollowedByTimeRange(RESET_BUCKETS_MESSAGE_CODE, params.getStart(), params.getEnd()); } @@ -247,25 +229,4 @@ public void writeStartBackgroundPersistMessage() throws IOException { fillCommandBuffer(); lengthEncodedWriter.flush(); } - - /** - * Transform the supplied control message to length encoded values and - * write to the OutputStream. - * The number of blank fields to make up a full record is deduced from - * analysisConfig. - * - * @param message The control message to write. - */ - private void writeMessage(String message) throws IOException { - - lengthEncodedWriter.writeNumFields(numberOfFields); - - // Write blank values for all fields other than the control field - for (int i = 1; i < numberOfFields; ++i) { - lengthEncodedWriter.writeField(""); - } - - // The control field comes last - lengthEncodedWriter.writeField(message); - } } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/process/writer/AbstractControlMsgWriter.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/process/writer/AbstractControlMsgWriter.java new file mode 100644 index 0000000000000..8d84c86f4a7c4 --- /dev/null +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/process/writer/AbstractControlMsgWriter.java @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.ml.process.writer; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Objects; + +/** + * A writer for sending control messages to the a native C++ process. + */ +public abstract class AbstractControlMsgWriter { + + /** + * This should be the same size as the buffer in the C++ native process. + */ + public static final int FLUSH_SPACES_LENGTH = 8192; + + protected final LengthEncodedWriter lengthEncodedWriter; + private final int numberOfFields; + + /** + * Construct the control message writer with a LengthEncodedWriter + * + * @param lengthEncodedWriter The writer + * @param numberOfFields The number of fields the process expects in each record + */ + public AbstractControlMsgWriter(LengthEncodedWriter lengthEncodedWriter, int numberOfFields) { + this.lengthEncodedWriter = Objects.requireNonNull(lengthEncodedWriter); + this.numberOfFields = numberOfFields; + } + + // todo(hendrikm): workaround, see + // https://github.com/elastic/machine-learning-cpp/issues/123 + protected void fillCommandBuffer() throws IOException { + char[] spaces = new char[FLUSH_SPACES_LENGTH]; + Arrays.fill(spaces, ' '); + writeMessage(new String(spaces)); + } + + /** + * Transform the supplied control message to length encoded values and + * write to the OutputStream. + * + * @param message The control message to write. + */ + protected void writeMessage(String message) throws IOException { + + lengthEncodedWriter.writeNumFields(numberOfFields); + + // Write blank values for all fields other than the control field + for (int i = 1; i < numberOfFields; ++i) { + lengthEncodedWriter.writeField(""); + } + + // The control field comes last + lengthEncodedWriter.writeField(message); + } +} diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/autodetect/NativeAutodetectProcessTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/autodetect/NativeAutodetectProcessTests.java index 18ee9434f0dab..52910fc6139ca 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/autodetect/NativeAutodetectProcessTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/autodetect/NativeAutodetectProcessTests.java @@ -12,7 +12,7 @@ import org.elasticsearch.xpack.ml.job.process.autodetect.params.DataLoadParams; import org.elasticsearch.xpack.ml.job.process.autodetect.params.FlushJobParams; import org.elasticsearch.xpack.ml.job.process.autodetect.params.TimeRange; -import org.elasticsearch.xpack.ml.job.process.autodetect.writer.ControlMsgToProcessWriter; +import org.elasticsearch.xpack.ml.job.process.autodetect.writer.AutodetectControlMsgWriter; import org.junit.Assert; import org.junit.Before; @@ -103,7 +103,7 @@ bos, mock(InputStream.class), mock(OutputStream.class), NUMBER_FIELDS, Collectio public void testFlush() throws IOException { InputStream logStream = mock(InputStream.class); when(logStream.read(new byte[1024])).thenReturn(-1); - ByteArrayOutputStream bos = new ByteArrayOutputStream(ControlMsgToProcessWriter.FLUSH_SPACES_LENGTH + 1024); + ByteArrayOutputStream bos = new ByteArrayOutputStream(AutodetectControlMsgWriter.FLUSH_SPACES_LENGTH + 1024); try (NativeAutodetectProcess process = new NativeAutodetectProcess("foo", logStream, bos, mock(InputStream.class), mock(OutputStream.class), NUMBER_FIELDS, Collections.emptyList(), new AutodetectResultsParser(), mock(Runnable.class))) { @@ -113,21 +113,21 @@ bos, mock(InputStream.class), mock(OutputStream.class), NUMBER_FIELDS, Collectio process.flushJob(params); ByteBuffer bb = ByteBuffer.wrap(bos.toByteArray()); - assertThat(bb.remaining(), is(greaterThan(ControlMsgToProcessWriter.FLUSH_SPACES_LENGTH))); + assertThat(bb.remaining(), is(greaterThan(AutodetectControlMsgWriter.FLUSH_SPACES_LENGTH))); } } public void testWriteResetBucketsControlMessage() throws IOException { DataLoadParams params = new DataLoadParams(TimeRange.builder().startTime("1").endTime("86400").build(), Optional.empty()); - testWriteMessage(p -> p.writeResetBucketsControlMessage(params), ControlMsgToProcessWriter.RESET_BUCKETS_MESSAGE_CODE); + testWriteMessage(p -> p.writeResetBucketsControlMessage(params), AutodetectControlMsgWriter.RESET_BUCKETS_MESSAGE_CODE); } public void testWriteUpdateConfigMessage() throws IOException { - testWriteMessage(p -> p.writeUpdateModelPlotMessage(new ModelPlotConfig()), ControlMsgToProcessWriter.UPDATE_MESSAGE_CODE); + testWriteMessage(p -> p.writeUpdateModelPlotMessage(new ModelPlotConfig()), AutodetectControlMsgWriter.UPDATE_MESSAGE_CODE); } public void testPersistJob() throws IOException { - testWriteMessage(p -> p.persistState(), ControlMsgToProcessWriter.BACKGROUND_PERSIST_MESSAGE_CODE); + testWriteMessage(p -> p.persistState(), AutodetectControlMsgWriter.BACKGROUND_PERSIST_MESSAGE_CODE); } public void testWriteMessage(CheckedConsumer writeFunction, String expectedMessageCode) throws IOException { diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/autodetect/writer/ControlMsgToProcessWriterTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/autodetect/writer/AutodetectControlMsgWriterTests.java similarity index 88% rename from x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/autodetect/writer/ControlMsgToProcessWriterTests.java rename to x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/autodetect/writer/AutodetectControlMsgWriterTests.java index 57554227e9ad3..27de6633712dc 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/autodetect/writer/ControlMsgToProcessWriterTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/process/autodetect/writer/AutodetectControlMsgWriterTests.java @@ -35,7 +35,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verifyNoMoreInteractions; -public class ControlMsgToProcessWriterTests extends ESTestCase { +public class AutodetectControlMsgWriterTests extends ESTestCase { private LengthEncodedWriter lengthEncodedWriter; @Before @@ -44,7 +44,7 @@ public void setUpMocks() { } public void testWriteFlushControlMessage_GivenAdvanceTime() throws IOException { - ControlMsgToProcessWriter writer = new ControlMsgToProcessWriter(lengthEncodedWriter, 4); + AutodetectControlMsgWriter writer = new AutodetectControlMsgWriter(lengthEncodedWriter, 4); FlushJobParams flushJobParams = FlushJobParams.builder().advanceTime("1234567890").build(); writer.writeFlushControlMessage(flushJobParams); @@ -57,7 +57,7 @@ public void testWriteFlushControlMessage_GivenAdvanceTime() throws IOException { } public void testWriteFlushControlMessage_GivenSkipTime() throws IOException { - ControlMsgToProcessWriter writer = new ControlMsgToProcessWriter(lengthEncodedWriter, 4); + AutodetectControlMsgWriter writer = new AutodetectControlMsgWriter(lengthEncodedWriter, 4); FlushJobParams flushJobParams = FlushJobParams.builder().skipTime("1234567890").build(); writer.writeFlushControlMessage(flushJobParams); @@ -70,7 +70,7 @@ public void testWriteFlushControlMessage_GivenSkipTime() throws IOException { } public void testWriteFlushControlMessage_GivenSkipAndAdvanceTime() throws IOException { - ControlMsgToProcessWriter writer = new ControlMsgToProcessWriter(lengthEncodedWriter, 4); + AutodetectControlMsgWriter writer = new AutodetectControlMsgWriter(lengthEncodedWriter, 4); FlushJobParams flushJobParams = FlushJobParams.builder().skipTime("1000").advanceTime("2000").build(); writer.writeFlushControlMessage(flushJobParams); @@ -81,7 +81,7 @@ public void testWriteFlushControlMessage_GivenSkipAndAdvanceTime() throws IOExce } public void testWriteFlushControlMessage_GivenCalcInterimResultsWithNoTimeParams() throws IOException { - ControlMsgToProcessWriter writer = new ControlMsgToProcessWriter(lengthEncodedWriter, 4); + AutodetectControlMsgWriter writer = new AutodetectControlMsgWriter(lengthEncodedWriter, 4); FlushJobParams flushJobParams = FlushJobParams.builder() .calcInterim(true).build(); @@ -95,7 +95,7 @@ public void testWriteFlushControlMessage_GivenCalcInterimResultsWithNoTimeParams } public void testWriteFlushControlMessage_GivenPlainFlush() throws IOException { - ControlMsgToProcessWriter writer = new ControlMsgToProcessWriter(lengthEncodedWriter, 4); + AutodetectControlMsgWriter writer = new AutodetectControlMsgWriter(lengthEncodedWriter, 4); FlushJobParams flushJobParams = FlushJobParams.builder().build(); writer.writeFlushControlMessage(flushJobParams); @@ -104,7 +104,7 @@ public void testWriteFlushControlMessage_GivenPlainFlush() throws IOException { } public void testWriteFlushControlMessage_GivenCalcInterimResultsWithTimeParams() throws IOException { - ControlMsgToProcessWriter writer = new ControlMsgToProcessWriter(lengthEncodedWriter, 4); + AutodetectControlMsgWriter writer = new AutodetectControlMsgWriter(lengthEncodedWriter, 4); FlushJobParams flushJobParams = FlushJobParams.builder() .calcInterim(true) .forTimeRange(TimeRange.builder().startTime("120").endTime("180").build()) @@ -120,7 +120,7 @@ public void testWriteFlushControlMessage_GivenCalcInterimResultsWithTimeParams() } public void testWriteFlushControlMessage_GivenCalcInterimAndAdvanceTime() throws IOException { - ControlMsgToProcessWriter writer = new ControlMsgToProcessWriter(lengthEncodedWriter, 4); + AutodetectControlMsgWriter writer = new AutodetectControlMsgWriter(lengthEncodedWriter, 4); FlushJobParams flushJobParams = FlushJobParams.builder() .calcInterim(true) .forTimeRange(TimeRange.builder().startTime("50").endTime("100").build()) @@ -140,7 +140,7 @@ public void testWriteFlushControlMessage_GivenCalcInterimAndAdvanceTime() throws } public void testWriteFlushMessage() throws IOException { - ControlMsgToProcessWriter writer = new ControlMsgToProcessWriter(lengthEncodedWriter, 4); + AutodetectControlMsgWriter writer = new AutodetectControlMsgWriter(lengthEncodedWriter, 4); long firstId = Long.parseLong(writer.writeFlushMessage()); Mockito.reset(lengthEncodedWriter); @@ -163,7 +163,7 @@ public void testWriteFlushMessage() throws IOException { } public void testWriteResetBucketsMessage() throws IOException { - ControlMsgToProcessWriter writer = new ControlMsgToProcessWriter(lengthEncodedWriter, 4); + AutodetectControlMsgWriter writer = new AutodetectControlMsgWriter(lengthEncodedWriter, 4); writer.writeResetBucketsMessage( new DataLoadParams(TimeRange.builder().startTime("0").endTime("600").build(), Optional.empty())); @@ -176,7 +176,7 @@ public void testWriteResetBucketsMessage() throws IOException { } public void testWriteUpdateModelPlotMessage() throws IOException { - ControlMsgToProcessWriter writer = new ControlMsgToProcessWriter(lengthEncodedWriter, 4); + AutodetectControlMsgWriter writer = new AutodetectControlMsgWriter(lengthEncodedWriter, 4); writer.writeUpdateModelPlotMessage(new ModelPlotConfig(true, "foo,bar")); @@ -188,7 +188,7 @@ public void testWriteUpdateModelPlotMessage() throws IOException { } public void testWriteUpdateDetectorRulesMessage() throws IOException { - ControlMsgToProcessWriter writer = new ControlMsgToProcessWriter(lengthEncodedWriter, 4); + AutodetectControlMsgWriter writer = new AutodetectControlMsgWriter(lengthEncodedWriter, 4); DetectionRule rule1 = new DetectionRule.Builder(createRule(5)).build(); DetectionRule rule2 = new DetectionRule.Builder(createRule(5)).build(); @@ -206,7 +206,7 @@ public void testWriteUpdateDetectorRulesMessage() throws IOException { } public void testWriteUpdateFiltersMessage() throws IOException { - ControlMsgToProcessWriter writer = new ControlMsgToProcessWriter(lengthEncodedWriter, 2); + AutodetectControlMsgWriter writer = new AutodetectControlMsgWriter(lengthEncodedWriter, 2); MlFilter filter1 = MlFilter.builder("filter_1").setItems("a").build(); MlFilter filter2 = MlFilter.builder("filter_2").setItems("b", "c").build(); @@ -221,7 +221,7 @@ public void testWriteUpdateFiltersMessage() throws IOException { } public void testWriteUpdateScheduledEventsMessage() throws IOException { - ControlMsgToProcessWriter writer = new ControlMsgToProcessWriter(lengthEncodedWriter, 2); + AutodetectControlMsgWriter writer = new AutodetectControlMsgWriter(lengthEncodedWriter, 2); ScheduledEvent.Builder event1 = new ScheduledEvent.Builder(); event1.calendarId("moon"); @@ -255,7 +255,7 @@ public void testWriteUpdateScheduledEventsMessage() throws IOException { } public void testWriteUpdateScheduledEventsMessage_GivenEmpty() throws IOException { - ControlMsgToProcessWriter writer = new ControlMsgToProcessWriter(lengthEncodedWriter, 2); + AutodetectControlMsgWriter writer = new AutodetectControlMsgWriter(lengthEncodedWriter, 2); writer.writeUpdateScheduledEventsMessage(Collections.emptyList(), TimeValue.timeValueHours(1)); @@ -267,7 +267,7 @@ public void testWriteUpdateScheduledEventsMessage_GivenEmpty() throws IOExceptio } public void testWriteStartBackgroundPersistMessage() throws IOException { - ControlMsgToProcessWriter writer = new ControlMsgToProcessWriter(lengthEncodedWriter, 2); + AutodetectControlMsgWriter writer = new AutodetectControlMsgWriter(lengthEncodedWriter, 2); writer.writeStartBackgroundPersistMessage(); InOrder inOrder = inOrder(lengthEncodedWriter); @@ -278,7 +278,7 @@ public void testWriteStartBackgroundPersistMessage() throws IOException { inOrder.verify(lengthEncodedWriter).writeNumFields(2); inOrder.verify(lengthEncodedWriter).writeField(""); StringBuilder spaces = new StringBuilder(); - IntStream.rangeClosed(1, ControlMsgToProcessWriter.FLUSH_SPACES_LENGTH).forEach(i -> spaces.append(' ')); + IntStream.rangeClosed(1, AutodetectControlMsgWriter.FLUSH_SPACES_LENGTH).forEach(i -> spaces.append(' ')); inOrder.verify(lengthEncodedWriter).writeField(spaces.toString()); inOrder.verify(lengthEncodedWriter).flush(); From c730d3953f477dac4673abab73d2a2f7c8ff883d Mon Sep 17 00:00:00 2001 From: Adrien Grand Date: Fri, 30 Nov 2018 10:32:46 +0100 Subject: [PATCH 043/115] Fix CompositeBytesReference#slice to not throw AIOOBE with legal offsets. (#35955) CompositeBytesReference#slice has two bugs: - One that makes it fail if the reference is empty and an empty slice is created, this is #35950 and is fixed by special-casing empty-slices. - One performance bug that makes it always create a composite slice when creating a slice that ends on a boundary, this is fixed by computing `limit` as the index of the sub reference that holds the last element rather than the next element after the slice. Closes #35950 --- .../common/bytes/CompositeBytesReference.java | 9 +++++- .../bytes/CompositeBytesReferenceTests.java | 15 ++++++++++ .../bytes/AbstractBytesReferenceTestCase.java | 29 ++++++++++--------- 3 files changed, 38 insertions(+), 15 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/common/bytes/CompositeBytesReference.java b/server/src/main/java/org/elasticsearch/common/bytes/CompositeBytesReference.java index 3538cba869ce5..10bc959db3375 100644 --- a/server/src/main/java/org/elasticsearch/common/bytes/CompositeBytesReference.java +++ b/server/src/main/java/org/elasticsearch/common/bytes/CompositeBytesReference.java @@ -22,6 +22,7 @@ import org.apache.lucene.util.BytesRef; import org.apache.lucene.util.BytesRefBuilder; import org.apache.lucene.util.BytesRefIterator; +import org.apache.lucene.util.FutureObjects; import org.apache.lucene.util.RamUsageEstimator; import java.io.IOException; @@ -77,10 +78,16 @@ public int length() { @Override public BytesReference slice(int from, int length) { + FutureObjects.checkFromIndexSize(from, length, this.length); + + if (length == 0) { + return BytesArray.EMPTY; + } + // for slices we only need to find the start and the end reference // adjust them and pass on the references in between as they are fully contained final int to = from + length; - final int limit = getOffsetIndex(from + length); + final int limit = getOffsetIndex(to - 1); final int start = getOffsetIndex(from); final BytesReference[] inSlice = new BytesReference[1 + (limit - start)]; for (int i = 0, j = start; i < inSlice.length; i++) { diff --git a/server/src/test/java/org/elasticsearch/common/bytes/CompositeBytesReferenceTests.java b/server/src/test/java/org/elasticsearch/common/bytes/CompositeBytesReferenceTests.java index 05d7b8dea6a5f..f99c9405502f5 100644 --- a/server/src/test/java/org/elasticsearch/common/bytes/CompositeBytesReferenceTests.java +++ b/server/src/test/java/org/elasticsearch/common/bytes/CompositeBytesReferenceTests.java @@ -23,6 +23,7 @@ import org.apache.lucene.util.BytesRefIterator; import org.elasticsearch.common.io.stream.BytesStreamOutput; import org.elasticsearch.common.io.stream.ReleasableBytesStreamOutput; +import org.hamcrest.Matchers; import java.io.IOException; import java.util.ArrayList; @@ -113,4 +114,18 @@ public void testSliceArrayOffset() throws IOException { public void testSliceToBytesRef() throws IOException { // CompositeBytesReference shifts offsets } + + public void testSliceIsNotCompositeIfMatchesSingleSubSlice() { + CompositeBytesReference bytesRef = new CompositeBytesReference( + new BytesArray(new byte[12]), + new BytesArray(new byte[15]), + new BytesArray(new byte[13])); + + // Slices that cross boundaries are composite too + assertThat(bytesRef.slice(5, 8), Matchers.instanceOf(CompositeBytesReference.class)); + + // But not slices that cover a single sub reference + assertThat(bytesRef.slice(13, 10), Matchers.not(Matchers.instanceOf(CompositeBytesReference.class))); // strictly within sub + assertThat(bytesRef.slice(12, 15), Matchers.not(Matchers.instanceOf(CompositeBytesReference.class))); // equal to sub + } } diff --git a/test/framework/src/main/java/org/elasticsearch/common/bytes/AbstractBytesReferenceTestCase.java b/test/framework/src/main/java/org/elasticsearch/common/bytes/AbstractBytesReferenceTestCase.java index f1c6bd412a502..7b1073e954f4e 100644 --- a/test/framework/src/main/java/org/elasticsearch/common/bytes/AbstractBytesReferenceTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/common/bytes/AbstractBytesReferenceTestCase.java @@ -68,20 +68,21 @@ public void testLength() throws IOException { } public void testSlice() throws IOException { - int length = randomInt(PAGE_SIZE * 3); - BytesReference pbr = newBytesReference(length); - int sliceOffset = randomIntBetween(0, length / 2); - int sliceLength = Math.max(0, length - sliceOffset - 1); - BytesReference slice = pbr.slice(sliceOffset, sliceLength); - assertEquals(sliceLength, slice.length()); - for (int i = 0; i < sliceLength; i++) { - assertEquals(pbr.get(i+sliceOffset), slice.get(i)); - } - BytesRef singlePageOrNull = getSinglePageOrNull(slice); - if (singlePageOrNull != null) { - // we can't assert the offset since if the length is smaller than the refercence - // the offset can be anywhere - assertEquals(sliceLength, singlePageOrNull.length); + for (int length : new int[] {0, 1, randomIntBetween(2, PAGE_SIZE), randomIntBetween(PAGE_SIZE + 1, 3 * PAGE_SIZE)}) { + BytesReference pbr = newBytesReference(length); + int sliceOffset = randomIntBetween(0, length / 2); + int sliceLength = Math.max(0, length - sliceOffset - 1); + BytesReference slice = pbr.slice(sliceOffset, sliceLength); + assertEquals(sliceLength, slice.length()); + for (int i = 0; i < sliceLength; i++) { + assertEquals(pbr.get(i+sliceOffset), slice.get(i)); + } + BytesRef singlePageOrNull = getSinglePageOrNull(slice); + if (singlePageOrNull != null) { + // we can't assert the offset since if the length is smaller than the refercence + // the offset can be anywhere + assertEquals(sliceLength, singlePageOrNull.length); + } } } From 928396e0f9de0c5b294dc3d06d402aa482ef227f Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Fri, 30 Nov 2018 10:58:46 +0100 Subject: [PATCH 044/115] [TEST] Set 'index.unassigned.node_left.delayed_timeout' to 0 in ccr tests Some tests kill nodes and otherwise it would take 60s by default for replicas to get allocated and that is longer than we wait for getting in a green state in tests. Relates to #35403 --- .../org/elasticsearch/xpack/CcrIntegTestCase.java | 15 +++++++++++++++ .../xpack/ccr/FollowerFailOverIT.java | 2 ++ 2 files changed, 17 insertions(+) diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/CcrIntegTestCase.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/CcrIntegTestCase.java index 07a4dc44cd20f..13ad32f27682b 100644 --- a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/CcrIntegTestCase.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/CcrIntegTestCase.java @@ -15,6 +15,7 @@ import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest; import org.elasticsearch.action.admin.indices.get.GetIndexResponse; import org.elasticsearch.action.admin.indices.refresh.RefreshResponse; +import org.elasticsearch.action.admin.indices.settings.put.UpdateSettingsRequest; import org.elasticsearch.action.admin.indices.stats.ShardStats; import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.search.SearchResponse; @@ -27,6 +28,7 @@ import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.cluster.metadata.MetaData; import org.elasticsearch.cluster.routing.ShardRouting; +import org.elasticsearch.cluster.routing.UnassignedInfo; import org.elasticsearch.cluster.routing.allocation.DiskThresholdSettings; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.Priority; @@ -142,6 +144,18 @@ public final void startClusters() throws Exception { assertAcked(followerClient().admin().cluster().updateSettings(updateSettingsRequest).actionGet()); } + /** + * Follower indices don't get all the settings from leader, for example 'index.unassigned.node_left.delayed_timeout' + * is not replicated and if tests kill nodes, we have to wait 60s by default... + */ + protected void disableDelayedAllocation(String index) { + UpdateSettingsRequest updateSettingsRequest = new UpdateSettingsRequest(index); + Settings.Builder settingsBuilder = Settings.builder(); + settingsBuilder.put(UnassignedInfo.INDEX_DELAYED_NODE_LEFT_TIMEOUT_SETTING.getKey(), 0); + updateSettingsRequest.settings(settingsBuilder); + assertAcked(followerClient().admin().indices().updateSettings(updateSettingsRequest).actionGet()); + } + @After public void afterTest() throws Exception { ensureEmptyWriteBuffers(); @@ -355,6 +369,7 @@ protected String getIndexSettings(final int numberOfShards, final int numberOfRe { builder.startObject("settings"); { + builder.field(UnassignedInfo.INDEX_DELAYED_NODE_LEFT_TIMEOUT_SETTING.getKey(), 0); builder.field("index.number_of_shards", numberOfShards); builder.field("index.number_of_replicas", numberOfReplicas); for (final Map.Entry additionalSetting : additionalIndexSettings.entrySet()) { diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/FollowerFailOverIT.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/FollowerFailOverIT.java index 6685776e9805d..d58a2d0a0f18d 100644 --- a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/FollowerFailOverIT.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/FollowerFailOverIT.java @@ -85,6 +85,7 @@ public void testFailOverOnFollower() throws Exception { follow.getFollowRequest().setMaxOutstandingWriteRequests(randomIntBetween(1, 10)); logger.info("--> follow params {}", Strings.toString(follow.getFollowRequest())); followerClient().execute(PutFollowAction.INSTANCE, follow).get(); + disableDelayedAllocation("follower-index"); ensureFollowerGreen("follower-index"); awaitGlobalCheckpointAtLeast(followerClient(), new ShardId(resolveFollowerIndex("follower-index"), 0), between(30, 80)); final ClusterState clusterState = getFollowerCluster().clusterService().state(); @@ -143,6 +144,7 @@ public void testFollowIndexAndCloseNode() throws Exception { followRequest.getFollowRequest().setMaxWriteRequestSize(new ByteSizeValue(randomIntBetween(1, 4096), ByteSizeUnit.KB)); followRequest.getFollowRequest().setMaxOutstandingWriteRequests(randomIntBetween(1, 10)); followerClient().execute(PutFollowAction.INSTANCE, followRequest).get(); + disableDelayedAllocation("index2"); logger.info("--> follow params {}", Strings.toString(followRequest.getFollowRequest())); int maxOpsPerRead = followRequest.getFollowRequest().getMaxReadRequestOperationCount(); From d8789afff26db4c9c45a7442194c734e3c49aab3 Mon Sep 17 00:00:00 2001 From: Jim Ferenczi Date: Fri, 30 Nov 2018 11:02:46 +0100 Subject: [PATCH 045/115] HLRC: Add get watch API (#35531) This changes adds the support for the get watch API in the high level rest client. --- .../elasticsearch/client/WatcherClient.java | 30 +++ .../client/WatcherRequestConverters.java | 15 +- .../client/watcher/GetWatchRequest.java | 54 +++++ .../client/watcher/GetWatchResponse.java | 148 +++++++++++ .../client/watcher/WatchStatus.java | 27 ++- .../client/WatcherRequestConvertersTests.java | 11 + .../documentation/WatcherDocumentationIT.java | 47 ++++ .../watcher/WatchRequestValidationTests.java | 6 + .../high-level/supported-apis.asciidoc | 2 + .../high-level/watcher/get-watch.asciidoc | 36 +++ .../support/xcontent/XContentSource.java | 21 ++ .../actions/get/GetWatchResponse.java | 54 ++++- .../xpack/core/watcher/watch/WatchStatus.java | 8 +- .../xpack/watcher/GetWatchResponseTests.java | 229 ++++++++++++++++++ .../actions/get/TransportGetWatchAction.java | 3 +- 15 files changed, 676 insertions(+), 15 deletions(-) create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/watcher/GetWatchRequest.java create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/watcher/GetWatchResponse.java create mode 100644 docs/java-rest/high-level/watcher/get-watch.asciidoc create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/watcher/GetWatchResponseTests.java diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/WatcherClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/WatcherClient.java index c06493aea7381..ed0043c801c7b 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/WatcherClient.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/WatcherClient.java @@ -26,6 +26,8 @@ import org.elasticsearch.client.watcher.ActivateWatchResponse; import org.elasticsearch.client.watcher.AckWatchRequest; import org.elasticsearch.client.watcher.AckWatchResponse; +import org.elasticsearch.client.watcher.GetWatchRequest; +import org.elasticsearch.client.watcher.GetWatchResponse; import org.elasticsearch.client.watcher.StartWatchServiceRequest; import org.elasticsearch.client.watcher.StopWatchServiceRequest; import org.elasticsearch.client.watcher.DeleteWatchRequest; @@ -129,6 +131,34 @@ public void putWatchAsync(PutWatchRequest request, RequestOptions options, PutWatchResponse::fromXContent, listener, emptySet()); } + /** + * Gets a watch from the cluster + * See + * the docs for more. + * @param request the request + * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @return the response + * @throws IOException in case there is a problem sending the request or parsing back the response + */ + public GetWatchResponse getWatch(GetWatchRequest request, RequestOptions options) throws IOException { + return restHighLevelClient.performRequestAndParseEntity(request, WatcherRequestConverters::getWatch, options, + GetWatchResponse::fromXContent, emptySet()); + } + + /** + * Asynchronously gets a watch into the cluster + * See + * the docs for more. + * @param request the request + * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @param listener the listener to be notified upon request completion + */ + public void getWatchAsync(GetWatchRequest request, RequestOptions options, + ActionListener listener) { + restHighLevelClient.performRequestAsyncAndParseEntity(request, WatcherRequestConverters::getWatch, options, + GetWatchResponse::fromXContent, listener, emptySet()); + } + /** * Deactivate an existing watch * See diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/WatcherRequestConverters.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/WatcherRequestConverters.java index a017779495b9f..1da7ef4c617ff 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/WatcherRequestConverters.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/WatcherRequestConverters.java @@ -28,12 +28,13 @@ import org.elasticsearch.client.watcher.DeactivateWatchRequest; import org.elasticsearch.client.watcher.ActivateWatchRequest; import org.elasticsearch.client.watcher.AckWatchRequest; +import org.elasticsearch.client.watcher.DeleteWatchRequest; +import org.elasticsearch.client.watcher.GetWatchRequest; +import org.elasticsearch.client.watcher.PutWatchRequest; import org.elasticsearch.client.watcher.StartWatchServiceRequest; import org.elasticsearch.client.watcher.StopWatchServiceRequest; import org.elasticsearch.client.watcher.WatcherStatsRequest; import org.elasticsearch.common.bytes.BytesReference; -import org.elasticsearch.client.watcher.DeleteWatchRequest; -import org.elasticsearch.client.watcher.PutWatchRequest; final class WatcherRequestConverters { @@ -76,6 +77,16 @@ static Request putWatch(PutWatchRequest putWatchRequest) { return request; } + + static Request getWatch(GetWatchRequest getWatchRequest) { + String endpoint = new RequestConverters.EndpointBuilder() + .addPathPartAsIs("_xpack", "watcher", "watch") + .addPathPart(getWatchRequest.getId()) + .build(); + + return new Request(HttpGet.METHOD_NAME, endpoint); + } + static Request deactivateWatch(DeactivateWatchRequest deactivateWatchRequest) { String endpoint = new RequestConverters.EndpointBuilder() .addPathPartAsIs("_xpack") diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/watcher/GetWatchRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/watcher/GetWatchRequest.java new file mode 100644 index 0000000000000..fae2d31a256be --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/watcher/GetWatchRequest.java @@ -0,0 +1,54 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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.elasticsearch.client.watcher; + +import org.elasticsearch.client.Validatable; +import org.elasticsearch.client.ValidationException; + +/** + * The request to get the watch by name (id) + */ +public final class GetWatchRequest implements Validatable { + + private final String id; + + public GetWatchRequest(String watchId) { + validateId(watchId); + this.id = watchId; + } + + private void validateId(String id) { + ValidationException exception = new ValidationException(); + if (id == null) { + exception.addValidationError("watch id is missing"); + } else if (PutWatchRequest.isValidId(id) == false) { + exception.addValidationError("watch id contains whitespace"); + } + if (exception.validationErrors().isEmpty() == false) { + throw exception; + } + } + + /** + * @return The name of the watch to retrieve + */ + public String getId() { + return id; + } +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/watcher/GetWatchResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/watcher/GetWatchResponse.java new file mode 100644 index 0000000000000..9f5934b33eb30 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/watcher/GetWatchResponse.java @@ -0,0 +1,148 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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.elasticsearch.client.watcher; + +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.lucene.uid.Versions; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentType; + +import java.io.IOException; +import java.util.Map; +import java.util.Objects; + +public class GetWatchResponse { + private final String id; + private final long version; + private final WatchStatus status; + + private final BytesReference source; + private final XContentType xContentType; + + /** + * Ctor for missing watch + */ + public GetWatchResponse(String id) { + this(id, Versions.NOT_FOUND, null, null, null); + } + + public GetWatchResponse(String id, long version, WatchStatus status, BytesReference source, XContentType xContentType) { + this.id = id; + this.version = version; + this.status = status; + this.source = source; + this.xContentType = xContentType; + } + + public String getId() { + return id; + } + + public long getVersion() { + return version; + } + + public boolean isFound() { + return version != Versions.NOT_FOUND; + } + + public WatchStatus getStatus() { + return status; + } + + /** + * Returns the {@link XContentType} of the source + */ + public XContentType getContentType() { + return xContentType; + } + + /** + * Returns the serialized watch + */ + public BytesReference getSource() { + return source; + } + + /** + * Returns the source as a map + */ + public Map getSourceAsMap() { + return source == null ? null : XContentHelper.convertToMap(source, false, getContentType()).v2(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + GetWatchResponse that = (GetWatchResponse) o; + return version == that.version && + Objects.equals(id, that.id) && + Objects.equals(status, that.status) && + Objects.equals(xContentType, that.xContentType) && + Objects.equals(source, that.source); + } + + @Override + public int hashCode() { + return Objects.hash(id, status, source, version); + } + + private static final ParseField ID_FIELD = new ParseField("_id"); + private static final ParseField FOUND_FIELD = new ParseField("found"); + private static final ParseField VERSION_FIELD = new ParseField("_version"); + private static final ParseField STATUS_FIELD = new ParseField("status"); + private static final ParseField WATCH_FIELD = new ParseField("watch"); + + private static ConstructingObjectParser PARSER = + new ConstructingObjectParser<>("get_watch_response", true, + a -> { + boolean isFound = (boolean) a[1]; + if (isFound) { + XContentBuilder builder = (XContentBuilder) a[4]; + BytesReference source = BytesReference.bytes(builder); + return new GetWatchResponse((String) a[0], (long) a[2], (WatchStatus) a[3], source, builder.contentType()); + } else { + return new GetWatchResponse((String) a[0]); + } + }); + + static { + PARSER.declareString(ConstructingObjectParser.constructorArg(), ID_FIELD); + PARSER.declareBoolean(ConstructingObjectParser.constructorArg(), FOUND_FIELD); + PARSER.declareLong(ConstructingObjectParser.optionalConstructorArg(), VERSION_FIELD); + PARSER.declareObject(ConstructingObjectParser.optionalConstructorArg(), + (parser, context) -> WatchStatus.parse(parser), STATUS_FIELD); + PARSER.declareObject(ConstructingObjectParser.optionalConstructorArg(), + (parser, context) -> { + try (XContentBuilder builder = XContentBuilder.builder(parser.contentType().xContent())) { + builder.copyCurrentStructure(parser); + return builder; + } + }, WATCH_FIELD); + } + + public static GetWatchResponse fromXContent(XContentParser parser) throws IOException { + return PARSER.parse(parser, null); + } +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/watcher/WatchStatus.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/watcher/WatchStatus.java index 04b747c03635a..ab543abbf594a 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/watcher/WatchStatus.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/watcher/WatchStatus.java @@ -20,6 +20,7 @@ package org.elasticsearch.client.watcher; import org.elasticsearch.ElasticsearchParseException; +import org.elasticsearch.common.Nullable; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.xcontent.XContentParser; import org.joda.time.DateTime; @@ -44,19 +45,22 @@ public class WatchStatus { private final DateTime lastMetCondition; private final long version; private final Map actions; + @Nullable private Map headers; public WatchStatus(long version, State state, ExecutionState executionState, DateTime lastChecked, DateTime lastMetCondition, - Map actions) { + Map actions, + Map headers) { this.version = version; this.lastChecked = lastChecked; this.lastMetCondition = lastMetCondition; this.actions = actions; this.state = state; this.executionState = executionState; + this.headers = headers; } public State state() { @@ -79,6 +83,10 @@ public ActionStatus actionStatus(String actionId) { return actions.get(actionId); } + public Map getActions() { + return actions; + } + public long version() { return version; } @@ -87,6 +95,10 @@ public ExecutionState getExecutionState() { return executionState; } + public Map getHeaders() { + return headers; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -98,7 +110,8 @@ public boolean equals(Object o) { Objects.equals(lastMetCondition, that.lastMetCondition) && Objects.equals(version, that.version) && Objects.equals(executionState, that.executionState) && - Objects.equals(actions, that.actions); + Objects.equals(actions, that.actions) && + Objects.equals(headers, that.headers); } @Override @@ -112,6 +125,7 @@ public static WatchStatus parse(XContentParser parser) throws IOException { DateTime lastChecked = null; DateTime lastMetCondition = null; Map actions = null; + Map headers = null; long version = -1; ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.currentToken(), parser::getTokenLocation); @@ -172,13 +186,17 @@ public static WatchStatus parse(XContentParser parser) throws IOException { throw new ElasticsearchParseException("could not parse watch status. expecting field [{}] to be an object, " + "found [{}] instead", currentFieldName, token); } + } else if (Field.HEADERS.match(currentFieldName, parser.getDeprecationHandler())) { + if (token == XContentParser.Token.START_OBJECT) { + headers = parser.mapStrings(); + } } else { parser.skipChildren(); } } actions = actions == null ? emptyMap() : unmodifiableMap(actions); - return new WatchStatus(version, state, executionState, lastChecked, lastMetCondition, actions); + return new WatchStatus(version, state, executionState, lastChecked, lastMetCondition, actions, headers); } public static class State { @@ -214,6 +232,8 @@ public static State parse(XContentParser parser) throws IOException { active = parser.booleanValue(); } else if (Field.TIMESTAMP.match(currentFieldName, parser.getDeprecationHandler())) { timestamp = parseDate(currentFieldName, parser); + } else { + parser.skipChildren(); } } return new State(active, timestamp); @@ -229,5 +249,6 @@ public interface Field { ParseField ACTIONS = new ParseField("actions"); ParseField VERSION = new ParseField("version"); ParseField EXECUTION_STATE = new ParseField("execution_state"); + ParseField HEADERS = new ParseField("headers"); } } diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/WatcherRequestConvertersTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/WatcherRequestConvertersTests.java index 2712dbc0438db..ff7050fd67ce7 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/WatcherRequestConvertersTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/WatcherRequestConvertersTests.java @@ -28,6 +28,7 @@ import org.elasticsearch.client.watcher.DeactivateWatchRequest; import org.elasticsearch.client.watcher.DeleteWatchRequest; import org.elasticsearch.client.watcher.PutWatchRequest; +import org.elasticsearch.client.watcher.GetWatchRequest; import org.elasticsearch.client.watcher.StartWatchServiceRequest; import org.elasticsearch.client.watcher.StopWatchServiceRequest; import org.elasticsearch.client.watcher.WatcherStatsRequest; @@ -91,6 +92,16 @@ public void testPutWatch() throws Exception { assertThat(bos.toString("UTF-8"), is(body)); } + public void testGetWatch() throws Exception { + String watchId = randomAlphaOfLength(10); + GetWatchRequest getWatchRequest = new GetWatchRequest(watchId); + + Request request = WatcherRequestConverters.getWatch(getWatchRequest); + assertEquals(HttpGet.METHOD_NAME, request.getMethod()); + assertEquals("/_xpack/watcher/watch/" + watchId, request.getEndpoint()); + assertThat(request.getEntity(), nullValue()); + } + public void testDeactivateWatch() { String watchId = randomAlphaOfLength(10); DeactivateWatchRequest deactivateWatchRequest = new DeactivateWatchRequest(watchId); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/WatcherDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/WatcherDocumentationIT.java index 74562a1d17fd0..03dfc2ea7e088 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/WatcherDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/WatcherDocumentationIT.java @@ -34,6 +34,8 @@ import org.elasticsearch.client.watcher.ActionStatus.AckStatus; import org.elasticsearch.client.watcher.DeactivateWatchRequest; import org.elasticsearch.client.watcher.DeactivateWatchResponse; +import org.elasticsearch.client.watcher.GetWatchRequest; +import org.elasticsearch.client.watcher.GetWatchResponse; import org.elasticsearch.client.watcher.StartWatchServiceRequest; import org.elasticsearch.client.watcher.StopWatchServiceRequest; import org.elasticsearch.client.watcher.WatchStatus; @@ -197,6 +199,51 @@ public void onFailure(Exception e) { assertTrue(latch.await(30L, TimeUnit.SECONDS)); } + { + //tag::get-watch-request + GetWatchRequest request = new GetWatchRequest("my_watch_id"); + //end::get-watch-request + + //tag::ack-watch-execute + GetWatchResponse response = client.watcher().getWatch(request, RequestOptions.DEFAULT); + //end::get-watch-request + + //tag::get-watch-response + String watchId = response.getId(); // <1> + boolean found = response.isFound(); // <2> + long version = response.getVersion(); // <3> + WatchStatus status = response.getStatus(); // <4> + BytesReference source = response.getSource(); // <5> + //end::get-watch-response + } + + { + GetWatchRequest request = new GetWatchRequest("my_other_watch_id"); + // tag::get-watch-execute-listener + ActionListener listener = new ActionListener() { + @Override + public void onResponse(GetWatchResponse response) { + // <1> + } + + @Override + public void onFailure(Exception e) { + // <2> + } + }; + // end::get-watch-execute-listener + + // Replace the empty listener by a blocking listener in test + final CountDownLatch latch = new CountDownLatch(1); + listener = new LatchedActionListener<>(listener, latch); + + // tag::get-watch-execute-async + client.watcher().getWatchAsync(request, RequestOptions.DEFAULT, listener); // <1> + // end::get-watch-execute-async + + assertTrue(latch.await(30L, TimeUnit.SECONDS)); + } + { //tag::x-pack-delete-watch-execute DeleteWatchRequest request = new DeleteWatchRequest("my_watch_id"); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/watcher/WatchRequestValidationTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/watcher/WatchRequestValidationTests.java index 1fea3bccb62a7..7f3bc2e0c8931 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/watcher/WatchRequestValidationTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/watcher/WatchRequestValidationTests.java @@ -91,4 +91,10 @@ public void testPutWatchContentNull() { () -> new PutWatchRequest("foo", BytesArray.EMPTY, null)); assertThat(exception.getMessage(), is("request body is missing")); } + + public void testGetWatchInvalidWatchId() { + ValidationException e = expectThrows(ValidationException.class, + () -> new GetWatchRequest("id with whitespaces")); + assertThat(e.validationErrors(), hasItem("watch id contains whitespace")); + } } diff --git a/docs/java-rest/high-level/supported-apis.asciidoc b/docs/java-rest/high-level/supported-apis.asciidoc index 026fa32e4c4ba..f9b89caaeeb2a 100644 --- a/docs/java-rest/high-level/supported-apis.asciidoc +++ b/docs/java-rest/high-level/supported-apis.asciidoc @@ -421,6 +421,7 @@ The Java High Level REST Client supports the following Watcher APIs: * <<{upid}-start-watch-service>> * <<{upid}-stop-watch-service>> * <> +* <> * <> * <> * <<{upid}-ack-watch>> @@ -430,6 +431,7 @@ The Java High Level REST Client supports the following Watcher APIs: include::watcher/start-watch-service.asciidoc[] include::watcher/stop-watch-service.asciidoc[] include::watcher/put-watch.asciidoc[] +include::watcher/get-watch.asciidoc[] include::watcher/delete-watch.asciidoc[] include::watcher/ack-watch.asciidoc[] include::watcher/deactivate-watch.asciidoc[] diff --git a/docs/java-rest/high-level/watcher/get-watch.asciidoc b/docs/java-rest/high-level/watcher/get-watch.asciidoc new file mode 100644 index 0000000000000..c4773d70ad731 --- /dev/null +++ b/docs/java-rest/high-level/watcher/get-watch.asciidoc @@ -0,0 +1,36 @@ +-- +:api: get-watch +:request: GetWatchRequest +:response: GetWatchResponse +-- + +[id="{upid}-{api}"] +=== Get Watch API + +[id="{upid}-{api}-request"] +==== Execution + +A watch can be retrieved as follows: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[{api}-request] +-------------------------------------------------- + +[id="{upid}-{api}-response"] +==== Response + +The returned +{response}+ contains `id`, `version`, `status` and `source` +information. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[{api}-response] +-------------------------------------------------- +<1> `_id`, id of the watch +<2> `found` is a boolean indicating whether the watch was found +<2> `_version` returns the version of the watch +<3> `status` contains status of the watch +<4> `source` the source of the watch + +include::../execution.asciidoc[] \ No newline at end of file diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/support/xcontent/XContentSource.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/support/xcontent/XContentSource.java index e0724795c297c..b54acc441e74a 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/support/xcontent/XContentSource.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/support/xcontent/XContentSource.java @@ -23,6 +23,7 @@ import java.io.InputStream; import java.util.List; import java.util.Map; +import java.util.Objects; /** * Encapsulates the xcontent source @@ -51,6 +52,13 @@ public XContentSource(XContentBuilder builder) { this(BytesReference.bytes(builder), builder.contentType()); } + /** + * @return The content type of the source + */ + public XContentType getContentType() { + return contentType; + } + /** * @return The bytes reference of the source */ @@ -133,4 +141,17 @@ private Object data() { return data; } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + XContentSource that = (XContentSource) o; + return Objects.equals(bytes, that.bytes) && + contentType == that.contentType; + } + + @Override + public int hashCode() { + return Objects.hash(bytes, contentType); + } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/transport/actions/get/GetWatchResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/transport/actions/get/GetWatchResponse.java index 6fb2a22a50495..ab492181c72d4 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/transport/actions/get/GetWatchResponse.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/transport/actions/get/GetWatchResponse.java @@ -7,21 +7,23 @@ import org.elasticsearch.Version; import org.elasticsearch.action.ActionResponse; -import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.lucene.uid.Versions; -import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.xpack.core.watcher.support.xcontent.XContentSource; import org.elasticsearch.xpack.core.watcher.watch.WatchStatus; import java.io.IOException; +import java.util.Objects; -public class GetWatchResponse extends ActionResponse { +public class GetWatchResponse extends ActionResponse implements ToXContent { private String id; private WatchStatus status; - private boolean found = false; + private boolean found; private XContentSource source; private long version; @@ -33,19 +35,20 @@ public GetWatchResponse() { */ public GetWatchResponse(String id) { this.id = id; + this.status = null; this.found = false; this.source = null; - version = Versions.NOT_FOUND; + this.version = Versions.NOT_FOUND; } /** * ctor for found watch */ - public GetWatchResponse(String id, long version, WatchStatus status, BytesReference source, XContentType contentType) { + public GetWatchResponse(String id, long version, WatchStatus status, XContentSource source) { this.id = id; this.status = status; this.found = true; - this.source = new XContentSource(source, contentType); + this.source = source; this.version = version; } @@ -82,6 +85,10 @@ public void readFrom(StreamInput in) throws IOException { } else { version = Versions.MATCH_ANY; } + } else { + status = null; + source = null; + version = Versions.NOT_FOUND; } } @@ -98,4 +105,37 @@ public void writeTo(StreamOutput out) throws IOException { } } } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.field("found", found); + builder.field("_id", id); + if (found) { + builder.field("_version", version); + builder.field("status", status, params); + builder.field("watch", source, params); + } + return builder; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + GetWatchResponse that = (GetWatchResponse) o; + return version == that.version && + Objects.equals(id, that.id) && + Objects.equals(status, that.status) && + Objects.equals(source, that.source); + } + + @Override + public int hashCode() { + return Objects.hash(id, status, version); + } + + @Override + public String toString() { + return Strings.toString(this); + } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/watch/WatchStatus.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/watch/WatchStatus.java index 38757b1204dc0..ed44d13cb29c9 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/watch/WatchStatus.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/watch/WatchStatus.java @@ -59,8 +59,8 @@ public WatchStatus(DateTime now, Map actions) { this(-1, new State(true, now), null, null, null, actions, Collections.emptyMap()); } - private WatchStatus(long version, State state, ExecutionState executionState, DateTime lastChecked, DateTime lastMetCondition, - Map actions, Map headers) { + public WatchStatus(long version, State state, ExecutionState executionState, DateTime lastChecked, DateTime lastMetCondition, + Map actions, Map headers) { this.version = version; this.lastChecked = lastChecked; this.lastMetCondition = lastMetCondition; @@ -350,6 +350,8 @@ public static WatchStatus parse(String watchId, XContentParser parser, Clock clo if (token == XContentParser.Token.START_OBJECT) { headers = parser.mapStrings(); } + } else { + parser.skipChildren(); } } @@ -405,6 +407,8 @@ public static State parse(XContentParser parser, Clock clock) throws IOException active = parser.booleanValue(); } else if (Field.TIMESTAMP.match(currentFieldName, parser.getDeprecationHandler())) { timestamp = parseDate(currentFieldName, parser, UTC); + } else { + parser.skipChildren(); } } return new State(active, timestamp); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/watcher/GetWatchResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/watcher/GetWatchResponseTests.java new file mode 100644 index 0000000000000..bcfed1c7b0be4 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/watcher/GetWatchResponseTests.java @@ -0,0 +1,229 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.protocol.xpack.watcher; + +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.xcontent.DeprecationHandler; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.protocol.AbstractHlrcStreamableXContentTestCase; +import org.elasticsearch.xpack.core.watcher.actions.ActionStatus; +import org.elasticsearch.xpack.core.watcher.execution.ExecutionState; +import org.elasticsearch.xpack.core.watcher.support.xcontent.XContentSource; +import org.elasticsearch.xpack.core.watcher.transport.actions.get.GetWatchResponse; +import org.elasticsearch.xpack.core.watcher.watch.WatchStatus; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Predicate; + +public class GetWatchResponseTests extends + AbstractHlrcStreamableXContentTestCase { + + private static final String[] SHUFFLE_FIELDS_EXCEPTION = new String[] { "watch" }; + + @Override + protected String[] getShuffleFieldsExceptions() { + return SHUFFLE_FIELDS_EXCEPTION; + } + + @Override + protected ToXContent.Params getToXContentParams() { + return new ToXContent.MapParams(Collections.singletonMap("hide_headers", "false")); + } + + @Override + protected Predicate getRandomFieldsExcludeFilter() { + return f -> f.contains("watch") || f.contains("actions") || f.contains("headers"); + } + + @Override + protected void assertEqualInstances(GetWatchResponse expectedInstance, GetWatchResponse newInstance) { + if (expectedInstance.isFound() && + expectedInstance.getSource().getContentType() != newInstance.getSource().getContentType()) { + /** + * The {@link GetWatchResponse#getContentType()} depends on the content type that + * was used to serialize the main object so we use the same content type than the + * expectedInstance to translate the watch of the newInstance. + */ + XContent from = XContentFactory.xContent(newInstance.getSource().getContentType()); + XContent to = XContentFactory.xContent(expectedInstance.getSource().getContentType()); + final BytesReference newSource; + // It is safe to use EMPTY here because this never uses namedObject + try (InputStream stream = newInstance.getSource().getBytes().streamInput(); + XContentParser parser = XContentFactory.xContent(from.type()).createParser(NamedXContentRegistry.EMPTY, + DeprecationHandler.THROW_UNSUPPORTED_OPERATION, stream)) { + parser.nextToken(); + XContentBuilder builder = XContentFactory.contentBuilder(to.type()); + builder.copyCurrentStructure(parser); + newSource = BytesReference.bytes(builder); + } catch (IOException e) { + throw new AssertionError(e); + } + newInstance = new GetWatchResponse(newInstance.getId(), newInstance.getVersion(), + newInstance.getStatus(), new XContentSource(newSource, expectedInstance.getSource().getContentType())); + } + super.assertEqualInstances(expectedInstance, newInstance); + } + + @Override + protected GetWatchResponse createBlankInstance() { + return new GetWatchResponse(); + } + + @Override + protected GetWatchResponse createTestInstance() { + String id = randomAlphaOfLength(10); + if (rarely()) { + return new GetWatchResponse(id); + } + long version = randomLongBetween(0, 10); + WatchStatus status = randomWatchStatus(); + BytesReference source = simpleWatch(); + return new GetWatchResponse(id, version, status, new XContentSource(source, XContentType.JSON)); + } + + private static BytesReference simpleWatch() { + try { + XContentBuilder builder = XContentBuilder.builder(XContentType.JSON.xContent()); + builder.startObject() + .startObject("trigger") + .startObject("schedule") + .field("interval", "10h") + .endObject() + .endObject() + .startObject("input") + .startObject("none").endObject() + .endObject() + .startObject("actions") + .startObject("logme") + .field("text", "{{ctx.payload}}") + .endObject() + .endObject().endObject(); + return BytesReference.bytes(builder); + } catch (IOException e) { + throw new AssertionError(e); + } + } + + private static WatchStatus randomWatchStatus() { + long version = randomLongBetween(-1, Long.MAX_VALUE); + WatchStatus.State state = new WatchStatus.State(randomBoolean(), DateTime.now(DateTimeZone.UTC)); + ExecutionState executionState = randomFrom(ExecutionState.values()); + DateTime lastChecked = rarely() ? null : DateTime.now(DateTimeZone.UTC); + DateTime lastMetCondition = rarely() ? null : DateTime.now(DateTimeZone.UTC); + int size = randomIntBetween(0, 5); + Map actionMap = new HashMap<>(); + for (int i = 0; i < size; i++) { + ActionStatus.AckStatus ack = new ActionStatus.AckStatus( + DateTime.now(DateTimeZone.UTC), + randomFrom(ActionStatus.AckStatus.State.values()) + ); + ActionStatus actionStatus = new ActionStatus( + ack, + randomBoolean() ? null : randomExecution(), + randomBoolean() ? null : randomExecution(), + randomBoolean() ? null : randomThrottle() + ); + actionMap.put(randomAlphaOfLength(10), actionStatus); + } + Map headers = randomBoolean() ? new HashMap<>() : null; + if (headers != null) { + int headerSize = randomIntBetween(1, 5); + for (int i = 0; i < headerSize; i++) { + headers.put(randomAlphaOfLengthBetween(5, 10), randomAlphaOfLengthBetween(1, 10)); + } + } + return new WatchStatus(version, state, executionState, lastChecked, lastMetCondition, actionMap, headers); + } + + private static ActionStatus.Throttle randomThrottle() { + return new ActionStatus.Throttle(DateTime.now(DateTimeZone.UTC), randomAlphaOfLengthBetween(10, 20)); + } + + private static ActionStatus.Execution randomExecution() { + if (randomBoolean()) { + return null; + } else if (randomBoolean()) { + return ActionStatus.Execution.failure(DateTime.now(DateTimeZone.UTC), randomAlphaOfLengthBetween(10, 20)); + } else { + return ActionStatus.Execution.successful(DateTime.now(DateTimeZone.UTC)); + } + } + + @Override + public org.elasticsearch.client.watcher.GetWatchResponse doHlrcParseInstance(XContentParser parser) throws IOException { + return org.elasticsearch.client.watcher.GetWatchResponse.fromXContent(parser); + } + + @Override + public GetWatchResponse convertHlrcToInternal(org.elasticsearch.client.watcher.GetWatchResponse instance) { + if (instance.isFound()) { + return new GetWatchResponse(instance.getId(), instance.getVersion(), convertHlrcToInternal(instance.getStatus()), + new XContentSource(instance.getSource(), instance.getContentType())); + } else { + return new GetWatchResponse(instance.getId()); + } + } + + private static WatchStatus convertHlrcToInternal(org.elasticsearch.client.watcher.WatchStatus status) { + final Map actions = new HashMap<>(); + for (Map.Entry entry : status.getActions().entrySet()) { + actions.put(entry.getKey(), convertHlrcToInternal(entry.getValue())); + } + return new WatchStatus(status.version(), + convertHlrcToInternal(status.state()), + status.getExecutionState() == null ? null : convertHlrcToInternal(status.getExecutionState()), + status.lastChecked(), status.lastMetCondition(), actions, status.getHeaders() + ); + } + + private static ActionStatus convertHlrcToInternal(org.elasticsearch.client.watcher.ActionStatus actionStatus) { + return new ActionStatus(convertHlrcToInternal(actionStatus.ackStatus()), + actionStatus.lastExecution() == null ? null : convertHlrcToInternal(actionStatus.lastExecution()), + actionStatus.lastSuccessfulExecution() == null ? null : convertHlrcToInternal(actionStatus.lastSuccessfulExecution()), + actionStatus.lastThrottle() == null ? null : convertHlrcToInternal(actionStatus.lastThrottle()) + ); + } + + private static ActionStatus.AckStatus convertHlrcToInternal(org.elasticsearch.client.watcher.ActionStatus.AckStatus ackStatus) { + return new ActionStatus.AckStatus(ackStatus.timestamp(), convertHlrcToInternal(ackStatus.state())); + } + + private static ActionStatus.AckStatus.State convertHlrcToInternal(org.elasticsearch.client.watcher.ActionStatus.AckStatus.State state) { + return ActionStatus.AckStatus.State.valueOf(state.name()); + } + + private static WatchStatus.State convertHlrcToInternal(org.elasticsearch.client.watcher.WatchStatus.State state) { + return new WatchStatus.State(state.isActive(), state.getTimestamp()); + } + + private static ExecutionState convertHlrcToInternal(org.elasticsearch.client.watcher.ExecutionState executionState) { + return ExecutionState.valueOf(executionState.name()); + } + + private static ActionStatus.Execution convertHlrcToInternal(org.elasticsearch.client.watcher.ActionStatus.Execution execution) { + if (execution.successful()) { + return ActionStatus.Execution.successful(execution.timestamp()); + } else { + return ActionStatus.Execution.failure(execution.timestamp(), execution.reason()); + } + } + + private static ActionStatus.Throttle convertHlrcToInternal(org.elasticsearch.client.watcher.ActionStatus.Throttle throttle) { + return new ActionStatus.Throttle(throttle.timestamp(), throttle.reason()); + } +} diff --git a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/transport/actions/get/TransportGetWatchAction.java b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/transport/actions/get/TransportGetWatchAction.java index 6aa2bfe270aeb..08ac2ddad391d 100644 --- a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/transport/actions/get/TransportGetWatchAction.java +++ b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/transport/actions/get/TransportGetWatchAction.java @@ -24,6 +24,7 @@ import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; import org.elasticsearch.xpack.core.watcher.support.xcontent.WatcherParams; +import org.elasticsearch.xpack.core.watcher.support.xcontent.XContentSource; import org.elasticsearch.xpack.core.watcher.transport.actions.get.GetWatchAction; import org.elasticsearch.xpack.core.watcher.transport.actions.get.GetWatchRequest; import org.elasticsearch.xpack.core.watcher.transport.actions.get.GetWatchResponse; @@ -79,7 +80,7 @@ protected void masterOperation(GetWatchRequest request, ClusterState state, watch.version(getResponse.getVersion()); watch.status().version(getResponse.getVersion()); listener.onResponse(new GetWatchResponse(watch.id(), getResponse.getVersion(), watch.status(), - BytesReference.bytes(builder), XContentType.JSON)); + new XContentSource(BytesReference.bytes(builder), XContentType.JSON))); } } else { listener.onResponse(new GetWatchResponse(request.getId())); From 1428be16d58e74173af065137da8a7dccc76ca6f Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Fri, 30 Nov 2018 06:50:56 -0500 Subject: [PATCH 046/115] Warn in multi-search on unknown keys in meatdata (#36104) In the metadata section for each request in a multi-search request, we allow for only certain keys to be recognized and parsed. However, the parsing here is silently lenient, so that unknown keys can go undetected. In 7.x we will reject such keys. This commit deprecates this lenient behavior in 6.x to avoid surprising users when they upgrade. --- .../client/RestHighLevelClient.java | 2 ++ .../client/ml/job/config/RuleScope.java | 4 ++++ .../DeleteRoleMappingResponseTests.java | 4 ++++ .../security/ExpressionRoleMappingTests.java | 4 ++++ .../security/GetPrivilegesResponseTests.java | 4 ++++ .../security/GetRoleMappingsResponseTests.java | 4 ++++ .../client/security/GetRolesResponseTests.java | 4 ++++ .../RoleMapperExpressionParserTests.java | 6 +++++- .../privileges/ApplicationPrivilegeTests.java | 4 ++++ .../common/xcontent/DeprecationHandler.java | 11 +++++++++++ .../action/search/MultiSearchRequest.java | 18 ++++++++++++++++-- .../xcontent/LoggingDeprecationHandler.java | 6 ++++++ .../action/search/MultiSearchRequestTests.java | 9 +++++++++ .../LoggingDeprecationAccumulationHandler.java | 7 +++++++ 14 files changed, 84 insertions(+), 3 deletions(-) diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java index e87fc4e328f3b..6b588e4412184 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java @@ -2067,6 +2067,8 @@ static boolean convertExistsResponse(Response response) { public void usedDeprecatedName(String usedName, String modernName) {} @Override public void usedDeprecatedField(String usedName, String replacedWith) {} + @Override + public void deprecated(String message, Object... params) {} }; static List getDefaultNamedXContents() { diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/RuleScope.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/RuleScope.java index 8b6886d582524..513b681af1952 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/RuleScope.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/job/config/RuleScope.java @@ -66,6 +66,10 @@ public void usedDeprecatedName(String usedName, String modernName) {} @Override public void usedDeprecatedField(String usedName, String replacedWith) {} + + @Override + public void deprecated(String message, Object... params) {} + }; private final Map scope; diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/DeleteRoleMappingResponseTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/DeleteRoleMappingResponseTests.java index d89deb44e9f68..613aaeee7b90a 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/DeleteRoleMappingResponseTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/DeleteRoleMappingResponseTests.java @@ -43,6 +43,10 @@ public void usedDeprecatedName(String usedName, String modernName) { @Override public void usedDeprecatedField(String usedName, String replacedWith) { } + + @Override + public void deprecated(String message, Object... params) { + } }, json)); final DeleteRoleMappingResponse expectedResponse = new DeleteRoleMappingResponse(true); assertThat(response, equalTo(expectedResponse)); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/ExpressionRoleMappingTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/ExpressionRoleMappingTests.java index 29bc7812f5b7e..39f302ad55a4b 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/ExpressionRoleMappingTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/ExpressionRoleMappingTests.java @@ -58,6 +58,10 @@ public void usedDeprecatedName(String usedName, String modernName) { @Override public void usedDeprecatedField(String usedName, String replacedWith) { } + + @Override + public void deprecated(String message, Object... params) { + } }, json), "example-role-mapping"); final ExpressionRoleMapping expectedRoleMapping = new ExpressionRoleMapping("example-role-mapping", FieldRoleMapperExpression .ofKeyValues("realm.name", "kerb1"), Collections.singletonList("superuser"), null, true); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/GetPrivilegesResponseTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/GetPrivilegesResponseTests.java index 74211892a09e8..d0858102f8c43 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/GetPrivilegesResponseTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/GetPrivilegesResponseTests.java @@ -88,6 +88,10 @@ public void usedDeprecatedName(String usedName, String modernName) { @Override public void usedDeprecatedField(String usedName, String replacedWith) { } + + @Override + public void deprecated(String message, Object... params) { + } }, json)); final ApplicationPrivilege readTestappPrivilege = diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/GetRoleMappingsResponseTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/GetRoleMappingsResponseTests.java index b612c9ead28a5..df396f8e3f515 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/GetRoleMappingsResponseTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/GetRoleMappingsResponseTests.java @@ -71,6 +71,10 @@ public void usedDeprecatedName(String usedName, String modernName) { @Override public void usedDeprecatedField(String usedName, String replacedWith) { } + + @Override + public void deprecated(String message, Object... params) { + } }, json)); final List expectedRoleMappingsList = new ArrayList<>(); expectedRoleMappingsList.add(new ExpressionRoleMapping("kerberosmapping", FieldRoleMapperExpression.ofKeyValues("realm.name", diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/GetRolesResponseTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/GetRolesResponseTests.java index 41de52a8cef75..98bbf2a7c18c3 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/GetRolesResponseTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/GetRolesResponseTests.java @@ -71,6 +71,10 @@ public void usedDeprecatedName(String usedName, String modernName) { @Override public void usedDeprecatedField(String usedName, String replacedWith) { } + + @Override + public void deprecated(String message, Object... params) { + } }, json))); assertThat(response.getRoles().size(), equalTo(1)); final Role role = response.getRoles().get(0); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/support/expressiondsl/parser/RoleMapperExpressionParserTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/support/expressiondsl/parser/RoleMapperExpressionParserTests.java index 24ed5684fa856..b72fe5a75ed00 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/support/expressiondsl/parser/RoleMapperExpressionParserTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/support/expressiondsl/parser/RoleMapperExpressionParserTests.java @@ -123,7 +123,11 @@ public void usedDeprecatedName(String usedName, String modernName) { @Override public void usedDeprecatedField(String usedName, String replacedWith) { } - }, json)); + + @Override + public void deprecated(String message, Object... params) { + } + }, json)); } } diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/user/privileges/ApplicationPrivilegeTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/user/privileges/ApplicationPrivilegeTests.java index f958cadaa7e80..2c08b1238a317 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/user/privileges/ApplicationPrivilegeTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/user/privileges/ApplicationPrivilegeTests.java @@ -58,6 +58,10 @@ public void usedDeprecatedName(String usedName, String modernName) { @Override public void usedDeprecatedField(String usedName, String replacedWith) { } + + @Override + public void deprecated(String message, Object... params) { + } }, json)); final Map metadata = new HashMap<>(); metadata.put("description", "Read access to myapp"); diff --git a/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/DeprecationHandler.java b/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/DeprecationHandler.java index 1b0dcf4568086..970d8097eb73e 100644 --- a/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/DeprecationHandler.java +++ b/libs/x-content/src/main/java/org/elasticsearch/common/xcontent/DeprecationHandler.java @@ -19,6 +19,8 @@ package org.elasticsearch.common.xcontent; +import java.util.Arrays; + /** * Callback for notifying the creator of the {@link XContentParser} that * parsing hit a deprecated field. @@ -41,6 +43,12 @@ public void usedDeprecatedName(String usedName, String modernName) { throw new UnsupportedOperationException("deprecated fields not supported here but got [" + usedName + "] which has been replaced with [" + modernName + "]"); } + + @Override + public void deprecated(String message, Object... params) { + throw new UnsupportedOperationException( + "deprecations are not supported here but got [" + message + "] and " + Arrays.toString(params)); + } }; /** @@ -57,4 +65,7 @@ public void usedDeprecatedName(String usedName, String modernName) { * @param replacedWith the name of the field that replaced this field */ void usedDeprecatedField(String usedName, String replacedWith); + + void deprecated(String message, Object... params); + } diff --git a/server/src/main/java/org/elasticsearch/action/search/MultiSearchRequest.java b/server/src/main/java/org/elasticsearch/action/search/MultiSearchRequest.java index 5e4e7e45a2967..59d5f88ad7101 100644 --- a/server/src/main/java/org/elasticsearch/action/search/MultiSearchRequest.java +++ b/server/src/main/java/org/elasticsearch/action/search/MultiSearchRequest.java @@ -212,6 +212,10 @@ public static void readMultiLineFormat(BytesReference data, try (InputStream stream = data.slice(from, nextMarker - from).streamInput(); XContentParser parser = xContent.createParser(registry, LoggingDeprecationHandler.INSTANCE, stream)) { Map source = parser.map(); + Object expandWildcards = null; + Object ignoreUnavailable = null; + Object ignoreThrottled = null; + Object allowNoIndices = null; for (Map.Entry entry : source.entrySet()) { Object value = entry.getValue(); if ("index".equals(entry.getKey()) || "indices".equals(entry.getKey())) { @@ -231,12 +235,22 @@ public static void readMultiLineFormat(BytesReference data, searchRequest.routing(nodeStringValue(value, null)); } else if ("allow_partial_search_results".equals(entry.getKey())) { searchRequest.allowPartialSearchResults(nodeBooleanValue(value, null)); + } else if ("expand_wildcards".equals(entry.getKey()) || "expandWildcards".equals(entry.getKey())) { + expandWildcards = value; + } else if ("ignore_unavailable".equals(entry.getKey()) || "ignoreUnavailable".equals(entry.getKey())) { + ignoreUnavailable = value; + } else if ("allow_no_indices".equals(entry.getKey()) || "allowNoIndices".equals(entry.getKey())) { + allowNoIndices = value; + } else if ("ignore_throttled".equals(entry.getKey()) || "ignoreThrottled".equals(entry.getKey())) { + ignoreThrottled = value; } else { - // TODO we should not be lenient here and fail if there is any unknown key in the source map + parser.getDeprecationHandler().deprecated( + "key [{}] is not supported in the metadata section and will be rejected in 7.x", entry.getKey()); } } - defaultOptions = IndicesOptions.fromMap(source, defaultOptions); + defaultOptions = IndicesOptions + .fromParameters(expandWildcards, ignoreUnavailable, allowNoIndices, ignoreThrottled, defaultOptions); } } searchRequest.indicesOptions(defaultOptions); diff --git a/server/src/main/java/org/elasticsearch/common/xcontent/LoggingDeprecationHandler.java b/server/src/main/java/org/elasticsearch/common/xcontent/LoggingDeprecationHandler.java index 5b92dec573df8..b8c37870fe3c9 100644 --- a/server/src/main/java/org/elasticsearch/common/xcontent/LoggingDeprecationHandler.java +++ b/server/src/main/java/org/elasticsearch/common/xcontent/LoggingDeprecationHandler.java @@ -57,4 +57,10 @@ public void usedDeprecatedName(String usedName, String modernName) { public void usedDeprecatedField(String usedName, String replacedWith) { DEPRECATION_LOGGER.deprecated("Deprecated field [{}] used, replaced by [{}]", usedName, replacedWith); } + + @Override + public void deprecated(final String message, final Object... params) { + DEPRECATION_LOGGER.deprecated(message, params); + } + } diff --git a/server/src/test/java/org/elasticsearch/action/search/MultiSearchRequestTests.java b/server/src/test/java/org/elasticsearch/action/search/MultiSearchRequestTests.java index 2099a019a6c79..a15628dcc2d11 100644 --- a/server/src/test/java/org/elasticsearch/action/search/MultiSearchRequestTests.java +++ b/server/src/test/java/org/elasticsearch/action/search/MultiSearchRequestTests.java @@ -90,6 +90,15 @@ public void testSimpleAdd() throws Exception { assertThat(request.requests().get(7).types().length, equalTo(0)); } + public void testWarnWithUnknownKey() throws IOException { + final String requestContent = "{\"index\":\"test\", \"ignore_unavailable\" : true, \"unknown_key\" : \"open,closed\"}}\r\n" + + "{\"query\" : {\"match_all\" :{}}}\r\n"; + final FakeRestRequest restRequest = + new FakeRestRequest.Builder(xContentRegistry()).withContent(new BytesArray(requestContent), XContentType.JSON).build(); + RestMultiSearchAction.parseRequest(restRequest, true); + assertWarnings("key [unknown_key] is not supported in the metadata section and will be rejected in 7.x"); + } + public void testSimpleAddWithCarriageReturn() throws Exception { final String requestContent = "{\"index\":\"test\", \"ignore_unavailable\" : true, \"expand_wildcards\" : \"open,closed\"}}\r\n" + "{\"query\" : {\"match_all\" :{}}}\r\n"; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/utils/LoggingDeprecationAccumulationHandler.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/utils/LoggingDeprecationAccumulationHandler.java index 1ab443b12de01..f90f639ea1098 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/utils/LoggingDeprecationAccumulationHandler.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/utils/LoggingDeprecationAccumulationHandler.java @@ -41,6 +41,13 @@ public void usedDeprecatedField(String usedName, String replacedWith) { replacedWith)); } + @Override + public void deprecated(final String message, final Object... params) { + final String formattedMessage = LoggerMessageFormat.format(message, params); + LoggingDeprecationHandler.INSTANCE.deprecated(formattedMessage); + deprecations.add(formattedMessage); + } + /** * The collected deprecation warnings */ From 39f8a683aea240118a88481c4523a3b8acd0d477 Mon Sep 17 00:00:00 2001 From: Jim Ferenczi Date: Fri, 30 Nov 2018 12:53:04 +0100 Subject: [PATCH 047/115] [TEST] fix typo in get-watch documentation --- docs/java-rest/high-level/watcher/get-watch.asciidoc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/java-rest/high-level/watcher/get-watch.asciidoc b/docs/java-rest/high-level/watcher/get-watch.asciidoc index c4773d70ad731..7321a66eeaaf4 100644 --- a/docs/java-rest/high-level/watcher/get-watch.asciidoc +++ b/docs/java-rest/high-level/watcher/get-watch.asciidoc @@ -29,8 +29,8 @@ include-tagged::{doc-tests-file}[{api}-response] -------------------------------------------------- <1> `_id`, id of the watch <2> `found` is a boolean indicating whether the watch was found -<2> `_version` returns the version of the watch -<3> `status` contains status of the watch -<4> `source` the source of the watch +<3> `_version` returns the version of the watch +<4> `status` contains status of the watch +<5> `source` the source of the watch include::../execution.asciidoc[] \ No newline at end of file From a357bd3cc9adddc73c953b9e7da2562a990b3250 Mon Sep 17 00:00:00 2001 From: Alpar Torok Date: Fri, 30 Nov 2018 15:16:23 +0200 Subject: [PATCH 048/115] Conditional conffiles for packages (#36046) Relates to #35810 --- distribution/packages/build.gradle | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/distribution/packages/build.gradle b/distribution/packages/build.gradle index b479921e6ed8e..0443d0fb62a44 100644 --- a/distribution/packages/build.gradle +++ b/distribution/packages/build.gradle @@ -167,10 +167,12 @@ Closure commonPackageConfig(String type, boolean oss) { configurationFile '/etc/elasticsearch/elasticsearch.yml' configurationFile '/etc/elasticsearch/jvm.options' configurationFile '/etc/elasticsearch/log4j2.properties' - configurationFile '/etc/elasticsearch/role_mapping.yml' - configurationFile '/etc/elasticsearch/roles.yml' - configurationFile '/etc/elasticsearch/users' - configurationFile '/etc/elasticsearch/users_roles' + if (oss == false) { + configurationFile '/etc/elasticsearch/role_mapping.yml' + configurationFile '/etc/elasticsearch/roles.yml' + configurationFile '/etc/elasticsearch/users' + configurationFile '/etc/elasticsearch/users_roles' + } into('/etc/elasticsearch') { dirMode 0750 fileMode 0660 From 9f95eae1c603f5add16d7da4b0be57f02a4b773a Mon Sep 17 00:00:00 2001 From: patrykk21 <39259934+patrykk21@users.noreply.github.com> Date: Fri, 30 Nov 2018 14:30:23 +0100 Subject: [PATCH 049/115] [Docs] Clarify search_after behavior Closes #34232 --- docs/reference/search/request/search-after.asciidoc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/reference/search/request/search-after.asciidoc b/docs/reference/search/request/search-after.asciidoc index 4efa69b49b0e7..53d25c613b928 100644 --- a/docs/reference/search/request/search-after.asciidoc +++ b/docs/reference/search/request/search-after.asciidoc @@ -37,6 +37,10 @@ of the sort specification. Otherwise the sort order for documents that have the same sort values would be undefined and could lead to missing or duplicate results. The <> has a unique value per document but it is not recommended to use it as a tiebreaker directly. +Beware that `search_after` looks for the first document which fully or partially +matches tiebreaker's provided value. Therefore if a document has a tiebreaker value of +`"654323"` and you `search_after` for `"654"` it would still match that document +and return results found after it. <> are disabled on this field so sorting on it requires to load a lot of data in memory. Instead it is advised to duplicate (client side or with a <>) the content From 86ee5c2391ed54a839007fb6889549e85ab1a111 Mon Sep 17 00:00:00 2001 From: Jim Ferenczi Date: Fri, 30 Nov 2018 14:40:09 +0100 Subject: [PATCH 050/115] [TEST] fix link in get-watch documentation --- docs/java-rest/high-level/supported-apis.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/java-rest/high-level/supported-apis.asciidoc b/docs/java-rest/high-level/supported-apis.asciidoc index f9b89caaeeb2a..9bf8959ad1c91 100644 --- a/docs/java-rest/high-level/supported-apis.asciidoc +++ b/docs/java-rest/high-level/supported-apis.asciidoc @@ -421,7 +421,7 @@ The Java High Level REST Client supports the following Watcher APIs: * <<{upid}-start-watch-service>> * <<{upid}-stop-watch-service>> * <> -* <> +* <<{upid}-get-watch>> * <> * <> * <<{upid}-ack-watch>> From 91ce0f68f818bd6b4c0705be015daf63c46a4f10 Mon Sep 17 00:00:00 2001 From: Jim Ferenczi Date: Fri, 30 Nov 2018 16:05:59 +0100 Subject: [PATCH 051/115] [TEST] Fix random test failure in GetWatchResponseTests --- .../org/elasticsearch/client/watcher/WatchStatus.java | 3 ++- .../protocol/xpack/watcher/GetWatchResponseTests.java | 10 ++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/watcher/WatchStatus.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/watcher/WatchStatus.java index ab543abbf594a..b11673f9e552d 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/watcher/WatchStatus.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/watcher/WatchStatus.java @@ -26,6 +26,7 @@ import org.joda.time.DateTime; import java.io.IOException; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Objects; @@ -125,7 +126,7 @@ public static WatchStatus parse(XContentParser parser) throws IOException { DateTime lastChecked = null; DateTime lastMetCondition = null; Map actions = null; - Map headers = null; + Map headers = Collections.emptyMap(); long version = -1; ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.currentToken(), parser::getTokenLocation); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/watcher/GetWatchResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/watcher/GetWatchResponseTests.java index bcfed1c7b0be4..1c00b6fd9dc27 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/watcher/GetWatchResponseTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/watcher/GetWatchResponseTests.java @@ -140,12 +140,10 @@ private static WatchStatus randomWatchStatus() { ); actionMap.put(randomAlphaOfLength(10), actionStatus); } - Map headers = randomBoolean() ? new HashMap<>() : null; - if (headers != null) { - int headerSize = randomIntBetween(1, 5); - for (int i = 0; i < headerSize; i++) { - headers.put(randomAlphaOfLengthBetween(5, 10), randomAlphaOfLengthBetween(1, 10)); - } + Map headers = new HashMap<>(); + int headerSize = randomIntBetween(0, 5); + for (int i = 0; i < headerSize; i++) { + headers.put(randomAlphaOfLengthBetween(5, 10), randomAlphaOfLengthBetween(1, 10)); } return new WatchStatus(version, state, executionState, lastChecked, lastMetCondition, actionMap, headers); } From adecb184395d51b793480b72d24686a692cd82f9 Mon Sep 17 00:00:00 2001 From: Christophe Bismuth Date: Fri, 30 Nov 2018 16:10:13 +0100 Subject: [PATCH 052/115] Add `minimum_should_match` section to the query_string docs Closes #34142 --- .../query-dsl/query-string-query.asciidoc | 132 ++++++++++++++++-- 1 file changed, 124 insertions(+), 8 deletions(-) diff --git a/docs/reference/query-dsl/query-string-query.asciidoc b/docs/reference/query-dsl/query-string-query.asciidoc index e4b4fb50904b9..1636cdf974080 100644 --- a/docs/reference/query-dsl/query-string-query.asciidoc +++ b/docs/reference/query-dsl/query-string-query.asciidoc @@ -28,14 +28,14 @@ GET /_search "query": { "query_string" : { "default_field" : "content", - "query" : "(new york city) OR (big apple)" + "query" : "(new york city) OR (big apple)" <1> } } } -------------------------------------------------- // CONSOLE -... will be split into `new york city` and `big apple` and each part is then +<1> will be split into `new york city` and `big apple` and each part is then analyzed independently by the analyzer configured for the field. WARNING: Whitespaces are not considered operators, this means that `new york city` @@ -48,7 +48,6 @@ When multiple fields are provided it is also possible to modify how the differen field queries are combined inside each textual part using the `type` parameter. The possible modes are described <> and the default is `best_fields`. - The `query_string` top level parameters include: [cols="<,<",options="header",] @@ -113,8 +112,8 @@ not analyzed. By setting this value to `true`, a best effort will be made to analyze those as well. |`max_determinized_states` |Limit on how many automaton states regexp -queries are allowed to create. This protects against too-difficult -(e.g. exponentially hard) regexps. Defaults to 10000. +queries are allowed to create. This protects against too-difficult +(e.g. exponentially hard) regexps. Defaults to 10000. |`minimum_should_match` |A value controlling how many "should" clauses in the resulting boolean query should match. It can be an absolute value @@ -166,7 +165,7 @@ fields in the mapping could be expensive. ==== Multi Field The `query_string` query can also run against multiple fields. Fields can be -provided via the `"fields"` parameter (example below). +provided via the `fields` parameter (example below). The idea of running the `query_string` query against multiple fields is to expand each query term to an OR clause like this: @@ -206,7 +205,7 @@ GET /_search // CONSOLE Since several queries are generated from the individual search terms, -combining them is automatically done using a `dis_max` query with a tie_breaker. +combining them is automatically done using a `dis_max` query with a `tie_breaker`. For example (the `name` is boosted by 5 using `^5` notation): [source,js] @@ -301,7 +300,7 @@ GET /_search The `query_string` query supports multi-terms synonym expansion with the <> token filter. When this filter is used, the parser creates a phrase query for each multi-terms synonyms. -For example, the following synonym: `"ny, new york" would produce:` +For example, the following synonym: `ny, new york` would produce: `(ny OR ("new york"))` @@ -329,4 +328,121 @@ The example above creates a boolean query: that matches documents with the term `ny` or the conjunction `new AND york`. By default the parameter `auto_generate_synonyms_phrase_query` is set to `true`. +[float] +==== Minimum should match + +The `query_string` splits the query around each operator to create a boolean +query for the entire input. You can use `minimum_should_match` to control how +many "should" clauses in the resulting query should match. + +[source,js] +-------------------------------------------------- +GET /_search +{ + "query": { + "query_string": { + "fields": [ + "title" + ], + "query": "this that thus", + "minimum_should_match": 2 + } + } +} +-------------------------------------------------- +// CONSOLE + +The example above creates a boolean query: + +`(title:this title:that title:thus)~2` + +that matches documents with at least two of the terms `this`, `that` or `thus` +in the single field `title`. + +[float] +===== Multi Field + +[source,js] +-------------------------------------------------- +GET /_search +{ + "query": { + "query_string": { + "fields": [ + "title", + "content" + ], + "query": "this that thus", + "minimum_should_match": 2 + } + } +} +-------------------------------------------------- +// CONSOLE + +The example above creates a boolean query: + +`((content:this content:that content:thus) | (title:this title:that title:thus))` + +that matches documents with the disjunction max over the fields `title` and +`content`. Here the `minimum_should_match` parameter can't be applied. + +[source,js] +-------------------------------------------------- +GET /_search +{ + "query": { + "query_string": { + "fields": [ + "title", + "content" + ], + "query": "this OR that OR thus", + "minimum_should_match": 2 + } + } +} +-------------------------------------------------- +// CONSOLE + +Adding explicit operators forces each term to be considered as a separate clause. + +The example above creates a boolean query: + +`((content:this | title:this) (content:that | title:that) (content:thus | title:thus))~2` + +that matches documents with at least two of the three "should" clauses, each of +them made of the disjunction max over the fields for each term. + +[float] +===== Cross Field + +[source,js] +-------------------------------------------------- +GET /_search +{ + "query": { + "query_string": { + "fields": [ + "title", + "content" + ], + "query": "this OR that OR thus", + "type": "cross_fields", + "minimum_should_match": 2 + } + } +} +-------------------------------------------------- +// CONSOLE + +The `cross_fields` value in the `type` field indicates that fields that have the +same analyzer should be grouped together when the input is analyzed. + +The example above creates a boolean query: + +`(blended(terms:[field2:this, field1:this]) blended(terms:[field2:that, field1:that]) blended(terms:[field2:thus, field1:thus]))~2` + +that matches documents with at least two of the three per-term blended queries. + include::query-string-syntax.asciidoc[] From 1836da83f32934a5bd974a8d924b75bdcc0d98ed Mon Sep 17 00:00:00 2001 From: Tim Brooks Date: Fri, 30 Nov 2018 09:04:32 -0700 Subject: [PATCH 053/115] Remove `Lifecycle` from `ConnectionManager` (#36092) Prior to #35441 `ConnectionManager` had a `Lifecycle` object to support the ping runnable. After that commit, the connection amanger only needs the existing `AtomicBoolean` to indicate if it is running. --- .../transport/ConnectionManager.java | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/transport/ConnectionManager.java b/server/src/main/java/org/elasticsearch/transport/ConnectionManager.java index d5c576784fc00..0ee4a71f9afe9 100644 --- a/server/src/main/java/org/elasticsearch/transport/ConnectionManager.java +++ b/server/src/main/java/org/elasticsearch/transport/ConnectionManager.java @@ -24,7 +24,6 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.common.CheckedBiConsumer; -import org.elasticsearch.common.component.Lifecycle; import org.elasticsearch.common.lease.Releasable; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ConcurrentCollections; @@ -58,8 +57,7 @@ public class ConnectionManager implements Closeable { private final Transport transport; private final ThreadPool threadPool; private final ConnectionProfile defaultProfile; - private final Lifecycle lifecycle = new Lifecycle(); - private final AtomicBoolean closed = new AtomicBoolean(false); + private final AtomicBoolean isClosed = new AtomicBoolean(false); private final ReadWriteLock closeLock = new ReentrantReadWriteLock(); private final DelegatingNodeConnectionListener connectionListener = new DelegatingNodeConnectionListener(); @@ -71,7 +69,6 @@ public ConnectionManager(ConnectionProfile connectionProfile, Transport transpor this.transport = transport; this.threadPool = threadPool; this.defaultProfile = connectionProfile; - this.lifecycle.moveToStarted(); } public void addListener(TransportConnectionListener listener) { @@ -187,8 +184,7 @@ public int size() { @Override public void close() { - if (closed.compareAndSet(false, true)) { - lifecycle.moveToStopped(); + if (isClosed.compareAndSet(false, true)) { CountDownLatch latch = new CountDownLatch(1); // TODO: Consider moving all read/write lock (in Transport and this class) to the TransportService @@ -213,14 +209,10 @@ public void close() { }); try { - try { - latch.await(30, TimeUnit.SECONDS); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - // ignore - } - } finally { - lifecycle.moveToClosed(); + latch.await(30, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + // ignore } } } @@ -239,7 +231,7 @@ private Transport.Connection internalOpenConnection(DiscoveryNode node, Connecti } private void ensureOpen() { - if (lifecycle.started() == false) { + if (isClosed.get()) { throw new IllegalStateException("connection manager is closed"); } } From f26454d5013b74b4130a05c6d5fd8cd3e14e6d01 Mon Sep 17 00:00:00 2001 From: Jim Ferenczi Date: Fri, 30 Nov 2018 17:19:46 +0100 Subject: [PATCH 054/115] [TEST] fix typo in get-watch documentation (bis) --- .../client/documentation/WatcherDocumentationIT.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/WatcherDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/WatcherDocumentationIT.java index 03dfc2ea7e088..fc6dba9c8fc61 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/WatcherDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/WatcherDocumentationIT.java @@ -204,9 +204,9 @@ public void onFailure(Exception e) { GetWatchRequest request = new GetWatchRequest("my_watch_id"); //end::get-watch-request - //tag::ack-watch-execute + //tag::get-watch-execute GetWatchResponse response = client.watcher().getWatch(request, RequestOptions.DEFAULT); - //end::get-watch-request + //end::get-watch-execute //tag::get-watch-response String watchId = response.getId(); // <1> From 4b482f2d87f940e29a08b7ec31baa584f1f57d3e Mon Sep 17 00:00:00 2001 From: Jim Ferenczi Date: Fri, 30 Nov 2018 18:02:37 +0100 Subject: [PATCH 055/115] Add support for rest_total_hits_as_int in watcher (#36035) This change adds the support for rest_total_hits_as_int in the watcher search inputs. Setting this parameter in the request will transform the search response to contain the total hits as a number (instead of an object). Note that this parameter is currently a noop since #35849 is not merged. Closes #36008 --- .../input/search/ExecutableSearchInput.java | 12 +- .../search/WatcherSearchTemplateRequest.java | 41 +++++- .../test/mustache/30_search_input.yml | 126 +++++++++++++++++ .../rest-api-spec/test/painless/10_basic.yml | 128 ++++++++++++++++++ 4 files changed, 300 insertions(+), 7 deletions(-) diff --git a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/input/search/ExecutableSearchInput.java b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/input/search/ExecutableSearchInput.java index 5654ccca4771e..50b33d5247b54 100644 --- a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/input/search/ExecutableSearchInput.java +++ b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/input/search/ExecutableSearchInput.java @@ -29,6 +29,7 @@ import org.elasticsearch.xpack.watcher.support.search.WatcherSearchTemplateRequest; import org.elasticsearch.xpack.watcher.support.search.WatcherSearchTemplateService; +import java.util.Collections; import java.util.Map; import static org.elasticsearch.xpack.watcher.input.search.SearchInput.TYPE; @@ -37,11 +38,12 @@ * An input that executes search and returns the search response as the initial payload */ public class ExecutableSearchInput extends ExecutableInput { - public static final SearchType DEFAULT_SEARCH_TYPE = SearchType.QUERY_THEN_FETCH; private static final Logger logger = LogManager.getLogger(ExecutableSearchInput.class); + private static final Params EMPTY_PARAMS = new MapParams(Collections.emptyMap()); + private final Client client; private final WatcherSearchTemplateService searchTemplateService; private final TimeValue timeout; @@ -86,7 +88,13 @@ SearchInput.Result doExecute(WatchExecutionContext ctx, WatcherSearchTemplateReq final Payload payload; if (input.getExtractKeys() != null) { - BytesReference bytes = XContentHelper.toXContent(response, XContentType.JSON, false); + Params params; + if (request.isRestTotalHitsAsint()) { + params = new MapParams(Collections.singletonMap("rest_total_hits_a_int", "true")); + } else { + params = EMPTY_PARAMS; + } + BytesReference bytes = XContentHelper.toXContent(response, XContentType.JSON, params, false); // EMPTY is safe here because we never use namedObject try (XContentParser parser = XContentHelper .createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, bytes)) { diff --git a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/support/search/WatcherSearchTemplateRequest.java b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/support/search/WatcherSearchTemplateRequest.java index e69d6d8681f09..6741f870167b6 100644 --- a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/support/search/WatcherSearchTemplateRequest.java +++ b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/support/search/WatcherSearchTemplateRequest.java @@ -40,8 +40,8 @@ public class WatcherSearchTemplateRequest implements ToXContentObject { private final SearchType searchType; private final IndicesOptions indicesOptions; private final Script template; - private final BytesReference searchSource; + private boolean restTotalHitsAsInt; public WatcherSearchTemplateRequest(String[] indices, String[] types, SearchType searchType, IndicesOptions indicesOptions, BytesReference searchSource) { @@ -72,6 +72,7 @@ public WatcherSearchTemplateRequest(WatcherSearchTemplateRequest original, Bytes this.indicesOptions = original.indicesOptions; this.searchSource = source; this.template = original.template; + this.restTotalHitsAsInt = original.restTotalHitsAsInt; } private WatcherSearchTemplateRequest(String[] indices, String[] types, SearchType searchType, IndicesOptions indicesOptions, @@ -105,6 +106,19 @@ public IndicesOptions getIndicesOptions() { return indicesOptions; } + public boolean isRestTotalHitsAsint() { + return restTotalHitsAsInt; + } + + /** + * Indicates whether the total hits in the response should be + * serialized as number (true) or as an object (false). + * Defaults to false. + */ + public void setRestTotalHitsAsInt(boolean value) { + this.restTotalHitsAsInt = restTotalHitsAsInt; + } + public BytesReference getSearchSource() { return searchSource; } @@ -129,6 +143,9 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws if (types != null) { builder.array(TYPES_FIELD.getPreferredName(), types); } + if (restTotalHitsAsInt) { + builder.field(REST_TOTAL_HITS_AS_INT_FIELD.getPreferredName(), restTotalHitsAsInt); + } if (searchSource != null && searchSource.length() > 0) { try (InputStream stream = searchSource.streamInput()) { builder.rawField(BODY_FIELD.getPreferredName(), stream); @@ -167,6 +184,7 @@ public static WatcherSearchTemplateRequest fromXContent(XContentParser parser, S IndicesOptions indicesOptions = DEFAULT_INDICES_OPTIONS; BytesReference searchSource = null; Script template = null; + boolean totalHitsAsInt = false; XContentParser.Token token; String currentFieldName = null; @@ -263,10 +281,19 @@ public static WatcherSearchTemplateRequest fromXContent(XContentParser parser, S types.addAll(Arrays.asList(Strings.delimitedListToStringArray(typesStr, ",", " \t"))); } else if (SEARCH_TYPE_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { searchType = SearchType.fromString(parser.text().toLowerCase(Locale.ROOT)); + } else if (REST_TOTAL_HITS_AS_INT_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { + totalHitsAsInt = parser.booleanValue(); } else { throw new ElasticsearchParseException("could not read search request. unexpected string field [" + currentFieldName + "]"); } + } else if (token == XContentParser.Token.VALUE_BOOLEAN) { + if (REST_TOTAL_HITS_AS_INT_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { + totalHitsAsInt = parser.booleanValue(); + } else { + throw new ElasticsearchParseException("could not read search request. unexpected boolean field [" + + currentFieldName + "]"); + } } else { throw new ElasticsearchParseException("could not read search request. unexpected token [" + token + "]"); } @@ -276,8 +303,10 @@ public static WatcherSearchTemplateRequest fromXContent(XContentParser parser, S searchSource = BytesArray.EMPTY; } - return new WatcherSearchTemplateRequest(indices.toArray(new String[0]), types.toArray(new String[0]), searchType, - indicesOptions, searchSource, template); + WatcherSearchTemplateRequest request = new WatcherSearchTemplateRequest(indices.toArray(new String[0]), + types.toArray(new String[0]), searchType, indicesOptions, searchSource, template); + request.setRestTotalHitsAsInt(totalHitsAsInt); + return request; } @Override @@ -291,13 +320,14 @@ public boolean equals(Object o) { Objects.equals(searchType, other.searchType) && Objects.equals(indicesOptions, other.indicesOptions) && Objects.equals(searchSource, other.searchSource) && - Objects.equals(template, other.template); + Objects.equals(template, other.template) && + Objects.equals(restTotalHitsAsInt, other.restTotalHitsAsInt); } @Override public int hashCode() { - return Objects.hash(indices, types, searchType, indicesOptions, searchSource, template); + return Objects.hash(indices, types, searchType, indicesOptions, searchSource, template, restTotalHitsAsInt); } private static final ParseField INDICES_FIELD = new ParseField("indices"); @@ -309,6 +339,7 @@ public int hashCode() { private static final ParseField IGNORE_UNAVAILABLE_FIELD = new ParseField("ignore_unavailable"); private static final ParseField ALLOW_NO_INDICES_FIELD = new ParseField("allow_no_indices"); private static final ParseField TEMPLATE_FIELD = new ParseField("template"); + private static final ParseField REST_TOTAL_HITS_AS_INT_FIELD = new ParseField("rest_total_hits_as_int"); public static final IndicesOptions DEFAULT_INDICES_OPTIONS = IndicesOptions.lenientExpandOpen(); } diff --git a/x-pack/qa/smoke-test-watcher/src/test/resources/rest-api-spec/test/mustache/30_search_input.yml b/x-pack/qa/smoke-test-watcher/src/test/resources/rest-api-spec/test/mustache/30_search_input.yml index 28e2788fedbbc..633a548df78da 100644 --- a/x-pack/qa/smoke-test-watcher/src/test/resources/rest-api-spec/test/mustache/30_search_input.yml +++ b/x-pack/qa/smoke-test-watcher/src/test/resources/rest-api-spec/test/mustache/30_search_input.yml @@ -163,3 +163,129 @@ setup: - match: { "watch_record.result.input.search.request.body.query.bool.must.0.term.value": "val_2" } - match: { "watch_record.result.input.search.request.template.id": "search-template" } - match: { "watch_record.result.input.search.request.template.params.num": 2 } + +--- +"Test search input mustache integration (using request body and rest_total_hits_as_int)": + - skip: + version: " - 6.5.99" + reason: "rest_total_hits_as_int support was added in 6.6" + - do: + xpack.watcher.execute_watch: + body: > + { + "trigger_data" : { + "scheduled_time" : "2015-01-04T00:00:00" + }, + "watch" : { + "trigger" : { "schedule" : { "interval" : "10s" } }, + "actions" : { + "dummy" : { + "logging" : { + "text" : "executed!" + } + } + }, + "input" : { + "search" : { + "request" : { + "indices" : "idx", + "rest_total_hits_as_int": true, + "body" : { + "query" : { + "bool" : { + "filter" : [ + { + "range" : { + "date" : { + "lte" : "{{ctx.trigger.scheduled_time}}", + "gte" : "{{ctx.trigger.scheduled_time}}||-3d" + } + } + } + ] + } + } + } + } + } + } + } + } + - match: { "watch_record.result.input.type": "search" } + - match: { "watch_record.result.input.status": "success" } + - match: { "watch_record.result.input.payload.hits.total": 4 } + # makes sure that the mustache template snippets have been resolved correctly: + - match: { "watch_record.result.input.search.request.body.query.bool.filter.0.range.date.gte": "2015-01-04T00:00:00.000Z||-3d" } + - match: { "watch_record.result.input.search.request.body.query.bool.filter.0.range.date.lte": "2015-01-04T00:00:00.000Z" } + +--- +"Test search input mustache integration (using request template and rest_total_hits_as_int)": + - skip: + version: " - 6.5.99" + reason: "rest_total_hits_as_int support was added in 6.6" + + - do: + put_script: + id: "search-template" + body: { + "script": { + "lang": "mustache", + "source": { + "query" : { + "bool" : { + "must" : [ + { + "term" : { + "value" : "val_{{num}}" + } + } + ] + } + } + } + } + } + - match: { acknowledged: true } + + - do: + xpack.watcher.execute_watch: + body: > + { + "trigger_data" : { + "scheduled_time" : "2015-01-04T00:00:00" + }, + "watch" : { + "trigger" : { "schedule" : { "interval" : "10s" } }, + "actions" : { + "dummy" : { + "logging" : { + "text" : "executed!" + } + } + }, + "input" : { + "search" : { + "request" : { + "rest_total_hits_as_int": true, + "indices" : "idx", + "template" : { + "id": "search-template", + "params": { + "num": 2 + } + } + } + } + } + } + } + - match: { "watch_record.result.input.type": "search" } + - match: { "watch_record.result.input.status": "success" } + - match: { "watch_record.result.input.payload.hits.total": 1 } + - match: { "watch_record.result.input.payload.hits.hits.0._id": "2" } + # makes sure that the mustache template snippets have been resolved correctly: + - match: { "watch_record.result.input.search.request.body.query.bool.must.0.term.value": "val_2" } + - match: { "watch_record.result.input.search.request.template.id": "search-template" } + - match: { "watch_record.result.input.search.request.template.params.num": 2 } + + diff --git a/x-pack/qa/smoke-test-watcher/src/test/resources/rest-api-spec/test/painless/10_basic.yml b/x-pack/qa/smoke-test-watcher/src/test/resources/rest-api-spec/test/painless/10_basic.yml index 5996273435ec6..b600db9142bb4 100644 --- a/x-pack/qa/smoke-test-watcher/src/test/resources/rest-api-spec/test/painless/10_basic.yml +++ b/x-pack/qa/smoke-test-watcher/src/test/resources/rest-api-spec/test/painless/10_basic.yml @@ -119,3 +119,131 @@ - match: { "watch_record.result.actions.0.status" : "simulated" } - match: { "watch_record.result.actions.0.type" : "email" } - match: { "watch_record.result.actions.0.email.message.subject" : "404 recently encountered" } + +--- +"Test execute watch api with rest_total_hits_as_int": + - skip: + version: " - 6.5.99" + reason: "rest_total_hits_as_int support was added in 6.6" + + - do: + cluster.health: + wait_for_status: green + + - do: + xpack.watcher.put_watch: + id: "my_exe_watch" + body: > + { + "trigger" : { + "schedule" : { "cron" : "0 0 0 1 * ? 2099" } + }, + "input" : { + "chain" : { + "inputs" : [ + { + "first" : { + "search" : { + "request" : { + "indices" : [ "logstash*" ], + "rest_total_hits_as_int": true, + "body" : { + "query" : { + "bool": { + "must" : { + "match": { + "response": 404 + } + }, + "filter": { + "range": { + "@timestamp" : { + "from": "{{ctx.trigger.scheduled_time}}||-5m", + "to": "{{ctx.trigger.triggered_time}}" + } + } + } + } + } + } + } + } + } + }, + { + "second" : { + "transform" : { + "script" : { + "source": "return [ 'hits' : [ 'total' : ctx.payload.first.hits.total ]]" + } + } + } + } + ] + } + }, + "condition" : { + "script" : { + "source" : "ctx.payload.hits.total > 1", + "lang" : "painless" + } + }, + "actions" : { + "email_admin" : { + "transform" : { + "script" : { + "source" : "return ['foo': 'bar']", + "lang" : "painless" + } + }, + "email" : { + "to" : "someone@domain.host.com", + "subject" : "404 recently encountered" + } + } + } + } + - match: { _id: "my_exe_watch" } + + - do: + xpack.watcher.get_watch: + id: "my_exe_watch" + + - match: { _id: "my_exe_watch" } + - match: { watch.actions.email_admin.transform.script.source: "return ['foo': 'bar']" } + - match: { watch.input.chain.inputs.1.second.transform.script.source: "return [ 'hits' : [ 'total' : ctx.payload.first.hits.total ]]" } + + - do: + xpack.watcher.execute_watch: + id: "my_exe_watch" + body: > + { + "trigger_data" : { + "scheduled_time" : "2015-05-05T20:58:02.443Z", + "triggered_time" : "2015-05-05T20:58:02.443Z" + }, + "alternative_input" : { + "foo" : "bar" + }, + "ignore_condition" : true, + "action_modes" : { + "_all" : "force_simulate" + }, + "record_execution" : true + } + - match: { "watch_record.watch_id": "my_exe_watch" } + - match: { "watch_record.state": "executed" } + - match: { "watch_record.trigger_event.type": "manual" } + - match: { "watch_record.trigger_event.triggered_time": "2015-05-05T20:58:02.443Z" } + - match: { "watch_record.trigger_event.manual.schedule.scheduled_time": "2015-05-05T20:58:02.443Z" } + - match: { "watch_record.result.input.type": "simple" } + - match: { "watch_record.result.input.status": "success" } + - match: { "watch_record.result.input.payload.foo": "bar" } + - match: { "watch_record.result.condition.type": "always" } + - match: { "watch_record.result.condition.status": "success" } + - match: { "watch_record.result.condition.met": true } + - match: { "watch_record.result.actions.0.id" : "email_admin" } + - match: { "watch_record.result.actions.0.status" : "simulated" } + - match: { "watch_record.result.actions.0.type" : "email" } + - match: { "watch_record.result.actions.0.email.message.subject" : "404 recently encountered" } + From 4b1d24136264710c463e6075a26e9673be2b46fe Mon Sep 17 00:00:00 2001 From: Tim Brooks Date: Fri, 30 Nov 2018 11:04:09 -0700 Subject: [PATCH 056/115] Upgrade Netty 4.3.32.Final (#36102) (#36121) This commit upgrades netty. This will close #35360. Netty started throwing an IllegalArgumentException if a CompositeByteBuf is created with < 2 components. Netty4Utils was updated to reflect this change. --- buildSrc/version.properties | 2 +- modules/transport-netty4/build.gradle | 2 ++ .../licenses/netty-buffer-4.1.31.Final.jar.sha1 | 1 - .../licenses/netty-buffer-4.1.32.Final.jar.sha1 | 1 + .../licenses/netty-codec-4.1.31.Final.jar.sha1 | 1 - .../licenses/netty-codec-4.1.32.Final.jar.sha1 | 1 + .../licenses/netty-codec-http-4.1.31.Final.jar.sha1 | 1 - .../licenses/netty-codec-http-4.1.32.Final.jar.sha1 | 1 + .../licenses/netty-common-4.1.31.Final.jar.sha1 | 1 - .../licenses/netty-common-4.1.32.Final.jar.sha1 | 1 + .../licenses/netty-handler-4.1.31.Final.jar.sha1 | 1 - .../licenses/netty-handler-4.1.32.Final.jar.sha1 | 1 + .../licenses/netty-resolver-4.1.31.Final.jar.sha1 | 1 - .../licenses/netty-resolver-4.1.32.Final.jar.sha1 | 1 + .../licenses/netty-transport-4.1.31.Final.jar.sha1 | 1 - .../licenses/netty-transport-4.1.32.Final.jar.sha1 | 1 + .../elasticsearch/transport/netty4/Netty4Utils.java | 11 ++++++++--- .../licenses/netty-buffer-4.1.32.Final.jar.sha1 | 1 + .../licenses/netty-codec-4.1.32.Final.jar.sha1 | 1 + .../licenses/netty-codec-http-4.1.32.Final.jar.sha1 | 1 + .../licenses/netty-common-4.1.32.Final.jar.sha1 | 1 + .../licenses/netty-handler-4.1.32.Final.jar.sha1 | 1 + .../licenses/netty-resolver-4.1.32.Final.jar.sha1 | 1 + .../licenses/netty-transport-4.1.32.Final.jar.sha1 | 1 + .../security/transport/ssl/SslIntegrationTests.java | 2 -- 25 files changed, 25 insertions(+), 13 deletions(-) delete mode 100644 modules/transport-netty4/licenses/netty-buffer-4.1.31.Final.jar.sha1 create mode 100644 modules/transport-netty4/licenses/netty-buffer-4.1.32.Final.jar.sha1 delete mode 100644 modules/transport-netty4/licenses/netty-codec-4.1.31.Final.jar.sha1 create mode 100644 modules/transport-netty4/licenses/netty-codec-4.1.32.Final.jar.sha1 delete mode 100644 modules/transport-netty4/licenses/netty-codec-http-4.1.31.Final.jar.sha1 create mode 100644 modules/transport-netty4/licenses/netty-codec-http-4.1.32.Final.jar.sha1 delete mode 100644 modules/transport-netty4/licenses/netty-common-4.1.31.Final.jar.sha1 create mode 100644 modules/transport-netty4/licenses/netty-common-4.1.32.Final.jar.sha1 delete mode 100644 modules/transport-netty4/licenses/netty-handler-4.1.31.Final.jar.sha1 create mode 100644 modules/transport-netty4/licenses/netty-handler-4.1.32.Final.jar.sha1 delete mode 100644 modules/transport-netty4/licenses/netty-resolver-4.1.31.Final.jar.sha1 create mode 100644 modules/transport-netty4/licenses/netty-resolver-4.1.32.Final.jar.sha1 delete mode 100644 modules/transport-netty4/licenses/netty-transport-4.1.31.Final.jar.sha1 create mode 100644 modules/transport-netty4/licenses/netty-transport-4.1.32.Final.jar.sha1 create mode 100644 plugins/transport-nio/licenses/netty-buffer-4.1.32.Final.jar.sha1 create mode 100644 plugins/transport-nio/licenses/netty-codec-4.1.32.Final.jar.sha1 create mode 100644 plugins/transport-nio/licenses/netty-codec-http-4.1.32.Final.jar.sha1 create mode 100644 plugins/transport-nio/licenses/netty-common-4.1.32.Final.jar.sha1 create mode 100644 plugins/transport-nio/licenses/netty-handler-4.1.32.Final.jar.sha1 create mode 100644 plugins/transport-nio/licenses/netty-resolver-4.1.32.Final.jar.sha1 create mode 100644 plugins/transport-nio/licenses/netty-transport-4.1.32.Final.jar.sha1 diff --git a/buildSrc/version.properties b/buildSrc/version.properties index 94f2470666c65..aefed2d6b3ad2 100644 --- a/buildSrc/version.properties +++ b/buildSrc/version.properties @@ -15,7 +15,7 @@ slf4j = 1.6.2 # when updating the JNA version, also update the version in buildSrc/build.gradle jna = 4.5.1 -netty = 4.1.31.Final +netty = 4.1.32.Final joda = 2.10.1 # test dependencies diff --git a/modules/transport-netty4/build.gradle b/modules/transport-netty4/build.gradle index afa82e015860c..d84c308294e23 100644 --- a/modules/transport-netty4/build.gradle +++ b/modules/transport-netty4/build.gradle @@ -109,6 +109,8 @@ thirdPartyAudit.excludes = [ 'org.jboss.marshalling.Unmarshaller', // from io.netty.util.internal.logging.InternalLoggerFactory (netty) - it's optional + 'org.slf4j.helpers.FormattingTuple', + 'org.slf4j.helpers.MessageFormatter', 'org.slf4j.Logger', 'org.slf4j.LoggerFactory', 'org.slf4j.spi.LocationAwareLogger', diff --git a/modules/transport-netty4/licenses/netty-buffer-4.1.31.Final.jar.sha1 b/modules/transport-netty4/licenses/netty-buffer-4.1.31.Final.jar.sha1 deleted file mode 100644 index 22b58c5241485..0000000000000 --- a/modules/transport-netty4/licenses/netty-buffer-4.1.31.Final.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -e086523d6bb01fcab1d8dd370eecfcd606311b92 \ No newline at end of file diff --git a/modules/transport-netty4/licenses/netty-buffer-4.1.32.Final.jar.sha1 b/modules/transport-netty4/licenses/netty-buffer-4.1.32.Final.jar.sha1 new file mode 100644 index 0000000000000..111093792d347 --- /dev/null +++ b/modules/transport-netty4/licenses/netty-buffer-4.1.32.Final.jar.sha1 @@ -0,0 +1 @@ +046ede57693788181b2cafddc3a5967ed2f621c8 \ No newline at end of file diff --git a/modules/transport-netty4/licenses/netty-codec-4.1.31.Final.jar.sha1 b/modules/transport-netty4/licenses/netty-codec-4.1.31.Final.jar.sha1 deleted file mode 100644 index 83e6eab1261c2..0000000000000 --- a/modules/transport-netty4/licenses/netty-codec-4.1.31.Final.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -cfa60b136f5ea57787e910eee37e240bb45402a7 \ No newline at end of file diff --git a/modules/transport-netty4/licenses/netty-codec-4.1.32.Final.jar.sha1 b/modules/transport-netty4/licenses/netty-codec-4.1.32.Final.jar.sha1 new file mode 100644 index 0000000000000..5830dd05a5027 --- /dev/null +++ b/modules/transport-netty4/licenses/netty-codec-4.1.32.Final.jar.sha1 @@ -0,0 +1 @@ +8f32bd79c5a16f014a4372ed979dc62b39ede33a \ No newline at end of file diff --git a/modules/transport-netty4/licenses/netty-codec-http-4.1.31.Final.jar.sha1 b/modules/transport-netty4/licenses/netty-codec-http-4.1.31.Final.jar.sha1 deleted file mode 100644 index b6d43d380ac17..0000000000000 --- a/modules/transport-netty4/licenses/netty-codec-http-4.1.31.Final.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -bf6321b3f10ea3aefc1970b30bb8928e833f236c \ No newline at end of file diff --git a/modules/transport-netty4/licenses/netty-codec-http-4.1.32.Final.jar.sha1 b/modules/transport-netty4/licenses/netty-codec-http-4.1.32.Final.jar.sha1 new file mode 100644 index 0000000000000..6ff945b6c2de4 --- /dev/null +++ b/modules/transport-netty4/licenses/netty-codec-http-4.1.32.Final.jar.sha1 @@ -0,0 +1 @@ +0b9218adba7353ad5a75fcb639e4755d64bd6ddf \ No newline at end of file diff --git a/modules/transport-netty4/licenses/netty-common-4.1.31.Final.jar.sha1 b/modules/transport-netty4/licenses/netty-common-4.1.31.Final.jar.sha1 deleted file mode 100644 index 1c0c67721e22d..0000000000000 --- a/modules/transport-netty4/licenses/netty-common-4.1.31.Final.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -39ddfa47808c8393a343513571e404fef02f45f0 \ No newline at end of file diff --git a/modules/transport-netty4/licenses/netty-common-4.1.32.Final.jar.sha1 b/modules/transport-netty4/licenses/netty-common-4.1.32.Final.jar.sha1 new file mode 100644 index 0000000000000..02dd7ce15b843 --- /dev/null +++ b/modules/transport-netty4/licenses/netty-common-4.1.32.Final.jar.sha1 @@ -0,0 +1 @@ +e95de4f762606f492328e180c8ad5438565a5e3b \ No newline at end of file diff --git a/modules/transport-netty4/licenses/netty-handler-4.1.31.Final.jar.sha1 b/modules/transport-netty4/licenses/netty-handler-4.1.31.Final.jar.sha1 deleted file mode 100644 index c344af3b70a7e..0000000000000 --- a/modules/transport-netty4/licenses/netty-handler-4.1.31.Final.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -7703c0696f2f34ec7c223c6a5750366a5f4dfb6f \ No newline at end of file diff --git a/modules/transport-netty4/licenses/netty-handler-4.1.32.Final.jar.sha1 b/modules/transport-netty4/licenses/netty-handler-4.1.32.Final.jar.sha1 new file mode 100644 index 0000000000000..06af1850f8cca --- /dev/null +++ b/modules/transport-netty4/licenses/netty-handler-4.1.32.Final.jar.sha1 @@ -0,0 +1 @@ +b4e3fa13f219df14a9455cc2111f133374428be0 \ No newline at end of file diff --git a/modules/transport-netty4/licenses/netty-resolver-4.1.31.Final.jar.sha1 b/modules/transport-netty4/licenses/netty-resolver-4.1.31.Final.jar.sha1 deleted file mode 100644 index f6d72804412c2..0000000000000 --- a/modules/transport-netty4/licenses/netty-resolver-4.1.31.Final.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -8ea7a47400beedd5bb901b96a0730eea8b7b6f2a \ No newline at end of file diff --git a/modules/transport-netty4/licenses/netty-resolver-4.1.32.Final.jar.sha1 b/modules/transport-netty4/licenses/netty-resolver-4.1.32.Final.jar.sha1 new file mode 100644 index 0000000000000..58d0dfb949c4c --- /dev/null +++ b/modules/transport-netty4/licenses/netty-resolver-4.1.32.Final.jar.sha1 @@ -0,0 +1 @@ +3e0114715cb125a12db8d982b2208e552a91256d \ No newline at end of file diff --git a/modules/transport-netty4/licenses/netty-transport-4.1.31.Final.jar.sha1 b/modules/transport-netty4/licenses/netty-transport-4.1.31.Final.jar.sha1 deleted file mode 100644 index e44515c5e8f65..0000000000000 --- a/modules/transport-netty4/licenses/netty-transport-4.1.31.Final.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -e3396bd65e9c76accac11c29dca035da1cc39cb1 \ No newline at end of file diff --git a/modules/transport-netty4/licenses/netty-transport-4.1.32.Final.jar.sha1 b/modules/transport-netty4/licenses/netty-transport-4.1.32.Final.jar.sha1 new file mode 100644 index 0000000000000..b248610a88623 --- /dev/null +++ b/modules/transport-netty4/licenses/netty-transport-4.1.32.Final.jar.sha1 @@ -0,0 +1 @@ +d5e5a8ff9c2bc7d91ddccc536a5aca1a4355bd8b \ No newline at end of file diff --git a/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/Netty4Utils.java b/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/Netty4Utils.java index 76d7864c71692..740217dcdf4d8 100644 --- a/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/Netty4Utils.java +++ b/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/Netty4Utils.java @@ -110,9 +110,14 @@ public static ByteBuf toByteBuf(final BytesReference reference) { while ((slice = iterator.next()) != null) { buffers.add(Unpooled.wrappedBuffer(slice.bytes, slice.offset, slice.length)); } - final CompositeByteBuf composite = Unpooled.compositeBuffer(buffers.size()); - composite.addComponents(true, buffers); - return composite; + + if (buffers.size() == 1) { + return buffers.get(0); + } else { + CompositeByteBuf composite = Unpooled.compositeBuffer(buffers.size()); + composite.addComponents(true, buffers); + return composite; + } } catch (IOException ex) { throw new AssertionError("no IO happens here", ex); } diff --git a/plugins/transport-nio/licenses/netty-buffer-4.1.32.Final.jar.sha1 b/plugins/transport-nio/licenses/netty-buffer-4.1.32.Final.jar.sha1 new file mode 100644 index 0000000000000..111093792d347 --- /dev/null +++ b/plugins/transport-nio/licenses/netty-buffer-4.1.32.Final.jar.sha1 @@ -0,0 +1 @@ +046ede57693788181b2cafddc3a5967ed2f621c8 \ No newline at end of file diff --git a/plugins/transport-nio/licenses/netty-codec-4.1.32.Final.jar.sha1 b/plugins/transport-nio/licenses/netty-codec-4.1.32.Final.jar.sha1 new file mode 100644 index 0000000000000..5830dd05a5027 --- /dev/null +++ b/plugins/transport-nio/licenses/netty-codec-4.1.32.Final.jar.sha1 @@ -0,0 +1 @@ +8f32bd79c5a16f014a4372ed979dc62b39ede33a \ No newline at end of file diff --git a/plugins/transport-nio/licenses/netty-codec-http-4.1.32.Final.jar.sha1 b/plugins/transport-nio/licenses/netty-codec-http-4.1.32.Final.jar.sha1 new file mode 100644 index 0000000000000..6ff945b6c2de4 --- /dev/null +++ b/plugins/transport-nio/licenses/netty-codec-http-4.1.32.Final.jar.sha1 @@ -0,0 +1 @@ +0b9218adba7353ad5a75fcb639e4755d64bd6ddf \ No newline at end of file diff --git a/plugins/transport-nio/licenses/netty-common-4.1.32.Final.jar.sha1 b/plugins/transport-nio/licenses/netty-common-4.1.32.Final.jar.sha1 new file mode 100644 index 0000000000000..02dd7ce15b843 --- /dev/null +++ b/plugins/transport-nio/licenses/netty-common-4.1.32.Final.jar.sha1 @@ -0,0 +1 @@ +e95de4f762606f492328e180c8ad5438565a5e3b \ No newline at end of file diff --git a/plugins/transport-nio/licenses/netty-handler-4.1.32.Final.jar.sha1 b/plugins/transport-nio/licenses/netty-handler-4.1.32.Final.jar.sha1 new file mode 100644 index 0000000000000..06af1850f8cca --- /dev/null +++ b/plugins/transport-nio/licenses/netty-handler-4.1.32.Final.jar.sha1 @@ -0,0 +1 @@ +b4e3fa13f219df14a9455cc2111f133374428be0 \ No newline at end of file diff --git a/plugins/transport-nio/licenses/netty-resolver-4.1.32.Final.jar.sha1 b/plugins/transport-nio/licenses/netty-resolver-4.1.32.Final.jar.sha1 new file mode 100644 index 0000000000000..58d0dfb949c4c --- /dev/null +++ b/plugins/transport-nio/licenses/netty-resolver-4.1.32.Final.jar.sha1 @@ -0,0 +1 @@ +3e0114715cb125a12db8d982b2208e552a91256d \ No newline at end of file diff --git a/plugins/transport-nio/licenses/netty-transport-4.1.32.Final.jar.sha1 b/plugins/transport-nio/licenses/netty-transport-4.1.32.Final.jar.sha1 new file mode 100644 index 0000000000000..b248610a88623 --- /dev/null +++ b/plugins/transport-nio/licenses/netty-transport-4.1.32.Final.jar.sha1 @@ -0,0 +1 @@ +d5e5a8ff9c2bc7d91ddccc536a5aca1a4355bd8b \ No newline at end of file diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/ssl/SslIntegrationTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/ssl/SslIntegrationTests.java index 39cecc664a4a0..21ff46e34fd1f 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/ssl/SslIntegrationTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/ssl/SslIntegrationTests.java @@ -92,8 +92,6 @@ public void testThatUnconfiguredCiphersAreRejected() throws Exception { } } - // no SSL exception as this is the exception is returned when connecting - @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/35360") public void testThatTransportClientUsingSSLv3ProtocolIsRejected() { assumeFalse("Can't run in a FIPS JVM as SSLv3 SSLContext not available", inFipsJvm()); try (TransportClient transportClient = new TestXPackTransportClient(Settings.builder() From 6eedfeb82632d5154751b172d1e59958ae71cf7c Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Fri, 30 Nov 2018 08:55:29 -0800 Subject: [PATCH 057/115] [DOCS] Refreshes population job examples (#36101) --- .../ml/images/ml-population-anomaly.jpg | Bin 180164 -> 165614 bytes .../reference/ml/images/ml-population-job.jpg | Bin 97772 -> 331718 bytes .../ml/images/ml-population-results.jpg | Bin 210263 -> 283347 bytes docs/reference/ml/populations.asciidoc | 25 +++++++++--------- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/docs/reference/ml/images/ml-population-anomaly.jpg b/docs/reference/ml/images/ml-population-anomaly.jpg index 9fa726d050c7483f72bed36c75b6a538f413453f..64a2d7a52b6222b2821db47993d6f77ddd23e7ae 100644 GIT binary patch literal 165614 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 z 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%0Dke$Gmu<3k(1%{{x|!L~-c^}W}=M^1T+ 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ν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

6diOygwGhaYfKc#WA9xS;15pOh1Q)tepiQY zS1KtZM-;bz$|-Kod;AP~DAktS4oyU3ZvDzp*ErVX3iI@?JN52jkw#OWcfSkCaA1OL zl|c=lJkcnza9*pKq!oo1P;ACa>|G!)VZEWj5X+OnG59%b;>q~|}A2AnBW zYrM=AZ4>U6{6?3H6v@fl!%Jh~jC^30n@8oh_G}6STd7Ri6+R{ixI+Opy}?WZHC%nO zl%0X|ja`Tgy$F;DS~D%=ga3rV-tH{92?Y524Pqt^oDZ}4CD%#aRX(R@!sXpD#tr2h z{$#bL0V(UrHKVnU-wM47%O6^YIu>1ce=@0{{HCc6ZaT{Qg~^bt-um!OJCeSO)gRe~ zrRiOICpqLVT0%{c*0jU`OXN_YIW}@{>EJ*jJP=^_wLR-Rj*brHVs?XN-jlH-O5o|{ za;MdHXcS~baRt)&C@T`!=$9D!o|S;|33hnA_uRW0%80=kto5gFDWS>ryJHs93oljL zTteHZguRn|t3H!Ma)mj!Li?Y=SdK~!X_S15YsjrObW(I71xzmYe|@jI;;w2uoo?*x zL$$e89{<*;h_vq=<)MdDpwJHg7Rt8zQeF+_pAnZkK$Ex}M=#b3TqHt*LF{i=(QvG|< zge%BZEokNkgN^9x%{-H^XlloJGt~9NES@6Tl_0MoVp*2;liOAf|I>;C0sNICO?lw zFdU9fu8GdpuC!aFZpi<*JiR4fT+I=OUU=+;lNUPVkMN&eG0wZravDW07= z)jo|jm%=CW3hG@_mM5#-BO?sFAQ|q$`8mhv!s6%raqn=KA|s`uB@aqKmw|aHRCKx* z8BOV|#u^pYd6M-BX_jLym*>04q8%WDwDh;sPV*tewy^9_q3!Rj@EuCG;}WbG9S;_A zF&sLpS+ir7lHK1<_yK$v4E)||6+MTvENhrQ^2)s6u`znG+y#=Mo;;xljS3T=j|;r7 zxI=p;xNKJ`ZYk-$YKJi^a>02%muwr#mmb zjhRJQ7odK`;GL=6OPgT5PI5p^gY$@DdheBpR?IvHP$V5twKg_qs$Q)Aq8kqa>uQ)Jjs_D_6n#o9aOw?3OFQtWQ`t|%xpZ3|L0GXAJ`WjPz_yY5*4 zCPQR|Z63Ytx3*~e&s=)3Z0M1D-Wktox;6s$Qhc`h zFNsNUo>7#C)>Ty0saAaF*Rv65bi|%J0%i-7|LkJmY>(2ji#>ZTJUF{2 z{NwIOd`KTRU{rhpFC(Y+^I2`%N>moAGfdmpyfmR@dNwDu)u{!+p;yd;=h z(GUNX`qrJuJbZ+wn<0p)XFyg4%>pBbDdYOv!4NGrDGJ})y%QS8C8Rkhi67zBKL<;L zZP5#VNp!$phFsbMk;Ypq**^v{Xy#@QwB=BkxKdlir z{0v!@1!gP-e{vTla|P!a=${R~?Fb8cdcWCkPL z0wB{u6QlGPZBn1c-`P2D$X88!s63>bpZa}75iF;34)e8(I0^Qe?$aB8Wcx8qsImJF zzCM3eTn0O80-L+?=Te(z+Jo@IhIhGEByp}xaO>1(_J>jLBXtusT{AYT+=ypxcb2Cw zuX+=^Ov7K2HE5>uG>XwJZ^|iQKqrOe9!Nxv9{e`~8Kw_T1I8Sg)%^>a>rgB7fCC&% z;3Dr^$e(qd`eW42Dg4{{h{iDWvG91cuA2E-*U0+M_=97yyL39oEmqZp$f@tMso##S zU%3%EGf8r%)Glhadmj!ce9kk9JrqNhJzszR^}9=pHz0Yc-leL@=3z>6YCz%CdD+(k zG;0rv7SE^f$@Qhl?9EVZ>>0GPg~qRBk6+&JeS*jEokYsE0vNv}#o+)bgAzZ+YYi`q zq-<$7K^YZTjC+2-XiS$eLvnbE(28@eLCt>F=q|Q%y~1_7UCZorlF3qd)h?mltqTu$ z-PtusT_pO`(O#@ck9xB!X}w?HA1{Jae&%_ex@=o%(5{fDZ4o|DWQ8nHtUonibmGAi zM4a{o>$96v`~&>LA`~X`7B*}76@JG1*^*au9eG6GUB08&uDgsP8GyekrbqJ15q{t{ zpcjg@Edru@h)s*R&CZ?2uiGJ><5a%3o=h>?bG92XF_}Wd755oRL@9=l@ zT0w;D&js(_t}e%hU-E;NsD#OHK6&6=+ox5dYcEDG0br{KB8)PcbG80b4$t6Jy`zh> zi-n7OQg4G@GcT#qS1vW>inhzC@UMSTPoL!!X$;eRo^Uy*+Rk{~icV=e4f)=;#My&y z{-TF&gF0CDh@H=I;SRb;&fyT;w35g;{j<-wVZ6PwS7`l-RGmSb?3s^0(CD;LDn&U%s9>wx!RB>t&}SwoBkxFAlO7xdViZ zlZDGu@CAW;dSMO30oq%g&6Y z_{5S&JTH%bO7Y1uI!4~r3t?_p$H?wau0f+5y1UL89rMV#aCqNf>5%{XSiRX_3YY(x zZm;=d79U-2?GpWjYUtcD8(+S9@B zDVkiM^N-e;AMh-MoW)-b=0kk#Z_uby748tY&L!Np#Op#7BQws8@=DQ}DLV_!3S8Nl ziVYVq2COLL(+Q-@bMyVsa6vA-zqlnJWNJf@yW@rFJ*|Is(g@5<;K0^&vYpk!2+M21 zEZgopUxT?O54>%rt*(T5Q-DRrL!M zbLvM0@;OipnDw!hXzR()PB;uuQ4swk?D}iIS?`=CS0>d0krBUrFK?v8ABe1=ATpXK zlj9RBpMM9>X->TVy7}~${CWm-a?%G{8ROu?{?-B3=!F+h&|QODeRxUaI~KYn_oyQ` z^6A?{rVq|4H8%}u$WO&Ay?@sBGi`ax%r_L%v39cFQ>A*?s`jl3yJ}@~aFPuSW_~x@ z_c@-BX=hc}(-5{*BWF*V>$tubizT^h%RM?^?yEN5FueWUm`mkkmS;|-{(fT%@m)gD z>Mzv>?~Od-kQmT*<}cLx%P!CVc8C8kEDrSK#H@xe&)^5R*)oqj^P;*wE-S zjEhRP+h0e^YbC^zK5y1v@`u`6%HaFv9r@H0ch{1Au@efGJWhF;ooqG2~V{OH>ZIIA0kTRIoZ1^oeBbn!~6r! z)&1%G@LD8PJZ`Su78-%Ay{!&50DaXMQ0@!f-2?^5EOd;hhtbtLx7A~BjP!3YTuPl% z*JwkCy{@%^V6soxji8fGrJu~!fA>r*kiBCe6;Dj>))682QBB-`&NRM*4tx2gbf`;l zG1-~_XC`EQc4As!c7(THd>b8Ei)}(qP@_ymwsj*Il_o|Ar9U+K$Z=<*J`cY-nNn*s+?ilTLH~0{dE-}H%`KG^7vwZcJy07G9F$48FpW$QqO?uCC zDZ@YT{ITP9l!N_JVc=Ook+>Xxa0U`!0sTQ@a>dCPV1C1jhB{+qL=N{8aHA`dmmAGs zzMNlSnWfZDSWpW2YSgM3H~IO-TD0EsraP_JIQz9u>ez=LO0ozL1n=RSEj1E1f4^i5 zE-f&RqhysdBgr>AE zem1wYpPov9;ZE|+2Jq5diNQRMT0Qkc_`Knyozud)BADEZ*96{^wUc_^ZT1B)AB{%X zkwD6yZ4c5Rq8_LeE!yYgp1Lo+*57NOCVjh zVR~zPGncp%JHmi$pKC;JzFhe6a2xmPeK9cyk&(xwM_zR&>#kxNFaH&%_Vvnxdm(0; zH92HiuZKBBr6L@26S+lW9njZ!;F;Y{NPqYZ1HKm~=i42cFc=lW3rA^5}f!p4%cfAP}UyY1exo9TdJr z&+c?+9}4f*nusa8j^41H3m6|~BQx%nV0Ql+COstChi4OCQu{}e%!H0uCqW+QrrtB_ zTuEbghH&#W!W`tA0NTdX)Ldg)8PAxY!bbbz_fEy04Y406B4P8itS(MI>!i%uq9bcF zKT1!CkU}@fdu>@jCLK!48|1C`1Aw^N5*UNjr2T>OtE@1?3)gtIQW7?{BHR;3MKEc5ptM^S*@Bxsr5E@1Y*JS)rm zir?$-U6Rg>b&o1Q*&u&QDMM5c_tt4si6#j3wg`Jo?2c3CuwH#}!Mpj6+1#>7nLdFf zv!rBRi)m`^I{Tq{?w9uy7hklsk3XL`p2cEJYD!i7tB-Db&l24NF?ab>WcbBpJS) zBH@Mk3~h(WV%VLn^hei90$OuqLtc5WjUJ)K?O`raU)I$Noz#a2<@8dP|J{|(4}Th( z^@_DGE$ISLe_?5%e@|-J`HR$3*t_+65R%`= zED514vByK0<~_|p8th)83`89Xr0zv)+ly;OA^gAv$pe79av5O0toj_c5?Te{@64S= z?q-K;G>>5u2HVrhp$(KgzTS9s^3bG@vA&dW`n33|!*(o%5e(xK)9~gZ13oOp;Se@& zx@Z0ezGEgI)*C=h7e#PaK0SW+kHzZhkS`GlO&dm~(ncOsqP^zA3;t40rRnMK-lk0x zp2htD%vDh8u-QC9uZc8rv@@t4D;-E@N)CgzF0aN!an)$gl~Q`rMqr2d#=JbDzce^= zJ8Phlv(lRG+ccq5u*H3E>y4h2)mOnA;>QFGJq6+|L9{nlK}Fj8 zbM%H-i8b8-J-z17KMW-mit2xlP-{cDS!j;a}t}N!Fdx7z^l&}n%vTKfmdaM*JV=sRGAS!uY zbs}~h<`1@>rSsMM=sUxh@zbfXT8@JkCPc0^mzOn$J8deG6C0M}qsl@kGj>dM-%Y`t zUoTtq|8DR``vI4r66T;VN`on3A9}Wqe~&yNZ9nx1;*XA>syzK|B-*wpi#4x$mt0hE zYV7g7hMW*Z|& zhO_nPxQHSDWuNRLweyKJ&f3}+Ze$kw=@y^RXdqdSmmFnw!JR&Tr_XKfF2CTNZ>}}% zJ>@VQB#B>Zte^J@A?jfh1l?dlG#@TV1FZ5V+(ASY4q|gNh}$3m*LbFaZvCT zf}-e1h2ccx_g!56^{s)_yEpxovPs||a+GEte9JT@Fi`0B^~Y^IIL%k23KRH?A46~! zEnh*^m?)M5&+k@4X?$ky5gAvDG{Ib8Gw5MIThXG?ecBzlhXtk~70uGkr^h($HY1tO zsnkJ%LNdmZ5*Ia^(K)&Y1CaoB-v&kmIn#JcFm7O8fhdTkgUXRYJE95-=K?+e1}_0E zqy~t3r4E6p?|mo0up_+{ho|g}pI!dgqgq~+!@@VlVyJV=#fDW~GbJHXi6hTMXwa53 zM9AGpBur`HcP1HEkhv@Jb#NG&f9gjKII<41)5%)@@aF&V|?f4AE2eK7e8v&1Mieh zuc`u8&Y0{L!6tEc8i{}HO)&y^b#v|0SCc5wuBg}#gITLlQ8F{DYvzX&_iQ3Q^KE}R zb4+H0-I6a-2^HYP+2*oG_^8ZID;_5EHq=f^yRZLdeGhiC$A_&3fdMOd&>mNc$r?%C z2)ZhN9ZUF$7?6CayF)1JJ5g0|!RiO^yZ^p2Y0}?CX zCS1`1rHGAYXzvDZLb|TWW*$wRFtF8{vHt8}%eZOd(Y$iZ5v7vArAnnprycj~Apnzh zw+wcCkx?8B1K$Eo{k|1uKoL8?Qyn9nf^uiG;hI>P0S7fcJ=x-XN8-u;ZpG>ER7Eht zBOlrR`ek>)V9?mwx_L))Q&)3)b34|wCNQBaB16~ssA=K0w^%q2!ysl<9Gn&%=Oo^- zo%8+i@dIA0-_@x|9;<0t<4BsLWc+386I$oQK%Mv04Mo~B=BCn$jfLUBiPBe4bu;Gw zSHyD2?#X2rkL-?o-k-EMR7v@|ncuVAGyg~h()}A|EQBC_tlIz$sb?01Jfh5ZdJe1p zehef)ruPwL{6!b&33Z;_abv`e%Mps$g1b?Te>ksR&2}#c%m~s=)v)sYd5@Wzgh_w3 zc;C*IOW%QI2r@xK&iVnYCL^AjEkZ3sBDt94$j%7Px_nss&GrEbWw}BrF;t=?7wBqY zwBJIu)Xmy2z~G5l@w@NaiU23UJ8=m;EPp%h6nGyf;Sm^grOyPl8ximy{9CikbFKQ? z2inv!WirihhEQLnwmL47YmD5tWV54Bqq~V?++_*hyQG1ukE=s7M~^+YX9HJ}OhSd7 z+5JuYXKem=W2!nIf9Yft-( zDRF2fmK<&4*)T2tOVYz;h?{>)r4xZEXt@X>i8Ai5`LWZ3g!|eEP|Sg6X=^Z>wF2Lm za8TP;g0;pzwPdZm|9Y3^9NWaTbsaXoI4&8heH)>YGcEm#jMOGQ6QyRS3J_grQ@AU2 zw03ro6Z+aU^xpQ#^?mKKf3`F!Ij&q|8?_(`>+F%xVTwdTHxuvv|PIK7anz6k6gdhu2)tUVe`K7 zTFsJ%TM-koMYNaCb)}WnqM66+hgT()iLpm~TJHGV8FRfI8ykJ}R$!0j(9ukd4x{~F z$!{>TLjnge={s^aWZC{_Cr|A*Bxo#eF`@Kj1!t~U`2xS2Liub}+DH-*W$tez44J8B zw;dXz(h}t2#?>O77%MyJ*B>sa9}7MugXY3nY=JjW^t?WiOJoHuz|)U@cU@ORm-aK; z_ymrmWas00@XWf~3x04}qV9UemB1h;hA(U2K?v3Fcb}CD9aHAQ2=M1p!JMwt8v?7k z_n~AI1?!d#D?QHFPj}G4$@0|)%guPf**s)=2?6#L5Trgwx4U%}_~)w`+8_^}4rO!0G9`N873c`fHDkb4K~!Ib>^P>=Y@r-8Fx}oIc}ZUOwPe*+0AXAJ*G+)&X)HQeR*s+_8O-(-OSE|+p+O#V%71tDz{g`%j!|EhgZmf>N#`4 z?x$z=g`B(i4SD3)`6JmeP@YZURhme>3__m;t)kDnJRqwZ+9TBepanqP++cp&!umci zZV;kzi##}qjwY@bCaTVxZ%HLqd{d&Etah39?aT(eH`|-M(qf^K!7p2<;O{n}* zwv#yU(kb_oS%pCpBD)F}b$RO~x!wST@em@F2tfkq#-D}=tg9q!8H1pL*N4uVD-i%Z z02sMvm$1+n*oTs#t&GS=1RqKE1&cTtw9MF5xI6YcR0wV__Lxs>!B!tn+8-vpl&C4l z5fgDvJhS~G8)(@EA%-3sqFPzTl|rnX?P!D&POA=V>QU3UWl}c}wW*lxeeJF3jphq5 zLGF7BI#NE}we8T6-BP`yhzP_?Xg^+_pWcVs1Epnd6P;gxr56ms@^O(>xIjRyo`cxo zYF==DJKmVTqa;Ib9Otlkf^pPmh!QkY_V_S%;aia3MQPYS;)+@7cYGS z(Y3mn(^ulEogH4pm|W#D`U4!wLtL9Ndq1dV1So11OVg^`ce9u<}LofO1CL|=2S4ves3Ciw7 zuIUuWGUZCb^z$x+$m!UO%9xl9!qUcY=aH!+++8wR+GYZUN2QsVhi4vM{I=b-@hyDa zr!TKp`e5gET4G`@GIaHBB71;69GS?rx1Fh(DQO|E+Lu`U=S)cM`b3SW9k-8qW#NDY z*}@-B1A+bYrp;7cz_OJm>tHVmY66ivt7{@8a$}JqmCg8_O;oGQjck-En0{{3V%28O zJQ}0-kR#_7*Jm2~jWeNbaQVf~F7m2Apd7$QUPSN{m9l8a{U$k1hmehqha4kY}%C z-5`F!U?LJMUdynEO5H|SD~@BX8jUMH-N#aAlC(N8AeyXN;oYF^#?# zrtGY0(YGM|Pw=>EIe?yK8p7XMUCY=>4W8Lf9LV+U z>(+P)UM9k5`3K%EJi3~Al`B?)nfw}&-#BtKe*budwbJU)4^Or0)mOcR$C2FiUDzLB zEaQckc^fd9jE*0?Z>I_G$NWIgvI`|eJUFtYN8&{8h@EJN5ZL^#G&SP^8Cf6bMh`1WkOIA94Nb%a&Gp?})<6{C*&c zc!e7#ve?;ma)?kL0J@%Q8jW$a&3>A~IvsIc&G>QTl$vesj}5fRr@z{fawfeCLo9j2 zL$7{$_z)i@#I&^hMbaScca(K4DZ5jxs?zzaIRQiF+>O`05%#Zz>AwkFGT;7|65^qi zvymKU8$iWX&q?}CWRl4>uGtJqKma`!2>YR- zgY8bh=9dw!1s$;~N@UEXvY42R;C)k_(HyEK7ISgZtQn~lQA$re#6PHgf`8|6BM9@E zyx!cxjGd)eVjoEl{3qsSR@}A#LIj8%%7COpcz~KggOF}koX{_oz#wNm9jdl2i633U zI`up3A*e4^oCJHgY|xX*mhV*N41i8CTZG(BSS5It!Ft|tu%tKOD)dmMLubURQep^a zlynjQ7OqLS2HibuLAJOQe+qYWJeI1+5_v(IvX+_P-gsw7?eJ~k^(DmKqx$X5d}_Ao zb-qFe4;t2F_8!UZiSNW4Crur_Y4SZ`SSQHk+~j(Ql*o0Au<(H0%IpwY;+X^~(Kf(b zqzoeJKNGjdVv-1ZBv!cd;xZ`jraG9}Da|ina}l`?djmvqpmSv;vs)<`UA48|`ztOi zZN)vif6=u-=;#_ZwV^Au6$mjnlTeZ|!9NiN`8mX04m(89EhLP@>I+&y3t<3mHxN3A z%qBMJXKVHcp8`Xl`eTv(7tyb9+kkZjDrH_8Sj@+1J&Op2Pkm2V$#(8^islS!fAb-_ z`4_lIaiFtKj}|mmh}a<2627CDXm*~UUW;` z@H|rF&CA!uV*JFFIBOmzSzFgqz4fHXfw>`jQ#}~6t;C%jH-&&aDVS!~y~GlsHgv@p zjogQNu&5a#SA76BGiQNHK{YLkl*=Hw6P{Lp3+LxE%yKHA(}VEz5<$B&kjJPRx?LHF z-oiI>=|6y8eX?rlyj8g#CSG$wMO?;+&tm`h@S@D`@u4E+Y{7Fdk^}!>#z?uTLm zlK7!tmW08QdHe~m9Psku5B9j4c=aK6)5D0WV5=vHrcg6+SL&Cw_t3{qCiC9g_U3!y=g~7Uhvs68R5ZoS<7!Z1*B!@*uz-!P|0P+MUp;oBz9HGYv7qmc? zng6g-3S;EY!l5ii9Vi)2E1XsQ>(|fBii6{J5j87xMIP`DFLVhs^yJzG)-ASNge|r5 zFli9p0w#O6TXsUEOIg{nTPI*@upJCmR2RB_@Q=>ufDaC}=WL>?-klBnc)-aT^=jnE z@u5GTPloJ!HxcbooZIVe`DLy#_R}}tCP#0`Yy8pUmjKpnPvl!v=pr@hy;ZjY7~MC2 z0Ekh<_D=VzYY5yDF6~_z}7q&p@oIAH+}1houEc;Bmf8pAQ6y zSYM6iYQY65FTf7r$$619KQEiV*y~_9eu&slR3LKrVlwW^UEcNS=5qahNYhHsS~s^h z-Ll@epzCHccJ#T_sSX0D=es&c5X#{P;JdGiAFSfFM2=$U2pg7-R1rA>>&bXS$$J?{ z0H+nFnm*u`+n8m|?`7JZCqY=iEG+d6&zJUc6k=@Vu^h?v^Tb$un=hQhlLNr~I)Ci1%k2%bAKjv(jwISiZ{fxH zh)_g2;nm`oZ@>@-$zZ2K?{pEG!N$4}G(HEz#?2EgAj^az=yi5n+(%D z<{!OS#_M*rPnyM^j*xpxxUF>N-L}+PktIlm^^cpHV^WWUKfK?tI}OW}@bDMjFr6{yDx6LfIwCGlADaBX_SFAvhxlRdKGmOh9ZcHw+d<&8%)z8n2lGeF zw~6XOwnWyR&+pzXnHu7l%`|cDkh!+`pfQQM8o5ACQ7;{25ELN!vf1}bIY-OYanhWf z@^wXUBIq+-Yt$n9G0ooB5wOb3E35Gf_fdMAP~(qGz?d)WZ-E0y!fXNq4%>j`cgu^5 zaECZZCFqSyda1Lsy{H$&GzVjm-2}XcF54-)km%b}`e!By+bi3MS6T|T=HIC#$YfZ^ zBb>UE&o{TGUeB#<%QR1w`fLsZ_4QYy`ALY~;RxtM4ReBwCI;BRWm!nb`CBTBxaYaU zA(1(MG3`5MX5AM=s^$|L=Jgs{w2$Q@r@2kQKo@5=(t0EDhMyTZ`*g|K3fpukF3uUM z1L)!gJNbGR*wOp9)GOa42Q!fw2-OEW;>FbnG#iG0>#KbV^ybjzL@wfrZbOkZFrCt= z%tgTbpplCaMwg~UF)`GwolXNnvsRy3XzmmI6<}^Dxq3=w2`N%;6_|rmAYCFmD?BH8 z4-IKbWUwdS1nd2WAo+@~o+go7+FDGpyS=%5B&pYX5SKOmu^<=yN9sB*+U2F3RO~hbe_@{)!yC3{*qwE#1n2|pl6$2=MQN=KbKx3k?d`rF=&wxI zuD8sQ@5K>bLf|)PZh8vd1iIC%b;Qpg__`IOflLc-i3<^7jmG`V_yGzBr46ccr~|MJ zw)XSx!?=?GWIkTSFDE+n9|xgM=Z2t!6v1SvkvPL<9uuVrTBn1xbh!5rHD+0Ot?5p; zg}E~fh9Y<)5yC3`E%g(PAC(?T4J#>8t2$$%PwSFbZEoGYdO+izPlWTXBX`{nH5$4c zFa71uPcyHr)H=VjCnU1ve(x@Z+jY*84C?j&SZ@)oU`lWQftLoM5*ePhMuTZD1;a)y zhNJxSBn+S=-MoAcn%UADl4%i9uD@xM`@QzY!>j9Q11EIy{ErmhDhj^%OY;Ges(_?# z%%%xYh(LeEGbeKbOH)FO6JBKb1V=_m0`7j?wCVdOfr_#S45&A~YR2yvB(&PyfgqnZ z?YtHc5G4<|xDM)5`^CHE*yU9B$i2EX=*8de$qM}3%l4S4g>I&V zNKA0+x1G7Q;m2I^77oA%+29dKU6aQnW``gk2jT{Go6`a+lL%a-e;DLJ3a~tSlP%kf zfu4E2F94r1PBtvx;n3)J*WBRoksiKVWMspKmG~V~-MRH(!*57*NX>FfiR_-ITCJ;R zp^JtiZV)+fVW82Smq6$OJxzbCTVDh@Y?H!q7saJ^`tWR#9LFBc)lL@b%`C-L*a8*V zk!n>YoicjmdZp%xYA_XlFb>&%Xy>E#a3yakJ~0pa#z-^JAZ|5?Lx+S7Xd_a@ZJWT$ zgF`$>lM%{GMk}_1M(q9F>ez=~{AsB99!lJf&F}3t012@?Iqb&YQc`~4@(0(dl{;hD zZ>QS<70JOYxic(g`OVEe+dkqT?n;u6$c_0OHFL*IN%DIIV!}>`4P0_1vWS9YFa!!u z^%OsLI9z77<22xdfgyQ!M)3+*LJwT-(3qVwFC%6pVkF%`eUvum9`iEad(uShoy65u zO5qT+&p%?*@)`bi`*|k%f^&e?zeRk%ks`nawf)f}h^|DCHz{g=lW{n50qiP71xT7-%$?$D?P`Rq={oYd;$%(ystLPk2sO~vQjlF-MRJUg9PBmHn#``xb$l9e5)9R>g;C`Va{v8qeW7jYr zgaY@D@mNGULK^xHR0dm%Yw2jJlt@?n7-zxlg7;S(9*3@y_1Tq+(<0S+ks;9Ek{Wl4 zX}+Gi8OPGk(wX{BXvPNi#y2u6V@d`+WOt0p2=s*(k`ZmS|MagBDTF4cr0S+*277n6 zS{Op%%bLc3ZCQkH*w)e24Hew=qU@$`!3t}#G_9ll^zlobZS*$fpIs&=nvhN1rFi@8 zQxicGfhb^HQX+gFjoM`=&3AR?zo#VdB9%G#2bE#4=+NqAZB@W0y%svNMNsZsSiea< z`XK6i!mfofv8f?H+Wga(GyBxgv$fBbUhB$uybGWizODPqmSVLT*zwA`-m9VZDyCzQSmrP*{ z&J^4M-AS+bh&05{_OPG8dgwPHbOLrIWYJ~K|4DO}Hn&gU4LqibWRp3hkwN*x12%V_ zRw!q!K||D~7pSXtLG>T8{f*7DXi}Qs-RYxJ+62g~qYr(wAOE>L5J?CHDk7AwfI8hM znIg8r``P*3(tuMAJ*3=qU~9+b!0Oet&4tYjoFe}uuOt<(gY`WF=;6+DZ3zeFu-6zw z;Kf>}&FVG1>>a}&_XgfeL_r^MQ4BqxprkjbF;pT_HoFX247k&$$LOK(4+(;&pdmlh zqlHgR+W5`Rg14@XyF=rLCdJsEifO#_r!cT-suPNI6DGQzZ|ayZ|LkL{KY#EHXE!R6AR9q*8P zwBOIyroSzzN?EF%aymtBxb@IQg2v#*mQ5YE7pWS#hF&>Gn7J;;qvj?O0vlQ@itR(+ z1gKSW=(3-;9AB_B!&`up%Wxp~GjmHJtu;>R?QQe;JC7qXxJmlI4(J*c1f8J${<8nb z`|QF$NP5#2?a{8KK0)(Dz>+y_nR5B+LmE@`@>A}7yGi8MI_XOowv_?#!vW|tYEtk2 zAzXuyX)b^5boYy?$Sd6wW|0=T4m@NVZSv-Gol?0W&5S5*;|~AIV#9^IIt7-FAz=oR z6WxbJyZP&1ETCn640z(MgH8ZL;3j_YPZ%mou9v7v4_VLh0(l%{CM|y!Hh2fFghSmX zRAVE~0p};Xk29~dfpI(oJIjjw{F6ZuR)VFg*IUM$Bjl(DA^9vDaUHP_|DLEsU&v?6 zdI*ZO0#nCaLs!<e{wo2R|PP_-5b_uK31}^tt+rOia5L$_WE% z0zf|KN@h5WNgi64Om7DBuqK2MhcIGOHQ`% z|IBwtT_EHlG#xDGaE^d)?^tw1Jw5X|G6``Yy$*JH!q8M_vyU#$2Lcs^#^ za{JAFD3U^nf&&mUn_DDa)W#PZ-7gl2UHJyoJU z9ogv*N$&Bjxpvl2c!u@?erUSL3&UAimjRQxNh^30nj0pd>s5wx=pELtJDt8HKx@qs zzv@;V7RnQPV}Szuo$zEj;6-GS1$lgN8<-C?^(kS!7S{Vw$b=e3TNv?UxXxO|d zuvSJ548PT^t4EvoWAe9Tx5*3Jo<1tdTA_%zK>%cn5J?Dkty)jvvP{Flzg&ClKj?~Yx5XN2_mk0LPl zoVqr;Ve{Ll18dIup~|G>A&|GXN+@kF83Jwoc~5HRL086 zT62E$``-6?yP6WLSoHFiM&x?5UfN=LX56eLp{3w^Y47^xUyWMZ9GML(DO|H#4a#un z7+z>4>CT*ETftxCVSx9%eO$rz~RbMlBc;DyQ% zE$0&FhCO8t9o*sTvZJ~9P(*D@At4wj_!ykd$iuCroJE?T44x+vEz5IqD-3)qMdB>^ zIa!}ZJnIHZ3v@KCTZ<9x$sL3zq19lo9>PBf>%!!Uo}fDBQkk0I={Hh6`W2?Pxx1V} z_Q;saD0`4n875O`Q~?bpx$w*ljMVF3n>y+3L}Ja ztuKl4LRArN4`m%}Jo7;Upvvmk0HQ=8D zfqLYFV2|+B2Yp+~C*}j{Tfz_6UC>dUT6=)9&+Oy9#7eTui2>VUN?;!52$B_1?jLlt zOyKDM$nBi&yQSZoP{##A9bbn0m*+^e!Vb~q;a9Q1_Fu&{22ZHSO#SkAGD|X-!`4?g zQBZIg&lH6J;PhJ?98QvbI!~e3PcFL4>m;-#!^GyZo;0J8EsgF6oqk3Q2$0QlGwm=& z?tz?CoS--(ba4R4q9SC`E|7#i()bR{stCu5jCtLkP$_Er!Fnf_W?+Op$GAVv6qxrH zya$_rE4}A>aAm}C&Y7{>zXj^I;ch%fNU*yalWdd+m2uEL6v`p>w(D)?=1B>g74bsT zqnBuXzrk8={Diiby2i>A#hudI+o$9=R0Z=_)aF4~F3oa4$cYQ|Jww28`Rsbo;*wG|!$T4jFS$>VdqtTa=SAk0Oi!Pvc*WbHr;M z=dCZa;i|qTg`dLEv6r5zR2bSHuC;0g7^5UJR4>GTs#m}%5d%jnC#LG3!Xh!=2Bl%%439Ast^X9aU!d^VQ5kPzRz)S|#>Mnxkmuw%3LoMmFOsCk1t8^WX zCbk;8UO&Sg^j1kD&c(JoPy7)zC}5PVh1GTClL??pW!!GQX~4ESkyvfqIkx`o{Oz0^ zw-@Ko;k|LX0SlL-&QEP_sSn-|zccjAbwSGBvaz(gx6f?ZTQRshA~4=9aTe5juM^

Z0qy>Yd1z1pwe-Dp&U`xHwuUM_(w-)pZdTJI(?tx zN^4j4C;F#!T*aGkNwi@XnV^d+9Feg2a~WgW41SO{(7ntUDiSGc2j=N8I{j;D6l^K; zNyF|cQ`$pw^2>FP-$i<{GFh?He+8UL>2)_fu79QI^}TbZ{7;$?A&o1(iC%|wLwB5m zOMwyKdzzQTpVqGPKy|~`l(se!=xjZS5|;4Y=+qk*0L{?J3;Q{{&%3VAZiTTYlKe}R zWE4N^Wr{cCS?#-vD!^HQ6=?&&@&g$EI#uzZaO8xhAz8yvtN{go)i(0t&k^ znD`hBeZt^)$=j6ac8(qxf<%(;iptV!TEniC`b{}peyFK(dh@+QN&C0&KUPb<^GHv} z{@*EX_m->xDa(ErkN$ubt;|7zSTJP2-A8Q3Av zceT`pm=&})J@;thUa(U$-e`hKrZ9~=5%Pu$MTCI<%Y+7)4fS*ozV4nNAhu8c)sTJF z`0us+@4PR1P96U7?HG+0(iLo-GUYDlEN91H4ajr7MX@Xggz2Z`LcT+Oy!it$Hn+RgaP!>;QeNd|9mPnLpwxoKrHf8_A5N(?OS+D2+ zf9vVqxwVtrA1Lv0l9oBsN!lt(NTPW5EK#k`hCx%Fv3iso5a_Ec8)WZ`t~zkVCaY=( zp(XI>31UDSbL*cPOEm&o32BbZY+L+>$el_Y7p}0Ca2e>CnD z-essF-`(qY?V$Hye`I5U!{s-TOMoY$+u{1UpY?A4pLO50ZVmZJP}z3qRnz7cbje(1 z1Mn;hScucg{(zUwxkA-~ysyH=fo#_3|2DtntG?*IYF; z98134)w>2vF#jv71$BcREG6Yre?sP^sC1@|@*Ko#H!)5JSw>iRes(+$XH3qKnpaju zj_rjRofwpBRFn+^_K}R%+&Wj`hb-wm?P}ey{*w*n)B1|6pN>sWUi|ZXQHGn#?w1#D z<=Zq`+IIhPZi7WIy{)gpQ#-APSm}DEr?$7!@mjZg_~gT`qPLC1pZ|RCos=nn*L(Zq z_3YeBu{c>6YFOf6#SdLe^zN3$oOQ|#akK4_0q9G}+lalVZdm9Y#&h!&P*lzvBMF!zSRbzaB$F8R{Y0*FY*L z9i@B7BJjU2F&;xrsuguy<*}Uv6-v7w;i>7v+UiZdB6XH7`s&zDl2>7$Yac%9hZ`*$_v5$RU z>UZR1ntxj0H%`_&?t8)uVmmI^9?m!eZp9f1}*3WrRd{7M)wMwM+lfjiTJ(HSfzTW=hx8j6@A@t{KZ- zFxa!|ZMW@_ZNJ_$I#!lj+P(S0y=%utFV*Bf*!{Hl@dN$e4{z>sEia0ktoHH@WuAWX zVg12VX|k;z2GMsm-w9cNSa|YAAczH+^e{(ph2P`Bz)->@XmpO8lpG-&f->fSS7?YRHk=%LNr1I_OF0N(gZ(7*lX z=_`vb$%`u+OqT}m2hq~u2+LzMQ}i1Yi4gi~0Et zRp&POXKd+o9Vnl6GtS5}sm(rBv3}e3s^V=&qI1LAyOeHsphkk*o*we$vj;MA-|u>R zFSmG=hrVm)$tsi8?b5QK+z0QsA2+&b`Pcg^TlVoraZ5qgv=1MXVZI&)V!Vuh^7{ULY2tG|hdnPZv27e2 z{vLG8;~(xX2$<*AcF_rkte1on_ zE&2F#=2{${TpG~NmHY}u>G*v0PvkxTO=G4m;*}OfbD1El2I#$jiEX`NP0>E5r@3@qX*L$}-hdxzX)R9!bYcfGMYzkqplt+=myVHvW z_oVnIWQKahO@$VDz}A}7McoQNd@=|EAw|L~4Q}7&xofA47rUO<5L`mek*df{-y&Ji zkWojkyZ2{_Bqw}XXH!O;rXqpnTARqMMNak5@}_D{BKb*Ws7W1 zdzuD4#OY6RbO(Om>VYB9DS-NE%21vWo0+z8&~E_+TaYofuD`)EGv2YRBHPwy=qlFA z4WzpghQD~L>Z!Ks*Z9>`du0-cM%_d08FOfW3tB)>$S*L1&!sch(#~U+O`}JugCT|A zP)N&l58Ntfu<$VwCIh-Hp5{G@V?B({v?Z;YEJ7(aQ&$hoetm-a1sOoo)m#%gumaH129F|;H*c>Qr;)TH> zjAZnYevT=*TovcOrvotm*#{EEs^C-4BF+tceP&mDN~f}Z+TA^F#kJ3SquOu*$UPRh zM+{Nm7C-9RjDPwgLbR#nc^vbbv~Zm^Z)C#5e)5=H97#vO!JNSOZ~ zvf&}*o6?zkHD?1GjdFCozQlL>}>yIPO)Qef;Y);A`6QBK%z=9BnY)TgY1!}Qc48O zO~!2kAY~*F#!ETr^11a$XYSK;BkV>X8%~>4SUy$h{jiIZ|G|z9R<&_ z9P$JLt7YmQ^Cc}vx0H}sw7^GY#sd^WZRI51kR8E76nv3hEHiVlRggv51Qihs$R$$? zXW4{|5)}IlWlO@sLcHNwV$tPV%5Ok+_wKHF5_}LyhJi9CC6D^|VloiThTuh3 zxPT~26w!!Ls@&K6iH#C7S4`_ji%-Zt>36`xoQ zrgu@f_%IYhv}RtECnuQU9LRWCuF$xPCS=L4s&1>7B7dphEE^KjQyS3AKxTYJGDtPG z`653H<+lWf#7kZg-H~&ZHB~Ua%b%3Rz6eq)N~TXnIq}BK3SBr&8U%)esNOQqh6a!X zz?>scL28MQCss&W@CJZh`>X#H2syu@v0-$a&&&zh(u=cH3*%`>d7OG z)XBpULkTIhh{R}U#fv|1J7~t!_&QPwg#gI{kOT4thz3rzW+MO~L&yz*skIf6<_>)g z6;S;cdZ>PO&!PN|pHUMwSMj^?;xbx2t{7Tq;}|1m!By`g#(qM^9|^;$Ke78ZpQ{95 z%kxEft&AMIy+1^IZjMjrkFig!pnfDc8Vv?*9ShiB{cXW~{Kk{mi|Y-l9nZwPci&*Y zy$mfo_q1{_eMg}0mi2b09{)DcyAblSwfg@1D9qH{{ke?cjHFx94q4pcca+OIydBJ) zuXmrY_~ly97pgt)1Ckr+lFUCIx3$CfysrPwm%Er85EeDR*6~UDh2THI^AVwblR#|= zkuir`PQgpN8fXyRe_#h&t19yRBu$EG zf8|`QW8*9e&he&Nk2mbza3*2h_M4xoe!cYPU@_L(wPGbn__OQDO7h`{f9FXX4kvBJ zI=peRAdnSqb>;C}75J^^{(pHle4Gs2k4m9taOa6BqTBC5eR{ z3ZOV4XyJ)DP7EOlsE6yf)dSyv;+Qf4WMPRi<@koRV6s5fx@|_4NzwTneX5G}%eYnyrInatFwcl}VFCypq!NMwA=-IlI^4b|N*?o`X?et0OIYIjhJSPIDQ} z?loD_v@3CCfkN^Z_l(a|J?=tY6G-g)Qz~ zxi>PZ5rFW(Z{LUT#7s@HcDEz`D%MFdw;ogvD{#6Xgzi2!l)Fy(qv}580BkBLsD!OL zy^C=7KuWaI18U3(gJj}s_SY7R=Ta}=tll-&)(Fb&21N83+bokw4QHQDbR&M&?LIOh z735fBw?XTKgMO2IgP6xgp64Z|V!Ci5)4; zd>6Rc&*v=yDs4&Gsiy&1nUpTC>nPdSkUid2WM7NB7bAhmZ5v)P8wInaP;fh;5yMC2 z8iD1Hod`5$s(YMeY5`tPy#X<70}JRFW)*O=zY?Q|WBoeECxk%8bw-BPBon=n29TRp zo?1$#ok7xNsMUvKDv;oleUIa;joqGTh`6d&e3TO^V;qFXYP^snIq?W4^I#`Jrez;n z6g$=ZwaE59$!YwmuL2WtfV81-W;jtrSL}DWAB1p!<|0ut>!3kE(k?=$%u(!U(ugw7 z+qi&3Pc%MKw;~l?&7@A+O6mX_%?^V{x|mNf`B;>~Zt7nR9gdy4i)Ka%ln_z~%i>f) z);6Lp{|Y`U)!zZMO$|T|3(6p)sYPm46sS}Cs5`pLkWD8W-^8gbdHNAUCS`5fca>m7 znvw-B7r;G`EjQDgBA{&`*PoN1f zRnCrmS3fdBSQGwf;mg$|Q%Keg#*|n)#Q}B40^X3b(U<&K-H)$hcA`;~Lr`}{>7Xr1 z(?tpd$e2|1c0@~hl+L!UA)53r9?iK>52!prhVcC0?dYW$Tsc?o<@su$5Fa6H?4j;b zk1*>kQ|(y77%32TnL?4%^2;g)$42Q4P!W~~V6ZA=|BCd$x(!?uWhd;zM;jm*-`aj_ z<$5tJN!lbGOh%_t@KQ5Fz83G%*)EK`!{?)F^Vfn9H1jPRTGO`9!58k zX+&K?Bs3PZDm=+nGWBC1%TPF{tu-NTzB*cYD0~kBZRS zq$t@eHQMOm;_|$!d-IIdD|ab^4}jS7uxe_0N2YOA( zkqW6OpI`!l^lpF0T8swE&>4N+L-VAiLNdz@#xY^jHaKWPD8GJb;S?F~LEIFPpt)!a z6J^?6o>ysg;89VA$wp`(@E)mr2n2&Gppj0k4#)UxTa1P&K9^Rnovbzz1tCK2VS$x}d-DmNUzT)74DDkDGY0|R*4 zRh%KLo&6ZA4{J-iDuDo>xkjxYNn6b(zKN zMtmg=XdzAqntUD;YmK8b36Ph7ai69aj;~utVJ}s<5R33Lof5oo6Tf+=gPX#vpX0MY z%XNTFlA$uqO}?iCWXQoYZp&Id#a(&<6O4pN0|K~fDdB}D1Fbq8>=QPq&5CVF`lJ3D ziN*|k6jxYyv=4s)JP!WaJZ@?Na4x?SwHr+N`_Qq!jqSr5VXw%lM8gbliQPmJikyBP zGZTx}iU8^J?3gHE1fe}878TSkf0ohP=kmNaXwFAtc#w9U0R-^ccIezOZPoobpyEa8 zk(*qQjSxvX*CAMkIPth9c3kb^MCk&bW2SV|oeH5}z10|`HKyr5MGzFc{Bh!({%rvBYDW{D(=(Rm_0 ztaX&cT1)Q#OjSOs-slF=I)?_)hF?7mY;Hs;v16--?A}@15&z`Y&r%LlOcoVacivsb zY8Q;;$m{wsxRvA<86gE+bq^r}yp~vMFTRdiRYuueDBUr(_X&)jnFO~Zzp#mBP+#rI z9~wV&ok!g@sMa8t4-U0C6#2-zlFNzBOJ8zq2W_PN)~B@+<3c zOj``i7gnUtPguv^R+%V{4!qZz|NAQ5RHRWu5-36tnKw#3^}!Mview70EqwBK-SRAsa%Q{BWaf-ZJN zh5DtiI}hA)V1v%-VAkWiQ2ON8LsAQXLe7k*;s({0onnE8(z$O3`($*I*teUGGn{^k zR7ug<4W4U#eTl+?8;#V}g&xLL7c~D-gQIau7PZf0D4psANR%#Obh4cL13pYNydaNL zmBukq%EQcV2ih8J%w6Sghz|*l=a;PPZNTVGUFH};HMPJCrt{F2WH9DSF6cfp6#BHb z`OWP4jV%Kkv`U%kl_1FzVaV?Rg&qSaP?pTG{h-@z_y%n{!0Mq4P0=Z$dgDyx{@St0 zl68zOyr~VxmK=8IIifG2=_nnC*o5FKRTSJ1c<&?|xxioWcsd4Qxb)juo1VCf+5^gk zL$vmTpzClqs<850#04iW<&ZO3sf*2r@6dG6{>WHc&l?Yu)2A@KY1aUm82= zD3f#)Z;#Ud8g;_Vp~LEtXzJ&70Iv|EP9s1%vvfHqU163y<87?9gV>{jrobaiSz$LO zW%hA6cNrCb_>ppNFLiY!ZXfiz5Dx1?ZufZ^zer^3fUpYG~SFG?m`a9G{dk21IR@%dtyk0!_-r{y6YSQ@I9MXX&iD z?D!BRu7bSh5XnGk=YDC#V<0xGCU)dSjmEC?c=26)V_e}?>_wrV4X-mw{)jPGzrg7r zfxxo~>@a2cFhUw``ye+0pJPN@$vz{|>A~a{Eg|=QIgPj`CI+(vQkpI;A|lA*P28)W z4&0v@f`j~aYDdF~@>nVBV zrC!Ugj;juu@Tpq*>0TMpVKJ`m>P6~$6%am=@Mt1avpWO$cr7DJOaa!zPTk90rd%WJ zry0$~ap~T)y1^>gxf>j@s|%h-oRc}g-s#Y7(8F6DxSqXL=u!KaOpTFQ^=ui4nr+*g z6r*05s5nlDK(AC9iWzBCeKsPhRAN=7Y!bX)iHAx7Dq~!nsPYkO&uU8PSx8kR$`d^qhUBbfC>KR`^66?MCp^mHb4N|#^7d#g2(J{g^#WRJ>38)5<4 zC}V6T;9`1I zi-^Z&mxaRdnYUZD$0!-S*r^`bkD#*SSGi39GaOH z&$@LMCDkIevH&72oxA?m{))GDOkJcApZ+R>bZ@GW6AE}ije`p{SIvolR4!3H$2u$U zWG264W1Z>_+fs(g0?Kg@7WlLAXoq!hTM>z`>&G!B(opx2!02kksdFb%>RZs$ZG4GL zlfJ$MU@C^b{^Uf@de}QJZdskAIKU;mWI}6#V505?zk~|>HT#h`vAcsjbP#(^u~+@d z+)#Cwvzw^i)re%>MQBpz1x6e#TUUbCQ10ui#(Ksiinkhr6YYxm7ofeS(^gCZs z`jQXF1&kgU8{y}$G!hDDUV)WV2T=jK_hsm2I=8LZgD^ZNHRdz(MW`rX6%Y?~D|B5X zp|_yxC*hJD2`v+FQAO1gXy_g#qMUMIh^kcSAC9Sqo>P;2&!yp)al=F_DF@%2XS@GG*;cLG(*QBMNb8YHA1+)tueMXVw-$B_ z$9W_@V#05IWKT&2z5cpAL-qU-`RV9>T%(F!eW8XH?#b|{w+pff8vwK51=R#(534s+ z{_G~D$4A<$$|w4sIeN$T22t_f!k zS>cP!-vwvV#tr5ccL2@}agKnErho3U(}V=PC+|VOam|lV?OU}$B-NJFhqW#bUr|ZM z^RqOx3y>>-)G!+_*G7nClWIp*8E$t49Un!?Q7;8Zk`OV*a27Bif)$R4j{J8-Chl2) zK63B5Ns529KJ8XYUMAw(1A`FS1>S6~)d=e`#hK?Hy$38sh9A?iy7lLgQC|gep=R5>1m5I}3e+HnaP&o?_N&gv(E` zPkux$Sn!9v3(wu3X!gkqqOF-&JudApw_s47o>PQyXAeW4Fv<8I*lwtHGU)6#dn7~Cog5Q)%PvB_ zdsD|d7p2F*%Pm$DQr$MQ3t zStbklI18$Psmt~gS37j8H^JwFetd@P0$;Ee;^SH*;+xDjvZTak1&a2@r+yJlMM2!E z$ps^a)Pv91EpHmCR#-kA_*FyM1&L<;fPunJb)8L8_!CV?e3m)&FErF!=*UdQV&IX^ zF$-9G*3NjQOv=?G2egmr!+74Ti`1wb=;i0;MY%L#5Eg9bf~*B0Jr6_GC0`o~EiIcG zZ1g{PHEFHFN9l?T0aNytusqz3$Vo9vPZvkri?VfRWG)$rWNjHxW(u z!EHybz~kx_FRCWgI}k$_P-(}PMvfVRawgo0p(P3zs&39@&NYlwvMnExO#@9biftP3 zX<%4LI$7JNJaEr1;A~`b@Ktg>zKH>PVsF(pWP9ErB%+LL4q7p@Bk0aRD;{+P!qeu@ z4foQLFCjT?G*mr)EybNXbe?J^0-w?1EyP^C?iBDO6Djy>$v7Q< zK_m>5`>2>$OcnKwxgO|87_GyLiwn5*EZ#IN!>0t_2wN&QbF{h0E-?x@fctSuy{!c{ zBtJk>rlnwFJ0Ac9GKnX8=T3d(@!wcx#4WpiqDZV&^91;RqR!nNMG~gPKB=C3-C)_! z%s6x%$Z9ZX;5se?&YiT99d*bg5wQM^)<8|2vqqTAm}&IVdD4pNkJY%G=tmKpU2hs% zmznx19jXX{Hbt5Ww0J^EK&96&XVb1oUb%F=o%AM3Z;O^kES6lCBvzk4cwe2Qbz(5i zIqb}n-hY@e{{B-YHKp;Z4xb`EzhnH%f3#A_h@6GIf-59~YK~ z5W#zYp+S(NBTZ!JAx95G3mxc8?i$j(BFeI(DC1@&aq$r**}F^N;#g9+^jmAl_0xSR zNtGHnSFlaiDo#^^;G?PF;Kcd?@sd+P{s`)vlqTdJ0(f8O}=X-HzGU-^V1T$j=If21S zetPAUfBKg|&{2iO~>fk%3Y;Wl5bjZYzA=PD+fYw`<>9rM!XKTKy9&tB6& zVM#)ICsz-*g*voYa9ieyn#4JP^`V6{Hp z3&e3K2s=1`%yv2|hF=#j>0UlD`144l|Gyep4LZcwXU{IKz-ds2L=E&=&_I%h{(hFS z6>>;s2Sdj4hroO<(sCDzGA~97nOPhSbsyCLF?&3vUjJ;+uG)~#S;^^-9S-;zYD||B zopQL#-$$x{>ZABA<31dTFgDu1f!Tou{i8cDx@~l=dMTM8(ea~bs_3}ywA3{OnII2m zfi&?&t4Y+KA_%J+qA;NAkU@#Ccor9BaSZDKRq+kLZe@h~@jbcAmE!U^LjNrJGSSW1 zd%n$)1O|QWF8B!G$dM<51iz*><`2IjD-@1o9XSaptC}#!(l+$KVcnb!NFP+yPB5Z) zN@g*q-ZT)wsm&z-V$s=Nwh1<@#B!vh`|P06kUP82w#AK@7c`+IuK^7%q|f|0>?5y6 zhzeX}5DxCX%30qHd3G_^&F7%#B|G|+S|Abwx1|>VtZB<5#=yU((QCefD0qK_)**Ln z?P{rcDA&D~*kl^@K1WlD z16w8Ron$4i1!M(JaNBb^dn20{GUq552`vk)g`rwc2{Bm>D)TA9S-7BDtn1?btg$!0 zk`h<#cD(<*ZBp zm#{W?``+We6Q^?@DfTv>o;ny8aq492KC*4h<2g&d458U{_Pz?VvF^EqZ80r5!^$c# z*17eM-Jb0JXy5ET`I}4wFjcM{g$>Ncs^is;c8@Aq=lAf@P3SI_x$GEnjAAApLPoT^ zP&s&Az%gd&Ag0ppPP}FwS`wD3G?j$_94<5hKB1h>kNBi?tx&o^jm2?gf~pr`5}kK; zBYAG#oNOJdE2DaPFQy*dVh+N@tyjE#{yYc0yK}*>KxTHfXKi)|){eq6DZQE-?x~ek zxib4&$#%W$isO1-(Z2bPf{5e!p7T4^25E}bb`d*%GI zrkv;-pfUCIag!DQ++M%vWg+RW!$0n-som8T>g$tIdSo~6OzBa>4Xs0^nf!xx;eAz} zOqAb?hYCy=E7O5vhgd7NQV#XGFVKNFhZ3jy1AEjtz5Jf-r2`d+pmS{9z=f}FRkuS^ zXA3fPzJy#^lOs=jce5be-(g#jw`X#O;a=(4r5?xs)j0X$+5NOI$B0VL73uuGyzs8G z@1L^wiTggL4%iR1%!b{{g@-Mdf3$5aH&wXd#WN==J4!ul;b$+c{odRt?Gfntm%NUs z@vGi&>N4&FD-Sob_5VuyLuF!J;neR@iz7PHRiZVV&16f-dq-e|8-*OL4Zx87_A#j6 zpWPhB7YWVg*L@wfOKdsRdaa!_;}2U4k2D4Jc;EclU7z%X_u)!GSjV%iAKtAB-S<|y zN;2qs!uhPrgGV952IW)RPhL;CU14X8$?9+=6rAOvvR+@xEzOP5F8WB`@ch@k#Gg)^ zJFGprs>;!?!ocy-?dnr2FBDa}zdE9P6n)|SUthV%dFHT{I-ejWsLjyJ{?+*Wkv%u> zmBpQ4zNSGV;Jo?)u)>-z(p77|q10wO2}(9z@|5-)YjGbfnTEuE2Ril?;Rigx(eplS z9}v)70A|sf^u@^z^jEKc@JMVs?emu#sz0&7Op%GhsjAt5VuHa0bthC)4R?2Ja)Z$s z4NmT1hx*;=GZM(blVjvqoYrI%PYC8VJb(K9vqiM-C|mwHs=)Hhv~-7!<KNOY(}gpx9oq3#H{i@r5Ug~<4iyeXYRY}Cg5rdciq@Iead(r< zU9D#>e^sH}45sJRj-tbbKR5J#6{Vx~SQOWX$e#8^YCcBqW?0z$AaVr&o<(JY&ie86U@qo_A z`4PFP6Sog;%BokOgf4Q1xxo;{54NyAh%q59RwRcYwU#M!c4iTAP6ZJaZ+zeNnB@vD z315{>3NC0ep*i)6YQ;w74#=Yw>TBn$Lg-N-ZG@>8Hy3)G)P955&u~KIj$Jx(bv#}#zF6}W|K?p3`i=;cIn1s!|t;d^qS3w$Jd(0p6({GT(d z$MX-v`sx>&G#Fx^nL5ZzPHKV2DnScHr~+AM2fys^=WHTZbaGb%5Re;8%4@*uOx8^- z>Xk_`AnySvCG18I{;T29Yok}F`?65-nz^*N^ixXL+n+Q(3(}a7Cw`b9zR z!CQkUm#$`T_?|~IMD^O)9J9zG2A?#08E*Yp2XQ;Oh7BTmLjTyN+kpcY=9hu7;uVe> zZdk6YdUyBPe!VYZ%b7TkLjO`&(o_wQ{?4CLtB6HLc@m@kIDrZBrMs!fGMVBKlXyiU zpaT0#h+K9ALZ^A;fZGwF{LA)cc(w3H-5$X(N$hbC3c-i9h?X3V zfi9BMu_qmpKMYY$#42XbP&YAicI$e~DZ(S0%L}i_29ttMzG=`=6dQUqT?;h$SHtt! zn~>}7zv$*26@x@yaB5kLsFI|ftKA-wF%q8~0F{n!&$(DjKJ^BX@c|yeIFlV{_c-x4 zHoQTuM!k)-+EzpPt9}-vIBb1pbAL_XRmahINkt09UB=W<9vl36hyz#fmQw;q;;8Vk zshv1FD!+c(%&PNPf%n42$pw+H(IhIb#q9VXTzzPX+a{9LUo)B#BlMHGneK85p#}L6 z3{6Q<=A%G6iQAKhmpCBFmMOL@b3}IJIcU-4Qa6zkB^1tfS_a1i)YW-Li)!9k_*1gP z*{QQAEuo-d$71dC{`nQ3uI}?RptU>Fe#09_(DdpF`jp)RcFqnGX0k0IU3j>jX`tTL z#@UJxC8I(3)!`zP+UhpJ^q^?H(zIgwZDXxA$VF?)js1fb_wYp2u>rfyEgG|NqIVVY z2-vc##Z##5i^LA$4Wi;edyC9*{Rup3kCkg5k!5Yd6Gf;L3Q!lN%Y%MuLL#JiG=e|; zTd#7LEGoVufu2?G=H$HF8Gj~i($VjU#^U{}cr4%_A(n;7_;5mYH5_vq7UbJv^c(VX zLN@qQD^c(2-`+i9>=|XW?QwbG2^|~7q=9K(^+Q24Is4k{dH6Y3D`ekbgo>5T54}ByMW+vpL!R=WBE& z_HS~tU)NzDQ^GF4I-;rPu3{|Jq+oaPaqDwY-Z`|>=~!%;Re)QG zjDxdvhI%hDv>!(0u%pGJF7AU$bcob5lc^uaq*K?x_KaYQ+kiL9<1S|@&)_>+H+3Z> zJ?OJ$<~(MbqB5f@3IZkorgpR@U`8;0y#(r>At;;?FX;fQYZ|XhZt_P`A55vOhLqNE zGFlG7NWGCvYD*C18!ckiPI2`pD`czi0bq8F+HekWXP?E6 z26;BTs0L6y0sL2#g<{r3_xGW75zT>x?`+${HOD!&DmpEcVKzKDxY(3DUu}8vEoYuL$y>o!s=uhA*VTak;R{NIe?^#@$1MjUCp>+SK}fG zEf^mrg_r!VUZMhYOUfnnUo?F?(i6EGZN5 zqx5)E-SX?OCBH4$aABJr()VzTIJOn) zPNR^ZzAmcy(8pBILGUgc36Vq}zpvA+4jm>lRD;hJ4NprJPHB8($0@YJgEj-YQ6ALC z!>vZ#B&cCa`ALLXL4D)zk=ETAv>5?<DL0O( zk1)D|_QGb@{ih1vs2accJkwYZtfvB6-cdeEABdzKa@6R+X$LggHfqyRJGeyjdFdk*zXe+Yk%{c)Qre~QH7ZeRQ%j63upnvw27iHx?Q5PkMswcabK5^ORtHI zGClE>(IG5p1s2gw;Wjg|w~3QAHg~je|7xgL$P^Zl{)HJAYB=Ud=7hO^6Ou%{uiTR= z9q)1?q-3Exs&qHGBjh+#vV07iBe8j+Hfnrhd!#lhpuMR)@zwU8Q(Q%8>GEfH^#li1 zd0>34qdYT2-9;wy%(p@d*-FnEq_C5^@$FZ9HgoM)R)&9fE;i%=0Jv_43+_RY-CaNl z{0zR#tq=tk`|wHPt^t;vX_npd3;B(-`fx2Oe%Mp|1JwkSQXi4Hb1@QHDtD#wB;+Rf zl$G>|oTBbN1g4<`orxeF*fZ_ICicL%<7`2lgrOTQJUlRzX?nUp%_tWd>vSIYr|;?& zIuHGh(S~1%+o9Y8i!#`5G9A`ZiXP(nePux}11j7qzIqo7LC)iR+UkjBCPV29(h1U~ z0wbi!yGXGgVx&?GC3w_}IO2T-&2tQ@4ld->;QV1XS%*E|S;-V{& zE`<*imPt8E)Xrnm9*|ADaT}3KZ}2?uMS*vgP)aWKM518hASl4!7rgTOs*sTm;ir4$ zHn$&i>$Wjm`waTE8tC#ML<`l4&*1{3FenDkD&3GTVs{Xp**T{#s{Z%@8z=VE9S)jo z4$ElBL;u|T{0+4YL>}6z+-8ab*}~BqCUG?lHuv-j%gY{W7n3n^eL&b;DhUi|Yc@`& zM8u0*Pt8e2`-9fPK3$BwpnY$!PR2k_{Ej>)`FK-Yyr+w{+HS;i$pmg4jJsJb5{Bbe z=N#^}`K7j@cV5S>di$EEcTw?3D@2!wx@mOkCNPNf;r#(3P&wQ4+vAcpvgQMSqTyW! zcEr6hJ=JGdy;5;vDAt*HXvlWd$^WfgtKq4>`KV3aPwM(@r+X5s$RB=2-HBx(mfRWM z-SD8TZ?Pa#&aB%s3Hfynne%vUqzd3A*;gbiL*xr@k(v34ggyMwmn`#zR|ZZmW>i$4AeV7R# z32VkjVNcn7dw&b$z*DqXyYZMtU`|uuI9Ll-qaf>{8XzraN{WX%Q2IcEK^US0NLxwX zusyeif@^JlYqyW>a;368&Q?(EsyVd0|FqWAURo$CCBdvIdB<1554@--NFc4?qw}aM z(x!16i(>&QcFl;?iu{G2I%`s2uAJ_cb~BW(th^kGw)_rjId+yoIB_I zP`hf1`?R~wIsOPV1QH?W3DoMl$Wd`TIMRoX>Kp^on1rbgk(kM_om(D(vxcl+v5QK} zE5aU6j%h*Fj~5Fnmrg70_sdQ`SF?5S}09!v|rL)uGH2eX%{x z_E%;@G~~_%xgVlaJwaQ?-jf7<@eI_Gji)N{Vsv)%;f%7HNpJH_P<%RN8zu$od^aE< zBBA;Y)wH&kZDm{J{1AHb_BY>^fAu=^BX(B{^?|+ZA_x`-cT4Sy8esbuL!A$0y&vQTbeOFD)ipOP1yd&p!luaGle9Az0*<$i9El zOvg__6Or!7DQ$#LAS~$+<}j^Q-Cn|5PA;jysAAy^y#$>VF?M;NI>)9Bzox!Hz=IU2 z9))WC%q@qYsH6Rr#eoSXGbkG{$6Q2@!gi1i0qRA(Rm>QIDH&tQ!0sI2jYol9)NI6; znl+u{x+ixc>9N_tz#u}0abp8ozNmL*6M<)cXzD_q{MFJ&BM3?|6 zPv&;m5tbQT;H2N);#~~EsjYS#JGymr@ut|lUj5 zB^gNCvvN6)EUW{`a}Wn<_?N#e# zRFN#wd=S5J9PE??(Nq@44*3734m<$G_6)|!aH?+&NVU|19SN>1vWFY8X9BAdL!rXg z%|7X2uv@{<;hUbrJKcBrSAa=}z>)3^ld|b#wbl01FJM#-h$fh76Rd0O$AxN?%fg3a zI7{kM_U?gB+R2$230}op+a<{wkS4BMy$`^Inbcn+G zFDF#FwuX76opUkd%|g4ahI4KYK6I+7H2$VE0cw>IJVqvPB+ENkYT?Zy_xXb9Lf2#c zyzvK%1dC~7o&N}v4Hh7%>{(R5+e|X{3*_|T1pnP2Cl(#Ju_f5y%iI%s0BJcr6iJwW znh@{8y~S{e6>Nk)6jH8SDI?1h>L0{kA#XIV87I7uwL~QEMP8|^0@RZ?yMfCzN~Ewv zsvf_M4f~WIr6F1%!EF^P_R^7hh+I%wK>JR52zs&M``99M#H!m%UTo+6midF(66%{N zl?^@mC8EEe7OnuQCVYAINYN(Hr(BMd!odKQ2XnIMKBcY2BMesfG`gJpJo4<>vchlHm}pAD!HRC|w0a!$C=BuINW%ZfvEvGMj4VTv zwILnSrH53g%&_P1=5ZjG?yN#-9CKuCO(^T~JN!^gCO|j-$+V({i>+zdr`V@^s(U{! zF_qIWIzI&1S~W#a;nbGM)i5!QPOPLDH^CQlX(fh}NCgF|F@$Sls-0nGmKt#rcoG&> z0c)u@znMS4tDt@j0_qK0cXg1lIoxng#`n?^tdU}g zg98|gAGwa;rCN*ajxRVIPl`fki&9Vw^S)xPF@7C zo)=^LBA%F6B6=0I4IoP@p3?f-UOgS#hFEBPL>sA-2|?6-{04~Bo*l8gQ_^^f_d4w< zIXA4UY6wXTI5d!4iZGhM$b((CnL`qrCLq&-;5WVJ9z%2e(GJ+#l$- zxsek3Z|TQELPnkA_V!4OjE=2p$Iba!vJ+A@MIJQG6tJCr)PU*eKk*k2cYS>2oPT&MIRfPo2wL;aT zvqRk z)hO@KDb*w35ZQkr_XLQp!A{gYov}mDde=rgKJn1J_lJKgsA$By!e|nUu*~R5cR)UC zHqO+;A6uH3tHNoHmAuou9=>q(Xg4`Ys(bz8+AB}$u0dPQZ2hG{kNbUU-Rsg5H?Rig zJO^*X=qFgra8m);#47{WrxU5=%!i3VuP z5d=iAHDS#v316DM(os#o32?XOC(lXm9zNLK7FP*!ACm&B@wJa*d?N)lhaQdiUM{7S zjD*ECSKlmo^ESr2UVh004YZU#^wqF5b4KbwW3_ElAWdQ);er|efH2>Bp+(?RK%@U^ zkg>*K&-eCj0Av#NXg5zU)C>s!%Iw zwzw26YSCU*mj^Q^5#!{=H8X_Imw18ll1EroN3|y#*Rmy~MTY%VWac8g-R>$N<)mhW zO&Qkej2oK!45v!vB30*<)fwWqpG@b@RWUkACiK8_kIBGrOevwI26P)20gd{V=RLfp zoRE_}(pIV+dogMmV)wU6prGHwEg{_fAkCqzIB4(5Tc5+S^_#2&<<`IOeN0xfZUU1m z=k4bf`)l8ql6_s-37td~ZK@RmRMnuKF$edAn}v45fP-jq#Np1C&m)N-d}Ix+Z2V9! z-9N5kY?~bUt__G;iMo5U@(canhx2z~LO`700uZjC4`sY6piUBj=Jy_PdBk<#;c$nU zVvG$vFs__d(IGI0=~MAgss`vRyb@JZG|lW-4=waog72(|$R<2SSr#W#fj%X5&co^((%o@%Y)Ta0TUs#uy;v>5s6F@<1#@#Qbb z_s@dh#I_=@Ui?OikI?!P*24qG1lG&R{rsr!dLn44`aPQ7uE`c036yWp5$a8WC-69? z9Gz{0Xz?l^5%ukP=G&oG4ho1T{vpV>H76fVJq8a^<~}^Q^331^prO?>oG%czs0nnJ zMhZSOJ=?GG-<84*?fdlcIK9QG0p4p|+@p6ie64rGdm@L{jUF?2fKGjD;1qDQNI(!3 zsBZU)Ig1VJ8cJFfU%;H(An{DnW^{ndS#*1O+H{dk4Se9iGdAi_!nR)do1I}MqlWAH zrN%CTbv2hqSVqHy*UF~o7eHXCg+^))ni`K(s8ypy9uQmP$4pJ~=02o}S`=SFQ<=CO z5_L)=f<0B&J>zF#l^l@wKA|({z=tu7x@Aqb@ErDSA_#&*H-Or>E1g&J%%Pd0D1S%;)3H5cWP$r8I}D=YMEKOXht< z1%&pT&3z|jb)5toGQ%3G{|Sih&g`}O>6x6%io43S7@51AOGT#>YWiOBLcdx5j-R{3 zT-a^?FM0@K4T-eJpmz#6F&8(U z^euemQvZ}pd0JXjYI)pch9}KM-5-1Z(_qvODd%@=?#^*L>rAvw=dwiFtv~*_`(>~W zIUBJVNxE0Q;r-F?%033wt7M*RjQ6kK@h8s}on!qx{`IrN)hJt*So7_fcP@v&|KkaS zytBQ8-+ZrPMagOFidUyLKKq_ta|J?WaB6%+Iq|JRbAdh0VQ(!Yxxu%EKXhs_IbS<% zfJ^hU41o!#G{c2+3DxCyD-$-+<5IML83L+ClAgo(63xMcT?OY{Ll(*hhvzF%=%~^2 zpO1)Eg`K%`8S7GX>URDBlq))?fmeohq_!T-A6R?qN;9eVSTr3dMjR9zTVGE;eJ8|`Vp;+*o$pyg2>GCr_<@p+1R-0jraBeC*)IN$B~ z$a01RQ`<@N+XbD(FX?x4Ghj87(ULiI#x$4&%pO9rqqOYhm9I1;ux7P&_wj?`;H4iU z;q^A16s}T1E6_wlcK-H4@VUyps!9d6ps-qLm8=7I))jZaE^lGIQyFSOh$+s=Ns3A{ zzOLAFW@XQ#jZcr=j&V7h__O<_L!CYJRU%=qNELPK>Cu&EFVNf4H{A8xTJlq{4?{&jyq8k>mGgT3(3t-aKildE&SA; zc3nuhFWOMy`%C4;byY_iZNvj7V^47-OT+FZY@=l!=9jwdDb) z6U?1uV|nOKS~R@iFTZ$+0;<)@On!FySmm{$J}${yh#j9ge}zaZAImk&Gb`3| z;FEuwbT_F<1`K!tKW`!yk)&9`o-hvS%s{kh+uqHu=4D^`FM;DSs|K z__xV=#>l<(3XePI{VhZr7r$Dp*)p?grUiQbB|Q3DSMZGFMNHon-EJJ zn_$Z67ydTcMOw;KA9@0Q6ZYUa9cLA@8!h4>Ct?YP&TiBH{QW=Q$A6xO|GW?XSr7iR z9{y)P`2T%B{Gv*F$Hz+U!`k!5WoL0%>av%Aq=9MNx!r57F8INw)p_T`r3L@MXuJQb znE2xVqYe45l(qk3{?Y93p8xSF&7eRXIHupa1x}5-JW=v|-Ji%6(5u)mpAR&eO}Zrr z6-w4f?$n_&?ImKaCH*9l2@Zo8D=dBujTHz6=bnCr!L7PRU2fb%Q*tM1J@dDG8c?=p{_O zmP8T}vkI(a;(2rR_DBP^|1$q6jPN_&JDqZPT-122;#qj;uezt?9SgmI z_in)+(h9>>gz{|vL(usRfqYJaXw+D$+$Ibm0-rZS2yr_I_9MXll43SNur~Nq#zy+R z03eP}4aJdNiJ+#ZFdQgSyYR^N#Q5uvZWA|}IRhd0tue;y$c=zQ#Iym-nDWWJWwAw0 z^Y;yV^=Ijudjx9?Zmc$I104xf|4^TLc2MA;D9`Y^*4T&hc^dHu_I}EYBgaLn$K9ja zea{Sf<^Y(SDMmy>HXjheL@;q7D$%qDyJTQ;N;ENcb1axKoJ`UR`eHa-eY>$#k}-|F z4Lo=l_yEz~fJgE#XdR9h8@zi%#OS2E*&6@7tqpT5c3tP8uYFxvA98$55}8W~Djw&B zqO-3AGWN_>Kd+Jy_MU0~Z@%DvrD z7u+X{?rUt+IeR{|7WaR-t_KA5VB3g@n{q-A7frR>C#EsX88SQHcFx2=r4fvZEIqNy z40h}wnJ|WQJIPX~^UaxUb=-imB}LH6PsN=W+hXNI>__j9h;~mdmBxQ{*{j_GB*`rCfF66>j@* zzcOKwhx?uC^NJS7h<^r>{~fISzwsZYfA{`BmI`>UE)_gfo&uU0nl|L7XXkGzia?^t_pEgm*0F0va`@Efd?&DoG-(J1NiN`pcivC;>2ta z6!UW^W%=kQj}u=Ow(OwZB|&?H>545#4nR$sw-7Q2D8Z^$)zsUe7G|yE>@DL?jx1yyvA9O*6^i=f~R zjOC;Hg^L#^%{@Pd(h$NlC zE{Q2xhG5i%fx7F^1!0E3W^~S{gR_(#o;4*_Z0R+eJNJxE6r^|Vc?recdWW%i6ZUo- z?fW<{zAKI#z0AB1Q_q; zXV___yZ`hIz0^@NEB;wlY*WE!Z9-kQ{ zESP4eZ>N>0loIeztVG;PY3C_jt{rnS-Q^lpoXxJL{_u92A&QS+L({;%#I0oE*N1@S zLZ;3)SYySJtNYTk;b2`SOlqHZP_rkPaFcG^{C!V7_jaTmbept*;Y7a-lts6_nAS92 z9l`pQPdnRg)X6cI*z5MGdINNy*Ix8s)xC|;F^?NeW}841o_QACS%Z2zDz@p-#!menA9_$}vNHva!heTI^nQQgJ?<;6!+CzakOQ%FN!`vIxOEZBM zeOKOhaf{pN5UQVJIs)i-vo{Tn4{4q4Y^@YMQUo06>=lS6$VnpVrAfqL*X~oIt7xUu zj@vj#doZN_b1w(N4?Rz;?aO&vSAQ{my{GBGe?=Vq@4sYV{yU`UziK73kB$@_Z=N%a zy>pLl+62FAVA@tW_x>@+_jyPQkT`9ugGwj`rxZ=a;WQ->$UuV?YJwSw;_H$&SW`we zddNJ(hlbtP32&M6nreYHR|J~(x9%{ck(=rvVm8B$*0&Eyg=)k4`SIqNI4ZfE9#DHP z5Vg>-9?ow+RE7kVV?`BBggarkuL+h4Yh`XYpNgUOM=pZ$)iRmA5zGP?&9p2~rYjph zwh^-INo`c8(J+7tz}!|^<4p}mk;$rYFj>6_V?{zc+TMc*nF^Jh7>qD@X$XtM$o7&n zPk=Z(|BhB7Ijz`|y0`C>`&Dr_LA|kdz=J#eaH%Y4z7cGmIV3gEDo0@389pFO6WAcz z>=N^oW=7vb6=RKxr|MBdco`6M07Q|&r9g2%SzU2G;YZxw(wKY(hT>v5SA6i5qlv4C zB$tRtEAf7`_!g4YWjaBNO2coT&%a8QZ*L>ncxI2k!h~g?L2^=5Pp-bXbg?1r+zY$B z;!nZfJ&(OsaO)j;Ht%17mKhHnOQ$9yAcrG}w45QGE6=%HC(QEg;DPVrHM=yn$P!jO zP|ruTW1y^8+61Wnd-#i4(K3Vo0PeW!?MJ`JilG@+*2O`W8eZeJxBU7B>wTso5JV~f zA$p`c0+;8&3?jTEKMfRd_LyvXz(YkOkk=I1L()u7j)^Bl`wA%Wv9|wYw6Zg!p@4!a z&A1~fTquMzE}}7=j&dRS50jG++pxk_=L-umwbJD=82jneM|2+~;X3f>&6U-v!3gc* zfYEyNGcoQuNUC<%kgP`CwH(&9S-UCAqV3ROTDGY5k2;L}OOQJ&zp>*wLG@Jm{5I-I zdpz88>0?ja)nQU(g8+y+zw7uY64(whI_Gm>GjX1bVVcfF(YFoahbi;_6bsyX%LWbZ z>L2mWY)c=|(l~_MUluNnFCM{ap3Fq-|5tc;nAf}KtsBkr8B&0Wm<1Ij6J?0kfYC>) z$jKBYwE>ZF{$0E??Mih5u@*RbeQx5h05L~?Y}i-yx5=^Rx3+XSpWq?8tCg`Y$h}G{ zKr-alG@jZ1jmHgtrkLa88Q~W^|1)VfN0h`)W5)PXkJa@i6-yvC7)aUH zHScXxIOU4aed7gHA2{l?d-{E{%Z7Dxmow&y%4XwZeJ_PMBIdRbQ8zMwUl=2_8D%U( zcEabS9rnr$oclA{P+KQ>ZX`L=R@e@PT{pFE9xL{h-ZTKgmE}kQDkqM=^J%O+j-8R; zCeP#I(-N+)u&?y4CVjmyv0XT%*X==UwIpj$JJ!IQ!w`V?t`ykjORN=0e21*S^~NMW ze#Hr(J&`cndih>VHE|(Xv58z<-_1kWP{J-0mC_^I1{3+|v~?ymWef%L1*1ZRT1^j3 z)rh@;LnR(?l@M%)U86MBzyv%|a*-SS*#3g-7S7LHl#|%jm?-uqg~09AwPk0K&5{F9 z{i7?IU#QE27W%=Ck{?Vje$>66|FO!rDn^JdMpB=S3Cv~m-5TnW@UuvwTGEbLL1P_- zeQw>GH(Yo~pOk{FpHph0n zmOO{g|KsDSrrOV6yP5tWbQG$J<>7}nV1(u5j#U@mk@MW7qpls zl?SvNZK&2{g+s5W73PFs;4wJTaXJ8YVz_<2xvOQ+i-?XXa(vNSM&o)MGaE&*F!j`l zJ~%22`j`&Vuzd%CuwXMxyn{nMMncX&AMb$j=m$9eh5=hnQ?}#n20^;;)Y6e8Y5L4K zW?^i-lUrRMZvTbS2nu8MPcv0Z+yjr^(tKEh!b9bT6@EGrERENl0ja<{zU&Y_l6qFq3f9=EH zCJ6;MBD;mEN6H#yhQVEl!GM@i-&9!=kJrY+*!?q=r z->#$w!>D~gExu@IKXjU>a(56Xkuqnzypz0!|Io&nT&Ie$L%P{xKx)^@OjFjniXzN4 z%xu7Q*$dvutJjS?Xyan$iw{{>K`G1F(2ZLQHiMXYh9?rrMW)20<(IvNuiK$4!jTQH zEgbFJX2oGePZ$2MChr(39wtqeLgs2_7T*<;X|Z>wnE=e5Fznir$Z&>!zJg@8C!s}? z!5YW07{Kdyqs(1R$+ewOfb^_oX-`i zW6shYN!i!9+Hpe0Kf(m;r6`EQUf(^z*gQIzpV&+HYKDtx=lE!fwzFxA!-Fd4ws<7n zpBjEUyq+DEtQ*JhOs=CqD?s&FxrDKSto(u=t)ebLdOxdK9hkde!im{R0D%!WfhV3z zH`R8&qMs`_eAU?4Ll00l?EqjfQ0uONB4p{zx@pJrp>)@3Aasy3nlI(c3hw9qU^-2b z5ami;^85qpbcCxGEnWdBYPlngvsrGQLEA-o=-YI+IJ8&+qQt0Xv}AE(QJRX2 zYMyzVB*~s>iXe#cY|idq(D68TYtD`Jxw?k=2PIIxnB>T9S1x%++pFHMyj97+Uj613 z^U2$oKEt7IgR6$fs*2ND_&w!~Q&-0be4)arG%VfEuHpjsZ<9Fr&}cr|C-4f@#C+#t z<2%BOj+bB~5L#Z9W+$CBdO($=Wsm#UrdYJsH`+b&X*+oxzj0Kt0#1{$R1oW~7nhZ} z0*v;5ieXrSrJ<5`y^0#uU-|XDwwbsVUt$`Q#|ByrxK^E?Y20oALCxqa{0iCuaQ-$H$shl0_5V5KL}aU1qsbXK zWbzbnLSgm*c4RhwNC3203JU&S2*qaB7E4euBV#s9L3b`xJLf-pzdjkR%w1_ zi2Q)i;ANp)M%E=U9>jk)WpDCrBWrRU)%{4OFrUJhGqBoe@d7 zVYiHat}JF7Qhu`66P1=nWDo4)>Tu5Qo^h zlP_g}TTMEQh>8)mnB+wFdWcBX9&EL{Ydan*03&XGKt<=nGqid+)-ooHtD~mJdHqN_ z0xxU=I{1sDj1~NtDg!a0$UWZOGz<}}G_+`foge4jI^+v*S~UGqhCp`K~{ zRnEF?bU_rP?B_8~4hW%`Mr_AeUp7Rmq#29_&CpaDJ@FZ_nC91>Q%vh@OR{_U6bTV0 zHCGK5mLjPpC&Iq@_DA~`_kl(34*FjS+n#}C9@6BvCqll#c4<=T3|#UkIN$1 z(Fs>nRg?~N<3*0@jnO$LzNH|^u>Cn)_^GyZw`x9G@)76?GP|aUj}*#%`6v_f$|1%Z z0J@3z*paFygwv1reS;EvVqHW@+Jr~nF-Q`&qCAqB|TSvWvm>>H~>DyZsdNs z;YfWCp8)9(4#dS>=%a6|L=xK?oGkkiPWG&YQJDz@JLF@#FDbR|OkK9W9n3j3XdP$b zzr799Fg1~*Ei?lhqi)95H)Xl16xUXPGi*UiZ3C~@-A9--{5s0F2(?A9y|<2Ji$on5 z8QT!nNl;k2HtHAtbnP;!1Ml3!b9WBB)k<|m=M8z3)Q46njxAk|9qQ8^ z&sTq_g?AW&y^W6D`1NFtIWNdV-%7Cd1Okw)z^C9gk>GBjo**ru@?GL!-_PxhpK_mu zQo_7*E~qWCn~8fk`tN?*IsM~h?0J*l<_iFWc8Rf@U}5y5U%a*P5t0byYF*peCgiaC z>{43&>8KegZ+AL#n#f3yMTb_?U`PNW0SzszN!8p_4)YdMg@$5z%L!^B{l4qYGZ8%Iujf6@;ZDf4;itRTw}t z_VmeBX8k;64#C2(y^_8MGB#qYO8n=0y zDldNnncuNp`y#pGy|3+{e*g0DRV}yKH z1?y-SQNiPj60nMj^irFVYd*{6ZnMuC(^B7=L;-B(@u@f3pdS8~>a+BaewR9UkfZ6M zw0ajX0o87gv-;pi;Gc6HVEmJr4|U~4VeUrEk?fiw_%KjHkYlts2adoz}p zbSe94izDmK-zJxdZ===59H7V&D_oHZDpifi1ovf%ja#ct!L|V%rt3`)oQ-B^$k9(! zP7LUm0 z-r2J=KQ^Ww?XKck9o|>>trZQhJF#z%l2{?k6*ijee|&BiU{~XciMe2adhsLyXN8*L z=xs%2IEp|_$YR%a2R8cE!*_c%8>a)A=AGA!_VJqiq>1&pCw8GOLirK^1H&PE(GvDT zL`sCQZDhV*CG9DW^f){=@n1O@Q5!<(2-@Ih&E5MfCf`1?^jrmv5$EC0Bo18im`-l~ z%JGCEe7y+-8jcGV&;M&J1bWiu#}Ix2hD8Twy(M%{*b=`U@)2{aXcdQH8Qb$$9{UUA zyN@klQ>hcUc-@o(xwbW4ZH8e*D#huvxasTFpN7BwdTCTBXd>hqe{1~Cu$B`96Yk)S zQ(&e>k|VG@!&z!@Lms45(EKErr3Q2b($`)P08)YJF6cY-El!~NQO zFTK^Zp`-b2x<N8J3_hA2jMcB+8+NCP@Y`Y6^g8w;8v8i3^PR%I zw=U?rzfFPxMK2=oseDLsA!$(M6s@5mn|&s(6w|*#(%`-O5mp5vJB-Aa(1_yvIRXoy zAp2#8{c84Vq>QkT7Se%V)HlPmBD;17z8J*qn=EgI&Dtg_X$uz@t^eHfp7;GTL4@#{-WLBiyPpcb@4zi9sxpxzldQBcN(-|7M z-o)vzf6K}Lr#9r7{~d7o@0QqS@M0;=fDm5^#*dEw2=Wc9s8C2d$&^5rW^6MA!%oc_ zq76+5YQ&X5yqT_5qtXPc0J!3TdeXH89211%IJ-Z!Eeyv2PpiGbFdO6~5GYT_!xGxd zs`Gp)XHDYq==p41e93>M8Kls6yoAxIapm(r@E24edo}K#^5k4V-6@RLg~QyIS6I4@ z8QHEX>3f9JZa3Fy6%&K?_}=RGDr3>Bm==sL&_%W4z-0=vgl0xN`Z~ZF!KUIZg-C?n zh7O_t)&Pf-7k4G{Nfh~@-As3x1U{B^^;MJ&ZAAtJLaNuHBHb|#?|DZ9SQ8!LCo@&lV&7ft;z&} zJij*z+pGd4jLi@hWS@|Xzlu~#e=*uN!Y3!6AY#L}FhM+;(7csa&^4tL-6Pu%H$Mxi z97hY*_})8`-$h?BmT%$|ik;Z44@I(dH4vwr=>%dqh}m>g6-GWDiTw4D_8<%yf@g!_ zO-N9(nhES=t)zuGAH9G5Ph`kKJtGml;Ya3uMtVEO#rD#ib$v%4#d~Ky9n1gon0c_d z!A8;{FK^+zHCJ%}?n7;bSE|ZISUFZL5qD8Ulh3-mekZJEETr#3jxT+xI(T`S;wAW{ zF9Ex1Z$+fXFIRW3x9dA7NNGLzvzZh})LSrQ7<)Ws%*hUEerD`4+V&frj3UBHLX`4$ zE#0f%kO0txelmeasKK{)hLA~Ewy;4Jx17m}TvgM@utmNlGp+buXtDVV;UI4)lOK{y zEg!u$y9*OQr_SHSrNCbihwi4>oP~mZR@Nyeh$`w^4#51!Q>+gaBiyz*=A>y{@)M*u zFvxDS%CHGMI(D8r-ARXyz5R=72kqlFt#V(`Z7yZF&!-V6gjL4gNPAe4iDRlb%MIHh z>SPcUujQmA05WZ&gx62s234qeES9xk5sjq#uJ39#^``IT^62U_qZ3C0#&!p3?yI^&b)uy>!xCnQYC!lA)1%*SNhck6wDQ2GvZ2# zm0!_P!(bl~Y5{GtVyZP=yN^dHbWfmKMJN=9s;rieR;}mVkK6E)yyk|PkCr(Rss&zU zYva4)!rgApaZvOOg&H&4n|;*2@`ObO zo27U_Z9SHV%obwM~dJB=>rOWokHkcv0kTRmusirsyXA!Mc>Dv-kYmLnh}= ze%E?Y_Rz_zobfBO?ponvMmMJRW16RQ@f+Ir3@=8TkSA$VW<~n)V1H>Vd*v9$94u1~ zBH&$ciVYLoEImuHYkYkl7!w3zfyw9>b^Yvgda^p@{KhlkO7~yP3mG!f62W!^-O98T zc++ad7!-e#o?*B?Uo7~N)~E9U5lgYymGq$QI?`&oBlO`ulo-%cE=wRSLbiv)R~Z{1 z*SqxVpxkG++jG+Sn?TWsI7jUJ9(3P3ha+yJc%^{nX$qhd;@kww4Bi9$B_LaXo~Sno z(xVK)GF~%P?CT?*TB4LUh=cYswt~?H04inR2(Z~{_;Lg6dbS^T3Y_6d(2<(4(1G(Z zY{8i3)eW2$?9~RF1`R$F>g=%h5o6zKb+Je`Y$|XtU=d<@tYYna4QQ&MR*)4qNO{u` z9i^$L7;*&5?%F@k$I89$Ye}qY94m8R$FxM6Ew|G0UcVl6_^ZX2WNnlPdCDy2 z#V#8ax}Ed|;}sy4e1lHJEcbGV3kSesfPRdG2`x=kpfhwwX;}qYeEtke3Z8(J0(WX# zNPS~G{XoBmz}65rkl^S~uevt4^Mvu8nUpG2G6+ zYyu%mSocqzu^p6#w}mQ@%tTN*#&`GtcOrgnDi!Z@h9>CT^#TTu0)1bKO=yqdWImV` zxc1i#2bH}rnVT}Mlxdbhf|_DbMAOy}kCC7g{uteFcd347mX(W!{j}yR#+pM9fBW0y zP5NH##U4*`<@wvrG5`GUwZ3xs!e4fwIFgA;eIOFt!l`9VVS5pl5mLcSELjBH1g<$?+CEob zRV|OTzg$?RXuWJYgONyY0{83>n7Z}Kx`2s(ju}6&5^2y~|7E_2PEr%^f*uWbEMt@}v)KHB835|I)L zl-kj@YrwgT`jCDEnwf2xN+VbbOla}CLTEtuQ*s#v(4A8Bj^Sf-4HHsLvsqBxt=WAw z&uai9jp~J2`6jmD_IHIAZTJPf?&}8#werAkmjFyQH0&cOy zn^#6VAUfM$cGm>et6XAO)DLgDuB&(QKlmgeb2D^d-Hy_i4 z5zuLYF6_3iG*kTB)f*^+0F>3=GWzGr9zZ1>Nwarw1P!1~tV9~%=+l5zhZSRZoXP#v zQrcb>yPeI#n?Y$x(Y&*+9a6IDu<|cvL!hZFgY6ZqzcuiIJ5u*WScn^0qM;|%mo9k@ zf42t3V_ukg^Z68F0~1;qI~qU`4%-w7*){(|C1Wv%Wn2QrZKw=0kz!jTBGY2BBiH91 zgv-S#UxB9odZ_CU!N!mjiWq&mQBhjy_NTP?j6C4t6VpGwHMWtQJefC`FPI|X0y+^2 zxG8WozE-T9e_R=&aeAAe@(G zke0g(FVA$(D9u7682xhT#1K>UlDCuh*WV;;PTa6gN>-e>5KXD1nw#%wGGr78|ZHQ`DcQ4jDe!%@ar1=0Y+cZ zYM3no{xkaSe)@imaAKVLwmit5HSw2*u>7Ax{Q3bfPnIl?okI=r*IxfXadLbqJNJ@v z%w!I$%bmZQ063-&8uN~DFfcAfD1f#5x5+tXhVn)B@LtFFvzb>(Hnges?HX{b&-YIsbvNdV4^5loMqF}x48$9 z1yJr$1URpxXc~b8JN*R5tEC+&!4k|eUPOGW!V*TM)3KG~%EbovhcQK8Kr{JSm+5OC z$0}U=rI|eaUz6@JW~KdvZ!mI^W-5o_VF*Inu@idLJOzXcGZEMFjH@VfIv%F7ftrez z2t1p>7@QNX;ofym;MxN3vhwz2FXb}YGtsF=s)c`kT3wZ|!>g+4h+}e5`74e4F|%h# zlYTY*YBNZbL&*v1(@&uXCt*gu{)hpC`~{7-VwNRfZDNd$uy-z?L`8`)wE=g*(GX=F zBUl030#sI0Cl)>Kv<8aKyc%KAvGr{vIq67J*^k}B!vQg-)8-R5p#aUtHqsY{IO3fK z=28?JwC)*575q#iRVPeka1CqKAC6@qy>;rjVk6*S7po+{9y8)QbY-;|4m zFJ`lFo0!jN6(T3l>haRArLRZKJj%<-4Pp4F$y(B4!*Qe?fb$AU)9>g$hs6{v5;P+B zqvF(sU-`%C<`l{!cJkYsFHL^>)UuN!Z_ttFpBkcISFI#V7@f_zAxk1oZqYlBDrDCu!5~{(xJ1+{u!19mcc;ha30k`f$`e{Fm{LP=U8Tm%vf-syS3#3abmv-#AGRz8Iv5_^AnZ4UX|3j}F}q^eN1A zg;NbAlq_Pn$cGVzQfqvDKDYDmCF+_tLG1KNNA6Z{06KXDEWv1;f^>5lO&YWx<4 zl2mIx8T{Dg-CS!R(}Zq~c_A~bFIf7*X3xJ+wqx#~i`MlhY{;V7i8$$KKH1pJen7tj z$TzxlumjQPr$B_5O$P?e$5zp{oRn2DDSD6`*BZH~pW#YBkL(65r3GW87ucK3i_8^B z6bY$Veo);70+%**DUH&>@Iq3b(1yf`%M6jQu|R1%m4I3q@MP?J zN|9ggK6XS8?{AZ3W07mRJxkovITj}LCa4^6)8LN*@U>Dcd!>^8Rky>L`DeiXkGcCw z?MJ#Jbj0IGF-6w%90LY4O2-@hXWL0R_A3FoiXTy_7|Sc@$c>MWemHVk6Iz*PwR3u= zd0>}+Vh8gDdU8RDA!HEmV{mT*9TiXwF@_sRAhZjI)&e1}sLgjng9@|E*xu+f0oW8_ zAG1Kx9j)=R$hftSx3-ej5U9tb^8-u9>{EC-X~XXeUqAX*PBSN{sc*QtyM12Lbi$&9 zh$z*+<_j3h8E+Ks^IT6X<_(GV;-i+G!K@do1z!74X^`k7shVx%N2lh5o?-w2;4|I?ZZbEVImC*7X$ll&OL)oA2SAICY|P~ zKMr`fH1^?l8f=gu=!N`q%PZz1d6llTRB1Tgmk|DD=FN2S^>uwyq2Ijxakp)YxwHtJ zC~lpjN7W!`*hbyapq{$bh;Dy?d8-vM%>=9H-=^yB%vUfxb^|HxS}n&6QM~Rvi=2bc zJIq_C5^w$!L*@-ND|inLeuFqlLNSSm^IhFLnnXX{pmG|9b9LVHWx|+Mf18lwoIAiV z`4%6mzsUHl$PC0hd^Ijo;NjU!9ygU~1EOEDbCK_~t@ZgNE4n)wCbi&QnqXr_!NgqI zBdA=Q6jYjCUvDStm7bqA{iLiROmu0$wHHXmY61lWYaHlj#x!l&GPPi&@#0fW&f$-3AYx|y;DQ2*y+oJCOIX|BW;(av ze@SO?;D3m7O&0grhM<5so*~rQuP?X4~52<>8#(%&tfbc1B(McO3htCQ;_S}tB}jRbHv4Cx1^_P`aRy) z{W74H>+JFlPMdZb_JJ)U8@C+-fHA;e1W_(nd0Qx78F&Urk5 z*Pix*TwCK@!=Vq&Pv~K=JUb;I;sx~i9WBZEkxM1Dvg=8w=P{F%{Sc_6Dez}a;^YJU z8N-fx#8wqUxWK)^S%62;Ljj*43-1QFx>Ff^uSbR>h>wPyt*`@f8$!d{raqTG{5p|c zYH?t>XCuL|`OKxM8P19^-?|ViHOBN$Y&KI>KL=(lZ@Idep6RJYXAQQ<_j&^3wPHD< zeh8wIe*%p!EQ_$({UY=+iyvQ#M1a^$rwD_;lXb5l<&omBdjcf{n$6}Dyb6cKjTfu+ zahE@uOfuFoTaPj}8((2Srb?;64^DreWwJR-8H-1S&{2N}wM zDLPdvU4Z}f3MkQ(1Vxl@%LJ=R(v{WF!721+*xp zVUl5mnT8FpXOBS;4#NzVfMcws!3p82Nq6V)yfP%Xg+3-_9gO!4JDKfosDD7i z^Pfu9!>)&T-TdQaT~RuJ=B@(+Rg)g?X{V{TDc4@Pg?A z!O!k%VSFKlnsEKQzN#nY#bMxx91IwoR`?yGvyCB{crRKl5dA@?FmjCU2m&e-C&5;O zFG7HT@Q*DLnv?NcDS^@~?;?|M(jxj!UQPN3X=?!~qfWRSxi@>7U*rHr7nwC{* zxNJi0Y!)+Sp4moPj`u{UAQVl-UQF|8Z=Aj9g))y}Y-xlf8RwBy7%O(#43%esgZ)C} zvglw>$-uei4UGbRU)bz~PUXZaW46Xs;VAl}bTh;Zz`NRr$lxXs z$INTW8Z>8CPdB+AzdDtsSPA>2C0IFVB40w2mldM+0=q)(7OVKB^Sjuy zW(tBT4^uxixlMm*gz3l+flE}R+X4q&^2VKBx8gjh&fy`f^y>9Tsin^$6 zJ@V#13L0)-D&F6ZU%U6|-eN>B`n&vMYU!S5cTGPNZvyj_y;D<_@oi#}=X{YGLf-)R zJTRt=lF>uJ?tBUQgHw0vGkw$G1l_OHWolybDH6ULURe0pa2O<5CPK=rI_WZmB?@!l z;y3o!jehKVC*s$`@#>H(mB!bNv*Mg!wM-IZqe;ANaDW1I zH{q+13v(p}?94jQL>#q7*&8;)_;U3y0Tg)T%1Nsi7G!#pcziogk%`v64XKGl`SL` z-}VXV42??U-;%b=oHq7<3auRMbqG)2Wd=G-;Zw`$Cn3%RXqH?4rC$MB66kiJypGX5 z=ada~$TS~Q`B;#)U~haKHkek9JdbtqmtCdtcUI7G!d@>5+lDN_HF)od^L6)?``d+g z9hAhFYKnZo1^;O5XPSF*v+@ZRe3vTvCHUe?Fm=w$)?rzDkWMZ8%g`9Sd2d1ly0zeU zjHSR)Nu!D)?O*m8{4`j~|FOQc&Z@oZRPD3Q`oprQLJY~6W4z;ipXc{n=i!GM#U!YGg~8l}spn^A2Pn+oBi!vh3m^m}@5-U4tt5^ROq z?5+zsN9l_7-II{n7R)USIlW{rNm;^DXM<9`?=18v`;l{x`PxVbd+Bg%W#J-zr!`3aTXMPsmDYio5F)1_` z5S8j)V3PyP7C@qE*B||Pb@X>Vqs;xiu1|MQHaz%n_q*6+ymLoklb2ybJ!(CA4rZl+ zj9p=^%~XsUliSdqDGb{J(z~(+4!z*u1F}JMEBjVm5D>yw`TBN$_6|7Em#?y6Eq?u& zd@WM8y)gXg{-nsr$e7Wm12f^@8U9YY06wyhBh(AvXvW?myfV(vyP|s`!c-~qiVH zg-eAB4t+Bm`dE8t3~L3h2^n}J>V2j06i>k(GW{aC5=OAoHKBG$Cd5*?O2ZYClvT+5 zq#sbc13h?@ZEE1V;dhfZ4IePQpgE#kZ1VP8vUC`Z;ZD4Hq$n zDH^)&8PElQdqHX&c8S0R1aR_=Rq}iRUJBar@|2F)|!)$alYtgDxTGuL^DUMvpWM>xd@m)Zkk=L0BP6zYofK~bX` z29p1xta@Z4;v28_23aopm1LKA|DM@qy$kgI>^#0gS5qR3oS(=(ck)HPzio0exiBy! z#runicJsd$aR0yj3iCg)A^)SQ{6taDp(}>IL^|s|(2Iyt&CVw9wrAQ*o0g;AUG%Te z^+-INMKAz=mp?*xL9@V`*>+QHzd@|Gse?mHOMJqXb&m`JKB&d}_oN9s2RriptB>ph zBrL?ysTpgJehrmNuwXX6T!7cV8QJ|M)!tk$t+!=O)x)O8ktB+Wk&4H}f#7SK)*w1h z$*>vp4F^(+srgOir7fkCH}d|xn0$z`sk7M-3s%&|xv9V5RB(}LBV-57wPIz)2eLJp z$j)-wa^y2KAYxd!xH8h^)~M$rDwxP6C`01gf{qohZtADd0=|g9$ZyT{@=#unYGB)y zIM1qGGwx7mN&*hFU=-j_t-}Ka=T7mm0sIbnubx0y!w4st&^wA8*S7SxEyx`ORRi>mK#zpJ}PNewhW zg59SL)hrWQwC<@MgUB^G#FVpoz}OVA91x1Qp^SY&>xpbQi74441;{Ist0T{>R z`ThuhV5vMQ%ioa)$Q19V9!Hz+o&dhQ!Y}+QSK3Bh4(*6`X<$V0dYI8Hv>T@rdeAtK zPbaTON+fdx%Q(=+m(2fO`XbUg(VmxH8D9})5qgfmoPBF6J(M@{+j!m$`K7O$u`c6S z=o{kTRSal-*y*vLP0gAnz(>KXIA6qCKwI3accdLhs(@4;aK-4L5CdrjD)|x=`0^^= zQ+Jsee+^CN);=EZInSG8#^oIP`(o>yBMxeH9nO+o$?=;4BMcbf8Kg1(EbqDY9h zNoIOeB>Q-8T*xkl@etS$#gE-RK%?o@;KXhl^wx99!{%DyBoN= z8J`R@Q466a2Cl!2-+?#m63~SWF&jOg-5jx|(8*TOs-+f(+=%dN~O?%FYn1Ww+FkYFSK>uk53w(KfwFC)dxTO)d$tXEV8nWsz4NbYIxSRW zGin*>=msr5f^CZ5uJ6Sz@`~2FBSvkErCv55E|JtK$54}0Wlc~=JAFsLS3)(_soiGI z!KjzNMP=pyqUPfp^6qsZzC=Nf1dG~1H~cHr;l#ap_ZrGp3N^q!rFs!irR0nvcq8On(I%4$i;5SHWJx(ASF!n z9nu|YF8$ocedN%nb*783=7ca%W1?5!01+nxtTvpN3*6YIcS5q0p_n!Yx1P_!+r>sO zD~EsZx{75p$;No?r6`BpPjutrbf*+m@8?0V&98OKTf$ABlz_{G2LW9n)LaJrWFc^A zJx~s8xh#oqKOc5ar)@|8SD>XpC3w;E;POD~ z<-b}JXEG=>w{Y@R*Vj)P4t(EiKPQ$Vn`}e?{$`GJXg#R|>ip^b@QgB`jgXedSj-m7 z*U%lu=q^O2!-Ae|Zk}daJCqN5XjYF7Jc(+Eny?ASs@kY9vctF|&`iw3({2WpNtAV7L zHJAimB+XNaTZNv2LERfoZo5unyym@zOXpgBpLuO5+fx}|=JKE_|MrDWq0+V|)@9F# zsrAdI^i{V~5{RuNWFv^{hV7ccj=K7~nw0Bc-Gp$o-?eLRl!N}P+RyYLJRoMt7RUyU z)ztLq?Izk==Jo^~&n^!d7lh;$RYexB<)_VTuO@H{{=D)h*Who?VNk@xgXu^h#7Jdx(l_jU2dnhp?!l89wFM6 z2AeFPr}-ppo)>TwgiOtiL`E)ub=62HFkD#rluq_rr~e~(+K}r_m-T;|_w)<3+~sAk zh9d%zvuqHAdX$uQis~7PaCV{gy8HUI^ftXE6R1>}GbQ;-fFiKLtey-?fB7wx^J9JS zZfNJGm!$182b+E&M16}{)-O~r39qU~#WSe@xi6GpWK3ly;s;^;b$J~A zC4i}KD^wC!0hA@%t%z|F-d+xgRl~#esh7I=K2JcjY+de z-jr53bObv{Y_-L;%`AKO7Iqdn*dXItM|8I?`?C~;?Y7F?1mNeUGEC0S6TsWNQdXb# zIeU_@vfT`=iSp=gN>Xjc^{v0lMLsV3^bbQeOxBwiYn{<3rYc&P^O=(E-va!|!3aUS ziF`LwzpZOqfFFbf%VJ#c705=9UQq%Sc2Lxf1dE9IxQ`{zNWRaIq*J-_se9B&m$ITY zMtMEpguO$mrO{U)5+xgelXd0zwNXG+77ft<2atzqp#ypp8qMHSULYl!p#IW+yqz@q zxvygLmDu4uv1C7+^Ig-iydL38?l8W6fA&6V>Fb}3ZXs6M#pqEu9JTV*fX(DqCK`tT zRDJ3Wj(8;kq6v-wo6Kffj7qxt&6<1%Y#(R~5iLR^=P$F%5KmG+-F|%DDb1CHIaKda z%r2A0I8->FAFPs$8Bc^y%|W5Swu2l{Vs7sN8keo;xq9e!(2MH`I2FV1T!QCX-%_Jw zAIhtDQ}0V+0^c2Z?v@(mR~8At@l%Q3MQIuD>ssC!$^JW(gfo`nM=U!9 zI}lSv@+XjlB3yx*!FHe>RcyNQ{=pC<<&7#xdkEe&VSDrS5tq2dd?a5fE(@x9I+?I^ z;*?jX-8V;?@rSzziW0dAKN6UcidO)6PluY>IycSP?p2al?@F1oTV3!7rX< zuddQB4;&L(oa#7@RH!L8@wUk8>&mx|AYhfF4iGmvh4k@BkR05tS}Cf$KG!ZK#VT@- z>_dq{=h@N##6NCT5d}V(6x^eF-$&+7z+~XhNdwKOWT;UUkSW zgIH-Ym%K(eKA;!1Q3>rq=3dz0UfT7@>t#rQZx`RD+TK?p*y*sk^UOu&+1jQ?X!th| zbxI-@b*J2>(3b_G;m|+}R%RoqyM{EXd~SM?6(Lv=-ux9jCj!$wnqx1o3fmq;U|pdR z)n5d{iN8;X{W~dFVjpzUj)3s%BjtreAl9Ky?2sRDGf0r;3KbO$Lob^W_R4oOyzUVY z96S5ziUq6neSpGWpMtmDUrA+vv#wc~^`^Q*2_&l=t#4}qzPkQI5JC1$9b5>}0RzCw0C8wByL@)Yh{LOXlK zlrVy^d}3rdV3C+}i`*q1y!7%UHQPEONXWE2GQeY)O$cmzXD7SVc0}{R!Qqfr!!h|1 zVKiy)_cQ2*6t9m>$)s_h?XGEY4z*?f?=X zR4j86=nyUk4cGWU7=Kq~e*mztdlbK9efTi$Vt{Y|53>L~QCG?!bC&8mP7Y2wcq8F( zY6!G7vR@|55;#&pv%$6xwO9D<8V%ufhN@eA{NY@t%6ELKl%IXN*Hvze#7I0!%Wmb{ zRt{#UKK*I%S8|em>1wcGIx!=S6_80ZU9Uw6SpU7J3NiJuRY<$FZPhLUp-BNTf zz+rnOUsqL1&5(c=*_oWr?Wm(Iuju~BSH=gO(3MHb(+#AX7rO*2>7B(Jko#&*Hg-9E zu~UngU9;_8q(lkSV9UH*fP+$)&EKs~3h|LN#NX!Hc@h`DiBwe*tZ~^KFZ&8A!@ry7 zY@bMtbKdl`5#co0a|_Vrbr3}?w1~hPf-5A^68-v1?0K4g1rpPexDpV6Fi*fZJ62$T zh!fOrZCZ@qHbK{Wwt4wfMhP~-rtP4&fLngx!~15#KlQu753U%uy^*c; zK{}Musn0N3#TK(}317*@L>U=0L|rYF-ULxGz7y|+Y-n@v>xJnuSC|JOHDycR|WP+Mz3rWPjUDv-wEl(ra&kn~QL(a180I21Au}HAI zFOF~%yY%t^kAn$BhExIX_(_gG=Yi~PTX<7#$yn$@KFHO&3cN5nQs8~@OP9wtn+adf zP9JIy=$Ueh-ZkMo?0)(cuQyiv&W>ht!~LaCOk|uXV`ac{-!(P$vEL?jE|UGXXcYX9 z2Eo4&v;F`ZmnIH*0B=w+MVK7MBE4%Q))>DL-3kpza0X4%EV>Oa3W-M61MT`7FViHI z-4PmIup2V_Sg7*b5Cjk|(P83(KWtIWPA?l7z2S`xuY$e2wQ6({gh{* zAsXu2^7Km}o2d#)VBKj~f0l*5oeWtk*GZehl3X0M@Hn8rl8y=hOoeV5wBKccP7 zN&2qf`$*ER^E}i5yNd=U6}N&$X!{4IKkJ;;PPg8%Gq613o$WMl5S5n?k^e8w?!KR4iw1fKJvRlcEKFc!(RJ+eh zm1)1cs_QZT;idRRNGZH01<pLex?yCUMIuVb8z;uf&3ofgGB#b|;k~K3l*_amCn< z;ca2&)NA;FDnNoYrRKNNZDF_BdE-{XGUSO?WjE&F3@xs`a49_D(O~F7^IH24rIh$m zgG5X-WiVSsXr6X&@d#i7387I-!x>>KNVDot$Epxa9-ah zw4$#?_kr0}ixOit&bFl6QZdT@@<=r!CCIo9yBhUiHiLq=3QT00DR$KCwxEL?(Oq?B z+_T5Xt-4~Xez)jg?WTWdeFkF9PC0u{8ekrS7L3gh!W0qKcx|Pxe=oxT3mlIP4s=;w zuu_>(hPN+Nax(P`k-M}NR`r^xFn?wsVWGM`9D-tCL;#_4|ifs~TqH#&uZ zw~qMTATFhM%~O~kAmMbdYDV^#=KH1w#NE1v0j}~4%NfDO^XORsW)BCyNM?B;Ed+^I zbzrg`Tsyf#-gvRHKFiCkoHQgg&T_N&A=Wl#7}X&DTC=E;B_IW6Mm3j%cO3?0SkV(2 z8R-L;#j_mj&1u@sM~Ga)w$ZPMWG|ZL5=}u4$o;eToAE0kr&PYDGRc0aX!JCkSNW{$ zPEXW)>%e?Y-=`F6JSajQ*d7-?`;zqFluKVp4)ubVS&S&(ZZ;MeoEfGl0||LSC1{F} z=@+zw#?DbBu9_+p*UNmeK(8tLWL*t3%JTA5dv6`}uVJ070|1;N!Z|-rKZ98g=t;NW zKNr!^Am{@NW{5-0kw6&atsjEqQ%!JqGkrC(<>}aDc#?v>n05flzs*mA=%YvB2kA_x z=h&++?@w}E67*q4G_tenX>E7xFwVEnHO}4Us^KVU1wm1O--Q&aiKZY6kpFMMmb|htyTTjAQeg1jN3A@KPTIfUf02L z5HzJ;MD~1pK$O?+*Haa}j{q_q?UHfCmp$7Qb#>gH7t>dwqhCy4cvV|p&%NbIUCXPj zJ5$fC%lfMSE0=H2q%V1JG4t#7X$G(V3q%$FZy8LCe<7tUos&4&_waNWQT_bL!$=j$ zGKd_Ycj&Cft;;^s|AMy1wTCl2UUy8D)EK>rY#X5Yv9qN8a-8$Jod;sfp=Clt>dbzG zrO;v89oB?InVGik=)nJ6>45+tWMB#g?atKETV&8VX$sw6y`O&)0p2(dLAO90(o$+1TS>!K>r2qnBJ8V|*SKTzbcsO5m`6d*vd}{ksV>}!H z7=%MnHbL0*7y37pYio=3F54l;KVe?a#h-$QqzP7%;imxx4t2j(a(8ddkFNaX=IfWo z``)a+vf$dTYbNcBQ}&YV5x{ebQOGR;MkDoXvZ=_!x92?GulH-QrU0C8KB+`$m{0}(FLenCBOM$0B;o->QnZfb_lX!XQL}hNHiaamiV(q z7TND*9Ie_+A5JI_{bm*9r9PWVl+$6_o!qL6GQ0a161?)_uMedp4HT}v z)A7OS$Ca<|C;lX=_T5RhiAp)Lx4Gr_9T7&qqO0K!?z55LNWvn1f||k% zB?$VsAQOuFmZ6+}kXNEgh)y-xDD0lz#!(d8X=A@@Anty1=SQ&tm6=XZJOUc-ELU76 zeFNA&?p9EJeUT#yGBFG9O0cA_0gdo842;<9D7i6ugKB$f2@T|)rSn)ehTbancGidu zoG%jEP)*M?_G|-JKwlUumSPcxSntj4MTsR7L03x^+9jyFn>>4(XS!SY6Yog)@=jwv zHAALVd^v`hBU}1$`1MgE4#;DrGjR>#eip*D5bnq=u5kD}8JEf@&IaX{xPv~tA@u>x zYv8O$QPFSKq3iS=!aS!3_j>=faX4Z8oQ6fdBwnkfVsJydhy+yEXM!xSue*TV@wV4K zS?Cw1$CZeB;`(4x*+b5|$VADD00)1*bMl4gjd!m+T=Dpx)3@dUA_AAw%)!#akxYQ? za8i+&UT0vYa#QwGGPpObe(Z-YjZ;**-1w;KI95OK*^V6F^@8^>y#Brv(oQ&qyi9xa z3kcghx4r7=&qg>U4w&P@)a98pf2B0dL!QAWQIsZb*~!FqYX$~oL_$x%cuy=FKsU(r z50$_#l@A_8JF3*bi+9s8uLO+7SCV%%Yvyv}4_=Ce3}V?9bOLQHa!>{C(GCU(Q=Pn9 zT;b*(8&zu3F#5 zF^-#`$HJ4o#n38l+b@mW`X7zP-hjDBBgt~S*c1d) z{^At^`Y8jMv23%Bqy^+478Bi@^mx2MQyX4xtvxn>^)4{fh&AzZ5$Sg1qA+6mp@7#J zI~I?{UHr$37r%bpaLeTD(>sB1EP49yXi=FD=; zQ2C3_dhpPM_Y z=}t*h#w~jucmRK6%i7)1tD{TS))_k)D89dbbKe?PE(*FzZteApNjk;%42aJ zj3kL0X0kqCehoWi#B!EJR+%%Em1h3uiu=9&18bs>YV5YSxaN3H2MH=%${t=yh%+$8 zD1|KfGSp8olSRkZ^?jTLWjENB7@+=KMBCp7rd^--Q9vX1Q?fHgws7J2JAh+i6^OV4^E1=yBruFR-PW%a zm!Lkq-*w~dCkwD*q6O8|7ZgWXvm}S8mv76c#M4kc(=FRC{KPHQKV%2uC|-vRrx*(v z5!f^#m%xkzLi`>5Jl}V;Enm0*V$c)ugoMb*ak6kfBrL;LLf%~(S!01Fi z>xT>I=ADb?B|8jEC!Q?_y9&ko!Jqt(hErwdL?*{FHmp1RL&NQL&~oPQ-D_Sh{XYJ? zu)Qk{l>#i7Fs}sKx5cOrJRMg#?X1kd%@H`BqcVf~xPE6nn>>iO^}AtQ3jY#LFLs8ja9wwu6#}-wIu3!PxUn$o;rDN+dLIW+b*PMA|<2hd;_5 zdbT}UVGu&pFE!P!r7{2qii^p{?~c%$Ae2aTurNLGcCmXG*nY69BiWW*E$>w?-FAXu z=C%LvIM9Y&p)tz;(cwLFdlY&#xu+peKb+SY!GL!@GH+Bfrx`#?6elQU)=vA7%bkwxo zMQ*}>8i|QyM3NII9})ugK)$^0Gf*Oc<3MOUnai#6$=K~S>DFl$bM0g`OF1ONc);h6VEVQMzi{>%u`t@UP=lmhuSA^1i8t$^@LV^-B5JUN zWHHAO)kCaYz!WQ9!v~ydiZ!~gl>wzAWJ&R+uFtIH$s9jx>-+Y>M{OQqp!(@bFuIkh z&&)JqFw{>U|57kQQw#E*~*N8Xu zn~(=eDHu(wDdIw493v|0GxQsMA#J;0BW&ElSOMr>e19?kulS#W+S8}w0EFg41R9X$%~%;}8;!c#uGG{-IsR_x?F(=y(4siu1K&w;>%%9ED5EZs6no)g_)si`+*vf>0Uq|!>>c0D@&2@ zi#;0ZevX*W>ERE;eU2@q;IAVVPDlxZ_s{Eqamy()ZO5>5;2%;L;d1 zoq>O)Lqc~-Z3TmE-1{-%ayMxf+r-V@scmF2^GH8_MIhjjn)WL?mNZ_3qJ#taAc5irYkF(zLoWLXB>6`HOf<|ik$s*=y0zAz6zF46{?#k zEezLXO~d5 z*fuLtCMh*Yz7C2Eb*GJjH3e(^Ci55z>APq^Elckaa^oeKboNr3pMtYuh<379Z+l)` zGU))4sY6W%wr_&Xek)#I_o^7P5V@*~6V_+>d5U)pjSuAQ8m~*inn%TefpRr!tUZWE z!vW0{8NnB$^^B$ z-iB(a&}70CCA~($vPY!g7f!jopm0#Z@j^9k9ZqKc>n zCX@5g)$RT?2G+3;+pVYeQe`lNXaq1#d6{}5}F0Tbf>v3@KFMl^ya3?@$ zNZU|_dUw0f_t3tE&ySoA7s*(+P~#G$@hCJ~aJ%k4HZxwovP<=Lx_f;}at>74&M*a! zEHztm5{Z61Q4oOcW{3&5os$+-KU{-f23V(P;02YvbF%}chexV&t(~m%FuwVxP|&Rx zJrN(`cUJ$=@Z-eYCn>bQD?$0aSyvBQ>8T>3;kU z>ZpHXaMR_seor#nsxWDq5YlJr)xfX2w?Enar=^|-^z-!J@P_c{O}Y)@ui&uscKVSK zPh^XN^e!8$>3^^D? zFKSQcaSVx1qC%beRPfuroS~FNX&Yg1Kr=$%GE(p!ph8=!H%5KBw-X|TH|H55&)0$t zw2&TtASlejq2mv#-#$flx6+ppZ#k^%;jgNnmwbE--t9Yh*E%@2{(2?#6=P|H`-$^_ zOxnq)ZFZ(PLjkQZ##1cq4$!G`Z)@};X~1Fw(`js$+zw5OhyFVM3W-!<(mmL>g^@*A z@c`H^^c!DhA3!pz(eH^l`qlck&_HTjFlXiXeYRD*su64ce(d4TMh}o|r8gNI59Z*f z0lq28gmH-?+fG@ce~$$juUY`c^oLaF4*14%a6WNj8EN`G$UcClqxQ!ISA9=P4PuES z(16F(q4RzCtpLVBYd9#+e9DCv*Qi`JY1&ShH!zqC>sc~;1-<mUGeqhip#aj=YL8R0+kHWb z%2KZbQ0ghT|1m(yox>Y+PCasP@7=~Ht9*S)rjg?gHP+?NgSao#fnZ2ufdssu#;b`_ zg>j(Mf#o{2NbM0jg?lSFaF=4#?5qZ3kWc;?=y(e&#*3}9ef@mf@LP&0=K4CFu18d z*ciT@b}K?w5k)gra%8e?WD{aiJx1VSGr$zTvGp2NE%pEdQdSPhBB#71iL*Qu36s`PmIP|xALLFF4WKUd5 zLzTQ4JIIvCK4hv0YrdIO;%@#$^VTvlKI85bV6quXTZ&zT>|O->J2PC_sBN&IPrvoP}LMBa}xC+-tij%>HPT1S1ant$FsFMmxn{W`^whDTC^uIGKW}VG&`@osY1*yt0^xyRh zUqW+YTw25TQKu$6ZoD>sSav|2z;TmXM4k0dxjrX1ECtgQ!XkvC8l8gS_I6>7I}p8T zHT~9vC44&tB|~Tt>$kY4&BH0h4Ldk05keJSJIODoB@*EhZqp&P+To$%@7d%H0g&(( zN>n9pIFUO*)WpHRd+HH&$HT)lhQjcr1`YJfQdv%%;Cy`=jQP+9>r2{e;#;k{q= z5+W!LAh=do+kVsQ0`%93F1-tByuoSeFQ`P3jR@MCJ?e_EclhfnXQm~fmNH0riDZCB7ryZ7WBnZ<*(Y)_(MXT!9@ApV#^)v< zfoT#IvVTh70c>Ygq*m)mO|))?Dv@)?ZEsVHb~w84XKT9 z3DzQFwT+l*W z_1!$%sO|0Px)kwrv*F)!FjkWQ)|lS`{n91{E1kYHUvxknJ_R_s%>cVw_d{SfsxfM{Sy5CMFZ(Utqr~|4f^t^f$J|7UE!cM;qIe?nTA4ogMM+5k`Ua?3Dhb`wHE3?8Xg%DOn~khuqSN*(z+N z9IE5T7swAT>w86-`62=(s+7Gs^M zigAk{UZ(%GlSY8)sVb#fYELD7!7`NQ-!ALp;jD3KU-E@X#%OTG-m+JvKVX za~}o^Q1ChN61|D;N4+iTuX_MCpKPKrT`YXbnYIFek{6H~aBFV-z+O&?DLT(a>b84Z|Fu1j-LXqf;$mDieVd&F9C^2!grS zerVT_f|5BMM>0j)Pu0L@pH3>jLY>1@&co5ADMn==uxBP~7xlg-NSC)ATpUT=ULjgf zzzlHsQj2E@ZQ>|~6SDZ0N$G7_E>C+HDHn~n0z&=pKAMYaI9}BYX2n~cxZeh$QlV7N z(Z0*@?NFz#SwF(pc}Xdl;r}p%{{O}gLio4X#sBqxsdfcQ0JBb%qQ+#*pG}PpS5vT& zbjvOpNwwt9Msih*FSKL8=l0ac3wbKvuS&XnGI{b^UMY`%j}g(K$!A~LwATFhf9HMK zDh>;y#3Uq8!pVz1JnHn;_3nWrF|_{9?4-;5q7|23lc`qUru_D4)#H>^CrR_|3cUGrB6e9W`EgsXdpr!)v!{csB|0R{jcxG^4GdJNw+m+=S+#QdlU; zzfT=K(oOS@D=TBE4iAX~uGJNK8YE$IQE+iwvh0WB=C8&NsoZL4$-3W%d^qnB~j+G^}oaI6@BKeSM8)-4kQR%m| zyGJt5$oiuJw&v}UN7eTrx4&d!q`O4B;!LSgdYK>Aucvue>@%x!(!jb$?cye)4?LPagUZm~TUx`q#1Ni)J;^|@+S8l);o(>(3B^e!1= zYP^Kz1OXYM0%t=zt}DWSMGHgHD$!$I_$?Fjc%WlSC9IldrPj^cq|+SB0TO;>Jr6MZ zE2CU0J%M3eJBFBrLo>5>kHsjdELFm0;cgPJ0LJ|DXwuIg1-};2Rm$t7o8X5bq-l6_) zq2T}ii}+WpMfw2P7@S&1+#qbLU(9~y379S3j0V`;UH0AY`4j3r_mw z%66ct;tn|n2bz~eQltA_G>pp+MtltZ4i~YZu|C zFjghtC}$Q^$*RvedcbKG6ELV_CGtTFfS91N97;Y8^XxG#iF?w@96N%C1A}Dm-@WsT zGMw@#pQ?w9Lb_wkj6;yH$K(PVQ0aj-N^I9`$mJ0KGu;;vXVtG>j?pTd!ARu5e43=V zvMlNcs-5VrG#)7ODxYgG z0L6F;tKCNZr@^7lSig8BZ~ABas($_E$$5tgB_@Mm$v0G0+Oczr9R)B$Qni-`QcYGP z@se63Oohq#>|8}`9l^Zr{{U5?qCQ5=J#TkE-xC<0E+E(<-E^?8>wQ79=`P5ZSJ2jRav$`=`|G*lPw(y{f+Y5N7>>Q5E7+I8 z_U%9uv@SFQgpJ9n60D`|?xF=mt10RDHOO8SIXP(ZT%MT^K(x8(d?caPo&6hoOMFe) z7;63*{s7?Wj#pk%MH9|yU37IdR==~$-$WVySwD3q8)qH#bcQSOO(_8qW%%*Y_ZM!#hjD|Nd? zBNm^L>1%-=e||Q5S>+#RN8VZKJzWC&hwwT~%?vaIJ*N>#-R(x(_^+ zDuO+md;(Ot~fBS0%S3LibNHk02%G2NWa>6?YmgE~-SmdpBv_vuBgT zzV7+#x2S5SSl|8Ctai|)c@qMZvx^q#Sa{sXdl`6nH`1QV#NhF160i9I`z-x+3;i6zvaNVMwumC(KY2F8HZt=HWL zz@@ijiHA+=WQD5~Gl?~IjXpJW3SodW#d$PLP04hjIYNaYbr2`R3tL8IpMc_7aYIjQ zp}Cw{-KM`p0T)fjb8A+5#A%08lJEY9ZI8}C!D_g5leSsM2A6pDDj@alsc^bYa|ZpQ zVgp23j?@yt5rVOiyOasl!Yl_17~Gh%Tu?hDXZax2zj?%z3z3*O9>!);S~`CAdnEmg zP>{RoE{K@^i~-L>Ob^Xo&B30ho$9&-^Z^umBwU$)$IafOJ*3~M1|H4vZ@Gu;7AMjl zJRNs$kY>!#NI>kpaF}|QXcjr&#L2jV`j&-^&Dj7M-!ns%gCTgidGrqYUV!zJ=KwE{ zCnR91f=9vUX#BQ*z}@DD*O~f_BRH^ ziN|)cE#8m2-w3GFDD2hr5#g(!Ymo(VAB0WFma9lLGuH(u_`65E)dSHCsgz{~%T0fc|C$cExp=rlm(?0)p3K+z~VFMVcS-8F0>z zMhHy=HW477+X_7kG;!H&BfIjDf7QxZwfpHvyX61AIr=sQrfU3zB>ZK)llnVLJ)19ed%_ zLQ@$-RM#Ju-suXQaCRgRCw1%>SSYUY2|Q3PqmH&*qo#^2e(wtN|MW2$sN#MEj>nZf zU>YF5g+nP9RJb+NVtYFkpex+A1zSZd=eE#l8yfRyl5NM!cE&B7^vJ*ECS5#EcV>xX z#Vqf+OxLO#F6)Ca7P3h9AVT`4ewwdaUiLM^IeK>GAbB4DpnAM)oI6eStLb}TJ4ftw zEfimRHgqsP>gsG$kW6~I=jt)bb;aUSwOnCseq7Tq*JcU4c5%^vpVPqbU+j(_*jYd* zdPL2V*=a5#O=VFHZsh=97KmQ2hc^`_JI+tcR|1W})g0y97m z<*vM$72M#5zE4u<$K~J-yhY#=nTQh|&XX!;+OCrUv~{oUN5Cquln5P!k-`+giXy%J zM6tWD4&KA8rY)~Y5@w88!dCgMeheS5Nq}qH$jaK>b1!DKZ?VDMIeK3B4Z~Sx2IB(I z?18IpMGP!el+Lv@&a6b-H9vLfaWpq1My(?OkK%1N64RYjLrNih2jm9_NHD!fNm&W3 z3HWV2Y_W}g1H{bWPFuG9aOA?GyrOE!#)AH*^XC=;B_!b`N3sfsnm(t|n-HApr!wS? z_8NK=5h}4u#Hu8YF}c8aI)?nL#W>Z~muL}j?R->S;%VC|zf3 z%KMg$6!L;1B2yo`JV=ZLQV1j8l%uz$d&n1VcMf$ za7h?IJRep8l8w)t-9>tB+t znR7JZx*vrR#FQV9lD0gO8RN3fNlh;;p{+))=-t?w99=cuEmD9($V#@|c@UW6*H(>nD>;L2V5Kixs*9xaERnrm> zfJ)cUU+aoEd{4MUa}sKw>Z^{Fulh^?fxiQ{dC7JX(B`axjr|;~AfR{#8}Ya;y$JqF z?4H6Y>kMw4%bU0albCC3NYf`UKj7Ese@k36F#R_~Qih9hHc<{P0O-3D96*j7ecy^V z%H*@{`}>8FjKL&PB1?oFOiV!ciM%a{tL4-%Nke-MP>NQ$ecrNd9kL&4(9f&-Z2W>d z&8B_zCU3Pzhe00dB4x)AC)HM8p>?DMA$`wKH{MHRK)Vm!RFAxY%GJrztnj|4AlE_w z#E1vs(H2HV@oN1**Ku~Vl01_YYBA6+4t&+Wa8Dxd^-F_s+J5k9YLUV6NcMs&dLt?h zL?WowI72GK=cND{fYh1{-P1CFFaSd|8VNRT%*PjLl=AuKiD7To&G`syqX=D-EI^jE!(A zJe6V8Mq2@1dlT|H4DxL!E%O4Qi8l*Ktwx}$CS1T@2S4x7+|*U*okEauF+fy3P+)8( zu1}E6>DvY;0j$YQIsAQlv+dPqdJD+u^_Oe^61`&1k_1Ocenri{5EL79}x* zVN2hFx&ow?ghRbYdI!+F=3JaDiOTltp5gmx*V1C(0m<8#gK%BNqcPj24?tD?51?M5 zXO>JB*ym^P%ywx!8#~^vP}HY8zr=L0%zafRDB+2pYRDdY3_jLyyvQkIb_Np_-!PJT zp}Og*-}Aw-hPINYV;QBNIE_xX^o1=(*OHNADjBT^+jmha+~o^0OlT$U5_T*aL?>OD8(^S)MR< zEJJ2Qw(WQL{*d+RGQU5!iQa89&sIOkRejWld~xbgdT zzt`p!y!ezj?!WfU|L0er;v(qE2;rnO{7z8Nilv!Cd-@8L04l7JH(q-c^3}j5RnGMC zdtra{&lj`(HC=AeX9rFecpv@j&~ZF}YSK#g2PHD%ulF0idE~(Qzq)&~sHU>@?H5Gp zF#-w#LR3Ulnv~L65E2y?k-{J-C_2n)99S`#ir#j4*>;z}=h@ zOJ7T3@;E@47gz0)N?unI9rTJ(0+s-oL1Y8J`y<7ymi;_g5;R;i;MnYa>^ASn&G+wY zZS6ntH?1^iRJja>dEAJ(u_ZB*&OQJ!TkvL_wImR8a@}tlX9(-Gao3>jm~hiRZ?ii! zLuUScjy--K{8^@9U#f1FzYj*tc8_|yfho|b#)B;u{r|S=t^Lcd;(yYUs{dq^k};Lq z<+5e6+GFx=_!!tL*D63xEs{sQ0TLVW>F0qE0dxn$dvzzHIcr&cj&_zUrW%Uvf|aDv z4(fVvXqmRKtd|)lV~qcH?%XHw0E(Gs9)m~u+()bjXyPPbaQzX+mMUK}>bRZMl)^1C z#BA4+E=-eOfMV_fW)i4&cFLGQsHBfx2mVl}d&~*+w%A})2FHB}W9<_y!mfio19Lu- zJohGHXF-H=6>B`}@Gl6v<=Ye&7$H9gp=OgCN4vi11OOj@-M67!6rP4c3F z9kHpQ_RalDbT?GNL$0%9A5Tow;QbBlT{YEsiCaXCD!BzTB^}1jt{?cQIP-v4;*V;- z!FQG-ra3mSxya1jQ|!`SrK>mqgd2*r9%2*^e}l3K01li1=a2wuT=uG^{h;T=1xhPY zR2KlV3f2NawK%su1)5}nk+8_K&8El2lZ7$wHNa|+@TP}8<nI{x~K1Y=j|1j2&s#)iXW(IRUa#f`ic#GWN3iV7Z+ z1aiuVQokXS;zk(?HP2f>0rDh6M9;KkS;;J}mccA(98i))U^YW@Ksp&p@;@chj;{9- zb@yzI?o|2A$hxH*N?=G6A!|u#2lMr)nDW3F9HydhDxl=NyLo`JUD?ZLq_7cC4w0sG zCK^RSd_4K+6PY`s1DR>spBpHvt`#F!M;bMHUS%Uh*p(-zEsrK0K<`tX=C0sIBF3)C zE0&wt`WK;@4uK} z1F;N!#Ol_|>#Cp2usB7YMf^mqCh3lV>wqP&*7cKDrtHF)^M^&?`cEy=v_`X4rEf*wUkbR@4sQ?p>G4U2r)jHlf8h~!TV2?_+tTvdy$qGO z|DJ={KdssS93m(p@-^hi%lA1JN}vl+!RRUgrcsDbD>Q}kp#}*+Qtfp~$80$TBSQ$@ zqL?D6?WMq{|1Q}~9KG9MJvmYL-KRzShV$f{&XCpSrismmz){7J+8Ri9h=jV@sQPFP z2;?HKK>-m@35svrMOh7k$vtT-M(Wp`A^IZd{RDHOP4oH7|GU`{A1Py?} zCD79-Zh&NBBTvSzMC%KwW4(L(F;-DnU5cgR$npcWyD-F~%f(`a>>LL%@JSI~G##py zyBGEsVa-EdM}57SB{ui(bd}0;AMvLKW-O4YOD8jNER{b&Lk-)&O#|u&#u|m%wYH)I z&<{%UWo|4H{8a-w50xH(Ux(R1_J#_dgSyZU9xOK3>8udZwL_v3wv4)9jI;e~nxIeF zmrG$f@5rt0#Iu{9JyTisg+aSN&0+_fD59<*`@@MEZHzS2f*Z9!q$F*~tm`#z6(tU{ z8&hr8D}sxHgCg@@=e)9s9=sS%+;EiDU+*@laYA|S zEvQI-PZocTom3f0@@T==md^Hw=rpV@8COas!eiyp7rRk3b}%ZP>{bbX2!$pa9z4NV z=C?J1wz-sJLc;9_b5kPg?!&q}(mn1P0kTo4$$hXl>{A*6;w<~JkhkOx&cs@>20RE2 zLLuPYU1B%tI`Vfz997h`FsYaYg*82;2f zB}8gbdKvYeoihx+zdR9QwhL}x&?D$X*$0WIR10DPrQ=gE_3Bd+wiR2uK)*_Mk_C5C z_2PgPcW-7(kUi7xZ32#0U^L)`_Lj_<^A#uh75Fj|Z*L~>JP*zaEna}m<~ERgQY(*r zRs-J^eseQepfO)2NGnj9FlsNXCPL^dUn)(r*np21>NJXtLT2SLdCNb6k4jlRRM&GP zIoem?!{%a-KI)tVb` zy!BNp`%jnR|7)fG-(#_={gbxhJ@+QIR$d@Ob%Kz(U6F;`;JYGRIztsJ+SLeMh6zlP zzbDXOQog&khta_Wgk3}y8(16=p`_q0V~f>LyGuOgU{+F@RL`PIz-&p8luA7vgpFCviSC1@bel`Ho@6qfKyNV z+66F(peF&C@R`-d@+g@li77I^N)UrqAsHnKacD`fXt-F3pLPwsBg(f6#B*gv-12Gu zyOHuTyIGYDcNP_f{-Q9!RnA6*6-|PWB1*~YWjCKq+CvR(X4v&4d*UjxsjP;4qEG2O zHE9C}+80BgmpTG2ex&NxZHp_=;T3`r!VLwYBuFrwXNxg%b^pWJ`M!C>73z=NRF2jt z0gW$L9D>JW)?GSFdp`oi$OyRR(?G*rUQq>SieqRHE)Z7x(P>*OQRAbj6D?&Uq+Z=O zP73A)9l2UE^))NCz&X#h@^n7#p4v1rB~&$66wd=A8g;7qx7o=lj%iI^k0MUy9dfOm zgOTay$)~?aDORI|y5${u2S0DRU@OXT$Gow+(_CqF@?qo#wZ6ZsM&Lk+@vQYj@~P$f z=eUvHM81RkEUu#XUxMnyPkg>jyS zVEqR$)*)ncv&rT1NN${6^u}x8>>cr;H!!>Rnu)B${;eG>)lU-hi)JeKk=Q z@B7VHbpyG9vl;H}_J*$(k#{u6@aYPhXc6;D4#VI92>u9UXtA-vxs@cpR|x%*TRS35 z@i)l7kcncc%bqJ!zR^GN|E>}ka!9RL5x=B1#WiF*gn`XUL+ZVBic}#A>rF4uNr{8CcD~l6TU@sBy}s)}O869L^e9S4!-h(W#K>gCQ8cxwmhI zP;KtSPZ^pIlh==IJDs&Tb&GHLhNESNBnLL^cvyD)Xp4&Kz?Cik#_#&yyOIB6>ki`Y zBmZ9#iCPb+*3$#PT<(sLL32s)8Kky_i%@A8I}2;VnKpUCK^g&)OIrIvPl9m5qG~?r zp*+L~ecHT-rA)a$-H6FuolQb}mVNvj`te1%3Q&+z$AL}rIz=7Gd1XRe8lY>9iRdZD zlk1h1Cb--}lG8jGj^yiGYpJt^J4xg}O9C`XY$9NmMLuGM} zhBpGy)c65roB$jV49KfwF8c@LOk$Hf0|CHMs@CZm!>I7s;O||64}~dzSMjZ{;~uh3 z7V>dh;*sHnTJ2{|isDuNLFUq|&2scp1=2vHylA-6#2?GGjs9 zguJ-~9FlX8AXKZcxaeiWW7P$q4)~`!^8fwcWiWc>s|h{Zk^*@9;9@|?_2gw;JQm_U zKVjB|U&FQ%xd$lif~G%pdFKrt8I6n9c_AD^okK}&KUt=To&n!$s`X{Ncl%WU9{DDI z6&oQz>XOkiMn`XIp@D2MYrjS-kmaIok{uqBm>nxVy{oKReO0vV?O#6}clvTdRoK~c z1Z(5?41%4aJLU7o6?eL%8j#VTcvWs&G&75;Ri)e>aRtw2KYE&xS+;L8CFgW$@>77Npu3Xv)%)lG!8jT>P4IhbmmsNnw9xqK}4^f8=ut01K_Iv5Fp$=r6hw4BRdR;+32rMki96#kLl01XkZMzfI|e{R zUKzaOC6Hh>wR0>6@_4*k@Z7=Nb{C)W%_pOD&Wrx`$K;MRAG5C{ZlMC7lDEuGsp|>O z(3K41hpgx^jIs4iG-z0_xjlR?_0b^qTCP84<1rtnTt5PzWK+6Jpf5jY;Hyqe0oNw+ z3dKcs57=tRCbA}xbtY%yco1*^l8`*CK6?|BRIlUXjJNcAmdgW#i?|9;ivkDRglkUs zmvqoS&Aj;H|BJ?xzdmsN^;(j{!8-lO#`_wRj2a>jkuIb31M`+tOM<;3 z$JZiLIa^5xA!q>UDRx+Ffe>kI4`viYOQSurCs7=p-rF`2dML`S^oD4#O^n)Ir?Wbb1R>tQB$$09DcB2v0d zZ344Gu?ZH4s3w4_i0fotK_V1hKskk^1u zaue;pbY3}7xDGgsXs3#bsdoRY|Lw?p+aw*&urRQ`9BA<&$) zA&&KW!>1odQ;dhHVc#r+EBouXD@h;ZKlPn+WZYorEA7ZXjyRnLcypv-EOsp{{vxUx zy~?7eCNNTC8OhmQ7hTc_3*-2;ry>I*OQKDEHHNYJ+^{)5!WVQnM_@oC(PjDo znY2zM9PAI{d~sv1K6pRt!-~Gmh>a%Z=$ViDIlZD0l{t)-vZD|U#LwK`5?yAVaOu8ZPKLvuev2N$n(#S>EteNj? z8U76?6z(=-DwLN0s%5tCA8Nlm78HJMmhE2-i+kG&P3vUU4W%bPe(k+s7i(uP=y2s^ zm)6hdq5Nel171|%r$%gDKy1o{H{skHo`D63j=irLV{bPr{m|4@XcioNBJA1o=g;nJ zY-nh>s+)~N53IYn{&ZH5SJ(`Tul zUVeS~Y3(x>ySAy}^2SEi^W;PC8=j?1nmmZT?ecS2lnU+Wa9!0V0y1D|et9QqLC1Y- z>w?*Mv{mzPoXLJu%H4?3T?g&sbpHH3_0>y*MHjzY8}yqLm8w4_HU*zeiHRXd`R@;A zOoZp{&b#bq5@WI3qvr0<-lVMsx847E(qUV<;^6H4wRKhT9;1XRRIE?<;gNH5^E2+D zopXnW&H>WXgthPLb57n~v+wh43S42&O|{J_ll->_-Nz`GyAn31u>o-RaJ$6QpGeZG`!{|GjeH-Lv1R4e=VO8Q41edVW&CJZPsn9!fJU`m)kdbG` zCv3D|d9Zv}iFl-g(_zXMCiGT?1d}xodp+;l`h@y*l>A*VyN!!ydi*Z{XRp&uY1~P05ode|%QR zpVks3CsG%no-KZ^2W&Ueon3yEefXer>)8sY;rUWl^0Je=du2>2foLQjtBUbNxBgvMK`qc8P)UB0MScj3(+=k?e`hllLXzwST3-Tv+A<4-?d+1?B`K%C2^hs*bu z{9fm$rAUHkJM(W5xUas`Ql-DrdN;Q!-e|q=s*KOCE`O@}v8g`q{fmdel`vBB!c}&(r)fC#F7ouUi%i`1!%xf9ztIPjnT$9rs!t*yB~` zO#fw4{h(0iA+Y1y8S-Czwmj?3(-!N2e`UU^rhu+&4h1k}CYgA;e>yxn(b? z_apP}*+lF1?|nS=Bhr@g2OgA^CzpGv)Qs~=zCfEA6$WQ7eSZSO0qhICTC*wrGMtsC(Y>J@yKfB>*}k)KzS))m#RUQ|-olPu>jj zZwx)#k?YAG;eg`H(4J#^FJY${+KTTX9IsG)gsc`VN~@bmIB+B&iQl4en3?>-)KQDIgSO=D zCFFzfW#;s#h4u*fxUj%&-}7c@qDyZ7v~(Ol84c8lvV)o8IpdGinyxlYw4uycxwfE_ zzO({V@aA!L{vf(<9;+Tq`hyhTiDb=XqBX>JmO(OFT5Q3-qrwV3)6awecl-Az>MFc) z1HH5OxHPm!WP<8f!|HGR5o$6xDJCLbhhesKNdU^hkh2S*WfohRH;gCOKtIGM&}PG| zQ)Adim-ftz0;}Z16QDibZn0*RvlgBcy^QLM^j$8TlW6eFUQ7KhHN>ebV_@yC*Fn#1 z^H*kPfsLHT-3g!$5pdUoDW%as9oIyHAKrg4=mg1rdXZ-&Ie6Vu^nzORspO<}|GT`8 zRr3wo?*E(X0onUOI`<6I7wMYlimRY&2m@UgU(W`_Ac>8`v zO$V)=s$V?HI|PPSRDR=N+wB>S&X800+b7K&_$c(WxSwMRCwESNISMZBFBhpIJrCT| z>y!62GWcM!W1;M()}cCx{P1HLivZx@4-``$mebiGOV*^)u7q}ojDxD1A1fS2kI}lyM#IME}PbILw-tLh32}HwlnQWa0sJ5!_{**Eq*S-S&b=iz^TO)MOw^cZk z@12G3_;Rv&xgzR!)!ZcqQld~B30n)&m1Ckq6kAGPsL^uGeDC<&iTrl%Sm<_{AXN+@ zvgVXdU<9Wa-ZW&#IysB6P+Vw$dTDhb-W{Vkw@ItLr1Qwtp%L<2=9z8k+h7fGK4|gd z>#b)Og(y|VYG=X zQIebD2jc7>B`7Fb%Fa;?^6rCS7FM5krtgVzJzN+;srptDZtD^S}`Eu zC3v>$QD*ktoStbPfbTugh?F_VE&G%j;NIQM(F`U%kE81;4nl}7ZZcy7I#7gEg)2@$ zxx8%@X9aP&9J4lxV$WhjsaMNRMod})Uc;fdWzQCYUTo4)dq;Ca&)Vd#dRDAWL?Fg^ zNf{4Cjnv@*)T0J&0&Ncv|G+S0TP`6fy+~E%Muju593Gi~lfU=@hC=1UHy7&n99T3wP|=6F;P{t$*-Px9s=5foEk#^FXG)hXQq$QNg#z{3E?DHej1R~`B7 z%)M|p*y2FfWi4hq$+g#mcBqCS4pCF0q&@wNZR~?R>eyHxT2(7 z;)4@5%+|`(gj8O$i~MNKu+mof3SZOEfm*BV=cJ4HGPv`Ak%Cc!wUs#$)Ax_p~q;FRRnMnNVSX!)v5%;t*<7TfXHsZT$EX*ac| z0l38gr3_!+mEuUv^rQ4PdXO&4e}P{xN!@ZRd`qSw>`mothvQETIUR~o1ed}ec!g%P z_8{X-vZr_kzi!4EuR}zc23MUOKh&V|w36%sQ8NLDDhm%h`X1sJ93#prD&*(md1yJ; zk*iDIE3;zoE-NiOmM$n+u`UbkAfM&`mk}=y|hpP)7Q-0^Fqc=%@9+oP< zqqg}ZRs>DLE*-|XJEL6z8AgM&D+$?-H6;6E3##v;^+li~85OVxxf`zU64e4oT&XPM z6G4khkRp{jIzTs@d(~+s>!>BnB0zaScLgrEIy;o8{(AD%!W?`o0W_w#Rd9DZKQ~1R zW_GFNrIT=Or*TfMY08a8_8w#sxF|H|5h}{p0_Z41^JC~{*>ab{+Ein9LZX-0PT43E zq>+8ZZ)L_eWARxwNz|elSHR>UQS>Zc>AC#?Rc9CNJ@XJw^y#l_B4Cf~JvX;oqlc?P663PY=rB@{slYmKV(+)PcpxEf-Gzeu}#2N6F0Qc)2K#htO$n$LWuZl z7(3D-@-CU+s=`G=)g;}-WyFK72*8UqprmMTVp-7mw1r8wsLrNjece_mIJS?2lI>S1->7egw&;76-U94CI}x4 z!e^ifkrZXvGBY!7$!@crfBU7y0p(}8`A?rd+dRi4rd(3#QzxbZyXf4Qp!Es-a=Z)_=HHQng{PU4tBVW7A-)TLiEMB^6#4E~(cv)E6H6QhGZe zQcWdrfO-}+R=XD}_;95TJ2odwr3(oMX15HX&59M#9ZXoV(J!k7xwdAS7JFvupM-<1$eO0C?ONfx%AMRM9fuDi<286B8|OM zh}Ze(YJGh{dW3guV8QMpFDNv(nE(1?P-js|biykJwS4N=V0`F_>957be|+9s7P3XC zgKZ^VYjD+DOsS?ADjXR+Q$}(fID+KT&O+8HZ~IsXT7qrv=Z}*sng{US(K^gkQwGy{ z_Zq`fN*j_8xa^~x+XJ0j+0VjNKz;?I=3u%IOV9ng%7!_%=jq2l)SXI%0Q854Uro~B zS*U_cHSG1k#>u;4yLcYPbZ~xMP6+GoUIYFAyPDz-M@qx={7a2CYYMbX%iyfoNIn2k)k)tbNkhWy`}LgJ5>#z4Ftv zTo^pQe{0jTLh+u#A3n91oBzw%@Nz>IMUT7*zEcHzw{!I=>lC|Xi)ji~N*sR0jM5SE zOeG(!B02c)8h;gs)Q-xVyqlCHnwhH$3Obck;Z-<51&?~!-UOZdtBCp_KhaL2XVdxB zIFq6em@o&^We&%m0Jd0!ax-L{!m2`bHBR0MD~W6jt(Caxd3!%+$($=nBEQN6@Qt_?4OWfCr2OTF#TO+y}|(Y6!)sarokqPbwY*#*exzS z7a=uzN`jpV3jig`rTpWfs|#(it!Hwer>?@ygBUeO&^i>--{J^#*TKRMH_6_@UK4m+ zgyrGalenTk+9aaI4E9kWUN;(8W^89{9V#agBMTw+bAHfJ#d#vp^+nOCG1p!qao3&O zYvWYN0(H)sH{`<&a1XYPOLHLO>jqw>N6P~6D@jqfG&z4~s`r^k6_K%)#}CjWj%%yF22%=6j^UJr zZa-&3BwiO|5y?QH-4ctVLXnUKL&((|gB|t_#oGNOXm>SNf;N1Tv%BFB&Hb#UV8X@2 zO*K!Gldvc1uld)Jh^M`NQ>z?BgY3nkBVdzk+X(DP|3QUPBApX!iM8=+%S42lM-MpT&vZc=QMH2@P!~3gF zV$>9fT=6lg$vfBus>a>WKe#zlTnSKbTYwT;l6CIYy>evM_#uqd@eudC)_v2_^)K@J z&Q9E2zXAwibQBiw=`y(J1RM>OiW+zm_|C=8(;SCkG7#bE!hwFWm{O;XjyHT-IC( z2EYM@!+?;&VS1}H;N6@PE+v4fOI}BSB!2AM?{ym3T<8ba&7`PL(rPIorst2Z(XV0% zRUdX~olgI#R$mWagyg-gUjupLxD_Pl(H7SYL5`-@SEY)b4?Tp-7Rc3tpePD5(;FHJ zuz5Ho`xta5CG|m(XJpul$lQ^D+)MjRqz__MzA%uKZe*=-2i3qwg37?w(p#wM7}PVs znRzA)q;uAgF*16(b?xA-qCx4_QQ&Bq@bu5&cREMR3U5z6tUnl*a?~qGW%@0+0b=lJ zdBlz>j3L=ETiluT2!WVF8Zufq#?-W+(B%3rLH3!Yq@KEZnR(7{Il0Ep<)!0MM}zEG z?U3z0lj4Ia(ke3c{_7c2ZrZt6pUu5^*-F!p`sV7OsrpM_I~xcOt@si3Upb|^T8ZU* zA6YGbjyxZVLtoNLyi}j5`TXAQ6%k5vFj=GBd>pH%|`wyxlaT zEibj{asIZA%Kj1JZToP3MqCf&4Xy=wq{Mm4^BbvCOrp9BPp zDm2bHWDrppvCrh;N&VuqLk=dlzw_?gu)_D58O?I{eeK5Ts^X791SP9#I>6MKUd+c) zfyAupU422x)QI!byD>!KT=m=3kyitgR*~V)p5^R5o#p-e^nI4f1q(fZA#M`e(v5j@ z=>QY@=GKY5k6$3K0Xmw%@P$Qg9J;=qpIwq(j;}d!>eP5Iah2q!$&9d#DGXF50|EC(86g8Y|e`*z;*{W?I#cE_sE;=Lum^){TWa z5kCyxzb%e<;4^TjtVMTNaa2^Gwq&-H*dAn*4m9{i72yv^iA(svL7>o`xOmhn>`8we zvHn!+a`|I{=gNVTQzMw9cR;1h(|PQ+OS7KUh)z+ELK-a+QEURaItkUhNb@>+Md*$#&a@YVhoU2IzyVkl& zc9=B)RZ}!Z&>kWI-NJfsw3DkfM?j;Zqr*!`hY|WOilP#niwN?9q}b(^5u*ty!{GMh z?OBgp6NfMXU`5V0YU{xT>ey089)hOaNY(@GNP@DpA;I(JJP`;)Rx7rbzBmtZx&3yd z1DUIX}3C}E1_}gO{YrGzv?0$3~XB71X z_U9#h*^=^Z*uD5P@~--)r1;<=fE>1~etTQ>`}^93 z+raBwr;KA)qus@VF-9kYRZjz&GY!SM9OUclc4`AY6SJP|GO&Q7f!}cfmog&Iw{rqX zAU|pzFY)?(yxJ25kPLzkt|%B;A#-Q4SALNq~KNV(xxd_ zYhJ;HUU`npr85L28f|AH0jCdx<}@|Q&OSW@LMy&|4pkRJAN+^sHRru;@KQtPg7a~Y zPpME|f}ih~h35(v2&>d;tqvml^iy;~ra;eyF_t9-W?Y(2|3a3r=z2EnAl54)&G`-} z_HGH%TWCn#{46_w9n`pRmuU9mIcbUDcHHh+sja@ml(lykMZ zqx$31H(7gsy8TJ0wt!G-y^xs!eexSYEir=|)jN&_bB}eLU6gJMYj~nc*)A30QyVoh zr1QiLiXTk5;MPgcJ6l{t4JUDs2$FG2ZXWUqv-0i0CxZRLi@8ruc83$+omSD1{{Lv@ z{u`F3f0$oU|4GdKhf{Adc9jBmQR3EX0M0R|#b(?z2~S71dgc^_NO6j0SfG-_GN|gP z8y~m>*0~*OZ4Rxvlj}{#1$rw^Jj_14WYd|oM@x(ZLXuU$Fpw1v(&_=bXe$V^q|xe_ zX|*I;i-qAxu}pqfWRmltEsvP>^H5!WuFQv)YOik%EfhA6%D>vateJW`-dOId6O`LJ zcRis8a5z|mZ|3QOz+)hJVpgFqN%960kyIj6aYopT{=pW#NnFugwJVsu1`cgM^(GUG znEo6Z>D(qht@rRU^O+h0YM7NQxs%L1N73#==2fIRd((YV{@!bC9IJjb3K+#*)o+QB zy20LMy+lz@{E>k=Z*O<}zT~7c0eR+@zNzB2@8f(9@4w={(0y1H&%@Tb%nF3fh$B!h zxXZ%ll0(-*lXQu@ix_hY^nvsg+aY_}!zt>Tx(FZ6ElH1?_n@Oqv>D1|D&TEv3H)$U zlhwW6D`VvATB{RkfkChaSPJtzvjDLknJmy^Nj1brnvofl%@K{gJf_BU0*+3`>0avV z5aUcEM;p=d!fm7C)K>%BSXmd$CJyeF5^lxpYt`JNi(LbnwzV#qK$D45ftm74u(6oB zg0yN2y@jJKL$u*kX2413)J19)y^XSAC{`y6$$Q$}o0oT>AkT|&&&6$zEnF*)T)xLn ze9WKWd=INa{+)Rf^wjLuoFHx{W*uzY9#p`%Ch@xk`vGFqi9VwJedtZZAGbc9Kb-fI z<*ur-fUx_kd%-%7F;yHMn4drZIC-xcP?+!UXG%bX53-YE*oQt2T>tk1jPp(f-OMkY zX-rx!&Qb)OEuxlsUXIoc)H2dF>Pn*`-@RkSe#+iXj^pjgvC>5sL<7qlMb1@pk1We!_fDQ88Ga2M7F-h zw1bh-GrwlCKwane`Pz7cQ~4gdD#|pX|JuFyU$!zJ|4EJg9=IaNXGay-rQ-_VFD8^k zd;u^gVk?9Gt$Q6xt*6GkjuL1As)xh=e%siIT(2|B1!dz3zmc%v13kqZFY ze}1wB>nH@(&g3h~0l`{q zM0qR$A&hzOSg`_I7v9-B_KmwWpww1`8HcXu!>pMgN!o5HB4yT6Ri`0_IxF8j7~E-Q zhd`8lWP5WvB zSv`ht`>q2Ma0GCmD7Z98HU|PqU3jAjiabm#f-icW!lQPCDn`+6Rt(JheAuFZRd0E~fd2v-Ftu-)j>rI(Q87}gjpAT{CecF_6L;cQZ@A5_|s zwtNd0Q_>|9)}n?ElR3p{(Z1ZEkz$cuxDR`Hp~7Vpy^~rNnUFa(Ix->`BG3B%!*2I~ zZ_dbUP!^R+$0XlKC3u+?v$eZd9UfQ|Apyo(W+)c$GP%h}Gjd}2$w&(;nPYtdPO*i% z9<5g~#nn@4zlJk;jrg>1Xn6F7t>QrW#B|UPjn^ApKstHr9rzh?Gdm5TpJ${aWRGQg8=VjT|mBme3Vz4JF)JCX|mK zA?t~y8f{cvM-&J+SiT>J(vPO>7_4L`*)87OSPQwdg3zv!B~gh9ssx zA}(={xdmhC3}uvJEz7+EZq#NCK%y9M0JmEkH1xh4~ zSZZ3RHiXHS*txVDHNLL}6(daDH>}WN!Xxhm2W2~=EtLkr`8qb|V^!bs7Zq!ZHTL9hTp99KKTt`Zx_;D7u9vdk70Ooa$fXm!1r=t%S(byynuIY^R50 zZ<){I=v>YY*^vfnxX^vaiC9K0DK~J4ur*E1Qfw>@T)G@pxN^CXVm+0xcon08Y6JE! zGaqEKA2R{VVhdguWVCd)MZWtZ|3#Z`xgXVET%qN#-TDAgs; z`_ul>t`=F9r>1fn?K&$SP4R#GiYZ0jDUWo(pX9~a-z__E_1)xabX9b`h58HetzUL# zsEqsv8CGz@@_7 zedGfhC{bfyN?VB$SIj$emo`vdH5zy7`Ah1r3Ee<3X5Ul|WcQ<6``AGFd=QFh5Kmm( z&)_xoW-icE^1?H#FhCy+;+j#h*orko(ILA%k%1fruBwT8jeS+@#0p-=>=)@$#-5h&Em@4fn*(| zIuu-Nw?N8P)0Xqa?k<6i*L)j>Z+|#4xTQ{I8ncFM1Sdh(ZHpKLd_GKU<72vbvT1Y< z-w}!dUYqu+3ts}7>uc)@-_LYl4cIvGoZ%}*7_jmt4}_eqtQ24M8<<=buB$p-_phcV zjo$?(+|#HrZ}~M@&I5@cQ=q+3Mof2V*4YD%rkew%ks9wSDzlENCBRQp^sjYt)liN7W-Wip0Z~c{0?m*}_bBmrkR4 z;IE}_t%{E;pT_KjPXm&rN2TD6Pkd#R%i!;Gd(5f4-Z2L--|$E9erKE|k2MXWy+psG_Y`j6-aA<|<-Iz;gcqS&Tt(>+)Z)_E`@p=6>=fd0 z{2A;3#=L)4ZUJIJ@sx!fjLvgz5G@f`f$Yj#qz7ns+3N8l18A>ro>Asa$KEy3e2-}3 z#h|n0k(Ym2bX4t(;zBsom`%_iTBk&yaF==I0{r*_Y88xc4`7%oU8f3NEwqPOX^2Fo_q~^cTzUXG-!1X-<#X`)YLC`s3aAyJ=(hVm-YvnQynuL zP1-z7Qx2GH5gk)~VSUrO3oZnK#3v(l2t$u5hN?9 zNJda#1_4Pjqkt_8Gq3S{=X~G&-&^%={p;SU_o_~7cmHPh+Iz3Hc3Nxgt>hWvp9pzAjl`wdwqgfc#615c^eZqVP|1a)J{ zIC!0ro}RmfmAQe@1^wSl!HYbuUcExe3IG9tVIfwAy2l(GosQAs07ifkpa+BiMh~yh zt2!1I7mmvO>;1d@zaM*3zj+77Wsl1GJNbVPuwC*F^@4=AfXF>Bg?a@*xETPb61=X4 zgaH7J8k8;)5q9+myFi#T1Y!`vSC6pIpE%(Nd;W><{-&|9(t~K~AoVhNc!c->081N` ze(ai;4^$4z41^T|FQEbf08R&CZ7*MsOAv5A5OL`FEKf z9zK8LKV^Y(LKUAwgSdBFUsgBFT^LTAk5e1sEv=xsq^yq%|9=M z+q?sH{-qO`n|G+g5pTccy5xOMAHtCAun*pWw*QhBwsP@d8xF+LTrxGAwH?hu7+70=@8mS)^E$ z|K{*Wr+@0>Pp$pQtLNW({fmSDx&D8DK>?Q_K283`>)({H6j&2%5Y`QQ1M7gb0>@xo zur}CR*vljQulfysx1!JAwORdcV}Gb+P=C|>C9emRa#ZJ0KL}N*RajFX{#iE0*D4~0!ctR@CT3!6ab|_Iq(Rm2c810KnKtb z3;@Hx2VfGIgL=<0umRwKJ>Yqx+&gJj8c50z*4MHe5cr_gi$h4a#9LWic`u{ zs!{4tno!zMx>EX322(~-CQxQk-lcp%Sx5PdvYT?4a*}e9a)WXg2EZ6$+_2*?S(qA3 z4`vQ?f?bA%z+z!(usqlUSOcsbYUgp-B5V`(i;9MdgX$QSEYyR~Q`u5^Q3X@QQe{vT zP(7l0M%70(PKBkyQ<12dsRgN}sWqsLs2!+%s3WM8sPm{RsavS~q0zKVy+=bs!%ZVW zqY8~iM;bqxD4GnKVwy&pS2UwEOEkN*w6uJ*(zIH%=CmHPVYErK_h{>AyJ*K~muW#d zMmhxDX*vTsC%QnoIJ!K#8oExpF}hW{1A10^F?tPpbNWm4*XgtAAJVtekI*mEA26^n zNHAzI*f97p++?`R(7@2o@QL9EBO~JpMs-F@Mik=>#=DG<8DBGE8GkXcGf6S&F}W~> zGi5PVF}-5?$n=AmnOU6qEVC1H7;_eLHFGcXJTu6`!6L_E#BzxR!&1odjAe}F8=M|4 z2G@bR!6V^$@Fw^Oe3O-)Rh(6i)q^#bwUG5W>on^w8wcAdHgmQBwhXo!wl{2F*=gCu z+4b2ku_v&Xv-h$ualkl4IP^HYIN~`Ta`ba7bJB21a2j#?ai(+DbH3-qbFp(NaoKU9 zxeB>naxHShxW&1RxUX<$b3f&t;@;;G;?d#p=1Jvg;2Goj$t%Enme-p%jkl3^l6Rl) z7@t0$Ki@6BR=&^tRQyu>mi!U?CH(#Tn*y8yX9T*q(?x(Rkvs2!n{4$Yqf`B0VB_QG}?eXtZdp=&TsMn3|ZcSiabx z*iUf@aeMJ(@mBG#61);d5|I+M5}!^opG2MvKKbC}gd~lmnxwyEvE&CSN-1S2U#UW= z5otiRw_}NQRYxKRZdp! zQ9e{rRS8jPP+3zIRlTTMq&ls}sb-;;q4rvxT3uHiqu!zZOG8B?OyjA>_8HkT{%2~= zeASfD^wxZ+xukVM%R{S7Yf<}{_C@Xc+Kb5JNDt%#B<`%p*-K|D&#vl7>iFr@>3q{w z&<)XT(fz5Xp%KyEx;kmSP@AcXBZS@QE=M7F6_!!h0;0=`xqYPi2r#^3T{?_?v zBOxO%qiQ3(v8r*5ai0m3iM2_A3GRZ_g^&yFrj(|}rngN$nTeSNnzfn(=0@hX%|Ba6 zSOiWlUlYc3K!%sn1>?0On`7J1^m^t|%DHZGmLbm!8Vx0ZK~_v&S>%Q=_7`e^yw_F3~q z`sVp=qV!M&D1zU4zf!-S{^tHw{^Tq6R~iH80xkwT4`dJY59|vP4nhZwUX{L@bagRU zBRDr0A7T16b7E2N98QX&q!6aaoZy4Tqbd%wx|IK%C3UN7cJMs4M?Fm8& zHxiZ-4HIjVSdv1LrjySkm!?pqpiFsBCj@?PSvyN)Z9)~|(e{%83)KinEJxyv&4b5WBMJ*gHY0oI0U4Mpe^=(~jb8H)ZZv4Eb{Y-ny z3%M6n9mhKgUvj<7?qujp=p=VVcJ02p`f9V=r+cZ#y=Si1p?9LsvhRJrN&o8sgMr>b zox!fxTCY3aXuNqoq&Cz#tTO!Ut@7KJcgpWt-mAQSHljMx_Cfta`>5t<=NNLVd;Hw^ zz=YAn@TA$~=#=f$%(Tn&;>@L)uOF{`B+Oo$J(#=kiTYF8XV%Yo^Mdma79m<&X^U;E=-bI} z&D&?U-{Kwc%Y+aD`Fq9>z8_UPN;^Hf7Q6F%0ec5O(?CA((J!@MgZuXTs|OK8TH@V9 z$-{P%3F#9#fK0yX5#n)V1OPDTro0S!)8DHAfY}iMI6gr3gY%z$-fs$uKYddOQ~c{b zO8zhSPharp17t%04bbbzT-SRA0FR+t9J-m$0>F`}ZgUo(R8;)?hEk5M(94Rae*+4p z5XdwE$>bkG06^Og0EZ!D@v z{kpbJ`2J&Ockd_oYyU_t3IO(}Sbt0QU*v+6KtV}G1*4)pl8b^e0=i*rRMf{$(Xi`S z(s~4QoH%`xj#Kwm*^_R15k)IJmuJW*1GlIWR*Y~Y+HcAJ_XLakZ%OvIVE>kD4lI=$WS5w&8+{!2TfQMT4p_byoAsL9rkbz}hcbcxvP;inA)cW+dfJg-9jcGa= zXylbY)PIP%0D6!C(O4L%M-hH#))4z)dH`{s8o-<+19(?+P}(01l;@0v>S$6;6HV@G zl~hST(2F+u5V&WAJrGDSc8eCrzpo1HUz!f5^ZwouQ9XOwsNCTU`ZB#i)%Dtj{QW8z zzO*^4spEa_TCKTUuPKJz1lvEEK02OmU&ixr`C#zdNn`gkboQX|8$0Z$lGg1@#hTMR zMr43NHflyn(cJj9+04Cx`S*zD<|Wo&NXQ!ul#eJNk9-kO16UM*#o%A|8_8J1?JlCg zbuu8yG(`qB=VHalz`8rWrxV^6^h=_Q40y@NlkW8+DnU~+AZF@*!0CnD6`{ghi*T~b zkKT~oEA+U2qM<)${rx<4Qn9$^`@klTXz~4|YRLfIYI4_CSUG0#=`b0{ zu%P%SHRcgz+1hIoR`^Y_5kbCHJ`lXb!ij zy7%Q(zV;fjD=qcIRo62a);UnZ{G-an217lL<#_^8a~uBt{`Wi!&Rq9KKeXAGDNF#< zQzUH~Z!bG!1Ta|dh1T;JXL4ChhfTWZ8{4cZ6@(;+N$M-jrlE$Ho4zSy-FWLw;3caL zE;J`Pv??^%GyQfypP8FA&f?GY`=fH!Pq853(zxY{pJ5ys=p7{kiv@@sS0Wo3NLs=n z$bf?K7N+m?Qt3Xc9U189ctHjVMqs29+sVisYsjR}K@!ts zgGnwBkuJjmXrGVy;ehFL#jldmt{_RQE2YHO2>cEP6i+_n!BAnaZ6L(>TV@1-scn^% zz}1Csl!J+_JJz^-JN5J79ov!ms$Xb_|hODLvgiA;Us zzU|O>K;MU1TPt1ICJK-NGH|j7REAx*M}DE3)s?3;!j8 zN9?iP#jJ;i?j?0990DXv*J6u1-cA7Xk)!)(S5yA0m=H+}ste)k(Gn2ZbKVD9`NbBu zoW%8~SCJj(i4hI6_h{pH);LWn=30qj_G!1oGg+#Ce&XxNR;d>pM3~k+O*kA1Y2V-S zRX%8${7^{-GDVl(Pd_zPRYknj)LHVr^&!tX&&FJ>Z~SoCp&5#oJ>KOZxNU4Gi(oux z3gZrU>T;ElSnZXSl|AV1?|W}0YItt$&2oE3M@M_^YWs^9&x=c<3Ty9u$%#r&EnX== z2|K!e9=87`DVmdJnPznIVNROy8_~KPBhef?6Me5=FI&Zz#g`>jC5=sotnL&BTHT&} zHW%>Hd2;tyrO3}Y5_1O`xRd0M*$e36!0gI{9`3X_RpM>PI(d@ELk2G3vd5#LNLfy0 z<6+vV(*0X;$POu@4Cyulv_XHL^2Wbkuq;AXpE+bKFzwXFMBtEF9YT0AaPGxK-HyMh z&j3kcR8z9T)t^89^bE>wKj}r32TG+P`t^p7m*Bh5kk>uDMb)zuGt$yEPZhi`@;RQD z8Zyw6tn$yT#sh^}q5)}&)E&fNN~n5eRBhN0)3}M4 z8Rw{v$z!&0$CRlQ%l5d*F6^I5e}6c8Bc}T-HFSo-g5s~2VBjr+0Cqb?(;kPU_^N#d zAK1E-O{?#NOz+@(h%P@j(P5AE4X1{CsHPCl_{3;GDeI5UX!H@U^6|@;>MoPxdlqhxVTN{1CCza0ZryxArGII zWfs-dU)LPuE-Jk?;n}b++K%XPQ!utBaHKqcxIC=c$@Zd4hTA^Qk8O){St5Yz%ly{! z*r4foM8R(jP_lD+r&ws@T&2aG-`@W9fijMcN-+MlNSSbXqkH4(S^DQOh87w>wP*v) z&_X{3BCVS?_elbWpY`@hU5#&DZf2BJ!9Fdi34>HW7h{a`0{6^Ki7zpYA7COxGEn*h zv;wGD>C|ceFx1sDbn9||{~v?tsJ)pomNYZi&n&Le61oS4vR$m3S~j;9s&)^FCtEp%WZJ*=0Y;fYw(E-=i#m4iNvxb*eCtiSUhG;#a>}EyUeF8}n9?&#gGaXu(C2 zn%dHnjkIgmUKYRDA$2v};Jg71WO!rOV(D~*oqH?oW`%M^-_%&wpghH_(jVtXUpOtS zkIuL|=ULGweH&WAP2m*34+kkdQ9JofU>Te}RZ15u@n4MW%(7uu=p=SIUt5&Ul zcu#cd^#KCzb24~@zRlP&Nb8kO+q1$<;~|dBX`>#iguJngf=Ct2eJ%_~=~p!iTJk6~ z?@RpW?nUwR^eEQr@Ig>7DQ8@qKFq@K zckNyQpLNkLZU2&dg4tc7CIgR}v)4+QkYBQ2VP*^<_oI69{ZA4DG{~t{@YzI;B`nbL z!TkaOvBg6`m`%u%xX`C@=T}=t%QD?&w{eE)Te%&tJ7n*>C~#lPNx*PySXSQh?U@yF z`(eI{b>pk=<9#u)Qjzbm%#+rT*A!K!%+pkB8Er-e+T&Lv+a$w-TX{ZUqp7PyY~KZ6 zE$H@`uc1fDxT5QA`ArX=j7~kuRcpSpjObbT2q)2b5v2DfK_P;~UddqtNdmpyjTDU4 zOd{m>@a;NGTTDJEwYs~G!Iu)9#u{?&{Bx1Q0Tv5g5uK0_6H*KKnOicPvd8-$due1s?SzCS+v z~c)@6Lwxf&`3e)t4TY0#|mo*($~K$Liq zfb526mIz>2!0}!y(I>;Om+upC0W50|Nkatp#Be=9qgr%?Lz+s?*qW8()=Ji@*+f9y zu8H)Vn)ZS0+sF5wvE-ehww|@D4fm-qw$2_ju8YV#AMR9HaL47rl6tI1;j-FhHo{r% zEh~?CiIg1|@F^G21RF==Q zY;msbAJf#w_qt7neKzcyvcUOoCzYIP@6-v^_{eAh-)SO$_^#Vqz!NcxA6-M82|Pr0 z#zpN~oRGf*fu(C#4wVcwTZ=V>{3VK-T zKBDc6oi4?@W`p8}ntgSD&^J?Bk>Lyeq)$bE>2LXHe4-k(j+}ReMplD;L}tQA0rJ z!jo+xXVl)g!xy^?>76`>1+k*@;BX&?vc*-^04yu>BQX=Q<96Md{rKy@byd#li7lSb>%0 z7}RE%RMrx(#UFmF!e+BM>R0==-OO|c|&PCvoRdM(T||taW(97DyhY-;@wkNo?y4^&o|@k zvjSrKqp;_d9WPGGHNQaZ3=g9VhCrnsv#vnwELsYRh5fu4%Z%2u8V~=fRGJve2BNSV z^{RdIGLx}`K~*559=^XQkAFI4w?5FsI!Nf*%>d?awbg!ObIl$S&pXj~jcs6r^Z|E# zD4fW)&|PvS|JCi-vG&%KFV8wjBhLvw@v$64Q-bQv9Pj)$dHzA{4ruTDlUGjsp-Q~a zd_LTSe#u??{mMFRmg8CErC8eN>!9j^Nwgw<|c0e0GQ9jZ9 z;@M4N?F;o+Y@bo>4W(-SG3Ex7vUR%wr`O!3EuKDWGsrxPOx*@0Fk1+`;vTfc97n)( zU)9!%9mTL~s^hD$_6b3q8r^kZZC~gGk=quNU8qL-{tDlAgJLi0`NqbL*}4xy1%b&A z92VAFD$&s8O;H)dG=*jUl;4VO6ejQU?J+`&C^MO za6vzYhK)+Ji7mnIwpY1dd*7OXa$f3T8B5C}L{6{S^FL0Xq}|j9Vwo`A-AKFzGNJ3K z`zC_0ilvMW7}umdup&l+rjO8}i}^Rh=3bsfAGnQm=;N5r^u#IT8TrR{G zHgdqh^WoB&r{4tyIK3YrwCDwK0T^uz{%m9N0$yh!O`H8NH4rP=pUk0-6g4YGarg@QK4pk6F`n2TI2C-H)rOY$F` z``u0w+>=P$=m&${B*9g};0c|G!^+CB zmH;2;_+F?bqP_8RI7ED>uwQ%q^4P~2-4ey=jx#HH_!!m>wcn`ZVvboI-hGI+OqTLCi=rdLn{HF4K?X8K; zwV$~M?Vs5r0!Pax0$XC#d-WbU%$aFc0f!ui_b`6$H{748_L#z#@;5&c$wpa7Ewzk7x^%10+EiAi)@QH3(jwhJ z7G=-yVC9J%uwHk(WA@yS!G)%(LHCSU<-=z!8}6xkOIT_RlOt0m!VY~nGo11 z_73|paJ5@QN%;7Wj!>4H+_#xW(lzzCGpk0z{mVrhFQWmZDG{2}sSOSjNxZQiN+BQl zGCUO{90pJB;_ieI6|$EQ{Q_{lwf61g4pr<3VDb=jcFwkS=Udmfq|H4UBsw1LgI&A- zVW-&Y!1;O3&g>lOhpSF7<&9`p`~ryu3ed;tM4N;8D15K}Vb#MAyFw<8>AGXl)=SyR z2v#Bw%;uCrLWb>`0*e^bK|kI&>YGf4V`|W z6~lBRpt;l7Ju!j|v6;5d|0TQ$>B7*>rC&{G@mo>bWmFap4u?cHy? zqLMXkPHVIJmA9+bRHpY!o>y=1C+=31%~L@%vUwD$}eJHr5JZ8e|}N{_aIrPB$Z2MFBvewsGxhF`SBQKJCvve1fj1sWD5dG-j1D3g zZpr(EV@osJ-5TqOs`%~XXn`fI2%KEYgRk16lSGkPzy0JJn>*&~MIdrz}WZ0u` zMEigTdXHQ_x-cuu*Ku^5ML3fBpe|?GHM2ot;KRCx_PamLACQ&CvEd))-9iucOh*Pj z!MY29IxTS30}XRlP_=JL!|gfB>rsjXe{3Uj{rk))8CCAR_JmfIX^XQI#Rt${V7DpK z69Gz0@UtG)lFs-dn3Wqj*T&aLT=>YflH&wqrlwKF?B~O{RqL@1d2E1)kBWowI`t;QrxkpSUV zOh713-&ECkTIp`Hv~SSEsJ2DmdnpL5tVEu|J&nUEBJF`R5df19&JE+T=P&~cOiDXo zVTDVyA_18cHrJRc%!8M!z-_1ecvtU;w!Z*kDxDl0(HPf>OC>$urVYN=GkiR?z^*{M zI^DMosz)t$=0no^@nXATV4f-&8vH5u`Refx+$C`KKD{X&v{0;Ut5#z^<&J6(Qq zfBjh}*Gs~edEwFU)M!QMG+`Hu5p8mdNxG1^rTimQ#3?s5+MVfxTn^pSDfcuRbN%6+d*>r%upFtjlqG+17L^ z;?#SbI>U`vf1(X^Ao3QpkNig|ee4PMXl2N}OE2YGFR&{Y&#eXHv0c5G6k$!gc4-WC z@9d`s=w6)hTALNv&}M5QO^+!H(3z)WKX|_^c)xm}E#2H&`~kM!I8#VsusqT&+a9peLq=9*PeCD@%^=Om9Sl-qN{&VBoyFs*IWv}rx&-uYsaTLE%m*BiWY z-pRlFWOepFBK+U+*#FzXP1<{qvzG6T_kzs&e);s-to(=uhiG{)Y6*_`{Ph{TsA_vl zS3(%$;Wbm0sq5_Lw^llxIRb?Gp1cucmM=bWT0|O%PF`9_g%;kR?rr*Lc|t@+xOq|+ zTMH(&l(&-^EvqdQt$?TAPL>#5By{X|#ERgzk~4@{=POzxE!L1L>McV{xp zIf#NZQX)qX&wNB0pKaZI9j(&Hy`)%MoFMvX~C8xwzT}AJu9*8#BUvEa6!C}@%cTB_0m0^++)q>CY8?Xg5uoF*V(*kMCMNImcZaM{=?kq z5%xWchk0 zFmc-|9~W^W_E@wIKC9w7ZrGfA7%OxV{GdBW?7zKcGB*vg;Yd^0QB;5;+ zrC4Vp&BoH_j0|I#{MsW@l|FMUxm7Kh!uo_o@v2Gnod~yRcEZ$c{G9c;rU$Mb(^n9Y zxKoCMoRp1%E84UZ4<|_7jVSivdPC7`kJ~M1>G2zT@20dB&?B#YM62Ts%Ibajt9lz} z`GZ)|EDP57@WB)@FcI&yl-(_#*vjr3t0F97PO~n|-!!fngv*SSei36nG#xhTAu4Q8 z&At{$F&g_3(NaA4iji5Qx09;y)z2<=xfOhAT8CcHQh=MZETRWP|1I_y7}3MOozk!+ zG7&a+a2V>|HW{d?wP<>pF(qf!I^K!E^|ACE{_^vHlQfuaGB7*c&rb&WUV6QPW>lJX zCFY2|d~gnb20PL(E88ItPYetq39o6NAXFybU--I)<0Co@2QO|jY~V|Igt(MmEU=*l z%}#Z5Ef;UzM-2H8xyngaTnA8?c*dt2v0vK(cc1CxBLi>Xe?X_->wHLcPm=M;=?mY3 z)v`&Qi^yHun@DwUQN!c3R3d*EdU7sK&&y8NAeEz#2C*q%;VW(0Bd;|%`UIZ2H6i*8 z{N`>DU+kN_g3<)fv3ic)_Qa~R4qAK;B*C+vkyibCMvK!8y8&h!c3J1|1yy#n=f%7` z(-duh5J=C5t=#?SS=-?)Bf&9LA~})Ynt-1nzAxdosEs%|lp$%ADPH=VSI6MfqeBE& z{-Os?&N6?HHYtmyK{dg{o~ByIqehSG#+%jDjJ1&O#%ifkwHj~we+)hR^t`ArUn2ZK z1I@mQjmYejiz&xtr&cx-F#YMwhK3*#n@tPG_b$-n_mD)o#)rNp*)sfUol@9Y z_w>#k;=PXbx9#op3oW;5e!@}LQo#^noiExJL?KfpP!xK(t%~V#fLVoLA!B@>$M>Kb zoS$`1-F)!{QyEu&wdQiGVzfmTe~HjRi@#mQn&FEv;R=JeAb;8x2WOr+cVhC2<{9zSD)6@5*bYtB zq3L`D50{%#m0*mYLREYT54>qIoNTJu?AWbUaQ!rh{*>sy+JlaIS*?o=PzRjMRWc1oe!JV~u7y}By3OcGC$ z#9L&}SpQrvqTSSuMWAN$2fKvO9blf8KXzpE$C5lR`ZO6hi=U3~FjY<7<(?u+VCT9x z930S7Yhg(QyIda}Pj)ZHzp3E;yvgvEu1W2g4O!*V*7Q7$u-`- z^CV@WqXbRS9;=$7-nJe!LviRVM}q(ZrLiIjOJNKp8SojeEnOm%I@vy*7aUkp2v-2F za7#HvW_5BfYnp1H*QDfmj7yH!scNa~%7s>=Z4wyX9B9s>x0kjze=WW<9OUmv-n)x8 zOzM>McN4jNc1(7cnc-ki7I$c{KDKcFamj-5bMCD&gO90o0YB$2{4v+d#3tfAA{PH9 z^sS3RSpNQt^gi#2tLrWHX3YxjwpBb*O;(Q@w-)-Ud+1@lmbSxzs(_q3_hE)MJ9v93HvhnssD?LB4$w4O!mvhj z;`^%-kH5&8U{+ zi;&s<(|HJ5J@vj3cHHv8YngoI;EK&9BFd~LuFAA3vLo9>(r|FGsGOt33K!ivlaNcCJZqZxGhNvg)88pEu6!`U{AoQ1nHtT9y&G;S{{74uRqp+&wC0GX zP$a0K*Svsgy#8CIM_P%9c$Pflt`kV4Wwp0?v{Ylp>ldbQN9se;2?Y~)dY^{viDl8l z68=`?WR0n+y1rl&wK-#{IV@v;ML_0ln-9YV?)1LWy61-HDRhlP+FjJdO5ft+ntI8a zu*2E5cI}aks|gRP2gi|qh&T+rrXC?^Z|Vi;4XtUM*@8qaLTev9Mf>QV4K03#>44#n zKywmzacfeCP6e9gr}1=POO$jY)jYb&IO}9fG5=7n%XnRjWEi;9%bfd=6al~dt5~ox z!Rs#e>VoyV$7n$o1e-aJQ>S)`#DR)K8rP}EwZ_F#w+0O1wq0IUwkIcYmDO#`LbEIS z)rHMd-+}Yj-Q>A;H*X8wUEwWKcHv^+3%GFNomXO?)hnCwlw7~iS0h~dV(xH;@U@=u z%Ic5-#UF=Jdl#CYg0#4<^lMW{0ZrLDG_5`ujAITN$7SE_P{(4JAPaS{L}ZjG8Pdt1 z;wCBr;p9_GoAMKbqQmQ4{M{HoC}99};knV>Mg~;u@ej?zbMEX5=(LeO4N+ma<|03y z$cR1d+VINtYk41>b@%LB`Mf{r-^2NSQQNe0y;+vOI&*h%P0C?tH+Uv;66NV<(P+iq z+Y8s=a^psgCz|^kD<(YrGJ$1oGUI%M&joMGcCWM%%}~8)qoobwPMSofE-u~7D`GOH z47SJ9>Qj?W#PtujjaA5d+1b<#NSz8>mVvzVOLNX-;D+#Nu%0@5po!@GCd*Kl>vu*W%1sz({B_86W;hSgef@uSAb?Hg(sXZqZ^>YGg<4<$H^aUw9TE zHD1)YeT?M*PI~F_q$GA@I?=^NEIEsNY`>%0uFR~frT6_+Gh5C} zOL5qfHrUO}0sPljc-U?XU;R~Vco%AcD=&DA8zh*Ms?DjD)2cIm+DUOitB}Q>L&4SC zmKE=)T)xFp@yN-fW}Vw9@{>#dx0So?GfXe|GJCN;ne|Bz)~$QSS66B#%6Tf( zph0YQI^0jnX5BUnt8C0(nBo1yQI0YRRj?RBG&^15uE*tSRh?e@y4*-@$i%5d(#pE} zWQ|R+U-P4ymSgLaRi?(lKZP)R(W5fjDj)_MUUjp}FG{R!RDKu33VPXt{#faS)MZ)E z&XQ>hd~sejE(p|DZlrhXrmh{$&bwmh;D3ZCfTdS8qY0@l1y7 zl~#Jqu~F+gQXZ0{eTR0P>|P8T`v#1rkfSZL#q^B z*bZqdYvEl*Ke_;P3h7`O$fiHnnBshK6u9!3WoD-_>GvD?+s=Dn<{AyCp`T?;r4w34g{hqryV0*(onfpV$GwyS9fM_ym|UbK0Oxs@#kUzT7EBjx>JH^0Yym;BleOfI^4@p zcEW6U?f6Dt66^(jq%W!~`44n_KhJDcYIFK$=iZ+Y>8N12()+WRD~5vneXqahRn2|g z^9=2n+qk~W)Fu0By8M!JPYXh2%Y1sWdHc( zbZ5W+1OFVfzhTMuF@a)({gYICy8A0QdAUY5i*8&~G$G2k%?Q81==r$q`b4hUP^EYJ z+vCaB*Qyybzt`&9*YbznL3MY{hx8lHmiM#}W@OTn-Jh0cC36L5F$$zr8QB(8-C2BE z(3TI!nstdPfXPYaQn#$Dm4*G>#zHiC_A(C(?t_Y0WHRZ@*ur4s<=ab{{{6B1h0u)6 z)!fMNwUjkX%ev>WZ|j>{BY!NM4V`Y>iEmBW*$U)Ss@*LZ!;M>ww`5AKNM|?cL3-gd# zeMO3b`Skf-Ynm}q4(l@=GLNsf$cQlwB)W+D+SnE;pzCb;u)#_jRmN3H5)5~0EsMN| z`EO%8S)--#n6zjif(b6aZ-foZhg@;EHdkZ3(#DG*s;~QQElY3?@CA;$qIw4Hb#@dt zt*j-jKVI+E{GsL0-)VAJAT@7x_=1VEvvln+7~Sx8Py3Uf+ zWO+ib$5qa?5e~Q7act}oU%a1ScXtSZE`xjUtdvk;V zt!D-nFCl1MVh86b2E9)E0DDOHh40R=C;jaR8OLVRc_=Hzth!8 zy*+m%)ZY9sD7y1Jwo2{IW3{(~NmuV`I94iB%dx!j{Pn$~J^rn~{?wqUZNEjaug6zY zb=@p!HoKQjVaoyoq3LnTh&?M%7C%o0mR@%CGj6Bbw_m<_pg`0BmltiGjz)`kEg_i) zml}n^GriCXE`uxS@unsy;#pDk9w#s0yc%CcM zQ!Z%&H&T4yxoy8WeF|EWTdG4M=@GumA&a}igW+jta%;j8gWVmMWg*O@%x&4~Qkk*q z9bd7ayX?qS2F|Ntgf{QHUU)UXI|Q4~RvAqK8f9C|S>}ZHYhKovPMA~^_J*RTrsJej zpn%2Hu_kb#%%21F!C}%g&)_x|C_z8@_|RVbPu=NgLsX@!f2*5#D?z1E?z#7U7gR)h zS8`ZXAsoK(W6(#GKZDivlSEk|LFRkWB}t<%yK9DQ`PdC?S!Q3yth|s@CcRB29kwF* z;iLD>e!6)l>U^9MT%Kbk4cNbbH)E?PT9mT07`eJzk@)VE!xirfG<=uF9IqkR(h?=wb*yr!jm=%7pOkm((9(sy}k$Kv(k>8D1IX5+G|j_2OV z_4Y`jioaE`U#Guh$TnJ|yz9zeC4##;h0f5mHb7#6$ZwIlL0IK5) z4Z*4deJFg~*a-Ia#L`V5{o(2E3>`46d%`?BeyY0WT?97jb;~xVzpBMX`$BSi*{g*7?}e z8N&SaKAJL<(YB~M0}*E3EFMO4w&W^?u!$&aUy{hCM-uw$~bsYJQCyzZclhKs2ew zcswij+B0Y`MGne@ebc8l<-d4vffTEiQ6H_ zhj-l1szki;@!sPvt9cI>o~rhw1Zk^1%f@wOU^r!B*`lops=d`=?g_DcU<@uu2sJN^ zuTL-!C;0aZ3$+mfD*6|0wgfo%X~f@LZ3>cOXT||uf;ClYtx2Cfp5FwyrVO;k31=KWhdsu z*s5jI6IVmaBP9N6|{U;2SXV@70TKv#5oe8y*Hooe*%E4St ziTnz==XGqVdDKse$7n6d1JS3Zp}p*YWN0Kwk)|i z;q`aFI}6jby;8Xwm+S3wop!ve>G_nZ?L#WmNo&fI{jc_m0^M_QYEg-j%O3ARr17B@#vHZL1VTAtJp-x*`ziTM-aK5fD;P z>GDJ&JVFxB+V|d>JLkOn&zv{T=wMh`>v`7tmCyIPx{-Hp5Pl$2vgn9t%_-uox@HKM z5{?y|pTW?GHqZ@FHik0Kj@QCbAOJ$m$c?S&EK{_O#C#ujYaCgw){ojc4KkQXY7?Wmb~TK4`z?*@cU* zHPj1oAa32BIo-ZG!W@((B5x*WcDyL*{VR;8ZKzmsnK?6vRwmTmbFE6@Rj%+uyO7G3 z!wkXYSq3g`=Fr=^VNFt?JW8bup6YB9gT_C!@*r?LJmuhWkEB*v66^>rZmh5A@l=8s?o1-WdqW57DPoyq^*#_oOGM>WA_5ts%%bD()wDOVZe(q>Ccxsub7m6P9tEyUWTr0{75lBGrBN z*w9>&T8~4AkBZ5og4MDf+vDi-Y*M^-ixP`2O*pd|5E4pMWqCz)%4QofV_Un&?v+n3 z7|HGv>`S%xcfjfK3q7p7}WK(*TB$@u56;c1LtdkSaCqk3FwI7Ly5LdHk zXNE@~+p%rmenTmsvsyGaGrvFejry%jdU&ahuiwQC@RrIq+up68MIb<^Z= zU8;K2fM-Zz{g*xkp-4*L#RjeVt{$a&DBGow1WZl4tIdi>4 zh=AitfZ-wFBG65egjdQ*pgA7u^qQ^updnOT>;}{I-g=+Aqa}Q=bbiA;-1>94XX}DL zgnM^%CylH6;z1Br8t)Q(r_9F=(VylmfG(6`QM>PF<;|~6J%NTiHK5LN`1Rex%tmaB{md)6_q`Vfo;DpQ4eFnF$XXVQ0s!QJXOv(?@0r|b6x+A z!%ZR>Y1tKoUZzV)uyCq%myFD-WV#gB;7z8k$}~Bte(UTVnHjsKK-OEYLo7*ABZYMG zI#-E2sPZx2wsNwMT^qaK$#Wy{{ac5wqG6}P;b$Hn9_J9plF~cEHbl)5uHViKaK!5O z<)h9DTSl$>O80y2p7~)gi=3v^dFxMx+2Toa>4R*I!`sacmsyh>P-W z`~EsC&D+5v;l@gSj)Qt0;(PQKslAE(^Pm1uet#1yln)j{RD;1i3sgF{k(?79e*_ku z+i!+Gp#O9N+(*q>J@CLDx7diq#gd2pKOmdP+$ZBJxge0u-})PP;JjeR3Xl%cAu9^N z9^){KVjL8LO={aY(_7+Bw1YNQ#MyPd7y%+KI)Mr$6CH*$iKa&%gB5goFBr$g?V8?r z(BdqSmAs|u_Sk-3-<{&mEdrAK4l-m%mm>lZRg3|6imj-O=j8gw_-3OOuPtf*2P6`u zRuZVJ9(b5-k%FyMCeyL08zbDN#Gg_=33a{Q`I6ohjbvwc_3?JEyj1USTV8hy)-Ftp zJCb(vjFT>2fGY=vb0m7=5AfU;@WaL8q|t|ouHfIU@JId#G^zGdlF1TWGe9~{3bxaC z>tj>UicQe!=e30*C@Z%b&N=nwYK!?D7GV1J5$^Xsnp$ZdtBy$3jZpJX`!Kq1#}XMn zic3OyVz~Pyi@Dc%PyYy9B25NyFaT2w#0V2;ok(&1e&T5u!$R&PoH8FFG2QUWM@Oo! z@cU?Ap|kNX>ENu$$^<1Uce6V|GUW2bjmbN)4V5PGpP%el_#-f}`&0Bg3;?QD93knB z0J<181hJja)XSHMtpTE+5>}NVB3(w$^WfB8eX1mUgGG@jP#WX=nRdalkm^KgC$&jIEpd@yE;F{W*`1K%vGr% z%CL8Jo=wsMb>-rWgs!Gt0vJ0g<1M|OH_nv`Wz2EKT{$?|o#+pqxR8EN<|;U~#TUj9 zZ?5W~?ERVaI0cle?h9iZ!7gtD_{sS8bJQC~u_1Eh2KDd%LQa+3BP!(+O(5+3HYDsl z1gJz&ShiyZ2}Q);bcIw`O9PPgo1tiLWS=h-!AIA{VwJa7DmPM&%ga| zYpyG#{t+0}V@upMqB6egr16blM*w(T<2`FrBAl{s8@`GOKuQqzL&$XPO4Xd{2E9B4%JTGl`{enKbu$jw+GDWM?cRdCdw?-M z#zri+L)I)Et}1RP3Qf;wvPfRc zq;1ueHsxCDWBM|$Jl&Mkkb>pWLi5D#Pa5zX1M5A^%b zuvcFW176^dfNT|7X<@QH#GRRJKaP@S*%+N?@A`5^Eh3O>Q1CvY+1g;wwqF#kE)5z6bXkJxE{u}F8uTNTv3tCJi!Mq?@5z%LS*=9rg%(W=QV6PDd3zdLK1F<9Ia?CAyC5pq zbfmHGa@GsvICZ&`E#eF}Z5i+fNzxIFifZC{_#jl!yE**wR)7IX4wgL4-diYs+^}RS zLY^ZaAd(_*{Me44zWlvSiFngRtJ&O)^WlCCg~|H zx`B~wDzn4O1CHw2xG5QQTdVbh_l!!HLNKq=QEU3&hv)Ka82jm5#X88E@dkXH8^4<> z1F&{Gq84bzc9O-vAtke!8xOKs@=y8d#IwT+SJ>tAODxx?^>HTF=Qp373;!7SE%k|8 zz>eR2ynDb}4F?vhiNciyLL*BCd2;*kMmKq98-g1GZ?P-7$ihcP))g@#L@Rc5HoJMN zLwQF%J2f-*TmGniDB?YSHyndSJ#ri;j-ajwiMi~npLc76vV{44r% zbNVvi?cQPPS~t@42L%bri=p?u_YI*0URkq|-DqjpuZoc52X)8gQa$dsE5!02`c|GZ zUrCIz%ztCim>wVqWSZ@*B*=#Td9jl(bO+L44W^<6h=*W+jd7Acam{DR3CddVLxzqp zVZ%P_!(4aXfgybzp}@z4$F`%7&q4e|=86WkDOkr=rUqM%&L1 z<_m$El;An86%Wu<{te_#WZmG35Mzo~O4NV#CXCnulLxYOXfE5+sA6p27Gd;^_?~TqRE^l3~=Hs)jlZ4R*Il!KW$+ zvz<5kP%hD`pHPBO5U9|Au2JDwZTq!J7it7E%0|OENzM0G{Z_@3(N8tE1-5qZ_ZIO| zFn*Lc5`;~U!^r_;Nf35~Dthy_@m{nDF>-Y-z(EWhQCNXB*N)bPn$jfQpq^rsUU6X9 zVoqS|P`HiN2d&c~ZS9d4z5D(R>TAl_vG$BF1e9nyYj_D$e4(H%G{Lu*R7dW>B`6={ z<&*peg@G<;7MV`;W27Sp&wAU`*{RZqJJO9r@ zqmhfZ%gegyfo;D*Q)Bln+@eY70}|sUZtu9=+t<)|Ecygd^X;&vf9}_$N&c^M@Vys! z*P))?OI~;nQf&CK)nWnDUQR^yPhyeqHeUWGLAEa#A6WOyY{0isFza*djXwgeO`TQ( zzu$67dD(3W(Vt1s&CEHFsifp+daJ^oq?cT6c1bEqHR8z7XF`)L9Iuj9n5ZE9N@ z-LAMRy*o*tdd$fJWH}BtaAlOFR7UV@G4e!jULMJh8pA)t){JbzP0|RF-H+hN&S=d? z!Db(gj*>uVVKJf~Z0q_sqRXWJi@W0qX@SiR{vIDNwmx1;eLAetwET;OQ;Hs-@8+u$ z=$sVzhHTerlD8I|3<1`l&3Me9gk8DObQ>4HLgAh+UW`;Y8_%TfeP1*`reK?CklSM9 z5~)xK9`vATuW4h!uR2_DDkmDkEK|TcpdLLxv|mAEZLv2Al%AKj|=n8)!t*E7ILMQO-bfzK%9Y%cT^E={q8qO z!8-P>Y<^)=iS6sxc8IkuI280fF?)#H7=y7?u@<9Z0_GhOK{O~JVA^M7hhE4`UWsK+ zZ|_kuvZ7TTl0T)jqBZN1vwG zfGG9ETpXP6@20phLXPM?yL=s;@rPlZ6S< zgI$|9O9J!Vzu#Y5)|GP(;{0AX5kp|G~3J`3VlF{J_6$8eN_Kx zSsV8HVBA_@i+|C4PIg*wNQ9W|dG_B~I#mFgU*3px@CwX|{N!#sZlruwYtH)hjvrv> zI$W~M-ok*0Y3DreK3b&{(qnwZh*L;cbmAh%+c5FW^q5QRs&AYI#*<2eXHyiZ-u&V> zVYezKyk{oyO%C*y&D4f(BSw#`l|J?z6a4j_Nr^%3@&nJn>lQyK4SSz1f@J=U6#n`; zo_U{$g6_e$#5$sTy`@ISzU%BF`KF6mTsH_jGHTNin5;YUE!)mN=*{J)?=apZ1`8=S z8QnvNELKxy_Vb=|b(!S*jW}82Ee7_%?bN1D9Yi2nu%><}`f$*0obuoBaplQlhumCt?I9o-59{a~mf(-zEjE zP?9twPATnuU%gg^a~mn5X;uAbu+H@=pyqv_9PXLg~IkGV{NPg%2i+mUxS zRmh!#v1n~Lh_&^Mx}6Y=scq5o3j(S*dOKC#Ke8Ku9eZzsWLrtkHaocSnJI5Hwf1NxDu?(&`Cc`@pDMh*DykZb!;aTiScfKi zPI%loDM50tSWe4h#zm(eZ}w3 z@O-g*Fa^DndxAZe9Lx=4-4O@Bo=U|>_^tO+h{ zd1d{U`wOkoF7iZ9Vl&OFN^VES2-S~tU!f$yvFq2oN4-Y{{Fr5tuzot2mAH7HS7;b~T^ zNA!b251$>45D~e%m&&3quR>({fEU{(PfJ+#BY~iB-=yn#I_`@vB#> zBE}m{8j6ze*My4dnp)B(nMa#r2!(M!zZ(hM%!UM+-l>rAH7y(4Qzd6n zf#;iNcVXn`(JIhjg2Ck`qqfs^Z-$6o6!)q5bj5(N?Tc!fS1)D_jIO-HBvXMzom;5E zN&h48YA^<~?n+Rm)(}B(^$2+ocyzlB^4xt9OoS};D=Q4iyYr5;Sg}1ieBYh}KM^}` zr;q=Rj@a=3-aFoGu@Y6W(Bv)7j-_|##NJ-*85AHyordW5+l&_9&pNVF4H0|hp#Wvk zaRa1?2cdSHIhWAl#aL0GVQW62EUF0Ji!9xuz(|t}liY79vLe3>2v;)Cv0l}D{H1=< z*02>1x8yx;uQaeoDqp%ZPwVOv(Yzz7tzKDT0^c@{m0eLk`@HOoLrPmuyxeG!=Ft?q^%phbqkL61}M7(Ff5jlacA+{hsDyX=ThHlB_2zd!aerYHVG^ zT5uGFG0n-2nX|7M`LR;x;Z z@zZJ3f1_M(D3*j6?XXtT+*v+QsOt0j1uBgaW0LJhjn#Bb$V%lLS)i1QKb~G;8&6uk*Z_K zJ~FAkq!3LG6G(Gut*LFj-_-*(?8kqcZy7w%$n%(;Zs=#uD$a9c(YG6Jb5=0UpB4HYYv&v$bp6|cK zq^Iey#@N~=rE(2&v%x`DXx$OatFMh|Gu`Muf6kW1M_d18%0E6DqdjA}SI=af(*JM+ zpQN3wsVk2eji%VBHK?S4UDY*ISy-+hV|)i22YIx%Bu`QG_^ zo}%gI+!%hO;qTT~o$3*%{kH;pA1Mz@2diQ4!%=L{E~@AnDbd3s?!j?t7ky7)G-4#e z2b~{O5#Tb3Nkgg4E@?S-Zrqg3>KS)QcZ1@dNeo`ozLYlR`FOwe^@DlZ%QwB^KdLj- zStrSyXzgn4Tn)!e_2G*9-533BTkwwA20DI8r7N<#zZOK9Tj??T>WH?kW_ks2_E^34 z;r5ZL+YhVaQ};(p9*@0%S{OiOW?q%nFH+IZSMv}iK29$o6mSaJp-JN`{qa0W$SbXk z9png~W?>RcsvrB8yIWJ>UZ(Vqiqh`c9hY;+oQSl#*@_j9Svz)z+mu)hx;Jf%F%xo{#R=4ipZbktI(=Sq`^57U$o;@%Xt~MiQIQ@Z=5ixYOeI(d$=jeO)rtX@ryztVx56-s8y+xh9`CGmBjJm$jcM^2TT6 zTRA?3C>c|HnS(&Gzl9W_JlVLIEjUVZWr$pcXs!l9p_@J z2)>;z3Tf1$fbqP8*=9p4p$44ci=KV4X-oFjZA65qUAty&M&15i`TOt7etvtR>DW>3 zb~{}RrPG+}VeH|S^sD4dk(*sAemEA1|8i%Zc5VLS>t0}f7c^Ru=|>QazT^1J@Z<4^ zJtnVnaGr4n%!Q$@OG`f2MiyDeKII*CI(DX5W!Nf2-C1f5PTukf!t4Vj`II3_2QE%o zhK*PeLdJcEsk2&A@Yn{9eU!o^%gih5)sKWXumzLW@91=7WqS-fP-%ns_k02iJ{kzI zw`--P<>22z-TTGPgML-u72}+flGU}9TA}4<)0DFgi+Ku}L@0?Gm^^uUHZ$l<1O8F@ z8|iPWV;stfS@nrj)B@?UAK?O4++y#q6hCD3pm<_cTsM>bs=|^5T80;yCZD55I++V+ zQd$dJNAl0;8=KzX{KLLU1WFXo_?Xq#ri9XShjZ&v)^HJ zuCqeBK;>g()-U+Vhf`){7x2gTlr;Z(nfcL1)^C5jh}o`37kxeVEqpbuMC%9{E^Jvf z93NrpGxwX9{e1nw`01M{70Vgj#$g8rrR(*_vp!w&DF9S-O@IiMGTP65T_@~Hk}KjW z9NdCNuRA7pw4WZ0I2>rZP+xc(m+(j6s+M)H7F!fg6+_Fzo?Rp*lq_=)6iVdyYJoSS z4aQK`G}@0v(Jy^CD}X3uAQ@Oi(f&C0UHBK_skAZI!jpF%E%f9Hc*5<^I?GQ+?tlAu{7sgky!Qjv$V+B?#XXMm$uD=Fx4z79 zQc(?>$a5;Y#@|Kjw)?1jOFcr?&^_ft%@a}d(dV=WqvxIX_f0u`b>KV<|Kc|&S1~!i zmK-{TbgT75in(*uAqC=*TG$@gZ;+sJ?FdRj_Lv@w=>g>{_V;Gbo(*s9$Gv0C&j=Jt znt{k}TEmpq(T>TX`hUYoue|BzVV&BtMQGyRzchpTRi83%gsvD5zdIAweGzIn?&Q#6 zA^ggrWF6n&g9$tv;ind5>3k#B2w}C~--BG&X{P>8^JSi6%6n3+pXZ4(TQ}6r6LOmy zy;w!f4ry=gO!Zgc6|{mq?ZZyaC!HMUh^Mz)2?ZVvK=J}1J6DD0HMUHbkaaGPUl6X- zaPinreB|N`3Pj3xwW}_VL`+-pH@e0qtCwyWq`Gj{7VWLWqROVeEb5u|hnmGfBUj1? z71O%LZ++7~Sb-B)-y2ag@(O4F^-+mc#=*Go6&*h-aZd1AsUn52Zzq@7A?o#hR(U(L zJ3ueZHRxKuCR#x*);7+|^Em(0_xojuxt6Q~7j{JXhv-o&#|l~J=J-=gG5}v@WZ1IH z(=1Od$6$vTE@w}C8+V=u}rx_5E-nJo$AmsV$R--Da{>boeonhK~B1z2WPRD$n z!#LiD6REEIEx5-n=^;54E;jrv@ng|T6Z{{#lNV^F^U=fJVvuV)c|v<&in-A-n3QJ# zgL0fmXt%v;GP0W*0*B6OInF9mSV%-;fgh7D9w4K9WSDZ*cZg^f%%n(mRxV(*DjlyT zX-qqet@iNWbzk}wqFRQ!>6gXSLoT=IZBj-r=)zXFY}BQB&zG3xg9sPNhyZKx_)L?= zjI8S88R+}GEA1C+y~RuRQ`P%x-rzWGTAH3cQRkcFT~AgecAEUtdFl8XTff}VDNMZc zQYK!j8|_r$Ax-rCn^)1M7T^G@Z%XFl6~^zksS({^N?65`2QlXvq1O$bD4f5I?6N`h zD1>Kg6fb(qkJnczL@BnSCnrDL3SMt@F>Kj6VM?j+j6X3s)J=Ot5jJjltJNp1ZXj(C zTaYnY<+;OC$o}+5Nn(LRrN4at0BR(#K{`-bOIfQw03~L=tnS)=?rOhd!TDw&0~BauEl6&l{Qg{iJN$AMq6%Be4Fd| zP669RNpeLW8r3eTm6?o~A%o;KA^Y({PH=`$_+|jDnvpc3KjigCAiz>z&%8eH%4Y>< z)&1V82|-S|#yxo^)wpbRDMnGhgY@y&a^;z-s?v^mC$2ZJ7>^w~B7SNPG#Fb8#xhe( zk2yWA$R7Im%l(r2dzxJdK>K>Gmskne&^R}eVkdJ-zZQw{kSwE^HnCn7RS>rQWeL1A zNf2$eJ)7&^?q+W^ttl9`sG`3ULF z(8lUe)rzH=m_4=)jss$mz9)^PwdCzgqB9(Hi`OjrutAGQfa+B0{Y=_t;(w2 z_n1q?*`kvj=a)h-SF0kOT1JBnRv*ws4Rp2zk#-VQAQ4A6=<gfE3n+3;uj@vI~E4WseyrN|IMO@#UP?n5e^!taf zj7BaRi-wNBvXw5KJ)BbFo$9w2pVX3d=y>#u8zlvo%tFW)k15ik02)89|O^&Q>X9*L(9uo*&+G z*r-Q(E-}|?!o}Tjm+Qg&&dlqtHT^`7)PB)EB$uKdYvo3*a_F+Fuu3WLHW(>hm?AUAq9y_kZJa*z)&A`J-8l>(N1h?qQiHm5{2^@o8cGS1`sLsqU(xLf#XRy|Z` zGL~CNcUHURTXh3jD4ZOtEq@9REC_T0BXUnOR98=Y`U&g(h}pb1m2in=WBsW9s;C!H zc66;z%gnvzXUNN8RrWS^RhLWS&kMaAe;^M(wDV)f#A)HS^syVaN4?Z1OcmU2*XEi$ zN%%feZYxzn_)r;e$3fe{p7?=W@3GY~ja6fwP|$b2E&t!&; z{_b1G_4uThuOjf505WKmqN~p~_83vZJD*&%rl9F9;wQY2*D+g2ho(mnDKcqW{9 z-lX60{bbK!Q#aJ#H6wwHy8Tw)C@8H9N7dsF+EHxIWF1DhG@NseRd)z>ig~(*=~zv7 z$x{a@`Krv)B*?w3si5*L7kxA^8@G>pihZLy+7z|~mM%>CgO*d$lzZ`=xfaY+&bjcdQ+ zmh)uUIa1SqeBp}J60O9+Dpz5xCakl2pz5kC`l$E@;^l{5@FnP0kDfU0NM6|UE8!(i zm~|=MJ{dlNey>p&?OlEi*WWP92QvIz8iZjrR*9T_{)1~BqmMW>KQ*XnxZV6c#H(X1 zj?fOK(F3A2;sMd8kWj(IbcqFZQDv*NTMViu=S>cd9vEIe#14%wWMl4y{}E8|3^JM# zuV)WrZBFy=Yvodt3R{!D|H4viPo52W_D%oQ#EgH^3$~lb>#KdcEW|PgjFV1OxlWio z&QfUZ^!0y`dF)bAOlfIH?X%8fuRHu7qPA52t^4q~8u{Tk;R%DCQFF`I$5Z?_G3qN;Y3kUw`Ti9#1LI4f`D&C zI6@W<>T~+_8OlYXxMeOSf)*;RftmQ6DFtrPZ^MMW}X|6D6#28G;)j}Fy|fZDiKStXU_7~SUa`~n`0GvQW3$Ic3oM792ad&Bx)0^54d*;qu#y1G z_Yn1Je^kc}qE0Pj(UBg9mVn|ra&fHU`~ujdza_YIDyR{sfKu^cP!l{tlThZBgvO0- z*Wtw+mBgf8&ziCDX`K130x#|FGa`z{hkmLaz(jd%X53-+ZXT-p1^aT}FPt z@f+LgSx#_An?kYWx@d()yU%q^7vIE=i)EEw9-v9s5HQ?1yD%GcN!K7uF4jBK2obmv zIP&1itgT&&+A%DXJ~@UES;!L~TBF|u_pMad(__h!2Dl{h?yKvq)~vHtq0m&9$sG5v zB>taUCkVrG#2daxiq$?_>jrKZJAJI(bPowu z*Z;X0iae7(cG%W+vw(1N3+0S=+$7gJ#%tzq63FTl@bypo*6SE?EgawOJ9fzFdyYdm{YW&99hsai$t8ue&ED?jh&IfkUQ=1Udq33ud;CTd*1sJ5b5Z1FdB0$%DG zlu^}x%&tVi{`xojbiEYE%tx-De_y?Fl<>t{N8bLdfi6%gmOqUBeHdl#KK9(_<2P+* zwFc!Y(@i6rt{lNX0*|M78JOi(qA_s8jT>&+3)1^X7Kx-~Ymh9{B+$=51N67Hu{FQ< zcPPK`Z{TN8&R4;F`Ukl4(*TV8KXYlm)4PQ@y`@hyhie!jn5psE(eW6}mDLy*qsSb= zyTD~8l%+=W(XL@x4+2TC;^_t`E54$aS+&DsoT6hesEq$*P@G>jWhs;CqtPEB{r%zW z9QH&fQx|dYo%-)ALrdG{Tm^i(ih-qT>EeL$SfN^J#{*aWORH+ve>Px9@h zHoPa2xG<(Nu^)x z0g~Pe5=b{J-wpYH{3m2b$x2xSgmVBMXQoTkI%-023l?(i#)wByj?%6W=BhY%y zu56E$dak8yj;+Qy`jzxi5^VU3?;sEEW148>0+YfKDl?WyNudA4n(=$wa6G*v^ms?; zZ*^c5^|=6$M`dz{w>l^tgA01!b*B8y$50kq^3mhU$`I#qSQ7wS8-z$SFldy)(Cs$nuP`b@0lv(kV1uGX}F%%L(-%e=d>Q(_P z(=bvZaAPu#O!zf@H<|;+Y>HukhDU?N%F@CkkK9EEkXsw}rdo30bsw8k{1v~|nV1%IvK~lSjKJ_zj5+%p#aJ6P} zlTnPpZt^~&%cmA>(U%2}ANN7flGYqjp`5yxT7=j2qz3};MXm{7w5GXsM2q>B)}STy z;!Wa}*wMR~JRSHz)3W83FJYW7Y_??>L~bZ~r;|e7+qR$7j^b}8T78N}F5x8G#m)>>~{Pshj3`|L&);9stet|4Z6qz+uHDgzLi?JI@V%JK;5xW-K z4m5x|V&|%)c8ykXZcy>js#1btS98`gRzu?I)lhxa=A#D?^%%wsuqtm_(E;r$u()$z zNw%&9)B%sYC%jh?K?@9+RGLg^v0!3^m)f*i;Crlo&+ajq;c2t{plCg|-@x-RZy6&U z60=pLGwb+bn{9sSg+Pt~@*zenTA9=ioX#%eU`;Ytipm!j0#@POVAl1CnowvjHhB`; zg%H5!Ad*nx6~u7ZlKK+1ssg1AEf`*eW%S0B`qWk4Pu++xG=!Ymgq+)mA^ zW(sIUPtm1Gj2BcWEV4UvVu|Ad2X6^;@w~?v|Iqko6j}o=m>o=M#w4t0hF7_44%-Mi zfslcFx{~18(JW;N%&?{jG4u}f?jcz>wobzO^d)yy-OjHTC3i_+?+51b`CJ1C856x9 zz9iEG(eI)TGpRe_+tdI0-#3G*Q~mM-kG~bzq3c`y>pw z+ntYjwJk!R;UBFD$IqtkgDu|e3s4q=jN%Q_WnjvMyzyC`&O2uom5`m2yYliY7OzhQ zav~#Xy?*|uI{kxg+&JKNG4s=Q3U{CNJLOX_4t(NM-b0iKTc;h+m65Y|1UuW%$zD)o zTFc2wf_1ltW)n`PC5jXzQ^j0~9_VzT6~3uyZfv=yqjey#O8da;R1f)wpLhIJQzh+qg_RA-g21k$ z3|oR8C>&v}S3sW;$u(A-7n$0jewTC>m}w&?kbm=lY}YqEom^Y|rrhEMsbdQId(-7U z%kpASDOYdS^w4HsSM(mX=op*N3GzDJor6xlMcS$2_l7px+-UZ+bjNQaULqAxx{w1B zKD5z(nHNLFhr&k%!I_(Gh!Zv#l=L7X;K$G%gQY={MLu%eUj^0I7U@H~PJOy4^D|FEX{&w2BVmboPE^f_-UEk3~uOi~~16!~`MN53O zv54f>Tl!O{6pbgBw*yJB0N(_xQu{c4SM*6Yt_)mv3&PsMegzYFq+Bq1Kio0P437Ou zs>5~VDqCCA6OPXhGEaGCj8e4pn_}*Gsd$}ToP3{gVNmM)HV%@jBo6__Y!3xKdWFmY z#dp!@gG3MZZQyJxY!DAofNc@{xHs>J*P-6i@IC1t<&((R`hHhOFOvb!3b!AW)Y8VL zsAu6h&VdCk0=yewSGr9gc|#?@uV0?v+ie2+P810^isKE-zFgufkFQPjD_;1to~y^C z#A~i=sk#kE$OTi@`mxxzah{$_B2xtID(y>;gji`#;dY3*Q+zRJScpByh^2F-yuceR z3)eugL{GLO^Ug-BA}hWVB%1=j?*2Vr^G@w*XK(jy!$$@co~Q3z3JKAt-H<|FZTR&R z1xMZ`d1q|n#**4Kz_uI?VY&NHgI&}fu9?NefU}Z#7^>^xkoU{BgUA329!mv55MIj~ z7O^XsGX*6&v0T6M`LR5u@qRuzFoJYhzshQP+I{N6h4$}eqK^*jV89~mkEHg=oRVH& zAmVrj9{{ON@KwjzgK)&`HmGdXVmYD!zEe5QLUJ(NLt5bm^Yp(BXI=HgypT4>bfh}} zLcRd7?Dh1HKLUv)ISeC%F2zMa*`V7TeGD}4Sn2QHPU-9c@|!H`Y^api!~$7j?NqF8fx_Po{U0 zQyrrF$iks58ZWu0ha+@E7M11TH_-Q48#XRpA0xBi(NNV`##|pUFjeJ?_g|#DtBgGk zthnH9TG86v^$wXqhiu8PdM{VXl`r%Q0>*+67Lh`QEOC=1vliXt`_~uwpvYQIICYk- zoE%sYU}Vz$?MLEE!ed9}-E5=y2(iOwUXN&NuF;iYU+)op00oM67A3w&T&Xq)0#pxQ zXxo-S^vnzZ^I|smBbfc9tA7OU@l~ME1im(RJA8k3RRU;W^Ay?2MbMefE|OFOH3qFa z#8)KvvTo5xOo_1w+|Kz2u*s@z%^2m!EEUS4$mPNA{Wsq!&2goCIVj$1DkBe|v;YX- z6~}vk-h-#t&vgza-VT}oM%24n4hNIU2(<%q4$$|fOp>CfWqv?ob9|yU_Y|0D#60@= zP>VgYAL&i*;Gd+a^65ighx{5mAq43YiNVB8(q~;tUJztC;G~Yxj{a-HCFo}OvFt{17mHRF|krL~x{Z_Fa@qm>9OQap7pUlxvc_7Z?WV8Dta7g#fK zdl%8Cnu8LEzNOPkF7JUOiQ(<8$*1vr2^$lZ0OB4dWDqv$wdZA#F0M{w&jJ(nO{MFz zoK@ZfG8Rr`4a#ycG>Siauy^bCx+h7J(C0c-Uvcbm>nO^Mff3ihW5l>8mFnxfxR}OK zq9|vw=-Yb6iMu4Qn84+DvaXlZh+Hm!ez69-srNDBd;#v2q;bA@C@9>Cwy6+yfCv>4 zx<*0H-{cG}DNy3VSLGH=Y&b;>vP9wwwqRU@;^pQz5v{-Hn<8y07ECe%wzZSmeM#S> z!B#rQ%b|CZd1piBcl-v4a*QU}4$Voy82BiCS)&B+5Zw;$NxP~ufnW{(XHljF2hFbD za~%7gO$LS?F=<#Cjmxj@3p0FN zByi^qI1o#C*}xDK10O?hrJH(?kUL`PFe5Y(;@bU>jyDw!1kymc%rHvSY)Q&WMbH5XqMPL?>D8e9dehcflO+(fbo0w_nm2&RJ6exAgrq>G@1_(PkrKz=rx z-Hx4j#%SuQVIt$HA_o6y`Leso_2e#$1lO82_NxmiYmgo{qX>O%Zj6P-J6+Ji!(83K zJ7JfDeMVaT8aJHGstCPvZPQI~?H723x_SRZtMeyGkhM-SX;+(QfF2>6)%Fb0iA@03 z7gw)$2bL15fvkXR7dCPwcMxJeQOR=RXgs3{qx1JOFrl2Ax|d0zoc?KT8F z?_I{*VVx_rwD5k3Rh#C~i?z+8KeB%Iy69f$zH}F90A>S)*m4IGehM7TZ}UJN8dJqR z#mht=jUEA%ekemp(UQH3(lM>*H6Bu5u?jnppkkCKbJ}C|^iag_7M1ELw*?c6WUdjZ zDiGYq@HO&8pk;KEJuX=QF@{Y9fhLJTB|%oWv^+7k(K4HD#L)(RMJS&zWV_9d*_Gn_ z!0PubYMcZiv^!6T{k`lmUwJ5`lB>k(_n38nV!t*T<`kSR0$LT^#09!-;*}T3$H*Ei zSNaBDSOh$d6d1|G0OQ5~Cd!>Z0=B=82T)2>4C{lWMDLjJdFyrzrt4|5NLiL@t5Kt|o9c%Vr2o%J&6p(|8JAO*Q*P;o1R zD)rfO5hKO5i7FY4`Y---3v6zvG~zg#Mfz)U9u|#O)?E0@TTOg^$1i_#@TG?_0Qm-K zj?!gVi|vQrV*_+ZhPEc>s4O@+# zxu@D{LU)(?`R=#2R9!H^5Z%r-v+S8N{yb()ec#HOk)5_Zi3B|bM&XFVw-bkm3m z{1zA+?MUtjsNRVKzsz}3JlMCdg1LKQ#y^NnK(5(d!ax+Q|Mz0<_yGPF`dVl-Nb1{H zV=%u<3;2gHJHgfB;8X-ApA+eS1dhjZ&ye?F=AU<(5hjVk(K@)+F0t%v;$>TDemUA%rG-F=jA> zJ1YAgvq;${?6x>v#GmS2bql{aT;PV~J+VK;KT; z{`ZfCMq;ftLS6e4z=zXnfSEc|qLsT82zAvX;0}|euLPg}G!;qsC0}7J!b%jcU`0(B z!79>0;A0*L#ws?9@NY)y|Kk|`Dn;r_;t@!+?Je{p2Fp`IZUFPpPddL`)Y-pW)I(DF z;7mI5ore=?BjBGilCV4-pyGfA1p>tWZ!lgfA=VCov?TGZEpSk0i25-wA2ae;k}}6) zAP0VN#Y~kTmMw>}hymLSe!Lre+qJ*v>GwSS-cP^x)BoSky(NM~ZVrTh|A?Olbqp79 zF)TuowsbnT;3ioaVH_6-^9yIptD_^sO3owUxaz5lZFes|oeqCOHIQVdO@#&r`InZ= zI(<6%?SwyvaBm!1hg_;zb0v)Id|T4p^68xG-#f46TdzBjx!w8vL%+VFgxo`0S6w}S zC+R_f<3ksl~rZjMJSE z5v|6GiMc;f&z?exml$JZIH4lA#Cm#m7d!r!NJ#pBsTDQ?6l!ipBYqNjy<2dJF5w|1jjO)JQnckKz%MwRiUh8*`7T5DjTtb#F}!(2jX#H(I6V63v&+^_nnN zBOW6xS4Kkcf#WML*2*cFNQt`;XQmLq-WhQsy$dcSB;)K!38wtY`%jf!l}rBSgVlCg zEt;2g^;-`dn>nc2`(GJR|2vFNFj7$C8Ijz$L*lRS3_XJ5er8hS{pkAy>{~#`0h^em z4O3z)cNaJm6XRLoU-G$2#gW*fUGwc{Ba-=ld<{KU3gqJNId3|%-bb1{hnS%`P>a#f zj6@FNVB#oRbTwpztIEdv+rMqW?brj_FsJJW#?){nqfatJ687ti?R{&HXsa! zgPZ7R7U73(4(N&K8%Z8DW1b=QKT%ulaZ5^`*aaOKh$Bcl4C(rQ-4<#-RMLdnymB$ za|!dQvP4a$)kZ;ja_~}RStlU<`-8@!$G}2z>l-(Ji(}oK*Yvsv+YNbY8*DWy%as$g z(#@;z00`YT-III#3E(6a@9Qa8`J((usnUz?g)R@iMpurTqEi+|;mC{4^)A=DbC0Yq za>`?*DRIM&ErbmD&ffPP9oqq}FMFub)>0Fo4I-)Je*kw@k=ZZwJXvSIo#@9RZ>p&H zN}JkN2NU8g4?Rcd=n9tnsO`46tyQZe;W!Vq$?c{S?-bAV$9rW>1e&tW%FGPv$Mfo)a0hGge|cb|PajU9^iDEzp!4 zn}i12K@sd(fbA4r5qjh3ENNEj5Fc&Cj46{QQ%8}?of3BD|HwrY=E6xutV0ZW*R*t z6td$fa?juG9K7s2($>GkG|-KqFf+Uu%qkd)IX%;4K2aZ6Q=*#`>{nQ2$24Whg=AwM zWMPmQK`riAjS7N>W}4VGuOSy!m^4C0dhcdwyV^VZ7vE-6cAf>+9N*IbrpNJo?PV zqay-8@mfxIcw?$(W`PVZ%6uBdt-^xPMIp%jk`dYuxTPxO&3tSBXbp;X2A%q*i3R^? z|LN-0pQAi`Z{k51UW#;Q*Y&4S78$5kAbhsGkDe3i|H9|HO2(l_!ES}77CTDknO9S` zHuP@0edYE)jT#nj4PA}b>lxosPKxqI{8}XPf+wNt5p1(WRdC)Zbz-!gOI?e1oee3! zdU@#DqWfrf(^A*KU$Ji2%g1;2O7)(URqzvFxusudbd?Gup5w<*{AWawnMOD0^d89k zdZe#=Gb8fQO>i1`ExBU8I+okRu+i;3jUTdp#wENhWTkH3YA9_7H`m!(~yTW+G zcJ+PBqV~O0!+{#&^I7AIFz-1Yb*4JPbBh6<0m#)kr`sZYP`s7o}=kxA`eaucUvYQEeuarvZtEhfPP zqd4`_&q||zK6UMss+;89^U3gsS*6cD{N?{*BmHFtuiroAEB(!hezi>wN>a&BMTZfH zXYcy$AUt=QrbV5p4Sgiec{)kg?J~7@ZEf=}7gyMq)xV-ueI1#&-x0glOd&cdTWecR z5q{XFnC)8|oA$_Soz1;(Cu?uE_3yoV^9VH{aLcJpp%)U=%ilI0+`^06MLRT3l_4H) zX>}Ah-=gVpGtC??+nb#f$R-4(XtbWhuiKLH*VP5hcE5<~pnGPAWXe^Jt&6iRy0GRp za3NkC9{bw%Nxt3UmFKIfx~dF|WWUE@caE(Veq+5;>TK84Yt{ejpUK9DNe}g!BCw_IhYH0fXW`8^|QBO;Y zNKVY!cQMX%dEPzSGd406D7ox4p-gQH7@LwdB~l;9t|GqHKW`f@Q4^@#rItR4DUYf3 z-}!ac?KNPu<;(4C@K!jef-HG#$mZQkIweDxHGb zctk*fZ<=0&z^+M{k(miRF14Pw241ZRaW?XcBq%J(m0s0Q@8dnlMMWlqwDqb0O11=u zCYf4s0?BKfs)$6lWElxm8z>6C9vcPDNed~~EbYw$rs@Us_3Y`z{^Ee(!fEGwGN-n_ z8)fzEJG=U`^fg(w$42BK{JF`wb1G}609=7Ic;I3m-R!E85^2#^_w<~XuZb|8kvps$GLEPy> z?@(m>Niex*fhxQ!uvb;~Y{kHTrWiMc2S9ObX@_Qvy2!6q@FmkPA5Y(-z-@nRrQXa< zG`Vx1J?ul)?;=nawh2reUB=!7KRcEe)*sjwg((gv=NI_)7e(gdhI~U#r=O#mEEO6V zT=x0Uy3sBv}q|*U$DAHOlOg)h!e`Abxp%yNQ8L;Oy}?Vi|*zF?-s|p!2V) zM+K`@-z3|;W+W{h3*Ch?*y#PC!GFt*gIe#v6eD0PmLr1Cx2*u)q)~_Q7iI%+2Te~R zV~~MEoe~wh9E8#6Dk*;^GL+lk>mGA zJ{;N^pX9SGF=PLwyDNzwQyyuZ+fyc^g32%P3pa}|+1eK@#{Jnj-%bi!yj=$2pPjR= z`|_hEy_`wVo(-sP31BNpwd2`J5;Z@SqKG`uUDqzUJn_(Qeohr3FZKAP3@s0!n%c}`u8#&Z?`X^p{lZQDRnq*{P*%*xHvK1$5N8gnt#HRDt;%jR5FefT@zWKC$@TkJg zgV}3Z{g4qlHbUFah_coF9zDiVAri*{49KhID2`^^6i%{h&%>IBR9A2*>NY&;+7B+1+MD4| zsobS@pKiOlV@8{fm3l+E%8xqVPIIo)&{>x} z(52dNp4)%3zfczNVCvL^N=A8k&DU@H^8d-s`DW?8N=>AT7T9Yj!F_=u1L7wWyrC;$ zSHpXgV&@qDr__*%VnW)U`+r@rPHf6xiysqMY_Xk0&AAat`H9*x zkDc`b)6laocb+ZB_*hg7#G7w#?$U z>{^mTzWq*S!1HPHrf)&icx}~Q4L!m-ziW0y)A0ZswN0qkiJr2RtZWCTWvs)X1yg&Y zI0pb1$PTw4(0*&}tpfmGC`sc}ky+wL06W`~4t&SKp={?`kH1WN{rF@+z&Jyd<$1r^ z^{t-&rFL$;*`J9GSRD}ZC#-J+m(jHXkxH1hi@VCqcvf$i*4=<52P8=0`P;MC_G$jX zxwc0x8x01=AK-b`Gw`6zW^T)Km|cK7cpXU+*$zQqBj7rLmSrMYCc^@!G0Bi+aMMT@ zC8U!*G=oC(3VcRu5@~nd2cpx+>6pBu5o6$9~% zUm#eA)LR&Bx|YZu(GqAjFq^+yZ5Djg=r0ZSQ%N2f+2c;KF#gD9ZG=d=qMU0!yqk+6OSv-$-C<Kn z-P1}phWEY>=I}im=sOv(FqezdM2ed-79NhqEXrhI1WuIV8S?{8V~3RU_Et{?=(&~D zMPbuYZ)0FPG-4*r9cY3QJTyU}gp-$XwPxwW5YfS%as(;kVTUP=S$h2rtagJv!f}V! z^`J~!;tem1=7Xq4MW)Zh)@8wEQ<#70LWIAyYOmCyKS)I>k{(Eve1P1*UME5W1CiR# zDS2Xw#PkivVVR%D*4{4*nZC@14E9aZZe9wu>Ar9J*N%wL0)*gH-+!UYruS?8?fZfF z{DbYD1;f@TSq@gTo+{iT0C?qe;RqOkTjchv{m6oVz@^Lva4D&Ptgwd^2&0?$;Rdpf zNYywHU>3+WVH9Zlp_22i>|_It;eU|FPQ5x=9jD44%_l3n@ABQ;dZs(-fSqL3jwQuO zEKDfi4=tepfDGtsk(@he3IeEQK*Q`7O`g{QP*nbqj~n9HOqQt-6vPF)m6{J&r?Mmp z0!FN*?}c=mf!R5)?%>D+!4~p@{pIF9vO=(&mn(>$NVFw=xN=-GIvT4a*+;Ap2Ecpk zw4;ms03%bRHJ%K`4^v`-#Im@aV7H!7p4K0W%)=UnRi1?4Y`?u88}CJ`Of%1<^Fz`N zHtYt9x4*WftoT=o%VylflvA1=lq9X}nyZhB$V1TTpdf(*Gt2m@wRTKwwr;>)&24Oma0AFT<_rW(uOrNcz5Fi4yp1opijWu2wj(jH03*P)qnW)@bTFDL$>FBxFg4!A_D!S}@!YRU298kv7?@9>k}QgciCJ#-J9%11 z-A9KqdFAkBk7NL4)Fm~?8=sh0$CQc zZXrS;)Gq!E{M7{s>F*yiI38(eZ+)^W7`MtU@BGwaer8Dvem2cVO?q*s_!i_tNrb9Y zMVgW0M#&Uansp(uf@jGtQ{~^_u-5@fcV80-{%DCeE85dD+Pi`s8JTo{{2NW0G<-s(EAyGb)>L21RD({3n3&KV6bo% z-y1L}PzRGMipVe??IV6(J`wQ;d_1TNx^%wJ&^b8D*SJIP?Rw>;->27N9mzs2= zM48-7!TbVKG+wka1Ane#csmTYk7JnVq4@DDRRvU**zs`x#dy|tigXYoJ_NVn>c+|? zQ=0kJyW>+Pa+i^aNS^uBNSOcm$3HO=91Ghw`yipyu9uo*XcF+S_HEoVd4ANv& zPiFL`OeFn1SF&cgU}6q{l8}v88G^lwTg!W`Hcvw3&bJArd>7vBv6*Ix zF%n93cVkvX>+fzae6!wg?iFf(i<)%V7^v~cuj>rafts-i!weQB9jRQ}gURL4V7$a>SWL&x zW|0m9KANI1G#d>vq?;^BpOvP#n!$OQffgx73**Jl-GmO{KO!U@!r`;6E&OZ5a--#g z#BVL7oCt>mbCyPRZty4kVq8_&wi@mnkg~7pFEusLWxlALOS;fKXY}Q8kd(lg|BVZ% z1Op<)b0W034B{kUYf^48HjoqqdbVAb5pV>PqG`*D5kN79L8W%5?z1$q(#ywqV1(== zo-(-|T1Y5x=zt}q1<2EWHw&$upPIKlkQT2YONmZbBU~VLJJ6DBvHRHw$qjJ^uo~WZ zjca$_noNoUx%RrOCg!_+d&J304zpudtRTHm^=!0jHkz9%SydDv@RO_>2lMw9i3D@` zpc2Uk8x>c9ZqhQg?v9+-0RXJv_%uT~6-C z$&fb@+uB>Kbl967E7R<-*|k^GEC4dv3D`0CeY2r zFD%ueaV=Svnc~~7F}tTe!o^aCusU7$zScYHEzEgZ-Mtl(ZUg<2HmO1I%Oz}OQJ4VY zri+xUh3bOP79(}ycTQ{5QoYV`1UsIh*=(NqqK?}kCpv*urpdqX#jd6u{dV5hL$#;M z4p<%lIgs#k|M-h6N~TrK)6!S(Isfic6!{$aEkMva3^|T_i&S%kbzmt*62F~9+9P3I zE;ddS#xqjz1<$emb1E-ZSdoWk%LS`ZEgi;R&p$3M4Y#XfX!`Zw4m3Y;9IYER>8Tj0 zJ0T@ziS~l`A&TV0nb-?BHAxe8m53m)<$i8#XDlZkx-RH&GSWcG26eKBZ6+imkI)J9EIf;aFGJzoL!~U`tu0;7$ zuyc&1a~|R`bC#aQTU;Dqx@MJ@y7=%zE$6&)55zrOeY3-%`u0Zj)Tbz{B9=3b{$Xvz zr~X7;K*vCfojAEkX&!a3Kv>bsxY&W1e&?q1t+QQ*e6{rv&m}9QOQhgc*!mgYL;T(h zZJ@bus4~Y2M;2;({`%z+xf5{3s+N;`3Wi6m>&?3u;5$bOl&XZwR9kIx+K zG(jqX13@Sgo-&Vo_HYj^5ha@gH5$-@1vL^mYp`+i1RX%MzL}v4jO`%dc?7VIW*9Q) zThuGGTKH;F1iVN{qAT>xC%ETTck1@dyX&#YD+KhEr={ufRjKRFP0@8KUY0#_RYf%; z-#LSTzzOJ<7#3zQRM;(uPm!Ia!qN?9;Wgk66SlUv&PCHq$J_a;+?tz~C%3JgGOJb` z^Uc~>E4W?9d*|(1=0iIaz$GYbi}if&GNp1=Nt(w-Ku=1vzDKwTkmEQnA@~G!Xn@0D zj$WVR#Pc0_=y)Mc1tea{=!7hjj7aSP7Djm!5*AvFK&(?U)bU8^69YlTGnu|CFZVw? zM-XY19K#2$cpPAmStvUI!Pl!}izM&E!P%-O#NQhSmn{z2svB?OuGat{mqA7`t}PV@ z-U<*~*6@n^g-FE7zTk+NE*?&461$RX*AX8cnnEz$K4+>cSd+Wjc|bU@{;<>nVfNSg zXvGCee(i!T>@Gq`TllZmPX6U$%YVu*pnrD$tF6%`kdVp< zkak#l+`B;&GiWQk2NeD27-BqkpfyER_&|K0ypd!_d(}f$p0={A%-CUR1AFu0Dz3I0 z#&R*5M{AB5%$^>ug#CsKhOV9{E5MtRiv*U3qFXs-Upw{;pDkO>EQrW~v}?QBOVU6RBVkS9C2=~HT16d7 z7w&}>zB3uikoHrzj~bG<6FWR;RSYCiu+={=X+^TbfT@LMz_Wikt%U=U?!JvlnhHnS zlorP{+spnYj(@X!cSJocFyZ#*dbPR|0}bC(Ozw*pPjb|)Kngfm*S5z>F*)!&Lsh?G z`D_g&&ci8bzNp{9N6r_rS?dOn9P!k2@%#xLj$nRZEAQEpeUE*j+k~;9*_8PK$+(<;uAAmAjxO0&NH(}^ijci+dlBd>@JQuU4`Gq)|^=PkT>sNgg){Mc4XMcTJBT|W$(0>uBXHF8sqpbb_5utCX zj&KVIN1lnofZGC-nYD93qDC?S9Ggk{u2IDH5JRAn%bAPw*elMU>2eeTThpCX)*YS6 zL!3_utda|TjgMbCwy8O#t~xiuQ-*^%12r~KU{`3cMpz_{cNQKN-y(0X;VTL(gf|6q z%mf0ThUVhbtqI(Y)x^MNptXCc?6qX9Gsv$P)VqsW<{rQ_1KpJ2pZ_JVY3|f3MpBcV z-*#$)F_!a!y&Q`t4Aw~$szGYzovGmu9Rw_PvJ&8PmQ-~ZPq)Ii&gZ% zFD*k(vw5FtauCQ>)v)$TZs7=iuNm4Tv`D!@+DeWDN(9j%R?<+T1!=h#AUAq9qz>x{ znS%62Emfw5Zc{NOx?JQcF0_nXvO}m%PK$QL@Wmb$S)(r)gSInbxz(gxu6W%;Grd^w z$5yx&cSL`49qKz8F@Dza?N9Lo=r4QYQ#T{rZT}QMtMs=xRaKk9tF2;`AvkWk+lXIb z-m;@FF1$`USCDj&x}k7#x@1+S>?5d=O2pQHneu#Q+pM)O63bg8cVW^379&o|u;Eag z=r&+Ap_lY1%Fw(3-WIhHJOeG5k+6#m?g=c-fLHI_V(vW~$-`(NEreU^kPGhOq@Sqf zjF4gu4wx9?n^l048uTX;7C7#2-LM#v^XYzgZC~Fl15;W_wst7E*jFJS-fv4mQ z57z)_Yy*vRLrdcX{tf}isK-*_39^Dn>$RY!nXn9jj#tLBw9OGJ2<8*Q2jae1x^v@T zvm5j8p!0o%h57(Xo8_~|D_q@%@)!EDWoIcO&1Ke5;|2+8&z(bp&#hlqeq2j7B@W00 z&n$>0r59Ibqu)a#J(5+@ODoC$m|+zp>&T-XTe~gybJ)X<_5yll9@OLyQjJq!IcfR! zH0Cl3a9tvQ!o^%9N!}vj2G4$XDO5F&4|*hgd-ZDRu+kbS5$F&$dWsH!$GV3^j-#Uh zhm(^`(bx=#Aby#iyp}9QF7OpAMQYB#EtwmHq0X*GiK5->|Cr+#X;9P*<8$#6dAL7c zE34k``IuWxObM?dtNePfLiFi_aoVpxp`>1WP0H_!a&!HJ@;-x4n0PKuzf+n;u zE3mUOG^3xW=M}tqR0tb(!~Agi*2ETqpygt!c)w@|;1hzA%ou6}g{ceFC=i1|H`yqL zA2#Vbgre^zcUUctTs7V#*#!)SUu*;ZUXPRc)+`83^Mn`OrJ1fJ8hlCuO}BVbaOm>o zYWh8)Uh-p~g0B4PzOASqBa)SyWRtN3ETYfggD<$;p+~fdN+fXnd#x#z$l;!J?l{E2!IW#t5(<0)w)VzaaoYK$m_{ESA@ISMr`>_JswMRg6MFIXY4 zNvFk1v)j@(7@o0(%@WQ{l@d(r2agK^;HlGv2F3^I>bP7 zBFwl0dL}eWS1?vzf`r;tcIWq7p0U*BY8g9(d`myoKN_rVvXtOoYKmI3H13^1-ivrk z6t*FD&qeaRme%=xI>J!I%zM1vp{d-X@x>5nQ!ja=%b@7^z2UhNdJ$e1b>qFWMp$Oo zXnyE8?|%EYi9T1=4_f`Iw0Qh79*quUcw-PAAO6as&4Kqq3Wl8Bxsfg>4*l0cDv z%wx1d>qIBbv!12_DydN{?GRUdwT*qWS9yNatjQ4xT{2^ushl1f^K;GcwROzcxCU%v z1-+Vne+Z95cE(t3f)9LV8U#%<a#hPF9+xTTkT8MPBMT~WcWer&49i(PFbW}%H+;z&$v33 zn#FUfkXT^wGh7o}^?1r;L~kZ18wdPv+~5xxef5>Xz2>t9{)KivcRcKUrG%=Y4XI1+ zBcPuEA5C2$@(?FTc6JC9g;xdMe4~pTY>L%Bc!SlJp2|o!>&_sC%HW8P>*%{Llsxx0 zoB`w!K6}vj`uRUh4BVXUSM~5!?|!h}z|mHQnd8nP&DXG*L~ew`*W`c@=?1nE$B!Ln z+^EIHko4dxx*X>DFa~C5KT!|%NI(%&2-e(BROxa!D+-GRJyi6NUo);65^0oj za6=;vyKF;2ql{AVIoXteI`?MFwG`);EGgs%j{v$jooQi7Ole}OBTUoAxFu$8j$d+o zckgJIORsbzgA5PG=9f)4((2C z#R=^5LYPNjVOd}SQp_AM@B1DX+_7>xZ8+pby19Sp`%J$`t(oMLXtX#6)Ssb7cLt~+ zs6ma^-?|_XC z3oJzbNNNca+$Kul=vb3T??0l?ye#XHY?!$=RqHn6G^R2@bI&eFG+f^MhP%YyEPoB1 z1Dtz*wWFg0tk#ozh5?0l4fz9hE!GD&Jnmdu-APjvqzgF%^lhWlwTAhHLmf}ST&!d# z(D3m0MhHEBz~jsNFYenqoCM2ze4S3YL*A3BY0IY1d85Rg;(9D62L#^e8y@>Q0cjP3 zY*$SrY3AdBp_3*yb=HKEj9p_SBEa`qSsD(2-L1KIxJsmfNi@kcbFS&FY)|ChuJXF5 zwB7&9Y?V>v>cdi^ljJe7mN<$oFJV*498$5In;5kPckl6z6v_6X5l|##H1H#u$@;Kv ze6YI6ntRT2w-@)F+o>cxqr%clc`h-Cy@7PVoLkm%?)PoCMY*D;>64dtVJ}bwvUA_5TJCXy|9xe*|H%Y^|E|g_Sxo>+T$yJ;aHg zJE*dywl-tTV*iUTpDN-B_q|Q~Sk-sKh+jp;E?azbz6!qnWiuq&CsDd*UwMxZa?gIT zp4|c6ldpboK95(O{NRamUdYH|=gq&DqyDFyu8qeXmIH_cHil-)F~T6;e+VcyNf&c{ zwY3De7@ET~ml+Q&fp+xsk@{x^xqhxka+HMTX^$X3{j~3C&ez%r0rIr#E&nw8NZ$wa zQESfU+3%b>C?2znu1b{Wd}l7XTc7MJrK!T~rdqRT&>wj2+asUicryNQ=U=ly$egKM zL)usF#`PbLO1I4(G?8i?B)785pFI@Reil9iv8p%rX0110J|Z|QB0^AMkN9)#&`D=x9U(t!(Uw*rZhk+{i!!BSB<+-n&?ZV^mIcxQw{vWp za46DwBs+Kok_xnyBvSA|JS}iMx7b(Ux0nMIqvqe4kDMF0o%7<*no?r0Ufun`!e*@x zbM(GkjdLIM>XL!>i61hyZyIzF4z@k?sfCu+a_o5)~RcA6fwOi71>7pMFHk`vo0fbaJ0}|oJH0q z+1;FW8^yMe)sTza6s&8P(Y7t^!Q40f&G(tk=+_V4Wp8`3q@KIF@c2vF0lGerp&*5E z>-`$(`{1$Yam-5OzQe0-k~J4n!mDU}j|X5<{@(Lb!}OHrZln0cl!r~E3fIw*cdMi4 zHZ^>Z`s&;WS|xzVux`nmI3rOZG_Z%{IkHT2!cp%f-CHzWkQ0YP2|;(Sr)V`znx_xu zy@J0wD4OhP&8g11ckrFOv?9>LqKUR6(lC~DP;dv)gOj-(O~F9Dcj2}Q%?WzqCL3wwFZv5DB2=UV_acJk#z(Ex2#r*WDz@1 ze#R-zsh&hqc-VL{n5r04>IOD|rB1&I<<;8(3H2`vGY|VrOK~S%s&;q>gcY&R-`raJ z=z|p5l-v$g5cr$0F@_AEU(jtivdIS#ww~5Y1Nd-YY7Q{T-H7$cUd)NK#qZ4P#P|~o zf9>h7_BwX=K1E2&+}}&Na@kWzOhScVRIJvJbP)k3#gR*GVZ@ECM5rJSbKONR=5!=@ zsC9|lxOGGRf7h3?hELSs!@Pxc$K>vH^DntQc0oTP=$|kP|9+D!Vc{x*Ym)fn z<>WTXGKm`Xb|py0*AUyrfxyNZnuT8>@i8;J#9>7~n9nn5?<4I_!hF#lOXqOq<`KNv z$`P-M3oAO&n~`r}0tew5Fw&nJplB@I(Z(%fNmTUdf~qXudzp37#w!V=g+-;dD1L!?5DSh^<3Qt+@Q(bCRewt=0jL18)!Zy^#+RM*`E zj)cRuw<=2wBMpJP<8Y(G+n0C)e5CJ7o8=!%PA!WHVSwzB^91)2H#@{jAO*m>!oy(Y zw72v>Ba{(anK)o6Dw{MvcOd#t6UMsp=RWvpxV}ay_sseC>VAp;@W=b(E zy7p_thD(w4F*jpAnm1D4`&6Re^$CuQO`*qbd(1o1Z+fJEHsha>Y420@X80Yi9}{IL z%gHDbTKrg~V-|(=2B{IWhAvOk{_H{%D7VIt)ri@ zIN;*8c-6S9_XI7&CziHbE0FM&K}T*>soa`6WqZkawykXYih`E8p%IP|-+&u|mXUNa zR!B5R{s6IF!sKb0F)>Os)okMDLuCWm6|^2szdx9>bL^p!>1%`2X2O86=2aiIR!zL! zBzm2BP8#SytnuiobdZM{RU{_F(1Sfed?0J5Ex^ov%b5({`^bLY>1`(MiMO z;j^KGcSl^#opW((#CzkHZP_b?TUQ?~OsbSB1{IAKaV*1=FboKjr@-G}fhu1QASyJF z5uO%Wcpbh?$werwSu4AOsq0n$HxS_0_2z(4zZS|`A7J4T$#_XL1|{p9*0fF=ik1R zdvV1(BQ(u9GCP`j%to#T`+PYy&?~M`sEAfr@YUV_v-X)OD#cPNc%JzMclh@ z`9aIa!V(}-%E`mZN{nYbmWv-loQI1Pz8y?)=n`3k=88Nk#m~uWNjsxmdaJbdSY#58 zf&ikn-~BoZ2^UlB%>rxg7KTCis%6`jMOj@3K<-#jiMj$!EjRxx(G8siMT>t-q!xdo zfRn9mBQzYy{irQ1&LyuF9YBh>c9XMJ79Gn;w($G-aeJfV{Z{Yg?Y+bW_yDbE1sA&C z`L8K^W?DMvVEj*}&ckR_7?ueQk+iH#IfcU-@3RD~8StCa)0o++z{w3nc zbpeH*LAMUAcUAEz7im3Lw+QSX=D$7l!DV>np2x`0`(GRg0m?Soiilf|j{`SSlRI{U3;Z!*r$^i(oiue5d85_CuFTw#$!=>)!LetpKKf%j zyi%TC4p}YrH9iXB{0%BUpn^nM?@u=Z+D6IPD{NWOv9exlRV72khaxB1iie*1K6NlP zbwIOf16_#F>D#BhK;hD+s;n-~@srJ-{oA6fZo_5`m>WPHiWVx+{Ux>s4g~;Zz>I4x zq!kD}TRjxvkmzh8m3Jh+z;t8~hp^py^h*~oiXN`V3UO};fyEucCWNq{X>P%tm$I3X zE=-y2?A;|d=n{i(w6tHe2^k6IzRItGg#`lHMsvgZQlF5Dx4N&&yMsrPar_jk7GAL8 z`E$c;TW`xpVHZOT4r;|mgX@Jb#O8u|eFJ%ey@N)axr`fTdtK8qL*eM-uu;5$WCMH1 z&)D>R8KDq4jBmB+b#{6k`noq`u`QJGI^?jenw!?ij!dEEwKY$)Rj?CBvJr zPV|@f6?v`51qMB4M~NJl%N|;4`fffDKc9?^fz|`5++9FUOizG27(GkZkqq_DiYzV~ z#(SyU;JKTf*gG6j;cqer`S};Q{q+^d74hfdD66eDh}2=Tc!~Ni=jd1~4Ina@St%r| z!Ppz~;H0%uhQ;}P{Qmp^|J+GeoBC&&1&vlYVWAQFA=QE{>CVRQ+b%yGin8)SK7-@% zvlz?|K2;g5MG6<+vsy+vEhzhXSh5Si_$VO#ek%5dH>RE$;8sMR?R%Pd@F=(U3w86k zK2N!cSR2$LmK)lHi)F}=G@pqqko~~#8$hjPbLY%)zuJemBXE4&Y}Uz}`0TEEJqyj{ zuU-wtefs>*l1+PI*vGpPFO&qt9$wIGs7#efL3V&K(sn@h>HKluQJnonvU7BVq|nE} z8=(NG3~KCHEi$VapgGsr!49$9E|Fc%l=+F=97eREr93F^JR)JSv85;Rt}iG=(@lk_ zKsM|QC1PP)_J~`9w$v~Cbx!IpbM$BlQ23)8BkA1er^F7Sx0qr|h;zMigG;pkV70kh zbO-{eHZ|If1+-z?@c`lF-)&@x<&GQ*;JI77bvvJk8H<9h(6z)jfGX%|@!#Myk6~1& zHe#X~mAKpVwW4*%MUEDMgDRQ1vzrul_)~i;Mzr)iQ@!YOy^JGc+P}MYLduRio13WtK!!hQBd~aYB)luY7OBDT)+8`j#-Zrq`?DRj;M)I8|YC%m5ypUyOX~T&?Kg6TXm^^Xz=P zSD1_$NXdQY`~#I7o1D%#pH#reA+}9f%K))}GbfFB9|R`rqyt{snb|~K)36l<9F}OG z43f<)C-Wo8c_YL4;ov7;gRTDF{;PMoB(<1npeB9(pWzB$*@+lw@+y+2-9Dhhxe;*} zoH(#w`@jJ!9intomYJK(-I}A-)*u&oW}rNnc9BpKc>P*fwwy$pq=Hz$ahi__F(5ht zVcf2o`c+075#Wk0kL=WZ8Nc1|DLj_`EWg3A?Zx4Y}Q&s(l#|dmA@m-6hb(0zyQ^Z6DFHEFT|L$qt z&)xNkiU&NM^1OS^r=RDvO?RBz{yNZQtUVnrmS6z9YdIR|#Es@d+~>$yAc*Hh1_}RK z>J7%EgCi&;xPTkc2rCN?u^yh8c+Dil%>}>n=^zMc+du{VCu*_=G-Z70%Fs7;ThSl% z6+n^ifP-j15TT=Cc_zOoKGwAu#HS7VaRu@~{&x*$iWn4N0Ebj9hDYi7QMt zn8st#H0KwCGQIP-)l-X^MbT%vXJ5!w)g`o*scmJiF4rMZw1|;SVP>nY3}v~eb)Zw4 zY*$X6CC>VvRdv>i%eCk!c0Dr@9;}NAUtjg*iRC=xXxxR}Na|xZZ>gY@>&Q)^N zwX%*eQ}g*drjP19*}>Zjz)bQO5O%M=DOp@8**j&o!K~(53j=;GQO+!D4%oT*BY?71 zomqYFSj#cp3`t#l?32~p6<@>CuK^$R{J+2O9s>dDM`S3ZOQKygj!h^lI<04s1r&W@5Q%h}*vVCu-s&#KHpw zuJ|A<-1PuxId6oR695|W6E*KvNfnvmRxy6u%9U)wNkg4k1(FRwWZMHH&>#UiU(P66 zWa5Pt{e$@*KV>_JtDPIz^SS^68LtnriS&5U5z0EoWGF}%6x1F5%_z^xQL+k*f=%6V z5X7H_K^NxlHtc`*=*sMQHxas(tJZ*PF22l$>haMrr*)+@0k5}Ci358 zf4}pq-&y1D4FCVCj{L4${I16Qt}*?t2mb%2%DKk@(Wi=(4td^+@J28bdr z(yxUfEm1()S?P9>JR=p+P9%&MN&aEL=db_fIzZFOovzLAc6GH;$bbF;=iaerWW-vT zwuyC(b@%yG;DO?OE_|Qm_}-?A<8Q3xA9=o<-#D@3`l+0x;uBB8@;>CZugrh9?IS;A z#Gf*(zG5fcV)L7TSC5|m{p5;c`lT{uCC}yN$Sp06sw+7%Im>t6fAIR!1J4MXEhP}J zsrnb4K*YKvDS$h&m8kVdR)ztD+jo%;1OuoWvr)1O*f0W|j2uut5^ZJ-dbe8bSb{`b zFe8v)yQJ-Fppe1G%sUu~L)&prJjFG_4oxv`I09OLWM}JB&8j{NmG(zcbQv9yuY>uo z7vH|g=!;fuObMCUvb$emkWkYq^nD>kUFrWQ?_IfZM?45;k*YvWB}0H7+r z{Na1?tim^w$xjavqum9G`F}gHpuf21H+f`j&L>;#2|u{OR)hpC^V)ATtc?ic~kL#ST;RI}daA4ERIQ}Eq)+u)sV zXYq;p9g&Ve4)IRW38%)3J(ZjZ)hi{lqZxj=SOdMOj$~{kgEP|ce62H05-eY|GoNt^z_WSO= z&v?hte;gxYK*-E9pLsv`eO*%JN}!n-&Bw=^ZMRTy575> zLjITnfF_8d+#XX>4rsUg%$N)Qz2J&1hVmT4LACa-7t)8~pGGT^TRDq@Q#n zZ#4`0|3!tHzpLWLb$`;?%>Sgjtq6W}`b{biEgDjFM9I|J+yRxCvIJ4uUxJWwLa@+J zy2J=jYgjqx0xpE{p?!}q^!Iea(8-1GpkA#UpZkx+Je}L{104zCTnx4AbXhe^#O?FU$oSDvA^*YmY8+utaxiSCG_YHCG0;taMnC33 zWm|;h==1&2#V^aXEsd{reH3Qf@~?gpy2w0NXZo_g!l-PIX}RrLk%VhT>78tj+g*Jb zx`{r^K6;{lSeeWmLB9+q)nv~;X#z!EOG3VALzgwG5WXRo<&Z}&KKeTP{n-1AZ-W!V z)r$;=26X=nf_(Xi#&YE+-5c{ma0*$ONt~p;r7*%&(igh9USRY%~sTY+<&h&C~Pi}#Cs-GdJ zy*`MMX6;eGNH%|6dvpHk-_9uxAkd7ooG1N$_r1e9A zBOlm`o3#@wzBYL->myzFD=!#Fd!!5|FR@cHTWcqu0u5PbmCsnQ%K=b3hLRsv*cki- z-o(pI=4nUG6;T26mXszx^&)Bo?lCjs5D9!EbAc&}TQ`Z2PWW%2a}~u}xK2|~>o8B_ z40zTiNivHfHaFCM)4EfYc5h_j^TCs780aQsyid${GW-kIi9E8DaybKx8>G2TaLYe~`AY zF$X&!x?0hI9{%3Q5}9vKf=4ONvwZ7OpBK>$a^HIpqL->#FBX2U>2Y$?`94maMNInA zLGcjd-RmDGI@@cZx1{U@F-WDR6mFB0dy^)_jPDnvRQbx-@ff;tx<74%-{X>}c{b56QbWFL_e ztE5*VFIOaK*U42?^{v=qlY83_Q*OE=XE~tvuKUWR>mZv_zjq9=-Qj*aZ`yaqU|MIx z^znABt*TGBZMsRvkbuE&afPqnPZ!39bGDFbrss1n^13IxO=h@GOg;&S2thw#jRP_1 zEI{sJS}62l=?3jQ2^agE^;ExNHpKRCZ??r`$pPc&jaMJmnlx6&_FFGWLuDg<&oo3! z%Li_$pS|=M8g(u^n#hDblR?-;naRg7JrQMubGMf2C>9!F?mQ>Bj!T$ER2N$g9kFkI zyFXeeeaU97j4`abrAMU8xj~8i!ablQjZQ6v0}>isMR6Qq(L0 ze0ygU*sSy!G?ghkHA6ccuj5iZ|_o7ja*`;I&vo4cRG4IiK zjPiM+u&Jgo)=>J2c;8L1T&Lg9z7ou8g;E}a1}<))iL!VBh3<^QD1TC4ryXA-$XW6{ zPR_`QAe_mOEJOtnwqOR8T$M8{q|*3iIx!y;8BH%r)t~-VTOwS zrw&5qtQ^|LrO{qh?dF9>?yQWaMQ>1r^BE%k;f9(Chl=vkZ^P{)`>UEjo*=_pJ0WHX zmA7hT7$#oSNDpCc6g$N4#IgAydHO|cJ_MxvL4c$Cq`BNhY3q+*LPZOt_*12eo#a@H zT$u%cFn-hRm|!sJUIRRD?)lS)cjnut$NV2QnOiHmAFWRohGAnm#=4C3#_`6ThB;R= ztecXBV2IOAds_M}bw@4JZ(rDJF&pq%{}zXNU9zRiWz1-o!&w|CfsDFsl!-9O7xgU~ zwiqShIt-%clt;>Z=g&~&K;QtCWe{C$yP?iSGKi|BicGIu=JP)g5Y=s|j`p;?9ktBD zNN2)kNmP)kKWC)q(wlAdCev*F-DFO|BxCZEuj@wTuX^|R zozCl~`c^dXn_W$IqEr=YCNE&BmRh&BoB}^jJ9o&&@J>c5!RViYmism#*Exw{hv*<@ zyqd>rs6h~s`6@lpav{@nxb%6WuYkIE-kT@kx_w9^i?@$slU3h+a84c0Ghj29`)GtW zb2I8R=_cgeDb^n_E*38^arvgMeN%DN%0m3{k5-h1jR1eKd~wWQ$;AYhM_%A%i?R7U zp3kdasz@A5%5r?ZT~@0yV5|-DZ0~GnPwWq9X^ua0x(;^aunn=|DzCFlXc_L^W*tO94>lx9OC*);sbu63rjq*iqjN*iJGD>g%V0@rg>(W zOW6`*D?0Cmr=Kt9tKh`1(e~6!{@Dui|GVn=aA-h0&82xPaooVsMxtmyCw-K$SmXZEwqZ>b@_M*uC zc3v+(dsCv@d}Dha6))y7sBGS#??@HQ57YCy{`~uh%{;$HRcM*|D?6DCt`y*ouIYL#JDN`*Rxrs6{xv!BIP#XAJKh>(cf z3s9jCYgR)sQ2Ip}qYLk@$FLIoB4BO%rRJMGLmD$?-Zho8u72TNbwZV%jG<+>sb%qG zUR=9_t0^YD+g^_2=}j~2T{g{H9Rq|(n5vHF_{>~g6QUT(uWy^aux3~q^U=|>#*6n$ zX;jB#o=#mFb!>e!)YYZDY3W1py{Q+I1b7$U4kIl7SsO3qj~3Dc*KOMMTJSk61C^@m zzMrT`KCOmWyDIic{qcuQ%}i?_<`M~~bSF}02|pR&c9{EW=|CcPHgC3u76t0I#iZ`M zTa%18&KNpRCiN-MFBrKkUU}WwcRsOorc=T_T4!)4R~Lb>SoUavm9GT^CltE|2!eQA zyDQ;=xvcAc$4&=1xn(m}z0%w6iinv=$GY*$^;wb5soj<%JdyF5T9=jYNg5Wa)vQfm zj4aJ*m7?9%xx((vr6WbboUXC)cepzX$wh@@(iMk5be`PCt#DIV{tKVutL@3 zeX-9-te2^iehI-Sv}E1Lt6s9ns3~Q-Cb&5aX?)@%-@i-wyeL1lAkWmN8f(kyY~|B# zh&78-9qrVVQ7jgp&lZ_|wy^ml;YK|*txegMLjMK*G)DH+MnD>V=pwS(nv@cnVV6+0 zu2$i}`t%{9DK~2SnVVu^UMCslmBWAVe(O2YxkGK3484 zN{QcOT&Komq&8qAUzeF@@Lh6c97Z`JR#;g7VQSWA!N*8*XwKcck%U*}<}#fQ8wBwt z6U104ZRn2DIuU!`&?}x^Tn)h{E~ZwI^(>hmvl8Iffs5wt2^S{Uy&+8!&lWFyIvwu8 z<i3Qi<@3rWwJBJ)Re}FUI=ik1bL{iWAz6x_Wz6tkPSc za?-7dc@ojZ?(2PT>c8w2E?HupN%k1_DKIpM&1drHo{1_9GXpX^HIp#gGuA`8H)}oo zUYFsA82)9vM3*IG0Bfg-Wl*Z7` zU+wE(?{tD8Dm0Gy*AC-56dpJEPkYF^Q61#u1Ks;iTCdOAmWVmJ;DgVEJ)i&0^NiH< zEN_X~2Wu;d-kyJvaCTiy54%2FN=X%ototZ2b}M_iGJ$c^MKPV_Y?4dE#zZKLK_crG z2M6>{7uZEx;BZ-ZchuD#pFAcckyVu@fq1J+!0~Lrxc!CT~oI%B_mWW&2L_R zUeynYGS2q~-B+6k7&a{fXbRCq#LB#Rl`7sKU3jbqSTV)v+Go z+_74mLju+it(fc20l$k`f#&IhZ0~ZM^iv*~pO`+l8Ue)wj>K6^S$0Uk-h+Y4EqW)8 zGw60p8Y=+9Uqb0Ys&ne|{zlJ*xY!p#LXKBn%Cs*I!#$n-WsgIiBY_oO5f5LX~f4OeKbq> zY@XJUw{fYe$n4bd6GZf)3ecFeKq^-NV~#ey^kqMTijUK}%hL>g>&qDL>bgykFp1MJ zlfeW|#Ddy#9*sb;z8_8G#~J!DAU-+6Bdkd{H(K!)WO1Te7GKV+@$~pZHu08%9uVUr z8v2}D$_=%qBn`8u3o#gV+mi0AvYWQKCg7itilYJ7*gh~4LSM;fmwIGrPs0_`@SzoWS5 zXO{g`Gh3xYVlTAObVh3fx207Mt-$QX8Z``LKrcArFH`ibnLsPj}qR^aQn9q$PeB(~8Zl zM4~5DDeEqelr1zVayRl}JVLJ{OeZPTNk@Ie+tkG*SvV@poKJ_c=j9Ho@X|w>bAdn2 zwgNopiSZU+xbmvFlV3dBfCHLZu6{LJ4Iy5tq?skaJ4P_#)=74}+1<;YoIh~T(VBC}e{Re|mzPC#(OG_RVJ&)uu zJyVy4#Fi|=q6iF$T@G@|72q=MSvHX<=)1HMsiP!uqUJW7^^-1;*QQdhRG8El?3~qy z*B44-bx_EHR%H3;t3YMW)=6&ozc7u$i1#q$g(YQ62u_q18)N%4CYO_%TVf&ns5{hm z2dscFd~@7qA%O>FkBeuG0Yj`#kp(EyB|y{OXOdHE*sPUvXv?D}lU-;qSMBg(WB82D z`0F*ZI%~qkF1|3g3M7BD;5Dz)Ug-~tn3w!-D9c(zTUg9E0gLNt``*A%S9)DtEhOF<)~A?ZO?p3 z`|EK&!4FpFLAm(Y*BH!QbBX^8JW8Ku?K(XbpUH&2Vt3mh?X(wM0rU`m7AFp&51NX9 zQ2k{6cAg*X1Q`_d?$DY5(Mj5Mlv;gK-`>5G{hxHAZbKQt=TSjGEBPng-?1W+cyQMY zB+Bvwy87TS2fk>)jMReT62Zk~$2V$Y<` zCE5+1gTEV;e^4&m1NcPtl1#2!6t1K{6BOdO!@wPDD*-5yA=X9Z#uwE_ypva~jFWtF zTaw==y#3S=p+e-Z?s{;;E1}b|A{hwei^tnbj``rhMdnq957G&t&Rt@^n$PfjRl?IS z>=s|NdJ>?wcypc+B?I71YT2$r)!8h2XF zr4%ER3@2K+3N#iHSm!9(ZJ<5-L#y=)uGG(^v&@6^-ChktA_Yv=sd ze#PMCz#hNg*)~&teZe@L=iz&y`9wmApszlfs%(_j2dhSJ85SYb zd4QD#bW|`WwHPF?$R15b557vn+eQ<{%O$r9q^ z_q2=&xm$8Y7WeL2t?okWWD)z|P>{JJRY;`!ksQBLOy~DTc2A#A)n5sDFS`lCqH1r$ zc{38@x!59$d2w6(iC9&HMD)0jY%-xtve=q2XTb+5%K69>{3REMAO?|uw?)(2RCt`> z;q%>oTYXCdzOaswLX9Bv=@To%3B~u{RJ91OnM^`%JnaH)4D6*+&X6UEZadvDZWr@o z0kGZN8E2-8ooLZ>DgvCf#%fd#R?D56nyMx?_itw3G#K0eQRa-0%rpygdh`3OpoH^J zxS^vF^8GJP$%dPT73<2c zv1LZKe@}ZwrPowU=Yx~!z+~5*GR`ooOP%7q-r-l-h4r(oytA@|8EuOpBIYBItN$cw zCO8q@eiglR1y?;p4MyGhNjGk=SOgfbp}&U>ffObXi#DFaM3rs-N%upgMU@8a`$=cY zy?2tHNj1S8{M~|QgZChO*AupTs`s6L6s+g&vV+%dQyBe|4&!5fw5iU3KG@Es{h6>Q zUrz>SfRmrWV(bAGO-*VsCgr!!lk{Y#b*+G5Qq@noMaU~5YC-U&nK?iby(NvSMmboI#K7xgfVo3t^q3%OHw3{d#^S2h6yti zvuWt~z2A{_e(zk3KvE@%&9L(bcs)q_Oq3jebzBH;KbmHV^E(l@SUWV94R;$^d603u zf)7^8wHm86A5*_pChsqFIv

MNVA$nVv7cbTb%q)D3n~DQ6L01=Cn!-cWGhNutrKOm@ zS)SE3LJnT+udtXq|8b;44HY9&7>#-Rq(SE^!oky`U@Z5<)R}t_qhabZTFRS*YN=8I z($ujAzxD9w+rJ{SUN4R_Rh>f{oU<1HTv;RM*Re?Q!DvefoKZE#=$JV1nqJ?d#YSGtEn(N#;^Z_AOt6QG7pCt|3FfY$<=-k&%67B=a9 z&w}ne=hHM9r&JnaoZ7l%Bs5}N4>t{=x392~PB-qF$it(Ilb>X1F(i9c9@hYAGChlO zHe=IiN?P&Zyj8<@Ox88JzTMfrxEe1B>E^M0CctFQL2~y`B9*L7_Dy%;L#rRup9-lp zSv*O3Ss0Udv))V|E7l7wtCxkgSyA)a_$YUP;uCi8#*j|Hdz=P}d&EUU)ujMc9D(D@ zfwb?l76X4W@33@@+aGJGN0J|G`=wXJWLqNp&C=x0$yxQU?fVPDPNkC3k@M5s{Wn;? zmQ-`mMy=RwNMHKaeM}>2k_GN##jv0IN0ZIU44r$)oYv`2X4weVeb-f5ypd8fXK95I z#kR&>y7I5%c-wlFIK_w$?;ekn0N`L=vevD}di3EavFp+D|$GbxGu@Zf~T5 z71^o13J78ibzdB55hg-5UEU*65QM{w)PC`SE${TV6tinuAlJmn;MAMDotp=a75+9h zyQq4;P>Ujqr+bc==!BZChlh1-GR70W_5o$O)|R>S=5j=FKm$J{-Mzy0aH#qjj+bOw z>2v6h^7J{hcwkVOwA9{_StD*j?ROZ_7VQl>vw-G6E`n*;ZiMpCCNA(m>k9umxfs!r zJC*46`;gWsk$fv{GoK769EG-Ak=k|T9go&{k9~ar;a)YDlsfL}m~*MGoV_cI87!4D zdx(pO!#@HXA(8#-UkGiIpZCiE8l<*?FiiepFlAE zS2Q@zgTro9S*j13dV18l+(Z<7P8^GYAyK<-kZK(DPh4p3@k?YdE(}PZ>F^)XdQ`DA ztT}A>@05u|#|YTwT~Hsfj$=XpV-e}U{c+}Rrr5cidI}#{35YXIfKR8HU{KGo4kvZA zv37d=5C!`Kj#9GVnH=%~(ECXztOq|~cZ3}Xokk-~0ZXa~S{X<3Vx>gms&c8Z^PIT- zp#>}I#>b(f9APkI6jl9`PJfYg8Ta1#1WONUJy7lYD^SXL)3GA^V@nXH?nxQ=Gi z``}Z3vEp}_2RfjIy?UyA%#l2Ko~qNBALmr8C>$e$-zh9#8}^&bm|HbAHk+6ONuL6QhgPcL26aY#Y}>NDQ8TlKaYLa z9{gO>m{n3^R=NDdf7vJ76l<+KAonBNN-{N>+)&gIp>4NiY$`IYIIXwqy{F{&qRhAa z#FRGby%sDve2w}SO+ouTa&6jx$E6%(T6(w;odX%&B+MqRxIHMH%6w2=Gg_uh_HuM% zE;(6BRgNZj3+Kes^e)NM0%wH;JFEPR0mWdvBsFyI4|X zLWojp!#&N5E-gbmpQGHfTwrdHXJd_pci5NmmqUflJ?;5M>f@Ukkl!6L9ChD*PQ9z& zRM$4TQ-V#L*zGqK8N@qIC{!*H7*3#S{6a(ss5!HqgcW<437qu zNAg2P;}+y1&ZT@>l1K|ffH8l${B07HyQFVfq;J-%>N4auT+|XwvvpkcRo1q) zx3lm*zU+IQp2NNGW#%PM{rT9^*2O`7HdFeegGiPWoG~O{vs|i8tgst89J9e)%T=vw z=iwnwj29ZbPU={0+;&SRcZ8J#ITkEdYs?;lUi} zQ{ce;;O{(lW-rfk;ZV87ol8%TuR#z}?zA3z=K}nhh-ux*hLiJkdoglJUJm`0s9S$N z;ZBL|dHmK+fwL5)DRkyesf#xSx|e)y-5ayL=i|Ly`Y!1s*__QOUF%g}HD&SCYt;N^ zgH5nV+?gS=w{e(75o!`OH}t0W$PE{Bijdpm5eGjjQ(HVAn#-wvnR26<%TLp7((P+|ApTXYb)b1bvR%qeGHw3)=iRJn#k>rsbp8&N7vIhk zNn^!D(o;$XIr2+J+Tt34r5d7vfelV?X?}^ltq<~*`@KICjVz)bRr*$JSH-xik4_D$ z4|nzI%coX)yD9ZgiH5YPqdWkA!Vm}Y=Eqzv^{dFc9l@$69uyHlUeZyFQZ^W-AwE`` z(PtC{*Q;%bpV+q5?A5BqywfX>$zeaBRg+UjK@+bv2PwpJ3sz zb$wWnr)gqnFIdjyc>Wbi&z`K?a~OOTfahru(Ba2h_0b3Ez8=3J zUHJl=@7J)NpYM5IuMQa|*5KOe5)4KK=cX<*9s>s|6-mrfT{Z4YLw)3aa1OdT45-f%;f!l+q&P5)jN9$*Z&y0me(eDVbj* zBb)Ey(r4#f1h2(UKb^E3b$3sg7OleV{gvu5r>3wwGts_4zSPloF%&-J~SM(Z_?I19Q}C8VjCqQ>489#5>BVAeR-L!+SL2Tx%xe!?b<$(+jt8nkZ2R;#Jx zKnTHCIvu@`O=HLim9BB*r{T`eC+8>eeP^s{>=-gl){HwG#77K6P5iCBVGa@e-xK2v z;paMM?K{5O;7+~Fvku#Za9dpBcENBQ1is$%wmT z`*rQ`h_}1)Oo_r!!GI^Ze>ifwtyp$pHY46J*=efo+;7>Fy(G_$9-g=k<6=XMs!6hV zZ&CJr`$g4;k@vI7_S_%J=4HE{WCi6;4k@%uUhA)xf#99Fe=G{W{OIU9{NX);!LZOw z5yRlq*_*A>Tl=&_##dPMd7^W7z+i0&GQn)jcjv9x5m*0F z-0W}1;MC`I6qq#>P8)(9{2()ewHhqGm;eRqphPYh7%zSb>`o#T_HpOHsSuI00_@3B zO&o`|9sa{W2`x}MSp;hq92(AD&M!iLG6wZ~6)7NrC1n6UUjc}Nr~W4N@f}!y?|fJB zPdd0L*c(uIG1+6MR7rYKO4{w~%DGZQ;%qIk03xhOPHm-uT;WPVeh)Na^d@`-I7E1Z zma+D{QZM|fKOS+e<~jJ%F?3rzxK?RG9_=73#pUZf7b7m|u(IjInFk~nP9SpF*KawW zujO$+w3)#};<2xbQ}J9;;qb^(CJhuj!obGZ`vz3TOmLF<@EIE_*DO1Q3thZ%AK`3{-F za{+snFQST{Q+ko)FJm6oJ!fQ5#La(8f}A{RoGhU!@i2MR)hI$TF{yPsz@klp_zVxi z4{0WS5$?!qIHAA~6;0`7vWd7o}d}Tao4yli(4WDCs)jM3hay zan<4?-gPjGx9}7KC!K!5kTl)e`^37DZ7Ja~LCI42Ik9)q0_xv&o%!!qmRvc*j`X{X zU$k?K_$=R~Li0B{{s!eZKWTbxN8(Mcq*tNHj8B5pjNh_*zb{Z8MYdqwY|*;l%(1)U z%nLUzPItgYJb#oJ$7Ag4reTy*EwTaecVx8*=}!_Tmgq!&Qls?k zZu=?yy5XKs4_|{?YTXJxUmL6x(N)4Jo56TD=K7@Xov+-MRIT$270_ZT0g9A`;3W8P|;c87HWta_aoTo0ca`*?Mn5`i#;8)l4jG+!%f{KE~)$%`jiF zK-EC7_Out3KsP73?nX512(ToAc^?H-%z*)2dB=d(;!fAbeE0?ipO5vy@GOOQ9sF=!RRd0&T;R9@qDr;9$Z-ah%7@3WrTuWGJalo*=@4=Da{z;MY}DoYFT1Ua*i z4*9O*9erX~xS-9K`?BoqS`~9dxuP=kxuNWjGkmSQmXl%|{TGtqidfdNZ8+`xS}9=A z&ZRCxjlsDYOwBJ$c2bRPNcH%HncqDk)9gff>xv18$eM3H7R{v6?c!1ZVOaNN2m9K9 z-2y=CPRK|nZOK`F<@pOq+`Zb+p@fi7ozP2Pyj~Gzqp4r9o)qd@_Vl(%oSN^|bxm5Y zsp#o_x9uUXw%?2IGD}cyb*h@3;!&&dY~1K-n%&D6=1mdc(XZtx^7bp|HCdjh3(c^> zW#`s&ZTb-5Uz>?k?5k}sc5ChP z`{|}cqu4jQvs!Qcsh5d zYzqG4(D#2(-|=yBUyf4uPmVwS-+uhu8W=DtN;Bd+fr8b`N+($K(A3I9$;!0*HlPWE zo0V$0Ff>Ae{G>~Y!RIda;|>fHV2x4#+XIVXp<3N_>=C69_V2*2W>9q zO6Xwx0`@1J-5NLwIS*R?B+;DZR8%f4xmQWk{@?ml|K7Ix*ZJ?_0Idjq47U1?=|6xU z+yHbx?LVZZ;l5A9oY5y&(DbkaIuaGlNQO|7Rd=ZIJ_c0%GIajm6VGcsEaR$2^qt!VJ(WdSD8JJYfRY~+5|WQ$x$Px0Nr zpoKU%Q8^<2N_h*HKXyY+Ib5b}$9!`*W?-h*;u9O8Xf)!OkWQ#na<#`~epg&|`kG;U z==+G!$NL38_F}0FqwnjGUzRp;2-D8DLcFZXdcqPWW#g?bQXDnl>1)3wHnw7v%8P7r zQI>+{8>R5*=%&r-&AiE(IdK5|z}!kxFv8_B6$g8QW<#k@E(0HrQ{oafxya_0!!%h6 zH{e`MaUq!=)OUe3LpY8_dRs9L{xMhzKohO)(SA4YDI%5NSNRYE63WB-Q^=}-x81UG z-H%mMr0#KE)aX<1xPyeci*QSjk@tyS()t{0<{!4$F@uB`v(z7b!$WJ2xuvkbb^g$J zCasmQ5{pFe8RguR?J&5G|LiCn+9AqA0CzeW7h4I}7dncgp~Aa66!s*Yu{K^>M=nVl z_X>B(K4WMxH#GPn#e!TPS!4UqvD1zd+=}MeUAW*Z`?tKsVI!MPoMs6gEZNU1Ta%k^ zZAC@js1$3%vgoU1fo+zr2q^P2yGwDZc)-_-UEIVXsvX-W+&{W(Gr3}PVh(ke4kcwN zxAVBmkoyZBd3}wpSwSQ2(npo*5nRS_tz5k&`NEvw3^Xv7Vp?7WQ~#%Ih)Xc=sH_Vx zo|w;3)Q8({B4yA#E;Lp_sWH^8)9S)xSU3&uU(`c5L9AIBFm3Qc>m8mx+xnvAcWt=S z__;VC#CgdeHUfo~kx3y9 zfiZcZ*xUuDxcIZa*f7H`BB^cAbWvZ)`nc3p2GyI)@nK$8dc15X6HOR|kPKvxuT0JZ z&(*;@WzP++3r@4)@winVFtf}FN9h2$?YDDT(5rW0l$*{F$}EJm!U~9{Q7>%lP}Cq` z5B*17KJ6+_0R3ed0=V~~e}uw_On?u_aRj}Xgc%i(?6?3r%T&^{L#ZKZQzS}WcW|IOjj6Dk~~Reu?O#vP0)RsewTK6 zC<3;BC6^{poj`ME1$-hXY<;vy-y@m;Yc#lL&wv#C|J$X5kk6u2!FOf?r9E3&=siy~ zJ`1*x1K+<}jw7w2z`c5TIsu5L_<~Q&bTpw^8hqgb_yH9NL)7!m;6-}|4qr1vX#50l z+t~&2{sKYZi+72*pL93I(4FRs<;VPZP{A+-&I7JT{Q8=HeNDgareAl{|Fw<(+Vp-s zn|?i;ew7ga8YP6lP{%wV(zEGx0z(;)AYJe<2*Wl-X)%}8wduW;W~FF?`elT+Yesn( z>NaNn>}c%}yQ|eRns3~<&dWnCS0Zt5c`P|BQJa&UGSY)2j@|w~g*X(aGH}w_qps>7^au%OJZVnh1G^>fES}-dueaRtOJ6n}6L3`$ zGij9oifS+eI;HndNH3O`&0JK2Usas35DdEyy8~lg`C;Cb8xU;ef(By;0ns_k2Qb_g zecA)ljH?YlTsHtL=Q-H=8=7t_?o`2edU!ojrs}R$3VTLEHkqa=Kep6 zTimbW``^wjzl!f)XQW?eq+dtbUmb^E9fx0ypZs$iYam7$-R>h+U1`ApiMxh%>~CGPyI2{BEA4S4Ori)yAc2hZxJH2p3KfWSTqn=u#gPgZlj5pC_m76kB9zOs9h!GRkMN%d9xE zT#36Ur`DwxqwU=#)|26#v=nVDBBiG71`KVHsE>~x=_Ureb4pEdQ?=Cyb>4`0Uaay$M8gi1MA zc(<3Gz4h;tQU#-VI$;6HKj~Ix$r3(EMmnP{P6K|yh_A=@$-#HSGrAU3ge39HUMNzh zX_r~2eWBSm6Kv@7JoZJ|0D2v0sR)KM2_{X0qL84S`~k|0?A)Cjr!Lt{PUw{7vBh*3 z7ujeDl!E_if;Hd1dz#_YV6DgZ$8gEGR86+XfElCp^Ur%S!VL|%Ya|tZ8S5}iNV}1V z=e}v)=^3|HXVuReYDsCWs?Bxt8jn%#PH!E3P;Myv^>Qgq$&1%kl;0ag*MR6|9mO@{ zxcu{4_fog&mcA=Zc;>dvliXG)?-fA7PqiD62{O9Tcm@D=5%)bOf*M%Kv5&_3N7OOm$O@cyKetG0&idZO*43=w{oa{hgaFIFOEoTTY_FL~35HVDZ%0$h?@K(*Blr~~4~A`~;9qBN`?+|rDf--&@)qK7lKQRm z-@VcNxC<1JntnkTFao_LCn(*Jb`s@}+>bFuR?5+Nq+5g4$`Ni9OPOe4<>6Jfjk-zR zduo{-bye#Z^bq{v7THfvRsQuqBWV9e5&5q-Xt&B>8=>>Z(gnZAhyA3xtj9-dxALPU zYWxBDISTH3qLkPiArLzr0XOxF5Ir+1Gg(uHnyz-@|N{7HxX z;4tyw-`Dm(Wh}S1Lw~}*Lr;b`f-z)Y!x_8GzLyU#pMHZ9GVUG zWasP2BD^ahB$u0}NKpZ1NJ(ZBHF%Qy4};hiQ+ra3odFZF*?6=DRLk|9iz@qTbX(?K zO+}(>h;2HUq|2PRqg)lhf{Bej(-S-Ig-r@!Po`WVDUBFCFq#~ zlw62G)A%wF=eWqqY74Lkh;%lzjxCXc+VVvs7DqSx-DS$*53Ld2@xm!LDjzba8FVLv zG#$lZ0hc_A1>jOb(X1P_pKyo%_8!>jI8KNw-4Bd;x+8sH*&^vuq*vDUVaQp^+;d+0 zQC*d_F8`Y3z17X%M%L67NiSNT3gMoD^p09c5-klO9eG8m~w5q)Z7$pz=pK47KwY_9F-wfZl>#| zdF`>pUl1RZCa_0RUO4D}>q7QeRPIO!tG61JSe=-^7z}!C+m(5%6?2Tr-m*sd#>wj* zx}aa2De6o6;qQ_15*l*&j#`g%HuwsD{}2Y0H5uEZ7M%2+@)W?5jtI*5ql&TT;<;uM z!B^K)4><*|%DeIornzvvYPsdCNYGuC6Y5m4a`aj3{0M5o@`41UOx)Cs=;Jwsreu~qKMQ6?pX9q?_5+Dw*| zKsAVZd>XASUnp`O9P?4Wnbx*Chtq*^qWFpGcAqj=InBcWy_R7;wBp74B_G+_ASffi ziHy)l>da*Ty?bm#j|lsgH2q9C5JcQz1%@V7gE#?&ERDcXN& zk|8vlYX0Z9EIB0)fI>Q=hJ%9C2TfgcZh{f3PfzjTb9ZG|afghRV6Mm0)l(!^8bc*H zpWwU+-ezaOE&sJT^POOsoTSsmM_BV{k?n{5#A@dX9H+|UU%kEs2zNkwU0Y|AYr&hlq4qHjiTd0`dzN#hQ&8-1Q_Z9DQP^+antc=+8wAoEtr zKnop27*G4lj8Y)8 z^~9Y(QNrL0gA^NpjU3oHCr2LY>WlF>PwQ>tYOQJ@VZ+pgDSwdIn z6vtbXJbfjtZ)QF;{{jX!)1VMGZi$551&<~yZz4O@oaRgA~K^?*evYL}wq z-f^YQ=vINxy!$$I6i^EO+7~%bP;Vv~6P@Gsf6~Q3cv0Z1XGR+VjT2`*Vs7{xj6B)B z0oFQexokD*EzhPoGkr87%jg@XP2HO-kZNX3Q+tZshSreB^*M6pmyFOS0>|D*%1Id3+D zRC-yW?at_wG)HbL1nkLBumf zTQ-}q?JE-+nDOk=X3^hGPA`>|VapJ9w%v)YHZXn~aYOlQIPCPZeV%t!Uk z(HhyQd9WmUO#;x}Kj~@?al~>KWWwUP%>*!??Bp1Y2m6*=8JclY{g?g60;y&9lL=lF8ijhC*^lhVy{X z(PqTSU6&j_2uXh@3|^H6J}nqf+@zlFnJu-Qg`+5#3;fjA)oDnCJrFr3WC0Z_2L>U( z-E}KM20$*|8@AgxI?}^f(*G~^-aD$vZEF{g1=$K11?fU;^d?9bi471DQIKAuBGSZ& zfJ6zrN|lyalUr%7zRiBucsGT&8O*IvMH{)MlTzZ8JQ6mHlb*k$aTae zwe9c+h<<_;je0!?@%Awp4BmM?w#dMAx(v%LILih8?4nWG<8S9f_RL4ct_(Ppum+tv zFTG{E5fd&mzEsAVC~bebZQyIk;5ptIojHg>8X2DB0%Hfml|m<=`XAtcxmq|j_glyZ zj9>wZhKw%7{+8zWen9R0;H-~r!+@)|&Mgz`F8`Lvicm1sp|qyDPy2J#R@J@y8(1WI z--Y5n^hhM8xcA-5P|f)unD5<4&2@^uF|YR#3^S8alHi6DO{30seC->-gjTCFFigP2LnEgvT;v%YF&q^J zA4R`|P`HE&t=;y8_POjn7`XYNEWexB;7#i&HK&;yw<<|$C(l|$-$kqXlp-TTK&%c` z1i^0$EX%>tS3;|evT$g48;agWg`zorjQvKP%ZGh7jaV-BDjtD$%+?;Ls7!69 zmQ6gN&I@)pd@l2T@Z8Kb)AM()TN6jLzn$E`c4ixSptj(_6mp)c-~)DfU=XFm$N{x_ z^Zk_Jig{A#5;;6MU7~~)oO29lQ+<=U%`nBLcIQd@asg)WrkBbqqnG>SyN8y(@l@&T z+OUeHht&j)9x>%WpV!hW1%RU6$>a5nu!!;l8rB;^o zsqIPca4xHRfn^SJyP+0ffpAY_~QR`-gwBf{ii4MR-2#ZV(W~3I|4d+;lG_;`447N zFmGQMSF4wer)75?H;pdxp+0Lk%P02nRpVC0w*$6^jr|CZHhxARG7yOOh>Hka#D@0> z-+M=eua&X|v{5CVy*9}r@Rf(FA~v4~C=;!N*@><91koDCt1p7zrM!MMz8!xc~^nYNaT>& zsm##MA+hIWNfm8AyM3EpB`@lJKOirngMISoK`P?GwQGrUgUa*g`gt9Xlrz-&g}aps z%Xi)0&^PWY^1kivvEAo37AB<{8Sr^td+k&j@3*Z^u<=G0|Cz2P(JO`Wv=e(=_fRX& z@!&LrWd}MX`FzyQRuoq}a0_kalk_OulXP}GSrSJ(<@&&J!uF)i=CK`TJvM$WFSmC~ z^BJ+9v^JgCxhMI`v2fEX+oX+-)#sP+?B@5+*kb>+-}AFd5z*cIl2^nNpMrU6***JD z&dFkex^=E?JKnmsbZogRE)A|73Np;zPIME=3Xm}JgQQ;U9QtO2TC1F_vTOKh9QEVC zLk$u=Z{!EUG1##FllQw=Gs(k_v8yLny1;#(oBI+sl$r-BpCb6mV;-5R2X2ZRIk(By ztV_^z!d$;mwyHAb$BcDpg}v(a5wXyTz@3q)v>k|;+x9Qw`1h+KF7lQ|#L6`IxOsf} zPJR+7=~!<$aVBiSNg=MFBgv}j+H9Mf9Sb?7TbINEBPVbCE!Cg=K(i&8g7QlF>|hY zoDf!KFr%%3ET{?XW-U-sJHl&0{*N+%?B|W($%Yd%J*uu2Kd-4_l8UAR*8)Z=yO&?7 zj8%184v80p^S7;%b}_DE6WtHKzvOx^Co*lD@Loq@<7_jwV^e^fym{HSfc^3V2Wzr! zz35+C;h`I!Akw*KI>eQmid4m(nJYfJVHa~@H8|=(=Gip;N2y7tOUK1s0`@)2xw3SP zD_#lS)CsB+f%oHL8^|S56w+2!-pXhPg@r$iB;b$GM`@(EAFwKJ%b)|fOZfl;6{lig zOEc@>Y4pi%$#eMbJzmQG>Ekiy|6Ttklex%Qq=jfuta2XH6g%zsPvp)iUOou!(Rvr| zuI6rhA=kKO;(5$=D&OiZTnT=Ul7#zF#nKTp0!x-WZ_XAA`GDesg*ggDTGac6 z`$L;q0V5E<$ABEgBB~cwWL%F|NvmG@f(tsG(0tah?PYYClr7tkHqMN``t!*Jd0uO_ zOh_G6vhW_-#t~$|nQVPL?-#TenGY|(!i*sAW5lbq=s&^C==2#E8G0Y3owq(+J}qR= z;FL`joXwm~Go0!5asIKdx~G&IeQSa5_O1%{fhJZQYpzVEJg z2U{6#*D>5#Lg#6Ij|xk~iJoOW9+uEtNARL4gsnP}$|0(?Zv$xVtTgvbDJ8b{U~@c8 zY4*E6D^JqDmt0mxYG^ z-RP9Mp?!?n`vYf&J-!SyH{2;o49IO-JJP&noG^c*J=-aSZK2)Cnb}a0AJRrFxmrWN zv%(DO4cUm_(+%@@iR!FW5P262rExac$e%7Vcjo(|7ZyPB*vy9qkUi6&KvG+fRLplN zFPhJpGinFCBN3#Lj(!uSD=QFHFJHYCob-{V$8a{hVHl>-4Ol_c$%xXohJtpiucr;% zeCR*C%~chuWl*sOtAuEDvGj-8cui8SWJ;j&h7QhgZZzadj>YWKB*qhWL2l%`s2#AH z$JGzh7{O|HddhoHb<583jNG67!Qe!`M}@~oXljO?LT(qGtaIKvtXQE zJvIVpO~<_uIowm~aA@^028xPz*%L-3CoB{Br`A$pt9g(ybFN(m7NX$>CjvcH^Qm7s z$8wB>6W*j+_vFpkJjkvYP7@5z*ktz0Xil;SG(wy{nnLFBghULmnN#WFjFg5#G)4bN zP6FPV+94p0j4D;z!FtJevbQXF|4EGx{)L9{Vvw`JyWnK`6)nUaf&X@0YeRHC)I1qQ zp3?xsGTeIQH+d|fPaG8+0_Wr(Kt_O5IuqbTlcSP<1bCM3x=UvTV}l{LtDPIa6n^g( z8Go-Uv$=EcMdapJd!#Efwh`&SPob7}gLF3C-EAGwJO#*9coZ15oH0QFwCb=Lj6TZC zIiSzW<<*2Z;{6a~4S}Za{fwT{%K}jM6CjC%&l|vzr|tK^O}_0Y;IeLEI3ahDtW*BT zwe21tJ#h$BB}SJJQ2mJv=(8V40@S6YfMQ(6CRk8v6C9R`hWY2nEG`1|uUGk(RQ{za z|FWQe`IY~-ysceXPwBgUe(gKS#6Wq=2_?ri&#~VQ@;!>N^~w01&vn!O2hR@jJSDN1 zfNc~BK~v=!+w?^__cXC=9?#)5Q(*-3pT1u19@iSgp^eZ;HfUPucn=_h0yh!VXOJRO zAcdq#s@?BpXu$Ft9Tq%w4Zokp%kW7i&HZe0=A6d2l~P~i9Y5`J`k>__H_857 zJya>qt+C(6K8*D6YSQ$`GsevZs=9UXfN*fNXMMIrSwOr({e|vK#rWel(%xo$cpj3A z7wZFWJj@4qLiZsl$h#wYxHu$?y>q6RMLgfDH7|Oe&WsOw&ayBs>n2hg)_CKDDEjF8 z%TMO5*!oIi0O2nO*%$mMQ=5BHQ5~e~Jzo5pT3}-z^?qFU+RV!Uq`R2aGsV*y`LeaI z^UgEN6zX(7bx}IQ6VFe4ImJFxn z+hmzNFq_rNGitIND}9EXXoLA;*>~{rTrgk}^gnSwzg$kUJr4sIG#z_p2B)8$U&DqEFJ1uI>|*0X>j=w(CMyw`2x8||;Ym)Mz8n|lOsJStg= zcp{zkGh>s|3|b$dQTzY;KY23v%^hL1!4Z&H2dJ-0wfLx{N#>daqKQ7qiCt6UhR#7t zsrrb4f7Eg0cBdo85(n<0B{@E9h!e;F3EBZ(bM7+^AJsyF^{W)|CZ(a{KCvXA@nr;w zHQ9_3GgRPcv7Th$_4^Dixl)M&vsL)D$$C>Q><;JYT7l1{9v2yMgv7cva?Db20A7z~ zv8_dy*E$0Rdnt|&2&AWsU@3&ybp&!5imyep3K>>l$9EV6jt=*`(o*Vg8wvoYz;7rt zjTVBaqJ3z0HA%c>r-7#*Z9#|>T`lDMe6}hoL35thl5QJ)onx9c;Dx#LCg?7*677bJ z*C)&ifO@Sei(EL>Ylvcq#%dmZ#j(now*1zSWyg9)x$`N9iWX_kG{-qGeKv4RGrb;8 zX$+OP7!qU>ofB_d(cCaRJe_(yhavsjvWj8zxdgS%XBjffFGM{h)9&!7NqkD!8AG=} zdfm z+YD55t$4Yi)SGVq8dYcxTGv9T7eGCuv_o3>venmL_tjiWk!_m% z%5wXPbX$(@%C|8b$SKKbGv58P`mGyD@r$Ev_Lra4R*mBByyL=$s)uLMmf09&6tTx6 zZ@K2BY|fh;uj@BfZl_M*2UM;tnKYs3i^E^v!4Z%ML@fm@WYrDh3WQ%06XZ6`vQ>j^ zLco_oJk)gr*8)u+M8Ph9eZquu{FB0b3Pg1xui8Nkb-&hW)xptjUPq*)xrd{%9sPVw zDf;8(N!0d`P1J>LxUi->&-y^ug8$TW!by62p7V^)j*6)KTGM%OGC6-^|C?qFgI}vx`lK<+9?^!*Evp%;~-x$Gvda0N$HMw&w z-8sHpskO-2qnYn+jzrS8+lrqobK7KEHk0IXjIThHX7Xj^DidfPz31CHVo|{7SCK6i zJ%!={!w9>f)l2PQ-q;F-yvElK(K|pvW*qP;q%`+FdUDVfbobc$ts_2YLJexPjy2vo zh#G)`?NAI9{xD!{)nG~dEFx^@5A$=Y(XK>kR{jY(HmYD@yq9wV*X!UPh~b|oE%)pl z3VQjo$nA|C^_W0)j-T|YZ3peHPc-2I8Y=l!@K^UG2x{J-TfL&jW$WF1YpidiSH2+g z+^nW2fm71-X>uW0=ByZRy#<+u1SKP6MENUAd$OZ8d^9^ zK`()hhmZD!&|(aM6^VCVM;z6KIBNCK;(IVmZJt`e6<|Ot&yUdRF)$PoOnAGjVs-9y zm+}H-$ji1jHTZ=Ooor|HaAu8m*wJIaCiVy?Hj^wq@6vqq^sce-yZm0WYm>nl7RArEdJy2_Ud!bqCt zV8+Ahv82H_-v>TY7@#tv!!)E5qYaKxoiX)8vd*>?{h@p+_lzT6@5@AmL}YxhVH!?l z0{(*0-|350OOm(#DI{?3nax+MfNr8_B!%`04B@Y`OVupi^{n`&B{LvX;`a%cmYZ$K zr)M1v-|haoAXO=;)-dO734#GixhnsNnky2P zc4Uh}pGh>J|EcJ8gmONX74gh!P!%Lb-a?BdDX=Ub@S$tz5Vai5sRkh~G$o`lFPjj- zv5(*(a2${9rf;#>WO;yo82KrcUJ~PYy8jjl4wG3&v<{E2M059W(57UT(;J|S53b#T zzSVQ!(P~EEJe4htLpeg*S+N$~g#mV@-+8T*e>0TO`sO=`BOLFlHpLx|K+q7QGOcgMY~Z^c(a6zVzE2AdYu8@otC)P8`tgbEFWA}5kIX_QJSs!)Yelj zd+ts@A^sU((&ZsO$@?FtY5PxXz5i%$QeQ!qg=BbEeC9&^>FsAD_dQLG%W0FP4S4?a zL(>@xU&G+wGKixUh~|VjqZxdx5^(W5hUE;B5XrmIe>NM`pPm4I1qRmyXW&I)(er<| z#6Oy5lo<^**v!&)}>apSZak%3a5CtNl&0q4Cq^MyCzF^>GY> zn0wX{L>~;8BIw)!x>oxv{=dtws3<3MD^U!Ogsu)ozy#@%w>B>$Y_V&$DbcdZydo<9 z)o|I9RAf2s|GMYk(oBV_w}QguTn8Q7v*viUr{%=>YF;bV#;Xq+uVOCLyB@@C>%46v zbHOeyc=z@F+q2G2x2mRNqWw}6&-sSl@{7?MX5m)mj;$jM2gtC*J0g6nh{yt_4csS7 zjiZ{ij%eGi_LsFF|NKxEycY!I&xU-Utq3@$zxmuhh>Y^$Ga0M)b+W+$8(Ze1(RMBf zy%`sR6!Hd@6NO6;RTM`X^idJCAdjIh4l}O#xL9-JP;}bG+wOX&oJd;G_R6P7tF6X` z{QJ_it6z5uHd%W1BuJOb&i&5 zcahz$d*|^t)s3ppFgm5gUiC+!${3BXN2?C8?mB;sm0F;n_6Ie-1l->r&G^3(VTS~N zMOc!gSC>bXKys$ak_0Nc+GyW5&@3>Z{t{FuPyJ$=a+YyH+IJgDXN6-$^<;!^^{E|1k7NxOZ$NN~ zwsqowy!;I;$bqJGfH!{0V6y4oHq?8Oo$wc(mL$Shi zt&n}5_RwKI`_spFP4LEcb(V4kU(Cnxn2+lI2kksM6dSt2^J!kjNizEJpAN=8>%94J zJf^4YYIZEZNIW9kXdl(tT)I;CNMBn_o&TV*1`ig+ zkIVCo8$GdkRJX~LdndWmlvE~ljlDlPlPmy(-U^4m-~QTIN=FOwOOx>@VR(59|(9EL62`(2JecBMPfHcMRRycIc_hk6_%&R151;qo>X?UicxI zy(~>!+2P&Xv3<~N_I6Prh`qnDvhzJvvQ^37A(c;rCQL=joZpCrFe|Dqeeav@y?qAi zdEY$-3T(*y<5d+2YwgUlufH{3`O+bVeWxf@poj2vVxfW2p&~NlC|~MNN6J3)>b%)x z8U9SRh$N~>W9J7?UUwhGZgkU&)KO}>Tp~}Si^V*4I_WuX7xOya;cVymTyf(=4%6~K zfMV*<+mYu}Kyw?di;M}N#RILpiPxO1$P zp(Rv5A$A8>p!xKyL5Y8!(I@0kcuoI-p7D<$X#N?;2mBF|-}qy!y!cn(kNP7T`D;X$ zN?Vz4V@pim;SJ}Q<{kd(S*!h~rwoZ)9p+yctE?>jqLoiZ4!0O+S2@>}DdhHB?D9OU z)Fx5zq1G|X$sZg~yHgqGB9SbLw0ybEKbNKwPkGf8@QSk2+}J90qDHUrouB>GTo2(a zDSr2+sJij5bHw0d+>-+f+kWTx?5S1R;>YJW!LU9b_ zn83Saq`;TkK{$M5P3D|11o$#zHyReDqE|1q{11ToKwEA#*|p|Sy@;W1DCJx)9vw`J z9scdW0EQA^c0b^qz`i!8bcOjcyVAFtc`e8lxF7p|x_XK~|KPu`B%9hN*xhvDeQSbX z!oGA7v1Whiz73DmbrUFAXb+ZjAv_l4G z^2TwgzL7$Fis$mRu-*#o`t&m0Y`Od?&}B%ehb|G3aGd|Ku-7F8;6%RMFHf=0Vo^5 z$b=zD=<~2Bh^^0>f$c~kBhc58Uk958xP(XpEsYt8iI=HiU+r}$&d0mvRbEb~RF!?a zJh5`BqBI&@Jb2^FlgSwE2R4WHCI5<@C|^Tfq?K?qxbOLR;>$d9*MrM?!P$0}vEw;9 z+HYr!ImL-3HAzLc#^FOZM7-m&?!LcjSH-HDDakTn>qwdn1Z@-c)e=48a4tu5?p1qp zy03Y@v(wL=AbNreO-^mbVHzLr0bhx9nM~aV_MLPGP$=D;azMr-9s)iDKmo!lhM??jv0(=EpH+IzB@323dCreC>!2NWi3;rsCOh|(eiv!}#KLAZZ>{Me zUF@3Q+zEN;r0lG$Bz#QtzGmK$V0dj7=hpD`j#ja-2xe}|GT$M)*r)dNxg^tm{p$&To6(=V;dlQtKJO zxTCb%y(t4}FJGo6hvj=Ys6PLlqn7URkbSy3wVrYLiSlVPiEoY{_9va~-@)s&$^IO@ zFGgKco~CfFt}0;Ks?faJ=fF3`y0Zc~nqA`$tGrEKmg}pB=+U?EiN@5~HeU6rMG9f~ z>`fFJJ}H(<7rg2@pZe&+^PH_R2MT)gribh?f8wCORPW#CdH}Tf0}Hzt4|WU1y)SGg z8I0Ms70vMaVS%{9VD2P-3^sB2Ba4v*?xotId0~4ecJ_ARLoz3taRfcTvKl;THHrq- zcbG1Ef1z55rliCQ2-a88_I^AcBHk8=AlP&k1U%J9+0kEp-J$thxhG;z^L}7H9|qsv z2$5o_qLKVI(g}hWy!0Q$CTl#C^v~}IQWkx$IM!1nPffzw)fUpyBrhOc=de4 z=lHD`E-V%poszjv>1?zt?SAOfN!rp`^J2iKwEH&nXJl1G`ZI(13vva9^(Wf%&l${1 z1_~rCcC&1deBZp74&lBya(%C~`j;~({_$sR&%JD`k|Ih|_@uI)dWZRDyYQAL3hqHQkU3sQPKZ`tG^~Ap@y*_K^aOCT-q^{5g z{>==rXRYcV->m7Si>4?xn78q{6yDJs*USdg-QdWDk!4f4E8q?y_rCyd@BBZ}FiI4> ze<2E55={kuhzR^p+or!f^w04Wb{+AHx|!1}BCA~k++L|h)7nooR9z(~;x&?f9C$~6 z>d9B2%xt^rc5lD_c+`UfF(RY>*~lM{ZM6V~Of6Cp3X9;EtEji4TqsD_NKuSb8eofx z|IC)v)$cF=!vqbtE1Ej0hy%BGI|%<;DTaB|J6siud1!3 zZGPkgVk27d|Dlux@_!;_@rA;^6VkLIy|cR-E0z4*UrMQ=N+&{12F;%>s#M@TVDqlP zlaXCq`t!=Yx$48Ynibpb?R&5VUbb<*;WN^clc@ZUPQ-0nyzlA-k0eaxT5ay#y$-81&c`soDWmMB<%Beg`?dAH4#sc~Hg-H)!xw$b5P#yOXRH+T3c7N}GEtyha(qX_0dF6E|yXH-YpGv9;c4tzzLD8*@v9M5EqA0aP=;Dpa zI8Y+kt_kS6ZQdxT5d`V`PNXPb7)*TQBa>gAGT{6c3$j+J6Jp9GywQK->lbc3_UKS* zuna47@!HirV+!bcqHUs$V+k<@-gbz+lcM~4O?iQ|!hz`bEZdvLtZ!KtV`VqHo!vf~ zUhQ8vN{te3QBV|9GL$TZa<%R=A2q}r!cLY;Nmul-7sz+siXWi z-u_xF_3N24p5r*c+r5NtyvjVUYwT-zwlk*0;pOuANr*c72M)fa>W>&fS(p5$?9Pe5 zYbQc(Zmky4!Fl*Asfg-W{HFmHyBI=neZ|1e53{9ka#kgs0keR-w=E!X*aqgT5B-zl zt3gwSOSBtJK-xH2N!vQrTp#{Ec`vl4y5RE%oh+We)At#x!s7{o=OXtDMW+0s@A(mh z`oh*v6JlN*c;3_fur}q?!biOK$eE%V14A{TC&t-M(!3}hevl$JFuW9z9Y@jbPEr0Y z@!2oTr`~1u>je5Q0wDh})vNpG^wwXV00iz28P!dHcFx;V9J$qyD-k&b@95J~QaJ{i zeRof-tWDfy;5Cx%-ClPE%v(@W%>4u%-VL-?r6=N|cV%e)o_F@GDbLN%^o3|&tzv(L z^Y!|VB|c|58@L@vI{irD^1kP*eH!3Gu|eAsAIyn&!lUjRFVg$3tClNiT=2W5b7Sp{zC9nXNEPf{DKEEuyYfw@={8F8VzDkKrbxC3wKZlf%{{ul zyT09)shK};FgH-1t^XR|QWjJw;bk}qGG{3f(3Hb3hP}0P0aka}y9{cMfBMg(yK8z0 zFFZpG=zgB5E@?TX0~WEpHpRu|rJmMX?pj~**ngaACYTwGcxZg+sB~DsIzqY+wOo(o ziga%ah?TsH*#4^EAg@?<+pg6|mtU+rG&Nn-dB=w5w~Wg^a(Y24 zkA3tk`1UPwTaKf!=Y9E2D}%5=HK3c3-VoD=ECem9u`XeFX*!(5&!C?r8PL%)G(s01 z5QK*>N!%E9;@X2ybhRQ!8BRP%LXTbgf}W)~f%3;vDx$dPySZ!TtWNKkGV_j2R=}p6 zmCC(LKl7c6H-6h5b4o7#+10!1*YHx*HysZx`idFYHK2;Y8+wEa~uPDjs@r` zTs{X;cpGRIZUoK3kt{Pfq7Wpsc|gPQWzcYZ;@6q@skJeR2Q0 z?h2@W40(<)8G#fLu@nWSY4wR0O^ap8i3QEroEL8l+I_qOvZn7mD6>DHvNuwE!QC++ z5K@%-J$I`_-d#)&dGse`m?;i%HD-5hD7}Nq%;3jvhdz*>uOq&yf-IG3R)#}C$5#TW~)dGBGk>9z^{ z@+f>&Y0VB6*@jpl-bF9&WL2yqt}+-Pe|ScFd1Mk~9$TTgj8jCx)}Ez$bz<GY#oiTD)-ZZ%}aoZFMpZ6wKJ?_eldZxX*wHH{y_7qZfB}+pi;JY(XNT|HL!! z1C$-Nt?xA7HPKnig=T@6ressU-+As%H{)n<2$^Wl;Z>zI0yyWh6y$g`&x0j%ILEzbIUEL$2?J2`7PpVwy7`xI{d6#B@cJ+XGuK@qSQAc`kLd!-M+_ zJvIXKg3q?2^TIFId~naqz#JuJAZU*&fZ!}^!>6I-v0|zPTmB8iJ^TFj)f#Zc@OM9$ zqECWTzS-tSts??6!8kroX!1%;I{D|-yP)Bv- zi6zYq3&W+hoOo#j=;;)o>#QTJCkU7$6zIp>C@#T|#I>_AB*(2IhTmY=2Xrk!A6cc< z`}}{XL^PoNg`Vyg_13mV1#DXn6NnHElO4vfe zuO7@m`c(miaQ%Xw8{q)SaHTTTOduf1!nARjh0R0rp<5`@8o!@2kL z))k8+pu2>7n3)hgup?C*W6=JZ5h{_>4ZBH z)ZD^@2wItY3k+-aJ$gk4n&F}4t-=bR-D(RO9!D>uw*^N5iM2Qf*Zf8agASKKqa8%1QFQh0#N8~ z&aug)sdV+-!!y~SD<9+(Gg7?+W}=C8YV@{6!w7CJdh9KZ4A%{T=}rN-mxZ&4gUTsRT{_k{LOUJT_Fj8TvZO=vzma;5A9SJV1zB zf&etXEe9`k6oIAtN%RmPHa={0>n4oL%&83kAt}{_UsjeLw z^Q2&y8cop5Qw>8BP?`s#Qqb2hm32gK&OU>EHKXGjfBA4f@QuMnGV`S25uiZN83DN6 zCh%k!*qb4vbws|C!tytuNc+z;!!0gq;MqWR1Ac$hF_+avKDVie4EMJoABl^;=*`ev z4i5O_v&jHW98+z#xz8yPivyLG3KucpWNtsU|;Jbb=N&@}bN6_mCv%LjC7#l0NcXGkZnnMn# z=FD{jW(st~e@!Q|&7c+v{yJEH=BOI?$amWZ6!fCS6*5z$9-4kkG;2mL+B0mqgc3Eb z=__1(P*iI?R5|>kVlm^_P2U-iCu&ui16Fs%aRdYB5x4bq+~{K`D8bEo!*HRx$yeVB zPW)>$^UMt*T*BJ)Ug;mUAB$Q%156)c%l;A#fG@49oGjQF4~)T8Sq;v6E!XSI%eh}E z1UKjr;nF9WBRxyJEfI)dHeH8FWIbhTz@N(32oxiFen8MFSyo}AVN8>jZ% z@Aj&W+5r%K85(%XCh~*+R|4?H6A#})G~pZMpA!(v*5X%%5L6luy6jI;yEvE>8(UhbeM=MVa)kN#?a4jbGHkVF6$b_ofE>d=rEDS8;h z&RD~JY7K1c*3$HipO^Ru+rQz0jNf*;->Wveoi{ksa-(ymV%osZB+Kraq;QtU*RY=w zoRx;{f~~l#k+xPUxEy7-Yfg&X%cWjL4hJcfH(zx{q|KaO0dt>RnS?FX#xz6yCikU7 zJt>QBmDg&^QD7F<=M-((B30odSxNRkmCh3sP3^yz|9G$#8}PpK=ky(E&zGv{!rvinL|(CYgmq9aia z9Rz?DO4S-j{R^W_WICZ)v{sU(MSnM{7KL}CXHolNVNu7z90DH8aIF&^66*ymg&i?O zQE516Ypq0G-hlTl{U6B$X_8j>+6k>xm(~rP18|@8MuuD?T9C7kE*BA^%mN=#um7WL zgd~bLqjyl81JAS6M~EnoRKD4PlyJN$L)yCRONxEx9&a<}t49ucmEX}W3G$?JU4FDV zJSf<;X_D9<%lufyaHnAUI~vK7UTr4OIT@1Nb_;IX2ub5T0?1z_t(LpD24p#-8TlLzrZR3=@CTS`A;O}Xp8uoJ(RVH+{Y>ZecE zwPd9QIsOBT!_hkFgXMk7{@)K#^iP5S<5j}2 z$@ywlByEfphN|GA&JW~<3VaOor~=oX_JEcOl}&hJjPm*EhE#b@%gTc4%(kEO?<_=iJ9~EEtaP zPAp3vS&xjqV{x~Zmo;t>C>O6%E9N<*&{?bfE#z|u5BK>sycJ{8ny!ou&c?Z4U<6VU z)#C9cuWBN*bEeBpGd*CGgmAcx&0Xm_EEOvbOvUpykc4(-MJ%>%z6W>)Z^Z@}YVm5w*Rgeb?`z z&7mvUunIDQd=(_WhPMScLuL%)w^Z`>w%auH4v&48sPk5h(uzH0YGSzbNeTu4IS`n` z-C;oi$dW*boZ`!RE_3g#^|Pv1ym`b`1>h6E#_Pbuk&NvVcef!pKfsP<0c7(6M}V8r z8=z@;8<&rQTva+a%;Y5i0Cy`yeRP=fv>=chw@BcgxySivyHw0k-nC%?jey4XGr(E6 z(NPrq*ask1&>;N?;GbjQkLhWYa~(*O(a`LDzlMFE--FW%YW@&`2pawcTIIkjOSnl7 zpxNg9zOY8rqQwM}i9~VHr`CRK1k9@qj4;s0ti|5{JDg!G@?oEH8Z+F_k3g$Z9AOZ_ zqawh=>`P9S3oE;^+vsy6-b_ z&o4JeD;>WSSZt2Fpree8+=RSk5drAWo{pumB z_lZB$*!i6NDdg{tYA5yYrGH`TLM?S1B>42=5hT}iX@H@d0x04F+z1#w*Ela&hX-nL zttT?iUjd>_JGs~%1fQsy-D*TNTT>jfY-eIx_X;s2t13;&eBUm`UUAi_1aSy^04kgkkzWVWNWdcIh!i06wjIIF7OQ)25SezP zv@w7RG$)ucygUo`s{5M4vb`I`L`rS$( zt-&B6(wB)$> zW@v`8D>wyQ63}A%1nPSR+~2D12SC)?gUOv&A(acS;h!|cY!K$_qw(#j|qTKr>7F@e4 zlRf9aqESFXjdGb@%seW+0Q|3M&b&I%TT0#V+B-uD+pu4}zbleF=LW)f zkM@;d7oafvRQ16tZE;*j6rMeaGYV>M1sr;KZW55ridbmwK0XNitPyR7u1nx{@B)8U z9lMUOskEA{RDzA45V#MkG3$teAD3}rQ-+~6KgUGczVIFGxXFEj9;1bT)%KsIN5N_H z`4k#M;o(uI=LeuiA zsl!dcDPi(}^0f#nozQ@jK&r}F39i2b!O17=ZDAhl+A>hRQ&&sTgcC6N3-ATSM<8Z0 zkXt0{J4hFtXu_-oRgv~bLSyMI(3~T1j9u!6DGAV62@SZ_tC+RKa}0?UWRe6(oshp{ z$B(9Wd?6l%JJE|rK=$fG#d|W>+EAF3P9?4Ew&f=Hk+}|+39m=7tCAO?xyMvA*PYIL z_us5u1U>cy{sKhF?2vv=tnL8gt)75*qT1#HQrcy+quiGtvS%}|b zmjwJf*2S{tUOWGpJhn9p`l@smc-2#{Hm!g8uT(C12Z0N5=LkhHJGeyv@^ehN=^=dI zILGK`lW1nczYWc1H9<)HA^woPT?M7w<7|tBzQ!TN&!i&rvX}WeEi#QY1<6k;cPh^- zo{pwnbuvnH6CREHs2+=S@b9q$eJU}8{#MI4w>uYdgafx8cv_n>LwMebv$}uiqbB3F z3r5*SS~&Z-b#Sf7L+0x^H+gw*T+C(|7a*%*spm{y3GDJTOD!%P29Dk9eLWUGuC& zr>!I=z-#<##Qg~Iya0(D8=~^Kv`4;m_<kHZyEF zvC_bQzTRF8=E<>RK)3<}oeo>9F~imoMluL@R($nwa6iw}dUcO3R-wPo`D%6E`#;ot z436mBH;S7yigm8)@*!zS`be0ca+J;W%^7fZpQOW9 zzs7+yxKJxSllG2P8ct3hJ{6%{Sg1LJSliFkW61yzq)SK6^X1~rxp``6cZ>ULP<-`X z>TndS&Wdnj1iADY?qNaiYk4XelhJq?DrqwgRgZp?@}7wJE=iEU{j{#_Eh7lJnCEdr z$Fs`cdCV0m@7^@677G0ad64fJs250Sts z!?>!7@KTn6RIb^LzKT@U+`#{jy*H0)D(%+9QB+jKs0auuDMbN6K?@KB0p(UI`qoGf%95< z!+hB17z%~(SZ3aMREen=6n@2D1Y2#_XVW))TyeDXb!P>0wyko=5k8yK!g>_&a{r4g zclRJ&)7I?o2M#RPT(Qk&!XvNK;aLKbC`jO!P+}7C-h1U)*AL5W&G>nua99+x#0BoB zzBaSn#5Jk(c@6oh!=!eTJ0hFUHZBOMp#9kD60Fhhyz|2q1qH3lP|-Tlos!w0J(uqg zBe#)F?T~MMBHn(#khh6Ly1<-i@v44`xUaIrfWbA0+K;C!7CUiBI{wd2vQFDt=%39D z(s@Ac(CIr@FhA?{nDNs6`2yWSvlTB-(?~o^eu(IzxK*|sE&Fwhsk58@%Llv3bhW4A zO#*3MjrV+kU+gi+oe^D4{>)dw{JPR%zI3$A`uN%Ii~}b0t$xTwxg&E{(N@iZnWsPa zcevi9T^v|APkH<>TG&r(?rHG7wc#Ru^g>_mAyVkwnLhSVk*$fxcs4%J2&|4=r}h77 zghb#!ZyjNh&SR=_vLLaizWPGyS?)ghM1dABN%ot3nLt0Uo6;7;wJgJri2N0^ywtnD zzX;>RIooU#2Ze^ocF+>J*IsdOr(M=_?Pj%_&=)=sJ9)M6%*A8%EiJI3vo zd+<+j$yHd4_j2(anZA4uo?5bUmpG*A8Io%mM_hq4Hgz3hw`sI7q~o{APtaB6q{AENr0$ohvE=> zR(?38B4X)By6Oy+9~%_7uZ6D7=`5yfYqC}EPVjCkV(w{@YjnTDNaCH)R~=mtknH~Z zOWM8_k$Xt@7(ctXKfAa;ySP8QxIeqNiZu0S7x!lu_y4Xg?x~0AP$+xX&6b&xXF>JI zo4%SgFDQn>$*6Mr(yuBBfb=Ge+l`}KPR{V9`>2xH|ofw zj~GuQ9qnJTW5T~a{uDogmd&y=B^tXX;pc2!??XbAi$g(uErrmruX;&j^^j+a*=4$O z#K%-(c(%*Bi|wntHS!z;6!~%+*7=v$Zlpa-=pKtH+b-TrJ6m~j#n*yt7-N!I$xIYH zBQ0m8d`$7V@pu86=ieBmb2P}c()voaw|QpWIMriN-w+us}x=;_0=y&niPLgxj}+u%6Y-=$z46o&`5RlT1jIy$GqTDK*8&5q1_(^7>h z>%@Mt2)biY5&k4%KCPh-+MA4vH>1WQ)9^bbHZ_^=IHa>^-k%rnTl~(dBVq$e(DiK8 zpRF9YER*iqlHYoBtxaW~xMznrB!}(M=6_9V7QJe-i+!|{LFLC?Z&yz-!bBI96FYs( zmJaU=Fd7LBF=6R?E;sNs3Ou*Uobzst9xq^zn5kvJ+`EdT9QQVDx!IdMlQ4VJH{@66 z9`gmk4y!9D%=z1syo&^_WD@3 z-PaMQXW30;ovGSurX${L+T>^JOwQgLP+O-uJ@&T5Agb?hmd-IfK|{te*_x`6P32qT zQDcFP+VyTh`_B0b6Nr!L6cy?K?akBpRe28vxA&zdBv_Tl4{T4lI=C%P|m1Q@@a!n zh>PDhA3RpPx}5V%>>6Id{LWMJJ9nq9yHa)}=y09+wo12P10Vue!(o7-r02;<~Uk+>4rH^mWO+ ziUF<%0L}lmS>@;5{QDmB|5MwXl@L27s6_v%bwnKJdV$Kb&dFOKrROA&JF>Hnh8w#C zJ^IM6?(&FRL)0&tcXR2c#G{AxR!+wM6zGgP5OYe zrE=@zO00-FWC~$G6M5LwYtv#X!giJ{X&#dq9{r3|-eg4|8={tn*_l3Rgbvcjql4sm z&tMr(b54u85oBx=zRx(w^3<+-$CX5|!7VjHWV0Va^dO%*;nRl+>&SQ5{2i#ooG}}U zt$p35I8sKvM}uu|>HhhEtA<|Y#rkSn=%yqdy9gbZl&w&Xy@cF|C9WY+XHKu}>n6$1 zML-Lxy-3lcYg8+ytpP092vcVGD-&O6L?DZnjs^r@kA;BLfNt$SW7;iAeP^$!C>_u z(U0h~@)}G{(@ONx!BrUT9jYWIcGkz$&px_kHA}>Wo+@Qigz76bQw}+0y zu&*CVpQ!jGU_dN*=6ya~*@hJby47|%X#4Mcgh;b?KC~YjF@{bh&kOdWCinF0OXzDE z+{V+E?cilI*7$LU0 zT?F7`Yz>vWO03Y=1|k&t7^k3(oaSxKQr06ZXQA!((86x=oF*)CC5Fr37B>1arOaEp zs(_&YV-5G1N-SBCWYHQq^g2JX0gzpW)~-(hZ`0lML#fzoFb|RFSplQsHZ0<_KON4& z3q8L@<#_-)5*z`f5Eumh3f$#~Qq>!NCafA~IINy(1}N-uTbfB8vHyyR-2Nm>)428( zgMSk~2=L`vbNnH~yH2x#oyV`jk$;3L(E&&8`ZS8j2T=-zotQC_I> z!L5XT1`z)K+TW>p2J+jKGO}!07bL=s#RW74Q)r0@9(~^@DsclA8!V%x%<1s>vK3&r zn||@IZ`9!9c*JMutN)(F+e&DdO2s5)BN{S8?avEG_;LEfzI&j zAaLi>Y~Y1tqWv>kVg#@?ND=9A0~XQ19y!FGkMSPS>VOp$=GuFx9*{Mrc-$eTbiYA^ zpdLWzU`-z${Png#T|IBkNGkQar7TO(mQkOtq}iG%(giGeG@^ z5$RtYnwwz_tAp8cF+fT*+x;W&`D`k9$)dQcW87j3LU+ zNVw%Wvs>uxvst+?d`<?)*dPeg~$CWXJy70S1l#q0akvE%}^8 z(PwcKzmSLJeLs8Gze`6qX z`ty4~K0eA&5pS%nb2`&k8L_m4|2b;BD6o>VBisQc#0jpG{4rCeXC8Y61PX4B|$PV?>gu*0=~ zYe>ABYJNL#jiFWh?iWWQf~$6S*{rVK80~v5T1)HNj>^48hFrn(^tYKyt&Z#)Y>hqV zr)Bsi*))IKtK0YQGU}&GmA^39A>!4e#5QeMx$V#7;CoFNCyGu!&mN)J|Ak61i$$FI zw+P3Zn8*+aD;uX0{49Q+baQzTETfp;VUt#a6TrT~U-|gyc|Hjj-OYUlcBo5|Li!}Y zo1S=LD_Rc=Za4}O`lCue|L>rRZ&-mEui}fNJBp9>rz(cwsi9oUeSNO#cLc)`Q=q!bH1t4=gldq zubSNKm2e9@xhNDyZ6dr;{;6v6)FKmGResM_zJ5#qhcZFUx?)p^Qky^Y!ugDM-OKXd z8EqQNn$MiX4sZkA@n;R`&l?v%Z}9c98~FOx{!q8>X|y-Z0S-5FwiG>c+UBN`q4MY} zVmbt!`j-Lrz2Gmvnb>&DbOzGHFZM zPZz+wYzR7B_o%rtnOsC0QAh4h| zp*)x#2ocS#GK)EIjyJ%91}RmZ!ybQ zvSmT(&0wD zet#L_G!|zCZFivzW@?~qU4;Ps-98`=BQ`8J29=9y_@;ZxLR)>vf*_F}79E6pVPvB1 zOB69wCbj0+sN!^5+(8wsevJu_MB2SNgY<@hOXP+qBD?GFC=)5qvG1%A$PU&YD4sPi z<&*=D16igDWHfJTP9s8t#-K_r6@aFgOv4AhqLVSE;7=}POh?-{B*KFnQ?IiamdqSv zinM{~Si&v#L}bMz=_tISKO7>#ElyUUZ7zBc#BP3*9X~A_>-pMAlmN8$W7hV+p~E}u ztWn_zleoqRg&J5dm#~0r1hU|(tuf_5MyC7;?xO54Vwc50dJDKr*+L&=?12JUTYFw1 zsX&Ur?_<6Gd=>msNF*j@nV*z=pAWhFTZDJ&4mwc&R4Df&@GEdsFDgf+Y2_)C|LGGH zu$F_&9BsQJFb4OlzYtXV2I~!}WQ*nl*mWRlM)-cnsh6-PqHQc&fbGXE07gb1@EuRx zX_ep7y#T`{RJc@pr?)0+3VCFQ8<-U(!7E$7UU&@YjVedmeZV=IHI;y~%(x1WR74rA zRl)BeJYzm2G}}TnTBG0tm&ZwH>tlFfTc4yi`%*_X2^jdfLo6bFKcvNfF${|6)1jOI z+~IevTub71k%9NjQ-VJskMxByP>^PL$TYb^FyPOfVX*FVd&&m8?EWxRW@Enf5>s#3 zBwoCNA-kp;ZuQXWIuB{jI5=~LcstKZg>VEa`2ba#{+7f{g&d141HZT%D^>2fR`-!~ z*Y6nr72z0D0lfAUSinu`n{sBf5*to1@wlgKY1|HBE=fX$8?kO*Sq_)d7xMLh(sL1$+HlfnR;Cg|Gqy~*78!pSe%ucn!EYMc))2FbN~~-@YbM8mOY`07QaJ;dlQ+i_QW6bR^kosEc8o@{-A}QN`C|Q zh?O%X`poc9t4?~3&5qHUNeJ_wqF*WQ1EOAiAwDdqF~Kzj>a(g??M(+ zez9u>INBlsIE<@STvjO;8Mh_l7e`EScMh;cfLpO25jiT93|?I5_PHyaK3=b2imXsx zTPK30lz2f5i_zBxDBxkwSR~LvNO_O(Jm9rA{%V&MiS_1js4}uICQaNdfFFu~D3w6C z+KJ(YSBhhi@-(nGY!DApzXqW?g?L03c-)d5*)!^-Zt|$3plZ+$F1cIHmnzo42Ae7P zqMILxwInI<#({fNDTGqD2jMcmCk#&_X`}t$2Z}CggU_5=3XgtJ9WZ5;QtvhJgZfz5 zP;Y=BfPH1qC%YX%cr~nOENS8?WMjCp><=iLjB`-gV)jJXl6Hu90)qsOA*yK2d-(r= zSi6b@tTz~rs$8FQ@B?P4?u37?d7#8x@soj(+kXWUK>)AoR{w=OV#cK^@V!FUNV~lA zP*eaC$s(xa2p_zI!th;1LF=;xB3T_W1-?iV$H3+~$|<1D#d;rAqit~};AryK(wErC zZ@y5*02WJnElCxG5uh5;&JgRR%M*Smf#dQ#_*%qm`F3i8xQrOQ1ljb)y-+#j$Beap zgp<<~md~Cw2=QRQ%J+Az@>8B@>ojc4!;Rx7eEL7&@iswb`!Df$?_R#7eR^-f=_OM< zqdVI44u5wIPX8ro`|00qBCNqDD797C_i@6K(+{qsT5a33_0{|*>0jnQvD`y%(|$jh z+$zK)B5~Ba_>?^I#`SB*{r0cfe|-5{%C@(aSHf$N?PYf--6j2p6Qi%k!it?avuAp{s5+Bj`RUhe1}qadL?yiS_8^IaRije zjg;(u-osBj{NL>l+O69L8UpReS}m(re^hh-jTM~X9JVM?M&4O|ow+%0OIp|*B52lc zr(;5Wl?MOPlu6*!OSHzQvdL{w;WR%wp#Wr><|;_gWY`W>=pF{q^bnaREv6cImdv{& zP^pnJ&d|ep7z@RXkUy0PPL=2SqiXcY+*|LoDmr}cw8o_Y{B4=ci{>WE60hY@;tSaYT5&?d?EH{+ zCO4c}p^DCTZarJLwmHz-~=?Y_B-9n>ACuzf~v^)`h53Kly*8b-Fh0 z6(9T6qjZ9_Ubd8u@Est|Y-e@XoV8m(>mXf7QG3c?os`*6o|EleHDsrU#GWfLZjz(^ z=CudS+k$Na9tWL_xey$Dw7>6Z-q{@&8ya@6;3TvulSJq7yAcbCw}mA-0+2;L!d){q ztnSf8Gjq=4w=kw;9X0JnhuRMwOsGyvf3TX{G3CN>$>WC0msE_$79bV78FN}I_m1>M zZ?D*v5Xjt}Wwf>TYLk|6f^|a4{+!~^7OeqvKW#zRrqb)4nx;0<);scMZ06_NrN%@r zc1*K&$uGWtC(vlsA(sZ{&sqL-IHgH@@nq~9S+E^iu4=1z>&h`8=qw;(XQ7HQau=ey zO(nugvN-tO52bfC@3DgMAqGEDbQ`~6x5S@3b43vqE*WKkMN7LL(w*+R_aCxI8jjod zmUO(9Y)3qO(N2O%WN zHNAid=ct_hyKzxW)G`Z0*oet9Or-nvgYrmF&hJ-du5vZ6s}fbvYE4kGxCBiaR^L43qZe!SxLs7EKc^kMtdFLkZNO1fa~Wo?BkH$OfD)jJOa$keIz5+ zF8`9Ofk7BYpUD^QunAvVN` zdq?r`X(JQBR)Tb51Eam3c)c2)I*ZKRKqod4!b++Lq{*^!dj_AU;uUAWl$YVnXT2Tc z2TGL{q{>#{qaQ@4V7zpBC>Y0~+=zb&8`;`N7Okko#=ZxJ@3$fT$Wg?%;~MAyqL!io zJvitUu;DVb&*?5;{dzv+Pg~d*H8}9+mBi)n+0}5`X53gL+(nTrJ~AvbvI0xGdhVb_ z{T)UZmYYf-#1Ej#^sF6-9PC^HXFq#b4&kHENwzfo9VLsymdp)p#S`aI@j%*9{>^cU zA`8RXA*Zp+fP5*j2O~1jg{}3V@Dmq+wsk^e(ENUq^!rqJd0l!$ZHXzQeLfbp)9koF zyv5!?O<*!GZ69e5k@I5Yx5=eHlw`&VnXM+KGfVvf3b8a8$CUv;2uyE0cT)@zf4w)sINpg|TZJoH29`xr9#&JaQZ|B%Vd`kxK@SLuAFmSL=#wYX z;cNSfMxi*dJQS)yJ_x4^U&I@RH9anfN;G^a}(UH7JSS1 zBKYo(UCI^EyaqbvC%GCdI5?X6e4dl#n zOnT=!=!*SC))nxLaY_v$v!tG9OAqt!(AErBU51K9GHd{kcMFRQ_@RFI<=t!|rH-Kj z$d6wXWduNe6w_3QufzDdB3CL>g^c`C13lk#BAr8d(oL2a=c2L4 zkS74NMtzm3%#Z+oZAx{`sDB!=MsvW1|Oz&vj|QmkIBcMIQv z3Eu%2K;8LZ@m4eOQbIcdSeOL{@?=vj_^M4QQ<@_;psp=vkLZ=7t#8AK$EzILL!s?_ zA{jpc<-<+MWCR-6x=;qBEK zdcW4=ly{8DkkNpl!UW0kTdk!G*#ZLkci)Dile)XH-n;cEkF?~6Qr8V(J5zr7_EK1e zF0hIla{odifHuP?b4;=tTY$D76+@zv`9q2Oh31O(J}iR_%3NU;mvC2-WYH*9W}HjH zw+R+;gn!ZLzr(?U_TG$WB#SoNLKc;k2if@=mOQ~K2%0VR_;wKG>LrgX2HeLi$XlDB z3;s8npqO}%j@Zx&iPteT7{0^Xgry9nuTOkbWCPW(peU$@VZ8y@!2-rrkhQ-5YVa4F zhy7P6EPuwhX?r5!V$jXNuMST|`@1@d?DWA+Pr*Y!JqR1aW>{sgfUeX>p}g>Guuz=M zs^P~ygp9xr!b`gEC^CdwPsUHh5LXyCvgLtnp_E13zPv5p%(S06qS1iy4AQ|N|FlR# zyLUpC?TU;=2ic2ZPr%8zje09=ZXpt@kOY9i*!n`E{VH?B$`7O+l8LJ46x>X`pVmh> z*}+?SDuTbxuprH59g=KijnBthh#k{-xb_%n2u?OVb`d4Qz2?Es>61ND$yqYbGiEvo0zB_sLqp+fT+l%JIlJ4P>iLCnQ=I(7^ECp$H-||0a#^ z;#N7ik-Cv3(GCs6Pxr!&UA|6!Wl0qb!P(;pujpei%nxs=TBu5_yJ?A^$sfj-#GXce zG|L+VDrI{57z?(xX-2GD*#iwj#G8=hw@oHgvHp$->D$l#(#nb+r5A$%a zd7uFj)i0n;GXkMnB$1CcX%XNi-}i!7SphQmEhb(TLb#;j?$ogb7(az7e}c3yq;NjJ zVdHNRjssb$4ZmUX3NUjMn&m{CO(xXq6f;Ssu!V1-O5kQ*FkdkFAt4ppIY?*g?q>Aa z24c8y883&?*l6>BZ9ckD8iXq=^led1)~R++PQA3 z;NFW5dv*2Mm%x_6mbzqN{p2I8p_!|)><5T9?fTZ~mic|+P^-y7hif8&x^Qc17xBqh zQc67ew+h&w|1qlkKmU(^wO944_zu5EbQgadVz*X&i%aIc5>Po1(Fd7MrP3@#!TjMk zTPOZ(VopD%W;GfDDbNr`pxKvT5?kHJBgK46_j(hqWc!R-KLy9 zX(+<5=vTT6gL9Y`J6IE;6Rg{}o_ZlS@Cn`g`ID9udsMA8W{;8#YMusSN)IU#qN$oP zRy1|i;=DWoD@JZ{ER5@==TZ{4qfu^=MZ4wIEO$G?QB`wMdiFAjan{3AS2G{WbImO( z{Mhp%*R|l9#$6KRblp$bBbycYzV$7jCiQ~VTC@n(dI~JEeE?EPRF>rR8z?X!u%e&7 zhyU7QM2<|7eUzVr)1FE~d*dTMvPEj*4X|VD8u3R-k_G%>oDRq)&-$!dtoM3Ph*VI4 zN{b>n1>gUwZX!SBkzkAdDUbA19_gn%(ocD$pYljQ<&pkhkw83{G6jQr56hiHcH!h=>;rpby{YbK>Fdw(P}?n+#Og-O7-$Km7|he`sfb|)>L z;4`cfrb9JvL@GkcRbCP zy2KQmGqOAOhEY&1_7Hy8s~4Y|T&2A>@WyI&5>?Hs!`Li4_DquP3d7~U_c$JX(7b#{ zkK3$AO?Q^ongkdULE_<3T`Mt_o9$7`o?T?XqUlvz&t>~Hg9xs(UAX0r+(hxhdw15Q zbnLP0=;|~Vxn1H0IbFE-=SkToT}zegdoy}g&bw*#c7^Xdr`a(#GhBi)Uv_LQhAEs@ zYnLQCe5J`>k(2ffbRSu+tEU`)&&0j$fb*JdSt?x(;`=R6GNy2>WFM~H?^$-(q9K%- zPvx&4maP2@ieU)6exbe?r6!6Ye_0Y`DqFY*6V6`{(b^5Q4-1U9{yI9a(-ZY+mgk`R zue&ivR(@j&IXURJlg>&9Kd98*93HBWX^N@t%|(`6YR90+hd+Kkf1%^(##Zx%Aw4za zpN)5aEC{#$>a^D0vH9q$VGWP2erZC&r)?cwyS>WP-W60Hwm09jIM+}mgNF71(oB=? zEJ!Y6%#b_sy^94toZ%SyGVbsKx@`k}eL9s^SY9$u@YZcR4e@Rn^ElBpQsX38pc$qf zV43*)Q?K%j<$uoR1*W zXEfQW_E6%eb1fJou^&e_TVP)fV=I1nnah8e#qsd6h)@^r?`WxUYA_!hp3=WEDAcpV zgO<4Eg+L|lVM#Nq)Yiscz>Y4mhk9-lHII@|pytLQm!Z>$Mnp?KTYy!jDC4&^+^}bjw<@ix3bHB=EPnQ`Dk`y)KyLJm@UU2MCpo_Qi`cVqa$@h; z^W!Cd_IM0De<=wMymC=h%_QK3OJ=%tO8M=tw{s*uYcdEy;+Go_{mQ&`zg(X&B_O_b zbiuf7c9qqEd)B5}XDhzuFZ*qme6yF;#<}0DFFzD$q|V&szWVke!$C1g^d}x`8!GdO z=ps+GozD-ei7@+6qDf!WB*Y?1H2KtvrYuvfYGPLI(_BFSKT576DQ>ZFcB+)-+yLg* zpVlR5S?%sH%KPS>M4L{J4JnrApBJi>7ir(*j(Td&De<|vJ@xj!P@3Uyzn1UwTeG*M zK*g!OA)TV;!fN^Am;_U=U2f;!y4I$v)g2$EE%;73Q=2y*LJdXbXV`TQU z#l$Fg|7CQqU?O>%_#`@&tmc0+t*bB~gw+*r@~GRKk|aEJX}RR^jeXagJ3Bvf+>&xk zx!-z^l-uS!Q(0X7n|Amv%0z)hPpq^mFf`TRHw*nHSC>C(T!S{YB)<#2|K7Lsle*pF z=rw+GkLWHgZA$TTd|)>DTt%IV&|SmN`?m4>q@~NOy+77%TK!=7$jD910ntbNvF@|% zky9@*{1OU1^8sf*ff)!uyM8(#$^kOI+6rft0?j!H#ykN!q3Q8+q9#k?Nxb{-8%HFr zhe$_Y!26sghYs19^WR|F6xMy2JP(CV?Cb$De><}yG&o{4jU;jsuNO2{*l9GF(p?{P zuJ6X(s=e2?9(@< aT}qc{Sf$_4o;Z{FAKj+JU%IhE0~h18PZC+Y#Dh2LJ9 z1}&NEd@&_usbEU%!2^jd z{I551yt?{R=3Er@k7e!Hv^1da{7oaB!q}D94PsPjl#0_Z!B_=I|O279U z{#Bh%+X60qt_ij^%Rg+WGFj#$b|9u3rw(BJja2Q3c51$5 zIl?$;P>G2R>vP9{oq3rZVyB9ZuMDo&wmPn5q<+IOwIIir zO?3Oz0PiLF&12!e5?uo$H9KAvE8xnvL3M^xfLN0eEDu6l%{EU3g{T6-6S*U8!RX70!4+M+p0l8vMMv(3laY(Qr zYikMre!w&9@cLNy>XDuePa5T0Nzt|j+3$ix#csQz2XvH1Tg5D{EWw*iOcRhwk@m@i zeh1yNX-qrY#BSk0f^2yee|WQaX)5vIxVK$_cwHWPU_IC4b4aD8_e8Luzc~7j#}=L8 zPBnu$UGCZGF*!=eAJEHZ#b2VMII~2H?h#jUiDuVR6|r#HW134tf5>B7GcJV`XxXJH z*cq9|&d$EnWS*c`c~d`Y?}I?{-X}r$YE_SCPjdqj^4F1>Wd5!YiM|3dc6Shw#(TC7 zDya6a>!GjxG@OiYij=pHB`Tl=GXU4p2e^bs8l=)U_&4G=KyaDHU?Uq5|6$n_`C@<} zFf?c4c z3izy_Q$c@D1^qb{^ncw{(Er2qWIy}|a@u?B+?b(Pq5Q#eC)r9my^&H$ZIe~se9fpT z(b_ExH>;wOQkx6Z!!l_YM^wzoDz@CMbuBEf?(t{Z;qRwDTkcFAT2-czR@FOvOt4^6 z`KlT&*0SK*{nAjEIZ6EI$vUnp+xi@tD{M`yFD}+tcKFSz9jWJIqa2-*3YX4H9XmLu z=Z5d|4dv~)>5$3H>egpw)6;fdGc|F|Jknh4E95t%d!*d%9i*tOaY^!fe?N6i-K`e8 z`Jd6U6>>e9BmJmgMNM3Zjk{cvpB^o@c^px{TB@7YLsc8>(ii-xd8V)0dQ#UfAC_m+ zJ<{F10=HH?6z>Z@?4@hacu=Ta;Jm^2S?-;pPbURh8khbY(~GM0y|pi>H7(^$fvx`H zi)7VhhxY~5+0+8+tUWvu1_`kmN{2^j$i!A zXJm5t8fffl$!DD+C}D1JoRWG#@L*F3xwV)06~LB@oM7r1jCQUe7l;AWzVuy)Y}S4l zxtvUH6Qah<_JL#w6pFe?5<-wS|Lf&>?*_Pdfm63w^gbpHlCdOo2 zFQ`PJ98HMH*U?>*9TJII_c8vZpdU(|G4@1lCv*g-3>nKD>gz>olanv3vw}~aIY2|G zcs6zr>oDLN#J5(bOu%0ve$;4c9y8um4S{@$A6{l2q z+_@WVO7b81cDVYA!-XtG7p#YakY zjqxP8R=A+XP3+2v%eO|#aI|Nu;8j3wz|M}P#jI#X{9`+EyZElZU@8xU$G*mP`JXn0 zocxdB7-BUbYtLl&Gc`(<1lq0OljIs&JH_D{K@kf+7Fm#^h0JpXobie_`oc8N0J}v@ z&z2$i_RP+p<0r%7%{x2qsBd_dSKQeB^hKj^s`l4Cq)Alq67tY?!9;3Xmubg#V!F*E z`kx<>y(|WKYKgI&%oiogfRR(W(|hU12+arA)ItQ5El&hp>De?EcV^bz8lU_9-oM6e zdD=j?ncZrdBVWo5cJm7V?X_|do{w~2XBQL>)xk*GZlJk!vP#F9Pg#|$^E(L>XeSuFZ~pk0&VCx86MpT6U*Fqtb^Ka+hp`80&E4jkLN!`Cs(&dtY2>@pD)R~V z{_~o%6E46!tv;9>30^DpZ822#A4BZd&Fcb!)MXTan@_wLM54pG`8)nG>=gWTXanD` z+w@!n)hUfhBDoPZgn#?`MI@_0O>JU7GxwGbKxcTA76h<7D1+NBo|xE7x=) z?Xp}=w$_ViBvuJ#Ch_Nr$yGb0VHJaG$y@Q1$9ALuva;M#HYEXu~#Sd zi(|6-LvoZNt`oI8sVZXtFHh}GV3isVIqVSIVKe1xO(Tyv2eal^o_v^m#NQ+xc+_}m zknbZ_e8ED;puu`=hm2iq1FLBF!@Z4V#y0skyM7^^Mt`DVe+~=%IV|+&u+X2wLVpem z{W&c3=djTKmBT{+^-T4jQVjojn92WC2;hHpv3-GTfrK+Gn}HEGj>Rf7{-|d0IXoE%fojl;*#cB_=DK@T(r4N+Qdb3dYZ{Ien4D@)|C6W>yy9k z6v1sR56y7Z%A3vU(YAh}4|jQ06ou!Sss=gF7*xgMPdEIc@&$r( zSjd?YttPsv0IMN>h0LqwF+EuD8dUU=XsU+YRGJ3|IX|YR(qr`SD+c@p?JGL6Unlu1cbkfr z12hjg3p2HkG4YKiK|&y(_WIpM+e#S3gHqyvbQAPR;-XRBkqgihogUNe8!?71nyhpx zxq>axBj<q6{8qTTwh9- zOqIjk<%_{CC(Q_w8a;Zo^)tEgbJQBh}R{+t8DrMMrsR27Om4AS7>LK z)0aWB<|2x(8W47aOkScS9X|`#pfuRMI*s+_!b_f+Ox)*^{t{O%qMzyf`a@}zzjhOi zOgNyE(KZeT#qHYMOl!6W8tf|41)q&P!if!ZwkNfje&=tlW+>FHkBEnvrysk z=0S#ngQiPS@XJ+*`weSn0>$`B7FdFJ?>%Ucowv;__9^{$h|qc6GkC2qQZ-YeiTEM1Upi3YF?vB` zRAL}&osG7QpwgT=Mh=%TJ`0I@m7jNgYR z3@snmmk(RI*1_qydBb zKpdb$t<(pf>k$q#hD=|K{ZQH+-^5=cizA@RYXtHSFrRwnUBCQ@_vDd7s4^JuF%AB` zEqry6%TjnZr@#T9?gX0o)=Z{z8FgeAKON?vBSiXE+dOZ;{C|G|=D!0)6sPaT&5$s^ z`T!VJb8PG!Z60g=N<(0*qc9UN$h2pnaPIjT4Ce3WqVR=8^m(3BnjB8Xp)xhK_uvFjZ$cAKGu zwoeg*6K0M&z*uT*+_QqHdC^KdHYQW6(P7ZtzM{~`&h%W??fMUw0Klze37Y4CUtrI{ zga^deie`X>mqKoF?~4#3^GQvMA9KW+Z-B>+*5m$9YtDkaIOsyY_>9&*{l*j>iU8v@ z`*H&L0L)#w0hJlRJ|oSoB)e@V{_r%wuUQS_TlbJ=fHnb$h#lcu$}gcE>O?uFj0yDx zUbJ1q5;BaTIQbrc@$;vL?*#u@!UW(=o5OTKQXJny^Zi>Sqo$N4L?E5#eU8vW`|HY& zb@ySK8=#Y`Nrlz^)USolEURLQHu#~^Qzo{$FrWd@!!nU4yg6YvS#}E(P87J(0jmR> z&RGCy90pbRD7y+!GR>(4Dl+&;CcRW5MT9^LeU3?|f>_!b5R#Nm38SilIvO2_{ka#q zQQYz9&F*(-uN$Cb+Fk(+`B4GILqQ!E!M*rAhS#`r-r@Qxw!|2ji}6y$ec(X}JJZ4D zmtDX&_VKic)NQ8FDDS;j4-XRtdY=)$jJ_G8%)1yO+y?-;JQWke_Y6-&2VU{-(8pEL*Nkc4Fss*$ zis^^ID=AQ@f^{*@A>oci$GoRF$PdEZ=Ei8loj+F}q-Ejkvv42rb%+pIZL0}m+QDq> zYU)S`bt1w$A|Cvqlv)A<;?IB`&#r|+iY+v=Px`g)M@KduO2!kc^1u!MR;&k+(L)XJ zU>$NWp>HE6jL7<-6qm$-H+tpPWCdHk5v-%VMz{m%jd_Ria$X}sr^=X^*1x17U&+C% zuLQ_8qwOpCVMInn z9pbWOi^;m+L|bVui)QKp2^}ErZNrMTdd^(wcW|c54}CxYfTacBwz-}%#hP>%WFSBV z<+l%JpRoNDi52q@6j&hbdVu&LV2s zlZgjSwi^NCz7hKrGVUSSVvslH_W{Xg$QcpvWVtK&uwA%PqJsLK2jMkv$3C}*!Rpsj zd2nKGY$MC`tzawJEuYY>@sIUi#Q^w19)c$-ls5+0f-iJpD?se+mcr9sgCDu>QXjNB zOZ@H$9=C4+IIa8U4}Zz!l%HPPxE)jg_VYvVWX5?^%Cej97U@Jd#{1#tbBA#X%ryWH zSfURm^Nl#BSYVg9hVyQLynk`@xRoDcyzrM;s%#J62e?h{}L4i4BD(W1r zgZ27Wu|>+@a046nMDRYNH3c>Jp<6%(DTvTSJ?0)Yl}z+!i>5bXV^@rnV9s8WK&z5H z-{5@Fy14AU+r3)0I*1~!AIiw^GKw#_;B#83cFFC-6n6rv@Y@|*vG+>O<0oTtij6#s zwm6*(lhtz;cyGE^>7~hpl>?F6{@WYkSFYe&Nsz?oa49?y+$CJU$&C%em)KMJm&P3#o zk*B}bOQ*nJ6`UnB0crHj0!03XOkvgg6P<6dJ}H#AVeZf_be}JCObzrKg2+V({~#;D z)``XItQ<0Cn|IRDo}xz9UxWqiK!;Pm#6ur2Yj7~gB&69^x9-p`mlat%qf8u`ZEbDrln2GF z=Sx4kyC0=KY&%F>60pcGdC%oiwSNh72pT&UXvnP)r3!(%$X;MNqUM;{eTlRKySTS`lk zjy!CmwR-X2Y*?c6`D-k10*}q744GC-O?keuwE^g?14Zi}(#|xNuC5JAqvaZZv)!I5 zUMpO zJ&ktCOAwLAnMz-p^Lg;uxV1%8l)UiD=l+KNcOlkP;=9$yC8MrAlDc^EwUVqT+CywQ zcNHymo}*P^+&!H+2?0%K&O~k4e#83YvyOtBgkDr7oGwG!r zTIc8}XI_aZ4P>gxo#LHc10pY#^}ENK`Gx1TsFB&Jzb(Gr<#@j<#L~_awMDHueRzX0e6A_})DIgJ&kU~U+&K(uP#(nA=6?D{Y{CzZDl`CC?!MYrJb_|{)(F=P zO#2F&-rzb;m<>*ze0NDAw^jX_xytG;QqAQTwGQ+@8Olqg?nx&Yf$lBVOn3J<>(^JW zZqyhGrDtf6jHh!Y0s<@G_ca``a5As(34ej>g$d~Eirbd5rg!Xk?_O)kKJX>}aX7LN z-H};l&^t92oK29TUdNdPVt-r|wMO0{9*lRVToAndmvCx4#VVL+Jg9*4fOhr0WLhB) zN$$M@+pUYW%9ecpsPPX^ijH!1;e5;vLb9pp++&kf*p${|1DY*b4~@76kz+CUX#T-H zULl_D`3}cak2sf;?!Wz;|As}Nr<;mIdV((|Qe{JZ@LC(d6ZnK|$s*#S5q?6t{<_ppS}L%1sNZ*Nl{?{VNv2H(uc z+lbaMEmKIpzT0}yZm@@nhfksY@omUywiu#-v+C-@SU zb|jLv_s!vg0Cg$v`166-uV#-(Y9VZ^9=f)ohAPufGfnDbIhRLZ_n^7(*t*xKgpKcRVz?^i`^|b{BZL(-=fcI8+ zpyPdrVPiyRMTEk@n^yP`d6*t4(NuU-sa~QQZ*OVUeo>}+m;15XJ^tFf125b?1f(_} z9ZmUIoAh%~a~%gIzaTdDO^|_-ePQ_T7B}X{FM|B;IplYbrL*9wM6@jtX$=U(w)_HD ze_PzeD`L5(lUUKVhvhf#6%=%~c=)lMol_hicz$+IW{@=aUx8_#3tq(DJf*ax8l~dy z;|>y6voCxAi6kZY^B+m3>Js~il_q9OA9&CDvqm#Dbi!1^b6PWoZLbt-zDDlzw5rIP z3_@S=Qx=k}7V!%2a-K)BSaqVqgyg9sFev=ECPx=^~_H zO1r9Wt3s|7W1wvydvx&ZIpT@Z+%!x3`Ya;@yy=HWxwy2>p?#i~MS91(f>i8@Xk{b) zspnUEgm+tIw6pE>?@sF*zSd0L*Jm9#N&WfQ{tsbW9ni?#M|s7Q=*1kIc|wB0?E`sl zi-I3p?Zu~e!TYl;qK7)&`)=K==_)3KZ{cx5!B^2P?%!(oqRNL+^KCtHQPQ+1!` zvS1i5+-39T$$`u&3k^j*^d6rRU6yAvbLW)dgZ1N*HIq50PskO;S8&`nIs6$m-E?|J z-cSv~a}TxsoY-K$e-!)OgL3e}h7<`mc}OUF8ea!BPs*7%7Yh+b+ZPEz_hycnJRuIX zOAo~c1kl=z?>yF6tt4DXSvw)|?%h!QhbP3Q z6VI7reXJaDL8HDbzw_ZQ^u8f_Czx|A8_GCNQN0u8m-iVb7@K-t%cW5~?|db3zS~6V zj&1aaP3-xXgKxhFK!6XfR0Yurso3eSL!sYA<|6A2o&OW0B>&CR+syul51s({YK#vY ziXnygfH56TX+tqBR`;Qzaome{+PkWk%ipc4MVhFYO~oc%N@g$8t_vwh*0)mxN~TZwr6EOk`9yAo9Latiar z3TDI|m;eIf951S!s`GhkRabK0`L#yL;KG4B-EgJ6?N{`+h={Q8acNSZjGOC6*E&D; zxJZu4iz2hvD*B4&THl#<@+<4>3?L+fcAeVmsJr9gsiU}gslu>JC7}&9L!%VqcuH@x za+O^fZnZ?mt*`8Cu7kQ<*4+<}Db-om?LSoiymzX;&wbAPn7d-IkU|e_=I`{AAcc9D zdFaeGU^zkmecRK~nq!O0eOX|{Q6I{}6_HBHdMeI}QsQQL7Y_s7<})}fUTX{X;U##0 z3Oqsk334J21Clcc7!;9d-ULRyjHI~u3^s|x=+tL7_JySpw%10jotZ?dbtWiKPB~|2 zg64907mnWE<#EE`X}eXNR6h)J+5Otqz6MI0YP?%3U#KN(wuRFNatSs`TB_n&>K-na zXISkQ6?a=dVqJ|bTRy8gqVG3e$@nqR`!C|)Z{&&eaYUj5Ptq$r46b7a(Tca=Nquky zOO$*R9#5|+02^{1!vbVf%MYgpp4&B%GbR8omEE7S$4ipjD=-NY#XE_rux-8E&hyV3 zfPfgk`_AKk^>I4`BgG$l!0n5gQ;{n5_>*{}P&c+P0Np7U`j`~~&fndzRtsCJ%%ex1_p;pk>BMH7t8EJW7|m<#NFc5NiY z-MlE!F7@(4U2PuX=QVDoet+mcQ@=a)|9*IAOoZJcbBY8&WL&E}(TxD-!SylhPgxvc zrM@K%fs-v#xzct{a<^KU4~3z&iV%%8Bo z=3vQWS${{&y{*cndZfr~kzAzF{mxithDd&%+L2#u)N2xsRJoWl)UUm%xI?}?@pbxU z#mzOhanZj@aH2M%izrZE&x%oHq{nH2~mw1wL62ep6b;A4)nwX1W-!f z1&cptjkYzVS3Go9tTW%NJpE;kAp6P9-65OVr4u*^pGs$$n;k6`0tYU*r>6c|#nJbT zyjG}s?Ha-MuS#VZqp-)z?krj)^4eov`vQ-YBrScKZe^}zh@051iqlg$RfEy7_RT2Y z`R82)Y%7a^w^~0&>g(U9lV+cO$S40hndgrG$UG?rldbA*@{LtzB6|XO<<#i0ys1sns_G{DkIB_D<9BGV{cY-pa&MVld z-mNJtQMCP7rQ_jMIp;w(U8qube2}ouGlX1!Jk`?*b45u6^SbCbG3<4nM#NkBwj&*G z^PfWNYyLl2De4EV`QKorA4uO=sr@%rnpTIEapby!>6JnE|KaofY9|>k5+nSQxmKQ3 zIg|0pTwvM0Gs8NJMHtIGZ1>f%D&aUxA?{qLnyAR0m^Gn3YhmN}P9Z&<3OzhDq=^Kg{+i|zISW3h%r^I<&;MfOB6gPOU-V;9 zq|B<%d(J*=s5kp~B?)Z{zeYBIOeR@z@M1k+R!2h?ltq^wr0SGnkiaI*PqVhtW$w81 z@1H$Q(*<4}GgL6!S8X6>3+(_LeMF$Kv4yPV;epW}Pu4+W^K zj30N^AiPryxEK#OdygBQyn7t36MRdeI*h2rQx?Rw+pgqxO#$-8E_%|I^t0JQOXu>Nk<*Yneh<{xpYo6^QAC`H6= zIbLB!3R;!)upB|o!MK<*I1$WNSap#{3QF9q8Ao583s^QYD+^BlDW&_7qW%q=HvhkI zRPq6Gn;Ho*sfOow`tU7MB*T6d`Sv&ci-ZME{y9D}>r_-*TM7cqY z04bW@tZtQ`$0G9J-l6uDvu!(9$~UbIy^n($c`6DsVs5B(;7# zr#~+Le^rwlA$t8QtKI{Ja+DM9K`$p|9!4F6g3N%ak8egqqBCco%wmoK%c=W54eX80 zcU(dn3a2I=Ir7A%>ktpz_xveF#r^4tyHrv23IRb6DEWQjX?wbk?O+?{Xu1~Re2J`a ze&A5P%9Rf#a~GmAhV2A)6kaHyhZUZgc96yFEHk?RB%fDEcmp_QR$3@!m#IvYm{lsh zP*$owoR0dhSNH$@q{6WB?Wj)3xt|}@h|+|ZR$1o(LONO&aG5dKVZYteW_bo%sF zT3riwap%MJ>)_MRQ~8I}T@Lzkm{3_dv#-n~%u*!3EVa#je+ho?Xs(c$xtJ8jPfNRo+&o=u~pj1A*mF;pB)Vr@S53r9WnH;F$7rjwwd!p{^3~le6O-hT$ zi8964!*m3s&`6trUk(nfMc6qP04}cMzw$yxf8-6y*{x~+Svvf86VV>_(6`nB9PPOg z6XWv3a59=(5i+qXI_sv;$f~K6`$fiYIJl~`$=q=&VIOmUx0ET77h;psDOH1JGSJuE zZ**Kx&b~0qJdjY8kaK2;u7A=tCgtw=#|3FiS=S@H!^2Q;_n! z3SHQDE&Q(id6MfJ1u5pH%r9Rrm5>QZgRh6U2m`Xoo1&n*@aTxtfuOfGjr~uYg3Mlz ztKIr>$HNK4cWsnR@^>sn%$eMVj6#mOjJi-dD6)pmE+?-(9FqfUd2GS`TfVlb|0*>|3>9Jg-s>r_-DExK2I zoAJ240??zQ+vt8gpreKd|eK_6rxYC+18A6Uq|O%wUYOu#S$-&JM`Rsp-4!>>I8>1b^VSL8KnN zO8T9`-FI>gMbiqUE*{N{Q@NySkbYND?X^|PbxX_T>eUm=Kc+)D!$0kyJ3p59cNxR) zejOs6L;}s|puZG&r+Y*~xiF-E->QlEzAR!$NyPp~;rX}0l||oW&T?w-ur^%1L$#~y zXo$JjEt|{R1H3I@7eoyCv|cn?G4OT8No{?weho6SZ9UmtT&s?$71eVvraydi3B28? zVoz5pOQjFcUkjRAIvCu2`f~K_^7WFrX{0H)>ICB);~QIzeK_^P)8+mirG015bsg0{ zdi2qUdYdZg-KCtD?_b-cC$j&q+T9O@9s3`eO~3wJ!u|fo{@j-W|D!LZ3@p>AB0XSe zhwd#B8d$1gkD5OWmfJDa&mJJzS3{Lc0k__l9;adIH69$sDj09Sc5I(kGP@nFK(}+W z_WW_}u58)uODdO?CBO02zV_bs&mFH9&8%z!ob|O|WLjd%u`tNHKXKID?P7YzCfz)m zexb5IU2GT3SW{`@w+(W{`i{pw)y%ZA3yjWcDL7I5(?WmzX`%CeTIin}cR!UR4*vA` zq9%4C$4%fl6k5tP#7eeYZpeonP?x34%eCdUf3ygqcw^+>l|3Qimh6OxEm(2-wVKOe znwL$!itYwY8R}Ppq=z%sOoZPy?2Z>?twHIw(&C`_!0$XwAN21J1_%GeVPOLF$Wari z2&<*Eba06LNiAFa+-;nX>g^UKgrKS>c6Y2oOzPy}uXigBFJ)W){{#w{hAo&*T%w9Z zy*8kvg}4eao&MoVMRqhrB~*N1RQu%nM>~`!1y+7wC^q{{A4e}%8vLawWt(*N?o;v4 zB>N6xx+U?{Ir6-(Ok&8$ zDg9`2bC!?RoEqH4f>8lOQs($A$6=J9vP}~x!-=KC&+aCyr2tAXz*eh>Ptl?JW?iRN z3L8er{B{Z*d4Qzfx72q&!G$1245H_+6>QIk!_IB=rjbs|^TD1uR=R4Dq-L3*>4>o= z#YS|s(uvBqP~BawrIQyFz`8SwHF7t%WY(_7yer9nLZZc9nvg(syd4b0z1F zJ}T?7E>qt>H274?!OZY{$jm_Ht}pFbpHe+OuXj*mTMt3vpiKu=u*D{{bS}IxCgjYb zq{}-^w=c9YrQUiOAtM3}qYkWoxW&+Tdy_*Ip3BE2*7v`UQB;1}_nZ<9F6Lr_nF?ofRvRC$;uv zzo@jUIMPujy;4>ZT-AG5mF17m^j!-@H#J#mt&j4ATVR=5ydQbZA0D^1ih?fU1vv0c z0ay$9FTLR(oD3ETR+VR#Q?`r>%#Wt|kd&E~oM)<7<%*n+N^^V=R`P~T4pUTWDau=<7H--6 z4qKr~6tXR(=UoRr| zts7SgA~!V0iD%EM-C3xHn!`fKz}iz(V#p|Rk2h=zU5_=~G&E*WbexxKnbVdUS1v>( zy*UV@I3+IsjxPHuIhf8OiA!zB|-hPEdM7e<)8h#wpKO;Oh@4jaX_JHL^JK1B$(FL%X^PN*^o~ zmR7$U%AZ-mS;#m3-l_nK6tewQR@2VCRqtCjV~s9=VV}23oCY2TJFA<%h%%0N==_!P zJnZV%OxeXl*}qO}IxPDbMVM&fsK3J{S~s@J5w+$K&?z)K+dit=&tGq_9m>K#0UBbe zpbXnD@GX#FXQ4Ec^#yoYw(^JfWPUxmgg)_7W38b*$F-JYtXEi#tJ7b%rbPZHMml}c#(v#H z?BdZHV8bx){o8`tIg8Uw?UaWLIKDV>?+kC@gEpY$&5HAH5em5hxl+thLKIHp9 z{~Z$qCM@EBf>(G6^f4fU!DNoeH*;W=^^OvH*WwrnP&jDpZcT0oC(Xd$o`%_pl$4?I zv)_aEiRz0VgJ0^9PI3G#7D?E{y~Owp<{!`b{^p8rUgLFR(9gmASvP%Z-%8I8qB)(q z*8|i`X_1*k?SzaO#Uk7THN`?uBL_jt*bfyLv$S!EV?w}k;u~p9B?qx{Y+m;muF~87 z*k2o=Nod2!M>kFeZ3gjr+}U33$do3{ipg;)*mr)QquL+HppET@%t0+WjP4|Fp8S)+ zdh)*dVw$jCdmprn!wLIf+_O*{8q~X$*@9^)G9y zz*HP}dLmHZhioWb7>A>F>^Og`&O8Q_OM8W{iyWt!qhdmjL(U8eh1fL7=^r`lCd}V{ zZsru+9NMkqr!)48(30mEzY-wPyUBbyk#3;%wA67slYafdoYrK zUd#fvF684SXA5Ji+#`P%shOKwV9k0Lb%ec~238F>Q~dQz-mUncB%Ew2})H;nj;7`g86i(Isb=I5^L{PUeK9ot3sZ#m%(+$mk+3AOcGKy28 z`)p?j%y1VEzUlB**h%n%_GI1+`SA)8t+ACUa|1FnRmp=eQi{0dNe@s{13G#gwkuvWh_ z0~yzH+MgpsMa+f6PKK)%GB&;ScNRkYF-x!cM5q%L9h3yv%97^F&slDd^jxOP!$zk) ziHq86bZ&JWp5H{?zaW zQEq&3b<@DdieyFJJ5I6ya+vMp9mPq~9XXSzXt)EYPy-de!UCD77A&mbO{>mzSC7LV? z$&beFtf^mfWNdJ5gYAFiUPM96j+|6Uy`ijK4}!$23+ovJw8i)_LC?!3d|!X* zSzFyI($q%E&PxuF#YlQpM1ET+@WC7$)9`=|rzZ}hKDYi4-vJ$=MFsYrsJ)PD4TNEK z@H8_xp4DZ323~O<@3j49ruwxJq9eXu=EXE8Za8sgKe++EK+`uuZgUIW9nK-3e5r)Sv_1*;`l|?{G1$ zH$>-bR@5QxwIM=_ z5ir`mHe%OUHRikCbUovdUudX7>WE1}qzAk3pucv+`(FpS79piR_Dv5MCGS7F%w;jW zUrFf}*Xy-MWTaik^DQONg<(1}FqBl+BREckXUozvgop@c_;mD2Z4I%J#^Z-lD3Ci2 zz^AMh3K*wPJ3g;_w_4B^QSLJ`@F_#HMC4|TZqYV-4q-76Fk~a>7AST?I{IYIFqFJB z)YiB$#mk$>H~_`8BN1?7mn5&+0_f4wj5!%;bs7Ta017Pyi6nO6`qBDqXNsinh>KI& zYTOFrKI`!k<=ijh2~=NvEbkE1g?beNf1*t|pzKv{0Mo|f^E$?N^AsQ%8g}omQ0POS zCa(-W~^axy>Q$>bO_0DDb2I{6iM#F-g4Mkv+ zS0hC_mZov6i{PFVqrb85-H70xCqA0n{vsmRX1DHeBavyED;aXW(r#%J6oI*7(J4?l zg)2FU+X;CwsKVM7o_G;oswhv!kEirbyIYTgySY#gWtnBsF}sVMYBML#y;F+qy2#1c z<2pC7zaBT*wHEvoa)%9bI|{3an~X7eXuV|KO#!lhKvZB$!T>D6{S8FLbB~q(!*>O0 z#FG;Y^x`yI{2$Cl?PE!+a7{IvANV|*&69Mxo^@E+seffrcZPp-nP)&!WMTGdnz)p|BSM$#4a5TuB2zH3?a1J?7F*P4>vQv5N)f?4$U4wR;frcG5*<$dn6xAr2QdL-WdUN?BEke+pEQ%%Me~hiwW= zz&Q!YI$Z@+A-|zk@jlXFv|@>$yv`6QsD`-oxd#Wh-JBo`Lv}Q~A+-Ht=)n@NSGzz% zx_zqX<5Yiu^tgHb%a`?hKpJetgxEy+7%0b+P6l$Qk{=xWMvy6Jt7=9$R|P7roFfa7 zP!mWASE3Me>@YN3;y|>Fw4Yc?naGfR?;9lsT1?Z`jJEq2G^BA5`WPCUy4P*?9@6Ku zx(#DLU@rU4LfU*!uR-Vjr)Ta@=Bn(?&eRu;DXCO_ZTt;_5C1C!e#WID4{(nGxRXzZ zxaRjr=Ae3b&cdK>IIbO@;k!3V9f~)tf6~;ytm)N@k%vO*dV|uDg>i-S&vVkrjw5Er zX%$XgA&*qPe$Htr15|6r3bjO(5qoGwJHWB#38XM&I2r$h zCkN?JxTauD0edt9LDGH(6?3Elg0Z_YWXEPgCBx&&I3H;qJIWaJ-{ew}^~(#3fi=8d z3^Idr`o@c`Z1k~iA!evQxb=*NB39?Y|5tw@F-DeBi}z{|Dz zF!ubSyuX@bbeG-brx%3DH;d~kXvh(9k}KjSB6X!Z=v8RAcw-9pASlURPQyq+J*QF_ z;o5G8fTD^pHd)R)T%snJEH-oPA3ksIrczy&;872NN($K>keZ3 z{NzAh4A=G%)VjRpOKt3n(r|+8+vN2@xxs?RE{=FN&0Fjg3mIBt9)9oYk>Wr4gPRL* ze8wC{{}$tmk?hVKol!U+w)X}v zUg}VDHE-*#ncXOV^x3<~wtJ~+PpVD6 zwX6qGZ|2?09Nc{EtNlE}9)8p)OKWMgR~p5!?ltwJruWi6$*2kWrF2#5w)t&mHJQ9Z z%8y2klWXgmlvCMlp( zIO8a^rH$I#dCzU8N$2BTA2wN{@RGkeWJ<3p;|uoL^yhV>ya3gCp2VKYz)e%=^(LHp zwpZ`N?}pk@`{v}2_SQ#?ixBgSLA`{-?`#}_9X+umn~Ongq(mnycNZBY1~J+ZLcX=m zevQ||BVXYqQdCCRx>fGtI(J3Go4=R^ z>mLZQ>*LakylwDh20>oTWVanb+>vO#%qZU7dYEY5CT}!A686l`ORXco#s9se)9f_8 zwz1;_X#H}nxxDKe5>JV!;4UNkbG@bg3o90$k?3vX`?0>bIHUmDk4@dq{p}lCnqm$Q zpylpoNVmaU5>p9O(Kd$YSW$TM3X~C>IpufQDb#I5&vk~IYVHtigJe)c^r8G|p$_rDx)n?>o^Unf4D`ewgd)N?FO6wV0rwvc z`!tLlx)q@G>_uSr)>c z2FbKD^_4BU7zNTXScjDm47Dmlnv?W%*q+KfHg1+#u>H zn9V)#9dTtQDGvolV*E9i&$F~0HAY?g9|w=#K)8blW=||e0#YrmGpU`7)J4YFKv-jN zDj}^}V^zbNWe+fuHPlgPRK)$+CXd3g#u`^B-^kfKf!3s(RX*nD^l`1sy&SHezP%s1 zqnR%(FP(Y?zG(qpqG^>RA|@@LqUeP->;yV80Zwuy;XCkBM8k<520>=lO%8I_j4Orm zCz)eL1?{b(17tQoz$>^1BXRkIF;!PBtN1s*V$6%$;g~0DeDd)1BhGX@Mh;SCGV=@w zX}!kT8)p|;IzEZqwpzQV~|^1rsE<|^{~!{Nq} zoTl!b0OD>v5;~H%P+38QL$wx08#K8Oe=KSE1j}pXPn$x{pq5=T6gENq1!}-0@Qgrm zhmmSm`?a~^U1*~ToZRR*5zAN~7u7&xx8iDMhZ+5TUB7 z2!A~`p}jbfED`c9S3**{yd*NTgo_5I6Dissb(y&OqA)`JkwuN^+ovlWUXE3TdAMKQ zHo1;$Brh-|s)0@1GjOb|W|k@nLwiMs6F!xz4{y2oaRkYbU7Ws=;yu2-o{;Ef$RgP{+t^IIOs~oaEmXp&aDWJHjhi-DUGf|AiKfh99; z2U+(AN!Ti%rm>WVMk%u+4TZ#7hmUVSSJQxPQ$?a%FR-4J19S3PwXBvPa=8z0H^5Bg z2BMO=wiC+-h~}?&>HwCOu~#{^^;f6|&dj-|;fsB;P#Qa9C$Y%S0^v4@mU}oa(J*9? zn-(O7E1z#tT!jScNpcWv0+E5;gFXmmuyd zGSwcZ$GIlgrpsB-}uP!c;1lI=EMgeg?QuCXFbVvco`^joHYs?xC|TkLjpG$}h3i^Qj9|J+K5Kp68ZD>H!A7IafMteNbxwan`=>AM zts1PA>Iht8i)c!L1ocUMZiM5ZKN&I-r}>utRR%o1+U?1tHEB{f!skAw=^3+7t7*T@ zZUmG)9#4_e9dzz0@hF)YY#eE=3jO;OJcxl+S0&SJvgV&VXxAutoGEmJl;pojis}&~ zhBBlX-9z+l89b1eMv?>Otsq+tA{zcN+Q>Bf*5go0d3QAl)4PI<&8Ng^kb)@1(%#pb z(i;`qI^+_cPSZ&l`X@XfHd8mOLkIMA+M((f9fo94uVb3kajg%pHm zbW!Gt4)+GsYoO>(_@rQ^cX_rDSubvN>ktgrt;%Fri+D{#pkps3wqCgr+_ScE<(~8i zZV`43afaHkuoowOs1&^HraIV)boRRj$#mrY!za8-NQye}P7$t`h$Ez-z73t=0hcNt?q*60zIKfL}7Y1?UQ+!J2VM4q`Ng?Po z3m+Qg?K7*JY0-P++Zn=MJgb2>?Ok;>wtjcmoVeQMZ{XbJJ;jIL7u5^*!<4~V_Rzv& zHT2>j!~ku~hw7;UM9iq!;Hsb|oX+`3zkNZDteP8?4(Ui>KdcFciz@SCG&)+|l5 z$%cHrM2yIT4NSSlZ~8sE{sqhK;B~^yHIQ@~7|JMTp)XbwJx4hL?YJbe2u$J(u(Xfv zrte50m0W#LIhBigFcYO+2Q*Z0@yiaBO+h+&cJ_9C z<;@LYVAP=%L@wYWVZda2r7pmW*Rzup1R@#O5hR-~f4mkzrbcO#6bZW_tOXzr2f9x$ z`s?nrDmao1TAqMQ#Sy@=d2`EP9c0B8=9= zCk87fmc%eNo%$%!xi$_ym-atAdm`y!JM5Enw)Dz_NX)*{N5Bg3T>W<+d)g5 z_USUBw=cNLqL21H_%H!lwZE3SZ7w7d{$~zxFis`t>{Z?^%(kfR@n^Oj6_aZqg)3%E zr|w^&1zzGw0u^zsd?aZW*BEH;=9*2u8bPK}?r+_zmov5>C}ByS0v->A*Xi05sv$|b zz1Hsp6u}ztHt=h@Z;A1HPzQau8s%qKgFT>1tc*Zomyejv9Zfwny{K9yso3du7Z&$S zZM=2NzHCXPJieNCCm}II|snU}*Z1wWAV8kCrcC75u5(w@+ z?1%smGzuTr1hVy;Kn)K$Lc2p`P{~2#W)bklhc}e=xTyDFkxqRr5E{3*LKWs(L)90n zhzV@V^i244kNx_5K-&odr{-}I= z-nrWLb&aIZ5gW$Y0dLm1yBA8-Y;%Uc4#{1-di#o-;Kgf??s%aC(_P*>Ih?VrkUfCte55$bq`Gskuw!ikD>;PweM!XctO$E zXGAot*I(;pCPkbXX($#2r$wvcLR@^g1~E z(#wRW&6$JXc^(=AuX{9zGc zar?lXi}fe>-G5sazZPhjW!9gk>Q`Lk@}X}k;JDlJAy$Q&e}lM-f8unOt)o+6){KCb!corI|bl*ol zXQDV37;f{!R2ur^Ymfa3s=FyRJ$3d`?w8;gZ3~#njWnXx>;Cr4`OMJOQSre%^#Q^2 zwV$f+b|sHsg($jl&Nz+TD)k zodH8*LSF8S^qbgJPT(GRJTJYNn^sAo`y&^n@7B(Zq~F)b-eBBsNdELzH_mmXH%#4) zlw}C3YQoI(3(xfAQmNL*QnDknAHN)s86NAdY3S?9*I4T8xSB2dFycv8p8CtjA^r^m zeIr{akBA(evKhQel9RWQ}Ug%eQbf^XYmtGuiyfHq6EEOXsF} z{q_P}Ue2`X#e; z-e3G6+&$$9+JKo~)@D;CA7g=YxRKASwX=Ux(@<28=VWh0st=W;I%jS`v}V!$Ci(nZ zcYpY0q>z~~@U~e_2qu z-nkv!|4nO(-F(~+Pzjql;>R&vjGTz!EZ+!(Qq~+HEQhiS?sLUg-XpmHn1U?|D6xJf zbI1W=fqLyt-7T2IEx6dEYF|72o>qD9c)zgvQRneQgX*=iYD%;^T4~!~yuYZmI3_h= z?8H`qc26=!0-(gVh;K?pby4?WJ@JCX#?vr|KHLIL>!otdfwLNtOx=y`2DUjv(JLv4 zouplTtHMEMpg3)gFo~4HRm=h(ecWU-+y-8uu^hw#7Nps2S1F{(7 z()G(pMf;%B;A`AXs)VF9_>6nH4vM6Z^r5NC!4{?_eRW+pX?)uxOdfrT4gzn+RGth> zutM}nrtFVc7a$?=2KE%zqbCM?0CNB)!;a>M-z1)`)+ea4LCnB7=dA!DxUR@BNA|kA}$_U~jGtYd1JM z#8(E1mp%V%@w(|oq(3JkzV$@^L8#Y&YYpO(wU?2Qg6%sJ66kU6Yeo9B?gf;pFgJrL zG~OoEct;8MaRZ4V&O?sN#f&i(P+cNTy}~m7?csDqK5pJtDfuEkgSwX|#QSR;VS1y0 zX*{vMKnMk3`4$d}1e-=D24AwV>3KX6$elek*y745NYnMhryy(lL82*aV~ml6*EHycSCJ_a*vbNa&g-cUrfCDAO5S3{8eVHvvunslNPB=qdud? z90CQB&=)xl+6XzQe6)in4iHi>vLyZd_BHj zKuiB9VY$u*pYR^|cMeCq z3Lkt5nA-uoglduv6t-ziavuu8@evK3fC{=W3Qpu&4-t_}ySSA?9c8atzR^6tGk&K_}CaeE+6!C0vA)X7lWO$`;+ADP=2HV%@&|0*ToM^10}== zdbt0}x?S$boY9k_8<=yzbUR^0VRFxSon9){Z~6UOq&P%{`$>m@fSYU$v9&b7zcHo` z7cYMRN+^dC+Q~bkkn>L4H;_py1W{j`zy9G9L;HcIj>X0=K)rMlw%q5KIh!Ip6D7o2 zxqx^AZv{j=RRZpi;W!~w`g5)^dy$`kl+Zzl!Gb?W&T@*uI1{^{bPysa$B@Jybh0)4 zI0=cGFb#6ba|FlICh^GOnWw}U`UpNo8}?U#aArdkb?wAo>-aY1zhMNw5JPfg=?gy)??g40;6ugD7YXz3$19XTatUzM@sJT$mO5yIvaL&l*@!u2dsp;dmJo}=zpXGEk zr9YcH$Hv{iis&dtkbc>sp#91#Ix>>UzbvR@eX$5`6m(z{mN*2o9wjPR0|a`S47|um#kw1YXC;6ZP2SnAU?(qb(E>IjNmXkR<&!u!<9dnr&DF?#0JrGZ*B$**(Md zlfRGeZaJXzqKp`106UB4IK=PuaYaDFYWd8;LWhjt={8~yr`L~I5haW6c9 zDlVehH_6djTp>bSL%ue-R=2FwSNbxw?$d3aTb2*lFl~}YN0n5(58x?$B%M%Z^WTUp z1l1;#>08%^*3LZpR{phx>MOa@TNQQWA8DXM2$+7+lFW&?fskzk5*DpM<+Y~47I)BVVs$(Y{$i3m z3FRBGDgcNNnnQgiOO7lga z+6f{&7Di)>vI^T+!n%rAQg#0PG*Qy;P$(ckchY0@@WbZ;DU6Yr(`(QZgYpT2qs9cz zqC2Oeqmy)uJ;@yP8I9vOJN{F0FKnBOJlz{!1?; zLhMr$Hn(FfAFgMu`d`8!-j9s!??oSDZ=MO>(!=pV1O{~{`3h1bidp=0dLf3r9Mj;Y z4=5FEqTxPx@nRT!^CUG&46sijRHF|7o9(Up`&;)CWo*IIWiKB`$v|15@-NDvAHvt0=An55lEw8VS+I-_BLFRy5`)-7?UZ2XUZl<&zKZyO*i8&) zwZodV@i=wGq6k=+DveEeJpJOrkd~8oQ%FMrL4(G}k2|dU*XJjE2M#3i|9Woc*0+9) zKu2*y>HlKy-NRyR`?ldp2rWiZ(Q1&CQlS!+8FZCVqNW)&Es&(;Xj0S6pta638K-99J+AwC-|bo4-?Mt(_j?!Lcl*P(X)|XtbDqa> z{C@ks|Ms2FF#mCbxpRa+kkn{SZx^M6iyRTUNYzjXgiDJ=Gk}azP0g{Nh@dL_!zsD% zqHLQnD-OO#s7_plp%MM&sTWN6%C%$l#p88n(=RAM>?EomE!EaQALcjl!8%#wU_OG5 zVOO^`u?H*#@16w}k@4O1^KAbREAh47uhtm#z%Rr7-SeUG;vTYf((lg!*CPno?~>pU zsrA?aZ;@5#W5M2q@sj!=3UF3)o}KJD(;bqLmxe~bZDK3L4e}P)w59k}_e*ygH`A2( z^1fOxrhyIA_a(j5lO6Lr(g1c28~WT(c-zs`$zzE#zp-m8pI5R`u;qtRF%Ic~VlCsh zW!jDYSwj>G#HAw3=sctMPnOT$`_9if0U8%@NAJ`gm# zw8udS5OvLWNLc{Vr#2r(LXUwO*J1Z##CR9#$F%oLS~Z%3iD%bpw|2%DC_rh=1TE=4 z$fMB&aM@M_xSFz}Tk0wVSyU@vr-WipgzXuEon~6)dk1F^6@1K6Z@GN2=mRHAvvA<+ z8RtW40peqbCEV!>O$Gy&aE4+iRYx+4rQXy;nl6I`HocoP^&D|7tXBL82|duO?K4X@ z6w(k6g9Y!a393OjG*exGUl`0X;4-a7BDI~K8m#YSDDSXof2w9E;!a(Mj|j73sG$wI zzlWIi6H;l~V8^E?Qk6r5aB9Bbs>42i2-66Zx_$4;1+IkVZ1a>>IwTL(LXrdqwE}zm z^6@T$?^rl5mzGd2Ived>q{ODQfJX>xD%}Qkw)g{JdNNNHnn_}98bS&M#!a{t(qnzt zZA98q^5S772pdNgOwPn-GrpGEjKIdt`r57R<r^v*kI3@@Awf*HVZ4`2-?Ace5>c+bDGn8aOW-(BNxYn zmIZGa4Ejd2IzHU;#QDOHO0N@T8FY+))F~tb`X=^6l14fcX)5I1Pske}ZhW~Lj21Dv*fFh%OjHy1xY@(M?(VI>bUVYtmKB$W!)!eHIt1 zO+ZVpmy>v-fW!yd)OL|&tG;pz$gUg9E^MJL4U+3XnYW1mq8|DDHTMCj-U*7kRSkv_ zj$>_mtgjE9C+;-l;PXjkir!5aa10RnwdWHgP7@dv#!QS{|Gg9gyENuG^g4vWQ_{fq z(vX5lS3x^|!611Va{1!L@&U4%OA=kpi&WZntu0o7_~YVF)z&~W3DO-Rm#oTu@-~He z|9?nQQv!Ap)FqT2f+m$%zEm0s8A@Elfyj}Hfp*B-7tl*Hio7P)X4p6?lx#0dOXI6E z4U0J;z|iCbq#2h88uidv-v!;uAA%A#FhnVPet5wKf&4 z9gGB)OGwwn&a#v+%K@dPI&PelsGV4V^iYdQNh9sG%{eVl6e-I9!ASU%35^pq z;}%ei#@QNSd=%)qq1G1ye)8!z!;wb>VNddgcXYGXl2k3HtJ8E`%1ADUVA~i6>OCZE4lu0{@kI-7C;$%b#qggp{1C@7HBW|zgGsv!gcH z;t-{m2D)$o>vkEjKPyYCM(0aa*rV}Gk6Y&29G+%DLuSrN1-P&oAB|xQ-;T*^3FK2RRBn^HzR10PAX2&67m#~XBbeut!xP>1m{)pL$XUHlo@CF61Pz5 zfn1khCvXd{m_-_W?(;)D;W)vUEU2f=s*_mk9W?JVIV^R%Gd`7lm5eL$_S}>m@Ytd?O@ z?DDECk!xEV1w+2T>;d&C&AEFmOi!b%LAE&TUQbzH2WOTG>(`(^vX>wf+*0HS$3Qc7 zf;h&s5lm}yVBe1__^SpfPX>cthizAR>3X3?=!rVZd7+!S^`*Y*UrUD;eodpS=sSJ^ zh+yPb;cpU}j}1HYhgRd0eJ#2uh9JcRs6;FmNNwbA|1zN{I5WJtn&yagfUnHbrW+g%u6@-EpQ zUny(JE;2?Kke%)$`$74lnW-KKEr?AKbf8-tIV@8yV_a?8IVk{8Zt+mkuH^L6$DJCwVthHL(`RjL5Qu7MEv%l*49%irVXf zdO*RF&5;&ihlhaHL|9xHQ=O(H-3L1rLQVL^LA4a~UOL&iY@&v}c#f)?t8}2QYiino zP(mb@1^+{bnu~>10nIM&q*dWt@Jf^p z+CtV!Bwd&mkJSm5FT&d4F{JA+LA!yLx;!LFTw!p2?Jn^eyEa@UoLMxP-Io#Vb@p+Z zn7weqU`2oC^}!;(yZNHB%n?l9{OFX1_g=694%auM{`7=~fFD>mm zPJ4{C7><1g8i#)0?eOqDLHT(wi^5XvW$s2t&&_C(9&ryo&)EaXz-k+l-o%>3r8@m1;uT;!5T-sIv{NFIBC{66ZkFY4Z^p&roQsQwvP< zJ044kwEz{|2|JFQeF%LMB+sv5^>v75oN@uv;P+H6sGop|(Oj#$RV>cpHJZwk^WU0Kzt|cEtII!6fDD`H! z%M(xZ3O=$29q)yWv3`I2xEJwSd0uI8kWhEy$&U&hNR@a4Oi#oGrzrYBj^usFZYZ<~ zqwnA;IXIEsBlUtn%8(CLgh|4xBkeE05edo^wc5C__HyH+y<|_3gr-ZXp(bR%&0AL6 zPu^LK2P-~~m%>k>Gp_Ga!SIyDAPk0akAfp%n(R_)TZhcV99b@9rM#1}6M*L0&EQmU zPLON)Aj++nRR^m?4VN1!YCzEouA_DQ1cDaXa1i1?Xv6LNVa!To8?Vii!Ki{4btX*R zz`p9GEF_JX)ZzADnQJ5tFFj)((N&NP5)>N&WVOw>>? zFgBqAp`3a$p0^X4{+fHSBIp_`i%J!j5{E1TLq6?$xdyI%2~ILlAy|G$Z_aQ2VSuYf zltrNlb=+pQE~yL&{S9`i4-!&u0DfP8T&GrS8(ob?Bj-mCzkn?huRQ+>lzG~FpH)|0 zPkJ75`xFoYfC>X8U5WIR$%pKy(`!qlTe)&gBmxg%nr9Xw^Hc*eIW&bwNLklUm#$+i ziW>2_X<@;r$KAXJ9zqq&3exNkju`|b2Y2d@B?$pZP&dnK^MO7njpw3vhy6ksa53MT zLS^=ctqx?`R(fs7nq9f;j4W zA;;ZZ-J;>X0-Y@v3e~)V_?ZrOi+7tzH>Zl7c>?Axp@UxO(WXwzheww8KGWRz`8nww zvv;C*6a?ZLA8rUv8J}^_+2`BV%CAnF4uGS?WqstGB~lNn;x4k~MUlSa{vH+vC>%RE z9eR^MKKy~{BBH5J*imE4&C}gsOd{jC`ONq7v$E0p`s^jIO(U|zTc=8uGMpZ7&Kr`r zJ8jK0RQFKF9&p&1yX<0y)6b3FQ5OrBjHOSW`8;xVU>IkYyHP|bYe=G-I4Z~F#}rO) zagVIoP%YZG_aGxUVSm9shMvXM4hB3GDM75~t~V?>dIUM{^&K7+igwKZ|~=68KjCwsMi_s-jJrtl3*v z6`pQYXq>r!NHKol$s5iO^)B{A$*HEO&07yHKDhPz{VPh`)!Rqe!Kl+MD^ErzTB{6x zFg1<6bou#>#olZ5o6p?N+pMZhEo`5cGZYT|_0ln>djD`~r#&@`cdapyVNWXkb*K$M z82=>N?LYr7g|~r(*;R0sEbd&~XF-Z0E^8cl|KGtF0(Iekl=%GL|3BA&#h6A+n-|#1 zlY&uyYXs2$H+#mvt@yd|5BEUff0mphWS@?UZj0%r+pkje;zzdHZRd@>A^H2S-|Vpb z0w8#kIJR21%Nybnn7jQ0mJfsk#yQoc^3H1 zrH_}#q1CA%To3m%?}4I3=?JjQ;6$P=t~<_5%0(3$Qkrd-D5*x;KHsJq^Rn z>pATGg}NbM^$Ydo+70yIuk!cr<^Pw@3&ml}FVx%bsNkS$J~ip*`o*n5DpmU~gFhSd zCT1tjU9GjXTFyFCp?NgqXzKQ^b&1xA)_YOq|G8NA|G{1SfBnlBY7^k;i@#7OHUQ7a zkX`>W(u(^xJLrF;>M~KN=Sn7jmM>slr!&J8LEM)37b+?g_jzqLUzQWv%$K>yS9vrd zpuk%{hnw;T(0b4I{5k$o_NU@fU^bH+Ab)%^ZDtu;8Ta_DS?AAHSMc)?7dN+a!ws9S zMzd@3F4--B(DPZ&WFRBmYJSEWaxf7&EOj_wH3sxR_(ye@mDUsRV8 z;6j>1&K}SI%lY5Ft-mm}=k}dTmxqg=7oPVXET5K9u=LC8OZIKN%$pE@I>;T5u%Xt^ zJPVN6p8(q{s3|sv^QD(EL{eJ(&tPh2r?e0wKaLsTjmlIoKV52mp+t9maj%g$bNtVr zz*j`{3)Qap&tIF=_7k`KbMY?}4mW$B`tu|;fJmF$O#N&D{L50^fDF!9sK>yKj>?au z{X%U*PfcHz$D*YLgH-s9CFYaVF|$xA1tDADE`7c!%dMck2cY%c`cV0fy??%`OJLKd z(S9DpNuT1X7@hO0aI;wdS}gP5?F@^@U^*h%nfd_aw+_Xw8bn|J*Epz@7M>np_{GEI5vbFv186L*xM>u9L7oG`gS+UP1BpW~T zv;CVy-X5KEP||wPyrO23J3oD09i&YDV{-PLj{NsBRg@p;vV5;hTizIsn|IYm1v8=F z;I>U&&g%)QDuev^Nb~07? z;5qCStd&_S);{*5U%Maj_4FO+)a)quT=3S#hS694b`$i``{SPRO&ZNJE1%iD*4?@_ z8}$s3LZ=#qRMis*nC59ZHoW%zR|ofkpnk8`Lct$4#HHljg+V&WRgHxcS@WABREs^w zx!(p)aQq`LhUhbTYreIqRKRvci$DK~(*F;2$Nw+9{NDwv-v3SM^lvwxH-CT$4Q0XT z4eUnw5Di6M4LiRahb>=nuN9`)`}3HEaSPi+bzF**V6GXx<9F_GVSe;nw`e=C*BbsE@?=!P=5z@P;}E2W1S2p?Wx9^8*tle*XU zGF>@q9!k+FShZ*KlYH0KWfLYyO+(aZCqER~_Dz0xbvcl1)cl!x-OCol zkTLQ6S-hjX^*BLhl?XV(r0IaCT&%^zq2Hg>n;eglScs+YlL*&V3p z>;_=d$RDz+9{~<VNVRdymgQ*RLNA3^9ls3sltQ&}J>i*ka{p zAe1<8Oz~$lXclfs4Mm-r@-Eub4f#;z9Os45g|3aNdBc_&8jT!MP=wMY?1Z3Gr zV}wqSE>DgzuK1+SHV+BA9d$_x2>O#9_ul8wERG;iAd!&R=?c799r5GdW zlI_w!7~Rr2@J&FUPJ!CQr;&sab4T&M_g64C>oydZO=K&@V%H*oZm&yAeFXCpvBu*% z4YC$74HrY!BHIRNw&XG%D2a=FnJcU_Bf?9Ur;BePwPoY|yzqyxhZxgESvyhZ8uy1dTzl9C%8CWfnWy~3 zvDwFJ7|(kPZuN9t_21&WI%D966^iT(`H7Rkh7wOwYW;;;0A#x|OA)OJgW=12agfWj zv#4{5O%;Sj&12)K0rYsF!(pmFsJ-Q2RhH{~0$vBE3@j5K{BEtwRwvP>xY?=2Mt$OY z#Qe6xT)PoPml2F~<)pYqR{Nb6lc$BO7Gadh7EHt&`1{Txx8W*rN52g<_F8BX4k(6$ zUWH1XA$Fr&Dem`%`W}o^`a!tx7THBK`hF9`+lC%|z1{r?*g5PQhAiXbbbIh@TLiXWq|f@WpHC<3+Uz zmhTeV1!k5jhe*NixF>+L=kd3>_s~Yp&OIf zHvwa7OzFr2s%F?*h;O87%Npcc_vMopCAIRhZUC{Jz2M5cRm+2*W7pnzk+NL+rz>a& zU-X_z9D0d!p^p4Ps4n=0nl_SH6Dd1kr8Xa;XeNoh(C2u{?9F*aIXNTCk#~ppP4R%~_<@5lP##ROrltN5xYUxPY zfQuM{J(}rJ^W3RP5I;J!RI-6S75YeOUIRp#oHQjQstvmt(uxs=*a~053-i`<<%VO2 zNrxtR{V^p$>SI}l2m^O-@p$8>s!=o5X^6?pbYT&xox^@=h;TTD;>kGqGLH)KBG@DK z5fbuH-&Ve=lS47Ur`Te2)<8#E45G8VXieTS`+YU_zZ<4lzOFmtG9U^@fo^y)phORX zw^~454OYP}c(f4*wj6bGBoqad6OEXHu zO4XTZDr_s*$&C(St@7qe4+72DPo|kl-U{e0%gk0h9}#0sT|jX0tH8xiSGUh-kSf2( zf9z*JuX$Qod*_fUpGOnll+ooz;O+lL-UyytsVqZoO~%UpSli9^_i+(FfD2nXSCcpN z0PSj2ORWZZ7gpiDbg2NplsGTH`H1u6c)PZk9dC0q_q(Cu(B9DXnA1zIDEthO90l@r zsv0gBkC@LSFc^?!Gi?>(Enm7tV$(tYh|pioV;LisdXqI?P6cZ8>!pa&;SfjX7m)nL zb#YFkVDMhWwE6T`!x~Tj<&Vo0#@X7`k%zRJ7syRWLwsCzw;I2KaV|Z= z#3kjKD0D!bj1KPI6PzwP$nt|o><4{ejHG?&Y~vfSPnw5{YFnpm+t`cCS?ech%YYG) zZQBnDrk}EbKRyH=I7saeMTYm&HS3UX|BXe zssMH(9O+S;_*NHXiF7YgC5b==M}WVWI>}h@F`^Ei;BXb{^nl+*2|_UwEKF95-Lc#< z#9{o}xz2Lw`np6uN4^GLG-X%K?eCxWy;tCUBXS2U65mC7Bo6X^8g$fqdhw{h2bAEQ z1C#QlHc)`jDF-g$k8q>rXnLetAvSGWokbXK#n1z0U9ZF1ZB?56RgcM77P|q&S6aH3 zS9R!ZdA-YkE-|9;*fm_K4R^{C5`q2q84xTvbHIYZOhpROJT#c<=dfcS(lYR^87DXx z0|hCX)DU9Mc;&DcM0E{v>OMayv`CLEnPi_<6&ly+xW;A#`0Ho9RGKsX2UXYqNu~F{ zgI9L_P0{_|@XCM7`gyWVx}si8ls)2#YMXGpI9FM&+?c!@SyX7*;-hgCj*FHm7LX5g z8|NHDB1QB#E3eLx9NO}+Zc@#=&gN?^4fJ>i8*A}tFwpA#>htY$pS4wIPq^Ll|AlgE z`F!W?g%4yht8}|tsV~X1KyR2-^R{fg*F3xS`D=(zA>wjeR!UjqTp=}s z=uM&O*ti1pk|8pv0dJi{Yz4GDg@O)N`wTu1= z1aaY9p!x;P25WgaWHEl`nTGjg_%-C2R#;@;mz7=k=uFT!NaGSadNRnmjeCzB#AlZ8 zb5E-&`uX_bMnkd6N)x-0pygOc7T>a=I?7A|~us3gqU)USwrL`W;M;IA5 zS1)E?c}P9AXQK0in`=1boh`m&C;H1<7raJ7(389k^{(f*HM!Zd0ft_GU|4)8WK{at z3LNHDAxIA3j;S0lkFU=DlouyUe#)K?SV_qd*h({Os8epB#^_B}mkB%s0dx0auKq$9 zs)730S7%fn_xSzdR8S$Uk<84014wDt)+65ZqsCQp{*fB;UGo7l(jaxL;Ne%<@rRJJ zbc-})|6brl^&f66kM>%g?kTejTgq7loJT{?F4_9U?6~jM1&TM0Khl4Y_k0`}MwfG2 zZ7=M-!ku57D~|SzN$8qkXfr%fXJTEtDvp~s7d_roY4q4FOZ8cJh_!<4fCB-<%69_w zgTRLR3)Qg~g*gLLWl9b>Fc{PK3k9U&D^Zc@Ex^=Q4`?^!8Xh2uvG`rJ(l|dADRZ@Vh0N;1GPmGgnLAHf`@jzI|2JaLxQ)+nXwzf~NfC zUK>tKqkc|`j9c;X?~$yL&L+Z59yNL$k`9+T!-dKDuyb>g4{)sw*zj_{W>KTSz5g5J_)gU?g$X zGtI~vqk~sABj-#XmAYq`hHX4r?$+CUsPN$3;BMF6%NcuD*;rN!lGBZwKff4VaZ&$3 z{Q$C>^IEKD!N8mu%CjC!cA z?tTkyGDZylLgn9Y@(^Ib*lTHDG-^3ME1 z$x!!!$VZz3`njsk8$h91)jI4T%foJ!ViEs*KGF$hO&rW1d>elW6AUzgBgWgMgw*B% z#U+J>0#-xQIsi7f`CyPhvo#X> zjGQ4}R{hLq7^)AwHL>LUNwOwi1ulD5)AQF4T4_$j%;aIC?bV^$YR0TI#AcL z?_u>d%)_T#eZ{Wz=?XB}yKq_5nl*ScPEue!n8y^@Xkv6CRylVvgbWyO4VU zBzTAp4m_v~*JnZVxon8GWTbVmT>si-1~*T>C1*e{;|U>p@yMBRSB$lUFvO z@DXSU{ShM(ZJ>Kpd=PfNm30JJbQK!ikGNbOu>y(deP<9VkYn3n?>)dQA0TSUH@bxs z2Gs!3DQ{zWMX6=_7uV~X-hCaxY(jjXE(`j8#7K3u zlbE5R7O42vd*`Ac1BITM@3}55<51g6;B7<03n}H0; zB2t?82-G%h3@eE;3Y#P}o|3p7OoVlUlWlc%Xw0#QX^7L5sq<+*Y7C<+HM4jT5KuHR zkBRH?t z&82C2V_jq0bZQEN{GW{5eb2n}o-kzIk^Chf7&T82UzGhpSuB4|MV|qhYNeMo!waDp zZhRP3JNP}7NE|loC+`&wkza2Bv*BYz44=t3Hx&yO5~zt2Cb60pfDZ z=<0ZAki**2tK*SLT|PFvvabtThHs}R~ zoa^jF49`nKa1oe@7`5J89Lk9>S zvYIjaPh^iRuxcFz(xYs+A`Le5Y50c)hlXg0@->0lnRT)0BHSwS4zMn&)8d9rVNJ&O z-11Mn)Ox}7@i4PL4u^P5cQ2Szn8P>W2HHpXFX`&!Gccj4Q;8k}W~+FLLVz{4bS6cJ zykWI6vInemxVrXP1DD4?(Z?|J(d!@*7ZL{QRCIjP4LaZt`BhaYKvhqJ7cE3&w}DIV zv|kg}NcNDj(lW`xbHrQK5kMx%)t0MBlY-G?>uE)g^o~B}wqX;HpGHt7MhxrqKDPK_ zXYlZP*pr`kKDuf$FMWJBd&8q2^d-5pXWV#i;bbsdsr(B@pQS{y6rV@(SNd*-Wxz%P zF^7H}^f7LfFH1GUuWU3(P)jOYH|SgzXy^hj>NX(`9saVr;l@^$6}8cZ>W4`S$0>(U z){)oexqw=DSgA+8n!FA6h^DNOx*%a9I{kGuJ+hQPO#>9kzk?}<%S~)Gzi!qVvL<4}uVQmtBgw8J-6{b) zYSwpe5iXit^&P$3Cn)>H^;he1zfWK4ISd=tJ&QaUcj2Xh4d_~df7mI426+L*pL$&) zMJE931uNk}I!tYx*Dp-N8U@YI#83<;d3mL!M1zfB<)xM}PlbwdO@AD!=h}o~b@A%X zXUu1Od1k*qOyt>LK<|*1ut##JDh}R|lDO=dESITNFovskZsmUv4vP1`Cm#{w^ho_7 z+eTl?Ro*}8mg|rLJkveW1gmKZhnPbtg?{9o5fcK=N)`S0*IlWg$S1p@QGrE{eX%7W0= z&Lpa8wY{XL*mrCgIFW<0YH5(|wS<(lL`jPM?*2{ZJ~ssBryAFhRAzLXl0Uocy>xo> zD!mgK2aYK(O}OvZzM9K%D>u^RXY7A`W;%R`9&$~VH)RoY@8l^{(k706bm7L!Z}N0C zePe9xJzqCv9N+u8bOVo-QcI>)!NOa%I0Q5%Qix?@6I0U!I|lucAQq+3Z}mh z1V9{?n0MUl-E^|wCh66*rB|zX_H{*h|Bmc;6JG`%&j#GqcWbYdS{H1>Dk<$boVLEe zS=A~1?s?j#`)exbE2n18rtcruOz5aOaK=x?*DtNPX~k$XQ3z}LExq){q3cMo&FbxM z+(Op>JkwuX)jIj~ThopIi6UTCeP{XZAN=p5fcSWH=1Hw@5k6q-&AhSn=3M7I3^X}i zE{;R<&m2e65Wk-~cN1~z*=J~j7-0yz5qIMnyA>CkXWPfrgf)a4%As3mo{|@w70XZt z!(uk?|t`c~AKjwt@>- z+#a>;USaJS)^*vU^0kAdgVQJ&9}58bLzdbt5_9QN;Ifx5h=^qZd{%s;9s*HMY)&Rs z(_!Ny5~0E8}#MogRyHZ9dp+n^W=0x9`ATiW_AU=#4Bj_ zMhIvE7a2nDo(V!1yMXcae!Hw@V0z&P7rrca=I}aOIu!yjs#1H<6)YfK#={}9Bxy*h zE1~IHHz}V}*B@6*wyW(rcb<`^owA4M_&CTeHm$;Qd<64wct!8`p#YAZRw*EQ*-=c# zqM*zyug(~n$5Za$uHLipHvO4l<(6_IUg3a|`Epj(0_7VHulgKA4yncd`}C6;*2+6X#E2`mN;aKR`|a+$43farwC2r`Arv5AhsU$LH)fLYix+3eU%-^u%sGkS_7e zpuzm{vkABHltR0qr%JVd<^TUBG!XZ2UI&Hj|0c(y#XCSBg_o7Gzi*MH5nGz6uXkrq zKhVKXd++}tQv}R>x_>KM>~TR!UP^)_6`rSWZQPLkTkI;g&zYV+eu*aehiCV#Pu}xr z-K}?1drEHYLuvT-1Z^0s^r{iN)DMOdo+C?IUKh3O*zfAJB{Af6?U_4XppZ28?lN$M z*y5XrF7~vUUQut1{UA609VRucAvd&tm2ij*1OqUQ3;0zrdPn7a z898gr<)5iLOfC|5 z{gZu5r(~-MfZ6^F_8!3Iw(;jT23}_$Ga;*kIU=gz;oHt%=%B^?;N3Q?3elQy(JXV> zgL6!ONO@$mc@m$ZMbU~A?0PSy(vi|Va7{UIBl6YjV}>fF+Jx9J6$G7ngh68XH6Pk7 zr9{i=xe!aoUI+Z;v$b~!H~c8=;CEc<)R%MF)wt;$4}#=do*|nXuK+ugfMljo1!?Qx zAtYZkq}{Yd-c49RUi$zkJkZ5-Hi!|fN5Y^s4n(xg_J3?w7(azI^cfEgE1WP3Gg+LM zD_KB#G&s`MWEta?)7?Cs zXPU!~68Y=e2xOw0& z51d{4x)|k#;@3!;#F6CU%IrPSsBKpohzF|qL}kiUxT~9`QI9PulU+w5xQk(sHlNHy<;Rr_h^!+ZQ4_!F6aM3 zVJNymyn;Jz#Xm%KgKluw43bPmw_S_lIocD=&u9Sy(x7l5>OPk)q zU*zBlRd5?wwxA?;qx2D$OLqrIY{(wPhE-^fP_UD-8z4_?tBAGr zg^d;TUD=7_3|mJX?Cqv%j`w#Blfu@aWXgy;RJ9AylG3bO_H|*Uzxu>DW>i5nu5=sT}7k*~xlHTwGfgbga_3 z;A4`>4czImf1(iCjt6);x|)^<{stq;pqrGQ63C2OS(sij?kb&(h123$7}Dr(sdB{? z!NVGGZS>3>AngMPJq25agR*NfB2~oPL#@YMmu3Pd3osXW>lC%o1Q&?@P$*=#q7$jr zwbGT|;tCnVm<=zkeF*v~S_?Xbro~8GfanUs22!SN()18ZUpV}sfUiaNZAkdkJ!9kO zXB<0c?*fdurtj;d(hbK?VxIqZAIU#|$d#|2t|dHGO8M&7%NWv`=L!igCv{!DIeBaQ z$u=dPgCx3;8x6v^Rtec%^m7FyhNUu+?%iDx^0lKSr!ms-^|{%{mfE-5F0$3Q!C@O; zpMI*IJno*mvA}z^Q_OD#OZPHN-GlcQUv}!QPpZTYj)^?dTQ=3K$d7G4oQH4DFg?^F z*o?f&Xl1ZYf>Biz{{ zJKuR2nGssX)n3CV$hVUC&0a@HR!zRkkTzkcTHt~v*VEg%zjp(K0EHYm8g;-}vhnMc zUXA1JdKDSa4&Mc-y0}-eJVRYK7t9e=)3A#l5}%4!2Kohe7q(C9e2O`+b9K@9V&RJi zVq5{jPPjNw8%r^C(a1crD)=rbU}??~`YnO9cB#+V?b5b5h{QZnQnZh`muwM~l5aZi zaj5)jhxL7GaoS*&9E@5wfhx%t0SIeJRg3$4+j8owBs&n&c?{HKMHzS@O$9h)iY}mK zndB|MP^D$+a}hD%C!WxZ>_#;(&>iF!IwMEflOJ(&cgd+%vTvYdI0Z1)xvADb+@wvX zM9mif-&>A#OK<{bOd-R|2 zK8JflAN5#ErkyUd>MGmk4D3;Pk=n0)uL*EFCCguIf4%ypwL+8%W$TMX`?y{QTw(RA zN?DRbM{;P}!`9C+GfwrVYqzdhdnUttt<#0@H3zg%SyOqIpj;V^1N}fY8%%CJx`D1F zvE^XS94IM+|KxVE6H(aLU|*S6BT+$TSqjj>U=x(YI0<~F9Rk4gY@h=izvzMD-!Jw5 zz3*yWHg2`|UZbX915B^Z${x+my2Q;d(@5Ou#;^800@PUXvcTQ5dG>b`TW{AjwDRqNYICdatN zLCZg;IJ&uL552NTuwlHHy}q~j-G#f}$r-)1Nv>wc;gYHC3;uWFG5@Gf|0jjRzw*OT zsV9h`r6;hb2>sN#q075;x3S^r5I{?;1vn3J*NW*AnYBvs;Uub#8eOcTC{nLXrB5SzX8ke@iie?;vL7+Vcm+Z6v+)(M}N zs`J#du#jEA^Z435k@keLjjRq%c_;E#7}waDf<-@rM>(OZIDY7O=9Z!p6Wb0ERILtI z^ehig&kOyz8&IUSLw)KmXw;OHX)UCi?Py?d2D~Vk}0AEH6YipiMqmJO_(vn zP4EX!cxmeG`5mR>S=rkjUJfWk@>}iR-Kk9vx)$?2dx(|yV;eklpufAtFpHq8(BxpB zDJ&LU@I1HibkR3Ecb{|a9!lHathnpvRsQ=%fH^kg>_6B+c9XEy!<-fi^ViSr<7luC zIovHxY_>WoRdHj}rr!dO6~-4V%edefu2WyA(;Ia3N}v1E=X+f}x3_;_pYK+h+9x^r z=_YM}D(X(Y@1G+GO>{qcrQK?IiCxw5#rqc2lcMCmd2(fmRcP67r7|jqh7-`F_s=W* zLREIg5?%)!H)H?C2z}Hy)tS$<*dm~-MK$VZ-C44e{oZ`CCtqqx`2H0$zaQ(oTK*H1 z$xpMvDIV@97>vf$`C-m1Hj%7vS_h`ngU}(oS zeeH3Dm67dZ6!0KoQs#2eZVk1m-wr>8A~RYV?nk-xS`RAQWdDZxY{TY>oLInpYkMF~ z#RjKVLEiuTY2?K?CZ2gq;9XTWb*QQsQP;7%wb*NeAVV&Ze)@rN9@gg{^Y_A#$Vt z&=`*z$0Qm@Unykv*f?cRx^>%3Up@fIo^=Uo(>>Dm_0lyC96~?DZjpBZry`WIAQO2Z z8nSJ&HLzl_dZZf*Z1SE}0KMdq@ngl2WiF3Np`2^ypV9fp1H&&lhS7tc2GO4chX2s& z3Fd~oeH>&yz-$LLwoKrdSt^dKMkKIHGgcoOjdO5>Yeig8;x6d&9Y_EbT=@46l zX{IS4M?Bt_k++-@hJ+q}4l6}TRYQd-2P3z2iw@o#ZOFrHXk5EYpJ zjA!2(qK51)+#L|MX_o;gAXQIEcA}FY>EbNm`HT?w@TBK+XecYr@(O?Xab7Uin9)@L zYqrH)>0CJ+D3+`DNXj)>{sol&N%9wk@>?bg_Uu}ieq+b_IYkg+{9Rh481){zo+6OyLy*bvi0qwCHjUw~SE3GX)=$#qS zQ<)_RtM*S@d~Hn`upK;N#o>KUOPJHwZoU?fE6b$^h3hn=@%&~hu&;{-_eO+{Hm{Dp z^z$(NkDEEfPv^I{?`UZ{99^3fG)SaRzL8}%CEv|oZN}TTk8s^4jJne`@pi_^hUypD zVi0-1y0E7(l_xj(#?zMV=Z}s!mINx*VD)wJk*}G<9-)>LazOf8O2rI!TeB-{oJM0I-`&h5n=MG*o#|pBw zSarO&<&TMVHXY1uRVTcN*_SaJDxtsv!l7h_+DyyvsSSn_8j z*dRVIvp8MDwIr4_}t=W7}s&8U--@K7sFtFTy>Kh+al zXOK!e8}Dj7=&-x&_*nSrJ@8HKL54CF>pGB#*?Ndxy`k8DgtlM7o>LbZOtO(DD13ZjXTjN3MxIqZJizF!GLw>=buMc{+qa~f*yaO0Ax#q zg1#*q^%9~=6Dg~~=Drwk<&T#iQgZ%3t-}7-9hLqqt?X3XlmeV4rr=|}0Jwz7nRIaR^#y8NZdJWx3epbk?OIjzP>5QUxE+47ud9f3j1u#Q}l@Z-K! z>Kl3SvCPXYC-&W~(==2u(B4#8_<{1^7#Js#?| z-TNPxN|GjJ6JoA5C_-gd!c;;C*<>42mt-?(5;8GnM96lEa79c)lkBn&!(?B%WKTvJ zGs<3Oit){Cdd{xD$6DK)zu*O#ry`>Q8YXR?HQtbub?{^BUd@9(FYVj z7t?zV7Dnb7^hfX1)jcm5PF5sWktLrD%4KK|eUjIcI+{;!f0l@c>lLK+{iZhXy4*SO zvOB_iL5*3+RqekjM%!o>=c69X!#;I(Km(L%I8I;m`UXGhttBk_naLaqLtZmRCD5&cL2E(n;97 zax4kOAh!`?M>v@_2{@VHaI!A(8SR~J9(i6)x$x0^y(Qr?6xFJ@x38MCWnQ<>wfb|G zdUsM{?={aaYuHbw)&*CL_K54*izT#L94tF|J>mA?`APjivYq0htIkFJb5akP?=OVw ziL;H=V!e~eJC%jCyR^<58tD3`UanHP(^a|2!S5Lq;;3D8k*$5`i0eF!ap;ACwY*GB zRwmiwV3FN+76I~Fq7eltqTG`Lqm6tZh$SyDMLr@`Cj@VPBh2J>lSmb>3E%p|W!65m z(3h_q(F1kfBlAE_dk~PxJoum@HTyH6N>d<#s33u>orV~_N#sk))(STL_%+0Qh8Usm z`2?M(Hc1jBzAE4+2!4h}7qwADL^YvPGl?Lcadu!u9nFZnQvy5&>|5~a{;FTpxQ1Ho zJ*)#d!$jGo|bq`bdQ6>47z9qhE(FvCRsyCHfdCxniM$?aNCd=B0y3I*G z9sY>xOTrXRTpRMUn=*fsP_!`TI<#lsxKeA|PG$F=JB+4HjH?RW*{W67z731;FLqqs zvEJihp+ixG%Qzu(g7t#xebh8vcZYX;>(Q_?Iu<$(s%FbV4!fU4z|L)gfj=){n_&m_ zg>H>WEs-+y!o%h+%=}f3^xW@dQ`rG7VdV=dR|Ax55IKrq`E-G4%5W~!)+HJ*YYQbY zdcMglqqytefw#4TgxKDTc?^F9Q>iS5w;iM^-$iv&tG#3U$Cm&LI027W^#Kf*84vd{ts%c2y7AN&mq}d3T!1)!D;GPw9 zKIff*2uuVmWK!=obs`5Tw~UIj#d>y(Z)}88FG?4%S0`3F64Ybk@k(gW%c(cXH#4q` zItd!Nbmp@O#|L?qf$N$M*3=Gmop-SinxTs#FQJG6=_W|16dT*DZ=Dt(0zQ{)q{<$N zSw=}>@mzD@7lIiRW0Il-$~;}jh5R9*L1)DDo}Z38+-|r^-jcYbxoB2D`V}SiOjG~V zL*XKR7Jd^C2Z^$^I6A|?Y%l6OTRIAy5Y|DEF;|sm4MctMMu_*8tPrMmM0JS@3RJt? zyo#{3r^CaVzm9$I8&)g984`A&+MoK2yUwgi*Qce8!NTaBa8aBK3%Q74HE?oRnl0*t z$!C_hP( zoLAh;E+F0Ec{7n4e2k6`EWhhWLdF-kJ6W@jjiIfcwKGOF7d4mfL>$&BizL(nxAO$* z`W2gak1bGb;T(MvXvXu`ViFT@mLcpYhGH65$ApDw#UEzh_YlgOSOO8)=DF9C?ikbn zPvjZaiee0Q0)+7Z&fd3h45|+v4xX=DZq%Op)Bp7EBb z5+9?@5Na}9izyPVxx5&9%v~(Flw622jI!?6SgP_453zoSd@gU7Ja#({pRkdQVIpt(pFbbWpQ%#5-IBNIGb3VPi%S&lgi%Pq&E^Qt9=tsmx-?` z@wClUbNe9Uey-j!e_)YOo8-}B;WePS+qA#wUdqR;ScCBUC2()a0`lIxBD>^tO4f(D z2ifZ9-5wtO=C!!{;kS?PgQx8(>eDh;uT)2q^aPioy4RO)D6 z&fR}qJ=M-`Rlaed*4$#1s5F@v!-oWF%Q_5%-_@$lF0F&ab{kHv%_EBpUF&`j#+_kz?x4D9$1`y$S<== zb&5J`el<7#RLtk2{vVk8%8MP-%x!ZUBBShX^jTSl>iH{_kOgTJzm&)#Coesa^92gx2W=Sm`9^N|#CMN~0Rb#6`L+c>U4lMwfEom#aSp6z+Gg8SWbh?kcLw zzNtLtFWamn{l0Ge;mB?K_=@mw%%#T#3si;WE_D>6YWAsXLNyV^I#np^n`Ue^DZYtz zS#5LN3!UmV_T5iI=el4!)yPItFNSa2SXG^W()6(e8l7!Sn$Mq|D_a7|je&bMEX-Q@ z6#l5Yw`nSKJc|)qUoj9ghKw9eHQSuo&|U!9u83PBw!glrJxZwFUF$C8t^bf_FK&KC z`ew=d^CsI%2OnN~YH`EAYa1t?^6dhnF%OT4eQ(p~E9Zo9?9)+@U<%M^;++kWv4*@k5ByYDqzc_UJzI!WP@$ zeXiPL0a4RVp>(F>z|>;-J@zh&Uf?6Jb4t$X`LZ<-+v#rD(B^7!Q$q)=t#^MQfLM$Mjy;O6!7f?<}2MI z6P&Wg&`4XlKozv6#Ig7WmPN+TP85v!3LRJpvvM+Pk_)J+x|M3(kp7uAJiVwVZGL5S zBxZ8@YqDHd<(n$Br+4D*nSr3(l~BQ3gVL-1FU&0rMs7?w`nw$a(oV9!8-Pc*1|8ag zYs&d42*g!4DUMQ(nVFGfpFw-FhP89qb!!Wa&$?3&R@^(=muNs45z7phpCHHSERCu# zEO`n8I;6N7xzcjbN4DwFVA9ujSU+Zqg0o(tPWNRmx54bHa3k7q%f11wnf<+wHP82d zDps~>mLw;VH`67{17qaQMmdue&#A^+BkIUD=)gBuZ5T;q+F>?`94Nsx2jyC77KzE~D&9<^z)UB~?!EDif zv*sm*_t=VQ3ye4b6NeYib7W{5abe`K4@r*jh54ZvX8)Y?)y1|#gY|0fURUxou;p;^ zn9hrWl7(-Ax|7=X8_STImG}LI5;MX1oZ=-1#Xf5uc>#X#45bgz#hv2YKEvkz zW&hd|*n#-6A4rE^%25S(&!64EL(csI@^ii*KNk;6iP&+kQ}&m8k7bTMeEci}mh@~TH$WNQ7^nlJI^_gl4_E|S-kPQLBG`SUfbGVzs&r=bt56Tb{=Pu7;Vm5{ly z_$=3ydEHG|IN-?9`J9`3wA}?HP2d{5eqVHLnjP2Tv#OrIic&~0&018p*)Y)Tqwv$e zyNCZ?V%<3P9cHh|4!M)~RK-UuOs?eA~(diQR-X4ox+CfKY9s6sx%t78Ij3P5Yd zpa~p*>UP44CUg-c!qnrX_5#&WI#3UtVG;*$KLYbh_rSu5=l|z9T21*EJJ?0 z*oT~NJr6pomos5wK`s)L0d;#F&Fnuo`(K12O<;k2Gqkjd1Ja=j&&SNZv`VHs4pjM) z(hNp~e%&|W<&=CdbSO}xd}Kr*|Al+UHpS8YhFe%UAvuHFG*A2;&Uq2TlXm?JE>t?> zE0y=tuHRvxEp+HW)$hDcbP< zw2GPyLNJNH@J6Mx)U6;iV{NBlaK?vYTpZW`A)D7q? z(tih^Mi=mwfLIJbd45n$YXxHJzmrGBEWgnOLRLL`J!*2c)LwRONBwt-U=H7(4kwLdDHsRz48dHh@X-p zgnC@9J_r!Q^pWs4i4Nz|YeO&%VKw%D{a}}j`L~H~e*fy^z4250QD%zlJ+By_@%^6H zWcMbU?E8_%cmGQoKNB|P$?9&e@?B4iH!$(`-71RlE6Hk4gLg|8FR=Dlu0zJh7S-Tc%o<`3cJeM}N5?jonDRtn$Ee zm}Psi$(z$Z*F}$tX|P1!3k{e^UNhNw@NxU*L;1F*5vpc8{UjtLFQ`6_cm{hZEG#Lc z^beq1{;&@GuY|#W^phuo{~gPlf0Q5^@s~cJ|Ga(0!?z{D3ORMT-KytbJps$TrlwXe zC9HLc|NDu{HY?fwh%?N=4^cm>>5NwrOO{zRgHX>T@9nOs%l3a?Z zj%LNSAY;}NXD>3sH?&$3+j0)G?fhJO;!QGQg1Wbxu(yh+0qerM{QN5VcHY5ZBSSy| zpfcd1AdcSLB#Q4DKf;&rJXmAz7>Dl(c$%bhZ=DhkUD$zfTZRk zMj-H|^Gd+BsYqCZqY0~DLAmXdKkS;(AvWl|77irSfJdu-Q&wPMB&T;HqyD~I(Kk$p zzN=4$IL5ES%V0KA?eGg?NQUw_Pthf*vm>8XrEt3c#{IfHso#bMcTevtw5EN@UkncO zFc>%TCV9D>n(6=TkVUk`T8lxNs*&ZF`cwyo-X`DK?!Bc0)!#0K9Q2(6=%f;gSy2xr zP%^ODz}~k^viG;h(*;Q*R;uh*iKQTkSG@6+PV2rmhwoo{=p{V~G+(kRmosMutaJos zC88bqyX>~v-=36h2?$Bd+Y5f8G&2Pd-iqopIY92R{G-)-X$l0YZ)heW|K~d}Wcf76 z=5T7i!wA|-0!Q$GAtL!A)l}{`GT}=NFgv;lnqhu47t*^yOq*5(BmD79BHn!k@p~8u zZmF3d*i{41uuhU7ibm)22B_cMz!U2K7}K94ctiVh4*r~jAM5ka`{2(x_>bHNYUY^W zi-bIXK$&{_c}29uJ91{!z-DW|jwO)Qo%323-(^^=A-fk5RJh(H8g})=A2JO7Q@~f@ zNt?r`^TzLetLT(@uIlV> zk&UutD;A*1zV)5ai`t_fDe?2R$v1zlA2`LSan1g%{#;qK8M2k&HP~IbX-8ra+`8fM zWITEQU`p_wP3L?&?w>E%RO*rs84u?tAZetHFNgd8E2BG3CDNij~&Xw5a{V+3q86 zAAS;TUy^OHKAb>5w|8J9Q9o)I%SXhXZ0t5MCFclo(NK#Qe#tk+0xehn3|ERJli zL}w!I07zb$G}hH7HWQV+h&v1z1>+6yJL&UQwF4`^YWHqQqhfSq&f*MWIpI)V0N3I* z+xRTxrnm4pXBXs{`KkJI2J1=JWcWxTg1K36G#1RsCR7aVNm{U1spg%g(Ew(nDZQCk z;5NsaLtcQbjZAd5mx=OjKu!~=1c8YD9Tt_yLwc9Zrq_2ro?V+tA6NqX3geODZDCIr zboj#dF<(!OT=}KGF22X{Ii)6fDk#qDnmCYTDXAj45>J2xgeNKV9`JP(W!Ux;*3oQB z$NrU~Zckg#W3Rp;``WOr+uMx#&>r=+04b8d@bm7Jo4khTuQFE}JVagy7*?G9}tEkk)W@2vV0m7n$5 z2ijKAK0fq1N<(LvyRK#hvK7 z7w@lM(~AG1I+*QOt>zGt?eaFCHu~oJ^|S`HMj0#Olh$5xOs<)@xsD~=B3sV4p?cHV zvyqshPw@@kVTbvbul!N==zqY!?N}r6@KBhbbvuxIM!7!{moI}jK*>?MoiJ1LXp_DS z@0K=(yWW?+W-bLIeE)}mYAY%Wpl68V4>_?PcXoh%wuLH*LqP##EWt(4#p=0TUC_6p zj_kyHB?=_j27Rx?$;O~jBz5eAcD-SHU-gR|CXG4O=@2utJXWTq*E;1~yz}WN>@(*x zXPy~nmbhLGNE1Kj>i7D^(I<_|r76A^GS>|sCdk~MC}+2MA5neB+q>W4W8TTK%KL}( zqH46dvdA1#S#zM##cV20RuC=ec;Y^*CXCaEtnCN@7Dg@lyN> zph(SQYDNdaBNX)q)k{|Vk!_j<$q3w=c=matKlkO+q=u!sHzD9?H z1U&J0?O*7p8;Ei_-{l=1e3ku%elCNF{)L6bneGv_7jiAuG*EI1z3+NeH`L5D<-YxG zxx|zJv$m;UEq3f|mAc(BS$NqwR7%p<^Qnfb^KFLgQIkL`cMI#o0rww$lAheJZnLAb zHR8YJ!Ls>QVh&mO{p1lvXx-{WxJAT&X-VzKtmWacU@>R{S8DfPBK~bfboGxWAfc~L z2~F$gn|AXbAHeI*=4hd|obUo%7aK?hBc4o}hN7%SaAA|g*e3M>oHyD1e8X&X&^p%n znfmY#`BdM=x~h8*L0j7O2%J3kpr9R5gB^5PnG6PO>N{j4!&Ocra^l1+%w z#>6|)%!rskE51*^Ct}QfKs&F%vdAx!6h|@PQ01Vq4w^NoVASW4 zR2j&#KA>;o*)Y@r$}p};1Ckp6P>GkC>v(po>4l=gnVc=d(`-D6375y6X4$l9{sfVj z3o^8iuDXRXEe%MRPm`Ftd`r@WrlaM9!DU-L<`j1}qQwvQ3imp3boh^%P&~o!DeflA zULf}b6Q4;;Q0dL@H3$S6p+k5D6mD~$V1o(ko5qNPqCu){rJNM8t&-Uhe<+wKTgE$} zE$=Vw7VbDGyhs|&n8-&}mc1mzkTy7ZU?j(}YA3KdJ=?R^_k;he>Ts~yk_1HcsOfi{ zTK1K8?Tpt+LfJm=H~N4JYEVlE+t5To2L|pF2_<|iFazGB+VnL-WW=#d*gsz$^LHBD z|4UzVXax!Jf54_yz@u?P!*MyiKlF1Q_#3F@e*pO=R zC_VWMgR3gS?vsat_A^xM&Rx6j-4)wu>ZcpCA4R`?)Bnxnz87ySMy{Gwrccdlv|M%$ zj%a48dFEPZ9~xqG$9EOjWUc;s6#d%H+T#4{_Ks_}8|SY-bZQ2uRFA!oJzK05uRU0` zsKq45F84_I(CG2_9k?@ESkg<>)`jJ;<{R25saaR@*y9{o^SdVqJNKL&_;k0&S2bl; z;{`>^z)`VeN8Lp~>xT?WsTlqToK|LKwrpp555>Pr&4o7Gl_m7+Q1dTOnJOD6eNUF? zS3k#g%wH_GC2L^3J+{~TE@z(a#eb=7xq$P5V#z#LsGKd#yqr1#--uHizy&brYEfcCF0o?|6&aDxI3 zsK(!|l~6%nSYP3ZSsx~vxDA`rG2=2DNb_Blc8w5O zq_&mO_JLHemeEf9D|&^tI=2AQr1Q6obRVJ=UxsPJ& zo$o{1B-jYSkCzXlnCX?0nI4784PL^_~OT`Qhc5+rWKaMa0`Rb@!Myl8{ z2IeJdbe+!+j4Nt1tA)B1iJ?Z2#G5p6j*vH7dYW;yE|>Q++cbO9hY}nRJh|h%>#VB> z>B;fn!q3&tSF4JSRyClxW4@l%YR3^s?1&pb9={n(t|4{eFwVrK*Uk(t}dAjR|fbNYSfZ_cDIc- zQuH>xM1+aEERp2iw*gz6YQKh2`HG;Q)( zQ%<@dI0`ke$(_<8edagAOFd378V@! zjm1mMBIWT~F0%+poG2@#1t}^}oCo>fQM{a$S^KzAa`-VvHYr{y*?gI>xua$yr{dlF z*M%FW>(MayE7ChH!6xI!_*c|&GEYjF(*mVUXvR0~21#W|&8(d$inU>g#0C3nwF(aY zsPGyAd|yX2V+n?>b!SVL5E!V~Q12L>Y>P1D_W8r8_fW_wOZkQ@$wu>%lnLRrP5gUJ zhJrzqTu;IXCxK6#;6GSc&PKNC*WTjUKF0S_w-_O@NMOz8h;+G+^7@5^NLn5MUH@ z?iE2VXBmU4nPnSP@h2C5Y~#qCnB;?6AR2Hnjy=PF_SZ{oNS!Rzq~hYJlrZ&Sn<9RRQTr z5Q3DyI>kCYXWh6La-;vuuBw4c6){ZH7igCqy&{!9Bo?SI?`&owMpZfpQpgLKgzqpB z>Lr^oHzvHbCz`Mhh@!v4w&ScJOyPWN2U2QzsRpNC$F6O5-(pGK4jpR|9LjbNGPJQD zwl2`UUsT-lcP~1#d)b(!Kb7b2jE?+6^bdMNBEHj~R47;4g;bqz`K zrx>H3jN*(iQHroy_NTUywFC8DA3E=d+}Zs?&|Pck#y>dwWq(~->Al+)LM-n#BBU;J zrz(SAV5H331~!LXxG+8dE_hNASTduh6O2jI6sl#>Tl8~BjCRVSAh!&ph+yly!23?q zelXRWw=u@t02z+O^h;zoQ9Y&%Z+q#NBOvS|#OQ;AERucPWj2!< z{gLeyr9*&W8(PK4zWBol9h|$3F;Q33>j@7EyDy5J=VuD!3t$UWFXUJcCkI*>CP?75 z3C&0Y7p|(H%XPIS9N0C+0$3Aw5+K*;l72N!k^@2W3W1iWojhe$Ht zIDy%`>Qq?Z{LZ7j&>}-^^Hrg*A~A%C7y2=|4C-xaRcs>R7y2CmpteQgd4W&~u&qRG zq55?@G0CkZ#%(hO{>-AX^;lu1P11Lm_XlK1B5oI}r$cd1JgtAk*+0->NSkZfWo7Zz z?BKYIQm@DpoGJLI3Z6+Ur-`47yx23^#n}mEam{fdkZ2K4xqb*?1|>}_!FUD;aiiT> zl{4(iWx!0b=@>gaQO+sqQ?KR<+D6>RNZhy#itSiUAcQ|DNJ@WzeI?||6>O?x*D~RY zD%{h&pMH@3FB&>=QmXhV2H^!bZD^!)9N$mf2-x-Q6ss_@^PF6EXM~Xq)K7EAw2jO3 z4YP9-+5qR{ku$nhdZ0DGs6Jvs0c?N=c((j(!bCY@gFp_gsKdV8$VC#Gjgj6 zjrNQz>6C~ukc)e`hemzZIOOn&jTogIB;1D}VI(t;w~5nM|JiM}RI_ zeIg4c|A1=e7r(!DeTLBt;k~^Z^kR`8QGJ938BaiI=>4Wkv|7i19gr89P*^xzLR~0l+Sds7@K( zO!wPPG*0EyaUZR@44F{(w8he#iJdA7n%qk^T&LL)IYAv06}xshkJ%GDE($eI5Tj^_ zTTS~QOo!rj$a6AIN3Soq5!FeOkhfp~uZ*H&cR)@uDBPw4DN5OYal#PZp6=R_fGL3@ zA6rl2Gmh zAr>S6a7aL3(#y!Kc~AoC`CEf9cU;X9SGv7hTt#9~FDra#ozZy%lygesq_Uqy$Ryel zCo6;5f`3&*3we2qYu?I6IkG-Yw-r*3v0=O&m>6jUY#Ea*jn|B2(69^~8WFctEspPE z6z9lc;CIhaiej5242UUdx|Fnd9jn{jT2h+%Mye3XSI1K)KJ1L zx=yh#M?ITU+`-1DJ(;sJ_SY!Le+Q9Fh8CBTEYNP|E8@Dx++LTcUw*I%qRJlL{igs$ zY?}2QX2Uuw68PVa+xSP!!2TuGu)&B7nyH(9G*g4ez}kfoA7RsZfnh0f{x*G0{pnB1 z(Lgz6B`;`p4H2Z697lZrN5OdiWmVX(PNXjfXGklIQo>BA73^xS$d^wOC}?+28#0bm zxyqCLvDYxNRVGvxt$rYhz}<=&gL8m2h_<0kODH)Br~cI0-Pe`ctSNmS%4%_{e^O3{ z+|D)1jo&MU^ju#=a%~25l1%x_`*d_FZonG39{5&54RW!J0f$sF7Q@+Gjwa;KcH<=T ze+?>&L%|7tsCQvN8G=*D#oA9IV+}$s7R>6$CJQ8RHms35IyM`8GKWg58C(Pcax!I` z7Cq_Q?3$Q^7#g(7Zh^8O@&;!rYJ3!zdSCITNA

1ony}C0NfwZ7blp!ln?L^vA3!z*u#%Kr6uphtI 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

j9v- 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 Rf4+9&U$AQW--Ab{{{!2S_+9`2 diff --git a/docs/reference/ml/images/ml-population-results.jpg b/docs/reference/ml/images/ml-population-results.jpg index ae4eb7609f5b0e4c1a4af8ad61c6e3f1c1a09439..7bf4c1d8de98a3d6990020a632c1c67cc5406fb4 100644 GIT binary patch literal 283347 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

HrZhb@^M+ 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�h4^<4J_q3^n)xx+MB~X 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

fmx3?kNke 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 + "field_name": "bytes", + "over_field_name": "clientip" <1> } ] }, "data_description" : { - "time_field":"@timestamp", + "time_field":"timestamp", "time_format": "epoch_ms" } } ---------------------------------- //CONSOLE // TEST[skip:needs-licence] -<1> This `over_field_name` property indicates that the metrics for each user ( - as identified by their `username` value) are analyzed relative to other users +<1> This `over_field_name` property indicates that the metrics for each client ( + as identified by their IP address) are analyzed relative to other clients in each bucket. If your data is stored in {es}, you can use the population job wizard in {kib} -to create a job with these same properties. For example, the population job -wizard provides the following job settings: +to create a job with these same properties. For example, if you add the sample +web logs in {kib}, you can use the following job settings in the population job +wizard: [role="screenshot"] image::images/ml-population-job.jpg["Job settings in the population job wizard] @@ -81,6 +82,6 @@ details about the anomalies: [role="screenshot"] image::images/ml-population-anomaly.jpg["Anomaly details for a specific user"] -In this example, the user identified as `antonette` sent a high volume of bytes -on the date and time shown. This event is anomalous because the mean is two times -higher than the expected behavior of the population. +In this example, the client IP address `29.64.62.83` received a high volume of +bytes on the date and time shown. This event is anomalous because the mean is +three times higher than the expected behavior of the population. From d06c5e2039c2978353ce56da485afcd091decfe0 Mon Sep 17 00:00:00 2001 From: lcawl Date: Fri, 30 Nov 2018 11:09:30 -0800 Subject: [PATCH 058/115] [DOCS] Adds placeholder for 6.5.2 release notes --- docs/reference/release-notes.asciidoc | 1 + docs/reference/release-notes/6.5.asciidoc | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/reference/release-notes.asciidoc b/docs/reference/release-notes.asciidoc index ceb0b85cd5bde..db2cd2901cf1a 100644 --- a/docs/reference/release-notes.asciidoc +++ b/docs/reference/release-notes.asciidoc @@ -5,6 +5,7 @@ -- This section summarizes the changes in each release. +* <> * <> * <> * <> diff --git a/docs/reference/release-notes/6.5.asciidoc b/docs/reference/release-notes/6.5.asciidoc index 2b35126a8c701..e0546a231c0f6 100644 --- a/docs/reference/release-notes/6.5.asciidoc +++ b/docs/reference/release-notes/6.5.asciidoc @@ -6,7 +6,7 @@ // TEMPLATE // [[release-notes-n.n.n]] -// == {es} n.n.n +// == {es} version n.n.n // coming[n.n.n] @@ -44,6 +44,14 @@ // === Known Issues //// +[[release-notes-6.5.2]] +== {es} version 6.5.2 + +coming[6.5.2] + +Also see <>. + + [[release-notes-6.5.1]] == {es} version 6.5.1 From 4949fe8eb388a9b9ad4fc3040c91763c52b7ed5f Mon Sep 17 00:00:00 2001 From: Gordon Brown Date: Fri, 30 Nov 2018 12:06:48 -0700 Subject: [PATCH 059/115] Add note about ILM and Snapshots (#36023) This commit documents how Index Lifecycle Management interacts with snapshot/restore, and documents a workaround for situations in which ILM should not immediately resume managing an index after it is restored. --- docs/reference/ilm/ilm-and-snapshots.asciidoc | 35 +++++++++++++++++++ docs/reference/ilm/index.asciidoc | 2 ++ 2 files changed, 37 insertions(+) create mode 100644 docs/reference/ilm/ilm-and-snapshots.asciidoc diff --git a/docs/reference/ilm/ilm-and-snapshots.asciidoc b/docs/reference/ilm/ilm-and-snapshots.asciidoc new file mode 100644 index 0000000000000..847bf6337888f --- /dev/null +++ b/docs/reference/ilm/ilm-and-snapshots.asciidoc @@ -0,0 +1,35 @@ +[role="xpack"] +[testenv="basic"] +[[index-lifecycle-and-snapshots]] +== Restoring Snapshots of Managed Indices + +beta[] + +When restoring a snapshot that contains indices managed by Index Lifecycle +Management, the lifecycle will automatically continue to execute after the +snapshot is restored. Notably, the `min_age` is relative to the original +creation or rollover of the index, rather than when the index was restored. For +example, a monthly index that is restored partway through its lifecycle after an +accidental deletion will be continue through its lifecycle as expected: The +index will be shrunk, reallocated to different nodes, or deleted on the same +schedule whether or not it has been restored from a snapshot. + +However, there may be cases where you need to restore an index from a snapshot, +but do not want it to automatically continue through its lifecycle, particularly +if the index would rapidly progress through lifecycle phases due to its age. For +example, you may wish to add or update documents in an index before it is marked +read only or shrunk, or prevent an index from automatically being deleted. + +To stop lifecycle policy execution on an index restored from a snapshot, before +restoring the snapshot, <> to allow the policy to be removed. + +For example, the following workflow can be used in the above situation to +prevent the execution of the lifecycle policy for an index: + +1. Pause execution of all lifecycle policies using the <> +2. Restore the snapshot. +3. Perform whatever operations you wish before resuming lifecycle execution, or + remove the lifecycle policy from the index using the + <> +4. Resume execution of lifecycle policies using the <> \ No newline at end of file diff --git a/docs/reference/ilm/index.asciidoc b/docs/reference/ilm/index.asciidoc index dcee0c06f4255..6721abdd473e3 100644 --- a/docs/reference/ilm/index.asciidoc +++ b/docs/reference/ilm/index.asciidoc @@ -64,4 +64,6 @@ include::update-lifecycle-policy.asciidoc[] include::error-handling.asciidoc[] +include::ilm-and-snapshots.asciidoc[] + include::start-stop-ilm.asciidoc[] From 42b6e8a89c7b5e442680b8e6b34db85828620f74 Mon Sep 17 00:00:00 2001 From: Gordon Brown Date: Fri, 30 Nov 2018 13:05:14 -0700 Subject: [PATCH 060/115] State default shard limit is not a recommendation (#36093) The new limit on the number of open shards in a cluster may be interpreted by users as a sizing recommendation, but it is not. This clarifies in the documentation that this is a safety limit, not a recommendation. --- docs/reference/modules/cluster/misc.asciidoc | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/docs/reference/modules/cluster/misc.asciidoc b/docs/reference/modules/cluster/misc.asciidoc index f397c3075b711..4a4624943d71b 100644 --- a/docs/reference/modules/cluster/misc.asciidoc +++ b/docs/reference/modules/cluster/misc.asciidoc @@ -39,6 +39,11 @@ in an error, rather than a deprecation warning. This property will be removed in Elasticsearch 7.0, as strict enforcement of the limit will be the default and only behavior. +IMPORTANT: This limit is intended as a safety net, not a sizing recommendation. The +exact number of shards your cluster can safely support depends on your hardware +configuration and workload, but should remain well below this limit in almost +all cases, as the default limit is set quite high. + If an operation, such as creating a new index, restoring a snapshot of an index, or opening a closed index would lead to the number of shards in the cluster going over this limit, the operation will issue a deprecation warning. @@ -53,16 +58,16 @@ Replicas count towards this limit, but closed indexes do not. An index with 5 primary shards and 2 replicas will be counted as 15 shards. Any closed index is counted as 0, no matter how many shards and replicas it contains. -The limit defaults to 1,000 shards per node, and be dynamically adjusted using -the following property: +The limit defaults to 1,000 shards per data node, and be dynamically adjusted +using the following property: `cluster.max_shards_per_node`:: - Controls the number of shards allowed in the cluster per node. + Controls the number of shards allowed in the cluster per data node. For example, a 3-node cluster with the default setting would allow 3,000 shards -total, across all open indexes. If the above setting is changed to 1,500, then -the cluster would allow 4,500 shards total. +total, across all open indexes. If the above setting is changed to 500, then +the cluster would allow 1,500 shards total. [[user-defined-data]] ==== User Defined Cluster Metadata From 0d911a81788640497b10ee439aa155470ab686e0 Mon Sep 17 00:00:00 2001 From: Tim Brooks Date: Fri, 30 Nov 2018 14:23:33 -0700 Subject: [PATCH 061/115] Make `TcpTransport#openConnection` fully async (#36095) (#36124) This is a follow-up to #35144. That commit made the underlying connection opening process in TcpTransport asynchronous. However the method still blocked on the process being complete before returning. This commit moves the blocking to the ConnectionManager level. This is another step towards the top-level TransportService api being async. --- .../transport/ConnectionManager.java | 14 ++++- .../elasticsearch/transport/TcpTransport.java | 30 ++++------ .../elasticsearch/transport/Transport.java | 9 ++- .../transport/FailAndRetryMockTransport.java | 10 +++- .../TransportClientNodesServiceTests.java | 23 ++++++-- .../cluster/NodeConnectionsServiceTests.java | 17 +++--- .../transport/ConnectionManagerTests.java | 17 +++++- .../RemoteClusterConnectionTests.java | 52 +++++++++------- .../transport/TcpTransportTests.java | 16 +++-- .../test/transport/CapturingTransport.java | 59 ++++++++++--------- .../test/transport/MockTransportService.java | 24 ++++---- .../transport/StubbableConnectionManager.java | 2 +- .../test/transport/StubbableTransport.java | 28 ++++++--- .../AbstractSimpleTransportTestCase.java | 32 ++++++---- ...stractSimpleSecurityTransportTestCase.java | 15 +++-- 15 files changed, 214 insertions(+), 134 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/transport/ConnectionManager.java b/server/src/main/java/org/elasticsearch/transport/ConnectionManager.java index 0ee4a71f9afe9..7c94de0823342 100644 --- a/server/src/main/java/org/elasticsearch/transport/ConnectionManager.java +++ b/server/src/main/java/org/elasticsearch/transport/ConnectionManager.java @@ -22,6 +22,7 @@ import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.message.ParameterizedMessage; import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.common.CheckedBiConsumer; import org.elasticsearch.common.lease.Releasable; @@ -218,7 +219,18 @@ public void close() { } private Transport.Connection internalOpenConnection(DiscoveryNode node, ConnectionProfile connectionProfile) { - Transport.Connection connection = transport.openConnection(node, connectionProfile); + PlainActionFuture future = PlainActionFuture.newFuture(); + Releasable pendingConnection = transport.openConnection(node, connectionProfile, future); + Transport.Connection connection; + try { + connection = future.actionGet(); + } catch (IllegalStateException e) { + // If the future was interrupted we must cancel the pending connection to avoid channels leaking + if (e.getCause() instanceof InterruptedException) { + pendingConnection.close(); + } + throw e; + } try { connectionListener.onConnectionOpened(connection); } finally { diff --git a/server/src/main/java/org/elasticsearch/transport/TcpTransport.java b/server/src/main/java/org/elasticsearch/transport/TcpTransport.java index 72d3715e8defe..659c264ab3749 100644 --- a/server/src/main/java/org/elasticsearch/transport/TcpTransport.java +++ b/server/src/main/java/org/elasticsearch/transport/TcpTransport.java @@ -27,7 +27,6 @@ import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.NotifyOnceListener; -import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.common.Booleans; import org.elasticsearch.common.Strings; @@ -46,6 +45,7 @@ import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.io.stream.ReleasableBytesStreamOutput; import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.lease.Releasable; import org.elasticsearch.common.metrics.MeanMetric; import org.elasticsearch.common.network.CloseableChannel; import org.elasticsearch.common.network.NetworkAddress; @@ -349,34 +349,24 @@ protected ConnectionProfile maybeOverrideConnectionProfile(ConnectionProfile con } @Override - public NodeChannels openConnection(DiscoveryNode node, ConnectionProfile connectionProfile) { - Objects.requireNonNull(connectionProfile, "connection profile cannot be null"); + public Releasable openConnection(DiscoveryNode node, ConnectionProfile profile, ActionListener listener) { + Objects.requireNonNull(profile, "connection profile cannot be null"); if (node == null) { throw new ConnectTransportException(null, "can't open connection to a null node"); } - connectionProfile = maybeOverrideConnectionProfile(connectionProfile); + ConnectionProfile finalProfile = maybeOverrideConnectionProfile(profile); closeLock.readLock().lock(); // ensure we don't open connections while we are closing try { ensureOpen(); - PlainActionFuture connectionFuture = PlainActionFuture.newFuture(); - List pendingChannels = initiateConnection(node, connectionProfile, connectionFuture); - - try { - return connectionFuture.actionGet(); - } catch (IllegalStateException e) { - // If the future was interrupted we can close the channels to improve the shutdown of the MockTcpTransport - if (e.getCause() instanceof InterruptedException) { - CloseableChannel.closeChannels(pendingChannels, false); - } - throw e; - } + List pendingChannels = initiateConnection(node, finalProfile, listener); + return () -> CloseableChannel.closeChannels(pendingChannels, false); } finally { closeLock.readLock().unlock(); } } private List initiateConnection(DiscoveryNode node, ConnectionProfile connectionProfile, - ActionListener listener) { + ActionListener listener) { int numConnections = connectionProfile.getNumConnections(); assert numConnections > 0 : "A connection profile must be configured with at least one connection"; @@ -432,7 +422,7 @@ public List getLocalAddresses() { protected void bindServer(ProfileSettings profileSettings) { // Bind and start to accept incoming connections. - InetAddress hostAddresses[]; + InetAddress[] hostAddresses; List profileBindHosts = profileSettings.bindHosts; try { hostAddresses = networkService.resolveBindHostAddresses(profileBindHosts.toArray(Strings.EMPTY_ARRAY)); @@ -1581,11 +1571,11 @@ private final class ChannelsConnectedListener implements ActionListener { private final DiscoveryNode node; private final ConnectionProfile connectionProfile; private final List channels; - private final ActionListener listener; + private final ActionListener listener; private final CountDown countDown; private ChannelsConnectedListener(DiscoveryNode node, ConnectionProfile connectionProfile, List channels, - ActionListener listener) { + ActionListener listener) { this.node = node; this.connectionProfile = connectionProfile; this.channels = channels; diff --git a/server/src/main/java/org/elasticsearch/transport/Transport.java b/server/src/main/java/org/elasticsearch/transport/Transport.java index 011c3214dfbef..e44e0b7877c2f 100644 --- a/server/src/main/java/org/elasticsearch/transport/Transport.java +++ b/server/src/main/java/org/elasticsearch/transport/Transport.java @@ -25,6 +25,7 @@ import org.elasticsearch.common.breaker.CircuitBreaker; import org.elasticsearch.common.breaker.NoopCircuitBreaker; import org.elasticsearch.common.component.LifecycleComponent; +import org.elasticsearch.common.lease.Releasable; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Setting.Property; import org.elasticsearch.common.transport.BoundTransportAddress; @@ -86,10 +87,12 @@ default CircuitBreaker getInFlightRequestBreaker() { } /** - * Opens a new connection to the given node and returns it. The returned connection is not managed by - * the transport implementation. This connection must be closed once it's not needed anymore. + * Opens a new connection to the given node. When the connection is fully connected, the listener is + * called. A {@link Releasable} is returned representing the pending connection. If the caller of this + * method decides to move on before the listener is called with the completed connection, they should + * release the pending connection to prevent hanging connections. */ - Connection openConnection(DiscoveryNode node, ConnectionProfile profile); + Releasable openConnection(DiscoveryNode node, ConnectionProfile profile, ActionListener listener); TransportStats getStats(); diff --git a/server/src/test/java/org/elasticsearch/client/transport/FailAndRetryMockTransport.java b/server/src/test/java/org/elasticsearch/client/transport/FailAndRetryMockTransport.java index 2e2b29e447838..1a101b3340295 100644 --- a/server/src/test/java/org/elasticsearch/client/transport/FailAndRetryMockTransport.java +++ b/server/src/test/java/org/elasticsearch/client/transport/FailAndRetryMockTransport.java @@ -20,6 +20,7 @@ package org.elasticsearch.client.transport; import org.elasticsearch.Version; +import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.admin.cluster.node.liveness.LivenessResponse; import org.elasticsearch.action.admin.cluster.node.liveness.TransportLivenessAction; import org.elasticsearch.action.admin.cluster.state.ClusterStateAction; @@ -30,6 +31,7 @@ import org.elasticsearch.common.collect.MapBuilder; import org.elasticsearch.common.component.Lifecycle; import org.elasticsearch.common.component.LifecycleListener; +import org.elasticsearch.common.lease.Releasable; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.transport.BoundTransportAddress; import org.elasticsearch.common.transport.TransportAddress; @@ -79,8 +81,8 @@ abstract class FailAndRetryMockTransport imp protected abstract ClusterState getMockClusterState(DiscoveryNode node); @Override - public Connection openConnection(DiscoveryNode node, ConnectionProfile profile) { - return new CloseableConnection() { + public Releasable openConnection(DiscoveryNode node, ConnectionProfile profile, ActionListener connectionListener) { + connectionListener.onResponse(new CloseableConnection() { @Override public DiscoveryNode getNode() { @@ -134,7 +136,9 @@ public void sendRequest(long requestId, String action, TransportRequest request, } } } - }; + }); + + return () -> {}; } protected abstract Response newResponse(); diff --git a/server/src/test/java/org/elasticsearch/client/transport/TransportClientNodesServiceTests.java b/server/src/test/java/org/elasticsearch/client/transport/TransportClientNodesServiceTests.java index 5813ea0379bdb..c95d4b9e74f21 100644 --- a/server/src/test/java/org/elasticsearch/client/transport/TransportClientNodesServiceTests.java +++ b/server/src/test/java/org/elasticsearch/client/transport/TransportClientNodesServiceTests.java @@ -26,6 +26,7 @@ import org.elasticsearch.action.admin.cluster.state.ClusterStateAction; import org.elasticsearch.action.admin.cluster.state.ClusterStateRequest; import org.elasticsearch.action.admin.cluster.state.ClusterStateResponse; +import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.cluster.ClusterName; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.node.DiscoveryNode; @@ -166,7 +167,9 @@ public void sendRequest(Transport.Connection conne transportService.addNodeConnectedBehavior((connectionManager, discoveryNode) -> false); transportService.addGetConnectionBehavior((connectionManager, discoveryNode) -> { // The FailAndRetryTransport does not use the connection profile - return transport.openConnection(discoveryNode, null); + PlainActionFuture future = PlainActionFuture.newFuture(); + transport.openConnection(discoveryNode, null, future); + return future.actionGet(); }); transportService.start(); transportService.acceptIncomingRequests(); @@ -361,11 +364,19 @@ public void testSniffNodesSamplerClosesConnections() throws Exception { try (MockTransportService clientService = createNewService(clientSettings, Version.CURRENT, threadPool, null)) { final List establishedConnections = new CopyOnWriteArrayList<>(); - clientService.addConnectBehavior(remoteService, (transport, discoveryNode, profile) -> { - Transport.Connection connection = transport.openConnection(discoveryNode, profile); - establishedConnections.add(connection); - return connection; - }); + clientService.addConnectBehavior(remoteService, (transport, discoveryNode, profile, listener) -> + transport.openConnection(discoveryNode, profile, new ActionListener() { + @Override + public void onResponse(Transport.Connection connection) { + establishedConnections.add(connection); + listener.onResponse(connection); + } + + @Override + public void onFailure(Exception e) { + listener.onFailure(e); + } + })); clientService.start(); diff --git a/server/src/test/java/org/elasticsearch/cluster/NodeConnectionsServiceTests.java b/server/src/test/java/org/elasticsearch/cluster/NodeConnectionsServiceTests.java index b8c39c48f8870..d0299a2858c35 100644 --- a/server/src/test/java/org/elasticsearch/cluster/NodeConnectionsServiceTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/NodeConnectionsServiceTests.java @@ -26,6 +26,7 @@ import org.elasticsearch.common.UUIDs; import org.elasticsearch.common.component.Lifecycle; import org.elasticsearch.common.component.LifecycleListener; +import org.elasticsearch.common.lease.Releasable; import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.transport.BoundTransportAddress; @@ -187,8 +188,6 @@ public HandshakeResponse handshake(Transport.Connection connection, long timeout private final class MockTransport implements Transport { private ResponseHandlers responseHandlers = new ResponseHandlers(); private volatile boolean randomConnectionExceptions = false; - private TransportMessageListener listener = new TransportMessageListener() { - }; @Override public void registerRequestHandler(RequestHandlerRegistry reg) { @@ -201,7 +200,6 @@ public RequestHandlerRegistry getRequestHandler(String action) { @Override public void addMessageListener(TransportMessageListener listener) { - this.listener = listener; } @Override @@ -225,13 +223,14 @@ public TransportAddress[] addressesFromString(String address, int perAddressLimi } @Override - public Connection openConnection(DiscoveryNode node, ConnectionProfile connectionProfile) { - if (connectionProfile == null) { + public Releasable openConnection(DiscoveryNode node, ConnectionProfile profile, ActionListener listener) { + if (profile == null) { if (randomConnectionExceptions && randomBoolean()) { - throw new ConnectTransportException(node, "simulated"); + listener.onFailure(new ConnectTransportException(node, "simulated")); + return () -> {}; } } - Connection connection = new Connection() { + listener.onResponse(new Connection() { @Override public DiscoveryNode getNode() { return node; @@ -257,8 +256,8 @@ public void close() { public boolean isClosed() { return false; } - }; - return connection; + }); + return () -> {}; } @Override diff --git a/server/src/test/java/org/elasticsearch/transport/ConnectionManagerTests.java b/server/src/test/java/org/elasticsearch/transport/ConnectionManagerTests.java index 976f0e905c050..578521190e2ff 100644 --- a/server/src/test/java/org/elasticsearch/transport/ConnectionManagerTests.java +++ b/server/src/test/java/org/elasticsearch/transport/ConnectionManagerTests.java @@ -20,6 +20,7 @@ package org.elasticsearch.transport; import org.elasticsearch.Version; +import org.elasticsearch.action.ActionListener; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.common.CheckedBiConsumer; import org.elasticsearch.common.settings.Settings; @@ -35,8 +36,10 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; public class ConnectionManagerTests extends ESTestCase { @@ -82,7 +85,11 @@ public void onNodeDisconnected(DiscoveryNode node) { DiscoveryNode node = new DiscoveryNode("", new TransportAddress(InetAddress.getLoopbackAddress(), 0), Version.CURRENT); Transport.Connection connection = new TestConnect(node); - when(transport.openConnection(node, connectionProfile)).thenReturn(connection); + doAnswer(invocationOnMock -> { + ActionListener listener = (ActionListener) invocationOnMock.getArguments()[2]; + listener.onResponse(connection); + return null; + }).when(transport).openConnection(eq(node), eq(connectionProfile), any(ActionListener.class)); assertFalse(connectionManager.nodeConnected(node)); @@ -126,7 +133,11 @@ public void onNodeDisconnected(DiscoveryNode node) { DiscoveryNode node = new DiscoveryNode("", new TransportAddress(InetAddress.getLoopbackAddress(), 0), Version.CURRENT); Transport.Connection connection = new TestConnect(node); - when(transport.openConnection(node, connectionProfile)).thenReturn(connection); + doAnswer(invocationOnMock -> { + ActionListener listener = (ActionListener) invocationOnMock.getArguments()[2]; + listener.onResponse(connection); + return null; + }).when(transport).openConnection(eq(node), eq(connectionProfile), any(ActionListener.class)); assertFalse(connectionManager.nodeConnected(node)); diff --git a/server/src/test/java/org/elasticsearch/transport/RemoteClusterConnectionTests.java b/server/src/test/java/org/elasticsearch/transport/RemoteClusterConnectionTests.java index a4f81a659c60c..da77e2e06abf2 100644 --- a/server/src/test/java/org/elasticsearch/transport/RemoteClusterConnectionTests.java +++ b/server/src/test/java/org/elasticsearch/transport/RemoteClusterConnectionTests.java @@ -1495,7 +1495,7 @@ public static Transport getProxyTransport(ThreadPool threadPool, Map { + stubbableTransport.setDefaultConnectBehavior((t, node, profile, listener) -> { Map proxyMapping = nodeMap.get(node.getAddress().toString()); if (proxyMapping == null) { throw new IllegalStateException("no proxy mapping for node: " + node); @@ -1509,34 +1509,44 @@ public static Transport getProxyTransport(ThreadPool threadPool, Map() { @Override - public DiscoveryNode getNode() { - return node; - } + public void onResponse(Transport.Connection connection) { + Transport.Connection proxyConnection = new Transport.Connection() { + @Override + public DiscoveryNode getNode() { + return node; + } - @Override - public void sendRequest(long requestId, String action, TransportRequest request, TransportRequestOptions options) - throws IOException, TransportException { - connection.sendRequest(requestId, action, request, options); - } + @Override + public void sendRequest(long requestId, String action, TransportRequest request, + TransportRequestOptions options) throws IOException, TransportException { + connection.sendRequest(requestId, action, request, options); + } - @Override - public void addCloseListener(ActionListener listener) { - connection.addCloseListener(listener); - } + @Override + public void addCloseListener(ActionListener listener) { + connection.addCloseListener(listener); + } - @Override - public boolean isClosed() { - return connection.isClosed(); + @Override + public boolean isClosed() { + return connection.isClosed(); + } + + @Override + public void close() { + connection.close(); + } + }; + listener.onResponse(proxyConnection); } @Override - public void close() { - connection.close(); + public void onFailure(Exception e) { + listener.onFailure(e); } - }; + }); }); return stubbableTransport; } diff --git a/server/src/test/java/org/elasticsearch/transport/TcpTransportTests.java b/server/src/test/java/org/elasticsearch/transport/TcpTransportTests.java index 199cf42546d12..c1a5535a8b151 100644 --- a/server/src/test/java/org/elasticsearch/transport/TcpTransportTests.java +++ b/server/src/test/java/org/elasticsearch/transport/TcpTransportTests.java @@ -21,12 +21,15 @@ import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.compress.CompressorFactory; import org.elasticsearch.common.io.stream.BytesStreamOutput; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.lease.Releasable; +import org.elasticsearch.common.network.CloseableChannel; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.transport.TransportAddress; import org.elasticsearch.common.util.BigArrays; @@ -196,16 +199,17 @@ protected void stopInternal() { } @Override - public NodeChannels openConnection(DiscoveryNode node, ConnectionProfile connectionProfile) { + public Releasable openConnection(DiscoveryNode node, ConnectionProfile profile, ActionListener listener) { if (compressed) { - assertTrue(connectionProfile.getCompressionEnabled()); + assertTrue(profile.getCompressionEnabled()); } - int numConnections = connectionProfile.getNumConnections(); + int numConnections = profile.getNumConnections(); ArrayList fakeChannels = new ArrayList<>(numConnections); for (int i = 0; i < numConnections; ++i) { fakeChannels.add(new FakeTcpChannel(false, messageCaptor)); } - return new NodeChannels(node, fakeChannels, connectionProfile, Version.CURRENT); + listener.onResponse(new NodeChannels(node, fakeChannels, profile, Version.CURRENT)); + return () -> CloseableChannel.closeChannels(fakeChannels, false); } }; @@ -216,7 +220,9 @@ public NodeChannels openConnection(DiscoveryNode node, ConnectionProfile connect } else { profileBuilder.setCompressionEnabled(false); } - Transport.Connection connection = transport.openConnection(node, profileBuilder.build()); + PlainActionFuture future = PlainActionFuture.newFuture(); + transport.openConnection(node, profileBuilder.build(), future); + Transport.Connection connection = future.actionGet(); connection.sendRequest(42, "foobar", request, TransportRequestOptions.EMPTY); BytesReference reference = messageCaptor.get(); diff --git a/test/framework/src/main/java/org/elasticsearch/test/transport/CapturingTransport.java b/test/framework/src/main/java/org/elasticsearch/test/transport/CapturingTransport.java index 1b8405a2d591a..455c83aabe7fd 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/transport/CapturingTransport.java +++ b/test/framework/src/main/java/org/elasticsearch/test/transport/CapturingTransport.java @@ -29,6 +29,7 @@ import org.elasticsearch.common.component.Lifecycle; import org.elasticsearch.common.component.LifecycleListener; import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.common.lease.Releasable; import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.transport.BoundTransportAddress; @@ -99,7 +100,7 @@ public TransportService createCapturingTransportService(Settings settings, Threa StubbableConnectionManager connectionManager = new StubbableConnectionManager(new ConnectionManager(settings, this, threadPool), settings, this, threadPool); connectionManager.setDefaultNodeConnectedBehavior((cm, discoveryNode) -> nodeConnected(discoveryNode)); - connectionManager.setDefaultConnectBehavior((cm, discoveryNode) -> openConnection(discoveryNode, null)); + connectionManager.setDefaultGetConnectionBehavior((cm, discoveryNode) -> createConnection(discoveryNode)); return new TransportService(settings, this, threadPool, interceptor, localNodeFactory, clusterSettings, taskHeaders, connectionManager); } @@ -223,32 +224,9 @@ public void handleError(final long requestId, final TransportException e) { } @Override - public Connection openConnection(DiscoveryNode node, ConnectionProfile profile) { - return new Connection() { - @Override - public DiscoveryNode getNode() { - return node; - } - - @Override - public void sendRequest(long requestId, String action, TransportRequest request, TransportRequestOptions options) - throws TransportException { - onSendRequest(requestId, action, request, node); - } - - @Override - public void addCloseListener(ActionListener listener) { - } - - @Override - public boolean isClosed() { - return false; - } - - @Override - public void close() { - } - }; + public Releasable openConnection(DiscoveryNode node, ConnectionProfile profile, ActionListener listener) { + listener.onResponse(createConnection(node)); + return () -> {}; } protected void onSendRequest(long requestId, String action, TransportRequest request, DiscoveryNode node) { @@ -347,4 +325,31 @@ public boolean removeMessageListener(TransportMessageListener listener) { return false; } + private Connection createConnection(DiscoveryNode node) { + return new Connection() { + @Override + public DiscoveryNode getNode() { + return node; + } + + @Override + public void sendRequest(long requestId, String action, TransportRequest request, TransportRequestOptions options) + throws TransportException { + onSendRequest(requestId, action, request, node); + } + + @Override + public void addCloseListener(ActionListener listener) { + } + + @Override + public boolean isClosed() { + return false; + } + + @Override + public void close() { + } + }; + } } diff --git a/test/framework/src/main/java/org/elasticsearch/test/transport/MockTransportService.java b/test/framework/src/main/java/org/elasticsearch/test/transport/MockTransportService.java index 4a2a5d0cb4290..fc22fd2fd33d1 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/transport/MockTransportService.java +++ b/test/framework/src/main/java/org/elasticsearch/test/transport/MockTransportService.java @@ -216,8 +216,9 @@ public void addFailToSendNoConnectRule(TransportService transportService) { * is added to fail as well. */ public void addFailToSendNoConnectRule(TransportAddress transportAddress) { - transport().addConnectBehavior(transportAddress, (transport, discoveryNode, profile) -> { - throw new ConnectTransportException(discoveryNode, "DISCONNECT: simulated"); + transport().addConnectBehavior(transportAddress, (transport, discoveryNode, profile, listener) -> { + listener.onFailure(new ConnectTransportException(discoveryNode, "DISCONNECT: simulated")); + return () -> {}; }); transport().addSendBehavior(transportAddress, (connection, requestId, action, request, options) -> { @@ -278,8 +279,9 @@ public void addUnresponsiveRule(TransportService transportService) { * and failing to connect once the rule was added. */ public void addUnresponsiveRule(TransportAddress transportAddress) { - transport().addConnectBehavior(transportAddress, (transport, discoveryNode, profile) -> { - throw new ConnectTransportException(discoveryNode, "UNRESPONSIVE: simulated"); + transport().addConnectBehavior(transportAddress, (transport, discoveryNode, profile, listener) -> { + listener.onFailure(new ConnectTransportException(discoveryNode, "UNRESPONSIVE: simulated")); + return () -> {}; }); transport().addSendBehavior(transportAddress, (connection, requestId, action, request, options) -> { @@ -310,10 +312,10 @@ public void addUnresponsiveRule(TransportAddress transportAddress, final TimeVal Supplier delaySupplier = () -> new TimeValue(duration.millis() - (System.currentTimeMillis() - startTime)); - transport().addConnectBehavior(transportAddress, (transport, discoveryNode, profile) -> { + transport().addConnectBehavior(transportAddress, (transport, discoveryNode, profile, listener) -> { TimeValue delay = delaySupplier.get(); if (delay.millis() <= 0) { - return original.openConnection(discoveryNode, profile); + return original.openConnection(discoveryNode, profile, listener); } // TODO: Replace with proper setting @@ -321,13 +323,15 @@ public void addUnresponsiveRule(TransportAddress transportAddress, final TimeVal try { if (delay.millis() < connectingTimeout.millis()) { Thread.sleep(delay.millis()); - return original.openConnection(discoveryNode, profile); + return original.openConnection(discoveryNode, profile, listener); } else { Thread.sleep(connectingTimeout.millis()); - throw new ConnectTransportException(discoveryNode, "UNRESPONSIVE: simulated"); + listener.onFailure(new ConnectTransportException(discoveryNode, "UNRESPONSIVE: simulated")); + return () -> {}; } } catch (InterruptedException e) { - throw new ConnectTransportException(discoveryNode, "UNRESPONSIVE: simulated"); + listener.onFailure(new ConnectTransportException(discoveryNode, "UNRESPONSIVE: simulated")); + return () -> {}; } }); @@ -467,7 +471,7 @@ public boolean addGetConnectionBehavior(TransportAddress transportAddress, Stubb * @return {@code true} if no default get connection behavior was registered. */ public boolean addGetConnectionBehavior(StubbableConnectionManager.GetConnectionBehavior behavior) { - return connectionManager().setDefaultConnectBehavior(behavior); + return connectionManager().setDefaultGetConnectionBehavior(behavior); } /** diff --git a/test/framework/src/main/java/org/elasticsearch/test/transport/StubbableConnectionManager.java b/test/framework/src/main/java/org/elasticsearch/test/transport/StubbableConnectionManager.java index 012369feb839f..994037be07a92 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/transport/StubbableConnectionManager.java +++ b/test/framework/src/main/java/org/elasticsearch/test/transport/StubbableConnectionManager.java @@ -52,7 +52,7 @@ public boolean addConnectBehavior(TransportAddress transportAddress, GetConnecti return getConnectionBehaviors.put(transportAddress, connectBehavior) == null; } - public boolean setDefaultConnectBehavior(GetConnectionBehavior behavior) { + public boolean setDefaultGetConnectionBehavior(GetConnectionBehavior behavior) { GetConnectionBehavior prior = defaultGetConnectionBehavior; defaultGetConnectionBehavior = behavior; return prior == null; diff --git a/test/framework/src/main/java/org/elasticsearch/test/transport/StubbableTransport.java b/test/framework/src/main/java/org/elasticsearch/test/transport/StubbableTransport.java index ce0e38a83f88d..0014e1225c595 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/transport/StubbableTransport.java +++ b/test/framework/src/main/java/org/elasticsearch/test/transport/StubbableTransport.java @@ -24,6 +24,7 @@ import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.common.component.Lifecycle; import org.elasticsearch.common.component.LifecycleListener; +import org.elasticsearch.common.lease.Releasable; import org.elasticsearch.common.transport.BoundTransportAddress; import org.elasticsearch.common.transport.TransportAddress; import org.elasticsearch.transport.ConnectionProfile; @@ -127,17 +128,28 @@ public List getLocalAddresses() { } @Override - public Connection openConnection(DiscoveryNode node, ConnectionProfile profile) { + public Releasable openConnection(DiscoveryNode node, ConnectionProfile profile, ActionListener listener) { TransportAddress address = node.getAddress(); OpenConnectionBehavior behavior = connectBehaviors.getOrDefault(address, defaultConnectBehavior); - Connection connection; + + ActionListener wrappedListener = new ActionListener() { + + @Override + public void onResponse(Connection connection) { + listener.onResponse(new WrappedConnection(connection)); + } + + @Override + public void onFailure(Exception e) { + listener.onFailure(e); + } + }; + if (behavior == null) { - connection = delegate.openConnection(node, profile); + return delegate.openConnection(node, profile, wrappedListener); } else { - connection = behavior.openConnection(delegate, node, profile); + return behavior.openConnection(delegate, node, profile, wrappedListener); } - - return new WrappedConnection(connection); } @Override @@ -243,7 +255,9 @@ public Transport.Connection getConnection() { @FunctionalInterface public interface OpenConnectionBehavior { - Connection openConnection(Transport transport, DiscoveryNode discoveryNode, ConnectionProfile profile); + + Releasable openConnection(Transport transport, DiscoveryNode discoveryNode, ConnectionProfile profile, + ActionListener listener); } @FunctionalInterface diff --git a/test/framework/src/main/java/org/elasticsearch/transport/AbstractSimpleTransportTestCase.java b/test/framework/src/main/java/org/elasticsearch/transport/AbstractSimpleTransportTestCase.java index aa8da669cdbbc..8879fe38a85f6 100644 --- a/test/framework/src/main/java/org/elasticsearch/transport/AbstractSimpleTransportTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/transport/AbstractSimpleTransportTestCase.java @@ -2021,14 +2021,17 @@ public void testKeepAlivePings() throws Exception { ConnectionProfile connectionProfile = new ConnectionProfile.Builder(defaultProfile) .setPingInterval(TimeValue.timeValueMillis(50)) .build(); - try (TransportService service = buildService("TS_TPC", Version.CURRENT, null); - TcpTransport.NodeChannels connection = originalTransport.openConnection( - new DiscoveryNode("TS_TPC", "TS_TPC", service.boundAddress().publishAddress(), emptyMap(), emptySet(), version0), - connectionProfile)) { - assertBusy(() -> { - assertTrue(originalTransport.getKeepAlive().successfulPingCount() > 30); - }); - assertEquals(0, originalTransport.getKeepAlive().failedPingCount()); + try (TransportService service = buildService("TS_TPC", Version.CURRENT, null)) { + PlainActionFuture future = PlainActionFuture.newFuture(); + DiscoveryNode node = new DiscoveryNode("TS_TPC", "TS_TPC", service.boundAddress().publishAddress(), emptyMap(), emptySet(), + version0); + originalTransport.openConnection(node, connectionProfile, future); + try (Transport.Connection connection = future.actionGet()) { + assertBusy(() -> { + assertTrue(originalTransport.getKeepAlive().successfulPingCount() > 30); + }); + assertEquals(0, originalTransport.getKeepAlive().failedPingCount()); + } } } @@ -2061,11 +2064,14 @@ protected String handleRequest(TcpChannel mockChannel, String profileName, Strea } ConnectionProfile connectionProfile = ConnectionProfile.buildDefaultConnectionProfile(Settings.EMPTY); - try (TransportService service = buildService("TS_TPC", Version.CURRENT, null); - TcpTransport.NodeChannels connection = originalTransport.openConnection( - new DiscoveryNode("TS_TPC", "TS_TPC", service.boundAddress().publishAddress(), emptyMap(), emptySet(), version0), - connectionProfile)) { - assertEquals(connection.getVersion(), Version.CURRENT); + try (TransportService service = buildService("TS_TPC", Version.CURRENT, null)) { + DiscoveryNode node = new DiscoveryNode("TS_TPC", "TS_TPC", service.boundAddress().publishAddress(), emptyMap(), emptySet(), + version0); + PlainActionFuture future = PlainActionFuture.newFuture(); + originalTransport.openConnection(node, connectionProfile, future); + try (Transport.Connection connection = future.actionGet()) { + assertEquals(connection.getVersion(), Version.CURRENT); + } } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/AbstractSimpleSecurityTransportTestCase.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/AbstractSimpleSecurityTransportTestCase.java index 1652b2ee1af34..3e10a884a77eb 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/AbstractSimpleSecurityTransportTestCase.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/AbstractSimpleSecurityTransportTestCase.java @@ -6,6 +6,7 @@ package org.elasticsearch.xpack.security.transport; import org.elasticsearch.Version; +import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.common.SuppressForbidden; import org.elasticsearch.common.io.stream.OutputStreamStreamOutput; @@ -21,6 +22,7 @@ import org.elasticsearch.transport.ConnectTransportException; import org.elasticsearch.transport.ConnectionProfile; import org.elasticsearch.transport.TcpTransport; +import org.elasticsearch.transport.Transport; import org.elasticsearch.transport.TransportService; import org.elasticsearch.xpack.core.common.socket.SocketAccess; import org.elasticsearch.xpack.core.ssl.SSLConfiguration; @@ -109,11 +111,14 @@ public void testTcpHandshake() { TcpTransport originalTransport = (TcpTransport) serviceA.getOriginalTransport(); ConnectionProfile connectionProfile = ConnectionProfile.buildDefaultConnectionProfile(Settings.EMPTY); - try (TransportService service = buildService("TS_TPC", Version.CURRENT, null); - TcpTransport.NodeChannels connection = originalTransport.openConnection( - new DiscoveryNode("TS_TPC", "TS_TPC", service.boundAddress().publishAddress(), emptyMap(), emptySet(), version0), - connectionProfile)) { - assertEquals(connection.getVersion(), Version.CURRENT); + try (TransportService service = buildService("TS_TPC", Version.CURRENT, null)) { + DiscoveryNode node = new DiscoveryNode("TS_TPC", "TS_TPC", service.boundAddress().publishAddress(), emptyMap(), emptySet(), + version0); + PlainActionFuture future = PlainActionFuture.newFuture(); + originalTransport.openConnection(node, connectionProfile, future); + try (TcpTransport.NodeChannels connection = (TcpTransport.NodeChannels) future.actionGet()) { + assertEquals(connection.getVersion(), Version.CURRENT); + } } } From b3be6d66e3bc4e5fd7e3c696a0b6dbcc18ae619e Mon Sep 17 00:00:00 2001 From: Jay Modi Date: Fri, 30 Nov 2018 14:38:10 -0700 Subject: [PATCH 062/115] Fix logic in dockerComposeSupported (#36125) The logic in the dockerComposeSupported method currently returns false even when docker and docker compose are available on the build machine. This change updates the check to see if docker compose is available in one of the two paths and allows the `tests.fixture.enabled` property to disable the tests even if docker compose is available. --- .../gradle/testfixtures/TestFixturesPlugin.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/buildSrc/src/main/java/org/elasticsearch/gradle/testfixtures/TestFixturesPlugin.java b/buildSrc/src/main/java/org/elasticsearch/gradle/testfixtures/TestFixturesPlugin.java index ed185e22a6cbb..26f99a9d62e05 100644 --- a/buildSrc/src/main/java/org/elasticsearch/gradle/testfixtures/TestFixturesPlugin.java +++ b/buildSrc/src/main/java/org/elasticsearch/gradle/testfixtures/TestFixturesPlugin.java @@ -103,10 +103,9 @@ public void apply(Project project) { @Input public boolean dockerComposeSupported(Project project) { // Don't look for docker-compose on the PATH yet that would pick up on Windows as well - return - project.file("/usr/local/bin/docker-compose").exists() == false && - project.file("/usr/bin/docker-compose").exists() == false && - Boolean.parseBoolean(System.getProperty("tests.fixture.enabled", "true")) == false; + final boolean hasDockerCompose = project.file("/usr/local/bin/docker-compose").exists() || + project.file("/usr/bin/docker-compose").exists(); + return hasDockerCompose && Boolean.parseBoolean(System.getProperty("tests.fixture.enabled", "true")); } private void setSystemProperty(Task task, String name, Object value) { From d1684408c77c4dc167d4a15dbc75665a087bc82b Mon Sep 17 00:00:00 2001 From: Julie Tibshirani Date: Fri, 30 Nov 2018 13:11:58 -0800 Subject: [PATCH 063/115] Deprecate the _termvector endpoint. (#36098) This endpoint was replaced by _termvectors (plural) in #8484. We deprecated the relevant transport client methods, but didn't introduce a deprecation warning. My plan was to add a deprecation warning in 6.6, and stop supporting this endpoint in 7.0. --- .../document/RestTermVectorsAction.java | 23 ++++--- .../document/RestTermVectorsActionTests.java | 67 +++++++++++++++++++ 2 files changed, 80 insertions(+), 10 deletions(-) create mode 100644 server/src/test/java/org/elasticsearch/rest/action/document/RestTermVectorsActionTests.java diff --git a/server/src/main/java/org/elasticsearch/rest/action/document/RestTermVectorsAction.java b/server/src/main/java/org/elasticsearch/rest/action/document/RestTermVectorsAction.java index a12b7ce16a724..a27c0f345ca99 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/document/RestTermVectorsAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/document/RestTermVectorsAction.java @@ -19,9 +19,11 @@ package org.elasticsearch.rest.action.document; +import org.apache.logging.log4j.LogManager; import org.elasticsearch.action.termvectors.TermVectorsRequest; import org.elasticsearch.client.node.NodeClient; import org.elasticsearch.common.Strings; +import org.elasticsearch.common.logging.DeprecationLogger; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.index.VersionType; @@ -43,18 +45,19 @@ * TermVectorsRequest. */ public class RestTermVectorsAction extends BaseRestHandler { + private static final DeprecationLogger deprecationLogger = new DeprecationLogger( + LogManager.getLogger(RestTermVectorsAction.class)); + public RestTermVectorsAction(Settings settings, RestController controller) { super(settings); - controller.registerHandler(GET, "/{index}/{type}/_termvectors", this); - controller.registerHandler(POST, "/{index}/{type}/_termvectors", this); - controller.registerHandler(GET, "/{index}/{type}/{id}/_termvectors", this); - controller.registerHandler(POST, "/{index}/{type}/{id}/_termvectors", this); - - // we keep usage of _termvector as alias for now - controller.registerHandler(GET, "/{index}/{type}/_termvector", this); - controller.registerHandler(POST, "/{index}/{type}/_termvector", this); - controller.registerHandler(GET, "/{index}/{type}/{id}/_termvector", this); - controller.registerHandler(POST, "/{index}/{type}/{id}/_termvector", this); + controller.registerWithDeprecatedHandler(GET, "/{index}/{type}/_termvectors", this, + GET, "/{index}/{type}/_termvector", deprecationLogger); + controller.registerWithDeprecatedHandler(POST, "/{index}/{type}/_termvectors", this, + POST, "/{index}/{type}/_termvector", deprecationLogger); + controller.registerWithDeprecatedHandler(GET, "/{index}/{type}/{id}/_termvectors", this, + GET, "/{index}/{type}/{id}/_termvector", deprecationLogger); + controller.registerWithDeprecatedHandler(POST, "/{index}/{type}/{id}/_termvectors", this, + POST, "/{index}/{type}/{id}/_termvector", deprecationLogger); } @Override diff --git a/server/src/test/java/org/elasticsearch/rest/action/document/RestTermVectorsActionTests.java b/server/src/test/java/org/elasticsearch/rest/action/document/RestTermVectorsActionTests.java new file mode 100644 index 0000000000000..88c867b0e56d1 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/rest/action/document/RestTermVectorsActionTests.java @@ -0,0 +1,67 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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.elasticsearch.rest.action.document; + +import org.elasticsearch.client.node.NodeClient; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.indices.breaker.NoneCircuitBreakerService; +import org.elasticsearch.rest.RestChannel; +import org.elasticsearch.rest.RestController; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.RestRequest.Method; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.rest.FakeRestChannel; +import org.elasticsearch.test.rest.FakeRestRequest; +import org.elasticsearch.usage.UsageService; + +import java.util.Collections; + +import static org.mockito.Mockito.mock; + +public class RestTermVectorsActionTests extends ESTestCase { + private RestController controller; + + public void setUp() throws Exception { + super.setUp(); + controller = new RestController(Collections.emptySet(), null, + mock(NodeClient.class), + new NoneCircuitBreakerService(), + new UsageService()); + new RestTermVectorsAction(Settings.EMPTY, controller); + } + + public void testDeprecatedEndpoint() { + RestRequest request = new FakeRestRequest.Builder(xContentRegistry()) + .withMethod(Method.POST) + .withPath("/some_index/some_type/some_id/_termvector") + .build(); + + performRequest(request); + assertWarnings("[POST /{index}/{type}/{id}/_termvector] is deprecated! Use" + + " [POST /{index}/{type}/{id}/_termvectors] instead."); + } + + private void performRequest(RestRequest request) { + RestChannel channel = new FakeRestChannel(request, false, 1); + ThreadContext threadContext = new ThreadContext(Settings.EMPTY); + controller.dispatchRequest(request, channel, threadContext); + } +} From c6f4ddfc9d7397a83f1e788afc3d4c6dd817fa6c Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Fri, 30 Nov 2018 16:58:19 -0800 Subject: [PATCH 064/115] [DOCS] Replace deprecated ldap setting (#36022) --- .../configuring-ldap-realm.asciidoc | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/x-pack/docs/en/security/authentication/configuring-ldap-realm.asciidoc b/x-pack/docs/en/security/authentication/configuring-ldap-realm.asciidoc index a5f8c3e441205..a70c8675bbbc4 100644 --- a/x-pack/docs/en/security/authentication/configuring-ldap-realm.asciidoc +++ b/x-pack/docs/en/security/authentication/configuring-ldap-realm.asciidoc @@ -2,12 +2,12 @@ [[configuring-ldap-realm]] === Configuring an LDAP realm -You can configure {security} to communicate with a Lightweight Directory Access -Protocol (LDAP) server to authenticate users. To integrate with LDAP, you -configure an `ldap` realm and map LDAP groups to user roles. +You can configure {es} to authenticate users by communicating with a Lightweight +Directory Access Protocol (LDAP) server. To integrate with LDAP, you configure +an `ldap` realm and map LDAP groups to user roles. For more information about LDAP realms, see -{xpack-ref}/ldap-realm.html[LDAP User Authentication]. +{stack-ov}/ldap-realm.html[LDAP User Authentication]. . Determine which mode you want to use. The `ldap` realm supports two modes of operation, a user search mode and a mode with specific templates for user DNs. @@ -52,7 +52,7 @@ xpack: bind_dn: "cn=ldapuser, ou=users, o=services, dc=example, dc=com" user_search: base_dn: "dc=example,dc=com" - attribute: cn + filter: "(cn={0})" group_search: base_dn: "dc=example,dc=com" files: @@ -115,12 +115,13 @@ All LDAP operations run as the authenticating user. -- -. (Optional) Configure how {security} should interact with multiple LDAP servers. +. (Optional) Configure how the {security-features} interact with multiple LDAP +servers. + -- -The `load_balance.type` setting can be used at the realm level. {security} -supports both failover and load balancing modes of operation. See -<>. +The `load_balance.type` setting can be used at the realm level. The {es} +{security-features} support both failover and load balancing modes of operation. +See <>. -- . (Optional) To protect passwords, @@ -186,9 +187,9 @@ user: <3> The LDAP distinguished name (DN) of the `users` group. For more information, see -{xpack-ref}/ldap-realm.html#mapping-roles-ldap[Mapping LDAP Groups to Roles] +{stack-ov}/ldap-realm.html#mapping-roles-ldap[Mapping LDAP Groups to Roles] and -{xpack-ref}/mapping-roles.html[Mapping Users and Groups to Roles]. +{stack-ov}/mapping-roles.html[Mapping Users and Groups to Roles]. NOTE: The LDAP realm supports {stack-ov}/realm-chains.html#authorization_realms[authorization realms] as an @@ -202,7 +203,7 @@ fields in the user's metadata. -- By default, `ldap_dn` and `ldap_groups` are populated in the user's metadata. For more information, see -{xpack-ref}/ldap-realm.html#ldap-user-metadata[User Metadata in LDAP Realms]. +{stack-ov}/ldap-realm.html#ldap-user-metadata[User Metadata in LDAP Realms]. The example below includes the user's common name (`cn`) as an additional field in their metadata. From bc332f7571c042b16de5900453a2b218f8687144 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Mon, 3 Dec 2018 06:16:12 +0100 Subject: [PATCH 065/115] TESTS: Fix IndexStatsIT#testFilterCacheStats (#36143) * Test randomly failed because of background merges * Fixed by force merging down to a single segment * Closes #32506 --- .../java/org/elasticsearch/indices/stats/IndexStatsIT.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/server/src/test/java/org/elasticsearch/indices/stats/IndexStatsIT.java b/server/src/test/java/org/elasticsearch/indices/stats/IndexStatsIT.java index f701164b68e72..e418bb5d15289 100644 --- a/server/src/test/java/org/elasticsearch/indices/stats/IndexStatsIT.java +++ b/server/src/test/java/org/elasticsearch/indices/stats/IndexStatsIT.java @@ -23,6 +23,7 @@ import org.elasticsearch.action.DocWriteResponse; import org.elasticsearch.action.admin.cluster.node.stats.NodesStatsResponse; import org.elasticsearch.action.admin.indices.create.CreateIndexRequest; +import org.elasticsearch.action.admin.indices.forcemerge.ForceMergeResponse; import org.elasticsearch.action.admin.indices.stats.CommonStats; import org.elasticsearch.action.admin.indices.stats.CommonStatsFlags; import org.elasticsearch.action.admin.indices.stats.CommonStatsFlags.Flag; @@ -894,7 +895,6 @@ private void assertCumulativeQueryCacheStats(IndicesStatsResponse response) { assertEquals(total, shardTotal); } - @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/32506") public void testFilterCacheStats() throws Exception { Settings settings = Settings.builder().put(indexSettings()).put("number_of_replicas", 0).build(); assertAcked(prepareCreate("index").setSettings(settings).get()); @@ -944,7 +944,11 @@ public void testFilterCacheStats() throws Exception { persistGlobalCheckpoint("index"); flush("index"); } + ForceMergeResponse forceMergeResponse = + client().admin().indices().prepareForceMerge("index").setFlush(true).setMaxNumSegments(1).get(); + assertAllSuccessful(forceMergeResponse); refresh(); + response = client().admin().indices().prepareStats("index").setQueryCache(true).get(); assertCumulativeQueryCacheStats(response); assertThat(response.getTotal().queryCache.getHitCount(), greaterThan(0L)); From 5a03a6325aa1edd4b36f156b7afd1930c1a2bee0 Mon Sep 17 00:00:00 2001 From: Alpar Torok Date: Mon, 3 Dec 2018 10:07:05 +0200 Subject: [PATCH 066/115] Remove transport-nio from 6.x Created via bad back-port --- .../netty-buffer-4.1.32.Final.jar.sha1 | 1 - .../netty-codec-4.1.32.Final.jar.sha1 | 1 - .../netty-codec-http-4.1.32.Final.jar.sha1 | 1 - .../netty-common-4.1.32.Final.jar.sha1 | 1 - .../netty-handler-4.1.32.Final.jar.sha1 | 1 - .../netty-resolver-4.1.32.Final.jar.sha1 | 1 - .../netty-transport-4.1.32.Final.jar.sha1 | 1 - .../transport/nio/NioTransportLoggingIT.java | 79 ------------------- 8 files changed, 86 deletions(-) delete mode 100644 plugins/transport-nio/licenses/netty-buffer-4.1.32.Final.jar.sha1 delete mode 100644 plugins/transport-nio/licenses/netty-codec-4.1.32.Final.jar.sha1 delete mode 100644 plugins/transport-nio/licenses/netty-codec-http-4.1.32.Final.jar.sha1 delete mode 100644 plugins/transport-nio/licenses/netty-common-4.1.32.Final.jar.sha1 delete mode 100644 plugins/transport-nio/licenses/netty-handler-4.1.32.Final.jar.sha1 delete mode 100644 plugins/transport-nio/licenses/netty-resolver-4.1.32.Final.jar.sha1 delete mode 100644 plugins/transport-nio/licenses/netty-transport-4.1.32.Final.jar.sha1 delete mode 100644 plugins/transport-nio/src/test/java/org/elasticsearch/transport/nio/NioTransportLoggingIT.java diff --git a/plugins/transport-nio/licenses/netty-buffer-4.1.32.Final.jar.sha1 b/plugins/transport-nio/licenses/netty-buffer-4.1.32.Final.jar.sha1 deleted file mode 100644 index 111093792d347..0000000000000 --- a/plugins/transport-nio/licenses/netty-buffer-4.1.32.Final.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -046ede57693788181b2cafddc3a5967ed2f621c8 \ No newline at end of file diff --git a/plugins/transport-nio/licenses/netty-codec-4.1.32.Final.jar.sha1 b/plugins/transport-nio/licenses/netty-codec-4.1.32.Final.jar.sha1 deleted file mode 100644 index 5830dd05a5027..0000000000000 --- a/plugins/transport-nio/licenses/netty-codec-4.1.32.Final.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -8f32bd79c5a16f014a4372ed979dc62b39ede33a \ No newline at end of file diff --git a/plugins/transport-nio/licenses/netty-codec-http-4.1.32.Final.jar.sha1 b/plugins/transport-nio/licenses/netty-codec-http-4.1.32.Final.jar.sha1 deleted file mode 100644 index 6ff945b6c2de4..0000000000000 --- a/plugins/transport-nio/licenses/netty-codec-http-4.1.32.Final.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -0b9218adba7353ad5a75fcb639e4755d64bd6ddf \ No newline at end of file diff --git a/plugins/transport-nio/licenses/netty-common-4.1.32.Final.jar.sha1 b/plugins/transport-nio/licenses/netty-common-4.1.32.Final.jar.sha1 deleted file mode 100644 index 02dd7ce15b843..0000000000000 --- a/plugins/transport-nio/licenses/netty-common-4.1.32.Final.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -e95de4f762606f492328e180c8ad5438565a5e3b \ No newline at end of file diff --git a/plugins/transport-nio/licenses/netty-handler-4.1.32.Final.jar.sha1 b/plugins/transport-nio/licenses/netty-handler-4.1.32.Final.jar.sha1 deleted file mode 100644 index 06af1850f8cca..0000000000000 --- a/plugins/transport-nio/licenses/netty-handler-4.1.32.Final.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -b4e3fa13f219df14a9455cc2111f133374428be0 \ No newline at end of file diff --git a/plugins/transport-nio/licenses/netty-resolver-4.1.32.Final.jar.sha1 b/plugins/transport-nio/licenses/netty-resolver-4.1.32.Final.jar.sha1 deleted file mode 100644 index 58d0dfb949c4c..0000000000000 --- a/plugins/transport-nio/licenses/netty-resolver-4.1.32.Final.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -3e0114715cb125a12db8d982b2208e552a91256d \ No newline at end of file diff --git a/plugins/transport-nio/licenses/netty-transport-4.1.32.Final.jar.sha1 b/plugins/transport-nio/licenses/netty-transport-4.1.32.Final.jar.sha1 deleted file mode 100644 index b248610a88623..0000000000000 --- a/plugins/transport-nio/licenses/netty-transport-4.1.32.Final.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -d5e5a8ff9c2bc7d91ddccc536a5aca1a4355bd8b \ No newline at end of file diff --git a/plugins/transport-nio/src/test/java/org/elasticsearch/transport/nio/NioTransportLoggingIT.java b/plugins/transport-nio/src/test/java/org/elasticsearch/transport/nio/NioTransportLoggingIT.java deleted file mode 100644 index b29df77cae1bb..0000000000000 --- a/plugins/transport-nio/src/test/java/org/elasticsearch/transport/nio/NioTransportLoggingIT.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Licensed to Elasticsearch under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch licenses this file to you 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.elasticsearch.transport.nio; - -import org.apache.logging.log4j.Level; -import org.elasticsearch.NioIntegTestCase; -import org.elasticsearch.action.admin.cluster.node.hotthreads.NodesHotThreadsRequest; -import org.elasticsearch.common.logging.Loggers; -import org.elasticsearch.test.ESIntegTestCase; -import org.elasticsearch.test.MockLogAppender; -import org.elasticsearch.test.junit.annotations.TestLogging; -import org.elasticsearch.transport.TransportLogger; - -@ESIntegTestCase.ClusterScope(numDataNodes = 2) -@TestLogging(value = "org.elasticsearch.transport.TransportLogger:trace") -public class NioTransportLoggingIT extends NioIntegTestCase { - - private MockLogAppender appender; - - public void setUp() throws Exception { - super.setUp(); - appender = new MockLogAppender(); - Loggers.addAppender(Loggers.getLogger(TransportLogger.class), appender); - appender.start(); - } - - public void tearDown() throws Exception { - Loggers.removeAppender(Loggers.getLogger(TransportLogger.class), appender); - appender.stop(); - super.tearDown(); - } - - public void testLoggingHandler() throws IllegalAccessException { - final String writePattern = - ".*\\[length: \\d+" + - ", request id: \\d+" + - ", type: request" + - ", version: .*" + - ", action: cluster:monitor/nodes/hot_threads\\[n\\]\\]" + - " WRITE: \\d+B"; - final MockLogAppender.LoggingExpectation writeExpectation = - new MockLogAppender.PatternSeenEventExcpectation( - "hot threads request", TransportLogger.class.getCanonicalName(), Level.TRACE, writePattern); - - final String readPattern = - ".*\\[length: \\d+" + - ", request id: \\d+" + - ", type: request" + - ", version: .*" + - ", action: cluster:monitor/nodes/hot_threads\\[n\\]\\]" + - " READ: \\d+B"; - - final MockLogAppender.LoggingExpectation readExpectation = - new MockLogAppender.PatternSeenEventExcpectation( - "hot threads request", TransportLogger.class.getCanonicalName(), Level.TRACE, readPattern); - - appender.addExpectation(writeExpectation); - appender.addExpectation(readExpectation); - client().admin().cluster().nodesHotThreads(new NodesHotThreadsRequest()).actionGet(); - appender.assertAllExpectationsMatched(); - } -} From 790238b2ab482d20e1de01bad3af672587ceefb9 Mon Sep 17 00:00:00 2001 From: Dimitrios Liappis Date: Mon, 3 Dec 2018 10:43:36 +0200 Subject: [PATCH 067/115] Fix error message when package install fails due to missing Java (#36077) Currently is `java` is not in $PATH the preinst script fails prematurely and prevents an appropriate message from getting displayed to the user. Make package installation more user friendly when java is not in $PATH and add a test for it. Also use a she-bang in the preinst script, as, at least in Debian, maintainer scripts must start with the #! convention [1]. Relates #31845 [1] https://www.debian.org/doc/debian-policy/ch-maintainerscripts.html --- distribution/packages/src/common/scripts/preinst | 16 +++++++++++----- distribution/src/bin/elasticsearch-env | 2 +- .../packaging/test/ArchiveTestCase.java | 2 +- .../packaging/test/PackageTestCase.java | 1 + 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/distribution/packages/src/common/scripts/preinst b/distribution/packages/src/common/scripts/preinst index 22f2405af3c2b..0718e31b05ebe 100644 --- a/distribution/packages/src/common/scripts/preinst +++ b/distribution/packages/src/common/scripts/preinst @@ -1,3 +1,4 @@ +#!/bin/bash # # This script is executed in the pre-installation phase # @@ -9,16 +10,22 @@ # $1=1 : indicates an new install # $1=2 : indicates an upgrade +err_exit() { + echo "$@" >&2 + exit 1 +} + # Check for these at preinst time due to failures in postinst if they do not exist if [ -x "$JAVA_HOME/bin/java" ]; then JAVA="$JAVA_HOME/bin/java" +elif command -v java; then + JAVA=`command -v java` else - JAVA=`which java` + JAVA="" fi if [ -z "$JAVA" ]; then - echo "could not find java; set JAVA_HOME or ensure java is in PATH" - exit 1 + err_exit "could not find java; set JAVA_HOME or ensure java is in PATH" fi case "$1" in @@ -75,8 +82,7 @@ case "$1" in ;; *) - echo "pre install script called with unknown argument \`$1'" >&2 - exit 1 + err_exit "pre install script called with unknown argument \`$1'" ;; esac diff --git a/distribution/src/bin/elasticsearch-env b/distribution/src/bin/elasticsearch-env index fc8b4a809fec4..d1dec54f93d77 100644 --- a/distribution/src/bin/elasticsearch-env +++ b/distribution/src/bin/elasticsearch-env @@ -45,7 +45,7 @@ else fi if [ ! -x "$JAVA" ]; then - echo "could not find java; set JAVA_HOME or ensure java is in PATH" + echo "could not find java; set JAVA_HOME or ensure java is in PATH" >&2 exit 1 fi diff --git a/qa/vagrant/src/main/java/org/elasticsearch/packaging/test/ArchiveTestCase.java b/qa/vagrant/src/main/java/org/elasticsearch/packaging/test/ArchiveTestCase.java index 0108f88ecd166..c225bff80744c 100644 --- a/qa/vagrant/src/main/java/org/elasticsearch/packaging/test/ArchiveTestCase.java +++ b/qa/vagrant/src/main/java/org/elasticsearch/packaging/test/ArchiveTestCase.java @@ -135,7 +135,7 @@ public void test30AbortWhenJavaMissing() { sh.run("chmod -x '" + javaPath + "'"); final Result runResult = sh.runIgnoreExitCode(bin.elasticsearch.toString()); assertThat(runResult.exitCode, is(1)); - assertThat(runResult.stdout, containsString("could not find java; set JAVA_HOME or ensure java is in PATH")); + assertThat(runResult.stderr, containsString("could not find java; set JAVA_HOME or ensure java is in PATH")); } finally { sh.run("chmod +x '" + javaPath + "'"); } diff --git a/qa/vagrant/src/main/java/org/elasticsearch/packaging/test/PackageTestCase.java b/qa/vagrant/src/main/java/org/elasticsearch/packaging/test/PackageTestCase.java index 8a21381cbdfe5..2d9cf22797581 100644 --- a/qa/vagrant/src/main/java/org/elasticsearch/packaging/test/PackageTestCase.java +++ b/qa/vagrant/src/main/java/org/elasticsearch/packaging/test/PackageTestCase.java @@ -91,6 +91,7 @@ public void test05InstallFailsWhenJavaMissing() { mv(originalJavaPath, relocatedJavaPath); final Result installResult = runInstallCommand(distribution()); assertThat(installResult.exitCode, is(1)); + assertThat(installResult.stderr, containsString("could not find java; set JAVA_HOME or ensure java is in PATH")); } finally { mv(relocatedJavaPath, originalJavaPath); } From 30221d20ffeec90d904015e239852aceb663ebab Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Mon, 3 Dec 2018 08:04:29 +0100 Subject: [PATCH 068/115] Replace Streamable w/ Writeable in BaseTasksRequest and subclasses (#35854) * Replace Streamable w/ Writeable in BaseTasksRequest and subclasses This commit replaces usages of Streamable with Writeable for the BaseTasksRequest / TransportTasksAction classes and subclasses of these classes. Relates to #34389 --- .../index/reindex/RethrottleRequest.java | 25 +++++---- .../index/reindex/RoundTripTests.java | 4 +- .../node/tasks/cancel/CancelTasksRequest.java | 9 +-- .../cancel/TransportCancelTasksAction.java | 32 ++++------- .../node/tasks/list/ListTasksRequest.java | 29 +++++----- .../support/tasks/BaseTasksRequest.java | 41 +++++++------- .../support/tasks/TransportTasksAction.java | 31 +++++----- .../cluster/node/tasks/TestTaskPlugin.java | 7 +++ .../node/tasks/TransportTasksActionTests.java | 5 ++ .../persistent/TestPersistentTasksPlugin.java | 5 +- .../action/TransportFollowStatsAction.java | 1 - .../xpack/ccr/action/StatsRequestTests.java | 9 +-- .../core/ccr/action/FollowStatsAction.java | 25 +++++---- .../xpack/core/ml/action/CloseJobAction.java | 51 +++++++++-------- .../xpack/core/ml/action/FlushJobAction.java | 47 ++++++++-------- .../core/ml/action/ForecastJobAction.java | 27 +++++---- .../core/ml/action/GetJobsStatsAction.java | 39 +++++++------ .../core/ml/action/IsolateDatafeedAction.java | 23 ++++---- .../xpack/core/ml/action/JobTaskRequest.java | 23 ++++---- .../core/ml/action/KillProcessAction.java | 4 ++ .../core/ml/action/PersistJobAction.java | 27 +++++---- .../xpack/core/ml/action/PostDataAction.java | 51 +++++++++-------- .../core/ml/action/StopDatafeedAction.java | 23 ++++---- .../core/ml/action/UpdateProcessAction.java | 56 +++++++++---------- .../rollup/action/DeleteRollupJobAction.java | 11 ++-- .../rollup/action/GetRollupJobsAction.java | 29 +++++----- .../rollup/action/StartRollupJobAction.java | 13 ++--- .../rollup/action/StopRollupJobAction.java | 29 +++++----- .../ml/action/CloseJobActionRequestTests.java | 9 +-- .../action/ForecastJobActionRequestTests.java | 9 +-- .../action/GetJobStatsActionRequestTests.java | 9 +-- .../action/PersistJobActionRequestTests.java | 10 ++-- .../ml/action/PostDataActionRequestTests.java | 9 +-- .../ml/action/PostDataFlushRequestTests.java | 13 +++-- .../StopDatafeedActionRequestTests.java | 9 +-- .../UpdateProcessActionRequestTests.java | 12 ++-- .../ml/action/TransportJobTaskAction.java | 2 +- .../action/DeleteJobActionRequestTests.java | 11 ++-- .../action/GetJobsActionRequestTests.java | 11 ++-- .../action/StartJobActionRequestTests.java | 9 +-- .../action/StopJobActionRequestTests.java | 10 ++-- 41 files changed, 400 insertions(+), 399 deletions(-) diff --git a/modules/reindex/src/main/java/org/elasticsearch/index/reindex/RethrottleRequest.java b/modules/reindex/src/main/java/org/elasticsearch/index/reindex/RethrottleRequest.java index 968d998974b47..dd8fac574bb7e 100644 --- a/modules/reindex/src/main/java/org/elasticsearch/index/reindex/RethrottleRequest.java +++ b/modules/reindex/src/main/java/org/elasticsearch/index/reindex/RethrottleRequest.java @@ -39,6 +39,20 @@ public class RethrottleRequest extends BaseTasksRequest { */ private Float requestsPerSecond; + public RethrottleRequest() { + } + + public RethrottleRequest(StreamInput in) throws IOException { + super(in); + this.requestsPerSecond = in.readFloat(); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeFloat(requestsPerSecond); + } + /** * The throttle to apply to all matching requests in sub-requests per second. 0 means set no throttle and that is the default. */ @@ -80,15 +94,4 @@ public ActionRequestValidationException validate() { return validationException; } - @Override - public void readFrom(StreamInput in) throws IOException { - super.readFrom(in); - requestsPerSecond = in.readFloat(); - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - super.writeTo(out); - out.writeFloat(requestsPerSecond); - } } diff --git a/modules/reindex/src/test/java/org/elasticsearch/index/reindex/RoundTripTests.java b/modules/reindex/src/test/java/org/elasticsearch/index/reindex/RoundTripTests.java index 32bd090f8af0a..5330c8f20c54a 100644 --- a/modules/reindex/src/test/java/org/elasticsearch/index/reindex/RoundTripTests.java +++ b/modules/reindex/src/test/java/org/elasticsearch/index/reindex/RoundTripTests.java @@ -189,9 +189,7 @@ public void testRethrottleRequest() throws IOException { } else { request.setTaskId(new TaskId(randomAlphaOfLength(5), randomLong())); } - RethrottleRequest tripped = new RethrottleRequest(); - // We use readFrom here because Rethrottle does not support the Writeable.Reader interface - tripped.readFrom(toInputByteStream(request)); + RethrottleRequest tripped = new RethrottleRequest(toInputByteStream(request)); assertEquals(request.getRequestsPerSecond(), tripped.getRequestsPerSecond(), 0.00001); assertArrayEquals(request.getActions(), tripped.getActions()); assertEquals(request.getTaskId(), tripped.getTaskId()); diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/node/tasks/cancel/CancelTasksRequest.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/node/tasks/cancel/CancelTasksRequest.java index e92695d61e242..5c87b1da45d12 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/node/tasks/cancel/CancelTasksRequest.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/node/tasks/cancel/CancelTasksRequest.java @@ -36,10 +36,11 @@ public class CancelTasksRequest extends BaseTasksRequest { private String reason = DEFAULT_REASON; - @Override - public void readFrom(StreamInput in) throws IOException { - super.readFrom(in); - reason = in.readString(); + public CancelTasksRequest() {} + + public CancelTasksRequest(StreamInput in) throws IOException { + super(in); + this.reason = in.readString(); } @Override diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/node/tasks/cancel/TransportCancelTasksAction.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/node/tasks/cancel/TransportCancelTasksAction.java index 0bd1ff2945bd7..b40862593c6cb 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/node/tasks/cancel/TransportCancelTasksAction.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/node/tasks/cancel/TransportCancelTasksAction.java @@ -64,13 +64,12 @@ public class TransportCancelTasksAction extends TransportTasksAction { private boolean detailed = false; private boolean waitForCompletion = false; + public ListTasksRequest() { + } + + public ListTasksRequest(StreamInput in) throws IOException { + super(in); + detailed = in.readBoolean(); + waitForCompletion = in.readBoolean(); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeBoolean(detailed); + out.writeBoolean(waitForCompletion); + } + /** * Should the detailed task information be returned. */ @@ -63,17 +79,4 @@ public ListTasksRequest setWaitForCompletion(boolean waitForCompletion) { return this; } - @Override - public void readFrom(StreamInput in) throws IOException { - super.readFrom(in); - detailed = in.readBoolean(); - waitForCompletion = in.readBoolean(); - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - super.writeTo(out); - out.writeBoolean(detailed); - out.writeBoolean(waitForCompletion); - } } diff --git a/server/src/main/java/org/elasticsearch/action/support/tasks/BaseTasksRequest.java b/server/src/main/java/org/elasticsearch/action/support/tasks/BaseTasksRequest.java index cbfdfc294c581..f44434192d74c 100644 --- a/server/src/main/java/org/elasticsearch/action/support/tasks/BaseTasksRequest.java +++ b/server/src/main/java/org/elasticsearch/action/support/tasks/BaseTasksRequest.java @@ -52,9 +52,30 @@ public class BaseTasksRequest> extends private TaskId taskId = TaskId.EMPTY_TASK_ID; + // NOTE: This constructor is only needed, because the setters in this class, + // otherwise it can be removed and above fields can be made final. public BaseTasksRequest() { } + protected BaseTasksRequest(StreamInput in) throws IOException { + super(in); + taskId = TaskId.readFromStream(in); + parentTaskId = TaskId.readFromStream(in); + nodes = in.readStringArray(); + actions = in.readStringArray(); + timeout = in.readOptionalTimeValue(); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + taskId.writeTo(out); + parentTaskId.writeTo(out); + out.writeStringArrayNullable(nodes); + out.writeStringArrayNullable(actions); + out.writeOptionalTimeValue(timeout); + } + @Override public ActionRequestValidationException validate() { ActionRequestValidationException validationException = null; @@ -137,26 +158,6 @@ public final Request setTimeout(String timeout) { return (Request) this; } - @Override - public void readFrom(StreamInput in) throws IOException { - super.readFrom(in); - taskId = TaskId.readFromStream(in); - parentTaskId = TaskId.readFromStream(in); - nodes = in.readStringArray(); - actions = in.readStringArray(); - timeout = in.readOptionalTimeValue(); - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - super.writeTo(out); - taskId.writeTo(out); - parentTaskId.writeTo(out); - out.writeStringArrayNullable(nodes); - out.writeStringArrayNullable(actions); - out.writeOptionalTimeValue(timeout); - } - public boolean match(Task task) { if (getActions() != null && getActions().length > 0 && Regex.simpleMatch(getActions(), task.getAction()) == false) { return false; diff --git a/server/src/main/java/org/elasticsearch/action/support/tasks/TransportTasksAction.java b/server/src/main/java/org/elasticsearch/action/support/tasks/TransportTasksAction.java index dde4d8f4c9f68..5d1ea03645352 100644 --- a/server/src/main/java/org/elasticsearch/action/support/tasks/TransportTasksAction.java +++ b/server/src/main/java/org/elasticsearch/action/support/tasks/TransportTasksAction.java @@ -73,24 +73,24 @@ public abstract class TransportTasksAction< protected final ClusterService clusterService; protected final TransportService transportService; - protected final Supplier requestSupplier; + protected final Writeable.Reader requestSupplier; protected final Supplier responseSupplier; protected final String transportNodeAction; protected TransportTasksAction(Settings settings, String actionName, ThreadPool threadPool, ClusterService clusterService, TransportService transportService, ActionFilters actionFilters, - IndexNameExpressionResolver indexNameExpressionResolver, Supplier requestSupplier, + IndexNameExpressionResolver indexNameExpressionResolver, Writeable.Reader requestSupplier, Supplier responseSupplier, String nodeExecutor) { - super(settings, actionName, threadPool, transportService, actionFilters, indexNameExpressionResolver, requestSupplier); + super(settings, actionName, threadPool, transportService, actionFilters, requestSupplier, indexNameExpressionResolver); this.clusterService = clusterService; this.transportService = transportService; this.transportNodeAction = actionName + "[n]"; this.requestSupplier = requestSupplier; this.responseSupplier = responseSupplier; - transportService.registerRequestHandler(transportNodeAction, NodeTaskRequest::new, nodeExecutor, new NodeTransportHandler()); + transportService.registerRequestHandler(transportNodeAction, nodeExecutor, NodeTaskRequest::new, new NodeTransportHandler()); } @Override @@ -372,20 +372,9 @@ public void onFailure(Exception e) { private class NodeTaskRequest extends TransportRequest { private TasksRequest tasksRequest; - protected NodeTaskRequest() { - super(); - } - - protected NodeTaskRequest(TasksRequest tasksRequest) { - super(); - this.tasksRequest = tasksRequest; - } - - @Override - public void readFrom(StreamInput in) throws IOException { - super.readFrom(in); - tasksRequest = requestSupplier.get(); - tasksRequest.readFrom(in); + protected NodeTaskRequest(StreamInput in) throws IOException { + super(in); + this.tasksRequest = requestSupplier.read(in); } @Override @@ -393,6 +382,12 @@ public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); tasksRequest.writeTo(out); } + + protected NodeTaskRequest(TasksRequest tasksRequest) { + super(); + this.tasksRequest = tasksRequest; + } + } private class NodeTasksResponse extends TransportResponse { diff --git a/server/src/test/java/org/elasticsearch/action/admin/cluster/node/tasks/TestTaskPlugin.java b/server/src/test/java/org/elasticsearch/action/admin/cluster/node/tasks/TestTaskPlugin.java index db2711e767be6..197fada2c7336 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/cluster/node/tasks/TestTaskPlugin.java +++ b/server/src/test/java/org/elasticsearch/action/admin/cluster/node/tasks/TestTaskPlugin.java @@ -406,6 +406,13 @@ public void writeTo(StreamOutput out) throws IOException { public static class UnblockTestTasksRequest extends BaseTasksRequest { + + UnblockTestTasksRequest() {} + + UnblockTestTasksRequest(StreamInput in) throws IOException { + super(in); + } + @Override public boolean match(Task task) { return task instanceof TestTask && super.match(task); diff --git a/server/src/test/java/org/elasticsearch/action/admin/cluster/node/tasks/TransportTasksActionTests.java b/server/src/test/java/org/elasticsearch/action/admin/cluster/node/tasks/TransportTasksActionTests.java index 366243aed9a2e..857ad003f8c7e 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/cluster/node/tasks/TransportTasksActionTests.java +++ b/server/src/test/java/org/elasticsearch/action/admin/cluster/node/tasks/TransportTasksActionTests.java @@ -212,6 +212,11 @@ public String getStatus() { static class TestTasksRequest extends BaseTasksRequest { + TestTasksRequest(StreamInput in) throws IOException { + super(in); + } + + TestTasksRequest() {} } static class TestTasksResponse extends BaseTasksResponse { diff --git a/server/src/test/java/org/elasticsearch/persistent/TestPersistentTasksPlugin.java b/server/src/test/java/org/elasticsearch/persistent/TestPersistentTasksPlugin.java index 87b8e3d2df6bb..39b6144be8563 100644 --- a/server/src/test/java/org/elasticsearch/persistent/TestPersistentTasksPlugin.java +++ b/server/src/test/java/org/elasticsearch/persistent/TestPersistentTasksPlugin.java @@ -454,9 +454,8 @@ public static class TestTasksRequest extends BaseTasksRequest public TestTasksRequest() { } - @Override - public void readFrom(StreamInput in) throws IOException { - super.readFrom(in); + public TestTasksRequest(StreamInput in) throws IOException { + super(in); operation = in.readOptionalString(); } diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportFollowStatsAction.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportFollowStatsAction.java index 9791c38bb8b7e..e2ac8fa4aeb7e 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportFollowStatsAction.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportFollowStatsAction.java @@ -93,7 +93,6 @@ protected FollowStatsAction.StatsResponse readTaskResponse(final StreamInput in) protected void processTasks(final FollowStatsAction.StatsRequest request, final Consumer operation) { final ClusterState state = clusterService.state(); final PersistentTasksCustomMetaData persistentTasksMetaData = state.metaData().custom(PersistentTasksCustomMetaData.TYPE); - if (persistentTasksMetaData == null) { return; } diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/StatsRequestTests.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/StatsRequestTests.java index 97c2b26a4a7e4..c42846bbde53c 100644 --- a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/StatsRequestTests.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/StatsRequestTests.java @@ -5,14 +5,15 @@ */ package org.elasticsearch.xpack.ccr.action; -import org.elasticsearch.test.AbstractStreamableTestCase; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.test.AbstractWireSerializingTestCase; import org.elasticsearch.xpack.core.ccr.action.FollowStatsAction; -public class StatsRequestTests extends AbstractStreamableTestCase { +public class StatsRequestTests extends AbstractWireSerializingTestCase { @Override - protected FollowStatsAction.StatsRequest createBlankInstance() { - return new FollowStatsAction.StatsRequest(); + protected Writeable.Reader instanceReader() { + return FollowStatsAction.StatsRequest::new; } @Override diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/FollowStatsAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/FollowStatsAction.java index baa44129b10d7..3534b2d0971c2 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/FollowStatsAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/FollowStatsAction.java @@ -132,6 +132,19 @@ public static class StatsRequest extends BaseTasksRequest implemen private String[] indices; + public StatsRequest() {} + + public StatsRequest(StreamInput in) throws IOException { + super(in); + indices = in.readOptionalStringArray(); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeOptionalStringArray(indices); + } + @Override public String[] indices() { return indices; @@ -164,18 +177,6 @@ public ActionRequestValidationException validate() { return null; } - @Override - public void readFrom(final StreamInput in) throws IOException { - super.readFrom(in); - indices = in.readOptionalStringArray(); - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - super.writeTo(out); - out.writeOptionalStringArray(indices); - } - @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/CloseJobAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/CloseJobAction.java index c66686136e7e5..303638daa5388 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/CloseJobAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/CloseJobAction.java @@ -84,6 +84,31 @@ public Request() { openJobIds = new String[] {}; } + public Request(StreamInput in) throws IOException { + super(in); + jobId = in.readString(); + timeout = in.readTimeValue(); + force = in.readBoolean(); + openJobIds = in.readStringArray(); + local = in.readBoolean(); + if (in.getVersion().onOrAfter(Version.V_6_1_0)) { + allowNoJobs = in.readBoolean(); + } + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeString(jobId); + out.writeTimeValue(timeout); + out.writeBoolean(force); + out.writeStringArray(openJobIds); + out.writeBoolean(local); + if (out.getVersion().onOrAfter(Version.V_6_1_0)) { + out.writeBoolean(allowNoJobs); + } + } + public Request(String jobId) { this(); this.jobId = jobId; @@ -133,32 +158,6 @@ public void setOpenJobIds(String [] openJobIds) { this.openJobIds = openJobIds; } - @Override - public void readFrom(StreamInput in) throws IOException { - super.readFrom(in); - jobId = in.readString(); - timeout = in.readTimeValue(); - force = in.readBoolean(); - openJobIds = in.readStringArray(); - local = in.readBoolean(); - if (in.getVersion().onOrAfter(Version.V_6_1_0)) { - allowNoJobs = in.readBoolean(); - } - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - super.writeTo(out); - out.writeString(jobId); - out.writeTimeValue(timeout); - out.writeBoolean(force); - out.writeStringArray(openJobIds); - out.writeBoolean(local); - if (out.getVersion().onOrAfter(Version.V_6_1_0)) { - out.writeBoolean(allowNoJobs); - } - } - @Override public boolean match(Task task) { for (String id : openJobIds) { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/FlushJobAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/FlushJobAction.java index 2da2505a77170..a07d92d14a452 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/FlushJobAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/FlushJobAction.java @@ -81,6 +81,29 @@ public static Request parseRequest(String jobId, XContentParser parser) { public Request() { } + public Request(StreamInput in) throws IOException { + super(in); + calcInterim = in.readBoolean(); + start = in.readOptionalString(); + end = in.readOptionalString(); + advanceTime = in.readOptionalString(); + if (in.getVersion().after(Version.V_5_5_0)) { + skipTime = in.readOptionalString(); + } + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeBoolean(calcInterim); + out.writeOptionalString(start); + out.writeOptionalString(end); + out.writeOptionalString(advanceTime); + if (out.getVersion().after(Version.V_5_5_0)) { + out.writeOptionalString(skipTime); + } + } + public Request(String jobId) { super(jobId); } @@ -125,30 +148,6 @@ public void setSkipTime(String skipTime) { this.skipTime = skipTime; } - @Override - public void readFrom(StreamInput in) throws IOException { - super.readFrom(in); - calcInterim = in.readBoolean(); - start = in.readOptionalString(); - end = in.readOptionalString(); - advanceTime = in.readOptionalString(); - if (in.getVersion().after(Version.V_5_5_0)) { - skipTime = in.readOptionalString(); - } - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - super.writeTo(out); - out.writeBoolean(calcInterim); - out.writeOptionalString(start); - out.writeOptionalString(end); - out.writeOptionalString(advanceTime); - if (out.getVersion().after(Version.V_5_5_0)) { - out.writeOptionalString(skipTime); - } - } - @Override public int hashCode() { return Objects.hash(jobId, calcInterim, start, end, advanceTime, skipTime); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/ForecastJobAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/ForecastJobAction.java index 4868a1e73da37..06554d0a96603 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/ForecastJobAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/ForecastJobAction.java @@ -74,6 +74,19 @@ public static Request parseRequest(String jobId, XContentParser parser) { public Request() { } + public Request(StreamInput in) throws IOException { + super(in); + this.duration = in.readOptionalTimeValue(); + this.expiresIn = in.readOptionalTimeValue(); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeOptionalTimeValue(duration); + out.writeOptionalTimeValue(expiresIn); + } + public Request(String jobId) { super(jobId); } @@ -114,20 +127,6 @@ public void setExpiresIn(TimeValue expiresIn) { } } - @Override - public void readFrom(StreamInput in) throws IOException { - super.readFrom(in); - this.duration = in.readOptionalTimeValue(); - this.expiresIn = in.readOptionalTimeValue(); - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - super.writeTo(out); - out.writeOptionalTimeValue(duration); - out.writeOptionalTimeValue(expiresIn); - } - @Override public int hashCode() { return Objects.hash(jobId, duration, expiresIn); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/GetJobsStatsAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/GetJobsStatsAction.java index 23bbd55d9af05..a19fe98565273 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/GetJobsStatsAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/GetJobsStatsAction.java @@ -81,6 +81,25 @@ public Request(String jobId) { public Request() {} + public Request(StreamInput in) throws IOException { + super(in); + jobId = in.readString(); + expandedJobsIds = in.readList(StreamInput::readString); + if (in.getVersion().onOrAfter(Version.V_6_1_0)) { + allowNoJobs = in.readBoolean(); + } + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeString(jobId); + out.writeStringList(expandedJobsIds); + if (out.getVersion().onOrAfter(Version.V_6_1_0)) { + out.writeBoolean(allowNoJobs); + } + } + public List getExpandedJobsIds() { return expandedJobsIds; } public void setExpandedJobsIds(List expandedJobsIds) { this.expandedJobsIds = expandedJobsIds; } @@ -107,26 +126,6 @@ public ActionRequestValidationException validate() { return null; } - @Override - public void readFrom(StreamInput in) throws IOException { - super.readFrom(in); - jobId = in.readString(); - expandedJobsIds = in.readList(StreamInput::readString); - if (in.getVersion().onOrAfter(Version.V_6_1_0)) { - allowNoJobs = in.readBoolean(); - } - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - super.writeTo(out); - out.writeString(jobId); - out.writeStringList(expandedJobsIds); - if (out.getVersion().onOrAfter(Version.V_6_1_0)) { - out.writeBoolean(allowNoJobs); - } - } - @Override public int hashCode() { return Objects.hash(jobId, allowNoJobs); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/IsolateDatafeedAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/IsolateDatafeedAction.java index 99aaf18fc84cc..9ccd369f77e5c 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/IsolateDatafeedAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/IsolateDatafeedAction.java @@ -84,6 +84,17 @@ public Request(String datafeedId) { public Request() { } + public Request(StreamInput in) throws IOException { + super(in); + datafeedId = in.readString(); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeString(datafeedId); + } + public String getDatafeedId() { return datafeedId; } @@ -99,18 +110,6 @@ public ActionRequestValidationException validate() { return null; } - @Override - public void readFrom(StreamInput in) throws IOException { - super.readFrom(in); - datafeedId = in.readString(); - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - super.writeTo(out); - out.writeString(datafeedId); - } - @Override public int hashCode() { return Objects.hash(datafeedId); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/JobTaskRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/JobTaskRequest.java index adc84b2cf46d8..e2ada4ee92868 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/JobTaskRequest.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/JobTaskRequest.java @@ -21,18 +21,9 @@ public class JobTaskRequest> extends BaseTasksReques JobTaskRequest() { } - JobTaskRequest(String jobId) { - this.jobId = ExceptionsHelper.requireNonNull(jobId, Job.ID.getPreferredName()); - } - - public String getJobId() { - return jobId; - } - - @Override - public void readFrom(StreamInput in) throws IOException { - super.readFrom(in); - jobId = in.readString(); + JobTaskRequest(StreamInput in) throws IOException { + super(in); + this.jobId = in.readString(); } @Override @@ -41,6 +32,14 @@ public void writeTo(StreamOutput out) throws IOException { out.writeString(jobId); } + JobTaskRequest(String jobId) { + this.jobId = ExceptionsHelper.requireNonNull(jobId, Job.ID.getPreferredName()); + } + + public String getJobId() { + return jobId; + } + @Override public boolean match(Task task) { return OpenJobAction.JobTaskMatcher.match(task, jobId); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/KillProcessAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/KillProcessAction.java index 48ee4432ff015..ffa6b855bae77 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/KillProcessAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/KillProcessAction.java @@ -52,6 +52,10 @@ public Request(String jobId) { public Request() { super(); } + + public Request(StreamInput in) throws IOException { + super(in); + } } public static class Response extends BaseTasksResponse implements Writeable { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/PersistJobAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/PersistJobAction.java index 71f65051464b2..8a1885eebdd14 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/PersistJobAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/PersistJobAction.java @@ -40,6 +40,19 @@ public static class Request extends JobTaskRequest { public Request() { } + public Request(StreamInput in) throws IOException { + super(in); + // isBackground for fwc + in.readBoolean(); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + // isBackground for fwc + out.writeBoolean(true); + } + public Request(String jobId) { super(jobId); } @@ -52,20 +65,6 @@ public boolean isForeground() { return !isBackGround(); } - @Override - public void readFrom(StreamInput in) throws IOException { - super.readFrom(in); - // isBackground for fwc - in.readBoolean(); - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - super.writeTo(out); - // isBackground for fwc - out.writeBoolean(true); - } - @Override public int hashCode() { return Objects.hash(jobId, isBackGround()); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/PostDataAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/PostDataAction.java index 9ba1bf574db8b..4cc3fee3b56f8 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/PostDataAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/PostDataAction.java @@ -131,6 +131,31 @@ public static class Request extends JobTaskRequest { public Request() { } + public Request(StreamInput in) throws IOException { + super(in); + resetStart = in.readOptionalString(); + resetEnd = in.readOptionalString(); + dataDescription = in.readOptionalWriteable(DataDescription::new); + content = in.readBytesReference(); + if (in.readBoolean()) { + xContentType = in.readEnum(XContentType.class); + } + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeOptionalString(resetStart); + out.writeOptionalString(resetEnd); + out.writeOptionalWriteable(dataDescription); + out.writeBytesReference(content); + boolean hasXContentType = xContentType != null; + out.writeBoolean(hasXContentType); + if (hasXContentType) { + out.writeEnum(xContentType); + } + } + public Request(String jobId) { super(jobId); } @@ -170,32 +195,6 @@ public void setContent(BytesReference content, XContentType xContentType) { this.xContentType = xContentType; } - @Override - public void readFrom(StreamInput in) throws IOException { - super.readFrom(in); - resetStart = in.readOptionalString(); - resetEnd = in.readOptionalString(); - dataDescription = in.readOptionalWriteable(DataDescription::new); - content = in.readBytesReference(); - if (in.readBoolean()) { - xContentType = in.readEnum(XContentType.class); - } - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - super.writeTo(out); - out.writeOptionalString(resetStart); - out.writeOptionalString(resetEnd); - out.writeOptionalWriteable(dataDescription); - out.writeBytesReference(content); - boolean hasXContentType = xContentType != null; - out.writeBoolean(hasXContentType); - if (hasXContentType) { - out.writeEnum(xContentType); - } - } - @Override public int hashCode() { // content stream not included diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/StopDatafeedAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/StopDatafeedAction.java index 5de0eba3a90a3..4e336d984f67d 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/StopDatafeedAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/StopDatafeedAction.java @@ -91,6 +91,17 @@ public Request(String datafeedId) { public Request() { } + public Request(StreamInput in) throws IOException { + super(in); + datafeedId = in.readString(); + resolvedStartedDatafeedIds = in.readStringArray(); + stopTimeout = in.readTimeValue(); + force = in.readBoolean(); + if (in.getVersion().onOrAfter(Version.V_6_1_0)) { + allowNoDatafeeds = in.readBoolean(); + } + } + public String getDatafeedId() { return datafeedId; } @@ -143,18 +154,6 @@ public ActionRequestValidationException validate() { return null; } - @Override - public void readFrom(StreamInput in) throws IOException { - super.readFrom(in); - datafeedId = in.readString(); - resolvedStartedDatafeedIds = in.readStringArray(); - stopTimeout = in.readTimeValue(); - force = in.readBoolean(); - if (in.getVersion().onOrAfter(Version.V_6_1_0)) { - allowNoDatafeeds = in.readBoolean(); - } - } - @Override public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/UpdateProcessAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/UpdateProcessAction.java index 31ba85232d504..ee090188d25b0 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/UpdateProcessAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/UpdateProcessAction.java @@ -115,7 +115,33 @@ public static class Request extends JobTaskRequest { private MlFilter filter; private boolean updateScheduledEvents = false; - public Request() { + public Request() {} + + public Request(StreamInput in) throws IOException { + super(in); + modelPlotConfig = in.readOptionalWriteable(ModelPlotConfig::new); + if (in.readBoolean()) { + detectorUpdates = in.readList(JobUpdate.DetectorUpdate::new); + } + if (in.getVersion().onOrAfter(Version.V_6_2_0)) { + filter = in.readOptionalWriteable(MlFilter::new); + updateScheduledEvents = in.readBoolean(); + } + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeOptionalWriteable(modelPlotConfig); + boolean hasDetectorUpdates = detectorUpdates != null; + out.writeBoolean(hasDetectorUpdates); + if (hasDetectorUpdates) { + out.writeList(detectorUpdates); + } + if (out.getVersion().onOrAfter(Version.V_6_2_0)) { + out.writeOptionalWriteable(filter); + out.writeBoolean(updateScheduledEvents); + } } public Request(String jobId, ModelPlotConfig modelPlotConfig, List detectorUpdates, MlFilter filter, @@ -143,34 +169,6 @@ public boolean isUpdateScheduledEvents() { return updateScheduledEvents; } - @Override - public void readFrom(StreamInput in) throws IOException { - super.readFrom(in); - modelPlotConfig = in.readOptionalWriteable(ModelPlotConfig::new); - if (in.readBoolean()) { - detectorUpdates = in.readList(JobUpdate.DetectorUpdate::new); - } - if (in.getVersion().onOrAfter(Version.V_6_2_0)) { - filter = in.readOptionalWriteable(MlFilter::new); - updateScheduledEvents = in.readBoolean(); - } - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - super.writeTo(out); - out.writeOptionalWriteable(modelPlotConfig); - boolean hasDetectorUpdates = detectorUpdates != null; - out.writeBoolean(hasDetectorUpdates); - if (hasDetectorUpdates) { - out.writeList(detectorUpdates); - } - if (out.getVersion().onOrAfter(Version.V_6_2_0)) { - out.writeOptionalWriteable(filter); - out.writeBoolean(updateScheduledEvents); - } - } - @Override public int hashCode() { return Objects.hash(getJobId(), modelPlotConfig, detectorUpdates, filter, updateScheduledEvents); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/action/DeleteRollupJobAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/action/DeleteRollupJobAction.java index 593b9070891da..69f17d85f3c45 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/action/DeleteRollupJobAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/action/DeleteRollupJobAction.java @@ -58,6 +58,11 @@ public Request(String id) { public Request() {} + public Request(StreamInput in) throws IOException { + super(in); + id = in.readString(); + } + @Override public boolean match(Task task) { return task.getDescription().equals(RollupField.NAME + "_" + id); @@ -67,12 +72,6 @@ public String getId() { return id; } - @Override - public void readFrom(StreamInput in) throws IOException { - super.readFrom(in); - id = in.readString(); - } - @Override public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/action/GetRollupJobsAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/action/GetRollupJobsAction.java index 8b25b99000b52..f397b062e478b 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/action/GetRollupJobsAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/action/GetRollupJobsAction.java @@ -72,6 +72,20 @@ public Request(String id) { public Request() {} + public Request(StreamInput in) throws IOException { + super(in); + id = in.readString(); + if (Strings.isNullOrEmpty(id) || id.equals("*")) { + this.id = MetaData.ALL; + } + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeString(id); + } + @Override public boolean match(Task task) { // If we are retrieving all the jobs, the task description just needs to start @@ -87,21 +101,6 @@ public String getId() { return id; } - @Override - public void readFrom(StreamInput in) throws IOException { - super.readFrom(in); - id = in.readString(); - if (Strings.isNullOrEmpty(id) || id.equals("*")) { - this.id = MetaData.ALL; - } - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - super.writeTo(out); - out.writeString(id); - } - @Override public ActionRequestValidationException validate() { return null; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/action/StartRollupJobAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/action/StartRollupJobAction.java index f18ca67586f9f..7b1c9ced1eef7 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/action/StartRollupJobAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/action/StartRollupJobAction.java @@ -54,13 +54,8 @@ public Request(String id) { public Request() {} - public String getId() { - return id; - } - - @Override - public void readFrom(StreamInput in) throws IOException { - super.readFrom(in); + public Request(StreamInput in) throws IOException { + super(in); id = in.readString(); } @@ -70,6 +65,10 @@ public void writeTo(StreamOutput out) throws IOException { out.writeString(id); } + public String getId() { + return id; + } + @Override public ActionRequestValidationException validate() { return null; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/action/StopRollupJobAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/action/StopRollupJobAction.java index 9983666b50550..f5f37034aa470 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/action/StopRollupJobAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rollup/action/StopRollupJobAction.java @@ -69,21 +69,8 @@ public Request(String id, boolean waitForCompletion, @Nullable TimeValue timeout public Request() {} - public String getId() { - return id; - } - - public TimeValue timeout() { - return timeout; - } - - public boolean waitForCompletion() { - return waitForCompletion; - } - - @Override - public void readFrom(StreamInput in) throws IOException { - super.readFrom(in); + public Request(StreamInput in) throws IOException { + super(in); id = in.readString(); if (in.getVersion().onOrAfter(Version.V_6_6_0)) { waitForCompletion = in.readBoolean(); @@ -101,6 +88,18 @@ public void writeTo(StreamOutput out) throws IOException { } } + public String getId() { + return id; + } + + public TimeValue timeout() { + return timeout; + } + + public boolean waitForCompletion() { + return waitForCompletion; + } + @Override public ActionRequestValidationException validate() { return null; diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/CloseJobActionRequestTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/CloseJobActionRequestTests.java index a8224d31d8d0b..ff161581e1bdf 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/CloseJobActionRequestTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/CloseJobActionRequestTests.java @@ -5,12 +5,13 @@ */ package org.elasticsearch.xpack.core.ml.action; +import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.test.AbstractStreamableXContentTestCase; +import org.elasticsearch.test.AbstractSerializingTestCase; import org.elasticsearch.xpack.core.ml.action.CloseJobAction.Request; -public class CloseJobActionRequestTests extends AbstractStreamableXContentTestCase { +public class CloseJobActionRequestTests extends AbstractSerializingTestCase { @Override protected Request createTestInstance() { @@ -33,8 +34,8 @@ protected boolean supportsUnknownFields() { } @Override - protected Request createBlankInstance() { - return new Request(); + protected Writeable.Reader instanceReader() { + return Request::new; } @Override diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/ForecastJobActionRequestTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/ForecastJobActionRequestTests.java index cdcac09e0736b..422244eff2b40 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/ForecastJobActionRequestTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/ForecastJobActionRequestTests.java @@ -5,14 +5,15 @@ */ package org.elasticsearch.xpack.core.ml.action; +import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.test.AbstractStreamableXContentTestCase; +import org.elasticsearch.test.AbstractSerializingTestCase; import org.elasticsearch.xpack.core.ml.action.ForecastJobAction.Request; import static org.hamcrest.Matchers.equalTo; -public class ForecastJobActionRequestTests extends AbstractStreamableXContentTestCase { +public class ForecastJobActionRequestTests extends AbstractSerializingTestCase { @Override protected Request doParseInstance(XContentParser parser) { @@ -37,8 +38,8 @@ protected Request createTestInstance() { } @Override - protected Request createBlankInstance() { - return new Request(); + protected Writeable.Reader instanceReader() { + return Request::new; } public void testSetDuration_GivenZero() { diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/GetJobStatsActionRequestTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/GetJobStatsActionRequestTests.java index edf3f73a8afc8..a78fc126fce20 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/GetJobStatsActionRequestTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/GetJobStatsActionRequestTests.java @@ -6,14 +6,15 @@ package org.elasticsearch.xpack.core.ml.action; import org.elasticsearch.cluster.metadata.MetaData; +import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.tasks.Task; -import org.elasticsearch.test.AbstractStreamableTestCase; +import org.elasticsearch.test.AbstractWireSerializingTestCase; import org.elasticsearch.xpack.core.ml.action.GetJobsStatsAction.Request; import static org.hamcrest.Matchers.is; import static org.mockito.Mockito.mock; -public class GetJobStatsActionRequestTests extends AbstractStreamableTestCase { +public class GetJobStatsActionRequestTests extends AbstractWireSerializingTestCase { @Override protected Request createTestInstance() { @@ -23,8 +24,8 @@ protected Request createTestInstance() { } @Override - protected Request createBlankInstance() { - return new Request(); + protected Writeable.Reader instanceReader() { + return Request::new; } public void testMatch_GivenAll_FailsForNonJobTasks() { diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/PersistJobActionRequestTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/PersistJobActionRequestTests.java index cf210403dd6b7..ad43e5329b57d 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/PersistJobActionRequestTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/PersistJobActionRequestTests.java @@ -5,12 +5,14 @@ */ package org.elasticsearch.xpack.core.ml.action; -import org.elasticsearch.test.AbstractStreamableTestCase; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.test.AbstractWireSerializingTestCase; + +public class PersistJobActionRequestTests extends AbstractWireSerializingTestCase { -public class PersistJobActionRequestTests extends AbstractStreamableTestCase { @Override - protected PersistJobAction.Request createBlankInstance() { - return new PersistJobAction.Request(); + protected Writeable.Reader instanceReader() { + return PersistJobAction.Request::new; } @Override diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/PostDataActionRequestTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/PostDataActionRequestTests.java index ba4a3ff06477d..8b3c2221bc6cd 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/PostDataActionRequestTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/PostDataActionRequestTests.java @@ -6,12 +6,13 @@ package org.elasticsearch.xpack.core.ml.action; import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.xcontent.XContentType; -import org.elasticsearch.test.AbstractStreamableTestCase; +import org.elasticsearch.test.AbstractWireSerializingTestCase; import org.elasticsearch.xpack.core.ml.job.config.DataDescription; import org.elasticsearch.xpack.core.ml.job.config.DataDescription.DataFormat; -public class PostDataActionRequestTests extends AbstractStreamableTestCase { +public class PostDataActionRequestTests extends AbstractWireSerializingTestCase { @Override protected PostDataAction.Request createTestInstance() { PostDataAction.Request request = new PostDataAction.Request(randomAlphaOfLengthBetween(1, 20)); @@ -33,7 +34,7 @@ protected PostDataAction.Request createTestInstance() { } @Override - protected PostDataAction.Request createBlankInstance() { - return new PostDataAction.Request(); + protected Writeable.Reader instanceReader() { + return PostDataAction.Request::new; } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/PostDataFlushRequestTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/PostDataFlushRequestTests.java index a4fd8c3c47069..2497336cbd9c4 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/PostDataFlushRequestTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/PostDataFlushRequestTests.java @@ -5,10 +5,11 @@ */ package org.elasticsearch.xpack.core.ml.action; -import org.elasticsearch.test.AbstractStreamableTestCase; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.test.AbstractWireSerializingTestCase; import org.elasticsearch.xpack.core.ml.action.FlushJobAction.Request; -public class PostDataFlushRequestTests extends AbstractStreamableTestCase { +public class PostDataFlushRequestTests extends AbstractWireSerializingTestCase { @Override protected Request createTestInstance() { @@ -27,11 +28,11 @@ protected Request createTestInstance() { } @Override - protected Request createBlankInstance() { - return new Request(); + protected Writeable.Reader instanceReader() { + return Request::new; } public void testNullJobIdThrows() { - expectThrows(IllegalArgumentException.class, () -> new Request(null)); + expectThrows(IllegalArgumentException.class, () -> new Request((String) null)); } -} \ No newline at end of file +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/StopDatafeedActionRequestTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/StopDatafeedActionRequestTests.java index 94fb84d7f64a1..09b2110e0c80a 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/StopDatafeedActionRequestTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/StopDatafeedActionRequestTests.java @@ -5,12 +5,13 @@ */ package org.elasticsearch.xpack.core.ml.action; +import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.test.AbstractStreamableXContentTestCase; +import org.elasticsearch.test.AbstractSerializingTestCase; import org.elasticsearch.xpack.core.ml.action.StopDatafeedAction.Request; -public class StopDatafeedActionRequestTests extends AbstractStreamableXContentTestCase { +public class StopDatafeedActionRequestTests extends AbstractSerializingTestCase { @Override protected Request createTestInstance() { @@ -36,8 +37,8 @@ protected boolean supportsUnknownFields() { } @Override - protected Request createBlankInstance() { - return new Request(); + protected Writeable.Reader instanceReader() { + return Request::new; } @Override diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/UpdateProcessActionRequestTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/UpdateProcessActionRequestTests.java index f7ee459bb1944..0895086c83d1d 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/UpdateProcessActionRequestTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/UpdateProcessActionRequestTests.java @@ -5,7 +5,8 @@ */ package org.elasticsearch.xpack.core.ml.action; -import org.elasticsearch.test.AbstractStreamableTestCase; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.test.AbstractWireSerializingTestCase; import org.elasticsearch.xpack.core.ml.job.config.JobUpdate; import org.elasticsearch.xpack.core.ml.job.config.MlFilter; import org.elasticsearch.xpack.core.ml.job.config.MlFilterTests; @@ -14,8 +15,7 @@ import java.util.ArrayList; import java.util.List; -public class UpdateProcessActionRequestTests extends AbstractStreamableTestCase { - +public class UpdateProcessActionRequestTests extends AbstractWireSerializingTestCase { @Override protected UpdateProcessAction.Request createTestInstance() { @@ -39,7 +39,7 @@ protected UpdateProcessAction.Request createTestInstance() { } @Override - protected UpdateProcessAction.Request createBlankInstance() { - return new UpdateProcessAction.Request(); + protected Writeable.Reader instanceReader() { + return UpdateProcessAction.Request::new; } -} \ No newline at end of file +} diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportJobTaskAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportJobTaskAction.java index d0da0907b7d5e..73c6ed4af1eb8 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportJobTaskAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportJobTaskAction.java @@ -42,7 +42,7 @@ public abstract class TransportJobTaskAction requestSupplier, + IndexNameExpressionResolver indexNameExpressionResolver, Writeable.Reader requestSupplier, Supplier responseSupplier, String nodeExecutor, AutodetectProcessManager processManager) { super(settings, actionName, threadPool, clusterService, transportService, actionFilters, indexNameExpressionResolver, requestSupplier, responseSupplier, nodeExecutor); diff --git a/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/action/DeleteJobActionRequestTests.java b/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/action/DeleteJobActionRequestTests.java index 46987d3c8f147..7dda04e150c14 100644 --- a/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/action/DeleteJobActionRequestTests.java +++ b/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/action/DeleteJobActionRequestTests.java @@ -5,12 +5,11 @@ */ package org.elasticsearch.xpack.rollup.action; - -import org.elasticsearch.test.AbstractStreamableTestCase; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.test.AbstractWireSerializingTestCase; import org.elasticsearch.xpack.core.rollup.action.DeleteRollupJobAction.Request; - -public class DeleteJobActionRequestTests extends AbstractStreamableTestCase { +public class DeleteJobActionRequestTests extends AbstractWireSerializingTestCase { @Override protected Request createTestInstance() { @@ -18,8 +17,8 @@ protected Request createTestInstance() { } @Override - protected Request createBlankInstance() { - return new Request(); + protected Writeable.Reader instanceReader() { + return Request::new; } } diff --git a/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/action/GetJobsActionRequestTests.java b/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/action/GetJobsActionRequestTests.java index bf73ceb88113e..cc59278a1928d 100644 --- a/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/action/GetJobsActionRequestTests.java +++ b/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/action/GetJobsActionRequestTests.java @@ -5,12 +5,12 @@ */ package org.elasticsearch.xpack.rollup.action; - import org.elasticsearch.cluster.ClusterName; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.metadata.MetaData; +import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.persistent.PersistentTasksCustomMetaData; -import org.elasticsearch.test.AbstractStreamableTestCase; +import org.elasticsearch.test.AbstractWireSerializingTestCase; import org.elasticsearch.xpack.core.rollup.ConfigTestHelpers; import org.elasticsearch.xpack.core.rollup.action.GetRollupJobsAction; import org.elasticsearch.xpack.core.rollup.action.GetRollupJobsAction.Request; @@ -20,8 +20,7 @@ import java.util.HashMap; import java.util.Map; - -public class GetJobsActionRequestTests extends AbstractStreamableTestCase { +public class GetJobsActionRequestTests extends AbstractWireSerializingTestCase { @Override protected Request createTestInstance() { @@ -32,8 +31,8 @@ protected Request createTestInstance() { } @Override - protected Request createBlankInstance() { - return new Request(); + protected Writeable.Reader instanceReader() { + return Request::new; } public void testStateCheckNoPersistentTasks() { diff --git a/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/action/StartJobActionRequestTests.java b/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/action/StartJobActionRequestTests.java index b89a8cce6329f..d9aaee6f3a2f5 100644 --- a/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/action/StartJobActionRequestTests.java +++ b/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/action/StartJobActionRequestTests.java @@ -5,10 +5,11 @@ */ package org.elasticsearch.xpack.rollup.action; -import org.elasticsearch.test.AbstractStreamableTestCase; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.test.AbstractWireSerializingTestCase; import org.elasticsearch.xpack.core.rollup.action.StartRollupJobAction.Request; -public class StartJobActionRequestTests extends AbstractStreamableTestCase { +public class StartJobActionRequestTests extends AbstractWireSerializingTestCase { @Override protected Request createTestInstance() { @@ -16,8 +17,8 @@ protected Request createTestInstance() { } @Override - protected Request createBlankInstance() { - return new Request(); + protected Writeable.Reader instanceReader() { + return Request::new; } } diff --git a/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/action/StopJobActionRequestTests.java b/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/action/StopJobActionRequestTests.java index 7c71791d7f098..73d9ac9f16cf0 100644 --- a/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/action/StopJobActionRequestTests.java +++ b/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/action/StopJobActionRequestTests.java @@ -5,10 +5,11 @@ */ package org.elasticsearch.xpack.rollup.action; -import org.elasticsearch.test.AbstractStreamableTestCase; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.test.AbstractWireSerializingTestCase; import org.elasticsearch.xpack.core.rollup.action.StopRollupJobAction.Request; -public class StopJobActionRequestTests extends AbstractStreamableTestCase { +public class StopJobActionRequestTests extends AbstractWireSerializingTestCase { @Override protected Request createTestInstance() { @@ -16,8 +17,7 @@ protected Request createTestInstance() { } @Override - protected Request createBlankInstance() { - return new Request(); + protected Writeable.Reader instanceReader() { + return Request::new; } - } From cc2a69c97d04607ad5a44ea91f00cb0c558bda8e Mon Sep 17 00:00:00 2001 From: Alpar Torok Date: Mon, 3 Dec 2018 11:01:05 +0200 Subject: [PATCH 069/115] Fix test fixtures on aufs (#36105) Closes #36073 The problem showed up on debian 8 which uses aufs docker storage driver by default as opposed to overlay2 used on other distros. aufs does not support acls and thus the failure. The --use-ntvfs option instructs samba not to rely on acls. From what I can tell this is an implementation detail that should not affect the tests ( which continue to pass ) --- .../test/smb-fixture/src/main/resources/provision/installsmb.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/test/smb-fixture/src/main/resources/provision/installsmb.sh b/x-pack/test/smb-fixture/src/main/resources/provision/installsmb.sh index 47f70b2b0c29c..1594cebe2b6dd 100644 --- a/x-pack/test/smb-fixture/src/main/resources/provision/installsmb.sh +++ b/x-pack/test/smb-fixture/src/main/resources/provision/installsmb.sh @@ -21,7 +21,7 @@ cat $SSL_DIR/ca.pem >> /etc/ssl/certs/ca-certificates.crt mv /etc/samba/smb.conf /etc/samba/smb.conf.orig -samba-tool domain provision --server-role=dc --use-rfc2307 --dns-backend=SAMBA_INTERNAL --realm=AD.TEST.ELASTICSEARCH.COM --domain=ADES --adminpass=Passw0rd +samba-tool domain provision --server-role=dc --use-rfc2307 --dns-backend=SAMBA_INTERNAL --realm=AD.TEST.ELASTICSEARCH.COM --domain=ADES --adminpass=Passw0rd --use-ntvfs cp /var/lib/samba/private/krb5.conf /etc/krb5.conf From 1983900fac82f153521dd7c31396a0ad55212eeb Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Mon, 3 Dec 2018 10:02:35 +0100 Subject: [PATCH 070/115] MINOR: Remove Dead Code in QueryCache (#36147) --- .../index/cache/query/DisabledQueryCache.java | 1 - .../index/cache/query/IndexQueryCache.java | 1 - .../elasticsearch/index/cache/query/QueryCache.java | 10 ---------- 3 files changed, 12 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/cache/query/DisabledQueryCache.java b/server/src/main/java/org/elasticsearch/index/cache/query/DisabledQueryCache.java index df5158b6d7a0d..d7c610dcc6721 100644 --- a/server/src/main/java/org/elasticsearch/index/cache/query/DisabledQueryCache.java +++ b/server/src/main/java/org/elasticsearch/index/cache/query/DisabledQueryCache.java @@ -23,7 +23,6 @@ import org.apache.lucene.search.Weight; import org.elasticsearch.index.AbstractIndexComponent; import org.elasticsearch.index.IndexSettings; -import org.elasticsearch.index.cache.query.QueryCache; public class DisabledQueryCache extends AbstractIndexComponent implements QueryCache { diff --git a/server/src/main/java/org/elasticsearch/index/cache/query/IndexQueryCache.java b/server/src/main/java/org/elasticsearch/index/cache/query/IndexQueryCache.java index 77a32a6789a0c..0f5597cc9c491 100644 --- a/server/src/main/java/org/elasticsearch/index/cache/query/IndexQueryCache.java +++ b/server/src/main/java/org/elasticsearch/index/cache/query/IndexQueryCache.java @@ -24,7 +24,6 @@ import org.elasticsearch.ElasticsearchException; import org.elasticsearch.index.AbstractIndexComponent; import org.elasticsearch.index.IndexSettings; -import org.elasticsearch.index.cache.query.QueryCache; import org.elasticsearch.indices.IndicesQueryCache; /** diff --git a/server/src/main/java/org/elasticsearch/index/cache/query/QueryCache.java b/server/src/main/java/org/elasticsearch/index/cache/query/QueryCache.java index 7a033acff9b57..132c727dee0aa 100644 --- a/server/src/main/java/org/elasticsearch/index/cache/query/QueryCache.java +++ b/server/src/main/java/org/elasticsearch/index/cache/query/QueryCache.java @@ -25,15 +25,5 @@ public interface QueryCache extends IndexComponent, Closeable, org.apache.lucene.search.QueryCache { - class EntriesStats { - public final long sizeInBytes; - public final long count; - - public EntriesStats(long sizeInBytes, long count) { - this.sizeInBytes = sizeInBytes; - this.count = count; - } - } - void clear(String reason); } From 2a01755f3f7c5dfa8dc299246760b786d515e4d5 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Mon, 3 Dec 2018 11:11:10 +0100 Subject: [PATCH 071/115] MINOR: BlobstoreRepository Cleanups (#36140) * Removed redundant private getter * Removed unused `version` field --- .../blobstore/BlobStoreRepository.java | 34 +++++++------------ 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java b/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java index 9acb3d147fcf4..fcccccc0fb0be 100644 --- a/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java +++ b/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java @@ -459,7 +459,7 @@ public void deleteSnapshot(SnapshotId snapshotId, long repositoryStateId) { if (indexMetaData != null) { for (int shardId = 0; shardId < indexMetaData.getNumberOfShards(); shardId++) { try { - delete(snapshotId, snapshot.version(), indexId, new ShardId(indexMetaData.getIndex(), shardId)); + delete(snapshotId, indexId, new ShardId(indexMetaData.getIndex(), shardId)); } catch (SnapshotException ex) { final int finalShardId = shardId; logger.warn(() -> new ParameterizedMessage("[{}] failed to delete shard data for shard [{}][{}]", @@ -865,7 +865,7 @@ public void snapshotShard(IndexShard shard, Store store, SnapshotId snapshotId, @Override public void restoreShard(IndexShard shard, SnapshotId snapshotId, Version version, IndexId indexId, ShardId snapshotShardId, RecoveryState recoveryState) { - final RestoreContext snapshotContext = new RestoreContext(shard, snapshotId, version, indexId, snapshotShardId, recoveryState); + final RestoreContext snapshotContext = new RestoreContext(shard, snapshotId, indexId, snapshotShardId, recoveryState); try { snapshotContext.restore(); } catch (Exception e) { @@ -875,7 +875,7 @@ public void restoreShard(IndexShard shard, SnapshotId snapshotId, Version versio @Override public IndexShardSnapshotStatus getShardSnapshotStatus(SnapshotId snapshotId, Version version, IndexId indexId, ShardId shardId) { - Context context = new Context(snapshotId, version, indexId, shardId); + Context context = new Context(snapshotId, indexId, shardId); BlobStoreIndexShardSnapshot snapshot = context.loadSnapshot(); return IndexShardSnapshotStatus.newDone(snapshot.startTime(), snapshot.time(), snapshot.incrementalFileCount(), snapshot.totalFileCount(), @@ -917,8 +917,8 @@ public void verify(String seed, DiscoveryNode localNode) { * @param snapshotId snapshot id * @param shardId shard id */ - private void delete(SnapshotId snapshotId, Version version, IndexId indexId, ShardId shardId) { - Context context = new Context(snapshotId, version, indexId, shardId, shardId); + private void delete(SnapshotId snapshotId, IndexId indexId, ShardId shardId) { + Context context = new Context(snapshotId, indexId, shardId, shardId); context.delete(); } @@ -930,10 +930,6 @@ public String toString() { ']'; } - BlobStoreFormat indexShardSnapshotFormat(Version version) { - return indexShardSnapshotFormat; - } - /** * Context for snapshot/restore operations */ @@ -945,15 +941,12 @@ private class Context { protected final BlobContainer blobContainer; - protected final Version version; - - Context(SnapshotId snapshotId, Version version, IndexId indexId, ShardId shardId) { - this(snapshotId, version, indexId, shardId, shardId); + Context(SnapshotId snapshotId, IndexId indexId, ShardId shardId) { + this(snapshotId, indexId, shardId, shardId); } - Context(SnapshotId snapshotId, Version version, IndexId indexId, ShardId shardId, ShardId snapshotShardId) { + Context(SnapshotId snapshotId, IndexId indexId, ShardId shardId, ShardId snapshotShardId) { this.snapshotId = snapshotId; - this.version = version; this.shardId = shardId; blobContainer = blobStore().blobContainer(basePath().add("indices").add(indexId.getId()).add(Integer.toString(snapshotShardId.getId()))); } @@ -974,7 +967,7 @@ public void delete() { int fileListGeneration = tuple.v2(); try { - indexShardSnapshotFormat(version).delete(blobContainer, snapshotId.getUUID()); + indexShardSnapshotFormat.delete(blobContainer, snapshotId.getUUID()); } catch (IOException e) { logger.debug("[{}] [{}] failed to delete shard snapshot file", shardId, snapshotId); } @@ -995,7 +988,7 @@ public void delete() { */ BlobStoreIndexShardSnapshot loadSnapshot() { try { - return indexShardSnapshotFormat(version).read(blobContainer, snapshotId.getUUID()); + return indexShardSnapshotFormat.read(blobContainer, snapshotId.getUUID()); } catch (IOException ex) { throw new SnapshotException(metadata.name(), snapshotId, "failed to read shard snapshot file for " + shardId, ex); } @@ -1176,7 +1169,7 @@ private class SnapshotContext extends Context { * @param snapshotStatus snapshot status to report progress */ SnapshotContext(Store store, SnapshotId snapshotId, IndexId indexId, IndexShardSnapshotStatus snapshotStatus, long startTime) { - super(snapshotId, Version.CURRENT, indexId, store.shardId()); + super(snapshotId, indexId, store.shardId()); this.snapshotStatus = snapshotStatus; this.store = store; this.startTime = startTime; @@ -1316,7 +1309,6 @@ public void snapshot(final IndexCommit snapshotIndexCommit) { // finalize the snapshot and rewrite the snapshot index with the next sequential snapshot index finalize(newSnapshotsList, fileListGeneration + 1, blobs, "snapshot creation [" + snapshotId + "]"); snapshotStatus.moveToDone(System.currentTimeMillis()); - } /** @@ -1479,8 +1471,8 @@ private class RestoreContext extends Context { * @param snapshotShardId shard in the snapshot that data should be restored from * @param recoveryState recovery state to report progress */ - RestoreContext(IndexShard shard, SnapshotId snapshotId, Version version, IndexId indexId, ShardId snapshotShardId, RecoveryState recoveryState) { - super(snapshotId, version, indexId, shard.shardId(), snapshotShardId); + RestoreContext(IndexShard shard, SnapshotId snapshotId, IndexId indexId, ShardId snapshotShardId, RecoveryState recoveryState) { + super(snapshotId, indexId, shard.shardId(), snapshotShardId); this.recoveryState = recoveryState; this.targetShard = shard; } From ef3b87befb29f00759cc8c226fab610bdba34c09 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Mon, 3 Dec 2018 11:21:42 +0100 Subject: [PATCH 072/115] MINOR: Some Cleanups around Store (#36139) * Moved method `canOpenIndex` is only used in tests -> moved to test CP * Simplify `org.elasticsearch.index.store.Store#renameTempFilesSafe` * Delete some dead methods --- .../org/elasticsearch/index/store/Store.java | 60 ++++--------------- .../index/shard/IndexShardTests.java | 3 +- .../index/shard/ShardUtilsTests.java | 5 -- .../elasticsearch/index/store/StoreTests.java | 6 +- .../elasticsearch/index/store/StoreUtils.java | 46 ++++++++++++++ 5 files changed, 64 insertions(+), 56 deletions(-) create mode 100644 server/src/test/java/org/elasticsearch/index/store/StoreUtils.java diff --git a/server/src/main/java/org/elasticsearch/index/store/Store.java b/server/src/main/java/org/elasticsearch/index/store/Store.java index 4e06838d0cd4f..e1457486f9f0c 100644 --- a/server/src/main/java/org/elasticsearch/index/store/Store.java +++ b/server/src/main/java/org/elasticsearch/index/store/Store.java @@ -91,7 +91,6 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; -import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.List; @@ -300,21 +299,18 @@ public MetadataSnapshot getMetadata(IndexCommit commit, boolean lockDirectory) t public void renameTempFilesSafe(Map tempFileMap) throws IOException { // this works just like a lucene commit - we rename all temp files and once we successfully // renamed all the segments we rename the commit to ensure we don't leave half baked commits behind. - final Map.Entry[] entries = tempFileMap.entrySet().toArray(new Map.Entry[tempFileMap.size()]); - ArrayUtil.timSort(entries, new Comparator>() { - @Override - public int compare(Map.Entry o1, Map.Entry o2) { - String left = o1.getValue(); - String right = o2.getValue(); - if (left.startsWith(IndexFileNames.SEGMENTS) || right.startsWith(IndexFileNames.SEGMENTS)) { - if (left.startsWith(IndexFileNames.SEGMENTS) == false) { - return -1; - } else if (right.startsWith(IndexFileNames.SEGMENTS) == false) { - return 1; - } + final Map.Entry[] entries = tempFileMap.entrySet().toArray(new Map.Entry[0]); + ArrayUtil.timSort(entries, (o1, o2) -> { + String left = o1.getValue(); + String right = o2.getValue(); + if (left.startsWith(IndexFileNames.SEGMENTS) || right.startsWith(IndexFileNames.SEGMENTS)) { + if (left.startsWith(IndexFileNames.SEGMENTS) == false) { + return -1; + } else if (right.startsWith(IndexFileNames.SEGMENTS) == false) { + return 1; } - return left.compareTo(right); } + return left.compareTo(right); }); metadataLock.writeLock().lock(); // we make sure that nobody fetches the metadata while we do this rename operation here to ensure we don't @@ -454,22 +450,6 @@ public static MetadataSnapshot readMetadataSnapshot(Path indexLocation, ShardId return MetadataSnapshot.EMPTY; } - /** - * Returns true iff the given location contains an index an the index - * can be successfully opened. This includes reading the segment infos and possible - * corruption markers. - */ - public static boolean canOpenIndex(Logger logger, Path indexLocation, - ShardId shardId, NodeEnvironment.ShardLocker shardLocker) throws IOException { - try { - tryOpenIndex(indexLocation, shardId, shardLocker, logger); - } catch (Exception ex) { - logger.trace(() -> new ParameterizedMessage("Can't open index for path [{}]", indexLocation), ex); - return false; - } - return true; - } - /** * Tries to open an index for the given location. This includes reading the * segment infos and possible corruption markers. If the index can not @@ -961,7 +941,6 @@ public Map asMap() { private static final String DEL_FILE_EXTENSION = "del"; // legacy delete file private static final String LIV_FILE_EXTENSION = "liv"; // lucene 5 delete file - private static final String FIELD_INFOS_FILE_EXTENSION = "fnm"; private static final String SEGMENT_INFO_EXTENSION = "si"; /** @@ -1015,12 +994,7 @@ public RecoveryDiff recoveryDiff(MetadataSnapshot recoveryTargetSnapshot) { // only treat del files as per-commit files fnm files are generational but only for upgradable DV perCommitStoreFiles.add(meta); } else { - List perSegStoreFiles = perSegment.get(segmentId); - if (perSegStoreFiles == null) { - perSegStoreFiles = new ArrayList<>(); - perSegment.put(segmentId, perSegStoreFiles); - } - perSegStoreFiles.add(meta); + perSegment.computeIfAbsent(segmentId, k -> new ArrayList<>()).add(meta); } } final ArrayList identicalFiles = new ArrayList<>(); @@ -1072,13 +1046,6 @@ public String getHistoryUUID() { return commitUserData.get(Engine.HISTORY_UUID_KEY); } - /** - * returns the translog uuid the store points at - */ - public String getTranslogUUID() { - return commitUserData.get(Translog.TRANSLOG_UUID_KEY); - } - /** * Returns true iff this metadata contains the given file. */ @@ -1626,15 +1593,14 @@ public void trimUnsafeCommits(final long lastSyncedGlobalCheckpoint, final long } } - - private void updateCommitData(IndexWriter writer, Map keysToUpdate) throws IOException { + private static void updateCommitData(IndexWriter writer, Map keysToUpdate) throws IOException { final Map userData = getUserData(writer); userData.putAll(keysToUpdate); writer.setLiveCommitData(userData.entrySet()); writer.commit(); } - private Map getUserData(IndexWriter writer) { + private static Map getUserData(IndexWriter writer) { final Map userData = new HashMap<>(); writer.getLiveCommitData().forEach(e -> userData.put(e.getKey(), e.getValue())); return userData; diff --git a/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java b/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java index fa2c3f55df32a..3e9dbce081802 100644 --- a/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java +++ b/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java @@ -106,6 +106,7 @@ import org.elasticsearch.index.snapshots.IndexShardSnapshotStatus; import org.elasticsearch.index.store.Store; import org.elasticsearch.index.store.StoreStats; +import org.elasticsearch.index.store.StoreUtils; import org.elasticsearch.index.translog.TestTranslog; import org.elasticsearch.index.translog.Translog; import org.elasticsearch.index.translog.TranslogStats; @@ -261,7 +262,7 @@ public void testFailShard() throws Exception { ShardStateMetaData shardStateMetaData = load(logger, shardPath.getShardStatePath()); assertEquals(shardStateMetaData, getShardStateMetadata(shard)); // but index can't be opened for a failed shard - assertThat("store index should be corrupted", Store.canOpenIndex(logger, shardPath.resolveIndex(), shard.shardId(), + assertThat("store index should be corrupted", StoreUtils.canOpenIndex(logger, shardPath.resolveIndex(), shard.shardId(), (shardId, lockTimeoutMS) -> new DummyShardLock(shardId)), equalTo(false)); } diff --git a/server/src/test/java/org/elasticsearch/index/shard/ShardUtilsTests.java b/server/src/test/java/org/elasticsearch/index/shard/ShardUtilsTests.java index 7e9c7b901a212..8801f8bf5d260 100644 --- a/server/src/test/java/org/elasticsearch/index/shard/ShardUtilsTests.java +++ b/server/src/test/java/org/elasticsearch/index/shard/ShardUtilsTests.java @@ -28,7 +28,6 @@ import org.apache.lucene.store.BaseDirectoryWrapper; import org.elasticsearch.core.internal.io.IOUtils; import org.elasticsearch.common.lucene.index.ElasticsearchDirectoryReader; -import org.elasticsearch.index.engine.Engine; import org.elasticsearch.test.ESTestCase; import java.io.IOException; @@ -64,8 +63,4 @@ public void testExtractShardId() throws IOException { } IOUtils.close(writer, dir); } - - public static Engine getShardEngine(IndexShard shard) { - return shard.getEngine(); - } } diff --git a/server/src/test/java/org/elasticsearch/index/store/StoreTests.java b/server/src/test/java/org/elasticsearch/index/store/StoreTests.java index 5ff6a9b7b1007..566203d1f85c7 100644 --- a/server/src/test/java/org/elasticsearch/index/store/StoreTests.java +++ b/server/src/test/java/org/elasticsearch/index/store/StoreTests.java @@ -943,17 +943,17 @@ public void testCanOpenIndex() throws IOException { IndexWriterConfig iwc = newIndexWriterConfig(); Path tempDir = createTempDir(); final BaseDirectoryWrapper dir = newFSDirectory(tempDir); - assertFalse(Store.canOpenIndex(logger, tempDir, shardId, (id, l) -> new DummyShardLock(id))); + assertFalse(StoreUtils.canOpenIndex(logger, tempDir, shardId, (id, l) -> new DummyShardLock(id))); IndexWriter writer = new IndexWriter(dir, iwc); Document doc = new Document(); doc.add(new StringField("id", "1", random().nextBoolean() ? Field.Store.YES : Field.Store.NO)); writer.addDocument(doc); writer.commit(); writer.close(); - assertTrue(Store.canOpenIndex(logger, tempDir, shardId, (id, l) -> new DummyShardLock(id))); + assertTrue(StoreUtils.canOpenIndex(logger, tempDir, shardId, (id, l) -> new DummyShardLock(id))); Store store = new Store(shardId, INDEX_SETTINGS, dir, new DummyShardLock(shardId)); store.markStoreCorrupted(new CorruptIndexException("foo", "bar")); - assertFalse(Store.canOpenIndex(logger, tempDir, shardId, (id, l) -> new DummyShardLock(id))); + assertFalse(StoreUtils.canOpenIndex(logger, tempDir, shardId, (id, l) -> new DummyShardLock(id))); store.close(); } diff --git a/server/src/test/java/org/elasticsearch/index/store/StoreUtils.java b/server/src/test/java/org/elasticsearch/index/store/StoreUtils.java new file mode 100644 index 0000000000000..31732612e03ae --- /dev/null +++ b/server/src/test/java/org/elasticsearch/index/store/StoreUtils.java @@ -0,0 +1,46 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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.elasticsearch.index.store; + +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.message.ParameterizedMessage; +import org.elasticsearch.env.NodeEnvironment; +import org.elasticsearch.index.shard.ShardId; + +import java.nio.file.Path; + +public final class StoreUtils { + + /** + * Returns {@code true} iff the given location contains an index an the index + * can be successfully opened. This includes reading the segment infos and possible + * corruption markers. + */ + public static boolean canOpenIndex(Logger logger, Path indexLocation, + ShardId shardId, NodeEnvironment.ShardLocker shardLocker) { + try { + Store.tryOpenIndex(indexLocation, shardId, shardLocker, logger); + } catch (Exception ex) { + logger.trace(() -> new ParameterizedMessage("Can't open index for path [{}]", indexLocation), ex); + return false; + } + return true; + } +} From 94556994d00653193417c973198dae63fa75d781 Mon Sep 17 00:00:00 2001 From: Boaz Leskes Date: Mon, 3 Dec 2018 13:45:19 +0100 Subject: [PATCH 073/115] Disable merges in testReuseInFileBasedPeerRecovery The test assumes lucene files don't change. Closes #35772 --- .../elasticsearch/gateway/RecoveryFromGatewayIT.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/server/src/test/java/org/elasticsearch/gateway/RecoveryFromGatewayIT.java b/server/src/test/java/org/elasticsearch/gateway/RecoveryFromGatewayIT.java index a73b0dbe09086..53629b59fad96 100644 --- a/server/src/test/java/org/elasticsearch/gateway/RecoveryFromGatewayIT.java +++ b/server/src/test/java/org/elasticsearch/gateway/RecoveryFromGatewayIT.java @@ -35,6 +35,7 @@ import org.elasticsearch.env.NodeEnvironment; import org.elasticsearch.index.Index; import org.elasticsearch.index.IndexSettings; +import org.elasticsearch.index.MergePolicyConfig; import org.elasticsearch.index.engine.Engine; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.index.shard.ShardId; @@ -45,6 +46,7 @@ import org.elasticsearch.test.ESIntegTestCase; import org.elasticsearch.test.ESIntegTestCase.ClusterScope; import org.elasticsearch.test.ESIntegTestCase.Scope; +import org.elasticsearch.test.InternalSettingsPlugin; import org.elasticsearch.test.InternalTestCluster; import org.elasticsearch.test.InternalTestCluster.RestartCallback; import org.elasticsearch.test.store.MockFSIndexStore; @@ -80,7 +82,7 @@ public class RecoveryFromGatewayIT extends ESIntegTestCase { @Override protected Collection> nodePlugins() { - return Arrays.asList(MockFSIndexStore.TestPlugin.class); + return Arrays.asList(MockFSIndexStore.TestPlugin.class, InternalSettingsPlugin.class); } public void testOneNodeRecoverFromGateway() throws Exception { @@ -403,8 +405,12 @@ public void testReuseInFileBasedPeerRecovery() throws Exception { .admin() .indices() .prepareCreate("test") - .setSettings(Settings.builder().put("number_of_shards", 1).put("number_of_replicas", 1)) - .get(); + .setSettings(Settings.builder() + .put("number_of_shards", 1) + .put("number_of_replicas", 1) + // disable merges to keep segments the same + .put(MergePolicyConfig.INDEX_MERGE_ENABLED, "false") + ).get(); logger.info("--> indexing docs"); int numDocs = randomIntBetween(1, 1024); From cef30247c092d8bbc14d416f707a69102c4f85f3 Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Mon, 3 Dec 2018 08:55:48 -0500 Subject: [PATCH 074/115] TEST: Adjust min_retained_seq_no expectation min_retained_seq_no is non-negative, however, if the number of retained operations is greater than 0, then the expectation may be negative. --- .../engine/CombinedDeletionPolicyTests.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/server/src/test/java/org/elasticsearch/index/engine/CombinedDeletionPolicyTests.java b/server/src/test/java/org/elasticsearch/index/engine/CombinedDeletionPolicyTests.java index d74b9b41a8867..76c71240bdb7e 100644 --- a/server/src/test/java/org/elasticsearch/index/engine/CombinedDeletionPolicyTests.java +++ b/server/src/test/java/org/elasticsearch/index/engine/CombinedDeletionPolicyTests.java @@ -90,8 +90,8 @@ public void testKeepCommitsAfterGlobalCheckpoint() throws Exception { } assertThat(translogPolicy.getMinTranslogGenerationForRecovery(), equalTo(translogGenList.get(keptIndex))); assertThat(translogPolicy.getTranslogGenerationOfLastCommit(), equalTo(lastTranslogGen)); - assertThat(softDeletesPolicy.getMinRetainedSeqNo(), - equalTo(Math.min(getLocalCheckpoint(commitList.get(keptIndex)) + 1, globalCheckpoint.get() + 1 - extraRetainedOps))); + assertThat(softDeletesPolicy.getMinRetainedSeqNo(), equalTo( + Math.max(0, Math.min(getLocalCheckpoint(commitList.get(keptIndex)) + 1, globalCheckpoint.get() + 1 - extraRetainedOps)))); } public void testAcquireIndexCommit() throws Exception { @@ -126,8 +126,8 @@ public void testAcquireIndexCommit() throws Exception { commitList.forEach(this::resetDeletion); indexPolicy.onCommit(commitList); IndexCommit safeCommit = CombinedDeletionPolicy.findSafeCommitPoint(commitList, globalCheckpoint.get()); - assertThat(softDeletesPolicy.getMinRetainedSeqNo(), - equalTo(Math.min(getLocalCheckpoint(safeCommit) + 1, globalCheckpoint.get() + 1 - extraRetainedOps))); + assertThat(softDeletesPolicy.getMinRetainedSeqNo(), equalTo( + Math.max(0, Math.min(getLocalCheckpoint(safeCommit) + 1, globalCheckpoint.get() + 1 - extraRetainedOps)))); // Captures and releases some commits int captures = between(0, 5); for (int n = 0; n < captures; n++) { @@ -157,8 +157,8 @@ public void testAcquireIndexCommit() throws Exception { equalTo(Long.parseLong(commitList.get(safeIndex).getUserData().get(Translog.TRANSLOG_GENERATION_KEY)))); assertThat(translogPolicy.getTranslogGenerationOfLastCommit(), equalTo(Long.parseLong(commitList.get(commitList.size() - 1).getUserData().get(Translog.TRANSLOG_GENERATION_KEY)))); - assertThat(softDeletesPolicy.getMinRetainedSeqNo(), - equalTo(Math.min(getLocalCheckpoint(commitList.get(safeIndex)) + 1, globalCheckpoint.get() + 1 - extraRetainedOps))); + assertThat(softDeletesPolicy.getMinRetainedSeqNo(), equalTo( + Math.max(0, Math.min(getLocalCheckpoint(commitList.get(safeIndex)) + 1, globalCheckpoint.get() + 1 - extraRetainedOps)))); } snapshottingCommits.forEach(indexPolicy::releaseCommit); globalCheckpoint.set(randomLongBetween(lastMaxSeqNo, Long.MAX_VALUE)); @@ -171,8 +171,8 @@ public void testAcquireIndexCommit() throws Exception { assertThat(translogPolicy.getMinTranslogGenerationForRecovery(), equalTo(lastTranslogGen)); assertThat(translogPolicy.getTranslogGenerationOfLastCommit(), equalTo(lastTranslogGen)); IndexCommit safeCommit = CombinedDeletionPolicy.findSafeCommitPoint(commitList, globalCheckpoint.get()); - assertThat(softDeletesPolicy.getMinRetainedSeqNo(), - equalTo(Math.min(getLocalCheckpoint(safeCommit) + 1, globalCheckpoint.get() + 1 - extraRetainedOps))); + assertThat(softDeletesPolicy.getMinRetainedSeqNo(), equalTo( + Math.max(0, Math.min(getLocalCheckpoint(safeCommit) + 1, globalCheckpoint.get() + 1 - extraRetainedOps)))); } public void testLegacyIndex() throws Exception { From 16cdff24832be0850c1c44548a20df41c7db6a14 Mon Sep 17 00:00:00 2001 From: Alan Woodward Date: Mon, 3 Dec 2018 17:34:15 +0000 Subject: [PATCH 075/115] Improve painless docs for score, similarity, weight and sort (#35629) --- .../painless-score-context.asciidoc | 37 +++++++++++++++-- .../painless-similarity-context.asciidoc | 16 +++++++- .../painless-sort-context.asciidoc | 41 +++++++++++++++++-- .../painless-weight-context.asciidoc | 7 +++- 4 files changed, 92 insertions(+), 9 deletions(-) diff --git a/docs/painless/painless-contexts/painless-score-context.asciidoc b/docs/painless/painless-contexts/painless-score-context.asciidoc index bd1e1de7f777d..2bec9021c1720 100644 --- a/docs/painless/painless-contexts/painless-score-context.asciidoc +++ b/docs/painless/painless-contexts/painless-score-context.asciidoc @@ -11,8 +11,10 @@ score to documents returned from a query. User-defined parameters passed in as part of the query. `doc` (`Map`, read-only):: - Contains the fields of the current document where each field is a - `List` of values. + Contains the fields of the current document. For single-valued fields, + the value can be accessed via `doc['fieldname'].value`. For multi-valued + fields, this returns the first value; other values can be accessed + via `doc['fieldname'].get(index)` `_score` (`double` read-only):: The similarity score of the current document. @@ -24,4 +26,33 @@ score to documents returned from a query. *API* -The standard <> is available. \ No newline at end of file +The standard <> is available. + +*Example* + +To run this example, first follow the steps in +<>. + +The following query finds all unsold seats, with lower 'row' values +scored higher. + +[source,js] +-------------------------------------------------- +GET /seats/_search +{ + "query": { + "function_score": { + "query": { + "match": { "sold": "false" } + }, + "script_score" : { + "script" : { + "source": "1.0 / doc['row'].value" + } + } + } + } +} +-------------------------------------------------- +// CONSOLE +// TEST[setup:seats] \ No newline at end of file diff --git a/docs/painless/painless-contexts/painless-similarity-context.asciidoc b/docs/painless/painless-contexts/painless-similarity-context.asciidoc index 53b37be52b6d7..9a8e59350d1a8 100644 --- a/docs/painless/painless-contexts/painless-similarity-context.asciidoc +++ b/docs/painless/painless-contexts/painless-similarity-context.asciidoc @@ -15,6 +15,9 @@ documents in a query. `params` (`Map`, read-only):: User-defined parameters passed in at query-time. +`weight` (`float`, read-only):: + The weight as calculated by a {ref}/painless-weight-context[weight script] + `query.boost` (`float`, read-only):: The boost value if provided by the query. If this is not provided the value is `1.0f`. @@ -37,12 +40,23 @@ documents in a query. The total occurrences of the current term in the index. `doc.length` (`long`, read-only):: - The number of tokens the current document has in the current field. + The number of tokens the current document has in the current field. This + is decoded from the stored {ref}/norms[norms] and may be approximate for + long fields `doc.freq` (`long`, read-only):: The number of occurrences of the current term in the current document for the current field. +Note that the `query`, `field`, and `term` variables are also available to the +{ref}/painless-weight-context[weight context]. They are more efficiently used +there, as they are constant for all documents. + +For queries that contain multiple terms, the script is called once for each +term with that term's calculated weight, and the results are summed. Note that some +terms might have a `doc.freq` value of `0` on a document, for example if a query +uses synonyms. + *Return* `double`:: diff --git a/docs/painless/painless-contexts/painless-sort-context.asciidoc b/docs/painless/painless-contexts/painless-sort-context.asciidoc index 9efd507668839..64c17ad07a664 100644 --- a/docs/painless/painless-contexts/painless-sort-context.asciidoc +++ b/docs/painless/painless-contexts/painless-sort-context.asciidoc @@ -10,8 +10,10 @@ Use a Painless script to User-defined parameters passed in as part of the query. `doc` (`Map`, read-only):: - Contains the fields of the current document where each field is a - `List` of values. + Contains the fields of the current document. For single-valued fields, + the value can be accessed via `doc['fieldname'].value`. For multi-valued + fields, this returns the first value; other values can be accessed + via `doc['fieldname'].get(index)` `_score` (`double` read-only):: The similarity score of the current document. @@ -23,4 +25,37 @@ Use a Painless script to *API* -The standard <> is available. \ No newline at end of file +The standard <> is available. + +*Example* + +To run this example, first follow the steps in +<>. + +To sort results by the length of the `theatre` field, submit the following query: + +[source,js] +---- +GET /_search +{ + "query" : { + "term" : { "sold" : "true" } + }, + "sort" : { + "_script" : { + "type" : "number", + "script" : { + "lang": "painless", + "source": "doc['theatre'].value.length() * params.factor", + "params" : { + "factor" : 1.1 + } + }, + "order" : "asc" + } + } +} + +---- +// CONSOLE +// TEST[setup:seats] \ No newline at end of file diff --git a/docs/painless/painless-contexts/painless-weight-context.asciidoc b/docs/painless/painless-contexts/painless-weight-context.asciidoc index ad215d5386b05..319b7999aa831 100644 --- a/docs/painless/painless-contexts/painless-weight-context.asciidoc +++ b/docs/painless/painless-contexts/painless-weight-context.asciidoc @@ -3,8 +3,11 @@ Use a Painless script to create a {ref}/index-modules-similarity.html[weight] for use in a -<>. Weight is used to prevent -recalculation of constants that remain the same across documents. +<>. The weight makes up the +part of the similarity calculation that is independent of the document being +scored, and so can be built up front and cached. + +Queries that contain multiple terms calculate a separate weight for each term. *Variables* From 26e679cfd9b9c407a707685e521bc95e6ba7b58c Mon Sep 17 00:00:00 2001 From: Luca Cavanna Date: Tue, 27 Nov 2018 19:04:44 +0100 Subject: [PATCH 076/115] Remove fromXContent from IndexUpgradeInfoResponse (#35934) Such method is there only for testing purposes, it is not needed. --- .../migration/IndexUpgradeInfoResponse.java | 40 ------------------- .../IndexUpgradeInfoResponseTests.java | 8 +--- 2 files changed, 1 insertion(+), 47 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/migration/IndexUpgradeInfoResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/migration/IndexUpgradeInfoResponse.java index 17115ac9b1711..d5769e419ce66 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/migration/IndexUpgradeInfoResponse.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/protocol/xpack/migration/IndexUpgradeInfoResponse.java @@ -9,54 +9,18 @@ import org.elasticsearch.common.ParseField; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; -import org.elasticsearch.common.xcontent.ConstructingObjectParser; import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; -import org.elasticsearch.common.xcontent.XContentParser; import java.io.IOException; import java.util.Map; import java.util.Objects; -import java.util.stream.Collectors; - -import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; public class IndexUpgradeInfoResponse extends ActionResponse implements ToXContentObject { private static final ParseField INDICES = new ParseField("indices"); private static final ParseField ACTION_REQUIRED = new ParseField("action_required"); - private static final ConstructingObjectParser PARSER = - new ConstructingObjectParser<>("IndexUpgradeInfoResponse", - true, - (a, c) -> { - @SuppressWarnings("unchecked") - Map map = (Map)a[0]; - Map actionsRequired = map.entrySet().stream() - .filter(e -> { - if (e.getValue() instanceof Map == false) { - return false; - } - @SuppressWarnings("unchecked") - Map value =(Map)e.getValue(); - return value.containsKey(ACTION_REQUIRED.getPreferredName()); - }) - .collect(Collectors.toMap( - Map.Entry::getKey, - e -> { - @SuppressWarnings("unchecked") - Map value = (Map) e.getValue(); - return UpgradeActionRequired.fromString((String)value.get(ACTION_REQUIRED.getPreferredName())); - } - )); - return new IndexUpgradeInfoResponse(actionsRequired); - }); - - static { - PARSER.declareObject(constructorArg(), (p, c) -> p.map(), INDICES); - } - - private Map actions; public IndexUpgradeInfoResponse() { @@ -113,8 +77,4 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(actions); } - - public static IndexUpgradeInfoResponse fromXContent(XContentParser parser) { - return PARSER.apply(parser, null); - } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/migration/IndexUpgradeInfoResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/migration/IndexUpgradeInfoResponseTests.java index 76f00ebb24309..77ad986f0c355 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/migration/IndexUpgradeInfoResponseTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/protocol/xpack/migration/IndexUpgradeInfoResponseTests.java @@ -8,7 +8,6 @@ import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.protocol.AbstractHlrcStreamableXContentTestCase; -import java.io.IOException; import java.util.AbstractMap; import java.util.HashMap; import java.util.Iterator; @@ -19,12 +18,7 @@ public class IndexUpgradeInfoResponseTests extends AbstractHlrcStreamableXContentTestCase { @Override - protected IndexUpgradeInfoResponse doParseInstance(XContentParser parser) { - return IndexUpgradeInfoResponse.fromXContent(parser); - } - - @Override - public org.elasticsearch.client.migration.IndexUpgradeInfoResponse doHlrcParseInstance(XContentParser parser) throws IOException { + public org.elasticsearch.client.migration.IndexUpgradeInfoResponse doHlrcParseInstance(XContentParser parser) { return org.elasticsearch.client.migration.IndexUpgradeInfoResponse.fromXContent(parser); } From f70f54acc98b88290c16f903b4c8c9ada6cee159 Mon Sep 17 00:00:00 2001 From: Luca Cavanna Date: Wed, 28 Nov 2018 20:06:40 +0100 Subject: [PATCH 077/115] Increase InternalHistogramTests coverage (#36004) In `InternalHistogramTests` we were randomizing different values but `minDocCount` was hardcoded to `1`. It's important to test other values, especially `0` as it's the default. To make this possible, the test needed some adapting in the way buckets are randomly generated: all aggs need to share the same `interval`, `minDocCount` and `emptyBucketInfo`. Also assertions need to take into account that more (or less) buckets are expected depending on `minDocCount`. This was originated by #35921 and its need to test adding empty buckets as part of the reduce phase. Also relates to #26856 as one more key comparison needed to use `Double.compare` to properly handle `NaN` values, which was triggered by the increased test coverage. --- .../bucket/histogram/InternalHistogram.java | 6 +- .../histogram/InternalHistogramTests.java | 69 ++++++++++++++++--- 2 files changed, 63 insertions(+), 12 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java index 1831e012a318c..d26ac47c9ea25 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java @@ -213,7 +213,7 @@ public int hashCode() { private final DocValueFormat format; private final boolean keyed; private final long minDocCount; - private final EmptyBucketInfo emptyBucketInfo; + final EmptyBucketInfo emptyBucketInfo; InternalHistogram(String name, List buckets, BucketOrder order, long minDocCount, EmptyBucketInfo emptyBucketInfo, DocValueFormat formatter, boolean keyed, List pipelineAggregators, @@ -302,7 +302,7 @@ private List reduceBuckets(List aggregations, Reduc final PriorityQueue pq = new PriorityQueue(aggregations.size()) { @Override protected boolean lessThan(IteratorAndCurrent a, IteratorAndCurrent b) { - return a.current.key < b.current.key; + return Double.compare(a.current.key, b.current.key) < 0; } }; for (InternalAggregation aggregation : aggregations) { @@ -405,7 +405,7 @@ private void addEmptyBuckets(List list, ReduceContext reduceContext) { iter.add(new Bucket(key, 0, keyed, format, reducedEmptySubAggs)); key = nextKey(key); } - assert key == nextBucket.key; + assert key == nextBucket.key || Double.isNaN(nextBucket.key) : "key: " + key + ", nextBucket.key: " + nextBucket.key; } lastBucket = iter.next(); } while (iter.hasNext()); diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogramTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogramTests.java index ca2750d5e4105..341142afed71a 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogramTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogramTests.java @@ -25,9 +25,9 @@ import org.elasticsearch.search.aggregations.BucketOrder; import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.aggregations.InternalAggregations; -import org.elasticsearch.test.InternalMultiBucketAggregationTestCase; import org.elasticsearch.search.aggregations.ParsedMultiBucketAggregation; import org.elasticsearch.search.aggregations.pipeline.PipelineAggregator; +import org.elasticsearch.test.InternalMultiBucketAggregationTestCase; import java.util.ArrayList; import java.util.Arrays; @@ -40,12 +40,36 @@ public class InternalHistogramTests extends InternalMultiBucketAggregationTestCa private boolean keyed; private DocValueFormat format; + private int interval; + private int minDocCount; + private InternalHistogram.EmptyBucketInfo emptyBucketInfo; + private int offset; @Override - public void setUp() throws Exception{ + public void setUp() throws Exception { super.setUp(); keyed = randomBoolean(); format = randomNumericDocValueFormat(); + //in order for reduction to work properly (and be realistic) we need to use the same interval, minDocCount, emptyBucketInfo + //and offset in all randomly created aggs as part of the same test run. This is particularly important when minDocCount is + //set to 0 as empty buckets need to be added to fill the holes. + interval = randomIntBetween(1, 3); + offset = randomIntBetween(0, 3); + if (randomBoolean()) { + minDocCount = randomIntBetween(1, 10); + emptyBucketInfo = null; + } else { + minDocCount = 0; + //it's ok if minBound and maxBound are outside the range of the generated buckets, that will just mean that + //empty buckets won't be added before the first bucket and/or after the last one + int minBound = randomInt(50) - 30; + int maxBound = randomNumberOfBuckets() * interval + randomIntBetween(0, 10); + emptyBucketInfo = new InternalHistogram.EmptyBucketInfo(interval, offset, minBound, maxBound, InternalAggregations.EMPTY); + } + } + + private double round(double key) { + return Math.floor((key - offset) / interval) * interval + offset; } @Override @@ -53,16 +77,18 @@ protected InternalHistogram createTestInstance(String name, List pipelineAggregators, Map metaData, InternalAggregations aggregations) { - final int base = randomInt(50) - 30; + final double base = round(randomInt(50) - 30); final int numBuckets = randomNumberOfBuckets(); - final int interval = randomIntBetween(1, 3); List buckets = new ArrayList<>(); for (int i = 0; i < numBuckets; ++i) { - final int docCount = TestUtil.nextInt(random(), 1, 50); - buckets.add(new InternalHistogram.Bucket(base + i * interval, docCount, keyed, format, aggregations)); + //rarely leave some holes to be filled up with empty buckets in case minDocCount is set to 0 + if (frequently()) { + final int docCount = TestUtil.nextInt(random(), 1, 50); + buckets.add(new InternalHistogram.Bucket(base + i * interval, docCount, keyed, format, aggregations)); + } } BucketOrder order = BucketOrder.key(randomBoolean()); - return new InternalHistogram(name, buckets, order, 1, null, format, keyed, pipelineAggregators, metaData); + return new InternalHistogram(name, buckets, order, minDocCount, emptyBucketInfo, format, keyed, pipelineAggregators, metaData); } // issue 26787 @@ -88,13 +114,36 @@ public void testHandlesNaN() { @Override protected void assertReduced(InternalHistogram reduced, List inputs) { - Map expectedCounts = new TreeMap<>(); + TreeMap expectedCounts = new TreeMap<>(); for (Histogram histogram : inputs) { for (Histogram.Bucket bucket : histogram.getBuckets()) { expectedCounts.compute((Double) bucket.getKey(), (key, oldValue) -> (oldValue == null ? 0 : oldValue) + bucket.getDocCount()); } } + if (minDocCount == 0) { + double minBound = round(emptyBucketInfo.minBound); + if (expectedCounts.isEmpty() && emptyBucketInfo.minBound < emptyBucketInfo.maxBound) { + expectedCounts.put(minBound, 0L); + } + if (expectedCounts.isEmpty() == false) { + Double nextKey = expectedCounts.firstKey(); + while (nextKey < expectedCounts.lastKey()) { + expectedCounts.putIfAbsent(nextKey, 0L); + nextKey += interval; + } + while (minBound < expectedCounts.firstKey()) { + expectedCounts.put(expectedCounts.firstKey() - interval, 0L); + } + double maxBound = round(emptyBucketInfo.maxBound); + while (expectedCounts.lastKey() < maxBound) { + expectedCounts.put(expectedCounts.lastKey() + interval, 0L); + } + } + } else { + expectedCounts.entrySet().removeIf(doubleLongEntry -> doubleLongEntry.getValue() < minDocCount); + } + Map actualCounts = new TreeMap<>(); for (Histogram.Bucket bucket : reduced.getBuckets()) { actualCounts.compute((Double) bucket.getKey(), @@ -121,6 +170,7 @@ protected InternalHistogram mutateInstance(InternalHistogram instance) { long minDocCount = instance.getMinDocCount(); List pipelineAggregators = instance.pipelineAggregators(); Map metaData = instance.getMetaData(); + InternalHistogram.EmptyBucketInfo emptyBucketInfo = instance.emptyBucketInfo; switch (between(0, 4)) { case 0: name += randomAlphaOfLength(5); @@ -135,6 +185,7 @@ protected InternalHistogram mutateInstance(InternalHistogram instance) { break; case 3: minDocCount += between(1, 10); + emptyBucketInfo = null; break; case 4: if (metaData == null) { @@ -147,6 +198,6 @@ protected InternalHistogram mutateInstance(InternalHistogram instance) { default: throw new AssertionError("Illegal randomisation branch"); } - return new InternalHistogram(name, buckets, order, minDocCount, null, format, keyed, pipelineAggregators, metaData); + return new InternalHistogram(name, buckets, order, minDocCount, emptyBucketInfo, format, keyed, pipelineAggregators, metaData); } } From feed66f7abb2e40f2a04f3da9d78f24f4acc3b82 Mon Sep 17 00:00:00 2001 From: Luca Cavanna Date: Fri, 30 Nov 2018 11:21:46 +0100 Subject: [PATCH 078/115] [TEST] Increase InternalDateHistogramTests coverage (#36064) In this test we were randomizing different values but minDocCount was hardcoded to 1. It's important to test other values, especially `0` as it's the default. The test needed some adapting in the way buckets are randomly generated: all aggs need to share the same interval, minDocCount and emptyBucketInfo. Also assertions need to take into account that more (or less) buckets are expected depending on minDocCount. --- .../histogram/InternalDateHistogram.java | 2 +- .../histogram/InternalDateHistogramTests.java | 85 ++++++++++++++++--- .../histogram/InternalHistogramTests.java | 2 +- 3 files changed, 74 insertions(+), 15 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalDateHistogram.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalDateHistogram.java index 669bda5574d31..5d6ec6f93b732 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalDateHistogram.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalDateHistogram.java @@ -219,7 +219,7 @@ public int hashCode() { private final boolean keyed; private final long minDocCount; private final long offset; - private final EmptyBucketInfo emptyBucketInfo; + final EmptyBucketInfo emptyBucketInfo; InternalDateHistogram(String name, List buckets, BucketOrder order, long minDocCount, long offset, EmptyBucketInfo emptyBucketInfo, diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalDateHistogramTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalDateHistogramTests.java index b2b7079815ea9..53eb7948c4ebf 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalDateHistogramTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalDateHistogramTests.java @@ -20,12 +20,14 @@ package org.elasticsearch.search.aggregations.bucket.histogram; import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.rounding.Rounding; +import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.search.DocValueFormat; import org.elasticsearch.search.aggregations.BucketOrder; import org.elasticsearch.search.aggregations.InternalAggregations; -import org.elasticsearch.test.InternalMultiBucketAggregationTestCase; import org.elasticsearch.search.aggregations.ParsedMultiBucketAggregation; import org.elasticsearch.search.aggregations.pipeline.PipelineAggregator; +import org.elasticsearch.test.InternalMultiBucketAggregationTestCase; import org.joda.time.DateTime; import java.util.ArrayList; @@ -42,12 +44,38 @@ public class InternalDateHistogramTests extends InternalMultiBucketAggregationTe private boolean keyed; private DocValueFormat format; + private long intervalMillis; + private long baseMillis; + private long minDocCount; + private InternalDateHistogram.EmptyBucketInfo emptyBucketInfo; @Override public void setUp() throws Exception { super.setUp(); keyed = randomBoolean(); format = randomNumericDocValueFormat(); + //in order for reduction to work properly (and be realistic) we need to use the same interval, minDocCount, emptyBucketInfo + //and base in all randomly created aggs as part of the same test run. This is particularly important when minDocCount is + //set to 0 as empty buckets need to be added to fill the holes. + long interval = randomIntBetween(1, 3); + intervalMillis = randomFrom(timeValueSeconds(interval), timeValueMinutes(interval), timeValueHours(interval)).getMillis(); + Rounding rounding = Rounding.builder(TimeValue.timeValueMillis(intervalMillis)).build(); + baseMillis = rounding.round(System.currentTimeMillis()); + if (randomBoolean()) { + minDocCount = randomIntBetween(1, 10); + emptyBucketInfo = null; + } else { + minDocCount = 0; + ExtendedBounds extendedBounds = null; + if (randomBoolean()) { + //it's ok if min and max are outside the range of the generated buckets, that will just mean that + //empty buckets won't be added before the first bucket and/or after the last one + long min = baseMillis - intervalMillis * randomNumberOfBuckets(); + long max = baseMillis + randomNumberOfBuckets() * intervalMillis + randomNumberOfBuckets(); + extendedBounds = new ExtendedBounds(min, max); + } + emptyBucketInfo = new InternalDateHistogram.EmptyBucketInfo(rounding, InternalAggregations.EMPTY, extendedBounds); + } } @Override @@ -57,29 +85,58 @@ protected InternalDateHistogram createTestInstance(String name, InternalAggregations aggregations) { int nbBuckets = randomNumberOfBuckets(); List buckets = new ArrayList<>(nbBuckets); - long startingDate = System.currentTimeMillis(); - - long interval = randomIntBetween(1, 3); - long intervalMillis = randomFrom(timeValueSeconds(interval), timeValueMinutes(interval), timeValueHours(interval)).getMillis(); - + //avoid having different random instance start from exactly the same base + long startingDate = baseMillis - intervalMillis * randomIntBetween(0, 100); for (int i = 0; i < nbBuckets; i++) { - long key = startingDate + (intervalMillis * i); - buckets.add(i, new InternalDateHistogram.Bucket(key, randomIntBetween(1, 100), keyed, format, aggregations)); + //rarely leave some holes to be filled up with empty buckets in case minDocCount is set to 0 + if (frequently()) { + long key = startingDate + intervalMillis * i; + buckets.add(new InternalDateHistogram.Bucket(key, randomIntBetween(1, 100), keyed, format, aggregations)); + } } - - BucketOrder order = randomFrom(BucketOrder.key(true), BucketOrder.key(false)); - return new InternalDateHistogram(name, buckets, order, 1, 0L, null, format, keyed, pipelineAggregators, metaData); + BucketOrder order = BucketOrder.key(randomBoolean()); + return new InternalDateHistogram(name, buckets, order, minDocCount, 0L, emptyBucketInfo, format, keyed, + pipelineAggregators, metaData); } @Override protected void assertReduced(InternalDateHistogram reduced, List inputs) { - Map expectedCounts = new TreeMap<>(); + TreeMap expectedCounts = new TreeMap<>(); for (Histogram histogram : inputs) { for (Histogram.Bucket bucket : histogram.getBuckets()) { expectedCounts.compute(((DateTime) bucket.getKey()).getMillis(), (key, oldValue) -> (oldValue == null ? 0 : oldValue) + bucket.getDocCount()); } } + if (minDocCount == 0) { + long minBound = -1; + long maxBound = -1; + if (emptyBucketInfo.bounds != null) { + minBound = emptyBucketInfo.rounding.round(emptyBucketInfo.bounds.getMin()); + maxBound = emptyBucketInfo.rounding.round(emptyBucketInfo.bounds.getMax()); + if (expectedCounts.isEmpty() && minBound <= maxBound) { + expectedCounts.put(minBound, 0L); + } + } + if (expectedCounts.isEmpty() == false) { + Long nextKey = expectedCounts.firstKey(); + while (nextKey < expectedCounts.lastKey()) { + expectedCounts.putIfAbsent(nextKey, 0L); + nextKey += intervalMillis; + } + if (emptyBucketInfo.bounds != null) { + while (minBound < expectedCounts.firstKey()) { + expectedCounts.put(expectedCounts.firstKey() - intervalMillis, 0L); + } + while (expectedCounts.lastKey() < maxBound) { + expectedCounts.put(expectedCounts.lastKey() + intervalMillis, 0L); + } + } + } + } else { + expectedCounts.entrySet().removeIf(doubleLongEntry -> doubleLongEntry.getValue() < minDocCount); + } + Map actualCounts = new TreeMap<>(); for (Histogram.Bucket bucket : reduced.getBuckets()) { actualCounts.compute(((DateTime) bucket.getKey()).getMillis(), @@ -106,6 +163,7 @@ protected InternalDateHistogram mutateInstance(InternalDateHistogram instance) { long minDocCount = instance.getMinDocCount(); long offset = instance.getOffset(); List pipelineAggregators = instance.pipelineAggregators(); + InternalDateHistogram.EmptyBucketInfo emptyBucketInfo = instance.emptyBucketInfo; Map metaData = instance.getMetaData(); switch (between(0, 5)) { case 0: @@ -121,6 +179,7 @@ protected InternalDateHistogram mutateInstance(InternalDateHistogram instance) { break; case 3: minDocCount += between(1, 10); + emptyBucketInfo = null; break; case 4: offset += between(1, 20); @@ -136,7 +195,7 @@ protected InternalDateHistogram mutateInstance(InternalDateHistogram instance) { default: throw new AssertionError("Illegal randomisation branch"); } - return new InternalDateHistogram(name, buckets, order, minDocCount, offset, null, format, keyed, pipelineAggregators, + return new InternalDateHistogram(name, buckets, order, minDocCount, offset, emptyBucketInfo, format, keyed, pipelineAggregators, metaData); } } diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogramTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogramTests.java index 341142afed71a..fb9f6dd29c73f 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogramTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogramTests.java @@ -123,7 +123,7 @@ protected void assertReduced(InternalHistogram reduced, List } if (minDocCount == 0) { double minBound = round(emptyBucketInfo.minBound); - if (expectedCounts.isEmpty() && emptyBucketInfo.minBound < emptyBucketInfo.maxBound) { + if (expectedCounts.isEmpty() && emptyBucketInfo.minBound <= emptyBucketInfo.maxBound) { expectedCounts.put(minBound, 0L); } if (expectedCounts.isEmpty() == false) { From 4428e9b63de6306c147a0d678db93cf044107dd8 Mon Sep 17 00:00:00 2001 From: Luca Cavanna Date: Fri, 30 Nov 2018 14:15:52 +0100 Subject: [PATCH 079/115] [TEST] Reduce number of buckets created in InternalDateHistogramTests New that we test with min_doc_count set to 0 as well, we may end up generating a lot more buckets. This commit adjusts the min bound and max bound, as well as the offset for each randomly generated agg instance so that we don't end up hitting the 10.000 max buckets limit. Relates to #36064 --- .../bucket/histogram/InternalDateHistogramTests.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalDateHistogramTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalDateHistogramTests.java index 53eb7948c4ebf..dd5d06f8785f7 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalDateHistogramTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalDateHistogramTests.java @@ -71,7 +71,7 @@ public void setUp() throws Exception { //it's ok if min and max are outside the range of the generated buckets, that will just mean that //empty buckets won't be added before the first bucket and/or after the last one long min = baseMillis - intervalMillis * randomNumberOfBuckets(); - long max = baseMillis + randomNumberOfBuckets() * intervalMillis + randomNumberOfBuckets(); + long max = baseMillis + randomNumberOfBuckets() * intervalMillis; extendedBounds = new ExtendedBounds(min, max); } emptyBucketInfo = new InternalDateHistogram.EmptyBucketInfo(rounding, InternalAggregations.EMPTY, extendedBounds); @@ -86,7 +86,7 @@ protected InternalDateHistogram createTestInstance(String name, int nbBuckets = randomNumberOfBuckets(); List buckets = new ArrayList<>(nbBuckets); //avoid having different random instance start from exactly the same base - long startingDate = baseMillis - intervalMillis * randomIntBetween(0, 100); + long startingDate = baseMillis - intervalMillis * randomNumberOfBuckets(); for (int i = 0; i < nbBuckets; i++) { //rarely leave some holes to be filled up with empty buckets in case minDocCount is set to 0 if (frequently()) { From 902a3e1ccf7981e9738669fe1c4f040c2e343974 Mon Sep 17 00:00:00 2001 From: Luca Cavanna Date: Fri, 30 Nov 2018 20:33:09 +0100 Subject: [PATCH 080/115] Histogram aggs: add empty buckets only in the final reduce step (#35921) Empty buckets don't need to be added when performing an incremental reduction step, they can be added later in the final reduction step. This will allow us to later remove the max buckets limit when performing non final reduction. --- .../histogram/InternalDateHistogram.java | 34 ++++++++----------- .../bucket/histogram/InternalHistogram.java | 34 ++++++++----------- .../test/InternalAggregationTestCase.java | 24 +++++++++---- 3 files changed, 48 insertions(+), 44 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalDateHistogram.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalDateHistogram.java index 5d6ec6f93b732..496f8efc60ccf 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalDateHistogram.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalDateHistogram.java @@ -444,26 +444,22 @@ private void addEmptyBuckets(List list, ReduceContext reduceContext) { @Override public InternalAggregation doReduce(List aggregations, ReduceContext reduceContext) { List reducedBuckets = reduceBuckets(aggregations, reduceContext); - - // adding empty buckets if needed - if (minDocCount == 0) { - addEmptyBuckets(reducedBuckets, reduceContext); - } - - if (InternalOrder.isKeyAsc(order) || reduceContext.isFinalReduce() == false) { - // nothing to do, data are already sorted since shards return - // sorted buckets and the merge-sort performed by reduceBuckets - // maintains order - } else if (InternalOrder.isKeyDesc(order)) { - // we just need to reverse here... - List reverse = new ArrayList<>(reducedBuckets); - Collections.reverse(reverse); - reducedBuckets = reverse; - } else { - // sorted by compound order or sub-aggregation, need to fall back to a costly n*log(n) sort - CollectionUtil.introSort(reducedBuckets, order.comparator(null)); + if (reduceContext.isFinalReduce()) { + if (minDocCount == 0) { + addEmptyBuckets(reducedBuckets, reduceContext); + } + if (InternalOrder.isKeyDesc(order)) { + // we just need to reverse here... + List reverse = new ArrayList<>(reducedBuckets); + Collections.reverse(reverse); + reducedBuckets = reverse; + } else if (InternalOrder.isKeyAsc(order) == false){ + // nothing to do when sorting by key ascending, as data is already sorted since shards return + // sorted buckets and the merge-sort performed by reduceBuckets maintains order. + // otherwise, sorted by compound order or sub-aggregation, we need to fall back to a costly n*log(n) sort + CollectionUtil.introSort(reducedBuckets, order.comparator(null)); + } } - return new InternalDateHistogram(getName(), reducedBuckets, order, minDocCount, offset, emptyBucketInfo, format, keyed, pipelineAggregators(), getMetaData()); } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java index d26ac47c9ea25..9f93929c0a186 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java @@ -421,26 +421,22 @@ private void addEmptyBuckets(List list, ReduceContext reduceContext) { @Override public InternalAggregation doReduce(List aggregations, ReduceContext reduceContext) { List reducedBuckets = reduceBuckets(aggregations, reduceContext); - - // adding empty buckets if needed - if (minDocCount == 0) { - addEmptyBuckets(reducedBuckets, reduceContext); - } - - if (InternalOrder.isKeyAsc(order) || reduceContext.isFinalReduce() == false) { - // nothing to do, data are already sorted since shards return - // sorted buckets and the merge-sort performed by reduceBuckets - // maintains order - } else if (InternalOrder.isKeyDesc(order)) { - // we just need to reverse here... - List reverse = new ArrayList<>(reducedBuckets); - Collections.reverse(reverse); - reducedBuckets = reverse; - } else { - // sorted by compound order or sub-aggregation, need to fall back to a costly n*log(n) sort - CollectionUtil.introSort(reducedBuckets, order.comparator(null)); + if (reduceContext.isFinalReduce()) { + if (minDocCount == 0) { + addEmptyBuckets(reducedBuckets, reduceContext); + } + if (InternalOrder.isKeyDesc(order)) { + // we just need to reverse here... + List reverse = new ArrayList<>(reducedBuckets); + Collections.reverse(reverse); + reducedBuckets = reverse; + } else if (InternalOrder.isKeyAsc(order) == false){ + // nothing to do when sorting by key ascending, as data is already sorted since shards return + // sorted buckets and the merge-sort performed by reduceBuckets maintains order. + // otherwise, sorted by compound order or sub-aggregation, we need to fall back to a costly n*log(n) sort + CollectionUtil.introSort(reducedBuckets, order.comparator(null)); + } } - return new InternalHistogram(getName(), reducedBuckets, order, minDocCount, emptyBucketInfo, format, keyed, pipelineAggregators(), getMetaData()); } diff --git a/test/framework/src/main/java/org/elasticsearch/test/InternalAggregationTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/InternalAggregationTestCase.java index bed00526698c7..4aebfdc10af31 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/InternalAggregationTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/InternalAggregationTestCase.java @@ -151,6 +151,7 @@ import static org.elasticsearch.test.XContentTestUtils.insertRandomFields; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertToXContentEquivalent; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.lessThanOrEqualTo; public abstract class InternalAggregationTestCase extends AbstractWireSerializingTestCase { public static final int DEFAULT_MAX_BUCKETS = 100000; @@ -267,7 +268,14 @@ public void testReduceRandom() { new InternalAggregation.ReduceContext(bigArrays, mockScriptService, bucketConsumer,false); @SuppressWarnings("unchecked") T reduced = (T) inputs.get(0).reduce(internalAggregations, context); - assertMultiBucketConsumer(reduced, bucketConsumer); + int initialBucketCount = 0; + for (InternalAggregation internalAggregation : internalAggregations) { + initialBucketCount += countInnerBucket(internalAggregation); + } + int reducedBucketCount = countInnerBucket(reduced); + //check that non final reduction never adds buckets + assertThat(reducedBucketCount, lessThanOrEqualTo(initialBucketCount)); + assertMultiBucketConsumer(reducedBucketCount, bucketConsumer); toReduce = new ArrayList<>(toReduce.subList(r, toReduceSize)); toReduce.add(reduced); } @@ -332,14 +340,14 @@ protected NamedXContentRegistry xContentRegistry() { public final void testFromXContent() throws IOException { final T aggregation = createTestInstance(); - final Aggregation parsedAggregation = parseAndAssert(aggregation, randomBoolean(), false); - assertFromXContent(aggregation, (ParsedAggregation) parsedAggregation); + final ParsedAggregation parsedAggregation = parseAndAssert(aggregation, randomBoolean(), false); + assertFromXContent(aggregation, parsedAggregation); } public final void testFromXContentWithRandomFields() throws IOException { final T aggregation = createTestInstance(); - final Aggregation parsedAggregation = parseAndAssert(aggregation, randomBoolean(), true); - assertFromXContent(aggregation, (ParsedAggregation) parsedAggregation); + final ParsedAggregation parsedAggregation = parseAndAssert(aggregation, randomBoolean(), true); + assertFromXContent(aggregation, parsedAggregation); } protected abstract void assertFromXContent(T aggregation, ParsedAggregation parsedAggregation) throws IOException; @@ -423,6 +431,10 @@ protected static DocValueFormat randomNumericDocValueFormat() { } public static void assertMultiBucketConsumer(Aggregation agg, MultiBucketConsumer bucketConsumer) { - assertThat(bucketConsumer.getCount(), equalTo(countInnerBucket(agg))); + assertMultiBucketConsumer(countInnerBucket(agg), bucketConsumer); + } + + private static void assertMultiBucketConsumer(int innerBucketCount, MultiBucketConsumer bucketConsumer) { + assertThat(bucketConsumer.getCount(), equalTo(innerBucketCount)); } } From f6d7d539084dad7014a235624e210d45b9fde18f Mon Sep 17 00:00:00 2001 From: Luca Cavanna Date: Mon, 3 Dec 2018 13:55:18 +0100 Subject: [PATCH 081/115] Enforce max_buckets limit only in the final reduction phase (#36152) Given that we check the max buckets limit on each shard when collecting the buckets, and that non final reduction cannot add buckets (see #35921), there is no point in counting and checking the number of buckets as part of non final reduction phases. Such check is still needed though in the final reduction phases to make sure that the number of returned buckets is not above the allowed threshold. Relates somehow to #32125 as we will make use of non final reduction phases in CCS alternate execution mode and that increases the chance that this check trips for nothing when reducing aggs in each remote cluster. --- .../elasticsearch/search/SearchService.java | 3 +- .../search/SearchServiceTests.java | 35 +++++++++++++------ 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/search/SearchService.java b/server/src/main/java/org/elasticsearch/search/SearchService.java index bbe03380148ad..ea79c19940cb7 100644 --- a/server/src/main/java/org/elasticsearch/search/SearchService.java +++ b/server/src/main/java/org/elasticsearch/search/SearchService.java @@ -1090,7 +1090,8 @@ public QueryRewriteContext getRewriteContext(LongSupplier nowInMillis) { } public InternalAggregation.ReduceContext createReduceContext(boolean finalReduce) { - return new InternalAggregation.ReduceContext(bigArrays, scriptService, multiBucketConsumerService.create(), finalReduce); + return new InternalAggregation.ReduceContext(bigArrays, scriptService, + finalReduce ? multiBucketConsumerService.create() : bucketCount -> {}, finalReduce); } public static final class CanMatchResponse extends SearchPhaseResult { diff --git a/server/src/test/java/org/elasticsearch/search/SearchServiceTests.java b/server/src/test/java/org/elasticsearch/search/SearchServiceTests.java index 50f654f4f497f..54ab48d1c7674 100644 --- a/server/src/test/java/org/elasticsearch/search/SearchServiceTests.java +++ b/server/src/test/java/org/elasticsearch/search/SearchServiceTests.java @@ -19,7 +19,6 @@ package org.elasticsearch.search; import com.carrotsearch.hppc.IntArrayList; - import org.apache.lucene.search.Query; import org.apache.lucene.store.AlreadyClosedException; import org.elasticsearch.action.ActionListener; @@ -59,6 +58,8 @@ import org.elasticsearch.script.MockScriptPlugin; import org.elasticsearch.script.Script; import org.elasticsearch.script.ScriptType; +import org.elasticsearch.search.aggregations.InternalAggregation; +import org.elasticsearch.search.aggregations.MultiBucketConsumerService; import org.elasticsearch.search.aggregations.bucket.global.GlobalAggregationBuilder; import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder; import org.elasticsearch.search.aggregations.support.ValueType; @@ -152,10 +153,11 @@ public void onQueryPhase(SearchContext context, long tookInNanos) { @Override protected Settings nodeSettings() { - return Settings.builder().put("search.default_search_timeout", "5s").build(); + return Settings.builder().put("search.default_search_timeout", "5s") + .put(MultiBucketConsumerService.MAX_BUCKET_SETTING.getKey(), MultiBucketConsumerService.SOFT_LIMIT_MAX_BUCKETS).build(); } - public void testClearOnClose() throws ExecutionException, InterruptedException { + public void testClearOnClose() { createIndex("index"); client().prepareIndex("index", "type", "1").setSource("field", "value").setRefreshPolicy(IMMEDIATE).get(); SearchResponse searchResponse = client().prepareSearch("index").setSize(1).setScroll("1m").get(); @@ -167,7 +169,7 @@ public void testClearOnClose() throws ExecutionException, InterruptedException { assertEquals(0, service.getActiveContexts()); } - public void testClearOnStop() throws ExecutionException, InterruptedException { + public void testClearOnStop() { createIndex("index"); client().prepareIndex("index", "type", "1").setSource("field", "value").setRefreshPolicy(IMMEDIATE).get(); SearchResponse searchResponse = client().prepareSearch("index").setSize(1).setScroll("1m").get(); @@ -179,7 +181,7 @@ public void testClearOnStop() throws ExecutionException, InterruptedException { assertEquals(0, service.getActiveContexts()); } - public void testClearIndexDelete() throws ExecutionException, InterruptedException { + public void testClearIndexDelete() { createIndex("index"); client().prepareIndex("index", "type", "1").setSource("field", "value").setRefreshPolicy(IMMEDIATE).get(); SearchResponse searchResponse = client().prepareSearch("index").setSize(1).setScroll("1m").get(); @@ -208,7 +210,7 @@ public void testCloseSearchContextOnRewriteException() { assertEquals(activeRefs, indexShard.store().refCount()); } - public void testSearchWhileIndexDeleted() throws IOException, InterruptedException { + public void testSearchWhileIndexDeleted() throws InterruptedException { createIndex("index"); client().prepareIndex("index", "type", "1").setSource("field", "value").setRefreshPolicy(IMMEDIATE).get(); @@ -443,15 +445,15 @@ protected QueryBuilder doRewrite(QueryRewriteContext queryRewriteContext) { } @Override - protected void doWriteTo(StreamOutput out) throws IOException { + protected void doWriteTo(StreamOutput out) { } @Override - protected void doXContent(XContentBuilder builder, Params params) throws IOException { + protected void doXContent(XContentBuilder builder, Params params) { } @Override - protected Query doToQuery(QueryShardContext context) throws IOException { + protected Query doToQuery(QueryShardContext context) { return null; } @@ -501,7 +503,6 @@ public void testCanMatch() throws IOException { assertFalse(service.canMatch(new ShardSearchLocalRequest(indexShard.shardId(), 1, SearchType.QUERY_THEN_FETCH, new SearchSourceBuilder().query(new MatchNoneQueryBuilder()), Strings.EMPTY_ARRAY, false, new AliasFilter(null, Strings.EMPTY_ARRAY), 1f, allowPartialSearchResults, null, null))); - } public void testCanRewriteToMatchNone() { @@ -519,7 +520,6 @@ public void testCanRewriteToMatchNone() { .suggest(new SuggestBuilder()))); assertFalse(SearchService.canRewriteToMatchNone(new SearchSourceBuilder().query(new TermQueryBuilder("foo", "bar")) .suggest(new SuggestBuilder()))); - } public void testSetSearchThrottled() { @@ -568,4 +568,17 @@ public void testExpandSearchThrottled() { assertHitCount(client().prepareSearch().get(), 0L); assertHitCount(client().prepareSearch().setIndicesOptions(IndicesOptions.STRICT_EXPAND_OPEN_FORBID_CLOSED).get(), 1L); } + + public void testCreateReduceContext() { + final SearchService service = getInstanceFromNode(SearchService.class); + { + InternalAggregation.ReduceContext reduceContext = service.createReduceContext(true); + expectThrows(MultiBucketConsumerService.TooManyBucketsException.class, + () -> reduceContext.consumeBucketsAndMaybeBreak(MultiBucketConsumerService.SOFT_LIMIT_MAX_BUCKETS + 1)); + } + { + InternalAggregation.ReduceContext reduceContext = service.createReduceContext(false); + reduceContext.consumeBucketsAndMaybeBreak(MultiBucketConsumerService.SOFT_LIMIT_MAX_BUCKETS + 1); + } + } } From 2761016d7cc350ebc804de60e6863c8c36c2956e Mon Sep 17 00:00:00 2001 From: Alan Woodward Date: Mon, 3 Dec 2018 18:25:18 +0000 Subject: [PATCH 082/115] Fix broken links in painless docs (#36170) --- .../painless-contexts/painless-similarity-context.asciidoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/painless/painless-contexts/painless-similarity-context.asciidoc b/docs/painless/painless-contexts/painless-similarity-context.asciidoc index 9a8e59350d1a8..a8d66233e66cc 100644 --- a/docs/painless/painless-contexts/painless-similarity-context.asciidoc +++ b/docs/painless/painless-contexts/painless-similarity-context.asciidoc @@ -41,7 +41,7 @@ documents in a query. `doc.length` (`long`, read-only):: The number of tokens the current document has in the current field. This - is decoded from the stored {ref}/norms[norms] and may be approximate for + is decoded from the stored {ref}/norms.html[norms] and may be approximate for long fields `doc.freq` (`long`, read-only):: @@ -49,7 +49,7 @@ documents in a query. document for the current field. Note that the `query`, `field`, and `term` variables are also available to the -{ref}/painless-weight-context[weight context]. They are more efficiently used +{painless}/painless-weight-context.html[weight context]. They are more efficiently used there, as they are constant for all documents. For queries that contain multiple terms, the script is called once for each From 8890d8cd48711532f6d249786e63bd8d1daea19a Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Mon, 3 Dec 2018 19:40:40 +0100 Subject: [PATCH 083/115] SNAPSHOT: Improve Resilience SnapshotShardService (#36113) (#36164) * Resolve the index in the snapshotting thread * Added test for routing table - snapshot state mismatch --- .../snapshots/SnapshotShardsService.java | 4 +- .../DedicatedClusterSnapshotRestoreIT.java | 46 ++++++++++ .../BusyMasterServiceDisruption.java | 89 +++++++++++++++++++ 3 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 test/framework/src/main/java/org/elasticsearch/test/disruption/BusyMasterServiceDisruption.java diff --git a/server/src/main/java/org/elasticsearch/snapshots/SnapshotShardsService.java b/server/src/main/java/org/elasticsearch/snapshots/SnapshotShardsService.java index 3843dea2e5af1..ed1c486eb43cc 100644 --- a/server/src/main/java/org/elasticsearch/snapshots/SnapshotShardsService.java +++ b/server/src/main/java/org/elasticsearch/snapshots/SnapshotShardsService.java @@ -342,15 +342,15 @@ private void processIndexShardSnapshots(ClusterChangedEvent event) { for (final Map.Entry shardEntry : entry.getValue().entrySet()) { final ShardId shardId = shardEntry.getKey(); - final IndexShard indexShard = indicesService.indexServiceSafe(shardId.getIndex()).getShardOrNull(shardId.id()); final IndexId indexId = indicesMap.get(shardId.getIndexName()); - assert indexId != null; executor.execute(new AbstractRunnable() { final SetOnce failure = new SetOnce<>(); @Override public void doRun() { + final IndexShard indexShard = indicesService.indexServiceSafe(shardId.getIndex()).getShardOrNull(shardId.id()); + assert indexId != null; snapshot(indexShard, snapshot, indexId, shardEntry.getValue()); } diff --git a/server/src/test/java/org/elasticsearch/snapshots/DedicatedClusterSnapshotRestoreIT.java b/server/src/test/java/org/elasticsearch/snapshots/DedicatedClusterSnapshotRestoreIT.java index bcaa4b6d772ad..ba095c7f02083 100644 --- a/server/src/test/java/org/elasticsearch/snapshots/DedicatedClusterSnapshotRestoreIT.java +++ b/server/src/test/java/org/elasticsearch/snapshots/DedicatedClusterSnapshotRestoreIT.java @@ -81,6 +81,8 @@ import org.elasticsearch.test.ESIntegTestCase.Scope; import org.elasticsearch.test.InternalTestCluster; import org.elasticsearch.test.TestCustomMetaData; +import org.elasticsearch.test.disruption.BusyMasterServiceDisruption; +import org.elasticsearch.test.disruption.ServiceDisruptionScheme; import org.elasticsearch.test.rest.FakeRestRequest; import java.io.IOException; @@ -1172,6 +1174,50 @@ public void testSnapshotTotalAndIncrementalSizes() throws IOException { assertThat(anotherStats.getTotalSize(), is(snapshot1FileSize)); } + public void testDataNodeRestartWithBusyMasterDuringSnapshot() throws Exception { + logger.info("--> starting a master node and two data nodes"); + internalCluster().startMasterOnlyNode(); + internalCluster().startDataOnlyNodes(2); + logger.info("--> creating repository"); + assertAcked(client().admin().cluster().preparePutRepository("test-repo") + .setType("mock").setSettings(Settings.builder() + .put("location", randomRepoPath()) + .put("compress", randomBoolean()) + .put("max_snapshot_bytes_per_sec", "1000b") + .put("chunk_size", randomIntBetween(100, 1000), ByteSizeUnit.BYTES))); + assertAcked(prepareCreate("test-idx", 0, Settings.builder() + .put("number_of_shards", 5).put("number_of_replicas", 0))); + ensureGreen(); + logger.info("--> indexing some data"); + final int numdocs = randomIntBetween(50, 100); + IndexRequestBuilder[] builders = new IndexRequestBuilder[numdocs]; + for (int i = 0; i < builders.length; i++) { + builders[i] = client().prepareIndex("test-idx", "type1", + Integer.toString(i)).setSource("field1", "bar " + i); + } + indexRandom(true, builders); + flushAndRefresh(); + final String dataNode = blockNodeWithIndex("test-repo", "test-idx"); + logger.info("--> snapshot"); + client(internalCluster().getMasterName()).admin().cluster() + .prepareCreateSnapshot("test-repo", "test-snap").setWaitForCompletion(false).setIndices("test-idx").get(); + ServiceDisruptionScheme disruption = new BusyMasterServiceDisruption(random(), Priority.HIGH); + setDisruptionScheme(disruption); + disruption.startDisrupting(); + logger.info("--> restarting data node, which should cause primary shards to be failed"); + internalCluster().restartNode(dataNode, InternalTestCluster.EMPTY_CALLBACK); + unblockNode("test-repo", dataNode); + disruption.stopDisrupting(); + // check that snapshot completes + assertBusy(() -> { + GetSnapshotsResponse snapshotsStatusResponse = client().admin().cluster() + .prepareGetSnapshots("test-repo").setSnapshots("test-snap").setIgnoreUnavailable(true).get(); + assertEquals(1, snapshotsStatusResponse.getSnapshots().size()); + SnapshotInfo snapshotInfo = snapshotsStatusResponse.getSnapshots().get(0); + assertTrue(snapshotInfo.state().toString(), snapshotInfo.state().completed()); + }, 30, TimeUnit.SECONDS); + } + private long calculateTotalFilesSize(List files) { return files.stream().mapToLong(f -> { try { diff --git a/test/framework/src/main/java/org/elasticsearch/test/disruption/BusyMasterServiceDisruption.java b/test/framework/src/main/java/org/elasticsearch/test/disruption/BusyMasterServiceDisruption.java new file mode 100644 index 0000000000000..3621cba1e7992 --- /dev/null +++ b/test/framework/src/main/java/org/elasticsearch/test/disruption/BusyMasterServiceDisruption.java @@ -0,0 +1,89 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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.elasticsearch.test.disruption; + +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.ClusterStateUpdateTask; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.Priority; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.test.InternalTestCluster; +import java.util.Random; +import java.util.concurrent.atomic.AtomicBoolean; + +public class BusyMasterServiceDisruption extends SingleNodeDisruption { + private final AtomicBoolean active = new AtomicBoolean(); + private final Priority priority; + + public BusyMasterServiceDisruption(Random random, Priority priority) { + super(random); + this.priority = priority; + } + + @Override + public void startDisrupting() { + disruptedNode = cluster.getMasterName(); + final String disruptionNodeCopy = disruptedNode; + if (disruptionNodeCopy == null) { + return; + } + ClusterService clusterService = cluster.getInstance(ClusterService.class, disruptionNodeCopy); + if (clusterService == null) { + return; + } + logger.info("making master service busy on node [{}] at priority [{}]", disruptionNodeCopy, priority); + active.set(true); + submitTask(clusterService); + } + + private void submitTask(ClusterService clusterService) { + clusterService.getMasterService().submitStateUpdateTask( + "service_disruption_block", + new ClusterStateUpdateTask(priority) { + @Override + public ClusterState execute(ClusterState currentState) { + if (active.get()) { + submitTask(clusterService); + } + return currentState; + } + + @Override + public void onFailure(String source, Exception e) { + logger.error("unexpected error during disruption", e); + } + } + ); + } + + @Override + public void stopDisrupting() { + active.set(false); + } + + @Override + public void removeAndEnsureHealthy(InternalTestCluster cluster) { + removeFromCluster(cluster); + } + + @Override + public TimeValue expectedTimeToHeal() { + return TimeValue.timeValueMinutes(0); + } +} From c29f65602d24e3ba49fc69c1abd9806674f33adf Mon Sep 17 00:00:00 2001 From: Tal Levy Date: Mon, 3 Dec 2018 10:55:31 -0800 Subject: [PATCH 084/115] [ILM] fix ilm.remove_policy rest-spec (#36165) The rest interface for remove-policy-from-index API does not support `_ilm/remove`, it requires that an `{index}` pattern be defined in the URL path. This fixes the rest-api-spec to reflect the implementation --- .../src/test/resources/rest-api-spec/api/ilm.remove_policy.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/api/ilm.remove_policy.json b/x-pack/plugin/src/test/resources/rest-api-spec/api/ilm.remove_policy.json index de3591d60269e..d9903ff8dc40d 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/api/ilm.remove_policy.json +++ b/x-pack/plugin/src/test/resources/rest-api-spec/api/ilm.remove_policy.json @@ -4,7 +4,7 @@ "methods": [ "POST" ], "url": { "path": "/{index}/_ilm/remove", - "paths": ["/{index}/_ilm/remove", "/_ilm/remove"], + "paths": ["/{index}/_ilm/remove"], "parts": { "index": { "type" : "string", From 82be7611524eca6236660b9be2d97d032b995219 Mon Sep 17 00:00:00 2001 From: Tal Levy Date: Mon, 3 Dec 2018 11:17:18 -0800 Subject: [PATCH 085/115] initial cleanup of deprecation checks for 6.x (#35326) The deprecation checks were primarily used for helping identity breaking changes before upgrading latest minor releases to next majors. This meant that these checks were not necessarily maintained across future minors. This cleanup is a first step in preparing for catching up on reporting all the existing deprecations in the 6.x branch. changes: - added cluster deprecation tests for existing updated checks - added node deprecation checks for azure and gcs plugins - removed stale index checks - added index check for indices created before 6.0 --- .../core/deprecation/DeprecationIssue.java | 6 + .../xpack/deprecation/DeprecationChecks.java | 12 +- .../deprecation/IndexDeprecationChecks.java | 167 +------------ .../deprecation/NodeDeprecationChecks.java | 52 ++++ .../ClusterDeprecationChecksTests.java | 68 ++++++ .../IndexDeprecationChecksTests.java | 230 ++---------------- .../NodeDeprecationChecksTests.java | 91 +++++++ 7 files changed, 244 insertions(+), 382 deletions(-) create mode 100644 x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/NodeDeprecationChecks.java create mode 100644 x-pack/plugin/deprecation/src/test/java/org/elasticsearch/xpack/deprecation/ClusterDeprecationChecksTests.java create mode 100644 x-pack/plugin/deprecation/src/test/java/org/elasticsearch/xpack/deprecation/NodeDeprecationChecksTests.java diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/deprecation/DeprecationIssue.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/deprecation/DeprecationIssue.java index ff1b0d303d022..bece28eb49345 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/deprecation/DeprecationIssue.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/deprecation/DeprecationIssue.java @@ -7,6 +7,7 @@ import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; @@ -132,5 +133,10 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(level, message, url, details); } + + @Override + public String toString() { + return Strings.toString(this); + } } diff --git a/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/DeprecationChecks.java b/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/DeprecationChecks.java index 86c164fd1ef74..f8d85129be760 100644 --- a/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/DeprecationChecks.java +++ b/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/DeprecationChecks.java @@ -37,19 +37,13 @@ private DeprecationChecks() { static List, List, DeprecationIssue>> NODE_SETTINGS_CHECKS = Collections.unmodifiableList(Arrays.asList( - // STUB + NodeDeprecationChecks::azureRepositoryChanges, + NodeDeprecationChecks::gcsRepositoryChanges )); static List> INDEX_SETTINGS_CHECKS = Collections.unmodifiableList(Arrays.asList( - IndexDeprecationChecks::allMetaFieldIsDisabledByDefaultCheck, - IndexDeprecationChecks::baseSimilarityDefinedCheck, - IndexDeprecationChecks::coercionCheck, - IndexDeprecationChecks::dynamicTemplateWithMatchMappingTypeCheck, - IndexDeprecationChecks::includeInAllCheck, - IndexDeprecationChecks::indexSharedFileSystemCheck, - IndexDeprecationChecks::indexStoreTypeCheck, - IndexDeprecationChecks::storeThrottleSettingsCheck, + IndexDeprecationChecks::oldIndicesCheck, IndexDeprecationChecks::delimitedPayloadFilterCheck)); /** diff --git a/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/IndexDeprecationChecks.java b/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/IndexDeprecationChecks.java index ecfd7ec24e4d9..a022a9c42a329 100644 --- a/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/IndexDeprecationChecks.java +++ b/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/IndexDeprecationChecks.java @@ -11,16 +11,11 @@ import org.elasticsearch.Version; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.cluster.metadata.MappingMetaData; -import org.elasticsearch.common.Booleans; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.index.analysis.AnalysisRegistry; -import org.elasticsearch.index.mapper.AllFieldMapper; -import org.elasticsearch.index.mapper.DynamicTemplate; import org.elasticsearch.xpack.core.deprecation.DeprecationIssue; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.function.BiConsumer; @@ -81,174 +76,30 @@ private static List findInPropertiesRecursively(String type, Map issues = new ArrayList<>(); - fieldLevelMappingIssue(indexMetaData, (mappingMetaData, sourceAsMap) -> { - issues.addAll(findInPropertiesRecursively(mappingMetaData.type(), sourceAsMap, - property -> "boolean".equals(property.get("type")))); - }); - if (issues.size() > 0) { - return new DeprecationIssue(DeprecationIssue.Level.INFO, "Coercion of boolean fields", - "https://www.elastic.co/guide/en/elasticsearch/reference/6.0/" + - "breaking_60_mappings_changes.html#_coercion_of_boolean_fields", - issues.toString()); - } - } - return null; - } - - @SuppressWarnings("unchecked") - static DeprecationIssue allMetaFieldIsDisabledByDefaultCheck(IndexMetaData indexMetaData) { - if (indexMetaData.getCreationVersion().before(Version.V_6_0_0_alpha1)) { - List issues = new ArrayList<>(); - fieldLevelMappingIssue(indexMetaData, (mappingMetaData, sourceAsMap) -> { - Map allMetaData = (Map) sourceAsMap.getOrDefault("_all", Collections.emptyMap()); - Object enabledObj = allMetaData.get("enabled"); - if (enabledObj != null) { - enabledObj = Booleans.parseBooleanLenient(enabledObj.toString(), - AllFieldMapper.Defaults.ENABLED.enabled); - } - if (Boolean.TRUE.equals(enabledObj)) { - issues.add(mappingMetaData.type()); - } - }); - if (issues.size() > 0) { - return new DeprecationIssue(DeprecationIssue.Level.INFO, - "The _all meta field is disabled by default on indices created in 6.0", - "https://www.elastic.co/guide/en/elasticsearch/reference/6.0/" + - "breaking_60_mappings_changes.html#_the_literal__all_literal_meta_field_is_now_disabled_by_default", - "types: " + issues.toString()); - } - } - return null; - } - - static DeprecationIssue includeInAllCheck(IndexMetaData indexMetaData) { - if (indexMetaData.getCreationVersion().before(Version.V_6_0_0_alpha1)) { - List issues = new ArrayList<>(); - fieldLevelMappingIssue(indexMetaData, (mappingMetaData, sourceAsMap) -> { - issues.addAll(findInPropertiesRecursively(mappingMetaData.type(), sourceAsMap, - property -> property.containsKey("include_in_all"))); - }); - if (issues.size() > 0) { - return new DeprecationIssue(DeprecationIssue.Level.CRITICAL, - "The [include_in_all] mapping parameter is now disallowed", - "https://www.elastic.co/guide/en/elasticsearch/reference/6.0/" + - "breaking_60_mappings_changes.html#_the_literal_include_in_all_literal_mapping_parameter_is_now_disallowed", - issues.toString()); - } - } - return null; - } - - static DeprecationIssue dynamicTemplateWithMatchMappingTypeCheck(IndexMetaData indexMetaData) { - if (indexMetaData.getCreationVersion().before(Version.V_6_0_0_alpha1)) { - List issues = new ArrayList<>(); - fieldLevelMappingIssue(indexMetaData, (mappingMetaData, sourceAsMap) -> { - List dynamicTemplates = (List) mappingMetaData - .getSourceAsMap().getOrDefault("dynamic_templates", Collections.emptyList()); - for (Object template : dynamicTemplates) { - for (Map.Entry prop : ((Map) template).entrySet()) { - Map val = (Map) prop.getValue(); - if (val.containsKey("match_mapping_type")) { - Object mappingMatchType = val.get("match_mapping_type"); - boolean isValidMatchType = Arrays.stream(DynamicTemplate.XContentFieldType.values()) - .anyMatch(v -> v.toString().equals(mappingMatchType)); - if (isValidMatchType == false) { - issues.add("type: " + mappingMetaData.type() + ", dynamicFieldDefinition" - + prop.getKey() + ", unknown match_mapping_type[" + mappingMatchType + "]"); - } - } - } - } - }); - if (issues.size() > 0) { - return new DeprecationIssue(DeprecationIssue.Level.CRITICAL, - "Unrecognized match_mapping_type options not silently ignored", - "https://www.elastic.co/guide/en/elasticsearch/reference/6.0/" + - "breaking_60_mappings_changes.html" + - "#_unrecognized_literal_match_mapping_type_literal_options_not_silently_ignored", - issues.toString()); - } - } - return null; - } - - static DeprecationIssue baseSimilarityDefinedCheck(IndexMetaData indexMetaData) { - if (indexMetaData.getCreationVersion().before(Version.V_6_0_0_alpha1)) { - Settings settings = indexMetaData.getSettings().getAsSettings("index.similarity.base"); - if (settings.size() > 0) { - return new DeprecationIssue(DeprecationIssue.Level.WARNING, - "The base similarity is now ignored as coords and query normalization have been removed." + - "If provided, this setting will be ignored and issue a deprecation warning", - "https://www.elastic.co/guide/en/elasticsearch/reference/6.0/" + - "breaking_60_settings_changes.html#_similarity_settings", null); - - } - } - return null; - } - static DeprecationIssue delimitedPayloadFilterCheck(IndexMetaData indexMetaData) { List issues = new ArrayList<>(); Map filters = indexMetaData.getSettings().getGroups(AnalysisRegistry.INDEX_ANALYSIS_FILTER); for (Map.Entry entry : filters.entrySet()) { if ("delimited_payload_filter".equals(entry.getValue().get("type"))) { issues.add("The filter [" + entry.getKey() + "] is of deprecated 'delimited_payload_filter' type. " - + "The filter type should be changed to 'delimited_payload'."); + + "The filter type should be changed to 'delimited_payload'."); } } if (issues.size() > 0) { return new DeprecationIssue(DeprecationIssue.Level.WARNING, "Use of 'delimited_payload_filter'.", - "https://www.elastic.co/guide/en/elasticsearch/reference/master/breaking_70_analysis_changes.html", issues.toString()); - } - return null; - } - - static DeprecationIssue indexStoreTypeCheck(IndexMetaData indexMetaData) { - if (indexMetaData.getCreationVersion().before(Version.V_6_0_0_alpha1) && - indexMetaData.getSettings().get("index.store.type") != null) { - return new DeprecationIssue(DeprecationIssue.Level.CRITICAL, - "The default index.store.type has been removed. If you were using it, " + - "we advise that you simply remove it from your index settings and Elasticsearch" + - "will use the best store implementation for your operating system.", - "https://www.elastic.co/guide/en/elasticsearch/reference/6.0/" + - "breaking_60_settings_changes.html#_store_settings", null); - - } - return null; - } - - static DeprecationIssue storeThrottleSettingsCheck(IndexMetaData indexMetaData) { - if (indexMetaData.getCreationVersion().before(Version.V_6_0_0_alpha1)) { - Settings settings = indexMetaData.getSettings(); - Settings throttleSettings = settings.getAsSettings("index.store.throttle"); - ArrayList foundSettings = new ArrayList<>(); - if (throttleSettings.get("max_bytes_per_sec") != null) { - foundSettings.add("index.store.throttle.max_bytes_per_sec"); - } - if (throttleSettings.get("type") != null) { - foundSettings.add("index.store.throttle.type"); - } - - if (foundSettings.isEmpty() == false) { - return new DeprecationIssue(DeprecationIssue.Level.CRITICAL, - "index.store.throttle settings are no longer recognized. these settings should be removed", - "https://www.elastic.co/guide/en/elasticsearch/reference/6.0/" + - "breaking_60_settings_changes.html#_store_throttling_settings", "present settings: " + foundSettings); - } + "https://www.elastic.co/guide/en/elasticsearch/reference/master/breaking_70_analysis_changes.html", issues.toString()); } return null; } - static DeprecationIssue indexSharedFileSystemCheck(IndexMetaData indexMetaData) { - if (indexMetaData.getCreationVersion().before(Version.V_6_0_0_alpha1) && - indexMetaData.getSettings().get("index.shared_filesystem") != null) { + static DeprecationIssue oldIndicesCheck(IndexMetaData indexMetaData) { + Version createdWith = indexMetaData.getCreationVersion(); + if (createdWith.before(Version.V_6_0_0)) { return new DeprecationIssue(DeprecationIssue.Level.CRITICAL, - "[index.shared_filesystem] setting should be removed", - "https://www.elastic.co/guide/en/elasticsearch/reference/6.0/" + - "breaking_60_indices_changes.html#_shadow_replicas_have_been_removed", null); + "Index created before 6.0", + "https://www.elastic.co/guide/en/elasticsearch/reference/master/" + + "breaking-changes-7.0.html", + "this index was created using version: " + createdWith); } return null; diff --git a/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/NodeDeprecationChecks.java b/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/NodeDeprecationChecks.java new file mode 100644 index 0000000000000..d379869f0d472 --- /dev/null +++ b/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/NodeDeprecationChecks.java @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.deprecation; + + +import org.elasticsearch.action.admin.cluster.node.info.NodeInfo; +import org.elasticsearch.action.admin.cluster.node.stats.NodeStats; +import org.elasticsearch.xpack.core.deprecation.DeprecationIssue; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * Node-specific deprecation checks + */ +public class NodeDeprecationChecks { + + static DeprecationIssue azureRepositoryChanges(List nodeInfos, List nodeStats) { + List nodesFound = nodeInfos.stream() + .filter(nodeInfo -> + nodeInfo.getPlugins().getPluginInfos().stream() + .anyMatch(pluginInfo -> "repository-azure".equals(pluginInfo.getName())) + ).map(nodeInfo -> nodeInfo.getNode().getName()).collect(Collectors.toList()); + if (nodesFound.size() > 0) { + return new DeprecationIssue(DeprecationIssue.Level.WARNING, + "Azure Repository settings changed", + "https://www.elastic.co/guide/en/elasticsearch/reference/master/breaking_70_cluster_changes.html" + + "#_azure_repository_plugin", + "nodes with repository-azure installed: " + nodesFound); + } + return null; + } + + static DeprecationIssue gcsRepositoryChanges(List nodeInfos, List nodeStats) { + List nodesFound = nodeInfos.stream() + .filter(nodeInfo -> + nodeInfo.getPlugins().getPluginInfos().stream() + .anyMatch(pluginInfo -> "repository-gcs".equals(pluginInfo.getName())) + ).map(nodeInfo -> nodeInfo.getNode().getName()).collect(Collectors.toList()); + if (nodesFound.size() > 0) { + return new DeprecationIssue(DeprecationIssue.Level.WARNING, + "GCS Repository settings changed", + "https://www.elastic.co/guide/en/elasticsearch/reference/master/breaking_70_cluster_changes.html" + + "#_google_cloud_storage_repository_plugin", + "nodes with repository-gcs installed: " + nodesFound); + } + return null; + } +} diff --git a/x-pack/plugin/deprecation/src/test/java/org/elasticsearch/xpack/deprecation/ClusterDeprecationChecksTests.java b/x-pack/plugin/deprecation/src/test/java/org/elasticsearch/xpack/deprecation/ClusterDeprecationChecksTests.java new file mode 100644 index 0000000000000..f2cdf401fbb00 --- /dev/null +++ b/x-pack/plugin/deprecation/src/test/java/org/elasticsearch/xpack/deprecation/ClusterDeprecationChecksTests.java @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.deprecation; + +import org.elasticsearch.Version; +import org.elasticsearch.cluster.ClusterName; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.metadata.IndexMetaData; +import org.elasticsearch.cluster.metadata.MetaData; +import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.cluster.node.DiscoveryNodes; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.transport.TransportAddress; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.core.deprecation.DeprecationIssue; + +import java.util.List; + +import static java.util.Collections.singletonList; +import static org.elasticsearch.xpack.deprecation.DeprecationChecks.CLUSTER_SETTINGS_CHECKS; + +public class ClusterDeprecationChecksTests extends ESTestCase { + + public void testCheckShardLimit() { + int shardsPerNode = randomIntBetween(1, 10000); + int nodeCount = randomIntBetween(1, 10); + int maxShardsInCluster = shardsPerNode * nodeCount; + int currentOpenShards = maxShardsInCluster + randomIntBetween(0, 100); + + DiscoveryNodes.Builder discoveryNodesBuilder = DiscoveryNodes.builder(); + for (int i = 0; i < nodeCount; i++) { + DiscoveryNode discoveryNode = DiscoveryNode.createLocal(Settings.builder().put("node.name", "node_check" + i).build(), + new TransportAddress(TransportAddress.META_ADDRESS, 9200 + i), "test" + i); + discoveryNodesBuilder.add(discoveryNode); + } + + // verify deprecation issue is returned when number of open shards exceeds cluster soft limit + MetaData metaData = MetaData.builder().put(IndexMetaData.builder("test") + .settings(settings(Version.CURRENT)) + .numberOfShards(currentOpenShards) + .numberOfReplicas(0)) + .persistentSettings(settings(Version.CURRENT) + .put(MetaData.SETTING_CLUSTER_MAX_SHARDS_PER_NODE.getKey(), String.valueOf(shardsPerNode)).build()) + .build(); + ClusterState state = ClusterState.builder(ClusterName.DEFAULT) + .metaData(metaData).nodes(discoveryNodesBuilder).build(); + List issues = DeprecationChecks.filterChecks(CLUSTER_SETTINGS_CHECKS, c -> c.apply(state)); + DeprecationIssue expected = new DeprecationIssue(DeprecationIssue.Level.WARNING, + "Number of open shards exceeds cluster soft limit", + "https://www.elastic.co/guide/en/elasticsearch/reference/master/breaking_70_cluster_changes.html", + "There are [" + currentOpenShards + "] open shards in this cluster, but the cluster is limited to [" + + shardsPerNode + "] per data node, for [" + maxShardsInCluster + "] maximum."); + assertEquals(singletonList(expected), issues); + + // verify no deprecation issues are returned when number of open shards is below the cluster soft limit + MetaData goodMetaData = MetaData.builder(metaData).put(IndexMetaData.builder("test") + .settings(settings(Version.CURRENT)) + .numberOfReplicas(0) + .numberOfShards(maxShardsInCluster - randomIntBetween(1, 100))).build(); + ClusterState goodState = ClusterState.builder(ClusterName.DEFAULT) + .metaData(goodMetaData).nodes(state.nodes()).build(); + issues = DeprecationChecks.filterChecks(CLUSTER_SETTINGS_CHECKS, c -> c.apply(goodState)); + assertTrue(issues.isEmpty()); + } +} diff --git a/x-pack/plugin/deprecation/src/test/java/org/elasticsearch/xpack/deprecation/IndexDeprecationChecksTests.java b/x-pack/plugin/deprecation/src/test/java/org/elasticsearch/xpack/deprecation/IndexDeprecationChecksTests.java index 551cd2fda1c52..9bb9587e05cbe 100644 --- a/x-pack/plugin/deprecation/src/test/java/org/elasticsearch/xpack/deprecation/IndexDeprecationChecksTests.java +++ b/x-pack/plugin/deprecation/src/test/java/org/elasticsearch/xpack/deprecation/IndexDeprecationChecksTests.java @@ -7,16 +7,12 @@ import org.elasticsearch.Version; import org.elasticsearch.cluster.metadata.IndexMetaData; -import org.elasticsearch.common.Strings; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.xcontent.XContentBuilder; -import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.VersionUtils; import org.elasticsearch.xpack.core.deprecation.DeprecationInfoAction; import org.elasticsearch.xpack.core.deprecation.DeprecationIssue; -import java.io.IOException; import java.util.List; import static java.util.Collections.singletonList; @@ -24,229 +20,33 @@ public class IndexDeprecationChecksTests extends ESTestCase { - private static void assertSettingsAndIssue(String key, String value, DeprecationIssue expected) { + public void testOldIndicesCheck() { + Version createdWith = VersionUtils.randomVersionBetween(random(), Version.V_5_0_0, + VersionUtils.getPreviousVersion(Version.V_6_0_0_alpha1)); IndexMetaData indexMetaData = IndexMetaData.builder("test") - .settings(settings(Version.V_5_6_0) - .put(key, value)) + .settings(settings(createdWith)) .numberOfShards(1) .numberOfReplicas(0) .build(); - List issues = DeprecationInfoAction.filterChecks(INDEX_SETTINGS_CHECKS, c -> c.apply(indexMetaData)); - assertEquals(singletonList(expected), issues); - } - - public void testCoerceBooleanDeprecation() throws IOException { - XContentBuilder mapping = XContentFactory.jsonBuilder(); - mapping.startObject(); { - mapping.startObject("_all"); { - mapping.field("enabled", false); - } - mapping.endObject(); - mapping.startObject("properties"); { - mapping.startObject("my_boolean"); { - mapping.field("type", "boolean"); - } - mapping.endObject(); - mapping.startObject("my_object"); { - mapping.startObject("properties"); { - mapping.startObject("my_inner_boolean"); { - mapping.field("type", "boolean"); - } - mapping.endObject(); - mapping.startObject("my_text"); { - mapping.field("type", "text"); - mapping.startObject("fields"); { - mapping.startObject("raw"); { - mapping.field("type", "boolean"); - } - mapping.endObject(); - } - mapping.endObject(); - } - mapping.endObject(); - } - mapping.endObject(); - } - mapping.endObject(); - } - mapping.endObject(); - } - mapping.endObject(); - - IndexMetaData indexMetaData = IndexMetaData.builder("test") - .putMapping("testBooleanCoercion", Strings.toString(mapping)) - .settings(settings(Version.V_5_6_0)) - .numberOfShards(1) - .numberOfReplicas(0) - .build(); - - DeprecationIssue expected = new DeprecationIssue(DeprecationIssue.Level.INFO, - "Coercion of boolean fields", - "https://www.elastic.co/guide/en/elasticsearch/reference/6.0/" + - "breaking_60_mappings_changes.html#_coercion_of_boolean_fields", - "[[type: testBooleanCoercion, field: my_boolean], [type: testBooleanCoercion, field: my_inner_boolean]," + - " [type: testBooleanCoercion, field: my_text, multifield: raw]]"); - List issues = DeprecationInfoAction.filterChecks(INDEX_SETTINGS_CHECKS, c -> c.apply(indexMetaData)); - assertEquals(singletonList(expected), issues); - } - - public void testAllMetaFieldIsDisabledByDefaultCheck() throws IOException { - XContentBuilder mapping = XContentFactory.jsonBuilder(); - mapping.startObject(); { - mapping.startObject("_all"); { - mapping.field("enabled", randomFrom("1", 1, "true", true)); - } - mapping.endObject(); - } - mapping.endObject(); - - IndexMetaData indexMetaData = IndexMetaData.builder("test") - .putMapping("testAllEnabled", Strings.toString(mapping)) - .settings(settings(Version.V_5_6_0)) - .numberOfShards(1) - .numberOfReplicas(0) - .build(); - - DeprecationIssue expected = new DeprecationIssue(DeprecationIssue.Level.INFO, - "The _all meta field is disabled by default on indices created in 6.0", - "https://www.elastic.co/guide/en/elasticsearch/reference/6.0/" + - "breaking_60_mappings_changes.html#_the_literal__all_literal_meta_field_is_now_disabled_by_default", - "types: [testAllEnabled]"); - List issues = DeprecationChecks.filterChecks(INDEX_SETTINGS_CHECKS, c -> c.apply(indexMetaData)); - assertEquals(singletonList(expected), issues); - } - - public void testIncludeInAllCheck() throws IOException { - XContentBuilder mapping = XContentFactory.jsonBuilder(); - mapping.startObject(); { - mapping.startObject("_all"); { - mapping.field("enabled", false); - } - mapping.endObject(); - mapping.startObject("properties"); { - mapping.startObject("my_field"); { - mapping.field("type", "text"); - mapping.field("include_in_all", false); - } - mapping.endObject(); - } - mapping.endObject(); - } - mapping.endObject(); - - IndexMetaData indexMetaData = IndexMetaData.builder("test") - .putMapping("testIncludeInAll", Strings.toString(mapping)) - .settings(settings(Version.V_5_6_0)) - .numberOfShards(1) - .numberOfReplicas(0) - .build(); - DeprecationIssue expected = new DeprecationIssue(DeprecationIssue.Level.CRITICAL, - "The [include_in_all] mapping parameter is now disallowed", - "https://www.elastic.co/guide/en/elasticsearch/reference/6.0/" + - "breaking_60_mappings_changes.html#_the_literal_include_in_all_literal_mapping_parameter_is_now_disallowed", - "[[type: testIncludeInAll, field: my_field]]"); + "Index created before 6.0", + "https://www.elastic.co/guide/en/elasticsearch/reference/master/" + + "breaking-changes-7.0.html", + "this index was created using version: " + createdWith); List issues = DeprecationChecks.filterChecks(INDEX_SETTINGS_CHECKS, c -> c.apply(indexMetaData)); assertEquals(singletonList(expected), issues); } - public void testMatchMappingTypeCheck() throws IOException { - XContentBuilder mapping = XContentFactory.jsonBuilder(); - mapping.startObject(); { - mapping.startObject("_all"); { - mapping.field("enabled", false); - } - mapping.endObject(); - mapping.startArray("dynamic_templates"); - { - mapping.startObject(); - { - mapping.startObject("integers"); - { - mapping.field("match_mapping_type", "UNKNOWN_VALUE"); - mapping.startObject("mapping"); - { - mapping.field("type", "integer"); - } - mapping.endObject(); - } - mapping.endObject(); - } - mapping.endObject(); - } - mapping.endArray(); - } - mapping.endObject(); - - IndexMetaData indexMetaData = IndexMetaData.builder("test") - .putMapping("test", Strings.toString(mapping)) - .settings(settings(Version.V_5_6_0)) - .numberOfShards(1) - .numberOfReplicas(0) - .build(); - - DeprecationIssue expected = new DeprecationIssue(DeprecationIssue.Level.CRITICAL, - "Unrecognized match_mapping_type options not silently ignored", - "https://www.elastic.co/guide/en/elasticsearch/reference/6.0/" + - "breaking_60_mappings_changes.html#_unrecognized_literal_match_mapping_type_literal_options_not_silently_ignored", - "[type: test, dynamicFieldDefinitionintegers, unknown match_mapping_type[UNKNOWN_VALUE]]"); - List issues = DeprecationInfoAction.filterChecks(INDEX_SETTINGS_CHECKS, c -> c.apply(indexMetaData)); - assertEquals(singletonList(expected), issues); - } - - public void testBaseSimilarityDefinedCheck() { - assertSettingsAndIssue("index.similarity.base.type", "classic", - new DeprecationIssue(DeprecationIssue.Level.WARNING, - "The base similarity is now ignored as coords and query normalization have been removed." + - "If provided, this setting will be ignored and issue a deprecation warning", - "https://www.elastic.co/guide/en/elasticsearch/reference/6.0/" + - "breaking_60_settings_changes.html#_similarity_settings", null)); - } - - public void testIndexStoreTypeCheck() { - assertSettingsAndIssue("index.store.type", "niofs", - new DeprecationIssue(DeprecationIssue.Level.CRITICAL, - "The default index.store.type has been removed. If you were using it, " + - "we advise that you simply remove it from your index settings and Elasticsearch" + - "will use the best store implementation for your operating system.", - "https://www.elastic.co/guide/en/elasticsearch/reference/6.0/" + - "breaking_60_settings_changes.html#_store_settings", null)); - } - public void testStoreThrottleSettingsCheck() { - assertSettingsAndIssue("index.store.throttle.max_bytes_per_sec", "32", - new DeprecationIssue(DeprecationIssue.Level.CRITICAL, - "index.store.throttle settings are no longer recognized. these settings should be removed", - "https://www.elastic.co/guide/en/elasticsearch/reference/6.0/" + - "breaking_60_settings_changes.html#_store_throttling_settings", - "present settings: [index.store.throttle.max_bytes_per_sec]")); - assertSettingsAndIssue("index.store.throttle.type", "none", - new DeprecationIssue(DeprecationIssue.Level.CRITICAL, - "index.store.throttle settings are no longer recognized. these settings should be removed", - "https://www.elastic.co/guide/en/elasticsearch/reference/6.0/" + - "breaking_60_settings_changes.html#_store_throttling_settings", - "present settings: [index.store.throttle.type]")); - } - - public void testSharedFileSystemSettingsCheck() { - assertSettingsAndIssue("index.shared_filesystem", "true", - new DeprecationIssue(DeprecationIssue.Level.CRITICAL, - "[index.shared_filesystem] setting should be removed", - "https://www.elastic.co/guide/en/elasticsearch/reference/6.0/" + - "breaking_60_indices_changes.html#_shadow_replicas_have_been_removed", null)); - } - - public void testDelimitedPayloadFilterCheck() throws IOException { + public void testDelimitedPayloadFilterCheck() { Settings settings = settings( - VersionUtils.randomVersionBetween(random(), Version.V_5_0_0, VersionUtils.getPreviousVersion(Version.CURRENT))) - .put("index.analysis.filter.my_delimited_payload_filter.type", "delimited_payload_filter") - .put("index.analysis.filter.my_delimited_payload_filter.delimiter", "^") - .put("index.analysis.filter.my_delimited_payload_filter.encoding", "identity").build(); - + VersionUtils.randomVersionBetween(random(), Version.V_6_0_0_alpha1, VersionUtils.getPreviousVersion(Version.CURRENT))) + .put("index.analysis.filter.my_delimited_payload_filter.type", "delimited_payload_filter") + .put("index.analysis.filter.my_delimited_payload_filter.delimiter", "^") + .put("index.analysis.filter.my_delimited_payload_filter.encoding", "identity").build(); IndexMetaData indexMetaData = IndexMetaData.builder("test").settings(settings).numberOfShards(1).numberOfReplicas(0).build(); - DeprecationIssue expected = new DeprecationIssue(DeprecationIssue.Level.WARNING, "Use of 'delimited_payload_filter'.", - "https://www.elastic.co/guide/en/elasticsearch/reference/master/breaking_70_analysis_changes.html", - "[The filter [my_delimited_payload_filter] is of deprecated 'delimited_payload_filter' type. " + "https://www.elastic.co/guide/en/elasticsearch/reference/master/breaking_70_analysis_changes.html", + "[The filter [my_delimited_payload_filter] is of deprecated 'delimited_payload_filter' type. " + "The filter type should be changed to 'delimited_payload'.]"); List issues = DeprecationInfoAction.filterChecks(INDEX_SETTINGS_CHECKS, c -> c.apply(indexMetaData)); assertEquals(singletonList(expected), issues); diff --git a/x-pack/plugin/deprecation/src/test/java/org/elasticsearch/xpack/deprecation/NodeDeprecationChecksTests.java b/x-pack/plugin/deprecation/src/test/java/org/elasticsearch/xpack/deprecation/NodeDeprecationChecksTests.java new file mode 100644 index 0000000000000..050d4aaf89ddb --- /dev/null +++ b/x-pack/plugin/deprecation/src/test/java/org/elasticsearch/xpack/deprecation/NodeDeprecationChecksTests.java @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.deprecation; + +import org.elasticsearch.Build; +import org.elasticsearch.Version; +import org.elasticsearch.action.admin.cluster.node.info.NodeInfo; +import org.elasticsearch.action.admin.cluster.node.info.PluginsAndModules; +import org.elasticsearch.action.admin.cluster.node.stats.NodeStats; +import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.transport.TransportAddress; +import org.elasticsearch.monitor.fs.FsInfo; +import org.elasticsearch.monitor.os.OsInfo; +import org.elasticsearch.plugins.PluginInfo; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.VersionUtils; +import org.elasticsearch.xpack.core.deprecation.DeprecationIssue; +import org.junit.Before; + +import java.util.Collections; +import java.util.List; + +import static java.util.Collections.singletonList; +import static org.elasticsearch.xpack.deprecation.DeprecationChecks.NODE_SETTINGS_CHECKS; + +public class NodeDeprecationChecksTests extends ESTestCase { + private DiscoveryNode discoveryNode; + private FsInfo.Path[] paths; + private OsInfo osInfo; + private PluginsAndModules pluginsAndModules; + + @Before + public void setupDefaults() { + discoveryNode = DiscoveryNode.createLocal(Settings.builder().put("node.name", "node_check").build(), + new TransportAddress(TransportAddress.META_ADDRESS, 9200), "test"); + paths = new FsInfo.Path[] {}; + osInfo = new OsInfo(0L, 1, 1, randomAlphaOfLength(10), + "foo-64", randomAlphaOfLength(10), randomAlphaOfLength(10)); + pluginsAndModules = new PluginsAndModules(Collections.emptyList(), Collections.emptyList()); + } + + private void assertSettingsAndIssue(String key, String value, DeprecationIssue expected) { + Settings settings = Settings.builder() + .put("cluster.name", "elasticsearch") + .put("node.name", "node_check") + .put(key, value) + .build(); + List nodeInfos = Collections.singletonList(new NodeInfo(Version.CURRENT, Build.CURRENT, + discoveryNode, settings, osInfo, null, null, + null, null, null, pluginsAndModules, null, null)); + List nodeStats = Collections.singletonList(new NodeStats(discoveryNode, 0L, null, + null, null, null, null, new FsInfo(0L, null, paths), null, null, null, + null, null, null, null)); + List issues = DeprecationChecks.filterChecks(NODE_SETTINGS_CHECKS, c -> c.apply(nodeInfos, nodeStats)); + assertEquals(singletonList(expected), issues); + } + + public void testAzurePluginCheck() { + Version esVersion = VersionUtils.randomVersionBetween(random(), Version.V_6_0_0, Version.CURRENT); + PluginInfo deprecatedPlugin = new PluginInfo( + "repository-azure", "dummy plugin description", "dummy_plugin_version", esVersion, + "javaVersion", "DummyPluginName", Collections.emptyList(), false); + pluginsAndModules = new PluginsAndModules(Collections.singletonList(deprecatedPlugin), Collections.emptyList()); + + DeprecationIssue expected = new DeprecationIssue(DeprecationIssue.Level.WARNING, + "Azure Repository settings changed", + "https://www.elastic.co/guide/en/elasticsearch/reference/master/breaking_70_cluster_changes.html" + + "#_azure_repository_plugin", + "nodes with repository-azure installed: [node_check]"); + assertSettingsAndIssue("foo", "bar", expected); + } + + public void testGCSPluginCheck() { + Version esVersion = VersionUtils.randomVersionBetween(random(), Version.V_6_0_0, Version.CURRENT); + PluginInfo deprecatedPlugin = new PluginInfo( + "repository-gcs", "dummy plugin description", "dummy_plugin_version", esVersion, + "javaVersion", "DummyPluginName", Collections.emptyList(), false); + pluginsAndModules = new PluginsAndModules(Collections.singletonList(deprecatedPlugin), Collections.emptyList()); + + DeprecationIssue expected = new DeprecationIssue(DeprecationIssue.Level.WARNING, + "GCS Repository settings changed", + "https://www.elastic.co/guide/en/elasticsearch/reference/master/breaking_70_cluster_changes.html" + + "#_google_cloud_storage_repository_plugin", + "nodes with repository-gcs installed: [node_check]"); + assertSettingsAndIssue("foo", "bar", expected); + } +} From 424b0a93f3054f6291cc60cb0609d1c2b8b4c8a5 Mon Sep 17 00:00:00 2001 From: lcawl Date: Mon, 3 Dec 2018 12:17:26 -0800 Subject: [PATCH 086/115] [DOCS] Fixes peer link --- .../painless-contexts/painless-similarity-context.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/painless/painless-contexts/painless-similarity-context.asciidoc b/docs/painless/painless-contexts/painless-similarity-context.asciidoc index a8d66233e66cc..58609ce705e10 100644 --- a/docs/painless/painless-contexts/painless-similarity-context.asciidoc +++ b/docs/painless/painless-contexts/painless-similarity-context.asciidoc @@ -49,7 +49,7 @@ documents in a query. document for the current field. Note that the `query`, `field`, and `term` variables are also available to the -{painless}/painless-weight-context.html[weight context]. They are more efficiently used +<>. They are more efficiently used there, as they are constant for all documents. For queries that contain multiple terms, the script is called once for each From ed92480a8550a3fc7be61e33f86be0dfd0871733 Mon Sep 17 00:00:00 2001 From: lcawl Date: Mon, 3 Dec 2018 12:38:53 -0800 Subject: [PATCH 087/115] [DOCs] More broken painless links --- .../painless-contexts/painless-similarity-context.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/painless/painless-contexts/painless-similarity-context.asciidoc b/docs/painless/painless-contexts/painless-similarity-context.asciidoc index 58609ce705e10..1d847f516c8be 100644 --- a/docs/painless/painless-contexts/painless-similarity-context.asciidoc +++ b/docs/painless/painless-contexts/painless-similarity-context.asciidoc @@ -16,7 +16,7 @@ documents in a query. User-defined parameters passed in at query-time. `weight` (`float`, read-only):: - The weight as calculated by a {ref}/painless-weight-context[weight script] + The weight as calculated by a <> `query.boost` (`float`, read-only):: The boost value if provided by the query. If this is not provided the From c6cf11e98fb19c07b8b56e86122a3bbe6e3552ef Mon Sep 17 00:00:00 2001 From: Jim Ferenczi Date: Mon, 3 Dec 2018 22:15:13 +0100 Subject: [PATCH 088/115] Deprecate the query filter context (#36156) This change deprecates the Elasticsearch's filter context and adds a deprecation warning to bool queries that automatically set their minimum should match to 1. Relates #35354 --- .../migration/migrate_6_0/search.asciidoc | 12 ++++++- .../index/query/AbstractQueryBuilder.java | 6 +++- .../index/query/BoolQueryBuilder.java | 11 +++++++ .../index/query/QueryShardContext.java | 3 ++ .../index/query/BoolQueryBuilderTests.java | 33 +++++++++++++++++++ .../bucket/filter/FilterAggregatorTests.java | 3 ++ .../bucket/filter/FiltersAggregatorTests.java | 3 ++ .../SignificantTermsAggregatorTests.java | 3 ++ .../test/AbstractQueryTestCase.java | 11 +++++++ .../RollupResponseTranslationTests.java | 4 +-- 10 files changed, 85 insertions(+), 4 deletions(-) diff --git a/docs/reference/migration/migrate_6_0/search.asciidoc b/docs/reference/migration/migrate_6_0/search.asciidoc index 3ddc620862c00..5b57fff8920d0 100644 --- a/docs/reference/migration/migrate_6_0/search.asciidoc +++ b/docs/reference/migration/migrate_6_0/search.asciidoc @@ -267,4 +267,14 @@ rewrite any prefix query on the field to a a single term query that matches the ==== Negative boosts are deprecated Setting a negative `boost` in a query is deprecated and will throw an error in the next version. -To deboost a specific query you can use a `boost` comprise between 0 and 1. \ No newline at end of file +To deboost a specific query you can use a `boost` comprise between 0 and 1. + +[float] +==== The filter context is deprecated + + The `filter` context is deprecated in Elasticsearch's query builders, +the distinction between queries and filters is decided in Lucene depending +on whether queries need to access score or not. As a result `bool` queries with +`should` clauses that don't need to access the score will issue a deprecation +warning when they automatically set `minimum_should_match` to 1. +This behavior will be removed in the next major version. \ No newline at end of file diff --git a/server/src/main/java/org/elasticsearch/index/query/AbstractQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/AbstractQueryBuilder.java index c330de507cd75..183d586454ca1 100644 --- a/server/src/main/java/org/elasticsearch/index/query/AbstractQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/AbstractQueryBuilder.java @@ -374,6 +374,10 @@ protected static void declareStandardFields(AbstractObjectParser { public static final String NAME = "bool"; + private static final DeprecationLogger deprecationLogger = new DeprecationLogger( + LogManager.getLogger(BoolQueryBuilder.class) + ); public static final boolean ADJUST_PURE_NEGATIVE_DEFAULT = true; @@ -386,6 +391,12 @@ protected Query doToQuery(QueryShardContext context) throws IOException { final String minimumShouldMatch; if (context.isFilter() && this.minimumShouldMatch == null && shouldClauses.size() > 0) { + if (mustClauses.size() > 0 || mustNotClauses.size() > 0 || filterClauses.size() > 0) { + deprecationLogger.deprecatedAndMaybeLog("filter_context_min_should_match", + "Should clauses in the filter context will no longer automatically set the minimum should " + + "match to 1 in the next major version. You should group them in a [filter] clause or explicitly set " + + "[minimum_should_match] to 1 to restore this behavior in the next major version." ); + } minimumShouldMatch = "1"; } else { minimumShouldMatch = this.minimumShouldMatch; diff --git a/server/src/main/java/org/elasticsearch/index/query/QueryShardContext.java b/server/src/main/java/org/elasticsearch/index/query/QueryShardContext.java index eee2c67af4d2d..329c36fcf42f4 100644 --- a/server/src/main/java/org/elasticsearch/index/query/QueryShardContext.java +++ b/server/src/main/java/org/elasticsearch/index/query/QueryShardContext.java @@ -179,7 +179,10 @@ public Map copyNamedQueries() { /** * Return whether we are currently parsing a filter or a query. + * @deprecated The distinction between query and filter context is removed + * in the next major version. */ + @Deprecated public boolean isFilter() { return isFilter; } diff --git a/server/src/test/java/org/elasticsearch/index/query/BoolQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/BoolQueryBuilderTests.java index fae252728d46d..2a520792522c1 100644 --- a/server/src/test/java/org/elasticsearch/index/query/BoolQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/BoolQueryBuilderTests.java @@ -235,6 +235,9 @@ public void testMinShouldMatchFilterWithShouldClauses() throws Exception { BooleanClause innerBooleanClause2 = innerBooleanQuery.clauses().get(1); assertThat(innerBooleanClause2.getOccur(), equalTo(BooleanClause.Occur.SHOULD)); assertThat(innerBooleanClause2.getQuery(), instanceOf(MatchAllDocsQuery.class)); + assertWarnings("Should clauses in the filter context will no longer automatically set the minimum should" + + " match to 1 in the next major version. You should group them in a [filter] clause or explicitly set" + + " [minimum_should_match] to 1 to restore this behavior in the next major version."); } public void testMinShouldMatchBiggerThanNumberOfShouldClauses() throws Exception { @@ -441,4 +444,34 @@ public void testRewriteWithMatchNone() throws IOException { rewritten = Rewriteable.rewrite(boolQueryBuilder, createShardContext()); assertEquals(new MatchNoneQueryBuilder(), rewritten); } + + public void testShouldFilterContextDeprecation() throws Exception { + QueryShardContext context = createShardContext(); + BoolQueryBuilder bq = new BoolQueryBuilder() + .should(new MatchAllQueryBuilder()) + .filter(new TermQueryBuilder("foo", "bar")); + bq.doToQuery(context); + BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder(); + boolQueryBuilder.should(bq); + boolQueryBuilder.doToQuery(context); + + boolQueryBuilder = new BoolQueryBuilder(); + boolQueryBuilder.filter(bq); + boolQueryBuilder.doToQuery(context); + assertWarnings("Should clauses in the filter context will no longer automatically set the minimum should" + + " match to 1 in the next major version. You should group them in a [filter] clause or explicitly set" + + " [minimum_should_match] to 1 to restore this behavior in the next major version."); + + ConstantScoreQueryBuilder query = new ConstantScoreQueryBuilder(bq); + query.doToQuery(context); + assertWarnings("Should clauses in the filter context will no longer automatically set the minimum should" + + " match to 1 in the next major version. You should group them in a [filter] clause or explicitly set" + + " [minimum_should_match] to 1 to restore this behavior in the next major version."); + + bq = new BoolQueryBuilder() + .should(new MatchAllQueryBuilder()); + boolQueryBuilder = new BoolQueryBuilder(); + boolQueryBuilder.filter(bq); + boolQueryBuilder.doToQuery(context); + } } diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/filter/FilterAggregatorTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/filter/FilterAggregatorTests.java index f3d057d8e8cd0..2877f4df0f46b 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/filter/FilterAggregatorTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/filter/FilterAggregatorTests.java @@ -124,5 +124,8 @@ public void testParsedAsFilter() throws IOException { // means the bool query has been parsed as a filter, if it was a query minShouldMatch would // be 0 assertEquals(1, ((BooleanQuery) parsedQuery).getMinimumNumberShouldMatch()); + assertWarnings("Should clauses in the filter context will no longer automatically set the minimum should" + + " match to 1 in the next major version. You should group them in a [filter] clause or explicitly set" + + " [minimum_should_match] to 1 to restore this behavior in the next major version."); } } diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/filter/FiltersAggregatorTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/filter/FiltersAggregatorTests.java index 6fdf207249f43..ea59df7c4efef 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/filter/FiltersAggregatorTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/filter/FiltersAggregatorTests.java @@ -220,5 +220,8 @@ public void testParsedAsFilter() throws IOException { // means the bool query has been parsed as a filter, if it was a query minShouldMatch would // be 0 assertEquals(1, ((BooleanQuery) parsedQuery).getMinimumNumberShouldMatch()); + assertWarnings("Should clauses in the filter context will no longer automatically set the minimum should" + + " match to 1 in the next major version. You should group them in a [filter] clause or explicitly set" + + " [minimum_should_match] to 1 to restore this behavior in the next major version."); } } diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantTermsAggregatorTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantTermsAggregatorTests.java index f6c6cfafe3c85..3c4df69d9b8fd 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantTermsAggregatorTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantTermsAggregatorTests.java @@ -106,6 +106,9 @@ public void testParsedAsFilter() throws IOException { // means the bool query has been parsed as a filter, if it was a query minShouldMatch would // be 0 assertEquals(1, ((BooleanQuery) parsedQuery).getMinimumNumberShouldMatch()); + assertWarnings("Should clauses in the filter context will no longer automatically set the minimum should" + + " match to 1 in the next major version. You should group them in a [filter] clause or explicitly set" + + " [minimum_should_match] to 1 to restore this behavior in the next major version."); } /** diff --git a/test/framework/src/main/java/org/elasticsearch/test/AbstractQueryTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/AbstractQueryTestCase.java index 027152deedba6..7647a9e6d6546 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/AbstractQueryTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/AbstractQueryTestCase.java @@ -48,6 +48,7 @@ import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.index.query.AbstractQueryBuilder; +import org.elasticsearch.index.query.BoolQueryBuilder; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryRewriteContext; import org.elasticsearch.index.query.QueryShardContext; @@ -471,6 +472,16 @@ public void testToQuery() throws IOException { context.setIsFilter(filterFlag); rewriteQuery(firstQuery, context).toQuery(context); assertEquals("isFilter should be unchanged", filterFlag, context.isFilter()); + if (filterFlag && firstQuery instanceof BoolQueryBuilder) { + BoolQueryBuilder bq = (BoolQueryBuilder) firstQuery; + if (bq.should().size() > 0 && + bq.minimumShouldMatch() == null && + (bq.filter().size() > 0 || bq.must().size() > 0 || bq.mustNot().size() > 0)) { + assertWarnings("Should clauses in the filter context will no longer automatically set the minimum" + + " should match to 1 in the next major version. You should group them in a [filter] clause or explicitly set" + + " [minimum_should_match] to 1 to restore this behavior in the next major version."); + } + } } } diff --git a/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/RollupResponseTranslationTests.java b/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/RollupResponseTranslationTests.java index 73a4d0665c4e1..f61e8c3f7fa03 100644 --- a/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/RollupResponseTranslationTests.java +++ b/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/RollupResponseTranslationTests.java @@ -443,8 +443,8 @@ public void testUnsupportedMultiBucket() throws IOException { fieldType.setIndexOptions(IndexOptions.DOCS); fieldType.setName("foo"); QueryBuilder filter = QueryBuilders.boolQuery() - .must(QueryBuilders.termQuery("field", "foo")) - .should(QueryBuilders.termQuery("field", "bar")); + .filter(QueryBuilders.termQuery("field", "foo")) + .filter(QueryBuilders.termQuery("field", "bar")); SignificantTermsAggregationBuilder builder = new SignificantTermsAggregationBuilder( "test", ValueType.STRING) .field("field") From 234e70d6174cbc6db31c43a3d1da982af1ec2130 Mon Sep 17 00:00:00 2001 From: Nik Everett Date: Fri, 30 Nov 2018 16:06:58 -0500 Subject: [PATCH 089/115] Tasks: Retry if task can't be written (#35054) Adds about a minute worth of backoffs and retries to saving task results so it is *much* more likely that a busy cluster won't lose task results. This isn't an ideal solution to losing task results, but it is an incremental improvement. If all of the retries fail when still log the task result, but that is far from ideal. Closes #33764 --- .../tasks/TaskResultsService.java | 34 ++++- .../node/tasks/TaskStorageRetryIT.java | 127 ++++++++++++++++++ .../tasks/TaskResultsServiceTests.java | 40 ++++++ 3 files changed, 195 insertions(+), 6 deletions(-) create mode 100644 server/src/test/java/org/elasticsearch/action/admin/cluster/node/tasks/TaskStorageRetryIT.java create mode 100644 server/src/test/java/org/elasticsearch/tasks/TaskResultsServiceTests.java diff --git a/server/src/main/java/org/elasticsearch/tasks/TaskResultsService.java b/server/src/main/java/org/elasticsearch/tasks/TaskResultsService.java index b968d17d7e94a..b05e87db91943 100644 --- a/server/src/main/java/org/elasticsearch/tasks/TaskResultsService.java +++ b/server/src/main/java/org/elasticsearch/tasks/TaskResultsService.java @@ -27,7 +27,7 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.admin.indices.create.CreateIndexRequest; import org.elasticsearch.action.admin.indices.create.CreateIndexResponse; -import org.elasticsearch.action.admin.indices.create.TransportCreateIndexAction; +import org.elasticsearch.action.bulk.BackoffPolicy; import org.elasticsearch.action.index.IndexRequestBuilder; import org.elasticsearch.action.index.IndexResponse; import org.elasticsearch.action.support.master.AcknowledgedResponse; @@ -40,18 +40,23 @@ import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.util.concurrent.EsRejectedExecutionException; import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.core.internal.io.Streams; +import org.elasticsearch.threadpool.ThreadPool; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; +import java.util.Iterator; import java.util.Map; +import static org.elasticsearch.common.unit.TimeValue.timeValueMillis; import static org.elasticsearch.action.admin.cluster.node.tasks.get.GetTaskAction.TASKS_ORIGIN; /** @@ -71,18 +76,24 @@ public class TaskResultsService { public static final int TASK_RESULT_MAPPING_VERSION = 2; + /** + * The backoff policy to use when saving a task result fails. The total wait + * time is 600000 milliseconds, ten minutes. + */ + static final BackoffPolicy STORE_BACKOFF_POLICY = + BackoffPolicy.exponentialBackoff(timeValueMillis(250), 14); + private final Client client; private final ClusterService clusterService; - private final TransportCreateIndexAction createIndexAction; + private final ThreadPool threadPool; @Inject - public TaskResultsService(Client client, ClusterService clusterService, - TransportCreateIndexAction createIndexAction) { + public TaskResultsService(Client client, ClusterService clusterService, ThreadPool threadPool) { this.client = new OriginSettingClient(client, TASKS_ORIGIN); this.clusterService = clusterService; - this.createIndexAction = createIndexAction; + this.threadPool = threadPool; } public void storeResult(TaskResult taskResult, ActionListener listener) { @@ -161,6 +172,10 @@ private void doStoreResult(TaskResult taskResult, ActionListener listener) } catch (IOException e) { throw new ElasticsearchException("Couldn't convert task result to XContent for [{}]", e, taskResult.getTask()); } + doStoreResult(STORE_BACKOFF_POLICY.iterator(), index, listener); + } + + private void doStoreResult(Iterator backoff, IndexRequestBuilder index, ActionListener listener) { index.execute(new ActionListener() { @Override public void onResponse(IndexResponse indexResponse) { @@ -169,7 +184,14 @@ public void onResponse(IndexResponse indexResponse) { @Override public void onFailure(Exception e) { - listener.onFailure(e); + if (false == (e instanceof EsRejectedExecutionException) + || false == backoff.hasNext()) { + listener.onFailure(e); + } else { + TimeValue wait = backoff.next(); + logger.warn(() -> new ParameterizedMessage("failed to store task result, retrying in [{}]", wait), e); + threadPool.schedule(wait, ThreadPool.Names.SAME, () -> doStoreResult(backoff, index, listener)); + } } }); } diff --git a/server/src/test/java/org/elasticsearch/action/admin/cluster/node/tasks/TaskStorageRetryIT.java b/server/src/test/java/org/elasticsearch/action/admin/cluster/node/tasks/TaskStorageRetryIT.java new file mode 100644 index 0000000000000..a2e645b457a8a --- /dev/null +++ b/server/src/test/java/org/elasticsearch/action/admin/cluster/node/tasks/TaskStorageRetryIT.java @@ -0,0 +1,127 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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.elasticsearch.action.admin.cluster.node.tasks; + +import org.elasticsearch.action.admin.cluster.node.tasks.get.GetTaskResponse; +import org.elasticsearch.action.support.PlainListenableActionFuture; +import org.elasticsearch.client.node.NodeClient; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.tasks.Task; +import org.elasticsearch.tasks.TaskId; +import org.elasticsearch.test.ESSingleNodeTestCase; +import org.elasticsearch.threadpool.ThreadPool; + +import java.util.Arrays; +import java.util.Collection; +import java.util.concurrent.CyclicBarrier; +import java.util.concurrent.TimeUnit; + +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonMap; + +/** + * Makes sure that tasks that attempt to store themselves on completion retry if + * they don't succeed at first. + */ +public class TaskStorageRetryIT extends ESSingleNodeTestCase { + @Override + protected Collection> getPlugins() { + return Arrays.asList(TestTaskPlugin.class); + } + + /** + * Lower the queue sizes to be small enough that both bulk and searches will time out and have to be retried. + */ + @Override + protected Settings nodeSettings() { + return Settings.builder() + .put(super.nodeSettings()) + .put("thread_pool.write.size", 2) + .put("thread_pool.write.queue_size", 0) + .build(); + } + + public void testRetry() throws Exception { + logger.info("block the write executor"); + CyclicBarrier barrier = new CyclicBarrier(2); + getInstanceFromNode(ThreadPool.class).executor(ThreadPool.Names.WRITE).execute(() -> { + try { + barrier.await(); + logger.info("blocking the write executor"); + barrier.await(); + logger.info("unblocked the write executor"); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + barrier.await(); + Task task; + PlainListenableActionFuture future = + PlainListenableActionFuture.newListenableFuture(); + try { + logger.info("start a task that will store its results"); + TestTaskPlugin.NodesRequest req = new TestTaskPlugin.NodesRequest("foo"); + req.setShouldStoreResult(true); + req.setShouldBlock(false); + task = nodeClient().executeLocally(TestTaskPlugin.TestTaskAction.INSTANCE, req, future); + + logger.info("verify that the task has started and is still running"); + assertBusy(() -> { + GetTaskResponse runningTask = client().admin().cluster() + .prepareGetTask(new TaskId(nodeClient().getLocalNodeId(), task.getId())) + .get(); + assertNotNull(runningTask.getTask()); + assertFalse(runningTask.getTask().isCompleted()); + assertEquals(emptyMap(), runningTask.getTask().getErrorAsMap()); + assertEquals(emptyMap(), runningTask.getTask().getResponseAsMap()); + assertFalse(future.isDone()); + }); + } finally { + logger.info("unblock the write executor"); + barrier.await(); + } + + logger.info("wait for the task to finish"); + future.get(10, TimeUnit.SECONDS); + + logger.info("check that it was written successfully"); + GetTaskResponse finishedTask = client().admin().cluster() + .prepareGetTask(new TaskId(nodeClient().getLocalNodeId(), task.getId())) + .get(); + assertTrue(finishedTask.getTask().isCompleted()); + assertEquals(emptyMap(), finishedTask.getTask().getErrorAsMap()); + assertEquals(singletonMap("failure_count", 0), + finishedTask.getTask().getResponseAsMap()); + } + + /** + * Get the {@linkplain NodeClient} local to the node being tested. + */ + private NodeClient nodeClient() { + /* + * Luckilly our test infrastructure already returns it, but we can't + * change the return type in the superclass because it is wrapped other + * places. + */ + return (NodeClient) client(); + } +} + diff --git a/server/src/test/java/org/elasticsearch/tasks/TaskResultsServiceTests.java b/server/src/test/java/org/elasticsearch/tasks/TaskResultsServiceTests.java new file mode 100644 index 0000000000000..7c896cb21595d --- /dev/null +++ b/server/src/test/java/org/elasticsearch/tasks/TaskResultsServiceTests.java @@ -0,0 +1,40 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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.elasticsearch.tasks; + +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.test.ESTestCase; + +import java.util.Iterator; + +/** + * Makes sure that tasks that attempt to store themselves on completion retry if + * they don't succeed at first. + */ +public class TaskResultsServiceTests extends ESTestCase { + public void testRetryTotalTime() { + Iterator times = TaskResultsService.STORE_BACKOFF_POLICY.iterator(); + long total = 0; + while (times.hasNext()) { + total += times.next().millis(); + } + assertEquals(600000L, total); + } +} \ No newline at end of file From 2658e34a5d521990b882b96be938bc3827a49d56 Mon Sep 17 00:00:00 2001 From: Gordon Brown Date: Mon, 3 Dec 2018 15:09:34 -0700 Subject: [PATCH 090/115] Fix occasional failure in deprecation test (#36172) Sometimes this test could end up trying to create an index with a negative or zero number of shards, which fails. This fixes the test. --- .../xpack/deprecation/ClusterDeprecationChecksTests.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugin/deprecation/src/test/java/org/elasticsearch/xpack/deprecation/ClusterDeprecationChecksTests.java b/x-pack/plugin/deprecation/src/test/java/org/elasticsearch/xpack/deprecation/ClusterDeprecationChecksTests.java index f2cdf401fbb00..dc9611c5e717a 100644 --- a/x-pack/plugin/deprecation/src/test/java/org/elasticsearch/xpack/deprecation/ClusterDeprecationChecksTests.java +++ b/x-pack/plugin/deprecation/src/test/java/org/elasticsearch/xpack/deprecation/ClusterDeprecationChecksTests.java @@ -25,7 +25,7 @@ public class ClusterDeprecationChecksTests extends ESTestCase { public void testCheckShardLimit() { - int shardsPerNode = randomIntBetween(1, 10000); + int shardsPerNode = randomIntBetween(2, 10000); int nodeCount = randomIntBetween(1, 10); int maxShardsInCluster = shardsPerNode * nodeCount; int currentOpenShards = maxShardsInCluster + randomIntBetween(0, 100); @@ -59,7 +59,7 @@ public void testCheckShardLimit() { MetaData goodMetaData = MetaData.builder(metaData).put(IndexMetaData.builder("test") .settings(settings(Version.CURRENT)) .numberOfReplicas(0) - .numberOfShards(maxShardsInCluster - randomIntBetween(1, 100))).build(); + .numberOfShards(maxShardsInCluster - randomIntBetween(1, (maxShardsInCluster - 1)))).build(); ClusterState goodState = ClusterState.builder(ClusterName.DEFAULT) .metaData(goodMetaData).nodes(state.nodes()).build(); issues = DeprecationChecks.filterChecks(CLUSTER_SETTINGS_CHECKS, c -> c.apply(goodState)); From 6c9cb728b2236d099aa415c8a8b3e70f61b51638 Mon Sep 17 00:00:00 2001 From: Andy Bristol Date: Mon, 3 Dec 2018 15:08:00 -0800 Subject: [PATCH 091/115] [test] generate unique user names (#36179) --- .../xpack/sql/qa/security/UserFunctionIT.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/x-pack/plugin/sql/qa/security/src/test/java/org/elasticsearch/xpack/sql/qa/security/UserFunctionIT.java b/x-pack/plugin/sql/qa/security/src/test/java/org/elasticsearch/xpack/sql/qa/security/UserFunctionIT.java index 68ae9fe9a068e..25c53e68e2513 100644 --- a/x-pack/plugin/sql/qa/security/src/test/java/org/elasticsearch/xpack/sql/qa/security/UserFunctionIT.java +++ b/x-pack/plugin/sql/qa/security/src/test/java/org/elasticsearch/xpack/sql/qa/security/UserFunctionIT.java @@ -59,11 +59,9 @@ protected String getProtocol() { private void setUpUsers() throws IOException { int usersCount = name.getMethodName().startsWith("testSingle") ? 1 : randomIntBetween(5, 15); users = new ArrayList(usersCount); - - for(int i = 0; i < usersCount; i++) { - String randomUserName = randomAlphaOfLengthBetween(1, 15); - users.add(randomUserName); - createUser(randomUserName, MINIMAL_ACCESS_ROLE); + users.addAll(randomUnique(() -> randomAlphaOfLengthBetween(1, 15), usersCount)); + for (String user : users) { + createUser(user, MINIMAL_ACCESS_ROLE); } } From 6cf5c849d834685d2e16f12591177c0c7521bba3 Mon Sep 17 00:00:00 2001 From: Andy Bristol Date: Mon, 3 Dec 2018 15:25:35 -0800 Subject: [PATCH 092/115] [test] mute testDeprecatedSettings --- .../java/org/elasticsearch/common/logging/EvilLoggerTests.java | 1 + 1 file changed, 1 insertion(+) diff --git a/qa/evil-tests/src/test/java/org/elasticsearch/common/logging/EvilLoggerTests.java b/qa/evil-tests/src/test/java/org/elasticsearch/common/logging/EvilLoggerTests.java index 017cf9eecf85f..847338ede4871 100644 --- a/qa/evil-tests/src/test/java/org/elasticsearch/common/logging/EvilLoggerTests.java +++ b/qa/evil-tests/src/test/java/org/elasticsearch/common/logging/EvilLoggerTests.java @@ -253,6 +253,7 @@ public void testDeprecationLoggerMaybeLog() throws IOException, UserException { } } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/35990") public void testDeprecatedSettings() throws IOException, UserException { setupLogging("settings"); From 4e0a403f0240c79f3ac23c9dff9eb43a22800ed2 Mon Sep 17 00:00:00 2001 From: Albert Zaharovits Date: Tue, 4 Dec 2018 01:35:05 +0200 Subject: [PATCH 093/115] Fix deprecation of audit log settings (#36175) I have botched deprecating the "prefix" logfile audit settings in #34475 , by not registering them. This commit fixes it and also adds a test that these deprecated settings are indeed still working and are dynamic. Closes #36162 --- .../logfile/DeprecatedLoggingAuditTrail.java | 8 ++-- .../audit/logfile/LoggingAuditTrail.java | 6 ++- .../AuditTrailSettingsUpdateTests.java | 44 +++++++++++++++++++ 3 files changed, 54 insertions(+), 4 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/logfile/DeprecatedLoggingAuditTrail.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/logfile/DeprecatedLoggingAuditTrail.java index c7a4d1964d35d..1f4d13bad9a2d 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/logfile/DeprecatedLoggingAuditTrail.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/logfile/DeprecatedLoggingAuditTrail.java @@ -98,9 +98,11 @@ public DeprecatedLoggingAuditTrail(Settings settings, ClusterService clusterServ // always read before `localNodeInfo` and `includeRequestBody`. this.events = parse(LoggingAuditTrail.INCLUDE_EVENT_SETTINGS.get(newSettings), LoggingAuditTrail.EXCLUDE_EVENT_SETTINGS.get(newSettings)); - }, Arrays.asList(LoggingAuditTrail.EMIT_HOST_ADDRESS_SETTING, LoggingAuditTrail.EMIT_HOST_NAME_SETTING, - LoggingAuditTrail.EMIT_NODE_NAME_SETTING, LoggingAuditTrail.INCLUDE_EVENT_SETTINGS, - LoggingAuditTrail.EXCLUDE_EVENT_SETTINGS, LoggingAuditTrail.INCLUDE_REQUEST_BODY)); + }, Arrays.asList(LoggingAuditTrail.EMIT_HOST_ADDRESS_SETTING, LoggingAuditTrail.DEPRECATED_EMIT_HOST_ADDRESS_SETTING, + LoggingAuditTrail.EMIT_HOST_NAME_SETTING, LoggingAuditTrail.DEPRECATED_EMIT_NODE_NAME_SETTING, + LoggingAuditTrail.EMIT_NODE_NAME_SETTING, LoggingAuditTrail.DEPRECATED_EMIT_NODE_NAME_SETTING, + LoggingAuditTrail.INCLUDE_EVENT_SETTINGS, LoggingAuditTrail.EXCLUDE_EVENT_SETTINGS, + LoggingAuditTrail.INCLUDE_REQUEST_BODY)); clusterService.getClusterSettings().addAffixUpdateConsumer(LoggingAuditTrail.FILTER_POLICY_IGNORE_PRINCIPALS, (policyName, filtersList) -> { final Optional policy = eventFilterPolicyRegistry.get(policyName); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrail.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrail.java index d8862dcf17daa..15e0abb205e7f 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrail.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrail.java @@ -184,7 +184,8 @@ public LoggingAuditTrail(Settings settings, ClusterService clusterService, Threa // `entryCommonFields` and `includeRequestBody` writes happen-before! `events` is // always read before `entryCommonFields` and `includeRequestBody`. this.events = parse(INCLUDE_EVENT_SETTINGS.get(newSettings), EXCLUDE_EVENT_SETTINGS.get(newSettings)); - }, Arrays.asList(EMIT_HOST_ADDRESS_SETTING, EMIT_HOST_NAME_SETTING, EMIT_NODE_NAME_SETTING, EMIT_NODE_ID_SETTING, + }, Arrays.asList(EMIT_HOST_ADDRESS_SETTING, DEPRECATED_EMIT_HOST_ADDRESS_SETTING, EMIT_HOST_NAME_SETTING, + DEPRECATED_EMIT_HOST_NAME_SETTING, EMIT_NODE_NAME_SETTING, DEPRECATED_EMIT_NODE_NAME_SETTING, EMIT_NODE_ID_SETTING, INCLUDE_EVENT_SETTINGS, EXCLUDE_EVENT_SETTINGS, INCLUDE_REQUEST_BODY)); clusterService.getClusterSettings().addAffixUpdateConsumer(FILTER_POLICY_IGNORE_PRINCIPALS, (policyName, filtersList) -> { final Optional policy = eventFilterPolicyRegistry.get(policyName); @@ -784,8 +785,11 @@ private static String effectiveRealmName(Authentication authentication) { public static void registerSettings(List> settings) { settings.add(EMIT_HOST_ADDRESS_SETTING); + settings.add(DEPRECATED_EMIT_HOST_ADDRESS_SETTING); settings.add(EMIT_HOST_NAME_SETTING); + settings.add(DEPRECATED_EMIT_HOST_NAME_SETTING); settings.add(EMIT_NODE_NAME_SETTING); + settings.add(DEPRECATED_EMIT_NODE_NAME_SETTING); settings.add(EMIT_NODE_ID_SETTING); settings.add(INCLUDE_EVENT_SETTINGS); settings.add(EXCLUDE_EVENT_SETTINGS); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/AuditTrailSettingsUpdateTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/AuditTrailSettingsUpdateTests.java index e05f4620ccca2..531749c8efbcd 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/AuditTrailSettingsUpdateTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/AuditTrailSettingsUpdateTests.java @@ -65,6 +65,10 @@ protected Settings nodeSettings(int nodeOrdinal) { settingsBuilder.put("xpack.security.audit.outputs", "logfile"); // add only startup filter policies settingsBuilder.put(startupFilterSettings); + // Remove non-deprecated version of prefix settings so that we can test the deprecated variant + settingsBuilder.remove(LoggingAuditTrail.EMIT_HOST_ADDRESS_SETTING.getKey()); + settingsBuilder.remove(LoggingAuditTrail.EMIT_HOST_NAME_SETTING.getKey()); + settingsBuilder.remove(LoggingAuditTrail.EMIT_NODE_NAME_SETTING.getKey()); return settingsBuilder.build(); } @@ -147,6 +151,46 @@ public void testDynamicHostSettings() { assertThat(loggingAuditTrail.entryCommonFields.commonFields.containsKey(LoggingAuditTrail.HOST_NAME_FIELD_NAME), is(false)); } + public void testDynamicHostDeprecatedSettings() { + final Settings.Builder settingsBuilder = Settings.builder(); + settingsBuilder.put(LoggingAuditTrail.DEPRECATED_EMIT_HOST_NAME_SETTING.getKey(), true); + settingsBuilder.put(LoggingAuditTrail.DEPRECATED_EMIT_HOST_ADDRESS_SETTING.getKey(), true); + settingsBuilder.put(LoggingAuditTrail.DEPRECATED_EMIT_NODE_NAME_SETTING.getKey(), true); + final boolean persistent = randomBoolean(); + updateSettings(settingsBuilder.build(), persistent); + final LoggingAuditTrail loggingAuditTrail = (LoggingAuditTrail) internalCluster().getInstances(AuditTrailService.class) + .iterator() + .next() + .getAuditTrails() + .iterator() + .next(); + assertThat(loggingAuditTrail.entryCommonFields.commonFields.get(LoggingAuditTrail.NODE_NAME_FIELD_NAME), startsWith("node_")); + assertThat(loggingAuditTrail.entryCommonFields.commonFields.get(LoggingAuditTrail.HOST_ADDRESS_FIELD_NAME), is("127.0.0.1")); + assertThat(loggingAuditTrail.entryCommonFields.commonFields.get(LoggingAuditTrail.HOST_NAME_FIELD_NAME), is("127.0.0.1")); + settingsBuilder.put(LoggingAuditTrail.DEPRECATED_EMIT_HOST_ADDRESS_SETTING.getKey(), false); + updateSettings(settingsBuilder.build(), persistent); + assertThat(loggingAuditTrail.entryCommonFields.commonFields.get(LoggingAuditTrail.NODE_NAME_FIELD_NAME), startsWith("node_")); + assertThat(loggingAuditTrail.entryCommonFields.commonFields.containsKey(LoggingAuditTrail.HOST_ADDRESS_FIELD_NAME), is(false)); + assertThat(loggingAuditTrail.entryCommonFields.commonFields.get(LoggingAuditTrail.HOST_NAME_FIELD_NAME), is("127.0.0.1")); + settingsBuilder.put(LoggingAuditTrail.DEPRECATED_EMIT_HOST_NAME_SETTING.getKey(), false); + updateSettings(settingsBuilder.build(), persistent); + assertThat(loggingAuditTrail.entryCommonFields.commonFields.get(LoggingAuditTrail.NODE_NAME_FIELD_NAME), startsWith("node_")); + assertThat(loggingAuditTrail.entryCommonFields.commonFields.containsKey(LoggingAuditTrail.HOST_ADDRESS_FIELD_NAME), is(false)); + assertThat(loggingAuditTrail.entryCommonFields.commonFields.containsKey(LoggingAuditTrail.HOST_NAME_FIELD_NAME), is(false)); + settingsBuilder.put(LoggingAuditTrail.DEPRECATED_EMIT_NODE_NAME_SETTING.getKey(), false); + updateSettings(settingsBuilder.build(), persistent); + assertThat(loggingAuditTrail.entryCommonFields.commonFields.containsKey(LoggingAuditTrail.NODE_NAME_FIELD_NAME), is(false)); + assertThat(loggingAuditTrail.entryCommonFields.commonFields.containsKey(LoggingAuditTrail.HOST_ADDRESS_FIELD_NAME), is(false)); + assertThat(loggingAuditTrail.entryCommonFields.commonFields.containsKey(LoggingAuditTrail.HOST_NAME_FIELD_NAME), is(false)); + settingsBuilder.put(LoggingAuditTrail.DEPRECATED_EMIT_HOST_NAME_SETTING.getKey(), true); + settingsBuilder.put(LoggingAuditTrail.DEPRECATED_EMIT_HOST_ADDRESS_SETTING.getKey(), true); + settingsBuilder.put(LoggingAuditTrail.DEPRECATED_EMIT_NODE_NAME_SETTING.getKey(), true); + updateSettings(settingsBuilder.build(), persistent); + assertThat(loggingAuditTrail.entryCommonFields.commonFields.get(LoggingAuditTrail.NODE_NAME_FIELD_NAME), startsWith("node_")); + assertThat(loggingAuditTrail.entryCommonFields.commonFields.get(LoggingAuditTrail.HOST_ADDRESS_FIELD_NAME), is("127.0.0.1")); + assertThat(loggingAuditTrail.entryCommonFields.commonFields.get(LoggingAuditTrail.HOST_NAME_FIELD_NAME), is("127.0.0.1")); + } + public void testDynamicRequestBodySettings() { final boolean persistent = randomBoolean(); final boolean enableRequestBody = randomBoolean(); From 521751e7f1790fc364f324de551b96cc7ab2887f Mon Sep 17 00:00:00 2001 From: Andy Bristol Date: Mon, 3 Dec 2018 16:35:15 -0800 Subject: [PATCH 094/115] [test] mute IndexAuditUpgradeIT --- .../java/org/elasticsearch/upgrades/IndexAuditUpgradeIT.java | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/IndexAuditUpgradeIT.java b/x-pack/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/IndexAuditUpgradeIT.java index 9b3005a34b06e..83f39ea97e79b 100644 --- a/x-pack/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/IndexAuditUpgradeIT.java +++ b/x-pack/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/IndexAuditUpgradeIT.java @@ -62,6 +62,7 @@ public void findMinVersionInCluster() throws IOException { } } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/33867") public void testAuditLogs() throws Exception { assertBusy(() -> { assertAuditDocsExist(); From 1bf861f1e27497f555e739ca5c199ace830cb1ea Mon Sep 17 00:00:00 2001 From: Tal Levy Date: Mon, 3 Dec 2018 17:28:53 -0800 Subject: [PATCH 095/115] [TEST] fix deprecated version check ranges in tests (#36184) --- .../xpack/deprecation/IndexDeprecationChecksTests.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugin/deprecation/src/test/java/org/elasticsearch/xpack/deprecation/IndexDeprecationChecksTests.java b/x-pack/plugin/deprecation/src/test/java/org/elasticsearch/xpack/deprecation/IndexDeprecationChecksTests.java index 9bb9587e05cbe..3020dc82e78f7 100644 --- a/x-pack/plugin/deprecation/src/test/java/org/elasticsearch/xpack/deprecation/IndexDeprecationChecksTests.java +++ b/x-pack/plugin/deprecation/src/test/java/org/elasticsearch/xpack/deprecation/IndexDeprecationChecksTests.java @@ -22,7 +22,7 @@ public class IndexDeprecationChecksTests extends ESTestCase { public void testOldIndicesCheck() { Version createdWith = VersionUtils.randomVersionBetween(random(), Version.V_5_0_0, - VersionUtils.getPreviousVersion(Version.V_6_0_0_alpha1)); + VersionUtils.getPreviousVersion(Version.V_6_0_0)); IndexMetaData indexMetaData = IndexMetaData.builder("test") .settings(settings(createdWith)) .numberOfShards(1) @@ -39,7 +39,7 @@ public void testOldIndicesCheck() { public void testDelimitedPayloadFilterCheck() { Settings settings = settings( - VersionUtils.randomVersionBetween(random(), Version.V_6_0_0_alpha1, VersionUtils.getPreviousVersion(Version.CURRENT))) + VersionUtils.randomVersionBetween(random(), Version.V_6_0_0, VersionUtils.getPreviousVersion(Version.CURRENT))) .put("index.analysis.filter.my_delimited_payload_filter.type", "delimited_payload_filter") .put("index.analysis.filter.my_delimited_payload_filter.delimiter", "^") .put("index.analysis.filter.my_delimited_payload_filter.encoding", "identity").build(); From 2e9482f0ec42b818826ecdd4b6ec185f6469983f Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Mon, 3 Dec 2018 10:05:57 +1100 Subject: [PATCH 096/115] Add DEBUG/TRACE logs for LDAP bind (#36028) Introduces a debug log message when a bind fails and a trace message when a bind succeeds. It may seem strange to only debug a bind failure, but failures of this nature are relatively common in some realm configurations (e.g. LDAP realm with multiple user templates, or additional realms configured after an LDAP realm). --- .../xpack/security/authc/ldap/support/LdapUtils.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ldap/support/LdapUtils.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ldap/support/LdapUtils.java index d2d87db683ca3..0d5dc3029e037 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ldap/support/LdapUtils.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ldap/support/LdapUtils.java @@ -134,11 +134,13 @@ public static void maybeForkThenBindAndRevert(LDAPConnectionPool ldapPool, BindR @SuppressForbidden(reason = "Bind allowed if forking of the LDAP Connection Reader Thread.") protected void doRun() throws Exception { privilegedConnect(() -> ldapPool.bindAndRevertAuthentication(bind.duplicate())); + LOGGER.trace("LDAP bind [{}] succeeded for [{}]", bind, ldapPool); runnable.run(); } @Override public void onFailure(Exception e) { + LOGGER.debug("LDAP bind [{}] failed for [{}] - [{}]", bind, ldapPool, e.toString()); runnable.onFailure(e); } @@ -179,11 +181,13 @@ public static void maybeForkThenBind(LDAPConnection ldap, BindRequest bind, Thre @SuppressForbidden(reason = "Bind allowed if forking of the LDAP Connection Reader Thread.") protected void doRun() throws Exception { privilegedConnect(() -> ldap.bind(bind.duplicate())); + LOGGER.trace("LDAP bind [{}] succeeded for [{}]", bind, ldap); runnable.run(); } @Override public void onFailure(Exception e) { + LOGGER.debug("LDAP bind [{}] failed for [{}] - [{}]", bind, ldap, e.toString()); runnable.onFailure(e); } From 6d8e91dc87923e0c5871d981a119a593c4057ace Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Tue, 4 Dec 2018 07:41:29 +0100 Subject: [PATCH 097/115] [HLRC] Added support for CCR Get Auto Follow Pattern apis (#36049) This change also adds documentation for the Get Auto Follow Pattern API. Relates to #33824 --- .../org/elasticsearch/client/CcrClient.java | 49 +++++- .../client/CcrRequestConverters.java | 10 ++ .../ccr/GetAutoFollowPatternRequest.java | 52 ++++++ .../ccr/GetAutoFollowPatternResponse.java | 159 ++++++++++++++++++ .../java/org/elasticsearch/client/CCRIT.java | 14 ++ .../GetAutoFollowPatternResponseTests.java | 107 ++++++++++++ .../documentation/CCRDocumentationIT.java | 60 +++++++ .../ccr/get_auto_follow_pattern.asciidoc | 35 ++++ .../high-level/supported-apis.asciidoc | 2 + 9 files changed, 487 insertions(+), 1 deletion(-) create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/ccr/GetAutoFollowPatternRequest.java create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/ccr/GetAutoFollowPatternResponse.java create mode 100644 client/rest-high-level/src/test/java/org/elasticsearch/client/ccr/GetAutoFollowPatternResponseTests.java create mode 100644 docs/java-rest/high-level/ccr/get_auto_follow_pattern.asciidoc diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/CcrClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/CcrClient.java index 86710ffdf8d04..25eb260eec4df 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/CcrClient.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/CcrClient.java @@ -21,6 +21,8 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.client.ccr.DeleteAutoFollowPatternRequest; +import org.elasticsearch.client.ccr.GetAutoFollowPatternRequest; +import org.elasticsearch.client.ccr.GetAutoFollowPatternResponse; import org.elasticsearch.client.ccr.PauseFollowRequest; import org.elasticsearch.client.ccr.PutAutoFollowPatternRequest; import org.elasticsearch.client.ccr.PutFollowRequest; @@ -291,7 +293,7 @@ public AcknowledgedResponse deleteAutoFollowPattern(DeleteAutoFollowPatternReque } /** - * Deletes an auto follow pattern. + * Asynchronously deletes an auto follow pattern. * * See * the docs for more. @@ -313,4 +315,49 @@ public void deleteAutoFollowPatternAsync(DeleteAutoFollowPatternRequest request, ); } + /** + * Gets an auto follow pattern. + * + * See + * the docs for more. + * + * @param request the request + * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @return the response + * @throws IOException in case there is a problem sending the request or parsing back the response + */ + public GetAutoFollowPatternResponse getAutoFollowPattern(GetAutoFollowPatternRequest request, + RequestOptions options) throws IOException { + return restHighLevelClient.performRequestAndParseEntity( + request, + CcrRequestConverters::getAutoFollowPattern, + options, + GetAutoFollowPatternResponse::fromXContent, + Collections.emptySet() + ); + } + + /** + * Asynchronously gets an auto follow pattern. + * + * See + * the docs for more. + * + * @param request the request + * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @param listener the listener to be notified upon request completion + */ + public void getAutoFollowPatternAsync(GetAutoFollowPatternRequest request, + RequestOptions options, + ActionListener listener) { + restHighLevelClient.performRequestAsyncAndParseEntity( + request, + CcrRequestConverters::getAutoFollowPattern, + options, + GetAutoFollowPatternResponse::fromXContent, + listener, + Collections.emptySet() + ); + } + } diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/CcrRequestConverters.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/CcrRequestConverters.java index 8963919bcd154..5bcb0c04d3b86 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/CcrRequestConverters.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/CcrRequestConverters.java @@ -20,9 +20,11 @@ package org.elasticsearch.client; import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; import org.elasticsearch.client.ccr.DeleteAutoFollowPatternRequest; +import org.elasticsearch.client.ccr.GetAutoFollowPatternRequest; import org.elasticsearch.client.ccr.PauseFollowRequest; import org.elasticsearch.client.ccr.PutAutoFollowPatternRequest; import org.elasticsearch.client.ccr.PutFollowRequest; @@ -90,4 +92,12 @@ static Request deleteAutoFollowPattern(DeleteAutoFollowPatternRequest deleteAuto return new Request(HttpDelete.METHOD_NAME, endpoint); } + static Request getAutoFollowPattern(GetAutoFollowPatternRequest getAutoFollowPatternRequest) { + String endpoint = new RequestConverters.EndpointBuilder() + .addPathPartAsIs("_ccr", "auto_follow") + .addPathPart(getAutoFollowPatternRequest.getName()) + .build(); + return new Request(HttpGet.METHOD_NAME, endpoint); + } + } diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ccr/GetAutoFollowPatternRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ccr/GetAutoFollowPatternRequest.java new file mode 100644 index 0000000000000..364fddb71989a --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ccr/GetAutoFollowPatternRequest.java @@ -0,0 +1,52 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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.elasticsearch.client.ccr; + +import org.elasticsearch.client.Validatable; + +import java.util.Objects; + +/** + * Request class for get auto follow pattern api. + */ +public final class GetAutoFollowPatternRequest implements Validatable { + + private final String name; + + /** + * Get all auto follow patterns + */ + public GetAutoFollowPatternRequest() { + this.name = null; + } + + /** + * Get auto follow pattern with the specified name + * + * @param name The name of the auto follow pattern to get + */ + public GetAutoFollowPatternRequest(String name) { + this.name = Objects.requireNonNull(name); + } + + public String getName() { + return name; + } +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ccr/GetAutoFollowPatternResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ccr/GetAutoFollowPatternResponse.java new file mode 100644 index 0000000000000..f4afb2d650e9b --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ccr/GetAutoFollowPatternResponse.java @@ -0,0 +1,159 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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.elasticsearch.client.ccr; + +import org.elasticsearch.common.unit.ByteSizeValue; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.ObjectParser; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentParser.Token; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public final class GetAutoFollowPatternResponse { + + public static GetAutoFollowPatternResponse fromXContent(final XContentParser parser) throws IOException { + final Map patterns = new HashMap<>(); + for (Token token = parser.nextToken(); token != Token.END_OBJECT; token = parser.nextToken()) { + if (token == Token.FIELD_NAME) { + final String name = parser.currentName(); + final Pattern pattern = Pattern.PARSER.parse(parser, null); + patterns.put(name, pattern); + } + } + return new GetAutoFollowPatternResponse(patterns); + } + + private final Map patterns; + + GetAutoFollowPatternResponse(Map patterns) { + this.patterns = Collections.unmodifiableMap(patterns); + } + + public Map getPatterns() { + return patterns; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + GetAutoFollowPatternResponse that = (GetAutoFollowPatternResponse) o; + return Objects.equals(patterns, that.patterns); + } + + @Override + public int hashCode() { + return Objects.hash(patterns); + } + + public static class Pattern extends FollowConfig { + + @SuppressWarnings("unchecked") + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + "pattern", args -> new Pattern((String) args[0], (List) args[1], (String) args[2])); + + static { + PARSER.declareString(ConstructingObjectParser.constructorArg(), PutFollowRequest.REMOTE_CLUSTER_FIELD); + PARSER.declareStringArray(ConstructingObjectParser.constructorArg(), PutAutoFollowPatternRequest.LEADER_PATTERNS_FIELD); + PARSER.declareString(ConstructingObjectParser.optionalConstructorArg(), PutAutoFollowPatternRequest.FOLLOW_PATTERN_FIELD); + PARSER.declareInt(Pattern::setMaxReadRequestOperationCount, FollowConfig.MAX_READ_REQUEST_OPERATION_COUNT); + PARSER.declareField( + Pattern::setMaxReadRequestSize, + (p, c) -> ByteSizeValue.parseBytesSizeValue(p.text(), FollowConfig.MAX_READ_REQUEST_SIZE.getPreferredName()), + PutFollowRequest.MAX_READ_REQUEST_SIZE, + ObjectParser.ValueType.STRING); + PARSER.declareInt(Pattern::setMaxOutstandingReadRequests, FollowConfig.MAX_OUTSTANDING_READ_REQUESTS); + PARSER.declareInt(Pattern::setMaxWriteRequestOperationCount, FollowConfig.MAX_WRITE_REQUEST_OPERATION_COUNT); + PARSER.declareField( + Pattern::setMaxWriteRequestSize, + (p, c) -> ByteSizeValue.parseBytesSizeValue(p.text(), FollowConfig.MAX_WRITE_REQUEST_SIZE.getPreferredName()), + PutFollowRequest.MAX_WRITE_REQUEST_SIZE, + ObjectParser.ValueType.STRING); + PARSER.declareInt(Pattern::setMaxOutstandingWriteRequests, FollowConfig.MAX_OUTSTANDING_WRITE_REQUESTS); + PARSER.declareInt(Pattern::setMaxWriteBufferCount, FollowConfig.MAX_WRITE_BUFFER_COUNT); + PARSER.declareField( + Pattern::setMaxWriteBufferSize, + (p, c) -> ByteSizeValue.parseBytesSizeValue(p.text(), FollowConfig.MAX_WRITE_BUFFER_SIZE.getPreferredName()), + PutFollowRequest.MAX_WRITE_BUFFER_SIZE, + ObjectParser.ValueType.STRING); + PARSER.declareField( + Pattern::setMaxRetryDelay, + (p, c) -> TimeValue.parseTimeValue(p.text(), FollowConfig.MAX_RETRY_DELAY_FIELD.getPreferredName()), + PutFollowRequest.MAX_RETRY_DELAY_FIELD, + ObjectParser.ValueType.STRING); + PARSER.declareField( + Pattern::setReadPollTimeout, + (p, c) -> TimeValue.parseTimeValue(p.text(), FollowConfig.READ_POLL_TIMEOUT.getPreferredName()), + PutFollowRequest.READ_POLL_TIMEOUT, + ObjectParser.ValueType.STRING); + } + + private final String remoteCluster; + private final List leaderIndexPatterns; + private final String followIndexNamePattern; + + Pattern(String remoteCluster, List leaderIndexPatterns, String followIndexNamePattern) { + this.remoteCluster = remoteCluster; + this.leaderIndexPatterns = leaderIndexPatterns; + this.followIndexNamePattern = followIndexNamePattern; + } + + public String getRemoteCluster() { + return remoteCluster; + } + + public List getLeaderIndexPatterns() { + return leaderIndexPatterns; + } + + public String getFollowIndexNamePattern() { + return followIndexNamePattern; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + Pattern pattern = (Pattern) o; + return Objects.equals(remoteCluster, pattern.remoteCluster) && + Objects.equals(leaderIndexPatterns, pattern.leaderIndexPatterns) && + Objects.equals(followIndexNamePattern, pattern.followIndexNamePattern); + } + + @Override + public int hashCode() { + return Objects.hash( + super.hashCode(), + remoteCluster, + leaderIndexPatterns, + followIndexNamePattern + ); + } + } + +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/CCRIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/CCRIT.java index 00b2d26abaf57..9c5db63ada9ed 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/CCRIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/CCRIT.java @@ -30,6 +30,8 @@ import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.action.support.WriteRequest; import org.elasticsearch.client.ccr.DeleteAutoFollowPatternRequest; +import org.elasticsearch.client.ccr.GetAutoFollowPatternRequest; +import org.elasticsearch.client.ccr.GetAutoFollowPatternResponse; import org.elasticsearch.client.ccr.PauseFollowRequest; import org.elasticsearch.client.ccr.PutAutoFollowPatternRequest; import org.elasticsearch.client.ccr.PutFollowRequest; @@ -48,6 +50,7 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; public class CCRIT extends ESRestHighLevelClientTestCase { @@ -148,6 +151,17 @@ public void testAutoFollowing() throws Exception { assertThat(indexExists("copy-logs-20200101"), is(true)); }); + GetAutoFollowPatternRequest getAutoFollowPatternRequest = + randomBoolean() ? new GetAutoFollowPatternRequest("pattern1") : new GetAutoFollowPatternRequest(); + GetAutoFollowPatternResponse getAutoFollowPatternResponse = + execute(getAutoFollowPatternRequest, ccrClient::getAutoFollowPattern, ccrClient::getAutoFollowPatternAsync); + assertThat(getAutoFollowPatternResponse.getPatterns().size(), equalTo(1L)); + GetAutoFollowPatternResponse.Pattern pattern = getAutoFollowPatternResponse.getPatterns().get("patterns1"); + assertThat(pattern, notNullValue()); + assertThat(pattern.getRemoteCluster(), equalTo(putAutoFollowPatternRequest.getRemoteCluster())); + assertThat(pattern.getLeaderIndexPatterns(), equalTo(putAutoFollowPatternRequest.getLeaderIndexPatterns())); + assertThat(pattern.getFollowIndexNamePattern(), equalTo(putAutoFollowPatternRequest.getFollowIndexNamePattern())); + // Cleanup: final DeleteAutoFollowPatternRequest deleteAutoFollowPatternRequest = new DeleteAutoFollowPatternRequest("pattern1"); AcknowledgedResponse deleteAutoFollowPatternResponse = diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ccr/GetAutoFollowPatternResponseTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ccr/GetAutoFollowPatternResponseTests.java new file mode 100644 index 0000000000000..64eb9ba4f9f75 --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ccr/GetAutoFollowPatternResponseTests.java @@ -0,0 +1,107 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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.elasticsearch.client.ccr; + +import org.elasticsearch.common.unit.ByteSizeValue; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.test.ESTestCase; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static org.elasticsearch.client.ccr.PutAutoFollowPatternRequest.FOLLOW_PATTERN_FIELD; +import static org.elasticsearch.client.ccr.PutAutoFollowPatternRequest.LEADER_PATTERNS_FIELD; +import static org.elasticsearch.client.ccr.PutFollowRequest.REMOTE_CLUSTER_FIELD; +import static org.elasticsearch.test.AbstractXContentTestCase.xContentTester; + +public class GetAutoFollowPatternResponseTests extends ESTestCase { + + public void testFromXContent() throws IOException { + xContentTester(this::createParser, + this::createTestInstance, + GetAutoFollowPatternResponseTests::toXContent, + GetAutoFollowPatternResponse::fromXContent) + .supportsUnknownFields(false) + .test(); + } + + private GetAutoFollowPatternResponse createTestInstance() { + int numPatterns = randomIntBetween(0, 16); + Map patterns = new HashMap<>(numPatterns); + for (int i = 0; i < numPatterns; i++) { + GetAutoFollowPatternResponse.Pattern pattern = new GetAutoFollowPatternResponse.Pattern( + randomAlphaOfLength(4), Collections.singletonList(randomAlphaOfLength(4)), randomAlphaOfLength(4)); + if (randomBoolean()) { + pattern.setMaxOutstandingReadRequests(randomIntBetween(0, Integer.MAX_VALUE)); + } + if (randomBoolean()) { + pattern.setMaxOutstandingWriteRequests(randomIntBetween(0, Integer.MAX_VALUE)); + } + if (randomBoolean()) { + pattern.setMaxReadRequestOperationCount(randomIntBetween(0, Integer.MAX_VALUE)); + } + if (randomBoolean()) { + pattern.setMaxReadRequestSize(new ByteSizeValue(randomNonNegativeLong())); + } + if (randomBoolean()) { + pattern.setMaxWriteBufferCount(randomIntBetween(0, Integer.MAX_VALUE)); + } + if (randomBoolean()) { + pattern.setMaxWriteBufferSize(new ByteSizeValue(randomNonNegativeLong())); + } + if (randomBoolean()) { + pattern.setMaxWriteRequestOperationCount(randomIntBetween(0, Integer.MAX_VALUE)); + } + if (randomBoolean()) { + pattern.setMaxWriteRequestSize(new ByteSizeValue(randomNonNegativeLong())); + } + if (randomBoolean()) { + pattern.setMaxRetryDelay(new TimeValue(randomNonNegativeLong())); + } + if (randomBoolean()) { + pattern.setReadPollTimeout(new TimeValue(randomNonNegativeLong())); + } + patterns.put(randomAlphaOfLength(4), pattern); + } + return new GetAutoFollowPatternResponse(patterns); + } + + public static void toXContent(GetAutoFollowPatternResponse response, XContentBuilder builder) throws IOException { + builder.startObject(); + { + for (Map.Entry entry : response.getPatterns().entrySet()) { + builder.startObject(entry.getKey()); + GetAutoFollowPatternResponse.Pattern pattern = entry.getValue(); + builder.field(REMOTE_CLUSTER_FIELD.getPreferredName(), pattern.getRemoteCluster()); + builder.field(LEADER_PATTERNS_FIELD.getPreferredName(), pattern.getLeaderIndexPatterns()); + if (pattern.getFollowIndexNamePattern()!= null) { + builder.field(FOLLOW_PATTERN_FIELD.getPreferredName(), pattern.getFollowIndexNamePattern()); + } + entry.getValue().toXContentFragment(builder, ToXContent.EMPTY_PARAMS); + builder.endObject(); + } + } + builder.endObject(); + } +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/CCRDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/CCRDocumentationIT.java index 1d1aef514cab9..95ee1b06f4580 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/CCRDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/CCRDocumentationIT.java @@ -34,6 +34,9 @@ import org.elasticsearch.client.Response; import org.elasticsearch.client.RestHighLevelClient; import org.elasticsearch.client.ccr.DeleteAutoFollowPatternRequest; +import org.elasticsearch.client.ccr.GetAutoFollowPatternRequest; +import org.elasticsearch.client.ccr.GetAutoFollowPatternResponse; +import org.elasticsearch.client.ccr.GetAutoFollowPatternResponse.Pattern; import org.elasticsearch.client.ccr.PauseFollowRequest; import org.elasticsearch.client.ccr.PutAutoFollowPatternRequest; import org.elasticsearch.client.ccr.PutFollowRequest; @@ -501,6 +504,63 @@ public void onFailure(Exception e) { assertTrue(latch.await(30L, TimeUnit.SECONDS)); } + public void testGetAutoFollowPattern() throws Exception { + RestHighLevelClient client = highLevelClient(); + + // Put auto follow pattern, so that we can get it: + { + final PutAutoFollowPatternRequest putRequest = + new PutAutoFollowPatternRequest("my_pattern", "local", Collections.singletonList("logs-*")); + AcknowledgedResponse putResponse = client.ccr().putAutoFollowPattern(putRequest, RequestOptions.DEFAULT); + assertThat(putResponse.isAcknowledged(), is(true)); + } + + // tag::ccr-get-auto-follow-pattern-request + GetAutoFollowPatternRequest request = + new GetAutoFollowPatternRequest("my_pattern"); // <1> + // end::ccr-get-auto-follow-pattern-request + + // tag::ccr-get-auto-follow-pattern-execute + GetAutoFollowPatternResponse response = client.ccr() + .getAutoFollowPattern(request, RequestOptions.DEFAULT); + // end::ccr-get-auto-follow-pattern-execute + + // tag::ccr-get-auto-follow-pattern-response + Map patterns = response.getPatterns(); + Pattern pattern = patterns.get("my_pattern"); // <1> + pattern.getLeaderIndexPatterns(); + // end::ccr-get-auto-follow-pattern-response + + // tag::ccr-get-auto-follow-pattern-execute-listener + ActionListener listener = + new ActionListener() { + @Override + public void onResponse(GetAutoFollowPatternResponse + response) { // <1> + Map patterns = response.getPatterns(); + Pattern pattern = patterns.get("my_pattern"); + pattern.getLeaderIndexPatterns(); + } + + @Override + public void onFailure(Exception e) { + // <2> + } + }; + // end::ccr-get-auto-follow-pattern-execute-listener + + // Replace the empty listener by a blocking listener in test + final CountDownLatch latch = new CountDownLatch(1); + listener = new LatchedActionListener<>(listener, latch); + + // tag::ccr-get-auto-follow-pattern-execute-async + client.ccr().getAutoFollowPatternAsync(request, + RequestOptions.DEFAULT, listener); // <1> + // end::ccr-get-auto-follow-pattern-execute-async + + assertTrue(latch.await(30L, TimeUnit.SECONDS)); + } + static Map toMap(Response response) throws IOException { return XContentHelper.convertToMap(JsonXContent.jsonXContent, EntityUtils.toString(response.getEntity()), false); } diff --git a/docs/java-rest/high-level/ccr/get_auto_follow_pattern.asciidoc b/docs/java-rest/high-level/ccr/get_auto_follow_pattern.asciidoc new file mode 100644 index 0000000000000..61ab8d58e9cc3 --- /dev/null +++ b/docs/java-rest/high-level/ccr/get_auto_follow_pattern.asciidoc @@ -0,0 +1,35 @@ +-- +:api: ccr-get-auto-follow-pattern +:request: GetAutoFollowPatternRequest +:response: GetAutoFollowPatternResponse +-- + +[id="{upid}-{api}"] +=== Get Auto Follow Pattern API + +[id="{upid}-{api}-request"] +==== Request + +The Get Auto Follow Pattern API allows you to get a specified auto follow pattern +or all auto follow patterns. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[{api}-request] +-------------------------------------------------- +<1> The name of the auto follow pattern to get. + Use the default constructor to get all auto follow patterns. + +[id="{upid}-{api}-response"] +==== Response + +The returned +{response}+ includes the requested auto follow pattern or +all auto follow patterns if default constructor or request class was used. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[{api}-response] +-------------------------------------------------- +<1> Get the requested pattern from the list of returned patterns + +include::../execution.asciidoc[] diff --git a/docs/java-rest/high-level/supported-apis.asciidoc b/docs/java-rest/high-level/supported-apis.asciidoc index 9bf8959ad1c91..3424babf7b25b 100644 --- a/docs/java-rest/high-level/supported-apis.asciidoc +++ b/docs/java-rest/high-level/supported-apis.asciidoc @@ -471,6 +471,7 @@ The Java High Level REST Client supports the following CCR APIs: * <<{upid}-ccr-unfollow>> * <<{upid}-ccr-put-auto-follow-pattern>> * <<{upid}-ccr-delete-auto-follow-pattern>> +* <<{upid}-ccr-get-auto-follow-pattern>> include::ccr/put_follow.asciidoc[] include::ccr/pause_follow.asciidoc[] @@ -478,6 +479,7 @@ include::ccr/resume_follow.asciidoc[] include::ccr/unfollow.asciidoc[] include::ccr/put_auto_follow_pattern.asciidoc[] include::ccr/delete_auto_follow_pattern.asciidoc[] +include::ccr/get_auto_follow_pattern.asciidoc[] == Index Lifecycle Management APIs From 1d0690d634279384efcc6054ee3034737f90ef6d Mon Sep 17 00:00:00 2001 From: Alpar Torok Date: Tue, 4 Dec 2018 10:16:51 +0200 Subject: [PATCH 098/115] Testclusters: implement starting, waiting for and stopping single cluster nodes (#35599) --- buildSrc/build.gradle | 4 + .../gradle/precommit/PrecommitTasks.groovy | 2 +- .../elasticsearch/GradleServicesAdapter.java | 2 +- .../elasticsearch/gradle/Distribution.java | 14 +- .../testclusters/ElasticsearchNode.java | 401 +++++++++++++++++- .../testclusters/TestClustersException.java | 33 ++ .../testclusters/TestClustersPlugin.java | 82 +++- .../test/GradleIntegrationTestCase.java | 5 +- .../testclusters/TestClustersPluginIT.java | 13 +- .../src/testKit/testclusters/build.gradle | 4 +- .../alpha/build.gradle | 4 +- .../bravo/build.gradle | 4 +- .../testclusters_multiproject/build.gradle | 4 +- test/framework/build.gradle | 2 +- 14 files changed, 531 insertions(+), 43 deletions(-) create mode 100644 buildSrc/src/main/java/org/elasticsearch/gradle/testclusters/TestClustersException.java diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 1e3ba1e88ff4b..442b0efc503f4 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -167,6 +167,10 @@ if (project != rootProject) { apply plugin: 'nebula.maven-base-publish' apply plugin: 'nebula.maven-scm' + // we need to apply these again to override the build plugin + targetCompatibility = "10" + sourceCompatibility = "10" + // groovydoc succeeds, but has some weird internal exception... groovydoc.enabled = false diff --git a/buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/PrecommitTasks.groovy b/buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/PrecommitTasks.groovy index 7032b05ed9064..bf06ac34766a1 100644 --- a/buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/PrecommitTasks.groovy +++ b/buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/PrecommitTasks.groovy @@ -217,7 +217,7 @@ class PrecommitTasks { private static Task configureNamingConventions(Project project) { if (project.sourceSets.findByName("test")) { Task namingConventionsTask = project.tasks.create('namingConventions', NamingConventionsTask) - namingConventionsTask.javaHome = project.runtimeJavaHome + namingConventionsTask.javaHome = project.compilerJavaHome return namingConventionsTask } return null diff --git a/buildSrc/src/main/java/org/elasticsearch/GradleServicesAdapter.java b/buildSrc/src/main/java/org/elasticsearch/GradleServicesAdapter.java index 5027a4403377d..0174f576e2bcc 100644 --- a/buildSrc/src/main/java/org/elasticsearch/GradleServicesAdapter.java +++ b/buildSrc/src/main/java/org/elasticsearch/GradleServicesAdapter.java @@ -41,7 +41,7 @@ */ public class GradleServicesAdapter { - public final Project project; + private final Project project; public GradleServicesAdapter(Project project) { this.project = project; diff --git a/buildSrc/src/main/java/org/elasticsearch/gradle/Distribution.java b/buildSrc/src/main/java/org/elasticsearch/gradle/Distribution.java index 365a12c076cc5..721eddb52915b 100644 --- a/buildSrc/src/main/java/org/elasticsearch/gradle/Distribution.java +++ b/buildSrc/src/main/java/org/elasticsearch/gradle/Distribution.java @@ -20,17 +20,23 @@ public enum Distribution { - INTEG_TEST("integ-test"), - ZIP("elasticsearch"), - ZIP_OSS("elasticsearch-oss"); + INTEG_TEST("integ-test", "zip"), + ZIP("elasticsearch", "zip"), + ZIP_OSS("elasticsearch-oss", "zip"); private final String fileName; + private final String fileExtension; - Distribution(String name) { + Distribution(String name, String fileExtension) { this.fileName = name; + this.fileExtension = fileExtension; } public String getFileName() { return fileName; } + + public String getFileExtension() { + return fileExtension; + } } diff --git a/buildSrc/src/main/java/org/elasticsearch/gradle/testclusters/ElasticsearchNode.java b/buildSrc/src/main/java/org/elasticsearch/gradle/testclusters/ElasticsearchNode.java index 4c7e84c423ed8..fa4415bbe1e91 100644 --- a/buildSrc/src/main/java/org/elasticsearch/gradle/testclusters/ElasticsearchNode.java +++ b/buildSrc/src/main/java/org/elasticsearch/gradle/testclusters/ElasticsearchNode.java @@ -20,25 +20,67 @@ import org.elasticsearch.GradleServicesAdapter; import org.elasticsearch.gradle.Distribution; +import org.elasticsearch.gradle.Version; import org.gradle.api.logging.Logger; import org.gradle.api.logging.Logging; +import org.gradle.internal.os.OperatingSystem; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import static java.util.Objects.requireNonNull; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.SECONDS; public class ElasticsearchNode { + private final Logger logger = Logging.getLogger(ElasticsearchNode.class); private final String name; private final GradleServicesAdapter services; private final AtomicBoolean configurationFrozen = new AtomicBoolean(false); - private final Logger logger = Logging.getLogger(ElasticsearchNode.class); + private final File artifactsExtractDir; + private final File workingDir; + + private static final int ES_DESTROY_TIMEOUT = 20; + private static final TimeUnit ES_DESTROY_TIMEOUT_UNIT = TimeUnit.SECONDS; + private static final int NODE_UP_TIMEOUT = 30; + private static final TimeUnit NODE_UP_TIMEOUT_UNIT = TimeUnit.SECONDS; + private final LinkedHashMap> waitConditions; private Distribution distribution; private String version; + private File javaHome; + private volatile Process esProcess; + private final String path; - public ElasticsearchNode(String name, GradleServicesAdapter services) { + ElasticsearchNode(String path, String name, GradleServicesAdapter services, File artifactsExtractDir, File workingDirBase) { + this.path = path; this.name = name; this.services = services; + this.artifactsExtractDir = artifactsExtractDir; + this.workingDir = new File(workingDirBase, safeName(name)); + this.waitConditions = new LinkedHashMap<>(); + waitConditions.put("http ports file", node -> node.getHttpPortsFile().exists()); + waitConditions.put("transport ports file", node -> node.getTransportPortFile().exists()); + waitForUri("cluster health yellow", "/_cluster/health?wait_for_nodes=>=1&wait_for_status=yellow"); } public String getName() { @@ -50,6 +92,7 @@ public String getVersion() { } public void setVersion(String version) { + requireNonNull(version, "null version passed when configuring test cluster `" + this + "`"); checkFrozen(); this.version = version; } @@ -59,22 +102,258 @@ public Distribution getDistribution() { } public void setDistribution(Distribution distribution) { + requireNonNull(distribution, "null distribution passed when configuring test cluster `" + this + "`"); checkFrozen(); this.distribution = distribution; } - void start() { + public void freeze() { + requireNonNull(distribution, "null distribution passed when configuring test cluster `" + this + "`"); + requireNonNull(version, "null version passed when configuring test cluster `" + this + "`"); + logger.info("Locking configuration of `{}`", this); + configurationFrozen.set(true); + } + + public void setJavaHome(File javaHome) { + requireNonNull(javaHome, "null javaHome passed when configuring test cluster `" + this + "`"); + checkFrozen(); + if (javaHome.exists() == false) { + throw new TestClustersException("java home for `" + this + "` does not exists: `" + javaHome + "`"); + } + this.javaHome = javaHome; + } + + public File getJavaHome() { + return javaHome; + } + + private void waitForUri(String description, String uri) { + waitConditions.put(description, (node) -> { + try { + URL url = new URL("http://" + this.getHttpPortInternal().get(0) + uri); + HttpURLConnection con = (HttpURLConnection) url.openConnection(); + con.setRequestMethod("GET"); + con.setConnectTimeout(500); + con.setReadTimeout(500); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(con.getInputStream()))) { + String response = reader.lines().collect(Collectors.joining("\n")); + logger.info("{} -> {} ->\n{}", this, uri, response); + } + return true; + } catch (IOException e) { + throw new IllegalStateException("Connection attempt to " + this + " failed", e); + } + }); + } + + synchronized void start() { logger.info("Starting `{}`", this); + + File distroArtifact = new File( + new File(artifactsExtractDir, distribution.getFileExtension()), + distribution.getFileName() + "-" + getVersion() + ); + if (distroArtifact.exists() == false) { + throw new TestClustersException("Can not start " + this + ", missing: " + distroArtifact); + } + if (distroArtifact.isDirectory() == false) { + throw new TestClustersException("Can not start " + this + ", is not a directory: " + distroArtifact); + } + services.sync(spec -> { + spec.from(new File(distroArtifact, "config")); + spec.into(getConfigFile().getParent()); + }); + configure(); + startElasticsearchProcess(distroArtifact); } - void stop(boolean tailLogs) { + private void startElasticsearchProcess(File distroArtifact) { + logger.info("Running `bin/elasticsearch` in `{}` for {}", workingDir, this); + final ProcessBuilder processBuilder = new ProcessBuilder(); + if (OperatingSystem.current().isWindows()) { + processBuilder.command( + "cmd", "/c", + new File(distroArtifact, "\\bin\\elasticsearch.bat").getAbsolutePath() + ); + } else { + processBuilder.command( + new File(distroArtifact.getAbsolutePath(), "bin/elasticsearch").getAbsolutePath() + ); + } + try { + processBuilder.directory(workingDir); + Map environment = processBuilder.environment(); + // Don't inherit anything from the environment for as that would lack reproductability + environment.clear(); + if (javaHome != null) { + environment.put("JAVA_HOME", getJavaHome().getAbsolutePath()); + } else if (System.getenv().get("JAVA_HOME") != null) { + logger.warn("{}: No java home configured will use it from environment: {}", + this, System.getenv().get("JAVA_HOME") + ); + environment.put("JAVA_HOME", System.getenv().get("JAVA_HOME")); + } else { + logger.warn("{}: No javaHome configured, will rely on default java detection", this); + } + environment.put("ES_PATH_CONF", getConfigFile().getParentFile().getAbsolutePath()); + environment.put("ES_JAVA_OPTIONS", "-Xms512m -Xmx512m"); + // don't buffer all in memory, make sure we don't block on the default pipes + processBuilder.redirectError(ProcessBuilder.Redirect.appendTo(getStdErrFile())); + processBuilder.redirectOutput(ProcessBuilder.Redirect.appendTo(getStdoutFile())); + esProcess = processBuilder.start(); + } catch (IOException e) { + throw new TestClustersException("Failed to start ES process for " + this, e); + } + } + + public String getHttpSocketURI() { + waitForAllConditions(); + return getHttpPortInternal().get(0); + } + + public String getTransportPortURI() { + waitForAllConditions(); + return getTransportPortInternal().get(0); + } + + synchronized void stop(boolean tailLogs) { + if (esProcess == null && tailLogs) { + // This is a special case. If start() throws an exception the plugin will still call stop + // Another exception here would eat the orriginal. + return; + } logger.info("Stopping `{}`, tailLogs: {}", this, tailLogs); + requireNonNull(esProcess, "Can't stop `" + this + "` as it was not started or already stopped."); + stopHandle(esProcess.toHandle()); + if (tailLogs) { + logFileContents("Standard output of node", getStdoutFile()); + logFileContents("Standard error of node", getStdErrFile()); + } + esProcess = null; } - public void freeze() { - logger.info("Locking configuration of `{}`", this); - configurationFrozen.set(true); - Objects.requireNonNull(version, "Version of test cluster `" + this + "` can't be null"); + private void stopHandle(ProcessHandle processHandle) { + // Stop all children first, ES could actually be a child when there's some wrapper process like on Windows. + if (processHandle.isAlive()) { + processHandle.children().forEach(this::stopHandle); + } + logProcessInfo("Terminating elasticsearch process:", processHandle.info()); + if (processHandle.isAlive()) { + processHandle.destroy(); + } else { + logger.info("Process was not running when we tried to terminate it."); + } + waitForProcessToExit(processHandle); + if (processHandle.isAlive()) { + logger.info("process did not terminate after {} {}, stopping it forcefully", + ES_DESTROY_TIMEOUT, ES_DESTROY_TIMEOUT_UNIT + ); + processHandle.destroyForcibly(); + } + waitForProcessToExit(processHandle); + if (processHandle.isAlive()) { + throw new TestClustersException("Was not able to terminate es process"); + } + } + + private void logProcessInfo(String prefix, ProcessHandle.Info info) { + logger.info(prefix + " commandLine:`{}` command:`{}` args:`{}`", + info.commandLine().orElse("-"), info.command().orElse("-"), + Arrays.stream(info.arguments().orElse(new String[]{})) + .map(each -> "'" + each + "'") + .collect(Collectors.joining(" ")) + ); + } + + private void logFileContents(String description, File from) { + logger.error("{} `{}`", description, this); + try (BufferedReader reader = new BufferedReader(new FileReader(from))) { + reader.lines() + .map(line -> " [" + name + "]" + line) + .forEach(logger::error); + } catch (IOException e) { + throw new TestClustersException("Error reading " + description, e); + } + } + + private void waitForProcessToExit(ProcessHandle processHandle) { + try { + processHandle.onExit().get(ES_DESTROY_TIMEOUT, ES_DESTROY_TIMEOUT_UNIT); + } catch (InterruptedException e) { + logger.info("Interrupted while waiting for ES process", e); + Thread.currentThread().interrupt(); + } catch (ExecutionException e) { + logger.info("Failure while waiting for process to exist", e); + } catch (TimeoutException e) { + logger.info("Timed out waiting for process to exit", e); + } + } + + private File getConfigFile() { + return new File(workingDir, "config/elasticsearch.yml"); + } + + private File getConfPathData() { + return new File(workingDir, "data"); + } + + private File getConfPathSharedData() { + return new File(workingDir, "sharedData"); + } + + private File getConfPathRepo() { + return new File(workingDir, "repo"); + } + + private File getConfPathLogs() { + return new File(workingDir, "logs"); + } + + private File getStdoutFile() { + return new File(getConfPathLogs(), "es.stdout.log"); + } + + private File getStdErrFile() { + return new File(getConfPathLogs(), "es.stderr.log"); + } + + private void configure() { + getConfigFile().getParentFile().mkdirs(); + getConfPathRepo().mkdirs(); + getConfPathData().mkdirs(); + getConfPathSharedData().mkdirs(); + getConfPathLogs().mkdirs(); + LinkedHashMap config = new LinkedHashMap<>(); + config.put("cluster.name", "cluster-" + safeName(name)); + config.put("node.name", "node-" + safeName(name)); + config.put("path.repo", getConfPathRepo().getAbsolutePath()); + config.put("path.data", getConfPathData().getAbsolutePath()); + config.put("path.logs", getConfPathLogs().getAbsolutePath()); + config.put("path.shared_data", getConfPathSharedData().getAbsolutePath()); + config.put("node.attr.testattr", "test"); + config.put("node.portsfile", "true"); + config.put("http.port", "0"); + config.put("transport.tcp.port", "0"); + // Default the watermarks to absurdly low to prevent the tests from failing on nodes without enough disk space + config.put("cluster.routing.allocation.disk.watermark.low", "1b"); + config.put("cluster.routing.allocation.disk.watermark.high", "1b"); + // increase script compilation limit since tests can rapid-fire script compilations + config.put("script.max_compilations_rate", "2048/1m"); + if (Version.fromString(version).getMajor() >= 6) { + config.put("cluster.routing.allocation.disk.watermark.flood_stage", "1b"); + } + try { + Files.write( + getConfigFile().toPath(), + config.entrySet().stream() + .map(entry -> entry.getKey() + ": " + entry.getValue()) + .collect(Collectors.joining("\n")) + .getBytes(StandardCharsets.UTF_8) + ); + } catch (IOException e) { + throw new TestClustersException("Could not write config file: " + getConfigFile(), e); + } + logger.info("Written config file:{} for {}", getConfigFile(), this); } private void checkFrozen() { @@ -83,21 +362,121 @@ private void checkFrozen() { } } + private static String safeName(String name) { + return name + .replaceAll("^[^a-zA-Z0-9]+", "") + .replaceAll("[^a-zA-Z0-9]+", "-"); + } + + private File getHttpPortsFile() { + return new File(getConfPathLogs(), "http.ports"); + } + + private File getTransportPortFile() { + return new File(getConfPathLogs(), "transport.ports"); + } + + private List getTransportPortInternal() { + File transportPortFile = getTransportPortFile(); + try { + return readPortsFile(getTransportPortFile()); + } catch (IOException e) { + throw new TestClustersException( + "Failed to read transport ports file: " + transportPortFile + " for " + this, e + ); + } + } + + private List getHttpPortInternal() { + File httpPortsFile = getHttpPortsFile(); + try { + return readPortsFile(getHttpPortsFile()); + } catch (IOException e) { + throw new TestClustersException( + "Failed to read http ports file: " + httpPortsFile + " for " + this, e + ); + } + } + + private List readPortsFile(File file) throws IOException { + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + return reader.lines() + .map(String::trim) + .collect(Collectors.toList()); + } + } + + private void waitForAllConditions() { + requireNonNull(esProcess, "Can't wait for `" + this + "` as it was stopped."); + long startedAt = System.currentTimeMillis(); + logger.info("Starting to wait for cluster to come up"); + waitConditions.forEach((description, predicate) -> { + long thisConditionStartedAt = System.currentTimeMillis(); + boolean conditionMet = false; + Throwable lastException = null; + while ( + System.currentTimeMillis() - startedAt < MILLISECONDS.convert(NODE_UP_TIMEOUT, NODE_UP_TIMEOUT_UNIT) + ) { + if (esProcess.isAlive() == false) { + throw new TestClustersException( + "process was found dead while waiting for " + description + ", " + this + ); + } + try { + if(predicate.test(this)) { + conditionMet = true; + break; + } + } catch (TestClustersException e) { + throw new TestClustersException(e); + } catch (Exception e) { + if (lastException == null) { + lastException = e; + } else { + e.addSuppressed(lastException); + lastException = e; + } + } + try { + Thread.sleep(500); + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + if (conditionMet == false) { + String message = "`" + this + "` failed to wait for " + description + " after " + + NODE_UP_TIMEOUT + " " + NODE_UP_TIMEOUT_UNIT; + if (lastException == null) { + throw new TestClustersException(message); + } else { + throw new TestClustersException(message, lastException); + } + } + logger.info( + "{}: {} took {} seconds", + this, description, + SECONDS.convert(System.currentTimeMillis() - thisConditionStartedAt, MILLISECONDS) + ); + }); + } + @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; ElasticsearchNode that = (ElasticsearchNode) o; - return Objects.equals(name, that.name); + return Objects.equals(name, that.name) && + Objects.equals(path, that.path); } @Override public int hashCode() { - return Objects.hash(name); + return Objects.hash(name, path); } @Override public String toString() { - return "ElasticsearchNode{name='" + name + "'}"; + return "node{" + path + ":" + name + "}"; } } diff --git a/buildSrc/src/main/java/org/elasticsearch/gradle/testclusters/TestClustersException.java b/buildSrc/src/main/java/org/elasticsearch/gradle/testclusters/TestClustersException.java new file mode 100644 index 0000000000000..9056fdec282be --- /dev/null +++ b/buildSrc/src/main/java/org/elasticsearch/gradle/testclusters/TestClustersException.java @@ -0,0 +1,33 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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.elasticsearch.gradle.testclusters; + +class TestClustersException extends RuntimeException { + TestClustersException(String message) { + super(message); + } + + TestClustersException(String message, Throwable cause) { + super(message, cause); + } + + TestClustersException(Throwable cause) { + super(cause); + } +} diff --git a/buildSrc/src/main/java/org/elasticsearch/gradle/testclusters/TestClustersPlugin.java b/buildSrc/src/main/java/org/elasticsearch/gradle/testclusters/TestClustersPlugin.java index 2ea5e62306a84..1fe8bec1902f6 100644 --- a/buildSrc/src/main/java/org/elasticsearch/gradle/testclusters/TestClustersPlugin.java +++ b/buildSrc/src/main/java/org/elasticsearch/gradle/testclusters/TestClustersPlugin.java @@ -40,6 +40,9 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; public class TestClustersPlugin implements Plugin { @@ -48,14 +51,17 @@ public class TestClustersPlugin implements Plugin { private static final String NODE_EXTENSION_NAME = "testClusters"; static final String HELPER_CONFIGURATION_NAME = "testclusters"; private static final String SYNC_ARTIFACTS_TASK_NAME = "syncTestClustersArtifacts"; + private static final int EXECUTOR_SHUTDOWN_TIMEOUT = 1; + private static final TimeUnit EXECUTOR_SHUTDOWN_TIMEOUT_UNIT = TimeUnit.MINUTES; - private final Logger logger = Logging.getLogger(TestClustersPlugin.class); + private static final Logger logger = Logging.getLogger(TestClustersPlugin.class); // this is static because we need a single mapping across multi project builds, as some of the listeners we use, // like task graph are singletons across multi project builds. private static final Map> usedClusters = new ConcurrentHashMap<>(); private static final Map claimsInventory = new ConcurrentHashMap<>(); private static final Set runningClusters = Collections.synchronizedSet(new HashSet<>()); + private static volatile ExecutorService executorService; @Override public void apply(Project project) { @@ -106,6 +112,9 @@ public void apply(Project project) { // After each task we determine if there are clusters that are no longer needed. configureStopClustersHook(project); + // configure hooks to make sure no test cluster processes survive the build + configureCleanupHooks(project); + // Since we have everything modeled in the DSL, add all the required dependencies e.x. the distribution to the // configuration so the user doesn't have to repeat this. autoConfigureClusterDependencies(project, rootProject, container); @@ -117,8 +126,11 @@ private NamedDomainObjectContainer createTestClustersContaine NamedDomainObjectContainer container = project.container( ElasticsearchNode.class, name -> new ElasticsearchNode( + project.getPath(), name, - GradleServicesAdapter.getInstance(project) + GradleServicesAdapter.getInstance(project), + SyncTestClustersConfiguration.getTestClustersConfigurationExtractDir(project), + new File(project.getBuildDir(), "testclusters") ) ); project.getExtensions().add(NODE_EXTENSION_NAME, container); @@ -137,14 +149,14 @@ private void createListClustersTask(Project project, NamedDomainObjectContainer< ); } - private void createUseClusterTaskExtension(Project project) { + private static void createUseClusterTaskExtension(Project project) { // register an extension for all current and future tasks, so that any task can declare that it wants to use a // specific cluster. project.getTasks().all((Task task) -> task.getExtensions().findByType(ExtraPropertiesExtension.class) .set( "useCluster", - new Closure(this, task) { + new Closure(project, task) { public void doCall(ElasticsearchNode node) { Object thisObject = this.getThisObject(); if (thisObject instanceof Task == false) { @@ -160,7 +172,7 @@ public void doCall(ElasticsearchNode node) { ); } - private void configureClaimClustersHook(Project project) { + private static void configureClaimClustersHook(Project project) { project.getGradle().getTaskGraph().whenReady(taskExecutionGraph -> taskExecutionGraph.getAllTasks() .forEach(task -> @@ -174,7 +186,7 @@ private void configureClaimClustersHook(Project project) { ); } - private void configureStartClustersHook(Project project) { + private static void configureStartClustersHook(Project project) { project.getGradle().addListener( new TaskActionListener() { @Override @@ -196,7 +208,7 @@ public void afterActions(Task task) {} ); } - private void configureStopClustersHook(Project project) { + private static void configureStopClustersHook(Project project) { project.getGradle().addListener( new TaskExecutionListener() { @Override @@ -226,6 +238,7 @@ public void afterExecute(Task task, TaskState state) { .filter(entry -> runningClusters.contains(entry.getKey())) .map(Map.Entry::getKey) .collect(Collectors.toList()); + runningClusters.removeAll(stoppable); } stoppable.forEach(each -> each.stop(false)); } @@ -251,7 +264,7 @@ public static NamedDomainObjectContainer getNodeExtension(Pro project.getExtensions().getByName(NODE_EXTENSION_NAME); } - private void autoConfigureClusterDependencies( + private static void autoConfigureClusterDependencies( Project project, Project rootProject, NamedDomainObjectContainer container @@ -272,6 +285,59 @@ private void autoConfigureClusterDependencies( })); } + private static void configureCleanupHooks(Project project) { + synchronized (runningClusters) { + if (executorService == null || executorService.isTerminated()) { + executorService = Executors.newSingleThreadExecutor(); + } else { + throw new IllegalStateException("Trying to configure executor service twice"); + } + } + // When the Gradle daemon is used, it will interrupt all threads when the build concludes. + executorService.submit(() -> { + while (true) { + try { + Thread.sleep(Long.MAX_VALUE); + } catch (InterruptedException interrupted) { + shutDownAllClusters(); + Thread.currentThread().interrupt(); + return; + } + } + }); + + project.getGradle().buildFinished(buildResult -> { + logger.info("Build finished"); + shutdownExecutorService(); + }); + // When the Daemon is not used, or runs into issues, rely on a shutdown hook + // When the daemon is used, but does not work correctly and eventually dies off (e.x. due to non interruptable + // thread in the build) process will be stopped eventually when the daemon dies. + Runtime.getRuntime().addShutdownHook(new Thread(TestClustersPlugin::shutDownAllClusters)); + } + + private static void shutdownExecutorService() { + executorService.shutdownNow(); + try { + if (executorService.awaitTermination(EXECUTOR_SHUTDOWN_TIMEOUT, EXECUTOR_SHUTDOWN_TIMEOUT_UNIT) == false) { + throw new IllegalStateException( + "Failed to shut down executor service after " + + EXECUTOR_SHUTDOWN_TIMEOUT + " " + EXECUTOR_SHUTDOWN_TIMEOUT_UNIT + ); + } + } catch (InterruptedException e) { + logger.info("Wait for testclusters shutdown interrupted", e); + Thread.currentThread().interrupt(); + } + } + + private static void shutDownAllClusters() { + logger.info("Shutting down all test clusters", new RuntimeException()); + synchronized (runningClusters) { + runningClusters.forEach(each -> each.stop(true)); + runningClusters.clear(); + } + } } diff --git a/buildSrc/src/test/java/org/elasticsearch/gradle/test/GradleIntegrationTestCase.java b/buildSrc/src/test/java/org/elasticsearch/gradle/test/GradleIntegrationTestCase.java index 025c549489afa..fc89a019f8dac 100644 --- a/buildSrc/src/test/java/org/elasticsearch/gradle/test/GradleIntegrationTestCase.java +++ b/buildSrc/src/test/java/org/elasticsearch/gradle/test/GradleIntegrationTestCase.java @@ -154,10 +154,11 @@ public void assertOutputOnlyOnce(String output, String... text) { for (String each : text) { int i = output.indexOf(each); if (i == -1 ) { - fail("Expected `" + text + "` to appear at most once, but it didn't at all.\n\nOutout is:\n"+ output); + fail("Expected \n```" + each + "```\nto appear at most once, but it didn't at all.\n\nOutout is:\n"+ output + ); } if(output.indexOf(each) != output.lastIndexOf(each)) { - fail("Expected `" + text + "` to appear at most once, but it did multiple times.\n\nOutout is:\n"+ output); + fail("Expected `" + each + "` to appear at most once, but it did multiple times.\n\nOutout is:\n"+ output); } } } diff --git a/buildSrc/src/test/java/org/elasticsearch/gradle/testclusters/TestClustersPluginIT.java b/buildSrc/src/test/java/org/elasticsearch/gradle/testclusters/TestClustersPluginIT.java index f153919ac06d2..ee366ac7b7c65 100644 --- a/buildSrc/src/test/java/org/elasticsearch/gradle/testclusters/TestClustersPluginIT.java +++ b/buildSrc/src/test/java/org/elasticsearch/gradle/testclusters/TestClustersPluginIT.java @@ -76,8 +76,8 @@ public void testUseClusterBySkippedAndWorkingTask() { assertOutputContains( result.getOutput(), "> Task :user1", - "Starting `ElasticsearchNode{name='myTestCluster'}`", - "Stopping `ElasticsearchNode{name='myTestCluster'}`" + "Starting `node{::myTestCluster}`", + "Stopping `node{::myTestCluster}`" ); } @@ -88,7 +88,6 @@ public void testMultiProject() { .withPluginClasspath() .build(); assertTaskSuccessful(result, ":user1", ":user2"); - assertStartedAndStoppedOnce(result); } @@ -98,7 +97,7 @@ public void testUseClusterByFailingOne() { assertStartedAndStoppedOnce(result); assertOutputContains( result.getOutput(), - "Stopping `ElasticsearchNode{name='myTestCluster'}`, tailLogs: true", + "Stopping `node{::myTestCluster}`, tailLogs: true", "Execution failed for task ':itAlwaysFails'." ); } @@ -110,7 +109,7 @@ public void testUseClusterByFailingDependency() { assertStartedAndStoppedOnce(result); assertOutputContains( result.getOutput(), - "Stopping `ElasticsearchNode{name='myTestCluster'}`, tailLogs: true", + "Stopping `node{::myTestCluster}`, tailLogs: true", "Execution failed for task ':itAlwaysFails'." ); } @@ -146,8 +145,8 @@ private GradleRunner getTestClustersRunner(String... tasks) { private void assertStartedAndStoppedOnce(BuildResult result) { assertOutputOnlyOnce( result.getOutput(), - "Starting `ElasticsearchNode{name='myTestCluster'}`", - "Stopping `ElasticsearchNode{name='myTestCluster'}`" + "Starting `node{::myTestCluster}`", + "Stopping `node{::myTestCluster}`" ); } } diff --git a/buildSrc/src/testKit/testclusters/build.gradle b/buildSrc/src/testKit/testclusters/build.gradle index 15e34bbccd4c4..67c9afdbc82c3 100644 --- a/buildSrc/src/testKit/testclusters/build.gradle +++ b/buildSrc/src/testKit/testclusters/build.gradle @@ -18,14 +18,14 @@ repositories { task user1 { useCluster testClusters.myTestCluster doLast { - println "user1 executing" + println "$path: Cluster running @ ${testClusters.myTestCluster.httpSocketURI}" } } task user2 { useCluster testClusters.myTestCluster doLast { - println "user2 executing" + println "$path: Cluster running @ ${testClusters.myTestCluster.httpSocketURI}" } } diff --git a/buildSrc/src/testKit/testclusters_multiproject/alpha/build.gradle b/buildSrc/src/testKit/testclusters_multiproject/alpha/build.gradle index dda6be2f6a55c..783e6d9a80efb 100644 --- a/buildSrc/src/testKit/testclusters_multiproject/alpha/build.gradle +++ b/buildSrc/src/testKit/testclusters_multiproject/alpha/build.gradle @@ -10,12 +10,12 @@ testClusters { task user1 { useCluster testClusters.myTestCluster doFirst { - println "$path" + println "$path: Cluster running @ ${testClusters.myTestCluster.httpSocketURI}" } } task user2 { useCluster testClusters.myTestCluster doFirst { - println "$path" + println "$path: Cluster running @ ${testClusters.myTestCluster.httpSocketURI}" } } diff --git a/buildSrc/src/testKit/testclusters_multiproject/bravo/build.gradle b/buildSrc/src/testKit/testclusters_multiproject/bravo/build.gradle index b62302d9d546e..d13cab6eaa934 100644 --- a/buildSrc/src/testKit/testclusters_multiproject/bravo/build.gradle +++ b/buildSrc/src/testKit/testclusters_multiproject/bravo/build.gradle @@ -12,13 +12,13 @@ testClusters { task user1 { useCluster testClusters.myTestCluster doFirst { - println "$path" + println "$path: Cluster running @ ${testClusters.myTestCluster.httpSocketURI}" } } task user2 { useCluster testClusters.myTestCluster doFirst { - println "$path" + println "$path: Cluster running @ ${testClusters.myTestCluster.httpSocketURI}" } } diff --git a/buildSrc/src/testKit/testclusters_multiproject/build.gradle b/buildSrc/src/testKit/testclusters_multiproject/build.gradle index 06234f4b3688c..18f7b277d01e3 100644 --- a/buildSrc/src/testKit/testclusters_multiproject/build.gradle +++ b/buildSrc/src/testKit/testclusters_multiproject/build.gradle @@ -20,13 +20,13 @@ testClusters { task user1 { useCluster testClusters.myTestCluster doFirst { - println "$path" + println "$path: Cluster running @ ${testClusters.myTestCluster.httpSocketURI}" } } task user2 { useCluster testClusters.myTestCluster doFirst { - println "$path" + println "$path: Cluster running @ ${testClusters.myTestCluster.httpSocketURI}" } } \ No newline at end of file diff --git a/test/framework/build.gradle b/test/framework/build.gradle index ccdd2876b4c16..f6328c7482b8b 100644 --- a/test/framework/build.gradle +++ b/test/framework/build.gradle @@ -65,7 +65,7 @@ thirdPartyAudit.excludes = [ task namingConventionsMain(type: org.elasticsearch.gradle.precommit.NamingConventionsTask) { checkForTestsInMain = true - javaHome = project.runtimeJavaHome + javaHome = project.compilerJavaHome } precommit.dependsOn namingConventionsMain From 026a14b7de133f045e725b80b52c710329034f52 Mon Sep 17 00:00:00 2001 From: Guido Lena Cota Date: Tue, 4 Dec 2018 11:09:08 +0100 Subject: [PATCH 099/115] (Minor) Fix some typos (#36180) --- docs/reference/mapping/types/parent-join.asciidoc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/reference/mapping/types/parent-join.asciidoc b/docs/reference/mapping/types/parent-join.asciidoc index 055109a4ce2d7..d2d68fc13590b 100644 --- a/docs/reference/mapping/types/parent-join.asciidoc +++ b/docs/reference/mapping/types/parent-join.asciidoc @@ -46,7 +46,7 @@ PUT my_index/_doc/1?refresh PUT my_index/_doc/2?refresh { - "text": "This is a another question", + "text": "This is another question", "my_join_field": { "name": "question" } @@ -417,7 +417,7 @@ The mapping above represents the following tree: | vote -Indexing a grand child document requires a `routing` value equals +Indexing a grandchild document requires a `routing` value equals to the grand-parent (the greater parent of the lineage): @@ -436,4 +436,4 @@ PUT my_index/_doc/3?routing=1&refresh <1> // TEST[continued] <1> This child document must be on the same shard than its grand-parent and parent -<2> The parent id of this document (must points to an `answer` document) \ No newline at end of file +<2> The parent id of this document (must points to an `answer` document) From e7cbaf81d2a36a97390aa904b5e3ec3b1940606c Mon Sep 17 00:00:00 2001 From: Jim Ferenczi Date: Tue, 4 Dec 2018 13:29:44 +0100 Subject: [PATCH 100/115] Added soft limit to open scroll contexts #25244 (#36009) (#36174) This change adds a soft limit to open scroll contexts that can be controlled with the dynamic cluster setting `search.max_open_scroll_context` (defaults to unlimited). Relates #36009 --- .../migration/migrate_6_0/search.asciidoc | 11 +++- docs/reference/search/request/scroll.asciidoc | 4 ++ .../common/settings/ClusterSettings.java | 1 + .../elasticsearch/search/SearchService.java | 37 +++++++++++ .../search/SearchServiceTests.java | 65 +++++++++++++++++++ 5 files changed, 117 insertions(+), 1 deletion(-) diff --git a/docs/reference/migration/migrate_6_0/search.asciidoc b/docs/reference/migration/migrate_6_0/search.asciidoc index 5b57fff8920d0..6283e571cc3ea 100644 --- a/docs/reference/migration/migrate_6_0/search.asciidoc +++ b/docs/reference/migration/migrate_6_0/search.asciidoc @@ -269,6 +269,15 @@ rewrite any prefix query on the field to a a single term query that matches the Setting a negative `boost` in a query is deprecated and will throw an error in the next version. To deboost a specific query you can use a `boost` comprise between 0 and 1. +[float] +==== Limit the number of open scroll contexts + +The number of scroll contexts allowed per node will be limited to 500 by default in the next major +version. Open scroll contexts are unlimited by default in this version, you can change the dynamic +cluster setting `search.max_open_scroll_context` to force a limit. If the limit is unchanged, a +deprecation warning will be printed if the number of open scroll context is greater than 500 (the +default limit in the next major version). + [float] ==== The filter context is deprecated @@ -277,4 +286,4 @@ the distinction between queries and filters is decided in Lucene depending on whether queries need to access score or not. As a result `bool` queries with `should` clauses that don't need to access the score will issue a deprecation warning when they automatically set `minimum_should_match` to 1. -This behavior will be removed in the next major version. \ No newline at end of file +This behavior will be removed in the next major version. diff --git a/docs/reference/search/request/scroll.asciidoc b/docs/reference/search/request/scroll.asciidoc index 7fd0dd46cbc6f..ad46aabd3b416 100644 --- a/docs/reference/search/request/scroll.asciidoc +++ b/docs/reference/search/request/scroll.asciidoc @@ -125,6 +125,10 @@ TIP: Keeping older segments alive means that more file handles are needed. Ensure that you have configured your nodes to have ample free file handles. See <>. +NOTE: To prevent against issues caused by having too many scrolls open, you +can limit the number of open scrolls per node with the +`search.max_open_scroll_context` cluster setting (defaults to unlimited). + You can check how many search contexts are open with the <>: diff --git a/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java b/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java index bc5c61ce099b8..7333c02203dd1 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java +++ b/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java @@ -389,6 +389,7 @@ public void apply(Settings value, Settings current, Settings previous) { SearchService.MAX_KEEPALIVE_SETTING, MultiBucketConsumerService.MAX_BUCKET_SETTING, SearchService.LOW_LEVEL_CANCELLATION_SETTING, + SearchService.MAX_OPEN_SCROLL_CONTEXT, Node.WRITE_PORTS_FILE_SETTING, Node.NODE_NAME_SETTING, Node.NODE_DATA_SETTING, diff --git a/server/src/main/java/org/elasticsearch/search/SearchService.java b/server/src/main/java/org/elasticsearch/search/SearchService.java index ea79c19940cb7..070a0269e7106 100644 --- a/server/src/main/java/org/elasticsearch/search/SearchService.java +++ b/server/src/main/java/org/elasticsearch/search/SearchService.java @@ -23,6 +23,7 @@ import org.apache.logging.log4j.Logger; import org.apache.lucene.search.FieldDoc; import org.apache.lucene.search.TopDocs; +import org.elasticsearch.common.logging.DeprecationLogger; import org.elasticsearch.core.internal.io.IOUtils; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.ExceptionsHelper; @@ -113,6 +114,7 @@ import java.util.Optional; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.function.LongSupplier; import java.util.function.Supplier; @@ -123,6 +125,7 @@ public class SearchService extends AbstractLifecycleComponent implements IndexEventListener { private static final Logger logger = LogManager.getLogger(SearchService.class); + private static final DeprecationLogger deprecationLogger = new DeprecationLogger(logger); // we can have 5 minutes here, since we make sure to clean with search requests and when shard/index closes public static final Setting DEFAULT_KEEPALIVE_SETTING = @@ -146,6 +149,9 @@ public class SearchService extends AbstractLifecycleComponent implements IndexEv public static final Setting DEFAULT_ALLOW_PARTIAL_SEARCH_RESULTS = Setting.boolSetting("search.default_allow_partial_results", true, Property.Dynamic, Property.NodeScope); + public static final Setting MAX_OPEN_SCROLL_CONTEXT = + Setting.intSetting("search.max_open_scroll_context", Integer.MAX_VALUE, 0, Property.Dynamic, Property.NodeScope); + private final ThreadPool threadPool; @@ -175,6 +181,8 @@ public class SearchService extends AbstractLifecycleComponent implements IndexEv private volatile boolean lowLevelCancellation; + private volatile int maxOpenScrollContext; + private final Cancellable keepAliveReaper; private final AtomicLong idGenerator = new AtomicLong(); @@ -183,6 +191,8 @@ public class SearchService extends AbstractLifecycleComponent implements IndexEv private final MultiBucketConsumerService multiBucketConsumerService; + private final AtomicInteger openScrollContexts = new AtomicInteger(); + public SearchService(ClusterService clusterService, IndicesService indicesService, ThreadPool threadPool, ScriptService scriptService, BigArrays bigArrays, FetchPhase fetchPhase, ResponseCollectorService responseCollectorService) { @@ -213,6 +223,8 @@ public SearchService(ClusterService clusterService, IndicesService indicesServic clusterService.getClusterSettings().addSettingsUpdateConsumer(DEFAULT_ALLOW_PARTIAL_SEARCH_RESULTS, this::setDefaultAllowPartialSearchResults); + maxOpenScrollContext = MAX_OPEN_SCROLL_CONTEXT.get(settings); + clusterService.getClusterSettings().addSettingsUpdateConsumer(MAX_OPEN_SCROLL_CONTEXT, this::setMaxOpenScrollContext); lowLevelCancellation = LOW_LEVEL_CANCELLATION_SETTING.get(settings); clusterService.getClusterSettings().addSettingsUpdateConsumer(LOW_LEVEL_CANCELLATION_SETTING, this::setLowLevelCancellation); @@ -244,6 +256,10 @@ public boolean defaultAllowPartialSearchResults() { return defaultAllowPartialSearchResults; } + private void setMaxOpenScrollContext(int maxOpenScrollContext) { + this.maxOpenScrollContext = maxOpenScrollContext; + } + private void setLowLevelCancellation(Boolean lowLevelCancellation) { this.lowLevelCancellation = lowLevelCancellation; } @@ -593,11 +609,31 @@ private SearchContext findContext(long id, TransportRequest request) throws Sear } final SearchContext createAndPutContext(ShardSearchRequest request) throws IOException { + if (request.scroll() != null) { + if (maxOpenScrollContext == Integer.MAX_VALUE && openScrollContexts.get() > 500) { + /** + * Logs a deprecation warning if the number of open scrolls is greater than the + * default limit in the next major version (500) and the cluster setting is set + * to the default value in this version ({@link Integer#MAX_VALUE}. + */ + deprecationLogger.deprecatedAndMaybeLog("max_open_scroll", "Trying to create more than 500 scroll contexts will" + + " not be allowed in the next major version by default. You can change the [" + + MAX_OPEN_SCROLL_CONTEXT.getKey() + "] setting to use a greater default value or lower the number of" + + " scrolls that you need to run in parallel."); + } else if (openScrollContexts.get() >= maxOpenScrollContext) { + throw new ElasticsearchException( + "Trying to create too many scroll contexts. Must be less than or equal to: [" + + maxOpenScrollContext + "]. " + "This limit can be set by changing the [" + + MAX_OPEN_SCROLL_CONTEXT.getKey() + "] setting."); + } + } + SearchContext context = createContext(request); boolean success = false; try { putContext(context); if (request.scroll() != null) { + openScrollContexts.incrementAndGet(); context.indexShard().getSearchOperationListener().onNewScrollContext(context); } context.indexShard().getSearchOperationListener().onNewContext(context); @@ -697,6 +733,7 @@ public boolean freeContext(long id) { try { context.indexShard().getSearchOperationListener().onFreeContext(context); if (context.scrollContext() != null) { + openScrollContexts.decrementAndGet(); context.indexShard().getSearchOperationListener().onFreeScrollContext(context); } } finally { diff --git a/server/src/test/java/org/elasticsearch/search/SearchServiceTests.java b/server/src/test/java/org/elasticsearch/search/SearchServiceTests.java index 54ab48d1c7674..a0c1ed3bf9921 100644 --- a/server/src/test/java/org/elasticsearch/search/SearchServiceTests.java +++ b/server/src/test/java/org/elasticsearch/search/SearchServiceTests.java @@ -21,12 +21,14 @@ import com.carrotsearch.hppc.IntArrayList; import org.apache.lucene.search.Query; import org.apache.lucene.store.AlreadyClosedException; +import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.index.IndexResponse; import org.elasticsearch.action.search.SearchPhaseExecutionException; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.action.search.SearchTask; import org.elasticsearch.action.search.SearchType; +import org.elasticsearch.action.search.ClearScrollRequest; import org.elasticsearch.action.support.IndicesOptions; import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.action.support.WriteRequest; @@ -76,6 +78,7 @@ import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.LinkedList; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; @@ -89,6 +92,7 @@ import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertHitCount; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSearchHits; +import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.CoreMatchers.is; @@ -418,6 +422,51 @@ searchSourceBuilder, new String[0], false, new AliasFilter(null, Strings.EMPTY_A } } + /** + * test that creating more than the allowed number of scroll contexts throws an exception + */ + public void testMaxOpenScrollContexts() throws RuntimeException, IOException { + createIndex("index"); + client().prepareIndex("index", "type", "1").setSource("field", "value").setRefreshPolicy(IMMEDIATE).get(); + + client().admin().cluster().prepareUpdateSettings() + .setPersistentSettings( + Settings.builder() + .put(SearchService.MAX_OPEN_SCROLL_CONTEXT.getKey(), 500)) + .get(); + try { + + LinkedList clearScrollIds = new LinkedList<>(); + + for (int i = 0; i < 500; i++) { + SearchResponse searchResponse = client().prepareSearch("index").setSize(1).setScroll("1m").get(); + + if (randomInt(4) == 0) clearScrollIds.addLast(searchResponse.getScrollId()); + } + + ClearScrollRequest clearScrollRequest = new ClearScrollRequest(); + clearScrollRequest.setScrollIds(clearScrollIds); + client().clearScroll(clearScrollRequest); + + for (int i = 0; i < clearScrollIds.size(); i++) { + client().prepareSearch("index").setSize(1).setScroll("1m").get(); + } + ElasticsearchException ex = expectThrows(ElasticsearchException.class, + () -> client().prepareSearch("index").setSize(1).setScroll("1m").get()); + assertThat(ex.getDetailedMessage(), containsString( + "Trying to create too many scroll contexts. Must be less than or equal to: [500]. " + + "This limit can be set by changing the [search.max_open_scroll_context] setting." + ) + ); + } finally { + client().admin().cluster().prepareUpdateSettings() + .setPersistentSettings( + Settings.builder() + .putNull(SearchService.MAX_OPEN_SCROLL_CONTEXT.getKey())) + .get(); + } + } + public static class FailOnRewriteQueryPlugin extends Plugin implements SearchPlugin { @Override public List> getQueries() { @@ -473,6 +522,22 @@ public String getWriteableName() { } } + public static class ShardScrollRequestTest extends ShardSearchLocalRequest { + private Scroll scroll; + + ShardScrollRequestTest(ShardId shardId) { + super(shardId, 1, SearchType.DEFAULT, new SearchSourceBuilder(), + new String[0], false, new AliasFilter(null, Strings.EMPTY_ARRAY), 1f, true, null, null); + + this.scroll = new Scroll(TimeValue.timeValueMinutes(1)); + } + + @Override + public Scroll scroll() { + return this.scroll; + } + } + public void testCanMatch() throws IOException { createIndex("index"); final SearchService service = getInstanceFromNode(SearchService.class); From cce08e86d756b1e34d73da2cc6b0ca8ea6558440 Mon Sep 17 00:00:00 2001 From: Tanguy Leroux Date: Tue, 4 Dec 2018 14:59:51 +0100 Subject: [PATCH 101/115] Combine the execution of an exclusive replica operation with primary term update (#36116) This commit changes how an operation which requires all index shard operations permits is executed when a primary term update is required: the operation and the update are combined so that the operation is executed after the primary term update under the same blocking operation. Closes #35850 Co-authored-by: Yannick Welsch --- .../elasticsearch/index/shard/IndexShard.java | 122 ++++++++++++------ .../index/shard/IndexShardTests.java | 64 ++++++++- 2 files changed, 142 insertions(+), 44 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java b/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java index 6763631124fe8..8baf189dba3fb 100644 --- a/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java +++ b/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java @@ -64,6 +64,7 @@ import org.elasticsearch.common.util.BigArrays; import org.elasticsearch.common.util.concurrent.AbstractRunnable; import org.elasticsearch.common.util.concurrent.AsyncIOProcessor; +import org.elasticsearch.common.util.concurrent.RunOnce; import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.core.internal.io.IOUtils; import org.elasticsearch.index.Index; @@ -575,7 +576,7 @@ public void onFailure(Exception e) { } catch (final AlreadyClosedException e) { // okay, the index was deleted } - }); + }, null); } } // set this last, once we finished updating all internal state. @@ -2362,14 +2363,26 @@ public void acquireAllPrimaryOperationsPermits(final ActionListener indexShardOperationPermits.asyncBlockOperations(onPermitAcquired, timeout.duration(), timeout.timeUnit()); } - private void bumpPrimaryTerm(final long newPrimaryTerm, final CheckedRunnable onBlocked) { + private void bumpPrimaryTerm(final long newPrimaryTerm, + final CheckedRunnable onBlocked, + @Nullable ActionListener combineWithAction) { assert Thread.holdsLock(mutex); - assert newPrimaryTerm > pendingPrimaryTerm; + assert newPrimaryTerm > pendingPrimaryTerm || (newPrimaryTerm >= pendingPrimaryTerm && combineWithAction != null); assert operationPrimaryTerm <= pendingPrimaryTerm; final CountDownLatch termUpdated = new CountDownLatch(1); indexShardOperationPermits.asyncBlockOperations(new ActionListener() { @Override public void onFailure(final Exception e) { + try { + innerFail(e); + } finally { + if (combineWithAction != null) { + combineWithAction.onFailure(e); + } + } + } + + private void innerFail(final Exception e) { try { failShard("exception during primary term transition", e); } catch (AlreadyClosedException ace) { @@ -2379,7 +2392,8 @@ public void onFailure(final Exception e) { @Override public void onResponse(final Releasable releasable) { - try (Releasable ignored = releasable) { + final RunOnce releaseOnce = new RunOnce(releasable::close); + try { assert operationPrimaryTerm <= pendingPrimaryTerm; termUpdated.await(); // indexShardOperationPermits doesn't guarantee that async submissions are executed @@ -2389,7 +2403,17 @@ public void onResponse(final Releasable releasable) { onBlocked.run(); } } catch (final Exception e) { - onFailure(e); + if (combineWithAction == null) { + // otherwise leave it to combineWithAction to release the permit + releaseOnce.run(); + } + innerFail(e); + } finally { + if (combineWithAction != null) { + combineWithAction.onResponse(releasable); + } else { + releaseOnce.run(); + } } } }, 30, TimeUnit.MINUTES); @@ -2417,7 +2441,7 @@ public void onResponse(final Releasable releasable) { public void acquireReplicaOperationPermit(final long opPrimaryTerm, final long globalCheckpoint, final long maxSeqNoOfUpdatesOrDeletes, final ActionListener onPermitAcquired, final String executorOnDelay, final Object debugInfo) { - innerAcquireReplicaOperationPermit(opPrimaryTerm, globalCheckpoint, maxSeqNoOfUpdatesOrDeletes, onPermitAcquired, + innerAcquireReplicaOperationPermit(opPrimaryTerm, globalCheckpoint, maxSeqNoOfUpdatesOrDeletes, onPermitAcquired, false, (listener) -> indexShardOperationPermits.acquire(listener, executorOnDelay, true, debugInfo)); } @@ -2439,7 +2463,7 @@ public void acquireAllReplicaOperationsPermits(final long opPrimaryTerm, final long maxSeqNoOfUpdatesOrDeletes, final ActionListener onPermitAcquired, final TimeValue timeout) { - innerAcquireReplicaOperationPermit(opPrimaryTerm, globalCheckpoint, maxSeqNoOfUpdatesOrDeletes, onPermitAcquired, + innerAcquireReplicaOperationPermit(opPrimaryTerm, globalCheckpoint, maxSeqNoOfUpdatesOrDeletes, onPermitAcquired, true, (listener) -> indexShardOperationPermits.asyncBlockOperations(listener, timeout.duration(), timeout.timeUnit())); } @@ -2447,41 +2471,16 @@ private void innerAcquireReplicaOperationPermit(final long opPrimaryTerm, final long globalCheckpoint, final long maxSeqNoOfUpdatesOrDeletes, final ActionListener onPermitAcquired, - final Consumer> consumer) { + final boolean allowCombineOperationWithPrimaryTermUpdate, + final Consumer> operationExecutor) { verifyNotClosed(); - if (opPrimaryTerm > pendingPrimaryTerm) { - synchronized (mutex) { - if (opPrimaryTerm > pendingPrimaryTerm) { - final IndexShardState shardState = state(); - // only roll translog and update primary term if shard has made it past recovery - // Having a new primary term here means that the old primary failed and that there is a new primary, which again - // means that the master will fail this shard as all initializing shards are failed when a primary is selected - // We abort early here to prevent an ongoing recovery from the failed primary to mess with the global / local checkpoint - if (shardState != IndexShardState.POST_RECOVERY && - shardState != IndexShardState.STARTED) { - throw new IndexShardNotStartedException(shardId, shardState); - } - if (opPrimaryTerm > pendingPrimaryTerm) { - bumpPrimaryTerm(opPrimaryTerm, () -> { - updateGlobalCheckpointOnReplica(globalCheckpoint, "primary term transition"); - final long currentGlobalCheckpoint = getGlobalCheckpoint(); - final long maxSeqNo = seqNoStats().getMaxSeqNo(); - logger.info("detected new primary with primary term [{}], global checkpoint [{}], max_seq_no [{}]", - opPrimaryTerm, currentGlobalCheckpoint, maxSeqNo); - if (currentGlobalCheckpoint < maxSeqNo) { - resetEngineToGlobalCheckpoint(); - } else { - getEngine().rollTranslogGeneration(); - } - }); - } - } - } - } - assert opPrimaryTerm <= pendingPrimaryTerm - : "operation primary term [" + opPrimaryTerm + "] should be at most [" + pendingPrimaryTerm + "]"; - consumer.accept(new ActionListener() { + // This listener is used for the execution of the operation. If the operation requires all the permits for its + // execution and the primary term must be updated first, we can combine the operation execution with the + // primary term update. Since indexShardOperationPermits doesn't guarantee that async submissions are executed + // in the order submitted, combining both operations ensure that the term is updated before the operation is + // executed. It also has the side effect of acquiring all the permits one time instead of two. + final ActionListener operationListener = new ActionListener() { @Override public void onResponse(final Releasable releasable) { if (opPrimaryTerm < operationPrimaryTerm) { @@ -2511,7 +2510,48 @@ public void onResponse(final Releasable releasable) { public void onFailure(final Exception e) { onPermitAcquired.onFailure(e); } - }); + }; + + if (requirePrimaryTermUpdate(opPrimaryTerm, allowCombineOperationWithPrimaryTermUpdate)) { + synchronized (mutex) { + if (requirePrimaryTermUpdate(opPrimaryTerm, allowCombineOperationWithPrimaryTermUpdate)) { + final IndexShardState shardState = state(); + // only roll translog and update primary term if shard has made it past recovery + // Having a new primary term here means that the old primary failed and that there is a new primary, which again + // means that the master will fail this shard as all initializing shards are failed when a primary is selected + // We abort early here to prevent an ongoing recovery from the failed primary to mess with the global / local checkpoint + if (shardState != IndexShardState.POST_RECOVERY && + shardState != IndexShardState.STARTED) { + throw new IndexShardNotStartedException(shardId, shardState); + } + + bumpPrimaryTerm(opPrimaryTerm, () -> { + updateGlobalCheckpointOnReplica(globalCheckpoint, "primary term transition"); + final long currentGlobalCheckpoint = getGlobalCheckpoint(); + final long maxSeqNo = seqNoStats().getMaxSeqNo(); + logger.info("detected new primary with primary term [{}], global checkpoint [{}], max_seq_no [{}]", + opPrimaryTerm, currentGlobalCheckpoint, maxSeqNo); + if (currentGlobalCheckpoint < maxSeqNo) { + resetEngineToGlobalCheckpoint(); + } else { + getEngine().rollTranslogGeneration(); + } + }, allowCombineOperationWithPrimaryTermUpdate ? operationListener : null); + + if (allowCombineOperationWithPrimaryTermUpdate) { + logger.debug("operation execution has been combined with primary term update"); + return; + } + } + } + } + assert opPrimaryTerm <= pendingPrimaryTerm + : "operation primary term [" + opPrimaryTerm + "] should be at most [" + pendingPrimaryTerm + "]"; + operationExecutor.accept(operationListener); + } + + private boolean requirePrimaryTermUpdate(final long opPrimaryTerm, final boolean allPermits) { + return (opPrimaryTerm > pendingPrimaryTerm) || (allPermits && opPrimaryTerm > operationPrimaryTerm); } public int getActiveOperationsCount() { diff --git a/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java b/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java index 3e9dbce081802..9ab80e4870e39 100644 --- a/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java +++ b/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java @@ -734,7 +734,6 @@ private Releasable acquireReplicaOperationPermitBlockingly(IndexShard indexShard return fut.get(); } - @AwaitsFix(bugUrl="https://github.com/elastic/elasticsearch/issues/35850") public void testOperationPermitOnReplicaShards() throws Exception { final ShardId shardId = new ShardId("test", "_na_", 0); final IndexShard indexShard; @@ -1025,7 +1024,6 @@ public void testGlobalCheckpointSync() throws IOException { closeShards(replicaShard, primaryShard); } - @AwaitsFix(bugUrl="https://github.com/elastic/elasticsearch/issues/35850") public void testRestoreLocalHistoryFromTranslogOnPromotion() throws IOException, InterruptedException { final IndexShard indexShard = newStartedShard(false); final int operations = 1024 - scaledRandomIntBetween(0, 1024); @@ -1090,7 +1088,6 @@ public void onFailure(Exception e) { closeShard(indexShard, false); } - @AwaitsFix(bugUrl="https://github.com/elastic/elasticsearch/issues/35850") public void testRollbackReplicaEngineOnPromotion() throws IOException, InterruptedException { final IndexShard indexShard = newStartedShard(false); @@ -3445,6 +3442,67 @@ public void testResetEngine() throws Exception { closeShard(shard, false); } + public void testConcurrentAcquireAllReplicaOperationsPermitsWithPrimaryTermUpdate() throws Exception { + final IndexShard replica = newStartedShard(false); + indexOnReplicaWithGaps(replica, between(0, 1000), Math.toIntExact(replica.getLocalCheckpoint())); + + final int nbTermUpdates = randomIntBetween(1, 5); + + for (int i = 0; i < nbTermUpdates; i++) { + long opPrimaryTerm = replica.getOperationPrimaryTerm() + 1; + final long globalCheckpoint = replica.getGlobalCheckpoint(); + final long maxSeqNoOfUpdatesOrDeletes = replica.getMaxSeqNoOfUpdatesOrDeletes(); + + final int operations = scaledRandomIntBetween(5, 32); + final CyclicBarrier barrier = new CyclicBarrier(1 + operations); + final CountDownLatch latch = new CountDownLatch(operations); + + final Thread[] threads = new Thread[operations]; + for (int j = 0; j < operations; j++) { + threads[j] = new Thread(() -> { + try { + barrier.await(); + } catch (final BrokenBarrierException | InterruptedException e) { + throw new RuntimeException(e); + } + replica.acquireAllReplicaOperationsPermits( + opPrimaryTerm, + globalCheckpoint, + maxSeqNoOfUpdatesOrDeletes, + new ActionListener() { + @Override + public void onResponse(final Releasable releasable) { + try (Releasable ignored = releasable) { + assertThat(replica.getPendingPrimaryTerm(), greaterThanOrEqualTo(opPrimaryTerm)); + assertThat(replica.getOperationPrimaryTerm(), equalTo(opPrimaryTerm)); + } finally { + latch.countDown(); + } + } + + @Override + public void onFailure(final Exception e) { + try { + throw new RuntimeException(e); + } finally { + latch.countDown(); + } + } + }, TimeValue.timeValueMinutes(30L)); + }); + threads[j].start(); + } + barrier.await(); + latch.await(); + + for (Thread thread : threads) { + thread.join(); + } + } + + closeShard(replica, false); + } + /** * Randomizes the usage of {@link IndexShard#acquireReplicaOperationPermit(long, long, long, ActionListener, String, Object)} and * {@link IndexShard#acquireAllReplicaOperationsPermits(long, long, long, ActionListener, TimeValue)} in order to acquire a permit. From b00585fe8bc7dc58bcc9524e81b7679f8583e9a2 Mon Sep 17 00:00:00 2001 From: Chris Koehnke Date: Fri, 30 Nov 2018 13:17:39 -0500 Subject: [PATCH 102/115] Docs: Fix release-state check for oss repositories (#36120) To get the newly added oss apt/yum sections to get rendered for `released` and `prerelease` versions the condition needs to be modified. --- docs/reference/setup/install/deb.asciidoc | 2 +- docs/reference/setup/install/rpm.asciidoc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference/setup/install/deb.asciidoc b/docs/reference/setup/install/deb.asciidoc index 3bbda0a579cc4..c5046f51ba112 100644 --- a/docs/reference/setup/install/deb.asciidoc +++ b/docs/reference/setup/install/deb.asciidoc @@ -105,7 +105,7 @@ endif::[] include::skip-set-kernel-parameters.asciidoc[] -ifeval::["{release-state}"=="released"] +ifeval::["{release-state}"!="unreleased"] [NOTE] ================================================== diff --git a/docs/reference/setup/install/rpm.asciidoc b/docs/reference/setup/install/rpm.asciidoc index 02e69a35d9c0c..11add03573db1 100644 --- a/docs/reference/setup/install/rpm.asciidoc +++ b/docs/reference/setup/install/rpm.asciidoc @@ -90,7 +90,7 @@ sudo zypper install elasticsearch <3> endif::[] -ifeval::["{release-state}"=="released"] +ifeval::["{release-state}"!="unreleased"] [NOTE] ================================================== From 7bd37c0db2aecc1ab8bb98899156d5aad64c6af1 Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Tue, 4 Dec 2018 15:55:15 +0100 Subject: [PATCH 103/115] [CCR] AutoFollowCoordinator should tolerate that auto follow patterns may be removed (#35945) AutoFollowCoordinator should take into account that after auto following an index and while updating that a leader index has been followed, that the auto follow pattern may have been removed via delete auto follow patterns api. Also fixed a bug that when a remote cluster connection has been removed, the auto follow coordinator does not die when it tries get a remote client for that cluster. Closes #35480 --- .../java/org/elasticsearch/client/CCRIT.java | 23 ++++++++++++--- .../xpack/ccr/CcrLicenseChecker.java | 11 +++++-- .../ccr/action/AutoFollowCoordinator.java | 7 +++++ .../action/AutoFollowCoordinatorTests.java | 29 +++++++++++++++++++ 4 files changed, 64 insertions(+), 6 deletions(-) diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/CCRIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/CCRIT.java index 9c5db63ada9ed..391ee1fcd18b4 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/CCRIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/CCRIT.java @@ -39,6 +39,7 @@ import org.elasticsearch.client.ccr.ResumeFollowRequest; import org.elasticsearch.client.ccr.UnfollowRequest; import org.elasticsearch.client.core.AcknowledgedResponse; +import org.elasticsearch.common.xcontent.ObjectPath; import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.common.xcontent.json.JsonXContent; @@ -55,7 +56,7 @@ public class CCRIT extends ESRestHighLevelClientTestCase { @Before - public void setupRemoteClusterConfig() throws IOException { + public void setupRemoteClusterConfig() throws Exception { // Configure local cluster as remote cluster: // TODO: replace with nodes info highlevel rest client code when it is available: final Request request = new Request("GET", "/_nodes"); @@ -69,6 +70,14 @@ public void setupRemoteClusterConfig() throws IOException { ClusterUpdateSettingsResponse updateSettingsResponse = highLevelClient().cluster().putSettings(updateSettingsRequest, RequestOptions.DEFAULT); assertThat(updateSettingsResponse.isAcknowledged(), is(true)); + + assertBusy(() -> { + Map localConnection = (Map) toMap(client() + .performRequest(new Request("GET", "/_remote/info"))) + .get("local"); + assertThat(localConnection, notNullValue()); + assertThat(localConnection.get("connected"), is(true)); + }); } public void testIndexFollowing() throws Exception { @@ -132,7 +141,6 @@ public void testIndexFollowing() throws Exception { assertThat(unfollowResponse.isAcknowledged(), is(true)); } - @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/35937") public void testAutoFollowing() throws Exception { CcrClient ccrClient = highLevelClient().ccr(); PutAutoFollowPatternRequest putAutoFollowPatternRequest = @@ -149,14 +157,21 @@ public void testAutoFollowing() throws Exception { assertBusy(() -> { assertThat(indexExists("copy-logs-20200101"), is(true)); + // TODO: replace with HLRC follow stats when available: + Map rsp = toMap(client().performRequest(new Request("GET", "/copy-logs-20200101/_ccr/stats"))); + String index = null; + try { + index = ObjectPath.eval("indices.0.index", rsp); + } catch (Exception e){ } + assertThat(index, equalTo("copy-logs-20200101")); }); GetAutoFollowPatternRequest getAutoFollowPatternRequest = randomBoolean() ? new GetAutoFollowPatternRequest("pattern1") : new GetAutoFollowPatternRequest(); GetAutoFollowPatternResponse getAutoFollowPatternResponse = execute(getAutoFollowPatternRequest, ccrClient::getAutoFollowPattern, ccrClient::getAutoFollowPatternAsync); - assertThat(getAutoFollowPatternResponse.getPatterns().size(), equalTo(1L)); - GetAutoFollowPatternResponse.Pattern pattern = getAutoFollowPatternResponse.getPatterns().get("patterns1"); + assertThat(getAutoFollowPatternResponse.getPatterns().size(), equalTo(1)); + GetAutoFollowPatternResponse.Pattern pattern = getAutoFollowPatternResponse.getPatterns().get("pattern1"); assertThat(pattern, notNullValue()); assertThat(pattern.getRemoteCluster(), equalTo(putAutoFollowPatternRequest.getRemoteCluster())); assertThat(pattern.getLeaderIndexPatterns(), equalTo(putAutoFollowPatternRequest.getLeaderIndexPatterns())); diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/CcrLicenseChecker.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/CcrLicenseChecker.java index d3ee59c96b1d9..d7f147fb33352 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/CcrLicenseChecker.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/CcrLicenseChecker.java @@ -161,15 +161,22 @@ public void checkRemoteClusterLicenseAndFetchClusterState( final ClusterStateRequest request, final Consumer onFailure, final Consumer leaderClusterStateConsumer) { - checkRemoteClusterLicenseAndFetchClusterState( + try { + Client remoteClient = systemClient(client.getRemoteClusterClient(clusterAlias)); + checkRemoteClusterLicenseAndFetchClusterState( client, clusterAlias, - systemClient(client.getRemoteClusterClient(clusterAlias)), + remoteClient, request, onFailure, leaderClusterStateConsumer, CcrLicenseChecker::clusterStateNonCompliantRemoteLicense, e -> clusterStateUnknownRemoteLicense(clusterAlias, e)); + } catch (Exception e) { + // client.getRemoteClusterClient(...) can fail with a IllegalArgumentException if remote + // connection is unknown + onFailure.accept(e); + } } /** diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/AutoFollowCoordinator.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/AutoFollowCoordinator.java index 0e86aa157adfc..6bddedc010406 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/AutoFollowCoordinator.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/AutoFollowCoordinator.java @@ -403,6 +403,13 @@ static Function recordLeaderIndexAsFollowFunction(St return currentState -> { AutoFollowMetadata currentAutoFollowMetadata = currentState.metaData().custom(AutoFollowMetadata.TYPE); Map> newFollowedIndexUUIDS = new HashMap<>(currentAutoFollowMetadata.getFollowedLeaderIndexUUIDs()); + if (newFollowedIndexUUIDS.containsKey(name) == false) { + // A delete auto follow pattern request can have removed the auto follow pattern while we want to update + // the auto follow metadata with the fact that an index was successfully auto followed. If this + // happens, we can just skip this step. + return currentState; + } + newFollowedIndexUUIDS.compute(name, (key, existingUUIDs) -> { assert existingUUIDs != null; List newUUIDs = new ArrayList<>(existingUUIDs); diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/AutoFollowCoordinatorTests.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/AutoFollowCoordinatorTests.java index 4624a3622b992..2b7fee13502af 100644 --- a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/AutoFollowCoordinatorTests.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/AutoFollowCoordinatorTests.java @@ -40,8 +40,10 @@ import java.util.function.Consumer; import java.util.function.Function; +import static org.elasticsearch.xpack.ccr.action.AutoFollowCoordinator.AutoFollower.recordLeaderIndexAsFollowFunction; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.sameInstance; import static org.mockito.Matchers.anyString; @@ -384,6 +386,33 @@ public void testGetLeaderIndicesToFollow_shardsNotStarted() { assertThat(result.get(1).getName(), equalTo("index2")); } + public void testRecordLeaderIndexAsFollowFunction() { + AutoFollowMetadata autoFollowMetadata = new AutoFollowMetadata(Collections.emptyMap(), + Collections.singletonMap("pattern1", Collections.emptyList()), Collections.emptyMap()); + ClusterState clusterState = new ClusterState.Builder(new ClusterName("name")) + .metaData(new MetaData.Builder().putCustom(AutoFollowMetadata.TYPE, autoFollowMetadata)) + .build(); + Function function = recordLeaderIndexAsFollowFunction("pattern1", new Index("index1", "index1")); + + ClusterState result = function.apply(clusterState); + AutoFollowMetadata autoFollowMetadataResult = result.metaData().custom(AutoFollowMetadata.TYPE); + assertThat(autoFollowMetadataResult.getFollowedLeaderIndexUUIDs().get("pattern1"), notNullValue()); + assertThat(autoFollowMetadataResult.getFollowedLeaderIndexUUIDs().get("pattern1").size(), equalTo(1)); + assertThat(autoFollowMetadataResult.getFollowedLeaderIndexUUIDs().get("pattern1").get(0), equalTo("index1")); + } + + public void testRecordLeaderIndexAsFollowFunctionNoEntry() { + AutoFollowMetadata autoFollowMetadata = new AutoFollowMetadata(Collections.emptyMap(), Collections.emptyMap(), + Collections.emptyMap()); + ClusterState clusterState = new ClusterState.Builder(new ClusterName("name")) + .metaData(new MetaData.Builder().putCustom(AutoFollowMetadata.TYPE, autoFollowMetadata)) + .build(); + Function function = recordLeaderIndexAsFollowFunction("pattern1", new Index("index1", "index1")); + + ClusterState result = function.apply(clusterState); + assertThat(result, sameInstance(clusterState)); + } + public void testGetFollowerIndexName() { AutoFollowPattern autoFollowPattern = new AutoFollowPattern("remote", Collections.singletonList("metrics-*"), null, null, null, null, null, null, null, null, null, null, null); From 89c3eaa329655b6233f3327834a42bd4033e00e7 Mon Sep 17 00:00:00 2001 From: Mayya Sharipova Date: Tue, 4 Dec 2018 10:30:06 -0500 Subject: [PATCH 104/115] Deprecate setting boost on inner span queries (#36191) Convert parsingException to deprcation warning Substitute for #35967, backport for #34112 --- docs/reference/migration/migrate_6_6.asciidoc | 5 +++-- .../reference/query-dsl/span-queries.asciidoc | 6 +++--- .../query/SpanContainingQueryBuilder.java | 4 ++-- .../index/query/SpanFirstQueryBuilder.java | 2 +- .../index/query/SpanNearQueryBuilder.java | 2 +- .../index/query/SpanNotQueryBuilder.java | 4 ++-- .../index/query/SpanOrQueryBuilder.java | 2 +- .../index/query/SpanQueryBuilder.java | 21 +++++++++---------- .../index/query/SpanWithinQueryBuilder.java | 4 ++-- .../SpanContainingQueryBuilderTests.java | 16 ++++++-------- .../query/SpanFirstQueryBuilderTests.java | 8 +++---- .../query/SpanNearQueryBuilderTests.java | 7 +++---- .../index/query/SpanNotQueryBuilderTests.java | 14 ++++++------- .../index/query/SpanOrQueryBuilderTests.java | 8 +++---- .../query/SpanWithinQueryBuilderTests.java | 16 ++++++-------- 15 files changed, 52 insertions(+), 67 deletions(-) diff --git a/docs/reference/migration/migrate_6_6.asciidoc b/docs/reference/migration/migrate_6_6.asciidoc index 2cfba69c249d9..b083ab200965c 100644 --- a/docs/reference/migration/migrate_6_6.asciidoc +++ b/docs/reference/migration/migrate_6_6.asciidoc @@ -34,9 +34,10 @@ if those are used on any APIs. We plan to drop support for `_source_exclude` and `_source_include` in 7.0. [float] -==== Boosts on inner span queries are not allowed. +==== Deprecate boosts on inner span queries. -Attempts to set `boost` on inner span queries will now throw a parsing exception. +Setting `boost` on inner span queries is deprecated. In the next major version +setting `boost` on inner span queries will throw a parsing exception. [float] ==== Deprecate `.values` and `.getValues()` on doc values in scripts diff --git a/docs/reference/query-dsl/span-queries.asciidoc b/docs/reference/query-dsl/span-queries.asciidoc index 7dc65433432ec..55acd39f1f8f3 100644 --- a/docs/reference/query-dsl/span-queries.asciidoc +++ b/docs/reference/query-dsl/span-queries.asciidoc @@ -5,11 +5,11 @@ Span queries are low-level positional queries which provide expert control over the order and proximity of the specified terms. These are typically used to implement very specific queries on legal documents or patents. -It is only allowed to set boost on an outer span query. Compound span queries, +Setting `boost` on inner span queries is deprecated. Compound span queries, like span_near, only use the list of matching spans of inner span queries in order to find their own spans, which they then use to produce a score. Scores -are never computed on inner span queries, which is the reason why boosts are not -allowed: they only influence the way scores are computed, not spans. +are never computed on inner span queries, which is the reason why their boosts +don't make sense. Span queries cannot be mixed with non-span queries (with the exception of the `span_multi` query). diff --git a/server/src/main/java/org/elasticsearch/index/query/SpanContainingQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/SpanContainingQueryBuilder.java index 164a5809f6e39..4412cae1aa5ba 100644 --- a/server/src/main/java/org/elasticsearch/index/query/SpanContainingQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/SpanContainingQueryBuilder.java @@ -119,14 +119,14 @@ public static SpanContainingQueryBuilder fromXContent(XContentParser parser) thr throw new ParsingException(parser.getTokenLocation(), "span_containing [big] must be of type span query"); } big = (SpanQueryBuilder) query; - checkNoBoost(NAME, currentFieldName, parser, big); + checkNoBoost(big); } else if (LITTLE_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { QueryBuilder query = parseInnerQueryBuilder(parser); if (query instanceof SpanQueryBuilder == false) { throw new ParsingException(parser.getTokenLocation(), "span_containing [little] must be of type span query"); } little = (SpanQueryBuilder) query; - checkNoBoost(NAME, currentFieldName, parser, little); + checkNoBoost(little); } else { throw new ParsingException(parser.getTokenLocation(), "[span_containing] query does not support [" + currentFieldName + "]"); diff --git a/server/src/main/java/org/elasticsearch/index/query/SpanFirstQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/SpanFirstQueryBuilder.java index dfd13f9f9fe2b..16fa6a430364a 100644 --- a/server/src/main/java/org/elasticsearch/index/query/SpanFirstQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/SpanFirstQueryBuilder.java @@ -120,7 +120,7 @@ public static SpanFirstQueryBuilder fromXContent(XContentParser parser) throws I throw new ParsingException(parser.getTokenLocation(), "span_first [match] must be of type span query"); } match = (SpanQueryBuilder) query; - checkNoBoost(NAME, currentFieldName, parser, match); + checkNoBoost(match); } else { throw new ParsingException(parser.getTokenLocation(), "[span_first] query does not support [" + currentFieldName + "]"); } diff --git a/server/src/main/java/org/elasticsearch/index/query/SpanNearQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/SpanNearQueryBuilder.java index d43c8120fe0c5..f1c3ce06bbbb8 100644 --- a/server/src/main/java/org/elasticsearch/index/query/SpanNearQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/SpanNearQueryBuilder.java @@ -171,7 +171,7 @@ public static SpanNearQueryBuilder fromXContent(XContentParser parser) throws IO throw new ParsingException(parser.getTokenLocation(), "span_near [clauses] must be of type span query"); } final SpanQueryBuilder clause = (SpanQueryBuilder) query; - checkNoBoost(NAME, currentFieldName, parser, clause); + checkNoBoost(clause); clauses.add(clause); } } else { diff --git a/server/src/main/java/org/elasticsearch/index/query/SpanNotQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/SpanNotQueryBuilder.java index 41e632b68f40c..d7f0e8d513622 100644 --- a/server/src/main/java/org/elasticsearch/index/query/SpanNotQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/SpanNotQueryBuilder.java @@ -186,14 +186,14 @@ public static SpanNotQueryBuilder fromXContent(XContentParser parser) throws IOE throw new ParsingException(parser.getTokenLocation(), "span_not [include] must be of type span query"); } include = (SpanQueryBuilder) query; - checkNoBoost(NAME, currentFieldName, parser, include); + checkNoBoost(include); } else if (EXCLUDE_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { QueryBuilder query = parseInnerQueryBuilder(parser); if (query instanceof SpanQueryBuilder == false) { throw new ParsingException(parser.getTokenLocation(), "span_not [exclude] must be of type span query"); } exclude = (SpanQueryBuilder) query; - checkNoBoost(NAME, currentFieldName, parser, exclude); + checkNoBoost(exclude); } else { throw new ParsingException(parser.getTokenLocation(), "[span_not] query does not support [" + currentFieldName + "]"); } diff --git a/server/src/main/java/org/elasticsearch/index/query/SpanOrQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/SpanOrQueryBuilder.java index d9b2d9cf4be47..a3efb023d5eeb 100644 --- a/server/src/main/java/org/elasticsearch/index/query/SpanOrQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/SpanOrQueryBuilder.java @@ -118,7 +118,7 @@ public static SpanOrQueryBuilder fromXContent(XContentParser parser) throws IOEx throw new ParsingException(parser.getTokenLocation(), "span_or [clauses] must be of type span query"); } final SpanQueryBuilder clause = (SpanQueryBuilder) query; - checkNoBoost(NAME, currentFieldName, parser, clause); + checkNoBoost(clause); clauses.add(clause); } } else { diff --git a/server/src/main/java/org/elasticsearch/index/query/SpanQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/SpanQueryBuilder.java index f7bf784d6cf99..311004536db96 100644 --- a/server/src/main/java/org/elasticsearch/index/query/SpanQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/SpanQueryBuilder.java @@ -19,8 +19,8 @@ package org.elasticsearch.index.query; -import org.elasticsearch.common.ParsingException; -import org.elasticsearch.common.xcontent.XContentParser; +import org.apache.logging.log4j.LogManager; +import org.elasticsearch.common.logging.DeprecationLogger; /** * Marker interface for a specific type of {@link QueryBuilder} that allows to build span queries. @@ -28,24 +28,23 @@ public interface SpanQueryBuilder extends QueryBuilder { class SpanQueryBuilderUtil { + + private static final DeprecationLogger DEPRECATION_LOGGER = + new DeprecationLogger(LogManager.getLogger(SpanQueryBuilderUtil.class)); + private SpanQueryBuilderUtil() { // utility class } /** - * Checks boost value of a nested span clause is equal to {@link AbstractQueryBuilder#DEFAULT_BOOST}. - * - * @param queryName a query name - * @param fieldName a field name - * @param parser a parser + * Checks boost value of a nested span clause is equal to {@link AbstractQueryBuilder#DEFAULT_BOOST}, + * and if not issues a deprecation warning * @param clause a span query builder - * @throws ParsingException if query boost value isn't equal to {@link AbstractQueryBuilder#DEFAULT_BOOST} */ - static void checkNoBoost(String queryName, String fieldName, XContentParser parser, SpanQueryBuilder clause) { + static void checkNoBoost(SpanQueryBuilder clause) { try { if (clause.boost() != AbstractQueryBuilder.DEFAULT_BOOST) { - throw new ParsingException(parser.getTokenLocation(), queryName + " [" + fieldName + "] " + - "as a nested span clause can't have non-default boost value [" + clause.boost() + "]"); + DEPRECATION_LOGGER.deprecatedAndMaybeLog("span_inner_queries", "setting boost on inner span queries is deprecated!"); } } catch (UnsupportedOperationException ignored) { // if boost is unsupported it can't have been set diff --git a/server/src/main/java/org/elasticsearch/index/query/SpanWithinQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/SpanWithinQueryBuilder.java index 8f970fc25c165..c978fffae1d8b 100644 --- a/server/src/main/java/org/elasticsearch/index/query/SpanWithinQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/SpanWithinQueryBuilder.java @@ -124,14 +124,14 @@ public static SpanWithinQueryBuilder fromXContent(XContentParser parser) throws throw new ParsingException(parser.getTokenLocation(), "span_within [big] must be of type span query"); } big = (SpanQueryBuilder) query; - checkNoBoost(NAME, currentFieldName, parser, big); + checkNoBoost(big); } else if (LITTLE_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { QueryBuilder query = parseInnerQueryBuilder(parser); if (query instanceof SpanQueryBuilder == false) { throw new ParsingException(parser.getTokenLocation(), "span_within [little] must be of type span query"); } little = (SpanQueryBuilder) query; - checkNoBoost(NAME, currentFieldName, parser, little); + checkNoBoost(little); } else { throw new ParsingException(parser.getTokenLocation(), "[span_within] query does not support [" + currentFieldName + "]"); diff --git a/server/src/test/java/org/elasticsearch/index/query/SpanContainingQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/SpanContainingQueryBuilderTests.java index e6e62d2909a2a..9c56234e5bb60 100644 --- a/server/src/test/java/org/elasticsearch/index/query/SpanContainingQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/SpanContainingQueryBuilderTests.java @@ -21,13 +21,11 @@ import org.apache.lucene.search.Query; import org.apache.lucene.search.spans.SpanContainingQuery; -import org.elasticsearch.common.ParsingException; import org.elasticsearch.search.internal.SearchContext; import org.elasticsearch.test.AbstractQueryTestCase; import java.io.IOException; -import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.instanceOf; public class SpanContainingQueryBuilderTests extends AbstractQueryTestCase { @@ -94,7 +92,7 @@ public void testFromJson() throws IOException { assertEquals(json, 2.0, parsed.boost(), 0.0); } - public void testFromJsoWithNonDefaultBoostInBigQuery() { + public void testFromJsoWithNonDefaultBoostInBigQuery() throws IOException { String json = "{\n" + " \"span_containing\" : {\n" + @@ -132,12 +130,11 @@ public void testFromJsoWithNonDefaultBoostInBigQuery() { " }\n" + "}"; - Exception exception = expectThrows(ParsingException.class, () -> parseQuery(json)); - assertThat(exception.getMessage(), - equalTo("span_containing [big] as a nested span clause can't have non-default boost value [2.0]")); + parseQuery(json); + assertWarnings("setting boost on inner span queries is deprecated!"); } - public void testFromJsonWithNonDefaultBoostInLittleQuery() { + public void testFromJsonWithNonDefaultBoostInLittleQuery() throws IOException { String json = "{\n" + " \"span_containing\" : {\n" + @@ -175,8 +172,7 @@ public void testFromJsonWithNonDefaultBoostInLittleQuery() { " }\n" + "}"; - Exception exception = expectThrows(ParsingException.class, () -> parseQuery(json)); - assertThat(exception.getMessage(), - equalTo("span_containing [little] as a nested span clause can't have non-default boost value [2.0]")); + parseQuery(json); + assertWarnings("setting boost on inner span queries is deprecated!"); } } diff --git a/server/src/test/java/org/elasticsearch/index/query/SpanFirstQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/SpanFirstQueryBuilderTests.java index 2ac3610f2d670..3e9b5bb29cf67 100644 --- a/server/src/test/java/org/elasticsearch/index/query/SpanFirstQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/SpanFirstQueryBuilderTests.java @@ -31,7 +31,6 @@ import java.io.IOException; import static org.elasticsearch.index.query.QueryBuilders.spanTermQuery; -import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.instanceOf; public class SpanFirstQueryBuilderTests extends AbstractQueryTestCase { @@ -100,7 +99,7 @@ public void testFromJson() throws IOException { } - public void testFromJsonWithNonDefaultBoostInMatchQuery() { + public void testFromJsonWithNonDefaultBoostInMatchQuery() throws IOException { String json = "{\n" + " \"span_first\" : {\n" + @@ -117,8 +116,7 @@ public void testFromJsonWithNonDefaultBoostInMatchQuery() { " }\n" + "}"; - Exception exception = expectThrows(ParsingException.class, () -> parseQuery(json)); - assertThat(exception.getMessage(), - equalTo("span_first [match] as a nested span clause can't have non-default boost value [2.0]")); + parseQuery(json); + assertWarnings("setting boost on inner span queries is deprecated!"); } } diff --git a/server/src/test/java/org/elasticsearch/index/query/SpanNearQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/SpanNearQueryBuilderTests.java index cde83fb6f7424..64927089272c1 100644 --- a/server/src/test/java/org/elasticsearch/index/query/SpanNearQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/SpanNearQueryBuilderTests.java @@ -188,7 +188,7 @@ public void testCollectPayloadsNoLongerSupported() throws Exception { assertThat(e.getMessage(), containsString("[span_near] query does not support [collect_payloads]")); } - public void testFromJsonWithNonDefaultBoostInInnerQuery() { + public void testFromJsonWithNonDefaultBoostInInnerQuery() throws IOException { String json = "{\n" + " \"span_near\" : {\n" + @@ -220,8 +220,7 @@ public void testFromJsonWithNonDefaultBoostInInnerQuery() { " }\n" + "}"; - Exception exception = expectThrows(ParsingException.class, () -> parseQuery(json)); - assertThat(exception.getMessage(), - equalTo("span_near [clauses] as a nested span clause can't have non-default boost value [2.0]")); + parseQuery(json); + assertWarnings("setting boost on inner span queries is deprecated!"); } } diff --git a/server/src/test/java/org/elasticsearch/index/query/SpanNotQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/SpanNotQueryBuilderTests.java index 7df58553e2768..f82a9663a2dfa 100644 --- a/server/src/test/java/org/elasticsearch/index/query/SpanNotQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/SpanNotQueryBuilderTests.java @@ -215,7 +215,7 @@ public void testFromJson() throws IOException { assertEquals(json, 2.0, parsed.boost(), 0.0); } - public void testFromJsonWithNonDefaultBoostInIncludeQuery() { + public void testFromJsonWithNonDefaultBoostInIncludeQuery() throws IOException { String json = "{\n" + " \"span_not\" : {\n" + @@ -255,13 +255,12 @@ public void testFromJsonWithNonDefaultBoostInIncludeQuery() { " }\n" + "}"; - Exception exception = expectThrows(ParsingException.class, () -> parseQuery(json)); - assertThat(exception.getMessage(), - equalTo("span_not [include] as a nested span clause can't have non-default boost value [2.0]")); + parseQuery(json); + assertWarnings("setting boost on inner span queries is deprecated!"); } - public void testFromJsonWithNonDefaultBoostInExcludeQuery() { + public void testFromJsonWithNonDefaultBoostInExcludeQuery() throws IOException { String json = "{\n" + " \"span_not\" : {\n" + @@ -301,8 +300,7 @@ public void testFromJsonWithNonDefaultBoostInExcludeQuery() { " }\n" + "}"; - Exception exception = expectThrows(ParsingException.class, () -> parseQuery(json)); - assertThat(exception.getMessage(), - equalTo("span_not [exclude] as a nested span clause can't have non-default boost value [2.0]")); + parseQuery(json); + assertWarnings("setting boost on inner span queries is deprecated!"); } } diff --git a/server/src/test/java/org/elasticsearch/index/query/SpanOrQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/SpanOrQueryBuilderTests.java index 9497cebc4ce2f..27f424e9aba9f 100644 --- a/server/src/test/java/org/elasticsearch/index/query/SpanOrQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/SpanOrQueryBuilderTests.java @@ -22,7 +22,6 @@ import org.apache.lucene.search.Query; import org.apache.lucene.search.spans.SpanOrQuery; import org.apache.lucene.search.spans.SpanQuery; -import org.elasticsearch.common.ParsingException; import org.elasticsearch.search.internal.SearchContext; import org.elasticsearch.test.AbstractQueryTestCase; @@ -106,7 +105,7 @@ public void testFromJson() throws IOException { assertEquals(json, 2.0, parsed.boost(), 0.0); } - public void testFromJsonWithNonDefaultBoostInInnerQuery() { + public void testFromJsonWithNonDefaultBoostInInnerQuery() throws IOException { String json = "{\n" + " \"span_or\" : {\n" + @@ -122,8 +121,7 @@ public void testFromJsonWithNonDefaultBoostInInnerQuery() { " }\n" + "}"; - Exception exception = expectThrows(ParsingException.class, () -> parseQuery(json)); - assertThat(exception.getMessage(), - equalTo("span_or [clauses] as a nested span clause can't have non-default boost value [2.0]")); + parseQuery(json); + assertWarnings("setting boost on inner span queries is deprecated!"); } } diff --git a/server/src/test/java/org/elasticsearch/index/query/SpanWithinQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/SpanWithinQueryBuilderTests.java index a288e2430235a..820a605a47763 100644 --- a/server/src/test/java/org/elasticsearch/index/query/SpanWithinQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/SpanWithinQueryBuilderTests.java @@ -21,13 +21,11 @@ import org.apache.lucene.search.Query; import org.apache.lucene.search.spans.SpanWithinQuery; -import org.elasticsearch.common.ParsingException; import org.elasticsearch.search.internal.SearchContext; import org.elasticsearch.test.AbstractQueryTestCase; import java.io.IOException; -import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.instanceOf; public class SpanWithinQueryBuilderTests extends AbstractQueryTestCase { @@ -94,7 +92,7 @@ public void testFromJson() throws IOException { assertEquals(json, 2.0, parsed.boost(), 0.0); } - public void testFromJsonWithNonDefaultBoostInBigQuery() { + public void testFromJsonWithNonDefaultBoostInBigQuery() throws IOException { String json = "{\n" + " \"span_within\" : {\n" + @@ -132,12 +130,11 @@ public void testFromJsonWithNonDefaultBoostInBigQuery() { " }\n" + "}"; - Exception exception = expectThrows(ParsingException.class, () -> parseQuery(json)); - assertThat(exception.getMessage(), - equalTo("span_within [big] as a nested span clause can't have non-default boost value [2.0]")); + parseQuery(json); + assertWarnings("setting boost on inner span queries is deprecated!"); } - public void testFromJsonWithNonDefaultBoostInLittleQuery() { + public void testFromJsonWithNonDefaultBoostInLittleQuery() throws IOException { String json = "{\n" + " \"span_within\" : {\n" + @@ -175,8 +172,7 @@ public void testFromJsonWithNonDefaultBoostInLittleQuery() { " }\n" + "}"; - Exception exception = expectThrows(ParsingException.class, () -> parseQuery(json)); - assertThat(exception.getMessage(), - equalTo("span_within [little] as a nested span clause can't have non-default boost value [2.0]")); + parseQuery(json); + assertWarnings("setting boost on inner span queries is deprecated!"); } } From ff6c194117b8bb26316328aa7c38e4adfe23efd5 Mon Sep 17 00:00:00 2001 From: Benjamin Trent Date: Tue, 4 Dec 2018 09:41:47 -0600 Subject: [PATCH 105/115] [ML] Add lazy parsing for DatafeedConfig:Aggs,Query (#36117) * Lazily parsing aggs and query in DatafeedConfigs * Adding parser tests * Fixing exception types && unneccessary checked ex * Adding semi aggregation parser * Adding tests, fixing up semi-parser * Reverting semi-parsing * Moving agg validations * Making bad configs throw badRequestException --- .../core/ml/datafeed/DatafeedConfig.java | 190 +++++++++++++++--- .../core/ml/datafeed/DatafeedUpdate.java | 9 +- .../xpack/core/ml/job/messages/Messages.java | 2 + .../ml/utils/XContentObjectTransformer.java | 6 + .../core/ml/datafeed/DatafeedConfigTests.java | 108 ++++++++-- .../core/ml/datafeed/DatafeedUpdateTests.java | 4 +- .../ml/integration/DelayedDataDetectorIT.java | 4 +- .../ml/action/TransportPutDatafeedAction.java | 2 + .../action/TransportStartDatafeedAction.java | 1 + .../DelayedDataDetectorFactory.java | 2 +- .../AggregationDataExtractorFactory.java | 4 +- .../RollupDataExtractorFactory.java | 8 +- .../chunked/ChunkedDataExtractorFactory.java | 2 +- .../scroll/ScrollDataExtractorFactory.java | 2 +- .../TransportPreviewDatafeedActionTests.java | 2 +- .../datafeed/DatafeedJobValidatorTests.java | 2 +- .../extractor/DataExtractorFactoryTests.java | 16 +- .../AggregationDataExtractorFactoryTests.java | 4 +- .../ChunkedDataExtractorFactoryTests.java | 4 +- .../integration/BasicDistributedJobsIT.java | 2 +- 20 files changed, 295 insertions(+), 79 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/datafeed/DatafeedConfig.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/datafeed/DatafeedConfig.java index 83e55c7c9c8fd..3c2e7dd4eb416 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/datafeed/DatafeedConfig.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/datafeed/DatafeedConfig.java @@ -13,9 +13,11 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.util.CachedSupplier; import org.elasticsearch.common.xcontent.ObjectParser; import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParseException; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.index.query.AbstractQueryBuilder; import org.elasticsearch.index.query.QueryBuilder; @@ -31,6 +33,7 @@ import org.elasticsearch.xpack.core.ml.utils.ExceptionsHelper; import org.elasticsearch.xpack.core.ml.utils.MlStrings; import org.elasticsearch.xpack.core.ml.utils.ToXContentParams; +import org.elasticsearch.xpack.core.ml.utils.XContentObjectTransformer; import org.elasticsearch.xpack.core.ml.utils.time.TimeUtils; import java.io.IOException; @@ -43,6 +46,7 @@ import java.util.Objects; import java.util.Random; import java.util.concurrent.TimeUnit; +import java.util.function.BiFunction; /** * Datafeed configuration options. Describes where to proactively pull input @@ -60,6 +64,45 @@ public class DatafeedConfig extends AbstractDiffable implements private static final int TWO_MINS_SECONDS = 2 * SECONDS_IN_MINUTE; private static final int TWENTY_MINS_SECONDS = 20 * SECONDS_IN_MINUTE; private static final int HALF_DAY_SECONDS = 12 * 60 * SECONDS_IN_MINUTE; + static final XContentObjectTransformer QUERY_TRANSFORMER = XContentObjectTransformer.queryBuilderTransformer(); + private static final BiFunction, String, QueryBuilder> lazyQueryParser = (objectMap, id) -> { + try { + return QUERY_TRANSFORMER.fromMap(objectMap); + } catch (IOException | XContentParseException exception) { + // Certain thrown exceptions wrap up the real Illegal argument making it hard to determine cause for the user + if (exception.getCause() instanceof IllegalArgumentException) { + throw ExceptionsHelper.badRequestException( + Messages.getMessage(Messages.DATAFEED_CONFIG_QUERY_BAD_FORMAT, + id, + exception.getCause().getMessage()), + exception.getCause()); + } else { + throw ExceptionsHelper.badRequestException( + Messages.getMessage(Messages.DATAFEED_CONFIG_QUERY_BAD_FORMAT, exception, id), + exception); + } + } + }; + + static final XContentObjectTransformer AGG_TRANSFORMER = XContentObjectTransformer.aggregatorTransformer(); + private static final BiFunction, String, AggregatorFactories.Builder> lazyAggParser = (objectMap, id) -> { + try { + return AGG_TRANSFORMER.fromMap(objectMap); + } catch (IOException | XContentParseException exception) { + // Certain thrown exceptions wrap up the real Illegal argument making it hard to determine cause for the user + if (exception.getCause() instanceof IllegalArgumentException) { + throw ExceptionsHelper.badRequestException( + Messages.getMessage(Messages.DATAFEED_CONFIG_AGG_BAD_FORMAT, + id, + exception.getCause().getMessage()), + exception.getCause()); + } else { + throw ExceptionsHelper.badRequestException( + Messages.getMessage(Messages.DATAFEED_CONFIG_AGG_BAD_FORMAT, exception.getMessage(), id), + exception); + } + } + }; // Used for QueryPage public static final ParseField RESULTS_FIELD = new ParseField("datafeeds"); @@ -90,6 +133,21 @@ public class DatafeedConfig extends AbstractDiffable implements public static final ObjectParser LENIENT_PARSER = createParser(true); public static final ObjectParser STRICT_PARSER = createParser(false); + public static void validateAggregations(AggregatorFactories.Builder aggregations) { + if (aggregations == null) { + return; + } + Collection aggregatorFactories = aggregations.getAggregatorFactories(); + if (aggregatorFactories.isEmpty()) { + throw ExceptionsHelper.badRequestException(Messages.DATAFEED_AGGREGATIONS_REQUIRES_DATE_HISTOGRAM); + } + + AggregationBuilder histogramAggregation = ExtractorUtils.getHistogramAggregation(aggregatorFactories); + Builder.checkNoMoreHistogramAggregations(histogramAggregation.getSubAggregations()); + Builder.checkHistogramAggregationHasChildMaxTimeAgg(histogramAggregation); + Builder.checkHistogramIntervalIsPositive(histogramAggregation); + } + private static ObjectParser createParser(boolean ignoreUnknownFields) { ObjectParser parser = new ObjectParser<>("datafeed_config", ignoreUnknownFields, Builder::new); @@ -102,9 +160,15 @@ private static ObjectParser createParser(boolean ignoreUnknownFie builder.setQueryDelay(TimeValue.parseTimeValue(val, QUERY_DELAY.getPreferredName())), QUERY_DELAY); parser.declareString((builder, val) -> builder.setFrequency(TimeValue.parseTimeValue(val, FREQUENCY.getPreferredName())), FREQUENCY); - parser.declareObject(Builder::setQuery, (p, c) -> AbstractQueryBuilder.parseInnerQueryBuilder(p), QUERY); - parser.declareObject(Builder::setAggregations, (p, c) -> AggregatorFactories.parseAggregators(p), AGGREGATIONS); - parser.declareObject(Builder::setAggregations, (p, c) -> AggregatorFactories.parseAggregators(p), AGGS); + if (ignoreUnknownFields) { + parser.declareObject(Builder::setQuery, (p, c) -> p.map(), QUERY); + parser.declareObject(Builder::setAggregations, (p, c) -> p.map(), AGGREGATIONS); + parser.declareObject(Builder::setAggregations, (p, c) -> p.map(), AGGS); + } else { + parser.declareObject(Builder::setParsedQuery, (p, c) -> AbstractQueryBuilder.parseInnerQueryBuilder(p), QUERY); + parser.declareObject(Builder::setParsedAggregations, (p, c) -> AggregatorFactories.parseAggregators(p), AGGREGATIONS); + parser.declareObject(Builder::setParsedAggregations, (p, c) -> AggregatorFactories.parseAggregators(p), AGGS); + } parser.declareObject(Builder::setScriptFields, (p, c) -> { List parsedScriptFields = new ArrayList<>(); while (p.nextToken() != XContentParser.Token.END_OBJECT) { @@ -146,16 +210,18 @@ private static ObjectParser createParser(boolean ignoreUnknownFie private final List indices; private final List types; - private final QueryBuilder query; - private final AggregatorFactories.Builder aggregations; + private final Map query; + private final Map aggregations; private final List scriptFields; private final Integer scrollSize; private final ChunkingConfig chunkingConfig; private final Map headers; private final DelayedDataCheckConfig delayedDataCheckConfig; + private final CachedSupplier querySupplier; + private final CachedSupplier aggSupplier; private DatafeedConfig(String id, String jobId, TimeValue queryDelay, TimeValue frequency, List indices, List types, - QueryBuilder query, AggregatorFactories.Builder aggregations, List scriptFields, + Map query, Map aggregations, List scriptFields, Integer scrollSize, ChunkingConfig chunkingConfig, Map headers, DelayedDataCheckConfig delayedDataCheckConfig) { this.id = id; @@ -171,6 +237,8 @@ private DatafeedConfig(String id, String jobId, TimeValue queryDelay, TimeValue this.chunkingConfig = chunkingConfig; this.headers = Collections.unmodifiableMap(headers); this.delayedDataCheckConfig = delayedDataCheckConfig; + this.querySupplier = new CachedSupplier<>(() -> lazyQueryParser.apply(query, id)); + this.aggSupplier = new CachedSupplier<>(() -> lazyAggParser.apply(aggregations, id)); } public DatafeedConfig(StreamInput in) throws IOException { @@ -188,8 +256,17 @@ public DatafeedConfig(StreamInput in) throws IOException { } else { this.types = null; } - this.query = in.readNamedWriteable(QueryBuilder.class); - this.aggregations = in.readOptionalWriteable(AggregatorFactories.Builder::new); + if (in.getVersion().before(Version.CURRENT)) { + this.query = QUERY_TRANSFORMER.toMap(in.readNamedWriteable(QueryBuilder.class)); + this.aggregations = AGG_TRANSFORMER.toMap(in.readOptionalWriteable(AggregatorFactories.Builder::new)); + } else { + this.query = in.readMap(); + if (in.readBoolean()) { + this.aggregations = in.readMap(); + } else { + this.aggregations = null; + } + } if (in.readBoolean()) { this.scriptFields = Collections.unmodifiableList(in.readList(SearchSourceBuilder.ScriptField::new)); } else { @@ -211,6 +288,8 @@ public DatafeedConfig(StreamInput in) throws IOException { } else { delayedDataCheckConfig = DelayedDataCheckConfig.defaultDelayedDataCheckConfig(); } + this.querySupplier = new CachedSupplier<>(() -> lazyQueryParser.apply(query, id)); + this.aggSupplier = new CachedSupplier<>(() -> lazyAggParser.apply(aggregations, id)); } public String getId() { @@ -241,11 +320,19 @@ public Integer getScrollSize() { return scrollSize; } - public QueryBuilder getQuery() { + public QueryBuilder getParsedQuery() { + return querySupplier.get(); + } + + public Map getQuery() { return query; } - public AggregatorFactories.Builder getAggregations() { + public AggregatorFactories.Builder getParsedAggregations() { + return aggSupplier.get(); + } + + public Map getAggregations() { return aggregations; } @@ -253,14 +340,14 @@ public AggregatorFactories.Builder getAggregations() { * Returns the histogram's interval as epoch millis. */ public long getHistogramIntervalMillis() { - return ExtractorUtils.getHistogramIntervalMillis(aggregations); + return ExtractorUtils.getHistogramIntervalMillis(getParsedAggregations()); } /** * @return {@code true} when there are non-empty aggregations, {@code false} otherwise */ public boolean hasAggregations() { - return aggregations != null && aggregations.count() > 0; + return aggregations != null && aggregations.size() > 0; } public List getScriptFields() { @@ -297,8 +384,16 @@ public void writeTo(StreamOutput out) throws IOException { } else { out.writeBoolean(false); } - out.writeNamedWriteable(query); - out.writeOptionalWriteable(aggregations); + if (out.getVersion().before(Version.CURRENT)) { + out.writeNamedWriteable(getParsedQuery()); + out.writeOptionalWriteable(getParsedAggregations()); + } else { + out.writeMap(query); + out.writeBoolean(aggregations != null); + if (aggregations != null) { + out.writeMap(aggregations); + } + } if (scriptFields != null) { out.writeBoolean(true); out.writeList(scriptFields); @@ -462,15 +557,20 @@ public static class Builder { private TimeValue frequency; private List indices = Collections.emptyList(); private List types = Collections.emptyList(); - private QueryBuilder query = QueryBuilders.matchAllQuery(); - private AggregatorFactories.Builder aggregations; + private Map query; + private Map aggregations; private List scriptFields; private Integer scrollSize = DEFAULT_SCROLL_SIZE; private ChunkingConfig chunkingConfig; private Map headers = Collections.emptyMap(); private DelayedDataCheckConfig delayedDataCheckConfig = DelayedDataCheckConfig.defaultDelayedDataCheckConfig(); + + public Builder() { + try { + this.query = QUERY_TRANSFORMER.toMap(QueryBuilders.matchAllQuery()); + } catch (IOException ex) { /*Should never happen*/ } } public Builder(String id, String jobId) { @@ -525,11 +625,47 @@ public void setFrequency(TimeValue frequency) { this.frequency = frequency; } - public void setQuery(QueryBuilder query) { + public void setParsedQuery(QueryBuilder query) { + try { + setQuery(QUERY_TRANSFORMER.toMap(ExceptionsHelper.requireNonNull(query, QUERY.getPreferredName()))); + } catch (IOException | XContentParseException exception) { + if (exception.getCause() instanceof IllegalArgumentException) { + // Certain thrown exceptions wrap up the real Illegal argument making it hard to determine cause for the user + throw ExceptionsHelper.badRequestException( + Messages.getMessage(Messages.DATAFEED_CONFIG_QUERY_BAD_FORMAT, + id, + exception.getCause().getMessage()), + exception.getCause()); + } else { + throw ExceptionsHelper.badRequestException( + Messages.getMessage(Messages.DATAFEED_CONFIG_QUERY_BAD_FORMAT, id, exception.getMessage()), exception); + } + } + } + + void setQuery(Map query) { this.query = ExceptionsHelper.requireNonNull(query, QUERY.getPreferredName()); } - public void setAggregations(AggregatorFactories.Builder aggregations) { + public void setParsedAggregations(AggregatorFactories.Builder aggregations) { + try { + setAggregations(AGG_TRANSFORMER.toMap(aggregations)); + } catch (IOException | XContentParseException exception) { + // Certain thrown exceptions wrap up the real Illegal argument making it hard to determine cause for the user + if (exception.getCause() instanceof IllegalArgumentException) { + throw ExceptionsHelper.badRequestException( + Messages.getMessage(Messages.DATAFEED_CONFIG_AGG_BAD_FORMAT, + id, + exception.getCause().getMessage()), + exception.getCause()); + } else { + throw ExceptionsHelper.badRequestException( + Messages.getMessage(Messages.DATAFEED_CONFIG_AGG_BAD_FORMAT, id, exception.getMessage()), exception); + } + } + } + + void setAggregations(Map aggregations) { this.aggregations = aggregations; } @@ -572,30 +708,22 @@ public DatafeedConfig build() { throw invalidOptionValue(TYPES.getPreferredName(), types); } - validateAggregations(); + validateScriptFields(); setDefaultChunkingConfig(); + setDefaultQueryDelay(); return new DatafeedConfig(id, jobId, queryDelay, frequency, indices, types, query, aggregations, scriptFields, scrollSize, chunkingConfig, headers, delayedDataCheckConfig); } - void validateAggregations() { + void validateScriptFields() { if (aggregations == null) { return; } if (scriptFields != null && !scriptFields.isEmpty()) { throw ExceptionsHelper.badRequestException( - Messages.getMessage(Messages.DATAFEED_CONFIG_CANNOT_USE_SCRIPT_FIELDS_WITH_AGGS)); + Messages.getMessage(Messages.DATAFEED_CONFIG_CANNOT_USE_SCRIPT_FIELDS_WITH_AGGS)); } - Collection aggregatorFactories = aggregations.getAggregatorFactories(); - if (aggregatorFactories.isEmpty()) { - throw ExceptionsHelper.badRequestException(Messages.DATAFEED_AGGREGATIONS_REQUIRES_DATE_HISTOGRAM); - } - - AggregationBuilder histogramAggregation = ExtractorUtils.getHistogramAggregation(aggregatorFactories); - checkNoMoreHistogramAggregations(histogramAggregation.getSubAggregations()); - checkHistogramAggregationHasChildMaxTimeAgg(histogramAggregation); - checkHistogramIntervalIsPositive(histogramAggregation); } private static void checkNoMoreHistogramAggregations(Collection aggregations) { @@ -638,7 +766,7 @@ private void setDefaultChunkingConfig() { if (aggregations == null) { chunkingConfig = ChunkingConfig.newAuto(); } else { - long histogramIntervalMillis = ExtractorUtils.getHistogramIntervalMillis(aggregations); + long histogramIntervalMillis = ExtractorUtils.getHistogramIntervalMillis(lazyAggParser.apply(aggregations, id)); chunkingConfig = ChunkingConfig.newManual(TimeValue.timeValueMillis( DEFAULT_AGGREGATION_CHUNKING_BUCKETS * histogramIntervalMillis)); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/datafeed/DatafeedUpdate.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/datafeed/DatafeedUpdate.java index b1b9929620a45..177cc236c3e62 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/datafeed/DatafeedUpdate.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/datafeed/DatafeedUpdate.java @@ -303,10 +303,11 @@ public DatafeedConfig apply(DatafeedConfig datafeedConfig, Map h builder.setTypes(types); } if (query != null) { - builder.setQuery(query); + builder.setParsedQuery(query); } if (aggregations != null) { - builder.setAggregations(aggregations); + DatafeedConfig.validateAggregations(aggregations); + builder.setParsedAggregations(aggregations); } if (scriptFields != null) { builder.setScriptFields(scriptFields); @@ -379,9 +380,9 @@ boolean isNoop(DatafeedConfig datafeed) { && (queryDelay == null || Objects.equals(queryDelay, datafeed.getQueryDelay())) && (indices == null || Objects.equals(indices, datafeed.getIndices())) && (types == null || Objects.equals(types, datafeed.getTypes())) - && (query == null || Objects.equals(query, datafeed.getQuery())) + && (query == null || Objects.equals(query, datafeed.getParsedQuery())) && (scrollSize == null || Objects.equals(scrollSize, datafeed.getQueryDelay())) - && (aggregations == null || Objects.equals(aggregations, datafeed.getAggregations())) + && (aggregations == null || Objects.equals(aggregations, datafeed.getParsedAggregations())) && (scriptFields == null || Objects.equals(scriptFields, datafeed.getScriptFields())) && (delayedDataCheckConfig == null || Objects.equals(delayedDataCheckConfig, datafeed.getDelayedDataCheckConfig())) && (chunkingConfig == null || Objects.equals(chunkingConfig, datafeed.getChunkingConfig())); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/messages/Messages.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/messages/Messages.java index fcec1ff32f906..038b9a7a1edd1 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/messages/Messages.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/messages/Messages.java @@ -26,6 +26,8 @@ public final class Messages { "delayed_data_check_config: check_window [{0}] must be greater than the bucket_span [{1}]"; public static final String DATAFEED_CONFIG_DELAYED_DATA_CHECK_SPANS_TOO_MANY_BUCKETS = "delayed_data_check_config: check_window [{0}] must be less than 10,000x the bucket_span [{1}]"; + public static final String DATAFEED_CONFIG_QUERY_BAD_FORMAT = "Datafeed [{0}] query is not parsable: {1}"; + public static final String DATAFEED_CONFIG_AGG_BAD_FORMAT = "Datafeed [{0}] aggregations are not parsable: {1}"; public static final String DATAFEED_DOES_NOT_SUPPORT_JOB_WITH_LATENCY = "A job configured with datafeed cannot support latency"; public static final String DATAFEED_NOT_FOUND = "No datafeed with id [{0}] exists"; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/utils/XContentObjectTransformer.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/utils/XContentObjectTransformer.java index 00453d3680fe9..5d25b9d71e618 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/utils/XContentObjectTransformer.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/utils/XContentObjectTransformer.java @@ -61,6 +61,9 @@ public static XContentObjectTransformer queryBuilderTransformer() } public T fromMap(Map stringObjectMap) throws IOException { + if (stringObjectMap == null) { + return null; + } LoggingDeprecationAccumulationHandler deprecationLogger = new LoggingDeprecationAccumulationHandler(); try(XContentBuilder xContentBuilder = XContentFactory.jsonBuilder().map(stringObjectMap); XContentParser parser = XContentType.JSON @@ -74,6 +77,9 @@ public T fromMap(Map stringObjectMap) throws IOException { } public Map toMap(T object) throws IOException { + if (object == null) { + return null; + } try(XContentBuilder xContentBuilder = XContentFactory.jsonBuilder()) { XContentBuilder content = object.toXContent(xContentBuilder, ToXContent.EMPTY_PARAMS); return XContentHelper.convertToMap(BytesReference.bytes(content), true, XContentType.JSON).v2(); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/datafeed/DatafeedConfigTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/datafeed/DatafeedConfigTests.java index 87aafe1fc3b66..b2743c7370976 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/datafeed/DatafeedConfigTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/datafeed/DatafeedConfigTests.java @@ -67,7 +67,7 @@ public static DatafeedConfig createRandomizedDatafeedConfig(String jobId, long b builder.setIndices(randomStringList(1, 10)); builder.setTypes(randomStringList(0, 10)); if (randomBoolean()) { - builder.setQuery(QueryBuilders.termQuery(randomAlphaOfLength(10), randomAlphaOfLength(10))); + builder.setParsedQuery(QueryBuilders.termQuery(randomAlphaOfLength(10), randomAlphaOfLength(10))); } boolean addScriptFields = randomBoolean(); if (addScriptFields) { @@ -91,7 +91,7 @@ public static DatafeedConfig createRandomizedDatafeedConfig(String jobId, long b MaxAggregationBuilder maxTime = AggregationBuilders.max("time").field("time"); aggs.addAggregator(AggregationBuilders.dateHistogram("buckets") .interval(aggHistogramInterval).subAggregation(maxTime).field("time")); - builder.setAggregations(aggs); + builder.setParsedAggregations(aggs); } if (randomBoolean()) { builder.setScrollSize(randomIntBetween(0, Integer.MAX_VALUE)); @@ -155,6 +155,43 @@ protected DatafeedConfig doParseInstance(XContentParser parser) { " \"scroll_size\": 1234\n" + "}"; + private static final String ANACHRONISTIC_QUERY_DATAFEED = "{\n" + + " \"datafeed_id\": \"farequote-datafeed\",\n" + + " \"job_id\": \"farequote\",\n" + + " \"frequency\": \"1h\",\n" + + " \"indices\": [\"farequote1\", \"farequote2\"],\n" + + //query:match:type stopped being supported in 6.x + " \"query\": {\"match\" : {\"query\":\"fieldName\", \"type\": \"phrase\"}},\n" + + " \"scroll_size\": 1234\n" + + "}"; + + private static final String ANACHRONISTIC_AGG_DATAFEED = "{\n" + + " \"datafeed_id\": \"farequote-datafeed\",\n" + + " \"job_id\": \"farequote\",\n" + + " \"frequency\": \"1h\",\n" + + " \"indices\": [\"farequote1\", \"farequote2\"],\n" + + " \"aggregations\": {\n" + + " \"buckets\": {\n" + + " \"date_histogram\": {\n" + + " \"field\": \"time\",\n" + + " \"interval\": \"360s\",\n" + + " \"time_zone\": \"UTC\"\n" + + " },\n" + + " \"aggregations\": {\n" + + " \"time\": {\n" + + " \"max\": {\"field\": \"time\"}\n" + + " },\n" + + " \"airline\": {\n" + + " \"terms\": {\n" + + " \"field\": \"airline\",\n" + + " \"size\": 0\n" + //size: 0 stopped being supported in 6.x + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + "}"; + public void testFutureConfigParse() throws IOException { XContentParser parser = XContentFactory.xContent(XContentType.JSON) .createParser(NamedXContentRegistry.EMPTY, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, FUTURE_DATAFEED); @@ -163,6 +200,44 @@ public void testFutureConfigParse() throws IOException { assertEquals("[6:5] [datafeed_config] unknown field [tomorrows_technology_today], parser not found", e.getMessage()); } + public void testPastQueryConfigParse() throws IOException { + try(XContentParser parser = XContentFactory.xContent(XContentType.JSON) + .createParser(NamedXContentRegistry.EMPTY, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, ANACHRONISTIC_QUERY_DATAFEED)) { + + DatafeedConfig config = DatafeedConfig.LENIENT_PARSER.apply(parser, null).build(); + ElasticsearchException e = expectThrows(ElasticsearchException.class, () -> config.getParsedQuery()); + assertEquals("[match] query doesn't support multiple fields, found [query] and [type]", e.getMessage()); + } + + try(XContentParser parser = XContentFactory.xContent(XContentType.JSON) + .createParser(NamedXContentRegistry.EMPTY, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, ANACHRONISTIC_QUERY_DATAFEED)) { + + XContentParseException e = expectThrows(XContentParseException.class, + () -> DatafeedConfig.STRICT_PARSER.apply(parser, null).build()); + assertEquals("[6:25] [datafeed_config] failed to parse field [query]", e.getMessage()); + } + } + + public void testPastAggConfigParse() throws IOException { + try(XContentParser parser = XContentFactory.xContent(XContentType.JSON) + .createParser(NamedXContentRegistry.EMPTY, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, ANACHRONISTIC_AGG_DATAFEED)) { + + DatafeedConfig.Builder configBuilder = DatafeedConfig.LENIENT_PARSER.apply(parser, null); + ElasticsearchException e = expectThrows(ElasticsearchException.class, () -> configBuilder.build()); + assertEquals( + "Datafeed [farequote-datafeed] aggregations are not parsable: [size] must be greater than 0. Found [0] in [airline]", + e.getMessage()); + } + + try(XContentParser parser = XContentFactory.xContent(XContentType.JSON) + .createParser(NamedXContentRegistry.EMPTY, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, ANACHRONISTIC_AGG_DATAFEED)) { + + XContentParseException e = expectThrows(XContentParseException.class, + () -> DatafeedConfig.STRICT_PARSER.apply(parser, null).build()); + assertEquals("[8:25] [datafeed_config] failed to parse field [aggregations]", e.getMessage()); + } + } + public void testFutureMetadataParse() throws IOException { XContentParser parser = XContentFactory.xContent(XContentType.JSON) .createParser(NamedXContentRegistry.EMPTY, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, FUTURE_DATAFEED); @@ -274,7 +349,7 @@ public void testBuild_GivenScriptFieldsAndAggregations() { datafeed.setTypes(Collections.singletonList("my_type")); datafeed.setScriptFields(Collections.singletonList(new SearchSourceBuilder.ScriptField(randomAlphaOfLength(10), mockScript(randomAlphaOfLength(10)), randomBoolean()))); - datafeed.setAggregations(new AggregatorFactories.Builder().addAggregator(AggregationBuilders.avg("foo"))); + datafeed.setParsedAggregations(new AggregatorFactories.Builder().addAggregator(AggregationBuilders.avg("foo"))); ElasticsearchException e = expectThrows(ElasticsearchException.class, datafeed::build); @@ -295,7 +370,7 @@ public void testHasAggregations_NonEmpty() { builder.setIndices(Collections.singletonList("myIndex")); builder.setTypes(Collections.singletonList("myType")); MaxAggregationBuilder maxTime = AggregationBuilders.max("time").field("time"); - builder.setAggregations(new AggregatorFactories.Builder().addAggregator( + builder.setParsedAggregations(new AggregatorFactories.Builder().addAggregator( AggregationBuilders.dateHistogram("time").interval(300000).subAggregation(maxTime).field("time"))); DatafeedConfig datafeedConfig = builder.build(); @@ -306,7 +381,7 @@ public void testBuild_GivenEmptyAggregations() { DatafeedConfig.Builder builder = new DatafeedConfig.Builder("datafeed1", "job1"); builder.setIndices(Collections.singletonList("myIndex")); builder.setTypes(Collections.singletonList("myType")); - builder.setAggregations(new AggregatorFactories.Builder()); + builder.setParsedAggregations(new AggregatorFactories.Builder()); ElasticsearchException e = expectThrows(ElasticsearchException.class, builder::build); @@ -318,13 +393,13 @@ public void testBuild_GivenHistogramWithDefaultInterval() { builder.setIndices(Collections.singletonList("myIndex")); builder.setTypes(Collections.singletonList("myType")); MaxAggregationBuilder maxTime = AggregationBuilders.max("time").field("time"); - builder.setAggregations(new AggregatorFactories.Builder().addAggregator( + builder.setParsedAggregations(new AggregatorFactories.Builder().addAggregator( AggregationBuilders.histogram("time").subAggregation(maxTime).field("time")) ); ElasticsearchException e = expectThrows(ElasticsearchException.class, builder::build); - assertThat(e.getMessage(), equalTo("Aggregation interval must be greater than 0")); + assertThat(e.getMessage(), containsString("[interval] must be >0 for histogram aggregation [time]")); } public void testBuild_GivenDateHistogramWithInvalidTimeZone() { @@ -341,7 +416,7 @@ public void testBuild_GivenDateHistogramWithDefaultInterval() { ElasticsearchException e = expectThrows(ElasticsearchException.class, () -> createDatafeedWithDateHistogram((String) null)); - assertThat(e.getMessage(), equalTo("Aggregation interval must be greater than 0")); + assertThat(e.getMessage(), containsString("Aggregation interval must be greater than 0")); } public void testBuild_GivenValidDateHistogram() { @@ -402,9 +477,8 @@ public void testValidateAggregations_GivenMulitpleHistogramAggs() { TermsAggregationBuilder toplevelTerms = AggregationBuilders.terms("top_level"); toplevelTerms.subAggregation(dateHistogram); - DatafeedConfig.Builder builder = new DatafeedConfig.Builder("foo", "bar"); - builder.setAggregations(new AggregatorFactories.Builder().addAggregator(toplevelTerms)); - ElasticsearchException e = expectThrows(ElasticsearchException.class, builder::validateAggregations); + ElasticsearchException e = expectThrows(ElasticsearchException.class, + () -> DatafeedConfig.validateAggregations(new AggregatorFactories.Builder().addAggregator(toplevelTerms))); assertEquals("Aggregations can only have 1 date_histogram or histogram aggregation", e.getMessage()); } @@ -520,7 +594,9 @@ private static DatafeedConfig createDatafeedWithDateHistogram(DateHistogramAggre DatafeedConfig.Builder builder = new DatafeedConfig.Builder("datafeed1", "job1"); builder.setIndices(Collections.singletonList("myIndex")); builder.setTypes(Collections.singletonList("myType")); - builder.setAggregations(new AggregatorFactories.Builder().addAggregator(dateHistogram)); + AggregatorFactories.Builder aggs = new AggregatorFactories.Builder().addAggregator(dateHistogram); + DatafeedConfig.validateAggregations(aggs); + builder.setParsedAggregations(aggs); return builder.build(); } @@ -556,11 +632,11 @@ protected DatafeedConfig mutateInstance(DatafeedConfig instance) throws IOExcept break; case 6: BoolQueryBuilder query = new BoolQueryBuilder(); - if (instance.getQuery() != null) { - query.must(instance.getQuery()); + if (instance.getParsedQuery() != null) { + query.must(instance.getParsedQuery()); } query.filter(new TermQueryBuilder(randomAlphaOfLengthBetween(1, 10), randomAlphaOfLengthBetween(1, 10))); - builder.setQuery(query); + builder.setParsedQuery(query); break; case 7: if (instance.hasAggregations()) { @@ -571,7 +647,7 @@ protected DatafeedConfig mutateInstance(DatafeedConfig instance) throws IOExcept aggBuilder .addAggregator(new DateHistogramAggregationBuilder(timeField).field(timeField).interval(between(10000, 3600000)) .subAggregation(new MaxAggregationBuilder(timeField).field(timeField))); - builder.setAggregations(aggBuilder); + builder.setParsedAggregations(aggBuilder); if (instance.getScriptFields().isEmpty() == false) { builder.setScriptFields(Collections.emptyList()); } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/datafeed/DatafeedUpdateTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/datafeed/DatafeedUpdateTests.java index a39cd8a780dbc..5fb0e095cd89b 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/datafeed/DatafeedUpdateTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/datafeed/DatafeedUpdateTests.java @@ -167,7 +167,7 @@ public void testApply_givenFullUpdateNoAggregations() { assertThat(updatedDatafeed.getTypes(), equalTo(Collections.singletonList("t_2"))); assertThat(updatedDatafeed.getQueryDelay(), equalTo(TimeValue.timeValueSeconds(42))); assertThat(updatedDatafeed.getFrequency(), equalTo(TimeValue.timeValueSeconds(142))); - assertThat(updatedDatafeed.getQuery(), equalTo(QueryBuilders.termQuery("a", "b"))); + assertThat(updatedDatafeed.getParsedQuery(), equalTo(QueryBuilders.termQuery("a", "b"))); assertThat(updatedDatafeed.hasAggregations(), is(false)); assertThat(updatedDatafeed.getScriptFields(), equalTo(Collections.singletonList(new SearchSourceBuilder.ScriptField("a", mockScript("b"), false)))); @@ -192,7 +192,7 @@ public void testApply_givenAggregations() { assertThat(updatedDatafeed.getIndices(), equalTo(Collections.singletonList("i_1"))); assertThat(updatedDatafeed.getTypes(), equalTo(Collections.singletonList("t_1"))); - assertThat(updatedDatafeed.getAggregations(), + assertThat(updatedDatafeed.getParsedAggregations(), equalTo(new AggregatorFactories.Builder().addAggregator( AggregationBuilders.histogram("a").interval(300000).field("time").subAggregation(maxTime)))); } diff --git a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/DelayedDataDetectorIT.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/DelayedDataDetectorIT.java index 84610b4279c8e..e9d07b0b8fbb8 100644 --- a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/DelayedDataDetectorIT.java +++ b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/DelayedDataDetectorIT.java @@ -153,13 +153,13 @@ public void testMissingDataDetectionWithAggregationsAndQuery() throws Exception DatafeedConfig.Builder datafeedConfigBuilder = createDatafeedBuilder(job.getId() + "-datafeed", job.getId(), Collections.singletonList(index)); - datafeedConfigBuilder.setAggregations(new AggregatorFactories.Builder().addAggregator( + datafeedConfigBuilder.setParsedAggregations(new AggregatorFactories.Builder().addAggregator( AggregationBuilders.histogram("time") .subAggregation(maxTime) .subAggregation(avgAggregationBuilder) .field("time") .interval(TimeValue.timeValueMinutes(5).millis()))); - datafeedConfigBuilder.setQuery(new RangeQueryBuilder("value").gte(numDocs/2)); + datafeedConfigBuilder.setParsedQuery(new RangeQueryBuilder("value").gte(numDocs/2)); datafeedConfigBuilder.setFrequency(TimeValue.timeValueMinutes(5)); datafeedConfigBuilder.setDelayedDataCheckConfig(DelayedDataCheckConfig.enabledDelayedDataCheckConfig(TimeValue.timeValueHours(12))); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportPutDatafeedAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportPutDatafeedAction.java index 89ae04dcdd7d7..7e7cf483fa1f5 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportPutDatafeedAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportPutDatafeedAction.java @@ -33,6 +33,7 @@ import org.elasticsearch.xpack.core.XPackSettings; import org.elasticsearch.xpack.core.ml.MlMetadata; import org.elasticsearch.xpack.core.ml.action.PutDatafeedAction; +import org.elasticsearch.xpack.core.ml.datafeed.DatafeedConfig; import org.elasticsearch.xpack.core.rollup.action.GetRollupIndexCapsAction; import org.elasticsearch.xpack.core.rollup.action.RollupSearchAction; import org.elasticsearch.xpack.core.security.SecurityContext; @@ -154,6 +155,7 @@ private void handlePrivsResponse(String username, PutDatafeedAction.Request requ private void putDatafeed(PutDatafeedAction.Request request, Map headers, ActionListener listener) { + DatafeedConfig.validateAggregations(request.getDatafeed().getParsedAggregations()); clusterService.submitStateUpdateTask( "put-datafeed-" + request.getDatafeed().getId(), new AckedClusterStateUpdateTask(request, listener) { diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStartDatafeedAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStartDatafeedAction.java index f556b1443fdf2..77c588b4a8366 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStartDatafeedAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStartDatafeedAction.java @@ -91,6 +91,7 @@ static void validate(String datafeedId, MlMetadata mlMetadata, PersistentTasksCu throw ExceptionsHelper.missingJobException(datafeed.getJobId()); } DatafeedJobValidator.validate(datafeed, job); + DatafeedConfig.validateAggregations(datafeed.getParsedAggregations()); JobState jobState = MlTasks.getJobState(datafeed.getJobId(), tasks); if (jobState.isAnyOf(JobState.OPENING, JobState.OPENED) == false) { throw ExceptionsHelper.conflictStatusException("cannot start datafeed [" + datafeedId + "] because job [" + job.getId() + diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/delayeddatacheck/DelayedDataDetectorFactory.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/delayeddatacheck/DelayedDataDetectorFactory.java index 6cf1ffac1c1c2..37f439df7c2d4 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/delayeddatacheck/DelayedDataDetectorFactory.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/delayeddatacheck/DelayedDataDetectorFactory.java @@ -44,7 +44,7 @@ public static DelayedDataDetector buildDetector(Job job, DatafeedConfig datafeed window, job.getId(), job.getDataDescription().getTimeField(), - datafeedConfig.getQuery(), + datafeedConfig.getParsedQuery(), datafeedConfig.getIndices().toArray(new String[0]), client); } else { diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/AggregationDataExtractorFactory.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/AggregationDataExtractorFactory.java index a4322275e039b..376e9507dcb7c 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/AggregationDataExtractorFactory.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/AggregationDataExtractorFactory.java @@ -35,8 +35,8 @@ public DataExtractor newExtractor(long start, long end) { job.getAnalysisConfig().analysisFields(), datafeedConfig.getIndices(), datafeedConfig.getTypes(), - datafeedConfig.getQuery(), - datafeedConfig.getAggregations(), + datafeedConfig.getParsedQuery(), + datafeedConfig.getParsedAggregations(), Intervals.alignToCeil(start, histogramInterval), Intervals.alignToFloor(end, histogramInterval), job.getAnalysisConfig().getSummaryCountFieldName().equals(DatafeedConfig.DOC_COUNT), diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/RollupDataExtractorFactory.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/RollupDataExtractorFactory.java index c8a96d6c306af..f0ee22ce85eae 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/RollupDataExtractorFactory.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/RollupDataExtractorFactory.java @@ -57,8 +57,8 @@ public DataExtractor newExtractor(long start, long end) { job.getAnalysisConfig().analysisFields(), datafeedConfig.getIndices(), datafeedConfig.getTypes(), - datafeedConfig.getQuery(), - datafeedConfig.getAggregations(), + datafeedConfig.getParsedQuery(), + datafeedConfig.getParsedAggregations(), Intervals.alignToCeil(start, histogramInterval), Intervals.alignToFloor(end, histogramInterval), job.getAnalysisConfig().getSummaryCountFieldName().equals(DatafeedConfig.DOC_COUNT), @@ -73,7 +73,7 @@ public static void create(Client client, ActionListener listener) { final AggregationBuilder datafeedHistogramAggregation = getHistogramAggregation( - datafeed.getAggregations().getAggregatorFactories()); + datafeed.getParsedAggregations().getAggregatorFactories()); if ((datafeedHistogramAggregation instanceof DateHistogramAggregationBuilder) == false) { listener.onFailure( new IllegalArgumentException("Rollup requires that the datafeed configuration use a [date_histogram] aggregation," + @@ -104,7 +104,7 @@ public static void create(Client client, return; } final List flattenedAggs = new ArrayList<>(); - flattenAggregations(datafeed.getAggregations().getAggregatorFactories(), datafeedHistogramAggregation, flattenedAggs); + flattenAggregations(datafeed.getParsedAggregations().getAggregatorFactories(), datafeedHistogramAggregation, flattenedAggs); if (validIntervalCaps.stream().noneMatch(rollupJobConfig -> hasAggregations(rollupJobConfig, flattenedAggs))) { listener.onFailure( diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/chunked/ChunkedDataExtractorFactory.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/chunked/ChunkedDataExtractorFactory.java index 67079cf2e6777..68161507ed742 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/chunked/ChunkedDataExtractorFactory.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/chunked/ChunkedDataExtractorFactory.java @@ -36,7 +36,7 @@ public DataExtractor newExtractor(long start, long end) { job.getDataDescription().getTimeField(), datafeedConfig.getIndices(), datafeedConfig.getTypes(), - datafeedConfig.getQuery(), + datafeedConfig.getParsedQuery(), datafeedConfig.getScrollSize(), timeAligner.alignToCeil(start), timeAligner.alignToFloor(end), diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/scroll/ScrollDataExtractorFactory.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/scroll/ScrollDataExtractorFactory.java index 67689bd51b8b5..986387c2ed808 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/scroll/ScrollDataExtractorFactory.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/scroll/ScrollDataExtractorFactory.java @@ -44,7 +44,7 @@ public DataExtractor newExtractor(long start, long end) { extractedFields, datafeedConfig.getIndices(), datafeedConfig.getTypes(), - datafeedConfig.getQuery(), + datafeedConfig.getParsedQuery(), datafeedConfig.getScriptFields(), datafeedConfig.getScrollSize(), start, diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/action/TransportPreviewDatafeedActionTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/action/TransportPreviewDatafeedActionTests.java index ab3fe083d5ff4..10f281b292154 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/action/TransportPreviewDatafeedActionTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/action/TransportPreviewDatafeedActionTests.java @@ -81,7 +81,7 @@ public void testBuildPreviewDatafeed_GivenAggregations() { DatafeedConfig.Builder datafeed = new DatafeedConfig.Builder("no_aggs_feed", "job_foo"); datafeed.setIndices(Collections.singletonList("my_index")); MaxAggregationBuilder maxTime = AggregationBuilders.max("time").field("time"); - datafeed.setAggregations(AggregatorFactories.builder().addAggregator( + datafeed.setParsedAggregations(AggregatorFactories.builder().addAggregator( AggregationBuilders.histogram("time").interval(300000).subAggregation(maxTime).field("time"))); datafeed.setChunkingConfig(ChunkingConfig.newManual(TimeValue.timeValueHours(1))); diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/DatafeedJobValidatorTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/DatafeedJobValidatorTests.java index 0af5b5a4f9b8e..7fdb9030999d1 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/DatafeedJobValidatorTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/DatafeedJobValidatorTests.java @@ -222,7 +222,7 @@ private static DatafeedConfig.Builder createValidDatafeedConfigWithAggs(double i HistogramAggregationBuilder histogram = AggregationBuilders.histogram("time").interval(interval).field("time").subAggregation(maxTime); DatafeedConfig.Builder datafeedConfig = createValidDatafeedConfig(); - datafeedConfig.setAggregations(new AggregatorFactories.Builder().addAggregator(histogram)); + datafeedConfig.setParsedAggregations(new AggregatorFactories.Builder().addAggregator(histogram)); return datafeedConfig; } diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/extractor/DataExtractorFactoryTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/extractor/DataExtractorFactoryTests.java index 7399d31f7c6ac..06475eabc6ecc 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/extractor/DataExtractorFactoryTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/extractor/DataExtractorFactoryTests.java @@ -143,7 +143,7 @@ public void testCreateDataExtractorFactoryGivenDefaultAggregation() { jobBuilder.setDataDescription(dataDescription); DatafeedConfig.Builder datafeedConfig = DatafeedManagerTests.createDatafeedConfig("datafeed1", "foo"); MaxAggregationBuilder maxTime = AggregationBuilders.max("time").field("time"); - datafeedConfig.setAggregations(AggregatorFactories.builder().addAggregator( + datafeedConfig.setParsedAggregations(AggregatorFactories.builder().addAggregator( AggregationBuilders.histogram("time").interval(300000).subAggregation(maxTime).field("time"))); ActionListener listener = ActionListener.wrap( @@ -162,7 +162,7 @@ public void testCreateDataExtractorFactoryGivenAggregationWithOffChunk() { DatafeedConfig.Builder datafeedConfig = DatafeedManagerTests.createDatafeedConfig("datafeed1", "foo"); datafeedConfig.setChunkingConfig(ChunkingConfig.newOff()); MaxAggregationBuilder maxTime = AggregationBuilders.max("time").field("time"); - datafeedConfig.setAggregations(AggregatorFactories.builder().addAggregator( + datafeedConfig.setParsedAggregations(AggregatorFactories.builder().addAggregator( AggregationBuilders.histogram("time").interval(300000).subAggregation(maxTime).field("time"))); ActionListener listener = ActionListener.wrap( @@ -180,7 +180,7 @@ public void testCreateDataExtractorFactoryGivenDefaultAggregationWithAutoChunk() jobBuilder.setDataDescription(dataDescription); DatafeedConfig.Builder datafeedConfig = DatafeedManagerTests.createDatafeedConfig("datafeed1", "foo"); MaxAggregationBuilder maxTime = AggregationBuilders.max("time").field("time"); - datafeedConfig.setAggregations(AggregatorFactories.builder().addAggregator( + datafeedConfig.setParsedAggregations(AggregatorFactories.builder().addAggregator( AggregationBuilders.histogram("time").interval(300000).subAggregation(maxTime).field("time"))); datafeedConfig.setChunkingConfig(ChunkingConfig.newAuto()); @@ -203,7 +203,7 @@ public void testCreateDataExtractorFactoryGivenRollupAndValidAggregation() { MaxAggregationBuilder maxTime = AggregationBuilders.max("time").field("time"); MaxAggregationBuilder myField = AggregationBuilders.max("myField").field("myField"); TermsAggregationBuilder myTerm = AggregationBuilders.terms("termAgg").field("termField").subAggregation(myField); - datafeedConfig.setAggregations(AggregatorFactories.builder().addAggregator( + datafeedConfig.setParsedAggregations(AggregatorFactories.builder().addAggregator( AggregationBuilders.dateHistogram("time").interval(600_000).subAggregation(maxTime).subAggregation(myTerm).field("time"))); ActionListener listener = ActionListener.wrap( dataExtractorFactory -> assertThat(dataExtractorFactory, instanceOf(RollupDataExtractorFactory.class)), @@ -223,7 +223,7 @@ public void testCreateDataExtractorFactoryGivenRollupAndValidAggregationAndAutoC MaxAggregationBuilder maxTime = AggregationBuilders.max("time").field("time"); MaxAggregationBuilder myField = AggregationBuilders.max("myField").field("myField"); TermsAggregationBuilder myTerm = AggregationBuilders.terms("termAgg").field("termField").subAggregation(myField); - datafeedConfig.setAggregations(AggregatorFactories.builder().addAggregator( + datafeedConfig.setParsedAggregations(AggregatorFactories.builder().addAggregator( AggregationBuilders.dateHistogram("time").interval(600_000).subAggregation(maxTime).subAggregation(myTerm).field("time"))); ActionListener listener = ActionListener.wrap( dataExtractorFactory -> assertThat(dataExtractorFactory, instanceOf(ChunkedDataExtractorFactory.class)), @@ -263,7 +263,7 @@ public void testCreateDataExtractorFactoryGivenRollupWithBadInterval() { MaxAggregationBuilder maxTime = AggregationBuilders.max("time").field("time"); MaxAggregationBuilder myField = AggregationBuilders.max("myField").field("myField"); TermsAggregationBuilder myTerm = AggregationBuilders.terms("termAgg").field("termField").subAggregation(myField); - datafeedConfig.setAggregations(AggregatorFactories.builder().addAggregator( + datafeedConfig.setParsedAggregations(AggregatorFactories.builder().addAggregator( AggregationBuilders.dateHistogram("time").interval(600_000).subAggregation(maxTime).subAggregation(myTerm).field("time"))); ActionListener listener = ActionListener.wrap( dataExtractorFactory -> fail(), @@ -288,7 +288,7 @@ public void testCreateDataExtractorFactoryGivenRollupMissingTerms() { MaxAggregationBuilder maxTime = AggregationBuilders.max("time").field("time"); MaxAggregationBuilder myField = AggregationBuilders.max("myField").field("myField"); TermsAggregationBuilder myTerm = AggregationBuilders.terms("termAgg").field("termField").subAggregation(myField); - datafeedConfig.setAggregations(AggregatorFactories.builder().addAggregator( + datafeedConfig.setParsedAggregations(AggregatorFactories.builder().addAggregator( AggregationBuilders.dateHistogram("time").interval(600_000).subAggregation(maxTime).subAggregation(myTerm).field("time"))); ActionListener listener = ActionListener.wrap( dataExtractorFactory -> fail(), @@ -312,7 +312,7 @@ public void testCreateDataExtractorFactoryGivenRollupMissingMetric() { MaxAggregationBuilder maxTime = AggregationBuilders.max("time").field("time"); MaxAggregationBuilder myField = AggregationBuilders.max("myField").field("otherField"); TermsAggregationBuilder myTerm = AggregationBuilders.terms("termAgg").field("termField").subAggregation(myField); - datafeedConfig.setAggregations(AggregatorFactories.builder().addAggregator( + datafeedConfig.setParsedAggregations(AggregatorFactories.builder().addAggregator( AggregationBuilders.dateHistogram("time").interval(600_000).subAggregation(maxTime).subAggregation(myTerm).field("time"))); ActionListener listener = ActionListener.wrap( dataExtractorFactory -> fail(), diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/AggregationDataExtractorFactoryTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/AggregationDataExtractorFactoryTests.java index 8f4aad57c3ffd..c9a2e8712e243 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/AggregationDataExtractorFactoryTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/AggregationDataExtractorFactoryTests.java @@ -64,8 +64,8 @@ private AggregationDataExtractorFactory createFactory(long histogramInterval) { jobBuilder.setDataDescription(dataDescription); jobBuilder.setAnalysisConfig(analysisConfig); DatafeedConfig.Builder datafeedConfigBuilder = new DatafeedConfig.Builder("foo-feed", jobBuilder.getId()); - datafeedConfigBuilder.setAggregations(aggs); + datafeedConfigBuilder.setParsedAggregations(aggs); datafeedConfigBuilder.setIndices(Arrays.asList("my_index")); return new AggregationDataExtractorFactory(client, datafeedConfigBuilder.build(), jobBuilder.build(new Date())); } -} \ No newline at end of file +} diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/extractor/chunked/ChunkedDataExtractorFactoryTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/extractor/chunked/ChunkedDataExtractorFactoryTests.java index 3dc2364cc2a0b..77a8c936beb37 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/extractor/chunked/ChunkedDataExtractorFactoryTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/extractor/chunked/ChunkedDataExtractorFactoryTests.java @@ -91,8 +91,8 @@ private ChunkedDataExtractorFactory createFactory(long histogramInterval) { jobBuilder.setDataDescription(dataDescription); jobBuilder.setAnalysisConfig(analysisConfig); DatafeedConfig.Builder datafeedConfigBuilder = new DatafeedConfig.Builder("foo-feed", jobBuilder.getId()); - datafeedConfigBuilder.setAggregations(aggs); + datafeedConfigBuilder.setParsedAggregations(aggs); datafeedConfigBuilder.setIndices(Arrays.asList("my_index")); return new ChunkedDataExtractorFactory(client, datafeedConfigBuilder.build(), jobBuilder.build(new Date()), dataExtractorFactory); } -} \ No newline at end of file +} diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/integration/BasicDistributedJobsIT.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/integration/BasicDistributedJobsIT.java index f45c2a5e92bd8..65eb3a0121eb1 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/integration/BasicDistributedJobsIT.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/integration/BasicDistributedJobsIT.java @@ -98,7 +98,7 @@ public void testFailOverBasics_withDataFeeder() throws Exception { HistogramAggregationBuilder histogramAggregation = AggregationBuilders.histogram("time").interval(60000) .subAggregation(maxAggregation).field("time"); - configBuilder.setAggregations(AggregatorFactories.builder().addAggregator(histogramAggregation)); + configBuilder.setParsedAggregations(AggregatorFactories.builder().addAggregator(histogramAggregation)); configBuilder.setFrequency(TimeValue.timeValueMinutes(2)); DatafeedConfig config = configBuilder.build(); PutDatafeedAction.Request putDatafeedRequest = new PutDatafeedAction.Request(config); From f7f54841973f6919e165a5d9177c407031b1f182 Mon Sep 17 00:00:00 2001 From: Benjamin Trent <4357155+benwtrent@users.noreply.github.com> Date: Tue, 4 Dec 2018 10:09:39 -0600 Subject: [PATCH 106/115] [ML] adjusting for backport (#36117) --- .../core/ml/datafeed/DatafeedConfig.java | 6 ++-- .../xpack/core/ml/utils/CachedSupplier.java | 29 +++++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/utils/CachedSupplier.java diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/datafeed/DatafeedConfig.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/datafeed/DatafeedConfig.java index 3c2e7dd4eb416..264d2cd9f7fef 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/datafeed/DatafeedConfig.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/datafeed/DatafeedConfig.java @@ -13,7 +13,6 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.unit.TimeValue; -import org.elasticsearch.common.util.CachedSupplier; import org.elasticsearch.common.xcontent.ObjectParser; import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; @@ -30,6 +29,7 @@ import org.elasticsearch.xpack.core.ml.datafeed.extractor.ExtractorUtils; import org.elasticsearch.xpack.core.ml.job.config.Job; import org.elasticsearch.xpack.core.ml.job.messages.Messages; +import org.elasticsearch.xpack.core.ml.utils.CachedSupplier; import org.elasticsearch.xpack.core.ml.utils.ExceptionsHelper; import org.elasticsearch.xpack.core.ml.utils.MlStrings; import org.elasticsearch.xpack.core.ml.utils.ToXContentParams; @@ -256,7 +256,7 @@ public DatafeedConfig(StreamInput in) throws IOException { } else { this.types = null; } - if (in.getVersion().before(Version.CURRENT)) { + if (in.getVersion().before(Version.V_6_6_0)) { this.query = QUERY_TRANSFORMER.toMap(in.readNamedWriteable(QueryBuilder.class)); this.aggregations = AGG_TRANSFORMER.toMap(in.readOptionalWriteable(AggregatorFactories.Builder::new)); } else { @@ -384,7 +384,7 @@ public void writeTo(StreamOutput out) throws IOException { } else { out.writeBoolean(false); } - if (out.getVersion().before(Version.CURRENT)) { + if (out.getVersion().before(Version.V_6_6_0)) { out.writeNamedWriteable(getParsedQuery()); out.writeOptionalWriteable(getParsedAggregations()); } else { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/utils/CachedSupplier.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/utils/CachedSupplier.java new file mode 100644 index 0000000000000..3e3c1ae79a2a2 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/utils/CachedSupplier.java @@ -0,0 +1,29 @@ +package org.elasticsearch.xpack.core.ml.utils; + +import java.util.function.Supplier; + +/** + * A {@link Supplier} that caches its return value. This may be useful to make + * a {@link Supplier} idempotent or for performance reasons if always returning + * the same instance is acceptable. + */ +public final class CachedSupplier implements Supplier { + + private Supplier supplier; + private T result; + private boolean resultSet; + + public CachedSupplier(Supplier supplier) { + this.supplier = supplier; + } + + @Override + public synchronized T get() { + if (resultSet == false) { + result = supplier.get(); + resultSet = true; + } + return result; + } + +} From 6c72925df32a219bfdcca479a0a976c7ec11afdf Mon Sep 17 00:00:00 2001 From: Benjamin Trent <4357155+benwtrent@users.noreply.github.com> Date: Tue, 4 Dec 2018 10:34:06 -0600 Subject: [PATCH 107/115] adding license header to cached supplier --- .../elasticsearch/xpack/core/ml/utils/CachedSupplier.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/utils/CachedSupplier.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/utils/CachedSupplier.java index 3e3c1ae79a2a2..62162a02c24c4 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/utils/CachedSupplier.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/utils/CachedSupplier.java @@ -1,3 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ package org.elasticsearch.xpack.core.ml.utils; import java.util.function.Supplier; From 2b0651d329499085dd7ad900665bff1cc77b2ef3 Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Tue, 4 Dec 2018 17:57:42 +0100 Subject: [PATCH 108/115] [TEST] Remove auto follow pattern at the end of the test, so that it does not collide with auto follow patterns in other tests. --- .../client/documentation/CCRDocumentationIT.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/CCRDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/CCRDocumentationIT.java index 95ee1b06f4580..fea6ad0fbd720 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/CCRDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/CCRDocumentationIT.java @@ -559,6 +559,13 @@ public void onFailure(Exception e) { // end::ccr-get-auto-follow-pattern-execute-async assertTrue(latch.await(30L, TimeUnit.SECONDS)); + + // Cleanup: + { + DeleteAutoFollowPatternRequest deleteRequest = new DeleteAutoFollowPatternRequest("my_pattern"); + AcknowledgedResponse deleteResponse = client.ccr().deleteAutoFollowPattern(deleteRequest, RequestOptions.DEFAULT); + assertThat(deleteResponse.isAcknowledged(), is(true)); + } } static Map toMap(Response response) throws IOException { From 6d8954dfc598f3b5f4feb6a9d3ff36b24efe371f Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Tue, 4 Dec 2018 20:53:51 +0100 Subject: [PATCH 109/115] SNAPSHOT: Repo Creation out of ClusterStateTask (#36157) * Move `createRepository` call out of cluster state tasks * Now only `RepositoriesService#applyClusterState` manipulates `this.repositories` * Closes #9488 --- .../repositories/RepositoriesService.java | 51 +++++-------------- .../snapshots/RepositoriesIT.java | 10 ++++ 2 files changed, 24 insertions(+), 37 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java b/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java index 980df387ef512..a119ac597fd8c 100644 --- a/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java +++ b/server/src/main/java/org/elasticsearch/repositories/RepositoriesService.java @@ -42,7 +42,6 @@ import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; -import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -102,6 +101,14 @@ public void registerRepository(final RegisterRepositoryRequest request, final Ac registrationListener = listener; } + // Trying to create the new repository on master to make sure it works + try { + closeRepository(createRepository(newRepositoryMetaData)); + } catch (Exception e) { + registrationListener.onFailure(e); + return; + } + clusterService.submitStateUpdateTask(request.cause, new AckedClusterStateUpdateTask(request, registrationListener) { @Override protected ClusterStateUpdateResponse newResponse(boolean acknowledged) { @@ -109,13 +116,8 @@ protected ClusterStateUpdateResponse newResponse(boolean acknowledged) { } @Override - public ClusterState execute(ClusterState currentState) throws IOException { + public ClusterState execute(ClusterState currentState) { ensureRepositoryNotInUse(currentState, request.name); - // Trying to create the new repository on master to make sure it works - if (!registerRepository(newRepositoryMetaData)) { - // The new repository has the same settings as the old one - ignore - return currentState; - } MetaData metaData = currentState.metaData(); MetaData.Builder mdBuilder = MetaData.builder(currentState.metaData()); RepositoriesMetaData repositories = metaData.custom(RepositoriesMetaData.TYPE); @@ -129,6 +131,10 @@ public ClusterState execute(ClusterState currentState) throws IOException { for (RepositoryMetaData repositoryMetaData : repositories.repositories()) { if (repositoryMetaData.name().equals(newRepositoryMetaData.name())) { + if (newRepositoryMetaData.equals(repositoryMetaData)) { + // Previous version is the same as this one no update is needed. + return currentState; + } found = true; repositoriesMetaData.add(newRepositoryMetaData); } else { @@ -355,37 +361,8 @@ public Repository repository(String repositoryName) { throw new RepositoryMissingException(repositoryName); } - /** - * Creates a new repository and adds it to the list of registered repositories. - *

- * If a repository with the same name but different types or settings already exists, it will be closed and - * replaced with the new repository. If a repository with the same name exists but it has the same type and settings - * the new repository is ignored. - * - * @param repositoryMetaData new repository metadata - * @return {@code true} if new repository was added or {@code false} if it was ignored - */ - private boolean registerRepository(RepositoryMetaData repositoryMetaData) throws IOException { - Repository previous = repositories.get(repositoryMetaData.name()); - if (previous != null) { - RepositoryMetaData previousMetadata = previous.getMetadata(); - if (previousMetadata.equals(repositoryMetaData)) { - // Previous version is the same as this one - ignore it - return false; - } - } - Repository newRepo = createRepository(repositoryMetaData); - if (previous != null) { - closeRepository(previous); - } - Map newRepositories = new HashMap<>(repositories); - newRepositories.put(repositoryMetaData.name(), newRepo); - repositories = newRepositories; - return true; - } - /** Closes the given repository. */ - private void closeRepository(Repository repository) throws IOException { + private void closeRepository(Repository repository) { logger.debug("closing repository [{}][{}]", repository.getMetadata().type(), repository.getMetadata().name()); repository.close(); } diff --git a/server/src/test/java/org/elasticsearch/snapshots/RepositoriesIT.java b/server/src/test/java/org/elasticsearch/snapshots/RepositoriesIT.java index bde25c5a42af9..2f4096c9518c2 100644 --- a/server/src/test/java/org/elasticsearch/snapshots/RepositoriesIT.java +++ b/server/src/test/java/org/elasticsearch/snapshots/RepositoriesIT.java @@ -97,6 +97,16 @@ public void testRepositoryCreation() throws Exception { assertThat(findRepository(repositoriesResponse.repositories(), "test-repo-1"), notNullValue()); assertThat(findRepository(repositoriesResponse.repositories(), "test-repo-2"), notNullValue()); + logger.info("--> check that trying to create a repository with the same settings repeatedly does not update cluster state"); + String beforeStateUuid = clusterStateResponse.getState().stateUUID(); + assertThat( + client.admin().cluster().preparePutRepository("test-repo-1") + .setType("fs").setSettings(Settings.builder() + .put("location", location) + ).get().isAcknowledged(), + equalTo(true)); + assertEquals(beforeStateUuid, client.admin().cluster().prepareState().clear().get().getState().stateUUID()); + logger.info("--> delete repository test-repo-1"); client.admin().cluster().prepareDeleteRepository("test-repo-1").get(); repositoriesResponse = client.admin().cluster().prepareGetRepositories().get(); From 0ccb1dbfa955ec419d351e6e9fb91408deb855cd Mon Sep 17 00:00:00 2001 From: Gordon Brown Date: Tue, 4 Dec 2018 14:03:31 -0700 Subject: [PATCH 110/115] Deprecation check for `:` in Cluster/Index name (#36185) Adds a deprecation check for cluster and index names that contain `:`, which is illegal in 7.0. --- .../deprecation/ClusterDeprecationChecks.java | 14 ++++++++++- .../xpack/deprecation/DeprecationChecks.java | 7 ++++-- .../deprecation/IndexDeprecationChecks.java | 12 +++++++++ .../ClusterDeprecationChecksTests.java | 17 +++++++++++++ .../IndexDeprecationChecksTests.java | 25 +++++++++++++++++++ 5 files changed, 72 insertions(+), 3 deletions(-) diff --git a/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/ClusterDeprecationChecks.java b/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/ClusterDeprecationChecks.java index 7f11c2c2944a7..f9269c9862c84 100644 --- a/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/ClusterDeprecationChecks.java +++ b/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/ClusterDeprecationChecks.java @@ -18,7 +18,7 @@ static DeprecationIssue checkShardLimit(ClusterState state) { int maxShardsInCluster = shardsPerNode * nodeCount; int currentOpenShards = state.getMetaData().getTotalOpenIndexShards(); - if (currentOpenShards >= maxShardsInCluster) { + if (nodeCount > 0 && currentOpenShards >= maxShardsInCluster) { return new DeprecationIssue(DeprecationIssue.Level.WARNING, "Number of open shards exceeds cluster soft limit", "https://www.elastic.co/guide/en/elasticsearch/reference/master/breaking_70_cluster_changes.html", @@ -27,4 +27,16 @@ static DeprecationIssue checkShardLimit(ClusterState state) { } return null; } + + static DeprecationIssue checkClusterName(ClusterState state) { + String clusterName = state.getClusterName().value(); + if (clusterName.contains(":")) { + return new DeprecationIssue(DeprecationIssue.Level.CRITICAL, + "Cluster name cannot contain ':'", + "https://www.elastic.co/guide/en/elasticsearch/reference/master/breaking-changes-7.0.html" + + "#_literal_literal_is_no_longer_allowed_in_cluster_name", + "This cluster is named [" + clusterName + "], which contains the illegal character ':'."); + } + return null; + } } diff --git a/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/DeprecationChecks.java b/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/DeprecationChecks.java index f8d85129be760..6dbbee49589a4 100644 --- a/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/DeprecationChecks.java +++ b/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/DeprecationChecks.java @@ -32,7 +32,8 @@ private DeprecationChecks() { static List> CLUSTER_SETTINGS_CHECKS = Collections.unmodifiableList(Arrays.asList( - ClusterDeprecationChecks::checkShardLimit + ClusterDeprecationChecks::checkShardLimit, + ClusterDeprecationChecks::checkClusterName )); static List, List, DeprecationIssue>> NODE_SETTINGS_CHECKS = @@ -44,7 +45,9 @@ private DeprecationChecks() { static List> INDEX_SETTINGS_CHECKS = Collections.unmodifiableList(Arrays.asList( IndexDeprecationChecks::oldIndicesCheck, - IndexDeprecationChecks::delimitedPayloadFilterCheck)); + IndexDeprecationChecks::delimitedPayloadFilterCheck, + IndexDeprecationChecks::indexNameCheck + )); /** * helper utility function to reduce repeat of running a specific {@link Set} of checks. diff --git a/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/IndexDeprecationChecks.java b/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/IndexDeprecationChecks.java index a022a9c42a329..5134d86f51891 100644 --- a/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/IndexDeprecationChecks.java +++ b/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/IndexDeprecationChecks.java @@ -104,4 +104,16 @@ static DeprecationIssue oldIndicesCheck(IndexMetaData indexMetaData) { } return null; } + + static DeprecationIssue indexNameCheck(IndexMetaData indexMetaData) { + String clusterName = indexMetaData.getIndex().getName(); + if (clusterName.contains(":")) { + return new DeprecationIssue(DeprecationIssue.Level.WARNING, + "Index name cannot contain ':'", + "https://www.elastic.co/guide/en/elasticsearch/reference/master/breaking-changes-7.0.html" + + "#_literal_literal_is_no_longer_allowed_in_index_name", + "This index is named [" + clusterName + "], which contains the illegal character ':'."); + } + return null; + } } diff --git a/x-pack/plugin/deprecation/src/test/java/org/elasticsearch/xpack/deprecation/ClusterDeprecationChecksTests.java b/x-pack/plugin/deprecation/src/test/java/org/elasticsearch/xpack/deprecation/ClusterDeprecationChecksTests.java index dc9611c5e717a..95315e9418cb1 100644 --- a/x-pack/plugin/deprecation/src/test/java/org/elasticsearch/xpack/deprecation/ClusterDeprecationChecksTests.java +++ b/x-pack/plugin/deprecation/src/test/java/org/elasticsearch/xpack/deprecation/ClusterDeprecationChecksTests.java @@ -24,6 +24,23 @@ public class ClusterDeprecationChecksTests extends ESTestCase { + public void testCheckClusterName() { + final String badClusterName = randomAlphaOfLengthBetween(0, 10) + ":" + randomAlphaOfLengthBetween(0, 10); + final ClusterState badClusterState = ClusterState.builder(new ClusterName(badClusterName)).build(); + + DeprecationIssue expected = new DeprecationIssue(DeprecationIssue.Level.CRITICAL, "Cluster name cannot contain ':'", + "https://www.elastic.co/guide/en/elasticsearch/reference/master/breaking-changes-7.0.html" + + "#_literal_literal_is_no_longer_allowed_in_cluster_name", + "This cluster is named [" + badClusterName + "], which contains the illegal character ':'."); + List issues = DeprecationChecks.filterChecks(CLUSTER_SETTINGS_CHECKS, c -> c.apply(badClusterState)); + assertEquals(singletonList(expected), issues); + + final String goodClusterName = randomAlphaOfLengthBetween(1,30); + final ClusterState goodClusterState = ClusterState.builder(new ClusterName(goodClusterName)).build(); + List noIssues = DeprecationChecks.filterChecks(CLUSTER_SETTINGS_CHECKS, c -> c.apply(goodClusterState)); + assertTrue(noIssues.isEmpty()); + } + public void testCheckShardLimit() { int shardsPerNode = randomIntBetween(2, 10000); int nodeCount = randomIntBetween(1, 10); diff --git a/x-pack/plugin/deprecation/src/test/java/org/elasticsearch/xpack/deprecation/IndexDeprecationChecksTests.java b/x-pack/plugin/deprecation/src/test/java/org/elasticsearch/xpack/deprecation/IndexDeprecationChecksTests.java index 3020dc82e78f7..8255461c1bcdd 100644 --- a/x-pack/plugin/deprecation/src/test/java/org/elasticsearch/xpack/deprecation/IndexDeprecationChecksTests.java +++ b/x-pack/plugin/deprecation/src/test/java/org/elasticsearch/xpack/deprecation/IndexDeprecationChecksTests.java @@ -51,4 +51,29 @@ public void testDelimitedPayloadFilterCheck() { List issues = DeprecationInfoAction.filterChecks(INDEX_SETTINGS_CHECKS, c -> c.apply(indexMetaData)); assertEquals(singletonList(expected), issues); } + + public void testIndexNameCheck(){ + final String badIndexName = randomAlphaOfLengthBetween(0, 10) + ":" + randomAlphaOfLengthBetween(0, 10); + final IndexMetaData badIndex = IndexMetaData.builder(badIndexName) + .settings(settings(Version.CURRENT)) + .numberOfShards(randomIntBetween(1,100)) + .numberOfReplicas(randomIntBetween(1,15)) + .build(); + + DeprecationIssue expected = new DeprecationIssue(DeprecationIssue.Level.WARNING, "Index name cannot contain ':'", + "https://www.elastic.co/guide/en/elasticsearch/reference/master/breaking-changes-7.0.html" + + "#_literal_literal_is_no_longer_allowed_in_index_name", + "This index is named [" + badIndexName + "], which contains the illegal character ':'."); + List issues = DeprecationChecks.filterChecks(INDEX_SETTINGS_CHECKS, c -> c.apply(badIndex)); + assertEquals(singletonList(expected), issues); + + final String goodIndexName = randomAlphaOfLengthBetween(1,30); + final IndexMetaData goodIndex = IndexMetaData.builder(goodIndexName) + .settings(settings(Version.CURRENT)) + .numberOfShards(randomIntBetween(1,100)) + .numberOfReplicas(randomIntBetween(1,15)) + .build(); + List noIssues = DeprecationChecks.filterChecks(INDEX_SETTINGS_CHECKS, c -> c.apply(goodIndex)); + assertTrue(noIssues.isEmpty()); + } } From ab041102821bb6581d82afd30faa6e1dc309e7d9 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Tue, 4 Dec 2018 13:18:54 -0800 Subject: [PATCH 111/115] [DOCS] Moves security config file info (#36232) --- .../security/reference/files.asciidoc | 25 +++++----- .../settings/security-settings.asciidoc | 47 ++++++++++--------- .../docs/en/security/configuring-es.asciidoc | 4 ++ x-pack/docs/en/security/reference.asciidoc | 11 ----- 4 files changed, 42 insertions(+), 45 deletions(-) delete mode 100644 x-pack/docs/en/security/reference.asciidoc diff --git a/docs/reference/security/reference/files.asciidoc b/docs/reference/security/reference/files.asciidoc index 64a004c6646eb..306fdcdddc164 100644 --- a/docs/reference/security/reference/files.asciidoc +++ b/docs/reference/security/reference/files.asciidoc @@ -1,30 +1,31 @@ [role="xpack"] +[testenv="gold"] [[security-files]] -=== Security Files +=== Security files -{security} uses the following files: +The {es} {security-features} use the following files: -* `ES_PATH_CONF/roles.yml` defines the roles in use on the cluster - (read more <>). +* `ES_PATH_CONF/roles.yml` defines the roles in use on the cluster. See +{stack-ov}/defining-roles.html[Defining roles]. * `ES_PATH_CONF/elasticsearch-users` defines the users and their hashed passwords for - the <>. + the `file` realm. See <>. * `ES_PATH_CONF/elasticsearch-users_roles` defines the user roles assignment for the - the <>. + the `file` realm. See <>. * `ES_PATH_CONF/role_mapping.yml` defines the role assignments for a Distinguished Name (DN) to a role. This allows for LDAP and Active Directory - groups and users and PKI users to be mapped to roles (read more - <>). + groups and users and PKI users to be mapped to roles. See + {stack-ov}/mapping-roles.html[Mapping users and groups to roles]. -* `ES_PATH_CONF/log4j2.properties` contains audit information (read more - <>). +* `ES_PATH_CONF/log4j2.properties` contains audit information. See +{stack-ov}/audit-log-output.html[Logfile audit output]. [[security-files-location]] -IMPORTANT: Any files that {security} uses must be stored in the Elasticsearch - configuration directory. Elasticsearch runs with restricted permissions +IMPORTANT: Any files that the {security-features} use must be stored in the {es} + configuration directory. {es} runs with restricted permissions and is only permitted to read from the locations configured in the directory layout for enhanced security. diff --git a/docs/reference/settings/security-settings.asciidoc b/docs/reference/settings/security-settings.asciidoc index 78fefff6cb18f..8ca20e4ea3fee 100644 --- a/docs/reference/settings/security-settings.asciidoc +++ b/docs/reference/settings/security-settings.asciidoc @@ -5,8 +5,9 @@ Security settings ++++ -By default, {security} is disabled when you have a basic or trial license. To -enable {security}, use the `xpack.security.enabled` setting. +By default, the {es} {security-features} are disabled when you have a basic or +trial license. To enable {security-features}, use the `xpack.security.enabled` +setting. You configure `xpack.security` settings to <> @@ -25,13 +26,15 @@ For more information about creating and updating the {es} keystore, see [[general-security-settings]] ==== General security settings `xpack.security.enabled`:: -Set to `true` to enable {security} on the node. + +Set to `true` to enable {es} {security-features} on the node. + + -- If set to `false`, which is the default value for basic and trial licenses, -{security} is disabled. It also affects all {kib} instances that connect to this -{es} instance; you do not need to disable {security} in those `kibana.yml` files. -For more information about disabling {security} in specific {kib} instances, see {kibana-ref}/security-settings-kb.html[{kib} security settings]. +{security-features} are disabled. It also affects all {kib} instances that +connect to this {es} instance; you do not need to disable {security-features} in +those `kibana.yml` files. For more information about disabling {security-features} +in specific {kib} instances, see +{kibana-ref}/security-settings-kb.html[{kib} security settings]. TIP: If you have gold or higher licenses, the default value is `true`; we recommend that you explicitly add this setting to avoid confusion. @@ -66,7 +69,7 @@ See <>. Defaults to `bcrypt`. [[anonymous-access-settings]] ==== Anonymous access settings You can configure the following anonymous access settings in -`elasticsearch.yml`. For more information, see {xpack-ref}/anonymous-access.html[ +`elasticsearch.yml`. For more information, see {stack-ov}/anonymous-access.html[ Enabling anonymous access]. `xpack.security.authc.anonymous.username`:: @@ -116,7 +119,7 @@ Defaults to `48h` (48 hours). You can set the following document and field level security settings in `elasticsearch.yml`. For more information, see -{xpack-ref}/field-and-document-access-control.html[Setting up document and field +{stack-ov}/field-and-document-access-control.html[Setting up document and field level security]. `xpack.security.dls_fls.enabled`:: @@ -172,7 +175,7 @@ xpack.security.authc.realms: ---------------------------------------- The valid settings vary depending on the realm type. For more -information, see {xpack-ref}/setting-up-authentication.html[Setting up authentication]. +information, see {stack-ov}/setting-up-authentication.html[Setting up authentication]. [float] [[ref-realm-settings]] @@ -211,7 +214,7 @@ Defaults to `ssha256`. `authentication.enabled`:: If set to `false`, disables authentication support in this realm, so that it only supports user lookups. -(See the {xpack-ref}/run-as-privilege.html[run as] and +(See the {stack-ov}/run-as-privilege.html[run as] and {stack-ov}/realm-chains.html#authorization_realms[authorization realms] features). Defaults to `true`. @@ -240,7 +243,7 @@ user credentials. See <>. Defaults to `ssha256`. `authentication.enabled`:: If set to `false`, disables authentication support in this realm, so that it only supports user lookups. -(See the {xpack-ref}/run-as-privilege.html[run as] and +(See the {stack-ov}/run-as-privilege.html[run as] and {stack-ov}/realm-chains.html#authorization_realms[authorization realms] features). Defaults to `true`. @@ -289,7 +292,7 @@ The DN template that replaces the user name with the string `{0}`. This setting is multivalued; you can specify multiple user contexts. Required to operate in user template mode. If `user_search.base_dn` is specified, this setting is not valid. For more information on -the different modes, see {xpack-ref}/ldap-realm.html[LDAP realms]. +the different modes, see {stack-ov}/ldap-realm.html[LDAP realms]. `authorization_realms`:: The names of the realms that should be consulted for delegated authorization. @@ -313,7 +316,7 @@ to `memberOf`. Specifies a container DN to search for users. Required to operated in user search mode. If `user_dn_templates` is specified, this setting is not valid. For more information on -the different modes, see {xpack-ref}/ldap-realm.html[LDAP realms]. +the different modes, see {stack-ov}/ldap-realm.html[LDAP realms]. `user_search.scope`:: The scope of the user search. Valid values are `sub_tree`, `one_level` or @@ -386,11 +389,11 @@ the filter. If not set, the user DN is passed into the filter. Defaults to Empt If set to `true`, the names of any unmapped LDAP groups are used as role names and assigned to the user. A group is considered to be _unmapped_ if it is not referenced in a -{xpack-ref}/mapping-roles.html#mapping-roles-file[role-mapping file]. API-based +{stack-ov}/mapping-roles.html#mapping-roles-file[role-mapping file]. API-based role mappings are not considered. Defaults to `false`. `files.role_mapping`:: -The {xpack-ref}/security-files.html[location] for the {xpack-ref}/mapping-roles.html#mapping-roles[ +The <> for the {stack-ov}/mapping-roles.html#mapping-roles[ YAML role mapping configuration file]. Defaults to `ES_PATH_CONF/role_mapping.yml`. @@ -508,7 +511,7 @@ in-memory cached user credentials. See <>. Defaults to `ssha256 `authentication.enabled`:: If set to `false`, disables authentication support in this realm, so that it only supports user lookups. -(See the {xpack-ref}/run-as-privilege.html[run as] and +(See the {stack-ov}/run-as-privilege.html[run as] and {stack-ov}/realm-chains.html#authorization_realms[authorization realms] features). Defaults to `true`. @@ -564,7 +567,7 @@ is not referenced in any role-mapping files. API-based role mappings are not considered. Defaults to `false`. `files.role_mapping`:: -The {xpack-ref}/security-files.html[location] for the YAML +The <> for the YAML role mapping configuration file. Defaults to `ES_PATH_CONF/role_mapping.yml`. `user_search.base_dn`:: @@ -755,7 +758,7 @@ the in-memory cached user credentials. See <>. Defaults to `ssh `authentication.enabled`:: If set to `false`, disables authentication support in this realm, so that it only supports user lookups. -(See the {xpack-ref}/run-as-privilege.html[run as] and +(See the {stack-ov}/run-as-privilege.html[run as] and {stack-ov}/realm-chains.html#authorization_realms[authorization realms] features). Defaults to `true`. @@ -796,8 +799,8 @@ The path of a truststore to use. Defaults to the trusted certificates configured for SSL. This setting cannot be used with `certificate_authorities`. `files.role_mapping`:: -Specifies the {xpack-ref}/security-files.html[location] of the -{xpack-ref}/mapping-roles.html[YAML role mapping configuration file]. +Specifies the <> of the +{stack-ov}/mapping-roles.html[YAML role mapping configuration file]. Defaults to `ES_PATH_CONF/role_mapping.yml`. `authorization_realms`:: @@ -1215,7 +1218,7 @@ through the list of URLs will continue until a successful connection is made. You can configure the following TLS/SSL settings in `elasticsearch.yml`. For more information, see -{xpack-ref}/encrypting-communications.html[Encrypting communications]. These settings will be used +{stack-ov}/encrypting-communications.html[Encrypting communications]. These settings will be used for all of {xpack} unless they have been overridden by more specific settings such as those for HTTP or Transport. @@ -1457,7 +1460,7 @@ See also <>. [[ip-filtering-settings]] ==== IP filtering settings -You can configure the following settings for {xpack-ref}/ip-filtering.html[IP filtering]. +You can configure the following settings for {stack-ov}/ip-filtering.html[IP filtering]. `xpack.security.transport.filter.allow`:: List of IP addresses to allow. diff --git a/x-pack/docs/en/security/configuring-es.asciidoc b/x-pack/docs/en/security/configuring-es.asciidoc index f4c61adf01026..6dd5607f77e6f 100644 --- a/x-pack/docs/en/security/configuring-es.asciidoc +++ b/x-pack/docs/en/security/configuring-es.asciidoc @@ -157,5 +157,9 @@ include::authentication/configuring-kerberos-realm.asciidoc[] include::fips-140-compliance.asciidoc[] :edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/docs/reference/settings/security-settings.asciidoc include::{es-repo-dir}/settings/security-settings.asciidoc[] + +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/docs/reference/security/reference/files.asciidoc +include::{es-repo-dir}/security/reference/files.asciidoc[] + :edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/docs/reference/settings/audit-settings.asciidoc include::{es-repo-dir}/settings/audit-settings.asciidoc[] diff --git a/x-pack/docs/en/security/reference.asciidoc b/x-pack/docs/en/security/reference.asciidoc deleted file mode 100644 index 75de1daee6d6b..0000000000000 --- a/x-pack/docs/en/security/reference.asciidoc +++ /dev/null @@ -1,11 +0,0 @@ -[role="xpack"] -[[security-reference]] -== Reference -* <> -* {ref}/security-settings.html[Security Settings] -* <> -* {ref}/security-api.html[Security API] -* {ref}/xpack-commands.html[Security Commands] - -:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/docs/reference/security/reference/files.asciidoc -include::{es-repo-dir}/security/reference/files.asciidoc[] From dce853b84540eabc5780aecc389ecc1036819e61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20B=C3=BCscher?= Date: Wed, 5 Dec 2018 00:31:52 +0100 Subject: [PATCH 112/115] Add javadocs about expected exceptions to RestHighLevelClient (#36216) Add a more detailed section about what exceptions to expect from the blocking calls in this class and removing the mostly redundant mentions of the IOExceptions from each method javadoc since it doesn't give much details about the expected exceptions anyway. Closes #30334 --- .../client/RestHighLevelClient.java | 108 ++++++++---------- docs/java-rest/high-level/execution.asciidoc | 14 ++- 2 files changed, 60 insertions(+), 62 deletions(-) diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java index 6b588e4412184..df80ae3b4edb0 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java @@ -59,10 +59,10 @@ import org.elasticsearch.action.update.UpdateResponse; import org.elasticsearch.client.core.CountRequest; import org.elasticsearch.client.core.CountResponse; -import org.elasticsearch.client.core.TermVectorsResponse; -import org.elasticsearch.client.core.TermVectorsRequest; import org.elasticsearch.client.core.MultiTermVectorsRequest; import org.elasticsearch.client.core.MultiTermVectorsResponse; +import org.elasticsearch.client.core.TermVectorsRequest; +import org.elasticsearch.client.core.TermVectorsResponse; import org.elasticsearch.client.tasks.TaskSubmissionResponse; import org.elasticsearch.common.CheckedConsumer; import org.elasticsearch.common.CheckedFunction; @@ -138,6 +138,8 @@ import org.elasticsearch.search.aggregations.metrics.geobounds.ParsedGeoBounds; import org.elasticsearch.search.aggregations.metrics.geocentroid.GeoCentroidAggregationBuilder; import org.elasticsearch.search.aggregations.metrics.geocentroid.ParsedGeoCentroid; +import org.elasticsearch.search.aggregations.metrics.mad.MedianAbsoluteDeviationAggregationBuilder; +import org.elasticsearch.search.aggregations.metrics.mad.ParsedMedianAbsoluteDeviation; import org.elasticsearch.search.aggregations.metrics.max.MaxAggregationBuilder; import org.elasticsearch.search.aggregations.metrics.max.ParsedMax; import org.elasticsearch.search.aggregations.metrics.min.MinAggregationBuilder; @@ -162,8 +164,6 @@ import org.elasticsearch.search.aggregations.metrics.tophits.TopHitsAggregationBuilder; import org.elasticsearch.search.aggregations.metrics.valuecount.ParsedValueCount; import org.elasticsearch.search.aggregations.metrics.valuecount.ValueCountAggregationBuilder; -import org.elasticsearch.search.aggregations.metrics.mad.MedianAbsoluteDeviationAggregationBuilder; -import org.elasticsearch.search.aggregations.metrics.mad.ParsedMedianAbsoluteDeviation; import org.elasticsearch.search.aggregations.pipeline.InternalSimpleValue; import org.elasticsearch.search.aggregations.pipeline.ParsedSimpleValue; import org.elasticsearch.search.aggregations.pipeline.bucketmetrics.InternalBucketMetricValue; @@ -201,13 +201,33 @@ import static java.util.stream.Collectors.toList; /** - * High level REST client that wraps an instance of the low level {@link RestClient} and allows to build requests and read responses. - * The {@link RestClient} instance is internally built based on the provided {@link RestClientBuilder} and it gets closed automatically - * when closing the {@link RestHighLevelClient} instance that wraps it. + * High level REST client that wraps an instance of the low level {@link RestClient} and allows to build requests and read responses. The + * {@link RestClient} instance is internally built based on the provided {@link RestClientBuilder} and it gets closed automatically when + * closing the {@link RestHighLevelClient} instance that wraps it. + *

+ * * In case an already existing instance of a low-level REST client needs to be provided, this class can be subclassed and the - * {@link #RestHighLevelClient(RestClient, CheckedConsumer, List)} constructor can be used. - * This class can also be sub-classed to expose additional client methods that make use of endpoints added to Elasticsearch through - * plugins, or to add support for custom response sections, again added to Elasticsearch through plugins. + * {@link #RestHighLevelClient(RestClient, CheckedConsumer, List)} constructor can be used. + *

+ * + * This class can also be sub-classed to expose additional client methods that make use of endpoints added to Elasticsearch through plugins, + * or to add support for custom response sections, again added to Elasticsearch through plugins. + *

+ * + * The majority of the methods in this class come in two flavors, a blocking and an asynchronous version (e.g. + * {@link #search(SearchRequest, RequestOptions)} and {@link #searchAsync(SearchRequest, RequestOptions, ActionListener)}, where the later + * takes an implementation of an {@link ActionListener} as an argument that needs to implement methods that handle successful responses and + * failure scenarios. Most of the blocking calls can throw an {@link IOException} or an unchecked {@link ElasticsearchException} in the + * following cases: + * + *

    + *
  • an {@link IOException} is usually thrown in case of failing to parse the REST response in the high-level REST client, the request + * times out or similar cases where there is no response coming back from the Elasticsearch server
  • + *
  • an {@link ElasticsearchException} is usually thrown in case where the server returns a 4xx or 5xx error code. The high-level client + * then tries to parse the response body error details into a generic ElasticsearchException and suppresses the original + * {@link ResponseException}
  • + *
+ * */ public class RestHighLevelClient implements Closeable { @@ -445,7 +465,6 @@ public SecurityClient security() { * @param bulkRequest the request * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized * @return the response - * @throws IOException in case there is a problem sending the request or parsing back the response */ public final BulkResponse bulk(BulkRequest bulkRequest, RequestOptions options) throws IOException { return performRequestAndParseEntity(bulkRequest, RequestConverters::bulk, options, BulkResponse::fromXContent, emptySet()); @@ -490,7 +509,6 @@ public final void bulkAsync(BulkRequest bulkRequest, ActionListenertrue if the ping succeeded, false otherwise - * @throws IOException in case there is a problem sending the request */ public final boolean ping(RequestOptions options) throws IOException { return performRequest(new MainRequest(), (request) -> RequestConverters.ping(), options, RestHighLevelClient::convertExistsResponse, @@ -696,7 +707,6 @@ public final boolean ping(Header... headers) throws IOException { * Get the cluster info otherwise provided when sending an HTTP request to '/' * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized * @return the response - * @throws IOException in case there is a problem sending the request or parsing back the response */ public final MainResponse info(RequestOptions options) throws IOException { return performRequestAndParseEntity(new MainRequest(), (request) -> RequestConverters.info(), options, @@ -719,7 +729,6 @@ public final MainResponse info(Header... headers) throws IOException { * @param getRequest the request * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized * @return the response - * @throws IOException in case there is a problem sending the request or parsing back the response */ public final GetResponse get(GetRequest getRequest, RequestOptions options) throws IOException { return performRequestAndParseEntity(getRequest, RequestConverters::get, options, GetResponse::fromXContent, singleton(404)); @@ -766,7 +775,6 @@ public final void getAsync(GetRequest getRequest, ActionListener li * @param multiGetRequest the request * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized * @return the response - * @throws IOException in case there is a problem sending the request or parsing back the response * @deprecated use {@link #mget(MultiGetRequest, RequestOptions)} instead */ @Deprecated @@ -781,7 +789,6 @@ public final MultiGetResponse multiGet(MultiGetRequest multiGetRequest, RequestO * @param multiGetRequest the request * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized * @return the response - * @throws IOException in case there is a problem sending the request or parsing back the response */ public final MultiGetResponse mget(MultiGetRequest multiGetRequest, RequestOptions options) throws IOException { return performRequestAndParseEntity(multiGetRequest, RequestConverters::multiGet, options, MultiGetResponse::fromXContent, @@ -843,7 +850,6 @@ public final void multiGetAsync(MultiGetRequest multiGetRequest, ActionListener< * @param getRequest the request * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized * @return true if the document exists, false otherwise - * @throws IOException in case there is a problem sending the request */ public final boolean exists(GetRequest getRequest, RequestOptions options) throws IOException { return performRequest(getRequest, RequestConverters::exists, options, RestHighLevelClient::convertExistsResponse, emptySet()); @@ -886,20 +892,19 @@ public final void existsAsync(GetRequest getRequest, ActionListener lis /** * Checks for the existence of a document with a "_source" field. Returns true if it exists, false otherwise. - * See Source exists API + * See Source exists API * on elastic.co * @param getRequest the request * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized * @return true if the document and _source field exists, false otherwise - * @throws IOException in case there is a problem sending the request */ public boolean existsSource(GetRequest getRequest, RequestOptions options) throws IOException { return performRequest(getRequest, RequestConverters::sourceExists, options, RestHighLevelClient::convertExistsResponse, emptySet()); - } - + } + /** * Asynchronously checks for the existence of a document with a "_source" field. Returns true if it exists, false otherwise. - * See Source exists API + * See Source exists API * on elastic.co * @param getRequest the request * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized @@ -908,15 +913,14 @@ public boolean existsSource(GetRequest getRequest, RequestOptions options) throw public final void existsSourceAsync(GetRequest getRequest, RequestOptions options, ActionListener listener) { performRequestAsync(getRequest, RequestConverters::sourceExists, options, RestHighLevelClient::convertExistsResponse, listener, emptySet()); - } - + } + /** * Index a document using the Index API. * See Index API on elastic.co * @param indexRequest the request * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized * @return the response - * @throws IOException in case there is a problem sending the request or parsing back the response */ public final IndexResponse index(IndexRequest indexRequest, RequestOptions options) throws IOException { return performRequestAndParseEntity(indexRequest, RequestConverters::index, options, IndexResponse::fromXContent, emptySet()); @@ -963,7 +967,6 @@ public final void indexAsync(IndexRequest indexRequest, ActionListenerDelete API on elastic.co - * @param deleteRequest the reuqest + * @param deleteRequest the request * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized * @return the response - * @throws IOException in case there is a problem sending the request or parsing back the response */ public final DeleteResponse delete(DeleteRequest deleteRequest, RequestOptions options) throws IOException { return performRequestAndParseEntity(deleteRequest, RequestConverters::delete, options, DeleteResponse::fromXContent, @@ -1084,7 +1085,6 @@ public final void deleteAsync(DeleteRequest deleteRequest, ActionListener * Clear Scroll API on elastic.co - * @param clearScrollRequest the reuqest + * @param clearScrollRequest the request * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized * @param listener the listener to be notified upon request completion */ @@ -1356,7 +1351,6 @@ public final void clearScrollAsync(ClearScrollRequest clearScrollRequest, * @param searchTemplateRequest the request * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized * @return the response - * @throws IOException in case there is a problem sending the request or parsing back the response */ public final SearchTemplateResponse searchTemplate(SearchTemplateRequest searchTemplateRequest, RequestOptions options) throws IOException { @@ -1382,7 +1376,6 @@ public final void searchTemplateAsync(SearchTemplateRequest searchTemplateReques * @param explainRequest the request * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized * @return the response - * @throws IOException in case there is a problem sending the request or parsing back the response */ public final ExplainResponse explain(ExplainRequest explainRequest, RequestOptions options) throws IOException { return performRequest(explainRequest, RequestConverters::explain, options, @@ -1480,7 +1473,6 @@ public final void mtermvectorsAsync(MultiTermVectorsRequest request, RequestOpti * @param rankEvalRequest the request * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized * @return the response - * @throws IOException in case there is a problem sending the request or parsing back the response */ public final RankEvalResponse rankEval(RankEvalRequest rankEvalRequest, RequestOptions options) throws IOException { return performRequestAndParseEntity(rankEvalRequest, RequestConverters::rankEval, options, RankEvalResponse::fromXContent, @@ -1558,7 +1550,6 @@ public final void rankEvalAsync(RankEvalRequest rankEvalRequest, ActionListener< * @param fieldCapabilitiesRequest the request * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized * @return the response - * @throws IOException in case there is a problem sending the request or parsing back the response */ public final FieldCapabilitiesResponse fieldCaps(FieldCapabilitiesRequest fieldCapabilitiesRequest, RequestOptions options) throws IOException { @@ -1573,7 +1564,6 @@ public final FieldCapabilitiesResponse fieldCaps(FieldCapabilitiesRequest fieldC * @param request the request * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized * @return the response - * @throws IOException in case there is a problem sending the request or parsing back the response */ public GetStoredScriptResponse getScript(GetStoredScriptRequest request, RequestOptions options) throws IOException { return performRequestAndParseEntity(request, RequestConverters::getScript, options, @@ -1601,7 +1591,6 @@ public void getScriptAsync(GetStoredScriptRequest request, RequestOptions option * @param request the request * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized * @return the response - * @throws IOException in case there is a problem sending the request or parsing back the response */ public AcknowledgedResponse deleteScript(DeleteStoredScriptRequest request, RequestOptions options) throws IOException { return performRequestAndParseEntity(request, RequestConverters::deleteScript, options, @@ -1629,7 +1618,6 @@ public void deleteScriptAsync(DeleteStoredScriptRequest request, RequestOptions * @param putStoredScriptRequest the request * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized * @return the response - * @throws IOException in case there is a problem sending the request or parsing back the response */ public AcknowledgedResponse putScript(PutStoredScriptRequest putStoredScriptRequest, RequestOptions options) throws IOException { @@ -1773,9 +1761,9 @@ private Resp internalPerformRequest(Req request, throw new IOException("Unable to parse response body for " + response, e); } } - + /** - * Defines a helper method for requests that can 404 and in which case will return an empty Optional + * Defines a helper method for requests that can 404 and in which case will return an empty Optional * otherwise tries to parse the response body */ protected final Optional performRequestAndParseOptionalEntity(Req request, @@ -1804,7 +1792,7 @@ protected final Optional performRequestAnd } catch (Exception e) { throw new IOException("Unable to parse response body for " + response, e); } - } + } @Deprecated protected final void performRequestAsyncAndParseEntity(Req request, @@ -1942,9 +1930,9 @@ public void onFailure(Exception exception) { } }; } - + /** - * Async request which returns empty Optionals in the case of 404s or parses entity into an Optional + * Asynchronous request which returns empty {@link Optional}s in the case of 404s or parses entity into an Optional */ protected final void performRequestAsyncAndParseOptionalEntity(Req request, CheckedFunction requestConverter, @@ -1964,11 +1952,11 @@ protected final void performRequestAsyncAndParse return; } req.setOptions(options); - ResponseListener responseListener = wrapResponseListener404sOptional(response -> parseEntity(response.getEntity(), + ResponseListener responseListener = wrapResponseListener404sOptional(response -> parseEntity(response.getEntity(), entityParser), listener); - client.performRequestAsync(req, responseListener); - } - + client.performRequestAsync(req, responseListener); + } + final ResponseListener wrapResponseListener404sOptional(CheckedFunction responseConverter, ActionListener> actionListener) { return new ResponseListener() { @@ -1997,7 +1985,7 @@ public void onFailure(Exception exception) { } } }; - } + } /** * Converts a {@link ResponseException} obtained from the low level REST client into an {@link ElasticsearchException}. diff --git a/docs/java-rest/high-level/execution.asciidoc b/docs/java-rest/high-level/execution.asciidoc index 4dfb11e196d9e..1028d9b6975c7 100644 --- a/docs/java-rest/high-level/execution.asciidoc +++ b/docs/java-rest/high-level/execution.asciidoc @@ -18,6 +18,15 @@ for the +{response}+ to be returned before continuing with code execution: include-tagged::{doc-tests-file}[{api}-execute] -------------------------------------------------- +Synchronous calls may throw an `IOException` in case of either failing to +parse the REST response in the high-level REST client, the request times out +or similar cases where there is no response coming back from the server. + +In cases where the server returns a `4xx` or `5xx` error code, the high-level +client tries to parse the response body error details instead and then throws +a generic `ElasticsearchException` and adds the original `ResponseException` as a +suppressed exception to it. + [id="{upid}-{api}-async"] ==== Asynchronous Execution @@ -36,7 +45,8 @@ the execution completes The asynchronous method does not block and returns immediately. Once it is completed the `ActionListener` is called back using the `onResponse` method if the execution successfully completed or using the `onFailure` method if -it failed. +it failed. Failure scenarios and expected exceptions are the same as in the +synchronous execution case. A typical listener for +{api}+ looks like: @@ -45,4 +55,4 @@ A typical listener for +{api}+ looks like: include-tagged::{doc-tests-file}[{api}-execute-listener] -------------------------------------------------- <1> Called when the execution is successfully completed. -<2> Called when the whole +{request}+ fails. \ No newline at end of file +<2> Called when the whole +{request}+ fails. From d0921f3c21b94b2e9d660fb09cc09cab898f44b5 Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Tue, 4 Dec 2018 11:15:34 -0500 Subject: [PATCH 113/115] Always set soft-deletes field of IndexWriterConfig (#36196) Today we configure the soft-deletes field iff soft-deletes enabled. Although this choice was correct, it prevents an engine with soft-deletes disabled from opening a Lucene index with soft-deletes. Moreover, this change should not have any side-effect if a Lucene index does not have any soft-deletes. Relates #36141 --- .../index/engine/InternalEngine.java | 3 +- .../index/engine/InternalEngineTests.java | 28 ++++++++ .../index/engine/EngineTestCase.java | 66 ++++++++++++++++--- 3 files changed, 88 insertions(+), 9 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java b/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java index ea2f90ee458d0..b92d447dd29db 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java +++ b/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java @@ -2189,8 +2189,9 @@ private IndexWriterConfig getIndexWriterConfig() { // Give us the opportunity to upgrade old segments while performing // background merges MergePolicy mergePolicy = config().getMergePolicy(); + // always configure soft-deletes field so an engine with soft-deletes disabled can open a Lucene index with soft-deletes. + iwc.setSoftDeletesField(Lucene.SOFT_DELETES_FIELD); if (softDeleteEnabled) { - iwc.setSoftDeletesField(Lucene.SOFT_DELETES_FIELD); mergePolicy = new RecoverySourcePruneMergePolicy(SourceFieldMapper.RECOVERY_SOURCE_NAME, softDeletesPolicy::getRetentionQuery, new SoftDeletesRetentionMergePolicy(Lucene.SOFT_DELETES_FIELD, softDeletesPolicy::getRetentionQuery, mergePolicy)); } diff --git a/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java b/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java index 8072e69520d64..412bd2f122011 100644 --- a/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java +++ b/server/src/test/java/org/elasticsearch/index/engine/InternalEngineTests.java @@ -5471,6 +5471,34 @@ public void testRebuildLocalCheckpointTracker() throws Exception { } } + public void testOpenSoftDeletesIndexWithSoftDeletesDisabled() throws Exception { + try (Store store = createStore()) { + Path translogPath = createTempDir(); + final AtomicLong globalCheckpoint = new AtomicLong(SequenceNumbers.NO_OPS_PERFORMED); + final IndexSettings softDeletesEnabled = IndexSettingsModule.newIndexSettings( + IndexMetaData.builder(defaultSettings.getIndexMetaData()).settings(Settings.builder(). + put(defaultSettings.getSettings()).put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true)).build()); + final List docs; + try (InternalEngine engine = createEngine( + config(softDeletesEnabled, store, translogPath, newMergePolicy(), null, null, globalCheckpoint::get))) { + List ops = generateReplicaHistory(between(1, 100), randomBoolean()); + applyOperations(engine, ops); + globalCheckpoint.set(randomLongBetween(globalCheckpoint.get(), engine.getLocalCheckpoint())); + engine.syncTranslog(); + engine.flush(); + docs = getDocIds(engine, true); + } + final IndexSettings softDeletesDisabled = IndexSettingsModule.newIndexSettings( + IndexMetaData.builder(defaultSettings.getIndexMetaData()).settings(Settings.builder() + .put(defaultSettings.getSettings()).put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), false)).build()); + EngineConfig config = config(softDeletesDisabled, store, translogPath, newMergePolicy(), null, null, globalCheckpoint::get); + trimUnsafeCommits(config); + try (InternalEngine engine = createEngine(config)) { + assertThat(getDocIds(engine, true), equalTo(docs)); + } + } + } + static void trimUnsafeCommits(EngineConfig config) throws IOException { final Store store = config.getStore(); final TranslogConfig translogConfig = config.getTranslogConfig(); diff --git a/test/framework/src/main/java/org/elasticsearch/index/engine/EngineTestCase.java b/test/framework/src/main/java/org/elasticsearch/index/engine/EngineTestCase.java index f1217bd5b0dfe..8305901304f9d 100644 --- a/test/framework/src/main/java/org/elasticsearch/index/engine/EngineTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/index/engine/EngineTestCase.java @@ -51,6 +51,7 @@ import org.elasticsearch.cluster.routing.AllocationId; import org.elasticsearch.common.CheckedFunction; import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.Randomness; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.bytes.BytesReference; @@ -717,6 +718,32 @@ public static List generateSingleDocHistory( return ops; } + public List generateReplicaHistory(int numOps, boolean allowGapInSeqNo) { + long seqNo = 0; + List operations = new ArrayList<>(numOps); + for (int i = 0; i < numOps; i++) { + String id = Integer.toString(between(1, 100)); + final ParsedDocument doc = EngineTestCase.createParsedDoc(id, null); + if (randomBoolean()) { + operations.add(new Engine.Index(EngineTestCase.newUid(doc), doc, seqNo, primaryTerm.get(), + i, VersionType.EXTERNAL, Engine.Operation.Origin.REPLICA, threadPool.relativeTimeInMillis(), + -1, true)); + } else if (randomBoolean()) { + operations.add(new Engine.Delete(doc.type(), doc.id(), EngineTestCase.newUid(doc), seqNo, primaryTerm.get(), + i, VersionType.EXTERNAL, Engine.Operation.Origin.REPLICA, threadPool.relativeTimeInMillis())); + } else { + operations.add(new Engine.NoOp(seqNo, primaryTerm.get(), Engine.Operation.Origin.REPLICA, + threadPool.relativeTimeInMillis(), "test-" + i)); + } + seqNo++; + if (allowGapInSeqNo && rarely()) { + seqNo++; + } + } + Randomness.shuffle(operations); + return operations; + } + public static void assertOpsOnReplica( final List ops, final InternalEngine replicaEngine, @@ -801,14 +828,7 @@ public static void concurrentlyApplyOps(List ops, InternalEngi int docOffset; while ((docOffset = offset.incrementAndGet()) < ops.size()) { try { - final Engine.Operation op = ops.get(docOffset); - if (op instanceof Engine.Index) { - engine.index((Engine.Index) op); - } else if (op instanceof Engine.Delete){ - engine.delete((Engine.Delete) op); - } else { - engine.noOp((Engine.NoOp) op); - } + applyOperation(engine, ops.get(docOffset)); if ((docOffset + 1) % 4 == 0) { engine.refresh("test"); } @@ -827,6 +847,36 @@ public static void concurrentlyApplyOps(List ops, InternalEngi } } + public static void applyOperations(Engine engine, List operations) throws IOException { + for (Engine.Operation operation : operations) { + applyOperation(engine, operation); + if (randomInt(100) < 10) { + engine.refresh("test"); + } + if (rarely()) { + engine.flush(); + } + } + } + + public static Engine.Result applyOperation(Engine engine, Engine.Operation operation) throws IOException { + final Engine.Result result; + switch (operation.operationType()) { + case INDEX: + result = engine.index((Engine.Index) operation); + break; + case DELETE: + result = engine.delete((Engine.Delete) operation); + break; + case NO_OP: + result = engine.noOp((Engine.NoOp) operation); + break; + default: + throw new IllegalStateException("No operation defined for [" + operation + "]"); + } + return result; + } + /** * Gets a collection of tuples of docId, sequence number, and primary term of all live documents in the provided engine. */ From 2891e05b8a28af986377cefda059d60d60b0e45f Mon Sep 17 00:00:00 2001 From: Yogesh Gaikwad <902768+bizybot@users.noreply.github.com> Date: Wed, 5 Dec 2018 16:44:49 +1100 Subject: [PATCH 114/115] [Kerberos] Find if port is available before using it for Kdc server (#36192) If the randomly selected port was already in use the Kerberos tests would fail. This commit adds check to see if the network port is available and if not continue to find one for KDC server. If it does not find port after 100 retries it throws an exception. Closes #34261 --- .../authc/kerberos/SimpleKdcLdapServer.java | 33 +++++++++++++++---- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/x-pack/qa/evil-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/SimpleKdcLdapServer.java b/x-pack/qa/evil-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/SimpleKdcLdapServer.java index 13601d2fe202f..8888ce33be57f 100644 --- a/x-pack/qa/evil-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/SimpleKdcLdapServer.java +++ b/x-pack/qa/evil-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/SimpleKdcLdapServer.java @@ -13,7 +13,6 @@ import org.apache.kerby.kerberos.kerb.client.KrbConfig; import org.apache.kerby.kerberos.kerb.server.KdcConfigKey; import org.apache.kerby.kerberos.kerb.server.SimpleKdcServer; -import org.apache.kerby.util.NetworkUtil; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.elasticsearch.ExceptionsHelper; @@ -22,6 +21,9 @@ import org.elasticsearch.test.ESTestCase; import java.io.IOException; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.ServerSocket; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -31,6 +33,8 @@ import java.util.Locale; import java.util.concurrent.TimeUnit; +import javax.net.ServerSocketFactory; + /** * Utility wrapper around Apache {@link SimpleKdcServer} backed by Unboundid * {@link InMemoryDirectoryServer}.
@@ -127,14 +131,14 @@ private void prepareKdcServerAndStart() throws Exception { simpleKdc.setWorkDir(workDir.toFile()); simpleKdc.setKdcHost(host); simpleKdc.setKdcRealm(realm); - if (kdcPort == 0) { - kdcPort = NetworkUtil.getServerPort(); - } if (transport != null) { - if (transport.trim().equals("TCP")) { + if (kdcPort == 0) { + kdcPort = getServerPort(transport); + } + if (transport.trim().equalsIgnoreCase("TCP")) { simpleKdc.setKdcTcpPort(kdcPort); simpleKdc.setAllowUdp(false); - } else if (transport.trim().equals("UDP")) { + } else if (transport.trim().equalsIgnoreCase("UDP")) { simpleKdc.setKdcUdpPort(kdcPort); simpleKdc.setAllowTcp(false); } else { @@ -221,4 +225,21 @@ public Void run() throws Exception { logger.info("SimpleKdcServer stoppped."); } + private static int getServerPort(String transport) { + if (transport != null && transport.trim().equalsIgnoreCase("TCP")) { + try (ServerSocket serverSocket = ServerSocketFactory.getDefault().createServerSocket(0, 1, + InetAddress.getByName("127.0.0.1"))) { + return serverSocket.getLocalPort(); + } catch (Exception ex) { + throw new RuntimeException("Failed to get a TCP server socket point"); + } + } else if (transport != null && transport.trim().equalsIgnoreCase("UDP")) { + try (DatagramSocket socket = new DatagramSocket(0, InetAddress.getByName("127.0.0.1"))) { + return socket.getLocalPort(); + } catch (Exception ex) { + throw new RuntimeException("Failed to get a UDP server socket point"); + } + } + throw new IllegalArgumentException("Invalid transport: " + transport); + } } From 8b9b2cb4b98360411c5031e7221e9616bf977b29 Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Wed, 5 Dec 2018 08:41:27 +0100 Subject: [PATCH 115/115] [CCR] Change get autofollow patterns API response format (#36203) The current response format is: ``` { "pattern1": { ... }, "pattern2": { ... } } ``` The new format is: ``` { "patterns": [ { "name": "pattern1", "pattern": { ... } }, { "name": "pattern2", "pattern": { ... } } ] } ``` This format is more structured and more friendly for parsing and generating specs. This is a breaking change, but it is better to do this now while ccr is still a beta feature than later. Follow up from #36049 --- .../ccr/GetAutoFollowPatternResponse.java | 52 +++++++++++++------ .../GetAutoFollowPatternResponseTests.java | 28 ++++++---- .../get-auto-follow-pattern.asciidoc | 22 ++++---- .../rest-api-spec/test/ccr/auto_follow.yml | 14 ++--- .../action/GetAutoFollowPatternAction.java | 19 +++++-- 5 files changed, 90 insertions(+), 45 deletions(-) diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ccr/GetAutoFollowPatternResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ccr/GetAutoFollowPatternResponse.java index f4afb2d650e9b..ce42c98e57c41 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/ccr/GetAutoFollowPatternResponse.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ccr/GetAutoFollowPatternResponse.java @@ -19,41 +19,59 @@ package org.elasticsearch.client.ccr; +import org.elasticsearch.common.ParseField; import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.ConstructingObjectParser; import org.elasticsearch.common.xcontent.ObjectParser; import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.common.xcontent.XContentParser.Token; -import java.io.IOException; +import java.util.AbstractMap; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.NavigableMap; import java.util.Objects; +import java.util.TreeMap; +import java.util.stream.Collectors; public final class GetAutoFollowPatternResponse { - public static GetAutoFollowPatternResponse fromXContent(final XContentParser parser) throws IOException { - final Map patterns = new HashMap<>(); - for (Token token = parser.nextToken(); token != Token.END_OBJECT; token = parser.nextToken()) { - if (token == Token.FIELD_NAME) { - final String name = parser.currentName(); - final Pattern pattern = Pattern.PARSER.parse(parser, null); - patterns.put(name, pattern); - } - } - return new GetAutoFollowPatternResponse(patterns); + static final ParseField PATTERNS_FIELD = new ParseField("patterns"); + static final ParseField NAME_FIELD = new ParseField("name"); + static final ParseField PATTERN_FIELD = new ParseField("pattern"); + + private static final ConstructingObjectParser, Void> ENTRY_PARSER = new ConstructingObjectParser<>( + "get_auto_follow_pattern_response", args -> new AbstractMap.SimpleEntry<>((String) args[0], (Pattern) args[1])); + + static { + ENTRY_PARSER.declareString(ConstructingObjectParser.constructorArg(), NAME_FIELD); + ENTRY_PARSER.declareObject(ConstructingObjectParser.constructorArg(), Pattern.PARSER, PATTERN_FIELD); + } + + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + "get_auto_follow_pattern_response", args -> { + @SuppressWarnings("unchecked") + List> entries = (List>) args[0]; + return new GetAutoFollowPatternResponse(new TreeMap<>(entries.stream() + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)))); + }); + + static { + PARSER.declareObjectArray(ConstructingObjectParser.constructorArg(), ENTRY_PARSER, PATTERNS_FIELD); + } + + public static GetAutoFollowPatternResponse fromXContent(final XContentParser parser) { + return PARSER.apply(parser, null); } - private final Map patterns; + private final NavigableMap patterns; - GetAutoFollowPatternResponse(Map patterns) { - this.patterns = Collections.unmodifiableMap(patterns); + GetAutoFollowPatternResponse(NavigableMap patterns) { + this.patterns = Collections.unmodifiableNavigableMap(patterns); } - public Map getPatterns() { + public NavigableMap getPatterns() { return patterns; } diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ccr/GetAutoFollowPatternResponseTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ccr/GetAutoFollowPatternResponseTests.java index 64eb9ba4f9f75..b4a37286b4ace 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/ccr/GetAutoFollowPatternResponseTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ccr/GetAutoFollowPatternResponseTests.java @@ -27,8 +27,9 @@ import java.io.IOException; import java.util.Collections; -import java.util.HashMap; import java.util.Map; +import java.util.NavigableMap; +import java.util.TreeMap; import static org.elasticsearch.client.ccr.PutAutoFollowPatternRequest.FOLLOW_PATTERN_FIELD; import static org.elasticsearch.client.ccr.PutAutoFollowPatternRequest.LEADER_PATTERNS_FIELD; @@ -48,7 +49,7 @@ public void testFromXContent() throws IOException { private GetAutoFollowPatternResponse createTestInstance() { int numPatterns = randomIntBetween(0, 16); - Map patterns = new HashMap<>(numPatterns); + NavigableMap patterns = new TreeMap<>(); for (int i = 0; i < numPatterns; i++) { GetAutoFollowPatternResponse.Pattern pattern = new GetAutoFollowPatternResponse.Pattern( randomAlphaOfLength(4), Collections.singletonList(randomAlphaOfLength(4)), randomAlphaOfLength(4)); @@ -90,17 +91,26 @@ private GetAutoFollowPatternResponse createTestInstance() { public static void toXContent(GetAutoFollowPatternResponse response, XContentBuilder builder) throws IOException { builder.startObject(); { + builder.startArray(GetAutoFollowPatternResponse.PATTERNS_FIELD.getPreferredName()); for (Map.Entry entry : response.getPatterns().entrySet()) { - builder.startObject(entry.getKey()); - GetAutoFollowPatternResponse.Pattern pattern = entry.getValue(); - builder.field(REMOTE_CLUSTER_FIELD.getPreferredName(), pattern.getRemoteCluster()); - builder.field(LEADER_PATTERNS_FIELD.getPreferredName(), pattern.getLeaderIndexPatterns()); - if (pattern.getFollowIndexNamePattern()!= null) { - builder.field(FOLLOW_PATTERN_FIELD.getPreferredName(), pattern.getFollowIndexNamePattern()); + builder.startObject(); + { + builder.field(GetAutoFollowPatternResponse.NAME_FIELD.getPreferredName(), entry.getKey()); + builder.startObject(GetAutoFollowPatternResponse.PATTERN_FIELD.getPreferredName()); + { + GetAutoFollowPatternResponse.Pattern pattern = entry.getValue(); + builder.field(REMOTE_CLUSTER_FIELD.getPreferredName(), pattern.getRemoteCluster()); + builder.field(LEADER_PATTERNS_FIELD.getPreferredName(), pattern.getLeaderIndexPatterns()); + if (pattern.getFollowIndexNamePattern()!= null) { + builder.field(FOLLOW_PATTERN_FIELD.getPreferredName(), pattern.getFollowIndexNamePattern()); + } + entry.getValue().toXContentFragment(builder, ToXContent.EMPTY_PARAMS); + } + builder.endObject(); } - entry.getValue().toXContentFragment(builder, ToXContent.EMPTY_PARAMS); builder.endObject(); } + builder.endArray(); } builder.endObject(); } diff --git a/docs/reference/ccr/apis/auto-follow/get-auto-follow-pattern.asciidoc b/docs/reference/ccr/apis/auto-follow/get-auto-follow-pattern.asciidoc index b154f6b907e9a..19eb2b928ae07 100644 --- a/docs/reference/ccr/apis/auto-follow/get-auto-follow-pattern.asciidoc +++ b/docs/reference/ccr/apis/auto-follow/get-auto-follow-pattern.asciidoc @@ -87,15 +87,19 @@ The API returns the following result: [source,js] -------------------------------------------------- { - "my_auto_follow_pattern" : - { - "remote_cluster" : "remote_cluster", - "leader_index_patterns" : - [ - "leader_index*" - ], - "follow_index_pattern" : "{{leader_index}}-follower" - } + "patterns": [ + { + "name": "my_auto_follow_pattern", + "pattern": { + "remote_cluster" : "remote_cluster", + "leader_index_patterns" : + [ + "leader_index*" + ], + "follow_index_pattern" : "{{leader_index}}-follower" + } + } + ] } -------------------------------------------------- // TESTRESPONSE diff --git a/x-pack/plugin/ccr/qa/rest/src/test/resources/rest-api-spec/test/ccr/auto_follow.yml b/x-pack/plugin/ccr/qa/rest/src/test/resources/rest-api-spec/test/ccr/auto_follow.yml index 4d4026f46a472..ebf9176c30a91 100644 --- a/x-pack/plugin/ccr/qa/rest/src/test/resources/rest-api-spec/test/ccr/auto_follow.yml +++ b/x-pack/plugin/ccr/qa/rest/src/test/resources/rest-api-spec/test/ccr/auto_follow.yml @@ -31,15 +31,17 @@ - do: ccr.get_auto_follow_pattern: name: my_pattern - - match: { my_pattern.remote_cluster: 'local' } - - match: { my_pattern.leader_index_patterns: ['logs-*'] } - - match: { my_pattern.max_outstanding_read_requests: 2 } + - match: { patterns.0.name: 'my_pattern' } + - match: { patterns.0.pattern.remote_cluster: 'local' } + - match: { patterns.0.pattern.leader_index_patterns: ['logs-*'] } + - match: { patterns.0.pattern.max_outstanding_read_requests: 2 } - do: ccr.get_auto_follow_pattern: {} - - match: { my_pattern.remote_cluster: 'local' } - - match: { my_pattern.leader_index_patterns: ['logs-*'] } - - match: { my_pattern.max_outstanding_read_requests: 2 } + - match: { patterns.0.name: 'my_pattern' } + - match: { patterns.0.pattern.remote_cluster: 'local' } + - match: { patterns.0.pattern.leader_index_patterns: ['logs-*'] } + - match: { patterns.0.pattern.max_outstanding_read_requests: 2 } - do: ccr.delete_auto_follow_pattern: diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/GetAutoFollowPatternAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/GetAutoFollowPatternAction.java index 72618926a8940..1de530b78c703 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/GetAutoFollowPatternAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/GetAutoFollowPatternAction.java @@ -119,10 +119,21 @@ public void writeTo(StreamOutput out) throws IOException { @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); - for (Map.Entry entry : autoFollowPatterns.entrySet()) { - builder.startObject(entry.getKey()); - entry.getValue().toXContent(builder, params); - builder.endObject(); + { + builder.startArray("patterns"); + for (Map.Entry entry : autoFollowPatterns.entrySet()) { + builder.startObject(); + { + builder.field("name", entry.getKey()); + builder.startObject("pattern"); + { + entry.getValue().toXContent(builder, params); + } + builder.endObject(); + } + builder.endObject(); + } + builder.endArray(); } builder.endObject(); return builder;