]> git.p6c8.net - dsmonrot.git/blob - dsmonrot.ps1
8cefa451e8988a8789cc3ea23a30ed12bbdb7f3b
[dsmonrot.git] / dsmonrot.ps1
1 # DSMonRot
2 # Script for rotating Drive Snapshot backups monthly
3 #
4 # Author: Patrick Canterino <patrick@patrick-canterino.de>
5 # WWW: https://www.patrick-canterino.de/
6 # https://github.com/pcanterino/dsmonrot
7 # License: 2-Clause BSD License
8 #
9 # Drive Snapshot is copyright by Tom Ehlert
10 # http://www.drivesnapshot.de/
11
12 # Config
13
14 # Path to backup directory
15 # This directory MUST exist, it is not created automatically!
16 [String]$backupDir = "Z:\"
17 # Disks to backup, see http://www.drivesnapshot.de/en/commandline.htm
18 [String]$disksToBackup = "HD1:1"
19 # Path to Drive Snapshot
20 [String]$dsPath = "C:\Users\Patrick\Desktop\DSMonRot\snapshot.exe"
21 # Keep backups for this amount of months (excluding the current month),
22 # -1 for indefinite
23 [Int32]$keepMonths = 2
24 # Rotate BEFORE the beginning of a full backup (default is after a successful
25 # full backup)
26 # WARNING: If this option is set to $True and the full backup fails you could
27 # have NO backup
28 [Boolean]$rotateBeforeBackup = $False
29 # Set to $True if you want to allow multiple backups for a day
30 [Boolean]$multipleDailyBackups = $False
31 # Path to Drive Snapshot log file (specify only the file name if you set
32 # $dsLogFileToBackup to $True)
33 #[String]$dsLogFile = "C:\Users\Patrick\Desktop\DSMonRot\snapshot.log"
34 [String]$dsLogFile = "snapshot.log"
35 # Set to $True if you want to put the log file of Drive Snapshot into the same
36 # directory as the backup
37 [Boolean]$dsLogFileToBackup = $True
38 # Path to directory where DSMonRot stores the log files
39 # Every month a new log file is created
40 [String]$logDir = "$PSScriptRoot\log"
41 # Keep log files for this amount of months (excluding the current month),
42 # 0 or less for indefinite
43 # You should set this to at least the same as $keepMonths
44 [Int32]$keepLogs = 2
45 # Comma separated lists of files and directories to exclude from the backup
46 # See http://www.drivesnapshot.de/en/commandline.htm
47 # Comment out if you don't want to use it
48 #[String]$excludedPaths = "Path1,Path2"
49
50 # Map network share to this drive letter, comment out if you don't want to use it
51 [String]$smbDrive = "Z"
52 # Path to network share
53 [String]$smbPath = "\\192.168.0.3\ds"
54 # User and password for connecting to network share, comment out if you don't want to use it
55 # (for example if you want to pass current Windows credentials)
56 [String]$smbUser = "patrick"
57 [String]$smbPassword = ""
58
59 # Send an email if an error occured
60 [Boolean]$emailOnError = $True
61 # From address of email notification
62 [String]$emailFromAddress = "alarm@test.local"
63 # To address of email notification
64 [String]$emailToAddress = "patrick@test.local"
65 # Subject of email notification
66 [String]$emailSubject = "DSMonRot on $env:computername"
67 # Mail server
68 [String]$emailMailserver = "localhost"
69 # SMTP Port
70 [Int32]$emailPort = 25
71 # Use SSL?
72 [Boolean]$emailSSL = $False
73 # Use SMTP Auth?
74 [Boolean]$emailAuth = $False
75 # SMTP Auth User
76 [String]$emailUser = ""
77 # SMTP Auth Password
78 [String]$emailPassword = ""
79
80 # End of config
81
82 <#
83 .Synopsis
84 Write-Log writes a message to a specified log file with the current time stamp.
85 .DESCRIPTION
86 The Write-Log function is designed to add logging capability to other scripts.
87 In addition to writing output and/or verbose you can write to a log file for
88 later debugging.
89 .NOTES
90 Created by: Jason Wasser @wasserja
91 Modified: 11/24/2015 09:30:19 AM
92
93 Changelog:
94 * Code simplification and clarification - thanks to @juneb_get_help
95 * Added documentation.
96 * Renamed LogPath parameter to Path to keep it standard - thanks to @JeffHicks
97 * Revised the Force switch to work as it should - thanks to @JeffHicks
98
99 To Do:
100 * Add error handling if trying to create a log file in a inaccessible location.
101 * Add ability to write $Message to $Verbose or $Error pipelines to eliminate
102 duplicates.
103 .PARAMETER Message
104 Message is the content that you wish to add to the log file.
105 .PARAMETER Path
106 The path to the log file to which you would like to write. By default the function will
107 create the path and file if it does not exist.
108 .PARAMETER Level
109 Specify the criticality of the log information being written to the log (i.e. Error, Warning, Informational)
110 .PARAMETER NoClobber
111 Use NoClobber if you do not wish to overwrite an existing file.
112 .EXAMPLE
113 Write-Log -Message 'Log message'
114 Writes the message to c:\Logs\PowerShellLog.log.
115 .EXAMPLE
116 Write-Log -Message 'Restarting Server.' -Path c:\Logs\Scriptoutput.log
117 Writes the content to the specified log file and creates the path and file specified.
118 .EXAMPLE
119 Write-Log -Message 'Folder does not exist.' -Path c:\Logs\Script.log -Level Error
120 Writes the message to the specified log file as an error message, and writes the message to the error pipeline.
121 .LINK
122 https://gallery.technet.microsoft.com/scriptcenter/Write-Log-PowerShell-999c32d0
123 #>
124 function Write-Log
125 {
126 [CmdletBinding()]
127 Param
128 (
129 [Parameter(Mandatory=$true,
130 ValueFromPipelineByPropertyName=$true)]
131 [ValidateNotNullOrEmpty()]
132 [Alias("LogContent")]
133 [string]$Message,
134
135 [Parameter(Mandatory=$false)]
136 [Alias('LogPath')]
137 [string]$Path='C:\Logs\PowerShellLog.log',
138
139 [Parameter(Mandatory=$false)]
140 [ValidateSet("Error","Warn","Info")]
141 [string]$Level="Info",
142
143 [Parameter(Mandatory=$false)]
144 [switch]$NoClobber
145 )
146
147 Begin
148 {
149 # Set VerbosePreference to Continue so that verbose messages are displayed.
150 $VerbosePreference = 'Continue'
151 }
152 Process
153 {
154
155 # If the file already exists and NoClobber was specified, do not write to the log.
156 if ((Test-Path $Path) -AND $NoClobber) {
157 Write-Error "Log file $Path already exists, and you specified NoClobber. Either delete the file or specify a different name."
158 Return
159 }
160
161 # If attempting to write to a log file in a folder/path that doesn't exist create the file including the path.
162 elseif (!(Test-Path $Path)) {
163 Write-Verbose "Creating $Path."
164 $NewLogFile = New-Item $Path -Force -ItemType File
165 }
166
167 else {
168 # Nothing to see here yet.
169 }
170
171 # Format Date for our Log File
172 $FormattedDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
173
174 # Write message to error, warning, or verbose pipeline and specify $LevelText
175 switch ($Level) {
176 'Error' {
177 Write-Error $Message
178 $LevelText = 'ERROR:'
179 }
180 'Warn' {
181 Write-Warning $Message
182 $LevelText = 'WARNING:'
183 }
184 'Info' {
185 Write-Verbose $Message
186 $LevelText = 'INFO:'
187 }
188 }
189
190 # Write log entry to $Path
191 "$FormattedDate $LevelText $Message" | Out-File -FilePath $Path -Append
192 }
193 End
194 {
195 }
196 }
197
198 function Send-Email([String]$body) {
199 try {
200 # Allow SMTP with SSL and SMTP Auth
201 # see: http://petermorrissey.blogspot.de/2013/01/sending-smtp-emails-with-powershell.html
202
203 $smtp = New-Object System.Net.Mail.SmtpClient($emailMailServer, $emailPort)
204
205 $smtp.EnableSSL = $emailSSL
206
207 if($emailAuth) {
208 $smtp.Credentials = New-Object System.Net.NetworkCredential($emailUser, $emailPassword)
209 }
210
211 $smtp.Send($emailFromAddress, $emailToAddress, $emailSubject, $body)
212 }
213 catch {
214 Write-Log "Could not send email: $_.Exception.Message" -Path $logFile -Level Error
215 }
216 }
217
218 function Rotate-Backup {
219 if($keepMonths -lt 0) {
220 return
221 }
222
223 $keepMonthsCount = $keepMonths
224
225 Get-ChildItem $backupDir -Directory | Where-Object {($_.Name -ne $currMonth) -and ($_.Name -match "^\d{4,}-\d{2}$")} | Sort-Object -Descending |
226 Foreach-Object {
227 if($keepMonthsCount -ge 0) {
228 $keepMonthsCount--
229 }
230
231 if($keepMonthsCount -eq -1) {
232 Write-Log "Deleting backup $($_.FullName)" -Path $logFile -Level Info
233 Remove-Item -Recurse -Force $_.FullName
234 }
235 }
236 }
237
238 function Rotate-Log {
239 if($keepLogs -le 0) {
240 return
241 }
242
243 $keepLogsCount = $keepLogs
244
245 Get-ChildItem $logDir -File | Where-Object {($_.Name -ne "$currMonth.log") -and ($_.Name -match "^\d{4,}-\d{2}\.log$")} | Sort-Object -Descending |
246 Foreach-Object {
247 if($keepLogsCount -ge 0) {
248 $keepLogsCount--
249 }
250
251 if($keepLogsCount -eq -1) {
252 Write-Log "Deleting log file $($_.FullName)" -Path $logFile -Level Info
253 Remove-Item -Force $_.FullName
254 }
255 }
256 }
257
258 $dsAdditionalArgs = @("--UseVSS")
259
260 $errorMessages = @()
261
262 $smbConnected = $False
263 $success = $False
264 $isDiff = $False
265 $dsCommand = ""
266
267 $currMonth = Get-Date -format "yyyy-MM"
268 $currDay = Get-Date -format "yyyy-MM-dd"
269 $currTime = Get-Date -format "HH-mm-ss" # no colon because we need this for a directory name
270
271 # Check if the directory for the log files exists and create it if necessary
272 if(!(Test-Path $logDir)) {
273 try {
274 New-Item -ItemType directory -Path $logDir -ErrorAction Stop | Out-Null
275 }
276 catch {
277 Write-Error "Could not create log directory $logDir`: $_.Exception.Message"
278 $errorMessages += "Could not create log directory $logDir`: $_.Exception.Message"
279 }
280 }
281
282 $logFile = "$logDir\$currMonth.log"
283
284 # Continue only if the log directory exists or if it was created successfully (no error message added)
285 if($errorMessages.Count -eq 0) {
286 $startTime = Get-Date -format "yyyy-MM-dd HH:mm:ss"
287 Write-Log "Started at $startTime" -Path $logFile
288
289 # Connect the network drive if necessary
290 if($smbDrive) {
291 Write-Log "Connecting network drive $smbDrive to $smbPath" -Path $logFile
292
293 try {
294 if($smbUser -and $smbPassword) {
295 Write-Log "Connecting to network drive with credentials" -Path $logFile
296
297 $secSmbPassword = $smbPassword | ConvertTo-SecureString -asPlainText -Force
298 $smbCredential = New-Object System.Management.Automation.PSCredential($smbUser, $secSmbPassword)
299
300 New-PSDrive -Name $smbDrive -PSProvider "FileSystem" -Root $smbPath -Credential $smbCredential -Persist -ErrorAction Stop | Out-Null
301 }
302 else {
303 Write-Log "Connecting to network drive without credentials" -Path $logFile
304
305 New-PSDrive -Name $smbDrive -PSProvider "FileSystem" -Root $smbPath -Persist -ErrorAction Stop | Out-Null
306 }
307
308 $smbConnected = $True
309 }
310 catch {
311 Write-Log "Could not connect to network drive $smbDrive`: $_.Exception.Message" -Path $logFile -Level Error
312 $errorMessages += "Could not connect to network drive $smbDrive`: $_.Exception.Message"
313 }
314 }
315
316 # Check if the backup directory exists
317 if(!(Test-Path $backupDir)) {
318 Write-Log "Directory $backupDir does not exist!" -Path $logFile -Level Error
319 $errorMessages += "Directory $backupDir does not exist!"
320 }
321
322 # Continue only if no error message was recorded (i.e. backup directory does not exist)
323 if($errorMessages.Count -eq 0) {
324 # Compose the backup target directories
325 $backupTarget = $backupDir + "\" + $currMonth
326 $backupTargetFull = $backupTarget + "\Full"
327
328 $backupTargetDiff = $backupTarget + "\Diff-" + $currDay
329
330 if($multipleDailyBackups) {
331 $backupTargetDiff = $backupTargetDiff + "-" + $currTime
332 }
333
334 # Compose the "exclude" parameter if necessary
335 if($excludedPaths) {
336 $dsAdditionalArgs += "--exclude:" + $excludedPaths
337 }
338
339 # Check if the backup target for this month, the directory for the full backup
340 # and the hash files exists. In this case we do a differential backup.
341 if((Test-Path $backupTarget) -and (Test-Path $backupTargetFull) -and (Test-Path "$backupTargetFull\*.hsh")) {
342 # Do a differential backup
343
344 Write-Log "Doing a differential backup" -Path $logFile
345
346 $isDiff = $True
347
348 if(!(Test-Path $backupTargetDiff)) {
349 try {
350 New-Item -ItemType directory -Path $backupTargetDiff -ErrorAction Stop | Out-Null
351 }
352 catch {
353 Write-Log "Could not create directory $backupTargetDiff`: $_.Exception.Message" -Path $logFile -Level Error
354 $errorMessages += "Could not create directory $backupTargetDiff`: $_.Exception.Message"
355 }
356
357 if($errorMessages.Count -eq 0) {
358 $dsLogPath = if($dsLogFileToBackup) { "$backupTargetDiff\$dsLogFile" } else { $dsLogFile }
359
360 $dsArgs = @($disksToBackup, "--logfile:$dsLogPath", "$backupTargetDiff\`$disk.sna", "-h$backupTargetFull\`$disk.hsh") + $dsAdditionalArgs
361 $dsCommand = "$dsPath $dsArgs"
362
363 Write-Log $dsCommand -Path $logFile
364
365 & $dsPath $dsArgs
366
367 if($LastExitCode -ne 0) {
368 Write-Log "Drive Snapshot failed to backup! Exit code: $LastExitCode" -Path $logFile -Level Error
369 $errorMessages += "Drive Snapshot failed to backup! Exit code: $LastExitCode"
370 }
371 else {
372 Write-Log "Drive Snapshot succeeded!" -Path $logFile
373 $success = $True
374 }
375 }
376 }
377 else {
378 Write-Log "Directory $backupTargetDiff already exists!" -Path $logFile -Level Error
379 $errorMessages += "Directory $backupTargetDiff already exists!"
380 }
381 }
382 else {
383 # Do a full backup
384
385 Write-Log "Doing a full backup" -Path $logFile
386
387 if(!(Test-Path $backupTarget)) {
388 try {
389 New-Item -ItemType directory -Path $backupTarget -ErrorAction Stop | Out-Null
390 }
391 catch {
392 Write-Log "Could not create directory $backupTarget`: $_.Exception.Message" -Path $logFile -Level Error
393 $errorMessages += "Could not create directory $backupTarget`: $_.Exception.Message"
394 }
395 }
396
397 if($errorMessages.Count -eq 0) {
398 if(!(Test-Path $backupTargetFull)) {
399 try {
400 New-Item -ItemType directory -Path $backupTargetFull -ErrorAction Stop | Out-Null
401 }
402 catch {
403 Write-Log "Could not create directory $backupTargetFull`: $_.Exception.Message" -Path $logFile -Level Error
404 $errorMessages += "Could not create directory $backupTargetFull`: $_.Exception.Message"
405 }
406 }
407
408 if($errorMessages.Count -eq 0) {
409 if($rotateBeforeBackup) {
410 Rotate-Backup
411 }
412
413 $dsLogPath = if($dsLogFileToBackup) { "$backupTargetFull\$dsLogFile" } else { $dsLogFile }
414
415 $dsArgs = @($disksToBackup, "--logfile:$dsLogPath", "$backupTargetFull\`$disk.sna") + $dsAdditionalArgs
416 $dsCommand = "$dsPath $dsArgs"
417
418 Write-Log $dsCommand -Path $logFile
419
420 & $dsPath $dsArgs
421
422 if($LastExitCode -ne 0) {
423 Write-Log "Drive Snapshot failed to backup! Exit code: $LastExitCode" -Path $logFile -Level Error
424 $errorMessages += "Drive Snapshot failed to backup! Exit code: $LastExitCode"
425 }
426 else {
427 Write-Log "Drive Snapshot succeeded!" -Path $logFile
428 $success = $True
429 }
430
431 if($rotateBeforeBackup -eq $False -and $success -eq $True) {
432 Rotate-Backup
433 }
434 }
435 }
436 }
437 }
438
439 # Disconnect the network drive if necessary
440 if($smbConnected) {
441 Write-Log "Disconnecting network drive" -Path $logFile
442
443 try {
444 Remove-PSDrive $smbDrive -ErrorAction Stop
445 }
446 catch {
447 Write-Log "Could not disconnect network drive $smbDrive`: $_.Exception.Message" -Path $logFile -Level Error
448 $errorMessages += "Could not disconnect network drive $smbDrive`: $_.Exception.Message"
449 }
450 }
451
452 # Rotate the log files
453 Rotate-Log
454 }
455
456 # If there was any error message recorded, send a mail if configured
457 if($emailOnError -and $errorMessages.Count -gt 0) {
458 $emailBody = "This is DSMonRot on $env:computername, started at $startTime.`n"
459 $emailBody += "An error occured while performing a backup. Below are the error messages and some status information.`n`n"
460 $emailBody += "Backup directory: $backupDir`n"
461 $emailBody += "Log directory: $logDir`n"
462 $emailBody += "Current log file: $logFile`n"
463 $emailBody += "Differential backup: $isDiff`n"
464 $emailBody += "Backup successful: $success`n"
465 $emailBody += "Drive Snapshot command: $dsCommand`n`n"
466 $emailBody += ($errorMessages -join "`n")
467
468 Send-Email $emailBody
469 }
470
471 $endTime = Get-Date -format "yyyy-MM-dd HH:mm:ss"
472 Write-Log "Ended at $endTime" -Path $logFile

patrick-canterino.de