# 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, [string]$UriPath, [string]$Method = "GET", [object]$Body = $null, [string]$Username = "", [string]$Password = "", [bool]$IgnoreSsl = $true, [int]$TimeoutSec = 15 ) 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" } if (-not [string]::IsNullOrWhiteSpace($Username)) { $authBytes = [System.Text.Encoding]::ASCII.GetBytes("$($Username):$($Password)") $authBase64 = [System.Convert]::ToBase64String($authBytes) $headers["Authorization"] = "Basic $authBase64" } $params = @{ 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) { $params["Body"] = ($Body | ConvertTo-Json -Depth 5) $params["ContentType"] = "application/json; charset=utf-8" } return Invoke-RestMethod @params } function Test-NetAppConnection { param( [string]$ClusterEndpoint, [string]$Username, [string]$Password, [bool]$IgnoreSsl = $true, [int]$TimeoutSec = 15 ) try { $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" } return [PSCustomObject]@{ Success = $true Message = "NetApp ONTAP ($clusterName) 연결 성공 - 버전: $version" Version = $version } } catch { return [PSCustomObject]@{ Success = $false Message = "NetApp 스토리지 연결 실패: $($_.Exception.Message)" } } } function Get-NetAppActiveDc { param( [string]$ClusterEndpoint = "", [string]$SvmName = "", [string]$Username = "", [string]$Password = "", [bool]$IgnoreSsl = $true, [bool]$UseLive = $false, [int]$TimeoutSec = 15, [object]$Config = $null ) $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 $discoveredList = [System.Collections.Generic.List[PSCustomObject]]::new() $attemptErrors = [System.Collections.Generic.List[string]]::new() 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 { if (Get-Command Get-NcCifsDomainDiscoveredServer -ErrorAction SilentlyContinue) { $ncServers = if ($svm) { Get-NcCifsDomainDiscoveredServer -Vserver $svm -ErrorAction Stop } else { Get-NcCifsDomainDiscoveredServer -ErrorAction Stop } 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 ($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 { $attemptErrors.Add("[DataONTAP Toolkit] $($_.Exception.Message)") } if ($attemptErrors.Count -gt 0) { Write-Warning "NetApp 실제 Active DC 조회 실패 사유:`n$($attemptErrors -join "`n")`n(데모 시뮬레이션 모드로 전환합니다)" } } # Fallback / Demo Mock return [PSCustomObject]@{ 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 ) $resolved = Resolve-NetAppParameters -ClusterEndpoint $ClusterEndpoint -Username $Username -Password $Password -IgnoreSsl $IgnoreSsl -UseLive $UseLive -Config $Config -BoundParameters $PSBoundParameters $useLive = $resolved.UseLive if ($useLive -and (-not [string]::IsNullOrWhiteSpace($resolved.Endpoint))) { try { $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 -TimeoutSec $TimeoutSec if ($res.records) { $list = [System.Collections.Generic.List[PSCustomObject]]::new() foreach ($item in $res.records) { $vol = if ($item.volume) { $item.volume.name } else { "" } $name = $item.name $sec = if ($item.security_style) { $item.security_style } else { "ntfs" } $op = if ($item.oplocks) { $item.oplocks } else { "enabled" } $st = if ($item.status) { $item.status } else { "normal" } $path = "/vol/$vol/$name" if ([string]::IsNullOrWhiteSpace($Filter) -or $vol -like "*$Filter*" -or $name -like "*$Filter*") { $list.Add([PSCustomObject]@{ Volume = $vol QtreeName = $name SecurityStyle = $sec Oplocks = $op Status = $st FullPath = $path IsLive = $true }) } } return $list.ToArray() } } catch { Write-Warning "실제 NetApp Qtree 조회 실패: $($_.Exception.Message) (Mock 데이터로 전환합니다)" } } return Get-NetAppSampleQtrees -Filter $Filter } function Get-NetAppQuotas { param( [string]$Filter = "", [string]$ClusterEndpoint = "", [string]$Username = "", [string]$Password = "", [bool]$IgnoreSsl = $true, [bool]$UseLive = $false, [int]$TimeoutSec = 15, [object]$Config = $null ) $resolved = Resolve-NetAppParameters -ClusterEndpoint $ClusterEndpoint -Username $Username -Password $Password -IgnoreSsl $IgnoreSsl -UseLive $UseLive -Config $Config -BoundParameters $PSBoundParameters $useLive = $resolved.UseLive if ($useLive -and (-not [string]::IsNullOrWhiteSpace($resolved.Endpoint))) { try { $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 -TimeoutSec $TimeoutSec if ($res.records) { $list = [System.Collections.Generic.List[PSCustomObject]]::new() foreach ($item in $res.records) { $type = $item.type $vol = if ($item.volume) { $item.volume.name } else { "" } $target = if ($item.users -and $item.users.Count -gt 0) { $item.users[0].name } elseif ($item.group -and $item.group.name) { $item.group.name } elseif ($item.qtree -and $item.qtree.name) { $item.qtree.name } else { "-" } $hardBytes = if ($item.space -and $item.space.hard_limit) { [int64]$item.space.hard_limit } else { 0 } $softBytes = if ($item.space -and $item.space.soft_limit) { [int64]$item.space.soft_limit } else { 0 } $hardStr = if ($hardBytes -gt 0) { "$([Math]::Round($hardBytes / 1GB, 1)) GB" } else { "무제한" } $softStr = if ($softBytes -gt 0) { "$([Math]::Round($softBytes / 1GB, 1)) GB" } else { "무제한" } $list.Add([PSCustomObject]@{ QuotaType = $type Target = $target VolumeQtree = $vol UsedSpace = "조회중" SoftLimit = $softStr HardLimit = $hardStr UsagePercent = "-" QuotaStatus = "규칙 적용됨" IsLive = $true }) } return $list.ToArray() } } catch { Write-Warning "실제 NetApp Quota 조회 실패: $($_.Exception.Message) (Mock 데이터로 전환합니다)" } } return Get-NetAppSampleQuotas -Filter $Filter } function Get-NetAppShares { param( [string]$Filter = "", [string]$ClusterEndpoint = "", [string]$Username = "", [string]$Password = "", [bool]$IgnoreSsl = $true, [bool]$UseLive = $false, [int]$TimeoutSec = 15, [object]$Config = $null ) $resolved = Resolve-NetAppParameters -ClusterEndpoint $ClusterEndpoint -Username $Username -Password $Password -IgnoreSsl $IgnoreSsl -UseLive $UseLive -Config $Config -BoundParameters $PSBoundParameters $useLive = $resolved.UseLive if ($useLive -and (-not [string]::IsNullOrWhiteSpace($resolved.Endpoint))) { try { $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 -TimeoutSec $TimeoutSec if ($res.records) { $list = [System.Collections.Generic.List[PSCustomObject]]::new() foreach ($item in $res.records) { $sName = $item.name $sPath = $item.path $sComment = if ($item.comment) { $item.comment } else { "-" } if ([string]::IsNullOrWhiteSpace($Filter) -or $sName -like "*$Filter*" -or $sPath -like "*$Filter*") { $list.Add([PSCustomObject]@{ Protocol = "CIFS (SMB)" ShareName = $sName Path = $sPath Comment = $sComment AclSummary = "ONTAP ACL 관리" ActiveSessions = "-" IsLive = $true }) } } return $list.ToArray() } } catch { Write-Warning "실제 NetApp Share 조회 실패: $($_.Exception.Message) (Mock 데이터로 전환합니다)" } } return Get-NetAppSampleShares -Filter $Filter } function New-NetAppQtreeAction { param( [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 ) $resolved = Resolve-NetAppParameters -ClusterEndpoint $ClusterEndpoint -Username $Username -Password $Password -SvmName $SvmName -IgnoreSsl $IgnoreSsl -UseLive $UseLive -Config $Config -BoundParameters $PSBoundParameters $useLive = $resolved.UseLive if ($useLive -and (-not [string]::IsNullOrWhiteSpace($resolved.Endpoint))) { try { $endpoint = $resolved.Endpoint $user = $resolved.Username $pass = $resolved.Password $svm = $resolved.SvmName $ignoreSsl = $resolved.IgnoreSsl $body = @{ name = $QtreeName volume = @{ name = $Volume } 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 -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 } } } return [PSCustomObject]@{ Success = $true Message = "[데모 모드] 볼륨 [$Volume]에 Qtree [$QtreeName]가 가상 생성되었습니다." IsLive = $false } } function New-NetAppShareAction { param( [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 ) $resolved = Resolve-NetAppParameters -ClusterEndpoint $ClusterEndpoint -Username $Username -Password $Password -SvmName $SvmName -IgnoreSsl $IgnoreSsl -UseLive $UseLive -Config $Config -BoundParameters $PSBoundParameters $useLive = $resolved.UseLive if ($useLive -and (-not [string]::IsNullOrWhiteSpace($resolved.Endpoint))) { try { $endpoint = $resolved.Endpoint $user = $resolved.Username $pass = $resolved.Password $svm = $resolved.SvmName $ignoreSsl = $resolved.IgnoreSsl $body = @{ name = $ShareName path = $Path comment = $Comment } 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 -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 } } } return [PSCustomObject]@{ Success = $true Message = "[데모 모드] CIFS 공유 [$ShareName] ($Path)가 가상 생성되었습니다." IsLive = $false } } 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, [string]$SvmName = "", [string]$ClusterEndpoint = "", [string]$Username = "", [string]$Password = "", [bool]$IgnoreSsl = $true, [bool]$UseLive = $false, [int]$TimeoutSec = 20, [object]$Config = $null ) $resolved = Resolve-NetAppParameters -ClusterEndpoint $ClusterEndpoint -Username $Username -Password $Password -SvmName $SvmName -IgnoreSsl $IgnoreSsl -UseLive $UseLive -Config $Config -BoundParameters $PSBoundParameters $useLive = $resolved.UseLive 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 = "" 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 -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 -TimeoutSec $TimeoutSec } 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, [string]$SvmName = "", [string]$ClusterEndpoint = "", [string]$Username = "", [string]$Password = "", [bool]$IgnoreSsl = $true, [bool]$UseLive = $false, [int]$TimeoutSec = 20, [object]$Config = $null ) $resolved = Resolve-NetAppParameters -ClusterEndpoint $ClusterEndpoint -Username $Username -Password $Password -SvmName $SvmName -IgnoreSsl $IgnoreSsl -UseLive $UseLive -Config $Config -BoundParameters $PSBoundParameters $useLive = $resolved.UseLive if ($useLive -and (-not [string]::IsNullOrWhiteSpace($resolved.Endpoint))) { try { $endpoint = $resolved.Endpoint $user = $resolved.Username $pass = $resolved.Password $svm = $resolved.SvmName $ignoreSsl = $resolved.IgnoreSsl $body = @{ volume = @{ name = $Volume } type = $QuotaType space = @{ hard_limit = $HardLimitGb * 1GB 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 -TimeoutSec $TimeoutSec 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 = "") $qtrees = @( [PSCustomObject]@{ Volume = "vol_cifs_share01" QtreeName = "qtree_finance_docs" SecurityStyle = "ntfs" Oplocks = "enabled" Status = "normal" FullPath = "/vol/vol_cifs_share01/qtree_finance_docs" IsLive = $false }, [PSCustomObject]@{ Volume = "vol_cifs_share01" QtreeName = "qtree_hr_confidential" SecurityStyle = "ntfs" Oplocks = "enabled" Status = "normal" FullPath = "/vol/vol_cifs_share01/qtree_hr_confidential" IsLive = $false }, [PSCustomObject]@{ Volume = "vol_nfs_appdata" QtreeName = "qtree_docker_registry" SecurityStyle = "unix" Oplocks = "disabled" Status = "normal" FullPath = "/vol/vol_nfs_appdata/qtree_docker_registry" IsLive = $false }, [PSCustomObject]@{ Volume = "vol_nfs_appdata" QtreeName = "qtree_backup_targets" SecurityStyle = "unix" Oplocks = "disabled" Status = "normal" FullPath = "/vol/vol_nfs_appdata/qtree_backup_targets" IsLive = $false }, [PSCustomObject]@{ Volume = "vol_user_homes" QtreeName = "qtree_dev_workspace" SecurityStyle = "mixed" Oplocks = "enabled" Status = "normal" FullPath = "/vol/vol_user_homes/qtree_dev_workspace" IsLive = $false } ) if ([string]::IsNullOrWhiteSpace($Filter)) { return $qtrees } else { return @($qtrees | Where-Object { $_.Volume -like "*$Filter*" -or $_.QtreeName -like "*$Filter*" }) } } function Get-NetAppSampleQuotas { param([string]$Filter = "") $quotas = @( [PSCustomObject]@{ QuotaType = "tree" Target = "qtree_finance_docs" VolumeQtree = "vol_cifs_share01" UsedSpace = "420 GB" SoftLimit = "450 GB" HardLimit = "500 GB" UsagePercent = "84.0%" QuotaStatus = "정상 (OK)" IsLive = $false }, [PSCustomObject]@{ QuotaType = "tree" Target = "qtree_hr_confidential" VolumeQtree = "vol_cifs_share01" UsedSpace = "192 GB" SoftLimit = "200 GB" HardLimit = "220 GB" UsagePercent = "87.2%" QuotaStatus = "주의 (Warning)" IsLive = $false }, [PSCustomObject]@{ QuotaType = "user" Target = "CORP\gdhong" VolumeQtree = "vol_user_homes" UsedSpace = "94.5 GB" SoftLimit = "90 GB" HardLimit = "100 GB" UsagePercent = "94.5%" QuotaStatus = "경고 (Soft 초과)" IsLive = $false }, [PSCustomObject]@{ QuotaType = "group" Target = "CORP\Domain Devs" VolumeQtree = "vol_user_homes" UsedSpace = "1.42 TB" SoftLimit = "1.8 TB" HardLimit = "2.0 TB" UsagePercent = "71.0%" QuotaStatus = "정상 (OK)" IsLive = $false }, [PSCustomObject]@{ QuotaType = "tree" Target = "qtree_docker_registry" VolumeQtree = "vol_nfs_appdata" UsedSpace = "890 GB" SoftLimit = "900 GB" HardLimit = "1.0 TB" UsagePercent = "89.0%" QuotaStatus = "주의 (Warning)" IsLive = $false } ) if ([string]::IsNullOrWhiteSpace($Filter)) { return $quotas } else { return @($quotas | Where-Object { $_.Target -like "*$Filter*" -or $_.VolumeQtree -like "*$Filter*" }) } } function Get-NetAppSampleShares { param([string]$Filter = "") $shares = @( [PSCustomObject]@{ Protocol = "CIFS (SMB)" ShareName = "Finance$" Path = "/vol/vol_cifs_share01/qtree_finance_docs" Comment = "재무기획팀 전용 기밀 문서 공유" AclSummary = "재무팀 (Full), 감사팀 (Read)" ActiveSessions = 14 IsLive = $false }, [PSCustomObject]@{ Protocol = "CIFS (SMB)" ShareName = "HR_Confidential" Path = "/vol/vol_cifs_share01/qtree_hr_confidential" Comment = "인사팀 보안 데이터 공유" AclSummary = "인사팀 (Full), Admin (Read)" ActiveSessions = 8 IsLive = $false }, [PSCustomObject]@{ Protocol = "CIFS (SMB)" ShareName = "PublicProjects" Path = "/vol/vol_user_homes/qtree_dev_workspace" Comment = "전사 프로젝트 공용 작업 디렉토리" AclSummary = "Everyone (Change)" ActiveSessions = 42 IsLive = $false }, [PSCustomObject]@{ Protocol = "NFS v4.1" ShareName = "/nfs/docker_registry" Path = "/vol/vol_nfs_appdata/qtree_docker_registry" Comment = "Kubernetes 클러스터 이미지 저장소 마운트" AclSummary = "10.200.10.0/24 (rw, root_squash)" ActiveSessions = 26 IsLive = $false } ) if ([string]::IsNullOrWhiteSpace($Filter)) { return $shares } else { return @($shares | Where-Object { $_.ShareName -like "*$Filter*" -or $_.Path -like "*$Filter*" }) } }