超大量更新

This commit is contained in:
zeaslity
2026-04-29 09:46:36 +08:00
parent ed945abdf1
commit e7c301023c
349 changed files with 83923 additions and 560 deletions

View 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 确认
```
> 安装过程约下载 **500MB1GB** 的包,根据网速需要 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
```
> **说明**:清理仓库后已安装的包不受影响,仅禁止后续从这些源拉取新包,符合"安装完成后去除"的要求。

View 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
}

View 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())

View 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 查询

View 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/NVMeROTA=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 通自己

View 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
# 格式化为 XFSSSD 优化参数)
mkfs.xfs -f \
-d agcount=8 \
-l size=128m \
/dev/sdb1
# 创建挂载点
mkdir -p /data/vm-storage
# 写入 fstabSSD 加 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:36vdsm: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

View 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。
### sdbNVMe 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
```
### sdcSATA 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)

View 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请你给出合适的修改意见

View 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

View 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 SSD2.6T NVMe138T 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. 能够一键复制虚拟机

View 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为SSDsdb为NVMesdc为SATA
需求如下:
1. sda为宿主机系统盘sdb为虚拟机系统盘sdc为虚拟机存储盘
2. 研究合适的虚拟机软件尽量选择oVirit之前我尝试在openEuler24.03上安装oVirit导致操作系统损坏
3. 不需要磁盘格式化的操作,我已经有了