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.