Path: blob/master/src/packages/file-server/zfs/test/pull.test.ts
1450 views
/*1DEVELOPMENT:23This tests pull replication by setting up two separate file-servers on disk locally4and doing pulls from one to the other over ssh. This involves password-less ssh5to root on localhost, and creating multiple pools, so use with caution and don't6expect this to work unless you really know what you're doing.7Also, these tests are going to take a while.89Efficient powerful backup isn't trivial and is very valuable, so10its' worth the wait!1112pnpm exec jest --watch pull.test.ts13*/1415import { join } from "path";16import { createTestPools, deleteTestPools, init, describe } from "./util";17import {18createFilesystem,19createSnapshot,20deleteSnapshot,21deleteFilesystem,22pull,23archiveFilesystem,24dearchiveFilesystem,25} from "@cocalc/file-server/zfs";26import { context, setContext } from "@cocalc/file-server/zfs/config";27import { filesystemMountpoint } from "@cocalc/file-server/zfs/names";28import { readFile, writeFile } from "fs/promises";29import { filesystemExists, get } from "@cocalc/file-server/zfs/db";30import { SYNCED_FIELDS } from "../pull";3132describe("create two separate file servers, then do pulls to sync one to the other under various conditions", () => {33let one: any = null,34two: any = null;35const prefix1 = context.PREFIX + ".1";36const prefix2 = context.PREFIX + ".2";37const remote = "root@localhost";3839beforeAll(async () => {40one = await createTestPools({ count: 1, size: "1G", prefix: prefix1 });41setContext({ prefix: prefix1 });42await init();43two = await createTestPools({44count: 1,45size: "1G",46prefix: prefix2,47});48setContext({ prefix: prefix2 });49await init();50});5152afterAll(async () => {53await deleteTestPools(one);54await deleteTestPools(two);55});5657it("creates a filesystem in pool one, writes a file and takes a snapshot", async () => {58setContext({ prefix: prefix1 });59const fs = await createFilesystem({60project_id: "00000000-0000-0000-0000-000000000001",61});62await writeFile(join(filesystemMountpoint(fs), "a.txt"), "hello");63await createSnapshot(fs);64expect(await filesystemExists(fs)).toEqual(true);65});6667it("pulls filesystem one to filesystem two, and confirms the fs and file were indeed sync'd", async () => {68setContext({ prefix: prefix2 });69expect(70await filesystemExists({71project_id: "00000000-0000-0000-0000-000000000001",72}),73).toEqual(false);7475// first dryRun76const { toUpdate, toDelete } = await pull({77remote,78prefix: prefix1,79dryRun: true,80});81expect(toDelete.length).toBe(0);82expect(toUpdate.length).toBe(1);83expect(toUpdate[0].remoteFs.owner_id).toEqual(84"00000000-0000-0000-0000-000000000001",85);86expect(toUpdate[0].localFs).toBe(undefined);8788// now for real89const { toUpdate: toUpdate1, toDelete: toDelete1 } = await pull({90remote,91prefix: prefix1,92});9394expect(toDelete1).toEqual(toDelete);95expect(toUpdate1).toEqual(toUpdate);96const fs = { project_id: "00000000-0000-0000-0000-000000000001" };97expect(await filesystemExists(fs)).toEqual(true);98expect(99(await readFile(join(filesystemMountpoint(fs), "a.txt"))).toString(),100).toEqual("hello");101102// nothing if we sync again:103const { toUpdate: toUpdate2, toDelete: toDelete2 } = await pull({104remote,105prefix: prefix1,106});107expect(toDelete2.length).toBe(0);108expect(toUpdate2.length).toBe(0);109});110111it("creates another file in our filesystem, creates another snapshot, syncs again, and sees that the sync worked", async () => {112setContext({ prefix: prefix1 });113const fs = { project_id: "00000000-0000-0000-0000-000000000001" };114await writeFile(join(filesystemMountpoint(fs), "b.txt"), "cocalc");115await createSnapshot({ ...fs, force: true });116const { snapshots } = get(fs);117expect(snapshots.length).toBe(2);118119setContext({ prefix: prefix2 });120await pull({ remote, prefix: prefix1 });121122expect(123(await readFile(join(filesystemMountpoint(fs), "b.txt"))).toString(),124).toEqual("cocalc");125});126127it("archives the project, does sync, and see the other one got archived", async () => {128const fs = { project_id: "00000000-0000-0000-0000-000000000001" };129setContext({ prefix: prefix2 });130const project2before = get(fs);131expect(project2before.archived).toBe(false);132133setContext({ prefix: prefix1 });134await archiveFilesystem(fs);135const project1 = get(fs);136expect(project1.archived).toBe(true);137138setContext({ prefix: prefix2 });139await pull({ remote, prefix: prefix1 });140const project2 = get(fs);141expect(project2.archived).toBe(true);142expect(project1.last_edited).toEqual(project2.last_edited);143});144145it("dearchives, does sync, then sees the other gets dearchived; this just tests that sync de-archives, but works even if there are no new snapshots", async () => {146const fs = { project_id: "00000000-0000-0000-0000-000000000001" };147setContext({ prefix: prefix1 });148await dearchiveFilesystem(fs);149const project1 = get(fs);150expect(project1.archived).toBe(false);151152setContext({ prefix: prefix2 });153await pull({ remote, prefix: prefix1 });154const project2 = get(fs);155expect(project2.archived).toBe(false);156});157158it("archives project, does sync, de-archives project, adds another snapshot, then does sync, thus testing that sync both de-archives *and* pulls latest snapshot", async () => {159const fs = { project_id: "00000000-0000-0000-0000-000000000001" };160setContext({ prefix: prefix1 });161expect(get(fs).archived).toBe(false);162await archiveFilesystem(fs);163expect(get(fs).archived).toBe(true);164setContext({ prefix: prefix2 });165await pull({ remote, prefix: prefix1 });166expect(get(fs).archived).toBe(true);167168// now dearchive169setContext({ prefix: prefix1 });170await dearchiveFilesystem(fs);171// write content172await writeFile(join(filesystemMountpoint(fs), "d.txt"), "hello");173// snapshot174await createSnapshot({ ...fs, force: true });175const project1 = get(fs);176177setContext({ prefix: prefix2 });178await pull({ remote, prefix: prefix1 });179const project2 = get(fs);180expect(project2.snapshots).toEqual(project1.snapshots);181expect(project2.archived).toBe(false);182});183184it("deletes project, does sync, then sees the other does NOT gets deleted without passing the deleteFilesystemCutoff option, and also with deleteFilesystemCutoff an hour ago, but does get deleted with it now", async () => {185const fs = { project_id: "00000000-0000-0000-0000-000000000001" };186setContext({ prefix: prefix1 });187expect(await filesystemExists(fs)).toEqual(true);188await deleteFilesystem(fs);189expect(await filesystemExists(fs)).toEqual(false);190191setContext({ prefix: prefix2 });192expect(await filesystemExists(fs)).toEqual(true);193await pull({ remote, prefix: prefix1 });194expect(await filesystemExists(fs)).toEqual(true);195196await pull({197remote,198prefix: prefix1,199deleteFilesystemCutoff: new Date(Date.now() - 1000 * 60 * 60),200});201expect(await filesystemExists(fs)).toEqual(true);202203await pull({204remote,205prefix: prefix1,206deleteFilesystemCutoff: new Date(),207});208expect(await filesystemExists(fs)).toEqual(false);209});210211const v = [212{ project_id: "00000000-0000-0000-0000-000000000001", affinity: "math" },213{214account_id: "00000000-0000-0000-0000-000000000002",215name: "cocalc",216affinity: "math",217},218{219group_id: "00000000-0000-0000-0000-000000000003",220namespace: "test",221name: "data",222affinity: "sage",223},224];225it("creates 3 filesystems in 2 different namespaces, and confirms sync works", async () => {226setContext({ prefix: prefix1 });227for (const fs of v) {228await createFilesystem(fs);229}230// write files to fs2 and fs3, so data will get sync'd too231await writeFile(join(filesystemMountpoint(v[1]), "a.txt"), "hello");232await writeFile(join(filesystemMountpoint(v[2]), "b.txt"), "cocalc");233// snapshot234await createSnapshot({ ...v[1], force: true });235await createSnapshot({ ...v[2], force: true });236const p = v.map((x) => get(x));237238// do the sync239setContext({ prefix: prefix2 });240await pull({ remote, prefix: prefix1 });241242// verify that we have everything243for (const fs of v) {244expect(await filesystemExists(fs)).toEqual(true);245}246const p2 = v.map((x) => get(x));247for (let i = 0; i < p.length; i++) {248// everything matches (even snapshots, since no trimming happened)249for (const field of SYNCED_FIELDS) {250expect({ i, field, value: p[i][field] }).toEqual({251i,252field,253value: p2[i][field],254});255}256}257});258259it("edits some files on one of the above filesystems, snapshots, sync's, goes back and deletes a snapshot, edits more files, sync's, and notices that snapshots on sync target properly match snapshots on source.", async () => {260// edits some files on one of the above filesystems, snapshots:261setContext({ prefix: prefix1 });262await writeFile(join(filesystemMountpoint(v[1]), "a2.txt"), "hello2");263await createSnapshot({ ...v[1], force: true });264265// sync's266setContext({ prefix: prefix2 });267await pull({ remote, prefix: prefix1 });268269// delete snapshot270setContext({ prefix: prefix1 });271const fs1 = get(v[1]);272await deleteSnapshot({ ...v[1], snapshot: fs1.snapshots[0] });273274// do more edits and make another snapshot275await writeFile(join(filesystemMountpoint(v[1]), "a3.txt"), "hello3");276await createSnapshot({ ...v[1], force: true });277const snapshots1 = get(v[1]).snapshots;278279// sync280setContext({ prefix: prefix2 });281await pull({ remote, prefix: prefix1 });282283// snapshots do NOT initially match, since we didn't enable snapshot deleting!284let snapshots2 = get(v[1]).snapshots;285expect(snapshots1).not.toEqual(snapshots2);286287await pull({ remote, prefix: prefix1, deleteSnapshots: true });288// now snapshots should match exactly!289snapshots2 = get(v[1]).snapshots;290expect(snapshots1).toEqual(snapshots2);291});292293it("test directly pulling one filesystem, rather than doing a full sync", async () => {294setContext({ prefix: prefix1 });295await writeFile(join(filesystemMountpoint(v[1]), "a3.txt"), "hello2");296await createSnapshot({ ...v[1], force: true });297await writeFile(join(filesystemMountpoint(v[2]), "a4.txt"), "hello");298await createSnapshot({ ...v[2], force: true });299const p = v.map((x) => get(x));300301setContext({ prefix: prefix2 });302await pull({ remote, prefix: prefix1, filesystem: v[1] });303const p2 = v.map((x) => get(x));304305// now filesystem 1 should match, but not filesystem 2306expect(p[1].snapshots).toEqual(p2[1].snapshots);307expect(p[2].snapshots).not.toEqual(p2[2].snapshots);308309// finally a full sync will get filesystem 2310await pull({ remote, prefix: prefix1 });311const p2b = v.map((x) => get(x));312expect(p[2].snapshots).toEqual(p2b[2].snapshots);313});314});315316317