Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/file-server/zfs/create.ts
1447 views
1
import { create, get, getDb, deleteFromDb, filesystemExists } from "./db";
2
import { exec } from "./util";
3
import {
4
filesystemArchivePath,
5
bupFilesystemMountpoint,
6
filesystemDataset,
7
filesystemMountpoint,
8
} from "./names";
9
import { getPools, initializePool } from "./pools";
10
import { dearchiveFilesystem } from "./archive";
11
import { UID, GID } from "./config";
12
import { createSnapshot } from "./snapshots";
13
import { type Filesystem, primaryKey, type PrimaryKey } from "./types";
14
15
export async function createFilesystem(
16
opts: PrimaryKey & {
17
affinity?: string;
18
clone?: PrimaryKey;
19
},
20
): Promise<Filesystem> {
21
if (filesystemExists(opts)) {
22
return get(opts);
23
}
24
const pk = primaryKey(opts);
25
const { namespace } = pk;
26
const { affinity, clone } = opts;
27
const source = clone ? get(clone) : undefined;
28
29
const db = getDb();
30
// select a pool:
31
let pool: undefined | string = undefined;
32
33
if (source != null) {
34
// use same pool as source filesystem. (we could use zfs send/recv but that's much slower and not a clone)
35
pool = source.pool;
36
} else {
37
if (affinity) {
38
// if affinity is set, have preference to use same pool as other filesystems with this affinity.
39
const x = db
40
.prepare(
41
"SELECT pool, COUNT(pool) AS cnt FROM filesystems WHERE namespace=? AND affinity=? ORDER by cnt DESC",
42
)
43
.get(namespace, affinity) as { pool: string; cnt: number } | undefined;
44
pool = x?.pool;
45
}
46
if (!pool) {
47
// assign one with *least* filesystems
48
const x = db
49
.prepare(
50
"SELECT pool, COUNT(pool) AS cnt FROM filesystems GROUP BY pool ORDER by cnt ASC",
51
)
52
.all() as any;
53
const pools = await getPools();
54
if (Object.keys(pools).length > x.length) {
55
// rare case: there exists a pool that isn't used yet, so not
56
// represented in above query at all; use it
57
const v = new Set<string>();
58
for (const { pool } of x) {
59
v.add(pool);
60
}
61
for (const name in pools) {
62
if (!v.has(name)) {
63
pool = name;
64
break;
65
}
66
}
67
} else {
68
if (x.length == 0) {
69
throw Error("cannot create filesystem -- no available pools");
70
}
71
// just use the least crowded
72
pool = x[0].pool;
73
}
74
}
75
}
76
if (!pool) {
77
throw Error("bug -- unable to select a pool");
78
}
79
80
const { cnt } = db
81
.prepare(
82
"SELECT COUNT(pool) AS cnt FROM filesystems WHERE pool=? AND namespace=?",
83
)
84
.get(pool, namespace) as { cnt: number };
85
86
if (cnt == 0) {
87
// initialize pool for use in this namespace:
88
await initializePool({ pool, namespace });
89
}
90
91
if (source == null) {
92
// create filesystem on the selected pool
93
const mountpoint = filesystemMountpoint(pk);
94
const dataset = filesystemDataset({ ...pk, pool });
95
await exec({
96
verbose: true,
97
command: "sudo",
98
args: [
99
"zfs",
100
"create",
101
"-o",
102
`mountpoint=${mountpoint}`,
103
"-o",
104
"compression=lz4",
105
"-o",
106
"dedup=on",
107
dataset,
108
],
109
what: {
110
...pk,
111
desc: `create filesystem ${dataset} for filesystem on the selected pool mounted at ${mountpoint}`,
112
},
113
});
114
await exec({
115
verbose: true,
116
command: "sudo",
117
args: ["chown", "-R", `${UID}:${GID}`, mountpoint],
118
whate: {
119
...pk,
120
desc: `setting permissions of filesystem mounted at ${mountpoint}`,
121
},
122
});
123
} else {
124
// clone source
125
// First ensure filesystem isn't archived
126
// (we might alternatively de-archive to make the clone...?)
127
if (source.archived) {
128
await dearchiveFilesystem(source);
129
}
130
// Get newest snapshot, or make one if there are none
131
const snapshot = await createSnapshot({ ...source, ifChanged: true });
132
if (!snapshot) {
133
throw Error("bug -- source should have snapshot");
134
}
135
const source_snapshot = `${filesystemDataset(source)}@${snapshot}`;
136
await exec({
137
verbose: true,
138
command: "sudo",
139
args: [
140
"zfs",
141
"clone",
142
"-o",
143
`mountpoint=${filesystemMountpoint(pk)}`,
144
"-o",
145
"compression=lz4",
146
"-o",
147
"dedup=on",
148
source_snapshot,
149
filesystemDataset({ ...pk, pool }),
150
],
151
what: {
152
...pk,
153
desc: `clone filesystem from ${source_snapshot}`,
154
},
155
});
156
}
157
158
// update database
159
create({ ...pk, pool, affinity });
160
return get(pk);
161
}
162
163
// delete -- This is very dangerous -- it deletes the filesystem,
164
// the archive, and any backups and removes knowledge the filesystem from the db.
165
166
export async function deleteFilesystem(fs: PrimaryKey) {
167
const filesystem = get(fs);
168
const dataset = filesystemDataset(filesystem);
169
if (!filesystem.archived) {
170
await exec({
171
verbose: true,
172
command: "sudo",
173
args: ["zfs", "destroy", "-r", dataset],
174
what: {
175
...filesystem,
176
desc: `destroy dataset ${dataset} containing the filesystem`,
177
},
178
});
179
}
180
await exec({
181
verbose: true,
182
command: "sudo",
183
args: ["rm", "-rf", filesystemMountpoint(filesystem)],
184
what: {
185
...filesystem,
186
desc: `delete directory '${filesystemMountpoint(filesystem)}' where filesystem was stored`,
187
},
188
});
189
await exec({
190
verbose: true,
191
command: "sudo",
192
args: ["rm", "-rf", bupFilesystemMountpoint(filesystem)],
193
what: {
194
...filesystem,
195
desc: `delete directory '${bupFilesystemMountpoint(filesystem)}' where backups were stored`,
196
},
197
});
198
await exec({
199
verbose: true,
200
command: "sudo",
201
args: ["rm", "-rf", filesystemArchivePath(filesystem)],
202
what: {
203
...filesystem,
204
desc: `delete directory '${filesystemArchivePath(filesystem)}' where archives were stored`,
205
},
206
});
207
deleteFromDb(filesystem);
208
}
209
210