前段时间白嫖了两个 4T 的 SSD,而研究硬盘直通的时候发现只有把 SATA 控制器直通进虚拟机后才能获取到 S.M.A.R.T 。而又因为我的 PVE 系统盘在同一个 SATA 控制器,因此要用另外一个硬盘做系统盘才能 SATA控制器。并且最近发现有一台虚拟机无故没法启动了,现在的系统盘可能有丢文件的情况。
所以要加快 PVE 的升级进度了,就上星期买了两条致钛 512G 的组 RAID1 再加了 PCIE 转 NVME 卡。在我备份完 PVE 的时候看到了 No Proxmox VE repository is enabled, you do not get any updates!
的错误,而因为我是把 Proxmox 的源换到清华源的,而迫于强迫症想看看 PVE 的后端是什么原因会出现这个错误,就暂停了升级 PVE 的进度。并写了这遍文章,希望能帮助到大家。
从HTML深入
我是先从前端的 REST API 的响应去搜索错误提示的关键字,然而是没有的。我也尝试过直接用 dev tools 去搜索 js 的源码同样也是没有(后面的分析发现实际上是姿势不对)。
接着我就直接去找 Proxmox 的官方源码了,本来想偷懒直接看 GitHub 的镜像,然而事实是 GitHub 的镜像是不全的,如果有想研究 PVE 的源码的老实直接 clone 官方 git 仓库的代码下来吧。
因为 API 响应是没有响应的错误信息的,我觉得应该会在 js 文件里面,且有可能 js 在编译过后会有压缩所以会搜索不出错误提示的关键字。看了一篇官方的仓库见到一个 PMG-GUI 感觉是 PVE 的前端 GUI 的源码,搜索了一篇但依然找不到有相关的代码(后来才知道 PMG 是 Proxmox Mail Gateway 的简称)。
又一波操作在html标签元素里我看到错误提示的 class name 是 proxmox-invalid-row 然后 dev tool 里面搜索相关字符串在 proxmoxlib.js 文件里存在,且看到附近一些带有 repository 字符串的函数,有可能有关但是依然不确定哪些代码是直接控制这个错误提示的。
前端代码分析
我先登陆了 ssh 找有关的 web server 的进程,然而系统没有装 netstat 命令,且懒得去安装,直接 ps -e fu
发现了有两个进程有可能是 web server 的后端,分别是 pvedaemon worker 与 pveproxy worker ,而 pveproxy worker 的用户名是 www-data 。直接 systemctl cat pveproxy
,很好什么重要的消息都发现不到,只知道是一个 perl 脚本。但起码知道不是 rust 和 flutter 的 git 项目,不过除此之外我数了下还有 81 个没有分类语言的项目。接下来就是凭感觉选一个碰碰运气,我看到了一个名称为 PVE Management Server 的项目,觉得还不错 clone 下来看看。
加载文件夹到 vscode 直接看到了一个 www 的目录,目录里有 css、images、manager6、mobile、touch 目录,因为有前车之鉴 PMG-GUI ,打开 manager6 目录,查看了 Makefile 生成的文件是 pvemanagerlib.js 并不是我们要的 proxmoxlib.js 文件。转而看 www 目录的 index.html.tpl ,看到文件确实加载了 proxmoxlib.js 那么可能只是在其实仓库编译这个文件的。
而 Git 自带的搜索功能好似形同虚设,搜索不到东西出来。在81个项目里找那一个是生成 proxmoxlib.js 就和大海捞针一样。我直接 Google proxmoxlib.js 与 PVE 的关键字,很庆幸看到了 proxmox-widget-toolkit 关键字,clone 了一下,看到 Makefile 生成 proxmoxlib.js 文件。
我全文搜索 repository 字符串,在文件 APTRepositories.js 同样发现了和在 proxmoxlib.js 里的 proxmox-invalid-row ,代码结构也和 dev tool 里很相近。
一共就下面这几个定义,直觉告诉我就是这几个函数控制那个错误信息的。
除了 Proxmox.node.APTRepositories
,其他几个定义的代码都比较小,那就直接研究它。里面有 selectionChange、updateState、check_subscription、updateStandardRepos、reload 这几个比较主要的函数。然后看到第 701 行有有一个 listeners ,根据我的直觉是用来监听一些用户动作的,这个监听会调用 reload 函数,以及708行的 initComponent ,应该是初始化组件并且请求了一个后端API。
reload 函数执行了两个 for 循环,对我没用忽略。之后调用了 updateStandardRepos 函数,在这之间我看过 dev tool 有请求过一个 /api2/json/nodes/lab/apt/repositories
的 API 接口,里面响应的内容是和错误提示的内容是有关的,所以我知道 updateStandardRepos 是相关的。下面是 updateStandardRepos 的代码,可以看到这里判断有没有 PVE 的订阅源,然后再调用 updateState 。
上面说的这两个函数都是在请求完 repositories 的 API 之后把响应的 json 传入到函数里面去做处理,接下来就是找后端的代码了。
后端代码分析
在上面提到的 PVE Management Server 项目根目录能看到有 bin、PVE、services 目录,其中 services 就是 systemd 的 daemon 配置文件,bin 里面就有 pveproxy.service 调用的 pveproxy , PVE 里面就是 pveproxy 实际调用的后端 perl 代码了。
在 pveproxy 文件的最后一行能是这样的:
PVE::Service::pveproxy->run_cli_handler(prepare => $prepare);
由于没学过 perl ,一开始不知道是什么意思。我把 PVE 目录里的代码翻来翻来发现规律,和 python 是差不多的。PVE 就等于根目录的 PVE ,Service 就对应子目录的 Service,pveproxy 则对应 pveproxy.pm 文件。在 pveproxy.pm 文件里只看到了93行有一个 title => 'Proxmox VE API'
,只能大概知道是 API 服务器在这里初始化。
其他代码看来看去都没看出来一个所以,当尝试一层层追踪代码的时候发现根本没法追踪,比如文件头里引用了 PVE::APIServer
,我搜了整个文件夹没有找到是在哪里定义的这个,而且也没有对应的文件。之后只能不理,直接看其他代码纯靠猜来分析代码逻辑。
在 PVE 目录的很多文件的文件头都引用了 PVE::API2
,而且 PVE 目录下有一个很显眼的 API2 目录。全文搜这个文件夹的 repository 关键字,出来的结果在 APT.pm 文件里最多,而且和我在研究的错误提示相关性很高。打开读了一下代码直接发现和前端 /api2/json/nodes/lab/apt/repositories
这个接口的响应内容都能对得上,基本上可以确认十有八九是这个文件来处理的了。
__PACKAGE__->register_method({
name => 'repositories',
path => 'repositories',
method => 'GET',
proxyto => 'node',
description => "Get APT repository information.",
permissions => {
check => ['perm', '/nodes/{node}', [ 'Sys.Audit' ]],
},
...
...
...
'standard-repos' => {
type => "array",
description => "List of standard repositories and their configuration status",
items => {
type => "object",
properties => {
handle => {
type => "string",
description => "Handle to identify the repository.",
},
name => {
type => "string",
description => "Full name of the repository.",
},
status => {
type => "boolean",
optional => 1,
description => "Indicating enabled/disabled status, if the " .
"repository is configured.",
},
},
},
},
},
},
code => sub {
my ($param) = @_;
return PVE::RS::APT::Repositories::repositories();
}});
这是 486-682 行的代码,很清晰啊。对应上面前端的代码就是根据 handle 返回的值来判断是否存在某一个源的,然而到函数末尾 return 的是 PVE::RS::APT::Repositories::repositories()
。同样和上面说法一样,也是不知道在哪里调用的一个函数,也是搜这个也搜不到,可以肯定又是依赖其他安装包的代码的。
没办法继续使用 Google 大法,因为这样一个开源大型项目,开发过程肯定有很多条讨论,一搜后出来的第一个结果就是我们想要的。这个 mailing list 提到了一个 pve-rs 的仓库,也根本不用进去看它说什么了,直接到 Proxmox 官方仓库看有没有这个库就可以了。
点开后第一个看到的 commit 是 This repository was moved to proxmox-perl-rs ,很简单嘛继续找 proxmox-perl-rs 。直接 clone 下来,翻代码看。
在 pve-rs/src/apt/repositories.rs 里面的 repositories() 是这样的:
#[export]
pub fn repositories() -> Result<RepositoriesResult, Error> {
let (files, errors, digest) = proxmox_apt::repositories::repositories()?;
let digest = hex::encode(&digest);
let suite = proxmox_apt::repositories::get_current_release_codename()?;
let infos = proxmox_apt::repositories::check_repositories(&files, suite);
let standard_repos = proxmox_apt::repositories::standard_repositories(&files, "pve", suite);
Ok(RepositoriesResult {
files,
errors,
digest,
infos,
standard_repos,
})
}
standard_repos 哪的一行就是我们需要的,同样又再调用了另一个东西,这个仓库里没有这个文件。继续官方仓库找 clone 下来。
找到 proxmox-apt/src/repositories/mod.rs 文件,里面的 standard_repositories ,通过调用 standard.rs 的 APTRepositoryHandle 函数处理后再返回结果回去。
APTRepositoryHandle 摘录代码如下完整代码:
/// Get package type, possible URIs and the component associated with the handle.
///
/// The first URI is the preferred one.
pub fn info(self, product: &str) -> (APTRepositoryPackageType, Vec<String>, String) {
match self {
APTRepositoryHandle::Enterprise => (
APTRepositoryPackageType::Deb,
match product {
"pve" => vec![
"https://enterprise.proxmox.com/debian/pve".to_string(),
"https://enterprise.proxmox.com/debian".to_string(),
],
_ => vec![format!("https://enterprise.proxmox.com/debian/{}", product)],
},
format!("{}-enterprise", product),
),
APTRepositoryHandle::NoSubscription => (
APTRepositoryPackageType::Deb,
match product {
"pve" => vec![
"http://download.proxmox.com/debian/pve".to_string(),
"http://download.proxmox.com/debian".to_string(),
],
_ => vec![format!("http://download.proxmox.com/debian/{}", product)],
},
format!("{}-no-subscription", product),
),
APTRepositoryHandle::Test => (
APTRepositoryPackageType::Deb,
match product {
"pve" => vec![
"http://download.proxmox.com/debian/pve".to_string(),
"http://download.proxmox.com/debian".to_string(),
],
_ => vec![format!("http://download.proxmox.com/debian/{}", product)],
},
format!("{}test", product),
),
总结
最终这个错误提示确实是写死的,你除了使用官方源,没有其他简单的办法让这个错误提示消失。如果自己把 download.proxmox.com 劫持了也不是不行,但是最终价值不大。直接用清华源也是可以正常升级的,只是多了一个错误提示而已。
最后说一下分析这个代码的感受,虽然 PVE 很好用而且免费,但是对于野生开发者想要贡献代码的话,体验很是一般。从分析中也能感受到,同一个功能的东西居然会开分几个包(仓库)去完成。可能在 PVE 现有开发团队下是合理的,每个人各司其职互不打扰。