# Modules\NetAppService.ps1 # NetApp ONTAP REST API Service Module (Live & Fallback) function Invoke-NetAppRestRequest { param( [string]$ClusterEndpoint, [string]$UriPath, [string]$Method = "GET", [object]$Body = $null, [string]$Username = "", [string]$Password = "", [bool]$IgnoreSsl = $true ) # SSL 우회 설정 (.NET 기반) if ($IgnoreSsl) { [System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true } [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 -bor [System.Net.SecurityProtocolType]::Tls13 } $base = $ClusterEndpoint.TrimEnd('/') $fullUri = "$base$UriPath" $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 = 3 } 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 ) try { $res = Invoke-NetAppRestRequest -ClusterEndpoint $ClusterEndpoint -UriPath "/api/cluster" -Method "GET" -Username $Username -Password $Password -IgnoreSsl $IgnoreSsl $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 = "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 = "", [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 $ignoreSsl = [bool]$Config.NetApp.IgnoreSslErrors $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 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 = "", [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 $ignoreSsl = [bool]$Config.NetApp.IgnoreSslErrors # 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 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 = "", [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 $ignoreSsl = [bool]$Config.NetApp.IgnoreSslErrors $res = Invoke-NetAppRestRequest -ClusterEndpoint $endpoint -UriPath "/api/protocols/cifs/shares?fields=name,path,comment,svm.name" -Method "GET" -Username $user -Password $pass -IgnoreSsl $ignoreSsl 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", [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 = @{ name = $QtreeName volume = @{ name = $Volume } svm = @{ name = $svm } security_style = $SecurityStyle } $res = Invoke-NetAppRestRequest -ClusterEndpoint $endpoint -UriPath "/api/storage/qtrees" -Method "POST" -Body $body -Username $user -Password $pass -IgnoreSsl $ignoreSsl return [PSCustomObject]@{ Success = $true Message = "실제 NetApp 볼륨 [$Volume]에 Qtree [$QtreeName]가 성공적으로 생성되었습니다." } } catch { return [PSCustomObject]@{ Success = $false Message = "NetApp Qtree 생성 실패: $($_.Exception.Message)" } } } return [PSCustomObject]@{ Success = $true Message = "[데모 모드] 볼륨 [$Volume]에 Qtree [$QtreeName]가 가상 생성되었습니다." } } function New-NetAppShareAction { param( [string]$ShareName, [string]$Path, [string]$Comment = "", [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 = @{ name = $ShareName path = $Path comment = $Comment svm = @{ name = $svm } } $res = Invoke-NetAppRestRequest -ClusterEndpoint $endpoint -UriPath "/api/protocols/cifs/shares" -Method "POST" -Body $body -Username $user -Password $pass -IgnoreSsl $ignoreSsl return [PSCustomObject]@{ Success = $true Message = "실제 NetApp SVM [$svm]에 CIFS 공유 [$ShareName] ($Path)가 생성되었습니다." } } catch { return [PSCustomObject]@{ Success = $false Message = "NetApp CIFS 공유 생성 실패: $($_.Exception.Message)" } } } return [PSCustomObject]@{ Success = $true Message = "[데모 모드] CIFS 공유 [$ShareName] ($Path)가 가상 생성되었습니다." } } 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 = "") $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*" }) } }