I have recently been tasked with finding all SQL Servers for inventory purposes. As I set to this task, I identified a couple of issues immediately. First, was that my new place of employment doesn't like to purchase any 3rd party applications. Also, most free tools including Microsoft Assessment and Planning toolkit (MAP) scan servers tend to look for the SQL Browser. These kinds of products aren't going to work well because if the SQL Browser is off, then the server will not be found. So, I set out to accomplish the task using PowerShell. Think of PowerShell as a sql query window for every Microsoft product. This script will look for the SQL Server service, running or stopped. Also, it will execute once a week through a SQL Agent job, so I can see what new servers have been installed recently. PowerShell is a wonderful tool, and not just for network administrators. All right, now the good stuff.
First, since it will be executing through a SQL job, the SQL Agent will need to be setup with a security ID strong enough to reach all Windows operating systems throughout the network. This script will go across domains, so no need to run in each domain.
IMPORTANT: if the ID does not have sufficient rights to all computers then it will simply skip past the server even though it saw the server on the network, so your list will not be complete. You may want to reach out to the network security group and let them know in advance, before they think there is a virus running rampant.
Next, we need to create a simple database and table to store the discovered SQL Server names. The following script sets up the tables and databases (you may need to change the file paths):
USE [master] GO CREATE DATABASE [SQL_Names] ON PRIMARY ( NAME = N'SQL_Names', FILENAME = N'C:\Program Files\Microsoft SQL Server\MSSQL.1\MSSQL\Data\SQL_Names.mdf' , SIZE = 3096KB , MAXSIZE = UNLIMITED, FILEGROWTH = 1024KB ) LOG ON ( NAME = N'SQL_Names_log', FILENAME = N'C:\Program Files\Microsoft SQL Server\MSSQL.1\MSSQL\Data\SQL_Names_log.ldf' , SIZE = 1024KB , MAXSIZE = 2048GB , FILEGROWTH = 10%) GO USE [SQL_Names] GO /****** Object: Table [dbo].[FoundSQLServers] Script Date: 02/15/2011 11:01:42 ******/SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO SET ANSI_PADDING ON GO CREATE TABLE [dbo].[FoundSQLServers]( [SQLServerName] [varchar](100) NULL, [ID] [int] IDENTITY(1,1) NOT NULL, [Date] [datetime] NOT NULL, CONSTRAINT [PK_FoundSQLServers] PRIMARY KEY CLUSTERED ( [ID] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ) ON [PRIMARY] GO SET ANSI_PADDING OFF GO ALTER TABLE [dbo].[FoundSQLServers] ADD CONSTRAINT [DF_FoundSQLServers_Date] DEFAULT (getdate()) FOR [Date] GO CREATE NONCLUSTERED INDEX [nc_index_FoundSQLServers] ON [dbo].[FoundSQLServers] ( [SQLServerName] ASC ) INCLUDE ([Date]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] GO
Next let's create the PowerShell script and save it to 'C:\PowerShell' as 'FoundSQLServers.ps1'. You need to update the $destserver variable with the name of your SQL Server:
$destserver = "YourSQLServer" $destDB = "SQL_Names" $destTable = "FoundSQLServers" function Set-FoundSqlServers { param($ComputerName) $Query = "INSERT INTO $destTable (SQLServerName) VALUES ('$($ComputerName)')" $conn = new-object System.Data.SqlClient.SQLConnection $ConnectionString = "Server={0};Database={1};Integrated Security=True;" -f $destserver,$destDB $conn.ConnectionString = $ConnectionString $conn.Open() $cmd = new-object system.Data.SqlClient.SqlCommand($Query,$conn) [void]$cmd.ExecuteNonQuery() $conn.Close() } function Find-Computers { $computers = @() net view |% { if ($_ -match "\\\\(\S+)") {$computers += $matches[1]} } $computers |? {(get-service -computername $_ |% {$_.name}) -match "sql"} } Find-Computers | foreach-object {Set-FoundSqlServers $_}
Now, let's create a stored procedure to build a list of new server names and email a report to yourself in HTML format. You will need to update the @EmailRecipients and @profile_name variables:
USE [SQL_Names]
GO
/****** Object: StoredProcedure [dbo].[sp_SendWeeklySQLReport] Script Date: 02/15/2011 10:50:17 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
/******
Stored Procedure builds a list of new SQL Servers found and emails a report to @EmailRecipients.
by Clay Punnett
PunnettC@Yahoo.com
@SQLHeap
******/
CREATE Procedure [dbo].[sp_SendWeeklySQLReport]
as
Set Nocount On
Declare @SQLCount int, @EmailRecipients varchar(100), @HTML nvarchar(MAX);
Set @EmailRecipients = 'your.email@yourplace.com'
Set @SQLCount = (
Select COUNT(*)
From SQL_Names.dbo.FoundSQLServers a (nolock)
Where a.SQLServerName not in (
Select Distinct SQLServerName
From SQL_Names.dbo.FoundSQLServers b (nolock)
Where LEFT(b.[Date], 11) < LEFT(GETDATE(), 11)
)
and LEFT(a.[Date], 11) = LEFT(GETDATE(), 11)
)
If (@SQLCount = 0)
Begin
Set @HTML =
N'<H1>New SQL servers found.</H1>' +
N'<table border="3" bordercolor="blue">' +
N'<tr>No new SQL Servers found in the passed week.</tr>' +
N'</table>'
End
If (@SQLCount > 0)
Begin
Set @HTML =
N'<H1>New SQL servers found.</H1>' +
N'<table border="3" bordercolor="blue">' +
N'<tr><th>SQLServerNames</th></tr>' +
CAST((Select td = SQLServerName, ''
From SQL_Names.dbo.FoundSQLServers a (nolock)
Where a.SQLServerName not in (
Select Distinct SQLServerName
From SQL_Names.dbo.FoundSQLServers b (nolock)
Where LEFT(b.[Date], 11) < LEFT(GETDATE(), 11)
)
and LEFT(a.[Date], 11) = LEFT(GETDATE(), 11)
Order by a.SQLServerName
For XML PATH('tr'), TYPE) as nvarchar(MAX)) +
N'</table>';
End
If (@SQLCount = 0)
Begin
EXEC msdb.dbo.sp_send_dbmail
@recipients = @EmailRecipients,
@subject = 'New SQL servers found Report',
@body = @HTML,
@body_format = 'HTML',
@profile_name = 'yourprofilename';
End
If (@SQLCount > 0)
Begin
EXEC msdb.dbo.sp_send_dbmail
@recipients = @EmailRecipients,
@subject = 'New SQL servers found Report',
@body = @HTML,
@body_format = 'HTML',
@profile_name = 'yourprofilename';
End
GO
Finally, let's create the job to run every Monday at 9am (or whenever) and email a list of only the new SQL Servers:
USE [msdb]
GO
/****** Object: Job [Find All SQL Servers] Script Date: 02/14/2011 14:43:41 ******/
BEGIN TRANSACTION
DECLARE @ReturnCode INT
SELECT @ReturnCode = 0
/****** Object: JobCategory [[Uncategorized (Local)]]] Script Date: 02/14/2011 14:43:41 ******/
IF NOT EXISTS (SELECT name FROM msdb.dbo.syscategories WHERE name=N'[Uncategorized (Local)]' AND category_class=1)
BEGIN
EXEC @ReturnCode = msdb.dbo.sp_add_category @class=N'JOB', @type=N'LOCAL', @name=N'[Uncategorized (Local)]'
IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback
END
DECLARE @jobId BINARY(16)
EXEC @ReturnCode = msdb.dbo.sp_add_job @job_name=N'Find All SQL Servers',
@enabled=1,
@notify_level_eventlog=0,
@notify_level_email=0,
@notify_level_netsend=0,
@notify_level_page=0,
@delete_level=0,
@description=N'Find All SQL Servers throughout the entire network.',
@category_name=N'[Uncategorized (Local)]',
@owner_login_name=N'sa', @job_id = @jobId OUTPUT
IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback
/****** Object: Step [Execute PowerShell script] Script Date: 02/14/2011 14:43:41 ******/
EXEC @ReturnCode = msdb.dbo.sp_add_jobstep @job_id=@jobId, @step_name=N'Execute PowerShell script',
@step_id=1,
@cmdexec_success_code=0,
@on_success_action=3,
@on_success_step_id=0,
@on_fail_action=2,
@on_fail_step_id=0,
@retry_attempts=0,
@retry_interval=0,
@os_run_priority=0, @subsystem=N'PowerShell',
@command=N'powershell.exe -File "C:\PowerShell\FoundSQLServers.ps1"',
@database_name=N'master',
@flags=0
IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback
/****** Object: Step [Email Report] Script Date: 02/14/2011 14:43:41 ******/
EXEC @ReturnCode = msdb.dbo.sp_add_jobstep @job_id=@jobId, @step_name=N'Email Report',
@step_id=2,
@cmdexec_success_code=0,
@on_success_action=1,
@on_success_step_id=0,
@on_fail_action=2,
@on_fail_step_id=0,
@retry_attempts=0,
@retry_interval=0,
@os_run_priority=0, @subsystem=N'TSQL',
@command=N'EXEC sp_SendWeeklySQLReport',
@database_name=N'SQL_Names',
@flags=0
IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback
EXEC @ReturnCode = msdb.dbo.sp_update_job @job_id = @jobId, @start_step_id = 1
IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback
EXEC @ReturnCode = msdb.dbo.sp_add_jobschedule @job_id=@jobId, @name=N'Monday Morning',
@enabled=1,
@freq_type=8,
@freq_interval=3,
@freq_subday_type=1,
@freq_subday_interval=0,
@freq_relative_interval=0,
@freq_recurrence_factor=1,
@active_start_date=20110214,
@active_end_date=99991231,
@active_start_time=90000,
@active_end_time=235959
IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback
EXEC @ReturnCode = msdb.dbo.sp_add_jobserver @job_id = @jobId, @server_name = N'(local)'
IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback
COMMIT TRANSACTION
GOTO EndSave
QuitWithRollback:
IF (@@TRANCOUNT > 0) ROLLBACK TRANSACTION
EndSave:
GO
Note that when you see new SQL Servers, it may be because the computer simply was not on during the previous runs. These scripts pull in just the computer name. Also, They can be modified to gather more data such as instance name, sql version, sql edition, etc.
Respectfully,
Clay Punnett