@lazycatcloud/lzc-cli 1.1.8 → 1.1.9
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 +69 -11
- package/lib/api.js +71 -39
- package/lib/app/index.js +76 -21
- package/lib/app/lpk_build.js +95 -63
- package/lib/app/lpk_create.js +63 -41
- package/lib/app/lpk_create_generator.js +202 -0
- package/lib/app/lpk_devshell.js +393 -328
- package/lib/app/lpk_devshell_docker.js +211 -0
- package/lib/app/lpk_installer.js +63 -26
- package/lib/app/lpk_log.js +68 -0
- package/lib/app/lpk_status.js +18 -0
- package/lib/app/lpk_uninstall.js +19 -0
- package/lib/appstore/index.js +37 -0
- package/lib/appstore/login.js +96 -93
- package/lib/appstore/publish.js +62 -0
- package/lib/autologin.js +0 -78
- package/lib/box/api/clientapi.js +1322 -0
- package/lib/box/api/empty.js +35 -0
- package/lib/box/check_qemu.js +1 -0
- package/lib/box/index.js +41 -94
- package/lib/box/qemu_vm_mgr.js +208 -239
- package/lib/box/schemes/vm_box_system_debian.json +1 -1
- package/lib/docker-compose.js +1 -2
- package/lib/env.js +19 -101
- package/lib/key.js +1 -0
- package/lib/sdk.js +10 -25
- package/lib/utils.js +156 -132
- package/package.json +19 -10
- package/scripts/cli.js +14 -135
- package/template/_lpk/README.md +31 -0
- package/template/_lpk/exec.sh +19 -0
- package/template/_lpk/golang.manifest.yml.in +16 -0
- package/template/_lpk/lazycat.png +0 -0
- package/template/_lpk/lite.manifest.yml.in +19 -0
- package/template/_lpk/local_devshell/Dockerfile +16 -0
- package/template/_lpk/local_devshell/build.sh +5 -0
- package/template/_lpk/local_devshell/entrypoint.sh +87 -0
- package/template/{_lazycat/debug/shell → _lpk/local_devshell}/sshd_config +8 -8
- package/template/_lpk/manifest.yml.in +0 -1
- package/template/{vue/lzc-build.yml → _lpk/vue.lzc-build.yml.in} +9 -1
- package/template/golang/README.md +0 -2
- package/template/golang/_gitignore +2 -0
- package/template/golang/build.sh +6 -0
- package/template/golang/lazycat.png +0 -0
- package/template/golang/lzc-build.yml +9 -1
- package/template/golang/rego.go +15 -16
- package/template/golang/rego_test.go +13 -0
- package/template/ionic_vue3/lazycat.png +0 -0
- package/template/ionic_vue3/lzc-build.yml +9 -1
- package/template/lite/error_pages/502.html.tpl +13 -0
- package/template/lite/lazycat.png +0 -0
- package/template/lite/lzc-build.yml +60 -0
- package/cmds/app.js +0 -133
- package/cmds/config.js +0 -55
- package/cmds/create.js +0 -55
- package/cmds/dev.js +0 -130
- package/cmds/init.js +0 -125
- package/cmds/log.js +0 -103
- package/cmds/publish.js +0 -116
- package/lib/archiver.js +0 -105
- package/lib/box/hportal.js +0 -120
- package/lib/builder.js +0 -313
- package/lib/dev.js +0 -314
- package/lib/generator.js +0 -146
- package/template/_lazycat/_gitignore +0 -1
- package/template/_lazycat/app-config +0 -1
- package/template/_lazycat/debug/devforward/50x.html +0 -30
- package/template/_lazycat/debug/devforward/Dockerfile +0 -16
- package/template/_lazycat/debug/devforward/docker-compose.override.yml.in +0 -11
- package/template/_lazycat/debug/devforward/entrypoint.sh +0 -10
- package/template/_lazycat/debug/devforward/nginx.conf.template +0 -56
- package/template/_lazycat/debug/devforward/sshd_config +0 -116
- package/template/_lazycat/debug/shell/50x.html +0 -32
- package/template/_lazycat/debug/shell/Dockerfile +0 -18
- package/template/_lazycat/debug/shell/build.sh +0 -15
- package/template/_lazycat/debug/shell/docker-compose.override.yml.in +0 -13
- package/template/_lazycat/debug/shell/entrypoint.sh +0 -12
- package/template/_lazycat/docker-compose.yml.in +0 -15
- package/template/_lazycat/icon.svg +0 -1
- package/template/_lazycat/screenshot.png +0 -0
- package/template/_lpk/sync/Dockerfile +0 -16
- package/template/_lpk/sync/build.sh +0 -5
- package/template/_lpk/sync/entrypoint.sh +0 -8
- package/template/_lpk/sync/sshd_config +0 -117
- package/template/_lpk/sync.manifest.yml.in +0 -3
- package/template/release/golang/Dockerfile +0 -18
- package/template/release/golang/build.sh +0 -13
- package/template/release/ionic_vue3/Dockerfile +0 -10
- package/template/release/ionic_vue3/build.sh +0 -7
- package/template/release/ionic_vue3/docker-compose.yml.in +0 -3
- package/template/release/vue/Dockerfile +0 -10
- package/template/release/vue/build.sh +0 -10
- package/template/release/vue/docker-compose.yml.in +0 -3
- package/template/vue/README.md +0 -29
- package/template/vue/_dockerignore +0 -1
- package/template/vue/babel.config.js +0 -3
- package/template/vue/package.json +0 -43
- package/template/vue/public/favicon.ico +0 -0
- package/template/vue/public/index.html +0 -33
- package/template/vue/src/App.vue +0 -39
- package/template/vue/src/main.js +0 -8
- package/template/vue/src/todo.vue +0 -640
- package/template/vue/src/top-bar.vue +0 -100
- package/template/vue/src/webdav.vue +0 -183
- package/template/vue/vue.config.js +0 -21
package/lib/box/qemu_vm_mgr.js
CHANGED
|
@@ -3,14 +3,36 @@ import fs from "node:fs";
|
|
|
3
3
|
import { spawn, spawnSync } from "node:child_process";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import inquirer from "inquirer";
|
|
6
|
-
import https from "node:https";
|
|
7
|
-
import http from "node:http";
|
|
8
6
|
import process from "node:process";
|
|
9
7
|
import net from "node:net";
|
|
10
8
|
import fetch from "node-fetch";
|
|
11
|
-
import zlib from "node:zlib";
|
|
12
9
|
import os from "node:os";
|
|
13
10
|
import logger from "loglevel";
|
|
11
|
+
import glob from "fast-glob";
|
|
12
|
+
import env from "../env.js";
|
|
13
|
+
|
|
14
|
+
import { Downloader, envsubstrDefault, contextDirname } from "../utils.js";
|
|
15
|
+
import { ShellCoreClientImpl, GrpcWebImpl } from "./api/clientapi.js";
|
|
16
|
+
|
|
17
|
+
const cc = new ShellCoreClientImpl(
|
|
18
|
+
new GrpcWebImpl("http://127.0.0.1:30553", {})
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const remoteBox = "远程盒子";
|
|
22
|
+
const unRegistry = "未注册";
|
|
23
|
+
|
|
24
|
+
function getBoxStatus(status) {
|
|
25
|
+
switch (status) {
|
|
26
|
+
case 0:
|
|
27
|
+
return "未激活";
|
|
28
|
+
case 1:
|
|
29
|
+
return "连接中";
|
|
30
|
+
case 2:
|
|
31
|
+
return "已连接";
|
|
32
|
+
case 3:
|
|
33
|
+
return "无法连接";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
14
36
|
|
|
15
37
|
async function getFreePort() {
|
|
16
38
|
return new Promise((resolve, reject) => {
|
|
@@ -34,9 +56,18 @@ function parseVmPID(vmDir) {
|
|
|
34
56
|
}
|
|
35
57
|
}
|
|
36
58
|
|
|
59
|
+
function isDefault(boxName) {
|
|
60
|
+
const defaultBoxName = env.get("DEFAULT_BOXNAME");
|
|
61
|
+
return boxName == defaultBoxName ? "yes" : "no";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isRunning(vmname) {
|
|
65
|
+
return parseVmPID(vmname) ? "running" : "stop";
|
|
66
|
+
}
|
|
67
|
+
|
|
37
68
|
export class QemuResource {
|
|
38
69
|
/**
|
|
39
|
-
* diskDir qemu 磁盘存储路径,默认 ~/.
|
|
70
|
+
* diskDir qemu 磁盘存储路径,默认 ~/.cache/box-emulator
|
|
40
71
|
* downloadUrl system-disk 下载路径
|
|
41
72
|
*/
|
|
42
73
|
constructor(
|
|
@@ -44,13 +75,26 @@ export class QemuResource {
|
|
|
44
75
|
downloadUrl = "https://dl.lazycat.cloud/sdk"
|
|
45
76
|
) {
|
|
46
77
|
this.diskDir = diskDir;
|
|
47
|
-
|
|
78
|
+
|
|
79
|
+
this.downloadUrlRoot = downloadUrl;
|
|
80
|
+
this.downloadUrl = undefined;
|
|
81
|
+
|
|
82
|
+
this.downloader = new Downloader();
|
|
48
83
|
}
|
|
49
84
|
|
|
50
85
|
async init() {
|
|
86
|
+
// fetch 最新的稳定版
|
|
87
|
+
const res = await fetch(`${this.downloadUrlRoot}/vm/index.json`);
|
|
88
|
+
if (res.status !== 200) {
|
|
89
|
+
throw "获取最新的版本信息出错";
|
|
90
|
+
}
|
|
91
|
+
const body = await res.json();
|
|
92
|
+
const version = body["stable-latest"];
|
|
93
|
+
this.downloadUrl = `${this.downloadUrlRoot}/vm/${version}`;
|
|
94
|
+
|
|
51
95
|
fs.mkdirSync(this.diskDir, { recursive: true });
|
|
52
|
-
let
|
|
53
|
-
if (
|
|
96
|
+
let { update, force } = await this.shouldUpdate();
|
|
97
|
+
if (!force && update) {
|
|
54
98
|
let answer = await inquirer.prompt([
|
|
55
99
|
{
|
|
56
100
|
type: "confirm",
|
|
@@ -60,10 +104,11 @@ export class QemuResource {
|
|
|
60
104
|
},
|
|
61
105
|
]);
|
|
62
106
|
if (!answer.yesorno) {
|
|
63
|
-
|
|
107
|
+
update = false;
|
|
64
108
|
}
|
|
65
109
|
}
|
|
66
|
-
await this.ensureSystemDisk(
|
|
110
|
+
await this.ensureSystemDisk(update);
|
|
111
|
+
return this;
|
|
67
112
|
}
|
|
68
113
|
|
|
69
114
|
/**
|
|
@@ -79,24 +124,24 @@ export class QemuResource {
|
|
|
79
124
|
try {
|
|
80
125
|
fs.accessSync(oldMd5Path);
|
|
81
126
|
} catch {
|
|
82
|
-
return true;
|
|
127
|
+
return { update: true, force: true };
|
|
83
128
|
}
|
|
84
129
|
|
|
85
130
|
// 拉取最新的 md5
|
|
86
131
|
let newMd5;
|
|
87
132
|
try {
|
|
88
|
-
let url = `${this.downloadUrl}/
|
|
133
|
+
let url = `${this.downloadUrl}/disk-system.qcow2.md5`;
|
|
89
134
|
let res = await fetch(url);
|
|
90
135
|
if (res.status !== 200) {
|
|
91
|
-
return false;
|
|
136
|
+
return { update: false };
|
|
92
137
|
}
|
|
93
138
|
newMd5 = await res.text();
|
|
94
139
|
} catch {
|
|
95
|
-
return false;
|
|
140
|
+
return { update: false };
|
|
96
141
|
}
|
|
97
142
|
|
|
98
143
|
let oldMd5 = fs.readFileSync(oldMd5Path, { encoding: "utf-8" });
|
|
99
|
-
return oldMd5 !== newMd5;
|
|
144
|
+
return { update: oldMd5 !== newMd5 };
|
|
100
145
|
}
|
|
101
146
|
|
|
102
147
|
/**
|
|
@@ -120,95 +165,64 @@ export class QemuResource {
|
|
|
120
165
|
async downloadOVMF() {
|
|
121
166
|
let name = `${os.arch()}.OVMF.fd`;
|
|
122
167
|
let savePath = path.join(this.diskDir, name);
|
|
123
|
-
let url = `${this.
|
|
124
|
-
await this.download(url, savePath);
|
|
168
|
+
let url = `${this.downloadUrlRoot}/${name}`;
|
|
169
|
+
await this.downloader.download(url, savePath);
|
|
125
170
|
}
|
|
126
171
|
|
|
127
172
|
async downloadSystemDiskMd5() {
|
|
128
173
|
let savePath = path.join(this.diskDir, "disk-system.qcow2.md5");
|
|
129
|
-
let url = `${this.downloadUrl}/
|
|
130
|
-
await this.download(url, savePath);
|
|
174
|
+
let url = `${this.downloadUrl}/disk-system.qcow2.md5`;
|
|
175
|
+
await this.downloader.download(url, savePath);
|
|
131
176
|
}
|
|
132
177
|
|
|
133
178
|
async downloadSystemDisk() {
|
|
134
179
|
let savePath = path.join(this.diskDir, "disk-system.qcow2");
|
|
135
|
-
let url = `${this.downloadUrl}/
|
|
136
|
-
await this.download(url, savePath, true);
|
|
180
|
+
let url = `${this.downloadUrl}/disk-system.qcow2.gz`;
|
|
181
|
+
await this.downloader.download(url, savePath, true);
|
|
137
182
|
}
|
|
138
183
|
|
|
139
184
|
async downloadPrivateKey() {
|
|
140
185
|
let savePath = path.join(this.diskDir, "id_rsa");
|
|
141
|
-
let url = `${this.downloadUrl}/
|
|
142
|
-
await this.download(url, savePath);
|
|
186
|
+
let url = `${this.downloadUrl}/id_rsa`;
|
|
187
|
+
await this.downloader.download(url, savePath);
|
|
143
188
|
fs.chmodSync(savePath, 0o400);
|
|
144
189
|
return;
|
|
145
190
|
}
|
|
146
|
-
|
|
147
|
-
showDownloadingProgress(received, total) {
|
|
148
|
-
var percentage = ((received * 100) / total).toFixed(2);
|
|
149
|
-
process.stdout.write("\r");
|
|
150
|
-
process.stdout.write(
|
|
151
|
-
percentage +
|
|
152
|
-
"% | " +
|
|
153
|
-
received +
|
|
154
|
-
" bytes downloaded out of " +
|
|
155
|
-
total +
|
|
156
|
-
" bytes."
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
download(url, savePath, enableGzip = false) {
|
|
161
|
-
let tmpPath = savePath + ".tmp";
|
|
162
|
-
const options = new URL(url);
|
|
163
|
-
let request = url.startsWith("https") ? https.request : http.request;
|
|
164
|
-
|
|
165
|
-
return new Promise((resolve, reject) => {
|
|
166
|
-
const req = request(options, (res) => {
|
|
167
|
-
if (res.statusCode != 200) {
|
|
168
|
-
reject(`下载 ${url} 失败`);
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
let total = parseInt(res.headers["content-length"]);
|
|
173
|
-
let recive = 0;
|
|
174
|
-
res.on("data", (chunk) => {
|
|
175
|
-
recive += chunk.length;
|
|
176
|
-
this.showDownloadingProgress(recive, total);
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
let outputFile = fs.createWriteStream(tmpPath, { flags: "w+" });
|
|
180
|
-
if (enableGzip) {
|
|
181
|
-
let decoder = zlib.createUnzip();
|
|
182
|
-
res.pipe(decoder).pipe(outputFile);
|
|
183
|
-
} else {
|
|
184
|
-
res.pipe(outputFile);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
outputFile.on("error", reject);
|
|
188
|
-
outputFile.on("finish", () => {
|
|
189
|
-
fs.renameSync(tmpPath, savePath);
|
|
190
|
-
resolve();
|
|
191
|
-
});
|
|
192
|
-
});
|
|
193
|
-
req.on("error", reject);
|
|
194
|
-
req.end();
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
191
|
}
|
|
198
192
|
|
|
199
193
|
export class QemuVM {
|
|
200
194
|
/**
|
|
201
195
|
* @param {Object} scheme - 各个操作系统的配置文件.
|
|
202
196
|
*/
|
|
203
|
-
constructor(
|
|
197
|
+
constructor() {
|
|
198
|
+
this.scheme = undefined;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async init() {
|
|
202
|
+
let defaultSchemeFile = path.resolve(
|
|
203
|
+
contextDirname(import.meta.url),
|
|
204
|
+
"schemes",
|
|
205
|
+
"vm_box_system_debian.json"
|
|
206
|
+
);
|
|
207
|
+
const content = await envsubstrDefault(
|
|
208
|
+
fs.readFileSync(defaultSchemeFile, "utf8"),
|
|
209
|
+
process.env
|
|
210
|
+
);
|
|
211
|
+
let scheme = JSON.parse(content);
|
|
212
|
+
if (scheme.path && !scheme.path.startsWith("/")) {
|
|
213
|
+
scheme.path = path.resolve(scheme.path);
|
|
214
|
+
}
|
|
215
|
+
fs.mkdirSync(scheme.path, { recursive: true });
|
|
216
|
+
|
|
204
217
|
this.scheme = scheme;
|
|
218
|
+
return this;
|
|
205
219
|
}
|
|
206
220
|
|
|
207
221
|
/**
|
|
208
222
|
* 运行虚拟盒子,并等待 boxid 的出现,确保盒子启动正常
|
|
209
223
|
*/
|
|
210
|
-
async startVM(
|
|
211
|
-
let args = await this.buildQemuArgs(
|
|
224
|
+
async startVM(vmname) {
|
|
225
|
+
let args = await this.buildQemuArgs(vmname);
|
|
212
226
|
let p = spawn("qemu-system-x86_64", args, {
|
|
213
227
|
detached: true,
|
|
214
228
|
stdio: ["pipe", "ignore", "inherit"],
|
|
@@ -216,7 +230,7 @@ export class QemuVM {
|
|
|
216
230
|
p.on("error", (e) => {
|
|
217
231
|
throw e;
|
|
218
232
|
});
|
|
219
|
-
logger.
|
|
233
|
+
logger.info("启动中...");
|
|
220
234
|
|
|
221
235
|
// 需要等待 boxid 的出现
|
|
222
236
|
return new Promise((resolve, reject) => {
|
|
@@ -235,7 +249,7 @@ export class QemuVM {
|
|
|
235
249
|
return;
|
|
236
250
|
}
|
|
237
251
|
|
|
238
|
-
let boxId = this.readBoxid(
|
|
252
|
+
let boxId = this.readBoxid(vmname);
|
|
239
253
|
if (boxId) {
|
|
240
254
|
clearInterval(id);
|
|
241
255
|
p.unref();
|
|
@@ -258,88 +272,37 @@ export class QemuVM {
|
|
|
258
272
|
* @param {string} name - 盒子的名称
|
|
259
273
|
* @return {string} id - 盒子的id
|
|
260
274
|
**/
|
|
261
|
-
async runVM(
|
|
262
|
-
|
|
263
|
-
if (!
|
|
264
|
-
logger.info(`${name} 盒子不存在,请通过 lzc-cli box create 创建`);
|
|
275
|
+
async runVM(nameOrBoxId) {
|
|
276
|
+
const { vmname, name, id } = await this.findVmname(nameOrBoxId);
|
|
277
|
+
if (!vmname) {
|
|
278
|
+
logger.info(`${name || id} 盒子不存在,请通过 lzc-cli box create 创建`);
|
|
265
279
|
return;
|
|
266
280
|
}
|
|
267
|
-
let
|
|
268
|
-
let pid = parseVmPID(vmDir);
|
|
281
|
+
let pid = parseVmPID(vmname);
|
|
269
282
|
if (pid) {
|
|
270
|
-
|
|
271
|
-
return boxid;
|
|
283
|
+
return id;
|
|
272
284
|
}
|
|
273
|
-
await this.startVM(
|
|
274
|
-
logger.info(`${name} 盒子启动成功!`);
|
|
285
|
+
await this.startVM(vmname);
|
|
286
|
+
logger.info(`${name || id} 盒子启动成功!`);
|
|
275
287
|
}
|
|
276
288
|
|
|
277
289
|
/**
|
|
278
290
|
* 创建一个指定的虚拟机,并启动等待注册成功。
|
|
279
291
|
**/
|
|
280
|
-
async createVM(
|
|
281
|
-
|
|
292
|
+
async createVM(resourcer, name) {
|
|
293
|
+
const vmDir = this.ensureVmDir(name);
|
|
282
294
|
this.ensureVolumeDir(vmDir);
|
|
283
295
|
await this.buildDisks(vmDir);
|
|
284
|
-
let boxId = await this.startVM(boxName, vmDir);
|
|
285
|
-
logger.info("盒子ID: ", boxId);
|
|
286
296
|
|
|
287
|
-
await this.
|
|
288
|
-
boxName,
|
|
289
|
-
adminName,
|
|
290
|
-
adminPass,
|
|
291
|
-
});
|
|
297
|
+
let boxId = await this.startVM(path.basename(vmDir));
|
|
292
298
|
return boxId;
|
|
293
299
|
}
|
|
294
300
|
|
|
295
|
-
/**
|
|
296
|
-
* 请求用户输入盒子的基本信息
|
|
297
|
-
**/
|
|
298
|
-
async askBoxInfo() {
|
|
299
|
-
const noEmpty = (value) => value != "";
|
|
300
|
-
|
|
301
|
-
return inquirer.prompt([
|
|
302
|
-
{
|
|
303
|
-
type: "input",
|
|
304
|
-
name: "boxName",
|
|
305
|
-
message: "请输入盒子名称:",
|
|
306
|
-
validate: noEmpty,
|
|
307
|
-
},
|
|
308
|
-
{
|
|
309
|
-
type: "input",
|
|
310
|
-
name: "adminName",
|
|
311
|
-
message: "请输入盒子管理员名称:",
|
|
312
|
-
default: "admin",
|
|
313
|
-
},
|
|
314
|
-
{
|
|
315
|
-
type: "password",
|
|
316
|
-
name: "adminPass",
|
|
317
|
-
message: "请输入盒子管理员密码:",
|
|
318
|
-
mask: "*",
|
|
319
|
-
validate: noEmpty,
|
|
320
|
-
},
|
|
321
|
-
]);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
/**
|
|
325
|
-
* 调用 hportal client 进行注册
|
|
326
|
-
**/
|
|
327
|
-
async registerVM(boxid, { boxName, adminName, adminPass }) {
|
|
328
|
-
// prettier-ignore
|
|
329
|
-
spawnSync("home-portal-client",[
|
|
330
|
-
"-setup", "-boxid", boxid,
|
|
331
|
-
"-boxname", boxName,
|
|
332
|
-
"-user", adminName,
|
|
333
|
-
"-password", adminPass,
|
|
334
|
-
], {stdio: "inherit"})
|
|
335
|
-
return boxName;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
301
|
/**
|
|
339
302
|
* 根据虚拟机名称确保缓存文件夹存在,返回对应的虚拟机缓存文件夹
|
|
340
303
|
*/
|
|
341
304
|
ensureVmDir(name) {
|
|
342
|
-
|
|
305
|
+
const vmDir = path.resolve(this.scheme.path, `vm-${name}`);
|
|
343
306
|
fs.mkdirSync(vmDir, { recursive: true });
|
|
344
307
|
return vmDir;
|
|
345
308
|
}
|
|
@@ -413,12 +376,13 @@ export class QemuVM {
|
|
|
413
376
|
/**
|
|
414
377
|
* 构建 qemu 启动的参数
|
|
415
378
|
*/
|
|
416
|
-
async buildQemuArgs(
|
|
379
|
+
async buildQemuArgs(vmname) {
|
|
380
|
+
const vmDir = path.resolve(this.scheme.path, vmname);
|
|
417
381
|
let sshPort = await getFreePort();
|
|
418
382
|
let bios = this.findBIOS();
|
|
419
383
|
// prettier-ignore
|
|
420
384
|
let args = [
|
|
421
|
-
"-name", `${this.scheme.name}-${this.scheme.uuid}
|
|
385
|
+
"-name", `${this.scheme.name}-${this.scheme.uuid}-${vmname}`,
|
|
422
386
|
"-machine", "pc,accel=kvm,accel=kvf,accel=xen,accel=hax,accel=nvmm,accel=whpx,accel=tcg",
|
|
423
387
|
"-m", `${this.scheme.memory}`,
|
|
424
388
|
"-smp", "4,sockets=4,cores=1,threads=1",
|
|
@@ -454,21 +418,21 @@ export class QemuVM {
|
|
|
454
418
|
/**
|
|
455
419
|
* 停止虚拟盒子
|
|
456
420
|
*/
|
|
457
|
-
async stopVM(
|
|
458
|
-
|
|
459
|
-
let pid = parseVmPID(
|
|
421
|
+
async stopVM(nameOrBoxId) {
|
|
422
|
+
const { vmname, name, id } = await this.findVmname(nameOrBoxId);
|
|
423
|
+
let pid = parseVmPID(vmname);
|
|
460
424
|
if (pid) {
|
|
461
425
|
process.kill(pid);
|
|
462
426
|
}
|
|
463
|
-
logger.info(`${name} 盒子已停止`);
|
|
427
|
+
logger.info(`${name || id} 盒子已停止`);
|
|
464
428
|
}
|
|
465
429
|
|
|
466
430
|
/**
|
|
467
431
|
* 清理盒子,用于注册失败或者在注册过程中用户手动中止的情况
|
|
468
432
|
*/
|
|
469
|
-
async cleanVM(
|
|
470
|
-
|
|
471
|
-
|
|
433
|
+
async cleanVM(nameOrBoxId) {
|
|
434
|
+
const { vmname } = await this.findVmname(nameOrBoxId);
|
|
435
|
+
const vmDir = path.resolve(this.scheme.path, vmname);
|
|
472
436
|
try {
|
|
473
437
|
fs.accessSync(vmDir);
|
|
474
438
|
} catch {
|
|
@@ -477,22 +441,23 @@ export class QemuVM {
|
|
|
477
441
|
fs.rmSync(vmDir, { recursive: true, force: true });
|
|
478
442
|
}
|
|
479
443
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
444
|
+
async deleteVM(nameOrBoxId) {
|
|
445
|
+
const boxes = await this.listAllVM();
|
|
446
|
+
const box = boxes.find((b) => b.id == nameOrBoxId || b.name == nameOrBoxId);
|
|
447
|
+
if (!box) {
|
|
448
|
+
logger.info(`${nameOrBoxId} 盒子不存在`);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
if (box.status !== unRegistry) {
|
|
452
|
+
await cc.DelBox({ id: box.id });
|
|
453
|
+
}
|
|
486
454
|
|
|
487
|
-
|
|
488
|
-
fs.accessSync(vmDir);
|
|
489
|
-
} catch {
|
|
490
|
-
if (!silence) {
|
|
491
|
-
logger.warn(`${name} 盒子不存在或者该盒子为一个真实盒子`);
|
|
492
|
-
}
|
|
455
|
+
if (box["vm status"] === remoteBox) {
|
|
493
456
|
return;
|
|
494
457
|
}
|
|
495
458
|
|
|
459
|
+
const { vmname, name, id } = box;
|
|
460
|
+
const vmDir = path.resolve(this.scheme.path, vmname);
|
|
496
461
|
let pid = parseVmPID(vmDir);
|
|
497
462
|
if (pid) {
|
|
498
463
|
await inquirer
|
|
@@ -500,7 +465,7 @@ export class QemuVM {
|
|
|
500
465
|
{
|
|
501
466
|
type: "confirm",
|
|
502
467
|
name: "yesorno",
|
|
503
|
-
message: `${name} 正在运行中,是否停止?`,
|
|
468
|
+
message: `${name || id} 正在运行中,是否停止?`,
|
|
504
469
|
default: false,
|
|
505
470
|
},
|
|
506
471
|
])
|
|
@@ -509,7 +474,7 @@ export class QemuVM {
|
|
|
509
474
|
return;
|
|
510
475
|
}
|
|
511
476
|
process.kill(pid);
|
|
512
|
-
logger.info(`${name} 盒子已停止`);
|
|
477
|
+
logger.info(`${name || id} 盒子已停止`);
|
|
513
478
|
});
|
|
514
479
|
}
|
|
515
480
|
|
|
@@ -518,7 +483,7 @@ export class QemuVM {
|
|
|
518
483
|
{
|
|
519
484
|
type: "confirm",
|
|
520
485
|
name: "yesorno",
|
|
521
|
-
message: `确定要删除 ${name} 盒子吗?`,
|
|
486
|
+
message: `确定要删除 ${name || id} 盒子吗?`,
|
|
522
487
|
default: false,
|
|
523
488
|
},
|
|
524
489
|
])
|
|
@@ -530,101 +495,67 @@ export class QemuVM {
|
|
|
530
495
|
});
|
|
531
496
|
}
|
|
532
497
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
sigleVM(name, defaultBoxName = "") {
|
|
537
|
-
let volumeDir = path.join(this.scheme.path, `vm-${name}`, `volume-config`);
|
|
538
|
-
|
|
539
|
-
try {
|
|
540
|
-
fs.accessSync(volumeDir);
|
|
541
|
-
} catch {
|
|
542
|
-
return;
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
const read = (name) => {
|
|
546
|
-
try {
|
|
547
|
-
return fs.readFileSync(path.join(volumeDir, name), {
|
|
548
|
-
encoding: "utf-8",
|
|
549
|
-
});
|
|
550
|
-
} catch {
|
|
551
|
-
return "not found";
|
|
552
|
-
}
|
|
553
|
-
};
|
|
554
|
-
|
|
555
|
-
let boxName = read("box.name");
|
|
556
|
-
|
|
557
|
-
return {
|
|
558
|
-
default: boxName == defaultBoxName ? "yes" : "no",
|
|
559
|
-
boxName,
|
|
560
|
-
boxId: read("box.id"),
|
|
561
|
-
};
|
|
562
|
-
}
|
|
498
|
+
async listAllVM() {
|
|
499
|
+
const boxes = (await cc.QueryBoxList({})).boxes;
|
|
500
|
+
const entries = await this.loadBoxIdAndBoxName();
|
|
563
501
|
|
|
564
|
-
/**
|
|
565
|
-
* 从缓存路径中列出所有的盒子信息
|
|
566
|
-
*/
|
|
567
|
-
infoAllVM(defaultBoxName = "") {
|
|
568
|
-
let entries = fs.readdirSync(this.scheme.path, { withFileTypes: true });
|
|
569
502
|
let result = [];
|
|
503
|
+
Object.keys(entries).forEach((vmname) => {
|
|
504
|
+
let info = entries[vmname];
|
|
505
|
+
const box = boxes.find((box) => box.boxId == info.id);
|
|
506
|
+
const status = box ? getBoxStatus(box.status) : unRegistry;
|
|
507
|
+
result.push({
|
|
508
|
+
...info,
|
|
509
|
+
status,
|
|
510
|
+
default: isDefault(info.name),
|
|
511
|
+
"vm status": isRunning(vmname),
|
|
512
|
+
});
|
|
513
|
+
});
|
|
570
514
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
let info = this.sigleVM(name, defaultBoxName);
|
|
582
|
-
if (info) {
|
|
583
|
-
let pid = parseVmPID(path.join(this.scheme.path, entry.name));
|
|
584
|
-
info["vm status"] = pid ? "running" : "stop";
|
|
585
|
-
result.push(info);
|
|
515
|
+
boxes.forEach((box) => {
|
|
516
|
+
let info = result.find((r) => r.id == box.boxId);
|
|
517
|
+
if (!info) {
|
|
518
|
+
result.push({
|
|
519
|
+
id: box.boxId,
|
|
520
|
+
name: box.boxName,
|
|
521
|
+
status: getBoxStatus(box.status),
|
|
522
|
+
default: isDefault(box.boxName),
|
|
523
|
+
"vm status": remoteBox,
|
|
524
|
+
});
|
|
586
525
|
}
|
|
587
526
|
});
|
|
588
|
-
|
|
589
527
|
return result;
|
|
590
528
|
}
|
|
591
529
|
|
|
592
|
-
/**
|
|
593
|
-
* 返回指定盒子信息
|
|
594
|
-
*/
|
|
595
|
-
async infoSigleVM(name, defaultBoxName = "") {
|
|
596
|
-
let baseInfo = this.sigleVM(name, defaultBoxName);
|
|
597
|
-
if (!baseInfo) {
|
|
598
|
-
return [];
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
let vmDir = path.join(this.scheme.path, `vm-${name}`);
|
|
602
|
-
baseInfo["vm status"] = parseVmPID(vmDir) ? "running" : "stop";
|
|
603
|
-
return [baseInfo];
|
|
604
|
-
}
|
|
605
|
-
|
|
606
530
|
/**
|
|
607
531
|
* 根据 scheme 中的 path 和 uuid 和 name 获取盒子的信息
|
|
608
532
|
*/
|
|
609
|
-
async
|
|
610
|
-
let infos;
|
|
611
|
-
if (
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
533
|
+
async listVM(nameOrBoxId) {
|
|
534
|
+
let infos = await this.listAllVM();
|
|
535
|
+
if (nameOrBoxId) {
|
|
536
|
+
let info = infos.find(
|
|
537
|
+
(i) => i.name === nameOrBoxId || i.id === nameOrBoxId
|
|
538
|
+
);
|
|
539
|
+
if (info) {
|
|
540
|
+
infos = [info];
|
|
541
|
+
}
|
|
615
542
|
}
|
|
616
|
-
|
|
543
|
+
|
|
544
|
+
return infos.map((info) => {
|
|
545
|
+
delete info["vmname"];
|
|
546
|
+
return info;
|
|
547
|
+
});
|
|
617
548
|
}
|
|
618
549
|
|
|
619
550
|
/**
|
|
620
551
|
* 根据 scheme 中的 path 和 uuid 和 name 获取盒子的boxid
|
|
621
552
|
*/
|
|
622
|
-
readBoxid(
|
|
553
|
+
readBoxid(vmname) {
|
|
623
554
|
if (!this.scheme.volume["config"]) {
|
|
624
555
|
return "";
|
|
625
556
|
}
|
|
626
557
|
|
|
627
|
-
let vmDir = path.join(this.scheme.path,
|
|
558
|
+
let vmDir = path.join(this.scheme.path, vmname);
|
|
628
559
|
let volumeDir = path.join(vmDir, `volume-config`);
|
|
629
560
|
|
|
630
561
|
try {
|
|
@@ -636,4 +567,42 @@ export class QemuVM {
|
|
|
636
567
|
return "";
|
|
637
568
|
}
|
|
638
569
|
}
|
|
570
|
+
|
|
571
|
+
async loadBoxIdAndBoxName() {
|
|
572
|
+
const files = await glob(
|
|
573
|
+
["vm-*/volume-config/box.id", "vm-*/volume-config/box.name"],
|
|
574
|
+
{
|
|
575
|
+
cwd: this.scheme.path,
|
|
576
|
+
dot: false,
|
|
577
|
+
}
|
|
578
|
+
);
|
|
579
|
+
const result = {};
|
|
580
|
+
files.forEach((file) => {
|
|
581
|
+
const vmDir = file.split(path.sep)[0];
|
|
582
|
+
const type = path.extname(file).toLowerCase() === ".name" ? "name" : "id";
|
|
583
|
+
const value = fs.readFileSync(
|
|
584
|
+
path.resolve(this.scheme.path, file),
|
|
585
|
+
"utf8"
|
|
586
|
+
);
|
|
587
|
+
if (!result[vmDir]) {
|
|
588
|
+
result[vmDir] = { id: "", name: "", vmname: "" };
|
|
589
|
+
}
|
|
590
|
+
result[vmDir][type] = value;
|
|
591
|
+
result[vmDir]["vmname"] = vmDir;
|
|
592
|
+
});
|
|
593
|
+
return result;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
async findVmname(nameOrBoxId) {
|
|
597
|
+
const entries = await this.loadBoxIdAndBoxName();
|
|
598
|
+
const vmname = Object.keys(entries).find((key) => {
|
|
599
|
+
const e = entries[key];
|
|
600
|
+
return e?.name === nameOrBoxId || e.id === nameOrBoxId;
|
|
601
|
+
});
|
|
602
|
+
if (!vmname) {
|
|
603
|
+
throw `${nameOrBoxId} 盒子不存在,请检查盒子名称或者ID是否正确`;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
return entries[vmname];
|
|
607
|
+
}
|
|
639
608
|
}
|
package/lib/docker-compose.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import yaml from "js-yaml";
|
|
2
2
|
import fs from "fs";
|
|
3
3
|
import mergeWith from "lodash.mergewith";
|
|
4
|
-
import isArray from "lodash.isarray";
|
|
5
4
|
|
|
6
5
|
export function load(filePath) {
|
|
7
6
|
return yaml.load(fs.readFileSync(filePath, "utf8"));
|
|
@@ -30,7 +29,7 @@ export default class DockerCompose {
|
|
|
30
29
|
}
|
|
31
30
|
|
|
32
31
|
this.obj = mergeWith(this.obj, newObj, (objValue, srcValue) => {
|
|
33
|
-
if (isArray(objValue)) {
|
|
32
|
+
if (Array.isArray(objValue)) {
|
|
34
33
|
return objValue.concat(srcValue);
|
|
35
34
|
}
|
|
36
35
|
});
|