镜像交付相关资料.md

Linux 自定义认证

背景

​ 结合https://doc.weixin.qq.com/doc/w3_ARkAWAYlAEQRlVumzgDTgeXJMzBBa?scode=ABwA9Qd2ABEQYUs45PARkAWAYlAEQ使用
为了限制用户登录到系统的后台随意查看、修改应用程序,通常来说不应该把特权账号的密码泄露给用户,然而使用固定密码始终会有密码泄露的风险,时间长了肯定会泄露。为此,特地调研了一下某厂商的D,F,T三个产品是怎么处理这种事情的。
产品代号 解决方案 实现难度
D 无法打开用户登录界面,自行提供了另一套交互界面,无需登录,也无法登录其他用户。命令行受限 未知
F 使用动态密码登录,用厂家提供的APP扫码(或输入token)来获取临时密码登录。只有有售后权限的人才能扫码登录 中,魔改程度小
T 提供低权限账户,使用固定密码登录,其余未知(因为全盘加密了,不清楚特权账户是否为固定密码) 中,魔改程度小
因为技术栈不统一,三个产品的防护各有不同,这里F产品使用的方法是一种简单有效的办法,所以考虑预研一下,看看效果。

pam demo
F产品的实现原理是自己编写了一个linux PAM模块,然后在配置文件中加载这段逻辑,就可以实现扩展linux登录时的验证。
这里先写个pam demo验证一下机制

1
2
3
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
30
31
32
33
34
35
#include <security/pam_appl.h>
#include <security/pam_modules.h>
#include <security/pam_ext.h>
#include <stdio.h>
#include <string.h>

PAM_EXTERN int pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv) {
const char *username;
const char *password;
int pam_err;

pam_err = pam_get_user(pamh, &username, "Username: ");
if (pam_err != PAM_SUCCESS || username == NULL) {
return PAM_AUTH_ERR;
}

// 获取用户输入的密码
pam_err = pam_get_authtok(pamh, PAM_AUTHTOK, &password, "Password: ");
if (pam_err != PAM_SUCCESS || password == NULL) {
return PAM_AUTH_ERR;
}

// 检查密码是否为预期的值(例如,"123")
if (strcmp(password, "123") == 0) {
return PAM_SUCCESS; // 密码验证成功
} else {
return PAM_AUTH_ERR; // 密码验证失败
}
}

PAM_EXTERN int pam_sm_setcred(pam_handle_t *pamh, int flags, int argc, const char **argv) {
// 在这里实现设置凭据(credential)的逻辑,如果不需要,可以留空
return PAM_SUCCESS;
}

将代码保存为custom_auth.c 编译并修改sudo的pam配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 编译
gcc -fPIC -shared -o custom_auth.so custom_auth.c -lpam
# 拷贝到正确的位置
cp custom_auth.so /usr/lib/x86_64-linux-gnu/security/
# -rw-r--r-- root:root 不需要执行权限
chmod -x /usr/lib/x86_64-linux-gnu/security/custom_auth.so

# 增加测试用户
sudo useradd -m testuser
sudo usermod -aG sudo testuser
# 将密码改为和用户名相同
passwd testuser

# 修改sudo的PAM配置,在第一行增加 auth sufficient custom_auth.so
vim /etc/pam.d/sudo
su testuser

# 此时输入testuser或者123都能成功,其他的密码则失败
sudo -k cat /etc/passwd

qrcode demo

​ 使用二维码输入token不仅方便,还能减少输错的可能,这里是一个简单的验证控制台输出二维码的例子,效果如图(还有多余的边框到时候再修修),能够在控制台、ssh等任意字符界面正常显示。

1
2
3
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
#include <qrencode.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static int margin;

static void writeANSI_margin(FILE* fp, int realwidth, char* buffer,
const char* white, int white_s) {
int y;

strncpy(buffer, white, (size_t)white_s);
memset(buffer + white_s, ' ', (size_t)realwidth * 2);
strcpy(buffer + white_s + realwidth * 2,
"\033[0m\n"); // reset to default colors
for (y = 0; y < margin; y++) {
fputs(buffer, fp);
}
}

static void writeANSI(const char* msg,FILE* fp, int size) {
const char *white, *black;
char* buffer;
int white_s, black_s, buffer_s;
unsigned char *row, *p;
int x, y;
int realwidth;
int last;
QRcode* qrcode =
QRcode_encodeString(msg, 0, QR_ECLEVEL_L, QR_MODE_8, 1);

if (qrcode == NULL) {
fprintf(stderr, "cannot parse msg to qrcode");
return;
}

white = "\033[47m";
white_s = 5;
black = "\033[40m";
black_s = 5;

realwidth = (qrcode->width + margin * 2) * size;
buffer_s = (realwidth * white_s) * 2;
buffer = (char*)malloc((size_t)buffer_s);
if (buffer == NULL) {
fprintf(stderr, "Failed to allocate memory.\n");
exit(EXIT_FAILURE);
}

/* top margin */
writeANSI_margin(fp, realwidth, buffer, white, white_s);

/* data */
p = qrcode->data;
for (y = 0; y < qrcode->width; y++) {
row = (p + (y * qrcode->width));

memset(buffer, 0, (size_t)buffer_s);
strncpy(buffer, white, (size_t)white_s);
for (x = 0; x < margin; x++) {
strncat(buffer, " ", 2);
}
last = 0;

for (x = 0; x < qrcode->width; x++) {
if (*(row + x) & 0x1) {
if (last != 1) {
strncat(buffer, black, (size_t)black_s);
last = 1;
}
} else if (last != 0) {
strncat(buffer, white, (size_t)white_s);
last = 0;
}
strncat(buffer, " ", 2);
}

if (last != 0) {
strncat(buffer, white, (size_t)white_s);
}
for (x = 0; x < margin; x++) {
strncat(buffer, " ", 2);
}
strncat(buffer, "\033[0m\n", 5);
fputs(buffer, fp);
}

/* bottom margin */
writeANSI_margin(fp, realwidth, buffer, white, white_s);

free(buffer);
}

int main()
{
margin = 1;
writeANSI("token:shrino.ouyrun.cn",stdout, 3);
}

编译运行:
先安装动态库 apt install libqrencode-dev
gcc qrtest.c -lqrencode

C/S demo
实现实现随机生成6位数数字密码,然后用RSA加密,将加密后的信息放到二维码,然后售后人员通过将二维码上传到服务器后(这里可以是用手机APP扫码,也可以是拍屏手动处理),服务器用RSA解密后,将结果返回给用户。

随机密码生成

1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int generateRandomPassword() {
srand(time(NULL));
return rand() % 1000000; // 生成0到999999之间的随机数
}


加密部分

1
2
3
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <stdio.h>
#include <stdlib.h>
#include <openssl/rsa.h>
#include <openssl/pem.h>
#include <string.h>

void rsaEncryptPassword(const char* password, const char* publicKeyPath, const char* encryptedPath) {
FILE* publicKeyFile = fopen(publicKeyPath, "rb");
if (!publicKeyFile) {
perror("Failed to open public key file");
return;
}

RSA* rsa = PEM_read_RSA_PUBKEY(publicKeyFile, NULL, NULL, NULL);
if (!rsa) {
perror("Failed to read public key");
fclose(publicKeyFile);
return;
}

fclose(publicKeyFile);

int passwordLen = strlen(password);
unsigned char encrypted[256]; // 要根据RSA密钥的长度来设置
int encryptedLen = RSA_public_encrypt(passwordLen, (unsigned char*)password, encrypted, rsa, RSA_PKCS1_PADDING);

if (encryptedLen == -1) {
perror("RSA encryption failed");
RSA_free(rsa);
return;
}

FILE* encryptedFile = fopen(encryptedPath, "wb");
if (!encryptedFile) {
perror("Failed to open encrypted file");
RSA_free(rsa);
return;
}

fwrite(encrypted, sizeof(unsigned char), encryptedLen, encryptedFile);
fclose(encryptedFile);

RSA_free(rsa);
}

int main()
{
rsaEncryptPassword("123456", "public_key.pem", "test_encrypt_out");
return 0;
}


安装开发库

1
2
3
4
5
6
7
# 安装开发库
apt install libssl-dev
# 生成密钥对(这里用1024位
openssl genrsa -out private_key.pem 1024
openssl rsa -pubout -in private_key.pem -out public_key.pem
# 编译
gcc rsa_enc.c -o rsa_enc -lssl -lcrypto

生成二维码部分

1
2
3
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
#include <stdio.h>
#include <qrencode.h>
#include <stdlib.h>
#include <string.h>

static int margin = 1;

static void writeANSI_margin(FILE* fp, int realwidth, char* buffer,
const char* white, int white_s) {
int y;

strncpy(buffer, white, (size_t)white_s);
memset(buffer + white_s, ' ', (size_t)realwidth * 2);
strcpy(buffer + white_s + realwidth * 2,
"\033[0m\n"); // reset to default colors
for (y = 0; y < margin; y++) {
fputs(buffer, fp);
}
}

static void writeANSI(QRcode* qrcode,FILE* fp, int size) {
const char *white, *black;
char* buffer;
int white_s, black_s, buffer_s;
unsigned char *row, *p;
int x, y;
int realwidth;
int last;

if (qrcode == NULL) {
return;
}

white = "\033[47m";
white_s = 5;
black = "\033[40m";
black_s = 5;

realwidth = (qrcode->width + margin * 2) * size;
buffer_s = (realwidth * white_s) * 2;
buffer = (char*)malloc((size_t)buffer_s);
if (buffer == NULL) {
fprintf(stderr, "Failed to allocate memory.\n");
exit(EXIT_FAILURE);
}

/* top margin */
writeANSI_margin(fp, realwidth, buffer, white, white_s);

/* data */
p = qrcode->data;
for (y = 0; y < qrcode->width; y++) {
row = (p + (y * qrcode->width));

memset(buffer, 0, (size_t)buffer_s);
strncpy(buffer, white, (size_t)white_s);
for (x = 0; x < margin; x++) {
strncat(buffer, " ", 2);
}
last = 0;

for (x = 0; x < qrcode->width; x++) {
if (*(row + x) & 0x1) {
if (last != 1) {
strncat(buffer, black, (size_t)black_s);
last = 1;
}
} else if (last != 0) {
strncat(buffer, white, (size_t)white_s);
last = 0;
}
strncat(buffer, " ", 2);
}

if (last != 0) {
strncat(buffer, white, (size_t)white_s);
}
for (x = 0; x < margin; x++) {
strncat(buffer, " ", 2);
}
strncat(buffer, "\033[0m\n", 5);
fputs(buffer, fp);
}

/* bottom margin */
writeANSI_margin(fp, realwidth, buffer, white, white_s);

free(buffer);
}

int generateQRCode(const char* encryptedPath) {
FILE* encryptedFile = fopen(encryptedPath, "rb");
if (!encryptedFile) {
perror("Failed to open encrypted file");
return 1;
}

fseek(encryptedFile, 0, SEEK_END);
long encryptedFileSize = ftell(encryptedFile);
rewind(encryptedFile);

unsigned char* encryptedData = (unsigned char*)malloc(encryptedFileSize);
if (!encryptedData) {
perror("Memory allocation failed");
fclose(encryptedFile);
return 1;
}

fread(encryptedData, sizeof(unsigned char), encryptedFileSize, encryptedFile);
fclose(encryptedFile);

QRcode* qrcode = QRcode_encodeData(encryptedFileSize, encryptedData, 0,QR_ECLEVEL_L);
if (!qrcode) {
perror("QRcode encoding failed");
free(encryptedData);
return 1;
}

writeANSI(qrcode,stdout,5);

return 0;
}

int main()
{
generateQRCode("test_encrypt_out");
}

服务端解码部分(未验证)

1
2
3
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
package main

import (
"fmt"
"io/ioutil"
"net/http"
"strings"

"github.com/rsc/qr"
"github.com/rsc/qr/handy"
)

func main() {
http.HandleFunc("/process_qr_code", func(w http.ResponseWriter, r *http.Request) {
// 读取上传的二维码图片
file, _, err := r.FormFile("qr_code")
if err != nil {
http.Error(w, "Failed to read QR code file", http.StatusBadRequest)
return
}
defer file.Close()

// 解析二维码
img, _, err := handy.Decode(file)
if err != nil {
http.Error(w, "Failed to decode QR code", http.StatusInternalServerError)
return
}

// 从解析的二维码中提取加密数据
encodedData := img.Content
encryptedData := strings.TrimSpace(encodedData)


// 加载RSA私钥
privateKeyPEM, err := ioutil.ReadFile("private_key.pem")
if err != nil {
http.Error(w, "Failed to read private key", http.StatusInternalServerError)
return
}

privateKeyBlock, _ := pem.Decode(privateKeyPEM)
if privateKeyBlock == nil {
http.Error(w, "Failed to decode private key", http.StatusInternalServerError)
return
}

privateKey, err := x509.ParsePKCS1PrivateKey(privateKeyBlock.Bytes)
if err != nil {
http.Error(w, "Failed to parse private key", http.StatusInternalServerError)
return
}

// 解密数据
decryptedData, err := rsa.DecryptPKCS1v15(nil, privateKey, encryptedData)
if err != nil {
http.Error(w, "Failed to decrypt data", http.StatusInternalServerError)
return
}
// 返回解密后的数据
w.Header().Set("Content-Type", "text/plain")
fmt.Fprintf(w, "Decrypted Data: %s", decryptedData)
})

http.ListenAndServe(":8080", nil)
}


参考资料
Linux下PAM模块学习总结 - 散尽浮华 - 博客园 https://www.cnblogs.com/kevingrace/p/8671964.html

Linux PAM开发示例二:登录系统时使用自己的PAM模块进行密码认证-CSDN博客

软件保护

软件保护目的:防止软件被未授权下载、复制、安装、运行、转让、出租、修改、破解

软件保护从来都是多方面实现的,不能仅靠软件自身。这里从4个维度讨论一下各个层面可以做的事情。
image-20240307114725324

软件授权通常出现在商业软件中,其可以对软件的安装、运行做出限制,例如未授权的用户使用该软件(或者该软件的某些功能)。
后台隐藏通常在TOB产品的系统级别产品中,通过WEBUI/GUI等隐藏实现细节,常常伴随着系统后台信息隐藏、(售后)角色认证等机制,它防范的是用户进入软件的运行基座——操作系统。而原本操作系统的一些基础功能(如时间日期修改、IP地址修改),则通过UI来提供给用户。
售后支持是在软件出现问题时,由特定的售后人员去通过网络直连、远程桌面、线下操作等方式。网络直连通常发生在一些内置了对应模块的软件中,它允许售后人员直接连接到软件的控制台,可以看到用户的配置和软件的信息等。线下操作的形式有多种,简单的可以通过用户界面操作,有些特殊的操作则需要进入系统后台操作,这时,前面提到的”后台隐藏“必须要留下售后人员的入口。
加密是一种简单而有效的保护方式,加密的级别有很多,例如文件加密、数据库加密、磁盘加密。加密的算法一般是防护的关键,需要用各种手段降低算法被探测的可能,而密钥的存储比密钥的强度更重要。

下面讨论一下各个部分的方案细节
对于在线使用的软件,让用户自行输入用户名是一种简单有效的机制,而对于允许离线使用的软件,需要收集软件的运行环境信息生成一个和环境相关的ID,这个ID通常成为机器码,收集的信息通常是硬件信息。然而,在虚拟化环境、docker环境下,情况可能略有不同,由于虚拟化环境的硬件信息不准确、容易发生变更,而docker就更加没有硬件信息,所以很多厂商都会采用一种叫授权服务器的东西(例如深信服VLS)来应对,这个后文再讨论。
软件厂商通过非对称或者对称加密技术,针对用户提供的ID来提供一段经过加密的序列号/授权文件,软件通过特定的算法校验ID和序列号/授权文件之间的对应关系。以某个厂商的序列号为例:软件通过计算网卡MAC地址的CRC32得到ID,用户提供ID后,厂商通过DES算法和固定密钥将ID变为一串序列号,这串序列号可以由用户手动输入软件中,软件通过DES算法和代码内置的固定密钥解密序列号,得到的信息和ID比较。
为了避免用户进入操作系统后台,通常厂商不会提供系统的特权账号密码,然而特权账号的密码在公司里面很多人都知道,例如研发员工、技术支持员工,这其中存在很多泄密的可能。所以有些厂商会修改操作系统的认证机制,实现自定义的认证逻辑,例如扫码登录、动态密码等。这里以linux系统为例,linux系统有一种叫PAM(Pluggable Authentication Modules)的东西,可以简单理解为认证插件,我们可以用c语言来实现对应的接口,然后把代码编译后以动态链接库的形式放到特定目录,修改配置文件即可,不需要修改操作系统的源代码。
相应的,我们虽然阻止了用户对系统后台的访问权限,但是也要给技术支持/售后等留下入口。首先是软硬件的信息收集,平常我们可以进入系统后台,手动查看、拷贝日志文件,查看环境信息,在高度封闭系统后,这些信息通常会通过一个界面,允许用户/技术支持来采集,有必要的时候,再通过售后系统获取系统的登录口令,进入后台手动操作。
有些软件会通过全盘加密的方式阻止用户将磁盘挂载到其他系统进行解析,常规的实现是用LUKS来全盘加密,然后在开机阶段去特定的位置获取密钥,大大增加了破解的难度。

设计文档

版本修订记录:

日期 版本 修订人 修改内容 备注
2023-11-1 0.1 岑思华 方案定型,文档基本完成
2023-11-6 0.2 岑思华 补充打包项目代码设计 Makefile
2023-11-7 0.3 岑思华 补充对其他模块影响;调整项目结构设计 已评审
2023-11-9 0.4 岑思华 1.微调;2. 确定磁盘显示方案
2023-11-14 0.5 岑思华
2023-11-15 0.6 袁菘壑 添加5.2.1.2章节
2023-11-22 0.7 岑思华 添加5.2.5章节
2023-12 0.8 吴松达 补充7.2.5的mongo动态配置实现

1 项目背景

安装包交付背景下,操作系统由用户提供,用户拥有最高权限,能够随意对软件进行查看、修改、复制等,极大的增加了反破解的难度。

镜像交付是友商广泛使用的一种做法,它除了体积比较大之外,能够提供比安装包交付更加标准、便捷的过程。

2 项目目标

提供镜像交付,将操作系统和石犀软件作为一个交付整体,不再让用户提供操作系统。

适配主流的虚拟化平台的交付。

闭环镜像交付各个环节可能需要解决的技术问题

TUI部分的设计在另一篇文档里面 TUI模块设计说明书

3 名词解释

镜像:本文泛指ISO镜像文件、OVA镜像文件、QCOW2镜像文件

ISO映像:即符合ISO 9660标准的文件系统,这个标准定义了目录结构、文件名规则、目录深度、文件属性等一系列东西。对于本文而言,ISO映像包含用于安装的引导程序、操作系统本身、其他附加应用,是一个文件。

ISO镜像:和ISO映像是一个概念。

系统安装:本文的系统安装指的是,将ISO文件挂载成虚拟DVD设备来引导操作系统的安装。

镜像导入:将特定格式的镜像文件上传到虚拟化平台称为镜像导入,不同的平台使用不同格式的文件,例如OVA。

VMDK: 是一种文件格式,用来作为虚拟磁盘的容器,最初是由vmware开发使用,后来发布OVF标准后,成为开放格式,为大多数虚拟化平台所支持。

QCOW2:即qcow的第二个版本,和vmdk类似,qcow也是一种虚拟磁盘容器。qcow目前有三个版本,国内大部分公有云支持的都是qcow2格式。

OVF文件:一种用于打包虚拟机配置信息(如CPU,内存,磁盘,网卡)的文件,它也是一种比较主流的规范,大多数虚拟化平台都支持。

OVA:一种打包格式,和tar是同一种标准,相当于tar包改了后缀名。里面包含至少两个文件:ovf和vmdk,还有一个可选的mf文件(存储其他文件的哈希值)

分区扩容:指在现有的分区上扩大分区空间,虽然虚拟磁盘可以调整大小,但是调整后的大小并不会直接反应到分区上,需要对分区进行扩容以适配新的磁盘大小。

关机扩容:即分区扩容需要关机或者重启,扩容才能生效。

开机扩容:即热扩容,分区扩容不需要重启就能生效。

包:包原本指将总控和引擎的二进制、配置、依赖等压缩在一起的一个可用于安装、升级的包。在本文中将其含义进一步泛化,将镜像文件也归类到包的范畴,会以镜像包指代镜像。

精简置备:精简置备是一种动态分配存储空间的技术,它允许在创建时仅分配必要的存储空间,而不是预先分配整个空间。这意味着虚拟磁盘的实际大小可以根据需要动态地增长,直到达到预设的上限。精简置备技术可以有效地提高存储利用率,因为它可以避免为未来可能用不上的空间预留存储容量。

厚置备:厚置备是一种静态分配存储空间的技术,它会在虚拟磁盘创建时立即分配整个设定空间。这意味着虚拟磁盘会占用所分配的空间,即使实际上并没有使用全部空间。厚置备会提供更高的性能,因为存储空间已经被预先分配并保留,但会导致低效的存储利用率。

4 总体设计

4.1 概要设计

4.1.1 实现的功能

概要描述该模块要实现的功能。列出要实现的功能点及子功能点,并对每一个功能点进行详细说明。功能点之间的层级和关联关系要明晰。这里仅描述功能,不需要涉及实现方案、功能取舍等问题。

  1. 实现自动化镜像制作

  2. 镜像按照需求的描述,统一各项系统设置,安装过程自动化

    1. 按照需求进行分区

    2. 按照需求定制grub引导

  3. 实现分区的关机扩容

4.1.2 设计的性能指标

4.2 覆盖范围

描述本次项目覆盖的范围,为了降低风险,建议项目上线前做好灰度范围控制

  1. 打包平台-出包流程

  2. 总控安装、部署、升级

5 详细设计

5.1 设计图

出包过程图

本图描述有哪些包,这些包的内容是从哪来的。

图标 描述已自动生成

镜像制作(image-builder)项目结构设计

安装部署流程

本图提供安装部署的流程对比,左边是没有做这块需求的时候的逻辑,右边是有了镜像交付之后的逻辑.

图标 描述已自动生成

镜像生命周期

安装前包括打包的部分、将包上传到网盘、客户下载包、导入包

安装中主要是导入进度条跑完之后,镜像的安装只需要调整配置,进度条即是上传的进度

图标 描述已自动生成

5.2 各模块详细设计

对各个模块进行详细阐述,数据之间的关联关系, 使用的何种算法, 明确的接口定义(包含输入/输出/异常/错误码), 尽量明确到代码层级,达到设计文档交给第三个人也可以按照文档要求进行代码开发

5.2.1 基ISO镜像详细设计

需求回顾

  1. 禁用snap包管理器

  2. 禁用系统自动更新

  3. 系统参数预定义(含账户、分区、语言等)

由于基镜像难以自动化制作,所以基镜像需要尽可能做到少更新,里面不包含总控应用,总控由打包流程集成到基镜像中作为正式镜像。

后续引擎也需要考虑镜像交付,所以基镜像既是总控的基镜像,也是引擎的基镜像。不管是总控还是引擎,都应该能够顺利的在该镜像里面安装,不会出现系统版本、内核版本问题,不会出现底层依赖缺失问题。

目前的设计仅考虑Legacy BIOS模式即可,不需要考虑UEFI模式。

方案一:使用cubic+seed制作

方案二:使用xorrios+cloud-init制作

方案一在ubuntu20.04不可行,故使用方案二

5.2.1.1 使用cubic制作基镜像流程
  1. 下载一个官方的ISO镜像,这里使用ubuntu 20.04.6

  2. 用cubic打开官方ISO镜像

  3. 定制发行版名字

  4. 进入chroot环境,通过命令行进行定制

    1. 删除不需要的包管理器(snap)

    2. 关闭系统自动更新(unattended-upgrades)(可以在preseed里搞定)

  5. 选择内核(如果有多个)

  6. 编写preseed文件,设定参数

    1. 定制网络配置

    2. 定制磁盘分区

    3. 定制语言及键盘

    4. 定制用户名密码、主机名

    5. 定制预装的软件(ssh server)

    6. 定制GRUB密码

  7. 修改/isolinux/txt.cfg,指向preseed文件

preseed文件详细设计

参考资料

【精选】使用 “Cubic” 制作自定义 “ubuntu” 系统镜像_cubic制作镜像_此木子的博客-CSDN博客

https://github.com/sexibytes/packer-sexigraf/blob/master/preseed.cfg

18.04 - Preseed config (using cubic) is ignored during installation - Ask Ubuntu

https://gist.github.com/styblope/2cf93a41662608f924de71fd0e91e0d1

B.4.Contents of the preconfiguration file (for bullseye)

使用preseed一键安装Ubuntu Server 1604-阿里云开发者社区

ubuntu preseed无人应答安装 · zhangguanzhang's Blog

https://sysin.org/blog/disable-ubuntu-auto-update/

GitHub - covertsh/ubuntu-autoinstall-generator: Generate a fully-automated Ubuntu ISO for unattended installations.

5.2.1.2 使用xorriso制作基镜像流程
  1. 下载一个官方的ISO镜像,这里使用ubuntu 20.04.6

  2. 使用xorriso解压镜像文件

  3. 通过unsquash将解压出来的文件filesystem.squashfs进行反编译

  4. 通过chroot操作解压出来的filesystem.squashfs文件系统,对应安装后的ubuntu根目录,执行完毕后,将squashfs文件重新打包成filesystem.squashfs文件,放入解压的镜像目录中。

  5. ubuntu20.04及其之后都使用cloud-init方式实现自动化安装,将自动安装参数autoinstall ds=nocloud;s=/cdrom/cloud_init/添加到isolinux/txt.cfg文件中quiet之前,然后将必要的文件user-data, meta-data复制到制定文件路径 /cloud_init目录下。在user-data和meta-data中即可开启自动安装设置,不设置即走默认的安装设置。

    1. 定制网络配置 // 已完成

    2. 前置资源检查 // 如何告诉用户?

    3. 定制磁盘分区 // 已完成

    4. 定制语言及键盘 // 已完成

    5. 定制用户名密码、主机名 // 已完成

    6. 定制预装的软件(ssh server) // 已完成

    7. 定制GRUB密码 // 已完成

    8. 修改grub菜单选项

  6. 镜像修改完成之后,通过xorriso再将文件打包成iso镜像。

user-data文件配置

制作镜像shell脚本(大概流程,实现与此不同)

参考资料

ubuntu nocloud-init文档

网络配置

curtin设置文件系统分区

grub密码设置文档

网卡名称兼容

5.2.2 扩容脚本详细设计

目的

镜像交付下的磁盘是已经分好区的,是确定的大小,而客户有不同的目的,需要的磁盘空间是不确定的。脚本的目的是自动调整文件系统的配置,跟踪物理磁盘的容量变化(只增不减)。

5.2.2.1 可选方案

方案一 单磁盘设计

思路: 监控特定的硬盘/dev/sda的物理容量是否发生变化, 变化后则调整数据分区大小

特点: 适用于虚拟化环境和带RAID卡的物理机环境,只能关机扩容。

要求: 以虚拟机为例, 编辑虚拟机的磁盘配置,调大磁盘大小即可。以物理机为例,插入一块新的硬盘,然后在BIOS里面调整RAID配置。

方案二 多磁盘设计

思路: 监控新增的磁盘,如果识别到多了一块没使用的硬盘,则将其容量增加到数据分区

特点:适用于虚拟化环境和不带RAID卡(或者RAID处于IT模式)的物理机环境,可以开机扩容

要求:以虚拟机为例,增加一个块任意大小的虚拟磁盘即可。以物理机为例,插入一个新的硬盘即可。

本节中,假定方案二更常见,并以此做进一步设计。

5.2.2.2 检测原理

考虑关机扩容,那么假设有一个脚本,它在开机后执行

基于单磁盘方案(方案一):

  1. 关机,修改硬盘大小

  2. 使用pvresize 调整pv大小

  3. 使用lvextend扩容lv到当前vg大小

  4. 使用resize2fs调整ext4分区大小

基于多磁盘方案(方案二):

遍历磁盘列表,如果发现有没有登记过的磁盘,则将其添加到vg中,然后再调整lv

手动操作过程:

  1. 添加一块磁盘

  2. 人肉判断哪个盘是新加的盘

  3. 用pvcreate将该盘添加到pv

  4. 用vgextend将pv添加到vg

  5. 用lvextend将lv扩容到当前vg的大小

  6. 用resize2fs将lv的容量应用到ext4分区大小

图标 描述已自动生成

5.2.2.3 开机运行实现

方案一:systemd

方案二:init.d

方案二在centos系统用的比较多,本文采用的首选方案为方案一

5.2.3 打包模块详细设计

5.2.3.1 ISO镜像打包方案

概述

在基ISO镜像的基础上,按照总控安装的流程,将二进制文件、配置文件、依赖的库拷贝到安装后的目录。基ISO镜像的制作使用了cubic,但cubic是一个图形化工具,不利于自动化,为此我们需要使用到命令行工具来达成目的。

相关工具

  • xorriso 用于解包和打包ISO

  • unsquashfs 用于展开casper压缩文件

  • mksquashfs 用于将文件系统压缩成casper

  • fakeroot 用于实现chroot,切换根目录,方便进行apt安装等操作

关键步骤实现

  • 文件拷贝

  • 系统初始化

ISO挂载

ISO解除挂载

切换根目录

5.2.3.2 OVA镜像打包方案

图标 描述已自动生成

如图,如果采用官方ISO镜像来制作OVA,那么制作出来的是基OVA镜像,它需要一次制作,不断更新。在无法实现制作石犀ISO的背景下,是比较合适的路。

无论前面的方案如何,最终制作OVA时,都需要一个VMDK文件(虚拟磁盘文件),这个文件的创建有两条路走,一条是使用vmware来操作一遍安装流程,一条是用命令创建一个空的vmdk文件,并通过某种方式挂载到文件系统中,然后通过某种方式来将系统安装到vmdk上。 前者实现简单,但是存在难以自动化的问题,后者实现不确定性比较大,但是是理论上可以自动化的方案。

综合多方考虑,取两种方案,分别为图中蓝色和绿色两种。绿色是比较理想的情况,它意味着ISO镜像的方案比较成熟,可以很好的支撑其他类型镜像的制作。蓝色是妥协的情况,这里着重说一下。

蓝色的制作步骤为:

  1. 获取官方ISO镜像

  2. 手工操作vmware,创建虚拟机,然后安装该镜像

  3. 安装完成后,不要重启,直接关机,避免进入系统

  4. 此时得到的vmdk可以认为是基OVA,下面步骤要实现自动化,否则全手操效率极低

  5. 将基OVA拷贝,挂载到其他系统

    1. ovftool将ova里的vmdk转化为可读写的vmdk

    2. guestmount挂载vmdk里的文件系统到某个目录

  6. 将一些定制化的内容放到该磁盘中(这一步可以前移到基镜像)

  7. 将总控安装到该磁盘中

    1. 文件校验

    2. 拷贝二进制、配置到/$挂载目录/opt/xxx(不含systemd)

    3. 拷贝systemd配置到/$挂载目录/lib/systemd/system/

    4. chroot + 依赖安装 + 添加自启动

    5. 初始化mysql,mongo

  8. 解除挂载,将vmdk和ovf封装成ova

相关工具

  • guestmount

  • ovftool

ovftool解包

ovftool打包

OVA挂载

OVA解除挂载

参考资料

How to Create Custom Debian BasedISO - DEV Community

https://github.com/covertsh/ubuntu-autoinstall-generator/blob/main/ubuntu-autoinstall-generator.sh

iso镜像文件制作

5.2.3.3 main.sh脚本设计

单纯的文件拷贝,可以由main.sh的各个函数完成,而有些需要在切换root后执行的命令,则作为单独的文件,在guest-install目录管理,将其拷贝到挂载路径后,在切换root后执行对应的脚本

图标 描述已自动生成

5.2.3.4 Makefile设计

调用main.sh 传递解压后的总控包,基镜像,以及目标镜像作为参数 。目标镜像就是不同的target

target依赖关系图

  • ubuntu-iso-20-official是官方镜像,使用Ubuntu 20.04.6

  • srhino-package是打包平台出的tar.gz包

红色部分是识别到的难以自动化的步骤,故而ubuntu-ova-m1这个分支当前不具有可行性,导致整个iso分支暂时失去了价值。目前计划仅实现绿色路径的target

图标 描述已自动生成

5.2.4 磁盘分区详细设计

5.2.4.1 初始分区大小规划

[TABLE]

5.2.4.2. LVM设计

将系统盘分为3个VG+1个普通分区,普通分区是/boot分区,使用ext3或ext4文件系统

3个VG分别和5.2.4.1中描述的分区大小一一对应,每个VG划分一个LV

VG(卷组) PV(物理卷) Size
vg_root /dev/sdaX 28G
vg_app /dev/sdaX 10G
vg_data /dev/sdaX /dev/sdbX … 60G+n
LV(逻辑卷) VG(卷组)
lv_root vg_root
lv_app vg_app
lv_data vg_data
5.2.4.3. 软链接

在当前版本的镜像里面,创建好以下的软连接,将占用空间比较大的,且非固定大小的目录都链接到数据分区,如:数据库文件、用户上传的文件。

软连接目前仅用于兼容原有设计,后续需要将各个程序的路径改为目的地所在的路径

软链接 目的地 说明
/opt/mongodb/data /data/mongodb/data mongodb数据存储目录
/opt/mongodb/log/ /data/mongodb/log mongo日志(有轮转)
/opt/etcd/data /data/etcd/data etcd数据
/opt/etcd/wal /data/etcd/wal etcd预写
/opt/nsq/data /data/nsq/data mq数据
/opt/redis/data /data/redis/data redis数据
/opt/sv/cache /data/sv/cache 可视化数据
/opt/mysql/data /data/mysql/data mysql数据
/opt/repo /data/repo 插件包/原始包
/opt/sc/data /data/sc/data 备份目录
/opt/data /data/installer/data 引擎的证书
/opt/installer/data
/opt/os/data /data/os/data 系统定制程序的数据
5.2.4.4. OEM分区文件

文件格式为INI格式

位于/oem/info,权限设计为rw——-

文件内容的管理目前为手动更新,其托管在git上,集成到基镜像中

注:关于SP版本,原设计是专注于系统内部程序补丁版本,现在既然把TUI和扩容集成进来,未来SP版本也会受到TUI和扩容脚本的版本影响。

5.2.5 预处理脚本详细设计

当前总控打出的包感觉格式不统一,在统一规范未实现之前,本项目通过一个脚本来修正一些不合理的文件、配置、布局。镜像制作程序会按照预处理后的格式进行编码,为了不出现维护问题,以后的打包脚本应该满足下面的规范,满足后通知镜像这边,镜像这边再移除相应的逻辑。

5.2.5.1 统一systemd文件

原则:

  • 在总控通过systemd托管的程序,其service文件都要放到systemd子目录下

  • service文件必须是LF换行符

当前实现:

  • api-gov/hack/*.service -> api-gov/systemd/

  • sv/hack/sv.service -> sv/systemd/sv.service

  • 将nsq/systemd/nsqlookupd.service的换行符改为LF

5.2.5.2 统一配置文件层级

原则:

  • 在总控运行的程序,其配置文件在config子目录下

  • 职责分明,依赖的处理应该由安装/升级阶段解决

当前实现:

  • 删除mysql/template目录

  • 移除mysqld.service的`ExecStartPre=/opt/mysql/bin/mysql-systemd-start pre`

  • 删除etcd/work目录

  • 修改etcd.service的`EnvironmentFile=/opt/etcd/config/etcd.conf`PI

  • 将redis/etc改为redis/confg

  • 修改redis.service的`ExecStart=/opt/redis/bin/redis-server /opt/redis/config/redis-stack.conf`

  • /etc/mysql/my.cnf从拷贝改为创建链接到/opt/mysql/config/my.cnf

暂时仅限于镜像:

有些东西暂时要求不了,仅放镜像这边,后面再看情况(2023-11-23)

  • 删除mongod.service的`ExecStartPre=ln -sf ……..`

  • 删除mongod.service的`Environment=”LD_LIBRARY_PATH=…`

5.2.5.3 统一data目录

TODO

5.3 功能实现

5.3.1 镜像制作相关实现

5.3.1.1 系统定制相关说明
  • root用户密码设置

cloud-init不支持直接使用在identity中设置的root用户,需要手动修改root用户的相关权限

首先解锁root用户后,设置相关密码

密码使用openssl passwd -6 -stdin <<< 123.com生成

  • 关闭swap文件

 

  • 关闭第一次开机sshinit信息

 

  • 禁用系统自动更新
5.3.1.2 grub定制相关说明
  • 设置重启后进入grub界面

通过设置grub菜单选择超时时间,重启默认进入grub,即使没有多个启动选项

  • 开启grub菜单编辑密码保护

 

  • 修改grub菜单显示样式
5.3.1.3 内核启动参数定制说明
  • 设置网卡默认名称

网卡名称通常分为两类(en卓和eth),这里全部默认为eth0

5.3.1.4 离线apt仓库制作
  1. 生成gpg密钥,用于离线apt仓库的签名。

  2. 制作离线仓库包

  3. 基础镜像添加离线源

  4. 使用echo输入密码

  5. 打包deb依赖到离线仓库

5.3.1.5 容器初始化数据库
  1. 利用docker实现

需要编写dockerFile 将文件映射到docker中,利用docker中的应用运行来初始化相关数据文件。优点: 不需要手动维护相关系统资源。缺点:打包需要引入docker组件,和一个dockerFile文件。

  1. 利用linux自带的命名空间实现

通过unshare来隔离主机中的各个命名空间实现资源隔离,为docker的实现原理,但是需要手动管理相关的隔离资源。优点:不需要引入第三方依赖,直接在squashfs文件中运行,不需要做数据文件拷贝。缺点:在使用上可能会出现一些问题需要手动解决。

  1. 使用systemd-nspawn实现

暂未调研

5.3.1.6 关闭cloud-init服务的ssh输出
5.3.1.7 系统启动信息输出服务

系统启动信息输出是操作系统开机时输出的系统相关的信息,主要为用户了解当前的开机情况,其中包含系统基础信息输出,以及石犀组件状态在开机过程中的输出。

  1. ubuntu的开机过程主要托管到了systemd服务中,我们包含的cpu、lvm、内存、网卡信息以及总控组件的相关状态输出

  2. 该服务不阻塞系统的正常启动,仅仅只是为了添加系统启动的一个输出画面,该服务在TUI服务启动之前完成,避免将console信息输出到TUI界面上造成混乱。

  • 实现

通过读取config/srhino_service获取当前总控所在机器的所有组件信息,然后进行服务状态输出。后续总控升级时添加了组件也需要将组件信息新增到这个配置文件。

5.3.1.8 添加服务状态菜单选项

将原来TUI第五个技术支持选项修改为服务状态选项,能够获取当前总控的各个组件的对应状态,在组件的右边如果是绿色则表示该组件是正常的,如果是红色则表示该组件是不正常的。

  • 实现

在TUI的菜单中显示组件状态,依然使用config/srhino_serivce文件中的配置信息。且考虑到组件可能是动态变化的,这里的TUI使用动态行高来展示组件状态。

5.3.1.9 系统硬件信息展示

在命令行选项里面,新增了一个hardware_info的命令,该命令能够展示当前系统的cpu,memory,disk的相关使用情况,该命令依赖于free, df命令。

  • 实现

获取系统硬件信息的时候,主要通过df和free命令获取相关内存信息和分区信息。

  1. 内存信息的使用为free -h中的used这一栏,目前没有展示剩余可用内存。

  2. 磁盘主要展示石犀自定义的三个分区相关使用情况(/ , /data, /opt)

  3. cpu的使用率为间隔一秒读取/proc/cpuinfo计算得到。目前仅仅只有当前cpu使用率。

5.3.2 iso自动化制作ova文件

5.3.2.1 虚拟机的相关配置

这个是srhino.vmx配置文件,通常用来描述一个虚拟机的配置,这里用于ovftool导出时,导出对应的虚拟机配置。

5.3.2.2 qemu启动参数

通过qemu挂载iso镜像之后启动镜像安装到指定vmdk磁盘

然后通过ovftool导出ova磁盘文件

5.3.2.3 镜像启动失败备用方案

6 自测

当前工程下自己需要自测的模块以及自测方法

6.1 自测模块

包含当前设计的模块与依赖的周边组件/模块

6.2 自测方式

进行测试验证的方法, 使用的工具, 请求的参数, 预期响应的结果

  • 扩容脚本自测方式

    • 增加一块SATA磁盘,10GB

    • 增加一块IDE/SCSI/NVME磁盘,10GB

    • 增加一块virtio磁盘,10GB

    • 误调系统盘大小测试

  • ISO镜像自测方式

    • 在vmware平台安装测试,需要提前准备ESXI6.5环境

    • 在HCI平台安装测试

  • OVA镜像自测方式

    • 参考部署文档,在不同平台测试

    • 需要测试引擎安装

  • 分区自测方式

    • 人肉判断,使用命令lsblk, df等
  • 单元测试

7 风险评估及对其它模块/系统影响

实现当前设计存在什么已知风险

  • 分区后,可能会存在遗漏的其他模块仍在使用系统分区(根分区),在清理策略不起效的情况下,可能会导致OS异常

  • 软连接可能有遗漏或者被应用程序误删导致有些大数据使用到了应用分区的空间

  • OVA和ISO镜像可能会比较大,估计大小为3~4GB,由于公司没有私有网盘,可能会对实施造成一些影响

7.1 已知的或可预知的风险

在这里加上已经知道的或可能会发生的风险,包括技术、业务等方面。最好针对每个风险,列出相应的应对措施。

7.1.1 商城的生产地址可能不可用

总控的配置项[shopCenter]下的两个地址,一个用于获取数据,一个用于上报数据。这里的风险是生产的总控不一定可用

7.1.2 CPU指令集检查

总控依赖AVX指令,需要检查CPU指令集。

ISO要在正式安装系统前,进行检查。

descript

OVA目前还不确定检查后在哪提示,而且没有AVX好像也装不了ESXI。

7.1.3 总控WEBUI修改IP长时间不能落地

当前TUI设计成配合WEBUI使用,但是WEBUI的需求都还没完全定下来,只知道1.4.0要做,在WEB界面修改IP实现以前,镜像也还有持久化修改IP的需求,目前的设想是通过一个脚本来代替WEBUI的操作。

不论是用于代替的脚本还是未来的WEB界面,在安装引擎之前都需要通过他们来设置IP,以便持久化和修改对应应用的配置,否则无法安装引擎。

脚本路径为/user/sbin/quickstart_network_set

7.2 与其它模块/系统可能的影响

在详细设计中描述了该模块与其它模块的依赖关系。在这里描述这些依赖关系可能带来的影响。包括本模块对其它模块可能造成的影响以及其它模块可能给本模块造成的影响两个方面。

7.2.1 对总控监控体系的影响

UI目录:

  • 系统管理-运行状态-硬盘

  • 系统管理-日志管理-系统日志配置

描述:

分区之后,原有磁盘空间使用率不能适应分区后的实际情况,也就是说显示的不准确,并且这块会被清理策略依赖,会产生进一步的影响。

原因:

分区之后,本质上是相当于将一个硬盘分为多个硬盘,但是UI显示上还是按照一个硬盘来显示。

解决方案:

方案1. 调整UI,将硬盘改为系统盘+数据盘双显。系统盘涵括根分区、应用分区和启动分区

方案2. 调整算法,显示数据盘的使用率

方案3. 调整算法,显示为(硬盘1使用+硬盘2使用+….+硬盘n使用)/(硬盘1+硬盘2+….+硬盘n)

方案4. 调整UI,每个盘(分区)分别显示

优缺点:

方案1. 优点:适配实现,能够减少对用户的误导,数据盘允许磁盘使用率低达1%;缺点:需要调整UI,清理策略UI文案也需要区分系统盘和数据盘。

方案2. 优点:实现简单,允许磁盘使用率低达1%;缺点:如果哪天需要显示硬盘的具体容量,这里显示的数据盘容量会引起用户的困惑(容量小于分区前的磁盘),另外,如果哪个应用不小心把数据写到数据盘,并且还写满了,界面上也无法反应出来。

方案3. 优点:相比其他方案相对中庸,没有明显的优点;缺点:磁盘使用率误导性强,无法反映用户最关心的指标,数据区满了也不会显示100%,数据区空了显示的可能是20%

放哪4. 优点:显示的最为精确,能够发现任意分区使用率过高,对排查问题比较友好;缺点:暴露系统细节、用户认知不便,不知道这些分区是什么

结论

  • 选择方案2:显示数据盘的使用率

  • 双机主从同步等未来场景不受分区影响

7.2.2 对文件目录规范的影响

原本各个应用的文件目录规范为:

  1. 每个应用一个文件夹,称为应用文件夹

  2. 应用文件夹下放置bin,config,data等多个目录,用于存储二进制、配置、数据等

这种方式对于数据分类存储不利,直接限制了分区的使用,调整后的目录规范为:

原有规范基本不变,将data目录调整到数据分区

调整前:${应用目录}/${应用名}/data

调整后:${数据目录}/${应用名}/data

  1. 用户上传的文件,在tmp目录暂存后,移动到合适的位置

调整前:/tmp/${tmpdir}/uploadxxx

调整后:/data/${tmpdir}/uploadxxx

另一个可选调整是,应用目录使用专有标识,不再使用opt(待评审确定)
调整前:${应用目录} == /opt

调整后:${应用目录} == /srhino

这样做的好处是,做清理类操作时,可以直接rm -rf /srhino/* 而不用小心翼翼的删除每个opt下的文件。但是清理这个需求理论上不应该出现在客户的场景里,特别是镜像交付的场景,唯一有用的场景可能是内部自测的时候,意义不太明确。

上面说的都是理论上的场景,实际考察发现原有目录规范并不是每个应用都能遵守的,并且出于过渡期的考量,不会马上切到新的规范

当前版本设计的是按照【5.2.4.3 软链接】描述,在制作镜像的时候,会将对应的软连接创建好。这样做有一些要求,主要是升级和重装的时候会有影响,在下面两节会有详细描述。

结论:

不需要/srhino,,需要一个规范文档

7.2.3 对升级的影响

导入过程:

  • 浏览器>>>>/tmp/${tmpdir}/xxx.tar.gz

  • /tmp/${tmpdir}/xxx.tar.gz >>> sc >>> 校验 >> 解压 >>> /opt/repo

升级过程:

  • 启动/opt/repo/sc/sc/bin/sc (简称sc_new),接管tcp listener socket

  • sc_new 拷贝/opt/repo/部分文件到/opt/对应的程序下,替换二进制和配置

  • sc_new 执行/opt/sc/hack/seeder,这里会涉及数据迁移等各种复杂处理

要求:

整体约束:

升级过程执行流比较不确定,需要确保过程中不会删除/opt下的目录,否则可能会导致软链接失效。

升级前约束:

升级前,检查/oem/info文件,如果不存在,则判定为软件部署

如果文件存在,则检查OS.os-release和OS.os-patch,当前版本仅判断os-release是否为srhino focal即可,以后如果有其他类型的系统,则需要在升级前匹配好。

系统升级包:

在操作系统也归我们管后,对于操作系统的安全更新也需要通过升级包的方式发布。系统更新主要是系统软件的更新,理论上来说应该是把对应的deb包打包,并且制作升级脚本即可。对于系统升级而言,需要更新/oem/info文件的SP版本

升级包和安装包分离:

目前升级包和安装包都是同一个包,经了解,该包用作升级用途时,是不需要安装脚本(install.sh)的,所以我认为应该将两个包分离开来,对外以后不发布安装包,仅发布镜像和升级包,升级包里面不包含安装脚本和卸载脚本。

结论:

分离

添加版本标识提升兼容性

7.2.4 对重装的影响

重装应该仅发生在公司内部,外部要么用镜像重新部署,要么用升级包升级。

卸载脚本删的比较彻底,软链接是一定会被删除的,所以要调整的是安装脚本,安装脚本需要

在启动各个进程之前,先把解压后的一些文件夹挪到/data目录(不存在则创建),然后创建好对应的软连接

7.2.5 mongodb的动态配置

7.2.5.1 mongodb使用的机器资源限制

CPU资源限制

  • 根据 CPU 核心数量(CPU_NUM)计算核心最大利用率:

    • CPU_NUM ≤ 1:每核最大利用率 20%

    • 1 < CPU_NUM < 4:每核最大利用率 40%

    • 4 ≤ CPU_NUM < 8:每核最大利用率 60%

    • 8 ≤ CPU_NUM < 10:每核最大利用率 80%

    • 10 ≤ CPU_NUM < 32:每核最大利用率 85%

    • CPU_NUM ≥ 32:每核最大利用率 90%

内存资源限制

  • MongoDB 限制为使用最多机器实际内存的 30%。
7.2.5.2 实现MongoDB 机器资源限制的方法
  • CPU使用率限制:利用 systemd 配置的 CPUQuota 参数来限制 CPU 总使用率。

  • 内存使用率限制:通过 systemd 配置中的 MemoryLimit(旧版)和 MemoryMax(新版)参数设置 MongoDB 内存使用上限。使用mongod的启动参数wiredTigerCacheSizeGB,来限制mongodb的缓冲区大小。

  • Mongodb缓冲区大小限制:使用 MongoDB 启动参数 wiredTigerCacheSizeGB 来限制缓冲区大小。

7.2.5.3 mongodb的service文件内容计算

目标:根据当前机器资源信息生成 mongod.service 文件。

内容包括:

  • 计算出CPU 和内存使用的限制配置。

  • 计算出MongoDB 启动参数,如 wiredTigerCacheSizeGB,以控制缓冲区大小。

具体实现:

  1. CPU使用率的计算:

    1. 使用 grep -c “processor” /proc/cpuinfo 命令得到当前CPU的核心数,并给 cpu_num 赋值

    2. 使用 nproc 命令得到当前CPU的核心数,并给 cpu_num 赋值

    3. 判断CPU的核心数所对应的核心使用率,并给quotient赋值

    4. 计算出CPUQuota的值:cpu_quota=$((${cpu_num} * $quotient))

  2. 内存限制计算:

    1. 根据/proc/meminfo文件中的MemTotal字段的值得到总内存,并计算出总内存30%的大小:$(($(awk ‘/MemTotal/ {print $2}’ /proc/meminfo) * 30 / 100)),并赋值给mongodb_limit_mem
  3. mongo缓冲区大小计算:

    1. 将mongodb_limit_mem由原来的KB转换成以GB为单位,并向上取整

    2. 具体实现:

      1. 1 GB = 1048576 KB

      2. 让KB的值加上1048575,让它达到下一个GB的阈值,或达到当前GB的最大值

      3. 再让KB的值除以1048576,利用计算自动向下取整的机制得到KB转换成GB然后向上取整的值

      4. mongodb_limit_mem_GB=$(( (mongodb_limit_mem + 1048575) / 1048576 ))

  4. 将计算好的值填入mongodb的模板service中并得到最终的service文件内容

mongo的service模板:

7.2.5.4 动态配置的实现

概要

  1. 在mongodb启动的时候设置一个前置执行脚本

  2. 前置脚本的作用使根据当前机器环境生成mongo的service内容,并与当前被配置到systemd的service内容比较,如果有差异,则更新systemd上的service内容,并调用systemd的配置重载操作,如果没有差异,则不作操作。

具体实现:

8 附件及参考资料

填写文档相关的附件或参考资料。

【企微文档】研发参与流程

研发参与流程

ubuntu server 20.04 安装流程

基于官方镜像制作基OVA过程

CPU指令集调研

LVM介绍及使用

软件保护需求文档

镜像交付培训资料

8.1 详细修改历史

2023-11-6,岑思华:

  • 增加5.2.3.3-5.2.3.5 三个小章节,描述打包项目的代码设计

  • 增加7.2.1-7.2.3 三个小章节,描述对其他模块产生的影响

  • 增加5.2.4.2一节,描述关于LVM的设计

2023-11-7, 岑思华:

  • 将保留分区显式的挂载为/oem,存储一些oem信息

  • 继续完善7.2部分的内容

  • 修改项目结构,去掉install.sh和init.sh,增加main.sh,补充main.sh的细节

2023-11-9,岑思华

  • 补充扩容脚本的目的

  • 补充附件及参考资料

  • 产品和技术部沟通后,确定磁盘百分比显示采用方案2(显示数据盘占用)

2023-11-14,岑思华

  • 补充mongodb配置文件的风险

  • 细化ova构建细节

2023-11-15,岑思华

  • 更新7.2.5的新方案与结论

2023-11-15,袁菘壑

  • 新增5.2.1.2章节

2023-11-18,沙汉路

  • 增加LVM相关参考资料

2023-11-20,岑思华

  • 更新5.2.3.2关于ova文件的处理方案:ovftool

2023-11-22,岑思华

  • 将数据分区从固定的60G改为使用剩余所有空间(60G+)

  • 增加5.2.5章节

2023-11-29 袁菘壑

  • 增加5.3功能实现章节 包含5.3.1镜像制作的相关实现

2023-11-30,岑思华

  • 更新5.2.4.4, 设定oem文件的管理位置和管理方式

2023-12-7,吴松达

  • 增加7.2.5,增加mongodb动态配置的实现

2023-12-7,袁菘壑

  • 增加5.3.1.5,增加容器初始化数据库实现

2023-12-8,岑思华

  • 更新基镜像的需求说明

  • 增加oem文件SP版本的含义备注

  • 增加7.1.3章节

2023-12-11,岑思华

  • 更新关于ISO镜像的UEFI补充说明,不支持UEFI

2023-12-12,袁菘壑

  • 更新关于grub密码兼容问题,在5.3.1.2章

  • 更新gpg禁用ui输入密码,在5.3.1.4章

  • 添加grub静默启动,在5.3.1.3章

  • 删除ssh初始化打印脚本,在5.3.1.6章

  • 修改7.1.3脚本路径

2023-12-14,岑思华

  • /user/sbin/quickstart_network_set.sh改为/user/sbin/quickstart_network_set

  • grub密码易混淆的小写L和数字1,改为大写L

2024-01-02,袁菘壑

  • 添加iso制作ova流程,在5.3.2章

2024-01-03,袁菘壑

  • 增加镜像启动不了后的备用方案

2024-01-15,袁菘壑

  • 新增5.3.1.7到5.3.1.9节

镜像交付相关资料.md
https://abrance.github.io/2024/03/07/mdstorage/domain/linux/镜像交付相关资料/
Author
xiaoy
Posted on
March 7, 2024
Licensed under