First commit

This commit is contained in:
Yorick Barbanneau 2021-01-15 12:19:10 +01:00
commit cce27559b1
9 changed files with 676 additions and 0 deletions

182
README.md Normal file
View file

@ -0,0 +1,182 @@
Powershell Backup Script
------------------------
This is a simple backup script write in Powershell for Microsoft Windows. It
needs `7za` and `rclone` executable into the `./bin` directory to work properly.
You must configure it with JSON files, one example is provided in
`./etc/example.json`.
## usage
```
powershell -File backup.ps1 [-conf ./etc/example.json] [-log ./log/example.log] [-debug]
```
- `-conf` : configuration file (JSON), default `./etc/backup.json`
- `log` : log file, default `$false`
- `-debug` : verbose output on stdout (not log file)
## Configuration example
Here is the `example.json` file :
```json
{
"actions" :
[
{
"name" : "document_backup",
"source" : "C:\\Users\\people\\document",
"source_eval" : false,
"dest" : "\\\\backup_srv\\srv01\\",
"type" : "compress",
"options" : {
"-mx" : 3,
"-mmt" : 4,
"-xr@" : "C:\\backup\\etc\\7z\\exclude.txt"
},
"hook_postexec" : "<command>",
"username" : "bkp_example",
"pass" : "<secure_string>"
},
{
"name" : "project_copy",
"source" : "C:\\Users\\people\\project",
"dest" : "X:\\",
"type" : "clone",
"options" : {
"--exclude" : "*.zip"
}
},
{
"name" : "database_backup",
"source" : false,
"dest" : "X:\\databases",
"type" : "dump_mssql",
"options" : {
"server": "srv_host",
"instance" : "SQL2016",
"databases": "mydatabase",
"compression" : false
}
}
]
}
```
All actions performed by the script are defined by `actions [...]` object :
- `name` : name of the action, it will be use as filename for the `.zip` output
in `compress` type (see below)
- `source` : the source path, can be `{{tmpdir}}` to give the temporary folder
or a powershell expression if `source_eval` is set to `true`
- `source_eval` : evaluate the powershell expression in source for example
`$env:HOMEDATA\Music` will be interpreted as `C:\Users\name\Music`
- `dest` : the destination path, can be a network path (like `\\<host>\<path>`
) can be `{{tmpdir}}` to give the script temporary folder.
- `type` define the action, action is represented by a powershell module file
in `lib/` directory
- `options` : options for the module
- `hook_postexec` : Powershell command to execute when current action is
terminated
- `username` : user name for destination network share (if needed)
- `pass`: password for destination network share (if needed)
## Modules
This script works with Powershell modules located in `lib/` directory. Here is
the list of defaults module
### `clone` : Clone a directory
This module Clone a directory from `source` to `dest`. It use
[rclone][l_rclone] so you can add rclone command line parameters in `options`.
### `compress` : Compress files / directory
This module create a compressed file in `dest` with `source`. It use
[7zip][l_7zip] so you can add `7za` command line parameters in `options`.
### `dump_mssql` : Dump MSSQL database(s)
This module backup Microsoft SQL Server databases, for this one, `source` must
be `false`. Here are json options :
- `server` : MSSQL Server, required parameter (localhost doesn't work 😢)
- `instance` : Server instance, required parameter
- `compression` : activate compression output (zip format - default `false`)
- `databases` : list of databases to be backuped (default all)
You must execute backup script with an user that have the right of backupping
MSSQL databases. For now, authentication for databases export is not supported.
The user that run the MSSQL daemon must have right to access destination path.
## temporary folder
You can use `{{tempdir}}` in `source` or `destination` to specify a temporary
directory (formely `<script_folder>\tmp`). This can be useful, for example, when
you want to dump database but use 7zip to compress them :
- first action dump databases to `{{tempdir}}`
- second action compress files in `{{tempdir}}` to final destination
## Password for network share
In this script, network share need a password. `password` **must be created on
the machine** and tha **user account** which execute the script with following
command
```powershell
"PassW0rd" | ConvertTo-SecureString -AsPlainText -Force | ConvertFrom-SecureString
```
Copy-paste the result in your json file into `pass` (in example, replace
`<secure_string>`).
## Log
This backup script log information to you console, but it can log it to a file,
useful when tou execute it in scheduled task for example. Log are in the `log/`
directory. Script create a logfile per day named with the configuration file
basename plus date of execution (eg `example.20210112.log`)
If a logfile is older that 15 day, it will be move to a compressed file
(`example.log.archives.7z`)
## disclaimer
Until I'm a Linux/*BSD system administrator, I'm not too familiar with Windows
system, in particular with MSSQL command line backup tool. So I could do some
misktakes, if i do then you can contact me on the fediverse at
[@ephase@toot.aquilenet.fr][l_fediverse] (and if you have some questions, bug
reports, suggestions too).
## Licence
This script is released under le [MIT licence][l_mit]
Copyright © 2021 Yorick Barbanneau
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice (including the next
paragraph) shall be included in all copies or substantial portions of the
Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
[l_rclone]:https://rclone.org
[l_7zip]:https://www.7-zip.org/
[l_fediverse]:https://toot.aquilenet.fr/@ephase
[l_mit]:https://opensource.org/licenses/mit-license.php

218
backup.ps1 Normal file
View file

@ -0,0 +1,218 @@
param (
[cmdletbinding()]
[string]$conf= "",
[switch]$debug = $false,
[switch]$log = $false
)
Set-StrictMode -Version 2
#$PSDefaultParameterValues['Out-File:Encoding'] = 'utf8'
#$OutputEncoding = [console]::InputEncoding = [console]::OutputEncoding = New-Object System.Text.UTF8Encoding
$net_default_letter = "S"
$global:logfile = $false
$tmp_path = "$PSScriptRoot\tmp"
$global:bin_path = "$PSScriptRoot\bin"
$ProgressPreference = "SilentlyContinue"
$ErrorActionPreference = "SilentlyContinue"
if ( $debug ) {
$DebugPreference = "Continue"
}
Import-Module -Name "$PSScriptRoot\lib\backupscript-global_utils.psm1" -Scope Global -Force
function json_to_hash () {
[CmdletBinding()]
Param(
[Parameter(
Mandatory=$True,
ValueFromPipeline=$True
)]
$json
)
Write-Debug "Call json_to_hash with $json"
$r = @{}
$json.psobject.properties | Foreach {
Write-Debug "Found value $_"
$r[$_.Name] = $_.Value
}
return $r
}
function eval_var () {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$True)]
[string]
$var
)
Write-Debug "$var Need to be evaluated"
$var = $ExecutionContext.InvokeCommand.ExpandString($var)
Write-Debug "Result $var"
return $var
}
if ( -not $conf ) {
$conf = "$PSScriptRoot\etc\backup.json"
Write-Debug "Configuration file not provided, use $conf"
}
if ( $log ) {
$logname = (Get-Item $conf ).Basename
$global:logfile = "$PSScriptRoot\log\$((Get-Date).ToString("yyyy.MM.dd"))_$($logname).log"
write-debug "Write fo Set-Messagefile $($global:logfile) "
# Zip old messah=ges (Archives)
# TODO: Test this functionnality
if ( Test-Path "$($PSScriptRoot)/bin/7za.exe") {
$command = -join( $PSScriptRoot, "/bin/7za.exe", " u")
Write-Debug " Archive old loh file with $($command)"
# Select Set-Message file older than 15 days
Get-ChildItem "$($PSScriptRoot)\log\*$($logname).log" -Recurse -File | Where CreationTime -lt (Get-Date).AddDays(-15) | % {
# Put file in the right zip file by name
$execute = -join(
$command,
" `"",
$PSScriptRoot,
"\log\",
$logname,
".archives.",
$_.CreationTime).ToString("yyyy.MM"),
".log.7z`"",
" `"",
$_.FullName,
"`""
)
write-debug "Set-Message command archive: $($execute)"
try {
Invoke-Expression $execute | Out-Null
Remove-Item -Path $_.FullName
}
catch {
Set-Message "WARNING" "Can't zip $($_.Fullname) log files"
}
}
}
}
Write-Debug "Loaded script from $PSScriptRoot"
# Temporary directory creation / clean
if ( Test-Path $tmp_path ) {
Write-Debug "Remove file in $tmp_path"
Remove-Item -Path $tmp_path\* -recurse -Force
}
else {
Write-Debug "Create directory $tmp_path"
New-Item -ItemType Directory -Force -Path $tmp_path
}
# Parse JSON
try {
$backup = Get-Content $(Get-ChildItem $conf -).FullName -Raw | ConvertFrom-Json
}
catch {
Set-Error "Error While Loading JSON : $conf"
exit
}
Write-Debug "configuration $conf loaded successfully"
$backup.actions | % {
$destination = ""
$source = ""
# Test input path
Write-Debug "Process action : $($_.name) | type: $($_.type)"
if ( $_.psobject.properties.match('source_eval').Count -and $_.source_eval -eq $true ) {
$_.source = eval_var $($_.source)
Write-Debug "Result $($_.source)"
}
if ( $_.source -eq "{{tmpdir}}" ) {
Write-Debug "source is the temp directory"
$source = $tmp_path
}
elseif ( -not (Test-Path $_.source) -and $_.source ) {
Set-Message "Source path $($_.source) not found `n`n"
continue
}
else {
Write-Debug "Source is a regular directory"
$source = $_.source
}
# Test output path
if ( $_.dest -match "^\\.*$"){
Write-Debug "Destination is a network path"
if ( -not $_.username -or -not $_.pass ) {
Set-Error "you must specify a username and a password"
return
}
#Mount Network Drive
$cred = New-Object System.Management.Automation.PSCredential($_.username, ($_.pass | ConvertTo-SecureString))
try {
# get-psdrive
# Use Securestring to secure Password
# get secured password :
# ("Passw00rd" | ConvertTo-SecureString -AsPlainText -Force | ConvertFrom-SecureString)
New-PSDrive -Credential $cred -Name $net_default_letter -Root $($_.dest) -PSProvider FileSystem -Persist -Scope Script -ErrorAction Stop | Out-Null
$destination = "$($net_default_letter):"
}
catch {
Set-Error "Can't mount the network share"
Write-Debug $error[0].Exception
return
}
}
elseif ( $_.dest -eq "{{tmpdir}}" ){
Write-Debug "Destination is the temporary folder ($tmp_path)"
$destination = $tmp_path
}
else {
Write-Debug "$($_.dest) seems to le a local path"
if ( Test-Path $_.dest ) {
$destination = $_.dest
}
else {
Set-Error "Destination '$($_.dest)' doest not exit"
return
}
}
Write-Debug "Begin backup of $($_.name)"
if ( $_.psobject.Properties.Name -contains "options") {
$opt = $_.options | json_to_hash
}
else{
$opt = $false
}
try {
Import-Module -Name "$PSScriptRoot\lib\backupscript-$($_.type).psm1" -Force
Backup-Create -source $source -dest $destination -name $_.name -opt $opt #-EA Stop
if ( $_.psobject.properties.match('hook_postexec').Count -and $_.hook_postexec -ne "" ) {
Set-Message "Postexec hook : execute $($_.hook_postexec)"
Invoke-Expression $_.hook_postexec
}
}
catch {
Set-Error $Error[0].FullyQualifiedErrorId
}
finally{
# Properly remove net drive if mounted
if ( Test-Path "$($net_default_letter):" ){
Remove-PSDrive -Name $net_default_letter
}
# Remove Module
Remove-Module -Name $_.type -Force
}
}
Set-Message "Backup operation done"

0
bin/.gitkeep Normal file
View file

27
etc/example.json Normal file
View file

@ -0,0 +1,27 @@
{
"actions" :
[
{
"name" : "document_backup",
"source" : "C:\\Users\\people\\document",
"dest" : "\\\\backup_srv\\srv01\\",
"type" : "compress",
"options" : {
"-mx" : 3,
"-mmt" : 4,
"-xr@" : "C:\\backup\\etc\\7z\\exclude.txt"
},
"username" : "bkp_example",
"pass" : "test"
},
{
"name" : "project_copy",
"source" : "C:\\Users\\people\\project",
"dest" : "X:\\",
"type" : "clone",
"options" : {
"--exclude" : "*.zip"
}
}
]
}

View file

@ -0,0 +1,46 @@
function Backup-Create () {
Param(
[Parameter(Mandatory=$True)]
[string]
$source,
[Parameter(Mandatory=$True)]
[string]
$dest,
[Parameter(Mandatory=$false)]
[string]
$name,
[Parameter(Mandatory=$false)]
[object]
$options
)
$copy_opt = "--stats=0 --log-level INFO --log-format `"shorfile`" "
Set-Message "Sync files from $source to $dest"
if ( -not (Test-Path "$global:bin_path\rclone.exe") ) {
Throw "Rclone executable not found"
}
$copy_opt = -join($copy_opt, $(ConvertTo-CmdlineOptions $options))
$command = -join($global:bin_path, "\rclone.exe", " sync", " `"$source`"", " `"$dest`"", " $copy_opt", " 2>&1")
Set-Message "$command"
Invoke-Expression $command -ErrorAction Ignore | % {
if ($_ -ne ""){
$type, $mdg = ($_ -split ':')[0]
$msg = -join ($_ -split ':')[1..2]
switch ( $type.trim() ){
"INFO" {
Set-Message $msg.trim()
}
"WARN" {
Set-Warning $msg.trim()
}
"ERROR" {
Set-Error $msg.trim()
}
}
}
}
}
Export-ModuleMember -Function Backup-Create

View file

@ -0,0 +1,50 @@
function Backup-GetModuleInfo {
$local:version = "0.1a"
$local:name = "backup_module-7zip"
Write-Log "INFO" "$name v.$version"
}
function Backup-Create {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$True)]
[string]
$source,
[Parameter(Mandatory=$True)]
[string]
$dest,
[Parameter(Mandatory=$True)]
[string]
$name,
[Parameter(Mandatory=$false)]
[object]
$options
)
Set-Message "Compression of $source to $dest"
if ( -not (Test-Path "$global:bin_path\7za.exe") ) {
Throw [System.IO.FileNotFoundException]::new("7zip executable not found")
return
}
$zip_opt = ConvertTo-CmdlineOptions $options ""
$zip_opt = -join($zip_opt, " -bb3")
$command = -join($global:bin_path, "/7za.exe", " u"," $zip_opt", " `"$dest", "\", "$name", ".7z`"", " `"$source`"")
Write-Debug "commande : $command"
Invoke-Expression $command | % {
if ($_ -ne "" ){ Set-Message $_ }
}
Set-Message "Compressing $source done"
}
function backup-verify {
Set-Message "Verify zip archive"
}
Export-ModuleMember -Function Backup-Create

View file

@ -0,0 +1,69 @@
$ProgressPreference = "SilentlyContinue"
function Backup-Create () {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$True)]
[string]
$source,
[Parameter(Mandatory=$True)]
[string]
$dest,
[Parameter(Mandatory=$True)]
[string]
$name,
[Parameter(Mandatory=$True)]
[object]
$options
)
if ( -not (Get-Module -ListAvailable -Name sqlserver) ) {
Throw "There is not sqlserver Powershell module"
}
if ( $options -eq $false ){
Throw "you must define options for MSSQL action"
}
if ( -not ($options.ContainsKey("instance")) -or -not ($options.ContainsKey("server")) ) {
Throw "Instance and server option needed"
}
if ( -not ( Get-SqlAgent -ServerInstance "$($options.server)\$($options.instance)" -EA SilentlyContinue ) ) {
Throw "Can't connect to $($options.server)\$($options.instance)"
}
if ( $options.ContainsKey("compression") -and $options.compression -eq $true ) {
Write-Debug "Compression output activated"
$comp = "on"
$ext = "zip"
}
else {
Write-Debug "Compression disable"
$comp = "off"
$ext = "bak"
}
# Get databases on instance
$dblist = Get-SqlDatabase -ServerInstance "$($options.server)\$($options.instance)" | Select Name
#
if (-not ( -not ($options.ContainsKey("databases")) -or $options.databases -eq "" -or $options.databases -eq "all" )) {
Write-Debug "Backup some databases : $($options.databases)"
$dblist = $dblist | Where-Object { $_.Name -in ($options.databases).split(" ") }
}
$dblist | % {
Set-Message "Backup database $($_.Name)"
$db = $($_.Name)
try {
Backup-SqlDatabase -Database "$db" -ServerInstance "$($options.server)\$($options.instance)" -CompressionOption $comp -BackupFile $dest\$db.$ext -EA SilentlyContinue
}
catch {
Set-Warning "Can't backup $db"
}
}
}
Export-ModuleMember -Function Backup-Create

View file

@ -0,0 +1,84 @@
Write-Output "chargement du module"
function Get-LogTimeStamp {
return (Get-Date).toString("yyyy.MM.dd HH:mm:ss")
}
function ConvertTo-CmdlineOptions {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$True)]
[object]
$options,
[Parameter(Mandatory=$False)]
[string]
$sep=" "
)
Write-Debug "Call parse_options with $options, separator: `"$sep`""
$opt = ""
if ($options -eq $false) {
return ""
}
$options.Keys | foreach {
if ( -not $($options.item($_)) -eq "" ) {
$opt = -join($opt, " $($_)", $sep, "$($options.item($_))")
}
else {
$opt = -join($opt, " $($_)")
}
}
return $opt
}
function Set-Error {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$True)]
[string]
$message
)
if ( $global:logfile ) {
Add-Content $global:logfile -Value "$(Get-LogTimeStamp)`tERROR`t$message"
}
else {
Write-Host -ForegroundColor Red $message
}
}
function Set-Warning {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$True)]
[string]
$message
)
if ( $global:logfile ) {
Add-Content $global:logfile -Value "$(Get-LogTimeStamp)`tWARN`t$message"
}
else {
Write-Host -ForegroundColor Yellow $message
}
}
function Set-Message {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$True)]
[string]
$message
)
if ( $global:logfile ) {
Add-Content $global:logfile -Value "$(Get-LogTimeStamp)`tINFO`t$message"
}
else {
Write-Host "$message"
}
}
Export-ModuleMember -Function ConvertTo-CmdlineOptions, Set-Message, Set-Warning, Set-Error

0
log/.gitkeep Normal file
View file