Files
BakNRet/tools/lab/Lab-Common.ps1
T
Shuery 2937eb6652 chore: 记录改造前基线
改造开始前的完整状态,作为可回退的基点。此提交之后:Pester 175 项、零依赖套件 101 项全绿;PowerShell 5.1 尚不可用(源文件无 BOM)。

包含此前未提交的在制品:安全描述符套件、Hyper-V 实验环境(tools/lab)、agent 约定(AGENTS.md 与 docs/agents)。

.gitignore 增加 *.key / *.pfx:BackupConfig.psd1 的 PasswordFile 此前默认指向仓库内的 baknret.key,一次 git add -A 就会把口令提交进版本库。默认值在后续提交中改为空。
2026-09-26 21:46:55 +08:00

183 lines
7.3 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<#
.SYNOPSIS
BakNRet 隔离测试环境的共享配置与工具函数(Hyper-V 真机级 VM)。
.DESCRIPTION
被 tools\lab\New-BakNRetLab.ps1(一次性搭建)与 tools\lab\Lab.ps1(日常使用)共同导入。
设计约定:
* 宿主机侧的一切状态(VHDX、日志、凭据、暂存包)都放在 $LabRoot 下,它刻意位于
**仓库之外**(默认 D:\VMs\BakNRet-Lab);测试用的归档、日志因此不会落进真实仓库,
真实仓库的 Backups\ 与 logs\ 在整套流程里只被读取、从不写入。
* VM 内的仓库副本在 C:\BakNRet,工具负载在 C:\BakNRet-Lab。
* 与 VM 的一切交互走 PowerShell Direct(VMBus),不依赖网络、不共享宿主机目录。
#>
$script:LabConfig = [ordered]@{
VmName = 'BakNRet-Lab'
LabRoot = 'D:\VMs\BakNRet-Lab'
VhdxPath = 'D:\VMs\BakNRet-Lab\vhdx\BakNRet-Lab.vhdx'
VhdxSizeGB = 80
IsoPath = 'F:\Images\Windows\Win11_25H2_Chinese_Simplified_x64_v2.iso'
ImageIndex = 4 # Windows 11 专业版
SwitchName = 'Default Switch'
MemoryStartupGB = 8
CpuCount = 8
GuestRepoPath = 'C:\BakNRet'
GuestLabPath = 'C:\BakNRet-Lab'
GuestUser = 'lab'
CheckpointName = 'clean-baseline'
RepoRoot = (Split-Path -Parent (Split-Path -Parent $PSScriptRoot))
}
function Get-LabConfig { return $script:LabConfig }
function Get-LabPath {
<# .SYNOPSIS 取宿主机侧实验目录下的路径(自动建父目录)。 #>
param([Parameter(Mandatory)][string]$Relative)
$full = Join-Path $script:LabConfig.LabRoot $Relative
$parent = Split-Path -Parent $full
if ($parent -and -not (Test-Path -LiteralPath $parent)) { New-Item -ItemType Directory -Force -Path $parent | Out-Null }
return $full
}
function Write-LabLog {
<# .SYNOPSIS 统一日志:同时进控制台与 $LabRoot\logs\lab.log。 #>
param([Parameter(Mandatory)][string]$Message, [ValidateSet('INFO','WARN','ERROR','STEP')][string]$Level = 'INFO')
$line = "[{0}] [{1,-5}] {2}" -f (Get-Date).ToString('HH:mm:ss'), $Level, $Message
switch ($Level) {
'STEP' { Write-Host $line -ForegroundColor Cyan }
'WARN' { Write-Host $line -ForegroundColor Yellow }
'ERROR' { Write-Host $line -ForegroundColor Red }
default { Write-Host $line }
}
Add-Content -LiteralPath (Get-LabPath 'logs\lab.log') -Value $line -Encoding UTF8 -ErrorAction SilentlyContinue
}
function Test-LabElevated {
param()
return ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}
function Assert-LabElevated {
<# .SYNOPSIS 需要管理员:非同权限时给出可直接复制的提权命令。 #>
param([Parameter(Mandatory)][string]$Why)
if (Test-LabElevated) { return }
$gsudo = (Get-Command gsudo -ErrorAction SilentlyContinue | Select-Object -First 1).Source
$self = $MyInvocation.PSCommandPath
$hint = if ($gsudo) { "`n $gsudo pwsh -NoProfile -File `"$self`" $($MyInvocation.Line)" } else { '' }
throw "需要管理员权限:$Why$hint"
}
function Get-LabCredentialPath { return (Join-Path $script:LabConfig.LabRoot 'state\credentials.json') }
function Save-LabCredential {
<# .SYNOPSIS 把 VM 内 lab 账户的口令写进宿主机侧的 credentials.json(在仓库之外)。 #>
param([Parameter(Mandatory)][string]$Password)
$path = Get-LabCredentialPath
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $path) | Out-Null
[ordered]@{
VmName = $script:LabConfig.VmName
User = $script:LabConfig.GuestUser
Password = $Password
SavedAt = (Get-Date).ToString('s')
} | ConvertTo-Json | Set-Content -LiteralPath $path -Encoding UTF8
return $path
}
function Get-LabCredential {
<# .SYNOPSIS 读出 VM 凭据为 PSCredential。 #>
param()
$path = Get-LabCredentialPath
if (-not (Test-Path -LiteralPath $path)) { throw "找不到 VM 凭据:$path(先跑 New-BakNRetLab.ps1)" }
$j = Get-Content -LiteralPath $path -Raw -Encoding UTF8 | ConvertFrom-Json
$sec = ConvertTo-SecureString $j.Password -AsPlainText -Force
return [pscredential]::new("$($j.User)", $sec)
}
function New-LabPassword {
<# .SYNOPSIS 生成只含字母数字的口令(避免 XML / 命令行转义问题)。 #>
param([int]$Length = 24)
$chars = 'abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789'
return -join (1..$Length | ForEach-Object { $chars[(Get-Random -Minimum 0 -Maximum $chars.Length)] })
}
function Get-LabVm {
param()
return Get-VM -Name $script:LabConfig.VmName -ErrorAction SilentlyContinue
}
function Wait-LabVMRunning {
<# .SYNOPSIS 等 VM 进入 Running。 #>
param([int]$TimeoutSeconds = 300)
$sw = [Diagnostics.Stopwatch]::StartNew()
while ($sw.Elapsed.TotalSeconds -lt $TimeoutSeconds) {
$vm = Get-LabVm
if ($vm -and $vm.State -eq 'Running') { return $true }
Start-Sleep -Seconds 3
}
return $false
}
function New-LabSession {
<# .SYNOPSIS 建立 PowerShell Direct 会话(VMBus,不经网络)。 #>
param([int]$RetrySeconds = 600)
$cred = Get-LabCredential
$sw = [Diagnostics.Stopwatch]::StartNew()
$lastError = $null
while ($sw.Elapsed.TotalSeconds -lt $RetrySeconds) {
try {
$s = New-PSSession -VMName $script:LabConfig.VmName -Credential $cred -ErrorAction Stop
Write-LabLog "PowerShell Direct 会话已建立(等待 $([math]::Round($sw.Elapsed.TotalSeconds)) 秒)"
return $s
} catch {
$lastError = $_.Exception.Message
Start-Sleep -Seconds 5
}
}
throw "无法建立 PowerShell Direct 会话:$lastError"
}
function Invoke-LabCommand {
<# .SYNOPSIS 在 VM 里跑一段脚本并回传结果(自动建/收会话)。 #>
param(
[Parameter(Mandatory)][scriptblock]$ScriptBlock,
[object[]]$ArgumentList = @(),
[int]$RetrySeconds = 600
)
$s = New-LabSession -RetrySeconds $RetrySeconds
try {
return Invoke-Command -Session $s -ScriptBlock $ScriptBlock -ArgumentList $ArgumentList -ErrorAction Stop
} finally {
Remove-PSSession -Session $s -ErrorAction SilentlyContinue
}
}
function Copy-LabFileToGuest {
<# .SYNOPSIS 宿主机 -> VM 传文件(Copy-VMFile,需要 Guest Service Interface)。 #>
param(
[Parameter(Mandatory)][string]$SourcePath,
[Parameter(Mandatory)][string]$DestinationPath
)
Copy-VMFile -VMName $script:LabConfig.VmName -SourcePath $SourcePath `
-DestinationPath $DestinationPath -CreateFullPath -FileSource Host -Force
}
function Get-HostSevenZip {
<# .SYNOPSIS 宿主机 7z 路径(用来打包仓库快照)。 #>
param()
$c = Get-Command 7z -ErrorAction SilentlyContinue | Select-Object -First 1
if (-not $c) { throw '宿主机找不到 7z' }
return $c.Source
}
function Test-LabGuestReady {
<# .SYNOPSIS 判断 VM 内供给是否完成(provision.ok)。 #>
param()
try {
$r = Invoke-LabCommand -ScriptBlock { Test-Path 'C:\BakNRet-Lab\state\provision.ok' } -RetrySeconds 60
return [bool]$r
} catch { return $false }
}