2026-05-13 21:43:50
一直想做但是一直没做,终于断断续续做好了。
目前在我的 homelab 内,我是将各种服务分散放置的——也就是有些服务在 Proxmox 的 LXC 内,有些服务在 VM 内。同时,在互联网上我也有几台 VPS,分别部署了不同的服务。这就带来了一个不可避免的问题——更新怎么办?手动登录每一台设备然后进行更新不仅很机械劳动,而且一旦 mirror 突然因为网络问题导致速度不稳定的话,非常费时间。久而久之我也感觉烦了,每周更新又很烦人,每月更新的话累积到一起积少成多了又很花时间。
那么该怎么办呢?搜了一圈最终打算使用 Ansible。它不仅开源,而且部署也很方便,主流的 Linux 发行版也都支持。不过,它只能承担日常的小版本升级,而向 Ubuntu 24.04 –> Ubuntu 26.04 这种大版本升级就不能靠它了。不过这种大版本升级肯定也是要人工介入的吧(想起来上周六对几台 vps 这么做因为性能比较差花了我 1-2 小时,还有一台差点炸了)。
最终我的想法是,内网一台 VM 当内部主控,然后这台机器能够更新其它的机器和海外的 VPS。但海外的 VPS 会遇到网络问题,所以同时在某台我也能比较流畅的访问的 VPS 上,也部署一套,但专门负责更新海外其它 VPS。同时,让它不仅能够控制开机的服务,在 homelab 内还能控制当前关机的机器自动开机、更新之后再关机(因为我都放在 Proxmox 内)。
因为我的内部主控节点是 Fedora,所以很好安装。但 Fedora 并没有打包 Proxmox API 所需要的依赖,所以需要 virtualenv 起一个虚拟环境,这样不至于破坏系统级别的 Python 包。
sudo dnf update
sudo dnf install -y ansible python3 python3-pip openssh-clients
python3 -m venv ~/.venvs/ansible-proxmox
~/.venvs/ansible-proxmox/bin/python -m pip install --upgrade pip
~/.venvs/ansible-proxmox/bin/python -m pip install proxmoxer requests
ansible-galaxy collection install community.proxmox community.general
而我的海外主控 VPS 是 Ubuntu,这里为了懒,直接用的 apt 源。26.04 提供的版本在我做这个的时候还算新。
sudo apt update
sudo apt install -y ansible python3-proxmoxer python3-requests python3-apt openssh-client
ansible-galaxy collection install community.proxmox
在这里我只放内部主控节点怎么配置的,海外主控 VPS 只是去掉了我 homelab 的部分。
ansible.cfg 作为配置文件在根目录,inventory 文件夹里面同时放我 homelab 内 Proxmox 的配置、海外 VPS 的配置和相关变量。
/srv/infra-patch/
├── ansible.cfg
├── inventory/
│ ├── proxmox.proxmox.yml
│ ├── vps.yml
│ ├── group_vars/
│ │ ├── all/
│ │ │ └── proxmox_api.yml
│ │ ├── tag_linux.yml
│ │ ├── vps.yml
│ │ ├── vps_us.yml
│ │ ├── vps_eu.yml
│ │ └── vps_asia.yml
│ └── host_vars/
│ ├── localhost.yml
│ ├── nextcloud.yml
│ ├── jellyfin.yml
│ ├── ...
├── playbooks/
│ ├── patch_running.yml
│ ├── patch_stopped.yml
│ ├── patch_vps.yml
│ └── patch_vps_controllers.yml
└── requirements.yml
[defaults]
inventory = ./inventory
host_key_checking = True
forks = 5
timeout = 30
interpreter_python = auto_silent
stdout_callback = ansible.builtin.default
callback_result_format = yaml
callback_result_indentation = 4
vault_password_file = /home/<your username>/.config/ansible/proxmox.vault_pass
[ssh_connection]
pipelining = True
其中的 vault_password_file 是一个本地只读的密码文件(chmod 600),这里只有 Vault 的密码,Ansible 用它来解密 Vault 的内容。它不是 SSH 密钥也不是 Proxmox token secret,而是”解密你那些被 Vault 加密过的配置”的密码。如果两台节点共享同一套 Vault 文件的话,这个密码内容也要一样。
inventory 文件夹在此之前,我们需要去 Proxmox 那边创建一个 API Tokens。选择 Datacenter –> Permissions –> API Tokens,添加 Token。弹出的窗口中,用户名选择 root@pam,TokenID 写 ansible,勾选 Privilege Separation。此时会有一个 Token Secret,一定要记下来。
然后回到 Permissions,点击 Add,选择 API Token Permission。弹出的窗口中,Path 选择 /,API Token 选择刚刚创建的 root@pam!ansible。因为 Role 一次只能添加一个,所以需要添加两次,我们要分别添加 PVEAuditor 和 PVEVMUser。后者是为了开关 VM/LXC,当然,你自己创建一个只有 VM.PowerMgmt 的 Role 也行。
之后,我们需要在配置文件内加密这个 Secret。还记得刚刚的 vault_password_file 吗?我们要用到它了。
ansible-vault encrypt_string \
--vault-password-file /home/<your username>/ansible/proxmox.vault_pass \
'<MyProxmoxTokenSecret>' \
--name token_secret
这个命令会输出这样一段内容:
token_secret: !vault |
$ANSIBLE_VAULT;1.1;AES256
6236393966...
我们要的就是这个内容!接下来创建对应的 Proxmox 配置即可:
# /srv/infra-patch/inventory/proxmox.proxmox.yml
plugin: community.proxmox.proxmox
url: "https://<PVE IP>:8006"
user: "root@pam"
token_id: "ansible"
token_secret: !vault |
$ANSIBLE_VAULT;1.1;AES256
<YOUR_ENCRYPT_DATA_HERE>
validate_certs: false
want_facts: true
want_proxmox_nodes_ansible_host: false
keyed_groups:
- key: proxmox_tags_parsed
prefix: "tag_"
separator: ""
compose:
ansible_host: >-
(
proxmox_lxc_interfaces
| default([])
| rejectattr('name', 'equalto', 'lo')
| map(attribute='inet')
| select('defined')
| map('regex_replace', '/.*', '')
| list
| first
)
同样的,我们还需要一个 Proxmo API 的变量文件,这个是给后面的 playbook 用的。同样的,编写完记得用 ansible-vault encrypt /srv/infra-patch/inventory/group_vars/all/proxmox_api.yml 指令加密。
# /srv/infra-patch/inventory/group_vars/all/proxmox_api.yml
proxmox_api_host: 10.0.10.228
proxmox_api_user: root@pam
proxmox_api_token_id: ansible
proxmox_api_token_secret: 这里填真实token
proxmox_validate_certs: false
在 Fedora 内网主控上,我给 localhost 也写了单独的 host_vars 文件,因为开启已关机的 LXC 和 VM 需要在本机调用 Proxmox API 模块(这就是为啥前面要起一个 venv)才行。
# /srv/infra-patch/inventory/host_vars/localhost.yml
ansible_connection: local
ansible_python_interpreter: /home/<your username>/.venvs/ansible-proxmox/bin/python
接下来,虽然 Ansible 对于 LXC 可以通过 Proxmox 的 API 看到容器的 IP,但原本已经关机的机器,或者没有装 qemu-guest-agent 的 VM,Ansible 可能都不能发现 IP。不过在这里,因为我的各个服务都在内网设置了静态 IP,所以对每台机器,都在 host_vars 内创建对应的 YAML 变量文件:
# /srv/infra-patch/inventory/host_vars/nextcloud.yml
ansible_host: 10.0.10.107
ansible_user: ansible
ansible_ssh_private_key_file: /home/<your username>/.ssh/id_rsa
可以发现,我是使用 SSH Key 来验证的。因为我的机器都预先配置了 SSH Key only 登录,所以我需要每一台机器都跑一遍创建对应的用户脚本:
sudo useradd -m -s /bin/bash -G sudo ansible
sudo mkdir -p /home/ansible/.ssh
sudo chmod 700 /home/ansible/.ssh
# 这里记得把你的 public Key 灌进去
printf '%s\n' '$PUBKEY' | sudo tee /home/ansible/.ssh/authorized_keys >/dev/null
sudo chown -R ansible:ansible /home/ansible/.ssh
sudo chmod 600 /home/ansible/.ssh/authorized_keys
echo 'ansible ALL=(ALL) NOPASSWD:ALL' | sudo tee /etc/sudoers.d/90-ansible >/dev/null
sudo chmod 440 /etc/sudoers.d/90-ansible
sudo visudo -cf /etc/sudoers.d/90-ansible
注意,如果是 Fedora 系,sudo 组要换成 wheel。
这里我最终写成了静态 YAML,因为多半不会变。在这里,我给海外主控 VPS 增加了 tag_controller,这样平时运行升级海外 VPS 的时候会自动排除它,如果我想从内网主控更新这台机器的话,会有单独的配置对他进行升级。
# /srv/infra-patch/inventory/vps.yml
all:
children:
vps:
children:
vps_oa:
hosts:
oa-01:
ansible_host: x.x.x.x
vps_asia:
hosts:
asia-01:
ansible_host: x.x.x.x
asia-02:
ansible_host: x.x.x.x
asia-03:
ansible_host: x.x.x.x
vps_us:
hosts:
us-01:
ansible_host: x.x.x.x
vps_eu:
hosts:
eu-01:
ansible_host: x.x.x.x
vps_af:
hosts:
af-01:
ansible_host: x.x.x.x
tag_linux:
children:
tag_autopatch:
children:
tag_ubuntu:
hosts:
oa-01:
asia-01:
asia-02:
asia-03:
us-01:
eu-01:
af-01:
tag_controller:
hosts:
oa-01:
同样的,我们也需要一个变量文件。这个我也写得简单,只不过对一些通过我 homelab 直接连过去因为线路问题比较慢的 VPS,我单独会对一些机器加一些参数。Ansible 在高延迟的环境里依然是可以用的,只是要把并发调低。
# /srv/infra-patch/inventory/group_vars/vps.yml
ansible_user: ansible
ansible_become: true
ansible_ssh_private_key_file: /home/<your username>/.ssh/id_rsa
ansible_connection: ssh
# /srv/infra-patch/inventory/group_vars/vps_af.yml
ansible_ssh_common_args: "-o ServerAliveInterval=30 -o ServerAliveCountMax=6"
playbooks 文件夹我在 Proxmox 内,给所有需要进行自动更新的机器都加了 autopatch 这个标签。
这部分很简单,逻辑就是,只对有 autopatch 标签且正在运行的机器们升级。
# /srv/infra-patch/playbooks/patch_running.yml
- name: Patch running Linux guests
hosts: "tag_autopatch:&proxmox_all_running"
serial: "20%"
gather_facts: true
become: true
max_fail_percentage: 20
tasks:
- name: Debian/Ubuntu | refresh apt cache
ansible.builtin.apt:
update_cache: true
cache_valid_time: 3600
update_cache_retries: 5
update_cache_retry_max_delay: 12
lock_timeout: 300
when: ansible_facts['os_family'] == 'Debian'
- name: Debian/Ubuntu | dist-upgrade
ansible.builtin.apt:
upgrade: dist
autoremove: true
clean: true
dpkg_options: "force-confdef,force-confold"
lock_timeout: 300
when: ansible_facts['os_family'] == 'Debian'
- name: Debian/Ubuntu | reboot required marker
ansible.builtin.stat:
path: /var/run/reboot-required
when: ansible_facts['os_family'] == 'Debian'
register: deb_reboot_required
- name: Fedora/RHEL | upgrade installed packages
ansible.builtin.dnf:
name: "*"
state: latest
update_only: true
update_cache: true
when: ansible_facts['os_family'] == 'RedHat'
- name: openSUSE | dist-upgrade
community.general.zypper:
name: "*"
state: dist-upgrade
update_cache: true
when: ansible_facts['os_family'] == 'Suse'
- name: Print reboot hint for Debian/Ubuntu
ansible.builtin.debug:
msg: "reboot required on {{ inventory_hostname }}"
when:
- ansible_facts['os_family'] == 'Debian'
- deb_reboot_required.stat.exists | default(false)
这部分就有点绕了。
首先,这些机器未必能自动拿到 IP(LXC 可以,VM 是多半不太行的),并且需要做开关机的操作,所以部分操作要在控制节点本身上通过 Proxmox API 执行。
其次,这些机器一旦开机,在 SSH 主机指纹较旧的情况下,可能会先被 Host key verification failed 这个错误直接卡到 timeout,所以要处理 known_hosts。这样,逻辑就变成了:
localhost 上构建一个只对有 autopatch 标签且已经关机的机器们的组,然后按 LXC 和 VM 分别启动,接着刷新 inventory,再等 TCP 22 端口通,通了之后,在主控上删除对应的旧 host key,重新 ssh-keyscan 写入 known_hosts,最后再去跑升级,完成之后再关机。
# /srv/infra-patch/playbooks/patch_stopped.yml
- name: Build groups for autopatch hosts that were initially stopped
hosts: localhost
gather_facts: false
vars:
autopatch_hosts: "{{ groups['tag_autopatch'] | default([]) }}"
stopped_hosts: "{{ groups['proxmox_all_stopped'] | default([]) }}"
stopped_lxc: "{{ groups['proxmox_all_lxc'] | default([]) }}"
stopped_qemu: "{{ groups['proxmox_all_qemu'] | default([]) }}"
tasks:
- name: Add initially stopped autopatch LXC hosts
ansible.builtin.add_host:
name: "{{ item }}"
groups:
- patchable_stopped
- patchable_stopped_lxc
loop: "{{ autopatch_hosts | intersect(stopped_hosts) | intersect(stopped_lxc) | unique }}"
- name: Add initially stopped autopatch QEMU hosts
ansible.builtin.add_host:
name: "{{ item }}"
groups:
- patchable_stopped
- patchable_stopped_qemu
loop: "{{ autopatch_hosts | intersect(stopped_hosts) | intersect(stopped_qemu) | unique }}"
- name: Start initially stopped LXC guests
hosts: patchable_stopped_lxc
gather_facts: false
serial: 1
tasks:
- name: Start LXC
delegate_to: localhost
community.proxmox.proxmox:
api_host: "{{ proxmox_api_host }}"
api_user: "{{ proxmox_api_user }}"
api_token_id: "{{ proxmox_api_token_id }}"
api_token_secret: "{{ proxmox_api_token_secret }}"
validate_certs: "{{ proxmox_validate_certs }}"
vmid: "{{ proxmox_vmid }}"
node: "{{ proxmox_node }}"
state: started
- name: Start initially stopped QEMU guests
hosts: patchable_stopped_qemu
gather_facts: false
serial: 1
tasks:
- name: Start QEMU VM
delegate_to: localhost
community.proxmox.proxmox_kvm:
api_host: "{{ proxmox_api_host }}"
api_user: "{{ proxmox_api_user }}"
api_token_id: "{{ proxmox_api_token_id }}"
api_token_secret: "{{ proxmox_api_token_secret }}"
validate_certs: "{{ proxmox_validate_certs }}"
vmid: "{{ proxmox_vmid }}"
node: "{{ proxmox_node }}"
state: started
- name: Refresh inventory after starting guests
hosts: localhost
gather_facts: false
tasks:
- ansible.builtin.meta: refresh_inventory
- name: Wait for SSH port and refresh host keys for initially stopped guests
hosts: patchable_stopped
gather_facts: false
serial: 1
tasks:
- name: Wait for TCP/22 to open
delegate_to: localhost
ansible.builtin.wait_for:
host: "{{ ansible_host }}"
port: 22
timeout: 900
sleep: 5
delay: 2
- name: Remove old host key from controller known_hosts
delegate_to: localhost
become: false
ansible.builtin.command:
cmd: "ssh-keygen -R {{ ansible_host }}"
changed_when: false
failed_when: false
- name: Add current host key to controller known_hosts
delegate_to: localhost
become: false
ansible.builtin.shell: >
ssh-keyscan -H {{ ansible_host }} >> {{ lookup('env', 'HOME') }}/.ssh/known_hosts
args:
executable: /bin/bash
changed_when: false
- name: Wait for SSH login to become ready
ansible.builtin.wait_for_connection:
timeout: 300
sleep: 5
- name: Patch guests that were initially stopped
hosts: patchable_stopped
gather_facts: true
become: true
serial: 1
max_fail_percentage: 20
tasks:
- name: Debian/Ubuntu | refresh apt cache
ansible.builtin.apt:
update_cache: true
cache_valid_time: 3600
update_cache_retries: 5
update_cache_retry_max_delay: 12
lock_timeout: 300
when: ansible_facts['os_family'] == 'Debian'
- name: Debian/Ubuntu | dist-upgrade
ansible.builtin.apt:
upgrade: dist
autoremove: true
clean: true
dpkg_options: "force-confdef,force-confold"
lock_timeout: 300
when: ansible_facts['os_family'] == 'Debian'
- name: Debian/Ubuntu | reboot required marker
ansible.builtin.stat:
path: /var/run/reboot-required
when: ansible_facts['os_family'] == 'Debian'
register: deb_reboot_required
- name: Fedora/RHEL | upgrade installed packages
ansible.builtin.dnf:
name: "*"
state: latest
update_only: true
update_cache: true
when: ansible_facts['os_family'] == 'RedHat'
- name: openSUSE | dist-upgrade
community.general.zypper:
name: "*"
state: dist-upgrade
update_cache: true
when: ansible_facts['os_family'] == 'Suse'
- name: Print reboot hint for Debian/Ubuntu
ansible.builtin.debug:
msg: "reboot required on {{ inventory_hostname }}"
when:
- ansible_facts['os_family'] == 'Debian'
- deb_reboot_required.stat.exists | default(false)
- name: Stop LXC that were initially stopped
hosts: patchable_stopped_lxc
gather_facts: false
serial: 1
tasks:
- name: Stop LXC
delegate_to: localhost
community.proxmox.proxmox:
api_host: "{{ proxmox_api_host }}"
api_user: "{{ proxmox_api_user }}"
api_token_id: "{{ proxmox_api_token_id }}"
api_token_secret: "{{ proxmox_api_token_secret }}"
validate_certs: "{{ proxmox_validate_certs }}"
vmid: "{{ proxmox_vmid }}"
node: "{{ proxmox_node }}"
state: stopped
- name: Stop QEMU VMs that were initially stopped
hosts: patchable_stopped_qemu
gather_facts: false
serial: 1
tasks:
- name: Stop QEMU VM
delegate_to: localhost
community.proxmox.proxmox_kvm:
api_host: "{{ proxmox_api_host }}"
api_user: "{{ proxmox_api_user }}"
api_token_id: "{{ proxmox_api_token_id }}"
api_token_secret: "{{ proxmox_api_token_secret }}"
validate_certs: "{{ proxmox_validate_certs }}"
vmid: "{{ proxmox_vmid }}"
node: "{{ proxmox_node }}"
state: stopped
这个的缺点是,无法预先 --check,因为等待连接和开关机这种不支持 check mode。如果真的要测试,最好的办法是真的用 --limit 'localhost:<MachineName>' 只跑一台机器,而不是批量所有都直接开跑。
这里,我们排除了 tag_controller,因为我们不想升级海外的主控 VPS。同样的,在海外主控 VPS 上,我们也是只跑这份 playbook。
# /srv/infra-patch/playbooks/patch_vps.yml
- name: Patch overseas VPS
hosts: "tag_autopatch:&vps:!tag_controller"
serial: 1
gather_facts: true
become: true
max_fail_percentage: 20
tasks:
- name: Debian/Ubuntu | refresh apt cache
ansible.builtin.apt:
update_cache: true
cache_valid_time: 3600
update_cache_retries: 5
update_cache_retry_max_delay: 12
lock_timeout: 300
when: ansible_facts['os_family'] == 'Debian'
- name: Debian/Ubuntu | dist-upgrade
ansible.builtin.apt:
upgrade: dist
autoremove: true
clean: true
dpkg_options: "force-confdef,force-confold"
lock_timeout: 300
when: ansible_facts['os_family'] == 'Debian'
- name: Reboot required marker
ansible.builtin.stat:
path: /var/run/reboot-required
when: ansible_facts['os_family'] == 'Debian'
register: deb_reboot_required
- name: Print reboot hint
ansible.builtin.debug:
msg: "reboot required on {{ inventory_hostname }}"
when:
- ansible_facts['os_family'] == 'Debian'
- deb_reboot_required.stat.exists | default(false)
从 homelab 升级海外主控 VPS 的时候,只需要选出来 tag_controller:&vps 即可。
# /srv/infra-patch/playbooks/patch_vps_controllers.yml
- name: Patch external controller VPS
hosts: "tag_controller:&vps"
serial: 1
gather_facts: true
become: true
max_fail_percentage: 20
tasks:
- name: Debian/Ubuntu | refresh apt cache
ansible.builtin.apt:
update_cache: true
cache_valid_time: 3600
update_cache_retries: 5
update_cache_retry_max_delay: 12
lock_timeout: 300
when: ansible_facts['os_family'] == 'Debian'
- name: Debian/Ubuntu | dist-upgrade
ansible.builtin.apt:
upgrade: dist
autoremove: true
clean: true
dpkg_options: "force-confdef,force-confold"
lock_timeout: 300
when: ansible_facts['os_family'] == 'Debian'
- name: Reboot required marker
ansible.builtin.stat:
path: /var/run/reboot-required
when: ansible_facts['os_family'] == 'Debian'
register: deb_reboot_required
- name: Show reboot hint
ansible.builtin.debug:
msg: "reboot required on {{ inventory_hostname }}"
when:
- ansible_facts['os_family'] == 'Debian'
- deb_reboot_required.stat.exists | default(false)
确实可以更方便。因为每想执行一次升级,我都要这样:
cd /srv/infra-patch/
ansible-playbook playbooks/patch_running.yml -i inventory/proxmox.proxmox.yml
更懒的做法,当然就是写个脚本,然后放到家目录下。
其中,我只把更新已停止服务的默认改成了 run,其它的默认都是 check。最后就可以直接这个样子:
# 先检查配置文件有没有问题,测试升级是什么样子
~/bin/run-proxmox-running-updates check
# 再开始正式跑
~/bin/run-proxmox-running-updates run
# 通常既然都关机了,那么可能是不重要的服务
# 或者节省资源才关的,不太需要所有已关闭的都要去维护
# 所以通常我用它单个来跑
~/bin/run-proxmox-stopped-updates run --limit 'localhost:bangumi'
# 同样的,先检查配置文件有没有问题,测试升级是什么样子
~/bin/run-vps-updates check
# 在开始正式跑
~/bin/run-vps-updates run
#!/usr/bin/env bash
set -euo pipefail
BASE="/srv/infra-patch"
PLAYBOOK="$BASE/playbooks/patch_vps.yml"
INVENTORY="$BASE/inventory/vps.yml"
LOGDIR="$HOME/logs/ansible"
LOCKFILE="/tmp/run-vps-updates.lock"
mkdir -p "$LOGDIR"
MODE="${1:-check}"
shift || true
case "$MODE" in
check)
EXTRA_ARGS=(--check --diff)
;;
run)
EXTRA_ARGS=()
;;
*)
echo "Usage: $0 [check|run] [extra ansible-playbook args...]"
exit 1
;;
esac
export ANSIBLE_CONFIG="$BASE/ansible.cfg"
export ANSIBLE_FORCE_COLOR=1
export PY_COLORS=1
unset NO_COLOR
cd "$BASE"
exec flock -n "$LOCKFILE" \
ansible-playbook "$PLAYBOOK" -i "$INVENTORY" "${EXTRA_ARGS[@]}" "$@" \
2>&1 | tee -a "$LOGDIR/vps-$(date +%F).log"
#!/usr/bin/env bash
set -euo pipefail
BASE="/srv/infra-patch"
PLAYBOOK="$BASE/playbooks/patch_running.yml"
INVENTORY="$BASE/inventory/proxmox.proxmox.yml"
LOGDIR="$HOME/logs/ansible"
LOCKFILE="/tmp/run-proxmox-running-updates.lock"
mkdir -p "$LOGDIR"
MODE="${1:-check}"
shift || true
case "$MODE" in
check)
EXTRA_ARGS=(--check --diff)
;;
run)
EXTRA_ARGS=()
;;
*)
echo "Usage: $0 [check|run] [extra ansible-playbook args...]"
exit 1
;;
esac
export ANSIBLE_CONFIG="$BASE/ansible.cfg"
export ANSIBLE_FORCE_COLOR=1
export PY_COLORS=1
unset NO_COLOR
cd "$BASE"
exec flock -n "$LOCKFILE" \
ansible-playbook "$PLAYBOOK" -i "$INVENTORY" "${EXTRA_ARGS[@]}" "$@" \
2>&1 | tee -a "$LOGDIR/proxmox-running-$(date +%F).log"
#!/usr/bin/env bash
set -euo pipefail
BASE="/srv/infra-patch"
PLAYBOOK="$BASE/playbooks/patch_stopped.yml"
INVENTORY="$BASE/inventory/proxmox.proxmox.yml"
LOGDIR="$HOME/logs/ansible"
LOCKFILE="/tmp/run-proxmox-stopped-updates.lock"
mkdir -p "$LOGDIR"
MODE="${1:-run}"
shift || true
case "$MODE" in
run)
EXTRA_ARGS=()
;;
*)
echo "Usage: $0 run [extra ansible-playbook args...]"
exit 1
;;
esac
export ANSIBLE_CONFIG="$BASE/ansible.cfg"
export ANSIBLE_FORCE_COLOR=1
export PY_COLORS=1
unset NO_COLOR
cd "$BASE"
exec flock -n "$LOCKFILE" \
ansible-playbook "$PLAYBOOK" -i "$INVENTORY" "${EXTRA_ARGS[@]}" "$@" \
2>&1 | tee -a "$LOGDIR/proxmox-stopped-$(date +%F).log"
大功告成。只需要开个 mosh 然后挂后台让它自己升级,剩下的时间就是自己的了。
如果喜欢本文,欢迎点击下方的「鼓掌」按钮!
<noframes>This browser has disabled frames.</noframes>如果上面没有加载出任何东西,可以点击这里。
2025-12-31 15:44:46
又是保守文化盛行的一年,如何在夹缝中坚信自己认为正确的东西呢?
差不多一年前写下了 2024 的总结,是时候写 2025 年的总结了!
今年也是压力非常高的一年,也是没有存到一分钱,甚至贷款问题也没解决,要不是能重新协商就变成失信人了。
规则:可以是二级或者三级域名,不能是内网域名,不能是我自己的站,主域名不能重复,如果常用多个浏览器的结果冲突了,选择自己最经常去的
虽然后面变成了周记,但是能坚持下来一年都写日记的感觉真好。
今年没有。
如果喜欢本文,欢迎点击下方的「鼓掌」按钮!
<noframes>This browser has disabled frames.</noframes>如果上面没有加载出任何东西,可以点击这里。
2025-10-04 18:23:28
困扰我很久的问题之一。
在中国大陆一个非常令人困扰的问题就是,不少程序检查更新的时候,基本都会访问海外的网站,但海外的网站从中国大陆访问又很慢。虽然不少都可以通过设置 http_proxy 和 https_proxy 的方法来解决,但 Home Assistant 是一个例外。它后台没有任何设置 proxy 的入口,并且本身命令行也是不支持 proxy 设置的。就算我安装了 Terminal 之后,在里面 export http_proxy & https_proxy 然后运行 ha os update 也是不走代理的。那该怎么办?
在这里,我在局域网下(或者公网)开了一个临时 HTTP 服务,然后将下载好的 raucb 文件放上去了。你可能会问,raucb 文件在哪里下载?在这里。虽然我是在 Proxmox 上的 amd64 QEMU 环境内安装的,但因为是一次升级,所以我可以直接选择
haos_ova-x.x.raucb,其中 x.x 为最新的版本号。
之后,我们去 Home Assistant 的命令行(不是 Terminal 插件),输入 login 进入真正的 root shell。接着,我们使用 curl 来从局域网内下载需要用到的 raucb 文件:
cd /mnt/data
curl http://example.com/haos_ova-16.2.raucb
rauc install haos_ova-16.2.raucb
systemctl reboot
大功告成!升级完之后,记得去 /mnt/data 删除你的 raucb 文件。
注意,在升级之前一定要通过 rauc status 确保你当前使用的是 [kernel.0],否则可能会报 Copying image to boot.0 failed. 这种错误,如果遇到这种错误,通常执行重启即可。
如果喜欢本文,欢迎点击下方的「鼓掌」按钮!
<noframes>This browser has disabled frames.</noframes>如果上面没有加载出任何东西,可以点击这里。
2025-03-20 23:12:03
多图预警。
上一篇在这里。
在日本的第三天,准备去丰桥了。我们选择租车是因为我们要拖着各自比较重又大的行李箱,上 JR 的话很不方便,而且开车的话可以到一些例如要等很久巴士的地方。在这里真的是十分感谢!
出了酒店之后,大概从秋叶原出发,就上首都高了。同行的朋友说,首都高之前是一条河,然后抽干编程的高速公路,并且三车道的高速公路在日本已经是豪华配置了,一般都是两条甚至一条车道的。也就是说,如果只有一条车道并且那个人开得很慢的话,后面所有人都要慢下来。
还有一点就是,白天开车的一般都很文明,但晚上就变得野蛮起来了(指超速)。

不过虽然叫首都高,但是在东京市区内就跟国内的环线没啥区别,毕竟成天都在堵车。

开了一段时间终于出城区了,出了城区之后就基本上看不到高楼大厦了。在海老名服务区适当休息了一下之后,继续往丰桥方向前进。

开了一会之后,已经可以能很显眼的看到富士山了。

然后又开了一段时间,在日本平服务区休息了一下。顺便在服务区看到了富士山 chiikawa。

然后继续前行,差不多到中午的时候,在浜名湖服务区下了车吃中午饭。

顺便这个服务区里面,有一个比较大的公园,名字叫做芝生公园。天气好的时候,你可以从这里看到整个浜名湖。

在这里还有一个叫做 “恋人の聖地” 的打卡地,旁边有个自动售卖机在卖可以锁在栏杆上的牌子。这个钟也是可以敲响的(不是左右晃动线,是拉下面那根线弄响)。

顺便在这个服务区还用 Apple 轻应用点了两杯星巴克,体验非常不错。不需要登录注册账号以及获取私人信息,并且直接下单之后用 Apple Pay 支付即可。
唯一缺点就是,需要登录日本 Apple ID 扫码才可以,如果登录的非日本 Apple ID 则会显示错误。
开到大概下午 2 点的时候到了丰桥,因为规划的行程是今天打算先逛完郊区的综合动植物公园和地下资源馆,明天再去市内的地方打卡。

不过丰桥的乡下也过于乡下了吧,或者说我来的时间不是什么高峰期,综合动植物公园西门的这边真的是一点车都没有。
从这里开始,后面的图如果是打卡的话,都会使用巡礼对比图生成器生成。

到达的当天丰桥依然在刮很大的风,也没有多少人去这里观光。门口有门票的自动售卖机,大人票 600 JPY,儿童票 100 JPY。我们去的时候不收新日元纸币。
每天营业时间为上午 9 点到下午 4 点 30 分,最后入场时间为下午 4 点。周一,法定节假日和 12 月 29 日到 1 月 1 日闭园。
综合动植物公园里面分为了 4 个部分:动物园、植物园、游乐场和自然史博物馆。

买了票过了检票口之后意外的看到了动物朋友的贴纸,可能之前有过联动?

进来之后首先往动物园的方向走。今天入园的客人看起来也很少,只有零星的人在这里。连鹿乃子都在睡觉!

在猴子园区旁发现了一个非常有意思的人造景,看似是为了庆祝丰桥建市 100 周年而设立的。丰桥市是于明治 39 年(1906 年)8 月 1 日实施市制,是日本全国第 62 个(爱知县继名古屋市之后的第 2 个)设市的城市。

这座于 2005 年落成的壁画上,印有从 1905 年到 2005 年间,每一年出生的人的手掌印。


随后乱转了一会发现因为下午 4:30 关门,而正好要去的地下资源馆也是差不多那个点关门,所以走马观花的看了一下之后放弃了逛其它动物园的想法,马不停蹄的先往自然史博物馆走。

自然史博物馆入场肯定是免费的,但里面有观影的地方,那个地方是单独收费的。


这张大概是跟原作最还原的一张了:


随后出来之后看到了小吃摊,其中远处的这家是实际存在的小吃摊千寿,我居然在这里忽略了始祖鸟的那个雕像,太可惜了。

通向温室的那边途中有一个瞭望塔,但赶时间也没有进去拍。

随后先到游乐园看了一下,大概没有人的原因所以基本所有的娱乐设施都是关闭的状态(除了摩天轮)。





游乐园入口的对面就是大花坛。





随后就是植物园和温室了,但实在没有太多时间了只好作罢。


走完温室走廊之后,就是东门了。东门的旁边有一个小的纪念品店。



随后去了丰桥市地下资源馆,它的旁边是丰桥市视听教育中心,正好一并打卡。

首先是丰桥市视听教育中心,进去之后就可以看到败犬女主的女主牌子!

旁边的告示板上也有角色的磁吸贴。


虽然有开放,但是找不到主角们使用的那些设施。可能是收起来了,或者上锁的地方其实蛮多的,在进不去的地方展示?



地下资源馆的门口跟视听教育的门口是挨着的,所以参观很方便。

莫名其妙这里有一只小熊维尼,不过动画里面没有画的很像。


下来之后,这里专门有一个小地方跟败犬女主联动,还布置的挺不错的,电视循环播放的是动画化的 CM。还专门有一面墙贴了地下资源馆在动画内出场的场景,方便巡礼的人打卡?





猜猜这面旗子的旁边是什么?

没错,就是这把用矿石做的椅子。坐上去的感觉还是比较奇特的。



有一个主角们玩答题的地方,可惜这个机器在维修中。

最后让我感觉比较有意思的是这张 2009 年 7 月 1 日的丰桥市卫星影像,1 张卖 1100 日元,可惜窗口没有人。

逛了一圈正好压在闭园时间前不久出来。出来的时候,虽然是晴天但突然开始飞毛毛雪了,真的是冷呀。在地下资源馆下坡的地方有一条小路,可以从这里走到岩屋公园口公交站的站台(需要在前面一点过马路)。


顺带一提这里的巴士是一小时一班。我在想,如果我坐巴士来这里的话估计要等好久的车。这大概就是乡下的坏处了,感觉哪里都是乡下公共交通不是很方便的问题。后来听了同行的在日小留吐槽说,日本乡下就跟美国乡下差不多,一定是要开车的。顺便他还吐槽日本政府的办事效率远低于中国政府,比如中国现在一些东西可以直接网上办理,或者在政务中心当天就能办结,而日本就要等好几天。

之后因为天还没黑,去酒店太早,所以我们便去往田原市的白谷海滨公园,希望能拍到个太阳下山的景色。在路途中拍到了一个非常奇特的自然现象,但我也解释不了,希望有人能知道这是什么吧。

半个小时后,终于在天泛红光的时候到了白谷海滨公园,我知道会刮海风,但我完全不知道能刮这么强的海风(阵风 20m/s)。还好我这次来日本玩带了风衣,并且有买手套。


虽然这是一个很大的公园,但今天并没有人,甚至硕大的停车场除了我们只有另外一辆车。

我们到的时候公园的楼已经休馆了,所以只能在外面走走。景色是真的好,但是风也是真的大。

从图上还看不出?直接看我在海边拍的影片吧。Insta 360 云台拍摄的时候自动加了降噪,但依然你能听到风声。顺带一提,我在拍的时候甚至风都能控制云台,真的恐怖啊。



在我尝试大概对着动画截图取景拍照的时候,同行的朋友早已经回车上取暖了。可能大家看着照片很亮,实际上一点也不——

顺带一提,我在拍照的时候有人居然在穿超轻便的运动装跑步,还有个大妈在遛狗……
对了,如果有其他小伙伴想来的话,切记这里不能 BBQ 和露营,以及记得带走自己的垃圾(因为日本公共场所没有垃圾桶)!

最终我也顶不住大风了,回车上了。在回到丰桥之前,我们顺路去了蔵王山展望台。去的路程是个弯弯的山路,而且越往山上走风越大。
开到展望台门口的停车场,意外的发现有几辆车还在这里。正面忘记拍了,不过上去之后灯光打在地上的这个灯光效果让我觉得眼前一亮。展望台本身虽然还可以进去,因为 10 点才关门,但是里面的商户早已经拉闸休息了,只剩下了自动贩卖机。


4F 就是可以看 360° 的展望台,但是可惜晚上了拍照效果并不是很好。

从展望台门口下去的另一条道上也是类似门口的风格,十分漂亮。

拿着我的 fubuki 破娃娃拍一张,风太大了根本不好拿,而且光线很暗,还是朋友打着手电筒拍的。虽然我是 35p 但很可惜当时我没多买一个 35 的。

之后我们便下山去找吃晚饭的地方了。最终暂时脱离爱知县,来到的是静冈县的さわやか 湖西浜名湖店。さわやか是只在静冈县才有的连锁店,出名的是它的炭烤汉堡肉,而且还比较便宜。这家店每周星期四休息。

我们各点了猪肉和牛肉的汉堡排。比较有意思的是,他们会先给你一个宣传单,这个宣传单后面等汉堡排上来的时候(这个时候超级烫!)用来挡住汉堡排溅出来的油。等一段时间汉堡排在你面前完全好了之后,店员会帮你切开,并会帮你淋酱汁。


总之非常好吃就对了,我还点了一份饭,非常下饭。吃完饭之后已经 8 点了,遂往今天的酒店前进。今天入住的酒店 HOTEL ASSOCIA TOYOHASHI 正好就在丰桥站内。非常好的是,丰桥站的旁边就是 JRF 豊橋駅前駐車場,所以开车过来非常方便。
停好车之后坐电梯下来就可以看到去丰桥站的引导路标。

穿过去之后,可以发现丰桥站周边建设的非常漂亮,有模有样的。

我们的酒店需要从丰桥站建筑内部进入,但这个建筑除了 JR 丰桥站之外还有商业店铺。从门口进去就可以看到败犬女主的海报。

酒店 check-in 之后,意外的发现除了能收 NHK World 台之外,还能收中天亚洲台……

之后想先出去转转,就先去了丰桥站门口的平台上。凑巧碰到了设置了男主和女主的灯箱,但这个灯箱据说只展示到今年的 2 月 14 日。




站外平台的灯光设置还是非常漂亮的。

从这里可以一眼望到 Rainbow Tower,也能看到旁边的 USA 飞船。这两者都是在《我们都是超能力者!》里面出现过的,只不过我忘记分别是多少集的时候了,但至少出现了好多次。

当然没有忘记拍夜间的 JR 丰桥站!

之后,沿着过街天桥走了一段路,想看看晚上 9 点半的丰桥市有没有什么夜生活呢?剧透:丰桥市作为乡下我还是想多了。
到了ときわ通り商业街,这个商业街也在《我们都是超能力者!》里面出现过,但目前上面挂的海报全是败犬女主的了。




商业街也基本只有酒馆在营业,出去之后发现基本上也只有酒馆在营业,甚至还有人在拉客。不过意外的是看到了一些外国人在门口聊天,估计是刚喝酒出来的?剩下的商铺都关门了,街上空空荡荡的。不愧是乡下!

偶然走到了精文館書店 豊橋本店的门口,打算明天一早过来看看。

以及看到了河南人绝对回来拾的好看东西:

回到站内,突发奇想说想逛逛 JR 入口的地方,结果真的有一个意外收获,为了让圣地巡礼的人找到动画里出现的景都在哪居然做了一个信息栏,这是我没想到的。

旁边则是丰桥鬼祭的宣传,这个应该是赤鬼?

顺带一提,丰桥市早在 1987 年就跟中国的南通市结为国际友好城市了,在 JR 入口的这个地板上也有表示。

差不多 9 点半快 10 点了,打算回酒店休息了。最后给铁道宅们一个 JR 东海铁道路线图来结束这次的日本游记。

P.S.本来从 2 月 27 就开始写了,写到今天才写完。我 Vol. 2 咕了一个月,那后面 Vol. 3 是不是要咕两个月????
如果喜欢本文,欢迎点击下方的「鼓掌」按钮!
<noframes>This browser has disabled frames.</noframes>如果上面没有加载出任何东西,可以点击这里。
2025-03-02 00:27:00
非常不错的产品。
其实并没有打算买任何 KVM 产品,直到看到 Jeff Geering 和 Wendell 的评测影片之后,突然我就有了非常浓厚的兴趣。虽然这是一个 Kickstarter 上的众筹产品,但我依然想购买。
进入宣传页面,感觉它不仅好看,而且还十分小巧。发现它还要做扩展板子,比如 ATX 电源板子和 DC 电源板子等等,遂下单的时候一起下单了这两个。虽然这个时候还没有想到要怎么用,但交给以后的我吧。
这也是我第一次正儿八经买 Kickstarter 上的东西(虽然这是我第二次购买,第一次是 VisionFive 2,但充其量那只是厂商的销售手段)。12 月买的,看他们公告说会 1 月发货,但 1 月都快月底了还没发。而不少 1 月的人都已经拿到本体开始玩了。
我感觉官方的发货策略是,不会按照先后顺序(也就是 Backing Details 里面看到的那个 Backer Number)来发货,而是按照迷之顺序发货的。因为都到 2 月中旬了我也没收到货,而正好官方当时又发了一个公告,说 12 月买的人他们已经发了 94% 了,剩下 6 % 会在月底发完,而我正好就是这 6% 里面的。不过还好,2 月 24 号终于收到了即将发货的邮件,2 月 27 日收到了顺丰特快包裹。不过他们的邮费收了我 5 USD,但实际上顺丰快递只花了 26 CNY。
虽然他们在开始发货的时候说会将扩展板子单独发货,但我除了本体之外一并收到了 ATX 电源板子。
正好最近我组了一台测试机,但这台测试机因为是个人 PC 用主板肯定是没有 IPMI 功能的,又因为是 AMD 所以也不可能用 Intel vPro 这种高端功能,所以正好用上了这个 JetKVM。
开箱就不开了,有不少人都放过开箱了。而且我也没拍。
第一印象就是非常好看,放在机箱上完全没有违和感。
这个屏幕包含了 JetKVM 本体的 IP 地址和 MAC 地址,并且还有 USB 和 HDMI 是否连接的提示,已经非常足够了。长按屏幕的话还可以看当前软件的版本等等。
在进行非常简单的初始化(也就是设置是否进入 KVM 网页界面需要密码)之后,就进入了后台。

这个页面也是非常好看的,而且 JetKVM 承诺产品出货不久后开源也开源了。在网页上用鼠标操作也是非常丝滑,我甚至在我用 Moonlight 远程连接这台电脑打游戏并在旁边开启 JetKVM 的屏幕看是否流畅,结果当然是非常的流畅。唯一不足的是目前的分辨率只能到 1080p60,而我的显示屏幕是 1440p 的,所以 Moonlight 串流的时候也会选择 1080p,导致 1440p 下面的显示略模糊。
不过在我成功 Setup 之后第一个问题就是当时已经有比较新的系统版本了,但我出厂的这个系统版本比较老。而出于某种原因,点击面板上的系统更新会因为网络原因失败,所以我便加入他们的 Discord 群组看看有没有人遇到跟我一样的问题,果然有。万幸的是,JetKVM 支持离线升级:
# 目前最新的版本是 0.2.3
# 首先在你自己的电脑上获取系统压缩包
wget https://update.jetkvm.com/system/0.2.3/system.tar
# 然后,在面板上打开 Developer Mode 并添加你的 SSH Public Key,随后尝试用 root 帐号登录一次。如果没有问题的话那么可以继续执行以下操作
# 将 YOUR_KEY KVM_IP 替换成你 JetKVM 的 IP
cat system.tar | ssh -o PreferredAuthentications=publickey -l root -i YOUR_KEY KVM_IP "cat > /userdata/jetkvm/update_system.tar"
# 随后,SSH 进入 JetKVM 并手动运行升级指令。
rk_ota --misc=update --tar_path=/userdata/jetkvm/update_system.tar --save_dir=/userdata/jetkvm/ota_save --partition=all
# 在升级完成之后等一会然后断电再重新上电即可。
之后愉快的使用了一两天,然后我发现了一个我环境下的问题。因为目前这台测试机器跟 JetKVM 是在不同的 VLAN 下,这就导致 JetKVM 无法发送 Wake On LAN 包给测试机器,此时我就把目光放到了一并发来的 ATX 电源扩展板上。
官方发送了一个裸板子过来,但贴心的送了一个半高和全高的 PCIE 挡板,可以在不同的环境下使用。安装完成之后,我发现后面有两对口。百思不得其解的我查询了官方文档之后才恍然大悟——因为如果 JetKVM 直接连接到主板上的话,前置的电源和重置按钮就无法使用了。所以给了两对口,一对是连接到主板上的,另一对是跟前置的电源和重置按钮等等连接的,这样就可以一并通过 JetKVM 和前挡板上的按钮控制电脑了。
但机箱提供的接口不能拆出来(见下图的右下角),还好之前买多了公对母口的杜邦线,这时候派上了用场,看着扩展板和主板的说明直接进行连接后就完事了。最后发现这个线会打到显卡的风扇,所以用了简单暴力的方法尽量让线在机箱底部走。

之后重新上电,首先测试了一下电源,发现可以使用,遂强制关机之后又在 JetKVM 内启动机器,发现也是能正常工作的,并且指示灯都可以正常显示状态。完美!

这大概是我近些年来感觉买的最值的一个产品了吧。BTW,JetKVM 官方说了后续会上 Amazon 等通贩平台,所以想购买的可以期待一下。不过我想,因为发货是深圳,所以国内的淘宝肯定也会上架的。
如果喜欢本文,欢迎点击下方的「鼓掌」按钮!
<noframes>This browser has disabled frames.</noframes>如果上面没有加载出任何东西,可以点击这里。