summaryrefslogtreecommitdiffstats
path: root/dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/web/WebServer.java
diff options
context:
space:
mode:
authorSimon Rettberg2024-05-08 18:38:31 +0200
committerSimon Rettberg2024-05-08 18:38:31 +0200
commit50866f7ef328bac854bd076ca48a3aa08aa3c29c (patch)
tree6415b45dc9c5344c76fc161ec22966d6c5898fc0 /dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/web/WebServer.java
parent[server] DbImage: Add log messages (diff)
downloadtutor-module-50866f7ef328bac854bd076ca48a3aa08aa3c29c.tar.gz
tutor-module-50866f7ef328bac854bd076ca48a3aa08aa3c29c.tar.xz
tutor-module-50866f7ef328bac854bd076ca48a3aa08aa3c29c.zip
[server] Add support for CoW sessions
Diffstat (limited to 'dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/web/WebServer.java')
-rw-r--r--dozentenmodulserver/src/main/java/org/openslx/bwlp/sat/web/WebServer.java223
1 files changed, 210 insertions, 13 deletions
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<String, String> 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<String, String> 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<NetShare> list) {
@@ -247,9 +415,30 @@ public class WebServer extends NanoHTTPD {
*/
private Response serveVmChooserList(Map<String, String> 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() {