PVE备份与美化

PVE备份:在黑群晖使用 Docker 部署 Proxmox Backup Server (hellowood.dev)

PVE备份

Proxmox Backup Server 是 PVE 容器、虚拟机的备份解决方案,支持增量、重复数据消除备份,可以节省存储空间,同时支持加密和完整性校验

Proxmox Backup Server 官方提供了 iso 格式的镜像,同时社区也有开源的 Docker 镜像的部署方式,为了在黑群晖上部署方便,使用 Docker 的方式进行部署,项目地址:https://github.com/ayufan/pve-backup-server-dockerfiles

部署 Proxmox Backup Server

使用 Docker 或者 Docker Compose 方式部署都可以

  • docker-compose.yaml

需要注意,要使用 tmpfs 方式挂载 /run 目录,用于容器内部创建临时文件和目录;

/etc/proxmox-backup: 用于存储 PVE Backup Server 的配置信息 /var/log/proxmox-backup: 用于存储日志信息 /var/lib/proxmox-backup:用于存储数据 /backups:存储容器、虚拟机的备份数据

version: '2.1'

services:
  pve-backup-server:
    image: ayufan/proxmox-backup-server:latest
    ports:
      - "8007:8007"
    volumes:
      - /docker/PVEBackup/etc:/etc/proxmox-backup
      - /docker/PVEBackup/log:/var/log/proxmox-backup
      - /docker/PVEBackup/lib:/var/lib/proxmox-backup
      - /BackupServer:/backups
    tmpfs:
      - /run
    restart: unless-stopped

配置完成后启动容器,访问 https://<ip>:8007/ 端口即可进行登录,默认的用户名是 admin,密码是 pbspbs,选择 Proxmox Backup authentication server 领域进行登录

homelab-pve-backup-server-login-page.png

配置存储

  • 配置存储路径

在 Proxmox Backup Server 的数据存储中添加数据存储,将刚才映射的 /backups 目录作为存储路径

homelab-pve-backup-server-add-backup-storage.png

  • 为用户添加权限

给用于备份的 root 和 admin 用户添加备份路径的访问权限

homelab-pve-backup-server-add-backup-permission.png

配置

  • 获取 Proxmox Backup Server 的指纹

指纹用于在 PVE 中添加备份时进行认证

homelab-pve-backup-server-show-printfinger.png

  • 添加存储

在 PVE 的数据中心-存储中选择添加 Proxmox Backup Server,输入认证信息和指纹;Datastore 为 Proxmox Backup Server 的数据存储的名称,如 Backup

homelab-pve-backup-server-add-data-storage.png

  • 添加备份作业

在 PVE 的数据中心-备份中添加备份计划,按需添加,添加完成后选择现在运行即可开始备份

homelab-pve-backup-server-add-backup-task.png

PVE美化:【Proxmox VE】看不懂正则没关系,DIY PVE 首页显示 CPU、主板、硬盘 温度等信息-恩山无线论坛 (right.com.cn)

PV美化

网上各种版本的 PVE 温度 DIY 脚本,但萝卜青菜各有所爱,总是难得遇到钟情的那一款

自行 DIY 的话,正则 + js 入门确实需要点门槛
所以这里提供另一种实现方式 json,清晰明了,便于阅读,修改简单
无论是 温度、风扇转速、硬盘温度、硬盘信息都可以轻松搞定

-----------------------------------------------------------------------------------------------------------------------------
因为重做了空值及报错处理
没人回复怪可怜的,还是回复可见吧
-----------------------------------------------------------------------------------------------------------------------------

  • 自行修改代码时,出错原因及处理方式

1. 首页转圈,只显示一部分数据
一般都是由于参数值读取错误造成的
假设硬盘 smartctl 返回值中没有温度值,而 var temperature = value['temperature']['current'].toFixed(1); 这一行命令中读取了温度值,就会造成参数读取错误,首页转圈
解决方法:
逐个参数查看是否输入错误(数据不存在或键值输入有误)


2. 首页转圈,所有自定义数据都不能显示
检查 CPU 温度代码段,是否有代码错误(参照第一条)
如修改了 Nodes.pm 文件,需要使用 systemctl restart pveproxy 命令重载 PVE 界面
仅修改 js 文件的情况下,无需重载 PVE 界面,浏览器强制刷新就能看到修改后的结果

3. 首页白屏,什么都不显示
一般都是代码有误引起的,请还原文件重新操作,或检查代码是否缺少相对应的 {} , () 等符号

4. 中文乱码
请不要将文件拖到本地使用 记事本 操作,直接在 WinSCP 中操作,或在本地使用 Notepad 等软件进行编辑
如操作无误依然乱码,请检查终端软件及 WinSCP 的编码设置,并在 PVE 终端中输入

  1. export LC_ALL=en_US.UTF-8

复制代码

5. 无报错,但显示不全
参考文末,修改显示范围
-----------------------------------------------------------------------------------------------------------------------------

  • 前置命令
  1. # 更新软件包列表:
  2. apt-get update
  3. # 安装 lm-sensors:
  4. apt-get install lm-sensors patch
  5. # 初始化 sensors(一路yes,回车):
  6. sensors-detect
  7. # 给予 smartctl 权限(如不需要硬盘信息可以忽略):
  8. chmod +s /usr/sbin/smartctl
  9. # 设置 PVE 编码为 UTF-8(如 PVE 安装时正确选择了 china 地区可以忽略):
  10. export LC_ALL=en_US.UTF-8
  11. # 获取温度信息,查看可以设置的数据:
  12. sensors
  13. # 这个时候是没有风扇等信息的,需要重启,被动散热式的主机无视这条:
  14. reboot

复制代码

 

  • 备份原文件

如命令未错误输入,文件备份在原文件相同目录下
如 : /usr/share/perl5/PVE/API2/Nodes.pm 备份为 /usr/share/perl5/PVE/API2/Nodes.pmbak

  1. proxmoxlib_js="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js"
  2. Nodes_pm="/usr/share/perl5/PVE/API2/Nodes.pm"
  3. pvemanagerlib_js="/usr/share/pve-manager/js/pvemanagerlib.js"
  4. cp ${proxmoxlib_js} ${proxmoxlib_js}bak
  5. cp ${Nodes_pm} ${Nodes_pm}bak
  6. cp ${pvemanagerlib_js} ${pvemanagerlib_js}bak

复制代码

-----------------------------------------------------------------------------------------------------------------------------

  • 懒人补丁法(若未修改过原文件,已包含高度修改、去除订阅提示,宽度未更改)
    将补丁文件放到 /tmp/ 文件夹
    大版本升级可能依然需要手动修改部分代码,补丁失败的文件及代码行数会在 patch 返回值中显示,请留意
    如遇页面显示错误,参照上文
    不保证可用
     PVE_7.2_temperatures.zip (3.77 KB, 下载次数: 769)
     PVE_8.0_temperatures.zip (3.54 KB, 下载次数: 704)

 

  1. # 应用补丁(请确认已经备份原文件)
  2. patch ${proxmoxlib_js} < /tmp/proxmoxlib_js.patch
  3. patch ${Nodes_pm} < /tmp/Nodes_pm.patch
  4. patch ${pvemanagerlib_js} < /tmp/pvemanagerlib_js.patch
  5. # 重载 PVE 界面
  6. systemctl restart pveproxy

复制代码

 

  • 自行制作补丁

可以在重装或升级后快速修改文件(大版本升级可能需要手动修改部分代码)
生成的补丁文件在 /tmp/ 文件夹:

  1. proxmoxlib_js="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js"
  2. Nodes_pm="/usr/share/perl5/PVE/API2/Nodes.pm"
  3. pvemanagerlib_js="/usr/share/pve-manager/js/pvemanagerlib.js"
  4. diff -uN ${proxmoxlib_js}bak ${proxmoxlib_js}  > /tmp/proxmoxlib_js.patch
  5. diff -uN ${Nodes_pm}bak ${Nodes_pm}  > /tmp/Nodes_pm.patch
  6. diff -uN ${pvemanagerlib_js}bak ${pvemanagerlib_js}  > /tmp/pvemanagerlib_js.patch

复制代码

-----------------------------------------------------------------------------------------------------------------------------

  • 自行修改文件

修改 Nodes.pm 文件加入以下代码
/usr/share/perl5/PVE/API2/Nodes.pm
搜索:$res->{pveversion} = PVE::pvecfg::package()
下方添加

  1. $res->{sensors_json} = `sensors -j`; # 获取 CPU 、主板温度及风扇转速
  2. $res->{smartctl_nvme_json} = `smartctl -a -j /dev/nvme?`; # 读取 nvme 硬盘 S.M.A.R.T. 值,获取硬盘寿命、容量、温度等,如果存在设备号错误无显示,自行修改 nvme? ,帖子内容只做了一个硬盘显示,需要多个硬盘的话还需要修改下方代码块
  3. $res->{smartctl_sda_json} = `smartctl -i -n standby /dev/sda|grep "STANDBY" || smartctl -i -n standby /dev/sda|grep "No such device" || smartctl -a -j /dev/sda`; #先检测硬盘是否为休眠状态,若否,则检查磁盘是否存在,若否,则返回 S.M.A.R.T. 值
  4. $res->{smartctl_sdb_json} = `smartctl -i -n standby /dev/sdb|grep "STANDBY" || smartctl -i -n standby /dev/sdb|grep "No such device" || smartctl -a -j /dev/sdb`; #先检测硬盘是否为休眠状态,若否,则检查磁盘是否存在,若否,则返回 S.M.A.R.T. 值
  5. $res->{cpusensors} = `lscpu | grep MHz`; # 读取 CPU 频率

复制代码

 

  • 关于获取硬盘名称
  1. lsblk | awk '$NF=="disk" {print $1}' | sort -u

复制代码

 

    • 终端输入

 sensors -j 

    • 命令,我们可以得到类似下面的结果(以下为 CPU 温度值截取)

 

    • 先眼熟一下 JSON 格式的返回值,等下会用到

 

  1. "coretemp-isa-0000":{
  2.       "Adapter": "ISA adapter",
  3.       "Package id 0":{
  4.          "temp1_input": 38.000,
  5.          "temp1_max": 84.000,
  6.          "temp1_crit": 100.000,
  7.          "temp1_crit_alarm": 0.000
  8.       },
  9.       "Core 0":{
  10.          "temp2_input": 34.000,
  11.          "temp2_max": 84.000,
  12.          "temp2_crit": 100.000,
  13.          "temp2_crit_alarm": 0.000
  14.       },
  15.       "Core 1":{
  16.          "temp3_input": 38.000,
  17.          "temp3_max": 84.000,
  18.          "temp3_crit": 100.000,
  19.          "temp3_crit_alarm": 0.000
  20.       }

复制代码

修改 pvemanagerlib.js 文件

    • /usr/share/pve-manager/js/pvemanagerlib.js

 

    • 搜索 PVE Manager Version

 

    • 下方添加

 

 

      • 原始版本及注释
  1.         {
  2.             itemId: 'thermal',  // thermal 代表了这一段代码的 id ,随便改一下不要重复就行了
  3.             colspan: 2,
  4.             printBar: false,
  5.                 title: gettext('温度'),  // 这里表示你想在页面中显示的左侧标题名
  6.                 textField: 'sensors_json',  // 这里需要填写 Nodes.pm 文件中对应的命令,也就是从哪一个返回值中获取数据
  7.                 renderer: function(value) {
  8.                         value = JSON.parse(value);  // 使用 JavaScript 内置函数 JSON.parse() 将字符串转换为 JavaScript 对象
  9.                         const cpu0 = value['coretemp-isa-0000']['Package id 0']['temp1_input'].toFixed(1);  // 这里表示读取 CPU 温度,对应 sensors -j 输出的 JSON 格式数据,toFixed(1) 表示将数字转换为字符,只保留 (1) 位小数
  10.                         const PECI0 = value['nct6798-isa-0290']['PECI Agent 0']['temp7_input'].toFixed(1); // 同上,自行修改
  11.                         const pch = value['pch_cometlake-virtual-0']['temp1']['temp1_input'].toFixed(1); // 同上,自行修改
  12.                         return `CPU: ${cpu0}°C || 南桥: ${pch} ℃ | 网卡: ${PECI0} ℃`; // return 表示输出值,也就是最后显示在 WEB 页面中的值,{}中填入上几行中定义的变量,格式自行调整
  13.                 }
  14.         },

复制代码

 

      • 加一点细节
  1.         {
  2.                 itemId: 'thermal',
  3.                 colspan: 2,
  4.                 printBar: false,
  5.                 title: gettext('温度'),
  6.                 textField: 'sensors_json',
  7.                 renderer: function(value) {
  8.                         value = value.replace(/temp([0-9]{1,})_input/g,'input');
  9.                         // Intel
  10.                         if (value.indexOf("coretemp-isa") != -1 ) {
  11.                                 value = value.replace(/coretemp-isa-(.{4})/g,'coretemp-isa');
  12.                                 value = value.replace(/nct6798-isa-(.{4})/g,'nct6798-isa');
  13.                                 value = JSON.parse(value);
  14.                                 try {var cpu_Intel = 'CPU: ' + value['coretemp-isa']['Package id 0']['input'].toFixed(1) + '°C';} catch(e) {var cpu_Intel = '';}
  15.                                 try {var acpi = ' || 主板:  ' + value['acpitz-acpi-0']['temp1']['input'].toFixed(1) + '°C';} catch(e) {var acpi = '';}
  16.                                 try {var pch = ' || 南桥:  ' + value['pch_cometlake-virtual-0']['temp1']['input'].toFixed(1) + '°C';} catch(e) {var pch = '';}
  17.                                 try {var pci0 = ' || 网卡:  ' + value['nct6798-isa']['PECI Agent 0']['input'].toFixed(1) + '°C';} catch(e) {var pci0 = '';}
  18.                                 // 如果存在主板、PCI网卡温度,优先显示
  19.                                 if (cpu_Intel.length > 0 && pch.length + acpi.length + pci0.length > 0) {
  20.                                         return `${cpu_Intel}${acpi}${pch}${pci0}`;
  21.                                 // 如果不存在,显示 CPU 全核温度,最高支持 8 核心
  22.                                 } else if (cpu_Intel.length > 0) {
  23.                                         try {var cpu0 = ' || 核心 0 : ' + value['coretemp-isa']['Core 0']['input'].toFixed(1) + '°C';} catch(e) {var cpu0 = '';}
  24.                                         try {var cpu1 = ' | 核心 1 : ' + value['coretemp-isa']['Core 1']['input'].toFixed(1) + '°C';} catch(e) {var cpu1 = '';}
  25.                                         try {var cpu2 = ' | 核心 2 : ' + value['coretemp-isa']['Core 2']['input'].toFixed(1) + '°C';} catch(e) {var cpu2 = '';}
  26.                                         try {var cpu3 = ' | 核心 3 : ' + value['coretemp-isa']['Core 3']['input'].toFixed(1) + '°C';} catch(e) {var cpu3 = '';}
  27.                                         try {var cpu4 = ' | 核心 4 : ' + value['coretemp-isa']['Core 4']['input'].toFixed(1) + '°C';} catch(e) {var cpu4 = '';}
  28.                                         try {var cpu5 = ' | 核心 5 : ' + value['coretemp-isa']['Core 5']['input'].toFixed(1) + '°C';} catch(e) {var cpu5 = '';}
  29.                                         try {var cpu6 = ' | 核心 6 : ' + value['coretemp-isa']['Core 6']['input'].toFixed(1) + '°C';} catch(e) {var cpu6 = '';}
  30.                                         try {var cpu7 = ' | 核心 7 : ' + value['coretemp-isa']['Core 7']['input'].toFixed(1) + '°C';} catch(e) {var cpu7 = '';}
  31.                                         return `${cpu_Intel}${cpu0}${cpu1}${cpu2}${cpu3}${cpu4}${cpu5}${cpu6}${cpu7}`;
  32.                                 }
  33.                         // AMD
  34.                         } else if (value.indexOf("amdgpu-pci") != -1 ) {
  35.                                 value = value.replace(/k10temp-pci-(.{4})/g,'k10temp-pci');
  36.                                 value = value.replace(/zenpower-pci-(.{4})/g,'zenpower-pci');
  37.                                 value = value.replace(/amdgpu-pci-(.{4})/g,'amdgpu-pci');
  38.                                 value = JSON.parse(value);
  39.                                 try {var cpu_amd_k10 = 'CPU: ' + value['k10temp-pci']['Tctl']['input'].toFixed(1) + '°C';} catch(e) {var cpu_amd_k10 = '';}
  40.                                 try {var cpu_amd_zen = 'CPU: ' + value['zenpower-pci']['Tctl']['input'].toFixed(1) + '°C';} catch(e) {var cpu_amd_zen = '';}
  41.                                 try {var amdgpu = ' | GPU:  ' + value['amdgpu-pci']['edge']['input'].toFixed(1) + '°C';} catch(e) {var amdgpu = '';}
  42.                                 return `${cpu_amd_k10}${cpu_amd_zen}${amdgpu}`;
  43.                         } else {
  44.                                 return `提示: CPU 及 主板 温度读取异常`;
  45.                         }
  46.                 }
  47.         },
  48.         {
  49.                 itemId: 'nvme_ssd',
  50.                 colspan: 2,
  51.                 printBar: false,
  52.                 title: gettext('NVME'),
  53.                 textField: 'smartctl_nvme_json',
  54.                 renderer: function(value) {
  55.                         value = JSON.parse(value);
  56.                         if (value['model_name']) {
  57.                                 try {var model_name = value['model_name'];} catch(e) {var model_name = '';}
  58.                                 try {var percentage_used = ' | 使用寿命: ' + value['nvme_smart_health_information_log']['percentage_used'].toFixed(0) + '% ';} catch(e) {var percentage_used = '';}
  59.                                 try {var data_units_read = value['nvme_smart_health_information_log']['data_units_read']*512/1024/1024/1024;var data_units_read = '(读: ' + data_units_read.toFixed(2) + 'TB, ';} catch(e) {var data_units_read = '';}
  60.                                 try {var data_units_written = value['nvme_smart_health_information_log']['data_units_written']*512/1024/1024/1024;var data_units_written = '写: ' + data_units_written.toFixed(2) + 'TB)';} catch(e) {var data_units_written = '';}
  61.                                 try {var power_on_time = ' | 通电: ' + value['power_on_time']['hours'].toFixed(0) + '小时';} catch(e) {var power_on_time = '';}
  62.                                 try {var temperature = ' | 温度: ' + value['temperature']['current'].toFixed(1) + '°C';} catch(e) {var temperature = '';}
  63.                                 return `${model_name}${percentage_used}${data_units_read}${data_units_written}${power_on_time}${temperature}`;
  64.                         } else {
  65.                                 return `提示: 未安装硬盘或已直通硬盘控制器`;
  66.                         }
  67.                 }
  68.         },
  69.         {
  70.                 itemId: 'SATA_sda',
  71.                 colspan: 2,
  72.                 printBar: false,
  73.                 title: gettext('SATA_sda'),
  74.                 textField: 'smartctl_sda_json',
  75.                 renderer: function(value) {
  76.                         if (value.indexOf("Device is in STANDBY mode") != -1 ) {
  77.                                 return `提示: 磁盘休眠中`;
  78.                         } else if (value.indexOf("No such device") != -1 ) {
  79.                                 return `提示: 未安装硬盘或已直通硬盘控制器`;
  80.                         } else {
  81.                         value = JSON.parse(value);
  82.                                 try {var model_name = value['model_name'];} catch(e) {var model_name = '';}
  83.                                 try {var user_capacity = value['user_capacity']['bytes']/1024/1024/1024;var user_capacity = ' | 容量: ' + user_capacity.toFixed(2) + ' GB';} catch(e) {var user_capacity = '';}
  84.                                 try {var power_on_time = ' | 已通电: ' + value['power_on_time']['hours'].toFixed(0) + ' 小时';} catch(e) {var power_on_time = '';}
  85.                                 try {var error_count = value['ata_smart_error_log']['summary']['count'].toFixed(0);if (error_count != 0){error_count = ' | 磁盘错误: ' + error_count;} else {var error_count = '';} } catch(e) {var error_count = '';}
  86.                                 try {var self_count = value['ata_smart_self_test_log']['standard']['error_count_total'].toFixed(0);if (self_count != 0){self_count = ' | 自检错误: ' + self_count;} else {var self_count = '';} } catch(e) {var self_count = '';}
  87.                                 try {var temperature = ' | 温度: ' + value['temperature']['current'].toFixed(1) + '°C';} catch(e) {var temperature = '';}
  88.                                 return `${model_name}${user_capacity}${power_on_time}${error_count}${self_count}${temperature}`;
  89.                         }
  90.                 }
  91.         },
  92.         {
  93.                 itemId: 'SATA_sdb',
  94.                 colspan: 2,
  95.                 printBar: false,
  96.                 title: gettext('SATA_sdb'),
  97.                 textField: 'smartctl_sdb_json',
  98.                 renderer: function(value) {
  99.                         if (value.indexOf("Device is in STANDBY mode") != -1 ) {
  100.                                 return `提示: 磁盘休眠中`;
  101.                         } else if (value.indexOf("No such device") != -1 ) {
  102.                                 return `提示: 未安装硬盘或已直通硬盘控制器`;
  103.                         } else {
  104.                         value = JSON.parse(value);
  105.                                 try {var model_name = value['model_name'];} catch(e) {var model_name = '';}
  106.                                 try {var user_capacity = value['user_capacity']['bytes']/1024/1024/1024;var user_capacity = ' | 容量: ' + user_capacity.toFixed(2) + ' GB';} catch(e) {var user_capacity = '';}
  107.                                 try {var power_on_time = ' | 已通电: ' + value['power_on_time']['hours'].toFixed(0) + ' 小时';} catch(e) {var power_on_time = '';}
  108.                                 try {var error_count = value['ata_smart_error_log']['summary']['count'].toFixed(0);if (error_count != 0){error_count = ' | 磁盘错误: ' + error_count;} else {var error_count = '';} } catch(e) {var error_count = '';}
  109.                                 try {var self_count = value['ata_smart_self_test_log']['standard']['error_count_total'].toFixed(0);if (self_count != 0){self_count = ' | 自检错误: ' + self_count;} else {var self_count = '';} } catch(e) {var self_count = '';}
  110.                                 try {var temperature = ' | 温度: ' + value['temperature']['current'].toFixed(1) + '°C';} catch(e) {var temperature = '';}
  111.                                 return `${model_name}${user_capacity}${power_on_time}${error_count}${self_count}${temperature}`;
  112.                         }
  113.                 }
  114.         },
  115.         {
  116.                 itemId: 'MHz',
  117.                 colspan: 2,
  118.                 printBar: false,
  119.                 title: gettext('CPU频率'),
  120.                 textField: 'cpusensors',
  121.                 renderer:function(value){
  122.                         var f0 = value.match(/CPU MHz.*?([\d]+)/)[1];
  123.                         var f1 = value.match(/CPU min MHz.*?([\d]+)/)[1];
  124.                         var f2 = value.match(/CPU max MHz.*?([\d]+)/)[1];
  125.                         return `实时: ${f0} MHz || 最小: ${f1} MHz | 最大: ${f2} MHz `
  126.                 }
  127.         },

复制代码

 

    • 如果是 PVE 8.0,CPU 不再返回实时频率,需要修改为下列代码

 

  1. {
  2.         itemId: 'MHz',
  3.         colspan: 2,
  4.         printBar: false,
  5.         title: gettext('CPU频率'),
  6.         textField: 'cpusensors',
  7.         renderer:function(value){
  8.                 var f1 = value.match(/CPU min MHz.*?([\d]+)/)[1];
  9.                 var f2 = value.match(/CPU max MHz.*?([\d]+)/)[1];
  10.                 var f0 = value.match(/CPU.*scaling MHz.*?([\d]+)/)[1];
  11.                 var f0 = f0*f2/100;
  12.                 return `实时: ${f0} MHz || 最小: ${f1} MHz | 最大: ${f2} MHz `
  13.         }
  14.         },

复制代码

 

      • 修改显示范围

 

    • 依然是 pvemanagerlib.js 文件

 

    • 搜索 widget.pveNodeStatus

 

    • 将 height: 300 (默认值) 改大为 420,或者更大,然后保存(每多一行大概增大 20~25)

 

 

      • 修改完成后重载 PVE 界面
      • 如无把握,建议不要一次加入太多代码,修改一段就重载一次

 

  1. systemctl restart pveproxy

复制代码

 

      • 关于虚拟机标签显示

 

     

版权声明:
作者:Jays
链接:https://ijays.com/2024/03/pve-backup-airbrush.html
来源:颓废的美
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
< <上一篇
下一篇>>