SQL Server database files and backup files exist on the same volume
http://go.microsoft.com/fwlink/?LinkId=199790
Target | Microsoft.KnowledgeServices.SQLServer.Database | ||
Parent Monitor | System.Health.ConfigurationState | ||
Category | Alert | ||
Enabled | True | ||
Alert Generate | True | ||
Alert Severity | Warning | ||
Alert Priority | Normal | ||
Alert Auto Resolve | True | ||
Monitor Type | Microsoft.KnowledgeServices.Library.PowerShellMonitorEx | ||
Remotable | True | ||
Accessibility | Public | ||
Alert Message |
| ||
RunAs | Default | ||
Comment | SupportTopic=TBD;VersionNumber=1.0.1.0; |
<UnitMonitor ID="Monitor_AK85906" Comment="SupportTopic=TBD;VersionNumber=1.0.1.0;" Accessibility="Public" Enabled="true" Target="KnowledgeServicesSQLServerLibrary!Microsoft.KnowledgeServices.SQLServer.Database" ParentMonitorID="Health!System.Health.ConfigurationState" Remotable="true" Priority="Normal" TypeID="KnowledgeServicesLibrary!Microsoft.KnowledgeServices.Library.PowerShellMonitorEx" ConfirmDelivery="true">
<Category>Alert</Category>
<AlertSettings AlertMessage="MonitorMessage1bb5c182717b4cb5afa53ee328c10fbc">
<AlertOnState>Error</AlertOnState>
<AutoResolve>true</AutoResolve>
<AlertPriority>Normal</AlertPriority>
<AlertSeverity>Warning</AlertSeverity>
<AlertParameters>
<AlertParameter1>$Data/Context/Property[@Name='StrAffectedDatabasesXMLOutput']$</AlertParameter1>
</AlertParameters>
</AlertSettings>
<OperationalStates>
<OperationalState ID="Success" MonitorTypeStateID="Success" HealthState="Success"/>
<OperationalState ID="Error" MonitorTypeStateID="Error" HealthState="Error"/>
</OperationalStates>
<Configuration>
<ScriptName>AK85906.ps1</ScriptName>
<Parameters>
<Parameter>
<Name>ConnectionString</Name>
<Value>$Target/Host/Property[Type="MicrosoftSQLServerLibrary!Microsoft.SQLServer.DBEngine"]/ConnectionString$</Value>
</Parameter>
<Parameter>
<Name>StrDataBaseNameInput</Name>
<Value>$Target/Property[Type="MicrosoftSQLServerLibrary!Microsoft.SQLServer.Database"]/DatabaseName$</Value>
</Parameter>
</Parameters>
<ScriptBody>
param($ConnectionString,$StrDataBaseNameInput)
$ErrorActionPreference = "Stop"
# Set up the arguments
$scriptargs = new-object psobject
$scriptargs | add-member NoteProperty "ConnectionString" $ConnectionString
$scriptargs | add-member NoteProperty "StrDataBaseNameInput" $StrDataBaseNameInput
# Set up the output
$global:scriptoutput = new-object psobject
$scriptoutput | add-member NoteProperty "HasIssue" $false
$scriptoutput | add-member NoteProperty "StrAffectedDatabasesXMLOutput" ""
#-----------------------------------------------------
# MAIN CODE SECTION
#-----------------------------------------------------
#region Environment
$scriptenv = New-Object System.Management.Automation.PSObject
$scriptenv | Add-Member NoteProperty "RuntimeError" $false
$scriptenv | Add-Member NoteProperty "IsDBAffectedEnv" $false
$scriptenv | Add-Member NoteProperty "AffectedDatabasesXMLEnv" ""
#endregion
#Private method to create affected databases entity object
function NewAffectedDBColumn($volumeOfDBFiles,$dbFilesPhysicalName,$volumeOfBackupFiles,$backupFilesPhysicalName)
{
$scriptCollectedInfo = New-Object System.Management.Automation.PSObject
$scriptCollectedInfo | Add-Member NoteProperty "VolumeOfDBFilesDC" $volumeOfDBFiles
$scriptCollectedInfo | Add-Member NoteProperty "DBFilesPhysicalNameDC" $dbFilesPhysicalName
$scriptCollectedInfo | Add-Member NoteProperty "VolumeOfBackupFilesDC" $volumeOfBackupFiles
$scriptCollectedInfo | Add-Member NoteProperty "BackupFilesPhysicalNameDC" $backupFilesPhysicalName
$scriptCollectedInfo
}
#Private method to format XML data for UI display
function FormatXMLData([array]$arrayList)
{
$xmlTemplate = "<row><VolumeOfDBFiles>{0}</VolumeOfDBFiles><DBFilesPhysicalName>{1}</DBFilesPhysicalName><VolumeOfBackupFiles>{2}</VolumeOfBackupFiles><BackupFilesPhysicalName>{3}</BackupFilesPhysicalName></row>"
if($arrayList)
{
$arrayList | ForEach-Object{
$scriptenv.AffectedDatabasesXMLEnv += ($xmlTemplate -f $_.VolumeOfDBFilesDC,$_.DBFilesPhysicalNameDC,$_.VolumeOfBackupFilesDC,$_.BackupFilesPhysicalNameDC)
}
}
}
function New-Type
{
#Support PowerShell 1.0, Same as Add-Type of PS 2.0
#http://huddledmasses.org/custom-icomparers-in-powershell-and-add-type-for-v1/
param([string]$TypeDefinition,[string[]]$ReferencedAssemblies)
## Obtains an ICodeCompiler from a CodeDomProvider class.
$provider = New-Object Microsoft.CSharp.CSharpCodeProvider
## Get the location for System.Management.Automation DLL
$dllName = [PsObject].Assembly.Location
## Configure the compiler parameters
$compilerParameters = New-Object System.CodeDom.Compiler.CompilerParameters
$assemblies = @("System.dll","System.Management.dll", $dllName)
$compilerParameters.ReferencedAssemblies.AddRange($assemblies)
if($ReferencedAssemblies)
{
$compilerParameters.ReferencedAssemblies.AddRange($ReferencedAssemblies)
}
$compilerParameters.IncludeDebugInformation = $true
$compilerParameters.GenerateInMemory = $true
$provider.CompileAssemblyFromSource($compilerParameters, $TypeDefinition) | Out-Null
}
function FormatVolumeResults($volumeName,$diskDeviceID)
{
$scriptInfo = New-Object System.Management.Automation.PSObject
$scriptInfo | Add-Member NoteProperty "VolumeName" $volumeName
$scriptInfo | Add-Member NoteProperty "DiskDeviceID" $diskDeviceID
$scriptInfo
}
function AddReparsePointsType()
{
$source = @'
using System;
using System.IO;
using System.Management;
using System.Runtime.InteropServices;
using System.Text;
namespace ReparsePoints
{
//Reference: http://www.codeproject.com/Articles/21202/Reparse-Points-in-Vista
public class ReparsePoint
{
private const uint IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003;
private const uint IO_REPARSE_TAG_SYMLINK = 0xA000000C;
private const UInt32 SE_PRIVILEGE_ENABLED = 0x00000002;
private const string SE_BACKUP_NAME = "SeBackupPrivilege";
private const uint FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;
private const uint FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000;
private const uint FILE_DEVICE_FILE_SYSTEM = 9;
private const uint FILE_ANY_ACCESS = 0;
private const uint METHOD_BUFFERED = 0;
private const int MAXIMUM_REPARSE_DATA_BUFFER_SIZE = 16 * 1024;
private const uint TOKEN_ADJUST_PRIVILEGES = 0x0020;
private const int FSCTL_GET_REPARSE_POINT = 42;
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct REPARSE_DATA_BUFFER
{
public uint ReparseTag;
public short ReparseDataLength;
public short Reserved;
public short SubsNameOffset;
public short SubsNameLength;
public short PrintNameOffset;
public short PrintNameLength;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = MAXIMUM_REPARSE_DATA_BUFFER_SIZE)]
public char[] ReparseTarget;
}
[StructLayout(LayoutKind.Sequential)]
private struct LUID
{
public UInt32 LowPart;
public Int32 HighPart;
}
[StructLayout(LayoutKind.Sequential)]
private struct LUID_AND_ATTRIBUTES
{
public LUID Luid;
public UInt32 Attributes;
}
private struct TOKEN_PRIVILEGES
{
public UInt32 PrivilegeCount;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)]
public LUID_AND_ATTRIBUTES[] Privileges;
}
[DllImport("kernel32.dll", ExactSpelling = true, SetLastError = true, CharSet = CharSet.Auto)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool DeviceIoControl(
IntPtr hDevice,
uint dwIoControlCode,
IntPtr lpInBuffer,
uint nInBufferSize,
out REPARSE_DATA_BUFFER outBuffer,
uint nOutBufferSize,
out uint lpBytesReturned,
IntPtr lpOverlapped);
[DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
static extern IntPtr CreateFile(
string fileName,
[MarshalAs(UnmanagedType.U4)] FileAccess fileAccess,
[MarshalAs(UnmanagedType.U4)] FileShare fileShare,
int securityAttributes,
[MarshalAs(UnmanagedType.U4)] FileMode creationDisposition,
uint flags,
IntPtr template);
[DllImport("advapi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool OpenProcessToken(IntPtr ProcessHandle,
UInt32 DesiredAccess, out IntPtr TokenHandle);
[DllImport("kernel32.dll")]
static extern IntPtr GetCurrentProcess();
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Auto)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool LookupPrivilegeValue(string lpSystemName, string lpName,
out LUID lpLuid);
[DllImport("advapi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool AdjustTokenPrivileges(IntPtr TokenHandle,
[MarshalAs(UnmanagedType.Bool)]bool DisableAllPrivileges,
ref TOKEN_PRIVILEGES NewState,
Int32 BufferLength,
IntPtr PreviousState,
IntPtr ReturnLength);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool CloseHandle(IntPtr hObject);
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool GetVolumeNameForVolumeMountPoint(string
lpszVolumeMountPoint, [Out] StringBuilder lpszVolumeName,
uint cchBufferLength);
private static string normalisedTarget = string.Empty;
private static string actualTarget = string.Empty;
private static TagType tag = TagType.None;
/// <summary>
/// Takes a full path to a reparse point and finds the target.
/// </summary>
/// <param name="path">Full path of the reparse point</param>
public static void CheckReparsePoint(string path)
{
bool success;
int lastError;
// Apparently we need to have backup privileges
IntPtr token;
TOKEN_PRIVILEGES tokenPrivileges = new TOKEN_PRIVILEGES();
tokenPrivileges.Privileges = new LUID_AND_ATTRIBUTES[1];
success = OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, out token);
lastError = Marshal.GetLastWin32Error();
if (success)
{
success = LookupPrivilegeValue(null, SE_BACKUP_NAME, out tokenPrivileges.Privileges[0].Luid); // null for local system
lastError = Marshal.GetLastWin32Error();
if (success)
{
tokenPrivileges.PrivilegeCount = 1;
tokenPrivileges.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
success = AdjustTokenPrivileges(token, false, ref tokenPrivileges, Marshal.SizeOf(tokenPrivileges), IntPtr.Zero, IntPtr.Zero);
lastError = Marshal.GetLastWin32Error();
}
CloseHandle(token);
}
if (success)
{
// Open the file and get its handle
IntPtr handle = CreateFile(path, FileAccess.Read, FileShare.None, 0, FileMode.Open, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, IntPtr.Zero);
lastError = Marshal.GetLastWin32Error();
if (handle.ToInt32() >= 0)
{
REPARSE_DATA_BUFFER buffer = new REPARSE_DATA_BUFFER();
// Make up the control code - see CTL_CODE on ntddk.h
uint controlCode = (FILE_DEVICE_FILE_SYSTEM << 16) | (FILE_ANY_ACCESS << 14) | (FSCTL_GET_REPARSE_POINT << 2) | METHOD_BUFFERED;
uint bytesReturned;
success = DeviceIoControl(handle, controlCode, IntPtr.Zero, 0, out buffer, MAXIMUM_REPARSE_DATA_BUFFER_SIZE, out bytesReturned, IntPtr.Zero);
lastError = Marshal.GetLastWin32Error();
if (success)
{
string subsString = "";
string printString = "";
// Note that according to http://wesnerm.blogs.com/net_undocumented/2006/10/symbolic_links_.html
// Symbolic links store relative paths, while junctions use absolute paths
// however, they can in fact be either, and may or may not have a leading \.
if (buffer.ReparseTag == IO_REPARSE_TAG_SYMLINK)
{
// for some reason symlinks seem to have an extra two characters on the front
subsString = new string(buffer.ReparseTarget, (buffer.SubsNameOffset / 2 + 2), buffer.SubsNameLength / 2);
printString = new string(buffer.ReparseTarget, (buffer.PrintNameOffset / 2 + 2), buffer.PrintNameLength / 2);
tag = TagType.SymbolicLink;
}
else if (buffer.ReparseTag == IO_REPARSE_TAG_MOUNT_POINT)
{
// This could be a junction or a mounted drive - a mounted drive starts with "\\??\\Volume"
subsString = new string(buffer.ReparseTarget, buffer.SubsNameOffset / 2, buffer.SubsNameLength / 2);
printString = new string(buffer.ReparseTarget, buffer.PrintNameOffset / 2, buffer.PrintNameLength / 2);
tag = subsString.StartsWith(@"\??\Volume") ? TagType.MountPoint : TagType.JunctionPoint;
}
// the printstring should give us what we want
if (!string.IsNullOrEmpty(printString))
{
normalisedTarget = printString;
}
else
{
// if not we can use the substring with a bit of tweaking
normalisedTarget = subsString;
if (normalisedTarget.StartsWith(@"\??\"))
{
normalisedTarget = normalisedTarget.Substring(4);
}
}
actualTarget = normalisedTarget;
// Symlinks can be relative.
if (buffer.ReparseTag == IO_REPARSE_TAG_SYMLINK && (normalisedTarget.Length < 2 || normalisedTarget[1] != ':'))
{
// it's relative, we need to tack it onto the path
if (normalisedTarget[0] == '\\')
{
normalisedTarget = normalisedTarget.Substring(1);
}
if (path.EndsWith(@"\"))
{
path = path.Substring(0, path.Length - 1);
}
// Need to take the symlink name off the path
normalisedTarget = path.Substring(0, path.LastIndexOf('\\')) + @"\" + normalisedTarget;
// Note that if the symlink target path contains any ..s these are not normalised but returned as is.
}
// Remove any final slash for consistency
if (normalisedTarget.EndsWith("\\"))
{
normalisedTarget = normalisedTarget.Substring(0, normalisedTarget.Length - 1);
}
}
CloseHandle(handle);
}
else
{
success = false;
}
}
}
public static string GetVolumeCaption(string mountPoint)
{
const int MaxVolumeNameLength = 100;
string volumeCaption = string.Empty;
StringBuilder volumeName = new StringBuilder(MaxVolumeNameLength);
if (!GetVolumeNameForVolumeMountPoint(string.Format(@"{0}\", mountPoint), volumeName, (uint)MaxVolumeNameLength))
return volumeCaption;
ManagementObjectSearcher searcher =
new ManagementObjectSearcher("root\\CIMV2",
"SELECT * FROM Win32_Volume");
foreach (ManagementObject queryObj in searcher.Get())
{
if (string.Compare(queryObj["DeviceID"].ToString(),
volumeName.ToString()) == 0)
{
volumeCaption = queryObj["Caption"].ToString();
}
}
return volumeCaption;
}
/// <summary>
/// This returns the normalised target, ie. if the actual target is relative it has been made absolute
/// Note that it is not fully normalised in that .s and ..s may still be included.
/// </summary>
/// <returns>The normalised path</returns>
public override string ToString()
{
return normalisedTarget;
}
/// <summary>
/// Gets the actual target string, before normalising
/// </summary>
public static string Target
{
get
{
return actualTarget;
}
}
/// <summary>
/// Gets the tag
/// </summary>
public static TagType Tag
{
get
{
return tag;
}
}
}
public enum TagType
{
None = 0,
MountPoint = 1,
SymbolicLink = 2,
JunctionPoint = 3
}
}
'@
New-Type -TypeDefinition $source
}
function GetMPDriveCaptionByPath($path)
{
[ReparsePoints.ReparsePoint]::CheckReparsePoint($path)
if([ReparsePoints.ReparsePoint]::Tag -eq [ReparsePoints.TagType]::MountPoint)
{
[ReparsePoints.ReparsePoint]::GetVolumeCaption($path)
}
}
function GetSubFolderListByPath($path)
{
if(Test-Path $path)
{
$folders = @()
$folders += $path.Split('\')
#Recursive method to get each sub folders
for($i = 0;$i -le $folders.Length -2;$i++)
{
$folders[$i+1] += $folders[$i] +"\" +$folders[$i+1]
$folders[$i+1] = $folders[$i+1].Substring($folders[$i+1].IndexOf($folders[$i]))
}
$folders
}
}
function IsExistMPForSpecifiedPath($path)
{
[array]$folders = GetSubFolderListByPath $path
if($folders)
{
for($i = 1; $i -ne $folders.Length; $i++)
{
if(GetMPDriveCaptionByPath $folders[$i])
{
return $true
}
}
}
}
function GetDriveVolumeBySpecifiedPath($path)
{
if(Test-Path $path)
{
if(IsExistMPForSpecifiedPath $path)
{
[array]$folders = GetSubFolderListByPath $path
if($folders)
{
for($i = $folders.Length-1; $i -ne 0; $i--)
{
$driveVolume = GetMPDriveCaptionByPath $folders[$i]
if(-not $driveVolume)
{
continue
}
return $driveVolume
}
}
}
return $path.Substring(0,$path.IndexOf("\")+1)
}
}
function GetDBConnectionString()
{
if($scriptargs.ConnectionString -and `
$scriptargs.StrDataBaseNameInput)
{
$connStrBulider = New-Object System.Data.SqlClient.SqlConnectionStringBuilder
$ConnStrBulider["Data Source"] = $scriptargs.ConnectionString
$ConnStrBulider["Database"] = $scriptargs.StrDataBaseNameInput
$ConnStrBulider["Trusted_Connection"] = "SSPI"
$connStrBulider.ConnectionString
}
}
function CheckDBAndBackupFiles()
{
$DBConnectionString = GetDBConnectionString
if($DBConnectionString)
{
# Create SqlConnection object and define connection string
$oConnection = New-Object System.Data.SqlClient.SqlConnection
$oConnection.ConnectionString = $DBConnectionString
$oConnection.Open()
# Create SqlCommand object, define command text, and set the connection
$oSQLCommand = New-Object System.Data.SqlClient.SqlCommand
$oSQLCommand.CommandText = "SELECT [dbfiles].[physical_name] AS DBFilesPhysicalName,[mediafamily].[physical_device_name] AS BackupFilesPhysicalName FROM msdb.dbo.backupmediafamily [mediafamily] JOIN (SELECT [backupsets].media_set_id, [backupsets].database_name FROM msdb.dbo.backupset [backupsets] JOIN (SELECT TOP 1 [backupsets].media_set_id, [backupsets].database_name, [backupsets].backup_finish_date FROM msdb.dbo.backupset [backupsets] JOIN sys.databases [databases] ON [backupsets].[database_name] = [databases].[name] and [databases].[is_in_standby] = 0 AND [databases].[source_database_id] IS NULL AND ([databases].name = 'master' OR DATEDIFF ( ss, [databases].[create_date] , [backupsets].[database_creation_date] ) = 0 ) WHERE [databases].[database_id] = DB_ID() AND [backupsets].type = 'D' ORDER BY [backupsets].backup_finish_date DESC) AS [latest_backupset] ON [backupsets].[database_name] = [latest_backupset].[database_name] AND [backupsets].[backup_finish_date] >= [latest_backupset].[backup_finish_date]) AS latest_backups ON [latest_backups].[media_set_id]=[mediafamily].[media_set_id] JOIN sys.databases ON [latest_backups].[database_name] = [databases].[name] JOIN sys.master_files [dbfiles] ON [dbfiles].[database_id] = [databases].[database_id] JOIN sys.database_mirroring [mirrors] ON [mirrors].[database_id] = [databases].[database_id] AND [mirrors].[mirroring_guid] IS NULL WHERE UPPER(SUBSTRING([dbfiles].[physical_name],1,2)) != '\\'"
$oSQLCommand.Connection = $oConnection
# Execute Command
$reader = $oSQLCommand.ExecuteReader()
$affectedDBArray = @()
while($reader.Read())
{
$volumeOfDBFile = GetDriveVolumeBySpecifiedPath $reader["DBFilesPhysicalName"]
$volumeOfBackupFile = GetDriveVolumeBySpecifiedPath $reader["BackupFilesPhysicalName"]
#SQL Server database files and backup files exist on the same volume
if(($volumeOfDBFile -and $volumeOfBackupFile) -and `
($volumeOfDBFile -eq $volumeOfBackupFile))
{
$affectedDBArray += NewAffectedDBColumn $volumeOfDBFile $reader["DBFilesPhysicalName"] $volumeOfBackupFile $reader["BackupFilesPhysicalName"]
}
}
if($affectedDBArray)
{
FormatXMLData $affectedDBArray
$scriptenv.IsDBAffectedEnv = $true
}
# Close reader object
$reader.Close()
# Close SqlConnection object
$oConnection.Close()
}
}
# Main function
function AdvisorRule($scriptargs, $scriptoutput)
{
# All parameters should be populated outside of the main function.
# The main function should only include the detection logic so that it can be easily reused by the Atlanta authoring tool.
trap [Exception] {
$scriptenv.RuntimeError = $true
continue
}
# Initialize parameters
$scriptoutput.HasIssue = $false
$scriptoutput.StrAffectedDatabasesXMLOutput = ""
AddReparsePointsType
CheckDBAndBackupFiles
if($scriptenv.IsDBAffectedEnv -eq $true)
{
if($scriptenv.RuntimeError -eq $false)
{
$scriptoutput.HasIssue = $true
$scriptoutput.StrAffectedDatabasesXMLOutput = $scriptenv.AffectedDatabasesXMLEnv
}
}
}
AdvisorRule $scriptargs $scriptoutput
# set the output
$mom = new-object -comobject "MOM.ScriptAPI"
$bag = $mom.CreatePropertyBag()
if ($scriptoutput.HasIssue -ne $null)
{
$bag.AddValue("HasIssue", $scriptoutput.HasIssue)
}
if ($scriptoutput.StrAffectedDatabasesXMLOutput -ne $null)
{
$bag.AddValue("StrAffectedDatabasesXMLOutput", $scriptoutput.StrAffectedDatabasesXMLOutput)
}
$bag
</ScriptBody>
<SnapIns/>
<TimeoutSeconds>300</TimeoutSeconds>
<Schedule>86396</Schedule>
<ErrorExpression>
<SimpleExpression>
<ValueExpression>
<XPathQuery Type="Boolean">Property[@Name='HasIssue']</XPathQuery>
</ValueExpression>
<Operator>Equal</Operator>
<ValueExpression>
<Value Type="Boolean">true</Value>
</ValueExpression>
</SimpleExpression>
</ErrorExpression>
<SuccessExpression>
<Not>
<Expression>
<SimpleExpression>
<ValueExpression>
<XPathQuery Type="Boolean">Property[@Name='HasIssue']</XPathQuery>
</ValueExpression>
<Operator>Equal</Operator>
<ValueExpression>
<Value Type="Boolean">true</Value>
</ValueExpression>
</SimpleExpression>
</Expression>
</Not>
</SuccessExpression>
</Configuration>
</UnitMonitor>