feat: implement modern admin studio with live AD, NetApp, MS SQL connectivity and comprehensive docs
This commit is contained in:
@@ -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*"
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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*"
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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*"
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user