Sauvegardes de bases de données et envoi d’email avec SQL Server Express

Windows Server Update Services (WSUS) est une application qui permet de gérer et distribuer les patch que Microsoft publie pour ses produits. Elle est supportée par SQL Server Express (with Adanced Services), et de ce fait :

  • On ne dispose pas de l’Agent SQL Server
  • On ne peut pas utiliser des plans de maintenance pour gérer les sauvegardes
  • On ne peut pas envoyer d’emails
  • La compression des fichiers de backup, introduite avec SQL Server 2008, n’est pas prise en charge

Si donc on souhaite planifier les sauvegardes de base de données, et envoyer un email en cas d’échec, on peut :

  • Remplacer un job de l’Agent SQL Server par une tâche du Planificateur de Tâches de Windows
  • Écrire une procédure stockée d’assembly pour envoyer des emails

Cet article détaille les étapes à suivre pour ce faire.

Une procédure stockée d’assembly pour envoyer des emails

Si l’Agent SQL Server n’est pas disponible, l’intégration CLR l’est. Ainsi en quelques lignes de code C#, on peut envoyer des e-mails de façon assez similaire à ce qu’on peut faire sous les éditions non-Express de SQL Server avec la procédure stockée système msdb.dbo.sp_send_dbmail.

Pour créer une procédure stockée d’assembly sous Visual Studio 2012 ou ultérieur, il faut tout d’abord installer SQL Server Data Tools. Pour ce faire, vous pouvez lire ce billet. Notez qu’il n’est pas nécessaire d’avoir Visual Studio déjà installé.

Une fois Visual Studio démarré, il suffit de créer un nouveau projet à partir de la page de démarrage, ou bien de suivre File > New > Project, et de choisir un projet SQL Server :

Il faut faire attention à la version du framework .NET. Par exemple, si l’on souhaite que l’assembly soit exécutable par SQL Server 2008, il faut spécifier la version 3.5. Une fois que l’on a validé le tout par OK, nous pouvons ajouter une procédure stockée :

Comme je préfère C# à VB.NET, j’ai choisi une procédure stockée C#, mais si vous préférez VB.NET, rien ne s’y oppose. Notons que par défaut, le langage de la procédure stockée n’est pas pré-sélectionné.

Une fois que l’on a validé le tout par un clic sur Add, on peut écrire :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using System;
using System.Net;
using System.Net.Mail;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;

public partial class StoredProcedures
{
    [Microsoft.SqlServer.Server.SqlProcedure]
    public static void spasb_mail_send (string sender, string recipients, string subject, string body)
    {
        SmtpClient mailServer = new SmtpClient("mail.myCompany.com");

        MailMessage mail = new MailMessage(sender, recipients, subject, body);
        mail.IsBodyHtml = true;

        mailServer.Send(mail);
    }
}

Ce qui, même pour l’ultra-débutant C# que je suis, est loin d’être complexe. Avant de builder le projet, il nous faut configurer l’assembly pour lui permettre d’accéder à des ressources externes, ce qui se fait après un clic-droit sur le projet, et choisi Properties (ou pour les aficionados du raccourci clavier, ALT + Entrée). Dans la page SQLCLR, nous commutons donc la liste déroulante Permission Level sur EXTERNAL_ACCESS. De la même façon, dans la page SQLCLR Build, nous commutons la liste déroulante Configuration sur Release, ce qui permet un léger gain de performances. Ce n’est pas très important dans notre cas, mais si l’assembly est plus complexe, cela peut le devenir.

Après un clic sur Build > Build Solution, nous pouvons aller chercher le fichier dll dans le dossier de la solution. Dans cet exemple, il s’agit de D:\SQLServer\Assemblies\SQLServerExpressDatabaseMail\SQLServerExpressDatabaseMail\bin\Debug, et le fichier est nommé SQLServerExpressDatabaseMail.dll.

De retour dans SQL Server Management Studio, une fois connecté à l’instance SQL Server Express, il est tout d’abord nécessaire d’activer la prise en charge de la CLR par cette dernière. Cela se fait sans redémarrage de l’instance, en quelques instruction :

1
2
3
4
EXEC sp_configure 'clr enabled', 1
GO
RECONFIGURE
GO

Il est maintenant requis de déclarer la base de données dans laquelle nous allons enregistrer la procédure stockée d’assembly comme « digne de confiance ». Ceci nous permet d’emprunter l’identité de SQL Server pour exécuter cette procédure qui va accéder à des ressources en dehors de la base de données : en l’occurrence, un serveur de mails (mais ce peut être le système de fichiers, un webservice, …). Là encore, une simple commande suffit :

1
2
3
ALTER DATABASE DBA
SET TRUSTWORTHY ON
GO

Nous devons maintenant enregistrer l’assembly dans la base de données, ce que l’on peut faire une fois que l’on a copié l’assembly dans un dossier de notre choix, sur le serveur hébergeant l’instance SQL Server Express / WSUS. Dans cet exemple, nous utilisons le dossier C:\SQLServer\.

1
2
3
4
5
6
7
USE DBA
GO

CREATE ASSEMBLY SQLServerExpressDatabaseMail
FROM 'C:\SQLServer\SQLServerExpressDatabaseMail.dll'
WITH PERMISSION_SET = EXTERNAL_ACCESS
GO

On peut vérifier l’enregistrement de l’assembly en interrogeant la vue système sys.assemblies. En ce point, on n’a plus besoin du fichier dll : le binaire de celle-ci est maintenant enregistré dans la base de données (de nom DBA dans cet exemple).
Créons maintenant la procédure stockée :

1
2
3
4
5
6
7
CREATE PROCEDURE dbo.spasb_mail_send
        @_sender nvarchar(256)
        , @_recipients nvarchar(256)
        , @_subject nvarchar(256)
        , @_body nvarchar(4000)
AS EXTERNAL NAME SQLServerExpressDatabaseMail.StoredProcedures.spasb_mail_send
GO

Et testons enfin :

1
2
3
4
5
EXEC dbo.spasb_mail_send
        'it@myCompany.com'
        , 'me@myCompany.com'
        , 'Test Mail from Assembly Stored Procedure'
        , 'Hey Hey !'

Une procédure stockée pour gérer les sauvegardes

Voici une procédure stockée générique qui permet de sauvegarder une base de données en particulier, ou toutes les bases de données hébergées par une instance SQL Server. Dans le second cas, les sauvegardes du fichier du journal des transactions n’est pas supportée. En revanche, on peut exécuter cette procédure stockée sur toutes les versions de SQL Server, à partir de 2005. Enfin, le cryptage des sauvegardes, introduit avec SQL Server 2014 n’est pas supporté non plus. Cela exige que quelques prérequis soient en place, et ceux-là sortent du cadre de cet article.

La procédure stockée d’assembly est référencée dans le bloc CATCH final : pour toute erreur, on envoie un email.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
CREATE PROCEDURE dbo.databaseBackup_manage
        @_backup_type varchar(12)
        , @_database_name varchar(128) = NULL
        , @_backup_server_network_path varchar(256)
        , @_print_only bit = 0
AS
BEGIN
        SET NOCOUNT ON

        DECLARE @sql nvarchar(4000)
                , @err_msg nvarchar(2048)
                , @compression bit
                , @checkum bit
                , @mail_subject nvarchar(4000) = @@SERVERNAME + ' Backups'

        -- SQL Server 2005 ne supporte pas l'affectation directe de valeurs*
        -- à des variables lors de la déclaration
        SELECT  @compression = 0

        BEGIN TRY
                -- Gestion de la compression des sauvegardes
                IF
                (
                        (
                                -- SQL Server 2008 Enterprise et Developer
                                CAST(SERVERPROPERTY('ProductVersion') AS varchar(32)) LIKE '10.0%'
                                AND
                                (
                                        CAST(SERVERPROPERTY('Edition') AS varchar(64)) LIKE 'Enterprise%'
                                        OR CAST(SERVERPROPERTY('Edition') AS varchar(64)) LIKE 'Developer%'
                                )
                        )
                        OR
                        (
                                -- SQL Server 2008 R2 Enterprise, Standard et Developer
                                -- Et toute version après celle-ci
                                (
                                        CAST(SERVERPROPERTY('ProductVersion') AS varchar(32)) LIKE '10.50%'
                                        OR CAST(REPLACE(LEFT(CAST(SERVERPROPERTY('ProductVersion') AS varchar(32)), 2), '.', '') AS tinyint) > 10
                                )
                                AND
                                (
                                        CAST(SERVERPROPERTY('Edition') AS varchar(64)) LIKE 'Enterprise%'
                                        OR CAST(SERVERPROPERTY('Edition') AS varchar(64)) LIKE 'Standard%'
                                        OR CAST(SERVERPROPERTY('Edition') AS varchar(64)) LIKE 'Developer%'
                                )
                        )
                )
                BEGIN
                        SET @compression = 1
                END
       
                -- Vérifie que le chemin réseau commence par un double anti-slash
                IF @_backup_server_network_path NOT LIKE '\\%'
                BEGIN
                        SET @err_msg = 'The network path ' + @_backup_server_network_path + ' is not valid.'
                        RAISERROR(@err_msg, 16, 1)
                END
       
                -- Ajoute l'anti-slash à la fin du chemin réseau, le cas échéant
                SELECT @_backup_server_network_path = @_backup_server_network_path + CASE
                        WHEN CHARINDEX('\', REVERSE(@_backup_server_network_path)) > 1 THEN '\'
                        ELSE ''
                END

                IF @_database_name IS NOT NULL
                BEGIN
                        --------------------------------------------------
                        -- Sauvegarde d'une base de données en particulier
                        --------------------------------------------------
                        IF NOT EXISTS
                        (
                                SELECT  *
                                FROM    sys.databases
                                WHERE   name = @_database_name
                        )
                        BEGIN
                                SET @err_msg = 'La base de données ''' + @_database_name + ''' n''existe pas.'
                                RAISERROR(@err_msg, 16, 1)
                        END

                        IF NOT EXISTS
                        (
                                SELECT  *
                                FROM    sys.databases
                                WHERE   name = @_database_name
                                AND     state_desc = 'ONLINE'
                                AND     user_access_desc = 'MULTI_USER'
                                AND     source_database_id IS NULL -- La base de données n'est pas un snapshot
                                AND     database_id  2 -- La base de données n'est pas TempDB
                        )
                        BEGIN
                                SET @err_msg = 'La base de données ''' + @_database_name + ''' n''est pas disponible ou est une capture intantanée.'
                                RAISERROR(@err_msg, 16, 1)
                        END

                        -- Vérifie le type de backup
                        IF @_backup_type NOT IN('FULL', 'DIFFERENTIAL', 'LOG')
                        BEGIN
                                SET @err_msg = 'The backup type ''' + @_backup_type + ''' is invalid.'
                                RAISERROR(@err_msg, 16, 1)
                        END

                        SET @sql = 'BACKUP ' + CASE
                                        WHEN @_backup_type IN ('FULL', 'DIFFERENTIAL') THEN 'DATABASE '
                                        WHEN @_backup_type = 'LOG' THEN 'LOG '
                                END + @_database_name
                                + ' TO DISK = ''' + @_backup_server_network_path + REPLACE(@@SERVERNAME, '\', '_') + '\' + @_database_name + '_' + @_backup_type + '_'
                                + REPLACE(REPLACE(CONVERT(char(19), GETDATE(), 120), ':', '-'), ' ', '_') + '.bak'''
                                + ' WITH ' + CASE @_backup_type
                                        WHEN 'DIFFERENTIAL' THEN 'DIFFERENTIAL, '
                                        WHEN 'FULL' THEN 'INIT, '
                                        ELSE ''
                                END + CASE @compression WHEN 1 THEN 'COMPRESSION, ' ELSE '' END
                                + 'CHECKSUM'

                        IF @_print_only = 1
                        BEGIN
                                PRINT @sql
                        END
                        ELSE
                        BEGIN
                                BEGIN TRY
                                        EXEC sp_executesql @sql
                                END TRY
                                BEGIN CATCH
                                        SET @err_msg = @_database_name + ' | ' + ERROR_MESSAGE()
                                        RAISERROR(@err_msg, 16, 1)
                                END CATCH
                        END
                END
                ELSE
                BEGIN
                        --------------------------------------------------------
                        -- Sauvegarde de toutes les bases de données disponibles
                        --------------------------------------------------------
                        IF @_backup_type = 'LOG'
                        BEGIN
                                SET @err_msg = 'On ne prend pas une sauvegarde du fichier du journal des transactions de toutes les bases de données.'
                                RAISERROR(@err_msg, 16, 1)
                        END

                        DECLARE @database_list TABLE
                        (
                                database_name varchar(128)
                        )

                        INSERT  INTO @database_list
                        (
                                database_name
                        )
                        SELECT  REPLACE(name, ' ', '') -- Certaines bases de données ne respectent pas la convention de nommage SQL ANSI.
                        FROM    sys.databases
                        WHERE   source_database_id IS NULL -- La base de données n'est pas un snapshot
                        AND     state_desc = 'ONLINE'
                        AND     user_access_desc = 'MULTI_USER'
                        AND     database_id  2 -- On exclut TempDB

                        WHILE EXISTS
                        (
                                SELECT  *
                                FROM    @database_list
                        )
                        BEGIN
                                SELECT  TOP 1 @_database_name = database_name
                                FROM    @database_list

                                SET @sql = 'BACKUP DATABASE ' + @_database_name
                                        + ' TO DISK = ''' + @_backup_server_network_path + REPLACE(@@SERVERNAME, '\', '_') + '\' + @_database_name + '_' + @_backup_type + '_'
                                        + REPLACE(REPLACE(CONVERT(char(19), GETDATE(), 120), ':', '-'), ' ', '_') + '.bak'''
                                        + ' WITH ' + CASE @_backup_type
                                                WHEN 'DIFFERENTIAL' THEN 'DIFFERENTIAL, '
                                                WHEN 'FULL' THEN 'INIT, '
                                        END + CASE @compression WHEN 1 THEN 'COMPRESSION, ' ELSE '' END
                                        + 'CHECKSUM'

                                IF @_print_only = 1
                                BEGIN
                                        PRINT @sql
                                END
                                ELSE
                                BEGIN
                                        BEGIN TRY
                                                EXEC sp_executesql @sql
                                        END TRY
                                        BEGIN CATCH
                                                SET @err_msg = @_database_name + ' | ' + ERROR_MESSAGE()
                                                RAISERROR(@err_msg, 16, 1)
                                        END CATCH
                                END            
                       
                                DELETE  FROM @database_list
                                WHERE   database_name = @_database_name
                        END
                END
        END TRY
        BEGIN CATCH
                -- Envoie un email en cas d'erreur
                EXEC dbo.spasb_mail_send
                        @_sender = 'it@myCompany.com'
                        , @_recipients = 'it@myCompany.com'
                        , @_subject = @mail_subject
                        , @_body = @err_msg
        END CATCH
END

Remplacer l’Agent SQL Server par le Planificateur de Tâches de Windows

Nous voilà à l’étape finale : prendre des sauvegardes de base de données régulièrement. Pour ce faire, on peut tout à fait utiliser le Planificateur de Tâches de Windows : en effet, on peut appeler l’utilitaire en ligne de commandes SQLCMD (la documentation, l’utilisation, et un exemple), qui permet d’interagir avec une instance SQL Server. Cet outil permet de passer en paramètre un script, d’exécuter une requête, mais surtout d’enregistrer la sortie dans un fichier. Cette dernière fonctionnalité facilite la tâche d’investigation sur l’échec d’exécution de code.

Une fois l’application Planificateur de Tâches démarrée, on créé une tâche assez simplement :

Une fois passé la première page ou nous nommons la tâche, un clic sur Next nous amène à la planification. Pour Windows Server Update Services, on peut estimer qu’une sauvegarde complète par jour est suffisante. Évidemment, cela varie avec la criticité de l’application. Nous spécifions donc une exécution quotidienne :

Nous planifions l’exécution à minuit et une minute :

Et nous choisissons de démarrer une application :

Dans la saisie texte Program / Script, nous spécifions l’invite de commandes. En ce qui concerne les arguments, nous écrivons :

  • Pour SQL Server 2008 :
    "C:\Program Files\Microsoft SQL Server\100\Tools\Binn\SQLCMD.EXE"
  • Pour SQL Server 2012 :
    "C:\Program Files\Microsoft SQL Server\110\Tools\Binn\SQLCMD.EXE"
  • Pour SQL Server 2014 :
    "C:\Program Files\Microsoft SQL Server\Client SDK\ODBC\110\Tools\Binn\SQLCMD.EXE"

En ce qui concerne les arguments, nous passons :

sqlcmd -E -S WSUS -i "C:\SQLServer\WSUSBackup.sql" -o "C:\SQLServerBackup\FullBackupLog.txt"

Attention, la casse des options est importante.

  • -E pour spécifier une connexion approuvée
  • -S : l’instance SQL Server à laquelle on souhaite se connecter
  • -i : le chemin et le nom du fichier de script T-SQL
  • -o : le fichier qui capturera les libellés des messages d’information, d’avertissement ou d’erreurs, le cas échéant

Ce qui donne :

Un clic sur Next nous offre une fenêtre de résumé, et on peut valider le tout. Si l’on souhaite ajouter des planifications ou modifier d’autres options pour cette tâche, il suffit de cocher la case Open the Properties dialog for this task when I click Finish.

Nous créons ensuite un fichier WSUSBackup.sql dans un répertoire C:\SQLServer\ sur la machine qui héberge l’application WSUS. Ce fichier appelle la procédure stockée DBA.dbo.databaseBackup_manage comme suit :

EXEC DBA.dbo.databaseBackup_manage 'FULL', NULL, '\\monPartageDeBackup\', 0

Et voilà nos sauvegardes de base de données en place.

Utiliser PowerShell pour supprimer les fichiers de sauvegardes trop anciens

Dans la liste des actions de la tâche que nous venons de créer, nous pouvons en ajouter une qui aura :

  • Pour programme : powershell
  • Pour arguments : -file "C:\SQLServer\DeleteOldBackupFiles.ps1"

Le fichier DeleteOldBackupFiles.ps1 contient la commande suivante, qui supprime tous les fichiers créés il y a plus de 7 jours, y compris dans les sous-dossiers du partage de backup (-Recurse) :

Get-ChildItem '\\monPartageDeBackup\' -Recurse | Where {$_.CreationTime -lt (Get-Date).AddDays(-7)} | Remove-Item -Force

Par défaut, PowerShell ne permet pas l’exécution de scripts. Pour l’autoriser, il faut démarrer l’interface PowerShell, et exécuter Set-ExecutionPolicy RemoteSigned (en accord avec votre Administrateur Systèmes !).

Retenons également que les statistiques de colonne et d’index de la base de données supportant WSUS ne sont pas maintenues. Ce sera probablement suffisant pour générer une lenteur après quelques mois d’exploitation, dont votre Administrateur Systèmes vous demandera l’origine ;)

ElSüket.

Laisser un commentaire