Path: blob/master/src/packages/frontend/course/compute/actions.ts
1503 views
import type { CourseActions } from "../actions";1import { cloneConfiguration } from "@cocalc/frontend/compute/clone";2import type { Unit } from "../store";3import { reuseInFlight } from "@cocalc/util/reuse-in-flight";4import type { ComputeServerConfig } from "../types";5import { merge } from "lodash";6import type { Command } from "./students";7import { getUnitId, MAX_PARALLEL_TASKS } from "./util";8import {9computeServerAction,10createServer,11deleteServer,12getServersById,13setServerOwner,14} from "@cocalc/frontend/compute/api";15import { webapp_client } from "@cocalc/frontend/webapp-client";16import { map as awaitMap } from "awaiting";17import { getComputeServers } from "./synctable";18import { join } from "path";1920declare var DEBUG: boolean;2122// const log = (..._args)=>{};23const log = DEBUG ? console.log : (..._args) => {};2425export class ComputeActions {26private course_actions: CourseActions;27private debugComputeServer?: {28project_id: string;29compute_server_id: number;30};3132constructor(course_actions: CourseActions) {33this.course_actions = course_actions;34}3536private getStore = () => {37const store = this.course_actions.get_store();38if (store == null) {39throw Error("no store");40}41return store;42};4344private getUnit = (45unit_id: string,46): {47unit: Unit;48table: "assignments" | "handouts";49} => {50// this code below is reasonable since the id is a random uuidv4, so no51// overlap between assignments and handouts in practice.52const assignment = this.course_actions.syncdb.get_one({53assignment_id: unit_id,54table: "assignments",55});56if (assignment != null) {57return { unit: assignment as unknown as Unit, table: "assignments" };58}59const handout = this.course_actions.syncdb.get_one({60handout_id: unit_id,61table: "handouts",62});63if (handout != null) {64return { unit: handout as unknown as Unit, table: "handouts" };65}66throw Error(`no assignment or handout with id '${unit_id}'`);67};6869setComputeServerConfig = ({70unit_id,71compute_server,72}: {73unit_id: string;74compute_server: ComputeServerConfig;75}) => {76let { table, unit } = this.getUnit(unit_id);77const obj = { ...unit.toJS(), table };78obj.compute_server = merge(obj.compute_server, compute_server);79this.course_actions.set(obj, true, true);80};8182// Create and compute server associated to a given assignment or handout83// for a specific student. Does nothing if (1) the compute server already84// exists, or (2) no compute server is configured for the given assignment.85private createComputeServer = reuseInFlight(86async ({87student_id,88unit_id,89}: {90student_id: string;91unit_id: string;92}): Promise<number | undefined> => {93// what compute server is configured for this assignment or handout?94const { unit } = this.getUnit(unit_id);95const compute_server = unit.get("compute_server");96if (compute_server == null) {97log("createComputeServer -- nothing to do - nothing configured.", {98student_id,99});100return;101}102const course_server_id = compute_server.get("server_id");103if (!course_server_id) {104log(105"createComputeServer -- nothing to do - compute server not configured for this unit.",106{107student_id,108},109);110return;111}112const cur_id = compute_server.getIn([113"students",114student_id,115"server_id",116]);117if (cur_id) {118log("compute server already exists", { cur_id, student_id });119return cur_id;120}121const store = this.getStore();122const course_project_id = store.get("course_project_id");123let student_project_id = store.get_student_project_id(student_id);124if (!student_project_id) {125student_project_id =126await this.course_actions.student_projects.create_student_project(127student_id,128);129}130if (!student_project_id) {131throw Error("unable to create the student's project");132}133134// Is there already a compute server in the target project135// with this course_server_id and course_project_id? If so,136// we use that one, since we don't want to have multiple copies137// of the *same* source compute server for multiple handouts138// or assignments.139const v = (140await getComputeServers({141project_id: student_project_id,142course_project_id,143course_server_id,144fields: ["id", "deleted"],145})146).filter(({ deleted }) => !deleted);147148let server_id;149if (v.length > 0) {150// compute server already exists -- use it151server_id = v[0].id;152} else {153// create new compute server154const server = await cloneConfiguration({155id: course_server_id,156noChange: true,157});158const studentServer = {159...server,160project_id: student_project_id,161course_server_id,162course_project_id,163};164// we must enable allowCollaboratorControl since it's needed for the165// student to start/stop the compute server.166studentServer.configuration.allowCollaboratorControl = true;167server_id = await createServer(studentServer);168}169170this.setComputeServerConfig({171unit_id,172compute_server: { students: { [student_id]: { server_id } } },173});174return server_id;175},176);177178// returns GLOBAL id of compute server for the given unit, or undefined if one isn't configured.179getComputeServerId = ({ unit, student_id }): number | undefined => {180return unit.getIn([181"compute_server",182"students",183student_id,184"server_id",185]) as number | undefined;186};187188computeServerCommand = async ({189command,190unit,191student_id,192}: {193command: Command;194unit: Unit;195student_id: string;196}) => {197if (command == "create") {198const unit_id = getUnitId(unit);199await this.createComputeServer({ student_id, unit_id });200return;201}202const server_id = this.getComputeServerId({ unit, student_id });203if (!server_id) {204throw Error("compute server doesn't exist");205}206switch (command) {207case "transfer":208const student = this.getStore()?.get_student(student_id);209const new_account_id = student?.get("account_id");210if (!new_account_id) {211throw Error("student does not have an account yet");212}213await setServerOwner({ id: server_id, new_account_id });214return;215case "start":216case "stop":217case "reboot":218case "deprovision":219await computeServerAction({ id: server_id, action: command });220return;221case "delete":222const unit_id = getUnitId(unit);223this.setComputeServerConfig({224unit_id,225compute_server: { students: { [student_id]: { server_id: 0 } } },226});227// only actually delete the server from the backend if no other228// units also refer to it:229if (230this.getUnitsUsingComputeServer({ student_id, server_id }).length == 0231) {232await deleteServer(server_id);233}234return;235case "transfer":236// todo237default:238throw Error(`command '${command}' not implemented`);239}240};241242private getUnitIds = () => {243const store = this.getStore();244if (store == null) {245throw Error("store must be defined");246}247return store.get_assignment_ids().concat(store.get_handout_ids());248};249250private getUnitsUsingComputeServer = ({251student_id,252server_id,253}: {254student_id: string;255server_id: number;256}): string[] => {257const v: string[] = [];258for (const id of this.getUnitIds()) {259const { unit } = this.getUnit(id);260if (261unit.getIn(["compute_server", "students", student_id, "server_id"]) ==262server_id263) {264v.push(id);265}266}267return v;268};269270private getDebugComputeServer = reuseInFlight(async () => {271if (this.debugComputeServer == null) {272const compute_server_id = 1;273const project_id = (274await getServersById({275ids: [compute_server_id],276fields: ["project_id"],277})278)[0].project_id as string;279this.debugComputeServer = { compute_server_id, project_id };280}281return this.debugComputeServer;282});283284private runTerminalCommandOneStudent = async ({285unit,286student_id,287...terminalOptions288}) => {289const store = this.getStore();290let project_id = store.get_student_project_id(student_id);291if (!project_id) {292throw Error("student project doesn't exist");293}294let compute_server_id = this.getComputeServerId({ unit, student_id });295if (!compute_server_id) {296throw Error("compute server doesn't exist");297}298if (DEBUG) {299log(300"runTerminalCommandOneStudent: in DEBUG mode, so actually using debug compute server",301);302({ compute_server_id, project_id } = await this.getDebugComputeServer());303}304305return await webapp_client.project_client.exec({306...terminalOptions,307project_id,308compute_server_id,309});310};311312// Run a terminal command in parallel on the compute servers of the given students.313// This does not throw an exception on error; instead, some entries in the output314// will have nonzero exit_code.315runTerminalCommand = async ({316unit,317student_ids,318setOutputs,319...terminalOptions320}) => {321let outputs: {322stdout?: string;323stderr?: string;324exit_code?: number;325student_id: string;326total_time: number;327}[] = [];328const timeout = terminalOptions.timeout;329const start = Date.now();330const task = async (student_id) => {331let result;332try {333result = {334...(await this.runTerminalCommandOneStudent({335unit,336student_id,337...terminalOptions,338err_on_exit: false,339})),340student_id,341total_time: (Date.now() - start) / 1000,342};343} catch (err) {344result = {345student_id,346stdout: "",347stderr: `${err}`,348exit_code: -1,349total_time: (Date.now() - start) / 1000,350timeout,351};352}353outputs = [...outputs, result];354setOutputs(outputs);355};356await awaitMap(student_ids, MAX_PARALLEL_TASKS, task);357return outputs;358};359360setComputeServerAssociations = async ({361src_path,362target_project_id,363target_path,364student_id,365unit_id,366}: {367src_path: string;368target_project_id: string;369target_path: string;370student_id: string;371unit_id: string;372}) => {373const { unit } = this.getUnit(unit_id);374const compute_server_id = this.getComputeServerId({ unit, student_id });375if (!compute_server_id) {376// If no compute server is configured for this student and unit,377// then nothing to do.378return;379}380381// Figure out which subdirectories in the src_path of the course project382// are on a compute server, and set them to be on THE compute server for383// this student/unit.384const store = this.getStore();385if (store == null) {386return;387}388const course_project_id = store.get("course_project_id");389390const courseAssociations =391webapp_client.project_client.computeServers(course_project_id);392393const studentAssociations =394webapp_client.project_client.computeServers(target_project_id);395396const ids = await courseAssociations.getServerIdForSubtree(src_path);397for (const source in ids) {398if (ids[source]) {399const tail = source.slice(src_path.length + 1);400const path = join(target_path, tail);401await studentAssociations.waitUntilReady();402// path is on a compute server.403studentAssociations.connectComputeServerToPath({404id: compute_server_id,405path,406});407}408}409};410}411412413