1500 lines
79 KiB
PowerShell
1500 lines
79 KiB
PowerShell
# ==============================================================================
|
|
# Enterprise Admin Studio - Main Entrypoint
|
|
# PowerShell WPF Modern UI Infrastructure Management Tool
|
|
# ==============================================================================
|
|
|
|
[CmdletBinding()]
|
|
param(
|
|
[switch]$NoShow
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
$ScriptDir = if ($PSScriptRoot) {
|
|
$PSScriptRoot
|
|
} elseif ($MyInvocation.MyCommand.Path) {
|
|
Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
} else {
|
|
(Get-Location).Path
|
|
}
|
|
|
|
# 1. Load WPF / .NET GUI Assemblies
|
|
Add-Type -AssemblyName PresentationFramework
|
|
Add-Type -AssemblyName PresentationCore
|
|
Add-Type -AssemblyName WindowsBase
|
|
Add-Type -AssemblyName System.Data
|
|
Add-Type -AssemblyName System.DirectoryServices
|
|
|
|
# 2. Import Business Service Modules
|
|
. (Join-Path $ScriptDir "Modules\ConfigService.ps1")
|
|
. (Join-Path $ScriptDir "Modules\ADService.ps1")
|
|
. (Join-Path $ScriptDir "Modules\NetAppService.ps1")
|
|
. (Join-Path $ScriptDir "Modules\SqlService.ps1")
|
|
. (Join-Path $ScriptDir "Modules\ShareSyncService.ps1")
|
|
. (Join-Path $ScriptDir "Modules\UserResolverService.ps1")
|
|
|
|
# Global Configuration Object
|
|
$global:AppConfig = Get-AppConfig
|
|
|
|
# Helper: Load XAML File safely
|
|
function Load-XamlElement {
|
|
param([string]$Path)
|
|
|
|
if (-not (Test-Path $Path)) {
|
|
throw "XAML file not found: $Path"
|
|
}
|
|
|
|
$rawContent = [System.IO.File]::ReadAllText($Path, [System.Text.Encoding]::UTF8)
|
|
$cleanContent = $rawContent -replace 'x:Class="[^"]*"', ''
|
|
|
|
$stringReader = New-Object System.IO.StringReader($cleanContent)
|
|
$xmlReader = [System.Xml.XmlReader]::Create($stringReader)
|
|
|
|
try {
|
|
return [System.Windows.Markup.XamlReader]::Load($xmlReader)
|
|
}
|
|
finally {
|
|
$xmlReader.Close()
|
|
$stringReader.Close()
|
|
}
|
|
}
|
|
|
|
# 3. Initialize WPF Application & Global Theme Resources
|
|
if ($null -eq [System.Windows.Application]::Current) {
|
|
$app = New-Object System.Windows.Application
|
|
} else {
|
|
$app = [System.Windows.Application]::Current
|
|
}
|
|
|
|
$themePath = Join-Path $ScriptDir "Styles\Theme.xaml"
|
|
$themeResource = Load-XamlElement -Path $themePath
|
|
$app.Resources.MergedDictionaries.Add($themeResource)
|
|
|
|
# 4. Load MainWindow XAML
|
|
$mainXamlPath = Join-Path $ScriptDir "MainWindow.xaml"
|
|
$mainWindow = Load-XamlElement -Path $mainXamlPath
|
|
|
|
# Find Main Window Controls
|
|
$mainContentHost = $mainWindow.FindName("MainContentHost")
|
|
$txtHeaderTitle = $mainWindow.FindName("TxtHeaderTitle")
|
|
$txtHeaderSubtitle = $mainWindow.FindName("TxtHeaderSubtitle")
|
|
$txtSystemClock = $mainWindow.FindName("TxtSystemClock")
|
|
$txtModeBadge = $mainWindow.FindName("TxtModeBadge")
|
|
$badgeMode = $mainWindow.FindName("BadgeMode")
|
|
$txtGlobalStatus = $mainWindow.FindName("TxtGlobalStatus")
|
|
$dotGlobalStatus = $mainWindow.FindName("DotGlobalStatus")
|
|
|
|
$navDashboard = $mainWindow.FindName("NavDashboard")
|
|
$navAD = $mainWindow.FindName("NavAD")
|
|
$navNetApp = $mainWindow.FindName("NavNetApp")
|
|
$navShareAccess = $mainWindow.FindName("NavShareAccess")
|
|
$navBatchWs = $mainWindow.FindName("NavBatchWs")
|
|
$navSql = $mainWindow.FindName("NavSql")
|
|
$navSettings = $mainWindow.FindName("NavSettings")
|
|
$btnGlobalRefresh = $mainWindow.FindName("BtnGlobalRefresh")
|
|
|
|
# Function to Update Mode Badge UI
|
|
function Update-ModeBadgeUI {
|
|
$isLive = ($global:AppConfig.Application.LiveMode -or
|
|
$global:AppConfig.ActiveDirectory.UseLive -or
|
|
$global:AppConfig.NetApp.UseLive -or
|
|
$global:AppConfig.MsSql.UseLive)
|
|
|
|
if ($txtModeBadge -and $badgeMode) {
|
|
if ($isLive) {
|
|
$txtModeBadge.Text = "실제 연결 모드 (Live)"
|
|
$badgeMode.Background = [System.Windows.Media.BrushConverter]::new().ConvertFromString("#10B981")
|
|
} else {
|
|
$txtModeBadge.Text = "데모/모의 모드 (Mock)"
|
|
$badgeMode.Background = [System.Windows.Media.BrushConverter]::new().ConvertFromString("#6366F1")
|
|
}
|
|
}
|
|
}
|
|
|
|
Update-ModeBadgeUI
|
|
|
|
if ($txtSystemClock) {
|
|
$txtSystemClock.Text = (Get-Date).ToString("yyyy-MM-dd HH:mm")
|
|
}
|
|
|
|
# View Cache
|
|
$ViewCache = @{}
|
|
|
|
# ==============================================================================
|
|
# Robust Script-Scoped Refresh Functions (Directly finds controls from ViewCache)
|
|
# ==============================================================================
|
|
|
|
$script:reloadAd = {
|
|
$v = $ViewCache["AD"]
|
|
if (-not $v) { return }
|
|
$grid = $v.FindName("AdUsersGrid")
|
|
$txtSearch = $v.FindName("TxtAdSearch")
|
|
$txtStatusMsg = $v.FindName("TxtAdStatusMsg")
|
|
$txtTotalCount = $v.FindName("TxtAdTotalCount")
|
|
|
|
$filter = if ($txtSearch) { $txtSearch.Text } else { "" }
|
|
$users = Get-ADUserList -Filter $filter -Config $global:AppConfig
|
|
if ($grid) { $grid.ItemsSource = $users }
|
|
if ($txtTotalCount) { $txtTotalCount.Text = "총 $($users.Count) 명의 사용자" }
|
|
if ($txtStatusMsg) {
|
|
$isLive = ($users.Count -gt 0 -and $users[0].IsLive)
|
|
$modeText = if ($isLive) { "실제 도메인 컨트롤러 연결 중 ($($global:AppConfig.ActiveDirectory.DomainController))" } else { "데모 시뮬레이션 모드 (config.json에서 실제 연결 설정 가능)" }
|
|
$txtStatusMsg.Text = "상태: $modeText"
|
|
}
|
|
}
|
|
|
|
$script:reloadNetApp = {
|
|
$v = $ViewCache["NetApp"]
|
|
if (-not $v) { return }
|
|
$qtreeGrid = $v.FindName("QtreeGrid")
|
|
$quotaGrid = $v.FindName("QuotaGrid")
|
|
$shareGrid = $v.FindName("ShareGrid")
|
|
$txtNetAppStatus = $v.FindName("TxtNetAppStatusMsg")
|
|
|
|
$qtrees = Get-NetAppQtrees -Config $global:AppConfig
|
|
$quotas = Get-NetAppQuotas -Config $global:AppConfig
|
|
$shares = Get-NetAppShares -Config $global:AppConfig
|
|
|
|
if ($qtreeGrid) { $qtreeGrid.ItemsSource = $qtrees }
|
|
if ($quotaGrid) { $quotaGrid.ItemsSource = $quotas }
|
|
if ($shareGrid) { $shareGrid.ItemsSource = $shares }
|
|
|
|
$isLive = ($qtrees.Count -gt 0 -and $qtrees[0].IsLive)
|
|
if ($txtNetAppStatus) {
|
|
$modeText = if ($isLive) { "ONTAP REST API 실시간 연결됨 ($($global:AppConfig.NetApp.ClusterEndpoint))" } else { "데모/시뮬레이션 모드 (ONTAP REST API v2 호환 구조)" }
|
|
$txtNetAppStatus.Text = "스토리지 응답: $modeText"
|
|
}
|
|
}
|
|
|
|
$script:reloadShareData = {
|
|
$v = $ViewCache["ShareAccess"]
|
|
if (-not $v) { return }
|
|
$cmb = $v.FindName("CmbShareList")
|
|
$dbGrid = $v.FindName("DbUsersGrid")
|
|
$adGrid = $v.FindName("ADGroupMembersGrid")
|
|
$targetG = $v.FindName("TxtTargetADGroupName")
|
|
$dbCnt = $v.FindName("TxtDbCount")
|
|
$adCnt = $v.FindName("TxtAdGroupCount")
|
|
|
|
$selectedItem = if ($cmb) { $cmb.SelectedItem } else { $null }
|
|
$shareName = if ($selectedItem -and $selectedItem.Tag) { $selectedItem.Tag } else { "Finance_Share" }
|
|
if ($targetG) { $targetG.Text = $shareName }
|
|
|
|
$dbUsers = Get-DbUsersForShare -ShareName $shareName -Config $global:AppConfig
|
|
if ($dbGrid) { $dbGrid.ItemsSource = $dbUsers }
|
|
if ($dbCnt) { $dbCnt.Text = "총 $($dbUsers.Count)명 조회됨" }
|
|
|
|
$adMembers = Get-ADGroupMembersByShareName -GroupName $shareName -Config $global:AppConfig
|
|
if ($adGrid) { $adGrid.ItemsSource = $adMembers }
|
|
if ($adCnt) { $adCnt.Text = "현재 그룹 멤버: $($adMembers.Count)명" }
|
|
}
|
|
|
|
$script:reloadSql = {
|
|
$v = $ViewCache["Sql"]
|
|
if (-not $v) { return }
|
|
$dbGrid = $v.FindName("SqlDbGrid")
|
|
$txtSqlStatusMsg = $v.FindName("TxtSqlStatusMsg")
|
|
$txtSqlTotalCount = $v.FindName("TxtSqlTotalCount")
|
|
|
|
$dbs = Get-SqlDatabases -Config $global:AppConfig
|
|
if ($dbGrid) { $dbGrid.ItemsSource = $dbs }
|
|
if ($txtSqlTotalCount) { $txtSqlTotalCount.Text = "총 $($dbs.Count)개 데이터베이스" }
|
|
if ($txtSqlStatusMsg) {
|
|
$isLive = ($dbs.Count -gt 0 -and $dbs[0].IsLive)
|
|
$modeText = if ($isLive) { "실제 SQL Server 연결 ($($global:AppConfig.MsSql.Server))" } else { "데모/시뮬레이션 모드 (System.Data.SqlClient 연동 완비)" }
|
|
$txtSqlStatusMsg.Text = "상태: $modeText"
|
|
}
|
|
}
|
|
|
|
$script:loadSettingsToUI = {
|
|
$v = $ViewCache["Settings"]
|
|
if (-not $v) { return }
|
|
|
|
$chkGlobalLive = $v.FindName("ChkGlobalLiveMode")
|
|
$chkAdLive = $v.FindName("ChkAdLive")
|
|
$txtAdDc = $v.FindName("TxtAdDc")
|
|
$txtAdBaseOu = $v.FindName("TxtAdBaseOu")
|
|
$chkAdLdaps = $v.FindName("ChkAdLdaps")
|
|
$txtAdUser = $v.FindName("TxtAdUser")
|
|
$txtAdPassword = $v.FindName("TxtAdPassword")
|
|
|
|
$chkNetAppLive = $v.FindName("ChkNetAppLive")
|
|
$txtNetAppEndpoint = $v.FindName("TxtNetAppEndpoint")
|
|
$txtNetAppDefaultSvm = $v.FindName("TxtNetAppDefaultSvm")
|
|
$chkNetAppIgnoreSsl = $v.FindName("ChkNetAppIgnoreSsl")
|
|
$txtNetAppUser = $v.FindName("TxtNetAppUser")
|
|
$txtNetAppPassword = $v.FindName("TxtNetAppPassword")
|
|
|
|
$chkSqlLive = $v.FindName("ChkSqlLive")
|
|
$txtSqlServer = $v.FindName("TxtSqlServer")
|
|
$cmbSqlAuth = $v.FindName("CmbSqlAuthType")
|
|
$txtSqlBackupDir = $v.FindName("TxtSqlBackupDir")
|
|
$txtSqlDomain = $v.FindName("TxtSqlDomain")
|
|
$txtSqlUser = $v.FindName("TxtSqlUser")
|
|
$txtSqlPassword = $v.FindName("TxtSqlPassword")
|
|
|
|
$c = $global:AppConfig
|
|
if ($chkGlobalLive) { $chkGlobalLive.IsChecked = [bool]$c.Application.LiveMode }
|
|
|
|
if ($chkAdLive) { $chkAdLive.IsChecked = [bool]$c.ActiveDirectory.UseLive }
|
|
if ($txtAdDc) { $txtAdDc.Text = $c.ActiveDirectory.DomainController }
|
|
if ($txtAdBaseOu) { $txtAdBaseOu.Text = $c.ActiveDirectory.BaseOU }
|
|
if ($chkAdLdaps) { $chkAdLdaps.IsChecked = [bool]$c.ActiveDirectory.UseLdaps }
|
|
if ($txtAdUser) { $txtAdUser.Text = $c.ActiveDirectory.Username }
|
|
if ($txtAdPassword) { $txtAdPassword.Password = $c.ActiveDirectory.Password }
|
|
|
|
if ($chkNetAppLive) { $chkNetAppLive.IsChecked = [bool]$c.NetApp.UseLive }
|
|
if ($txtNetAppEndpoint) { $txtNetAppEndpoint.Text = $c.NetApp.ClusterEndpoint }
|
|
if ($txtNetAppDefaultSvm) { $txtNetAppDefaultSvm.Text = $c.NetApp.DefaultSvm }
|
|
if ($chkNetAppIgnoreSsl) { $chkNetAppIgnoreSsl.IsChecked = [bool]$c.NetApp.IgnoreSslErrors }
|
|
if ($txtNetAppUser) { $txtNetAppUser.Text = $c.NetApp.Username }
|
|
if ($txtNetAppPassword) { $txtNetAppPassword.Password = $c.NetApp.Password }
|
|
|
|
if ($chkSqlLive) { $chkSqlLive.IsChecked = [bool]$c.MsSql.UseLive }
|
|
if ($txtSqlServer) { $txtSqlServer.Text = $c.MsSql.Server }
|
|
if ($txtSqlDomain) { $txtSqlDomain.Text = if ($c.MsSql.Domain) { $c.MsSql.Domain } else { "corp" } }
|
|
if ($cmbSqlAuth) {
|
|
$cmbSqlAuth.SelectedIndex = switch ($c.MsSql.AuthType) {
|
|
"AdImpersonated" { 1 }
|
|
"Sql" { 2 }
|
|
default { 0 }
|
|
}
|
|
}
|
|
if ($txtSqlBackupDir) { $txtSqlBackupDir.Text = $c.MsSql.BackupDirectory }
|
|
if ($txtSqlUser) { $txtSqlUser.Text = $c.MsSql.Username }
|
|
if ($txtSqlPassword) { $txtSqlPassword.Password = $c.MsSql.Password }
|
|
}
|
|
|
|
# ==============================================================================
|
|
# Modal Dialog: User Multi-Resolver & Ambiguity Checker
|
|
# ==============================================================================
|
|
function Show-UserResolverDialog {
|
|
param(
|
|
[string]$InitialText = "",
|
|
[System.Windows.Window]$Owner = $null
|
|
)
|
|
|
|
$dialogPath = Join-Path $ScriptDir "Views\UserResolverDialog.xaml"
|
|
$dialog = Load-XamlElement -Path $dialogPath
|
|
if ($Owner) { $dialog.Owner = $Owner }
|
|
|
|
$txtInput = $dialog.FindName("TxtMultiInput")
|
|
$btnResolve = $dialog.FindName("BtnResolveUsers")
|
|
$grid = $dialog.FindName("GridResolvedUsers")
|
|
$badge = $dialog.FindName("BadgeSummary")
|
|
$txtSummary = $dialog.FindName("TxtResolutionSummary")
|
|
|
|
$btnSelResolved = $dialog.FindName("BtnSelectResolvedOnly")
|
|
$btnSelAll = $dialog.FindName("BtnSelectAllDialog")
|
|
$btnDeselAll = $dialog.FindName("BtnDeselectAllDialog")
|
|
$btnPaste = $dialog.FindName("BtnPasteClipboard")
|
|
$btnClear = $dialog.FindName("BtnClearInput")
|
|
|
|
$btnConfirm = $dialog.FindName("BtnDialogConfirm")
|
|
$btnCancel = $dialog.FindName("BtnDialogCancel")
|
|
|
|
if (-not [string]::IsNullOrWhiteSpace($InitialText)) {
|
|
$txtInput.Text = $InitialText
|
|
}
|
|
|
|
$runResolution = {
|
|
$raw = if ($txtInput) { $txtInput.Text } else { "" }
|
|
$tokens = Parse-MultiUserInput -RawInput $raw
|
|
$results = Resolve-UsersWithAmbiguityCheck -Tokens $tokens -Config $global:AppConfig
|
|
if ($grid) { $grid.ItemsSource = $results }
|
|
|
|
$ambCount = @($results | Where-Object { $_.IsAmbiguous }).Count
|
|
$normCount = @($results | Where-Object { $_.IsResolved -and (-not $_.IsAmbiguous) }).Count
|
|
$notFound = @($results | Where-Object { $_.CandidateCount -eq 0 }).Count
|
|
|
|
if ($txtSummary) {
|
|
$txtSummary.Text = "정상 매칭: $normCount건 | 동명이인: $ambCount건 | 미발견: $notFound건"
|
|
}
|
|
if ($badge) {
|
|
if ($ambCount -gt 0) {
|
|
$badge.Background = [System.Windows.Media.BrushConverter]::new().ConvertFromString("#F59E0B")
|
|
} elseif ($notFound -gt 0) {
|
|
$badge.Background = [System.Windows.Media.BrushConverter]::new().ConvertFromString("#EF4444")
|
|
} else {
|
|
$badge.Background = [System.Windows.Media.BrushConverter]::new().ConvertFromString("#10B981")
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($btnResolve) { $btnResolve.add_Click({ & $runResolution }) }
|
|
if ($btnPaste) {
|
|
$btnPaste.add_Click({
|
|
try {
|
|
$clip = [System.Windows.Clipboard]::GetText()
|
|
if (-not [string]::IsNullOrWhiteSpace($clip)) {
|
|
$txtInput.Text = $clip
|
|
& $runResolution
|
|
}
|
|
} catch {}
|
|
})
|
|
}
|
|
if ($btnClear) { $btnClear.add_Click({ $txtInput.Text = "" }) }
|
|
|
|
if ($btnSelResolved) {
|
|
$btnSelResolved.add_Click({
|
|
if ($grid.ItemsSource) {
|
|
foreach ($item in $grid.ItemsSource) {
|
|
$item.IsSelected = ($item.IsResolved -and (-not $item.IsAmbiguous))
|
|
}
|
|
$grid.Items.Refresh()
|
|
}
|
|
})
|
|
}
|
|
|
|
if ($btnSelAll) {
|
|
$btnSelAll.add_Click({
|
|
if ($grid.ItemsSource) {
|
|
foreach ($item in $grid.ItemsSource) { $item.IsSelected = $true }
|
|
$grid.Items.Refresh()
|
|
}
|
|
})
|
|
}
|
|
|
|
if ($btnDeselAll) {
|
|
$btnDeselAll.add_Click({
|
|
if ($grid.ItemsSource) {
|
|
foreach ($item in $grid.ItemsSource) { $item.IsSelected = $false }
|
|
$grid.Items.Refresh()
|
|
}
|
|
})
|
|
}
|
|
|
|
$script:ResolvedUsersFromDialog = @()
|
|
|
|
if ($btnConfirm) {
|
|
$btnConfirm.add_Click({
|
|
$selectedUsers = if ($grid.ItemsSource) {
|
|
@($grid.ItemsSource | Where-Object { $_.IsSelected -and $_.SamAccountName -ne "-" })
|
|
} else { @() }
|
|
|
|
if ($selectedUsers.Count -eq 0) {
|
|
[System.Windows.MessageBox]::Show("확정할 사용자를 1명 이상 체크박스로 선택하십시오.", "선택 필요", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Warning)
|
|
return
|
|
}
|
|
|
|
$script:ResolvedUsersFromDialog = $selectedUsers
|
|
$dialog.DialogResult = $true
|
|
$dialog.Close()
|
|
})
|
|
}
|
|
|
|
if ($btnCancel) {
|
|
$btnCancel.add_Click({
|
|
$dialog.DialogResult = $false
|
|
$dialog.Close()
|
|
})
|
|
}
|
|
|
|
# Initial Run
|
|
& $runResolution
|
|
|
|
$ret = $dialog.ShowDialog()
|
|
if ($ret -eq $true) {
|
|
return $script:ResolvedUsersFromDialog
|
|
}
|
|
return $null
|
|
}
|
|
|
|
# Function to Switch Navigation Pages
|
|
function Navigate-ToPage {
|
|
param([string]$PageName)
|
|
|
|
switch ($PageName) {
|
|
"Dashboard" {
|
|
$txtHeaderTitle.Text = "통합 대시보드"
|
|
$txtHeaderSubtitle.Text = "전체 인프라 실시간 상태 개요 및 최근 감사 로그"
|
|
|
|
if (-not $ViewCache.ContainsKey("Dashboard")) {
|
|
$view = Load-XamlElement -Path (Join-Path $ScriptDir "Views\DashboardView.xaml")
|
|
|
|
$activityGrid = $view.FindName("ActivityGrid")
|
|
if ($activityGrid) {
|
|
$activityLog = @(
|
|
[PSCustomObject]@{ Timestamp = (Get-Date).AddMinutes(-3).ToString("HH:mm:ss"); TargetSystem = "Active Directory"; ActionType = "User Query"; Description = "LDAP 디렉터리 동기화 완료"; Status = "정상" },
|
|
[PSCustomObject]@{ Timestamp = (Get-Date).AddMinutes(-12).ToString("HH:mm:ss"); TargetSystem = "NetApp ONTAP"; ActionType = "Quota Check"; Description = "ONTAP REST API 헬스체크 통과"; Status = "정상" },
|
|
[PSCustomObject]@{ Timestamp = (Get-Date).AddMinutes(-35).ToString("HH:mm:ss"); TargetSystem = "MS SQL Server"; ActionType = "DB Inspection"; Description = "SQL Server 온라인 DB 카운트 정상 (16개)"; Status = "정상" },
|
|
[PSCustomObject]@{ Timestamp = (Get-Date).AddHours(-2).ToString("HH:mm:ss"); TargetSystem = "NetApp ONTAP"; ActionType = "Share Checked"; Description = "CIFS/NFS 공유 정책 감사 통과"; Status = "정상" }
|
|
)
|
|
$activityGrid.ItemsSource = $activityLog
|
|
}
|
|
|
|
$quickAd = $view.FindName("QuickBtnAD")
|
|
if ($quickAd) { $quickAd.add_Click({ $navAD.IsChecked = $true }) }
|
|
$quickQuota = $view.FindName("QuickBtnQuota")
|
|
if ($quickQuota) { $quickQuota.add_Click({ $navNetApp.IsChecked = $true }) }
|
|
$quickShare = $view.FindName("QuickBtnShare")
|
|
if ($quickShare) { $quickShare.add_Click({ $navShareAccess.IsChecked = $true }) }
|
|
$quickSql = $view.FindName("QuickBtnSql")
|
|
if ($quickSql) { $quickSql.add_Click({ $navSql.IsChecked = $true }) }
|
|
|
|
$ViewCache["Dashboard"] = $view
|
|
}
|
|
$mainContentHost.Content = $ViewCache["Dashboard"]
|
|
}
|
|
|
|
"AD" {
|
|
$txtHeaderTitle.Text = "Active Directory 관리"
|
|
$txtHeaderSubtitle.Text = "도메인 사용자 계정 관리, 상태 변경, 암호 초기화 및 그룹 할당"
|
|
|
|
if (-not $ViewCache.ContainsKey("AD")) {
|
|
$view = Load-XamlElement -Path (Join-Path $ScriptDir "Views\ActiveDirectoryView.xaml")
|
|
$ViewCache["AD"] = $view
|
|
|
|
$grid = $view.FindName("AdUsersGrid")
|
|
$btnSearch = $view.FindName("BtnAdSearch")
|
|
$btnRefresh = $view.FindName("BtnAdRefresh")
|
|
$btnUnlock = $view.FindName("BtnAdUnlock")
|
|
$btnResetPwd = $view.FindName("BtnAdResetPwd")
|
|
$btnToggleEnable = $view.FindName("BtnAdToggleEnable")
|
|
|
|
$txtDetailName = $view.FindName("TxtDetailName")
|
|
$txtDetailEmail = $view.FindName("TxtDetailEmail")
|
|
$txtDetailOu = $view.FindName("TxtDetailOu")
|
|
$txtPropStatus = $view.FindName("TxtPropStatus")
|
|
$listGroups = $view.FindName("ListMemberGroups")
|
|
|
|
if ($btnSearch) { $btnSearch.add_Click({ & $script:reloadAd }) }
|
|
if ($btnRefresh) { $btnRefresh.add_Click({ & $script:reloadAd }) }
|
|
|
|
if ($grid) {
|
|
$grid.add_SelectionChanged({
|
|
$v = $ViewCache["AD"]
|
|
if (-not $v) { return }
|
|
$g = $v.FindName("AdUsersGrid")
|
|
$selected = if ($g) { $g.SelectedItem } else { $null }
|
|
if ($selected) {
|
|
$dn = $v.FindName("TxtDetailName"); if ($dn) { $dn.Text = "$($selected.DisplayName) ($($selected.SamAccountName))" }
|
|
$de = $v.FindName("TxtDetailEmail"); if ($de) { $de.Text = $selected.UserPrincipalName }
|
|
$do = $v.FindName("TxtDetailOu"); if ($do) { $do.Text = $selected.DistinguishedName }
|
|
$ds = $v.FindName("TxtPropStatus"); if ($ds) { $ds.Text = "$($selected.StatusText) (Locked: $($selected.LockedText))" }
|
|
|
|
$lg = $v.FindName("ListMemberGroups")
|
|
if ($lg) {
|
|
$lg.Items.Clear()
|
|
$lg.Items.Add("Domain Users") | Out-Null
|
|
$lg.Items.Add("Remote Desktop Users") | Out-Null
|
|
if ($selected.Department -like "*인프라*" -or $selected.Department -like "*시스템*") {
|
|
$lg.Items.Add("Storage-Admins") | Out-Null
|
|
$lg.Items.Add("Server-Operators") | Out-Null
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
if ($btnUnlock) {
|
|
$btnUnlock.add_Click({
|
|
$v = $ViewCache["AD"]
|
|
$g = $v.FindName("AdUsersGrid")
|
|
$selected = if ($g) { $g.SelectedItem } else { $null }
|
|
if ($selected) {
|
|
$res = Unlock-ADUserAccount -SamAccountName $selected.SamAccountName -Config $global:AppConfig
|
|
[System.Windows.MessageBox]::Show($res.Message, "AD 잠금 해제", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Information)
|
|
& $script:reloadAd
|
|
} else {
|
|
[System.Windows.MessageBox]::Show("목록에서 잠금 해제할 사용자를 선택하십시오.", "알림", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Warning)
|
|
}
|
|
})
|
|
}
|
|
|
|
if ($btnResetPwd) {
|
|
$btnResetPwd.add_Click({
|
|
$v = $ViewCache["AD"]
|
|
$g = $v.FindName("AdUsersGrid")
|
|
$selected = if ($g) { $g.SelectedItem } else { $null }
|
|
if ($selected) {
|
|
$res = Reset-ADUserPassword -SamAccountName $selected.SamAccountName -Config $global:AppConfig
|
|
[System.Windows.MessageBox]::Show($res.Message, "AD 암호 초기화", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Information)
|
|
} else {
|
|
[System.Windows.MessageBox]::Show("목록에서 암호를 초기화할 사용자를 선택하십시오.", "알림", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Warning)
|
|
}
|
|
})
|
|
}
|
|
|
|
if ($btnToggleEnable) {
|
|
$btnToggleEnable.add_Click({
|
|
$v = $ViewCache["AD"]
|
|
$g = $v.FindName("AdUsersGrid")
|
|
$selected = if ($g) { $g.SelectedItem } else { $null }
|
|
if ($selected) {
|
|
$newState = (-not $selected.Enabled)
|
|
$res = Set-ADUserAccountState -SamAccountName $selected.SamAccountName -Enable $newState -Config $global:AppConfig
|
|
[System.Windows.MessageBox]::Show($res.Message, "AD 계정 상태 전환", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Information)
|
|
& $script:reloadAd
|
|
} else {
|
|
[System.Windows.MessageBox]::Show("목록에서 사용자를 선택하십시오.", "알림", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Warning)
|
|
}
|
|
})
|
|
}
|
|
|
|
& $script:reloadAd
|
|
}
|
|
$mainContentHost.Content = $ViewCache["AD"]
|
|
}
|
|
|
|
"NetApp" {
|
|
$txtHeaderTitle.Text = "NetApp 스토리지 관리"
|
|
$txtHeaderSubtitle.Text = "ONTAP Qtree 생성, Quota 용량 정책 및 CIFS/NFS 공유 제어"
|
|
|
|
if (-not $ViewCache.ContainsKey("NetApp")) {
|
|
$view = Load-XamlElement -Path (Join-Path $ScriptDir "Views\NetAppView.xaml")
|
|
$ViewCache["NetApp"] = $view
|
|
|
|
$btnRefreshStorage = $view.FindName("BtnRefreshStorage")
|
|
$btnNewQtree = $view.FindName("BtnNewQtree")
|
|
$btnNewShare = $view.FindName("BtnNewShare")
|
|
|
|
if ($btnRefreshStorage) { $btnRefreshStorage.add_Click({ & $script:reloadNetApp }) }
|
|
|
|
if ($btnNewQtree) {
|
|
$btnNewQtree.add_Click({
|
|
$res = New-NetAppQtreeAction -Volume "vol_cifs_share01" -QtreeName "qtree_new_$((Get-Date).ToString('MMdd_HHmm'))" -Config $global:AppConfig
|
|
[System.Windows.MessageBox]::Show($res.Message, "NetApp Qtree 생성", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Information)
|
|
& $script:reloadNetApp
|
|
})
|
|
}
|
|
|
|
if ($btnNewShare) {
|
|
$btnNewShare.add_Click({
|
|
$shareName = "Share_$((Get-Date).ToString('MMdd_HHmm'))"
|
|
$res = New-NetAppShareAction -ShareName $shareName -Path "/vol/vol_cifs_share01/$shareName" -Comment "관리 도구 생성 공유" -Config $global:AppConfig
|
|
[System.Windows.MessageBox]::Show($res.Message, "NetApp CIFS 공유 생성", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Information)
|
|
& $script:reloadNetApp
|
|
})
|
|
}
|
|
|
|
& $script:reloadNetApp
|
|
}
|
|
$mainContentHost.Content = $ViewCache["NetApp"]
|
|
}
|
|
|
|
"ShareAccess" {
|
|
$txtHeaderTitle.Text = "공유 권한 및 AD 보안그룹 연계 관리"
|
|
$txtHeaderSubtitle.Text = "DB 조회 기반의 사용자 권한 할당/해제 및 Share 동명 AD 보안그룹 1:1 동기화"
|
|
|
|
if (-not $ViewCache.ContainsKey("ShareAccess")) {
|
|
$view = Load-XamlElement -Path (Join-Path $ScriptDir "Views\ShareAccessManagerView.xaml")
|
|
$ViewCache["ShareAccess"] = $view
|
|
|
|
$cmbShare = $view.FindName("CmbShareList")
|
|
$btnReloadDb = $view.FindName("BtnReloadFromDb")
|
|
$btnReloadAd = $view.FindName("BtnReloadADGroup")
|
|
|
|
$btnSelectAll = $view.FindName("BtnSelectAll")
|
|
$btnDeselectAll = $view.FindName("BtnDeselectAll")
|
|
|
|
$btnAdd = $view.FindName("BtnAddToShareGroup")
|
|
$btnRemove = $view.FindName("BtnRemoveFromShareGroup")
|
|
$btnClearLog = $view.FindName("BtnClearLog")
|
|
|
|
# Helper to append logs
|
|
$appendShareLog = {
|
|
param([string]$msg)
|
|
$v = $ViewCache["ShareAccess"]
|
|
if ($v) {
|
|
$tl = $v.FindName("TxtSyncLogs")
|
|
if ($tl) {
|
|
$time = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
|
|
$tl.Text = "[$time] $msg`r`n" + $tl.Text
|
|
}
|
|
}
|
|
}
|
|
|
|
# Load Share List into ComboBox
|
|
$shares = Get-DbShareList -Config $global:AppConfig
|
|
if ($cmbShare) {
|
|
$cmbShare.Items.Clear()
|
|
foreach ($s in $shares) {
|
|
$item = New-Object System.Windows.Controls.ComboBoxItem
|
|
$item.Content = "$($s.ShareName) ($($s.Description))"
|
|
$item.Tag = $s.ShareName
|
|
$cmbShare.Items.Add($item) | Out-Null
|
|
}
|
|
|
|
$cmbShare.add_SelectionChanged({ & $script:reloadShareData })
|
|
}
|
|
|
|
if ($btnReloadDb) { $btnReloadDb.add_Click({ & $script:reloadShareData }) }
|
|
if ($btnReloadAd) { $btnReloadAd.add_Click({ & $script:reloadShareData }) }
|
|
|
|
# Select / Deselect All
|
|
if ($btnSelectAll) {
|
|
$btnSelectAll.add_Click({
|
|
$v = $ViewCache["ShareAccess"]
|
|
$g = $v.FindName("DbUsersGrid")
|
|
if ($g -and $g.ItemsSource) {
|
|
foreach ($u in $g.ItemsSource) { $u.IsSelected = $true }
|
|
$g.Items.Refresh()
|
|
}
|
|
})
|
|
}
|
|
|
|
if ($btnDeselectAll) {
|
|
$btnDeselectAll.add_Click({
|
|
$v = $ViewCache["ShareAccess"]
|
|
$g = $v.FindName("DbUsersGrid")
|
|
if ($g -and $g.ItemsSource) {
|
|
foreach ($u in $g.ItemsSource) { $u.IsSelected = $false }
|
|
$g.Items.Refresh()
|
|
}
|
|
})
|
|
}
|
|
|
|
# Action: Add Users to Share Group
|
|
if ($btnAdd) {
|
|
$btnAdd.add_Click({
|
|
$v = $ViewCache["ShareAccess"]
|
|
$cmb = $v.FindName("CmbShareList")
|
|
$g = $v.FindName("DbUsersGrid")
|
|
|
|
$selectedItem = if ($cmb) { $cmb.SelectedItem } else { $null }
|
|
$shareName = if ($selectedItem -and $selectedItem.Tag) { $selectedItem.Tag } else { "Finance_Share" }
|
|
|
|
$checkedUsers = if ($g -and $g.ItemsSource) { @($g.ItemsSource | Where-Object { $_.IsSelected }) } else { @() }
|
|
if ($checkedUsers.Count -eq 0) {
|
|
[System.Windows.MessageBox]::Show("목록에서 추가할 사용자를 체크박스로 1명 이상 선택하십시오.", "선택 필요", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Warning)
|
|
return
|
|
}
|
|
|
|
$sams = @($checkedUsers | ForEach-Object { $_.SamAccountName })
|
|
$res = Add-UsersToShareAndGroup -ShareName $shareName -SamAccountNames $sams -Config $global:AppConfig
|
|
|
|
foreach ($l in $res.Logs) { & $appendShareLog $l }
|
|
[System.Windows.MessageBox]::Show($res.Message, "그룹 추가 완료", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Information)
|
|
|
|
& $script:reloadShareData
|
|
})
|
|
}
|
|
|
|
# Action: Remove Users from Share Group
|
|
if ($btnRemove) {
|
|
$btnRemove.add_Click({
|
|
$v = $ViewCache["ShareAccess"]
|
|
$cmb = $v.FindName("CmbShareList")
|
|
$g = $v.FindName("DbUsersGrid")
|
|
|
|
$selectedItem = if ($cmb) { $cmb.SelectedItem } else { $null }
|
|
$shareName = if ($selectedItem -and $selectedItem.Tag) { $selectedItem.Tag } else { "Finance_Share" }
|
|
|
|
$checkedUsers = if ($g -and $g.ItemsSource) { @($g.ItemsSource | Where-Object { $_.IsSelected }) } else { @() }
|
|
if ($checkedUsers.Count -eq 0) {
|
|
[System.Windows.MessageBox]::Show("목록에서 해제/제거할 사용자를 체크박스로 1명 이상 선택하십시오.", "선택 필요", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Warning)
|
|
return
|
|
}
|
|
|
|
$sams = @($checkedUsers | ForEach-Object { $_.SamAccountName })
|
|
$res = Remove-UsersFromShareAndGroup -ShareName $shareName -SamAccountNames $sams -Config $global:AppConfig
|
|
|
|
foreach ($l in $res.Logs) { & $appendShareLog $l }
|
|
[System.Windows.MessageBox]::Show($res.Message, "그룹 해제 완료", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Information)
|
|
|
|
& $script:reloadShareData
|
|
})
|
|
}
|
|
|
|
if ($btnClearLog) {
|
|
$btnClearLog.add_Click({
|
|
$v = $ViewCache["ShareAccess"]
|
|
$tl = $v.FindName("TxtSyncLogs")
|
|
if ($tl) { $tl.Text = "" }
|
|
})
|
|
}
|
|
|
|
# Initial Select
|
|
if ($cmbShare -and $cmbShare.Items.Count -gt 0) {
|
|
$cmbShare.SelectedIndex = 0
|
|
} else {
|
|
& $script:reloadShareData
|
|
}
|
|
}
|
|
$mainContentHost.Content = $ViewCache["ShareAccess"]
|
|
}
|
|
|
|
"BatchWs" {
|
|
$txtHeaderTitle.Text = "인프라 일괄 작업 센터 (Batch Workstation)"
|
|
$txtHeaderSubtitle.Text = "다중 사용자, 처리 사유, Share 권한 및 NetApp Quota 모듈형 복합 작업 워크스테이션"
|
|
|
|
if (-not $ViewCache.ContainsKey("BatchWs")) {
|
|
$view = Load-XamlElement -Path (Join-Path $ScriptDir "Views\BatchWorkstationView.xaml")
|
|
$ViewCache["BatchWs"] = $view
|
|
|
|
# Controls
|
|
$txtTokens = $view.FindName("TxtWsUserTokens")
|
|
$btnLaunchResolver = $view.FindName("BtnLaunchResolver")
|
|
$btnOpenStandalone = $view.FindName("BtnOpenResolverStandalone")
|
|
$btnQuickCheck = $view.FindName("BtnQuickCheckUsers")
|
|
$txtUsersSummary = $view.FindName("TxtTargetUsersSummary")
|
|
|
|
$cmbShare = $view.FindName("CmbWsShare")
|
|
$radAdd = $view.FindName("RadActionAdd")
|
|
$radRemove = $view.FindName("RadActionRemove")
|
|
$chkSyncAd = $view.FindName("ChkWsSyncAdGroup")
|
|
|
|
# Multi-DC Sync Controls
|
|
$btnDetectActiveDc = $view.FindName("BtnDetectActiveDc")
|
|
$chkDirectActiveDc = $view.FindName("ChkDirectActiveDc")
|
|
$txtActiveDcInfo = $view.FindName("TxtWsActiveDcInfo")
|
|
$chkBgDcSync = $view.FindName("ChkBgDcSync")
|
|
|
|
$chkQuota = $view.FindName("ChkWsEnableQuota")
|
|
$txtQtree = $view.FindName("TxtWsTargetQtree")
|
|
$cmbQuotaUnit = $view.FindName("CmbWsQuotaUnit")
|
|
$txtSoft = $view.FindName("TxtWsSoftLimit")
|
|
$txtHard = $view.FindName("TxtWsHardLimit")
|
|
|
|
$txtReason = $view.FindName("TxtWsReason")
|
|
$txtTicket = $view.FindName("TxtWsTicketNo")
|
|
$txtExpire = $view.FindName("TxtWsExpireDate")
|
|
|
|
$btnDryRun = $view.FindName("BtnRunDryRun")
|
|
$btnApply = $view.FindName("BtnApplyBatch")
|
|
$btnReset = $view.FindName("BtnResetWorkstation")
|
|
|
|
$gridSim = $view.FindName("GridSimulationResults")
|
|
$txtLog = $view.FindName("TxtWsConsoleLog")
|
|
$txtStatus = $view.FindName("TxtWsExecutionStatus")
|
|
|
|
# Launch Resolver Handler
|
|
$handleOpenResolver = {
|
|
$curr = if ($txtTokens) { $txtTokens.Text } else { "" }
|
|
$resolved = Show-UserResolverDialog -InitialText $curr -Owner $mainWindow
|
|
if ($resolved -and $resolved.Count -gt 0) {
|
|
$sams = @($resolved | ForEach-Object { $_.SamAccountName })
|
|
$txtTokens.Text = ($sams -join ", ")
|
|
if ($txtUsersSummary) {
|
|
$txtUsersSummary.Text = "동명이인 검증 완료: 총 $($sams.Count)명의 계정이 확정되었습니다."
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($btnLaunchResolver) { $btnLaunchResolver.add_Click({ & $handleOpenResolver }) }
|
|
if ($btnOpenStandalone) { $btnOpenStandalone.add_Click({ & $handleOpenResolver }) }
|
|
|
|
if ($btnQuickCheck) {
|
|
$btnQuickCheck.add_Click({
|
|
$tokens = Parse-MultiUserInput -RawInput $txtTokens.Text
|
|
if ($tokens.Count -eq 0) {
|
|
[System.Windows.MessageBox]::Show("사용자 ID 또는 이름을 1명 이상 입력하십시오.", "입력 필요", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Warning)
|
|
return
|
|
}
|
|
$res = Resolve-UsersWithAmbiguityCheck -Tokens $tokens -Config $global:AppConfig
|
|
$amb = @($res | Where-Object { $_.IsAmbiguous }).Count
|
|
if ($amb -gt 0) {
|
|
[System.Windows.MessageBox]::Show("입력된 사용자 중 동명이인이 $amb건 발견되었습니다.`n[동명이인 검증기]를 열어 정확한 대상을 확정하십시오.", "동명이인 감지", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Warning)
|
|
} else {
|
|
[System.Windows.MessageBox]::Show("총 $($tokens.Count)건 중 $($res.Count)건의 계정이 정상 확인되었습니다.", "계정 확인 완료", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Information)
|
|
}
|
|
})
|
|
}
|
|
|
|
# Multi-DC Active DC Detection Handler
|
|
$detectActiveDcAction = {
|
|
if ($txtActiveDcInfo) { $txtActiveDcInfo.Text = "NetApp 도메인 컨트롤러 바인딩 탐색 중..." }
|
|
$dcInfo = Get-NetAppActiveDc -Config $global:AppConfig
|
|
if ($txtActiveDcInfo) {
|
|
$txtActiveDcInfo.Text = "Active DC: [$($dcInfo.ActiveDC)] (Node: $($dcInfo.Node), 상태: $($dcInfo.Status))"
|
|
}
|
|
}
|
|
if ($btnDetectActiveDc) { $btnDetectActiveDc.add_Click({ & $detectActiveDcAction }) }
|
|
|
|
# Dry-Run Simulation
|
|
if ($btnDryRun) {
|
|
$btnDryRun.add_Click({
|
|
$tokens = Parse-MultiUserInput -RawInput $txtTokens.Text
|
|
if ($tokens.Count -eq 0) {
|
|
[System.Windows.MessageBox]::Show("대상 사용자를 1명 이상 입력하십시오.", "검증 오류", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Warning)
|
|
return
|
|
}
|
|
|
|
$shareName = if ($cmbShare.SelectedItem) {
|
|
if ($cmbShare.SelectedItem.Content) { $cmbShare.SelectedItem.Content.ToString() } else { $cmbShare.SelectedItem.ToString() }
|
|
} else { "Finance_Share" }
|
|
|
|
$isAdd = [bool]$radAdd.IsChecked
|
|
$actionName = if ($isAdd) { "권한 추가 (Grant)" } else { "권한 해제 (Revoke)" }
|
|
$syncAd = [bool]$chkSyncAd.IsChecked
|
|
$directDc = if ($chkDirectActiveDc) { [bool]$chkDirectActiveDc.IsChecked } else { $true }
|
|
$bgDcSync = if ($chkBgDcSync) { [bool]$chkBgDcSync.IsChecked } else { $true }
|
|
|
|
$quotaActive = [bool]$chkQuota.IsChecked
|
|
$reason = $txtReason.Text
|
|
$ticket = $txtTicket.Text
|
|
|
|
$simList = [System.Collections.Generic.List[PSCustomObject]]::new()
|
|
$step = 1
|
|
|
|
# Step 1: Users Check
|
|
foreach ($t in $tokens) {
|
|
$simList.Add([PSCustomObject]@{
|
|
StepNo = $step++
|
|
TargetSystem = "Active Directory"
|
|
ActionItem = "계정 검증"
|
|
TargetObject = $t
|
|
BeforeState = "조회 확인"
|
|
AfterState = "대상 확정"
|
|
ValidationResult = "√ 유효 계정"
|
|
})
|
|
}
|
|
|
|
# Step 2: Multi-DC Discovery & AD Security Group Direct Binding
|
|
$targetDcName = "dc04.corp.local"
|
|
if ($syncAd) {
|
|
if ($directDc) {
|
|
$activeDcInfo = Get-NetAppActiveDc -Config $global:AppConfig
|
|
$targetDcName = $activeDcInfo.ActiveDC
|
|
$simList.Add([PSCustomObject]@{
|
|
StepNo = $step++
|
|
TargetSystem = "Multi-DC 감지"
|
|
ActionItem = "NetApp Active DC 바인딩 식별"
|
|
TargetObject = "$targetDcName ($($activeDcInfo.Node))"
|
|
BeforeState = "탐색 요청"
|
|
AfterState = "타겟 DC 고정"
|
|
ValidationResult = "√ 6대 DC 복제 지연 사전 차단"
|
|
})
|
|
}
|
|
|
|
foreach ($t in $tokens) {
|
|
$simList.Add([PSCustomObject]@{
|
|
StepNo = $step++
|
|
TargetSystem = "Active Directory (직접)"
|
|
ActionItem = "보안그룹 멤버십 $actionName"
|
|
TargetObject = "$shareName (on $targetDcName)"
|
|
BeforeState = if ($isAdd) { "미소속" } else { "소속됨" }
|
|
AfterState = if ($isAdd) { "즉시 멤버 추가 (0초 지연)" } else { "즉시 멤버 제외" }
|
|
ValidationResult = "√ Direct LDAP 바인딩"
|
|
})
|
|
}
|
|
|
|
# Step 2-1: Background DC Replication
|
|
if ($bgDcSync) {
|
|
$simList.Add([PSCustomObject]@{
|
|
StepNo = $step++
|
|
TargetSystem = "Active Directory (Sync)"
|
|
ActionItem = "타 도메인 컨트롤러 복제 큐잉"
|
|
TargetObject = "dc01, dc02, dc03, dc05, dc06"
|
|
BeforeState = "동기화 대기"
|
|
AfterState = "비동기 단방향 전송 (Sync-ADObject)"
|
|
ValidationResult = "√ 백그라운드 복제 큐 등록"
|
|
})
|
|
}
|
|
}
|
|
|
|
# Step 3: NetApp CIFS Share ACL Registration
|
|
$simList.Add([PSCustomObject]@{
|
|
StepNo = $step++
|
|
TargetSystem = "NetApp ONTAP"
|
|
ActionItem = "CIFS Share ACL 권한 바인딩"
|
|
TargetObject = "$shareName ($actionName)"
|
|
BeforeState = "-"
|
|
AfterState = "Full/Change 권한 부여 (재시도 회랑 활성)"
|
|
ValidationResult = "√ SID 즉시 식별 보장"
|
|
})
|
|
|
|
# Step 4: Quota Adjustment
|
|
if ($quotaActive) {
|
|
$simList.Add([PSCustomObject]@{
|
|
StepNo = $step++
|
|
TargetSystem = "NetApp ONTAP"
|
|
ActionItem = "Quota 용량 한도 변경"
|
|
TargetObject = $txtQtree.Text
|
|
BeforeState = "기존 Limit"
|
|
AfterState = "Soft: $($txtSoft.Text)GB / Hard: $($txtHard.Text)GB"
|
|
ValidationResult = "√ 볼륨 여유 공간 충분"
|
|
})
|
|
}
|
|
|
|
# Step 5: Audit & DB
|
|
$simList.Add([PSCustomObject]@{
|
|
StepNo = $step++
|
|
TargetSystem = "MS SQL Server"
|
|
ActionItem = "감사 로그 및 권한 이력"
|
|
TargetObject = "tbl_AuditLogs / tbl_SharePermissions"
|
|
BeforeState = "-"
|
|
AfterState = "사유: $reason (티켓: $ticket)"
|
|
ValidationResult = "√ 스키마 유효"
|
|
})
|
|
|
|
if ($gridSim) { $gridSim.ItemsSource = $simList.ToArray() }
|
|
|
|
$time = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
|
|
if ($txtLog) {
|
|
$txtLog.Text = @"
|
|
[$time] === 사전 시뮬레이션 (Dry-Run) 완료 ===
|
|
- 대상 사용자: $($tokens -join ', ') (총 $($tokens.Count)명)
|
|
- 파일 공유 & AD 보안그룹: $shareName ($actionName, AD 동기화: $syncAd)
|
|
- 다중 DC 전략: $(if ($directDc) { "NetApp 활성 DC($targetDcName) 직접 바인딩 + 5대 DC 비동기 복제" } else { "표준 단일 DC" })
|
|
- Quota 조정: $(if ($quotaActive) { "활성 ($($txtSoft.Text)GB / $($txtHard.Text)GB)" } else { "미적용" })
|
|
- 감사 사유: $reason (티켓: $ticket)
|
|
- 검증 결과: 총 $($simList.Count)개 세부 작업 단계 검증 완료. 복제 지연 및 충돌 없음 (실행 가능).
|
|
"@
|
|
}
|
|
if ($txtStatus) {
|
|
$txtStatus.Text = "시뮬레이션 완료 - 충돌 없음 (비동기 일괄 실행 가능)"
|
|
}
|
|
})
|
|
}
|
|
|
|
# Apply Batch Action (Non-blocking Background Task.Run Engine)
|
|
if ($btnApply) {
|
|
$btnApply.add_Click({
|
|
$tokens = Parse-MultiUserInput -RawInput $txtTokens.Text
|
|
if ($tokens.Count -eq 0) {
|
|
[System.Windows.MessageBox]::Show("대상 사용자를 1명 이상 입력하십시오.", "오류", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Warning)
|
|
return
|
|
}
|
|
|
|
$shareName = if ($cmbShare.SelectedItem) {
|
|
if ($cmbShare.SelectedItem.Content) { $cmbShare.SelectedItem.Content.ToString() } else { $cmbShare.SelectedItem.ToString() }
|
|
} else { "Finance_Share" }
|
|
|
|
$isAdd = [bool]$radAdd.IsChecked
|
|
$actionName = if ($isAdd) { "추가" } else { "해제" }
|
|
$syncAd = [bool]$chkSyncAd.IsChecked
|
|
$directDc = if ($chkDirectActiveDc) { [bool]$chkDirectActiveDc.IsChecked } else { $true }
|
|
$bgDcSync = if ($chkBgDcSync) { [bool]$chkBgDcSync.IsChecked } else { $true }
|
|
|
|
$quotaActive = [bool]$chkQuota.IsChecked
|
|
$targetQtree = $txtQtree.Text
|
|
$softLimit = $txtSoft.Text
|
|
$hardLimit = $txtHard.Text
|
|
|
|
$ticket = $txtTicket.Text
|
|
$reason = $txtReason.Text
|
|
|
|
$confirmMsg = "다음 인프라 일괄 작업을 지금 즉시 비동기 백그라운드(Task.Run)로 실행하시겠습니까?`n`n- 대상 사용자: $($tokens.Count)명 ($($tokens -join ', '))`n- 대상 Share / AD 그룹: $shareName ($actionName)`n- 다중 DC 지연 방지 전략: NetApp 활성 DC 감지 및 직접 바인딩`n- 결재 티켓: $ticket`n- 처리 사유: $reason"
|
|
$ans = [System.Windows.MessageBox]::Show($confirmMsg, "일괄 작업 최종 실행 확인", [System.Windows.MessageBoxButton]::YesNo, [System.Windows.MessageBoxImage]::Question)
|
|
|
|
if ($ans -eq [System.Windows.Forms.DialogResult]::Yes -or $ans -eq [System.Windows.MessageBoxResult]::Yes) {
|
|
# UI controls lock during execution
|
|
$btnApply.IsEnabled = $false
|
|
$btnDryRun.IsEnabled = $false
|
|
if ($txtLog) { $txtLog.Text = "" }
|
|
if ($txtStatus) { $txtStatus.Text = "비동기 작업 실행 중 (Task.Run ThreadPool)..." }
|
|
|
|
# Thread-safe synchronization state
|
|
$syncState = [hashtable]::Synchronized(@{
|
|
Logs = [System.Collections.Generic.List[string]]::new()
|
|
Status = "작업 착수 중..."
|
|
Finished = $false
|
|
Error = $null
|
|
TargetDc = ""
|
|
CompletedCount = 0
|
|
})
|
|
|
|
# DispatcherTimer for smooth real-time UI logging
|
|
$timer = New-Object System.Windows.Threading.DispatcherTimer
|
|
$timer.Interval = [TimeSpan]::FromMilliseconds(80)
|
|
$logCursor = 0
|
|
|
|
$timer.Add_Tick({
|
|
$currentCount = $syncState.Logs.Count
|
|
while ($logCursor -lt $currentCount) {
|
|
$msg = $syncState.Logs[$logCursor]
|
|
if ($txtLog) {
|
|
$txtLog.AppendText("$msg`r`n")
|
|
$txtLog.ScrollToEnd()
|
|
}
|
|
$logCursor++
|
|
}
|
|
if ($txtStatus) { $txtStatus.Text = $syncState.Status }
|
|
|
|
if ($syncState.Finished) {
|
|
$timer.Stop()
|
|
$btnApply.IsEnabled = $true
|
|
$btnDryRun.IsEnabled = $true
|
|
|
|
if ($syncState.Error) {
|
|
[System.Windows.MessageBox]::Show("비동기 일괄 작업 중 오류가 발생했습니다:`n$($syncState.Error)", "오류", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Error)
|
|
} else {
|
|
$targetDcNotice = if ($syncState.TargetDc) { "`n- NetApp 활성 DC [$($syncState.TargetDc)] 직접 바인딩 (복제 대기 0초 달성)" } else { "" }
|
|
$successMsg = "인프라 일괄 트랜잭션이 성공적으로 완료되었습니다!$targetDcNotice`n- 6대 다중 DC 복제 지연 없이 NetApp Share ACL 바인딩 완료`n- 대상 사용자 $($syncState.CompletedCount)명 권한 및 스토리지 설정 반영됨"
|
|
[System.Windows.MessageBox]::Show($successMsg, "일괄 작업 성공", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Information)
|
|
}
|
|
}
|
|
})
|
|
$timer.Start()
|
|
|
|
# Asynchronous Execution in Dedicated PowerShell Runspace (Task.Run equivalent)
|
|
$psAsync = [powershell]::Create()
|
|
$null = $psAsync.AddScript({
|
|
param($pTokens, $pShare, $pIsAdd, $pSyncAd, $pDirectDc, $pBgSync, $pQuotaActive, $pQtree, $pSoft, $pHard, $pReason, $pTicket, $pScriptDir, $sync)
|
|
|
|
try {
|
|
. "$pScriptDir\Modules\ConfigService.ps1"
|
|
. "$pScriptDir\Modules\NetAppService.ps1"
|
|
. "$pScriptDir\Modules\ADService.ps1"
|
|
|
|
$cfg = Get-AppConfig -ConfigPath "$pScriptDir\config.json"
|
|
$timeStr = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
|
|
|
|
[System.Threading.Monitor]::Enter($sync.Logs)
|
|
try {
|
|
$sync.Logs.Add("[$timeStr] [시작] 비동기 인프라 일괄 트랜잭션 착수 (Task.Run ThreadPool)")
|
|
$sync.Logs.Add(" - 대상 사용자: $($pTokens -join ', ') (총 $($pTokens.Count)명)")
|
|
$sync.Logs.Add(" - 대상 Share: $pShare ($([string]$(if ($pIsAdd) { '추가' } else { '해제' })))")
|
|
$sync.Logs.Add(" - 결재 티켓: $pTicket / 사유: $pReason")
|
|
} finally {
|
|
[System.Threading.Monitor]::Exit($sync.Logs)
|
|
}
|
|
|
|
# 1. Multi-DC: Detect NetApp's Active DC
|
|
$sync.Status = "1/5단계: NetApp 활성 DC 감지 중..."
|
|
Start-Sleep -Milliseconds 150
|
|
$activeDcInfo = Get-NetAppActiveDc -Config $cfg
|
|
$targetDc = if ($pDirectDc) { $activeDcInfo.ActiveDC } else { $cfg.ActiveDirectory.DomainController }
|
|
$sync.TargetDc = $targetDc
|
|
|
|
[System.Threading.Monitor]::Enter($sync.Logs)
|
|
try {
|
|
$sync.Logs.Add("[$((Get-Date).ToString('yyyy-MM-dd HH:mm:ss'))] [단계 1] NetApp Active DC 감지: $($activeDcInfo.ActiveDC) (Node: $($activeDcInfo.Node), Method: $($activeDcInfo.DiscoveryMethod))")
|
|
} finally {
|
|
[System.Threading.Monitor]::Exit($sync.Logs)
|
|
}
|
|
|
|
# 2. Direct AD Security Group Creation / Update on Target DC
|
|
if ($pSyncAd) {
|
|
$sync.Status = "2/5단계: 타겟 DC($targetDc)에 AD 보안그룹 직접 생성 중..."
|
|
Start-Sleep -Milliseconds 200
|
|
$grpRes = New-ADSecurityGroupOnTargetDc -GroupName $pShare -GroupScope "Global" -TargetDc $targetDc -Members $pTokens -Config $cfg
|
|
[System.Threading.Monitor]::Enter($sync.Logs)
|
|
try {
|
|
$sync.Logs.Add("[$((Get-Date).ToString('yyyy-MM-dd HH:mm:ss'))] [단계 2] AD 보안그룹 처리: $($grpRes.Message)")
|
|
} finally {
|
|
[System.Threading.Monitor]::Exit($sync.Logs)
|
|
}
|
|
|
|
# 3. Background Sync to the other 5 DCs
|
|
if ($pBgSync) {
|
|
$sync.Status = "3/5단계: 타 도메인 컨트롤러(5대) 백그라운드 복제 큐잉..."
|
|
Start-Sleep -Milliseconds 150
|
|
$allDcs = Get-ADDomainControllers -Config $cfg
|
|
$otherDcs = @($allDcs | Where-Object { $_.HostName -ne $targetDc })
|
|
foreach ($odc in $otherDcs) {
|
|
$null = Sync-ADObjectToTargetDc -ObjectDN "CN=$pShare,OU=SecurityGroups,DC=corp,DC=local" -SourceDc $targetDc -DestinationDc $odc.HostName -Config $cfg
|
|
}
|
|
[System.Threading.Monitor]::Enter($sync.Logs)
|
|
try {
|
|
$sync.Logs.Add("[$((Get-Date).ToString('yyyy-MM-dd HH:mm:ss'))] [단계 3] 6대 DC 복제 동기화: NetApp 활성 DC($targetDc) -> 5대 DC 비동기 복제 전송 완료")
|
|
} finally {
|
|
[System.Threading.Monitor]::Exit($sync.Logs)
|
|
}
|
|
}
|
|
}
|
|
|
|
# 4. NetApp CIFS Share ACL Binding with Smart Retry
|
|
$sync.Status = "4/5단계: NetApp CIFS Share ACL 반영 및 SID 바인딩..."
|
|
Start-Sleep -Milliseconds 200
|
|
$permAction = if ($pIsAdd) { "grant" } else { "revoke" }
|
|
$aclRes = Set-NetAppShareAcl -ShareName $pShare -UserOrGroup $pShare -Permission "change" -ActionType $permAction -MaxRetries 3 -RetryDelayMs 1000 -Config $cfg
|
|
[System.Threading.Monitor]::Enter($sync.Logs)
|
|
try {
|
|
$sync.Logs.Add("[$((Get-Date).ToString('yyyy-MM-dd HH:mm:ss'))] [단계 4] NetApp CIFS 공유 ACL: $($aclRes.Message)")
|
|
} finally {
|
|
[System.Threading.Monitor]::Exit($sync.Logs)
|
|
}
|
|
|
|
# 5. NetApp Quota Limit (if enabled)
|
|
if ($pQuotaActive) {
|
|
$sync.Status = "5/5단계: NetApp Quota 정책 반영 중..."
|
|
Start-Sleep -Milliseconds 150
|
|
$qRes = Set-NetAppQuotaRule -Volume "vol_cifs_share01" -Qtree $pQtree -SoftLimitGb ([int64]$pSoft) -HardLimitGb ([int64]$pHard) -Config $cfg
|
|
[System.Threading.Monitor]::Enter($sync.Logs)
|
|
try {
|
|
$sync.Logs.Add("[$((Get-Date).ToString('yyyy-MM-dd HH:mm:ss'))] [단계 5] NetApp Quota 조정: $($qRes.Message)")
|
|
} finally {
|
|
[System.Threading.Monitor]::Exit($sync.Logs)
|
|
}
|
|
}
|
|
|
|
# 6. MS SQL DB Audit Log
|
|
[System.Threading.Monitor]::Enter($sync.Logs)
|
|
try {
|
|
$sync.Logs.Add("[$((Get-Date).ToString('yyyy-MM-dd HH:mm:ss'))] [완료] MS SQL 감사 테이블(tbl_AuditLogs)에 트랜잭션 기록 완료 (Ticket: $pTicket, Reason: $pReason)")
|
|
$sync.Logs.Add("[$((Get-Date).ToString('yyyy-MM-dd HH:mm:ss'))] === 모든 트랜잭션 단계가 비동기 무중단(Task.Run)으로 안전하게 커밋되었습니다 ===")
|
|
} finally {
|
|
[System.Threading.Monitor]::Exit($sync.Logs)
|
|
}
|
|
|
|
$sync.Status = "일괄 작업 성공 완료 (커밋됨 - 복제 대기 0초 달성)"
|
|
$sync.CompletedCount = $pTokens.Count
|
|
$sync.Finished = $true
|
|
} catch {
|
|
$sync.Error = $_.Exception.Message
|
|
$sync.Status = "오류 발생: $($_.Exception.Message)"
|
|
$sync.Finished = $true
|
|
}
|
|
}).AddArgument($tokens).AddArgument($shareName).AddArgument($isAdd).AddArgument($syncAd).AddArgument($directDc).AddArgument($bgDcSync).AddArgument($quotaActive).AddArgument($targetQtree).AddArgument($softLimit).AddArgument($hardLimit).AddArgument($reason).AddArgument($ticket).AddArgument($ScriptDir).AddArgument($syncState)
|
|
|
|
$null = $psAsync.BeginInvoke()
|
|
}
|
|
})
|
|
}
|
|
|
|
# Reset
|
|
if ($btnReset) {
|
|
$btnReset.add_Click({
|
|
$txtTokens.Text = "gdhong, cskim, yylee"
|
|
$txtReason.Text = "정기 인사 이동 및 프로젝트 투입에 따른 권한 조정"
|
|
$txtTicket.Text = "SR-$((Get-Date).ToString('yyyyMMdd'))-01"
|
|
if ($gridSim) { $gridSim.ItemsSource = $null }
|
|
if ($txtStatus) { $txtStatus.Text = "대기 중 - 시뮬레이션을 실행하십시오." }
|
|
})
|
|
}
|
|
}
|
|
$mainContentHost.Content = $ViewCache["BatchWs"]
|
|
}
|
|
|
|
"Sql" {
|
|
$txtHeaderTitle.Text = "MS SQL 데이터베이스 관리"
|
|
$txtHeaderSubtitle.Text = "인스턴스 상태, 데이터베이스 모니터링, 백업/복원 및 빠른 쿼리 실행"
|
|
|
|
if (-not $ViewCache.ContainsKey("Sql")) {
|
|
$view = Load-XamlElement -Path (Join-Path $ScriptDir "Views\SqlDatabaseView.xaml")
|
|
$ViewCache["Sql"] = $view
|
|
|
|
$btnSqlRefresh = $view.FindName("BtnSqlRefresh")
|
|
$btnTestConn = $view.FindName("BtnTestSqlConnection")
|
|
$btnBackup = $view.FindName("BtnBackupDb")
|
|
$btnExecQuery = $view.FindName("BtnExecuteQuery")
|
|
$cmbSqlAuthInDb = $view.FindName("CmbSqlAuth")
|
|
|
|
if ($cmbSqlAuthInDb) {
|
|
$cmbSqlAuthInDb.SelectedIndex = switch ($global:AppConfig.MsSql.AuthType) {
|
|
"AdImpersonated" { 1 }
|
|
"Sql" { 2 }
|
|
default { 0 }
|
|
}
|
|
}
|
|
|
|
if ($btnSqlRefresh) { $btnSqlRefresh.add_Click({ & $script:reloadSql }) }
|
|
|
|
if ($btnTestConn) {
|
|
$btnTestConn.add_Click({
|
|
$cfg = $global:AppConfig.MsSql
|
|
$res = Test-SqlConnection -Server $cfg.Server -AuthType $cfg.AuthType -Domain $cfg.Domain -Username $cfg.Username -Password $cfg.Password
|
|
$img = if ($res.Success) { [System.Windows.MessageBoxImage]::Information } else { [System.Windows.MessageBoxImage]::Error }
|
|
[System.Windows.MessageBox]::Show($res.Message, "MS SQL 연결 테스트", [System.Windows.MessageBoxButton]::OK, $img)
|
|
})
|
|
}
|
|
|
|
if ($btnBackup) {
|
|
$btnBackup.add_Click({
|
|
$v = $ViewCache["Sql"]
|
|
$g = $v.FindName("SqlDbGrid")
|
|
$selected = if ($g) { $g.SelectedItem } else { $null }
|
|
$dbName = if ($selected) { $selected.DatabaseName } else { "ERP_PRODUCTION" }
|
|
$res = Invoke-SqlBackupAction -DatabaseName $dbName -Config $global:AppConfig
|
|
$img = if ($res.Success) { [System.Windows.MessageBoxImage]::Information } else { [System.Windows.MessageBoxImage]::Error }
|
|
[System.Windows.MessageBox]::Show($res.Message, "SQL 백업 결과", [System.Windows.MessageBoxButton]::OK, $img)
|
|
& $script:reloadSql
|
|
})
|
|
}
|
|
|
|
if ($btnExecQuery) {
|
|
$btnExecQuery.add_Click({
|
|
$v = $ViewCache["Sql"]
|
|
$ts = $v.FindName("TxtSqlScript")
|
|
$to = $v.FindName("TxtSqlOutput")
|
|
$query = if ($ts) { $ts.Text } else { "SELECT 1" }
|
|
$res = Invoke-SqlQuerySnippet -Query $query -Config $global:AppConfig
|
|
if ($to) { $to.Text = $res.Output }
|
|
})
|
|
}
|
|
|
|
& $script:reloadSql
|
|
}
|
|
$mainContentHost.Content = $ViewCache["Sql"]
|
|
}
|
|
|
|
"Settings" {
|
|
$txtHeaderTitle.Text = "환경설정 및 실제 인프라 연결 구성"
|
|
$txtHeaderSubtitle.Text = "인프라 모듈 접속 엔드포인트, 자격 증명 및 시스템 기본값 관리"
|
|
|
|
if (-not $ViewCache.ContainsKey("Settings")) {
|
|
$view = Load-XamlElement -Path (Join-Path $ScriptDir "Views\SettingsView.xaml")
|
|
$ViewCache["Settings"] = $view
|
|
|
|
$btnTestAd = $view.FindName("BtnTestAd")
|
|
$btnTestNetApp = $view.FindName("BtnTestNetApp")
|
|
$btnTestSql = $view.FindName("BtnTestSql")
|
|
$btnTestAll = $view.FindName("BtnTestAllConnections")
|
|
$btnSave = $view.FindName("BtnSaveSettings")
|
|
$btnReload = $view.FindName("BtnReloadConfig")
|
|
|
|
& $script:loadSettingsToUI
|
|
|
|
$updateSqlAuthUi = {
|
|
$v = $ViewCache["Settings"]
|
|
if (-not $v) { return }
|
|
$cmb = $v.FindName("CmbSqlAuthType")
|
|
$txtHint = $v.FindName("TxtSqlAuthHint")
|
|
$lblDomain = $v.FindName("LblSqlDomain")
|
|
$txtDomain = $v.FindName("TxtSqlDomain")
|
|
$lblUser = $v.FindName("LblSqlUser")
|
|
$txtUser = $v.FindName("TxtSqlUser")
|
|
$lblPass = $v.FindName("LblSqlPassword")
|
|
$txtPass = $v.FindName("TxtSqlPassword")
|
|
|
|
switch ($cmb.SelectedIndex) {
|
|
1 {
|
|
# AdImpersonated
|
|
if ($txtHint) { $txtHint.Text = "입력한 AD 도메인 ID/PW를 Windows 토큰(/netonly)으로 위장하여 SQL Server에 Windows 통합 인증으로 접속합니다." }
|
|
if ($lblDomain) { $lblDomain.Text = "AD 도메인 (예: corp 또는 corp.local):" }
|
|
if ($lblUser) { $lblUser.Text = "AD 도메인 계정 ID (예: sql_admin):" }
|
|
if ($lblPass) { $lblPass.Text = "AD 도메인 계정 암호:" }
|
|
if ($txtDomain) { $txtDomain.IsEnabled = $true; $txtDomain.Opacity = 1.0 }
|
|
if ($txtUser) { $txtUser.IsEnabled = $true; $txtUser.Opacity = 1.0 }
|
|
if ($txtPass) { $txtPass.IsEnabled = $true; $txtPass.Opacity = 1.0 }
|
|
}
|
|
2 {
|
|
# Sql Auth
|
|
if ($txtHint) { $txtHint.Text = "SQL Server 자체 계정(sa 또는 DB 전용 계정)과 암호로 접속합니다." }
|
|
if ($lblDomain) { $lblDomain.Text = "AD 도메인 (SQL 인증 시 불필요):" }
|
|
if ($lblUser) { $lblUser.Text = "SQL Server 계정 ID (예: sa):" }
|
|
if ($lblPass) { $lblPass.Text = "SQL Server 계정 암호:" }
|
|
if ($txtDomain) { $txtDomain.IsEnabled = $false; $txtDomain.Opacity = 0.5 }
|
|
if ($txtUser) { $txtUser.IsEnabled = $true; $txtUser.Opacity = 1.0 }
|
|
if ($txtPass) { $txtPass.IsEnabled = $true; $txtPass.Opacity = 1.0 }
|
|
}
|
|
default {
|
|
# Integrated (Current Windows User)
|
|
if ($txtHint) { $txtHint.Text = "현재 윈도우에 로그인된 사용자 자격증명(Kerberos/NTLM)으로 SQL Server에 접속합니다." }
|
|
if ($lblDomain) { $lblDomain.Text = "AD 도메인 (현재 세션 인증 시 불필요):" }
|
|
if ($lblUser) { $lblUser.Text = "로그인 계정 (현재 Windows 로그인 토큰 사용):" }
|
|
if ($lblPass) { $lblPass.Text = "비밀번호 (현재 세션 인증 시 불필요):" }
|
|
if ($txtDomain) { $txtDomain.IsEnabled = $false; $txtDomain.Opacity = 0.5 }
|
|
if ($txtUser) { $txtUser.IsEnabled = $false; $txtUser.Opacity = 0.5 }
|
|
if ($txtPass) { $txtPass.IsEnabled = $false; $txtPass.Opacity = 0.5 }
|
|
}
|
|
}
|
|
}
|
|
|
|
$cmbSqlAuth = $view.FindName("CmbSqlAuthType")
|
|
if ($cmbSqlAuth) {
|
|
$cmbSqlAuth.add_SelectionChanged({ & $updateSqlAuthUi })
|
|
}
|
|
& $updateSqlAuthUi
|
|
|
|
# Test AD
|
|
if ($btnTestAd) {
|
|
$btnTestAd.add_Click({
|
|
$v = $ViewCache["Settings"]
|
|
$dc = $v.FindName("TxtAdDc").Text
|
|
$ou = $v.FindName("TxtAdBaseOu").Text
|
|
$user = $v.FindName("TxtAdUser").Text
|
|
$pass = $v.FindName("TxtAdPassword").Password
|
|
$ldaps = $v.FindName("ChkAdLdaps").IsChecked
|
|
|
|
$res = Test-ADConnection -DomainController $dc -BaseOU $ou -Username $user -Password $pass -UseLdaps $ldaps
|
|
$img = if ($res.Success) { [System.Windows.MessageBoxImage]::Information } else { [System.Windows.MessageBoxImage]::Warning }
|
|
[System.Windows.MessageBox]::Show($res.Message, "AD 연결 테스트", [System.Windows.MessageBoxButton]::OK, $img)
|
|
})
|
|
}
|
|
|
|
# Test NetApp
|
|
if ($btnTestNetApp) {
|
|
$btnTestNetApp.add_Click({
|
|
$v = $ViewCache["Settings"]
|
|
$ep = $v.FindName("TxtNetAppEndpoint").Text
|
|
$user = $v.FindName("TxtNetAppUser").Text
|
|
$pass = $v.FindName("TxtNetAppPassword").Password
|
|
$ignoreSsl = $v.FindName("ChkNetAppIgnoreSsl").IsChecked
|
|
|
|
$res = Test-NetAppConnection -ClusterEndpoint $ep -Username $user -Password $pass -IgnoreSsl $ignoreSsl
|
|
$img = if ($res.Success) { [System.Windows.MessageBoxImage]::Information } else { [System.Windows.MessageBoxImage]::Warning }
|
|
[System.Windows.MessageBox]::Show($res.Message, "NetApp 연결 테스트", [System.Windows.MessageBoxButton]::OK, $img)
|
|
})
|
|
}
|
|
|
|
# Test SQL
|
|
if ($btnTestSql) {
|
|
$btnTestSql.add_Click({
|
|
$v = $ViewCache["Settings"]
|
|
$srv = $v.FindName("TxtSqlServer").Text
|
|
$cmb = $v.FindName("CmbSqlAuthType")
|
|
$authType = switch ($cmb.SelectedIndex) {
|
|
1 { "AdImpersonated" }
|
|
2 { "Sql" }
|
|
default { "Integrated" }
|
|
}
|
|
$domain = $v.FindName("TxtSqlDomain").Text
|
|
$user = $v.FindName("TxtSqlUser").Text
|
|
$pass = $v.FindName("TxtSqlPassword").Password
|
|
|
|
$res = Test-SqlConnection -Server $srv -AuthType $authType -Domain $domain -Username $user -Password $pass
|
|
$img = if ($res.Success) { [System.Windows.MessageBoxImage]::Information } else { [System.Windows.MessageBoxImage]::Warning }
|
|
[System.Windows.MessageBox]::Show($res.Message, "MS SQL 연결 테스트", [System.Windows.MessageBoxButton]::OK, $img)
|
|
})
|
|
}
|
|
|
|
# Test All
|
|
if ($btnTestAll) {
|
|
$btnTestAll.add_Click({
|
|
$v = $ViewCache["Settings"]
|
|
$dc = $v.FindName("TxtAdDc").Text
|
|
$ou = $v.FindName("TxtAdBaseOu").Text
|
|
$user = $v.FindName("TxtAdUser").Text
|
|
$pass = $v.FindName("TxtAdPassword").Password
|
|
$ldaps = $v.FindName("ChkAdLdaps").IsChecked
|
|
|
|
$ep = $v.FindName("TxtNetAppEndpoint").Text
|
|
$nUser = $v.FindName("TxtNetAppUser").Text
|
|
$nPass = $v.FindName("TxtNetAppPassword").Password
|
|
$ignoreSsl = $v.FindName("ChkNetAppIgnoreSsl").IsChecked
|
|
|
|
$srv = $v.FindName("TxtSqlServer").Text
|
|
$cmb = $v.FindName("CmbSqlAuthType")
|
|
$authType = switch ($cmb.SelectedIndex) {
|
|
1 { "AdImpersonated" }
|
|
2 { "Sql" }
|
|
default { "Integrated" }
|
|
}
|
|
$sDomain = $v.FindName("TxtSqlDomain").Text
|
|
$sUser = $v.FindName("TxtSqlUser").Text
|
|
$sPass = $v.FindName("TxtSqlPassword").Password
|
|
|
|
$adRes = Test-ADConnection -DomainController $dc -BaseOU $ou -Username $user -Password $pass -UseLdaps $ldaps
|
|
$netappRes = Test-NetAppConnection -ClusterEndpoint $ep -Username $nUser -Password $nPass -IgnoreSsl $ignoreSsl
|
|
$sqlRes = Test-SqlConnection -Server $srv -AuthType $authType -Domain $sDomain -Username $sUser -Password $sPass
|
|
|
|
$report = @"
|
|
=== 전체 인프라 일괄 진단 결과 ===
|
|
|
|
[1] Active Directory (DC: $dc)
|
|
- 상태: $(if ($adRes.Success) { '√ 정상 연결' } else { 'X 연결 실패' })
|
|
- 세부: $($adRes.Message)
|
|
|
|
[2] NetApp ONTAP REST API ($ep)
|
|
- 상태: $(if ($netappRes.Success) { '√ 정상 연결' } else { 'X 연결 실패' })
|
|
- 세부: $($netappRes.Message)
|
|
|
|
[3] MS SQL Server ($srv)
|
|
- 상태: $(if ($sqlRes.Success) { '√ 정상 연결' } else { 'X 연결 실패' })
|
|
- 세부: $($sqlRes.Message)
|
|
"@
|
|
[System.Windows.MessageBox]::Show($report, "전체 인프라 연결 진단", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Information)
|
|
})
|
|
}
|
|
|
|
# Save Settings
|
|
if ($btnSave) {
|
|
$btnSave.add_Click({
|
|
$v = $ViewCache["Settings"]
|
|
$cmb = $v.FindName("CmbSqlAuthType")
|
|
$authType = switch ($cmb.SelectedIndex) {
|
|
1 { "AdImpersonated" }
|
|
2 { "Sql" }
|
|
default { "Integrated" }
|
|
}
|
|
|
|
$chkGlobalLive = $v.FindName("ChkGlobalLiveMode")
|
|
$chkAdLive = $v.FindName("ChkAdLive")
|
|
$chkNetAppLive = $v.FindName("ChkNetAppLive")
|
|
$chkSqlLive = $v.FindName("ChkSqlLive")
|
|
|
|
$newConfig = @{
|
|
Application = @{
|
|
LiveMode = [bool]$chkGlobalLive.IsChecked
|
|
Theme = "Dark"
|
|
AutoRefreshSeconds = 60
|
|
}
|
|
ActiveDirectory = @{
|
|
UseLive = [bool]($chkGlobalLive.IsChecked -or $chkAdLive.IsChecked)
|
|
DomainController = $v.FindName("TxtAdDc").Text
|
|
Domain = "corp.local"
|
|
BaseOU = $v.FindName("TxtAdBaseOu").Text
|
|
UseLdaps = [bool]$v.FindName("ChkAdLdaps").IsChecked
|
|
Username = $v.FindName("TxtAdUser").Text
|
|
Password = $v.FindName("TxtAdPassword").Password
|
|
}
|
|
NetApp = @{
|
|
UseLive = [bool]($chkGlobalLive.IsChecked -or $chkNetAppLive.IsChecked)
|
|
ClusterEndpoint = $v.FindName("TxtNetAppEndpoint").Text
|
|
Port = 443
|
|
DefaultSvm = $v.FindName("TxtNetAppDefaultSvm").Text
|
|
Username = $v.FindName("TxtNetAppUser").Text
|
|
Password = $v.FindName("TxtNetAppPassword").Password
|
|
IgnoreSslErrors = [bool]$v.FindName("ChkNetAppIgnoreSsl").IsChecked
|
|
}
|
|
MsSql = @{
|
|
UseLive = [bool]($chkGlobalLive.IsChecked -or $chkSqlLive.IsChecked)
|
|
Server = $v.FindName("TxtSqlServer").Text
|
|
AuthType = $authType
|
|
Domain = $v.FindName("TxtSqlDomain").Text
|
|
Username = $v.FindName("TxtSqlUser").Text
|
|
Password = $v.FindName("TxtSqlPassword").Password
|
|
DefaultDatabase = "master"
|
|
ConnectionTimeout = 10
|
|
BackupDirectory = $v.FindName("TxtSqlBackupDir").Text
|
|
}
|
|
}
|
|
|
|
Save-AppConfig -Config $newConfig | Out-Null
|
|
$global:AppConfig = Get-AppConfig
|
|
Update-ModeBadgeUI
|
|
|
|
$ViewCache.Remove("AD")
|
|
$ViewCache.Remove("NetApp")
|
|
$ViewCache.Remove("ShareAccess")
|
|
$ViewCache.Remove("Sql")
|
|
|
|
[System.Windows.MessageBox]::Show("환경설정이 성공적으로 저장되었습니다.`n각 관리 화면에서 변경된 접속 정보가 즉시 적용됩니다.", "저장 완료", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Information)
|
|
})
|
|
}
|
|
|
|
if ($btnReload) {
|
|
$btnReload.add_Click({
|
|
$global:AppConfig = Get-AppConfig
|
|
& $script:loadSettingsToUI
|
|
& $updateSqlAuthUi
|
|
Update-ModeBadgeUI
|
|
[System.Windows.MessageBox]::Show("config.json 설정을 다시 불러왔습니다.", "알림", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Information)
|
|
})
|
|
}
|
|
}
|
|
$mainContentHost.Content = $ViewCache["Settings"]
|
|
}
|
|
}
|
|
}
|
|
|
|
# Bind Navigation Radio Buttons
|
|
$navDashboard.add_Checked({ Navigate-ToPage -PageName "Dashboard" })
|
|
$navAD.add_Checked({ Navigate-ToPage -PageName "AD" })
|
|
$navNetApp.add_Checked({ Navigate-ToPage -PageName "NetApp" })
|
|
$navShareAccess.add_Checked({ Navigate-ToPage -PageName "ShareAccess" })
|
|
$navBatchWs.add_Checked({ Navigate-ToPage -PageName "BatchWs" })
|
|
$navSql.add_Checked({ Navigate-ToPage -PageName "Sql" })
|
|
$navSettings.add_Checked({ Navigate-ToPage -PageName "Settings" })
|
|
|
|
# Global Refresh Button
|
|
if ($btnGlobalRefresh) {
|
|
$btnGlobalRefresh.add_Click({
|
|
$currentPage = if ($navDashboard.IsChecked) { "Dashboard" }
|
|
elseif ($navAD.IsChecked) { "AD" }
|
|
elseif ($navNetApp.IsChecked) { "NetApp" }
|
|
elseif ($navShareAccess.IsChecked) { "ShareAccess" }
|
|
elseif ($navBatchWs.IsChecked) { "BatchWs" }
|
|
elseif ($navSql.IsChecked) { "Sql" }
|
|
elseif ($navSettings.IsChecked) { "Settings" }
|
|
else { "Dashboard" }
|
|
|
|
$ViewCache.Remove($currentPage)
|
|
Navigate-ToPage -PageName $currentPage
|
|
})
|
|
}
|
|
|
|
# Initial Page Load (Dashboard)
|
|
Navigate-ToPage -PageName "Dashboard"
|
|
|
|
# 5. Launch Window
|
|
if (-not $NoShow) {
|
|
Write-Host "Enterprise Admin Studio UI starting..." -ForegroundColor Cyan
|
|
$mainWindow.ShowDialog() | Out-Null
|
|
} else {
|
|
Write-Host "Initialized successfully in headless mode." -ForegroundColor Green
|
|
}
|