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

patrick-canterino.de