2026-10-20 16:13:34 +09:00
|
|
|
# Modules\NetAppService.ps1
|
|
|
|
|
# NetApp ONTAP REST API Service Module (Live & Fallback)
|
|
|
|
|
|
2026-10-21 18:58:58 +09:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-20 16:13:34 +09:00
|
|
|
function Invoke-NetAppRestRequest {
|
|
|
|
|
param(
|
|
|
|
|
[string]$ClusterEndpoint,
|
|
|
|
|
[string]$UriPath,
|
|
|
|
|
[string]$Method = "GET",
|
|
|
|
|
[object]$Body = $null,
|
|
|
|
|
[string]$Username = "",
|
|
|
|
|
[string]$Password = "",
|
2026-10-21 18:58:58 +09:00
|
|
|
[bool]$IgnoreSsl = $true,
|
|
|
|
|
[int]$TimeoutSec = 15
|
2026-10-20 16:13:34 +09:00
|
|
|
)
|
|
|
|
|
|
2026-10-21 18:58:58 +09:00
|
|
|
if ([string]::IsNullOrWhiteSpace($ClusterEndpoint)) {
|
|
|
|
|
throw "NetApp 클러스터 엔드포인트 URL이 지정되지 않았습니다."
|
2026-10-20 16:13:34 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$base = $ClusterEndpoint.TrimEnd('/')
|
2026-10-21 18:58:58 +09:00
|
|
|
if (-not ($base -like "http*")) {
|
|
|
|
|
$base = "https://$base"
|
|
|
|
|
}
|
2026-10-20 16:13:34 +09:00
|
|
|
$fullUri = "$base$UriPath"
|
|
|
|
|
|
2026-10-21 18:58:58 +09:00
|
|
|
# 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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-20 16:13:34 +09:00
|
|
|
$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 = @{
|
2026-10-21 18:58:58 +09:00
|
|
|
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
|
2026-10-20 16:13:34 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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,
|
2026-10-21 18:58:58 +09:00
|
|
|
[bool]$IgnoreSsl = $true,
|
|
|
|
|
[int]$TimeoutSec = 15
|
2026-10-20 16:13:34 +09:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
try {
|
2026-10-21 18:58:58 +09:00
|
|
|
$res = Invoke-NetAppRestRequest -ClusterEndpoint $ClusterEndpoint -UriPath "/api/cluster" -Method "GET" -Username $Username -Password $Password -IgnoreSsl $IgnoreSsl -TimeoutSec $TimeoutSec
|
2026-10-20 16:13:34 +09:00
|
|
|
$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)"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-20 21:34:57 +09:00
|
|
|
function Get-NetAppActiveDc {
|
|
|
|
|
param(
|
|
|
|
|
[string]$ClusterEndpoint = "",
|
2026-10-21 18:58:58 +09:00
|
|
|
[string]$SvmName = "",
|
2026-10-20 21:34:57 +09:00
|
|
|
[string]$Username = "",
|
|
|
|
|
[string]$Password = "",
|
|
|
|
|
[bool]$IgnoreSsl = $true,
|
2026-10-21 18:58:58 +09:00
|
|
|
[bool]$UseLive = $false,
|
|
|
|
|
[int]$TimeoutSec = 15,
|
2026-10-20 21:34:57 +09:00
|
|
|
[object]$Config = $null
|
|
|
|
|
)
|
|
|
|
|
|
2026-10-21 18:58:58 +09:00
|
|
|
$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")
|
2026-10-20 21:34:57 +09:00
|
|
|
|
2026-10-21 18:58:58 +09:00
|
|
|
$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)")
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-20 21:34:57 +09:00
|
|
|
|
2026-10-21 18:58:58 +09:00
|
|
|
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 모듈 폴백
|
2026-10-20 21:34:57 +09:00
|
|
|
try {
|
2026-10-21 18:58:58 +09:00
|
|
|
if (Get-Command Get-NcCifsDomainDiscoveredServer -ErrorAction SilentlyContinue) {
|
|
|
|
|
$ncServers = if ($svm) {
|
|
|
|
|
Get-NcCifsDomainDiscoveredServer -Vserver $svm -ErrorAction Stop
|
|
|
|
|
} else {
|
|
|
|
|
Get-NcCifsDomainDiscoveredServer -ErrorAction Stop
|
|
|
|
|
}
|
2026-10-20 21:34:57 +09:00
|
|
|
|
2026-10-21 18:58:58 +09:00
|
|
|
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)
|
2026-10-20 21:34:57 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
2026-10-21 18:58:58 +09:00
|
|
|
$attemptErrors.Add("[DataONTAP Toolkit] $($_.Exception.Message)")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($attemptErrors.Count -gt 0) {
|
|
|
|
|
Write-Warning "NetApp 실제 Active DC 조회 실패 사유:`n$($attemptErrors -join "`n")`n(데모 시뮬레이션 모드로 전환합니다)"
|
2026-10-20 21:34:57 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-21 18:58:58 +09:00
|
|
|
# Fallback / Demo Mock
|
2026-10-20 21:34:57 +09:00
|
|
|
return [PSCustomObject]@{
|
2026-10-21 18:58:58 +09:00
|
|
|
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" }
|
|
|
|
|
)
|
2026-10-20 21:34:57 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-20 16:13:34 +09:00
|
|
|
function Get-NetAppQtrees {
|
|
|
|
|
param(
|
|
|
|
|
[string]$Filter = "",
|
2026-10-21 18:58:58 +09:00
|
|
|
[string]$ClusterEndpoint = "",
|
|
|
|
|
[string]$Username = "",
|
|
|
|
|
[string]$Password = "",
|
|
|
|
|
[bool]$IgnoreSsl = $true,
|
|
|
|
|
[bool]$UseLive = $false,
|
|
|
|
|
[int]$TimeoutSec = 15,
|
2026-10-20 16:13:34 +09:00
|
|
|
[object]$Config = $null
|
|
|
|
|
)
|
|
|
|
|
|
2026-10-21 18:58:58 +09:00
|
|
|
$resolved = Resolve-NetAppParameters -ClusterEndpoint $ClusterEndpoint -Username $Username -Password $Password -IgnoreSsl $IgnoreSsl -UseLive $UseLive -Config $Config -BoundParameters $PSBoundParameters
|
|
|
|
|
$useLive = $resolved.UseLive
|
2026-10-20 16:13:34 +09:00
|
|
|
|
2026-10-21 18:58:58 +09:00
|
|
|
if ($useLive -and (-not [string]::IsNullOrWhiteSpace($resolved.Endpoint))) {
|
2026-10-20 16:13:34 +09:00
|
|
|
try {
|
2026-10-21 18:58:58 +09:00
|
|
|
$endpoint = $resolved.Endpoint
|
|
|
|
|
$user = $resolved.Username
|
|
|
|
|
$pass = $resolved.Password
|
|
|
|
|
$ignoreSsl = $resolved.IgnoreSsl
|
2026-10-20 16:13:34 +09:00
|
|
|
|
2026-10-21 18:58:58 +09:00
|
|
|
$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
|
2026-10-20 16:13:34 +09:00
|
|
|
|
|
|
|
|
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 = "",
|
2026-10-21 18:58:58 +09:00
|
|
|
[string]$ClusterEndpoint = "",
|
|
|
|
|
[string]$Username = "",
|
|
|
|
|
[string]$Password = "",
|
|
|
|
|
[bool]$IgnoreSsl = $true,
|
|
|
|
|
[bool]$UseLive = $false,
|
|
|
|
|
[int]$TimeoutSec = 15,
|
2026-10-20 16:13:34 +09:00
|
|
|
[object]$Config = $null
|
|
|
|
|
)
|
|
|
|
|
|
2026-10-21 18:58:58 +09:00
|
|
|
$resolved = Resolve-NetAppParameters -ClusterEndpoint $ClusterEndpoint -Username $Username -Password $Password -IgnoreSsl $IgnoreSsl -UseLive $UseLive -Config $Config -BoundParameters $PSBoundParameters
|
|
|
|
|
$useLive = $resolved.UseLive
|
2026-10-20 16:13:34 +09:00
|
|
|
|
2026-10-21 18:58:58 +09:00
|
|
|
if ($useLive -and (-not [string]::IsNullOrWhiteSpace($resolved.Endpoint))) {
|
2026-10-20 16:13:34 +09:00
|
|
|
try {
|
2026-10-21 18:58:58 +09:00
|
|
|
$endpoint = $resolved.Endpoint
|
|
|
|
|
$user = $resolved.Username
|
|
|
|
|
$pass = $resolved.Password
|
|
|
|
|
$ignoreSsl = $resolved.IgnoreSsl
|
2026-10-20 16:13:34 +09:00
|
|
|
|
|
|
|
|
# ONTAP REST quota rules
|
2026-10-21 18:58:58 +09:00
|
|
|
$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
|
2026-10-20 16:13:34 +09:00
|
|
|
|
|
|
|
|
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 = "",
|
2026-10-21 18:58:58 +09:00
|
|
|
[string]$ClusterEndpoint = "",
|
|
|
|
|
[string]$Username = "",
|
|
|
|
|
[string]$Password = "",
|
|
|
|
|
[bool]$IgnoreSsl = $true,
|
|
|
|
|
[bool]$UseLive = $false,
|
|
|
|
|
[int]$TimeoutSec = 15,
|
2026-10-20 16:13:34 +09:00
|
|
|
[object]$Config = $null
|
|
|
|
|
)
|
|
|
|
|
|
2026-10-21 18:58:58 +09:00
|
|
|
$resolved = Resolve-NetAppParameters -ClusterEndpoint $ClusterEndpoint -Username $Username -Password $Password -IgnoreSsl $IgnoreSsl -UseLive $UseLive -Config $Config -BoundParameters $PSBoundParameters
|
|
|
|
|
$useLive = $resolved.UseLive
|
2026-10-20 16:13:34 +09:00
|
|
|
|
2026-10-21 18:58:58 +09:00
|
|
|
if ($useLive -and (-not [string]::IsNullOrWhiteSpace($resolved.Endpoint))) {
|
2026-10-20 16:13:34 +09:00
|
|
|
try {
|
2026-10-21 18:58:58 +09:00
|
|
|
$endpoint = $resolved.Endpoint
|
|
|
|
|
$user = $resolved.Username
|
|
|
|
|
$pass = $resolved.Password
|
|
|
|
|
$ignoreSsl = $resolved.IgnoreSsl
|
2026-10-20 16:13:34 +09:00
|
|
|
|
2026-10-21 18:58:58 +09:00
|
|
|
$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
|
2026-10-20 16:13:34 +09:00
|
|
|
|
|
|
|
|
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",
|
2026-10-21 18:58:58 +09:00
|
|
|
[string]$SvmName = "",
|
|
|
|
|
[string]$ClusterEndpoint = "",
|
|
|
|
|
[string]$Username = "",
|
|
|
|
|
[string]$Password = "",
|
|
|
|
|
[bool]$IgnoreSsl = $true,
|
|
|
|
|
[bool]$UseLive = $false,
|
|
|
|
|
[int]$TimeoutSec = 20,
|
2026-10-20 16:13:34 +09:00
|
|
|
[object]$Config = $null
|
|
|
|
|
)
|
|
|
|
|
|
2026-10-21 18:58:58 +09:00
|
|
|
$resolved = Resolve-NetAppParameters -ClusterEndpoint $ClusterEndpoint -Username $Username -Password $Password -SvmName $SvmName -IgnoreSsl $IgnoreSsl -UseLive $UseLive -Config $Config -BoundParameters $PSBoundParameters
|
|
|
|
|
$useLive = $resolved.UseLive
|
2026-10-20 16:13:34 +09:00
|
|
|
|
2026-10-21 18:58:58 +09:00
|
|
|
if ($useLive -and (-not [string]::IsNullOrWhiteSpace($resolved.Endpoint))) {
|
2026-10-20 16:13:34 +09:00
|
|
|
try {
|
2026-10-21 18:58:58 +09:00
|
|
|
$endpoint = $resolved.Endpoint
|
|
|
|
|
$user = $resolved.Username
|
|
|
|
|
$pass = $resolved.Password
|
|
|
|
|
$svm = $resolved.SvmName
|
|
|
|
|
$ignoreSsl = $resolved.IgnoreSsl
|
2026-10-20 16:13:34 +09:00
|
|
|
|
|
|
|
|
$body = @{
|
|
|
|
|
name = $QtreeName
|
|
|
|
|
volume = @{ name = $Volume }
|
|
|
|
|
security_style = $SecurityStyle
|
|
|
|
|
}
|
2026-10-21 18:58:58 +09:00
|
|
|
if (-not [string]::IsNullOrWhiteSpace($svm)) {
|
|
|
|
|
$body["svm"] = @{ name = $svm }
|
|
|
|
|
}
|
2026-10-20 16:13:34 +09:00
|
|
|
|
2026-10-21 18:58:58 +09:00
|
|
|
$res = Invoke-NetAppRestRequest -ClusterEndpoint $endpoint -UriPath "/api/storage/qtrees" -Method "POST" -Body $body -Username $user -Password $pass -IgnoreSsl $ignoreSsl -TimeoutSec $TimeoutSec
|
2026-10-20 16:13:34 +09:00
|
|
|
|
|
|
|
|
return [PSCustomObject]@{
|
|
|
|
|
Success = $true
|
|
|
|
|
Message = "실제 NetApp 볼륨 [$Volume]에 Qtree [$QtreeName]가 성공적으로 생성되었습니다."
|
2026-10-21 18:58:58 +09:00
|
|
|
IsLive = $true
|
2026-10-20 16:13:34 +09:00
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
return [PSCustomObject]@{
|
|
|
|
|
Success = $false
|
|
|
|
|
Message = "NetApp Qtree 생성 실패: $($_.Exception.Message)"
|
2026-10-21 18:58:58 +09:00
|
|
|
IsLive = $true
|
2026-10-20 16:13:34 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [PSCustomObject]@{
|
|
|
|
|
Success = $true
|
|
|
|
|
Message = "[데모 모드] 볼륨 [$Volume]에 Qtree [$QtreeName]가 가상 생성되었습니다."
|
2026-10-21 18:58:58 +09:00
|
|
|
IsLive = $false
|
2026-10-20 16:13:34 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function New-NetAppShareAction {
|
|
|
|
|
param(
|
|
|
|
|
[string]$ShareName,
|
|
|
|
|
[string]$Path,
|
|
|
|
|
[string]$Comment = "",
|
2026-10-21 18:58:58 +09:00
|
|
|
[string]$SvmName = "",
|
|
|
|
|
[string]$ClusterEndpoint = "",
|
|
|
|
|
[string]$Username = "",
|
|
|
|
|
[string]$Password = "",
|
|
|
|
|
[bool]$IgnoreSsl = $true,
|
|
|
|
|
[bool]$UseLive = $false,
|
|
|
|
|
[int]$TimeoutSec = 20,
|
2026-10-20 16:13:34 +09:00
|
|
|
[object]$Config = $null
|
|
|
|
|
)
|
|
|
|
|
|
2026-10-21 18:58:58 +09:00
|
|
|
$resolved = Resolve-NetAppParameters -ClusterEndpoint $ClusterEndpoint -Username $Username -Password $Password -SvmName $SvmName -IgnoreSsl $IgnoreSsl -UseLive $UseLive -Config $Config -BoundParameters $PSBoundParameters
|
|
|
|
|
$useLive = $resolved.UseLive
|
2026-10-20 16:13:34 +09:00
|
|
|
|
2026-10-21 18:58:58 +09:00
|
|
|
if ($useLive -and (-not [string]::IsNullOrWhiteSpace($resolved.Endpoint))) {
|
2026-10-20 16:13:34 +09:00
|
|
|
try {
|
2026-10-21 18:58:58 +09:00
|
|
|
$endpoint = $resolved.Endpoint
|
|
|
|
|
$user = $resolved.Username
|
|
|
|
|
$pass = $resolved.Password
|
|
|
|
|
$svm = $resolved.SvmName
|
|
|
|
|
$ignoreSsl = $resolved.IgnoreSsl
|
2026-10-20 16:13:34 +09:00
|
|
|
|
|
|
|
|
$body = @{
|
|
|
|
|
name = $ShareName
|
|
|
|
|
path = $Path
|
|
|
|
|
comment = $Comment
|
2026-10-21 18:58:58 +09:00
|
|
|
}
|
|
|
|
|
if (-not [string]::IsNullOrWhiteSpace($svm)) {
|
|
|
|
|
$body["svm"] = @{ name = $svm }
|
2026-10-20 16:13:34 +09:00
|
|
|
}
|
|
|
|
|
|
2026-10-21 18:58:58 +09:00
|
|
|
$res = Invoke-NetAppRestRequest -ClusterEndpoint $endpoint -UriPath "/api/protocols/cifs/shares" -Method "POST" -Body $body -Username $user -Password $pass -IgnoreSsl $ignoreSsl -TimeoutSec $TimeoutSec
|
2026-10-20 16:13:34 +09:00
|
|
|
|
|
|
|
|
return [PSCustomObject]@{
|
|
|
|
|
Success = $true
|
|
|
|
|
Message = "실제 NetApp SVM [$svm]에 CIFS 공유 [$ShareName] ($Path)가 생성되었습니다."
|
2026-10-21 18:58:58 +09:00
|
|
|
IsLive = $true
|
2026-10-20 16:13:34 +09:00
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
return [PSCustomObject]@{
|
|
|
|
|
Success = $false
|
|
|
|
|
Message = "NetApp CIFS 공유 생성 실패: $($_.Exception.Message)"
|
2026-10-21 18:58:58 +09:00
|
|
|
IsLive = $true
|
2026-10-20 16:13:34 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [PSCustomObject]@{
|
|
|
|
|
Success = $true
|
|
|
|
|
Message = "[데모 모드] CIFS 공유 [$ShareName] ($Path)가 가상 생성되었습니다."
|
2026-10-21 18:58:58 +09:00
|
|
|
IsLive = $false
|
2026-10-20 16:13:34 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-20 21:34:57 +09:00
|
|
|
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,
|
2026-10-21 18:58:58 +09:00
|
|
|
[string]$SvmName = "",
|
|
|
|
|
[string]$ClusterEndpoint = "",
|
|
|
|
|
[string]$Username = "",
|
|
|
|
|
[string]$Password = "",
|
|
|
|
|
[bool]$IgnoreSsl = $true,
|
|
|
|
|
[bool]$UseLive = $false,
|
|
|
|
|
[int]$TimeoutSec = 20,
|
2026-10-20 21:34:57 +09:00
|
|
|
[object]$Config = $null
|
|
|
|
|
)
|
|
|
|
|
|
2026-10-21 18:58:58 +09:00
|
|
|
$resolved = Resolve-NetAppParameters -ClusterEndpoint $ClusterEndpoint -Username $Username -Password $Password -SvmName $SvmName -IgnoreSsl $IgnoreSsl -UseLive $UseLive -Config $Config -BoundParameters $PSBoundParameters
|
|
|
|
|
$useLive = $resolved.UseLive
|
2026-10-20 21:34:57 +09:00
|
|
|
|
2026-10-21 18:58:58 +09:00
|
|
|
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
|
2026-10-20 21:34:57 +09:00
|
|
|
|
|
|
|
|
$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"
|
2026-10-21 18:58:58 +09:00
|
|
|
$null = Invoke-NetAppRestRequest -ClusterEndpoint $endpoint -UriPath $path -Method "POST" -Body $body -Username $user -Password $pass -IgnoreSsl $ignoreSsl -TimeoutSec $TimeoutSec
|
2026-10-20 21:34:57 +09:00
|
|
|
} else {
|
|
|
|
|
$path = "/api/protocols/cifs/shares/$svm/$ShareName/acls/$UserOrGroup"
|
2026-10-21 18:58:58 +09:00
|
|
|
$null = Invoke-NetAppRestRequest -ClusterEndpoint $endpoint -UriPath $path -Method "DELETE" -Username $user -Password $pass -IgnoreSsl $ignoreSsl -TimeoutSec $TimeoutSec
|
2026-10-20 21:34:57 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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,
|
2026-10-21 18:58:58 +09:00
|
|
|
[string]$SvmName = "",
|
|
|
|
|
[string]$ClusterEndpoint = "",
|
|
|
|
|
[string]$Username = "",
|
|
|
|
|
[string]$Password = "",
|
|
|
|
|
[bool]$IgnoreSsl = $true,
|
|
|
|
|
[bool]$UseLive = $false,
|
|
|
|
|
[int]$TimeoutSec = 20,
|
2026-10-20 21:34:57 +09:00
|
|
|
[object]$Config = $null
|
|
|
|
|
)
|
|
|
|
|
|
2026-10-21 18:58:58 +09:00
|
|
|
$resolved = Resolve-NetAppParameters -ClusterEndpoint $ClusterEndpoint -Username $Username -Password $Password -SvmName $SvmName -IgnoreSsl $IgnoreSsl -UseLive $UseLive -Config $Config -BoundParameters $PSBoundParameters
|
|
|
|
|
$useLive = $resolved.UseLive
|
2026-10-20 21:34:57 +09:00
|
|
|
|
2026-10-21 18:58:58 +09:00
|
|
|
if ($useLive -and (-not [string]::IsNullOrWhiteSpace($resolved.Endpoint))) {
|
2026-10-20 21:34:57 +09:00
|
|
|
try {
|
2026-10-21 18:58:58 +09:00
|
|
|
$endpoint = $resolved.Endpoint
|
|
|
|
|
$user = $resolved.Username
|
|
|
|
|
$pass = $resolved.Password
|
|
|
|
|
$svm = $resolved.SvmName
|
|
|
|
|
$ignoreSsl = $resolved.IgnoreSsl
|
2026-10-20 21:34:57 +09:00
|
|
|
|
|
|
|
|
$body = @{
|
|
|
|
|
volume = @{ name = $Volume }
|
|
|
|
|
type = $QuotaType
|
|
|
|
|
space = @{
|
|
|
|
|
hard_limit = $HardLimitGb * 1GB
|
|
|
|
|
soft_limit = $SoftLimitGb * 1GB
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-21 18:58:58 +09:00
|
|
|
if (-not [string]::IsNullOrWhiteSpace($svm)) {
|
|
|
|
|
$body["svm"] = @{ name = $svm }
|
|
|
|
|
}
|
2026-10-20 21:34:57 +09:00
|
|
|
if ($Qtree) { $body["qtree"] = @{ name = $Qtree } }
|
|
|
|
|
if ($Target) {
|
|
|
|
|
if ($QuotaType -eq "user") { $body["users"] = @(@{ name = $Target }) }
|
|
|
|
|
elseif ($QuotaType -eq "group") { $body["group"] = @{ name = $Target } }
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-21 18:58:58 +09:00
|
|
|
$res = Invoke-NetAppRestRequest -ClusterEndpoint $endpoint -UriPath "/api/storage/quota/rules" -Method "POST" -Body $body -Username $user -Password $pass -IgnoreSsl $ignoreSsl -TimeoutSec $TimeoutSec
|
2026-10-20 21:34:57 +09:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-20 16:13:34 +09:00
|
|
|
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*"
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|