kiwivm-cli 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -36
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +370 -50
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/commands/admin.test.ts +135 -22
- package/src/commands/admin.ts +57 -19
- package/src/commands/backup.test.ts +26 -23
- package/src/commands/backup.ts +13 -15
- package/src/commands/help.test.ts +27 -7
- package/src/commands/help.ts +61 -26
- package/src/commands/info.test.ts +47 -43
- package/src/commands/info.ts +11 -13
- package/src/commands/iso.test.ts +58 -0
- package/src/commands/iso.ts +21 -0
- package/src/commands/migrate.test.ts +105 -0
- package/src/commands/migrate.ts +38 -0
- package/src/commands/network.test.ts +107 -30
- package/src/commands/network.ts +56 -18
- package/src/commands/power.test.ts +58 -40
- package/src/commands/power.ts +27 -16
- package/src/commands/shell.test.ts +66 -0
- package/src/commands/shell.ts +25 -0
- package/src/commands/snapshot.test.ts +141 -71
- package/src/commands/snapshot.ts +85 -33
- package/src/commands/stats.test.ts +81 -0
- package/src/commands/stats.ts +25 -0
- package/src/commands/system.test.ts +109 -40
- package/src/commands/system.ts +55 -23
- package/src/index.test.ts +435 -148
- package/src/index.ts +129 -57
- package/src/types.ts +57 -1
- package/dist/admin-fOud1ZmX.mjs +0 -15
- package/dist/admin-fOud1ZmX.mjs.map +0 -1
- package/dist/backup-D1UJ4aap.mjs +0 -12
- package/dist/backup-D1UJ4aap.mjs.map +0 -1
- package/dist/help-Dk-WApoi.mjs +0 -40
- package/dist/help-Dk-WApoi.mjs.map +0 -1
- package/dist/info-DKExtFYH.mjs +0 -13
- package/dist/info-DKExtFYH.mjs.map +0 -1
- package/dist/monitoring-BSuv8fj9.mjs +0 -13
- package/dist/monitoring-BSuv8fj9.mjs.map +0 -1
- package/dist/network-1ycEIJqT.mjs +0 -15
- package/dist/network-1ycEIJqT.mjs.map +0 -1
- package/dist/power-CDg0Mx1A.mjs +0 -14
- package/dist/power-CDg0Mx1A.mjs.map +0 -1
- package/dist/snapshot-LO_ufoj5.mjs +0 -23
- package/dist/snapshot-LO_ufoj5.mjs.map +0 -1
- package/dist/system-Bl-dsqX9.mjs +0 -21
- package/dist/system-Bl-dsqX9.mjs.map +0 -1
- package/src/commands/monitoring.test.ts +0 -82
- package/src/commands/monitoring.ts +0 -20
package/src/commands/help.ts
CHANGED
|
@@ -1,32 +1,67 @@
|
|
|
1
|
-
const HELP = `Usage: kiwivm-cli <
|
|
1
|
+
const HELP = `Usage: kiwivm-cli <command> [<subcommand>] [args...] [--flags...]
|
|
2
2
|
|
|
3
3
|
Auth: --veid <VEID> --api-key <KEY> (or KIWIVM_VEID / KIWIVM_API_KEY env vars)
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
5
|
+
Commands:
|
|
6
|
+
|
|
7
|
+
start Start the VPS
|
|
8
|
+
stop Stop the VPS
|
|
9
|
+
restart Reboot the VPS
|
|
10
|
+
kill Force-stop a stuck VPS
|
|
11
|
+
|
|
12
|
+
info Get service info (plan, IPs, bandwidth, etc.)
|
|
13
|
+
status Get live status (CPU, RAM, disk, uptime)
|
|
14
|
+
|
|
15
|
+
snapshot list List all snapshots
|
|
16
|
+
snapshot create [--desc] Create a new snapshot
|
|
17
|
+
snapshot delete <token> Delete a snapshot
|
|
18
|
+
snapshot restore <token> Restore from snapshot
|
|
19
|
+
snapshot sticky <token> Toggle sticky (--on | --off)
|
|
20
|
+
snapshot export <token> Generate transfer token
|
|
21
|
+
snapshot import <veid> <token> Import snapshot from another instance
|
|
22
|
+
|
|
23
|
+
backup list List automatic backups
|
|
24
|
+
backup copy <token> Convert backup to restorable snapshot
|
|
25
|
+
|
|
26
|
+
os list List available OS templates
|
|
27
|
+
os reinstall <template> Reinstall OS
|
|
28
|
+
|
|
29
|
+
hostname <name> Set new hostname
|
|
30
|
+
password Reset root password
|
|
31
|
+
|
|
32
|
+
ssh-key Show SSH keys
|
|
33
|
+
ssh-key set <keys> Set SSH keys
|
|
34
|
+
|
|
35
|
+
rdns set <ip> <ptr> Set reverse DNS record
|
|
36
|
+
|
|
37
|
+
ipv6 add Assign new IPv6 /64 subnet
|
|
38
|
+
ipv6 delete <subnet> Release IPv6 subnet
|
|
39
|
+
|
|
40
|
+
private-ip list List available private IPs
|
|
41
|
+
private-ip assign [<ip>] Assign private IP (random if omitted)
|
|
42
|
+
private-ip delete <ip> Release private IP
|
|
43
|
+
|
|
44
|
+
iso mount <name> Mount ISO for boot (VM must be off)
|
|
45
|
+
iso unmount Unmount ISO, boot from disk
|
|
46
|
+
|
|
47
|
+
shell exec <command> Execute command synchronously
|
|
48
|
+
shell script <script> Execute script asynchronously
|
|
49
|
+
|
|
50
|
+
migrate locations List available migration locations
|
|
51
|
+
migrate start <location> Start migration to new location
|
|
52
|
+
clone <ip> <password> [--port] Clone from external server (OVZ only)
|
|
53
|
+
|
|
54
|
+
stats usage Get detailed usage statistics
|
|
55
|
+
stats audit Get audit log
|
|
56
|
+
stats rate-limit Check API rate limit status
|
|
57
|
+
|
|
58
|
+
suspensions View suspension details
|
|
59
|
+
unsuspend <record-id> Clear abuse issue and unsuspend
|
|
60
|
+
violations View policy violations
|
|
61
|
+
violations resolve <record-id> Resolve policy violation
|
|
62
|
+
|
|
63
|
+
notifications Get notification preferences
|
|
64
|
+
notifications set <json> Update notification preferences
|
|
30
65
|
|
|
31
66
|
Output: JSON to stdout. Errors to stderr with exit code 1.
|
|
32
67
|
`;
|
|
@@ -1,67 +1,71 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from "vitest";
|
|
2
2
|
import type { KiwiVMClient } from "../client.ts";
|
|
3
|
-
import {
|
|
3
|
+
import { info, status } from "./info.ts";
|
|
4
4
|
|
|
5
5
|
function mockClient() {
|
|
6
6
|
const call = vi.fn();
|
|
7
7
|
return { client: { call } as unknown as KiwiVMClient, call };
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
describe("info
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
10
|
+
describe("info handlers", () => {
|
|
11
|
+
describe("info", () => {
|
|
12
|
+
it("calls client.call('getServiceInfo')", async () => {
|
|
13
|
+
const { client, call } = mockClient();
|
|
14
|
+
call.mockResolvedValueOnce({
|
|
15
|
+
error: 0,
|
|
16
|
+
hostname: "my-vps",
|
|
17
|
+
plan_disk: 50,
|
|
18
|
+
plan_ram: 1024,
|
|
19
|
+
});
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
expect(result).toMatchObject({ hostname: "my-vps" });
|
|
22
|
-
});
|
|
21
|
+
const result = await info([], {}, client);
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
expect(call).toHaveBeenCalledExactlyOnceWith("getServiceInfo");
|
|
24
|
+
expect(result).toMatchObject({ hostname: "my-vps" });
|
|
25
|
+
});
|
|
27
26
|
|
|
28
|
-
|
|
27
|
+
it("returns the complete API response", async () => {
|
|
28
|
+
const { client, call } = mockClient();
|
|
29
|
+
const apiResponse = {
|
|
30
|
+
error: 0,
|
|
31
|
+
hostname: "my-vps",
|
|
32
|
+
plan_disk: 50,
|
|
33
|
+
plan_ram: 1024,
|
|
34
|
+
};
|
|
35
|
+
call.mockResolvedValueOnce(apiResponse);
|
|
29
36
|
|
|
30
|
-
|
|
31
|
-
});
|
|
37
|
+
const result = await info([], {}, client);
|
|
32
38
|
|
|
33
|
-
|
|
34
|
-
const { client, call } = mockClient();
|
|
35
|
-
call.mockResolvedValueOnce({
|
|
36
|
-
error: 0,
|
|
37
|
-
ve_status: "Running",
|
|
39
|
+
expect(result).toEqual(apiResponse);
|
|
38
40
|
});
|
|
39
41
|
|
|
40
|
-
|
|
42
|
+
it("propagates errors from the client", async () => {
|
|
43
|
+
const { client, call } = mockClient();
|
|
44
|
+
call.mockRejectedValueOnce(new Error("Network error"));
|
|
41
45
|
|
|
42
|
-
|
|
43
|
-
|
|
46
|
+
await expect(info([], {}, client)).rejects.toThrow("Network error");
|
|
47
|
+
});
|
|
44
48
|
});
|
|
45
49
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
};
|
|
54
|
-
call.mockResolvedValueOnce(apiResponse);
|
|
50
|
+
describe("status", () => {
|
|
51
|
+
it("calls client.call('getLiveServiceInfo')", async () => {
|
|
52
|
+
const { client, call } = mockClient();
|
|
53
|
+
call.mockResolvedValueOnce({
|
|
54
|
+
error: 0,
|
|
55
|
+
ve_status: "Running",
|
|
56
|
+
});
|
|
55
57
|
|
|
56
|
-
|
|
58
|
+
const result = await status([], {}, client);
|
|
57
59
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
+
expect(call).toHaveBeenCalledExactlyOnceWith("getLiveServiceInfo");
|
|
61
|
+
expect(result).toMatchObject({ ve_status: "Running" });
|
|
62
|
+
});
|
|
60
63
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
+
it("propagates errors from the client", async () => {
|
|
65
|
+
const { client, call } = mockClient();
|
|
66
|
+
call.mockRejectedValueOnce(new Error("Network error"));
|
|
64
67
|
|
|
65
|
-
|
|
68
|
+
await expect(status([], {}, client)).rejects.toThrow("Network error");
|
|
69
|
+
});
|
|
66
70
|
});
|
|
67
71
|
});
|
package/src/commands/info.ts
CHANGED
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
import type { KiwiVMClient } from "../client.ts";
|
|
2
2
|
import type { LiveServiceInfo, ServiceInfo } from "../types.ts";
|
|
3
3
|
|
|
4
|
-
export async function
|
|
5
|
-
|
|
4
|
+
export async function info(
|
|
5
|
+
_args: string[],
|
|
6
6
|
_flags: Record<string, string>,
|
|
7
7
|
client: KiwiVMClient,
|
|
8
8
|
): Promise<unknown> {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
);
|
|
19
|
-
}
|
|
9
|
+
return client.call<ServiceInfo>("getServiceInfo");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function status(
|
|
13
|
+
_args: string[],
|
|
14
|
+
_flags: Record<string, string>,
|
|
15
|
+
client: KiwiVMClient,
|
|
16
|
+
): Promise<unknown> {
|
|
17
|
+
return client.call<LiveServiceInfo>("getLiveServiceInfo");
|
|
20
18
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { KiwiVMClient } from "../client.ts";
|
|
3
|
+
import { mount, unmount } from "./iso.ts";
|
|
4
|
+
|
|
5
|
+
function mockClient() {
|
|
6
|
+
const call = vi.fn();
|
|
7
|
+
return { client: { call } as unknown as KiwiVMClient, call };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe("iso handlers", () => {
|
|
11
|
+
describe("mount", () => {
|
|
12
|
+
it("calls iso/mount with iso name from args", async () => {
|
|
13
|
+
const { client, call } = mockClient();
|
|
14
|
+
call.mockResolvedValueOnce({ error: 0 });
|
|
15
|
+
|
|
16
|
+
const result = await mount(["ubuntu.iso"], {}, client);
|
|
17
|
+
|
|
18
|
+
expect(call).toHaveBeenCalledExactlyOnceWith("iso/mount", {
|
|
19
|
+
iso: "ubuntu.iso",
|
|
20
|
+
});
|
|
21
|
+
expect(result).toEqual({ error: 0 });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("throws when no ISO name provided", async () => {
|
|
25
|
+
const { client } = mockClient();
|
|
26
|
+
|
|
27
|
+
await expect(mount([], {}, client)).rejects.toThrow(/name/);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("propagates errors from the client", async () => {
|
|
31
|
+
const { client, call } = mockClient();
|
|
32
|
+
call.mockRejectedValueOnce(new Error("Mount failed"));
|
|
33
|
+
|
|
34
|
+
await expect(mount(["ubuntu.iso"], {}, client)).rejects.toThrow(
|
|
35
|
+
"Mount failed",
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("unmount", () => {
|
|
41
|
+
it("calls iso/unmount with no params", async () => {
|
|
42
|
+
const { client, call } = mockClient();
|
|
43
|
+
call.mockResolvedValueOnce({ error: 0 });
|
|
44
|
+
|
|
45
|
+
const result = await unmount([], {}, client);
|
|
46
|
+
|
|
47
|
+
expect(call).toHaveBeenCalledExactlyOnceWith("iso/unmount");
|
|
48
|
+
expect(result).toEqual({ error: 0 });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("propagates errors from the client", async () => {
|
|
52
|
+
const { client, call } = mockClient();
|
|
53
|
+
call.mockRejectedValueOnce(new Error("Unmount failed"));
|
|
54
|
+
|
|
55
|
+
await expect(unmount([], {}, client)).rejects.toThrow("Unmount failed");
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { KiwiVMClient } from "../client.ts";
|
|
2
|
+
|
|
3
|
+
export async function mount(
|
|
4
|
+
args: string[],
|
|
5
|
+
_flags: Record<string, string>,
|
|
6
|
+
client: KiwiVMClient,
|
|
7
|
+
): Promise<unknown> {
|
|
8
|
+
const iso = args[0];
|
|
9
|
+
if (!iso) {
|
|
10
|
+
throw new Error("iso mount requires a <name> argument");
|
|
11
|
+
}
|
|
12
|
+
return client.call("iso/mount", { iso });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function unmount(
|
|
16
|
+
_args: string[],
|
|
17
|
+
_flags: Record<string, string>,
|
|
18
|
+
client: KiwiVMClient,
|
|
19
|
+
): Promise<unknown> {
|
|
20
|
+
return client.call("iso/unmount");
|
|
21
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { KiwiVMClient } from "../client.ts";
|
|
3
|
+
import { clone, locations, migrateStart } from "./migrate.ts";
|
|
4
|
+
|
|
5
|
+
function mockClient() {
|
|
6
|
+
const call = vi.fn();
|
|
7
|
+
return { client: { call } as unknown as KiwiVMClient, call };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe("migrate handlers", () => {
|
|
11
|
+
describe("locations", () => {
|
|
12
|
+
it("calls migrate/getLocations", async () => {
|
|
13
|
+
const { client, call } = mockClient();
|
|
14
|
+
call.mockResolvedValueOnce({
|
|
15
|
+
error: 0,
|
|
16
|
+
locations: ["lasvegas", "newyork", "luxembourg"],
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const result = await locations([], {}, client);
|
|
20
|
+
|
|
21
|
+
expect(call).toHaveBeenCalledExactlyOnceWith("migrate/getLocations");
|
|
22
|
+
expect(result).toMatchObject({
|
|
23
|
+
locations: ["lasvegas", "newyork", "luxembourg"],
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("propagates errors from the client", async () => {
|
|
28
|
+
const { client, call } = mockClient();
|
|
29
|
+
call.mockRejectedValueOnce(new Error("API failure"));
|
|
30
|
+
|
|
31
|
+
await expect(locations([], {}, client)).rejects.toThrow("API failure");
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("migrateStart", () => {
|
|
36
|
+
it("calls migrate/start with location from args", async () => {
|
|
37
|
+
const { client, call } = mockClient();
|
|
38
|
+
call.mockResolvedValueOnce({ error: 0 });
|
|
39
|
+
|
|
40
|
+
await migrateStart(["lasvegas"], {}, client);
|
|
41
|
+
|
|
42
|
+
expect(call).toHaveBeenCalledExactlyOnceWith("migrate/start", {
|
|
43
|
+
location: "lasvegas",
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("throws when no location provided", async () => {
|
|
48
|
+
const { client } = mockClient();
|
|
49
|
+
|
|
50
|
+
await expect(migrateStart([], {}, client)).rejects.toThrow("location");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("propagates errors from the client", async () => {
|
|
54
|
+
const { client, call } = mockClient();
|
|
55
|
+
call.mockRejectedValueOnce(new Error("Migration not available"));
|
|
56
|
+
|
|
57
|
+
await expect(migrateStart(["lasvegas"], {}, client)).rejects.toThrow(
|
|
58
|
+
"Migration not available",
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("clone", () => {
|
|
64
|
+
it("calls cloneFromExternalServer with IP, default port, and password", async () => {
|
|
65
|
+
const { client, call } = mockClient();
|
|
66
|
+
call.mockResolvedValueOnce({ error: 0 });
|
|
67
|
+
|
|
68
|
+
await clone(["1.2.3.4", "mypassword"], {}, client);
|
|
69
|
+
|
|
70
|
+
expect(call).toHaveBeenCalledExactlyOnceWith("cloneFromExternalServer", {
|
|
71
|
+
externalServerIP: "1.2.3.4",
|
|
72
|
+
externalServerSSHport: "22",
|
|
73
|
+
externalServerRootPassword: "mypassword",
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("calls cloneFromExternalServer with custom port from flags", async () => {
|
|
78
|
+
const { client, call } = mockClient();
|
|
79
|
+
call.mockResolvedValueOnce({ error: 0 });
|
|
80
|
+
|
|
81
|
+
await clone(["1.2.3.4", "mypassword"], { port: "2222" }, client);
|
|
82
|
+
|
|
83
|
+
expect(call).toHaveBeenCalledExactlyOnceWith("cloneFromExternalServer", {
|
|
84
|
+
externalServerIP: "1.2.3.4",
|
|
85
|
+
externalServerSSHport: "2222",
|
|
86
|
+
externalServerRootPassword: "mypassword",
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("throws when password not provided", async () => {
|
|
91
|
+
const { client } = mockClient();
|
|
92
|
+
|
|
93
|
+
await expect(clone(["1.2.3.4"], {}, client)).rejects.toThrow("password");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("propagates errors from the client", async () => {
|
|
97
|
+
const { client, call } = mockClient();
|
|
98
|
+
call.mockRejectedValueOnce(new Error("Clone failed"));
|
|
99
|
+
|
|
100
|
+
await expect(
|
|
101
|
+
clone(["1.2.3.4", "mypassword"], {}, client),
|
|
102
|
+
).rejects.toThrow("Clone failed");
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { KiwiVMClient } from "../client.ts";
|
|
2
|
+
|
|
3
|
+
export async function locations(
|
|
4
|
+
_args: string[],
|
|
5
|
+
_flags: Record<string, string>,
|
|
6
|
+
client: KiwiVMClient,
|
|
7
|
+
): Promise<unknown> {
|
|
8
|
+
return client.call("migrate/getLocations");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function migrateStart(
|
|
12
|
+
args: string[],
|
|
13
|
+
_flags: Record<string, string>,
|
|
14
|
+
client: KiwiVMClient,
|
|
15
|
+
): Promise<unknown> {
|
|
16
|
+
const location = args[0];
|
|
17
|
+
if (!location) {
|
|
18
|
+
throw new Error("migrate start requires a <location> argument");
|
|
19
|
+
}
|
|
20
|
+
return client.call("migrate/start", { location });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function clone(
|
|
24
|
+
args: string[],
|
|
25
|
+
flags: Record<string, string>,
|
|
26
|
+
client: KiwiVMClient,
|
|
27
|
+
): Promise<unknown> {
|
|
28
|
+
const ip = args[0];
|
|
29
|
+
const password = args[1];
|
|
30
|
+
if (!ip || !password) {
|
|
31
|
+
throw new Error("clone requires both <ip> and <password> arguments");
|
|
32
|
+
}
|
|
33
|
+
return client.call("cloneFromExternalServer", {
|
|
34
|
+
externalServerIP: ip,
|
|
35
|
+
externalServerSSHport: flags["port"] || "22",
|
|
36
|
+
externalServerRootPassword: password,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -1,85 +1,162 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from "vitest";
|
|
2
2
|
import type { KiwiVMClient } from "../client.ts";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
ipv6Add,
|
|
5
|
+
ipv6Delete,
|
|
6
|
+
privateIpAssign,
|
|
7
|
+
privateIpDelete,
|
|
8
|
+
privateIpList,
|
|
9
|
+
rdnsSet,
|
|
10
|
+
} from "./network.ts";
|
|
4
11
|
|
|
5
12
|
function mockClient() {
|
|
6
13
|
const call = vi.fn();
|
|
7
14
|
return { client: { call } as unknown as KiwiVMClient, call };
|
|
8
15
|
}
|
|
9
16
|
|
|
10
|
-
describe("network
|
|
11
|
-
describe("
|
|
17
|
+
describe("network handlers", () => {
|
|
18
|
+
describe("rdnsSet", () => {
|
|
19
|
+
it("calls setPTR with ip and ptr from args", async () => {
|
|
20
|
+
const { client, call } = mockClient();
|
|
21
|
+
call.mockResolvedValueOnce({ error: 0 });
|
|
22
|
+
|
|
23
|
+
await rdnsSet(["1.2.3.4", "my.domain.com"], {}, client);
|
|
24
|
+
|
|
25
|
+
expect(call).toHaveBeenCalledExactlyOnceWith("setPTR", {
|
|
26
|
+
ip: "1.2.3.4",
|
|
27
|
+
ptr: "my.domain.com",
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("propagates errors from the client", async () => {
|
|
32
|
+
const { client, call } = mockClient();
|
|
33
|
+
call.mockRejectedValueOnce(new Error("Invalid PTR"));
|
|
34
|
+
|
|
35
|
+
await expect(
|
|
36
|
+
rdnsSet(["1.2.3.4", "my.domain.com"], {}, client),
|
|
37
|
+
).rejects.toThrow("Invalid PTR");
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("ipv6Add", () => {
|
|
12
42
|
it("calls ipv6/add with no extra params", async () => {
|
|
13
43
|
const { client, call } = mockClient();
|
|
14
44
|
call.mockResolvedValueOnce({ error: 0 });
|
|
15
45
|
|
|
16
|
-
const result = await
|
|
46
|
+
const result = await ipv6Add([], {}, client);
|
|
17
47
|
|
|
18
|
-
expect(
|
|
48
|
+
expect(call).toHaveBeenCalledExactlyOnceWith("ipv6/add");
|
|
19
49
|
expect(result).toEqual({ error: 0 });
|
|
20
50
|
});
|
|
51
|
+
|
|
52
|
+
it("propagates errors from the client", async () => {
|
|
53
|
+
const { client, call } = mockClient();
|
|
54
|
+
call.mockRejectedValueOnce(new Error("IPv6 not available"));
|
|
55
|
+
|
|
56
|
+
await expect(ipv6Add([], {}, client)).rejects.toThrow(
|
|
57
|
+
"IPv6 not available",
|
|
58
|
+
);
|
|
59
|
+
});
|
|
21
60
|
});
|
|
22
61
|
|
|
23
|
-
describe("
|
|
24
|
-
it("calls ipv6/delete with ip
|
|
62
|
+
describe("ipv6Delete", () => {
|
|
63
|
+
it("calls ipv6/delete with ip from args", async () => {
|
|
25
64
|
const { client, call } = mockClient();
|
|
26
65
|
call.mockResolvedValueOnce({ error: 0 });
|
|
27
66
|
|
|
28
|
-
await
|
|
67
|
+
await ipv6Delete(["2001:db8::1"], {}, client);
|
|
29
68
|
|
|
30
|
-
expect(
|
|
69
|
+
expect(call).toHaveBeenCalledExactlyOnceWith("ipv6/delete", {
|
|
31
70
|
ip: "2001:db8::1",
|
|
32
71
|
});
|
|
33
72
|
});
|
|
73
|
+
|
|
74
|
+
it("propagates errors from the client", async () => {
|
|
75
|
+
const { client, call } = mockClient();
|
|
76
|
+
call.mockRejectedValueOnce(new Error("Invalid IP"));
|
|
77
|
+
|
|
78
|
+
await expect(ipv6Delete(["2001:db8::1"], {}, client)).rejects.toThrow(
|
|
79
|
+
"Invalid IP",
|
|
80
|
+
);
|
|
81
|
+
});
|
|
34
82
|
});
|
|
35
83
|
|
|
36
|
-
describe("
|
|
84
|
+
describe("privateIpList", () => {
|
|
37
85
|
it("calls privateIp/getAvailableIps", async () => {
|
|
38
86
|
const { client, call } = mockClient();
|
|
39
87
|
call.mockResolvedValueOnce({
|
|
40
88
|
error: 0,
|
|
41
|
-
availableIps: ["10.0.0.1"],
|
|
89
|
+
availableIps: ["10.0.0.1", "10.0.0.2"],
|
|
42
90
|
});
|
|
43
91
|
|
|
44
|
-
const result = await
|
|
92
|
+
const result = await privateIpList([], {}, client);
|
|
93
|
+
|
|
94
|
+
expect(call).toHaveBeenCalledExactlyOnceWith("privateIp/getAvailableIps");
|
|
95
|
+
expect(result).toMatchObject({ availableIps: ["10.0.0.1", "10.0.0.2"] });
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("propagates errors from the client", async () => {
|
|
99
|
+
const { client, call } = mockClient();
|
|
100
|
+
call.mockRejectedValueOnce(new Error("Private IP not available"));
|
|
45
101
|
|
|
46
|
-
expect(client
|
|
47
|
-
"
|
|
102
|
+
await expect(privateIpList([], {}, client)).rejects.toThrow(
|
|
103
|
+
"Private IP not available",
|
|
48
104
|
);
|
|
49
|
-
expect(result).toMatchObject({ availableIps: ["10.0.0.1"] });
|
|
50
105
|
});
|
|
51
106
|
});
|
|
52
107
|
|
|
53
|
-
describe("
|
|
54
|
-
it("calls privateIp/assign with ip
|
|
108
|
+
describe("privateIpAssign", () => {
|
|
109
|
+
it("calls privateIp/assign with ip from args", async () => {
|
|
55
110
|
const { client, call } = mockClient();
|
|
56
111
|
call.mockResolvedValueOnce({ error: 0 });
|
|
57
112
|
|
|
58
|
-
await
|
|
113
|
+
await privateIpAssign(["10.0.0.5"], {}, client);
|
|
59
114
|
|
|
60
|
-
expect(
|
|
61
|
-
ip: "10.0.0.
|
|
115
|
+
expect(call).toHaveBeenCalledExactlyOnceWith("privateIp/assign", {
|
|
116
|
+
ip: "10.0.0.5",
|
|
62
117
|
});
|
|
63
118
|
});
|
|
64
|
-
});
|
|
65
119
|
|
|
66
|
-
|
|
67
|
-
it("calls privateIp/delete with ip flag", async () => {
|
|
120
|
+
it("calls privateIp/assign with undefined ip when no arg", async () => {
|
|
68
121
|
const { client, call } = mockClient();
|
|
69
122
|
call.mockResolvedValueOnce({ error: 0 });
|
|
70
123
|
|
|
71
|
-
await
|
|
124
|
+
await privateIpAssign([], {}, client);
|
|
72
125
|
|
|
73
|
-
expect(
|
|
74
|
-
ip:
|
|
126
|
+
expect(call).toHaveBeenCalledExactlyOnceWith("privateIp/assign", {
|
|
127
|
+
ip: undefined,
|
|
75
128
|
});
|
|
76
129
|
});
|
|
130
|
+
|
|
131
|
+
it("propagates errors from the client", async () => {
|
|
132
|
+
const { client, call } = mockClient();
|
|
133
|
+
call.mockRejectedValueOnce(new Error("Assignment failed"));
|
|
134
|
+
|
|
135
|
+
await expect(privateIpAssign(["10.0.0.5"], {}, client)).rejects.toThrow(
|
|
136
|
+
"Assignment failed",
|
|
137
|
+
);
|
|
138
|
+
});
|
|
77
139
|
});
|
|
78
140
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
141
|
+
describe("privateIpDelete", () => {
|
|
142
|
+
it("calls privateIp/delete with ip from args", async () => {
|
|
143
|
+
const { client, call } = mockClient();
|
|
144
|
+
call.mockResolvedValueOnce({ error: 0 });
|
|
145
|
+
|
|
146
|
+
await privateIpDelete(["10.0.0.5"], {}, client);
|
|
147
|
+
|
|
148
|
+
expect(call).toHaveBeenCalledExactlyOnceWith("privateIp/delete", {
|
|
149
|
+
ip: "10.0.0.5",
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("propagates errors from the client", async () => {
|
|
154
|
+
const { client, call } = mockClient();
|
|
155
|
+
call.mockRejectedValueOnce(new Error("Delete failed"));
|
|
82
156
|
|
|
83
|
-
|
|
157
|
+
await expect(privateIpDelete(["10.0.0.5"], {}, client)).rejects.toThrow(
|
|
158
|
+
"Delete failed",
|
|
159
|
+
);
|
|
160
|
+
});
|
|
84
161
|
});
|
|
85
162
|
});
|