feat: implement 6-DC active binding detection, targeted sync, and Task.Run async batch engine with comprehensive guide
This commit is contained in:
@@ -376,3 +376,210 @@ function Get-ADSampleUsers {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function Get-ADDomainControllers {
|
||||
param(
|
||||
[object]$Config = $null
|
||||
)
|
||||
|
||||
$useLive = if ($Config -and $Config.ActiveDirectory) { [bool]$Config.ActiveDirectory.UseLive } else { $false }
|
||||
|
||||
if ($useLive) {
|
||||
try {
|
||||
if (Get-Command Get-ADDomainController -ErrorAction SilentlyContinue) {
|
||||
$dcs = Get-ADDomainController -Filter *
|
||||
$list = [System.Collections.Generic.List[PSCustomObject]]::new()
|
||||
foreach ($d in $dcs) {
|
||||
$list.Add([PSCustomObject]@{
|
||||
HostName = $d.HostName
|
||||
IPv4Address = $d.IPv4Address
|
||||
Site = $d.Site
|
||||
OperatingSystem = $d.OperatingSystem
|
||||
IsPdc = ($d.OperationMasterRoles -contains "PDCEmulator")
|
||||
IsLive = $true
|
||||
})
|
||||
}
|
||||
return $list.ToArray()
|
||||
}
|
||||
} catch {
|
||||
Write-Warning "실제 AD DC 목록 조회 실패: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
# 6개 다중 DC 환경 모사 데이터
|
||||
return @(
|
||||
[PSCustomObject]@{ HostName = "dc01.corp.local"; IPv4Address = "10.10.1.11"; Site = "Seoul-HQ"; OperatingSystem = "Windows Server 2022 Datacenter"; IsPdc = $true; IsLive = $false },
|
||||
[PSCustomObject]@{ HostName = "dc02.corp.local"; IPv4Address = "10.10.1.12"; Site = "Seoul-HQ"; OperatingSystem = "Windows Server 2022 Datacenter"; IsPdc = $false; IsLive = $false },
|
||||
[PSCustomObject]@{ HostName = "dc03.corp.local"; IPv4Address = "10.10.1.13"; Site = "Seoul-HQ"; OperatingSystem = "Windows Server 2019 Datacenter"; IsPdc = $false; IsLive = $false },
|
||||
[PSCustomObject]@{ HostName = "dc04.corp.local"; IPv4Address = "10.20.1.14"; Site = "Pangyo-DC"; OperatingSystem = "Windows Server 2022 Datacenter"; IsPdc = $false; IsLive = $false },
|
||||
[PSCustomObject]@{ HostName = "dc05.corp.local"; IPv4Address = "10.30.1.15"; Site = "Daejeon-DR"; OperatingSystem = "Windows Server 2019 Datacenter"; IsPdc = $false; IsLive = $false },
|
||||
[PSCustomObject]@{ HostName = "dc06.corp.local"; IPv4Address = "10.40.1.16"; Site = "Azure-KOR"; OperatingSystem = "Windows Server 2022 Datacenter"; IsPdc = $false; IsLive = $false }
|
||||
)
|
||||
}
|
||||
|
||||
function New-ADSecurityGroupOnTargetDc {
|
||||
param(
|
||||
[string]$GroupName,
|
||||
[string]$GroupScope = "Global",
|
||||
[string]$Description = "ModernAdminStudio 자동 생성 보안그룹",
|
||||
[string]$TargetDc = "",
|
||||
[string]$TargetOU = "",
|
||||
[string[]]$Members = @(),
|
||||
[object]$Config = $null
|
||||
)
|
||||
|
||||
$useLive = if ($Config -and $Config.ActiveDirectory) { [bool]$Config.ActiveDirectory.UseLive } else { $false }
|
||||
|
||||
if (-not $TargetDc) {
|
||||
$TargetDc = if ($Config -and $Config.ActiveDirectory) { $Config.ActiveDirectory.DomainController } else { "dc04.corp.local" }
|
||||
}
|
||||
if (-not $TargetOU) {
|
||||
$TargetOU = if ($Config -and $Config.ActiveDirectory) { $Config.ActiveDirectory.BaseOU } else { "OU=SecurityGroups,DC=corp,DC=local" }
|
||||
}
|
||||
|
||||
if ($useLive) {
|
||||
try {
|
||||
$user = $Config.ActiveDirectory.Username
|
||||
$pass = $Config.ActiveDirectory.Password
|
||||
$useLdaps = [bool]$Config.ActiveDirectory.UseLdaps
|
||||
$proto = if ($useLdaps) { "LDAP://$TargetDc:636" } else { "LDAP://$TargetDc" }
|
||||
$ouPath = "$proto/$TargetOU"
|
||||
|
||||
$ouEntry = if ([string]::IsNullOrWhiteSpace($user)) {
|
||||
New-Object System.DirectoryServices.DirectoryEntry($ouPath)
|
||||
} else {
|
||||
New-Object System.DirectoryServices.DirectoryEntry($ouPath, $user, $pass)
|
||||
}
|
||||
|
||||
# 그룹 생성
|
||||
$groupEntry = $ouEntry.Children.Add("CN=$GroupName", "group")
|
||||
$groupEntry.Properties["sAMAccountName"].Value = $GroupName
|
||||
$groupEntry.Properties["description"].Value = $Description
|
||||
|
||||
# Global Security Group Type: -2147483646 (0x80000002)
|
||||
$groupType = switch ($GroupScope) {
|
||||
"DomainLocal" { -2147483644 } # 0x80000004
|
||||
"Universal" { -2147483640 } # 0x80000008
|
||||
default { -2147483646 } # Global
|
||||
}
|
||||
$groupEntry.Properties["groupType"].Value = $groupType
|
||||
$groupEntry.CommitChanges()
|
||||
|
||||
# 멤버 추가
|
||||
$addedMembers = [System.Collections.Generic.List[string]]::new()
|
||||
if ($Members -and $Members.Count -gt 0) {
|
||||
foreach ($m in $Members) {
|
||||
if ([string]::IsNullOrWhiteSpace($m)) { continue }
|
||||
try {
|
||||
# TargetDc 상에서 사용자 검색
|
||||
$searcher = New-Object System.DirectoryServices.DirectorySearcher($ouEntry)
|
||||
$searcher.Filter = "(&(objectCategory=person)(objectClass=user)(sAMAccountName=$m))"
|
||||
$found = $searcher.FindOne()
|
||||
if ($found) {
|
||||
$userDn = $found.Properties["distinguishedname"][0]
|
||||
$groupEntry.Properties["member"].Add($userDn)
|
||||
$addedMembers.Add($m)
|
||||
}
|
||||
} catch {
|
||||
Write-Warning "사용자 [$m] 그룹 멤버 추가 중 오류: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
$groupEntry.CommitChanges()
|
||||
}
|
||||
|
||||
return [PSCustomObject]@{
|
||||
Success = $true
|
||||
Message = "타겟 DC [$TargetDc]에 보안그룹 [$GroupName] 직접 생성 완료 (멤버 $($addedMembers.Count)명 추가)"
|
||||
TargetDc = $TargetDc
|
||||
GroupDN = "CN=$GroupName,$TargetOU"
|
||||
AddedMembers = $addedMembers.ToArray()
|
||||
IsLive = $true
|
||||
}
|
||||
} catch {
|
||||
return [PSCustomObject]@{
|
||||
Success = $false
|
||||
Message = "타겟 DC [$TargetDc]에 그룹 생성 실패: $($_.Exception.Message)"
|
||||
TargetDc = $TargetDc
|
||||
IsLive = $true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Mock / Fallback
|
||||
return [PSCustomObject]@{
|
||||
Success = $true
|
||||
Message = "[Direct DC Binding] 타겟 DC [$TargetDc]에 보안그룹 [$GroupName] 직접 생성 성공 (NetApp 복제 대기 0초 달성)"
|
||||
TargetDc = $TargetDc
|
||||
GroupDN = "CN=$GroupName,$TargetOU"
|
||||
AddedMembers = $Members
|
||||
IsLive = $false
|
||||
}
|
||||
}
|
||||
|
||||
function Sync-ADObjectToTargetDc {
|
||||
param(
|
||||
[string]$ObjectDN,
|
||||
[string]$SourceDc = "dc01.corp.local",
|
||||
[string]$DestinationDc = "dc04.corp.local",
|
||||
[object]$Config = $null
|
||||
)
|
||||
|
||||
$useLive = if ($Config -and $Config.ActiveDirectory) { [bool]$Config.ActiveDirectory.UseLive } else { $false }
|
||||
|
||||
if ($useLive) {
|
||||
try {
|
||||
# 1. PowerShell ActiveDirectory Module의 Sync-ADObject 시도
|
||||
if (Get-Command Sync-ADObject -ErrorAction SilentlyContinue) {
|
||||
Sync-ADObject -Object $ObjectDN -Destination $DestinationDc -Source $SourceDc -ErrorAction Stop
|
||||
return [PSCustomObject]@{
|
||||
Success = $true
|
||||
Message = "Sync-ADObject 완료: [$ObjectDN] ($SourceDc -> $DestinationDc 즉시 단방향 복제)"
|
||||
SourceDc = $SourceDc
|
||||
DestinationDc = $DestinationDc
|
||||
Method = "Sync-ADObject"
|
||||
IsLive = $true
|
||||
}
|
||||
}
|
||||
|
||||
# 2. repadmin 명령행 도구 폴백 시도
|
||||
$repOutput = & repadmin.exe /replicate $DestinationDc $SourceDc $ObjectDN 2>&1
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
return [PSCustomObject]@{
|
||||
Success = $true
|
||||
Message = "repadmin 복제 성공 ($SourceDc -> $DestinationDc)"
|
||||
SourceDc = $SourceDc
|
||||
DestinationDc = $DestinationDc
|
||||
Method = "repadmin /replicate"
|
||||
IsLive = $true
|
||||
}
|
||||
} else {
|
||||
return [PSCustomObject]@{
|
||||
Success = $false
|
||||
Message = "repadmin 실패: $repOutput"
|
||||
SourceDc = $SourceDc
|
||||
DestinationDc = $DestinationDc
|
||||
Method = "repadmin /replicate"
|
||||
IsLive = $true
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return [PSCustomObject]@{
|
||||
Success = $false
|
||||
Message = "AD 개체 대상 복제 실패: $($_.Exception.Message)"
|
||||
SourceDc = $SourceDc
|
||||
DestinationDc = $DestinationDc
|
||||
IsLive = $true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Mock / Fallback
|
||||
return [PSCustomObject]@{
|
||||
Success = $true
|
||||
Message = "[Targeted Sync] 개체 [$ObjectDN]가 소스 DC [$SourceDc]에서 목적지 DC [$DestinationDc]로 즉시 단방향 복제되었습니다."
|
||||
SourceDc = $SourceDc
|
||||
DestinationDc = $DestinationDc
|
||||
Method = "Targeted Replication (Simulated)"
|
||||
IsLive = $false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +72,70 @@ function Test-NetAppConnection {
|
||||
}
|
||||
}
|
||||
|
||||
function Get-NetAppActiveDc {
|
||||
param(
|
||||
[string]$ClusterEndpoint = "",
|
||||
[string]$SvmName = "svm_cifs_prod01",
|
||||
[string]$Username = "",
|
||||
[string]$Password = "",
|
||||
[bool]$IgnoreSsl = $true,
|
||||
[object]$Config = $null
|
||||
)
|
||||
|
||||
if ($Config -and $Config.NetApp) {
|
||||
if (-not $ClusterEndpoint) { $ClusterEndpoint = $Config.NetApp.ClusterEndpoint }
|
||||
if (-not $SvmName) { $SvmName = $Config.NetApp.DefaultSvm }
|
||||
if (-not $Username) { $Username = $Config.NetApp.Username }
|
||||
if (-not $Password) { $Password = $Config.NetApp.Password }
|
||||
$IgnoreSsl = [bool]$Config.NetApp.IgnoreSslErrors
|
||||
}
|
||||
|
||||
$useLive = if ($Config -and $Config.NetApp) { [bool]$Config.NetApp.UseLive } else { $false }
|
||||
|
||||
if ($useLive -and $ClusterEndpoint) {
|
||||
try {
|
||||
# 1. ONTAP REST API CIFS Domain Discovered Servers 질의
|
||||
$path = "/api/protocols/cifs/domains?svm.name=$SvmName&fields=discovered_servers"
|
||||
$res = Invoke-NetAppRestRequest -ClusterEndpoint $ClusterEndpoint -UriPath $path -Method "GET" -Username $Username -Password $Password -IgnoreSsl $IgnoreSsl
|
||||
|
||||
if ($res.records) {
|
||||
foreach ($rec in $res.records) {
|
||||
if ($rec.discovered_servers) {
|
||||
# 상태가 "ok"인 Active DC 검색
|
||||
$matched = $rec.discovered_servers | Where-Object {
|
||||
($_.state -eq "ok" -or $_.status -eq "ok") -and ($_.type -like "*dc*" -or $_.server_type -like "*dc*")
|
||||
} | Select-Object -First 1
|
||||
|
||||
if ($matched -and $matched.name) {
|
||||
return [PSCustomObject]@{
|
||||
ActiveDC = $matched.name
|
||||
Node = if ($matched.node) { $matched.node.name } else { "node-01" }
|
||||
SvmName = $SvmName
|
||||
Status = "OK (Active Connected)"
|
||||
IsLive = $true
|
||||
DiscoveryMethod = "ONTAP REST API (/api/protocols/cifs/domains)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
Write-Warning "NetApp Active DC 조회 실패 (기본값 폴백): $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
# 6개 다중 DC 환경을 모사한 시뮬레이션 / 데모 Active DC 반환
|
||||
# (현업 시뮬레이션: 6대 DC 중 NetApp이 현재 바라보고 있는 DC04 반환)
|
||||
return [PSCustomObject]@{
|
||||
ActiveDC = "dc04.corp.local"
|
||||
Node = "ontap-node-02"
|
||||
SvmName = $SvmName
|
||||
Status = "OK (Active Binding)"
|
||||
IsLive = $false
|
||||
DiscoveryMethod = "ONTAP Domain Discovery (Simulated)"
|
||||
}
|
||||
}
|
||||
|
||||
function Get-NetAppQtrees {
|
||||
param(
|
||||
[string]$Filter = "",
|
||||
@@ -313,6 +377,137 @@ function New-NetAppShareAction {
|
||||
}
|
||||
}
|
||||
|
||||
function Set-NetAppShareAcl {
|
||||
param(
|
||||
[string]$ShareName,
|
||||
[string]$UserOrGroup,
|
||||
[string]$Permission = "change", # "full_control", "change", "read", "no_access"
|
||||
[string]$ActionType = "grant", # "grant", "revoke"
|
||||
[int]$MaxRetries = 3,
|
||||
[int]$RetryDelayMs = 1000,
|
||||
[object]$Config = $null
|
||||
)
|
||||
|
||||
$useLive = if ($Config -and $Config.NetApp) { [bool]$Config.NetApp.UseLive } else { $false }
|
||||
|
||||
if ($useLive) {
|
||||
$endpoint = $Config.NetApp.ClusterEndpoint
|
||||
$user = $Config.NetApp.Username
|
||||
$pass = $Config.NetApp.Password
|
||||
$svm = $Config.NetApp.DefaultSvm
|
||||
$ignoreSsl = [bool]$Config.NetApp.IgnoreSslErrors
|
||||
|
||||
$attempt = 0
|
||||
$lastError = ""
|
||||
|
||||
while ($attempt -lt $MaxRetries) {
|
||||
$attempt++
|
||||
try {
|
||||
if ($ActionType -eq "grant") {
|
||||
$body = @{
|
||||
user_or_group = $UserOrGroup
|
||||
permission = $Permission
|
||||
type = "allow"
|
||||
}
|
||||
$path = "/api/protocols/cifs/shares/$svm/$ShareName/acls"
|
||||
$null = Invoke-NetAppRestRequest -ClusterEndpoint $endpoint -UriPath $path -Method "POST" -Body $body -Username $user -Password $pass -IgnoreSsl $ignoreSsl
|
||||
} else {
|
||||
$path = "/api/protocols/cifs/shares/$svm/$ShareName/acls/$UserOrGroup"
|
||||
$null = Invoke-NetAppRestRequest -ClusterEndpoint $endpoint -UriPath $path -Method "DELETE" -Username $user -Password $pass -IgnoreSsl $ignoreSsl
|
||||
}
|
||||
|
||||
return [PSCustomObject]@{
|
||||
Success = $true
|
||||
Message = "NetApp Share [$ShareName]에 [$UserOrGroup] ($Permission) 권한이 반영되었습니다 (시도 횟수: $attempt)."
|
||||
Attempts = $attempt
|
||||
IsLive = $true
|
||||
}
|
||||
} catch {
|
||||
$lastError = $_.Exception.Message
|
||||
# AD DC 동기화 지연(객체를 찾을 수 없음) 또는 네트워크 지연인 경우 재시도
|
||||
if ($attempt -lt $MaxRetries) {
|
||||
Start-Sleep -Milliseconds $RetryDelayMs
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [PSCustomObject]@{
|
||||
Success = $false
|
||||
Message = "NetApp Share ACL 반영 실패 ($MaxRetries회 재시도 초과): $lastError"
|
||||
Attempts = $attempt
|
||||
IsLive = $true
|
||||
}
|
||||
}
|
||||
|
||||
# Mock / Fallback
|
||||
return [PSCustomObject]@{
|
||||
Success = $true
|
||||
Message = "[데모 모드] NetApp Share [$ShareName]에 [$UserOrGroup] 권한 ($Permission) 등록 완료 (즉시 바인딩 성공)"
|
||||
Attempts = 1
|
||||
IsLive = $false
|
||||
}
|
||||
}
|
||||
|
||||
function Set-NetAppQuotaRule {
|
||||
param(
|
||||
[string]$Volume,
|
||||
[string]$Qtree = "",
|
||||
[string]$Target = "",
|
||||
[string]$QuotaType = "user",
|
||||
[int64]$SoftLimitGb = 450,
|
||||
[int64]$HardLimitGb = 500,
|
||||
[object]$Config = $null
|
||||
)
|
||||
|
||||
$useLive = if ($Config -and $Config.NetApp) { [bool]$Config.NetApp.UseLive } else { $false }
|
||||
|
||||
if ($useLive) {
|
||||
try {
|
||||
$endpoint = $Config.NetApp.ClusterEndpoint
|
||||
$user = $Config.NetApp.Username
|
||||
$pass = $Config.NetApp.Password
|
||||
$svm = $Config.NetApp.DefaultSvm
|
||||
$ignoreSsl = [bool]$Config.NetApp.IgnoreSslErrors
|
||||
|
||||
$body = @{
|
||||
svm = @{ name = $svm }
|
||||
volume = @{ name = $Volume }
|
||||
type = $QuotaType
|
||||
space = @{
|
||||
hard_limit = $HardLimitGb * 1GB
|
||||
soft_limit = $SoftLimitGb * 1GB
|
||||
}
|
||||
}
|
||||
if ($Qtree) { $body["qtree"] = @{ name = $Qtree } }
|
||||
if ($Target) {
|
||||
if ($QuotaType -eq "user") { $body["users"] = @(@{ name = $Target }) }
|
||||
elseif ($QuotaType -eq "group") { $body["group"] = @{ name = $Target } }
|
||||
}
|
||||
|
||||
$res = Invoke-NetAppRestRequest -ClusterEndpoint $endpoint -UriPath "/api/storage/quota/rules" -Method "POST" -Body $body -Username $user -Password $pass -IgnoreSsl $ignoreSsl
|
||||
|
||||
return [PSCustomObject]@{
|
||||
Success = $true
|
||||
Message = "NetApp 볼륨 [$Volume] Quota 규칙 (Soft: ${SoftLimitGb}GB, Hard: ${HardLimitGb}GB) 생성/적용 완료"
|
||||
IsLive = $true
|
||||
}
|
||||
} catch {
|
||||
return [PSCustomObject]@{
|
||||
Success = $false
|
||||
Message = "NetApp Quota 설정 실패: $($_.Exception.Message)"
|
||||
IsLive = $true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Mock / Fallback
|
||||
return [PSCustomObject]@{
|
||||
Success = $true
|
||||
Message = "[데모 모드] 볼륨 [$Volume] (Qtree: $Qtree) Quota 정책 (Soft: ${SoftLimitGb}GB / Hard: ${HardLimitGb}GB) 반영 완료"
|
||||
IsLive = $false
|
||||
}
|
||||
}
|
||||
|
||||
function Get-NetAppSampleQtrees {
|
||||
param([string]$Filter = "")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user