提交 584cfe65 authored 作者: 陈泽健's avatar 陈泽健

feat(skills): 新增X86/ARM集群监控、Cockpit-KVM部署三大skill,补充集群部署文档与脚本

新增skill:
- X86-Cluster-Monitor: 5网段四台麒麟V10服务器三机热备集群夜间监控,22:00-09:00每2h执行
- ARM-CLUSTER-MONITOR: 9网段四台ARM架构服务器集群监控
- Cockpit-KVM-Deploy: Cockpit平台KVM虚拟机自动化部署

补充文档与脚本:
- EMQX/FastDFS/Nginx负载均衡/Nacos集群部署指南
- KVM虚拟机创建操作指南
- 达梦数据库导入脚本、EMQX集群部署/监控脚本
- 远程部署辅助脚本
Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
上级 b151dba1
此差异已折叠。
---
name: Cockpit-KVM-Deploy
description: CentOS 7服务器Cockpit虚拟化管理平台部署 - 修复yum源、安装Cockpit+插件、配置桥接网络、存储池,使服务器具备Web VM管理能力
---
CentOS 7 服务器上部署 Cockpit Web 虚拟化管理平台(功能对标 Proxmox VE),包括修复 yum 源、安装 Cockpit + cockpit-machines、配置桥接网络、配置存储池和防火墙。
## 使用场景
1. **新服务器**:在已有 CentOS 7 的物理服务器上快速搭建 Web 虚拟化管理平台
2. **迁移部署**:其他服务器需要相同能力时复用此 skill
3. **故障恢复**:服务异常后快速重建
## Usage
```
/Cockpit-KVM-Deploy <服务器IP> [root密码]
```
不传参数则交互确认服务器信息。默认参数:
- SSH 用户:`root`
- 密码:`Ubains@123`
- 服务器 IP:`192.168.5.3`
## 前置条件
| 条件 | 要求 |
|------|------|
| 操作系统 | CentOS 7 x86_64 |
| CPU | 支持 Intel VT-x / AMD-V(`vmx``svm` 标志) |
| 内存 | ≥ 4G(建议 ≥ 32G 以支持 VM) |
| 磁盘 | ≥ 20G 空闲 |
| 网络 | 至少一个物理网口,有静态 IP |
| KVM | `kvm``kvm_intel`/`kvm_amd` 模块已加载 |
## 部署架构
```
Cockpit Web 面板 (端口 9090)
├── cockpit-machines → VM 创建/管理/控制台
├── cockpit-storaged → 磁盘/存储池管理
└── cockpit-networkmanager → 网络配置
底层: libvirt + QEMU/KVM
网络: br0 桥接 (VM 直通物理网络)
存储: /var/lib/libvirt/images/
```
## 执行步骤
### Step 1:修复 yum 源
CentOS 7 EOL 后 mirrorlist 下线,必须改为 vault.centos.org 归档源。
```python
# 备份原 repo
cp /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.bak
# 写入 vault 源
cat > /etc/yum.repos.d/CentOS-Base.repo << 'REPOEOF'
[base]
name=CentOS-7 - Base
baseurl=http://vault.centos.org/centos/7/os/$basearch/
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7
[updates]
name=CentOS-7 - Updates
baseurl=http://vault.centos.org/centos/7/updates/$basearch/
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7
[extras]
name=CentOS-7 - Extras
baseurl=http://vault.centos.org/centos/7/extras/$basearch/
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7
REPOEOF
# 清理缓存
yum clean all && yum makecache
```
安装 EPEL(也需改 archives 源):
```bash
yum install -y epel-release
sed -i "s|^metalink=|#metalink=|g" /etc/yum.repos.d/epel*.repo
sed -i "s|^#baseurl=http://download.fedoraproject.org/pub/epel|baseurl=https://archives.fedoraproject.org/pub/archive/epel|g" /etc/yum.repos.d/epel.repo
```
### Step 2:安装 Cockpit + 插件
```bash
yum install -y cockpit cockpit-machines cockpit-storaged
systemctl enable --now cockpit.socket
```
Cockpit 是 socket-activated 的,`systemctl status cockpit` 显示 `inactive (dead)` 是正常的(有人访问才启动 woker)。
### Step 3:安装 virt-install(关键,缺了 Create VM 按钮灰色)
```bash
yum install -y virt-install
```
**这是最容易漏的步骤。** Cockpit 的 Create VM 功能依赖 `virt-install` 命令行工具,缺了按钮灰显、hover 提示 "virt-install not available"。
### Step 4:配置桥接网络
> ⚠️ **安全铁律**:只使用空闲网卡创建桥接,绝不动管理网卡!
1. 检查网卡状态:`nmcli connection show` + `ip link show`
2. 找空闲网卡(`state DOWN`、无 IP 的),将其物理接入交换机
3. 创建桥接:
```bash
# 创建桥接 br0(不配 IP,纯二层)
nmcli connection add type bridge autoconnect yes con-name br0 ifname br0 bridge.stp no
# 把空闲网卡加入桥接
nmcli connection modify <空闲网卡> connection.slave-type bridge master br0
# 启用
nmcli connection up br0
nmcli connection up <空闲网卡>
```
4. 验证:`brctl show br0` 应有对应 interface
### Step 5:配置防火墙和存储池
```bash
# 防火墙开放 Cockpit
firewall-cmd --permanent --add-service=cockpit 2>/dev/null
firewall-cmd --reload 2>/dev/null
# 配置 libvirt 默认存储池
virsh pool-define-as default dir --target /var/lib/libvirt/images
virsh pool-start default
virsh pool-autostart default
```
## 验证方法
```bash
# 1. Cockpit 服务
systemctl is-active cockpit.socket
# 2. 端口监听
ss -tlnp | grep 9090
# 3. Web 可达
curl -sk https://localhost:9090/ | head -3
# 4. bridge 正常
brctl show br0
# 5. libvirt 存储池
virsh pool-list --all
# 6. virt-install 可用
virt-install --version
```
浏览器打开 `https://<服务器IP>:9090`,用 root 登录后进入「虚拟机」页面,**Create VM 按钮应为可点击状态(非灰色)**
## 🔧 实战教训
### 教训1:eno1 被搞掉导致失联
**现象**:在 eno1(管理网卡)上创建桥接,`nmcli connection down eno1` 后 SSH 断开,br0 没 up,服务器彻底失联。
**根因**:动了正在使用的管理网卡。br0 上配静态 IP 和宿主网卡桥接需要精确的配置顺序,远程操作风险极高。
**对策**
- **永不碰管理网卡**:用空闲网卡做桥接
- **bro 不配 IP**:纯二层桥接,不设 IP/Gateway/DNS,降低配置复杂度
- **若只有一个网口**:先用 NAT(virbr0),后面再找机会加物理网口
### 教训2:virt-install 未安装 → Create VM 按钮灰色
**现象**:Cockpit 虚拟机页面 Create VM 按钮灰显,hover 提示 "virt-install not available"。
**根因**:CentOS 7 最小化安装不包含 `virt-install` 包,cockpit-machines 依赖它执行实际的 VM 创建。
**对策**`yum install -y virt-install`
### 教训3:EPEL 源也需改为 archives
**现象**`yum install epel-release``yum makecache` 报 "Cannot find a valid baseurl for repo: epel"。
**根因**:EPEL 7 的 mirrorlist 也随 CentOS 7 EOL 停止服务。
**对策**:EPEL 也需改为 `https://archives.fedoraproject.org/pub/archive/epel/7/$basearch/`
## 关联
- 操作指南文档:`Docs/虚拟机创建操作指南.md`
- 服务器参考:192.168.5.3(联想 ThinkServer TD340,125G/1.8T/8核)
- 网盘上传模块:`UploadSelfCheck`(可用于传递 ISO)
## 参考服务器
| 服务器 | IP | SSH | 用途 |
|--------|-----|-----|------|
| 默认目标 | 192.168.5.3 | root/Ubains@123 | 本次已部署 |
## 命令速查(paramiko Python 连接模板)
```python
import paramiko
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect('<服务器IP>', port=22, username='root', password='<密码>', timeout=10)
def run(cmd):
stdin, stdout, stderr = client.exec_command(cmd)
out = stdout.read().decode('utf-8', errors='replace')
err = stderr.read().decode('utf-8', errors='replace')
return out.strip(), err.strip()
```
此差异已折叠。
{
"_comment": "=== X86集群监控配置 === 麒麟V10三机热备集群(节点1-3)+ 达梦数据库节点(节点4)",
"_updated_at": "2026-07-09",
"cluster_info": {
"name": "新统一平台X86集群",
"architecture": "X86",
"os": "麒麟V10",
"mode": "三机热备",
"purpose": "测试环境",
"node_count": 4
},
"servers": [
{
"id": 1,
"name": "X86集群节点1",
"ip": "192.168.5.41",
"port": 22,
"username": "openkylin",
"password": "Ubains@123",
"use_sudo": true,
"role": "master",
"cpu_cores": 8,
"memory_gb": 16,
"disk_gb": 80,
"containers": ["ujava2", "utengine", "umysql", "redis-master", "redis-sentinel", "uemqx", "unacos", "utracker", "paperless"],
"services": {
"mysql": {"port": 8306, "type": "master"},
"redis": {"port": 6379, "type": "master"},
"emqx": {"port": 1883, "dashboard_port": 18083},
"nacos": {"port": 8848}
},
"note": "主节点,承担核心业务"
},
{
"id": 2,
"name": "X86集群节点2",
"ip": "192.168.5.42",
"port": 22,
"username": "openkylin",
"password": "Ubains@123",
"use_sudo": true,
"role": "slave1",
"cpu_cores": 8,
"memory_gb": 16,
"disk_gb": 80,
"containers": ["ujava2", "utengine", "umysql", "redis-slave-1", "redis-sentinel-2", "uemqx", "unacos", "utracker", "paperless"],
"services": {
"redis": {"port": 6379, "type": "slave"}
},
"note": "从节点1"
},
{
"id": 3,
"name": "X86集群节点3",
"ip": "192.168.5.43",
"port": 22,
"username": "openkylin",
"password": "Ubains@123",
"use_sudo": true,
"role": "slave2",
"cpu_cores": 8,
"memory_gb": 16,
"disk_gb": 80,
"containers": ["ujava2", "utengine", "umysql", "redis-slave-2", "redis-sentinel-3", "uemqx", "unacos", "utracker", "paperless"],
"services": {
"redis": {"port": 6379, "type": "slave"}
},
"note": "从节点2"
},
{
"id": 4,
"name": "X86集群节点4",
"ip": "192.168.5.40",
"port": 22,
"username": "openkylin",
"password": "Ubains@123",
"use_sudo": true,
"role": "dm8-server",
"cpu_cores": 8,
"memory_gb": 8,
"disk_gb": 60,
"containers": ["dm8-server", "utengine"],
"services": {
"dm8": {"port": 5236}
},
"note": "达梦数据库存储使用"
}
],
"cluster_services": {
"redis_cluster": {
"mode": "1主2从3哨兵",
"master": "192.168.5.41",
"slaves": ["192.168.5.42", "192.168.5.43"],
"sentinels": ["192.168.5.41", "192.168.5.42", "192.168.5.43"],
"password": "Ubains@123"
},
"emqx_cluster": {
"mode": "3节点集群",
"nodes": ["192.168.5.41", "192.168.5.42", "192.168.5.43"],
"dashboard_username": "admin",
"dashboard_password": "public"
},
"nacos_cluster": {
"mode": "3节点集群",
"nodes": ["192.168.5.41", "192.168.5.42", "192.168.5.43"],
"console_username": "nacos",
"console_password": "nacos"
},
"fastdfs_cluster": {
"tracker_nodes": ["192.168.5.41", "192.168.5.42", "192.168.5.43"],
"storage_nodes": ["192.168.5.41", "192.168.5.42", "192.168.5.43"]
}
},
"middleware_credentials": {
"mysql": {
"username": "root",
"password": "Ubains@123",
"port": 8306
},
"redis": {
"password": "Ubains@123",
"port": 6379
},
"emqx": {
"dashboard_username": "admin",
"dashboard_password": "public",
"dashboard_port": 18083
},
"nacos": {
"username": "nacos",
"password": "nacos",
"port": 8848
}
},
"thresholds": {
"cpu_warning": 80,
"cpu_critical": 95,
"memory_warning": 85,
"memory_critical": 95,
"disk_warning": 90,
"disk_critical": 95,
"inode_warning": 90,
"container_restart_warning": 3,
"container_restart_critical": 5,
"log_error_warning": 10,
"log_error_critical": 50
},
"schedule": {
"window": "22:00-09:00",
"interval": "2h",
"cron": "0 22,0,2,4,6,8 * * *",
"execution_times": ["22:00", "00:00", "02:00", "04:00", "06:00", "08:00"]
},
"report": {
"output_dir": "AuxiliaryTool/ScriptTool/ClusterMonitor/reports",
"filename_format": "cluster_monitor_{YYYYMMDD_HHMMSS}.md",
"history_days": 30
}
}
\ No newline at end of file
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
阶段4补充:下载激活文件 - 获取当前服务器机器码
"""
import time, os
from playwright.sync_api import sync_playwright
HOST = "192.168.5.70"
ADMIN_USER = "superadmin"
ADMIN_PASS = "Ubains@1357"
CAPTCHA = "csba"
SCREEN_DIR = r"E:\GithubData\ubains-module-test\AuxiliaryTool\ScriptTool\RemoteDeploy\screenshots"
os.makedirs(SCREEN_DIR, exist_ok=True)
def log(msg):
try:
print(f"[{time.strftime('%H:%M:%S')}] {msg}", flush=True)
except:
print(f"[{time.strftime('%H:%M:%S')}] {msg.encode('gbk','replace').decode('gbk')}", flush=True)
def js_click(page, el, desc=""):
try:
el.evaluate("el => el.click()")
log(f" JS-click: {desc}")
return True
except Exception as e:
log(f" Click-fail: {desc} - {e}")
return False
def ss(page, name):
page.screenshot(path=os.path.join(SCREEN_DIR, f"dl_{name}.png"))
def main():
log("========== Download Activation File ==========")
with sync_playwright() as p:
browser = p.chromium.launch(headless=False, args=['--ignore-certificate-errors'])
context = browser.new_context(ignore_https_errors=True, viewport={'width': 1920, 'height': 1080})
page = context.new_page()
page.set_default_timeout(20000)
try:
# Login
log("Step 1: Login")
page.goto(f"https://{HOST}/#/LoginConfig", timeout=60000, wait_until='domcontentloaded')
page.wait_for_timeout(5000)
if 'backend' not in page.url:
inputs = page.query_selector_all('input')
if len(inputs) >= 1:
inputs[0].fill(ADMIN_USER)
if len(inputs) >= 2:
inputs[1].fill(ADMIN_PASS)
if len(inputs) >= 3:
inputs[2].fill(CAPTCHA)
page.wait_for_timeout(500)
submit = page.query_selector('input[type="submit"]')
if submit:
js_click(page, submit, "Login")
page.wait_for_timeout(12000)
log(f" After login: {page.url}")
ss(page, "01_logged_in")
page.wait_for_timeout(3000)
# Click "下载激活文件"
log("Step 2: Click 'Download Activation File'")
download_btn = page.query_selector('button:has-text("下载激活文件")')
if download_btn:
js_click(page, download_btn, "Download activation file")
page.wait_for_timeout(3000)
ss(page, "02_dialog")
# Fill identity verification dialog
log(" Filling identity verification dialog...")
pwd = page.query_selector('.el-dialog__wrapper:not([style*="display: none"]) input[type="password"]')
if pwd:
pwd.fill(ADMIN_PASS)
log(" Password filled")
for ci in page.query_selector_all('.el-dialog__wrapper:not([style*="display: none"]) input[type="text"]'):
if ci.get_attribute('disabled') is not None:
continue
ph = ci.get_attribute('placeholder') or ''
if '验证' in ph:
ci.fill(CAPTCHA)
log(" Captcha filled")
break
ss(page, "03_filled")
# Set up download listener
log(" Setting up download listener...")
try:
with page.expect_download(timeout=15000) as download_info:
confirm = page.query_selector('.el-dialog__wrapper:not([style*="display: none"]) button:has-text("确定")')
if not confirm:
confirm = page.query_selector('.el-dialog__wrapper:not([style*="display: none"]) .el-button--primary')
if confirm:
js_click(page, confirm, "Confirm download")
else:
page.keyboard.press('Enter')
download = download_info.value
download_path = os.path.join(SCREEN_DIR, "activation_file.zip")
download.save_as(download_path)
log(f" ✅ Activation file downloaded to: {download_path}")
log(f" Filename: {download.suggested_filename}")
except Exception as e:
log(f" ⚠️ Download not triggered: {e}")
page.wait_for_timeout(5000)
ss(page, "04_after_download_attempt")
# Check for any dialog/message
try:
msgs = page.query_selector_all('.el-message, .el-notification')
for m in msgs:
txt = (m.inner_text() or '')[:300]
if txt.strip():
log(f" Message: {txt}")
except:
pass
else:
log(" ❌ 'Download Activation File' button not found!")
ss(page, "99_final")
except Exception as e:
log(f"ERROR: {e}")
ss(page, "99_error")
import traceback
traceback.print_exc()
finally:
page.wait_for_timeout(2000)
browser.close()
if __name__ == '__main__':
main()
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
ARM麒麟V10部署脚本包装器
严格按照需求文档执行部署操作
"""
import paramiko
import time
import sys
def deploy_arm_kylin():
"""执行ARM麒麟V10部署"""
# SSH连接配置
print('=' * 60)
print('ARM麒麟V10自动化部署开始')
print('目标服务器: 192.168.9.70')
print('部署时间: 2026-06-30')
print('=' * 60)
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
print('\n[步骤1] SSH连接服务器...')
ssh.connect('192.168.9.70', username='openkylin', password='Ubains@123', timeout=30)
print('连接成功')
print('\n[步骤2] 切换到root用户...')
channel = ssh.invoke_shell()
time.sleep(1)
# 切换到root
channel.send('sudo su -\n')
time.sleep(2)
channel.send('Ubains@123\n')
time.sleep(2)
# 设置环境变量
print('\n[步骤3] 设置TERM=dumb环境变量...')
channel.send('export TERM=dumb\n')
time.sleep(1)
# 进入部署目录
print('\n[步骤4] 进入部署目录...')
channel.send('cd /data/arm_offline_auto_unifiedPlatform\n')
time.sleep(1)
# 执行部署脚本
print('\n[步骤5] 开始执行部署脚本(预计40分钟)...')
print('自动应答: 7个y + 1个n(无企业NTP服务器)')
# 使用管道输入自动应答
channel.send('printf \'y\\ny\\ny\\ny\\ny\\ny\\ny\\nn\\n\' | ./arm_new_auto.sh --all\n')
# 监控部署进度
start_time = time.time()
last_output_time = start_time
deployment_complete = False
print('\n部署进度监控:')
print('-' * 60)
while True:
if channel.recv_ready():
output = channel.recv(4096).decode('utf-8', errors='ignore')
# 过滤掉一些无关的ANSI控制字符
output_clean = output.replace('[?2004h', '').replace('[?2004l', '')
# 输出关键信息
for line in output_clean.split('\n'):
if line.strip() and (
'执行' in line or
'完成' in line or
'部署' in line or
'成功' in line or
'失败' in line or
'错误' in line or
'source /etc/profile' in line or
'whiptail' in line.lower()
):
print(f'[{time.strftime("%H:%M:%S")}] {line.strip()}')
# 检查是否完成
if 'source /etc/profile' in output or '部署完成' in output:
deployment_complete = True
print('\n' + '=' * 60)
print('部署脚本执行完成!')
print('=' * 60)
break
last_output_time = time.time()
# 每5分钟报告一次进度
current_time = time.time()
elapsed = current_time - start_time
if int(elapsed) % 300 == 0 and elapsed > 0:
minutes = int(elapsed / 60)
print(f'[{time.strftime("%H:%M:%S")}] 已运行 {minutes} 分钟...')
# 检查是否超时(60分钟)
if elapsed > 3600:
print('\n警告: 部署脚本执行超过60分钟,可能存在问题')
break
# 检查是否长时间无输出(超过5分钟)
if current_time - last_output_time > 300:
print(f'\n警告: 超过5分钟无输出,可能脚本卡住')
break
time.sleep(2)
if deployment_complete:
print('\n[步骤6] 执行 source /etc/profile...')
channel.send('source /etc/profile\n')
time.sleep(2)
print('\n[步骤7] 检查容器状态...')
channel.send('docker ps --format "table {{.Names}}\\t{{.Status}}"\n')
time.sleep(3)
output = channel.recv(65535).decode('utf-8', errors='ignore')
print('容器状态:')
print(output)
# 统计容器数量
container_count = len([line for line in output.split('\n') if 'Running' in line])
print(f'\n正在运行的容器数量: {container_count}')
if container_count >= 11:
print('[OK] 容器状态正常(预期11个容器全部Running)')
else:
print(f'[WARN] 容器数量不足(预期11个,实际{container_count}个)')
print('\n部署阶段完成!')
print('总耗时:', f'{int(elapsed / 60)} 分钟')
channel.close()
except Exception as e:
print(f'\n部署过程出错: {e}')
import traceback
traceback.print_exc()
return False
finally:
ssh.close()
return True
if __name__ == '__main__':
success = deploy_arm_kylin()
sys.exit(0 if success else 1)
\ No newline at end of file
# Cockpit 虚拟机创建操作指南
> 服务器:192.168.5.3 | Cockpit 面板:https://192.168.5.3:9090
---
## 一、登录 Cockpit
1. 浏览器打开 **https://192.168.5.3:9090**
2. 点击「高级」→「继续访问」(证书是自签名的,忽略警告即可)
3. 输入用户名和密码:
| 字段 | 值 |
|------|-----|
| 用户名 | `root` |
| 密码 | `Ubains@123` |
4. 勾选「重用我的密码以执行特权任务」→ 点击「登录」
---
## 二、上传 ISO 镜像
> Cockpit Web 界面不支持上传文件,需要用 WinSCP 或 scp 将 ISO 传到服务器。
**目标路径:** `/var/lib/libvirt/images/`
**scp 方式(在你自己电脑的终端执行):**
```bash
scp CentOS-7-x86_64-Minimal-2009.iso root@192.168.5.3:/var/lib/libvirt/images/
```
**WinSCP 方式:**
- 主机:`192.168.5.3`
- 用户名:`root`,密码:`Ubains@123`
- 协议:SCP
- 左侧选本地 ISO 文件,右侧导航到 `/var/lib/libvirt/images/`,拖拽上传
---
## 三、创建虚拟机
### 3.1 进入虚拟机管理页面
登录后,点击左侧导航栏的「**虚拟机**」。
### 3.2 打开创建对话框
点击页面右上角的「**Create VM**」按钮。
### 3.3 填写虚拟机参数
#### Connection(连接方式)
保持默认:**QEMU/KVM System connection**
#### Name(名称)
给 VM 起个名字,如 `centos7-test`
#### Installation Source Type(安装源类型)
选择 **Local Install Media**(本地安装介质)
#### Installation Source(安装源路径)
填写之前上传的 ISO 文件路径,例如:
```
/var/lib/libvirt/images/CentOS-7-x86_64-Minimal-2009.iso
```
> 也可以选 **URL**,直接填网络下载地址
#### Storage(存储)
- 保持 **Create New Volume**(创建新虚拟磁盘)
- **Size**:根据需求设置,一般 Linux 系统 20-40 GiB 即可
- 单位选 **GiB**
#### Memory(内存)
根据需求设置,最小 1 GiB。这台服务器有 125G 内存,够用。
> 建议:测试用 2-4 GiB,生产环境按需分配
#### OS Vendor / Operating System(操作系统)
选择你 ISO 实际对应的系统:
| 你要装的系统 | 选择 |
|-------------|------|
| 麒麟 V10 | Unspecified → Other OS |
| CentOS 7 | CentOS → CentOS 7 |
| Ubuntu | Debian Project → Ubuntu |
| Windows | Microsoft Corporation → 对应版本 |
| 其他 | Unspecified → Other OS |
> 如果列表里没找到你的系统,选 Unspecified 也能正常安装运行。
#### Immediately Start VM
- **勾选**:创建完立刻开机,然后弹出 VNC 控制台让你装系统
- **不勾选**:创建完保持关机状态,需要手动开机
### 3.4 点击「Create」
对话框底部点击「**Create**」按钮,等待 VM 创建完成。
---
## 四、安装操作系统
### 4.1 打开控制台
VM 创建并开机后,VM 列表中会出现一行。点击 VM 名称进入详情页,再点击「**Console**」标签页进入 VNC 控制台。
如果鼠标点不动,按一下 `Ctrl+Alt` 释放光标。
### 4.2 常见操作
| 操作 | 方法 |
|------|------|
| 发送 Ctrl+Alt+Del | 点击页面上方的「Send Key」按钮 |
| 释放鼠标 | 按 `Ctrl+Alt` |
| 强制关机 | 点击「Force Off」 |
| 重启 | 点击「Reboot」 |
---
## 五、系统安装后必须配置(命令行操作)
> ⚠️ Cockpit Web 界面的 VM 详情页功能有限,以下配置必须通过命令行完成。
### 5.1 修复启动顺序(防止重启进安装界面)
VM 创建后默认 `cdrom` 优先启动,系统装完后必须改为硬盘优先:
```bash
# 1. 关机
virsh destroy "VM名称"
# 2. 导出 XML,去掉 cdrom 启动项
virsh dumpxml "VM名称" | perl -ne "print unless /boot dev=.cdrom/" > /tmp/VM名称_fix.xml
# 3. 如果 XML 里有 UUID 且和已有 VM 冲突,去 UUID
perl -ne "print unless /<uuid>/" /tmp/VM名称_fix.xml > /tmp/VM名称_final.xml
# 4. undefine + define + start
virsh undefine "VM名称"
virsh define "/tmp/VM名称_final.xml"
virsh start "VM名称"
# 5. 验证(应只有 boot dev='hd',没有 cdrom)
virsh dumpxml "VM名称" | grep "boot dev"
```
### 5.2 修改网卡为 br0 桥接(接入 192.168.5.x 网段)
VM 创建后默认网卡接在 `virbr0`(NAT 内网),无法从外部访问。需改为 br0 桥接:
```bash
# 1. 确认 br0 存在
brctl show br0
# 2. 如果 br0 不存在,手动创建
ip link add name br0 type bridge
ip link set eno2 master br0
ip link set br0 up
# 3. 关机 VM,修改网卡 XML
virsh destroy "VM名称"
virsh dumpxml "VM名称" | \
perl -pe "s|type='network'|type='bridge'|g; s|<source network='default' bridge='virbr0'/>|<source bridge='br0'/>|g; s|rtl8139|virtio|g" \
> /tmp/VM名称_net.xml
# 4. undefine + define + start
virsh undefine "VM名称"
virsh define "/tmp/VM名称_net.xml"
virsh start "VM名称"
# 5. 验证(应显示 bridge br0 + virtio)
virsh domiflist "VM名称"
```
> **网卡型号选 virtio**:麒麟 V10、CentOS 7 等现代 Linux 内核自带 virtio 驱动,比 rtl8139(老式百兆网卡)性能好且稳定。
### 5.3 调整 CPU 核数
VM 创建后默认只有 1 核,需要改为 8 核:
```bash
virsh destroy "VM名称"
virsh setvcpus "VM名称" 8 --config --maximum
virsh setvcpus "VM名称" 8 --config
virsh start "VM名称"
# 验证
virsh vcpucount "VM名称"
```
### 5.4 创建快照
系统安装配置完成后,创建快照方便后续回滚:
```bash
virsh snapshot-create-as "VM名称" "initial-install" "系统安装完成,8核+br0桥接"
# 查看快照列表
virsh snapshot-list "VM名称"
# 恢复快照
virsh snapshot-revert "VM名称" --snapshotname initial-install
# 删除快照
virsh snapshot-delete "VM名称" --snapshotname initial-install
```
---
## 六、完整部署流程示例(麒麟 V10)
以下为 192.168.5.3 上三台麒麟 V10 VM 的完整部署记录:
| VM | IP | CPU | 网卡 | 快照 |
|-----|-----|-----|------|------|
| 5.41招商局负载1 | 192.168.5.41 | 8 核 | br0 + virtio | ✅ initial-install |
| 5.42招商局负载2 | 192.168.5.42 | 8 核 | br0 + virtio | ✅ initial-install |
| 5.43招商局负载3 | 192.168.5.43 | 8 核 | br0 + virtio | ✅ initial-install |
### 一键配置脚本
将以下脚本保存为 `/tmp/setup_vm.sh`,创建新 VM 后执行:
```bash
#!/bin/bash
# 用法: bash /tmp/setup_vm.sh "VM名称"
VM="$1"
echo "=== 关机 ==="
virsh destroy "$VM"
echo "=== 导出并修改 XML ==="
virsh dumpxml "$VM" | \
perl -ne "print unless /boot dev=.cdrom/" | \
perl -pe "s|type='network'|type='bridge'|g; s|<source network='default' bridge='virbr0'/>|<source bridge='br0'/>|g; s|rtl8139|virtio|g" | \
perl -ne "print unless /<uuid>/" \
> "/tmp/${VM}_setup.xml"
echo "=== 重新定义 ==="
virsh undefine "$VM"
virsh define "/tmp/${VM}_setup.xml"
echo "=== 设置 8 核 ==="
virsh setvcpus "$VM" 8 --config --maximum
virsh setvcpus "$VM" 8 --config
echo "=== 启动 ==="
virsh start "$VM"
sleep 5
echo "=== 创建快照 ==="
virsh snapshot-create-as "$VM" "initial-install" "系统安装完成,8核+br0桥接"
echo "=== 验证 ==="
virsh domiflist "$VM"
virsh vcpucount "$VM"
virsh dumpxml "$VM" | grep "boot dev"
virsh snapshot-list "$VM"
```
---
## 七、常用管理操作
### Cockpit Web 界面
| 操作 | 位置 |
|------|------|
| 开机/关机/重启 | VM 详情页顶部按钮 |
| VNC 控制台 | VM 详情页 → Console 标签 |
| 修改内存 | VM 详情页 → Overview 标签 |
| 删除 VM | VM 列表页 → 勾选 VM → 点击「Delete」 |
### 命令行
| 操作 | 命令 |
|------|------|
| 查看所有 VM | `virsh list --all` |
| 查看 VM 网卡 | `virsh domiflist "VM名称"` |
| 查看 vCPU | `virsh vcpucount "VM名称"` |
| 查看快照列表 | `virsh snapshot-list "VM名称"` |
| 创建快照 | `virsh snapshot-create-as "VM名称" "快照名" "描述"` |
| 恢复快照 | `virsh snapshot-revert "VM名称" --snapshotname "快照名"` |
| 删除快照 | `virsh snapshot-delete "VM名称" --snapshotname "快照名"` |
| 桥接状态 | `brctl show` |
---
## 八、注意事项
1. **ISO 文件用完后不要删除**:重装系统或救援模式时还会用到
2. **内存不要全部分配**:给宿主机留至少 2-4G,服务器总共 125G,单台 VM 建议不超过 64G
3. **桥接网络选 br0**:VM 才能直接接入 192.168.5.x 网段,virbr0 是 NAT 内网(外部无法访问)
4. **网卡型号选 virtio**:现代 Linux 内核自带驱动,性能优于 rtl8139
5. **系统装完后必须改启动顺序**:否则重启会从 ISO 引导进安装界面
6. **安装完成后立即创建快照**:方便后续搞坏了快速回滚
7. **br0 桥接需手动维护**:NetworkManager 可能导致 br0 消失,建议写入 `/etc/rc.local` 开机自动创建
此差异已折叠。
# FastDFS ARM64 集群部署操作指南
> 服务器:192.168.9.89 / 192.168.9.90 / 192.168.9.91 | 架构:ARM64 | 镜像:ufastdfs:v2
> 部署日期:2026-07-08
---
## 一、架构说明
### 1.1 部署架构
每台服务器上运行一个 `utracker` 容器,同时包含 Tracker 和 Storage 两个角色:
| 服务器 | IP | 角色 | Tracker(22122) | Storage(23000) | HTTP(8888) |
|--------|----|------|---------------|----------------|------------|
| 节点1 | 192.168.9.89 | Tracker + Storage | ✅ | ✅ | ✅ |
| 节点2 | 192.168.9.90 | Tracker + Storage | ✅ | ✅ | ✅ |
| 节点3 | 192.168.9.91 | Tracker + Storage | ✅ | ✅ | ✅ |
### 1.2 文件存储结构
```
容器内: /home/fastdfs/
├── data/00/00/xxx.txt # 实际存储的文件(按256*256子目录分散)
├── data/00/01/xxx.txt
├── logs/ # 运行日志
│ ├── trackerd.log
│ └── storaged.log
├── fdfs_trackerd.pid
├── fdfs_storaged.pid
└── storage_stat.dat # storage 状态元数据
宿主机: /data/storage/storage/ → 映射到容器内 /home/fastdfs/
```
### 1.3 端口说明
| 端口 | 组件 | 用途 | 防火墙必开 |
|------|------|------|-----------|
| 22122 | Tracker | Storage 注册、客户端查询、Tracker 集群同步 | ✅ |
| 23000 | Storage | 文件上传/下载、节点间同步 | ✅ |
| 8888 | Nginx | HTTP 文件下载 | ✅ |
---
## 二、前置条件
1. **Docker 已安装**(版本 ≥ 20.x)
2. **三台服务器之间网络互通**(22122、23000、8888 端口互相可达)
3. **存储目录已创建**`mkdir -p /data/storage/storage`
4. **防火墙开放端口**
```bash
firewall-cmd --permanent --add-port=22122/tcp
firewall-cmd --permanent --add-port=23000/tcp
firewall-cmd --permanent --add-port=8888/tcp
firewall-cmd --reload
```
---
## 三、部署步骤(顺序执行)
### 第一步:修改配置文件(解决 docker 镜像 entrypoint bug)
`ufastdfs:v2` 镜像的 entrypoint 有个已知 bug:启动后 `storage.conf``client.conf` 里的 `tracker_server` 写成了 `192.168.0.165`(docker bridge 地址),需要用以下步骤修复。
### 第二步:逐个服务器部署
#### 1. 在 192.168.9.89 上
```bash
# 启动容器
docker run -d \
--network=host \
--name utracker \
--restart=always \
-e TRACKER_SERVER="192.168.9.89:22122,192.168.9.90:22122,192.168.9.91:22122" \
-e FASTDFS_IPADDR=192.168.9.89 \
-v /data/storage/storage:/home/fastdfs \
-v /etc/localtime:/etc/localtime:ro \
ufastdfs:v2
# 修复配置文件(删掉旧的 0.165 地址,加正确的 3 台 tracker)
docker exec utracker sed -i '/^tracker_server = 192.168.0.165:22122/d' /etc/fdfs/storage.conf
docker exec utracker sed -i '/^tracker_server = 192.168.0.165:22122/d' /etc/fdfs/client.conf
docker exec utracker bash -c 'cat >> /etc/fdfs/storage.conf << EOF
tracker_server = 192.168.9.89:22122
tracker_server = 192.168.9.90:22122
tracker_server = 192.168.9.91:22122
EOF'
docker exec utracker bash -c 'cat >> /etc/fdfs/client.conf << EOF
tracker_server = 192.168.9.89:22122
tracker_server = 192.168.9.90:22122
tracker_server = 192.168.9.91:22122
EOF'
# 清理旧元数据并重启
docker exec utracker bash -c 'cd /home/fastdfs/data && rm -f storage_*.dat fdfs_*.pid'
docker restart utracker
sleep 45
docker exec utracker fdfs_monitor /etc/fdfs/client.conf | grep -E "ip_addr|ACTIVE|OFFLINE"
```
#### 2. 在 192.168.9.90 上
```bash
# 启动容器(注意 FASTDFS_IPADDR 改成 90)
docker run -d \
--network=host \
--name utracker \
--restart=always \
-e TRACKER_SERVER="192.168.9.89:22122,192.168.9.90:22122,192.168.9.91:22122" \
-e FASTDFS_IPADDR=192.168.9.90 \
-v /data/storage/storage:/home/fastdfs \
-v /etc/localtime:/etc/localtime:ro \
ufastdfs:v2
# 同样的修复步骤
docker exec utracker sed -i '/^tracker_server = 192.168.0.165:22122/d' /etc/fdfs/storage.conf
docker exec utracker sed -i '/^tracker_server = 192.168.0.165:22122/d' /etc/fdfs/client.conf
docker exec utracker bash -c 'cat >> /etc/fdfs/storage.conf << EOF
tracker_server = 192.168.9.89:22122
tracker_server = 192.168.9.90:22122
tracker_server = 192.168.9.91:22122
EOF'
docker exec utracker bash -c 'cat >> /etc/fdfs/client.conf << EOF
tracker_server = 192.168.9.89:22122
tracker_server = 192.168.9.90:22122
tracker_server = 192.168.9.91:22122
EOF'
docker exec utracker bash -c 'cd /home/fastdfs/data && rm -f storage_*.dat fdfs_*.pid'
docker restart utracker
sleep 45
docker exec utracker fdfs_monitor /etc/fdfs/client.conf | grep -E "ip_addr|ACTIVE|OFFLINE"
```
#### 3. 在 192.168.9.91 上
```bash
# 同 90,FASTDFS_IPADDR 改成 91
# ...(同上)
docker run -d \
--network=host \
--name utracker \
--restart=always \
-e TRACKER_SERVER="192.168.9.89:22122,192.168.9.90:22122,192.168.9.91:22122" \
-e FASTDFS_IPADDR=192.168.9.91 \
# ...(其余同上)
```
### 第三步:验证集群
在任意一台服务器上执行:
```bash
docker exec utracker fdfs_monitor /etc/fdfs/client.conf | grep -E "ip_addr|ACTIVE|OFFLINE"
```
预期输出三台都是 ACTIVE:
```
ip_addr = 192.168.9.89 ACTIVE
ip_addr = 192.168.9.90 ACTIVE
ip_addr = 192.168.9.91 ACTIVE
```
---
## 四、文件上传与访问测试
### 4.1 上传测试
```bash
echo "hello fastdfs" > /tmp/test.txt
docker exec utracker fdfs_upload_file /etc/fdfs/client.conf /tmp/test.txt
# 返回: group1/M00/00/00/xxxxxxxxxxxx.txt
```
### 4.2 HTTP 访问
```bash
# 直接访问(需端口开放)
curl http://192.168.9.89:8888/group1/M00/00/00/xxxxxxxxxxxx.txt
# 通过 nginx 反向代理访问(HTTPS)
# 前提:nginx 容器配置了 /group1 的 proxy_pass
curl -k https://192.168.9.89/group1/M00/00/00/xxxxxxxxxxxx.txt
```
### 4.3 文件下载验证
```bash
# 下载到本地
docker exec utracker fdfs_download_file /etc/fdfs/client.conf group1/M00/00/00/xxxxxxxxxxxx.txt /tmp/downloaded.txt
cat /tmp/downloaded.txt
```
---
## 五、文件路径对照
FastDFS 返回的路径和磁盘实际路径:
```
group1/M00/00/00/xxxxxxxxxxxx.txt
↓ 容器内:
/home/fastdfs/data/00/00/xxxxxxxxxxxx.txt
↓ 宿主机:
/data/storage/storage/data/00/00/xxxxxxxxxxxx.txt
```
| FastDFS 路径段 | 含义 |
|---------------|------|
| `group1` | 存储组名 |
| `M00` | store_path0 的挂载标识 |
| `00/00` | 子目录(256×256 个,分散文件) |
| `xxx.txt` | 实际文件名(base64 编码) |
---
## 六、运维命令
### 6.1 查看集群状态
```bash
docker exec utracker fdfs_monitor /etc/fdfs/client.conf
```
### 6.2 查看日志
```bash
docker exec utracker cat /home/fastdfs/logs/trackerd.log | tail -30
docker exec utracker cat /home/fastdfs/logs/storaged.log | tail -30
docker exec utracker tail -30 /usr/local/nginx/logs/error.log
```
### 6.3 删除 OFFLINE storage 记录
```bash
docker exec utracker fdfs_monitor /etc/fdfs/client.conf delete group1 192.168.9.xx
```
### 6.4 清理元数据(IP 变更后或恢复)
```bash
docker exec utracker bash -c 'cd /home/fastdfs/data && rm -f storage_*.dat fdfs_*.pid'
docker restart utracker
```
---
## 七、踩坑记录
### 坑1:tracker_server 写成了 docker bridge 地址
**现象**`storage.conf``tracker_server = 192.168.0.165:22122`,storage 连不上 tracker
**根因**`ufastdfs:v2` 镜像 entrypoint 没有正确解析环境变量 `TRACKER_SERVER`,写入了 docker 内网地址
**对策**:部署后必须用 `sed` 修复配置文件(见部署步骤)
### 坑2:ngx_fastdfs_module 段错误(signal 11)
**现象**:nginx worker 进程不断崩溃,8888 端口无法访问
**根因**:启动时没有传 `-e FASTDFS_IPADDR=xxx` 参数,entrypoint 没有生成正确的 `mod_fastdfs.conf`,nginx 模块找不到配置导致段错误
**对策**`docker run` 时必须传 `-e FASTDFS_IPADDR=<本机IP>`
### 坑3:tracker leader 脑裂(split-brain)
**现象**:多台 tracker 同时认为自己是 leader,storage 注册失败
**根因**:tracker 集群需要多数派,新启动的 tracker 与已有 leader 冲突
**对策**
1. 如果单台跑稳定了再加新节点,新节点启动前先清理元数据再启动
2. 配置完全一致(所有节点的 tracker_server 列表相同)
### 坑4:私服 Docker 镜像缺少 `curl`
**现象**:容器内没有 `curl``netstat` 等工具,排查困难
**对策**:在宿主机上通过 `docker exec utracker curl` 测试,会比较慢(走 TCP),但能验证
---
## 八、常见问题
**Q: storage 注册失败,日志报 `response status 14`?**
A: 检查磁盘空间(`df -h`),错误码 14 = ENOSPC。但也可能是 old metadata 冲突,清理 `storage_*.dat` 重启。
**Q: storage 注册失败,日志报 `response status 11`?**
A: 错误码 11 = EAGAIN(资源暂时不可用)。通常发生在集群 tracker 选主期间,等待 30-60 秒自动恢复。
**Q: 节点部署完后其他节点显示 OFFLINE?**
A: 检查新节点的防火墙端口是否开放;检查新节点的 `storage.conf``tracker_server` 列表是否完整。
**Q: 如何卸载重装?**
A:
```bash
docker stop utracker && docker rm utracker
# 数据还在 /data/storage/storage/,不会丢失
# 如果不需要旧数据:
# rm -rf /data/storage/storage/*
```
\ No newline at end of file
此差异已折叠。
#!/bin/bash
#==============================================================#
# 脚本名: import_dm8_databases.sh
# 功能: 将 databases/ 目录下的 DDL SQL 文件导入到 DM8 容器
# 说明: 1. 自动创建用户(如果不存在),密码满足达梦复杂度规则
# 2. 用管道方式执行 DDL SQL(避免 disql \` backtick 截断问题)
# 3. 验证导入后的表数量
# 作者: [ubains]
# 注意事项:
# - 达梦密码需满足复杂度:≥9位,含大小写+数字+特殊符号
# - disql 不支持分号分隔多语句,CREATE 和 GRANT 必分开执行
# - 密码含 ! 时需用单引号避免 bash 历史扩展
#==============================================================#
set -e
# ------------------- 可配置参数 -------------------
CONTAINER="${DM_CONTAINER:-dm8-server}"
SYSDBA_PWD="${DM_PWD:-dNrprU&2S!}"
DM_PORT="${DM_PORT:-5236}"
DM_INSTALL_DIR="${DM_INSTALL_DIR:-/usr/local/dm8}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DDL_DIR="$SCRIPT_DIR/databases"
HOST_BACKUP_DIR="/data/middleware/dm8/bak"
# 要导入的用户列表(按依赖顺序)
USERS=(
"DM_NACOS"
"DM_UBAINS"
"DM_UBAINS_SSO"
"DM_DEVOPS"
"DM_DEVOPS_VOICE"
"DM_HUAZHAO2"
"DM_OFFLINE"
)
# 用户默认密码(必须满足达梦复杂度:≥9位,含大小写+数字+特殊符号)
declare -A USER_PASSWORDS=(
["DM_NACOS"]="dNrprU&2S!"
["DM_UBAINS"]="dNrprU&2S!"
["DM_UBAINS_SSO"]="dNrprU&2S!"
["DM_DEVOPS"]="dNrprU&2S!"
["DM_DEVOPS_VOICE"]="dNrprU&2S!"
["DM_HUAZHAO2"]="dNrprU&2S!"
["DM_OFFLINE"]="dNrprU&2S!"
)
# ------------------- 颜色输出 -------------------
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
# ------------------- 检查容器 -------------------
check_container() {
if ! docker ps --format '{{.Names}}' | grep -qw "$CONTAINER"; then
log_error "容器 '$CONTAINER' 未运行,请先启动"
exit 1
fi
log_info "容器 '$CONTAINER' 已就绪"
}
# ------------------- 创建用户(密码满足达梦复杂度) -------------------
create_user() {
local user=$1
local pwd=${USER_PASSWORDS[$user]:-"User@2026Pwd"}
log_info "检查用户 $user 是否存在..."
# 检查用户是否存在(用单引号避免 ! 历史扩展)
local exists=$(docker exec "$CONTAINER" "${DM_INSTALL_DIR}/bin/disql" SYSDBA/'dNrprU&2S!'@localhost:$DM_PORT \
-e "SELECT COUNT(*) FROM DBA_USERS WHERE USERNAME='$user';" 2>/dev/null | grep -E '^[0-9]+$' | head -1)
if [[ "$exists" == "0" ]]; then
log_info "创建用户 $user(密码复杂度达标)..."
# CREATE 和 GRANT 分开执行(disql 不支持分号合并)
docker exec "$CONTAINER" "${DM_INSTALL_DIR}/bin/disql" SYSDBA/'dNrprU&2S!'@localhost:$DM_PORT \
-e "CREATE USER \"$user\" IDENTIFIED BY \"$pwd\";" 2>/dev/null || {
log_warn "创建用户失败,尝试用复杂密码重试..."
docker exec "$CONTAINER" "${DM_INSTALL_DIR}/bin/disql" SYSDBA/'dNrprU&2S!'@localhost:$DM_PORT \
-e "CREATE USER \"$user\" IDENTIFIED BY \"${user}Pwd@2026\";"
}
docker exec "$CONTAINER" "${DM_INSTALL_DIR}/bin/disql" SYSDBA/'dNrprU&2S!'@localhost:$DM_PORT \
-e "GRANT DBA TO \"$user\";" 2>/dev/null
log_info "用户 $user 创建成功,密码: $pwd"
else
log_info "用户 $user 已存在,跳过创建"
fi
}
# ------------------- 导入 DDL SQL(管道方式,避免截断) -------------------
import_ddl() {
local user=$1
local ddl_file=$(ls "$DDL_DIR"/${user}_ddl.sql 2>/dev/null | head -1)
if [[ -z "$ddl_file" ]]; then
log_warn "未找到 $user 的 DDL SQL 文件,跳过"
return 0
fi
local ddl_name=$(basename "$ddl_file")
log_info "导入 $user DDL..."
log_info " 文件: $ddl_name ($(wc -l < "$ddl_file") 行)"
# 清理 DDL 文件:去掉 spool 残留的 SQL> 提示符、SELECT 查询行、空行、多余分号
local cleaned_file="/tmp/${ddl_name}"
cp "$ddl_file" "$cleaned_file"
sed -i '/^SQL>/d' "$cleaned_file" 2>/dev/null || true
sed -i '/^SELECT DBMS_METADATA/d' "$cleaned_file" 2>/dev/null || true
sed -i '/^$/d' "$cleaned_file" 2>/dev/null || true
perl -i -pe 's/;{2,}$/;/g' "$cleaned_file" 2>/dev/null || true
log_info " 清理后: $(wc -l < "$cleaned_file") 行"
# 复制 SQL 到容器
docker cp "$cleaned_file" "$CONTAINER:${HOST_BACKUP_DIR}/$ddl_name"
rm -f "$cleaned_file"
# 复制 SQL 到容器
docker cp "$ddl_file" "$CONTAINER:${HOST_BACKUP_DIR}/$ddl_name"
# 用管道方式执行 disql(用单引号避免 ! 历史扩展)
# 关键: cat | disql 方式不会截断超长 DDL,且能正确执行所有 CREATE TABLE 语句
local result=$(docker exec "$CONTAINER" bash -c \
"export LD_LIBRARY_PATH=${DM_INSTALL_DIR}/bin && \
cat ${HOST_BACKUP_DIR}/$ddl_name | ${DM_INSTALL_DIR}/bin/disql SYSDBA/dNrprU\&2S\!@localhost:$DM_PORT" 2>&1)
# 统计执行成功的语句数
local success_count=$(echo "$result" | grep -c "executed successfully" || true)
log_info " 执行成功: $success_count 条语句"
# 清理容器内 SQL
docker exec "$CONTAINER" rm -f "${HOST_BACKUP_DIR}/$ddl_name"
log_info "导入 $user DDL 完成"
}
# ------------------- 验证表数量(取数字行,避免端口号干扰) -------------------
verify_tables() {
log_info "验证各用户表数量..."
echo ""
printf "%-20s %10s\n" "用户" "表数量"
printf "%-20s %10s\n" "----" "------"
for user in "${USERS[@]}"; do
local cnt=$(docker exec "$CONTAINER" "${DM_INSTALL_DIR}/bin/disql" SYSDBA/'dNrprU&2S!'@localhost:$DM_PORT \
-e "SELECT COUNT(*) FROM DBA_TABLES WHERE OWNER='$user';" 2>/dev/null | \
grep -E '^[0-9]+$' | head -1)
printf "%-20s %10s\n" "$user" "${cnt:-0}"
done
echo ""
}
# ------------------- 主流程 -------------------
main() {
log_info "=============================================="
log_info "DM8 DDL 导入工具(SQL 方式,ARM)"
log_info "=============================================="
log_info "容器: $CONTAINER"
log_info "DDL 目录: $DDL_DIR"
log_info "目标用户: ${USERS[*]}"
echo ""
check_container
mkdir -p "$HOST_BACKUP_DIR"
docker exec "$CONTAINER" mkdir -p "$HOST_BACKUP_DIR" 2>/dev/null || true
for user in "${USERS[@]}"; do
echo ""
log_info "=========================================="
log_info "处理用户: $user"
log_info "=========================================="
create_user "$user"
import_ddl "$user"
done
echo ""
log_info "=============================================="
log_info "导入完成"
log_info "=============================================="
verify_tables
}
main "$@"
\ No newline at end of file
此差异已折叠。
此差异已折叠。
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论