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