@lazycatcloud/lzc-cli 1.1.4 → 1.1.7
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 +1 -5
- package/cmds/dev.js +12 -4
- 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 +39 -16
- package/lib/box/hportal.js +7 -1
- package/lib/box/index.js +9 -6
- package/lib/box/qemu_vm_mgr.js +37 -23
- package/lib/builder.js +12 -1
- package/lib/dev.js +4 -9
- package/lib/env.js +9 -5
- package/lib/generator.js +2 -2
- package/lib/sdk.js +29 -5
- package/lib/utils.js +69 -46
- package/package.json +8 -2
- package/scripts/cli.js +91 -12
- 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/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
|
+
}
|
package/lib/box/check_qemu.js
CHANGED
|
@@ -1,27 +1,50 @@
|
|
|
1
1
|
// 检测当前系统是否安装 qemu 软件
|
|
2
2
|
import commandExist from "command-exists";
|
|
3
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
|
+
}
|
|
4
29
|
|
|
5
30
|
export class CheckQemu {
|
|
6
31
|
constructor() {
|
|
7
|
-
this.commands = ["qemu-img", "qemu-system-x86_64"
|
|
32
|
+
this.commands = ["qemu-img", "qemu-system-x86_64"];
|
|
8
33
|
}
|
|
9
34
|
|
|
10
35
|
async init() {
|
|
11
|
-
let
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
});
|
|
25
|
-
return Promise.all(ps);
|
|
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
|
+
}
|
|
26
49
|
}
|
|
27
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 [];
|
package/lib/box/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import { contextDirname } from "../utils.js";
|
|
|
7
7
|
import env, { sdkEnv } from "../env.js";
|
|
8
8
|
import { HportalManager, showBoxInfo } from "./hportal.js";
|
|
9
9
|
import { CheckQemu } from "./check_qemu.js";
|
|
10
|
+
import logger from "loglevel";
|
|
10
11
|
|
|
11
12
|
async function initQemuVM(ensureResources = false) {
|
|
12
13
|
let defaultSchemeFile = path.join(
|
|
@@ -25,6 +26,8 @@ async function initQemuVM(ensureResources = false) {
|
|
|
25
26
|
scheme.path = path.join(contextDirname(), "box-emulator", scheme.path);
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
fs.mkdirSync(scheme.path, { recursive: true });
|
|
30
|
+
|
|
28
31
|
if (ensureResources) {
|
|
29
32
|
// 使用本地网络 api 调试 new QemuResource(scheme.path, "http://127.0.0.1");
|
|
30
33
|
let resource = new QemuResource(scheme.path);
|
|
@@ -58,7 +61,7 @@ export function boxCommand(box) {
|
|
|
58
61
|
// 创建盒子阶段,如果出错,将直接删除所有的资源。
|
|
59
62
|
boxId = await m.createVM(answer);
|
|
60
63
|
} catch (error) {
|
|
61
|
-
|
|
64
|
+
logger.error(error);
|
|
62
65
|
await m.cleanVM(answer.boxName);
|
|
63
66
|
return;
|
|
64
67
|
}
|
|
@@ -72,11 +75,11 @@ export function boxCommand(box) {
|
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
let h = await initHportal();
|
|
75
|
-
|
|
78
|
+
logger.debug("添加盒子到 Hportal ......");
|
|
76
79
|
await h.addBox(answer.boxName);
|
|
77
|
-
|
|
80
|
+
logger.debug("正在使用管理员帐号密码登录中......");
|
|
78
81
|
await h.loginBox(boxId, answer.adminName, answer.adminPass);
|
|
79
|
-
|
|
82
|
+
logger.debug("登录成功!");
|
|
80
83
|
},
|
|
81
84
|
},
|
|
82
85
|
{
|
|
@@ -86,7 +89,7 @@ export function boxCommand(box) {
|
|
|
86
89
|
let m = await initQemuVM();
|
|
87
90
|
let boxid = await m.runVM(boxName);
|
|
88
91
|
if (boxid) {
|
|
89
|
-
|
|
92
|
+
logger.info("盒子ID: ", boxid);
|
|
90
93
|
}
|
|
91
94
|
},
|
|
92
95
|
},
|
|
@@ -125,7 +128,7 @@ export function boxCommand(box) {
|
|
|
125
128
|
|
|
126
129
|
// 过滤条件不满足
|
|
127
130
|
if (vmInfos.length == 0 && rmInfos == 0 && boxName) {
|
|
128
|
-
|
|
131
|
+
logger.info(`${boxName} 盒子不存在`);
|
|
129
132
|
let allVmInfos = await m.infoVM("", defaultBoxName);
|
|
130
133
|
let allRmInfos = await rm.boxs("", defaultBoxName);
|
|
131
134
|
showBoxInfo(allVmInfos, allRmInfos);
|
package/lib/box/qemu_vm_mgr.js
CHANGED
|
@@ -5,10 +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";
|
|
11
10
|
import fetch from "node-fetch";
|
|
11
|
+
import zlib from "node:zlib";
|
|
12
|
+
import os from "node:os";
|
|
13
|
+
import logger from "loglevel";
|
|
12
14
|
|
|
13
15
|
async function getFreePort() {
|
|
14
16
|
return new Promise((resolve, reject) => {
|
|
@@ -39,7 +41,7 @@ export class QemuResource {
|
|
|
39
41
|
*/
|
|
40
42
|
constructor(
|
|
41
43
|
diskDir = "~/.cache/box-emulator",
|
|
42
|
-
downloadUrl = "https://dl.lazycat.cloud/sdk
|
|
44
|
+
downloadUrl = "https://dl.lazycat.cloud/sdk"
|
|
43
45
|
) {
|
|
44
46
|
this.diskDir = diskDir;
|
|
45
47
|
this.downloadUrl = downloadUrl;
|
|
@@ -71,7 +73,7 @@ export class QemuResource {
|
|
|
71
73
|
* 目前的更新,只是使用 md5 来判断是否需要重新下载 md5, 并不会使用 md5 来判断所下载的数据是否正确。
|
|
72
74
|
*/
|
|
73
75
|
async shouldUpdate() {
|
|
74
|
-
let oldMd5Path = path.join(this.diskDir, "disk-system.qcow2.
|
|
76
|
+
let oldMd5Path = path.join(this.diskDir, "disk-system.qcow2.md5");
|
|
75
77
|
|
|
76
78
|
// 如果不存在 md5 文件,直接返回需要更新
|
|
77
79
|
try {
|
|
@@ -83,7 +85,7 @@ export class QemuResource {
|
|
|
83
85
|
// 拉取最新的 md5
|
|
84
86
|
let newMd5;
|
|
85
87
|
try {
|
|
86
|
-
let url = `${this.downloadUrl}/disk-system.qcow2.
|
|
88
|
+
let url = `${this.downloadUrl}/vm/disk-system.qcow2.md5`;
|
|
87
89
|
let res = await fetch(url);
|
|
88
90
|
if (res.status !== 200) {
|
|
89
91
|
return false;
|
|
@@ -109,26 +111,34 @@ export class QemuResource {
|
|
|
109
111
|
} catch {}
|
|
110
112
|
}
|
|
111
113
|
|
|
114
|
+
await this.downloadOVMF();
|
|
112
115
|
await this.downloadSystemDisk();
|
|
113
116
|
await this.downloadPrivateKey();
|
|
114
117
|
await this.downloadSystemDiskMd5();
|
|
115
118
|
}
|
|
116
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
|
+
|
|
117
127
|
async downloadSystemDiskMd5() {
|
|
118
|
-
let savePath = path.join(this.diskDir, "disk-system.qcow2.
|
|
119
|
-
let url = `${this.downloadUrl}/disk-system.qcow2.
|
|
128
|
+
let savePath = path.join(this.diskDir, "disk-system.qcow2.md5");
|
|
129
|
+
let url = `${this.downloadUrl}/vm/disk-system.qcow2.md5`;
|
|
120
130
|
await this.download(url, savePath);
|
|
121
131
|
}
|
|
122
132
|
|
|
123
133
|
async downloadSystemDisk() {
|
|
124
134
|
let savePath = path.join(this.diskDir, "disk-system.qcow2");
|
|
125
|
-
let url = `${this.downloadUrl}/disk-system.qcow2.
|
|
135
|
+
let url = `${this.downloadUrl}/vm/disk-system.qcow2.gz`;
|
|
126
136
|
await this.download(url, savePath, true);
|
|
127
137
|
}
|
|
128
138
|
|
|
129
139
|
async downloadPrivateKey() {
|
|
130
140
|
let savePath = path.join(this.diskDir, "id_rsa");
|
|
131
|
-
let url = `${this.downloadUrl}/id_rsa`;
|
|
141
|
+
let url = `${this.downloadUrl}/vm/id_rsa`;
|
|
132
142
|
await this.download(url, savePath);
|
|
133
143
|
fs.chmodSync(savePath, 0o400);
|
|
134
144
|
return;
|
|
@@ -147,7 +157,7 @@ export class QemuResource {
|
|
|
147
157
|
);
|
|
148
158
|
}
|
|
149
159
|
|
|
150
|
-
download(url, savePath,
|
|
160
|
+
download(url, savePath, enableGzip = false) {
|
|
151
161
|
let tmpPath = savePath + ".tmp";
|
|
152
162
|
const options = new URL(url);
|
|
153
163
|
let request = url.startsWith("https") ? https.request : http.request;
|
|
@@ -167,8 +177,8 @@ export class QemuResource {
|
|
|
167
177
|
});
|
|
168
178
|
|
|
169
179
|
let outputFile = fs.createWriteStream(tmpPath, { flags: "w+" });
|
|
170
|
-
if (
|
|
171
|
-
let decoder =
|
|
180
|
+
if (enableGzip) {
|
|
181
|
+
let decoder = zlib.createUnzip();
|
|
172
182
|
res.pipe(decoder).pipe(outputFile);
|
|
173
183
|
} else {
|
|
174
184
|
res.pipe(outputFile);
|
|
@@ -206,7 +216,7 @@ export class QemuVM {
|
|
|
206
216
|
p.on("error", (e) => {
|
|
207
217
|
throw e;
|
|
208
218
|
});
|
|
209
|
-
|
|
219
|
+
logger.debug("启动中...");
|
|
210
220
|
|
|
211
221
|
// 需要等待 boxid 的出现
|
|
212
222
|
return new Promise((resolve, reject) => {
|
|
@@ -230,7 +240,7 @@ export class QemuVM {
|
|
|
230
240
|
clearInterval(id);
|
|
231
241
|
p.unref();
|
|
232
242
|
resolve(boxId);
|
|
233
|
-
|
|
243
|
+
logger.debug("启动成功!");
|
|
234
244
|
}
|
|
235
245
|
}, 1000);
|
|
236
246
|
|
|
@@ -251,17 +261,17 @@ export class QemuVM {
|
|
|
251
261
|
async runVM(name) {
|
|
252
262
|
let boxid = this.readBoxid(name);
|
|
253
263
|
if (!boxid) {
|
|
254
|
-
|
|
264
|
+
logger.info(`${name} 盒子不存在,请通过 lzc-cli box create 创建`);
|
|
255
265
|
return;
|
|
256
266
|
}
|
|
257
267
|
let vmDir = path.join(this.scheme.path, `vm-${name}`);
|
|
258
268
|
let pid = parseVmPID(vmDir);
|
|
259
269
|
if (pid) {
|
|
260
|
-
|
|
270
|
+
logger.info(`${name} 盒子已经启动`);
|
|
261
271
|
return boxid;
|
|
262
272
|
}
|
|
263
273
|
await this.startVM(name, vmDir);
|
|
264
|
-
|
|
274
|
+
logger.info(`${name} 盒子启动成功!`);
|
|
265
275
|
}
|
|
266
276
|
|
|
267
277
|
/**
|
|
@@ -272,7 +282,7 @@ export class QemuVM {
|
|
|
272
282
|
this.ensureVolumeDir(vmDir);
|
|
273
283
|
await this.buildDisks(vmDir);
|
|
274
284
|
let boxId = await this.startVM(boxName, vmDir);
|
|
275
|
-
|
|
285
|
+
logger.info("盒子ID: ", boxId);
|
|
276
286
|
|
|
277
287
|
await this.registerVM(boxId, {
|
|
278
288
|
boxName,
|
|
@@ -363,7 +373,7 @@ export class QemuVM {
|
|
|
363
373
|
}
|
|
364
374
|
|
|
365
375
|
async buildSystemDisk(name, diskPath, diskInfo) {
|
|
366
|
-
|
|
376
|
+
logger.debug(`构建系统盘快照:${diskPath}`);
|
|
367
377
|
let baseImage = path.join(this.scheme.path, name);
|
|
368
378
|
return spawnSync(
|
|
369
379
|
"qemu-img",
|
|
@@ -373,7 +383,7 @@ export class QemuVM {
|
|
|
373
383
|
}
|
|
374
384
|
|
|
375
385
|
async buildDataDisk(name, diskPath, diskInfo) {
|
|
376
|
-
|
|
386
|
+
logger.debug(`构建数据盘:${diskPath}`);
|
|
377
387
|
return spawnSync(
|
|
378
388
|
"qemu-img",
|
|
379
389
|
["create", "-f", "qcow2", diskPath, diskInfo.size],
|
|
@@ -385,7 +395,11 @@ export class QemuVM {
|
|
|
385
395
|
* 寻找默认可用的 bios 文件
|
|
386
396
|
*/
|
|
387
397
|
findBIOS() {
|
|
388
|
-
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
|
+
];
|
|
389
403
|
for (let file of includes) {
|
|
390
404
|
try {
|
|
391
405
|
fs.accessSync(file, fs.constants.F_OK);
|
|
@@ -446,7 +460,7 @@ export class QemuVM {
|
|
|
446
460
|
if (pid) {
|
|
447
461
|
process.kill(pid);
|
|
448
462
|
}
|
|
449
|
-
|
|
463
|
+
logger.info(`${name} 盒子已停止`);
|
|
450
464
|
}
|
|
451
465
|
|
|
452
466
|
/**
|
|
@@ -474,7 +488,7 @@ export class QemuVM {
|
|
|
474
488
|
fs.accessSync(vmDir);
|
|
475
489
|
} catch {
|
|
476
490
|
if (!silence) {
|
|
477
|
-
|
|
491
|
+
logger.warn(`${name} 盒子不存在或者该盒子为一个真实盒子`);
|
|
478
492
|
}
|
|
479
493
|
return;
|
|
480
494
|
}
|
|
@@ -495,7 +509,7 @@ export class QemuVM {
|
|
|
495
509
|
return;
|
|
496
510
|
}
|
|
497
511
|
process.kill(pid);
|
|
498
|
-
|
|
512
|
+
logger.info(`${name} 盒子已停止`);
|
|
499
513
|
});
|
|
500
514
|
}
|
|
501
515
|
|
package/lib/builder.js
CHANGED
|
@@ -4,7 +4,7 @@ import path from "path";
|
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
import execa from "execa";
|
|
6
6
|
// import { archiveFolder } from "./utils.js";
|
|
7
|
-
import { DockerClient } from "./sdk.js";
|
|
7
|
+
import { dockerPullLzcAppsImage, DockerClient } from "./sdk.js";
|
|
8
8
|
import { DockerfileParser } from "dockerfile-ast";
|
|
9
9
|
import glob from "fast-glob";
|
|
10
10
|
import ignore from "@balena/dockerignore";
|
|
@@ -213,6 +213,17 @@ export default class Builder {
|
|
|
213
213
|
console.log(host);
|
|
214
214
|
const docker = await new DockerClient(host).init();
|
|
215
215
|
try {
|
|
216
|
+
// TODO, 使用 docker.pull 方法会失败, 原因未知, 使用 workaround 方案
|
|
217
|
+
// docker.pull(
|
|
218
|
+
// "hello-world",
|
|
219
|
+
// // "registry.lazycat.cloud/lzc/lzcapp:0.1",
|
|
220
|
+
// // { authconfig: { auth: "bG5rczpsbmtzMjAyMA==" } },
|
|
221
|
+
// (err, stream) => {
|
|
222
|
+
// console.log("====", err, stream);
|
|
223
|
+
// }
|
|
224
|
+
// );
|
|
225
|
+
await dockerPullLzcAppsImage(host);
|
|
226
|
+
|
|
216
227
|
return new Promise(async (resolve, reject) => {
|
|
217
228
|
let refresher = new StatusRefresher();
|
|
218
229
|
|
package/lib/dev.js
CHANGED
|
@@ -13,8 +13,8 @@ import path from "path";
|
|
|
13
13
|
import env, { sdkEnv } from "./env.js";
|
|
14
14
|
import _get from "lodash.get";
|
|
15
15
|
import { execPreBuild } from "./builder.js";
|
|
16
|
-
import Key from "./key.js";
|
|
17
16
|
import chokidar from "chokidar";
|
|
17
|
+
import commandExists from "command-exists";
|
|
18
18
|
|
|
19
19
|
import Archiver from "../lib/archiver.js";
|
|
20
20
|
|
|
@@ -138,14 +138,12 @@ class DevModule {
|
|
|
138
138
|
keyFile,
|
|
139
139
|
].join(" ");
|
|
140
140
|
// 检查rsync工具是否存在:提示用户
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
stdio: "ignore",
|
|
144
|
-
});
|
|
145
|
-
} catch {
|
|
141
|
+
const rsyncExisted = commandExists.sync('rsync')
|
|
142
|
+
if (!rsyncExisted) {
|
|
146
143
|
console.log(chalk.red("请检查 rsync 是否安装,路径是否正确!"));
|
|
147
144
|
process.exit();
|
|
148
145
|
}
|
|
146
|
+
// FIXME: 下方执行命令不确定是否有兼容性问题
|
|
149
147
|
execSync(
|
|
150
148
|
`rsync --rsh='${rsh}' -czrPR . root@${appAddr}:/root/${this.appId} --filter=':- .gitignore' --exclude='node_modules' --exclude='.git' --delete`,
|
|
151
149
|
{ stdio: ["ignore", "ignore", "inherit"] }
|
|
@@ -263,9 +261,6 @@ class DevModule {
|
|
|
263
261
|
let appAddr;
|
|
264
262
|
|
|
265
263
|
try {
|
|
266
|
-
// 确保 sdk 能通过公钥正常访问
|
|
267
|
-
await new Key().ensure(sdkEnv.sdkUrl);
|
|
268
|
-
|
|
269
264
|
keyFile = path.join(this.tempDir, "debug/ssh/id_ed25519");
|
|
270
265
|
appAddr = this.getAddress();
|
|
271
266
|
|
package/lib/env.js
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
import { debuglog } from "util";
|
|
13
13
|
import inquirer from "inquirer";
|
|
14
14
|
import chalk from "chalk";
|
|
15
|
-
import
|
|
15
|
+
import { request } from "./autologin.js";
|
|
16
16
|
const debug = debuglog("lib/env");
|
|
17
17
|
|
|
18
18
|
const GLOBAL_CONFIG_NAME = "box-config.json";
|
|
@@ -83,7 +83,7 @@ async function checkURL(url, suffix = "") {
|
|
|
83
83
|
}, 5000);
|
|
84
84
|
|
|
85
85
|
try {
|
|
86
|
-
const resp = await
|
|
86
|
+
const resp = await request(url + suffix, { signal: controller.signal });
|
|
87
87
|
if (resp.status != 200) {
|
|
88
88
|
// 设备可以访问 (client 有正常连接), 但是sdk 这个服务无法访问
|
|
89
89
|
throw new Error(
|
|
@@ -99,9 +99,6 @@ async function checkURL(url, suffix = "") {
|
|
|
99
99
|
case "FetchError":
|
|
100
100
|
console.log(chalk.red("盒子入口地址有误,请核对后再试"));
|
|
101
101
|
process.exit();
|
|
102
|
-
case "AbortError":
|
|
103
|
-
console.log(chalk.red(`请求 ${url} 超时, 请确保懒猫云客户端已连接`));
|
|
104
|
-
process.exit();
|
|
105
102
|
default:
|
|
106
103
|
throw error;
|
|
107
104
|
}
|
|
@@ -246,6 +243,13 @@ class SDKEnv {
|
|
|
246
243
|
return "";
|
|
247
244
|
}
|
|
248
245
|
|
|
246
|
+
get sdkHostName() {
|
|
247
|
+
if (env.has("DEFAULT_BOXNAME")) {
|
|
248
|
+
return `sdk.${env.get("DEFAULT_BOXNAME")}.heiyu.space`;
|
|
249
|
+
}
|
|
250
|
+
return "";
|
|
251
|
+
}
|
|
252
|
+
|
|
249
253
|
async ensure() {
|
|
250
254
|
if (this.sdkUrl) {
|
|
251
255
|
try {
|
package/lib/generator.js
CHANGED
|
@@ -12,7 +12,7 @@ export const TemplateConfig = {
|
|
|
12
12
|
ionic_vue3: {
|
|
13
13
|
template: "ionic_vue3",
|
|
14
14
|
defaultEnvs: {
|
|
15
|
-
HTTP_SERVICE_PORT: "
|
|
15
|
+
HTTP_SERVICE_PORT: "80",
|
|
16
16
|
BUILD_CONTEXT: ".lazycat/release",
|
|
17
17
|
},
|
|
18
18
|
after: async function (name) {
|
|
@@ -43,7 +43,7 @@ export const TemplateConfig = {
|
|
|
43
43
|
vue: {
|
|
44
44
|
template: "vue",
|
|
45
45
|
defaultEnvs: {
|
|
46
|
-
HTTP_SERVICE_PORT: "
|
|
46
|
+
HTTP_SERVICE_PORT: "80",
|
|
47
47
|
BUILD_CONTEXT: ".lazycat/release",
|
|
48
48
|
},
|
|
49
49
|
after: async function (name) {
|
package/lib/sdk.js
CHANGED
|
@@ -4,8 +4,8 @@ import Docker from "dockerode";
|
|
|
4
4
|
import process from "process";
|
|
5
5
|
import fs from "fs";
|
|
6
6
|
import { Client } from "ssh2";
|
|
7
|
-
import chalk from "chalk";
|
|
8
7
|
import Key from "./key.js";
|
|
8
|
+
import logger from "loglevel";
|
|
9
9
|
|
|
10
10
|
export async function connectOptions(host) {
|
|
11
11
|
const pairs = await new Key().getKeyPair();
|
|
@@ -38,7 +38,7 @@ export class DockerClient {
|
|
|
38
38
|
async init() {
|
|
39
39
|
this.docker = !!this.host
|
|
40
40
|
? new Docker({
|
|
41
|
-
protocal: "
|
|
41
|
+
protocal: "ssh",
|
|
42
42
|
agent: ssh(await connectOptions(this.host)),
|
|
43
43
|
})
|
|
44
44
|
: new Docker();
|
|
@@ -47,9 +47,7 @@ export class DockerClient {
|
|
|
47
47
|
|
|
48
48
|
async function onUncaughtException(e) {
|
|
49
49
|
if (e.code == "ECONNREFUSED") {
|
|
50
|
-
|
|
51
|
-
chalk.red(`无法连接 sdk 服务, 请确保 ${chalk.yellow(host)} 可以访问`)
|
|
52
|
-
);
|
|
50
|
+
logger.error(`无法连接 sdk 服务, 请确保 ${host} 可以访问`);
|
|
53
51
|
process.exit(1);
|
|
54
52
|
} else {
|
|
55
53
|
throw e;
|
|
@@ -78,6 +76,7 @@ export class SSHClient {
|
|
|
78
76
|
this.con
|
|
79
77
|
.on("ready", () => resolve(this.con))
|
|
80
78
|
.on("error", (err) => {
|
|
79
|
+
logger.error("ssh client ", err);
|
|
81
80
|
reject(err);
|
|
82
81
|
})
|
|
83
82
|
.on("close", () => resolve(null))
|
|
@@ -124,3 +123,28 @@ export class SSHClient {
|
|
|
124
123
|
);
|
|
125
124
|
}
|
|
126
125
|
}
|
|
126
|
+
|
|
127
|
+
export async function dockerPullLzcAppsImage(host) {
|
|
128
|
+
logger.warn("* 更新lzcapp镜像, 未来可能会移除此操作");
|
|
129
|
+
const opts = await connectOptions(host);
|
|
130
|
+
const client = new SSHClient(opts);
|
|
131
|
+
await client.connect();
|
|
132
|
+
try {
|
|
133
|
+
const stream = await client.exec(
|
|
134
|
+
"docker pull registry.lazycat.cloud/lzc/lzcapp:0.1"
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
return new Promise((resolve, reject) => {
|
|
138
|
+
stream.stdout.pipe(process.stdout);
|
|
139
|
+
stream.stderr.pipe(process.stdout);
|
|
140
|
+
stream.on("close", () => {
|
|
141
|
+
client.close();
|
|
142
|
+
resolve();
|
|
143
|
+
});
|
|
144
|
+
stream.on("error", (e) => reject(e));
|
|
145
|
+
});
|
|
146
|
+
} catch (e) {
|
|
147
|
+
client.close();
|
|
148
|
+
throw e;
|
|
149
|
+
}
|
|
150
|
+
}
|