Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

392 rindas
15KB

  1. #
  2. # Restic Windows Backup Script
  3. #
  4. # =========== start configuration =========== #
  5. # set restic configuration parmeters (destination, passwords, etc.)
  6. $SecretsScript = Join-Path $PSScriptRoot "secrets.ps1"
  7. # backup configuration variables
  8. $ConfigScript = Join-Path $PSScriptRoot "config.ps1"
  9. # =========== end configuration =========== #
  10. # globals for state storage
  11. $Script:ResticStateRepositoryInitialized = $null
  12. $Script:ResticStateLastMaintenance = $null
  13. $Script:ResticStateLastDeepMaintenance = $null
  14. $Script:ResticStateMaintenanceCounter = $null
  15. # restore backup state from disk
  16. function Get-BackupState {
  17. if(Test-Path $StateFile) {
  18. Import-Clixml $StateFile | ForEach-Object{ Set-Variable -Scope Script $_.Name $_.Value }
  19. }
  20. }
  21. function Set-BackupState {
  22. Get-Variable ResticState* | Export-Clixml $StateFile
  23. }
  24. # unlock the repository if need be
  25. function Invoke-Unlock {
  26. Param($SuccessLog, $ErrorLog)
  27. $locks = & $ResticExe list locks --no-lock -q 3>&1 2>> $ErrorLog
  28. if($locks.Length -gt 0) {
  29. # unlock the repository (assumes this machine is the only one that will ever use it)
  30. & $ResticExe unlock 3>&1 2>> $ErrorLog | Tee-Object -Append $SuccessLog
  31. Write-Output "[[Unlock]] Repository was locked. Unlocking. Past script failure?" | Tee-Object -Append $ErrorLog | Tee-Object -Append $SuccessLog
  32. Start-Sleep 120
  33. }
  34. }
  35. # run maintenance on the backup set
  36. function Invoke-Maintenance {
  37. Param($SuccessLog, $ErrorLog)
  38. # skip maintenance if disabled
  39. if($SnapshotMaintenanceEnabled -eq $false) {
  40. Write-Output "[[Maintenance]] Skipped - maintenance disabled" | Tee-Object -Append $SuccessLog
  41. return
  42. }
  43. # skip maintenance if it's been done recently
  44. if(($null -ne $ResticStateLastMaintenance) -and ($null -ne $ResticStateMaintenanceCounter)) {
  45. $Script:ResticStateMaintenanceCounter += 1
  46. $delta = New-TimeSpan -Start $ResticStateLastMaintenance -End $(Get-Date)
  47. if(($delta.Days -lt $SnapshotMaintenanceDays) -and ($ResticStateMaintenanceCounter -lt $SnapshotMaintenanceInterval)) {
  48. Write-Output "[[Maintenance]] Skipped - last maintenance $ResticStateLastMaintenance ($($delta.Days) days, $ResticStateMaintenanceCounter backups ago)" | Tee-Object -Append $SuccessLog
  49. return
  50. }
  51. }
  52. Write-Output "[[Maintenance]] Start $(Get-Date)" | Tee-Object -Append $SuccessLog
  53. $maintenance_success = $true
  54. Start-Sleep 120
  55. # forget snapshots based upon the retention policy
  56. Write-Output "[[Maintenance]] Start forgetting..." | Tee-Object -Append $SuccessLog
  57. & $ResticExe --verbose -q forget $SnapshotRetentionPolicy 3>&1 2>> $ErrorLog | Tee-Object -Append $SuccessLog
  58. if(-not $?) {
  59. Write-Output "[[Maintenance]] Forget operation completed with errors" | Tee-Object -Append $ErrorLog | Tee-Object -Append $SuccessLog
  60. $maintenance_success = $false
  61. }
  62. # prune (remove) data from the backup step. Running this separate from `forget` because
  63. # `forget` only prunes when it detects removed snapshots upon invocation, not previously removed
  64. Write-Output "[[Maintenance]] Start pruning..." | Tee-Object -Append $SuccessLog
  65. & $ResticExe --verbose -q prune 3>&1 2>> $ErrorLog | Tee-Object -Append $SuccessLog
  66. if(-not $?) {
  67. Write-Output "[[Maintenance]] Prune operation completed with errors" | Tee-Object -Append $ErrorLog | Tee-Object -Append $SuccessLog
  68. $maintenance_success = $false
  69. }
  70. # check data to ensure consistency
  71. Write-Output "[[Maintenance]] Start checking..." | Tee-Object -Append $SuccessLog
  72. # check to determine if we want to do a full data check or not
  73. $data_check = @()
  74. if($null -ne $ResticStateLastDeepMaintenance) {
  75. $delta = New-TimeSpan -Start $ResticStateLastDeepMaintenance -End $(Get-Date)
  76. if($delta.Days -ge $SnapshotDeepMaintenanceDays) {
  77. Write-Output "[[Maintenance]] Performing full data check - deep '--read-data' check last ran $ResticStateLastDeepMaintenance ($($delta.Days) days ago)" | Tee-Object -Append $SuccessLog
  78. $data_check = @("--read-data")
  79. $Script:ResticStateLastDeepMaintenance = Get-Date
  80. }
  81. else {
  82. Write-Output "[[Maintenance]] Performing fast data check - deep '--read-data' check last ran $ResticStateLastDeepMaintenance ($($delta.Days) days ago)" | Tee-Object -Append $SuccessLog
  83. }
  84. }
  85. else {
  86. # set the date, but don't do a deep check if we've never done a full data read
  87. $Script:ResticStateLastDeepMaintenance = Get-Date
  88. }
  89. & $ResticExe --verbose -q check @data_check 3>&1 2>> $ErrorLog | Tee-Object -Append $SuccessLog
  90. if(-not $?) {
  91. Write-Output "[[Maintenance]] Check completed with errors" | Tee-Object -Append $ErrorLog | Tee-Object -Append $SuccessLog
  92. $maintenance_success = $false
  93. }
  94. Write-Output "[[Maintenance]] End $(Get-Date)" | Tee-Object -Append $SuccessLog
  95. if($maintenance_success -eq $true) {
  96. $Script:ResticStateLastMaintenance = Get-Date
  97. $Script:ResticStateMaintenanceCounter = 0;
  98. }
  99. }
  100. # Run restic backup
  101. function Invoke-Backup {
  102. Param($SuccessLog, $ErrorLog)
  103. Write-Output "[[Backup]] Start $(Get-Date)" | Tee-Object -Append $SuccessLog
  104. $return_value = $true
  105. # Launch Restic
  106. & $ResticExe --verbose backup --use-fs-snapshot --files-from=$LocalIncludeFile --iexclude-file=$WindowsExcludeFile --iexclude-file=$LocalExcludeFile 3>&1 2>> $ErrorLog | Tee-Object -Append $SuccessLog
  107. Switch ($LastExitCode)
  108. {
  109. 1 {
  110. Write-Output "[[Backup]] Failed with errors, see log for more information" | Tee-Object -Append $ErrorLog | Tee-Object -Append $SuccessLog
  111. $return_value = $false
  112. }
  113. 3 {
  114. if ($FailOnIncomplete) {
  115. Write-Output "[[Backup]] Failed with errors, see log for more information" | Tee-Object -Append $ErrorLog | Tee-Object -Append $SuccessLog
  116. $return_value = $false
  117. } else {
  118. Write-Output "[[Backup]] Completed with errors, see log for more information" | Tee-Object -Append $ErrorLog | Tee-Object -Append $SuccessLog
  119. }
  120. }
  121. }
  122. Write-Output "[[Backup]] End $(Get-Date)" | Tee-Object -Append $SuccessLog
  123. return $return_value
  124. }
  125. function Send-Email {
  126. Param($SuccessLog, $ErrorLog)
  127. $password = ConvertTo-SecureString $ResticEmailPassword -AsPlainText -Force
  128. $credentials = New-Object System.Management.Automation.PSCredential ($ResticEmailUsername, $password)
  129. $status = "SUCCESS"
  130. $success_after_failure = $false
  131. $body = ""
  132. if (($null -ne $SuccessLog) -and (Test-Path $SuccessLog) -and (Get-Item $SuccessLog).Length -gt 0) {
  133. $body = $(Get-Content -Raw $SuccessLog)
  134. # if previous run contained an error, send the success email confirming that the error has been resolved
  135. # (i.e. get previous error log, if it's not empty, trigger the send of the success-after-failure email)
  136. $previous_error_log = Get-ChildItem $LogPath -Filter '*err.txt' | Sort-Object -Descending LastWriteTime | Select-Object -Skip 1 | Select-Object -First 1
  137. if(($null -ne $previous_error_log) -and ($previous_error_log.Length -gt 0)){
  138. $success_after_failure = $true
  139. }
  140. }
  141. else {
  142. $body = "Critical Error! Restic backup log is empty or missing. Check log file path."
  143. $status = "ERROR"
  144. }
  145. $attachments = @{}
  146. if (($null -ne $ErrorLog) -and (Test-Path $ErrorLog) -and (Get-Item $ErrorLog).Length -gt 0) {
  147. $attachments = @{Attachments = $ErrorLog}
  148. $status = "ERROR"
  149. }
  150. if((($status -eq "SUCCESS") -and ($SendEmailOnSuccess -ne $false)) -or ((($status -eq "ERROR") -or $success_after_failure) -and ($SendEmailOnError -ne $false))) {
  151. $subject = "$env:COMPUTERNAME Restic Backup Report [$status]"
  152. Send-MailMessage @ResticEmailConfig -From $ResticEmailFrom -To $ResticEmailTo -Credential $credentials -Subject $subject -Body $body @attachments
  153. }
  154. }
  155. function Invoke-ConnectivityCheck {
  156. Param($SuccessLog, $ErrorLog)
  157. # skip the internet connectivity check for local repos
  158. if(Test-Path $env:RESTIC_REPOSITORY) {
  159. Write-Output "[[Internet]] Skipping internet connectivity check." | Tee-Object -Append $SuccessLog
  160. return $true
  161. }
  162. $repository_host = ''
  163. # use generic internet service for non-specific repo types (e.g. swift:, rclone:, etc. )
  164. if(($env:RESTIC_REPOSITORY -match "^swift:") -or
  165. ($env:RESTIC_REPOSITORY -match "^rclone:")) {
  166. $repository_host = "cloudflare.com"
  167. }
  168. elseif($env:RESTIC_REPOSITORY -match "^b2:") {
  169. $repository_host = "api.backblazeb2.com"
  170. }
  171. elseif($env:RESTIC_REPOSITORY -match "^azure:") {
  172. $repository_host = "azure.microsoft.com"
  173. }
  174. elseif($env:RESTIC_REPOSITORY -match "^gs:") {
  175. $repository_host = "storage.googleapis.com"
  176. }
  177. else {
  178. # parse connection string for hostname
  179. # Uri parser doesn't handle leading connection type info (s3:, sftp:, rest:)
  180. $connection_string = $env:RESTIC_REPOSITORY -replace "^s3:" -replace "^sftp:" -replace "^rest:"
  181. $repository_host = ([System.Uri]$connection_string).host
  182. }
  183. if([string]::IsNullOrEmpty($repository_host)) {
  184. Write-Output "[[Internet]] Repository string could not be parsed." | Tee-Object -Append $SuccessLog | Tee-Object -Append $ErrorLog
  185. return $false
  186. }
  187. # test for internet connectivity
  188. $connections = 0
  189. $sleep_count = $InternetTestAttempts
  190. if (Test-NetMetered) {
  191. if ($BackupoverMetered) {
  192. Write-Output "[[Internet]] Current connection is metered. Change config to DISALLOW backup over metered connection." | Tee-Object -Append $SuccessLog | Tee-Object -Append $Successlog
  193. }
  194. else {
  195. Write-Output "[[Internet]] Current connection is metered. Change config to allow backup over metered." | Tee-Object -Append $SuccessLog | Tee-Object -Append $ErrorLog
  196. return $false
  197. }
  198. }
  199. while($true) {
  200. $connections = Get-NetRoute | Where-Object DestinationPrefix -eq '0.0.0.0/0' | Get-NetIPInterface | Where-Object ConnectionState -eq 'Connected' | Measure-Object | ForEach-Object{$_.Count}
  201. if($sleep_count -le 0) {
  202. Write-Output "[[Internet]] Connection to repository ($repository_host) could not be established." | Tee-Object -Append $SuccessLog | Tee-Object -Append $ErrorLog
  203. return $false
  204. }
  205. if(($null -eq $connections) -or ($connections -eq 0)) {
  206. Write-Output "[[Internet]] Waiting for internet connectivity... $sleep_count" | Tee-Object -Append $SuccessLog
  207. Start-Sleep 30
  208. }
  209. elseif(!(Test-Connection -Server $repository_host -Quiet)) {
  210. Write-Output "[[Internet]] Waiting for connection to repository ($repository_host)... $sleep_count" | Tee-Object -Append $SuccessLog
  211. Start-Sleep 30
  212. }
  213. else {
  214. return $true
  215. }
  216. $sleep_count--
  217. }
  218. }
  219. # check previous logs
  220. function Invoke-HistoryCheck {
  221. Param($SuccessLog, $ErrorLog)
  222. $logs = Get-ChildItem $LogPath -Filter '*err.txt' | ForEach-Object{$_.Length -gt 0}
  223. $logs_with_success = ($logs | Where-Object {($_ -eq $false)}).Count
  224. if($logs.Count -gt 0) {
  225. Write-Output "[[History]] Backup success rate: $logs_with_success / $($logs.Count) ($(($logs_with_success / $logs.Count).tostring("P")))" | Tee-Object -Append $SuccessLog
  226. }
  227. }
  228. #
  229. # .SYNOPSIS
  230. # Returns if current connection is a metered connection or not.
  231. #
  232. # .DESCRIPTION
  233. # This cmdlet checks if connection is metered or not.
  234. #
  235. # This is based on the example in https://msdn.microsoft.com/en-us/library/windows/apps/xaml/jj835821.aspx
  236. #
  237. # .EXAMPLE
  238. # Check if connected to a metered connection
  239. #
  240. # If(Test-NetMetered) { Write-Host "Metered" }
  241. function Test-NetMetered
  242. {
  243. [void][Windows.Networking.Connectivity.NetworkInformation, Windows, ContentType = WindowsRuntime]
  244. $networkprofile = [Windows.Networking.Connectivity.NetworkInformation]::GetInternetConnectionProfile()
  245. if ($networkprofile -eq $null)
  246. {
  247. Write-Warning "Can't find any internet connections!"
  248. return $false
  249. }
  250. $cost = $networkprofile.GetConnectionCost()
  251. if ($cost -eq $null)
  252. {
  253. Write-Warning "Can't find any internet connections with a cost!"
  254. return $false
  255. }
  256. if ($cost.Roaming -or $cost.OverDataLimit)
  257. {
  258. return $true
  259. }
  260. if ($cost.NetworkCostType -eq [Windows.Networking.Connectivity.NetworkCostType]::Fixed -or
  261. $cost.NetworkCostType -eq [Windows.Networking.Connectivity.NetworkCostType]::Variable)
  262. {
  263. return $true
  264. }
  265. if ($cost.NetworkCostType -eq [Windows.Networking.Connectivity.NetworkCostType]::Unrestricted)
  266. {
  267. return $false
  268. }
  269. throw "Network cost type is unknown!"
  270. }
  271. # main function
  272. function Invoke-Main {
  273. # check for elevation, required for creation of shadow copy (VSS)
  274. if (-not (New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator))
  275. {
  276. Write-Error "[[Backup]] Elevation required (run as administrator). Exiting."
  277. exit
  278. }
  279. # initialize secrets
  280. . $SecretsScript
  281. # initialize config
  282. . $ConfigScript
  283. Get-BackupState
  284. if(!(Test-Path $LogPath)) {
  285. Write-Error "[[Backup]] Log file directory $LogPath does not exist. Exiting."
  286. Send-Email
  287. exit
  288. }
  289. $error_count = 0;
  290. $attempt_count = $GlobalRetryAttempts
  291. while ($attempt_count -gt 0) {
  292. # setup logfiles
  293. $timestamp = Get-Date -Format FileDateTime
  294. $success_log = Join-Path $LogPath ($timestamp + ".log.txt")
  295. $error_log = Join-Path $LogPath ($timestamp + ".err.txt")
  296. $internet_available = Invoke-ConnectivityCheck $success_log $error_log
  297. if($internet_available -eq $true) {
  298. Invoke-Unlock $success_log $error_log
  299. $backup_success = Invoke-Backup $success_log $error_log
  300. if($backup_success) {
  301. Invoke-Maintenance $success_log $error_log
  302. }
  303. if (!(Test-Path $error_log) -or ((Get-Item $error_log).Length -eq 0)) {
  304. # successful with no errors; end
  305. $total_attempts = $GlobalRetryAttempts - $attempt_count + 1
  306. Write-Output "Succeeded after $total_attempts attempt(s)" | Tee-Object -Append $success_log
  307. Invoke-HistoryCheck $success_log $error_log
  308. Send-Email $success_log $error_log
  309. break;
  310. }
  311. }
  312. Write-Warning "Errors found! Error Log: $error_log"
  313. $error_count++
  314. $attempt_count--
  315. if($attempt_count -gt 0) {
  316. Write-Output "Sleeping for 15 min and then retrying..." | Tee-Object -Append $success_log
  317. }
  318. else {
  319. Write-Output "Retry limit has been reached. No more attempts to backup will be made." | Tee-Object -Append $success_log
  320. }
  321. if($internet_available -eq $true) {
  322. Invoke-HistoryCheck $success_log $error_log
  323. Send-Email $success_log $error_log
  324. }
  325. if($attempt_count -gt 0) {
  326. Start-Sleep (15*60)
  327. }
  328. }
  329. Set-BackupState
  330. # cleanup older log files
  331. Get-ChildItem $LogPath | Where-Object {$_.CreationTime -lt $(Get-Date).AddDays(-$LogRetentionDays)} | Remove-Item
  332. exit $error_count
  333. }
  334. Invoke-Main