mirror of
https://github.com/google/nomulus.git
synced 2025-05-01 20:47:52 +02:00
As previously written, it loaded up all history entries into memory and then processed them. This was OOMing for some registrars on sandbox who had performed a large number of testing actions, most of them long OT&E was passed. This commit changes the verify OT&E action to stream the history entries in batches, ordered by when they were made, and then terminates once all tests have passed. This prevents OOMing because only a single batch of history entries need reside in memory at once. This does necessitate the creation of a new composite Datastore index on HistoryEntry, so we'll need to run the ResaveAllHistoryEntriesAction in sandbox after this change is deployed before the new verify OT&E code will work. Note that the "history viewer" is long dead, but that the pre-existing index on HistoryEntries is still used for many other purposes. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=223163337
335 lines
14 KiB
Java
335 lines
14 KiB
Java
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package google.registry.tools.server;
|
|
|
|
import static com.google.common.base.Predicates.equalTo;
|
|
import static com.google.common.collect.ImmutableList.toImmutableList;
|
|
import static com.google.common.collect.Maps.toMap;
|
|
import static google.registry.flows.EppXmlTransformer.unmarshal;
|
|
import static google.registry.model.ofy.ObjectifyService.ofy;
|
|
import static google.registry.util.CollectionUtils.isNullOrEmpty;
|
|
import static google.registry.util.DomainNameUtils.ACE_PREFIX;
|
|
|
|
import com.google.common.base.Ascii;
|
|
import com.google.common.base.Joiner;
|
|
import com.google.common.base.Predicates;
|
|
import com.google.common.collect.HashMultiset;
|
|
import com.google.common.collect.ImmutableList;
|
|
import com.google.common.collect.Multiset;
|
|
import com.googlecode.objectify.Key;
|
|
import com.googlecode.objectify.cmd.Query;
|
|
import google.registry.flows.EppException;
|
|
import google.registry.model.domain.DomainCommand;
|
|
import google.registry.model.domain.fee.FeeCreateCommandExtension;
|
|
import google.registry.model.domain.launch.LaunchCreateExtension;
|
|
import google.registry.model.domain.secdns.SecDnsCreateExtension;
|
|
import google.registry.model.domain.secdns.SecDnsUpdateExtension;
|
|
import google.registry.model.eppinput.EppInput;
|
|
import google.registry.model.eppinput.EppInput.ResourceCommandWrapper;
|
|
import google.registry.model.host.HostCommand;
|
|
import google.registry.model.reporting.HistoryEntry;
|
|
import google.registry.model.reporting.HistoryEntry.Type;
|
|
import google.registry.request.Action;
|
|
import google.registry.request.JsonActionRunner;
|
|
import google.registry.request.JsonActionRunner.JsonAction;
|
|
import google.registry.request.auth.Auth;
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.EnumSet;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Optional;
|
|
import java.util.function.Predicate;
|
|
import java.util.stream.Collectors;
|
|
import java.util.stream.IntStream;
|
|
import java.util.stream.Stream;
|
|
import javax.inject.Inject;
|
|
|
|
/**
|
|
* A servlet that verifies a registrar's OTE status. Note that this is eventually consistent, so
|
|
* OT&E commands that have been run just previously to verification may not be picked up yet.
|
|
*/
|
|
@Action(
|
|
path = VerifyOteAction.PATH,
|
|
method = Action.Method.POST,
|
|
auth = Auth.AUTH_INTERNAL_OR_ADMIN
|
|
)
|
|
public class VerifyOteAction implements Runnable, JsonAction {
|
|
|
|
public static final String PATH = "/_dr/admin/verifyOte";
|
|
|
|
@Inject JsonActionRunner jsonActionRunner;
|
|
|
|
@Inject
|
|
VerifyOteAction() {}
|
|
|
|
@Override
|
|
public void run() {
|
|
jsonActionRunner.run(this);
|
|
}
|
|
|
|
@Override
|
|
@SuppressWarnings("unchecked")
|
|
public Map<String, Object> handleJsonRequest(Map<String, ?> json) {
|
|
final boolean summarize = Boolean.parseBoolean((String) json.get("summarize"));
|
|
return toMap(
|
|
(List<String>) json.get("registrars"), registrar -> checkRegistrar(registrar, summarize));
|
|
}
|
|
|
|
/** Checks whether the provided registrar has passed OT&E and returns relevant information. */
|
|
private String checkRegistrar(String registrarName, boolean summarize) {
|
|
HistoryEntryStats historyEntryStats =
|
|
new HistoryEntryStats().recordRegistrarHistory(registrarName);
|
|
List<String> failureMessages = historyEntryStats.findFailures();
|
|
int testsPassed = StatType.NUM_REQUIREMENTS - failureMessages.size();
|
|
String status = failureMessages.isEmpty() ? "PASS" : "FAIL";
|
|
return summarize
|
|
? String.format(
|
|
"# actions: %4d - Reqs: [%s] %2d/%2d - Overall: %s",
|
|
historyEntryStats.statCounts.size(),
|
|
historyEntryStats.toSummary(),
|
|
testsPassed,
|
|
StatType.NUM_REQUIREMENTS,
|
|
status)
|
|
: String.format(
|
|
"%s\n%s\nRequirements passed: %2d/%2d\nOverall OT&E status: %s\n",
|
|
historyEntryStats,
|
|
Joiner.on('\n').join(failureMessages),
|
|
testsPassed,
|
|
StatType.NUM_REQUIREMENTS,
|
|
status);
|
|
}
|
|
|
|
private static final Predicate<EppInput> HAS_CLAIMS_NOTICE =
|
|
eppInput -> {
|
|
Optional<LaunchCreateExtension> launchCreate =
|
|
eppInput.getSingleExtension(LaunchCreateExtension.class);
|
|
return launchCreate.isPresent() && launchCreate.get().getNotice() != null;
|
|
};
|
|
|
|
private static final Predicate<EppInput> HAS_SEC_DNS =
|
|
eppInput ->
|
|
(eppInput.getSingleExtension(SecDnsCreateExtension.class).isPresent())
|
|
|| (eppInput.getSingleExtension(SecDnsUpdateExtension.class).isPresent());
|
|
private static final Predicate<EppInput> IS_SUNRISE =
|
|
eppInput -> {
|
|
Optional<LaunchCreateExtension> launchCreate =
|
|
eppInput.getSingleExtension(LaunchCreateExtension.class);
|
|
return launchCreate.isPresent() && !isNullOrEmpty(launchCreate.get().getSignedMarks());
|
|
};
|
|
|
|
private static final Predicate<EppInput> IS_IDN =
|
|
eppInput ->
|
|
((DomainCommand.Create)
|
|
((ResourceCommandWrapper) eppInput.getCommandWrapper().getCommand())
|
|
.getResourceCommand())
|
|
.getFullyQualifiedDomainName()
|
|
.startsWith(ACE_PREFIX);
|
|
private static final Predicate<EppInput> IS_SUBORDINATE =
|
|
eppInput ->
|
|
!isNullOrEmpty(
|
|
((HostCommand.Create)
|
|
((ResourceCommandWrapper) eppInput.getCommandWrapper().getCommand())
|
|
.getResourceCommand())
|
|
.getInetAddresses());
|
|
/** Enum defining the distinct statistics (types of registrar actions) to record. */
|
|
public enum StatType {
|
|
CONTACT_CREATES(0, equalTo(Type.CONTACT_CREATE)),
|
|
CONTACT_DELETES(0, equalTo(Type.CONTACT_DELETE)),
|
|
CONTACT_TRANSFER_APPROVES(0, equalTo(Type.CONTACT_TRANSFER_APPROVE)),
|
|
CONTACT_TRANSFER_CANCELS(0, equalTo(Type.CONTACT_TRANSFER_CANCEL)),
|
|
CONTACT_TRANSFER_REJECTS(0, equalTo(Type.CONTACT_TRANSFER_REJECT)),
|
|
CONTACT_TRANSFER_REQUESTS(0, equalTo(Type.CONTACT_TRANSFER_REQUEST)),
|
|
CONTACT_UPDATES(0, equalTo(Type.CONTACT_UPDATE)),
|
|
DOMAIN_APPLICATION_CREATES(0, equalTo(Type.DOMAIN_APPLICATION_CREATE)),
|
|
DOMAIN_APPLICATION_CREATES_LANDRUSH(
|
|
0, equalTo(Type.DOMAIN_APPLICATION_CREATE), IS_SUNRISE.negate()),
|
|
DOMAIN_APPLICATION_CREATES_SUNRISE(0, equalTo(Type.DOMAIN_APPLICATION_CREATE), IS_SUNRISE),
|
|
DOMAIN_APPLICATION_DELETES(0, equalTo(Type.DOMAIN_APPLICATION_DELETE)),
|
|
DOMAIN_APPLICATION_UPDATES(0, equalTo(Type.DOMAIN_APPLICATION_UPDATE)),
|
|
DOMAIN_AUTORENEWS(0, equalTo(Type.DOMAIN_AUTORENEW)),
|
|
DOMAIN_CREATES(0, equalTo(Type.DOMAIN_CREATE)),
|
|
DOMAIN_CREATES_ASCII(1, equalTo(Type.DOMAIN_CREATE), IS_IDN.negate()),
|
|
DOMAIN_CREATES_IDN(1, equalTo(Type.DOMAIN_CREATE), IS_IDN),
|
|
DOMAIN_CREATES_START_DATE_SUNRISE(1, equalTo(Type.DOMAIN_CREATE), IS_SUNRISE),
|
|
DOMAIN_CREATES_WITH_CLAIMS_NOTICE(1, equalTo(Type.DOMAIN_CREATE), HAS_CLAIMS_NOTICE),
|
|
DOMAIN_CREATES_WITH_FEE(
|
|
1,
|
|
equalTo(Type.DOMAIN_CREATE),
|
|
eppInput -> eppInput.getSingleExtension(FeeCreateCommandExtension.class).isPresent()),
|
|
DOMAIN_CREATES_WITH_SEC_DNS(1, equalTo(Type.DOMAIN_CREATE), HAS_SEC_DNS),
|
|
DOMAIN_CREATES_WITHOUT_SEC_DNS(0, equalTo(Type.DOMAIN_CREATE), HAS_SEC_DNS.negate()),
|
|
DOMAIN_DELETES(2, equalTo(Type.DOMAIN_DELETE)),
|
|
DOMAIN_RENEWS(0, equalTo(Type.DOMAIN_RENEW)),
|
|
DOMAIN_RESTORES(1, equalTo(Type.DOMAIN_RESTORE)),
|
|
DOMAIN_TRANSFER_APPROVES(1, equalTo(Type.DOMAIN_TRANSFER_APPROVE)),
|
|
DOMAIN_TRANSFER_CANCELS(1, equalTo(Type.DOMAIN_TRANSFER_CANCEL)),
|
|
DOMAIN_TRANSFER_REJECTS(1, equalTo(Type.DOMAIN_TRANSFER_REJECT)),
|
|
DOMAIN_TRANSFER_REQUESTS(1, equalTo(Type.DOMAIN_TRANSFER_REQUEST)),
|
|
DOMAIN_UPDATES(0, equalTo(Type.DOMAIN_UPDATE)),
|
|
DOMAIN_UPDATES_WITH_SEC_DNS(1, equalTo(Type.DOMAIN_UPDATE), HAS_SEC_DNS),
|
|
DOMAIN_UPDATES_WITHOUT_SEC_DNS(0, equalTo(Type.DOMAIN_UPDATE), HAS_SEC_DNS.negate()),
|
|
HOST_CREATES(0, equalTo(Type.HOST_CREATE)),
|
|
HOST_CREATES_EXTERNAL(0, equalTo(Type.HOST_CREATE), IS_SUBORDINATE.negate()),
|
|
HOST_CREATES_SUBORDINATE(1, equalTo(Type.HOST_CREATE), IS_SUBORDINATE),
|
|
HOST_DELETES(1, equalTo(Type.HOST_DELETE)),
|
|
HOST_UPDATES(1, equalTo(Type.HOST_UPDATE)),
|
|
UNCLASSIFIED_FLOWS(0, Predicates.alwaysFalse());
|
|
|
|
/** The number of StatTypes with a non-zero requirement. */
|
|
private static final int NUM_REQUIREMENTS =
|
|
(int) Stream.of(values()).filter(statType -> statType.requirement > 0).count();
|
|
|
|
/** Required number of times registrars must complete this action. */
|
|
final int requirement;
|
|
|
|
/** Filter to check the HistoryEntry Type */
|
|
@SuppressWarnings("ImmutableEnumChecker") // Predicates are immutable.
|
|
private final Predicate<HistoryEntry.Type> typeFilter;
|
|
|
|
/** Optional filter on the EppInput. */
|
|
@SuppressWarnings("ImmutableEnumChecker") // Predicates are immutable.
|
|
private final Optional<Predicate<EppInput>> eppInputFilter;
|
|
|
|
StatType(int requirement, Predicate<HistoryEntry.Type> typeFilter) {
|
|
this(requirement, typeFilter, null);
|
|
}
|
|
|
|
StatType(
|
|
int requirement,
|
|
Predicate<HistoryEntry.Type> typeFilter,
|
|
Predicate<EppInput> eppInputFilter) {
|
|
this.requirement = requirement;
|
|
this.typeFilter = typeFilter;
|
|
if (eppInputFilter == null) {
|
|
this.eppInputFilter = Optional.empty();
|
|
} else {
|
|
this.eppInputFilter = Optional.of(eppInputFilter);
|
|
}
|
|
}
|
|
|
|
/** Returns a more human-readable translation of the enum constant. */
|
|
String description() {
|
|
return Ascii.toLowerCase(this.name().replace('_', ' '));
|
|
}
|
|
|
|
/**
|
|
* Check if the {@link HistoryEntry} type matches as well as the {@link EppInput} if supplied.
|
|
*/
|
|
boolean matches(HistoryEntry.Type historyType, Optional<EppInput> eppInput) {
|
|
if (eppInputFilter.isPresent() && eppInput.isPresent()) {
|
|
return typeFilter.test(historyType) && eppInputFilter.get().test(eppInput.get());
|
|
} else {
|
|
return typeFilter.test(historyType);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Class to represent stats derived from HistoryEntry objects on actions taken by registrars. */
|
|
static class HistoryEntryStats {
|
|
|
|
/** Stores counts of how many times each action type was performed. */
|
|
Multiset<StatType> statCounts = HashMultiset.create();
|
|
|
|
/**
|
|
* Records data in the passed historyEntryStats object on what actions have been performed by
|
|
* the four numbered OT&E variants of the registrar name.
|
|
*
|
|
* <p>Stops when it notices that all tests have passed.
|
|
*/
|
|
HistoryEntryStats recordRegistrarHistory(String registrarName) {
|
|
ImmutableList<String> clientIds =
|
|
IntStream.rangeClosed(1, 4)
|
|
.mapToObj(i -> String.format("%s-%d", registrarName, i))
|
|
.collect(toImmutableList());
|
|
|
|
Query<HistoryEntry> query =
|
|
ofy()
|
|
.load()
|
|
.type(HistoryEntry.class)
|
|
.filter("clientId in", clientIds)
|
|
.order("modificationTime");
|
|
for (HistoryEntry historyEntry : query) {
|
|
try {
|
|
record(historyEntry);
|
|
} catch (EppException e) {
|
|
throw new RuntimeException("Couldn't parse history entry " + Key.create(historyEntry), e);
|
|
}
|
|
// Break out early if all tests were passed.
|
|
if (wereAllTestsPassed()) {
|
|
break;
|
|
}
|
|
}
|
|
return this;
|
|
}
|
|
|
|
/** Interprets the data in the provided HistoryEntry and increments counters. */
|
|
void record(final HistoryEntry historyEntry) throws EppException {
|
|
byte[] xmlBytes = historyEntry.getXmlBytes();
|
|
// xmlBytes can be null on contact create and update for safe-harbor compliance.
|
|
final Optional<EppInput> eppInput =
|
|
(xmlBytes == null) ? Optional.empty() : Optional.of(unmarshal(EppInput.class, xmlBytes));
|
|
if (!statCounts.addAll(
|
|
EnumSet.allOf(StatType.class).stream()
|
|
.filter(statType -> statType.matches(historyEntry.getType(), eppInput))
|
|
.collect(toImmutableList()))) {
|
|
statCounts.add(StatType.UNCLASSIFIED_FLOWS);
|
|
}
|
|
}
|
|
|
|
boolean wereAllTestsPassed() {
|
|
return Arrays.stream(StatType.values()).allMatch(s -> statCounts.count(s) >= s.requirement);
|
|
}
|
|
|
|
/**
|
|
* Returns a list of failure messages describing any cases where the passed stats fail to meet
|
|
* the required thresholds, or the empty list if all requirements are met.
|
|
*/
|
|
List<String> findFailures() {
|
|
List<String> messages = new ArrayList<>();
|
|
for (StatType statType : StatType.values()) {
|
|
if (statCounts.count(statType) < statType.requirement) {
|
|
messages.add(
|
|
String.format(
|
|
"Failure: %s %s found.",
|
|
(statType.requirement == 1 ? "No" : "Not enough"), statType.description()));
|
|
}
|
|
}
|
|
return messages;
|
|
}
|
|
|
|
/** Returns a string showing all possible actions and how many times each was performed. */
|
|
@Override
|
|
public String toString() {
|
|
return String.format(
|
|
"%s\nTOTAL: %d",
|
|
EnumSet.allOf(StatType.class)
|
|
.stream()
|
|
.map(stat -> String.format("%s: %d", stat.description(), statCounts.count(stat)))
|
|
.collect(Collectors.joining("\n")),
|
|
statCounts.size());
|
|
}
|
|
|
|
/** Returns a string showing the results of each test, one character per test. */
|
|
String toSummary() {
|
|
return EnumSet.allOf(StatType.class)
|
|
.stream()
|
|
.filter(statType -> statType.requirement > 0)
|
|
.sorted()
|
|
.map(statType -> (statCounts.count(statType) < statType.requirement) ? "." : "-")
|
|
.collect(Collectors.joining(""));
|
|
}
|
|
}
|
|
}
|