学习FreeBSD 从入门到跑路:使用 Qjail 管理 Jail

学习FreeBSD 从入门到跑路里面的一章:使用 Qjail 管理 Jail 。发现推荐使用了Qjail,于是略作了解,发现还是挺不错的。

学到这么几点:

1 ezjail 在 2015 年更新至 3.4.2 后未再进行关键更新,其 ports 更新依赖 portsnap,现已废弃。所以要用Qjail

2 Qjail 下载base.txz 要求文件版本与宿主机一致

3 可以使用国内镜像,如:

复制代码
# 从镜像服务器下载 FreeBSD 基础系统文件
# fetch https://mirrors.ustc.edu.cn/freebsd/release/amd64/15.0-RELEASE/base.txz

# 使用 qjail 安装基础系统到指定 jail
# qjail install base.txz

4 使用gitup更新源代码和源代码树。gitup可以自行安装。

复制代码
# 更新源代码树
# gitup src

# 更新 Ports 树
# gitup ports

11.3 使用 Qjail 管理 Jail

文档地址:11.3 使用 Qjail 管理 Jail | FreeBSD 中文社区

Qjail 是用于部署 Jail 环境的工具,源自 ezjail 3.1。

常见的 Jail 管理工具包括 ezjail、Qjail 和 iocage。

ezjail 在 2015 年更新至 3.4.2 后未再进行关键更新,其 ports 更新依赖 portsnap,现已废弃。

iocage 依赖 ZFS 文件系统,因此使用 UFS 文件系统的用户无法使用。

Qjail 不存在这些方面的限制。

ezjail 不支持 Jail 的 vnet 功能,而 iocage 和 Qjail 支持该功能。

ezjail 和 Qjail 使用 sh 编写,iocage 使用 Python 编写。

下文中部署的 Jail 在概念上结构如下图:

预留 jail 的 ip

/etc/rc.conf 文件中添加如下配置

复制代码
cloned_interfaces="lo1"  # 克隆出 lo1,尽量和宿主机网络配置分开。①
ifconfig_lo1_alias0="inet 192.168.1.0-9" # 此处示例选择 `192.168.1.0/24` 网段,旨在为 jail 创建一个与宿主机实际网络分离的独立网段,请根据您的实际网络环境调整 IP 地址范围

注意

① 如果要生成多个接口,也应在同一行中以空格分隔描述,而不是另外创建多行,例如 cloned_interfaces="lo1 lo2" 。分行书写时,只有第一行会生效。

运行以下命令重启所有网络接口:

复制代码
# service netif restart

lo1 将获得 10 个 IP 地址,其中 1-9 号 IP 将用于分配给各个 jail。

安装 Qjail 工具

  • 使用 pkg 安装:

    pkg install qjail

  • 或者使用 Ports 安装:

    cd /usr/ports/sysutils/qjail/

    make install clean

设置系统服务开机启动 qjail:

复制代码
# sysrc qjail_enable=YES

部署 Qjail 使用的目录结构

在使用 Qjail 之前,首先需要部署 Qjail 所使用的目录结构,可通过以下两种方式完成:

附录:从官方镜像站自动下载

复制代码
# qjail install

此时 Qjail 会从 FreeBSD 官网下载 base.txz 文件,示例输出如下:

复制代码
# qjail install
resolving server address: ftp.freebsd.org:80
requesting http://ftp.freebsd.org/pub/FreeBSD/releases/amd64/amd64/15.0-RELEASE/base.txz
remote size / mtime: 195363380 / 1652346155
...

附录:从境内镜像站下载

由于境内网络访问限制,也可以使用镜像站手动下载,以中国科学技术大学镜像为例(注意下载文件版本号,Qjail 要求文件版本与宿主机一致,此处示例为 FreeBSD amd64 15.0)。

复制代码
# 从镜像服务器下载 FreeBSD 基础系统文件
# fetch https://mirrors.ustc.edu.cn/freebsd/release/amd64/15.0-RELEASE/base.txz

# 使用 qjail 安装基础系统到指定 jail
# qjail install base.txz

部署好 Qjail 的目录结构后 /usr/jails 目录下会自动生成 sharedfs template archive flavors 四个目录:

  • sharedfs 包含一份只读的操作系统可执行文件库,通过 nullfs 挂载,在各个 jail 之间共享,以节省存储空间

  • template 包含操作系统的配置文件,将被复制到每个 jail 的基本文件系统中

  • archive 保存 jail archive 命令产生的存档文件

  • flavors 包含系统风格(flavors)和用户创建的自定义风格,其实就是自己定义的配置文件等

部署 jail

复制代码
# qjail create -n lo1 -4 192.168.1.1 jail1
  • -n 指定使用 lo1 作为网络接口

  • -4 指定 ipv4 地址

生成 jail1 后,/usr/jails/ 目录下会创建 jail1 目录(/usr/jails/jail1/)用于保存对应文件。

可以在前述的 flavors 目录中创建自定义配置文件,以便在部署新的 jail 时自动复制。例如,新建 /usr/jails/flavors/default/usr/local/etc/pkg/repos/FreeBSD.conf,则之后创建的 jail 会自动复制该文件,即

复制代码
# qjail create -n lo1 -4 192.168.1.2 jail2

建立 jail2 后,自动建立 /usr/jails/jail2/usr/local/etc/pkg/repos/FreeBSD.conf,即修改了之后所有 jail 的默认 pkg 镜像。但对应 jail1 并没有生成这个文件,因为生成 jail1 时,还没有在 flavors 目录中写入相应文件。

Qjail 基本用法

  • 列出 Qjail 管理的 jail

    qjail list

  • 启用 jail

    qjail start # 启动所有 jail

    qjail start jail1 # 启动 jail1

  • 停止 jail

    qjail stop # 停止所有 jail

    qjail stop jail1 # 停止 jail1

  • 重启 jail

    qjail restart # 重启所有 jail

    qjail restart jail1 # 重启 jail1

  • 进入 jail 控制台

    qjail console jail1 # 进入 jail1 控制台

进入 jail 控制台后,将以 jail 中的 root 账户身份操作(无需输入密码)。由于 jail 可能开启对外服务,为安全起见,建议设置 root 账户密码。

  • 备份 jail

    qjail archive -A # 备份所有 jail

    qjail archive jail1 # 备份 jail1

  • 从备份中恢复 jail

    qjail restore jail1 # 从备份中恢复 jail1

  • 删除 jail

    qjail delete jail1 # 删除 jail1

    qjail delete -A # 删除所有 jail

更新 jail

下面更新 jail 的部分不针对单个 jail,而是针对每个 jail,因为这些文件利用 nullfs 共享一份。

更新 jail 中的基本系统

即上面提到的 sharedfs 中的文件

复制代码
# qjail update -b

更新 ports

这里有 -p(小写)、-P(大写)两个选项,-p(小写)使用 portsnap 更新 jail 的 ports tree。-P(大写)使用宿主机的 ports 更新 jail 的 ports。若主机已有 ports,则建议使用 -P(大写),避免重复下载 ports。

复制

复制代码
# qjail update -P  # 这里注意要大写

更新系统源代码

复制代码
# qjail update -S # 大写

更新过程

请先自行安装配置 gitup

开始更新:

复制代码
# 获取并安装 FreeBSD 系统更新
# freebsd-update fetch install

# 更新源代码树
# gitup src

# 更新 Ports 树
# gitup ports

# 停止所有 qjail jail
# qjail stop

# 更新 qjail 基础系统
# qjail update -b

# 更新 qjail 源代码
# qjail update -S

# 更新 qjail Ports
# qjail update -P

# 启动所有 qjail jail
# qjail start

jail 设置

Qjail 可以用 qjail config 命令对每个 jail 另作设置,运行 qjail config 前须先停用指定的 jail。

qjail config 命令选项较多,这里列出几个常用的,更多的请参考手册页

qjail -- Utility for deployment of jail environments

-h

复制代码
# qjail config -h jail1

快速开启 jail1 的 ssh 服务,新建一个 wheel 组用户,用户名和密码同 jail 名,首次用这个用户登录要求修改密码。也可以在登录 jail 控制台后,自行配置 sshd 服务。

-m -M

复制代码
# qjail config -m jail1

设置 jail1 需手动启动(manual 状态),qjail_enable="YES" 写入 /etc/rc.conf 后在系统启动时会自动启动各个 jail,设为手动启动后则不会在系统启动时自动启动相应的 jail,须用 qjail start jailname 启动。

对应小写的 -m 选项,有大写的 -M 选项,作用为关闭手动启动状态,即清除 manual 状态,可以在系统启动时自动启用 jail。Qjail 中有大量类似的选项,小写字母的选项启用某个功能,大写字母的选项关闭对应功能。如果下文中同时出现小写和大写的选项就不再过多作出说明。

-r -R

复制代码
# qjail config -r jail1

将 jail1 设为不允许启动(norun 状态),相当于禁用该 jail。

-y -Y

复制代码
# qjail config -y jail1

启用该 jail 的 SysV IPC,在 jail 中安装 PostgreSQL 时,需要打开这个选项,PostgreSQL 运行基于这个功能。

网络设定

注意

有的教程里会教你用 qjail config -k jailname 打开 raw_sockets 功能来打开外网访问的能力,其实这是个误区。raw_sockets 只是像 ping 一类的工具需要使用而已,并不是说网络访问一定要打开 raw_sockets。而且在 jail 中打开 raw_sockets 本身有安全风险,这是 jail 环境默认的一种安全设计。所以除非你一定要在 jail 中用 ping 之类的工具,否则无论是用什么方式构建的 jail 都不建议打开 raw_sockets 功能。

此时的 jail 还不能连接网络,因为 jail 绑定在 lo1 网络接口上,lo1 并不能直接访问外网,接下来通过 pf 设定网络,其中 em0 为外网接口

  • /etc/pf.conf 中写入

    nat pass on em0 inet from lo1 to any -> em0 # 使 jail 可以访问网络,从 lo1 接口发出的连接通过 nat 转发到 em0
    rdr pass on em0 inet proto tcp from any to em0 port 22 -> 192.168.1.1 port 22 # 使宿主机外可以访问指定 jail,端口重定向,将连接到 em0 上 22 端口上的 tcp 连接重定向到 192.168.1.1 地址(即 jail1)的 22 端口上

  • 启动防火墙

    启用防火墙服务 pf 开机自启动

    service pf enable

    启动防火墙服务 pf

    service pf start

此时,绑定在 lo1 上的 jail 可以访问宿主机外网络,宿主机外网络可以通过宿主机 22 号端口连接 jail1 的 22 号端口。

示例:部署 PostgreSQL jail

假设已经如上文所述预留 jail ip,并成功运行 qjail install 命令。

这里以 PostgreSQL 15 为例,其它版本也适用。

宿主机中操作

在 jail 中创建并启用 PostgreSQL:

复制代码
# 创建名为 postgres 的 jail,绑定到 lo1 接口,IPv4 地址为 192.168.1.3
# qjail create -n lo1 -4 192.168.1.3 postgres

# 配置 postgres jail,启用 SysV IPC
# qjail config -y postgres

# 启动 postgres jail
# qjail start postgres

编辑 /etc/pf.conf 文件:

复制代码
nat pass on em0 inet from lo1 to any ->em0  # 上文已作说明
rdr pass on em0 inet proto tcp from any to em0 port 5432 -> 192.168.1.3 port 5432 # 不建议写下此句,作用为使宿主机外可以访问 jail 中的 postgresql,此处应考虑安全和实际需要开启端口转发,不建议直接向外提供 postgresql 连接

启动防火墙服务 pf:

复制代码
# service pf start

进入名为 postgres 的 jail 的控制台:

复制代码
# qjail console postgres

jail 控制台中的操作

下面命令皆在 jail 控制台下运行,pkg 安装与否使用镜像请自行决定,若使用镜像可以在 jail 控制台中如同宿主机般进行设置,请参考相关文章。

  • 使用 pkg 安装:

    pkg install postgresql15-server

  • 或者使用 Ports 安装

    cd /usr/ports/databases/postgresql15-server/

    make install clean


配置 PostgreSQL:

复制代码
# service postgresql enable # 设置 PostgreSQL 服务开机自启动
# mkdir -p -m 0700 /var/db/postgres/data15  # 注意版本号
# chown postgres:postgres /var/db/postgres/data15  # 这个目录应属于 postgres 用户
# su postgres   # 这里切换到 postgres 用户,注意下面提示符的变化
$ initdb -A scram-sha-256 -E UTF8 -W -D /var/db/postgres/data15
$ exit   #  回到 jail root 用户,注意提示符变化
# service postgresql start # 立刻启动 PostgreSQL 服务

此处使用 initdb 而非安装时提示的 /usr/local/etc/rc.d/postgresql initdb,目的是避免在设置数据库密码时反复修改 pg_hba.conf 文件。下面对各选项进行简要说明:

  • -A 为本地用户指定在 pg_hba.conf 中使用的默认认证方法

  • -E 选择模板数据库的编码。

  • -W 让 initdb 提示要求为数据库超级用户给予一个口令

  • -D 指定数据库集簇应该存放的目录

至此 PostgreSQL 服务已经可以运行。

如果在上述过程中未使用 qjail config -y postgres 命令开启 SysV IPC,可能会出现如下错误:

初始化数据库集簇时的错误

启动 PostgreSQL 时的错误


此时在宿主机控制台下执行 qjail config -y postgres 即可修正错误,具体如下:

复制

复制代码
# 停止 postgres jail
# qjail stop postgres

# 配置 postgres jail,启用 SysV IPC
# qjail config -y postgres

# 启动 postgres jail
# qjail start postgres

再次进入 jail 的控制台就可以正常初始化数据库集簇和运行 PostgreSQL 服务了。

上一页11.2 更新 Jail下一页

实践

预留 jail 的 ip

/etc/rc.conf 文件中添加如下配置

复制代码
cloned_interfaces="qjail1"  # 克隆出 lo1,尽量和宿主机网络配置分开。①
ifconfig_qjail1_alias0="inet 192.168.100.0-255" # 此处示例选择 `192.168.1.0/24` 网段,旨在为 jail 创建一个与宿主机实际网络分离的独立网段,请根据您的实际网络环境调整 IP 地址范围

手册上写的不对,应该这样

复制代码
# add qjail
cloned_interfaces="igb0"  # 克隆出 lo1,尽量和宿主机网络配置分开。①
ifconfig_igb0_alias0="inet 192.168.100.1 netmask 255.255.255.0" # 此处示例选择 `192.168.100.0/24` 网段
#defaultrouter="192.168.1.1"

重启网络接口

复制代码
# service netif restart

对我的系统来说,重启网络之后,路由丢了,我又重新加上的

复制代码
sudo route add default 192.168.1.1

安装qjail

复制代码
sudo pkg install qjail

安装好后提示

复制代码
Message from qjail-5.5_1:

--
Use the qjail utility to deploy small or large numbers of jails quickly.

First issue "rehash" command to enable the qjail command (if using csh).
Then issue
"man qjail-intro" To read the qjail introduction.
"man qjail"       For qjail usage details.
"man qjail-drive-traffic For example of driving public traffic to jails.
"man qjail-vnet-howto"   For example of creating vnet jails.
"man qjail-ipv6-testing" For example of testing jails with ipv6 addresses.

设置开机启动

复制代码
sudo sysrc qjail_enable=YES

部署目录结构

从官方源直接部署

复制代码
qjail install

如果速度慢,可以从镜像站点下载base.txz然后部署

复制代码
qjail install base.txz

会自动部署到/usr/jails目录。

部署好 Qjail 的目录结构后 /usr/jails 目录下会自动生成 sharedfs template archive flavors 四个目录:

  • sharedfs 包含一份只读的操作系统可执行文件库,通过 nullfs 挂载,在各个 jail 之间共享,以节省存储空间

  • template 包含操作系统的配置文件,将被复制到每个 jail 的基本文件系统中

  • archive 保存 jail archive 命令产生的存档文件

  • flavors 包含系统风格(flavors)和用户创建的自定义风格,其实就是自己定义的配置文件等

部署 jail

复制代码
# qjail create -n lo1 -4 192.168.1.1 jail1
  • -n 指定使用 lo1 作为网络接口

  • -4 指定 ipv4 地址

我是用的这句

复制代码
qjail create   -4 192.168.100.5 jail5

秒创建完成:Successfully created jail5

Qjail 基本用法

  • 列出 Qjail 管理的 jail

复制

复制代码
# qjail list
  • 启用 jail

复制

复制代码
# qjail start # 启动所有 jail
# qjail start jail1 # 启动 jail1
  • 停止 jail

复制

复制代码
# qjail stop # 停止所有 jail
# qjail stop jail1 # 停止 jail1
  • 重启 jail

复制

复制代码
# qjail restart # 重启所有 jail
# qjail restart jail1 # 重启 jail1
  • 进入 jail 控制台

复制

复制代码
# qjail console jail1  # 进入 jail1 控制台

进入 jail 控制台后,将以 jail 中的 root 账户身份操作(无需输入密码)。由于 jail 可能开启对外服务,为安全起见,建议设置 root 账户密码。

  • 备份 jail

复制

复制代码
# qjail archive -A  # 备份所有 jail
# qjail archive jail1  # 备份 jail1
  • 从备份中恢复 jail

复制

复制代码
# qjail restore jail1  # 从备份中恢复 jail1
  • 删除 jail

复制

复制代码
# qjail delete jail1  # 删除 jail1
# qjail delete -A     # 删除所有 jail

实践,创建,列表,启动

复制代码
skywalk@fb5:/usr/jails $ sudo qcreate   -4 192.168.100.5 jail5
Password:
Successfully created  jail5
skywalk@fb5:/usr/jails $ sudo qlist


STATUS JID  NIC    IP              Jailname
------ ---- ------ --------------- --------------------------------------------
DS     N/A  igb0   192.168.100.5   jail5


skywalk@fb5:/usr/jails $ sudo qjail start
Jail successfully started  jail5
skywalk@fb5:/usr/jails $ sudo qjail list


STATUS JID  NIC    IP              Jailname
------ ---- ------ --------------- --------------------------------------------
DR     25   igb0   192.168.100.5   jail5


skywalk@fb5:/usr/jails $ sudo qjail console
Syntax: qjail console [-z zone] [-u userid] [-c cmd] jailname
        -z [Apply to this zone] = :zone name:
        -c [execute this command in jail] = :command to execute:
        -u [user to login as] = :username:
        [Jailname of running jail to login to]
skywalk@fb5:/usr/jails $ sudo qjail console jail5
FreeBSD 14.3-RELEASE (GENERIC) releng/14.3-n271432-8c9ce319fef7

Welcome to FreeBSD!

Release Notes, Errata: https://www.FreeBSD.org/releases/
Security Advisories:   https://www.FreeBSD.org/security/
FreeBSD Handbook:      https://www.FreeBSD.org/handbook/
FreeBSD FAQ:           https://www.FreeBSD.org/faq/
Questions List:        https://www.FreeBSD.org/lists/questions/
FreeBSD Forums:        https://forums.FreeBSD.org/

Documents installed with the system are in the /usr/local/share/doc/freebsd/
directory, or can be installed later with:  pkg install en-freebsd-doc
For other languages, replace "en" with a language code like de or fr.

Show the version of FreeBSD installed:  freebsd-version ; uname -a
Please include that output and any error messages when posting questions.
Introduction to manual pages:  man man
FreeBSD directory layout:      man hier

To change this login announcement, see motd(5).
root@jail5:~ # uname -a
FreeBSD jail5 14.3-RELEASE FreeBSD 14.3-RELEASE releng/14.3-n271432-8c9ce319fef7 GENERIC amd64

更新

更新 jail 中的基本系统

即上面提到的 sharedfs 中的文件,注意,更新的时候需要停止jail

复制代码
# qjail update -b

更新 ports

这里有 -p(小写)、-P(大写)两个选项,-p(小写)使用 portsnap 更新 jail 的 ports tree。-P(大写)使用宿主机的 ports 更新 jail 的 ports。若主机已有 ports,则建议使用 -P(大写),避免重复下载 ports。

需要先自己更新宿主机的ports系统。

复制代码
# qjail update -P  # 这里注意要大写

更新系统源代码

复制

复制代码
# qjail update -S # 大写

一体化命令,先更新宿主机:

复制代码
sudo freebsd-update fetch install
sudo pkg install gitup

sudo gitup release
sudo gitup ports

再停掉jail并更新

复制代码
# 停止所有 qjail jail
# qjail stop

# 更新 qjail 基础系统
# qjail update -b

# 更新 qjail 源代码
# qjail update -S

# 更新 qjail Ports
# qjail update -P

# 启动所有 qjail jail
# qjail start

但是我测试的时候,gitup ports没成功过,报错:

复制代码
sudo gitup ports
# Scanning local repository...
# Host: git.freebsd.org
# Port: 443
# Repository Path: /ports.git
# Target Directory: /usr/ports
# Want: c71c571a6d22f2792581f9be7838c32d73c47409
# Branch: main
gitup: fetch_pack: malformed pack data:

: Inappropriate file type or format

jial设置

Qjail 中有大量的选项,小写字母的选项启用某个功能,大写字母的选项关闭对应功能。

快速启动ssh

复制代码
qjail config -h jail1
快速开启 jail1 的 ssh 服务,新建一个 wheel 组用户,用户名和密码同 jail 名,首次用这个用户登录要求修改密码。也可以在登录 jail 控制台后,自行配置 sshd 服务。

sudo qjail config -h jail5

我试了一下,没搞定,账户登录没搞定。

复制代码
 ssh jail5@192.168.100.5

不过我的jail5 启动后,就自动开了sshd服务了。

启用该 jail 的 SysV IPC

复制代码
# qjail config -y jail1

启用该 jail 的 SysV IPC,在 jail 中安装 PostgreSQL 时,需要打开这个选项,PostgreSQL 运行基于这个功能。

复制代码
sudo qjail config -y jail5

jail中安装数据库例子

jail 控制台中的操作

下面命令皆在 jail 控制台下运行,pkg 安装与否使用镜像请自行决定,若使用镜像可以在 jail 控制台中如同宿主机般进行设置,请参考相关文章。

  • 使用 pkg 安装:

复制

复制代码
# pkg install postgresql15-server
  • 或者使用 Ports 安装

复制

复制代码
# cd /usr/ports/databases/postgresql15-server/ 
# make install clean

配置 PostgreSQL:

复制

复制代码
# service postgresql enable # 设置 PostgreSQL 服务开机自启动
# mkdir -p -m 0700 /var/db/postgres/data15  # 注意版本号
# chown postgres:postgres /var/db/postgres/data15  # 这个目录应属于 postgres 用户
# su postgres   # 这里切换到 postgres 用户,注意下面提示符的变化
$ initdb -A scram-sha-256 -E UTF8 -W -D /var/db/postgres/data15
$ exit   #  回到 jail root 用户,注意提示符变化
# service postgresql start # 立刻启动 PostgreSQL 服务

此处使用 initdb 而非安装时提示的 /usr/local/etc/rc.d/postgresql initdb,目的是避免在设置数据库密码时反复修改 pg_hba.conf 文件。下面对各选项进行简要说明:

  • -A 为本地用户指定在 pg_hba.conf 中使用的默认认证方法

  • -E 选择模板数据库的编码。

  • -W 让 initdb 提示要求为数据库超级用户给予一个口令

  • -D 指定数据库集簇应该存放的目录

至此 PostgreSQL 服务已经可以运行。

如果在上述过程中未使用 qjail config -y postgres 命令开启 SysV IPC,可能会出现如下错误:

初始化数据库集簇时的错误

启动 PostgreSQL 时的错误


此时在宿主机控制台下执行 qjail config -y postgres 即可修正错误,具体如下:

复制

复制代码
# 停止 postgres jail
# qjail stop postgres

# 配置 postgres jail,启用 SysV IPC
# qjail config -y postgres

# 启动 postgres jail
# qjail start postgres

再次进入 jail 的控制台就可以正常初始化数据库集簇和运行 PostgreSQL 服务了。

总结

发现自己对jail和网络的理解和认识还是有些欠缺,网络配置那块应该还是可以继续改进的,原来文档里面的配置应该是对的,抽空再去做实验。

做完这个实验后,发现原来的jial虚拟机192.168.1.12ping不通了,也无法ssh过来了....

个人猜测是配置qjail的网络时,把1.12这台机器的弄不通了....试试怎么改回去。

相关推荐
a35354138211 小时前
设计模式-原型模式
开发语言·c++
Connie145111 小时前
K8s修改Kubelet过程(命令版本)
容器·kubernetes·kubelet
Tinachen8811 小时前
YonBIP旗舰版本地开发环境搭建教程
java·开发语言·oracle·eclipse·前端框架
草莓熊Lotso11 小时前
脉脉独家【AI创作者xAMA】| 多维价值与深远影响
运维·服务器·数据库·人工智能·脉脉
liulilittle11 小时前
libxdp: No bpffs found at /sys/fs/bpf
linux·运维·服务器·开发语言·c++
hqwest11 小时前
码上通QT实战07--主窗体消息栏设计
开发语言·qt·qt事件·主窗体·stackedwidget
hqwest11 小时前
码上通QT实战06--导航按钮事件
开发语言·qt·mousepressevent·qfont·qpainter·qlineargradient·setbrush
shughui11 小时前
实现Python多版本共存
开发语言·python·pip
dhdjjsjs11 小时前
Day58 PythonStudy
开发语言·python·机器学习
你真的可爱呀11 小时前
自定义颜色选择功能
开发语言·前端·javascript