All-in-One(一):PVE

发布时间 2023-09-23 15:23:49作者: 一克猫

简介

  Proxmox Virtual Environment(简称:Proxmox VEPVE),是一个开源的服务器虚拟化环境 Linux 发行版。Proxmox VE 基于 Debian,使用基于 Ubuntu 的定制内核,包含安装程序、网页控制台和命令行工具,并且向第三方工具提供了 REST API,在Affero通用公共许可证第三版下发行。Proxmox VE 支持两类虚拟化技术:基于容器的 LXC(自4.0版开始,3.4版及以前使用 OpenVZ 技术)和硬件抽象层全虚拟化的 KVM 。
  Proxmox 名字本身没有意义。

硬件

  本来想成为新晋的“垃圾佬”,但和闲鱼淘宝二手店扯皮又烦又浪费时间,除了CPU和主板没新的在淘宝闲鱼上买,还有固态硬盘是用剩下的,其他都在京东旗舰店上买了,各位不要学我,折腾 AIO 的话应该遵循这个原则:用剩下的>买二手的>买全新的。
  以下搭配是随便搭的,看得顺眼买的,应该不具有什么性价比,内存买单条是为了之后有需要时组 64G 双通道,硬盘买一个是暂时没有组 RAID 的需求,先折腾看看。
  需要注意的是,不是所有硬件兼容性都这么好,适合 AIO 且少折腾的基本上不会是最新的硬件,具体的得自己查查,比如Intel 12代以上的 CPU 好像就需要一些额外配置才能正常使用 PVE 或 硬件直通。

  • CPU:Intel i910900T X1
  • 散热器:利民 AXP90-X53 X1
  • 机箱:乔思伯N2 X1
  • 电源:全汉 MS450 SFX X1
  • 机械硬盘:西部数据 Ultrastar DC HC310 SATA 6TB X1
  • 内存条:金士顿FURY 32GB X1
  • 独立网卡:乐扩 8125B芯 4口 X1
  • 主板:ROG Z490I X1
  • 固态硬盘:三星970Plus EVO X1

安装

  安装和平常装 Windows 是一样的,用 Ventoy 安装 ISO ,简单又方便,这里略过。
  需要注意几点:

  1. 把 PVE 安装在非存储盘上,我用的是固态硬盘。
  2. 网卡的选择,我选的是板载网卡,据说 I225-V 的网卡普遍都断流,所以我选择这个用于 PVE 的管理口。
  3. 在设置 IP 时,保证 PVE、电脑、路由器在同一网段,不然进不了 PVE 后台也上不了网。
  4. 安装前在 BIOS 上开启 VT-x 和 VT-d,即 CPU虚拟化。

配置

换源

  安装完后进入 PVE 的 Shell 换源,我这里用的时中科大的源

## 进入系统软件源配置文件目录
cd /etc/apt

## 将默认软件源配置文件进行备份
cp sources.list sources.list.bak

## 替换系统软件仓库
sed -i 's|^deb http://ftp.debian.org|deb https://mirrors.ustc.edu.cn|g' /etc/apt/sources.list
sed -i 's|^deb http://security.debian.org|deb https://mirrors.ustc.edu.cn/debian-security|g' /etc/apt/sources.list

## 输出系统源配置文件,检查是否正确
cat /etc/apt/sources.list

## 进入订阅源目录
cd /etc/apt/sources.list.d

## 删除该目录下的所有配置
rm -rvf *.list

## 创建 PVE 免费源
source /etc/os-release
echo "deb https://mirrors.ustc.edu.cn/proxmox/debian/pve $VERSION_CODENAME pve-no-subscription" > /etc/apt/sources.list.d/pve-no-subscription.list

## 检查PVE免费源
cat /etc/apt/sources.list.d/pve-no-subscription.list

## 替换 CT Templates 源
cp /usr/share/perl5/PVE/APLInfo.pm /usr/share/perl5/PVE/APLInfo.pm.bak
sed -i 's|http://download.proxmox.com|https://mirrors.ustc.edu.cn/proxmox|g' /usr/share/perl5/PVE/APLInfo.pm

## 清理软件包
apt clean && apt autoclean && apt autoremove --purge

## 同步软件源
apt update

## 更新系统
apt dist-upgrade

  可选的完整源:

## Debian Bullseye 完整源 (USTC)

deb https://mirrors.ustc.edu.cn/debian/ bullseye main contrib non-free
# deb-src https://mirrors.ustc.edu.cn/debian/ bullseye main contrib non-free

deb https://mirrors.ustc.edu.cn/debian/ bullseye-updates main contrib non-free
# deb-src https://mirrors.ustc.edu.cn/debian/ bullseye-updates main contrib non-free

deb https://mirrors.ustc.edu.cn/debian/ bullseye-backports main contrib non-free
# deb-src https://mirrors.ustc.edu.cn/debian/ bullseye-backports main contrib non-free

deb https://mirrors.ustc.edu.cn/debian-security/ bullseye-security main contrib non-free
# deb-src https://mirrors.ustc.edu.cn/debian-security/ bullseye-security main contrib non-free

脚本

  去除企业订阅弹窗、显示CPU和硬盘信息脚本:

#!/usr/bin/env bash

# version: 2023.9.5
#添加硬盘信息的控制变量,如果你想不显示硬盘信息就设置为false
#NVME硬盘
sNVMEInfo=true
#固态和机械硬盘
sODisksInfo=true
#debug,显示修改后的内容,用于调试
dmode=false

#脚本路径
sdir=$(cd "$(dirname "${BASH_SOURCE[0]}")"; pwd)
cd "$sdir"

sname=$(basename "${BASH_SOURCE[0]}")
sap=$sdir/$sname
echo 脚本路径:"$sap"

#需要修改的文件
np=/usr/share/perl5/PVE/API2/Nodes.pm
pvejs=/usr/share/pve-manager/js/pvemanagerlib.js
plibjs=/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js

if ! command -v sensors > /dev/null; then
	echo 你需要先安装 lm-sensors 和 linux-cpupower,脚本尝试给你自动安装
	if apt update ; apt install -y lm-sensors; then 
		echo lm-sensors 安装成功
		
		echo 尝试继续安装linux-cpupower获取功耗信息
		if apt install -y linux-cpupower;then
			echo linux-cpupower安装成功
		else
			echo -e "linux-cpupower安装失败,可能无法正常获取功耗信息,你可以使用\033[34mapt update ; apt install linux-cpupower && modprobe msr && echo msr > /etc/modules-load.d/turbostat-msr.conf && chmod +s /usr/sbin/turbostat && echo 成功!\033[0m 手动安装"
		fi
	else
		echo 脚本自动安装所需依赖失败
		echo -e "请使用蓝色命令:\033[34mapt update ; apt install -y lm-sensors linux-cpupower && chmod +s /usr/sbin/turbostat && echo 成功! \033[0m 手动安装后重新运行本脚本"
		echo 脚本退出
		exit 1
	fi
fi


#获取版本号
pvever=$(pveversion | awk -F"/" '{print $2}')
echo "你的PVE版本号:$pvever"

restore() {
	[ -e $np.$pvever.bak ]     && mv $np.$pvever.bak $np
	[ -e $pvejs.$pvever.bak ]  && mv $pvejs.$pvever.bak $pvejs
	[ -e $plibjs.$pvever.bak ] && mv $plibjs.$pvever.bak $plibjs
}

fail() {
	echo "修改失败,可能不兼容你的pve版本:$pvever,开始还原"
	restore
	echo 还原完成
	exit 1
}

#还原修改
case $1 in 
	restore)
		restore
		echo 已还原修改
		
		if [ "$2" != 'remod' ];then 
			echo -e "请刷新浏览器缓存:\033[31mShift+F5\033[0m"
			systemctl restart pveproxy
		else 
			echo -----
		fi
		
		exit 0
	;;
	remod)
		echo 强制重新修改
		echo -----------
		"$sap" restore remod > /dev/null 
		"$sap"
		exit 0
	;;
esac

#检测是否已经修改过
[ $(grep 'modbyshowtempfreq' $np $pvejs $plibjs | wc -l) -eq 3 ]  && {
	echo -e "
已经修改过,请勿重复修改
如果没有生效,或者页面一直转圈圈
请使用 \033[31mShift+F5\033[0m 刷新浏览器缓存
如果一直异常,请执行:\033[31m\"$sap\" restore\033[0m 命令,可以还原修改
如果想强制重新修改,请执行:\033[31m\"$sap\" remod\033[0m 命令,可以还原修改
"
	exit 1
}


contentfornp=/tmp/.contentfornp.tmp

[ -e /usr/sbin/turbostat ] && {
	modprobe msr
	chmod +s /usr/sbin/turbostat
}
echo msr > /etc/modules-load.d/turbostat-msr.conf

cat > $contentfornp << 'EOF'

#modbyshowtempfreq

$res->{thermalstate} = `sensors -A`;
$res->{cpuFreq} = `
	cat /proc/cpuinfo | grep -i  "cpu mhz"
	echo -n 'gov:'
	cat /sys/devices/system/cpu/cpufreq/policy0/scaling_governor
	echo -n 'min:'
	cat /sys/devices/system/cpu/cpufreq/policy0/cpuinfo_min_freq
	echo -n 'max:'
	cat /sys/devices/system/cpu/cpufreq/policy0/cpuinfo_max_freq
	echo -n 'pkgwatt:'
	[ -e /usr/sbin/turbostat ] && turbostat --quiet --cpu package --show "PkgWatt" -S sleep 0 2>&1| tail -n1

`;
EOF



contentforpvejs=/tmp/.contentforpvejs.tmp

cat > $contentforpvejs << 'EOF'
//modbyshowtempfreq
	{
		itemId: 'thermal',
		colspan: 2,
		printBar: false,
		title: gettext('温度(°C)'),
		textField: 'thermalstate',
		renderer:function(value){
			//value进来的值是有换行符的
			
			let b = value.trim().split(/\s+(?=^\w+-)/m).sort();
			let c = b.map(function (v){
				let name = v.match(/^[^-]+/)[0].toUpperCase();
				
				let temp = v.match(/(?<=:\s+[+-]?)\d+/g);
				// 某些没有数据的传感器
				if ( ! temp ) {
					
					return 'null'
				}
				
				if (/coretemp/i.test(name)) {
					name = 'CPU';
					temp = temp[0] + ' ( ' +   temp.slice(1).join(' | ') + ' )';
				} else {
					temp = temp[0];
				}
				
				let crit = v.match(/(?<=\bcrit\b[^+]+\+)\d+/);
				
				
				return name + ': ' + temp + ( crit? ` ,crit: ${crit[0]}` : '');
			});
			// 排除null值的
			c=c.filter( v => ! /^null$/.test(v) )
			//console.log(c);
			//排序,把cpu温度放最前
			let cpuIdx = c.findIndex(v => /CPU/i.test(v) );
			if (cpuIdx > 0) {
				c.unshift(c.splice(cpuIdx, 1)[0]);
			}
			
			console.log(c)
			c = c.join(' | ');
			return c;
		 }
	},
	{
		  itemId: 'cpumhz',
		  colspan: 2,
		  printBar: false,
		  title: gettext('CPU频率(GHz)'),
		  textField: 'cpuFreq',
		  renderer:function(v){
			//return v;
			console.log(v)
			let m = v.match(/(?<=cpu[^\d]+)\d+/ig);
			let m2 = m.map( e => ( e / 1000 ).toFixed(1) );
			m2 = m2.join(' | ');
			let gov = v.match(/(?<=gov:).+/i)[0].toUpperCase();
			let min = (v.match(/(?<=min:)\d+/i)[0]/1000000).toFixed(1);
			let max = (v.match(/(?<=max:)\d+/i)[0]/1000000).toFixed(1);
			let watt= v.match(/(?<=pkgwatt:)[\d.]+/i);
			watt = watt? " | 功耗: " + (watt[0]/1).toFixed(1) + 'W' : '';
			return `${m2} | MAX: ${max} | MIN: ${min}${watt} | 调速器: ${gov}`
		 }
	},
EOF


#检测nvme硬盘
echo 检测系统中的NVME硬盘
nvi=0
if $sNVMEInfo;then
	for nvme in $(ls /dev/nvme[0-9] 2> /dev/null); do
		chmod +s /usr/sbin/smartctl

		cat >> $contentfornp << EOF
	\$res->{nvme$nvi} = \`smartctl $nvme -a -j\`;
EOF
		
		
		cat >> $contentforpvejs << EOF
		{
			  itemId: 'nvme${nvi}0',
			  colspan: 2,
			  printBar: false,
			  title: gettext('NVME${nvi}'),
			  textField: 'nvme${nvi}',
			  renderer:function(value){
				//return value;
				try{
					let  v = JSON.parse(value);
					//名字
					let model = v.model_name;
					if (! model) {
						return '找不到硬盘,直通或已被卸载';
					}
					// 温度
					let temp = v.temperature?.current;
					temp = ( temp !== undefined ) ? " | " + temp + '°C' : '' ;
					
					// 通电时间
					let pot = v.power_on_time?.hours;
					let poth = v.power_cycle_count;
					
					pot = ( pot !== undefined ) ? (" | 通电: " + pot + '时' + ( poth ? ',次: '+ poth : '' )) : '';
					
					// 读写
					let log = v.nvme_smart_health_information_log;
					let rw=''
					let health=''
					if (log) {
						let read = log.data_units_read;
						let write = log.data_units_written;
						read = read ? (log.data_units_read / 1956882).toFixed(1) + 'T' : '';
						write = write ? (log.data_units_written / 1956882).toFixed(1) + 'T' : '';
						if (read && write) {
							rw = ' | R/W: ' + read + '/' + write;
						}
						let pu = log.percentage_used;
						let me = log.media_errors;
						if ( pu !== undefined ) {
							health = ' | 健康: ' + ( 100 - pu ) + '%'
							if ( me !== undefined ) {
								health += ',0E: ' + me
							}
						}
					}

					// smart状态
					let smart = v.smart_status?.passed;
					if (smart === undefined ) {
						smart = '';
					} else {
						smart = ' | SMART: ' + (smart ? '正常' : '警告!');
					}
					
					
					let t = model  + temp + health + pot + rw + smart;
					//console.log(t);
					return t;
				}catch(e){
					return '无法获得有效消息';
				};

			 }
		},
EOF
		let nvi++
	done
fi
echo "已添加 $nvi 块NVME硬盘"



#检测机械键盘
echo 检测系统中的SATA固态和机械硬盘
sdi=0
if $sODisksInfo;then
	for sd in $(ls /dev/sd[a-z] 2> /dev/null);do
		chmod +s /usr/sbin/smartctl
		#检测是否是真的机械键盘
		sdsn=$(echo $sd | awk -F '/' '{print $NF}')
		sdcr=/sys/block/$sdsn/queue/rotational
		sdtype="机械硬盘$sdi"
		
		if [ ! -e $sdcr ];then
			continue
		else
			if [ "$(cat $sdcr)" -eq 0 ];then 
				sdtype="固态硬盘$sdi"
			fi
		fi

		#[] && 型条件判断,嵌套的条件判断的非 || 后面一定要写动作,否则会穿透到上一层的非条件
		#机械/固态硬盘输出信息逻辑,
		#如果硬盘不存在就输出空JSON

		cat >> $contentfornp << EOF
	\$res->{sd$sdi} = \`
		if [ -b $sd ];then
			smartctl $sd -a -j
		else
			echo '{}'
		fi
	\`;
EOF

		cat >> $contentforpvejs << EOF
		{
			  itemId: 'sd${sdi}0',
			  colspan: 2,
			  printBar: false,
			  title: gettext('${sdtype}'),
			  textField: 'sd${sdi}',
			  renderer:function(value){
				//return value;
				try{
					let  v = JSON.parse(value);
					//名字
					let model = v.model_name;
					if (! model) {
						return '找不到硬盘,直通或已被卸载';
					}
					// 温度
					let temp = v.temperature?.current;
					temp = ( temp !== undefined ) ? " | 温度: " + temp + '°C' : '' ;
					
					// 通电时间
					let pot = v.power_on_time?.hours;
					let poth = v.power_cycle_count;
					
					pot = ( pot !== undefined ) ? (" | 通电: " + pot + '时' + ( poth ? ',次: '+ poth : '' )) : '';
					
					// smart状态
					let smart = v.smart_status?.passed;
					if (smart === undefined ) {
						smart = '';
					} else {
						smart = ' | SMART: ' + (smart ? '正常' : '警告!');
					}
					
					
					let t = model + temp  + pot + smart;
					//console.log(t);
					return t;
				}catch(e){
					return '无法获得有效消息';
				};
			 }
		},
EOF
		let sdi++
	done
fi
echo "已添加 $sdi 块SATA固态和机械硬盘"

echo 开始修改nodes.pm文件
if ! grep -q 'modbyshowtempfreq' $np ;then
	[ ! -e $np.$pvever.bak ] && cp $np $np.$pvever.bak
	
	if [ "$(sed -n "/PVE::pvecfg::version_text()/{=;p;q}" "$np")" ];then #确认修改点
		#r追加文本后面必须跟回车,否则r 后面的文字都会被当成文件名,导致脚本出错
		sed -i "/PVE::pvecfg::version_text()/{
			r $contentfornp
		}" $np
		$dmode && sed -n "/PVE::pvecfg::version_text()/,+5p" $np
	else
		echo '找不到nodes.pm文件的修改点'
		
		fail
	fi
else
	echo 已经修改过
fi

echo 开始修改pvemanagerlib.js文件
if ! grep -q 'modbyshowtempfreq' $pvejs ;then
	[ ! -e $pvejs.$pvever.bak ]  && cp $pvejs $pvejs.$pvever.bak
	
	if [ "$(sed -n '/pveversion/,+3{
			/},/{=;p;q}
		}' $pvejs)" ];then 
		
		sed -i "/pveversion/,+3{
			/},/r $contentforpvejs
		}" $pvejs
		
		$dmode && sed -n "/pveversion/,+8p" $pvejs
	else
		echo '找不到pvemanagerlib.js文件的修改点'
		fail
	fi


	echo 修改页面高度
	#统计加了几条
	addRs=$(grep -c '\$res' $contentfornp)
	addHei=$(( 28 * addRs))
	$dmode && echo "添加了$addRs条内容,增加高度为:${addHei}px"


	#原高度300
	echo 修改左栏高度
	if [ "$(sed -n '/widget.pveNodeStatus/,+4{
			/height:/{=;p;q}
		}' $pvejs)" ]; then 
		
		#获取原高度
		wph=$(sed -n -E "/widget\.pveNodeStatus/,+4{
			/height:/{s/[^0-9]*([0-9]+).*/\1/p;q}
		}" $pvejs)
		
		sed -i -E "/widget\.pveNodeStatus/,+4{
			/height:/{
				s#[0-9]+#$(( wph + addHei))#
			}
		}" $pvejs
		
		$dmode && sed -n '/widget.pveNodeStatus/,+4{
			/height/{
				p;q
			}
		}' $pvejs

		#修改右边栏高度,让它和左边一样高,双栏的时候否则导致浮动出问题
		#原高度325
		echo 修改右栏高度和左栏一致,解决浮动错位
		if [ "$(sed -n '/nodeStatus:\s*nodeStatus/,+10{
				/minHeight:/{=;p;q}
			}' $pvejs)" ]; then 
			#获取原高度
			nph=$(sed -n -E '/nodeStatus:\s*nodeStatus/,+10{
				/minHeight:/{s/[^0-9]*([0-9]+).*/\1/p;q}
			}' "$pvejs")
			
			sed -i -E "/nodeStatus:\s*nodeStatus/,+10{
				/minHeight:/{
					s#[0-9]+#$(( nph + addHei - (nph - wph) ))#
				}
			}" $pvejs
			
			$dmode && sed -n '/nodeStatus:\s*nodeStatus/,+10{
				/minHeight/{
					p;q
				}
			}' $pvejs

		else
			echo 右边栏高度找不到修改点,修改失败
			
		fi

	else
		echo 找不到修改高度的修改点
		fail
	fi

else
	echo 已经修改过
fi


echo 温度,频率,硬盘信息相关修改已完成
echo ------------------------
echo ------------------------
echo 开始修改proxmoxlib.js文件
echo 去除订阅弹窗

if ! grep -q 'modbyshowtempfreq' $plibjs ;then

	[ ! -e $plibjs.$pvever.bak ] && cp $plibjs $plibjs.$pvever.bak
	
	if [ "$(sed -n '/\/nodes\/localhost\/subscription/{=;p;q}' $plibjs)" ];then 
		sed -i '/\/nodes\/localhost\/subscription/,+10{
			/res === null/{
				N
				s/(.*)/(false)/
				a //modbyshowtempfreq
			}
		}' $plibjs
		
		$dmode && sed -n "/\/nodes\/localhost\/subscription/,+10p" $plibjs
	else 
		echo 找不到修改点,放弃修改这个
	fi
else
	echo 已经修改过
fi
echo -e "------------------------
修改完成
请刷新浏览器缓存:\033[31mShift+F5\033[0m
如果你看到主页面提示连接错误或者没看到温度和频率,请按:\033[31mShift+F5\033[0m,刷新浏览器缓存!
如果你对效果不满意,请执行:\033[31m\"$sap\" restore\033[0m 命令,可以还原修改
"

systemctl restart pveproxy

  脚本使用:

chmod +x /tmp/temp.sh && /tmp/temp.sh remod

  如果没有显示 CPU 功耗,则执行下面的命令安装依赖:

apt update ; apt install linux-cpupower && modprobe msr && echo msr > /etc/modules-load.d/turbostat-msr.conf && chmod +s /usr/sbin/turbostat && echo 成功!

网络

image.png
  上图中,enp9s0 就是一开始安装 PVE 选择的网卡,enp3s0~enp6s0则是4口的外置网卡,vmbr0 是 PVE 自动创建的网桥,保持默认即可。
  新增一个不绑定任何网卡的vmbr1,用于其他系统联网,我这里只装了两个,一个是 Openwrt,一个是群晖,后面会写到。

参考

  1. [ Proxmox 折腾手记 ] PVE初始化配置
  2. PVE 去除企业订阅弹窗、显示CPU和硬盘信息脚本