@lazycatcloud/lzc-cli 1.1.3 → 1.1.6
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/cmds/app.js +133 -0
- package/cmds/config.js +55 -0
- package/cmds/create.js +55 -0
- package/cmds/dev.js +130 -0
- package/cmds/init.js +125 -0
- package/cmds/log.js +103 -0
- package/cmds/publish.js +116 -0
- package/lib/api.js +21 -7
- package/lib/app/index.js +92 -0
- package/lib/app/lpk_build.js +229 -0
- package/lib/app/lpk_create.js +201 -0
- package/lib/app/lpk_devshell.js +621 -0
- package/lib/app/lpk_installer.js +67 -0
- package/lib/archiver.js +0 -42
- package/lib/autologin.js +84 -0
- package/lib/box/check_qemu.js +50 -0
- package/lib/box/hportal.js +8 -1
- package/lib/box/index.js +35 -15
- package/lib/box/qemu_vm_mgr.js +142 -56
- package/lib/builder.js +14 -3
- package/lib/dev.js +12 -6
- package/lib/env.js +10 -6
- package/lib/generator.js +2 -2
- package/lib/git/git-commit.sh +3 -3
- package/lib/sdk.js +29 -5
- package/lib/utils.js +69 -46
- package/package.json +13 -6
- package/scripts/cli.js +92 -13
- package/template/_lazycat/debug/shell/Dockerfile +5 -3
- package/template/_lazycat/debug/shell/docker-compose.override.yml.in +2 -12
- package/template/_lazycat/debug/shell/entrypoint.sh +3 -1
- package/template/_lpk/Dockerfile.in +8 -0
- package/template/_lpk/devshell/Dockerfile +18 -0
- package/template/_lpk/devshell/build.sh +5 -0
- package/template/_lpk/devshell/entrypoint.sh +8 -0
- package/template/_lpk/devshell/sshd_config +117 -0
- package/template/_lpk/manifest.yml.in +17 -0
- package/template/_lpk/sync/Dockerfile +16 -0
- package/template/_lpk/sync/build.sh +5 -0
- package/template/_lpk/sync/entrypoint.sh +8 -0
- package/template/_lpk/sync/sshd_config +117 -0
- package/template/_lpk/sync.manifest.yml.in +3 -0
- package/template/golang/build.sh +6 -0
- package/template/golang/lzc-build.yml +52 -0
- package/template/ionic_vue3/lzc-build.yml +53 -0
- package/template/ionic_vue3/vite.config.ts +1 -1
- package/template/release/golang/build.sh +1 -1
- package/template/release/ionic_vue3/Dockerfile +1 -1
- package/template/release/ionic_vue3/build.sh +1 -3
- package/template/release/ionic_vue3/docker-compose.yml.in +0 -5
- package/template/release/vue/Dockerfile +1 -1
- package/template/release/vue/build.sh +2 -3
- package/template/release/vue/docker-compose.yml.in +0 -5
- package/template/vue/lzc-build.yml +53 -0
- package/template/vue/src/main.js +3 -14
- package/template/vue/vue.config.js +16 -0
- package/scripts/auto-completion.sh +0 -46
- package/template/_lazycat/debug/shell/nginx.conf.template +0 -64
- package/template/vue/src/lzc.js +0 -110
package/lib/archiver.js
CHANGED
|
@@ -94,54 +94,12 @@ class Archiver {
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
async finalize(zip = true) {
|
|
97
|
-
// await this._changeContent();
|
|
98
97
|
if (zip) {
|
|
99
98
|
return await archiveFolder(this.tmpDir);
|
|
100
99
|
} else {
|
|
101
100
|
return this.tmpDir;
|
|
102
101
|
}
|
|
103
102
|
}
|
|
104
|
-
//
|
|
105
|
-
// permissions:
|
|
106
|
-
// - lzcapis
|
|
107
|
-
// need REMOVE deprecated
|
|
108
|
-
async _changeContent() {
|
|
109
|
-
let base = new DockerCompose(path.join(this.tmpDir, "docker-compose.yml"));
|
|
110
|
-
base
|
|
111
|
-
.pipe((template) => {
|
|
112
|
-
const meta = template[META_MARK];
|
|
113
|
-
if (
|
|
114
|
-
meta &&
|
|
115
|
-
Array.isArray(meta["permissions"]) &&
|
|
116
|
-
meta["permissions"].includes("lzcapis")
|
|
117
|
-
) {
|
|
118
|
-
template[META_MARK]["ingress"].push({
|
|
119
|
-
service: "lazycat-apis-sidecar",
|
|
120
|
-
port: 8888,
|
|
121
|
-
subdomain: env.get("APP_ID"),
|
|
122
|
-
path: "/lzcapis/",
|
|
123
|
-
auth: "oidc",
|
|
124
|
-
authcallback: "/lzcapis/oidc-callback",
|
|
125
|
-
});
|
|
126
|
-
template["services"]["lazycat-apis-sidecar"] = {
|
|
127
|
-
image: "registry.lazycat.cloud/lazycat-apis-sidecar",
|
|
128
|
-
pull_policy: "always",
|
|
129
|
-
// volumes_from: ["${APP_NAME}:rw"],
|
|
130
|
-
volumes: ["lzcapis-lzcapp:/lzcapp"],
|
|
131
|
-
command: [
|
|
132
|
-
"--client-id=${LAZYCAT_AUTH_OIDC_CLIENT_ID}",
|
|
133
|
-
"--client-secret=${LAZYCAT_AUTH_OIDC_CLIENT_SECRET}",
|
|
134
|
-
"--client-url=https://${LAZYCAT_APP_ORIGIN}/lzcapis/",
|
|
135
|
-
"--issuer=${LAZYCAT_AUTH_OIDC_ISSUER_URL}",
|
|
136
|
-
"--prefix=lzcapis",
|
|
137
|
-
"--fs-root=/lzcapp/documents",
|
|
138
|
-
],
|
|
139
|
-
};
|
|
140
|
-
template["volumes"]["lzcapis-lzcapp"] = null;
|
|
141
|
-
}
|
|
142
|
-
})
|
|
143
|
-
.save();
|
|
144
|
-
}
|
|
145
103
|
}
|
|
146
104
|
|
|
147
105
|
export default Archiver;
|
package/lib/autologin.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import fetch from "node-fetch";
|
|
2
|
+
|
|
3
|
+
// autologin.js 提供一个封装好的 fetch request 请求方法,通过这个中间件,可以不用考虑
|
|
4
|
+
// auth:auto 所做的授权验证。
|
|
5
|
+
let AutoLoginToken = "";
|
|
6
|
+
let AutoLoginFailed = false;
|
|
7
|
+
|
|
8
|
+
function fetchTokenAction(url) {
|
|
9
|
+
return fetch(url).then(async (res) => {
|
|
10
|
+
const body = await res.text();
|
|
11
|
+
const token = body.match(/name=\"token\" value=\"(.*?)\"/i)[1];
|
|
12
|
+
const action = body.match(/action=\"(.*?)\"/i)[1];
|
|
13
|
+
|
|
14
|
+
return { token, action };
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function fetchTokenActionAndRedirect(dataToken, dataAction) {
|
|
19
|
+
return fetch(dataAction, {
|
|
20
|
+
method: "POST",
|
|
21
|
+
body: `token=${dataToken}`,
|
|
22
|
+
redirect: "follow",
|
|
23
|
+
headers: {
|
|
24
|
+
accept:
|
|
25
|
+
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
|
|
26
|
+
"accept-language": "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7",
|
|
27
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
28
|
+
},
|
|
29
|
+
}).then(async (res) => {
|
|
30
|
+
let body = await res.text();
|
|
31
|
+
const token = body.match(/name=\"token\" value=\"(.*?)\"/i)[1];
|
|
32
|
+
const action = body.match(/action=\"(.*?)\"/i)[1];
|
|
33
|
+
const redirect = body.match(/name=\"redirect\" value=\"(.*?)\"/i)[1];
|
|
34
|
+
|
|
35
|
+
return { token, action, redirect };
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function request(url, options = {}) {
|
|
40
|
+
if (AutoLoginFailed) {
|
|
41
|
+
return fetch(url, options);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
await tryAuthLogin(url);
|
|
45
|
+
|
|
46
|
+
if (!AutoLoginToken) {
|
|
47
|
+
return fetch(url, options);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let headers = { cookie: `HC-Auth-Token=${AutoLoginToken}` };
|
|
51
|
+
if (options.headers) {
|
|
52
|
+
headers = Object.assign({}, options.headers, headers);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
options = Object.assign({}, options, { headers });
|
|
56
|
+
return fetch(url, options);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function tryAuthLogin(url) {
|
|
60
|
+
if (!url.startsWith("http")) {
|
|
61
|
+
url = "https://" + url;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
if (!AutoLoginToken) {
|
|
66
|
+
let _url = new URL(url);
|
|
67
|
+
let u = _url.host.split(".").reverse().slice(0, 3).reverse().join(".");
|
|
68
|
+
|
|
69
|
+
console.log(`${u} 尝试自动登录...`);
|
|
70
|
+
|
|
71
|
+
let data = await fetchTokenAction(`${_url.protocol}//${u}`);
|
|
72
|
+
let data2 = await fetchTokenActionAndRedirect(data.token, data.action);
|
|
73
|
+
|
|
74
|
+
AutoLoginToken = data2.token;
|
|
75
|
+
console.log(`自动登录成功!`);
|
|
76
|
+
return AutoLoginToken;
|
|
77
|
+
}
|
|
78
|
+
} catch (e) {
|
|
79
|
+
console.error(e);
|
|
80
|
+
console.log(`尝试自动登录失败,正在使用默认的登录方式...`);
|
|
81
|
+
AutoLoginFailed = true;
|
|
82
|
+
return "";
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// 检测当前系统是否安装 qemu 软件
|
|
2
|
+
import commandExist from "command-exists";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
|
|
6
|
+
function linuxPlatform(pkg) {
|
|
7
|
+
let r = os.release();
|
|
8
|
+
if (r.search(/arch/gi) > -1) {
|
|
9
|
+
return `sudo pacman -S ${pkg}`;
|
|
10
|
+
} else if (r.search(/debian/gi) > -1) {
|
|
11
|
+
return `sudo apt install ${pkg}`;
|
|
12
|
+
} else {
|
|
13
|
+
return ``;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function platformPackage(pkg) {
|
|
18
|
+
let cmd = "";
|
|
19
|
+
switch (os.platform()) {
|
|
20
|
+
case "darwin":
|
|
21
|
+
cmd = `brew install ${pkg}`;
|
|
22
|
+
break;
|
|
23
|
+
case "linux":
|
|
24
|
+
cmd = linuxPlatform(pkg);
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
return `${pkg}包没有发现,请先通过系统包管理器安装。\n${cmd}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class CheckQemu {
|
|
31
|
+
constructor() {
|
|
32
|
+
this.commands = ["qemu-img", "qemu-system-x86_64"];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async init() {
|
|
36
|
+
for (let cmd of this.commands) {
|
|
37
|
+
if (!commandExist.sync(cmd)) {
|
|
38
|
+
let cmdErr = chalk.red(`${cmd} 命令没有发现`);
|
|
39
|
+
let pkgTips = chalk.blue(platformPackage("qemu"));
|
|
40
|
+
let tips = `${cmdErr}
|
|
41
|
+
|
|
42
|
+
${pkgTips}
|
|
43
|
+
|
|
44
|
+
查看更多信息 https://www.qemu.org/download/
|
|
45
|
+
`;
|
|
46
|
+
throw tips;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
package/lib/box/hportal.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fetch from "node-fetch";
|
|
2
|
+
import logger from "loglevel";
|
|
2
3
|
|
|
3
4
|
export class HportalManager {
|
|
4
5
|
constructor(apiHost) {
|
|
@@ -58,11 +59,16 @@ export class HportalManager {
|
|
|
58
59
|
async boxs(boxName, defaultBoxName = "") {
|
|
59
60
|
let url = this.apiHost + "/admin/boxes";
|
|
60
61
|
|
|
61
|
-
let origin;
|
|
62
|
+
let origin = [];
|
|
62
63
|
try {
|
|
63
64
|
const resp = await fetch(url);
|
|
64
65
|
if (resp.status == 200) {
|
|
65
66
|
origin = await resp.json();
|
|
67
|
+
} else {
|
|
68
|
+
logger.debug(
|
|
69
|
+
`status: ${resp.status}, url: ${resp.url}, text: ${resp.statusText}`
|
|
70
|
+
);
|
|
71
|
+
return [];
|
|
66
72
|
}
|
|
67
73
|
} catch {
|
|
68
74
|
return [];
|
|
@@ -101,6 +107,7 @@ export function showBoxInfo(vmInfos, rmInfos) {
|
|
|
101
107
|
|
|
102
108
|
if (boxIds.length == 0) {
|
|
103
109
|
console.log("没有找到任何盒子,创建一个吧!");
|
|
110
|
+
return;
|
|
104
111
|
}
|
|
105
112
|
|
|
106
113
|
let result = [];
|
package/lib/box/index.js
CHANGED
|
@@ -6,8 +6,10 @@ import { QemuVM, QemuResource } from "./qemu_vm_mgr.js";
|
|
|
6
6
|
import { contextDirname } from "../utils.js";
|
|
7
7
|
import env, { sdkEnv } from "../env.js";
|
|
8
8
|
import { HportalManager, showBoxInfo } from "./hportal.js";
|
|
9
|
+
import { CheckQemu } from "./check_qemu.js";
|
|
10
|
+
import logger from "loglevel";
|
|
9
11
|
|
|
10
|
-
async function initQemuVM() {
|
|
12
|
+
async function initQemuVM(ensureResources = false) {
|
|
11
13
|
let defaultSchemeFile = path.join(
|
|
12
14
|
contextDirname(),
|
|
13
15
|
"box",
|
|
@@ -24,9 +26,13 @@ async function initQemuVM() {
|
|
|
24
26
|
scheme.path = path.join(contextDirname(), "box-emulator", scheme.path);
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
fs.mkdirSync(scheme.path, { recursive: true });
|
|
30
|
+
|
|
31
|
+
if (ensureResources) {
|
|
32
|
+
// 使用本地网络 api 调试 new QemuResource(scheme.path, "http://127.0.0.1");
|
|
33
|
+
let resource = new QemuResource(scheme.path);
|
|
34
|
+
await resource.init();
|
|
35
|
+
}
|
|
30
36
|
|
|
31
37
|
let m = new QemuVM(scheme);
|
|
32
38
|
return m;
|
|
@@ -44,22 +50,36 @@ export function boxCommand(box) {
|
|
|
44
50
|
command: "create",
|
|
45
51
|
desc: "创建一个虚拟盒子,并注册运行",
|
|
46
52
|
handler: async () => {
|
|
47
|
-
let
|
|
48
|
-
|
|
53
|
+
let cq = new CheckQemu();
|
|
54
|
+
await cq.init();
|
|
55
|
+
|
|
56
|
+
let boxId;
|
|
57
|
+
let m = await initQemuVM(true);
|
|
58
|
+
let answer = await m.askBoxInfo();
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
// 创建盒子阶段,如果出错,将直接删除所有的资源。
|
|
62
|
+
boxId = await m.createVM(answer);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
logger.error(error);
|
|
65
|
+
await m.cleanVM(answer.boxName);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
49
69
|
// 如果没有指定默认的盒子名称将会将第一个盒子作为默认的盒子
|
|
50
70
|
let defaultBoxName = env.get("DEFAULT_BOXNAME");
|
|
51
71
|
if (!defaultBoxName) {
|
|
52
72
|
// 新创建的盒子在初始化的时候,盒子里面需要安装hc的组件,需要一点时间,
|
|
53
73
|
// 所以这里并不对sdk url进行校验是否有效
|
|
54
|
-
await sdkEnv.setDefaultBoxName(boxName, false);
|
|
74
|
+
await sdkEnv.setDefaultBoxName(answer.boxName, false);
|
|
55
75
|
}
|
|
56
76
|
|
|
57
77
|
let h = await initHportal();
|
|
58
|
-
|
|
59
|
-
await h.addBox(boxName);
|
|
60
|
-
|
|
61
|
-
await h.loginBox(boxId,
|
|
62
|
-
|
|
78
|
+
logger.debug("添加盒子到 Hportal ......");
|
|
79
|
+
await h.addBox(answer.boxName);
|
|
80
|
+
logger.debug("正在使用管理员帐号密码登录中......");
|
|
81
|
+
await h.loginBox(boxId, answer.adminName, answer.adminPass);
|
|
82
|
+
logger.debug("登录成功!");
|
|
63
83
|
},
|
|
64
84
|
},
|
|
65
85
|
{
|
|
@@ -69,7 +89,7 @@ export function boxCommand(box) {
|
|
|
69
89
|
let m = await initQemuVM();
|
|
70
90
|
let boxid = await m.runVM(boxName);
|
|
71
91
|
if (boxid) {
|
|
72
|
-
|
|
92
|
+
logger.info("盒子ID: ", boxid);
|
|
73
93
|
}
|
|
74
94
|
},
|
|
75
95
|
},
|
|
@@ -107,8 +127,8 @@ export function boxCommand(box) {
|
|
|
107
127
|
let rmInfos = await rm.boxs(boxName, defaultBoxName);
|
|
108
128
|
|
|
109
129
|
// 过滤条件不满足
|
|
110
|
-
if (vmInfos.length == 0 && rmInfos == 0 && boxName
|
|
111
|
-
|
|
130
|
+
if (vmInfos.length == 0 && rmInfos == 0 && boxName) {
|
|
131
|
+
logger.info(`${boxName} 盒子不存在`);
|
|
112
132
|
let allVmInfos = await m.infoVM("", defaultBoxName);
|
|
113
133
|
let allRmInfos = await rm.boxs("", defaultBoxName);
|
|
114
134
|
showBoxInfo(allVmInfos, allRmInfos);
|
package/lib/box/qemu_vm_mgr.js
CHANGED
|
@@ -5,9 +5,12 @@ import path from "node:path";
|
|
|
5
5
|
import inquirer from "inquirer";
|
|
6
6
|
import https from "node:https";
|
|
7
7
|
import http from "node:http";
|
|
8
|
-
import lz4 from "lz4";
|
|
9
8
|
import process from "node:process";
|
|
10
9
|
import net from "node:net";
|
|
10
|
+
import fetch from "node-fetch";
|
|
11
|
+
import zlib from "node:zlib";
|
|
12
|
+
import os from "node:os";
|
|
13
|
+
import logger from "loglevel";
|
|
11
14
|
|
|
12
15
|
async function getFreePort() {
|
|
13
16
|
return new Promise((resolve, reject) => {
|
|
@@ -37,8 +40,8 @@ export class QemuResource {
|
|
|
37
40
|
* downloadUrl system-disk 下载路径
|
|
38
41
|
*/
|
|
39
42
|
constructor(
|
|
40
|
-
diskDir = "~/.
|
|
41
|
-
downloadUrl = "https://dl.lazycat.cloud/sdk
|
|
43
|
+
diskDir = "~/.cache/box-emulator",
|
|
44
|
+
downloadUrl = "https://dl.lazycat.cloud/sdk"
|
|
42
45
|
) {
|
|
43
46
|
this.diskDir = diskDir;
|
|
44
47
|
this.downloadUrl = downloadUrl;
|
|
@@ -46,32 +49,96 @@ export class QemuResource {
|
|
|
46
49
|
|
|
47
50
|
async init() {
|
|
48
51
|
fs.mkdirSync(this.diskDir, { recursive: true });
|
|
49
|
-
await this.
|
|
52
|
+
let shouldUpdate = await this.shouldUpdate();
|
|
53
|
+
if (shouldUpdate) {
|
|
54
|
+
let answer = await inquirer.prompt([
|
|
55
|
+
{
|
|
56
|
+
type: "confirm",
|
|
57
|
+
name: "yesorno",
|
|
58
|
+
message: `检测到盒子系统具有更新,是否需要重新下载盒子系统镜像?`,
|
|
59
|
+
default: true,
|
|
60
|
+
},
|
|
61
|
+
]);
|
|
62
|
+
if (!answer.yesorno) {
|
|
63
|
+
shouldUpdate = false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
await this.ensureSystemDisk(shouldUpdate);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 根据下载到的 md5 和本地缓存的 md5 进行比较,如果不一样,则代表具有更新,
|
|
71
|
+
* 然后提示用户是否更新。
|
|
72
|
+
* 如果在获取 md5 的过程中,发生网络错误,则跳过更新检查。
|
|
73
|
+
* 目前的更新,只是使用 md5 来判断是否需要重新下载 md5, 并不会使用 md5 来判断所下载的数据是否正确。
|
|
74
|
+
*/
|
|
75
|
+
async shouldUpdate() {
|
|
76
|
+
let oldMd5Path = path.join(this.diskDir, "disk-system.qcow2.md5");
|
|
77
|
+
|
|
78
|
+
// 如果不存在 md5 文件,直接返回需要更新
|
|
79
|
+
try {
|
|
80
|
+
fs.accessSync(oldMd5Path);
|
|
81
|
+
} catch {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 拉取最新的 md5
|
|
86
|
+
let newMd5;
|
|
87
|
+
try {
|
|
88
|
+
let url = `${this.downloadUrl}/vm/disk-system.qcow2.md5`;
|
|
89
|
+
let res = await fetch(url);
|
|
90
|
+
if (res.status !== 200) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
newMd5 = await res.text();
|
|
94
|
+
} catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let oldMd5 = fs.readFileSync(oldMd5Path, { encoding: "utf-8" });
|
|
99
|
+
return oldMd5 !== newMd5;
|
|
50
100
|
}
|
|
51
101
|
|
|
52
102
|
/**
|
|
53
103
|
* 确保 qemu 系统盘的镜像存在,如果不存在,将会从 downloadUrl 中下载
|
|
54
104
|
*/
|
|
55
|
-
async ensureSystemDisk() {
|
|
105
|
+
async ensureSystemDisk(update = false) {
|
|
56
106
|
let systemDiskFile = path.join(this.diskDir, "disk-system.qcow2");
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
107
|
+
if (!update) {
|
|
108
|
+
try {
|
|
109
|
+
fs.accessSync(systemDiskFile);
|
|
110
|
+
return;
|
|
111
|
+
} catch {}
|
|
112
|
+
}
|
|
61
113
|
|
|
114
|
+
await this.downloadOVMF();
|
|
62
115
|
await this.downloadSystemDisk();
|
|
63
116
|
await this.downloadPrivateKey();
|
|
117
|
+
await this.downloadSystemDiskMd5();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async downloadOVMF() {
|
|
121
|
+
let name = `${os.arch()}.OVMF.fd`;
|
|
122
|
+
let savePath = path.join(this.diskDir, name);
|
|
123
|
+
let url = `${this.downloadUrl}/${name}`;
|
|
124
|
+
await this.download(url, savePath);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async downloadSystemDiskMd5() {
|
|
128
|
+
let savePath = path.join(this.diskDir, "disk-system.qcow2.md5");
|
|
129
|
+
let url = `${this.downloadUrl}/vm/disk-system.qcow2.md5`;
|
|
130
|
+
await this.download(url, savePath);
|
|
64
131
|
}
|
|
65
132
|
|
|
66
133
|
async downloadSystemDisk() {
|
|
67
134
|
let savePath = path.join(this.diskDir, "disk-system.qcow2");
|
|
68
|
-
let url = `${this.downloadUrl}/disk-system.qcow2.
|
|
135
|
+
let url = `${this.downloadUrl}/vm/disk-system.qcow2.gz`;
|
|
69
136
|
await this.download(url, savePath, true);
|
|
70
137
|
}
|
|
71
138
|
|
|
72
139
|
async downloadPrivateKey() {
|
|
73
140
|
let savePath = path.join(this.diskDir, "id_rsa");
|
|
74
|
-
let url = `${this.downloadUrl}/id_rsa`;
|
|
141
|
+
let url = `${this.downloadUrl}/vm/id_rsa`;
|
|
75
142
|
await this.download(url, savePath);
|
|
76
143
|
fs.chmodSync(savePath, 0o400);
|
|
77
144
|
return;
|
|
@@ -90,7 +157,7 @@ export class QemuResource {
|
|
|
90
157
|
);
|
|
91
158
|
}
|
|
92
159
|
|
|
93
|
-
download(url, savePath,
|
|
160
|
+
download(url, savePath, enableGzip = false) {
|
|
94
161
|
let tmpPath = savePath + ".tmp";
|
|
95
162
|
const options = new URL(url);
|
|
96
163
|
let request = url.startsWith("https") ? https.request : http.request;
|
|
@@ -110,8 +177,8 @@ export class QemuResource {
|
|
|
110
177
|
});
|
|
111
178
|
|
|
112
179
|
let outputFile = fs.createWriteStream(tmpPath, { flags: "w+" });
|
|
113
|
-
if (
|
|
114
|
-
let decoder =
|
|
180
|
+
if (enableGzip) {
|
|
181
|
+
let decoder = zlib.createUnzip();
|
|
115
182
|
res.pipe(decoder).pipe(outputFile);
|
|
116
183
|
} else {
|
|
117
184
|
res.pipe(outputFile);
|
|
@@ -144,9 +211,12 @@ export class QemuVM {
|
|
|
144
211
|
let args = await this.buildQemuArgs(name, vmDir);
|
|
145
212
|
let p = spawn("qemu-system-x86_64", args, {
|
|
146
213
|
detached: true,
|
|
147
|
-
stdio: ["
|
|
214
|
+
stdio: ["pipe", "ignore", "inherit"],
|
|
148
215
|
});
|
|
149
|
-
|
|
216
|
+
p.on("error", (e) => {
|
|
217
|
+
throw e;
|
|
218
|
+
});
|
|
219
|
+
logger.debug("启动中...");
|
|
150
220
|
|
|
151
221
|
// 需要等待 boxid 的出现
|
|
152
222
|
return new Promise((resolve, reject) => {
|
|
@@ -165,14 +235,21 @@ export class QemuVM {
|
|
|
165
235
|
return;
|
|
166
236
|
}
|
|
167
237
|
|
|
168
|
-
let
|
|
169
|
-
if (
|
|
170
|
-
p.unref();
|
|
171
|
-
resolve(boxid);
|
|
238
|
+
let boxId = this.readBoxid(name);
|
|
239
|
+
if (boxId) {
|
|
172
240
|
clearInterval(id);
|
|
173
|
-
|
|
241
|
+
p.unref();
|
|
242
|
+
resolve(boxId);
|
|
243
|
+
logger.debug("启动成功!");
|
|
174
244
|
}
|
|
175
245
|
}, 1000);
|
|
246
|
+
|
|
247
|
+
// 当在等待期间,ctrl-c 退出
|
|
248
|
+
process.on("SIGINT", () => {
|
|
249
|
+
p.kill();
|
|
250
|
+
clearInterval(id);
|
|
251
|
+
reject("exit");
|
|
252
|
+
});
|
|
176
253
|
});
|
|
177
254
|
}
|
|
178
255
|
|
|
@@ -184,38 +261,35 @@ export class QemuVM {
|
|
|
184
261
|
async runVM(name) {
|
|
185
262
|
let boxid = this.readBoxid(name);
|
|
186
263
|
if (!boxid) {
|
|
187
|
-
|
|
264
|
+
logger.info(`${name} 盒子不存在,请通过 lzc-cli box create 创建`);
|
|
188
265
|
return;
|
|
189
266
|
}
|
|
190
267
|
let vmDir = path.join(this.scheme.path, `vm-${name}`);
|
|
191
268
|
let pid = parseVmPID(vmDir);
|
|
192
269
|
if (pid) {
|
|
193
|
-
|
|
270
|
+
logger.info(`${name} 盒子已经启动`);
|
|
194
271
|
return boxid;
|
|
195
272
|
}
|
|
196
273
|
await this.startVM(name, vmDir);
|
|
197
|
-
|
|
274
|
+
logger.info(`${name} 盒子启动成功!`);
|
|
198
275
|
}
|
|
199
276
|
|
|
200
277
|
/**
|
|
201
278
|
* 创建一个指定的虚拟机,并启动等待注册成功。
|
|
202
279
|
**/
|
|
203
|
-
async createVM() {
|
|
204
|
-
let
|
|
205
|
-
|
|
206
|
-
let vmDir = this.ensureVmDir(answer.boxName);
|
|
280
|
+
async createVM({ boxName, adminName, adminPass }) {
|
|
281
|
+
let vmDir = this.ensureVmDir(boxName);
|
|
207
282
|
this.ensureVolumeDir(vmDir);
|
|
208
283
|
await this.buildDisks(vmDir);
|
|
209
|
-
let boxId = await this.startVM(
|
|
210
|
-
|
|
284
|
+
let boxId = await this.startVM(boxName, vmDir);
|
|
285
|
+
logger.info("盒子ID: ", boxId);
|
|
211
286
|
|
|
212
|
-
|
|
213
|
-
return {
|
|
287
|
+
await this.registerVM(boxId, {
|
|
214
288
|
boxName,
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
289
|
+
adminName,
|
|
290
|
+
adminPass,
|
|
291
|
+
});
|
|
292
|
+
return boxId;
|
|
219
293
|
}
|
|
220
294
|
|
|
221
295
|
/**
|
|
@@ -244,29 +318,20 @@ export class QemuVM {
|
|
|
244
318
|
mask: "*",
|
|
245
319
|
validate: noEmpty,
|
|
246
320
|
},
|
|
247
|
-
{
|
|
248
|
-
type: "input",
|
|
249
|
-
name: "origin",
|
|
250
|
-
message: "注册服务器地址",
|
|
251
|
-
default: "origin.lazycat.cloud",
|
|
252
|
-
},
|
|
253
321
|
]);
|
|
254
322
|
}
|
|
255
323
|
|
|
256
324
|
/**
|
|
257
325
|
* 调用 hportal client 进行注册
|
|
258
326
|
**/
|
|
259
|
-
async registerVM(boxid, { boxName, adminName, adminPass
|
|
327
|
+
async registerVM(boxid, { boxName, adminName, adminPass }) {
|
|
260
328
|
// prettier-ignore
|
|
261
|
-
spawnSync("
|
|
262
|
-
"-S",
|
|
263
|
-
"home-portal-client",
|
|
329
|
+
spawnSync("home-portal-client",[
|
|
264
330
|
"-setup", "-boxid", boxid,
|
|
265
331
|
"-boxname", boxName,
|
|
266
332
|
"-user", adminName,
|
|
267
333
|
"-password", adminPass,
|
|
268
|
-
|
|
269
|
-
], {stdio: ["inherit", "inherit", "inherit"]})
|
|
334
|
+
], {stdio: "inherit"})
|
|
270
335
|
return boxName;
|
|
271
336
|
}
|
|
272
337
|
|
|
@@ -308,7 +373,7 @@ export class QemuVM {
|
|
|
308
373
|
}
|
|
309
374
|
|
|
310
375
|
async buildSystemDisk(name, diskPath, diskInfo) {
|
|
311
|
-
|
|
376
|
+
logger.debug(`构建系统盘快照:${diskPath}`);
|
|
312
377
|
let baseImage = path.join(this.scheme.path, name);
|
|
313
378
|
return spawnSync(
|
|
314
379
|
"qemu-img",
|
|
@@ -318,7 +383,7 @@ export class QemuVM {
|
|
|
318
383
|
}
|
|
319
384
|
|
|
320
385
|
async buildDataDisk(name, diskPath, diskInfo) {
|
|
321
|
-
|
|
386
|
+
logger.debug(`构建数据盘:${diskPath}`);
|
|
322
387
|
return spawnSync(
|
|
323
388
|
"qemu-img",
|
|
324
389
|
["create", "-f", "qcow2", diskPath, diskInfo.size],
|
|
@@ -330,7 +395,11 @@ export class QemuVM {
|
|
|
330
395
|
* 寻找默认可用的 bios 文件
|
|
331
396
|
*/
|
|
332
397
|
findBIOS() {
|
|
333
|
-
let includes = [
|
|
398
|
+
let includes = [
|
|
399
|
+
"/usr/share/ovmf/x64/OVMF.fd",
|
|
400
|
+
"/usr/share/ovmf/OVMF.fd",
|
|
401
|
+
path.join(this.scheme.path, `${os.arch()}.OVMF.fd`),
|
|
402
|
+
];
|
|
334
403
|
for (let file of includes) {
|
|
335
404
|
try {
|
|
336
405
|
fs.accessSync(file, fs.constants.F_OK);
|
|
@@ -350,7 +419,7 @@ export class QemuVM {
|
|
|
350
419
|
// prettier-ignore
|
|
351
420
|
let args = [
|
|
352
421
|
"-name", `${this.scheme.name}-${this.scheme.uuid}-vm-${name}`,
|
|
353
|
-
"-machine", "pc,accel=kvm",
|
|
422
|
+
"-machine", "pc,accel=kvm,accel=kvf,accel=xen,accel=hax,accel=nvmm,accel=whpx,accel=tcg",
|
|
354
423
|
"-m", `${this.scheme.memory}`,
|
|
355
424
|
"-smp", "4,sockets=4,cores=1,threads=1",
|
|
356
425
|
"-bios", bios,
|
|
@@ -391,19 +460,36 @@ export class QemuVM {
|
|
|
391
460
|
if (pid) {
|
|
392
461
|
process.kill(pid);
|
|
393
462
|
}
|
|
394
|
-
|
|
463
|
+
logger.info(`${name} 盒子已停止`);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* 清理盒子,用于注册失败或者在注册过程中用户手动中止的情况
|
|
468
|
+
*/
|
|
469
|
+
async cleanVM(name) {
|
|
470
|
+
let vmDir = path.join(this.scheme.path, `vm-${name}`);
|
|
471
|
+
|
|
472
|
+
try {
|
|
473
|
+
fs.accessSync(vmDir);
|
|
474
|
+
} catch {
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
fs.rmSync(vmDir, { recursive: true, force: true });
|
|
395
478
|
}
|
|
396
479
|
|
|
397
480
|
/**
|
|
398
481
|
* 删除虚拟盒子
|
|
482
|
+
* @param {boolean} silence - 是否提示错误,如果被删除的盒子不存在
|
|
399
483
|
*/
|
|
400
|
-
async deleteVM(name) {
|
|
484
|
+
async deleteVM(name, silence = false) {
|
|
401
485
|
let vmDir = path.join(this.scheme.path, `vm-${name}`);
|
|
402
486
|
|
|
403
487
|
try {
|
|
404
488
|
fs.accessSync(vmDir);
|
|
405
489
|
} catch {
|
|
406
|
-
|
|
490
|
+
if (!silence) {
|
|
491
|
+
logger.warn(`${name} 盒子不存在或者该盒子为一个真实盒子`);
|
|
492
|
+
}
|
|
407
493
|
return;
|
|
408
494
|
}
|
|
409
495
|
|
|
@@ -423,7 +509,7 @@ export class QemuVM {
|
|
|
423
509
|
return;
|
|
424
510
|
}
|
|
425
511
|
process.kill(pid);
|
|
426
|
-
|
|
512
|
+
logger.info(`${name} 盒子已停止`);
|
|
427
513
|
});
|
|
428
514
|
}
|
|
429
515
|
|