# 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 = 10 } 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-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 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*" }) } }