SQL Server 2012 AlwaysOn: need to automatically add databases by script- T-sql or PowerShell
Lots and lots of caveats here. I've tested this in a very limited way in a scenario where the data/log paths are congruent across all replicas, haven't added error handling, etc. You can call this stored procedure from the DDL trigger if you decide to go that way, as Kin suggested, or from a job, or what have you.
See additional comments inline.
CREATE PROCEDURE dbo.AddNewDBsToGroup
@group SYSNAME = N'your_group_name', -- *** SPECIFY YOUR GROUP NAME HERE ***
@path SYSNAME = N'\\atel-web-be2\backups\',
@debug BIT = 1
AS
BEGIN
SET NOCOUNT ON;
DECLARE
@sql NVARCHAR(MAX) = N'',
@remote_sql NVARCHAR(MAX) = N'';
DECLARE @t TABLE(db SYSNAME);
INSERT @t SELECT name FROM sys.databases
WHERE replica_id IS NULL AND database_id > 4;
DECLARE @r TABLE(s NVARCHAR(512));
-- get the *healthy* replicas available for this group
-- you'll need error handling to handle cases where any
-- of the replicas is currently *not* healthy. This
-- script does not tell you this happened.
INSERT @r SELECT r.replica_server_name
FROM sys.availability_groups AS g
INNER JOIN sys.dm_hadr_availability_group_states AS s
ON g.group_id = s.group_id
INNER JOIN sys.availability_replicas AS r
ON g.group_id = r.group_id
AND r.replica_server_name <> @@SERVERNAME
WHERE g.name = @group
AND s.primary_replica = @@SERVERNAME
AND s.primary_recovery_health_desc = 'ONLINE'
AND s.synchronization_health_desc = 'HEALTHY';
-- add the database to the group on the primary:
SELECT @sql += N'ALTER AVAILABILITY GROUP '
+ QUOTENAME(@group) + ' ADD DATABASE ' + QUOTENAME(db) + ';'
FROM @t;
IF @debug = 1
BEGIN
PRINT @sql;
END
ELSE
BEGIN
EXEC master..sp_executesql @sql;
END
-- back up the database locally:
-- this assumes your database names don't have characters illegal for paths
SET @sql = N'';
SELECT @sql += N'BACKUP DATABASE ' + QUOTENAME(db) -- ** BACKUP HAPPENS HERE **
+ ' TO DISK = ''' + @path + db + '.BAK'' WITH COPY_ONLY, FORMAT, INIT, COMPRESSION;
BACKUP LOG ' + QUOTENAME(db) +
' TO DISK = ''' + @path + db + '.TRN'' WITH INIT, COMPRESSION;'
FROM @t;
IF @debug = 1
BEGIN
PRINT @sql;
END
ELSE
BEGIN
EXEC master..sp_executesql @sql;
END
-- restore the database remotely:
-- this assumes linked servers match replica names, security works, etc.
-- it also assumes that each replica has the exact sime data/log paths
-- (in other words, your restore doesn't need WITH MOVE)
SET @sql = N'';
SELECT @sql += N'RESTORE DATABASE ' + QUOTENAME(db) -- ** RESTORE HAPPENS HERE **
+ ' FROM DISK = ''' + @path + db + '.BAK'' WITH REPLACE, NORECOVERY;
RESTORE LOG ''' + @path + db + '.TRN'' WITH NORECOVERY;
ALTER DATABASE ' + QUOTENAME(db) + ' SET HADR AVAILABILITY GROUP = '
+ QUOTENAME(@group) + ';'
FROM @t;
SET @remote_sql = N'';
SELECT @remote_sql += N'EXEC ' + QUOTENAME(s) + '.master..sp_executesql @sql;'
FROM @r;
IF @debug = 1
BEGIN
PRINT @sql;
PRINT @remote_sql;
END
ELSE
BEGIN
EXEC sp_executesql @remote_sql, N'@sql NVARCHAR(MAX)', N'SELECT @@SERVERNAME;';
END
END
GO
Once you've created the stored procedure, you can call it this way and look at the messages pane to see if it has identified the right group, databases, and servers before ever running it:
EXEC dbo.AddNewDBsToGroup @debug = 1;
When you are confident it is going to do the right thing (and you fully understand what 'the right thing" is), then change that to:
EXEC dbo.AddNewDBsToGroup @debug = 0;
If it fails, don't worry, it will tell you.
You don't have to write a cursor tsql script to check for new database created and schedule it to run for e.g. every minute. Instead use EVENTDATA() function in conjunction with server level trigger.
IF EXISTS (SELECT * FROM sys.server_triggers
WHERE name = 'ddl_trig_database')
DROP TRIGGER ddl_trig_database
ON ALL SERVER;
GO
CREATE TRIGGER ddl_trig_database
ON ALL SERVER
FOR CREATE_DATABASE
AS
PRINT 'Database Created.'
SELECT EVENTDATA().value('(/EVENT_INSTANCE/TSQLCommand/CommandText)[1]','nvarchar(max)')
GO
DROP TRIGGER ddl_trig_database
ON ALL SERVER;
GO
Since you now have an automated mechanism in place that will fire up when a new database is created, you can use ALTER AVAILABILITY GROUP and ALTER DATABASE - SET HADR
basically you have to just include :
-- Move each database into the Availability Group
-- Change database name and Group as per your environment.
ALTER DATABASE Test1 SET HADR AVAILABILITY GROUP = TestAG
ALTER DATABASE Test2 SET HADR AVAILABILITY GROUP = TestAG
GO
Thinking of this a little bit more, you can be more creative to automate it --
-- create a driver table
create table AlwaysON_Candidates (
DatabaseName sysname
,createdate datetime default getdate()
,IsAlwaysOnMember bit default 0 -- 0 = Not a part of AG
) -- 1 = Is part of AG
go
-- below insert will be governed by the server level trigger
insert into AlwaysON_Candidates (DatabaseName)
values ('Test1')
--- check the values in the driver table
select *
from AlwaysON_Candidates
--- add database to AG
alter database Test1
set HADR AVAILABILITY group = TestAG
-- update the bit in the driver table AlwaysON_Candidates
update AlwaysON_Candidates
set IsAlwaysOnMember = 1
where DatabaseName = 'Test1'
some good references for setting it up using tsql can be found here and here
EDIT: Below script will help you. Obviously you have to Understand it and test it in a test environment.
/************************************************************************************
Author : Kin Shah
Version : 1.0.0 for dba.stackexchange.com
Note: This script does not have ERROR handling and is not tested.
Use at your own risk, It will print out the sql statements, but wont execute it
unless the print statements have been modified to use "exec"
UNDERSTAND the script and then test it on a TEST environment !!!!!!!!
*************************************************************************************/
-- create table
set ansi_nulls on
go
set quoted_identifier on
go
create table AlwaysON_Candidates (
ID int identity(1, 1)
,EventType nvarchar(128) null
,DatabaseName nvarchar(128) null
,LoginName nvarchar(128) null
,UserName nvarchar(128) null
,AuditDateTime datetime null
,IsAlwaysOnMember bit default 0
)
go
alter table [dbo].[AlwaysON_Candidates] add default((0))
for [IsAlwaysOnMember]
go
-- create server trigger
if exists (
select *
from master.sys.server_triggers
where parent_class_desc = 'SERVER'
and name = N'ddl_trig_database'
)
drop trigger [ddl_trig_database] on all server
go
set ansi_nulls on
go
set quoted_identifier on
go
create trigger [ddl_trig_database] on all server
for CREATE_DATABASE as
insert into NewDatabases
select EVENTDATA().value('(/EVENT_INSTANCE/EventType)[1]', 'nvarchar(128)') as EventType
,EVENTDATA().value('(/EVENT_INSTANCE/DatabaseName)[1]', 'nvarchar(128)') as DatabaseName
,EVENTDATA().value('(/EVENT_INSTANCE/LoginName)[1]', 'nvarchar(128)') as LoginName
,EVENTDATA().value('(/EVENT_INSTANCE/UserName)[1]', 'nvarchar(128)') as UserName
,GETDATE() as AuditDateTime
,0 as IsAlwaysOnMember
go
set ansi_nulls off
go
set quoted_identifier off
go
ENABLE trigger [ddl_trig_database] on all server
go
--- PREREQUISITE ... CREATE A LINKED SERVER FROM PRIMARY TO SECONDARY SERVER !!!
--- fill in *** CHANGE HERE values
--- test it on a TEST server
--- Not tested and not responsible for any dataloss. UNDERSTAND it and test it before implementing it.
declare @databasename varchar(max)
declare @sqlbackup varchar(max)
declare @sqlrestore varchar(max)
declare @PrimaryAG varchar(max)
declare @SecondaryAG varchar(max)
declare @backupPath varchar(max)
set @backupPath = '\\servername\sharedfolder\' --- *** CHANGE HERE
declare @group sysname
set @group = N'your_group_name' --- *** CHANGE HERE
declare @remotesql1 varchar(max)
declare @remotesql2 varchar(max)
declare @linkedserverName sysname
set @linkedserverName = 'kin_test_AG_LS' --- *** CHANGE HERE
select @databasename = min(DatabaseName)
from AlwaysON_Candidates
where IsAlwaysOnMember = 0
while @databasename is not null
begin
-- ** BACKUP HAPPENS HERE **
select @sqlbackup = N'BACKUP DATABASE ' + QUOTENAME(@databasename) + ' TO DISK = ''' + @backupPath + @databasename + '_forAG.BAK'' WITH COPY_ONLY, FORMAT, INIT, COMPRESSION;
BACKUP LOG ' + QUOTENAME(@databasename) + ' TO DISK = ''' + @backupPath + @databasename + '_forAG.TRN'' WITH INIT, COMPRESSION;'
from AlwaysON_Candidates
where IsAlwaysOnMember = 0
print @sqlbackup --- *** CHANGE HERE for EXEC master..sp_executesql @sqltext
-- ** RESTORE HAPPENS HERE **
select @sqlrestore = N'RESTORE DATABASE ' + QUOTENAME(@databasename) + ' FROM DISK = ''' + @backupPath + @databasename + '_forAG.BAK'' WITH REPLACE, NORECOVERY;
RESTORE LOG ''' + @backupPath + @databasename + '_forAG.TRN'' WITH NORECOVERY;'
print @sqlrestore
select @remotesql1 = N'EXEC ' + QUOTENAME(@linkedserverName) + '.master..sp_executesql @sqlrestore;'
print @remotesql1 --- *** CHANGE HERE for EXEC master..sp_executesql @sqltext
-- join the AG group on primary
select @PrimaryAG = N'ALTER AVAILABILITY GROUP ' + QUOTENAME(@group) + ' ADD DATABASE ' + QUOTENAME(@databasename) + ';'
print @PrimaryAG --- *** CHANGE HERE for EXEC master..sp_executesql @sqltext
-- join the AG group on secondary
select @SecondaryAG = 'ALTER DATABASE ' + QUOTENAME(@databasename) + ' SET HADR AVAILABILITY GROUP = ' + QUOTENAME(@group) + ' ;'
print @SecondaryAG
select @remotesql2 = N'EXEC ' + QUOTENAME(@linkedserverName) + '.master..sp_executesql @sqlrestore;'
print @remotesql2 --- *** CHANGE HERE for EXEC master..sp_executesql @SecondaryAG
-- finally update the table
update AlwaysON_Candidates
set IsAlwaysOnMember = 1
where DatabaseName = @databasename
-- go to another database if it is added newly
select @databasename = min(DatabaseName)
from AlwaysON_Candidates
where IsAlwaysOnMember = 0
and DatabaseName > @databasename
end