feat: implement modern admin studio with live AD, NetApp, MS SQL connectivity and comprehensive docs

This commit is contained in:
2026-10-20 16:13:34 +09:00
commit ccc1b47dfd
19 changed files with 3533 additions and 0 deletions
+378
View File
@@ -0,0 +1,378 @@
# Modules\ADService.ps1
# Active Directory Service Module (.NET System.DirectoryServices Live & Fallback)
function Test-ADConnection {
param(
[string]$DomainController,
[string]$BaseOU,
[string]$Username,
[string]$Password,
[bool]$UseLdaps = $false
)
try {
$protocol = if ($UseLdaps) { "LDAP://$DomainController:636" } else { "LDAP://$DomainController" }
$path = if ([string]::IsNullOrWhiteSpace($BaseOU)) { "$protocol/RootDSE" } else { "$protocol/$BaseOU" }
$entry = if ([string]::IsNullOrWhiteSpace($Username)) {
New-Object System.DirectoryServices.DirectoryEntry($path)
} else {
New-Object System.DirectoryServices.DirectoryEntry($path, $Username, $Password)
}
# 바인딩 강제 테스트
$null = $entry.NativeObject
return [PSCustomObject]@{
Success = $true
Message = "Active Directory DC ($DomainController) 연결 성공 (RootDSE 응답 확인)"
LdapPath = $path
}
} catch {
return [PSCustomObject]@{
Success = $false
Message = "AD 연결 실패: $($_.Exception.Message)"
LdapPath = $path
}
}
}
function Get-ADUserList {
param(
[string]$Filter = "",
[object]$Config = $null
)
$useLive = if ($Config -and $Config.ActiveDirectory) { [bool]$Config.ActiveDirectory.UseLive } else { $false }
if ($useLive) {
$dc = $Config.ActiveDirectory.DomainController
$baseOu = $Config.ActiveDirectory.BaseOU
$useLdaps = [bool]$Config.ActiveDirectory.UseLdaps
$user = $Config.ActiveDirectory.Username
$pass = $Config.ActiveDirectory.Password
try {
$prefix = if ($useLdaps) { "LDAP://$dc:636" } else { "LDAP://$dc" }
$rootPath = if ([string]::IsNullOrWhiteSpace($baseOu)) { $prefix } else { "$prefix/$baseOu" }
$rootEntry = if ([string]::IsNullOrWhiteSpace($user)) {
New-Object System.DirectoryServices.DirectoryEntry($rootPath)
} else {
New-Object System.DirectoryServices.DirectoryEntry($rootPath, $user, $pass)
}
$searcher = New-Object System.DirectoryServices.DirectorySearcher($rootEntry)
$searcher.PageSize = 1000
if ([string]::IsNullOrWhiteSpace($Filter)) {
$searcher.Filter = "(&(objectCategory=person)(objectClass=user))"
} else {
$esc = $Filter -replace '[*()\\\x00]', ''
$searcher.Filter = "(&(objectCategory=person)(objectClass=user)(|(sAMAccountName=*$esc*)(displayName=*$esc*)(department=*$esc*)))"
}
$props = @("sAMAccountName", "displayName", "department", "title", "userPrincipalName", "mail", "userAccountControl", "lockoutTime", "lastLogonTimestamp", "distinguishedName")
foreach ($p in $props) { [void]$searcher.PropertiesToLoad.Add($p) }
$results = $searcher.FindAll()
$userList = [System.Collections.Generic.List[PSCustomObject]]::new()
foreach ($res in $results) {
$p = $res.Properties
$sam = if ($p["samaccountname"].Count -gt 0) { $p["samaccountname"][0] } else { "" }
$disp = if ($p["displayname"].Count -gt 0) { $p["displayname"][0] } else { $sam }
$dept = if ($p["department"].Count -gt 0) { $p["department"][0] } else { "-" }
$title = if ($p["title"].Count -gt 0) { $p["title"][0] } else { "-" }
$upn = if ($p["userprincipalname"].Count -gt 0) { $p["userprincipalname"][0] }
elseif ($p["mail"].Count -gt 0) { $p["mail"][0] } else { "$sam@domain.local" }
$dn = if ($p["distinguishedname"].Count -gt 0) { $p["distinguishedname"][0] } else { "" }
$uac = if ($p["useraccountcontrol"].Count -gt 0) { [int]$p["useraccountcontrol"][0] } else { 512 }
$isDisabled = ($uac -band 2) -eq 2
$statusText = if ($isDisabled) { "비활성" } else { "활성" }
$lockVal = if ($p["lockouttime"].Count -gt 0) { [int64]$p["lockouttime"][0] } else { 0 }
$isLocked = ($lockVal -gt 0)
$lockedText = if ($isLocked) { "잠김" } else { "정상" }
$lastLogonStr = "-"
if ($p["lastlogontimestamp"].Count -gt 0) {
try {
$fileTime = [int64]$p["lastlogontimestamp"][0]
if ($fileTime -gt 0) {
$lastLogonStr = [DateTime]::FromFileTime($fileTime).ToString("yyyy-MM-dd HH:mm:ss")
}
} catch {}
}
$userList.Add([PSCustomObject]@{
SamAccountName = $sam
DisplayName = $disp
Department = $dept
Title = $title
UserPrincipalName = $upn
StatusText = $statusText
LockedText = $lockedText
LastLogonDate = $lastLogonStr
Enabled = (-not $isDisabled)
IsLocked = $isLocked
DistinguishedName = $dn
IsLive = $true
})
}
return $userList.ToArray()
}
catch {
Write-Warning "실제 AD 조회 실패: $($_.Exception.Message) (Mock 데이터로 전환합니다)"
}
}
# Fallback / Mock Data
return Get-ADSampleUsers -Filter $Filter
}
function Unlock-ADUserAccount {
param(
[string]$SamAccountName,
[object]$Config = $null
)
$useLive = if ($Config -and $Config.ActiveDirectory) { [bool]$Config.ActiveDirectory.UseLive } else { $false }
if ($useLive) {
$dc = $Config.ActiveDirectory.DomainController
$baseOu = $Config.ActiveDirectory.BaseOU
$user = $Config.ActiveDirectory.Username
$pass = $Config.ActiveDirectory.Password
try {
$rootPath = if ([string]::IsNullOrWhiteSpace($baseOu)) { "LDAP://$dc" } else { "LDAP://$dc/$baseOu" }
$rootEntry = if ([string]::IsNullOrWhiteSpace($user)) {
New-Object System.DirectoryServices.DirectoryEntry($rootPath)
} else {
New-Object System.DirectoryServices.DirectoryEntry($rootPath, $user, $pass)
}
$searcher = New-Object System.DirectoryServices.DirectorySearcher($rootEntry)
$searcher.Filter = "(&(objectCategory=person)(objectClass=user)(sAMAccountName=$SamAccountName))"
$result = $searcher.FindOne()
if ($null -eq $result) {
return [PSCustomObject]@{ Success = $false; Message = "AD에서 [$SamAccountName] 사용자를 찾을 수 없습니다." }
}
$userEntry = $result.GetDirectoryEntry()
$userEntry.Properties["lockoutTime"].Value = 0
$userEntry.CommitChanges()
return [PSCustomObject]@{
Success = $true
Message = "[$SamAccountName] 계정의 잠금이 실제 AD에서 해제되었습니다."
}
} catch {
return [PSCustomObject]@{
Success = $false
Message = "AD 잠금 해제 실패: $($_.Exception.Message)"
}
}
}
# Demo Simulation
return [PSCustomObject]@{
Success = $true
Message = "[데모 모드] [$SamAccountName] 계정의 잠금이 해제되었습니다."
}
}
function Reset-ADUserPassword {
param(
[string]$SamAccountName,
[string]$NewPassword = "Passw0rd!#$((Get-Random -Minimum 1000 -Maximum 9999))",
[object]$Config = $null
)
$useLive = if ($Config -and $Config.ActiveDirectory) { [bool]$Config.ActiveDirectory.UseLive } else { $false }
if ($useLive) {
$dc = $Config.ActiveDirectory.DomainController
$baseOu = $Config.ActiveDirectory.BaseOU
$user = $Config.ActiveDirectory.Username
$pass = $Config.ActiveDirectory.Password
try {
$rootPath = if ([string]::IsNullOrWhiteSpace($baseOu)) { "LDAP://$dc:636" } else { "LDAP://$dc:636/$baseOu" }
$rootEntry = if ([string]::IsNullOrWhiteSpace($user)) {
New-Object System.DirectoryServices.DirectoryEntry($rootPath)
} else {
New-Object System.DirectoryServices.DirectoryEntry($rootPath, $user, $pass)
}
$searcher = New-Object System.DirectoryServices.DirectorySearcher($rootEntry)
$searcher.Filter = "(&(objectCategory=person)(objectClass=user)(sAMAccountName=$SamAccountName))"
$result = $searcher.FindOne()
if ($null -eq $result) {
return [PSCustomObject]@{ Success = $false; Message = "AD에서 [$SamAccountName] 사용자를 찾을 수 없습니다." }
}
$userEntry = $result.GetDirectoryEntry()
$userEntry.Invoke("SetPassword", @($NewPassword))
$userEntry.Properties["pwdLastSet"].Value = 0 # 다음 로그온 시 암호 변경 강제
$userEntry.CommitChanges()
return [PSCustomObject]@{
Success = $true
Message = "[$SamAccountName] 암호가 성공적으로 초기화되었습니다.`n임시 암호: $NewPassword"
}
} catch {
return [PSCustomObject]@{
Success = $false
Message = "AD 암호 초기화 실패: $($_.Exception.Message)`n(참고: AD 암호 변경은 보안상 LDAPS 636 포트 연결이 필요합니다.)"
}
}
}
return [PSCustomObject]@{
Success = $true
Message = "[데모 모드] [$SamAccountName] 계정의 암호가 초기화되었습니다.`n임시 암호: $NewPassword"
}
}
function Set-ADUserAccountState {
param(
[string]$SamAccountName,
[bool]$Enable,
[object]$Config = $null
)
$useLive = if ($Config -and $Config.ActiveDirectory) { [bool]$Config.ActiveDirectory.UseLive } else { $false }
if ($useLive) {
$dc = $Config.ActiveDirectory.DomainController
$baseOu = $Config.ActiveDirectory.BaseOU
$user = $Config.ActiveDirectory.Username
$pass = $Config.ActiveDirectory.Password
try {
$rootPath = if ([string]::IsNullOrWhiteSpace($baseOu)) { "LDAP://$dc" } else { "LDAP://$dc/$baseOu" }
$rootEntry = if ([string]::IsNullOrWhiteSpace($user)) {
New-Object System.DirectoryServices.DirectoryEntry($rootPath)
} else {
New-Object System.DirectoryServices.DirectoryEntry($rootPath, $user, $pass)
}
$searcher = New-Object System.DirectoryServices.DirectorySearcher($rootEntry)
$searcher.Filter = "(&(objectCategory=person)(objectClass=user)(sAMAccountName=$SamAccountName))"
$result = $searcher.FindOne()
if ($null -eq $result) {
return [PSCustomObject]@{ Success = $false; Message = "사용자를 찾을 수 없습니다." }
}
$userEntry = $result.GetDirectoryEntry()
$uac = [int]$userEntry.Properties["userAccountControl"].Value
if ($Enable) {
$userEntry.Properties["userAccountControl"].Value = $uac -band (-bnot 2)
} else {
$userEntry.Properties["userAccountControl"].Value = $uac -bor 2
}
$userEntry.CommitChanges()
$action = if ($Enable) { "활성화" } else { "비활성화" }
return [PSCustomObject]@{ Success = $true; Message = "[$SamAccountName] 계정이 $action 되었습니다." }
} catch {
return [PSCustomObject]@{ Success = $false; Message = "AD 상태 변경 실패: $($_.Exception.Message)" }
}
}
$action = if ($Enable) { "활성화" } else { "비활성화" }
return [PSCustomObject]@{ Success = $true; Message = "[데모 모드] [$SamAccountName] 계정이 $action 되었습니다." }
}
function Get-ADSampleUsers {
param([string]$Filter = "")
$users = @(
[PSCustomObject]@{
SamAccountName = "gdhong"
DisplayName = "홍길동"
Department = "클라우드인프라팀"
Title = "수석 엔지니어"
UserPrincipalName = "gdhong@corp.local"
StatusText = "활성"
LockedText = "정상"
LastLogonDate = "2026-10-20 09:14:22"
Enabled = $true
IsLocked = $false
DistinguishedName = "CN=홍길동,OU=Engineers,OU=Users,DC=corp,DC=local"
IsLive = $false
},
[PSCustomObject]@{
SamAccountName = "cskim"
DisplayName = "김철수"
Department = "데이터플랫폼팀"
Title = "DBA"
UserPrincipalName = "cskim@corp.local"
StatusText = "활성"
LockedText = "정상"
LastLogonDate = "2026-10-20 11:45:01"
Enabled = $true
IsLocked = $false
DistinguishedName = "CN=김철수,OU=DBA,OU=Users,DC=corp,DC=local"
IsLive = $false
},
[PSCustomObject]@{
SamAccountName = "yylee"
DisplayName = "이영희"
Department = "보안관제팀"
Title = "정보보안책임"
UserPrincipalName = "yylee@corp.local"
StatusText = "잠김"
LockedText = "잠김 (5회 실패)"
LastLogonDate = "2026-10-19 18:22:10"
Enabled = $true
IsLocked = $true
DistinguishedName = "CN=이영희,OU=Security,OU=Users,DC=corp,DC=local"
IsLive = $false
},
[PSCustomObject]@{
SamAccountName = "mschoi"
DisplayName = "최민수"
Department = "개발운영팀"
Title = "DevOps 엔지니어"
UserPrincipalName = "mschoi@corp.local"
StatusText = "비활성"
LockedText = "정상"
LastLogonDate = "2026-09-01 14:02:00"
Enabled = $false
IsLocked = $false
DistinguishedName = "CN=최민수,OU=DevOps,OU=Users,DC=corp,DC=local"
IsLive = $false
},
[PSCustomObject]@{
SamAccountName = "jhbak"
DisplayName = "박지훈"
Department = "시스템관리팀"
Title = "스토리지 관리자"
UserPrincipalName = "jhbak@corp.local"
StatusText = "활성"
LockedText = "정상"
LastLogonDate = "2026-10-20 13:02:15"
Enabled = $true
IsLocked = $false
DistinguishedName = "CN=박지훈,OU=Admins,OU=Users,DC=corp,DC=local"
IsLive = $false
}
)
if ([string]::IsNullOrWhiteSpace($Filter)) {
return $users
} else {
return @($users | Where-Object {
$_.SamAccountName -like "*$Filter*" -or
$_.DisplayName -like "*$Filter*" -or
$_.Department -like "*$Filter*"
})
}
}
+68
View File
@@ -0,0 +1,68 @@
# Modules\ConfigService.ps1
# Configuration Management Module for Enterprise Admin Studio
$global:AppConfigPath = Join-Path (Split-Path -Parent $PSScriptRoot) "config.json"
function Get-AppConfig {
if (-not (Test-Path $global:AppConfigPath)) {
$defaultConfig = @{
Application = @{
LiveMode = $false
Theme = "Dark"
AutoRefreshSeconds = 60
}
ActiveDirectory = @{
UseLive = $false
DomainController = "dc01.corp.local"
Domain = "corp.local"
BaseOU = "DC=corp,DC=local"
UseLdaps = $false
Username = ""
Password = ""
}
NetApp = @{
UseLive = $false
ClusterEndpoint = "https://10.200.10.50"
Port = 443
DefaultSvm = "svm_cifs_prod01"
Username = "admin"
Password = ""
IgnoreSslErrors = $true
}
MsSql = @{
UseLive = $false
Server = "localhost"
AuthType = "Integrated"
Username = "sa"
Password = ""
DefaultDatabase = "master"
ConnectionTimeout = 10
BackupDirectory = "C:\Backups"
}
}
$json = $defaultConfig | ConvertTo-Json -Depth 5
[System.IO.File]::WriteAllText($global:AppConfigPath, $json, [System.Text.Encoding]::UTF8)
return $defaultConfig
}
try {
$raw = [System.IO.File]::ReadAllText($global:AppConfigPath, [System.Text.Encoding]::UTF8)
return ($raw | ConvertFrom-Json)
} catch {
Write-Warning "설정 파일 로드 실패, 기본값을 사용합니다: $_"
return $null
}
}
function Save-AppConfig {
param([object]$Config)
try {
$json = $Config | ConvertTo-Json -Depth 5
[System.IO.File]::WriteAllText($global:AppConfigPath, $json, [System.Text.Encoding]::UTF8)
return $true
} catch {
Write-Error "설정 저장 실패: $_"
return $false
}
}
+498
View File
@@ -0,0 +1,498 @@
# 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*"
})
}
}
+314
View File
@@ -0,0 +1,314 @@
# Modules\SqlService.ps1
# Microsoft SQL Server Management Service Module (.NET System.Data.SqlClient Live & Fallback)
function Build-SqlConnectionString {
param(
[string]$Server,
[string]$AuthType = "Integrated",
[string]$Username = "",
[string]$Password = "",
[string]$DefaultDatabase = "master",
[int]$Timeout = 10
)
$builder = New-Object System.Data.SqlClient.SqlConnectionStringBuilder
$builder["Data Source"] = $Server
$builder["Initial Catalog"] = if ([string]::IsNullOrWhiteSpace($DefaultDatabase)) { "master" } else { $DefaultDatabase }
$builder["Connect Timeout"] = $Timeout
$builder["Application Name"] = "EnterpriseAdminStudio"
if ($AuthType -eq "Integrated" -or [string]::IsNullOrWhiteSpace($Username)) {
$builder["Integrated Security"] = $true
} else {
$builder["Integrated Security"] = $false
$builder["User ID"] = $Username
$builder["Password"] = $Password
}
return $builder.ConnectionString
}
function Test-SqlConnection {
param(
[string]$Server,
[string]$AuthType = "Integrated",
[string]$Username = "",
[string]$Password = "",
[int]$Timeout = 5
)
$connStr = Build-SqlConnectionString -Server $Server -AuthType $AuthType -Username $Username -Password $Password -Timeout $Timeout
$conn = New-Object System.Data.SqlClient.SqlConnection($connStr)
try {
$conn.Open()
$cmd = $conn.CreateCommand()
$cmd.CommandText = "SELECT @@VERSION AS [Version], SERVERPROPERTY('ProductVersion') AS [ProductVersion], SERVERPROPERTY('Edition') AS [Edition]"
$reader = $cmd.ExecuteReader()
if ($reader.Read()) {
$ver = $reader["ProductVersion"].ToString()
$edition = $reader["Edition"].ToString()
return [PSCustomObject]@{
Success = $true
Message = "SQL Server 연결 성공 - 버전: $ver ($edition)"
Version = "$ver ($edition)"
}
}
return [PSCustomObject]@{ Success = $true; Message = "SQL Server 연결 성공" }
} catch {
return [PSCustomObject]@{
Success = $false
Message = "SQL Server 연결 실패: $($_.Exception.Message)"
}
} finally {
if ($conn.State -eq [System.Data.ConnectionState]::Open) {
$conn.Close()
}
$conn.Dispose()
}
}
function Get-SqlDatabases {
param(
[string]$Filter = "",
[object]$Config = $null
)
$useLive = if ($Config -and $Config.MsSql) { [bool]$Config.MsSql.UseLive } else { $false }
if ($useLive) {
$server = $Config.MsSql.Server
$auth = $Config.MsSql.AuthType
$user = $Config.MsSql.Username
$pass = $Config.MsSql.Password
$timeout = if ($Config.MsSql.ConnectionTimeout) { [int]$Config.MsSql.ConnectionTimeout } else { 10 }
$connStr = Build-SqlConnectionString -Server $server -AuthType $auth -Username $user -Password $pass -Timeout $timeout
$conn = New-Object System.Data.SqlClient.SqlConnection($connStr)
try {
$conn.Open()
$cmd = $conn.CreateCommand()
$cmd.CommandText = @"
SELECT
d.name AS DatabaseName,
d.state_desc AS StateDesc,
d.recovery_model_desc AS RecoveryModel,
d.collation_name AS CollationName,
CAST(ROUND(SUM(CAST(mf.size AS bigint)) * 8.0 / 1024, 1) AS varchar(30)) + ' MB' AS TotalSizeMB,
ISNULL(CONVERT(varchar(20), MAX(b.backup_finish_date), 120) + ' (' + b.type + ')', '백업 없음') AS LastBackupDate
FROM sys.databases d
LEFT JOIN sys.master_files mf ON d.database_id = mf.database_id
LEFT JOIN msdb.dbo.backupset b ON d.name = b.database_name AND b.type = 'D'
GROUP BY d.name, d.state_desc, d.recovery_model_desc, d.collation_name, b.type
ORDER BY d.name;
"@
$adapter = New-Object System.Data.SqlClient.SqlDataAdapter($cmd)
$table = New-Object System.Data.DataTable
$adapter.Fill($table) | Out-Null
$list = [System.Collections.Generic.List[PSCustomObject]]::new()
foreach ($row in $table.Rows) {
$dbName = $row["DatabaseName"]
if ([string]::IsNullOrWhiteSpace($Filter) -or $dbName -like "*$Filter*") {
$list.Add([PSCustomObject]@{
DatabaseName = $dbName
StateDesc = $row["StateDesc"]
TotalSizeMB = $row["TotalSizeMB"]
FreeSpaceMB = "실시간 계산"
RecoveryModel = $row["RecoveryModel"]
CollationName = $row["CollationName"]
LastBackupDate = $row["LastBackupDate"]
IsLive = $true
})
}
}
return $list.ToArray()
} catch {
Write-Warning "실제 SQL Server 조회 실패: $($_.Exception.Message) (Mock 데이터로 전환합니다)"
} finally {
if ($conn.State -eq [System.Data.ConnectionState]::Open) { $conn.Close() }
$conn.Dispose()
}
}
return Get-SqlSampleDatabases -Filter $Filter
}
function Invoke-SqlBackupAction {
param(
[string]$DatabaseName,
[string]$BackupType = "FULL",
[object]$Config = $null
)
$useLive = if ($Config -and $Config.MsSql) { [bool]$Config.MsSql.UseLive } else { $false }
if ($useLive) {
$server = $Config.MsSql.Server
$auth = $Config.MsSql.AuthType
$user = $Config.MsSql.Username
$pass = $Config.MsSql.Password
$backupDir = if ($Config.MsSql.BackupDirectory) { $Config.MsSql.BackupDirectory } else { "C:\Backups" }
if (-not (Test-Path $backupDir)) {
New-Item -ItemType Directory -Path $backupDir -Force | Out-Null
}
$timeStamp = (Get-Date).ToString("yyyyMMdd_HHmmss")
$fileName = "$DatabaseName`_$timeStamp.bak"
$targetPath = Join-Path $backupDir $fileName
$connStr = Build-SqlConnectionString -Server $server -AuthType $auth -Username $user -Password $pass -Timeout 30
$conn = New-Object System.Data.SqlClient.SqlConnection($connStr)
try {
$conn.Open()
$cmd = $conn.CreateCommand()
$cmd.CommandTimeout = 300 # 백업은 5분 타임아웃
$cmd.CommandText = "BACKUP DATABASE [$DatabaseName] TO DISK = N'$targetPath' WITH INIT, STATS = 10, NAME = N'$DatabaseName-Full Backup'"
$cmd.ExecuteNonQuery() | Out-Null
return [PSCustomObject]@{
Success = $true
Database = $DatabaseName
BackupPath = $targetPath
Message = "실제 SQL Server 데이터베이스 [$DatabaseName] 백업이 완료되었습니다.`n저장 경로: $targetPath"
}
} catch {
return [PSCustomObject]@{
Success = $false
Database = $DatabaseName
Message = "SQL 백업 실패: $($_.Exception.Message)"
}
} finally {
if ($conn.State -eq [System.Data.ConnectionState]::Open) { $conn.Close() }
$conn.Dispose()
}
}
return [PSCustomObject]@{
Success = $true
Database = $DatabaseName
BackupPath = "\\backup-nas.corp.local\sqlbackups\$DatabaseName-$((Get-Date).ToString('yyyyMMdd_HHmmss')).bak"
Message = "[데모 모드] 데이터베이스 [$DatabaseName]의 $BackupType 백업이 가상 완료되었습니다."
}
}
function Invoke-SqlQuerySnippet {
param(
[string]$Query,
[object]$Config = $null
)
$useLive = if ($Config -and $Config.MsSql) { [bool]$Config.MsSql.UseLive } else { $false }
if ($useLive) {
$server = $Config.MsSql.Server
$auth = $Config.MsSql.AuthType
$user = $Config.MsSql.Username
$pass = $Config.MsSql.Password
$connStr = Build-SqlConnectionString -Server $server -AuthType $auth -Username $user -Password $pass -Timeout 15
$conn = New-Object System.Data.SqlClient.SqlConnection($connStr)
try {
$conn.Open()
$cmd = $conn.CreateCommand()
$cmd.CommandText = $Query
$adapter = New-Object System.Data.SqlClient.SqlDataAdapter($cmd)
$table = New-Object System.Data.DataTable
$adapter.Fill($table) | Out-Null
$sw = [System.IO.StringWriter]::new()
$table | Format-Table -AutoSize | Out-String | ForEach-Object { $sw.WriteLine($_) }
return [PSCustomObject]@{
Success = $true
RowCount = $table.Rows.Count
Output = "(성공: $($table.Rows.Count)개 행 반환됨 - $((Get-Date).ToString('HH:mm:ss')))`n$($sw.ToString())"
}
} catch {
return [PSCustomObject]@{
Success = $false
RowCount = 0
Output = "(오류 발생)`n$($_.Exception.Message)"
}
} finally {
if ($conn.State -eq [System.Data.ConnectionState]::Open) { $conn.Close() }
$conn.Dispose()
}
}
return [PSCustomObject]@{
Success = $true
RowCount = 10
Output = "(데모 모드 성공: 10개 행 반환됨 - $((Get-Date).ToString('HH:mm:ss')))`n1. tbl_Users (Row: 14,200)`n2. tbl_AuditLogs (Row: 240,119)`n3. tbl_Orders (Row: 82,900)"
}
}
function Get-SqlSampleDatabases {
param([string]$Filter = "")
$databases = @(
[PSCustomObject]@{
DatabaseName = "ERP_PRODUCTION"
StateDesc = "ONLINE"
TotalSizeMB = "56,370 MB"
FreeSpaceMB = "6,410 MB"
RecoveryModel = "FULL"
CollationName = "Korean_Wansung_CI_AS"
LastBackupDate = "2026-10-20 02:00:15 (Full)"
IsLive = $false
},
[PSCustomObject]@{
DatabaseName = "HR_PORTAL_DB"
StateDesc = "ONLINE"
TotalSizeMB = "12,450 MB"
FreeSpaceMB = "2,120 MB"
RecoveryModel = "FULL"
CollationName = "Korean_Wansung_CI_AS"
LastBackupDate = "2026-10-20 02:30:00 (Full)"
IsLive = $false
},
[PSCustomObject]@{
DatabaseName = "WAREHOUSE_ANALYTICS"
StateDesc = "ONLINE"
TotalSizeMB = "184,000 MB"
FreeSpaceMB = "18,900 MB"
RecoveryModel = "SIMPLE"
CollationName = "Korean_Wansung_CI_AS"
LastBackupDate = "2026-10-19 23:00:12 (Full)"
IsLive = $false
},
[PSCustomObject]@{
DatabaseName = "DEV_STAGING_TEST"
StateDesc = "ONLINE"
TotalSizeMB = "4,200 MB"
FreeSpaceMB = "850 MB"
RecoveryModel = "SIMPLE"
CollationName = "Korean_Wansung_CI_AS"
LastBackupDate = "2026-10-18 10:15:00 (Full)"
IsLive = $false
},
[PSCustomObject]@{
DatabaseName = "ARCHIVE_2025"
StateDesc = "READ_ONLY"
TotalSizeMB = "92,500 MB"
FreeSpaceMB = "45 MB"
RecoveryModel = "SIMPLE"
CollationName = "Korean_Wansung_CI_AS"
LastBackupDate = "2026-01-01 00:00:00 (Full)"
IsLive = $false
}
)
if ([string]::IsNullOrWhiteSpace($Filter)) {
return $databases
} else {
return @($databases | Where-Object {
$_.DatabaseName -like "*$Filter*" -or
$_.StateDesc -like "*$Filter*"
})
}
}