超大量更新
This commit is contained in:
271
20-物理服务器虚拟机/1-2026年3月25日-安装.md
Normal file
271
20-物理服务器虚拟机/1-2026年3月25日-安装.md
Normal file
@@ -0,0 +1,271 @@
|
||||
关键发现:`collectd` 不在 EPEL,而在 **CentOS Stream 9 OpsTools SIG** 仓库中 。oVirt 完整安装需要 **10+ 个 CentOS SIG 仓库**,以下给出从头开始的完整操作。 [computingforgeeks](https://computingforgeeks.com/how-to-install-ovirt-engine-on-centos-stream/)
|
||||
|
||||
***
|
||||
|
||||
## 第一步:修复 OpenEuler dnf 兼容性
|
||||
|
||||
```bash
|
||||
# 解决 "Detection of Platform Module failed" 问题
|
||||
echo "module_platform_id=platform:el9" >> /etc/dnf/dnf.conf
|
||||
|
||||
# 验证
|
||||
grep module_platform_id /etc/dnf/dnf.conf
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## 第二步:清理之前添加的错误仓库
|
||||
|
||||
```bash
|
||||
# 删除上次添加的错误仓库
|
||||
rm -f /etc/yum.repos.d/ovirt-master.repo
|
||||
rm -f /etc/yum.repos.d/ovirt-4.5.repo
|
||||
rm -f /etc/yum.repos.d/epel9-manual.repo
|
||||
|
||||
dnf clean all
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## 第三步:添加全套依赖仓库(全部使用国内镜像)
|
||||
|
||||
```bash
|
||||
|
||||
# 直接覆盖写入正确的 SIG 仓库文件
|
||||
cat > /etc/yum.repos.d/centos9-stream-sigs.repo << 'EOF'
|
||||
[c9s-ovirt45]
|
||||
name=CentOS Stream 9 - oVirt 4.5 SIG
|
||||
baseurl=https://mirrors.tuna.tsinghua.edu.cn/centos-stream/SIGs/9-stream/virt/x86_64/ovirt-45/
|
||||
enabled=1
|
||||
gpgcheck=0
|
||||
|
||||
[c9s-opstools]
|
||||
name=CentOS Stream 9 - OpsTools SIG (collectd)
|
||||
baseurl=https://mirrors.tuna.tsinghua.edu.cn/centos-stream/SIGs/9-stream/opstools/x86_64/collectd-5/
|
||||
enabled=1
|
||||
gpgcheck=0
|
||||
|
||||
[c9s-openstack-yoga]
|
||||
name=CentOS Stream 9 - OpenStack Yoga SIG
|
||||
baseurl=https://mirrors.tuna.tsinghua.edu.cn/centos-stream/SIGs/9-stream/cloud/x86_64/openstack-yoga/
|
||||
enabled=1
|
||||
gpgcheck=0
|
||||
|
||||
[c9s-rabbitmq]
|
||||
name=CentOS Stream 9 - RabbitMQ 38 SIG
|
||||
baseurl=https://mirrors.tuna.tsinghua.edu.cn/centos-stream/SIGs/9-stream/messaging/x86_64/rabbitmq-38/
|
||||
enabled=1
|
||||
gpgcheck=0
|
||||
|
||||
[c9s-nfv-openvswitch]
|
||||
name=CentOS Stream 9 - NFV OpenvSwitch SIG
|
||||
baseurl=https://mirrors.tuna.tsinghua.edu.cn/centos-stream/SIGs/9/nfv/x86_64/openvswitch-common/
|
||||
enabled=1
|
||||
gpgcheck=0
|
||||
|
||||
[c9s-ceph-pacific]
|
||||
name=CentOS Stream 9 - Ceph Pacific SIG
|
||||
baseurl=https://mirrors.tuna.tsinghua.edu.cn/centos-stream/SIGs/9-stream/storage/x86_64/ceph-pacific/
|
||||
enabled=1
|
||||
gpgcheck=0
|
||||
|
||||
[c9s-gluster10]
|
||||
name=CentOS Stream 9 - Gluster 10 SIG
|
||||
baseurl=https://mirrors.tuna.tsinghua.edu.cn/centos-stream/SIGs/9-stream/storage/x86_64/gluster-10/
|
||||
enabled=1
|
||||
gpgcheck=0
|
||||
|
||||
[c9s-extras]
|
||||
name=CentOS Stream 9 - Extras
|
||||
baseurl=https://mirrors.aliyun.com/centos-stream/SIGs/9-stream/extras/x86_64/extras-common/
|
||||
enabled=1
|
||||
gpgcheck=0
|
||||
EOF
|
||||
|
||||
# 一次性写入所有必需仓库文件
|
||||
cat > /etc/yum.repos.d/centos9-stream-base.repo << 'EOF'
|
||||
[c9s-baseos]
|
||||
name=CentOS Stream 9 - BaseOS
|
||||
baseurl=https://mirrors.aliyun.com/centos-stream/9-stream/BaseOS/x86_64/os/
|
||||
enabled=1
|
||||
gpgcheck=0
|
||||
|
||||
[c9s-appstream]
|
||||
name=CentOS Stream 9 - AppStream
|
||||
baseurl=https://mirrors.aliyun.com/centos-stream/9-stream/AppStream/x86_64/os/
|
||||
enabled=1
|
||||
gpgcheck=0
|
||||
|
||||
[c9s-crb]
|
||||
name=CentOS Stream 9 - CRB
|
||||
baseurl=https://mirrors.aliyun.com/centos-stream/9-stream/CRB/x86_64/os/
|
||||
enabled=1
|
||||
gpgcheck=0
|
||||
EOF
|
||||
|
||||
cat > /etc/yum.repos.d/epel9.repo << 'EOF'
|
||||
[epel9]
|
||||
name=EPEL 9 - x86_64 (Aliyun Mirror)
|
||||
baseurl=https://mirrors.aliyun.com/epel/9/Everything/x86_64/
|
||||
enabled=1
|
||||
gpgcheck=0
|
||||
priority=99
|
||||
EOF
|
||||
|
||||
cat > /etc/yum.repos.d/ovirt-4.5-upstream.repo << 'EOF'
|
||||
[ovirt-4.5]
|
||||
name=oVirt 4.5 Upstream
|
||||
baseurl=https://resources.ovirt.org/pub/ovirt-4.5/rpm/el9/
|
||||
enabled=1
|
||||
gpgcheck=0
|
||||
EOF
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## 第四步:验证仓库并更新缓存
|
||||
|
||||
```bash
|
||||
dnf clean all && dnf makecache
|
||||
|
||||
# 验证关键包可以被找到
|
||||
dnf info collectd | grep -E "Name|Version|Repo"
|
||||
dnf info python3-os-brick | grep -E "Name|Version|Repo"
|
||||
dnf info ovirt-engine | grep -E "Name|Version|Repo"
|
||||
```
|
||||
|
||||
预期输出示例:
|
||||
```
|
||||
Name : collectd
|
||||
Version : 5.12.0
|
||||
Repository : c9s-opstools
|
||||
|
||||
Name : ovirt-engine
|
||||
Version : 4.5.7
|
||||
Repository : ovirt-4.5
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## 第五步:安装 oVirt Engine
|
||||
|
||||
```bash
|
||||
# 安装 oVirt Engine(忽略 OpenEuler 与 RHEL 的轻微包名差异)
|
||||
dnf install -y ovirt-engine \
|
||||
--setopt=module_platform_id=platform:el9 \
|
||||
--allowerasing
|
||||
|
||||
# 安装过程中如有提示 "is it ok [y/N]" 类问题,输入 y 确认
|
||||
```
|
||||
|
||||
> 安装过程约下载 **500MB~1GB** 的包,根据网速需要 5-15 分钟。
|
||||
|
||||
***
|
||||
|
||||
## 第六步:配置主机名(FQDN 必须可解析)
|
||||
|
||||
```bash
|
||||
# 设置 FQDN
|
||||
hostnamectl set-hostname ovirt.local.lan
|
||||
|
||||
# 添加本地解析
|
||||
echo "192.168.11.14 ovirt.local.lan ovirt" >> /etc/hosts
|
||||
|
||||
# 验证(必须返回 FQDN,不能是 localhost)
|
||||
hostname -f
|
||||
ping -c 2 $(hostname -f)
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## 第七步:运行配置向导
|
||||
|
||||
```bash
|
||||
engine-setup
|
||||
```
|
||||
|
||||
**交互配置参考(直接回车使用默认值即可):**
|
||||
|
||||
```
|
||||
Configure Engine on this host (Yes, No) [Yes]: ↵ 回车
|
||||
Configure Image I/O Proxy on this engine (Yes, No) [Yes]: ↵ 回车
|
||||
Configure WebSocket Proxy on this machine (Yes, No) [Yes]: ↵ 回车
|
||||
Configure Data Warehouse on this engine (Yes, No) [Yes]: ↵ 回车
|
||||
Application mode (both, virt, gluster) [both]: ↵ 回车
|
||||
Default SHE Storage Domain type (glusterfs, nfs) [nfs]: ↵ 回车
|
||||
|
||||
Engine database host [localhost]: ↵ 回车
|
||||
Engine database port [5432]: ↵ 回车
|
||||
Engine database name [engine]: ↵ 回车
|
||||
Engine database user [engine]: ↵ 回车
|
||||
Engine database password: 输入密码
|
||||
oVirt Engine FQDN [ovirt.local.lan]: ↵ 回车
|
||||
|
||||
Use default credentials (admin@internal) [Yes]: ↵ 回车
|
||||
Engine admin password: 输入Web登录密码
|
||||
|
||||
Firewall manager to configure (iptables, firewalld) [firewalld]: ↵ 回车
|
||||
|
||||
Confirm installation settings [OK]: ↵ 回车
|
||||
```
|
||||
|
||||
成功后输出:
|
||||
```
|
||||
--== SUMMARY ==--
|
||||
Web access is enabled at:
|
||||
https://ovirt.local.lan/ovirt-engine
|
||||
http://ovirt.local.lan/ovirt-engine
|
||||
Please use "admin" user to login
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## 第八步:安装 VDSM(宿主节点代理)
|
||||
|
||||
```bash
|
||||
dnf install -y ovirt-host \
|
||||
--setopt=module_platform_id=platform:el9 \
|
||||
--allowerasing
|
||||
|
||||
systemctl enable --now vdsmd supervdsmd
|
||||
|
||||
# 验证
|
||||
systemctl status vdsmd | grep -E "Active|Main"
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## 第九步:登录 Web Portal 添加宿主机
|
||||
|
||||
浏览器访问 `https://192.168.11.14/ovirt-engine`,用户名 `admin`,密码为 `engine-setup` 中设置的密码。
|
||||
|
||||
```
|
||||
计算 → 主机 → 新建
|
||||
名称: node-openeuler
|
||||
主机名: 192.168.11.14
|
||||
SSH端口: 22
|
||||
认证方式: 密码(填 root 密码)
|
||||
→ 确定
|
||||
|
||||
# 等待约 3-5 分钟,状态变为「已开机」即成功
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## 第十步:安装完成后清理临时仓库
|
||||
|
||||
```bash
|
||||
# 保留 ovirt-4.5-upstream(后续升级用),删除临时的 CentOS 依赖仓库
|
||||
rm -f /etc/yum.repos.d/centos9-stream-base.repo
|
||||
rm -f /etc/yum.repos.d/centos9-stream-sigs.repo
|
||||
rm -f /etc/yum.repos.d/epel9.repo
|
||||
|
||||
# 仅保留:
|
||||
ls /etc/yum.repos.d/
|
||||
# openEuler 原始仓库(不动)
|
||||
# ovirt-4.5-upstream.repo(保留,用于 oVirt 后续更新)
|
||||
|
||||
dnf clean all
|
||||
```
|
||||
|
||||
> **说明**:清理仓库后已安装的包不受影响,仅禁止后续从这些源拉取新包,符合"安装完成后去除"的要求。
|
||||
151
20-物理服务器虚拟机/1-2026年4月15日-管理方案/init-ip-change.ps1
Normal file
151
20-物理服务器虚拟机/1-2026年4月15日-管理方案/init-ip-change.ps1
Normal file
@@ -0,0 +1,151 @@
|
||||
# ================================
|
||||
# Windows 主机名 / IP 配置脚本
|
||||
# 使用方法:
|
||||
# 1. 先修改下面的配置区
|
||||
# 2. 用管理员权限运行 PowerShell
|
||||
# 3. 执行本脚本
|
||||
# 4. 执行完成后重启系统
|
||||
# ================================
|
||||
|
||||
# ===== 配置区开始 =====
|
||||
$NewComputerName = "ws2022-192-168-11-160"
|
||||
$IPAddress = "192.168.11.160"
|
||||
$PrefixLength = 24
|
||||
$Gateway = "192.168.11.1"
|
||||
$DnsServers = @("192.168.34.40","223.5.5.5")
|
||||
# 如需指定网卡名,可填写,例如 "以太网"
|
||||
# 留空则自动选择当前状态为 Up 的第一块物理/虚拟以太网卡
|
||||
$PreferredAdapterName = ""
|
||||
# ===== 配置区结束 =====
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Write-Info($msg) {
|
||||
Write-Host "[INFO] $msg" -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
function Write-Ok($msg) {
|
||||
Write-Host "[ OK ] $msg" -ForegroundColor Green
|
||||
}
|
||||
|
||||
function Write-WarnMsg($msg) {
|
||||
Write-Host "[WARN] $msg" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
function Write-Fail($msg) {
|
||||
Write-Host "[FAIL] $msg" -ForegroundColor Red
|
||||
}
|
||||
|
||||
try {
|
||||
Write-Info "开始执行主机名与网络配置"
|
||||
|
||||
# 1. 检查管理员权限
|
||||
$currentIdentity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
$principal = New-Object Security.Principal.WindowsPrincipal($currentIdentity)
|
||||
$isAdmin = $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||
if (-not $isAdmin) {
|
||||
throw "请使用管理员权限运行 PowerShell"
|
||||
}
|
||||
Write-Ok "管理员权限检查通过"
|
||||
|
||||
# 2. 校验 IP
|
||||
[void][System.Net.IPAddress]::Parse($IPAddress)
|
||||
[void][System.Net.IPAddress]::Parse($Gateway)
|
||||
foreach ($dns in $DnsServers) {
|
||||
[void][System.Net.IPAddress]::Parse($dns)
|
||||
}
|
||||
if ($PrefixLength -lt 0 -or $PrefixLength -gt 32) {
|
||||
throw "PrefixLength 必须在 0 到 32 之间"
|
||||
}
|
||||
Write-Ok "IP 参数校验通过"
|
||||
|
||||
# 3. 选择网卡
|
||||
if ($PreferredAdapterName -and $PreferredAdapterName.Trim() -ne "") {
|
||||
$nic = Get-NetAdapter -Name $PreferredAdapterName -ErrorAction Stop
|
||||
}
|
||||
else {
|
||||
$nic = Get-NetAdapter |
|
||||
Where-Object {
|
||||
$_.Status -eq "Up" -and
|
||||
$_.InterfaceDescription -notmatch "Bluetooth|Wireless|Wi-Fi|VPN" -and
|
||||
$_.Name -notmatch "Bluetooth|无线|Wi-Fi|VPN"
|
||||
} |
|
||||
Sort-Object InterfaceMetric, ifIndex |
|
||||
Select-Object -First 1
|
||||
}
|
||||
|
||||
if (-not $nic) {
|
||||
throw "未找到可用网卡,请手工指定 `$PreferredAdapterName"
|
||||
}
|
||||
|
||||
Write-Info "选中的网卡: Name=$($nic.Name), ifIndex=$($nic.ifIndex), Status=$($nic.Status)"
|
||||
Write-Ok "网卡选择完成"
|
||||
|
||||
# 4. 删除旧 IPv4 地址
|
||||
$oldIPs = Get-NetIPAddress -InterfaceIndex $nic.ifIndex -AddressFamily IPv4 -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.IPAddress -ne "127.0.0.1" }
|
||||
|
||||
foreach ($item in $oldIPs) {
|
||||
Write-Info "删除旧 IP: $($item.IPAddress)"
|
||||
Remove-NetIPAddress `
|
||||
-InterfaceIndex $nic.ifIndex `
|
||||
-IPAddress $item.IPAddress `
|
||||
-Confirm:$false `
|
||||
-ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
# 5. 删除旧默认路由
|
||||
$oldRoutes = Get-NetRoute -InterfaceIndex $nic.ifIndex -AddressFamily IPv4 -DestinationPrefix "0.0.0.0/0" -ErrorAction SilentlyContinue
|
||||
foreach ($route in $oldRoutes) {
|
||||
Write-Info "删除旧默认路由: NextHop=$($route.NextHop)"
|
||||
Remove-NetRoute `
|
||||
-InterfaceIndex $nic.ifIndex `
|
||||
-DestinationPrefix "0.0.0.0/0" `
|
||||
-NextHop $route.NextHop `
|
||||
-Confirm:$false `
|
||||
-ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
Write-Ok "旧 IP 和默认路由清理完成"
|
||||
|
||||
# 6. 配置新 IP / 网关
|
||||
Write-Info "写入新 IP: $IPAddress/$PrefixLength, Gateway=$Gateway"
|
||||
New-NetIPAddress `
|
||||
-InterfaceIndex $nic.ifIndex `
|
||||
-IPAddress $IPAddress `
|
||||
-PrefixLength $PrefixLength `
|
||||
-DefaultGateway $Gateway `
|
||||
-AddressFamily IPv4 `
|
||||
-ErrorAction Stop | Out-Null
|
||||
|
||||
Write-Ok "静态 IP 配置完成"
|
||||
|
||||
# 7. 配置 DNS
|
||||
Write-Info "写入 DNS: $($DnsServers -join ', ')"
|
||||
Set-DnsClientServerAddress `
|
||||
-InterfaceIndex $nic.ifIndex `
|
||||
-ServerAddresses $DnsServers `
|
||||
-ErrorAction Stop
|
||||
|
||||
Write-Ok "DNS 配置完成"
|
||||
|
||||
# 8. 修改主机名
|
||||
if ($env:COMPUTERNAME -ne $NewComputerName) {
|
||||
Write-Info "当前主机名: $env:COMPUTERNAME"
|
||||
Write-Info "目标主机名: $NewComputerName"
|
||||
Rename-Computer -NewName $NewComputerName -Force -ErrorAction Stop
|
||||
Write-Ok "主机名修改完成,重启后生效"
|
||||
}
|
||||
else {
|
||||
Write-WarnMsg "主机名已经是目标值,无需修改"
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Ok "全部配置已完成"
|
||||
Write-Host "请执行以下命令重启系统使主机名完全生效:" -ForegroundColor Yellow
|
||||
Write-Host "Restart-Computer" -ForegroundColor Green
|
||||
}
|
||||
catch {
|
||||
Write-Fail $_.Exception.Message
|
||||
exit 1
|
||||
}
|
||||
773
20-物理服务器虚拟机/1-2026年4月15日-管理方案/vm_manager.py
Normal file
773
20-物理服务器虚拟机/1-2026年4月15日-管理方案/vm_manager.py
Normal file
@@ -0,0 +1,773 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import argparse
|
||||
import ipaddress
|
||||
import json
|
||||
import os
|
||||
import secrets
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import uuid
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
||||
DEFAULT_SYS_DIR = "/vm/sys"
|
||||
DEFAULT_DATA_DIR = "/vm/data"
|
||||
DEFAULT_SYS_POOL = "vm-sys"
|
||||
DEFAULT_DATA_POOL = "vm-data"
|
||||
DEFAULT_BRIDGE = "br0"
|
||||
DEFAULT_GATEWAY = "192.168.11.1"
|
||||
DEFAULT_PREFIX = 24
|
||||
DEFAULT_DNS = ["223.5.5.5", "114.114.114.114"]
|
||||
DEFAULT_NVRAM_DIR = "/var/lib/libvirt/qemu/nvram"
|
||||
|
||||
|
||||
class CommandError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class DiskInfo:
|
||||
xml_elem: ET.Element
|
||||
device: str
|
||||
source_path: Optional[str]
|
||||
target_dev: Optional[str]
|
||||
target_bus: Optional[str]
|
||||
driver_type: Optional[str]
|
||||
|
||||
|
||||
@dataclass
|
||||
class InterfaceInfo:
|
||||
xml_elem: ET.Element
|
||||
mac: Optional[str]
|
||||
bridge: Optional[str]
|
||||
model: Optional[str]
|
||||
|
||||
|
||||
@dataclass
|
||||
class ClonePlan:
|
||||
name: str
|
||||
hostname: str
|
||||
ip: Optional[str]
|
||||
prefix: Optional[int]
|
||||
gateway: Optional[str]
|
||||
dns: List[str]
|
||||
template: str
|
||||
guest_type: str
|
||||
vcpus: Optional[int]
|
||||
memory_mb: Optional[int]
|
||||
bridge: str
|
||||
sys_dir: str
|
||||
data_dir: str
|
||||
sys_pool: str
|
||||
data_pool: str
|
||||
data_size: Optional[str]
|
||||
autostart: bool
|
||||
|
||||
|
||||
def run(cmd: List[str], check: bool = True, capture_output: bool = True, text: bool = True) -> subprocess.CompletedProcess:
|
||||
proc = subprocess.run(cmd, check=False, capture_output=capture_output, text=text)
|
||||
if check and proc.returncode != 0:
|
||||
raise CommandError(
|
||||
f"命令执行失败: {' '.join(cmd)}\n退出码: {proc.returncode}\nSTDOUT:\n{proc.stdout}\nSTDERR:\n{proc.stderr}"
|
||||
)
|
||||
return proc
|
||||
|
||||
|
||||
def require_cmd(name: str) -> None:
|
||||
if shutil.which(name) is None:
|
||||
raise CommandError(f"缺少命令: {name}")
|
||||
|
||||
|
||||
def first_existing_cmd(candidates: List[str]) -> str:
|
||||
for name in candidates:
|
||||
path = shutil.which(name)
|
||||
if path:
|
||||
return path
|
||||
raise CommandError(f"缺少可用命令,候选: {', '.join(candidates)}")
|
||||
|
||||
|
||||
def validate_ip(ip_str: str) -> str:
|
||||
return str(ipaddress.ip_address(ip_str))
|
||||
|
||||
|
||||
def validate_prefix(prefix: int) -> int:
|
||||
if prefix < 0 or prefix > 32:
|
||||
raise ValueError("prefix 必须在 0~32 之间")
|
||||
return prefix
|
||||
|
||||
|
||||
def normalize_dns(dns_value: str) -> List[str]:
|
||||
if not dns_value:
|
||||
return []
|
||||
items = [x.strip() for x in dns_value.split(",") if x.strip()]
|
||||
return [validate_ip(x) for x in items]
|
||||
|
||||
|
||||
def ensure_dir(path: str) -> None:
|
||||
Path(path).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def random_mac() -> str:
|
||||
suffix = [secrets.randbelow(256) for _ in range(3)]
|
||||
return "52:54:00:%02x:%02x:%02x" % tuple(suffix)
|
||||
|
||||
|
||||
def safe_hostname(name: str) -> str:
|
||||
value = name.strip().lower().replace("_", "-")
|
||||
value = value.replace(".", "-")
|
||||
if len(value) > 63:
|
||||
value = value[:63]
|
||||
return value.strip("-")
|
||||
|
||||
|
||||
def default_name(prefix: str, ip: str) -> str:
|
||||
return f"{prefix}-{ip.replace('.', '-')}"
|
||||
|
||||
|
||||
def vm_exists(name: str) -> bool:
|
||||
proc = run(["virsh", "dominfo", name], check=False)
|
||||
return proc.returncode == 0
|
||||
|
||||
|
||||
def pool_refresh(pool: str) -> None:
|
||||
run(["virsh", "pool-refresh", pool], check=False)
|
||||
|
||||
|
||||
def prefix_to_netmask(prefix: int) -> str:
|
||||
network = ipaddress.IPv4Network(f"0.0.0.0/{prefix}")
|
||||
return str(network.netmask)
|
||||
|
||||
|
||||
def dumpxml(domain: str, inactive: bool = True) -> ET.ElementTree:
|
||||
cmd = ["virsh", "dumpxml", domain]
|
||||
if inactive:
|
||||
cmd.insert(2, "--inactive")
|
||||
xml_text = run(cmd).stdout
|
||||
return ET.ElementTree(ET.fromstring(xml_text))
|
||||
|
||||
|
||||
def get_domain_disks(root: ET.Element) -> List[DiskInfo]:
|
||||
result: List[DiskInfo] = []
|
||||
for disk in root.findall("./devices/disk"):
|
||||
device = disk.get("device", "disk")
|
||||
source = disk.find("source")
|
||||
target = disk.find("target")
|
||||
driver = disk.find("driver")
|
||||
source_path = None
|
||||
if source is not None:
|
||||
source_path = source.get("file") or source.get("dev") or source.get("name")
|
||||
result.append(
|
||||
DiskInfo(
|
||||
xml_elem=disk,
|
||||
device=device,
|
||||
source_path=source_path,
|
||||
target_dev=target.get("dev") if target is not None else None,
|
||||
target_bus=target.get("bus") if target is not None else None,
|
||||
driver_type=driver.get("type") if driver is not None else None,
|
||||
)
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def pick_system_disk(disks: List[DiskInfo], sys_dir: str) -> DiskInfo:
|
||||
file_disks = [d for d in disks if d.device == "disk" and d.source_path]
|
||||
if not file_disks:
|
||||
raise CommandError("模板虚拟机未找到可用系统盘")
|
||||
|
||||
for d in file_disks:
|
||||
if d.source_path and os.path.abspath(d.source_path).startswith(os.path.abspath(sys_dir) + os.sep):
|
||||
return d
|
||||
return file_disks[0]
|
||||
|
||||
|
||||
def get_domain_interface(root: ET.Element) -> InterfaceInfo:
|
||||
iface = root.find("./devices/interface[@type='bridge']")
|
||||
if iface is None:
|
||||
iface = root.find("./devices/interface")
|
||||
if iface is None:
|
||||
raise CommandError("模板虚拟机未找到网卡配置")
|
||||
mac_elem = iface.find("mac")
|
||||
source_elem = iface.find("source")
|
||||
model_elem = iface.find("model")
|
||||
return InterfaceInfo(
|
||||
xml_elem=iface,
|
||||
mac=mac_elem.get("address") if mac_elem is not None else None,
|
||||
bridge=source_elem.get("bridge") if source_elem is not None else None,
|
||||
model=model_elem.get("type") if model_elem is not None else None,
|
||||
)
|
||||
|
||||
|
||||
def set_domain_identity(root: ET.Element, name: str) -> None:
|
||||
name_elem = root.find("name")
|
||||
if name_elem is None:
|
||||
name_elem = ET.SubElement(root, "name")
|
||||
name_elem.text = name
|
||||
|
||||
uuid_elem = root.find("uuid")
|
||||
if uuid_elem is None:
|
||||
uuid_elem = ET.SubElement(root, "uuid")
|
||||
uuid_elem.text = str(uuid.uuid4())
|
||||
|
||||
for tag in ["id", "title", "description"]:
|
||||
elem = root.find(tag)
|
||||
if elem is not None and tag == "id":
|
||||
root.remove(elem)
|
||||
|
||||
|
||||
def set_domain_resources(root: ET.Element, memory_mb: Optional[int], vcpus: Optional[int]) -> None:
|
||||
if memory_mb:
|
||||
memory_kib = str(memory_mb * 1024)
|
||||
memory_elem = root.find("memory")
|
||||
if memory_elem is None:
|
||||
memory_elem = ET.SubElement(root, "memory", {"unit": "KiB"})
|
||||
else:
|
||||
memory_elem.set("unit", "KiB")
|
||||
memory_elem.text = memory_kib
|
||||
|
||||
current_elem = root.find("currentMemory")
|
||||
if current_elem is None:
|
||||
current_elem = ET.SubElement(root, "currentMemory", {"unit": "KiB"})
|
||||
else:
|
||||
current_elem.set("unit", "KiB")
|
||||
current_elem.text = memory_kib
|
||||
|
||||
if vcpus:
|
||||
vcpu_elem = root.find("vcpu")
|
||||
if vcpu_elem is None:
|
||||
vcpu_elem = ET.SubElement(root, "vcpu")
|
||||
vcpu_elem.text = str(vcpus)
|
||||
|
||||
|
||||
def next_disk_target(existing_targets: List[str], bus: str) -> str:
|
||||
if bus == "virtio":
|
||||
prefix = "vd"
|
||||
elif bus in ("sata", "scsi"):
|
||||
prefix = "sd"
|
||||
else:
|
||||
prefix = "vd"
|
||||
|
||||
for letter_ord in range(ord("b"), ord("z") + 1):
|
||||
cand = f"{prefix}{chr(letter_ord)}"
|
||||
if cand not in existing_targets:
|
||||
return cand
|
||||
raise CommandError("没有可用的数据盘 target 名称")
|
||||
|
||||
|
||||
def clone_nvram_if_needed(root: ET.Element, vm_name: str, nvram_dir: str = DEFAULT_NVRAM_DIR) -> None:
|
||||
nvram_elem = root.find("./os/nvram")
|
||||
if nvram_elem is None:
|
||||
return
|
||||
|
||||
src_path = (nvram_elem.text or "").strip()
|
||||
if not src_path:
|
||||
return
|
||||
|
||||
ensure_dir(nvram_dir)
|
||||
dst_path = os.path.join(nvram_dir, f"{vm_name}_VARS.fd")
|
||||
shutil.copy2(src_path, dst_path)
|
||||
nvram_elem.text = dst_path
|
||||
|
||||
|
||||
def remove_nonessential_disks(root: ET.Element, keep_disk: DiskInfo) -> None:
|
||||
devices = root.find("devices")
|
||||
if devices is None:
|
||||
raise CommandError("XML 中缺少 devices 节点")
|
||||
|
||||
for disk in list(devices.findall("disk")):
|
||||
if disk is keep_disk.xml_elem:
|
||||
continue
|
||||
devices.remove(disk)
|
||||
|
||||
|
||||
def sanitize_virtio_disk_addresses(root: ET.Element) -> None:
|
||||
for disk in root.findall("./devices/disk[@device='disk']"):
|
||||
target = disk.find("target")
|
||||
if target is not None and target.get("bus") == "virtio":
|
||||
addr = disk.find("address")
|
||||
if addr is not None and addr.get("type") == "drive":
|
||||
disk.remove(addr)
|
||||
|
||||
|
||||
def update_system_disk_source(os_disk: DiskInfo, new_path: str) -> None:
|
||||
source_elem = os_disk.xml_elem.find("source")
|
||||
if source_elem is None:
|
||||
raise CommandError("系统盘 disk 节点缺少 source")
|
||||
for attr in ["file", "dev", "name"]:
|
||||
if source_elem.get(attr) is not None:
|
||||
source_elem.set(attr, new_path)
|
||||
for other in ["file", "dev", "name"]:
|
||||
if other != attr and source_elem.get(other) is not None:
|
||||
del source_elem.attrib[other]
|
||||
return
|
||||
source_elem.set("file", new_path)
|
||||
|
||||
|
||||
def add_data_disk(root: ET.Element, data_path: str, bus: str = "virtio", driver_type: str = "qcow2") -> None:
|
||||
devices = root.find("devices")
|
||||
if devices is None:
|
||||
raise CommandError("XML 中缺少 devices 节点")
|
||||
|
||||
existing_targets: List[str] = []
|
||||
for disk in root.findall("./devices/disk"):
|
||||
target = disk.find("target")
|
||||
if target is not None and target.get("dev"):
|
||||
existing_targets.append(target.get("dev"))
|
||||
|
||||
target_dev = next_disk_target(existing_targets, bus)
|
||||
|
||||
disk_elem = ET.SubElement(devices, "disk", {"type": "file", "device": "disk"})
|
||||
ET.SubElement(disk_elem, "driver", {"name": "qemu", "type": driver_type})
|
||||
ET.SubElement(disk_elem, "source", {"file": data_path})
|
||||
ET.SubElement(disk_elem, "target", {"dev": target_dev, "bus": bus})
|
||||
|
||||
if bus in ("sata", "ide", "scsi"):
|
||||
ET.SubElement(disk_elem, "address", {
|
||||
"type": "drive",
|
||||
"controller": "0",
|
||||
"bus": "0",
|
||||
"target": "0",
|
||||
"unit": str(len(existing_targets) + 1),
|
||||
})
|
||||
|
||||
|
||||
def add_cdrom_iso(root: ET.Element, iso_path: str, bus: str = "sata") -> None:
|
||||
devices = root.find("devices")
|
||||
if devices is None:
|
||||
raise CommandError("XML 中缺少 devices 节点")
|
||||
|
||||
existing: List[str] = []
|
||||
for disk in root.findall("./devices/disk"):
|
||||
target = disk.find("target")
|
||||
if target is not None and target.get("dev"):
|
||||
existing.append(target.get("dev"))
|
||||
|
||||
if bus == "sata":
|
||||
prefix = "sd"
|
||||
base = "c"
|
||||
else:
|
||||
prefix = "hd"
|
||||
base = "c"
|
||||
|
||||
target_dev = f"{prefix}{base}"
|
||||
idx = ord(base)
|
||||
while target_dev in existing:
|
||||
idx += 1
|
||||
target_dev = f"{prefix}{chr(idx)}"
|
||||
|
||||
disk_elem = ET.SubElement(devices, "disk", {"type": "file", "device": "cdrom"})
|
||||
ET.SubElement(disk_elem, "driver", {"name": "qemu", "type": "raw"})
|
||||
ET.SubElement(disk_elem, "source", {"file": iso_path})
|
||||
ET.SubElement(disk_elem, "target", {"dev": target_dev, "bus": bus})
|
||||
ET.SubElement(disk_elem, "readonly")
|
||||
|
||||
|
||||
def update_network(root: ET.Element, bridge: str, mac_addr: str) -> None:
|
||||
iface = get_domain_interface(root).xml_elem
|
||||
source_elem = iface.find("source")
|
||||
if source_elem is None:
|
||||
source_elem = ET.SubElement(iface, "source")
|
||||
for attr in list(source_elem.attrib.keys()):
|
||||
del source_elem.attrib[attr]
|
||||
source_elem.set("bridge", bridge)
|
||||
|
||||
mac_elem = iface.find("mac")
|
||||
if mac_elem is None:
|
||||
mac_elem = ET.SubElement(iface, "mac")
|
||||
mac_elem.set("address", mac_addr)
|
||||
|
||||
|
||||
def write_domain_xml(root: ET.Element, path: str) -> None:
|
||||
xml_bytes = ET.tostring(root, encoding="utf-8")
|
||||
with open(path, "wb") as f:
|
||||
f.write(xml_bytes)
|
||||
|
||||
|
||||
def qemu_img_clone(src: str, dst: str) -> None:
|
||||
ensure_dir(str(Path(dst).parent))
|
||||
run(["qemu-img", "convert", "-p", "-f", "qcow2", "-O", "qcow2", src, dst])
|
||||
|
||||
|
||||
def qemu_img_create(dst: str, size: str) -> None:
|
||||
ensure_dir(str(Path(dst).parent))
|
||||
run(["qemu-img", "create", "-f", "qcow2", dst, size])
|
||||
|
||||
|
||||
def build_iso_from_dir(src_dir: str, out_iso: str, volid: str) -> None:
|
||||
ensure_dir(str(Path(out_iso).parent))
|
||||
iso_cmd = first_existing_cmd(["genisoimage", "mkisofs", "xorrisofs"])
|
||||
cmd_name = os.path.basename(iso_cmd)
|
||||
|
||||
if "xorrisofs" in cmd_name:
|
||||
cmd = [iso_cmd, "-as", "mkisofs", "-output", out_iso, "-volid", volid, "-joliet", "-rock", src_dir]
|
||||
else:
|
||||
cmd = [iso_cmd, "-output", out_iso, "-volid", volid, "-joliet", "-rock", src_dir]
|
||||
run(cmd)
|
||||
|
||||
|
||||
def render_linux_user_data(hostname: str) -> str:
|
||||
return f"""#cloud-config
|
||||
preserve_hostname: false
|
||||
hostname: {hostname}
|
||||
fqdn: {hostname}
|
||||
manage_etc_hosts: true
|
||||
"""
|
||||
|
||||
|
||||
def render_linux_meta_data(hostname: str, instance_id: str) -> str:
|
||||
return f"instance-id: {instance_id}\nlocal-hostname: {hostname}\n"
|
||||
|
||||
|
||||
def render_linux_network_config_v1(mac_addr: str, ip: str, prefix: int, gateway: str, dns: List[str]) -> str:
|
||||
netmask = prefix_to_netmask(prefix)
|
||||
dns_lines = "\n".join([f" - {item}" for item in dns]) if dns else ""
|
||||
return f"""version: 1
|
||||
config:
|
||||
- type: physical
|
||||
name: eth0
|
||||
mac_address: "{mac_addr.lower()}"
|
||||
subnets:
|
||||
- type: static
|
||||
address: {ip}
|
||||
netmask: {netmask}
|
||||
gateway: {gateway}
|
||||
dns_nameservers:
|
||||
{dns_lines if dns_lines else ' []'}
|
||||
"""
|
||||
|
||||
|
||||
def create_linux_seed_iso(work_dir: str, vm_name: str, hostname: str, mac_addr: str, ip: str, prefix: int, gateway: str, dns: List[str], sys_dir: str) -> str:
|
||||
seed_dir = os.path.join(work_dir, "seed-linux")
|
||||
ensure_dir(seed_dir)
|
||||
|
||||
with open(os.path.join(seed_dir, "user-data"), "w", encoding="utf-8") as f:
|
||||
f.write(render_linux_user_data(hostname))
|
||||
with open(os.path.join(seed_dir, "meta-data"), "w", encoding="utf-8") as f:
|
||||
f.write(render_linux_meta_data(hostname, str(uuid.uuid4())))
|
||||
with open(os.path.join(seed_dir, "network-config"), "w", encoding="utf-8") as f:
|
||||
f.write(render_linux_network_config_v1(mac_addr, ip, prefix, gateway, dns))
|
||||
|
||||
iso_path = os.path.join(sys_dir, f"{vm_name}-seed.iso")
|
||||
build_iso_from_dir(seed_dir, iso_path, "cidata")
|
||||
return iso_path
|
||||
|
||||
|
||||
def virsh_list_all_names() -> List[str]:
|
||||
out = run(["virsh", "list", "--all", "--name"]).stdout
|
||||
return [line.strip() for line in out.splitlines() if line.strip()]
|
||||
|
||||
|
||||
def dom_state(name: str) -> str:
|
||||
out = run(["virsh", "domstate", name], check=False)
|
||||
return out.stdout.strip() if out.returncode == 0 else "unknown"
|
||||
|
||||
|
||||
def domifaddr(name: str) -> List[Tuple[str, str, str, str]]:
|
||||
for source in ["agent", "lease", "arp"]:
|
||||
proc = run(["virsh", "domifaddr", name, "--source", source, "--full"], check=False)
|
||||
if proc.returncode != 0:
|
||||
continue
|
||||
lines = [ln.rstrip() for ln in proc.stdout.splitlines() if ln.strip()]
|
||||
if len(lines) < 3:
|
||||
continue
|
||||
rows = []
|
||||
for line in lines[2:]:
|
||||
parts = line.split()
|
||||
if len(parts) >= 4:
|
||||
iface, mac, proto, addr = parts[:4]
|
||||
rows.append((iface, mac, proto, addr))
|
||||
if rows:
|
||||
return rows
|
||||
return []
|
||||
|
||||
|
||||
def map_vm_disks(name: str) -> List[Dict[str, str]]:
|
||||
tree = dumpxml(name, inactive=True)
|
||||
root = tree.getroot()
|
||||
disks = get_domain_disks(root)
|
||||
rows = []
|
||||
for d in disks:
|
||||
if d.device != "disk":
|
||||
continue
|
||||
role = "unknown"
|
||||
if d.source_path:
|
||||
abs_path = os.path.abspath(d.source_path)
|
||||
if abs_path.startswith(os.path.abspath(DEFAULT_SYS_DIR) + os.sep):
|
||||
role = "system"
|
||||
elif abs_path.startswith(os.path.abspath(DEFAULT_DATA_DIR) + os.sep):
|
||||
role = "data"
|
||||
rows.append({
|
||||
"vm": name,
|
||||
"target": d.target_dev or "-",
|
||||
"bus": d.target_bus or "-",
|
||||
"role": role,
|
||||
"path": d.source_path or "-",
|
||||
})
|
||||
return rows
|
||||
|
||||
|
||||
def prepare_clone_plan(args: argparse.Namespace, guest_type: str) -> ClonePlan:
|
||||
ip = validate_ip(args.ip) if getattr(args, "ip", None) else None
|
||||
prefix = validate_prefix(args.prefix) if getattr(args, "prefix", None) is not None else None
|
||||
gateway = validate_ip(args.gateway) if getattr(args, "gateway", None) else None
|
||||
dns = normalize_dns(args.dns) if getattr(args, "dns", None) else list(DEFAULT_DNS)
|
||||
|
||||
if guest_type in ("linux", "windows") and not ip:
|
||||
raise CommandError(f"{guest_type} 克隆必须提供 --ip")
|
||||
if guest_type in ("linux", "windows") and prefix is None:
|
||||
raise CommandError(f"{guest_type} 克隆必须提供 --prefix")
|
||||
if guest_type in ("linux", "windows") and not gateway:
|
||||
raise CommandError(f"{guest_type} 克隆必须提供 --gateway")
|
||||
|
||||
name = args.name or default_name(args.name_prefix, ip)
|
||||
hostname = args.hostname or safe_hostname(name)
|
||||
|
||||
data_size = (args.data_size or "").strip() or None
|
||||
|
||||
return ClonePlan(
|
||||
name=name,
|
||||
hostname=hostname,
|
||||
ip=ip,
|
||||
prefix=prefix,
|
||||
gateway=gateway,
|
||||
dns=dns,
|
||||
template=args.template,
|
||||
guest_type=guest_type,
|
||||
vcpus=args.vcpus,
|
||||
memory_mb=args.memory,
|
||||
bridge=args.bridge,
|
||||
sys_dir=args.sys_dir,
|
||||
data_dir=args.data_dir,
|
||||
sys_pool=args.sys_pool,
|
||||
data_pool=args.data_pool,
|
||||
data_size=data_size,
|
||||
autostart=args.autostart,
|
||||
)
|
||||
|
||||
|
||||
def clone_vm(plan: ClonePlan) -> Dict[str, str]:
|
||||
require_cmd("virsh")
|
||||
require_cmd("qemu-img")
|
||||
if plan.guest_type == "linux":
|
||||
first_existing_cmd(["genisoimage", "mkisofs", "xorrisofs"])
|
||||
|
||||
if vm_exists(plan.name):
|
||||
raise CommandError(f"目标虚拟机已存在: {plan.name}")
|
||||
|
||||
ensure_dir(plan.sys_dir)
|
||||
ensure_dir(plan.data_dir)
|
||||
|
||||
tree = dumpxml(plan.template, inactive=True)
|
||||
root = tree.getroot()
|
||||
|
||||
disks = get_domain_disks(root)
|
||||
os_disk = pick_system_disk(disks, plan.sys_dir)
|
||||
if not os_disk.source_path:
|
||||
raise CommandError("无法识别模板系统盘路径")
|
||||
|
||||
mac_addr = random_mac()
|
||||
|
||||
sys_disk_path = os.path.join(plan.sys_dir, f"{plan.name}.qcow2")
|
||||
data_disk_path = os.path.join(plan.data_dir, f"{plan.name}-data.qcow2") if plan.data_size else ""
|
||||
|
||||
qemu_img_clone(os_disk.source_path, sys_disk_path)
|
||||
if plan.data_size:
|
||||
qemu_img_create(data_disk_path, plan.data_size)
|
||||
|
||||
seed_iso = ""
|
||||
if plan.guest_type == "linux":
|
||||
with tempfile.TemporaryDirectory(prefix=f"vmclone-{plan.name}-") as work_dir:
|
||||
seed_iso = create_linux_seed_iso(
|
||||
work_dir=work_dir,
|
||||
vm_name=plan.name,
|
||||
hostname=plan.hostname,
|
||||
mac_addr=mac_addr,
|
||||
ip=plan.ip or "",
|
||||
prefix=plan.prefix or DEFAULT_PREFIX,
|
||||
gateway=plan.gateway or DEFAULT_GATEWAY,
|
||||
dns=plan.dns,
|
||||
sys_dir=plan.sys_dir,
|
||||
)
|
||||
elif plan.guest_type == "windows":
|
||||
seed_iso = ""
|
||||
else:
|
||||
raise CommandError(f"不支持的 guest_type: {plan.guest_type}")
|
||||
|
||||
set_domain_identity(root, plan.name)
|
||||
set_domain_resources(root, plan.memory_mb, plan.vcpus)
|
||||
clone_nvram_if_needed(root, plan.name)
|
||||
remove_nonessential_disks(root, os_disk)
|
||||
update_system_disk_source(os_disk, sys_disk_path)
|
||||
update_network(root, plan.bridge, mac_addr)
|
||||
sanitize_virtio_disk_addresses(root)
|
||||
|
||||
if plan.data_size:
|
||||
add_data_disk(root, data_disk_path, bus="virtio", driver_type="qcow2")
|
||||
if seed_iso:
|
||||
add_cdrom_iso(root, seed_iso, bus="sata")
|
||||
|
||||
with tempfile.NamedTemporaryFile(prefix=f"{plan.name}-", suffix=".xml", delete=False) as tmp:
|
||||
tmp_path = tmp.name
|
||||
try:
|
||||
write_domain_xml(root, tmp_path)
|
||||
run(["virsh", "define", tmp_path])
|
||||
if plan.autostart:
|
||||
run(["virsh", "autostart", plan.name])
|
||||
run(["virsh", "start", plan.name])
|
||||
finally:
|
||||
try:
|
||||
os.unlink(tmp_path)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
pool_refresh(plan.sys_pool)
|
||||
pool_refresh(plan.data_pool)
|
||||
|
||||
return {
|
||||
"name": plan.name,
|
||||
"hostname": plan.hostname,
|
||||
"ip": plan.ip or "",
|
||||
"mac": mac_addr,
|
||||
"system_disk": sys_disk_path,
|
||||
"data_disk": data_disk_path,
|
||||
"seed_iso": seed_iso,
|
||||
"bridge": plan.bridge,
|
||||
}
|
||||
|
||||
|
||||
def print_json(data) -> None:
|
||||
print(json.dumps(data, ensure_ascii=False, indent=2))
|
||||
|
||||
|
||||
def cmd_clone_linux(args: argparse.Namespace) -> None:
|
||||
plan = prepare_clone_plan(args, "linux")
|
||||
result = clone_vm(plan)
|
||||
print_json(result)
|
||||
|
||||
|
||||
def cmd_clone_windows(args: argparse.Namespace) -> None:
|
||||
plan = prepare_clone_plan(args, "windows")
|
||||
result = clone_vm(plan)
|
||||
print_json(result)
|
||||
|
||||
|
||||
def cmd_list_vms(args: argparse.Namespace) -> None:
|
||||
rows = []
|
||||
for name in virsh_list_all_names():
|
||||
ips = [x[3] for x in domifaddr(name) if x[2].lower().startswith("ipv4")]
|
||||
rows.append({
|
||||
"name": name,
|
||||
"state": dom_state(name),
|
||||
"ipv4": ips,
|
||||
})
|
||||
print_json(rows)
|
||||
|
||||
|
||||
def cmd_vm_ips(args: argparse.Namespace) -> None:
|
||||
targets = [args.name] if args.name else virsh_list_all_names()
|
||||
rows = []
|
||||
for name in targets:
|
||||
for iface, mac, proto, addr in domifaddr(name):
|
||||
rows.append({
|
||||
"vm": name,
|
||||
"iface": iface,
|
||||
"mac": mac,
|
||||
"proto": proto,
|
||||
"addr": addr,
|
||||
})
|
||||
print_json(rows)
|
||||
|
||||
|
||||
def cmd_map_disks(args: argparse.Namespace) -> None:
|
||||
targets = [args.name] if args.name else virsh_list_all_names()
|
||||
rows = []
|
||||
for name in targets:
|
||||
rows.extend(map_vm_disks(name))
|
||||
print_json(rows)
|
||||
|
||||
|
||||
def cmd_show_vm(args: argparse.Namespace) -> None:
|
||||
name = args.name
|
||||
rows = {
|
||||
"name": name,
|
||||
"state": dom_state(name),
|
||||
"ips": [{"iface": i[0], "mac": i[1], "proto": i[2], "addr": i[3]} for i in domifaddr(name)],
|
||||
"disks": map_vm_disks(name),
|
||||
}
|
||||
print_json(rows)
|
||||
|
||||
|
||||
def add_common_clone_args(p: argparse.ArgumentParser) -> None:
|
||||
p.add_argument("--template", required=True, help="模板虚拟机名称")
|
||||
p.add_argument("--name", help="新虚拟机名称;不传则按 name-prefix + IP 自动生成")
|
||||
p.add_argument("--name-prefix", default="vm", help="自动生成虚拟机名称前缀")
|
||||
p.add_argument("--hostname", help="客体系统主机名;不传则自动按 name 生成")
|
||||
p.add_argument("--ip", required=True, help="静态 IP,例如 192.168.11.101")
|
||||
p.add_argument("--prefix", type=int, default=DEFAULT_PREFIX, help="前缀长度,例如 24")
|
||||
p.add_argument("--gateway", default=DEFAULT_GATEWAY, help="默认网关")
|
||||
p.add_argument("--dns", default=",".join(DEFAULT_DNS), help="DNS,逗号分隔")
|
||||
p.add_argument("--bridge", default=DEFAULT_BRIDGE, help="libvirt bridge 名称")
|
||||
p.add_argument("--vcpus", type=int, help="虚拟 CPU 数")
|
||||
p.add_argument("--memory", type=int, help="内存,单位 MB")
|
||||
p.add_argument("--data-size", default="", help="新数据盘大小,例如 100G;默认不创建数据盘")
|
||||
p.add_argument("--sys-dir", default=DEFAULT_SYS_DIR, help="系统盘目录")
|
||||
p.add_argument("--data-dir", default=DEFAULT_DATA_DIR, help="数据盘目录")
|
||||
p.add_argument("--sys-pool", default=DEFAULT_SYS_POOL, help="系统盘存储池名称")
|
||||
p.add_argument("--data-pool", default=DEFAULT_DATA_POOL, help="数据盘存储池名称")
|
||||
p.add_argument("--autostart", action="store_true", help="定义完成后设置开机自启")
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="基于 libvirt/virsh 的 KVM 虚拟机批量管理脚本(Windows/Linux 模板克隆、IP 查询、磁盘映射)"
|
||||
)
|
||||
sub = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
p1 = sub.add_parser("clone-linux", help="从 Linux 模板克隆新虚拟机")
|
||||
add_common_clone_args(p1)
|
||||
p1.set_defaults(func=cmd_clone_linux)
|
||||
|
||||
p2 = sub.add_parser("clone-windows", help="从 Windows 模板克隆新虚拟机")
|
||||
add_common_clone_args(p2)
|
||||
p2.set_defaults(func=cmd_clone_windows)
|
||||
|
||||
p3 = sub.add_parser("list-vms", help="列出全部虚拟机及 IPv4")
|
||||
p3.set_defaults(func=cmd_list_vms)
|
||||
|
||||
p4 = sub.add_parser("vm-ips", help="查询单个或全部虚拟机 IP")
|
||||
p4.add_argument("name", nargs="?", help="虚拟机名称;不传则全部")
|
||||
p4.set_defaults(func=cmd_vm_ips)
|
||||
|
||||
p5 = sub.add_parser("map-disks", help="查看系统盘/数据盘与虚拟机关系")
|
||||
p5.add_argument("name", nargs="?", help="虚拟机名称;不传则全部")
|
||||
p5.set_defaults(func=cmd_map_disks)
|
||||
|
||||
p6 = sub.add_parser("show-vm", help="查看单台虚拟机详情")
|
||||
p6.add_argument("name", help="虚拟机名称")
|
||||
p6.set_defaults(func=cmd_show_vm)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def main() -> int:
|
||||
try:
|
||||
parser = build_parser()
|
||||
args = parser.parse_args()
|
||||
args.func(args)
|
||||
return 0
|
||||
except KeyboardInterrupt:
|
||||
print("用户中断", file=sys.stderr)
|
||||
return 130
|
||||
except Exception as e:
|
||||
print(f"ERROR: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
103
20-物理服务器虚拟机/1-2026年4月15日-管理方案/vm_manager_README.md
Normal file
103
20-物理服务器虚拟机/1-2026年4月15日-管理方案/vm_manager_README.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# vm_manager.py 使用说明
|
||||
|
||||
## 1. 宿主机依赖
|
||||
|
||||
```bash
|
||||
yum install -y python3 libvirt-client qemu-img genisoimage
|
||||
```
|
||||
|
||||
说明:
|
||||
- `virsh` 用于定义/启动/查询虚拟机
|
||||
- `qemu-img` 用于复制系统盘、创建数据盘
|
||||
- `genisoimage` 用于生成 Linux cloud-init 配置盘和 Windows 配置盘
|
||||
|
||||
## 2. Linux 模板一次性准备
|
||||
|
||||
模板机内建议至少完成:
|
||||
|
||||
```bash
|
||||
# Ubuntu / openEuler / CentOS 模板内
|
||||
sudo yum install -y qemu-guest-agent cloud-init || sudo apt-get update && sudo apt-get install -y qemu-guest-agent cloud-init
|
||||
sudo systemctl enable qemu-guest-agent
|
||||
sudo systemctl enable cloud-init cloud-config cloud-final cloud-init-local
|
||||
|
||||
# 建议清理模板痕迹
|
||||
sudo cloud-init clean --logs
|
||||
sudo truncate -s 0 /etc/machine-id
|
||||
sudo rm -f /var/lib/dbus/machine-id
|
||||
sudo sync
|
||||
sudo shutdown -h now
|
||||
```
|
||||
|
||||
## 3. Windows 模板一次性准备
|
||||
|
||||
模板机内建议至少完成:
|
||||
|
||||
1. 安装 VirtIO 驱动和 qemu guest agent
|
||||
2. 放置 `windows_template_bootstrap.ps1` 到:
|
||||
- `C:\ProgramData\VmBootstrap\bootstrap.ps1`
|
||||
3. 注册开机任务(可直接运行 `SetupComplete.cmd` 中的 `schtasks` 命令)
|
||||
4. 最后执行 Sysprep 通用化并关机
|
||||
|
||||
推荐命令:
|
||||
|
||||
```powershell
|
||||
C:\Windows\System32\Sysprep\Sysprep.exe /generalize /oobe /shutdown
|
||||
```
|
||||
|
||||
## 4. Linux 克隆示例
|
||||
|
||||
```bash
|
||||
python3 vm_manager.py clone-linux \
|
||||
--template ubuntu2204-vm \
|
||||
--name-prefix ubuntu2204 \
|
||||
--ip 192.168.11.171 \
|
||||
--prefix 24 \
|
||||
--gateway 192.168.11.1 \
|
||||
--dns 192.168.34.40,223.5.5.5 \
|
||||
--vcpus 4 \
|
||||
--memory 8192 \
|
||||
--data-size 300G \
|
||||
--autostart
|
||||
```
|
||||
|
||||
## 5. Windows 克隆示例
|
||||
|
||||
```bash
|
||||
python3 vm_manager.py clone-windows \
|
||||
--template win-server-2022-template \
|
||||
--name-prefix ws2022 \
|
||||
--ip 192.168.11.161 \
|
||||
--prefix 24 \
|
||||
--gateway 192.168.11.1 \
|
||||
--dns 223.5.5.5,114.114.114.114 \
|
||||
--vcpus 8 \
|
||||
--memory 16384 \
|
||||
--data-size 500G \
|
||||
--data-drive-letter D \
|
||||
--autostart
|
||||
```
|
||||
|
||||
## 6. 查询命令
|
||||
|
||||
```bash
|
||||
# 全部虚拟机与 IP
|
||||
python3 vm_manager.py list-vms
|
||||
|
||||
# 查询单台虚拟机 IP
|
||||
python3 vm_manager.py vm-ips ws2022-192-168-11-120
|
||||
|
||||
# 查询系统盘/数据盘映射
|
||||
python3 vm_manager.py map-disks
|
||||
|
||||
# 查询单台详情
|
||||
python3 vm_manager.py show-vm ws2022-192-168-11-120
|
||||
```
|
||||
|
||||
## 7. 注意事项
|
||||
|
||||
1. 本脚本默认把模板的第一块系统盘克隆到 `/vm/sys/<vm>.qcow2`
|
||||
2. 模板中原有附加数据盘会被忽略,不会复制;新数据盘按参数重新创建到 `/vm/data/<vm>-data.qcow2`
|
||||
3. 脚本会自动生成新的 UUID、MAC,并把网卡桥接到 `br0`
|
||||
4. 如果模板使用 UEFI/NVRAM,脚本会自动复制一份新的 VARS 文件
|
||||
5. `vm-ips` 依赖客体已安装 qemu-guest-agent;否则会退回 lease/arp 查询
|
||||
27
20-物理服务器虚拟机/2-2026年3月25日-实操.sh
Normal file
27
20-物理服务器虚拟机/2-2026年3月25日-实操.sh
Normal file
@@ -0,0 +1,27 @@
|
||||
# 检查当前主机名
|
||||
hostnamectl
|
||||
|
||||
# 如无 DNS 服务器,用 hosts 文件模拟(测试环境可行)
|
||||
echo "192.168.11.14 wdd.ovirt.local.lan ovirt" >> /etc/hosts
|
||||
hostnamectl set-hostname wdd.ovirt.local.lan
|
||||
|
||||
|
||||
# 1. 验证 CPU 虚拟化支持(必须有输出)
|
||||
grep -E 'svm|vmx' /proc/cpuinfo | head -3
|
||||
|
||||
# 2. 验证磁盘类型
|
||||
lsblk -d -o NAME,ROTA,TYPE,SIZE,MODEL
|
||||
# ROTA=0 表示 SSD/NVMe,ROTA=1 表示 HDD
|
||||
|
||||
# 3. 检查内核模块
|
||||
lsmod | grep kvm
|
||||
# 应看到 kvm_intel 和 kvm
|
||||
|
||||
# 4. 如未加载,手动加载
|
||||
modprobe kvm_intel
|
||||
|
||||
# 5. 确认 hostname 与 hosts 文件一致
|
||||
hostname -f # 应返回完整 FQDN,如 ovirt.local.lan
|
||||
ping -c 2 $(hostname -f) # 必须能 ping 通自己
|
||||
|
||||
|
||||
106
20-物理服务器虚拟机/3-磁盘格式化.sh
Normal file
106
20-物理服务器虚拟机/3-磁盘格式化.sh
Normal file
@@ -0,0 +1,106 @@
|
||||
# sdb 是 SSD,容量 2.6TB(> 2TB),必须用 GPT
|
||||
parted /dev/sdb --script mklabel gpt
|
||||
parted /dev/sdb --script mkpart primary xfs 0% 100%
|
||||
|
||||
# 验证分区
|
||||
lsblk /dev/sdb
|
||||
partprobe /dev/sdb
|
||||
|
||||
# 格式化为 XFS(SSD 优化参数)
|
||||
mkfs.xfs -f \
|
||||
-d agcount=8 \
|
||||
-l size=128m \
|
||||
/dev/sdb1
|
||||
|
||||
# 创建挂载点
|
||||
mkdir -p /data/vm-storage
|
||||
|
||||
# 写入 fstab(SSD 加 noatime 减少写放大)
|
||||
echo '/dev/sdb1 /data/vm-storage xfs defaults,noatime 0 2' >> /etc/fstab
|
||||
mount -a
|
||||
|
||||
# 验证
|
||||
df -hT /data/vm-storage
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# 138.3TB 必须用 GPT
|
||||
parted /dev/sdc --script mklabel gpt
|
||||
parted /dev/sdc --script mkpart primary xfs 0% 100%
|
||||
|
||||
partprobe /dev/sdc
|
||||
|
||||
# 方法A:完全让 xfsprogs 自动决定(推荐,最简单)
|
||||
mkfs.xfs -f -i size=512 /dev/sdc1
|
||||
|
||||
# 挂载(超过 2TB 必须加 inode64 确保 inode 分布在整个分区)
|
||||
mkdir -p /data/bulk-storage
|
||||
|
||||
echo '/dev/sdc1 /data/bulk-storage xfs defaults,noatime,inode64 0 2' >> /etc/fstab
|
||||
mount -a
|
||||
|
||||
# 验证(确认容量正确显示)
|
||||
df -hT /data/bulk-storage
|
||||
|
||||
|
||||
# oVirt 要求固定 UID:GID = 36:36(vdsm:kvm)
|
||||
groupadd kvm -g 36 2>/dev/null || true
|
||||
useradd vdsm -u 36 -g 36 -s /sbin/nologin 2>/dev/null || true
|
||||
|
||||
# sdb 上的目录(VM 磁盘)
|
||||
mkdir -p /data/vm-storage/data # VM 数据域
|
||||
mkdir -p /data/vm-storage/export # 导出域(用于 VM 备份/迁移)
|
||||
chown -R 36:36 /data/vm-storage/
|
||||
chmod -R 0755 /data/vm-storage/
|
||||
|
||||
# sdc 上的目录(大容量镜像)
|
||||
mkdir -p /data/bulk-storage/iso # ISO 镜像域
|
||||
mkdir -p /data/bulk-storage/backup # 可选:额外备份
|
||||
chown -R 36:36 /data/bulk-storage/iso
|
||||
chmod 0755 /data/bulk-storage/iso
|
||||
|
||||
|
||||
dnf install -y nfs-utils
|
||||
systemctl enable --now rpcbind nfs-server
|
||||
|
||||
# 添加 NFS 导出规则
|
||||
cat >> /etc/exports << 'EOF'
|
||||
/data/vm-storage/data 192.168.11.0/24(rw,sync,no_subtree_check,no_root_squash)
|
||||
/data/vm-storage/export 192.168.11.0/24(rw,sync,no_subtree_check,no_root_squash)
|
||||
/data/bulk-storage/iso 192.168.11.0/24(rw,sync,no_subtree_check,no_root_squash)
|
||||
EOF
|
||||
|
||||
# 刷新导出表
|
||||
exportfs -ra
|
||||
|
||||
# 验证导出是否正常
|
||||
exportfs -v
|
||||
showmount -e 192.168.11.14
|
||||
|
||||
|
||||
oVirt 中存储域配置
|
||||
存储配置完成后,在 oVirt Web Portal 中按以下顺序添加存储域:
|
||||
存储 → 存储域 → 新建域
|
||||
|
||||
① 数据域(VM 磁盘,SSD)
|
||||
名称: vm-data-ssd
|
||||
域功能: 数据
|
||||
存储类型: NFS
|
||||
宿主机: node01
|
||||
导出路径: 192.168.11.14:/data/vm-storage/data
|
||||
NFS版本: V4_1(推荐)
|
||||
|
||||
② ISO 域(安装镜像,HDD)
|
||||
名称: iso-library
|
||||
域功能: ISO
|
||||
存储类型: NFS
|
||||
导出路径: 192.168.11.14:/data/bulk-storage/iso
|
||||
|
||||
③ 导出域(VM 备份/导入导出)
|
||||
名称: vm-export
|
||||
域功能: 导出
|
||||
存储类型: NFS
|
||||
导出路径: 192.168.11.14:/data/vm-storage/export
|
||||
|
||||
321
20-物理服务器虚拟机/4-2026年4月13日-操作.md
Normal file
321
20-物理服务器虚拟机/4-2026年4月13日-操作.md
Normal file
@@ -0,0 +1,321 @@
|
||||
以下是针对你服务器(Xeon Silver 4214 双路、192GB、openEuler 22.03 LTS-SP3)的完整 KVM + libvirt + Cockpit 部署方案,按操作顺序排列。
|
||||
|
||||
***
|
||||
|
||||
## 第一步:环境预检
|
||||
|
||||
```bash
|
||||
# 确认 CPU 支持 VT-x 虚拟化
|
||||
egrep -c '(vmx|svm)' /proc/cpuinfo # 结果应 > 0
|
||||
|
||||
# 确认 KVM 模块已加载(openEuler 内核已内置)
|
||||
ls /dev/kvm && ls /sys/module/kvm # 两个路径均存在即正常
|
||||
|
||||
# 查看 NUMA 拓扑(双路服务器,涉及后续内存优化)
|
||||
numactl --hardware
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## 第二步:磁盘格式化与挂载
|
||||
|
||||
你的 sda 已是系统盘不动,仅操作 sdb 和 sdc。
|
||||
|
||||
### sdb(NVMe 2.6T → VM 系统盘)
|
||||
|
||||
```bash
|
||||
# 建立 GPT 分区表,整盘一个分区
|
||||
parted /dev/sdb mklabel gpt
|
||||
parted /dev/sdb mkpart primary xfs 0% 100%
|
||||
parted /dev/sdb print # 确认分区为 /dev/sdb1
|
||||
|
||||
# 格式化为 XFS(大文件、并发读写性能最佳)
|
||||
mkfs.xfs -f -L vm-sys /dev/sdb1
|
||||
|
||||
# 创建挂载点并挂载
|
||||
mkdir -p /vm/sys
|
||||
mount /dev/sdb1 /vm/sys
|
||||
|
||||
# 写入 fstab(用 UUID,防设备名变更)
|
||||
UUID_SDB=$(blkid -s UUID -o value /dev/sdb1)
|
||||
echo "UUID=${UUID_SDB} /vm/sys xfs defaults,noatime,nodiratime 0 2" >> /etc/fstab
|
||||
```
|
||||
|
||||
### sdc(SATA 138.3T → VM 数据存储盘)
|
||||
|
||||
```bash
|
||||
# 同样 GPT + 整盘分区
|
||||
parted /dev/sdc mklabel gpt
|
||||
parted /dev/sdc mkpart primary xfs 0% 100%
|
||||
|
||||
# XFS 格式化(大容量盘推荐加 -i size=512 增大 inode 密度)
|
||||
mkfs.xfs -f -L vm-data -i size=512 /dev/sdc1
|
||||
|
||||
mkdir -p /vm/data
|
||||
mount /dev/sdc1 /vm/data
|
||||
|
||||
UUID_SDC=$(blkid -s UUID -o value /dev/sdc1)
|
||||
echo "UUID=${UUID_SDC} /vm/data xfs defaults,noatime,nodiratime 0 2" >> /etc/fstab
|
||||
|
||||
# 验证挂载
|
||||
mount -a && df -hT /vm/sys /vm/data
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## 第三步:安装 KVM 虚拟化组件
|
||||
|
||||
```bash
|
||||
# 安装核心组件 + Cockpit
|
||||
dnf install -y qemu libvirt libvirt-client virt-install \
|
||||
cockpit cockpit-machines
|
||||
|
||||
# 修改 QEMU 配置,以 root 权限运行(避免权限问题)
|
||||
sed -i 's/#user = "root"/user = "root"/' /etc/libvirt/qemu.conf
|
||||
sed -i 's/#group = "root"/group = "root"/' /etc/libvirt/qemu.conf
|
||||
|
||||
# 启动并设置开机自启
|
||||
systemctl enable --now libvirtd
|
||||
systemctl enable --now cockpit.socket
|
||||
|
||||
# 验证
|
||||
virt-host-validate # 关键项全部 PASS
|
||||
```
|
||||
|
||||
访问 Cockpit:浏览器打开 `https://192.168.11.14:9090`,用 root 账号登录,**Virtual Machines** 标签即为虚拟机管理界面。 [docs.openeuler](https://docs.openeuler.org/zh/docs/22.03_LTS/docs/Virtualization/%E5%AE%89%E8%A3%85%E8%99%9A%E6%8B%9F%E5%8C%96%E7%BB%84%E4%BB%B6.html)
|
||||
|
||||
***
|
||||
|
||||
## 第四步:网络配置(Bond → Bridge)
|
||||
|
||||
> ⚠️ **高风险操作**,修改网络可能中断 SSH。务必先执行保底命令,再执行网络变更。
|
||||
|
||||
### 保底恢复方案(必须先执行)
|
||||
|
||||
```bash
|
||||
# 安装 at 包
|
||||
dnf install -y at
|
||||
|
||||
# 启动 atd 守护进程
|
||||
systemctl enable --now atd
|
||||
|
||||
# 验证
|
||||
which at && systemctl is-active atd
|
||||
|
||||
# 60秒后自动恢复 bond0 IP(防止配置错误断网)
|
||||
at now + 2 minutes << 'EOF'
|
||||
nmcli con down br0 2>/dev/null
|
||||
ip addr add 192.168.11.14/24 dev bond0
|
||||
ip route add default via 192.168.11.1 dev bond0
|
||||
EOF
|
||||
```
|
||||
|
||||
### 创建 br0 桥接(桥接到 bond0)
|
||||
|
||||
```bash
|
||||
# 1. 创建 bridge br0
|
||||
nmcli con add type bridge ifname br0 con-name br0 \
|
||||
ipv4.addresses 192.168.11.14/24 \
|
||||
ipv4.gateway 192.168.11.1 \
|
||||
ipv4.dns "114.114.114.114 8.8.8.8" \
|
||||
ipv4.method manual \
|
||||
bridge.stp no \
|
||||
connection.autoconnect yes
|
||||
|
||||
# 2. 将 bond0 作为 br0 的从属(slave)
|
||||
nmcli con add type bridge-slave \
|
||||
ifname bond0 master br0 \
|
||||
con-name br0-slave-bond0
|
||||
|
||||
# 3. 先启动新配置(此时会短暂断连)
|
||||
nmcli con up br0
|
||||
|
||||
# 4. 验证(重连后执行)
|
||||
ip addr show br0 # 应显示 192.168.11.14/24
|
||||
|
||||
dnf install -y bridge-utils
|
||||
brctl show br0 # 应显示 bond0 为成员
|
||||
```
|
||||
|
||||
如果连通后取消保底恢复任务:
|
||||
|
||||
```bash
|
||||
atrm $(atq | awk '{print $1}')
|
||||
```
|
||||
|
||||
### 虚拟机内部网络建议
|
||||
|
||||
```bash
|
||||
# 使用 virsh 定义 bridge 类型网络(替代默认 NAT 网络)
|
||||
cat > /tmp/bridge-net.xml << 'EOF'
|
||||
<network>
|
||||
<name>bridge-net</name>
|
||||
<forward mode="bridge"/>
|
||||
<bridge name="br0"/>
|
||||
</network>
|
||||
EOF
|
||||
|
||||
virsh net-define /tmp/bridge-net.xml
|
||||
virsh net-autostart bridge-net
|
||||
virsh net-start bridge-net
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## 第五步:内核参数优化
|
||||
|
||||
这是宿主机调优的核心,写入持久化配置文件。 [blog.csdn](https://blog.csdn.net/weixin_34378969/article/details/92925024)
|
||||
|
||||
```bash
|
||||
cat > /etc/sysctl.d/99-kvm-host.conf << 'EOF'
|
||||
# ── 内存管理 ──────────────────────────────────────────────────
|
||||
# 宿主机上降低 swap 使用倾向(KVM 宿主机关键配置)
|
||||
vm.swappiness = 10
|
||||
|
||||
# 脏页回写优化(减少 IO 抖动)
|
||||
vm.dirty_ratio = 20
|
||||
vm.dirty_background_ratio = 5
|
||||
vm.dirty_writeback_centisecs = 1500
|
||||
|
||||
# 透明大页:启用(内核自动为 VM 分配,提升 TLB 命中率)
|
||||
# 注:通过 /sys 控制,sysctl 不直接设置
|
||||
|
||||
# 最大内存映射数量(VM 密集场景需增大)
|
||||
vm.max_map_count = 524288
|
||||
|
||||
# ── 网络优化 ──────────────────────────────────────────────────
|
||||
net.core.netdev_max_backlog = 250000
|
||||
net.core.rmem_max = 134217728
|
||||
net.core.wmem_max = 134217728
|
||||
net.ipv4.tcp_rmem = 4096 87380 134217728
|
||||
net.ipv4.tcp_wmem = 4096 65536 134217728
|
||||
net.ipv4.tcp_mtu_probing = 1
|
||||
|
||||
# 允许 IP 转发(虚拟机流量需要)
|
||||
net.ipv4.ip_forward = 1
|
||||
|
||||
# ── 内核调度 ──────────────────────────────────────────────────
|
||||
# NUMA 自动均衡(双路服务器开启,减少跨节点内存访问)
|
||||
kernel.numa_balancing = 1
|
||||
|
||||
# 增大 PID 上限(多 VM 场景进程数多)
|
||||
kernel.pid_max = 4194304
|
||||
EOF
|
||||
|
||||
# 立即生效
|
||||
sysctl --system
|
||||
```
|
||||
|
||||
### 透明大页(THP)配置
|
||||
|
||||
```bash
|
||||
# 将 THP 设置为 madvise 模式(应用主动申请才分配,更可控)
|
||||
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled
|
||||
echo defer+madvise > /sys/kernel/mm/transparent_hugepage/defrag
|
||||
|
||||
# 持久化(写入 rc.local 或 systemd 服务)
|
||||
cat > /etc/rc.d/rc.local << 'EOF'
|
||||
#!/bin/bash
|
||||
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled
|
||||
echo defer+madvise > /sys/kernel/mm/transparent_hugepage/defrag
|
||||
EOF
|
||||
chmod +x /etc/rc.d/rc.local
|
||||
systemctl enable rc-local
|
||||
```
|
||||
|
||||
### GRUB 内核启动参数(重启生效)
|
||||
|
||||
```bash
|
||||
# 编辑 /etc/default/grub,在 GRUB_CMDLINE_LINUX 末尾添加:
|
||||
# intel_iommu=on iommu=pt(启用 IOMMU,支持 PCIe 直通)
|
||||
# default_hugepagesz=2M hugepagesz=2M(预留 2MB 大页)
|
||||
# nohz=on(减少不必要的时钟中断)
|
||||
|
||||
sed -i 's/GRUB_CMDLINE_LINUX="\(.*\)"/GRUB_CMDLINE_LINUX="\1 intel_iommu=on iommu=pt nohz=on"/' \
|
||||
/etc/default/grub
|
||||
|
||||
grub2-mkconfig -o /boot/grub2/grub.cfg
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## 第六步:Swap 虚拟内存策略
|
||||
|
||||
你的 sda3 上已有 16G swap(来自 LVM),以下是建议策略: [ask.vmlib](https://ask.vmlib.com/how-to-configure-memory-locking-in-kvm-virtual-machines/lock-memory-kvm)
|
||||
|
||||
| 场景 | 建议 | 理由 |
|
||||
|---|---|---|
|
||||
| 当前(192GB 内存充足) | **保留但降低优先级**(`vm.swappiness=10`) | KVM 宿主机内存被 swap 换出后 VM 会卡顿,但完全关闭在内存耗尽时会触发 OOM kill |
|
||||
| VM 内存超分配(开多 VM) | **保留 swap,监控使用率** | 超分场景 swap 是最后防线 |
|
||||
| 生产高性能数据库 VM | 可关闭:`swapoff -a` + 注释 fstab | 数据库对 swap 延迟极度敏感 |
|
||||
|
||||
当前配置只需确保 `vm.swappiness=10` 已生效即可:
|
||||
|
||||
```bash
|
||||
sysctl vm.swappiness # 确认输出为 10
|
||||
free -h # 查看 swap 使用状态
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## 第七步:libvirt 存储池配置
|
||||
|
||||
```bash
|
||||
# 定义 sdb 存储池(VM 系统盘)
|
||||
virsh pool-define-as vm-sys dir --target /vm/sys
|
||||
virsh pool-autostart vm-sys
|
||||
virsh pool-start vm-sys
|
||||
|
||||
# 定义 sdc 存储池(VM 数据盘)
|
||||
virsh pool-define-as vm-data dir --target /vm/data
|
||||
virsh pool-autostart vm-data
|
||||
virsh pool-start vm-data
|
||||
|
||||
# 验证
|
||||
virsh pool-list --all
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## 第八步:创建第一台虚拟机示例
|
||||
|
||||
```bash
|
||||
# 在 sdb 池中创建 100G 系统盘镜像
|
||||
virsh vol-create-as vm-sys centos8.qcow2 100G --format qcow2
|
||||
|
||||
# 用 virt-install 创建 VM(以 ISO 安装为例)
|
||||
virt-install \
|
||||
--name vm01 \
|
||||
--memory 16384 \
|
||||
--vcpus 8,sockets=1,cores=8,threads=1 \
|
||||
--cpu host-model \
|
||||
--disk vol=vm-sys/centos8.qcow2,bus=virtio,cache=writeback \
|
||||
--network bridge=br0,model=virtio \
|
||||
--graphics vnc,listen=0.0.0.0,port=5901 \
|
||||
--video virtio \
|
||||
--os-variant centos8 \
|
||||
--cdrom /vm/data/iso/CentOS-8.iso \
|
||||
--noautoconsole
|
||||
|
||||
# 连接 VNC 查看安装过程(本地端口转发后用 VNC 客户端连接)
|
||||
# ssh -L 5901:127.0.0.1:5901 root@192.168.11.14
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## 部署完成检查清单
|
||||
|
||||
```bash
|
||||
# 全面验证宿主机状态
|
||||
echo "=== KVM ===" && ls /dev/kvm
|
||||
echo "=== libvirtd ===" && systemctl is-active libvirtd
|
||||
echo "=== Cockpit ===" && systemctl is-active cockpit.socket
|
||||
echo "=== 存储池 ===" && virsh pool-list --all
|
||||
echo "=== 网络 ===" && virsh net-list --all
|
||||
echo "=== 桥接 ===" && brctl show
|
||||
echo "=== 磁盘挂载 ===" && df -hT /vm/sys /vm/data
|
||||
echo "=== THP ===" && cat /sys/kernel/mm/transparent_hugepage/enabled
|
||||
echo "=== swappiness ===" && sysctl vm.swappiness
|
||||
```
|
||||
|
||||
所有输出正常后,通过浏览器 `https://192.168.11.14:9090` 的 Cockpit 控制台即可进行日常虚拟机管理,无需命令行。 [cloud.tencent](https://cloud.tencent.com/developer/article/2417991)
|
||||
67
20-物理服务器虚拟机/5-2026年4月13日-虚拟机创建.md
Normal file
67
20-物理服务器虚拟机/5-2026年4月13日-虚拟机创建.md
Normal file
@@ -0,0 +1,67 @@
|
||||
我现在再openEuler22.03 (LTS-SP3)安装了cockpit-178-17.oe2203sp3.src.rpm
|
||||
|
||||
virsh pool已经设置
|
||||
sdb 存储池(VM 系统盘) /vm/sys
|
||||
sdc 存储池(VM 数据盘) /vm/data
|
||||
|
||||
网卡信息如下
|
||||
[root@wdd-phy-11-14 data]# ip addr
|
||||
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
|
||||
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
|
||||
inet 127.0.0.1/8 scope host lo
|
||||
valid_lft forever preferred_lft forever
|
||||
inet6 ::1/128 scope host
|
||||
valid_lft forever preferred_lft forever
|
||||
2: eno3: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc mq state DOWN group default qlen 1000
|
||||
link/ether bc:16:95:28:6e:6f brd ff:ff:ff:ff:ff:ff
|
||||
3: eno4: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc mq state DOWN group default qlen 1000
|
||||
link/ether bc:16:95:28:6e:70 brd ff:ff:ff:ff:ff:ff
|
||||
4: ens5f0np0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc mq state DOWN group default qlen 1000
|
||||
link/ether bc:16:95:25:12:43 brd ff:ff:ff:ff:ff:ff
|
||||
5: ens5f1np1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
|
||||
link/ether bc:16:95:25:12:44 brd ff:ff:ff:ff:ff:ff
|
||||
6: ens6f0np0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc mq state DOWN group default qlen 1000
|
||||
link/ether bc:16:95:25:6b:5f brd ff:ff:ff:ff:ff:ff
|
||||
7: ens6f1np1: <BROADCAST,MULTICAST,SLAVE,UP,LOWER_UP> mtu 1500 qdisc mq master bond0 state UP group default qlen 1000
|
||||
link/ether bc:16:95:25:6b:60 brd ff:ff:ff:ff:ff:ff
|
||||
8: br0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default qlen 1000
|
||||
link/ether fa:97:af:82:90:0f brd ff:ff:ff:ff:ff:ff
|
||||
inet 192.168.11.14/24 brd 192.168.11.255 scope global noprefixroute br0
|
||||
valid_lft forever preferred_lft forever
|
||||
9: bond0: <BROADCAST,MULTICAST,MASTER,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
|
||||
link/ether bc:16:95:25:6b:60 brd ff:ff:ff:ff:ff:ff
|
||||
inet 192.168.11.14/24 brd 192.168.11.255 scope global noprefixroute bond0
|
||||
valid_lft forever preferred_lft forever
|
||||
inet6 fe80::b027:c114:f952:dd90/64 scope link noprefixroute
|
||||
valid_lft forever preferred_lft forever
|
||||
10: virbr0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default qlen 1000
|
||||
link/ether 52:54:00:1f:04:83 brd ff:ff:ff:ff:ff:ff
|
||||
inet 192.168.122.1/24 brd 192.168.122.255 scope global virbr0
|
||||
valid_lft forever preferred_lft forever
|
||||
11: virbr0-nic: <BROADCAST,MULTICAST> mtu 1500 qdisc fq_codel master virbr0 state DOWN group default qlen 1000
|
||||
link/ether 52:54:00:1f:04:83 brd ff:ff:ff:ff:ff:ff
|
||||
|
||||
我有一个操作系统的ISO文件 /vm/data/ubuntu-22.04.5-live-server-amd64.iso 请你给出虚拟机的创建命令 虚拟机的IP地址应该为 192.168.11.158 与现在的192.168.11.14处于同一CIDR中 虚拟机系统盘为20GB,数据盘为40GB
|
||||
|
||||
请给出命令行创建这台虚拟机,我需要能够追踪设置虚拟机的安装过程,请给出方法
|
||||
|
||||
|
||||
我现在再openEuler22.03 (LTS-SP3)安装了cockpit-178-17.oe2203sp3.src.rpm
|
||||
启动虚拟机的命令如下
|
||||
virt-install \
|
||||
--name ubuntu2204-vm \
|
||||
--vcpus 4 \
|
||||
--ram 8192 \
|
||||
--cpu mode=host-passthrough \
|
||||
--os-variant ubuntu21.04 \
|
||||
--disk path=/vm/sys/ubuntu2204-sys.qcow2,format=qcow2,bus=virtio \
|
||||
--disk path=/vm/data/ubuntu2204-data.qcow2,format=qcow2,bus=virtio \
|
||||
--cdrom /vm/data/ubuntu-22.04.5-live-server-amd64.iso \
|
||||
--network bridge=br0,model=virtio \
|
||||
--graphics vnc,listen=127.0.0.1 \
|
||||
--video vga \
|
||||
--console pty,target_type=serial \
|
||||
--noautoconsole \
|
||||
--autostart
|
||||
|
||||
我现在不想要远程VNC 想要使用控制网页上面自带的VNC,请你给出合适的修改意见
|
||||
37
20-物理服务器虚拟机/6-2026年4月13日-虚拟机创建.sh
Normal file
37
20-物理服务器虚拟机/6-2026年4月13日-虚拟机创建.sh
Normal file
@@ -0,0 +1,37 @@
|
||||
# 将 bond0 加入 br0 桥
|
||||
nmcli con modify bond0 master br0 slave-type bridge
|
||||
nmcli con up br0
|
||||
|
||||
# 或用 ip 命令(重启后失效,仅测试用)
|
||||
ip addr del 192.168.11.14/24 dev bond0
|
||||
ip link set bond0 master br0
|
||||
ip link set br0 up
|
||||
|
||||
|
||||
# 系统盘 20GB → /vm/sys
|
||||
virsh vol-create-as vm-sys ubuntu2204-sys.qcow2 20G --format qcow2
|
||||
|
||||
# 数据盘 40GB → /vm/data
|
||||
virsh vol-create-as vm-data ubuntu2204-data.qcow2 40G --format qcow2
|
||||
|
||||
qemu-img create -f qcow2 /vm/sys/ubuntu2204-sys.qcow2 20G
|
||||
qemu-img create -f qcow2 /vm/data/ubuntu2204-data.qcow2 40G
|
||||
|
||||
|
||||
# --graphics vnc,listen=0.0.0.0,port=5910,password=Passw0rd \
|
||||
|
||||
virt-install \
|
||||
--name ubuntu2204-vm \
|
||||
--vcpus 4 \
|
||||
--ram 8192 \
|
||||
--cpu mode=host-passthrough \
|
||||
--os-variant ubuntu21.04 \
|
||||
--disk path=/vm/sys/ubuntu2204-sys.qcow2,format=qcow2,bus=virtio \
|
||||
--disk path=/vm/data/ubuntu2204-data.qcow2,format=qcow2,bus=virtio \
|
||||
--cdrom /vm/data/ubuntu-22.04.5-live-server-amd64.iso \
|
||||
--network bridge=br0,model=virtio \
|
||||
--graphics vnc,listen=127.0.0.1 \
|
||||
--video vga \
|
||||
--console pty,target_type=serial \
|
||||
--noautoconsole \
|
||||
--autostart
|
||||
30
20-物理服务器虚拟机/7-虚拟机管理指南.md
Normal file
30
20-物理服务器虚拟机/7-虚拟机管理指南.md
Normal file
@@ -0,0 +1,30 @@
|
||||
我现在有一台物理服务器 操作系统为openEuler22.03 (LTS-SP3) 虚拟化软件为cockpit-178-17.oe2203sp3.src.rpm
|
||||
|
||||
配置如下 Intel(R) Xeon(R) Silver 4214 CPU @ 2.20GHz 2颗,192G内存,447G SSD,2.6T NVMe,138T SATA
|
||||
|
||||
virsh pool已经设置
|
||||
sdb 存储池(VM 系统盘)2.6T NVMe /vm/sys vm-sys
|
||||
sdc 存储池(VM 数据盘)138T SATA /vm/data vm-data
|
||||
|
||||
虚拟机的网卡设置为绑定br0 虚拟机的网段为192.168.11.x/24 网关为192.168.11.1
|
||||
|
||||
|
||||
我现在需要一套方案,请在linux环境实现python3的脚本,实现如下的内容
|
||||
1. windows虚拟机管理
|
||||
1. 我已经创建了windows server 2022 standard作为模板
|
||||
2. 需要通过脚本参数 设置主机的的IP地址
|
||||
2. 需要考虑自动设置计算机名
|
||||
3. 数据盘不需要复制,直接新建
|
||||
4. 新建主机名称 包含IP地址信息
|
||||
7. 需要修改主机的硬件信息
|
||||
3. linux主机关系
|
||||
1. 虚拟机的模板我已经创建
|
||||
2. 需要支持ubuntu 22.04 centos 22.03等操作系统
|
||||
3. 需要修改主机的硬件信息
|
||||
4. 需要设置主机的计算机名
|
||||
5. 考虑数据盘解绑的问题
|
||||
6. 可以通过参数修改主机的静态IP地址
|
||||
4. 虚拟机管理
|
||||
1. 系统盘和数据盘与虚拟机主机之间的关系需要能够查看
|
||||
2. 运行中的虚拟机的IP地址能够获取并查看
|
||||
3. 能够一键复制虚拟机
|
||||
94
20-物理服务器虚拟机/物理服务器信息.md
Normal file
94
20-物理服务器虚拟机/物理服务器信息.md
Normal file
@@ -0,0 +1,94 @@
|
||||
[root@localhost ~]# lscpu
|
||||
Architecture: x86_64
|
||||
CPU op-mode(s): 32-bit, 64-bit
|
||||
Address sizes: 46 bits physical, 48 bits virtual
|
||||
Byte Order: Little Endian
|
||||
CPU(s): 48
|
||||
On-line CPU(s) list: 0-47
|
||||
Vendor ID: GenuineIntel
|
||||
BIOS Vendor ID: Intel(R) Corporation
|
||||
Model name: Intel(R) Xeon(R) Silver 4214 CPU @ 2.20GHz
|
||||
BIOS Model name: Intel(R) Xeon(R) Silver 4214 CPU @ 2.20GHz CPU @ 2.2GHz
|
||||
BIOS CPU family: 179
|
||||
CPU family: 6
|
||||
Model: 85
|
||||
Thread(s) per core: 2
|
||||
Core(s) per socket: 12
|
||||
Socket(s): 2
|
||||
Stepping: 7
|
||||
CPU(s) scaling MHz: 33%
|
||||
CPU max MHz: 3200.0000
|
||||
CPU min MHz: 1000.0000
|
||||
|
||||
[root@localhost ~]# lsmem
|
||||
RANGE SIZE STATE REMOVABLE BLOCK
|
||||
0x0000000000000000-0x000000007fffffff 2G online yes 0
|
||||
0x0000000100000000-0x000000307fffffff 190G online yes 2-96
|
||||
|
||||
Memory block size: 2G
|
||||
Total online memory: 192G
|
||||
Total offline memory: 0B
|
||||
[root@localhost ~]# lsblk
|
||||
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
|
||||
sda 8:0 0 447.1G 0 disk
|
||||
├─sda1 8:1 0 600M 0 part /boot/efi
|
||||
├─sda2 8:2 0 1G 0 part /boot
|
||||
└─sda3 8:3 0 445.5G 0 part
|
||||
├─openeuler-root 253:0 0 200G 0 lvm /
|
||||
├─openeuler-swap 253:1 0 16G 0 lvm [SWAP]
|
||||
└─openeuler-home 253:2 0 229.5G 0 lvm /home
|
||||
sdb 8:16 0 2.6T 0 disk
|
||||
sdc 8:32 0 138.3T 0 disk
|
||||
sr0 11:0 1 1024M 0 rom
|
||||
|
||||
[root@localhost ~]# cat /etc/os-release
|
||||
NAME="openEuler"
|
||||
VERSION="22.03 (LTS-SP3)"
|
||||
ID="openEuler"
|
||||
VERSION_ID="22.03"
|
||||
PRETTY_NAME="openEuler 22.03 (LTS-SP3)"
|
||||
ANSI_COLOR="0;31"
|
||||
|
||||
[root@localhost ~]# uname -a
|
||||
Linux localhost.localdomain 5.10.0-182.0.0.95.oe2203sp3.x86_64 #1 SMP Sat Dec 30 13:10:36 CST 2023 x86_64 x86_64 x86_64 GNU/Linux
|
||||
|
||||
[root@localhost ~]# ip addr
|
||||
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
|
||||
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
|
||||
inet 127.0.0.1/8 scope host lo
|
||||
valid_lft forever preferred_lft forever
|
||||
inet6 ::1/128 scope host noprefixroute
|
||||
valid_lft forever preferred_lft forever
|
||||
2: eno3: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc mq state DOWN group default qlen 1000
|
||||
link/ether bc:16:95:28:6e:6f brd ff:ff:ff:ff:ff:ff
|
||||
3: ens5f0np0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc mq state DOWN group default qlen 1000
|
||||
link/ether bc:16:95:25:12:43 brd ff:ff:ff:ff:ff:ff
|
||||
4: eno4: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc mq state DOWN group default qlen 1000
|
||||
link/ether bc:16:95:28:6e:70 brd ff:ff:ff:ff:ff:ff
|
||||
5: ens5f1np1: <BROADCAST,MULTICAST,SLAVE,UP,LOWER_UP> mtu 1500 qdisc mq master bond0 state UP group default qlen 1000
|
||||
link/ether bc:16:95:25:12:44 brd ff:ff:ff:ff:ff:ff
|
||||
6: ens6f0np0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc mq state DOWN group default qlen 1000
|
||||
link/ether bc:16:95:25:6b:5f brd ff:ff:ff:ff:ff:ff
|
||||
7: ens6f1np1: <BROADCAST,MULTICAST,SLAVE,UP,LOWER_UP> mtu 1500 qdisc mq master bond0 state UP group default qlen 1000
|
||||
link/ether bc:16:95:25:12:44 brd ff:ff:ff:ff:ff:ff permaddr bc:16:95:25:6b:60
|
||||
8: bond0: <BROADCAST,MULTICAST,MASTER,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
|
||||
link/ether bc:16:95:25:12:44 brd ff:ff:ff:ff:ff:ff
|
||||
inet 192.168.11.14/24 brd 192.168.11.255 scope global noprefixroute bond0
|
||||
valid_lft forever preferred_lft forever
|
||||
inet6 fe80::be16:95ff:fe25:1244/64 scope link proto kernel_ll
|
||||
valid_lft forever preferred_lft forever
|
||||
|
||||
我有一台物理服务的相关信息如上
|
||||
|
||||
如下基本条件:
|
||||
1. 无法使用物理机的管理平台,无法使用物理机管理的VNC
|
||||
2. 宿主机操作系统尽量不会要更换,有操作系统审计
|
||||
3. 端口是全开放的
|
||||
4. 尽量避免破坏操作系统,重装非常麻烦
|
||||
5. sda为SSD,sdb为NVMe,sdc为SATA
|
||||
|
||||
需求如下:
|
||||
1. sda为宿主机系统盘,sdb为虚拟机系统盘,sdc为虚拟机存储盘
|
||||
2. 研究合适的虚拟机软件,尽量选择oVirit,之前我尝试在openEuler24.03上安装oVirit导致操作系统损坏
|
||||
3. 不需要磁盘格式化的操作,我已经有了
|
||||
|
||||
Reference in New Issue
Block a user