diff --git a/CHANGELOG.md b/CHANGELOG.md index 069d5c4..a5e6022 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,21 @@ -# Enterprise Admin Studio - 변경 이력 (CHANGELOG.md) +# Enterprise Admin Studio - 변경 이력 (CHANGELOG.md) + +## [v1.7.0] - 2026-10-21 +### Fixed & Improved +- **NetApp ONTAP REST API Active DC 감지 및 단독 스크립트 실행(Standalone) 지원 강화**: + - **REST API JSON 필드 매핑 수정 (`Modules/NetAppService.ps1`)**: + - ONTAP REST API `/api/protocols/cifs/domains`의 `discovered_servers` 하위 개체에서 도메인 컨트롤러 호스트명 키(`server_name`)가 `name`으로 잘못 매핑되어 실서버 연결 시에도 항상 가상 데모 DC(`dc04.corp.local`)로 폴백되던 버그 수정 + - `server_name`, `server_ip`, `server_type`, `state`, `node.name` 필드 정상 파싱 및 `Active Connected` 상태 검증 로직 강화 + - **파라미터 해석 헬퍼 (`Resolve-NetAppParameters`) 신설**: + - `NetAppService.ps1`을 GUI 애플리케이션 외부의 독립 PowerShell 콘솔에서 단독 발췌 실행할 때, `$Config` 객체 없이도 `-ClusterEndpoint`, `-Username`, `-Password`, `-SvmName`, `-UseLive` 파라미터만으로 즉각 실제 스토리지와 통신하도록 구조 개선 + - `Get-NetAppActiveDc`, `Get-NetAppQtrees`, `Get-NetAppQuotas`, `Get-NetAppShares`, `New-NetAppQtreeAction`, `New-NetAppShareAction`, `Set-NetAppShareAcl`, `Set-NetAppQuotaRule` 등 모든 NetApp 모듈 함수에 단독 파라미터 전달 체계 적용 + - **PowerShell 7 (Core) 자체 서명 SSL 인증서 우회(`-SkipCertificateCheck`) 호환성 확보**: + - Windows PowerShell 5.1의 `ServicePointManager` 방식 외에, PowerShell 7 `Invoke-RestMethod`의 `-SkipCertificateCheck` 매개변수를 동적 인식하여 SSL/TLS 핸드셰이크 예외 완벽 차단 + - **다단계 API 엔드포인트 폴백 및 타임아웃 현실화**: + - 클러스터 도메인 질의 시 기본 타임아웃을 3초에서 15초로 현실화하여 노드 간 브로드캐스트 지연에 의한 타임아웃 예외 방지 + - SVM별 필터 쿼리(`/api/protocols/cifs/domains?svm.name=...`), 전체 조회 쿼리(`?fields=discovered_servers,svm.name`), 컬렉션 단건 self-link 조회, 그리고 NetApp DataONTAP Toolkit(`Get-NcCifsDomainDiscoveredServer`)까지 4단계 복합 폴백 엔진 구축 + - **상세 오류 추적 지원**: + - 실제 접속 실패 시 `Write-Warning`으로 각 엔드포인트별 에러 메시지를 즉시 출력하고, 반환 객체의 `FallbackReason` 속성에 기록하여 원인 분석을 용이하게 함 ## [v1.6.0] - 2026-10-21 ### Added diff --git a/MULTI_DC_SYNC_GUIDE.md b/MULTI_DC_SYNC_GUIDE.md index 6bbb8d3..ce632b3 100644 --- a/MULTI_DC_SYNC_GUIDE.md +++ b/MULTI_DC_SYNC_GUIDE.md @@ -1,4 +1,4 @@ -# 다중 도메인 컨트롤러(6대 DC) 환경에서의 NetApp Storage - Active Directory 실시간 동기화 및 비동기 처리 가이드 +# 다중 도메인 컨트롤러(6대 DC) 환경에서의 NetApp Storage - Active Directory 실시간 동기화 및 비동기 처리 가이드 본 문서는 Active Directory(AD)가 6대로 다중화된 대규모 엔터프라이즈 인프라에서, NetApp 스토리지(Qtree, Quota, CIFS Share)와 AD 보안그룹을 실시간 생성·연동할 때 발생하는 **도메인 복제 지연(Replication Lag) 오류**를 원천 차단하고, UI 멈춤 없이 무중단으로 작업을 처리하는 **비동기(`Task.Run`) 아키텍처**에 대해 상세히 기술합니다. @@ -152,6 +152,32 @@ repadmin /kcc * repadmin /showobjmeta dc04.corp.local "CN=Finance_Share,OU=SecurityGroups,DC=corp,DC=local" ``` +### 5.3 PowerShell 콘솔에서 NetAppService.ps1 단독 발췌 테스트 +GUI 앱(ModernAdminStudio)을 띄우지 않고, 관리자 PowerShell 콘솔에서 모듈 파일만 단독으로 로드하여 NetApp Active DC 바인딩을 즉각 검증할 수 있습니다: + +```powershell +# 1. 모듈 로드 +. .\Modules\NetAppService.ps1 + +# 2. 클러스터 기본 연결 테스트 +Test-NetAppConnection -ClusterEndpoint "https://10.20.1.10" -Username "admin" -Password "YourPassword" + +# 3. NetApp이 현재 바인딩 중인 Active DC 실시간 조회 +$dcInfo = Get-NetAppActiveDc ` + -ClusterEndpoint "https://10.20.1.10" ` + -Username "admin" ` + -Password "YourPassword" ` + -SvmName "svm_cifs_prod01" ` + -TimeoutSec 15 + +# 4. 결과 확인 (ActiveDC, IP, 상태, 전체 발견된 DC 목록) +$dcInfo | Format-List ActiveDC, ActiveDcIp, Node, SvmName, Status, State, DiscoveryMethod, IsLive +$dcInfo.AllDiscoveredServers | Format-Table ServerName, ServerIp, ServerType, State, Node, SvmName +``` + +> **기술 참고 (REST API JSON Schema)**: +> ONTAP REST API `/api/protocols/cifs/domains`의 `discovered_servers[]` 배열에서 각 서버의 FQDN 호스트명 필드는 `name`이 아니라 **`server_name`**입니다. 스크립트 단독 호출 시 `-ClusterEndpoint`를 제공하면 자동으로 `UseLive = $true`로 승격되어 실제 REST API를 호출합니다. + --- ## 6. 결론 및 기대 효과 diff --git a/Modules/NetAppService.ps1 b/Modules/NetAppService.ps1 index b05ff20..724a2ea 100644 --- a/Modules/NetAppService.ps1 +++ b/Modules/NetAppService.ps1 @@ -1,6 +1,55 @@ # Modules\NetAppService.ps1 # NetApp ONTAP REST API Service Module (Live & Fallback) +function Resolve-NetAppParameters { + param( + [string]$ClusterEndpoint = "", + [string]$Username = "", + [string]$Password = "", + [string]$SvmName = "", + [bool]$IgnoreSsl = $true, + [bool]$UseLive = $false, + [object]$Config = $null, + [hashtable]$BoundParameters = @{} + ) + + $resolvedEndpoint = $ClusterEndpoint + $resolvedUser = $Username + $resolvedPass = $Password + $resolvedSvm = $SvmName + $resolvedIgnoreSsl = $IgnoreSsl + $resolvedLive = $UseLive + + if ($Config -and $Config.NetApp) { + if (-not $resolvedEndpoint) { $resolvedEndpoint = $Config.NetApp.ClusterEndpoint } + if (-not $resolvedUser) { $resolvedUser = $Config.NetApp.Username } + if (-not $resolvedPass) { $resolvedPass = $Config.NetApp.Password } + if (-not $resolvedSvm) { $resolvedSvm = $Config.NetApp.DefaultSvm } + if ($null -ne $Config.NetApp.IgnoreSslErrors) { $resolvedIgnoreSsl = [bool]$Config.NetApp.IgnoreSslErrors } + if ($null -ne $Config.NetApp.UseLive) { $resolvedLive = [bool]$Config.NetApp.UseLive } + } + + # 사용자가 파라미터로 엔드포인트를 직접 지정한 경우, UseLive가 명시적으로 false로 주어지지 않았다면 실제 연결로 판단 + if (-not [string]::IsNullOrWhiteSpace($resolvedEndpoint)) { + if ($BoundParameters.ContainsKey('UseLive')) { + $resolvedLive = $UseLive + } elseif ($Config -and $Config.NetApp -and ($null -ne $Config.NetApp.UseLive)) { + $resolvedLive = [bool]$Config.NetApp.UseLive + } else { + $resolvedLive = $true + } + } + + return @{ + Endpoint = $resolvedEndpoint + Username = $resolvedUser + Password = $resolvedPass + SvmName = $resolvedSvm + IgnoreSsl = $resolvedIgnoreSsl + UseLive = $resolvedLive + } +} + function Invoke-NetAppRestRequest { param( [string]$ClusterEndpoint, @@ -9,18 +58,30 @@ function Invoke-NetAppRestRequest { [object]$Body = $null, [string]$Username = "", [string]$Password = "", - [bool]$IgnoreSsl = $true + [bool]$IgnoreSsl = $true, + [int]$TimeoutSec = 15 ) - # SSL 우회 설정 (.NET 기반) - if ($IgnoreSsl) { - [System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true } - [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 -bor [System.Net.SecurityProtocolType]::Tls13 + if ([string]::IsNullOrWhiteSpace($ClusterEndpoint)) { + throw "NetApp 클러스터 엔드포인트 URL이 지정되지 않았습니다." } $base = $ClusterEndpoint.TrimEnd('/') + if (-not ($base -like "http*")) { + $base = "https://$base" + } $fullUri = "$base$UriPath" + # SSL 우회 설정 (.NET Framework 4.8 / Windows PowerShell 5.1 호환) + if ($IgnoreSsl) { + [System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true } + try { + [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 -bor [System.Net.SecurityProtocolType]::Tls13 + } catch { + [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 + } + } + $headers = @{ "Accept" = "application/json" } @@ -32,10 +93,15 @@ function Invoke-NetAppRestRequest { } $params = @{ - Uri = $fullUri - Method = $Method - Headers = $headers - TimeoutSec = 3 + Uri = $fullUri + Method = $Method + Headers = $headers + TimeoutSec = $TimeoutSec + } + + # PowerShell 7 (Core) SkipCertificateCheck 지원 (ServicePointManager 우회 무시 문제 해결) + if ($IgnoreSsl -and (Get-Command Invoke-RestMethod).Parameters.ContainsKey('SkipCertificateCheck')) { + $params["SkipCertificateCheck"] = $true } if ($Body) { @@ -51,11 +117,12 @@ function Test-NetAppConnection { [string]$ClusterEndpoint, [string]$Username, [string]$Password, - [bool]$IgnoreSsl = $true + [bool]$IgnoreSsl = $true, + [int]$TimeoutSec = 15 ) try { - $res = Invoke-NetAppRestRequest -ClusterEndpoint $ClusterEndpoint -UriPath "/api/cluster" -Method "GET" -Username $Username -Password $Password -IgnoreSsl $IgnoreSsl + $res = Invoke-NetAppRestRequest -ClusterEndpoint $ClusterEndpoint -UriPath "/api/cluster" -Method "GET" -Username $Username -Password $Password -IgnoreSsl $IgnoreSsl -TimeoutSec $TimeoutSec $clusterName = if ($res.name) { $res.name } else { "ONTAP Cluster" } $version = if ($res.version -and $res.version.full) { $res.version.full } else { "ONTAP 9.x" } @@ -75,83 +142,224 @@ function Test-NetAppConnection { function Get-NetAppActiveDc { param( [string]$ClusterEndpoint = "", - [string]$SvmName = "svm_cifs_prod01", + [string]$SvmName = "", [string]$Username = "", [string]$Password = "", [bool]$IgnoreSsl = $true, + [bool]$UseLive = $false, + [int]$TimeoutSec = 15, [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 - } + $resolved = Resolve-NetAppParameters -ClusterEndpoint $ClusterEndpoint -Username $Username -Password $Password -SvmName $SvmName -IgnoreSsl $IgnoreSsl -UseLive $UseLive -Config $Config -BoundParameters $PSBoundParameters + $endpoint = $resolved.Endpoint + $user = $resolved.Username + $pass = $resolved.Password + $svm = $resolved.SvmName + $ignoreSslVal = $resolved.IgnoreSsl + $liveMode = $resolved.UseLive - $useLive = if ($Config -and $Config.NetApp) { [bool]$Config.NetApp.UseLive } else { $false } + $discoveredList = [System.Collections.Generic.List[PSCustomObject]]::new() + $attemptErrors = [System.Collections.Generic.List[string]]::new() - if ($useLive -and $ClusterEndpoint) { + if ($liveMode -and (-not [string]::IsNullOrWhiteSpace($endpoint))) { + # 1. NetApp ONTAP REST API 질의 시도 + $queryPaths = [System.Collections.Generic.List[string]]::new() + if (-not [string]::IsNullOrWhiteSpace($svm)) { + $queryPaths.Add("/api/protocols/cifs/domains?svm.name=$svm&fields=discovered_servers,svm.name") + $queryPaths.Add("/api/protocols/cifs/domains?svm.name=$svm&fields=*") + } + $queryPaths.Add("/api/protocols/cifs/domains?fields=discovered_servers,svm.name") + $queryPaths.Add("/api/protocols/cifs/domains?fields=*") + $queryPaths.Add("/api/protocols/cifs/domains") + + $records = @() + foreach ($qPath in $queryPaths) { + try { + $res = Invoke-NetAppRestRequest -ClusterEndpoint $endpoint -UriPath $qPath -Method "GET" -Username $user -Password $pass -IgnoreSsl $ignoreSslVal -TimeoutSec $TimeoutSec + if ($res -and $res.records -and $res.records.Count -gt 0) { + $records = @($res.records) + break + } elseif ($res -and $res.discovered_servers) { + $records = @($res) + break + } + } catch { + $attemptErrors.Add("[$qPath] $($_.Exception.Message)") + } + } + + if ($records.Count -gt 0) { + # SVM 필터링 (일치하는 SVM 우선, 없으면 전체 records) + $targetRecs = if (-not [string]::IsNullOrWhiteSpace($svm)) { + $matched = @($records | Where-Object { $_.svm -and ($_.svm.name -eq $svm -or $_.svm.uuid -eq $svm) }) + if ($matched.Count -gt 0) { $matched } else { $records } + } else { + $records + } + + foreach ($rec in $targetRecs) { + $curSvm = if ($rec.svm -and $rec.svm.name) { $rec.svm.name } elseif ($svm) { $svm } else { "default_svm" } + + # 만약 discovered_servers가 컬렉션에서 펼쳐지지 않았고 self 링크가 있으면 단건 조회 + $dsList = $rec.discovered_servers + if (-not $dsList -and $rec._links -and $rec._links.self -and $rec._links.self.href) { + try { + $selfRes = Invoke-NetAppRestRequest -ClusterEndpoint $endpoint -UriPath "$($rec._links.self.href)?fields=discovered_servers,svm.name" -Method "GET" -Username $user -Password $pass -IgnoreSsl $ignoreSslVal -TimeoutSec $TimeoutSec + if ($selfRes -and $selfRes.discovered_servers) { + $dsList = $selfRes.discovered_servers + } + } catch { + $attemptErrors.Add("[SelfHref $($rec._links.self.href)] $($_.Exception.Message)") + } + } + + if ($dsList) { + foreach ($ds in $dsList) { + # ONTAP REST API JSON 스키마 필드 추출 (server_name, server_ip, server_type, state, node.name 등) + $srvName = if ($ds.server_name) { $ds.server_name } + elseif ($ds.name) { $ds.name } + elseif ($ds.server) { $ds.server } + elseif ($ds.server_ip) { $ds.server_ip } + else { "" } + + $srvIp = if ($ds.server_ip) { $ds.server_ip } else { "" } + $srvType = if ($ds.server_type) { $ds.server_type } elseif ($ds.type) { $ds.type } else { "dc" } + $srvState = if ($ds.state) { $ds.state } elseif ($ds.status) { $ds.status } else { "unknown" } + $srvNode = if ($ds.node -and $ds.node.name) { $ds.node.name } + elseif ($ds.node) { $ds.node.ToString() } else { "node-01" } + $srvDomain = if ($ds.domain) { $ds.domain } else { "" } + + $discoveredList.Add([PSCustomObject]@{ + ServerName = $srvName + ServerIp = $srvIp + ServerType = $srvType + State = $srvState + Node = $srvNode + Domain = $srvDomain + SvmName = $curSvm + }) + } + } + } + + # 1순위: 상태가 "ok", "active", "up" 이면서 type이 DC인 서버 + $bestMatch = $discoveredList | Where-Object { + ($_.State -like "*ok*" -or $_.State -like "*up*" -or $_.State -like "*active*") -and + ($_.ServerType -like "*dc*" -or $_.ServerType -like "*primary*") + } | Select-Object -First 1 + + # 2순위: 상태 무관 DC 타입 서버 + if (-not $bestMatch) { + $bestMatch = $discoveredList | Where-Object { + $_.ServerType -like "*dc*" + } | Select-Object -First 1 + } + + # 3순위: 목록의 첫 번째 유효 서버 + if (-not $bestMatch -and $discoveredList.Count -gt 0) { + $bestMatch = $discoveredList[0] + } + + if ($bestMatch -and -not [string]::IsNullOrWhiteSpace($bestMatch.ServerName)) { + return [PSCustomObject]@{ + ActiveDC = $bestMatch.ServerName + ActiveDcIp = $bestMatch.ServerIp + Node = $bestMatch.Node + SvmName = $bestMatch.SvmName + Status = "OK (Active Connected)" + State = $bestMatch.State + ServerType = $bestMatch.ServerType + IsLive = $true + DiscoveryMethod = "ONTAP REST API (/api/protocols/cifs/domains)" + DiscoveredServerCount= $discoveredList.Count + AllDiscoveredServers = $discoveredList.ToArray() + } + } + } + + # 2. NetApp DataONTAP PowerShell Toolkit 모듈 폴백 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 (Get-Command Get-NcCifsDomainDiscoveredServer -ErrorAction SilentlyContinue) { + $ncServers = if ($svm) { + Get-NcCifsDomainDiscoveredServer -Vserver $svm -ErrorAction Stop + } else { + Get-NcCifsDomainDiscoveredServer -ErrorAction Stop + } - 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 ($ncServers) { + $activeNc = $ncServers | Where-Object { ($_.ServerType -like "*dc*" -or $_.ServerType -like "*domain-controller*") -and ($_.ServerStatus -like "*ok*" -or $_.ServerStatus -like "*active*") } | Select-Object -First 1 + if (-not $activeNc) { $activeNc = $ncServers | 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)" - } + if ($activeNc) { + return [PSCustomObject]@{ + ActiveDC = if ($activeNc.ServerName) { $activeNc.ServerName } else { $activeNc.ServerAddress } + ActiveDcIp = $activeNc.ServerAddress + Node = if ($activeNc.Node) { $activeNc.Node } else { "node-01" } + SvmName = if ($activeNc.Vserver) { $activeNc.Vserver } else { $svm } + Status = "OK (Active Binding)" + State = $activeNc.ServerStatus + ServerType = $activeNc.ServerType + IsLive = $true + DiscoveryMethod = "NetApp DataONTAP Toolkit (Get-NcCifsDomainDiscoveredServer)" + DiscoveredServerCount= @($ncServers).Count + AllDiscoveredServers = @($ncServers) } } } } } catch { - Write-Warning "NetApp Active DC 조회 실패 (기본값 폴백): $($_.Exception.Message)" + $attemptErrors.Add("[DataONTAP Toolkit] $($_.Exception.Message)") + } + + if ($attemptErrors.Count -gt 0) { + Write-Warning "NetApp 실제 Active DC 조회 실패 사유:`n$($attemptErrors -join "`n")`n(데모 시뮬레이션 모드로 전환합니다)" } } - # 6개 다중 DC 환경을 모사한 시뮬레이션 / 데모 Active DC 반환 - # (현업 시뮬레이션: 6대 DC 중 NetApp이 현재 바라보고 있는 DC04 반환) + # Fallback / Demo Mock return [PSCustomObject]@{ - ActiveDC = "dc04.corp.local" - Node = "ontap-node-02" - SvmName = $SvmName - Status = "OK (Active Binding)" - IsLive = $false - DiscoveryMethod = "ONTAP Domain Discovery (Simulated)" + ActiveDC = "dc04.corp.local" + ActiveDcIp = "10.20.1.14" + Node = "ontap-node-02" + SvmName = if ($svm) { $svm } else { "svm_cifs_prod01" } + Status = "OK (Simulated Active Binding)" + State = "ok" + ServerType = "ms_dc" + IsLive = $false + DiscoveryMethod = "ONTAP Domain Discovery (Simulated)" + DiscoveredServerCount= 2 + FallbackReason = if ($attemptErrors.Count -gt 0) { ($attemptErrors -join "; ") } else { "Live mode not configured" } + AllDiscoveredServers = @( + [PSCustomObject]@{ ServerName = "dc04.corp.local"; ServerIp = "10.20.1.14"; ServerType = "ms_dc"; State = "ok"; Node = "ontap-node-02"; SvmName = "svm_cifs_prod01" }, + [PSCustomObject]@{ ServerName = "dc01.corp.local"; ServerIp = "10.10.1.11"; ServerType = "ms_dc"; State = "ok"; Node = "ontap-node-01"; SvmName = "svm_cifs_prod01" } + ) } } function Get-NetAppQtrees { param( [string]$Filter = "", + [string]$ClusterEndpoint = "", + [string]$Username = "", + [string]$Password = "", + [bool]$IgnoreSsl = $true, + [bool]$UseLive = $false, + [int]$TimeoutSec = 15, [object]$Config = $null ) - $useLive = if ($Config -and $Config.NetApp) { [bool]$Config.NetApp.UseLive } else { $false } + $resolved = Resolve-NetAppParameters -ClusterEndpoint $ClusterEndpoint -Username $Username -Password $Password -IgnoreSsl $IgnoreSsl -UseLive $UseLive -Config $Config -BoundParameters $PSBoundParameters + $useLive = $resolved.UseLive - if ($useLive) { + if ($useLive -and (-not [string]::IsNullOrWhiteSpace($resolved.Endpoint))) { try { - $endpoint = $Config.NetApp.ClusterEndpoint - $user = $Config.NetApp.Username - $pass = $Config.NetApp.Password - $ignoreSsl = [bool]$Config.NetApp.IgnoreSslErrors + $endpoint = $resolved.Endpoint + $user = $resolved.Username + $pass = $resolved.Password + $ignoreSsl = $resolved.IgnoreSsl - $res = Invoke-NetAppRestRequest -ClusterEndpoint $endpoint -UriPath "/api/storage/qtrees?fields=name,volume.name,security_style,oplocks,status" -Method "GET" -Username $user -Password $pass -IgnoreSsl $ignoreSsl + $res = Invoke-NetAppRestRequest -ClusterEndpoint $endpoint -UriPath "/api/storage/qtrees?fields=name,volume.name,security_style,oplocks,status" -Method "GET" -Username $user -Password $pass -IgnoreSsl $ignoreSsl -TimeoutSec $TimeoutSec if ($res.records) { $list = [System.Collections.Generic.List[PSCustomObject]]::new() @@ -188,20 +396,27 @@ function Get-NetAppQtrees { function Get-NetAppQuotas { param( [string]$Filter = "", + [string]$ClusterEndpoint = "", + [string]$Username = "", + [string]$Password = "", + [bool]$IgnoreSsl = $true, + [bool]$UseLive = $false, + [int]$TimeoutSec = 15, [object]$Config = $null ) - $useLive = if ($Config -and $Config.NetApp) { [bool]$Config.NetApp.UseLive } else { $false } + $resolved = Resolve-NetAppParameters -ClusterEndpoint $ClusterEndpoint -Username $Username -Password $Password -IgnoreSsl $IgnoreSsl -UseLive $UseLive -Config $Config -BoundParameters $PSBoundParameters + $useLive = $resolved.UseLive - if ($useLive) { + if ($useLive -and (-not [string]::IsNullOrWhiteSpace($resolved.Endpoint))) { try { - $endpoint = $Config.NetApp.ClusterEndpoint - $user = $Config.NetApp.Username - $pass = $Config.NetApp.Password - $ignoreSsl = [bool]$Config.NetApp.IgnoreSslErrors + $endpoint = $resolved.Endpoint + $user = $resolved.Username + $pass = $resolved.Password + $ignoreSsl = $resolved.IgnoreSsl # ONTAP REST quota rules - $res = Invoke-NetAppRestRequest -ClusterEndpoint $endpoint -UriPath "/api/storage/quota/rules?fields=type,users.name,group.name,qtree.name,volume.name,space.hard_limit,space.soft_limit" -Method "GET" -Username $user -Password $pass -IgnoreSsl $ignoreSsl + $res = Invoke-NetAppRestRequest -ClusterEndpoint $endpoint -UriPath "/api/storage/quota/rules?fields=type,users.name,group.name,qtree.name,volume.name,space.hard_limit,space.soft_limit" -Method "GET" -Username $user -Password $pass -IgnoreSsl $ignoreSsl -TimeoutSec $TimeoutSec if ($res.records) { $list = [System.Collections.Generic.List[PSCustomObject]]::new() @@ -244,19 +459,26 @@ function Get-NetAppQuotas { function Get-NetAppShares { param( [string]$Filter = "", + [string]$ClusterEndpoint = "", + [string]$Username = "", + [string]$Password = "", + [bool]$IgnoreSsl = $true, + [bool]$UseLive = $false, + [int]$TimeoutSec = 15, [object]$Config = $null ) - $useLive = if ($Config -and $Config.NetApp) { [bool]$Config.NetApp.UseLive } else { $false } + $resolved = Resolve-NetAppParameters -ClusterEndpoint $ClusterEndpoint -Username $Username -Password $Password -IgnoreSsl $IgnoreSsl -UseLive $UseLive -Config $Config -BoundParameters $PSBoundParameters + $useLive = $resolved.UseLive - if ($useLive) { + if ($useLive -and (-not [string]::IsNullOrWhiteSpace($resolved.Endpoint))) { try { - $endpoint = $Config.NetApp.ClusterEndpoint - $user = $Config.NetApp.Username - $pass = $Config.NetApp.Password - $ignoreSsl = [bool]$Config.NetApp.IgnoreSslErrors + $endpoint = $resolved.Endpoint + $user = $resolved.Username + $pass = $resolved.Password + $ignoreSsl = $resolved.IgnoreSsl - $res = Invoke-NetAppRestRequest -ClusterEndpoint $endpoint -UriPath "/api/protocols/cifs/shares?fields=name,path,comment,svm.name" -Method "GET" -Username $user -Password $pass -IgnoreSsl $ignoreSsl + $res = Invoke-NetAppRestRequest -ClusterEndpoint $endpoint -UriPath "/api/protocols/cifs/shares?fields=name,path,comment,svm.name" -Method "GET" -Username $user -Password $pass -IgnoreSsl $ignoreSsl -TimeoutSec $TimeoutSec if ($res.records) { $list = [System.Collections.Generic.List[PSCustomObject]]::new() @@ -292,36 +514,48 @@ function New-NetAppQtreeAction { [string]$Volume, [string]$QtreeName, [string]$SecurityStyle = "ntfs", + [string]$SvmName = "", + [string]$ClusterEndpoint = "", + [string]$Username = "", + [string]$Password = "", + [bool]$IgnoreSsl = $true, + [bool]$UseLive = $false, + [int]$TimeoutSec = 20, [object]$Config = $null ) - $useLive = if ($Config -and $Config.NetApp) { [bool]$Config.NetApp.UseLive } else { $false } + $resolved = Resolve-NetAppParameters -ClusterEndpoint $ClusterEndpoint -Username $Username -Password $Password -SvmName $SvmName -IgnoreSsl $IgnoreSsl -UseLive $UseLive -Config $Config -BoundParameters $PSBoundParameters + $useLive = $resolved.UseLive - if ($useLive) { + if ($useLive -and (-not [string]::IsNullOrWhiteSpace($resolved.Endpoint))) { try { - $endpoint = $Config.NetApp.ClusterEndpoint - $user = $Config.NetApp.Username - $pass = $Config.NetApp.Password - $svm = $Config.NetApp.DefaultSvm - $ignoreSsl = [bool]$Config.NetApp.IgnoreSslErrors + $endpoint = $resolved.Endpoint + $user = $resolved.Username + $pass = $resolved.Password + $svm = $resolved.SvmName + $ignoreSsl = $resolved.IgnoreSsl $body = @{ name = $QtreeName volume = @{ name = $Volume } - svm = @{ name = $svm } security_style = $SecurityStyle } + if (-not [string]::IsNullOrWhiteSpace($svm)) { + $body["svm"] = @{ name = $svm } + } - $res = Invoke-NetAppRestRequest -ClusterEndpoint $endpoint -UriPath "/api/storage/qtrees" -Method "POST" -Body $body -Username $user -Password $pass -IgnoreSsl $ignoreSsl + $res = Invoke-NetAppRestRequest -ClusterEndpoint $endpoint -UriPath "/api/storage/qtrees" -Method "POST" -Body $body -Username $user -Password $pass -IgnoreSsl $ignoreSsl -TimeoutSec $TimeoutSec return [PSCustomObject]@{ Success = $true Message = "실제 NetApp 볼륨 [$Volume]에 Qtree [$QtreeName]가 성공적으로 생성되었습니다." + IsLive = $true } } catch { return [PSCustomObject]@{ Success = $false Message = "NetApp Qtree 생성 실패: $($_.Exception.Message)" + IsLive = $true } } } @@ -329,6 +563,7 @@ function New-NetAppQtreeAction { return [PSCustomObject]@{ Success = $true Message = "[데모 모드] 볼륨 [$Volume]에 Qtree [$QtreeName]가 가상 생성되었습니다." + IsLive = $false } } @@ -337,36 +572,48 @@ function New-NetAppShareAction { [string]$ShareName, [string]$Path, [string]$Comment = "", + [string]$SvmName = "", + [string]$ClusterEndpoint = "", + [string]$Username = "", + [string]$Password = "", + [bool]$IgnoreSsl = $true, + [bool]$UseLive = $false, + [int]$TimeoutSec = 20, [object]$Config = $null ) - $useLive = if ($Config -and $Config.NetApp) { [bool]$Config.NetApp.UseLive } else { $false } + $resolved = Resolve-NetAppParameters -ClusterEndpoint $ClusterEndpoint -Username $Username -Password $Password -SvmName $SvmName -IgnoreSsl $IgnoreSsl -UseLive $UseLive -Config $Config -BoundParameters $PSBoundParameters + $useLive = $resolved.UseLive - if ($useLive) { + if ($useLive -and (-not [string]::IsNullOrWhiteSpace($resolved.Endpoint))) { try { - $endpoint = $Config.NetApp.ClusterEndpoint - $user = $Config.NetApp.Username - $pass = $Config.NetApp.Password - $svm = $Config.NetApp.DefaultSvm - $ignoreSsl = [bool]$Config.NetApp.IgnoreSslErrors + $endpoint = $resolved.Endpoint + $user = $resolved.Username + $pass = $resolved.Password + $svm = $resolved.SvmName + $ignoreSsl = $resolved.IgnoreSsl $body = @{ name = $ShareName path = $Path comment = $Comment - svm = @{ name = $svm } + } + if (-not [string]::IsNullOrWhiteSpace($svm)) { + $body["svm"] = @{ name = $svm } } - $res = Invoke-NetAppRestRequest -ClusterEndpoint $endpoint -UriPath "/api/protocols/cifs/shares" -Method "POST" -Body $body -Username $user -Password $pass -IgnoreSsl $ignoreSsl + $res = Invoke-NetAppRestRequest -ClusterEndpoint $endpoint -UriPath "/api/protocols/cifs/shares" -Method "POST" -Body $body -Username $user -Password $pass -IgnoreSsl $ignoreSsl -TimeoutSec $TimeoutSec return [PSCustomObject]@{ Success = $true Message = "실제 NetApp SVM [$svm]에 CIFS 공유 [$ShareName] ($Path)가 생성되었습니다." + IsLive = $true } } catch { return [PSCustomObject]@{ Success = $false Message = "NetApp CIFS 공유 생성 실패: $($_.Exception.Message)" + IsLive = $true } } } @@ -374,6 +621,7 @@ function New-NetAppShareAction { return [PSCustomObject]@{ Success = $true Message = "[데모 모드] CIFS 공유 [$ShareName] ($Path)가 가상 생성되었습니다." + IsLive = $false } } @@ -385,17 +633,25 @@ function Set-NetAppShareAcl { [string]$ActionType = "grant", # "grant", "revoke" [int]$MaxRetries = 3, [int]$RetryDelayMs = 1000, + [string]$SvmName = "", + [string]$ClusterEndpoint = "", + [string]$Username = "", + [string]$Password = "", + [bool]$IgnoreSsl = $true, + [bool]$UseLive = $false, + [int]$TimeoutSec = 20, [object]$Config = $null ) - $useLive = if ($Config -and $Config.NetApp) { [bool]$Config.NetApp.UseLive } else { $false } + $resolved = Resolve-NetAppParameters -ClusterEndpoint $ClusterEndpoint -Username $Username -Password $Password -SvmName $SvmName -IgnoreSsl $IgnoreSsl -UseLive $UseLive -Config $Config -BoundParameters $PSBoundParameters + $useLive = $resolved.UseLive - if ($useLive) { - $endpoint = $Config.NetApp.ClusterEndpoint - $user = $Config.NetApp.Username - $pass = $Config.NetApp.Password - $svm = $Config.NetApp.DefaultSvm - $ignoreSsl = [bool]$Config.NetApp.IgnoreSslErrors + if ($useLive -and (-not [string]::IsNullOrWhiteSpace($resolved.Endpoint))) { + $endpoint = $resolved.Endpoint + $user = $resolved.Username + $pass = $resolved.Password + $svm = if ($resolved.SvmName) { $resolved.SvmName } else { "default_svm" } + $ignoreSsl = $resolved.IgnoreSsl $attempt = 0 $lastError = "" @@ -410,10 +666,10 @@ function Set-NetAppShareAcl { 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 + $null = Invoke-NetAppRestRequest -ClusterEndpoint $endpoint -UriPath $path -Method "POST" -Body $body -Username $user -Password $pass -IgnoreSsl $ignoreSsl -TimeoutSec $TimeoutSec } 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 + $null = Invoke-NetAppRestRequest -ClusterEndpoint $endpoint -UriPath $path -Method "DELETE" -Username $user -Password $pass -IgnoreSsl $ignoreSsl -TimeoutSec $TimeoutSec } return [PSCustomObject]@{ @@ -456,21 +712,28 @@ function Set-NetAppQuotaRule { [string]$QuotaType = "user", [int64]$SoftLimitGb = 450, [int64]$HardLimitGb = 500, + [string]$SvmName = "", + [string]$ClusterEndpoint = "", + [string]$Username = "", + [string]$Password = "", + [bool]$IgnoreSsl = $true, + [bool]$UseLive = $false, + [int]$TimeoutSec = 20, [object]$Config = $null ) - $useLive = if ($Config -and $Config.NetApp) { [bool]$Config.NetApp.UseLive } else { $false } + $resolved = Resolve-NetAppParameters -ClusterEndpoint $ClusterEndpoint -Username $Username -Password $Password -SvmName $SvmName -IgnoreSsl $IgnoreSsl -UseLive $UseLive -Config $Config -BoundParameters $PSBoundParameters + $useLive = $resolved.UseLive - if ($useLive) { + if ($useLive -and (-not [string]::IsNullOrWhiteSpace($resolved.Endpoint))) { try { - $endpoint = $Config.NetApp.ClusterEndpoint - $user = $Config.NetApp.Username - $pass = $Config.NetApp.Password - $svm = $Config.NetApp.DefaultSvm - $ignoreSsl = [bool]$Config.NetApp.IgnoreSslErrors + $endpoint = $resolved.Endpoint + $user = $resolved.Username + $pass = $resolved.Password + $svm = $resolved.SvmName + $ignoreSsl = $resolved.IgnoreSsl $body = @{ - svm = @{ name = $svm } volume = @{ name = $Volume } type = $QuotaType space = @{ @@ -478,13 +741,16 @@ function Set-NetAppQuotaRule { soft_limit = $SoftLimitGb * 1GB } } + if (-not [string]::IsNullOrWhiteSpace($svm)) { + $body["svm"] = @{ name = $svm } + } 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 + $res = Invoke-NetAppRestRequest -ClusterEndpoint $endpoint -UriPath "/api/storage/quota/rules" -Method "POST" -Body $body -Username $user -Password $pass -IgnoreSsl $ignoreSsl -TimeoutSec $TimeoutSec return [PSCustomObject]@{ Success = $true