
com.yahoo.vespa.hosted.controller.restapi.changemanagement.ChangeManagementApiHandler Maven / Gradle / Ivy
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.restapi.changemanagement;
import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.HostName;
import com.yahoo.config.provision.zone.ZoneId;
import com.yahoo.container.jdisc.HttpRequest;
import com.yahoo.container.jdisc.HttpResponse;
import com.yahoo.container.jdisc.LoggingRequestHandler;
import com.yahoo.restapi.ErrorResponse;
import com.yahoo.restapi.Path;
import com.yahoo.restapi.SlimeJsonResponse;
import com.yahoo.slime.ArrayTraverser;
import com.yahoo.slime.Cursor;
import com.yahoo.slime.Inspector;
import com.yahoo.slime.Slime;
import com.yahoo.slime.SlimeUtils;
import com.yahoo.vespa.hosted.controller.Controller;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeFilter;
import com.yahoo.vespa.hosted.controller.api.integration.vcmr.ChangeRequest;
import com.yahoo.vespa.hosted.controller.api.integration.vcmr.VespaChangeRequest;
import com.yahoo.vespa.hosted.controller.auditlog.AuditLoggingRequestHandler;
import com.yahoo.vespa.hosted.controller.maintenance.ChangeManagementAssessor;
import com.yahoo.vespa.hosted.controller.persistence.ChangeRequestSerializer;
import com.yahoo.yolean.Exceptions;
import javax.ws.rs.BadRequestException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.logging.Level;
import java.util.stream.Collectors;
public class ChangeManagementApiHandler extends AuditLoggingRequestHandler {
private final ChangeManagementAssessor assessor;
private final Controller controller;
public ChangeManagementApiHandler(LoggingRequestHandler.Context ctx, Controller controller) {
super(ctx, controller.auditLogger());
this.assessor = new ChangeManagementAssessor(controller.serviceRegistry().configServer().nodeRepository());
this.controller = controller;
}
@Override
public HttpResponse auditAndHandle(HttpRequest request) {
try {
switch (request.getMethod()) {
case GET:
return get(request);
case POST:
return post(request);
case PATCH:
return patch(request);
case DELETE:
return delete(request);
default:
return ErrorResponse.methodNotAllowed("Method '" + request.getMethod() + "' is unsupported");
}
} catch (IllegalArgumentException e) {
return ErrorResponse.badRequest(Exceptions.toMessageString(e));
} catch (RuntimeException e) {
log.log(Level.WARNING, "Unexpected error handling '" + request.getUri() + "'", e);
return ErrorResponse.internalServerError(Exceptions.toMessageString(e));
}
}
private HttpResponse get(HttpRequest request) {
Path path = new Path(request.getUri());
if (path.matches("/changemanagement/v1/assessment/{changeRequestId}")) return changeRequestAssessment(path.get("changeRequestId"));
if (path.matches("/changemanagement/v1/vcmr")) return getVCMRs();
if (path.matches("/changemanagement/v1/vcmr/{vcmrId}")) return getVCMR(path.get("vcmrId"));
return ErrorResponse.notFoundError("Nothing at " + path);
}
private HttpResponse post(HttpRequest request) {
Path path = new Path(request.getUri());
if (path.matches("/changemanagement/v1/assessment")) return doAssessment(request);
return ErrorResponse.notFoundError("Nothing at " + path);
}
private HttpResponse patch(HttpRequest request) {
Path path = new Path(request.getUri());
if (path.matches("/changemanagement/v1/vcmr/{vcmrId}")) return patchVCMR(request, path.get("vcmrId"));
return ErrorResponse.notFoundError("Nothing at " + path);
}
private HttpResponse delete(HttpRequest request) {
Path path = new Path(request.getUri());
if (path.matches("/changemanagement/v1/vcmr/{vcmrId}")) return deleteVCMR(path.get("vcmrId"));
return ErrorResponse.notFoundError("Nothing at " + path);
}
private Inspector inspectorOrThrow(HttpRequest request) {
try {
return SlimeUtils.jsonToSlime(request.getData().readAllBytes()).get();
} catch (IOException e) {
throw new BadRequestException("Failed to parse request body");
}
}
private static Inspector getInspectorFieldOrThrow(Inspector inspector, String field) {
if (!inspector.field(field).valid())
throw new BadRequestException("Field " + field + " cannot be null");
return inspector.field(field);
}
private HttpResponse changeRequestAssessment(String changeRequestId) {
var optionalChangeRequest = controller.curator().readChangeRequests()
.stream()
.filter(request -> changeRequestId.equals(request.getChangeRequestSource().getId()))
.findFirst();
if (optionalChangeRequest.isEmpty())
return ErrorResponse.notFoundError("Could not find any upcoming change requests with id " + changeRequestId);
var changeRequest = optionalChangeRequest.get();
return doAssessment(changeRequest.getImpactedHosts());
}
// The structure here should be
//
// {
// hosts: string[]
// switches: string[]
// switchInSequence: boolean
// }
//
// Only hosts is supported right now
private HttpResponse doAssessment(HttpRequest request) {
Inspector inspector = inspectorOrThrow(request);
// For now; mandatory fields
Inspector hostArray = inspector.field("hosts");
Inspector switchArray = inspector.field("switches");
// The impacted hostnames
List hostNames = new ArrayList<>();
if (hostArray.valid()) {
hostArray.traverse((ArrayTraverser) (i, host) -> hostNames.add(host.asString()));
}
if (switchArray.valid()) {
List switchNames = new ArrayList<>();
switchArray.traverse((ArrayTraverser) (i, switchName) -> switchNames.add(switchName.asString()));
hostNames.addAll(hostsOnSwitch(switchNames));
}
if (hostNames.isEmpty())
return ErrorResponse.badRequest("No prod hosts in provided host/switch list");
return doAssessment(hostNames);
}
private HttpResponse doAssessment(List hostNames) {
var zone = affectedZone(hostNames);
if (zone.isEmpty())
return ErrorResponse.notFoundError("Could not infer prod zone from host list: " + hostNames);
ChangeManagementAssessor.Assessment assessments = assessor.assessment(hostNames, zone.get());
Slime slime = new Slime();
Cursor root = slime.setObject();
// This is the main structure that might be part of something bigger later
Cursor assessmentCursor = root.setObject("assessment");
// Updated gives clue to if the assessment is old
assessmentCursor.setString("updated", "2021-03-12:12:12:12Z");
// Assessment on the cluster level
Cursor clustersCursor = assessmentCursor.setArray("clusters");
assessments.getClusterAssessments().forEach(assessment -> {
Cursor oneCluster = clustersCursor.addObject();
oneCluster.setString("app", assessment.app);
oneCluster.setString("zone", assessment.zone);
oneCluster.setString("cluster", assessment.cluster);
oneCluster.setLong("clusterSize", assessment.clusterSize);
oneCluster.setLong("clusterImpact", assessment.clusterImpact);
oneCluster.setLong("groupsTotal", assessment.groupsTotal);
oneCluster.setLong("groupsImpact", assessment.groupsImpact);
oneCluster.setString("upgradePolicy", assessment.upgradePolicy);
oneCluster.setString("suggestedAction", assessment.suggestedAction);
oneCluster.setString("impact", assessment.impact);
});
Cursor hostsCursor = assessmentCursor.setArray("hosts");
assessments.getHostAssessments().forEach(assessment -> {
Cursor hostObject = hostsCursor.addObject();
hostObject.setString("hostname", assessment.hostName);
hostObject.setString("switchName", assessment.switchName);
hostObject.setLong("numberOfChildren", assessment.numberOfChildren);
hostObject.setLong("numberOfProblematicChildren", assessment.numberOfProblematicChildren);
});
return new SlimeJsonResponse(slime);
}
private HttpResponse getVCMRs() {
var changeRequests = controller.curator().readChangeRequests();
var slime = new Slime();
var cursor = slime.setObject().setArray("vcmrs");
changeRequests.forEach(changeRequest -> {
var changeCursor = cursor.addObject();
ChangeRequestSerializer.writeChangeRequest(changeCursor, changeRequest);
});
return new SlimeJsonResponse(slime);
}
private HttpResponse getVCMR(String vcmrId) {
var changeRequest = controller.curator().readChangeRequest(vcmrId);
if (changeRequest.isEmpty()) {
return ErrorResponse.notFoundError("No VCMR with id: " + vcmrId);
}
var slime = new Slime();
var cursor = slime.setObject();
ChangeRequestSerializer.writeChangeRequest(cursor, changeRequest.get());
return new SlimeJsonResponse(slime);
}
private HttpResponse patchVCMR(HttpRequest request, String vcmrId) {
var optionalChangeRequest = controller.curator().readChangeRequest(vcmrId);
if (optionalChangeRequest.isEmpty()) {
return ErrorResponse.notFoundError("No VCMR with id: " + vcmrId);
}
var changeRequest = optionalChangeRequest.get();
var inspector = inspectorOrThrow(request);
if (inspector.field("approval").valid()) {
var approval = ChangeRequest.Approval.valueOf(inspector.field("approval").asString());
changeRequest = changeRequest.withApproval(approval);
}
if (inspector.field("actionPlan").valid()) {
var actionPlan = ChangeRequestSerializer.readHostActionPlan(inspector.field("actionPlan"));
changeRequest = changeRequest.withActionPlan(actionPlan);
}
if (inspector.field("status").valid()) {
var status = VespaChangeRequest.Status.valueOf(inspector.field("status").asString());
changeRequest = changeRequest.withStatus(status);
}
try (var lock = controller.curator().lockChangeRequests()) {
controller.curator().writeChangeRequest(changeRequest);
}
var slime = new Slime();
var cursor = slime.setObject();
ChangeRequestSerializer.writeChangeRequest(cursor, changeRequest);
return new SlimeJsonResponse(slime);
}
private HttpResponse deleteVCMR(String vcmrId) {
var changeRequest = controller.curator().readChangeRequest(vcmrId);
if (changeRequest.isEmpty()) {
return ErrorResponse.notFoundError("No VCMR with id: " + vcmrId);
}
try (var lock = controller.curator().lockChangeRequests()) {
controller.curator().deleteChangeRequest(changeRequest.get());
}
var slime = new Slime();
var cursor = slime.setObject();
ChangeRequestSerializer.writeChangeRequest(cursor, changeRequest.get());
return new SlimeJsonResponse(slime);
}
private Optional affectedZone(List hosts) {
NodeFilter affectedHosts = NodeFilter.all().hostnames(hosts.stream()
.map(HostName::from)
.collect(Collectors.toSet()));
for (var zone : getProdZones()) {
var affectedHostsInZone = controller.serviceRegistry().configServer().nodeRepository().list(zone, affectedHosts);
if (!affectedHostsInZone.isEmpty())
return Optional.of(zone);
}
return Optional.empty();
}
private List hostsOnSwitch(List switches) {
return getProdZones().stream()
.flatMap(zone -> controller.serviceRegistry().configServer().nodeRepository().list(zone, NodeFilter.all()).stream())
.filter(node -> node.switchHostname().map(switches::contains).orElse(false))
.map(node -> node.hostname().value())
.collect(Collectors.toList());
}
private List getProdZones() {
return controller.zoneRegistry()
.zones()
.reachable()
.in(Environment.prod)
.ids();
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy