AlwaysON – Script to sync SQL Server Agent Jobs from Primary Replica to Secondary Replica in an Always On Availability Group

Environment

Blog29_1

-> Create a Job called “SQL Server Agent Job Synchronization” on all the Database Servers as part of your Alwayson Availability group. In my Environment, the Job will be created on Database Server JBSERVER1,Β JBSERVER2 andΒ JBSERVER3. The Job “SQL Server Agent Job Synchronization” will have the below script executed as part of it.

-- Script to sync SQL Server Agent Jobs from Primary Replica to Secondary Replica in an Always On Availability Group
-- Dont forgot to change the listener name below
SET NOCOUNT ON;

DECLARE @primary_replica NVARCHAR(128),
        @local_replica NVARCHAR(128),
        @job_name NVARCHAR(128),
        @job_id UNIQUEIDENTIFIER,
        @tsql NVARCHAR(MAX),
        @sql NVARCHAR(MAX);

				

-- Get the primary replica name
SELECT @Primary_Replica = primary_replica
FROM sys.dm_hadr_availability_group_states a INNER JOIN sys.availability_group_listeners b
ON a.group_id=b.group_id where b.dns_name='DISL' ---Change the LISTENER NAME here

-- Get the current replica name (where this script is running)
SELECT @local_replica = @@SERVERNAME;

-- If this server is the primary replica, no need to sync jobs
IF @local_replica = @primary_replica
BEGIN
    PRINT 'This server is the primary replica. No job sync required.';
    RETURN;
END


-- Create a table to store jobs from the primary replica
IF OBJECT_ID('tempdb..#primary_jobs') IS NOT NULL
    DROP TABLE #primary_jobs;

CREATE TABLE #primary_jobs (
    job_id UNIQUEIDENTIFIER,
    job_name NVARCHAR(128)
);

-- Insert jobs from primary replica into the temp table
SET @sql = 'INSERT INTO #primary_jobs (job_id, job_name)
            SELECT job_id, name FROM [' + @primary_replica + '].msdb.dbo.sysjobs';

EXEC sp_executesql @sql;

-- Loop through jobs on primary replica and compare with local (secondary) replica
DECLARE job_cursor CURSOR FOR
SELECT job_id, job_name
FROM #primary_jobs;

OPEN job_cursor;
FETCH NEXT FROM job_cursor INTO @job_id, @job_name;

WHILE @@FETCH_STATUS = 0
BEGIN
    -- Check if the job exists on the local (secondary) replica
    IF NOT EXISTS (SELECT 1 FROM msdb.dbo.sysjobs WHERE name = @job_name)
    BEGIN
        PRINT 'Job missing on secondary replica: ' + @job_name;

        -- Script job creation from the primary replica
        DECLARE @job_creation_script NVARCHAR(MAX) = '';
        DECLARE @step_creation_script NVARCHAR(MAX) = '';
        DECLARE @schedule_creation_script NVARCHAR(MAX) = '';

        -- Step 1: Script the job creation
        SET @job_creation_script = 'EXEC msdb.dbo.sp_add_job @job_name = ''' + @job_name + ''', @enabled = 1, @description = ''' + @job_name + ''';';
        
        -- Step 2: Script the job steps from the primary replica
        DECLARE @step_id INT,
                @step_name NVARCHAR(128),
                @subsystem NVARCHAR(128),
                @command NVARCHAR(MAX),
                @on_success_action INT,
                @on_fail_action INT;
				

						set @sql=N''
				set @sql =         'SELECT step_id, step_name, subsystem, command, on_success_action, on_fail_action  INTO ##Primary_Job_jbs_wiki_details
        FROM [' + @primary_replica + '].msdb.dbo.sysjobsteps 
        WHERE job_id = '''+convert(nvarchar(max),@job_id)+''';'
		EXECUTE master.sys.sp_executesql @sql;

        DECLARE step_cursor CURSOR FOR 
        SELECT step_id, step_name, subsystem, command, on_success_action, on_fail_action 
        FROM ##Primary_Job_jbs_wiki_details;

        OPEN step_cursor;
        FETCH NEXT FROM step_cursor INTO @step_id, @step_name, @subsystem, @command, @on_success_action, @on_fail_action;

        WHILE @@FETCH_STATUS = 0
        BEGIN
		
            SET @step_creation_script = @step_creation_script + 'EXEC msdb.dbo.sp_add_jobstep 
                    @job_name = ''' + @job_name + ''', 
                    @step_name = ''' + @step_name + ''', 
                    @subsystem = ''' + @subsystem + ''', 
                    @command = ''' + REPLACE(@command, '''', '''''') + ''', 
                    @on_success_action = ' + CAST(@on_success_action AS NVARCHAR(10)) + ',
                    @on_fail_action = ' + CAST(@on_fail_action AS NVARCHAR(10)) + ';';
                    
            FETCH NEXT FROM step_cursor INTO @step_id, @step_name, @subsystem, @command, @on_success_action, @on_fail_action;
        END
		drop table ##Primary_Job_jbs_wiki_details
        CLOSE step_cursor;
        DEALLOCATE step_cursor;

        -- Step 3: Script the job schedule from the primary replica
        DECLARE @schedule_name NVARCHAR(128),
                @enabled INT,
                @freq_type INT,
                @freq_interval INT,
                @freq_subday_type INT,
                @freq_subday_interval INT,
                @freq_relative_interval INT,
                @freq_recurrence_factor INT,
                @active_start_date INT,
                @active_start_time INT;

				set @sql = N''
		set @sql = 'SELECT s.name, s.enabled, s.freq_type, s.freq_interval, s.freq_subday_type, s.freq_subday_interval, 
               s.freq_relative_interval, s.freq_recurrence_factor, s.active_start_date, s.active_start_time INTO ##Primary_Job_jbs_wiki_details1
        FROM [' + @primary_replica + '].msdb.dbo.sysschedules AS s
        INNER JOIN [' + @primary_replica + '].msdb.dbo.sysjobschedules AS js ON s.schedule_id = js.schedule_id
        WHERE js.job_id = '''+convert(nvarchar(max),@job_id)+''';'
		EXECUTE master.sys.sp_executesql @sql;

        DECLARE schedule_cursor CURSOR DYNAMIC FOR 
        SELECT s.name, s.enabled, s.freq_type, s.freq_interval, s.freq_subday_type, s.freq_subday_interval, 
               s.freq_relative_interval, s.freq_recurrence_factor, s.active_start_date, s.active_start_time 
        FROM ##Primary_Job_jbs_wiki_details1 s;

        OPEN schedule_cursor;
        FETCH NEXT FROM schedule_cursor INTO @schedule_name, @enabled, @freq_type, @freq_interval, @freq_subday_type, 
                                              @freq_subday_interval, @freq_relative_interval, @freq_recurrence_factor, 
                                              @active_start_date, @active_start_time;

        WHILE @@FETCH_STATUS = 0
        BEGIN
			SET @schedule_creation_script = @schedule_creation_script + 'EXEC msdb.dbo.sp_add_jobschedule 
                    @job_name = ''' + @job_name + ''', 
                    @name = ''' + @schedule_name + ''', 
                    @enabled = ' + CAST(@enabled AS NVARCHAR(10)) + ', 
                    @freq_type = ' + CAST(@freq_type AS NVARCHAR(10)) + ', 
                    @freq_interval = ' + CAST(@freq_interval AS NVARCHAR(10)) + ', 
                    @freq_subday_type = ' + CAST(@freq_subday_type AS NVARCHAR(10)) + ', 
                    @freq_subday_interval = ' + CAST(@freq_subday_interval AS NVARCHAR(10)) + ', 
                    @freq_relative_interval = ' + CAST(@freq_relative_interval AS NVARCHAR(10)) + ', 
                    @freq_recurrence_factor = ' + CAST(@freq_recurrence_factor AS NVARCHAR(10)) + ', 
                    @active_start_date = ' + CAST(@active_start_date AS NVARCHAR(10)) + ', 
                    @active_start_time = ' + CAST(@active_start_time AS NVARCHAR(10)) + ';';

            FETCH NEXT FROM schedule_cursor INTO @schedule_name, @enabled, @freq_type, @freq_interval, @freq_subday_type, 
                                                  @freq_subday_interval, @freq_relative_interval, @freq_recurrence_factor, 
                                                  @active_start_date, @active_start_time;
        END
		DROP TABLE ##Primary_Job_jbs_wiki_details1
        CLOSE schedule_cursor;
        DEALLOCATE schedule_cursor;

        -- Combine all scripts and execute to create the job on the secondary replica
        SET @tsql = @job_creation_script + @step_creation_script + @schedule_creation_script;

        EXEC sp_executesql @tsql;
        
        PRINT 'Job created on secondary replica: ' + @job_name;
    END

    FETCH NEXT FROM job_cursor INTO @job_id, @job_name;
END

CLOSE job_cursor;
DEALLOCATE job_cursor;

-- Cleanup
DROP TABLE #primary_jobs;


PRINT 'Job sync completed.';

-> Create a Linked Server to query the primary Replica. In my Environment, Linked servers JBSERVER2 and JBSERVER3 will be created on JBSERVER1. Linked servers JBSERVER1 and JBSERVER3 will be created on JBSERVER2. Linked servers JBSERVER1 and JBSERVER2 will be created on JBSERVER3.

-> The job will gracefully exit with a message “Script cannot run on primary Replica” if the job executes on Primary Replica. If the Job executes on the Secondary replica, It queries the list of SQL Server Agent Jobs on the primary replica and will create the jobs that are missing on the Secondary Replicas.

-> This solution just adds the missing jobs on the Secondary Replicas, but will not Drop Jobs on the Secondary Replica that are not present on the Primary.

Thank You,
Vivek Janakiraman

Disclaimer:
The views expressed on this blog are mine alone and do not reflect the views of my company or anyone else. All postings on this blog are provided β€œAS IS” with no warranties, and confers no rights.

SQL Server Always On Series: Availability group DDL operations are permitted only when you are using the master database


Introduction

During a recent attempt to perform a manual failover from Always On Availability Group from JBSAG1 to JBSAG2 using Microsoft SQL Server Management Studio (SSMS), an unexpected error disrupted the process. The error, specifically identified as Microsoft SQL Server Error 35208, posed a challenge to the manual failover operation. In this detailed account, we’ll explore the encountered issue, outline the steps taken to address it, and ultimately achieve a successful manual failover.

Issue
Upon initiating the Always On Availability Group manual failover, the process encountered a hurdle with the following error:

TITLE: Microsoft SQL Server Management Studio
Manual Failover failed (Microsoft.SqlServer.Management.HadrTasks)

ADDITIONAL INFORMATION:
Failed to perform a manual failover of the availability group ‘JBSWiki’ to server instance ‘JBSAG2’. (Microsoft.SqlServer.Management.HadrModel)

For help, click: https://go.microsoft.com/fwlink?ProdName=Microsoft+SQL+Server&ProdVer=16.100.47021.0&EvtSrc=Microsoft.SqlServer.Management.Smo.ExceptionTemplates.FailedOperationExceptionText&LinkId=20476

An exception occurred while executing a Transact-SQL statement or batch. (Microsoft.SqlServer.ConnectionInfo)

Availability group DDL operations are permitted only when you are using the master database. Run the ‘USE master’ statement, and retry your availability group DDL statement. (Microsoft SQL Server, Error: 35208)
For help, click: https://docs.microsoft.com/sql/relational-databases/errors-events/mssqlserver-35208-database-engine-error

Navigating through this error was the initial challenge, particularly when attempting the failover using the SSMS graphical user interface (GUI). Executing the ‘USE master’ statement in this context presented uncertainties, prompting a reevaluation of the failover approach.

Solution
To address the dilemma, the following steps were taken:

GUI Failover Attempt:

  • Initially, the failover was attempted through the SSMS GUI, raising questions about how to execute the ‘USE master’ statement within the graphical interface.

Scripted Failover Action:

  • The failover wizard was restarted, and instead of concluding the process through the GUI, the failover action was scripted for manual execution. I started the failover wizard again and this time instead of clicking finish at the end, tried scripting the failover action.

Manual Execution of Failover Command:

  • The failover command was manually executed from the SSMS SQLCMD query window, successfully completing the failover process.

Database Context Discovery:

  • Investigation revealed that the database context for a new query window was set to a user database (JBDB) instead of ‘master,’ leading to a pivotal realization. I tried clicking on a new query window and I saw that the database context for that query window was set to an user database JBDB and not master.

Connection Options Adjustment:

  • This is when I realized that I might have connected to the SQL Server instance with an User Database specified on the “Connect to database” in “Options <<” as part of making a connection from SSMS. Please check screenshot below,

After realizing this, I disconnected the existing sessions on SSMS and changed “Connect to database” in “Options <<” to “Master” and connected to SQL server JBSAG1 and JBSAG2.

Success After Correction:

  • Following these corrective actions, subsequent manual failover and failback attempts via SSMS were executed seamlessly.

Summary
In summary, this journey through troubleshooting manual failover with error 35208 underscored the importance of the database context, especially when initiating DDL operations. The solution involved a meticulous adjustment of connection options within SSMS, ensuring a connection to the ‘master’ database before attempting manual failover. This article provides a detailed account of the encountered challenge, the thought process behind the solution, and the successful resolution achieved through careful steps and insights.

Thank You,
Vivek Janakiraman

Disclaimer:
The views expressed on this blog are mine alone and do not reflect the views of my company or anyone else. All postings on this blog are provided β€œAS IS” with no warranties, and confers no rights.

Querying Connect Permissions on Endpoints in SQL Server

Introduction πŸš€

Securing your SQL Server environment involves a meticulous examination of permissions granted to various entities. In this blog post, we’ll explore a T-SQL script designed to query if endpoints have been granted connect permissions. Understanding and managing these permissions is crucial for maintaining a secure and well-configured SQL Server infrastructure.

Requirements πŸ› οΈ

Before using the T-SQL script, make sure you have the following prerequisites:

  1. SQL Server Endpoints: The script is designed for environments with configured endpoints.
  2. Permissions: The account executing the script should have sufficient permissions to query the necessary system views (sys.server_permissions, sys.server_principals, and sys.tcp_endpoints).

T-SQL Script πŸ“œ

Use the following T-SQL script to determine if endpoints have connect permissions:

SELECT

perm.class_desc,
prin.name,
perm.permission_name,
perm.state_desc,
prin.type_desc as PrincipalType,
prin.is_disabled
FROM
sys.server_permissions perm
LEFT JOIN
sys.server_principals prin ON perm.grantee_principal_id = prin.principal_id
LEFT JOIN
sys.tcp_endpoints tep ON perm.major_id = tep.endpoint_id
WHERE
perm.class_desc = 'ENDPOINT'
AND perm.permission_name = 'CONNECT'
AND tep.type = 4;

This script retrieves information about connect permissions on endpoints, including details about the principal, permission state, and principal type.

Conclusion πŸŽ‰

Checking connect permissions on SQL Server endpoints is a critical step in ensuring a secure database environment. By utilizing the provided T-SQL script, database administrators can easily identify whether connect permissions have been granted to specific entities.

Regularly auditing and managing these permissions is essential for maintaining the integrity of your SQL Server infrastructure. May your endpoints always be secure, and your data remain protected! πŸ”’

Thank You,
Vivek Janakiraman

Disclaimer:
The views expressed on this blog are mine alone and do not reflect the views of my company or anyone else. All postings on this blog are provided β€œAS IS” with no warranties, and confers no rights.