diff options
author | Ola Aunrønning <olaa@verizonmedia.com> | 2021-05-20 15:11:00 +0200 |
---|---|---|
committer | Ola Aunrønning <olaa@verizonmedia.com> | 2021-05-20 15:22:51 +0200 |
commit | c723d3178323e7b86d099ceaf80a0546df59918d (patch) | |
tree | 59d9becf81eeccb234b088911543c137b4c07b3e | |
parent | 865814b367fed14121415dd994264f481d351ca4 (diff) |
Write VCMR reports to node repo
5 files changed, 206 insertions, 4 deletions
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/Node.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/Node.java index 0f9e12d8cf2..5f46b949844 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/Node.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/Node.java @@ -548,6 +548,11 @@ public class Node { return this; } + public Builder reports(Map<String, JsonNode> reports) { + this.reports = reports; + return this; + } + public Node build() { return new Node(hostname, parentHostname, state, type, resources, owner, currentVersion, wantedVersion, currentOsVersion, wantedOsVersion, currentFirmwareCheck, wantedFirmwareCheck, serviceState, diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/vcmr/VCMRReport.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/vcmr/VCMRReport.java new file mode 100644 index 00000000000..33d10083b63 --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/vcmr/VCMRReport.java @@ -0,0 +1,149 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.controller.api.integration.vcmr; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.yahoo.vespa.hosted.controller.api.integration.configserver.Node; + +import java.time.ZonedDateTime; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import static com.yahoo.yolean.Exceptions.uncheck; + +/** + * @author olaa + * + * Node repository report containing list of upcoming VCMRs impacting a node + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class VCMRReport { + + private static final String REPORT_ID = "vcmr"; + private static final ObjectMapper objectMapper = new ObjectMapper() + .registerModule(new JavaTimeModule()); + + private Set<VCMR> vcmrs; + + public VCMRReport() { + this(new HashSet<>()); + } + + public VCMRReport(Set<VCMR> vcmrs) { + this.vcmrs = vcmrs; + } + + public Set<VCMR> getVcmrs() { + return vcmrs; + } + + /** + * @return true if list of VCMRs is changed + */ + public boolean addVcmr(String id, ZonedDateTime plannedStartTime, ZonedDateTime plannedEndtime) { + var vcmr = new VCMR(id, plannedStartTime, plannedEndtime); + if (vcmrs.contains(vcmr)) + return false; + + // Remove to catch any changes in start/end time + removeVcmr(id); + return vcmrs.add(vcmr); + } + + public boolean removeVcmr(String id) { + return vcmrs.removeIf(vcmr -> id.equals(vcmr.getId())); + } + + public static String getReportId() { + return REPORT_ID; + } + + /** + * Serialization functions - mapped to {@link Node#reports()} + */ + public static VCMRReport fromReports(Map<String, JsonNode> reports) { + var serialized = reports.get(REPORT_ID); + if (serialized == null) + return new VCMRReport(); + + var typeRef = new TypeReference<Set<VCMR>>() {}; + var vcmrs = uncheck(() -> objectMapper.readValue(objectMapper.treeAsTokens(serialized), typeRef)); + return new VCMRReport(vcmrs); + } + + /** + * Set report to 'null' if list is empty - clearing the report + * See NodePatcher in node-repository + */ + public Map<String, JsonNode> toNodeReports() { + Map<String, JsonNode> reports = new HashMap<>(); + JsonNode jsonNode = vcmrs.isEmpty() ? + null : uncheck(() -> objectMapper.valueToTree(vcmrs)); + reports.put(REPORT_ID, jsonNode); + return reports; + } + + @Override + public String toString() { + return "VCMRReport{" + vcmrs + "}"; + } + + public static class VCMR { + + private String id; + private ZonedDateTime plannedStartTime; + private ZonedDateTime plannedEndTime; + + VCMR(@JsonProperty("id") String id, + @JsonProperty("plannedStartTime") ZonedDateTime plannedStartTime, + @JsonProperty("plannedEndTime") ZonedDateTime plannedEndTime) { + this.id = id; + this.plannedStartTime = plannedStartTime; + this.plannedEndTime = plannedEndTime; + } + + public String getId() { + return id; + } + + public ZonedDateTime getPlannedStartTime() { + return plannedStartTime; + } + + public ZonedDateTime getPlannedEndTime() { + return plannedEndTime; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + VCMR vcmr = (VCMR) o; + return Objects.equals(id, vcmr.id) && + Objects.equals(plannedStartTime, vcmr.plannedStartTime) && + Objects.equals(plannedEndTime, vcmr.plannedEndTime); + } + + @Override + public int hashCode() { + return Objects.hash(id, plannedStartTime, plannedEndTime); + } + + @Override + public String toString() { + return "VCMR{" + + "id='" + id + '\'' + + ", plannedStartTime=" + plannedStartTime + + ", plannedEndTime=" + plannedEndTime + + '}'; + } + } + +} diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/VCMRMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/VCMRMaintainer.java index 111a332bc81..fedf3d90760 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/VCMRMaintainer.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/VCMRMaintainer.java @@ -15,6 +15,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.vcmr.ChangeRequest.Impa import com.yahoo.vespa.hosted.controller.api.integration.vcmr.ChangeRequestClient; import com.yahoo.vespa.hosted.controller.api.integration.vcmr.HostAction; import com.yahoo.vespa.hosted.controller.api.integration.vcmr.HostAction.State; +import com.yahoo.vespa.hosted.controller.api.integration.vcmr.VCMRReport; import com.yahoo.vespa.hosted.controller.api.integration.vcmr.VespaChangeRequest; import com.yahoo.vespa.hosted.controller.api.integration.vcmr.VespaChangeRequest.Status; import com.yahoo.vespa.hosted.controller.persistence.CuratorDb; @@ -144,12 +145,15 @@ public class VCMRMaintainer extends ControllerMaintainer { if (changeRequest.getChangeRequestSource().isClosed()) { logger.fine(() -> changeRequest.getChangeRequestSource().getId() + " is closed, recycling " + node.hostname()); recycleNode(changeRequest.getZoneId(), node, hostAction); + removeReport(changeRequest, node); return hostAction.withState(State.COMPLETE); } if (isLowImpact(changeRequest)) return hostAction; + addReport(changeRequest, node); + if (isPostponed(changeRequest, hostAction)) { logger.fine(() -> changeRequest.getChangeRequestSource().getId() + " is postponed, recycling " + node.hostname()); recycleNode(changeRequest.getZoneId(), node, hostAction); @@ -285,4 +289,28 @@ public class VCMRMaintainer extends ControllerMaintainer { logger.info("Approving " + changeRequest.getChangeRequestSource().getId()); changeRequestClient.approveChangeRequest(changeRequest); } + + private void removeReport(VespaChangeRequest changeRequest, Node node) { + var report = VCMRReport.fromReports(node.reports()); + + if (report.removeVcmr(changeRequest.getChangeRequestSource().getId())) { + updateReport(changeRequest.getZoneId(), node, report); + } + } + + private void addReport(VespaChangeRequest changeRequest, Node node) { + var report = VCMRReport.fromReports(node.reports()); + + var source = changeRequest.getChangeRequestSource(); + if (report.addVcmr(source.getId(), source.getPlannedStartTime(), source.getPlannedEndTime())) { + updateReport(changeRequest.getZoneId(), node, report); + } + } + + private void updateReport(ZoneId zoneId, Node node, VCMRReport report) { + logger.info(String.format("Updating report for %s: %s", node.hostname(), report)); + var newNode = new NodeRepositoryNode(); + newNode.setReports(report.toNodeReports()); + nodeRepository.patchNode(zoneId, node.hostname().value(), newNode); + } } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/NodeRepositoryMock.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/NodeRepositoryMock.java index fe241976d13..afb56f10c38 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/NodeRepositoryMock.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/NodeRepositoryMock.java @@ -292,6 +292,8 @@ public class NodeRepositoryMock implements NodeRepository { newNode.modelName(node.getModelName()); if (node.getWantToRetire() != null) newNode.wantToRetire(node.getWantToRetire()); + if (!node.getReports().isEmpty()) + newNode.reports(node.getReports()); putNodes(zoneId, newNode.build()); } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/VCMRMaintainerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/VCMRMaintainerTest.java index c2983b6343d..16ed6b7ef98 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/VCMRMaintainerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/VCMRMaintainerTest.java @@ -9,6 +9,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.vcmr.ChangeRequest; import com.yahoo.vespa.hosted.controller.api.integration.vcmr.ChangeRequestSource; import com.yahoo.vespa.hosted.controller.api.integration.vcmr.HostAction; import com.yahoo.vespa.hosted.controller.api.integration.vcmr.HostAction.State; +import com.yahoo.vespa.hosted.controller.api.integration.vcmr.VCMRReport; import com.yahoo.vespa.hosted.controller.api.integration.vcmr.VespaChangeRequest; import com.yahoo.vespa.hosted.controller.api.integration.vcmr.VespaChangeRequest.Status; import com.yahoo.vespa.hosted.controller.integration.NodeRepositoryMock; @@ -39,22 +40,32 @@ public class VCMRMaintainerTest { public void setup() { tester = new ControllerTester(); maintainer = new VCMRMaintainer(tester.controller(), Duration.ofMinutes(1)); - nodeRepo = tester.serviceRegistry().configServer().nodeRepository(); + nodeRepo = tester.serviceRegistry().configServer().nodeRepository().allowPatching(true); } @Test public void recycle_hosts_after_completion() { + var vcmrReport = new VCMRReport(); + vcmrReport.addVcmr("id123", ZonedDateTime.now(), ZonedDateTime.now()); var parkedNode = createNode(host1, NodeType.host, Node.State.parked, true); var failedNode = createNode(host2, NodeType.host, Node.State.failed, false); + parkedNode = new Node.Builder(parkedNode) + .reports(vcmrReport.toNodeReports()) + .build(); + nodeRepo.putNodes(zoneId, List.of(parkedNode, failedNode)); tester.curator().writeChangeRequest(canceledChangeRequest()); maintainer.maintain(); - // Only the parked node is recycled + // Only the parked node is recycled, VCMR report is cleared var nodeList = nodeRepo.list(zoneId, List.of(host1, host2)); assertEquals(Node.State.dirty, nodeList.get(0).state()); assertEquals(Node.State.failed, nodeList.get(1).state()); + + var report = nodeList.get(0).reports(); + assertNull(report.get(VCMRReport.getReportId())); + var writtenChangeRequest = tester.curator().readChangeRequest(changeRequestId).get(); assertEquals(Status.COMPLETED, writtenChangeRequest.getStatus()); } @@ -82,7 +93,7 @@ public class VCMRMaintainerTest { var activeNode = createNode(host1, NodeType.host, Node.State.active, false); var failedNode = createNode(host2, NodeType.host, Node.State.failed, false); nodeRepo.putNodes(zoneId, List.of(activeNode, failedNode)); - nodeRepo.allowPatching(true).hasSpareCapacity(true); + nodeRepo.hasSpareCapacity(true); tester.curator().writeChangeRequest(startingChangeRequest()); maintainer.maintain(); @@ -150,6 +161,13 @@ public class VCMRMaintainerTest { var approvedChangeRequests = tester.serviceRegistry().changeRequestClient().getApprovedChangeRequests(); assertEquals(1, approvedChangeRequests.size()); + + activeNode = nodeRepo.list(zoneId, List.of(host2)).get(0); + var report = VCMRReport.fromReports(activeNode.reports()); + var reportAdded = report.getVcmrs().stream() + .filter(vcmr -> vcmr.getId().equals(changeRequestId)) + .count() == 1; + assertTrue(reportAdded); } @Test @@ -157,7 +175,7 @@ public class VCMRMaintainerTest { var parkedNode = createNode(host1, NodeType.host, Node.State.parked, false); var retiringNode = createNode(host2, NodeType.host, Node.State.active, true); nodeRepo.putNodes(zoneId, List.of(parkedNode, retiringNode)); - nodeRepo.allowPatching(true).hasSpareCapacity(true); + nodeRepo.hasSpareCapacity(true); tester.curator().writeChangeRequest(postponedChangeRequest()); maintainer.maintain(); |