summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/Node.java5
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/vcmr/VCMRReport.java149
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/VCMRMaintainer.java28
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/NodeRepositoryMock.java2
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/VCMRMaintainerTest.java26
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();