From 50866f7ef328bac854bd076ca48a3aa08aa3c29c Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Wed, 8 May 2024 18:38:31 +0200 Subject: [server] Add support for CoW sessions --- .../java/org/openslx/bwlp/sat/web/WebServer.java | 223 +++++++++++++++++++-- 1 file changed, 210 insertions(+), 13 deletions(-) (limited to 'dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/web/WebServer.java') diff --git a/dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/web/WebServer.java b/dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/web/WebServer.java index c3655572..eed9651c 100644 --- a/dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/web/WebServer.java +++ b/dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/web/WebServer.java @@ -2,6 +2,7 @@ package org.openslx.bwlp.sat.web; import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.InputStream; import java.io.PipedInputStream; import java.io.PipedOutputStream; import java.sql.SQLException; @@ -16,16 +17,24 @@ import java.util.concurrent.TimeUnit; import org.apache.commons.io.output.ByteArrayOutputStream; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.openslx.bwlp.sat.RuntimeConfig; import org.openslx.bwlp.sat.database.mappers.DbImage; import org.openslx.bwlp.sat.database.mappers.DbLecture; import org.openslx.bwlp.sat.database.mappers.DbLecture.LaunchData; import org.openslx.bwlp.sat.database.mappers.DbLecture.RunScript; import org.openslx.bwlp.sat.fileserv.FileServer; +import org.openslx.bwlp.sat.fileserv.cow.CowSession; +import org.openslx.bwlp.sat.fileserv.cow.CowSessionManager; +import org.openslx.bwlp.sat.permissions.User; +import org.openslx.bwlp.sat.thrift.SessionManager; +import org.openslx.bwlp.sat.util.BashVars; import org.openslx.bwlp.sat.util.Configuration; +import org.openslx.bwlp.thrift.iface.AuthorizationError; import org.openslx.bwlp.thrift.iface.NetRule; import org.openslx.bwlp.thrift.iface.NetShare; import org.openslx.bwlp.thrift.iface.NetShareAuth; import org.openslx.bwlp.thrift.iface.TNotFoundException; +import org.openslx.bwlp.thrift.iface.UserInfo; import org.openslx.util.GrowingThreadPoolExecutor; import org.openslx.util.Json; import org.openslx.util.PrioThreadFactory; @@ -73,9 +82,13 @@ public class WebServer extends NanoHTTPD { private Response handle(IHTTPSession session, String uri) { // Our special stuff + // REMEMBER: Call session.parseBody() if you need post params in session.getParms() + // Normalize during split String[] parts = uri.replaceFirst("^/+", "").split("/+"); + if (parts.length < 2) + return notFound(); // /vmchooser/* - if (parts.length > 1 && parts[0].equals("vmchooser")) { + if (parts[0].equals("vmchooser")) { if (parts[1].equals("list")) { try { return serveVmChooserList(session.getParms()); @@ -88,7 +101,7 @@ public class WebServer extends NanoHTTPD { if (parts.length < 4) return badRequest("Bad Request"); if (parts[3].equals("metadata")) - return serveMetaData(parts[2]); + return serveMetaData(parts[2], session.getParms()); if (parts[3].equals("netrules")) return serveLectureNetRules(parts[2]); if (parts[3].equals("imagemeta")) @@ -96,6 +109,38 @@ public class WebServer extends NanoHTTPD { } return notFound(); } + // /cow/* + if (parts[0].equals("cow")) { + if (parts.length < 3) + return badRequest("Bad request"); + if (session.getMethod() == Method.POST) { + try { + session.parseBody(); + } catch (IOException | ResponseException e) { + LOGGER.debug("could not parse request body", e); + return internalServerError(); + } + } + // Requests by dnbd3-fuse + if (parts[1].equals("v1") && parts[2].equals("file")) { + if (parts.length < 4) + return badRequest("No action"); + if (parts[3].equals("merge")) + return cowMerge(session.getParms()); + if (parts[3].equals("update")) + return cowUploadCluster(session); + return badRequest("No such API endpoint: " + parts[3]); + } + // Requests by the wrapper program + if (parts[1].equals("status")) + return cowServeStatus(parts[2]); + if (parts[1].equals("finish")) + return cowFinish(parts[2]); + if (parts[1].equals("abort")) + return cowAbort(parts[2], session.getMethod()); + // Huh? + return badRequest("No such API endpoint"); + } if (uri.startsWith("/bwlp/container/clusterimages")) { return serverContainerImages(); } @@ -111,7 +156,7 @@ public class WebServer extends NanoHTTPD { } if (session.getMethod() == Method.POST && uri.startsWith("/do/")) { try { - session.parseBody(null); + session.parseBody(); } catch (IOException | ResponseException e) { LOGGER.debug("could not parse request body", e); return internalServerError(); @@ -122,6 +167,114 @@ public class WebServer extends NanoHTTPD { return notFound(); } + private Response cowUploadCluster(IHTTPSession session) { + if (session.getMethod() != Method.POST && session.getMethod() != Method.PUT) + return badRequest("Not PUT or POST"); + InputStream is = session.getInputStream(); + if (is == null) + return internalServerError("Cannot get input stream"); + String uuid = session.getParms().get("uuid"); + String sIdx = session.getParms().get("clusterindex"); + if (uuid == null || sIdx == null) + return badRequest("uuid or clusterindex missing"); + long clusterIndex = Util.parseLong(sIdx, -1); + if (clusterIndex < 0) + return badRequest("Invalid clusterindex"); + CowSession cowSession = CowSessionManager.get(uuid); + if (cowSession == null) + return notFound("Invalid session ID"); + byte[] data; + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(9001)) { + int n; + byte[] b = new byte[65536]; + while ((n = is.read(b)) > 0) { + baos.write(b, 0, n); + } + data = baos.toByteArray(); + } catch (IOException e) { + LOGGER.warn("Cannot read cluster", e); + return internalServerError("Cannot read cluster data from http stream"); + } + int cowRet = cowSession.addCluster(clusterIndex, data); + if (cowRet == 0) + return new NanoHTTPD.Response(NanoHTTPD.Response.Status.OK, "text/plain; charset=utf-8", "OK"); + // Error? + if (cowRet < 0) + return internalServerError("Error writing received chunk to disk"); + // Throttle request + Response reply = new NanoHTTPD.Response(NanoHTTPD.Response.Status.SERVICE_UNAVAILABLE, + "text/plain; charset=utf-8", "Slow down!"); + reply.addHeader("Retry-After", Integer.toString(cowRet)); + return reply; + } + + /** + * dnbd3-fuse is requesting merge. We take that as signal that the upload is + * done. + */ + private Response cowMerge(Map parms) { + String uuid = parms.get("uuid"); + String newSz = parms.get("newFileSize"); + if (uuid == null || newSz == null) + return badRequest("uuid or newFileSize missing"); + CowSession session = CowSessionManager.get(uuid); + if (session == null) + return notFound("Invalid session ID"); + long finalSize = Util.parseLong(newSz, -1); + long limit = (RuntimeConfig.getVmSizeLimit() * 12) / 10; + if (limit <= 0) { + limit = 300_000_000_000l; + } + if (finalSize < 0 || finalSize > limit) + return badRequest("Illegal final file size"); + LOGGER.info("Got upload finished for CoW session"); + try { + session.uploadFinished(finalSize); + } catch (Exception e) { + return badRequest("Cannot finish upload: " + e.getMessage()); + } + return new NanoHTTPD.Response(NanoHTTPD.Response.Status.OK, "text/plain; charset=utf-8", "OK"); + } + + /** + * User-facing tool on the client was used to confirm keeping the modified + * image. + */ + private Response cowFinish(String sessionId) { + CowSession session = CowSessionManager.get(sessionId); + if (session == null) + return notFound("Invalid session ID"); + LOGGER.info("User requested finalization for CoW session"); + try { + session.requestFinalization(); + } catch (Exception e) { + return badRequest("Cannot finalize image version: " + e.getMessage()); + } + return new NanoHTTPD.Response(NanoHTTPD.Response.Status.OK, "text/plain; charset=utf-8", "OK"); + } + + /** + * Abort a COW session, delete temporary files. + */ + private Response cowAbort(String sessionId, Method method) { + if (method != Method.POST) + return badRequest("Need abort request as POST"); + CowSession session = CowSessionManager.get(sessionId); + if (session == null) + return notFound("Invalid session ID"); + LOGGER.info("User sent abort request for CoW session"); + session.abort(); + return new NanoHTTPD.Response(NanoHTTPD.Response.Status.OK, "text/plain; charset=utf-8", "OK"); + } + + private Response cowServeStatus(String sessionId) { + CowSession session = CowSessionManager.get(sessionId); + if (session == null) + return notFound("Invalid session ID"); + return new NanoHTTPD.Response(NanoHTTPD.Response.Status.OK, "application/json; charset=utf-8", + session.getStatusJson()); + } + private Response serveStatus() { return new NanoHTTPD.Response(NanoHTTPD.Response.Status.OK, "application/json; charset=utf-8", Json.serialize(FileServer.instance().getStatus())); @@ -129,13 +282,11 @@ public class WebServer extends NanoHTTPD { /** * Return meta data (eg. *.vmx) required to start the given lecture. - * - * @param lectureId - * @return */ - private Response serveMetaData(final String lectureId) { + private Response serveMetaData(final String lectureId, Map parms) { PipedInputStream sink = new PipedInputStream(10000); try { + final BashVars vars = new BashVars(); final TarArchiveWriter tarArchiveWriter = new TarArchiveWriter(new PipedOutputStream(sink)); final LaunchData ld; try { @@ -146,19 +297,35 @@ public class WebServer extends NanoHTTPD { } catch (SQLException e) { return internalServerError(); } + vars.addVar("DMSD_IMAGE_PATH", ld.imagePath); + // See if a CoW session is requested + try { + String sessionType = parms.get("cow-type"); + String cowUserId = parms.get("cow-user"); + if (cowUserId != null) { + String cowSession = CowSessionManager.create(cowUserId, ld, sessionType); + vars.addVar("DMSD_COW_SESSION", cowSession); + } + } catch (Exception e) { + LOGGER.warn("Error creating cow session for " + ld.imageBaseId + ", " + ld.imagePath, e); + return internalServerError("Cannot create COW session: " + e.getMessage()); + } // Meta is required, everything else is optional tpe.execute(new Runnable() { @Override public void run() { try { tarArchiveWriter.writeFile("vmx", ld.configuration); + tarArchiveWriter.writeFile("config.inc", vars.toString()); tarArchiveWriter.writeFile("runscript", ld.legacyRunScript); tarArchiveWriter.writeFile("netshares", serializeNetShares(ld.netShares)); if (ld.runScript != null) { int cnt = 0; for (RunScript rs : ld.runScript) { - tarArchiveWriter.writeFile(String.format("adminrun/%04d-%d-%d.%s", cnt++, rs.visibility, - rs.passCreds ? 1 : 0, rs.extension), rs.content); + tarArchiveWriter.writeFile( + String.format("adminrun/%04d-%d-%d.%s", cnt++, rs.visibility, + rs.passCreds ? 1 : 0, rs.extension), + rs.content); } } } catch (IOException e) { @@ -206,7 +373,8 @@ public class WebServer extends NanoHTTPD { sb.append("IN * 0 REJECT\n"); sb.append("OUT * 0 REJECT\n"); } - return new NanoHTTPD.Response(NanoHTTPD.Response.Status.OK, "text/plain; charset=utf-8", sb.toString()); + return new NanoHTTPD.Response(NanoHTTPD.Response.Status.OK, "text/plain; charset=utf-8", + sb.toString()); } private String serializeNetShares(List list) { @@ -247,9 +415,30 @@ public class WebServer extends NanoHTTPD { */ private Response serveVmChooserList(Map params) throws Exception { String locations = params.get("locations"); + String userToken = params.get("cow-user"); boolean exams = params.containsKey("exams"); + String addUserError = null; + UserInfo user = null; + + if (!Util.isEmptyString(userToken)) { + user = SessionManager.get(userToken); + if (user == null || user.userId == null) { + addUserError = "Invalid session token, edit mode not available\n" + + "Ungültiges Sitzungstoken, Editiermodus nicht verfügbar"; + } else { + AuthorizationError err = User.canLogin(user); + if (err != null) { + user = null; + addUserError = "You are not allowed to edit VMs\n" + + "Sie haben keine Berechtigung, um VMs zu bearbeiten."; + } + } + } - VmChooserListXml listXml = DbLecture.getUsableListXml(exams, locations); + VmChooserListXml listXml = DbLecture.getUsableListXml(exams, locations, user); + if (addUserError != null) { + listXml.setError(addUserError); + } ByteArrayOutputStream baos = new ByteArrayOutputStream(); serializer.write(listXml, baos); return new NanoHTTPD.Response(NanoHTTPD.Response.Status.OK, "text/xml; charset=utf-8", @@ -273,7 +462,14 @@ public class WebServer extends NanoHTTPD { * Helper for returning "404 Not Found" Status */ public static Response notFound() { - return new NanoHTTPD.Response(NanoHTTPD.Response.Status.NOT_FOUND, "text/plain", "Nicht gefunden!"); + return notFound("Nicht gefunden"); + } + + /** + * Helper for returning "404 Not Found" Status with custom body + */ + private static Response notFound(String string) { + return new NanoHTTPD.Response(NanoHTTPD.Response.Status.NOT_FOUND, "text/plain", string); } /** @@ -287,7 +483,8 @@ public class WebServer extends NanoHTTPD { } /** - * create a json response with information about existing container images in + * create a json response with information about existing container images + * in * bwlehrpool */ private Response serverContainerImages() { -- cgit v1.2.3-55-g7522