dbaclone.psm1
$script:ModuleRoot = $PSScriptRoot $script:ModuleVersion = (Import-PowerShellDataFile -Path "$($script:ModuleRoot)\dbaclone.psd1").ModuleVersion # Detect whether at some level dotsourcing was enforced $script:doDotSource = Get-PSFConfigValue -FullName dbaclone.Import.DoDotSource -Fallback $false if ($dbaclone_dotsourcemodule) { $script:doDotSource = $true } <# Note on Resolve-Path: All paths are sent through Resolve-Path/Resolve-PSFPath in order to convert them to the correct path separator. This allows ignoring path separators throughout the import sequence, which could otherwise cause trouble depending on OS. Resolve-Path can only be used for paths that already exist, Resolve-PSFPath can accept that the last leaf my not exist. This is important when testing for paths. #> # Detect whether at some level loading individual module files, rather than the compiled module was enforced $importIndividualFiles = Get-PSFConfigValue -FullName dbaclone.Import.IndividualFiles -Fallback $false if ($dbaclone_importIndividualFiles) { $importIndividualFiles = $true } if (Test-Path (Resolve-PSFPath -Path "$($script:ModuleRoot)\..\.git" -SingleItem -NewChild)) { $importIndividualFiles = $true } if ("<was compiled>" -eq '<was not compiled>') { $importIndividualFiles = $true } function Import-ModuleFile { <# .SYNOPSIS Loads files into the module on module import. .DESCRIPTION This helper function is used during module initialization. It should always be dotsourced itself, in order to proper function. This provides a central location to react to files being imported, if later desired .PARAMETER Path The path to the file to load .EXAMPLE PS C:\> . Import-ModuleFile -File $function.FullName Imports the file stored in $function according to import policy #> [CmdletBinding()] Param ( [string] $Path ) $resolvedPath = $ExecutionContext.SessionState.Path.GetResolvedPSPathFromPSPath($Path).ProviderPath if ($doDotSource) { . $resolvedPath } else { $ExecutionContext.InvokeCommand.InvokeScript($false, ([scriptblock]::Create([io.file]::ReadAllText($resolvedPath))), $null, $null) } } #region Load individual files if ($importIndividualFiles) { # Execute Preimport actions . Import-ModuleFile -Path "$ModuleRoot\internal\scripts\preimport.ps1" # Import all internal functions foreach ($function in (Get-ChildItem "$ModuleRoot\internal\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore)) { . Import-ModuleFile -Path $function.FullName } # Import all public functions foreach ($function in (Get-ChildItem "$ModuleRoot\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore)) { . Import-ModuleFile -Path $function.FullName } # Execute Postimport actions . Import-ModuleFile -Path "$ModuleRoot\internal\scripts\postimport.ps1" # End it here, do not load compiled code below return } #endregion Load individual files #region Load compiled code function Test-DcnConfiguration { <# .SYNOPSIS Test the configuration of the module .DESCRIPTION The configuration of the module is vital to let it function. This function checks several configurations .PARAMETER SqlInstance The instance that represents the PSDatabaseClone instance that holds the database .PARAMETER SqlCredential Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. To use: $scred = Get-Credential, then pass $scred object to the -SourceSqlCredential parameter. Windows Authentication will be used if SqlCredential is not specified. SQL Server does not accept Windows credentials being passed as credentials. To connect as a different Windows user, run PowerShell as that user. .PARAMETER Database The database that holds all the information for the PSDatabaseClone module .PARAMETER EnableException By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .NOTES Author: Sander Stad (@sqlstad, sqlstad.nl) Website: https://psdatabaseclone.org Copyright: (C) Sander Stad, sander@sqlstad.nl License: MIT https://opensource.org/licenses/MIT .LINK https://psdatabaseclone.org/ .EXAMPLE Test-DcnConfiguration Test the configuration of the module retrieving the set configurations .EXAMPLE Test-DcnConfiguration -SqlInstance SQLDB1 -Database PSDatabaseClone Test the configuration with the instance and database set #> [CmdLetBinding()] param( [DbaInstanceParameter]$SqlInstance, [System.Management.Automation.PSCredential] $SqlCredential, [string]$Database, [switch]$EnableException ) Write-PSFMessage -Message "SqlInstance: $SqlInstance, Database: $Database" -Level Debug # Check if the values for the PSDatabaseClone database are set if (($null -eq $SqlInstance) -or ($null -eq $Database) -or ($null -eq $SqlCredential)) { # Get the configurations for the program database $Database = Get-PSFConfigValue -FullName psdatabaseclone.database.name -Fallback "NotConfigured" $SqlInstance = Get-PSFConfigValue -FullName psdatabaseclone.database.server -Fallback "NotConfigured" $SqlCredential = Get-PSFConfigValue -FullName psdatabaseclone.informationstore.credential -Fallback $null } Write-PSFMessage -Message "Checking configurations" -Level Verbose # Check the module database server and database name configurations if ($SqlInstance -eq 'NotConfigured') { Stop-PSFFunction -Message "The PSDatabaseClone database server is not yet configured. Please run Set-DcnConfiguration" -Target $SqlInstance -Continue } if ($Database -eq 'NotConfigured') { Stop-PSFFunction -Message "The PSDatabaseClone database is not yet configured. Please run Set-DcnConfiguration" -Target $Database -Continue } Write-PSFMessage -Message "Attempting to connect to PSDatabaseClone database server $SqlInstance.." -Level Verbose try { $pdcServer = Connect-DbaInstance -SqlInstance $SqlInstance -SqlCredential $SqlCredential -NonPooledConnection } catch { Stop-PSFFunction -Message "Could not connect to Sql Server instance $SqlInstance" -ErrorRecord $_ -Target $pdcServer -Continue } # Check if the PSDatabaseClone database is present if ($pdcServer.Databases.Name -notcontains $Database) { Stop-PSFFunction -Message "PSDatabaseClone database $Database is not present on $SqlInstance" -Target $pdcServer -Continue } Write-PSFMessage -Message "Finished checking configurations" -Level Verbose } function Get-DcnClone { <# .SYNOPSIS Get-DcnClone get on or more clones .DESCRIPTION Get-DcnClone will retrieve the clones and apply filters if needed. By default all the clones are returned .PARAMETER SqlCredential Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. To use: $scred = Get-Credential, then pass $scred object to the -SqlCredential parameter. Windows Authentication will be used if SqlCredential is not specified. SQL Server does not accept Windows credentials being passed as credentials. To connect as a different Windows user, run PowerShell as that user. .PARAMETER DcnSqlCredential Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. This works similar as SqlCredential but is only meant for authentication to the PSDatabaseClone database server and database. .PARAMETER Credential Allows you to login to servers or use authentication to access files and folder/shares $scred = Get-Credential, then pass $scred object to the -Credential parameter. .PARAMETER HostName Filter based on the hostname .PARAMETER Database Filter based on the database .PARAMETER ImageID Filter based on the image id .PARAMETER ImageName Filter based on the image name .PARAMETER ImageLocation Filter based on the image location .PARAMETER EnableException By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .NOTES Author: Sander Stad (@sqlstad, sqlstad.nl) Website: https://psdatabaseclone.org Copyright: (C) Sander Stad, sander@sqlstad.nl License: MIT https://opensource.org/licenses/MIT .LINK https://psdatabaseclone.org/ .EXAMPLE Get-DcnClone Get all the clones .EXAMPLE Get-DcnClone -HostName host1, host2 Retrieve the clones for host1 and host2 .EXAMPLE Get-DcnClone -Database DB1 Get all the clones that have the name DB1 .EXAMPLE Get-DcnClone -ImageName DB1_20180703085917 Get all the clones that were made with image "DB1_20180703085917" #> [CmdLetBinding()] param( [PSCredential]$SqlCredential, [PSCredential]$DcnSqlCredential, [PSCredential]$Credential, [string[]]$HostName, [string[]]$Database, [int[]]$ImageID, [string[]]$ImageName, [string[]]$ImageLocation, [switch]$EnableException ) begin { # Check if the setup has ran if (-not (Test-DcnModule -SetupStatus)) { Stop-PSFFunction -Message "The module setup has NOT yet successfully run. Please run 'Set-DcnConfiguration'" -Continue } # Get the information store $informationStore = Get-PSFConfigValue -FullName psdatabaseclone.informationstore.mode if ($informationStore -eq 'SQL') { # Get the module configurations $pdcSqlInstance = Get-PSFConfigValue -FullName psdatabaseclone.database.server $pdcDatabase = Get-PSFConfigValue -FullName psdatabaseclone.database.name if (-not $DcnSqlCredential) { $pdcCredential = Get-PSFConfigValue -FullName psdatabaseclone.informationstore.credential -Fallback $null } else { $pdcCredential = $DcnSqlCredential } # Test the module database setup try { Test-DcnConfiguration -SqlCredential $pdcCredential -EnableException } catch { Stop-PSFFunction -Message "Something is wrong in the module configuration" -ErrorRecord $_ -Continue } $query = " SELECT c.CloneID, c.CloneLocation, c.AccessPath, c.SqlInstance, c.DatabaseName, c.IsEnabled, i.ImageID, i.ImageName, i.ImageLocation, h.HostName FROM dbo.Clone AS c INNER JOIN dbo.Host AS h ON h.HostID = c.HostID INNER JOIN dbo.Image AS i ON i.ImageID = c.ImageID; " try { $results = @() $results = Invoke-DbaQuery -SqlInstance $pdcSqlInstance -SqlCredential $pdcCredential -Database $pdcDatabase -Query $query -As PSObject } catch { Stop-PSFFunction -Message "Could not execute query" -ErrorRecord $_ -Target $query } } elseif ($informationStore -eq 'File') { # Create the PS Drive and get the results try { if (Test-Path -Path "DCNJSONFolder:\") { # Get the clones $results = Get-ChildItem -Path "DCNJSONFolder:\" -Filter "*clones.json" | ForEach-Object { Get-Content $_.FullName | ConvertFrom-Json } } else { Stop-PSFFunction -Message "Could not reach clone information location 'DCNJSONFolder:\'" -ErrorRecord $_ -Target "DCNJSONFolder:\" return } } catch { Stop-PSFFunction -Message "Couldn't get results from JSN folder" -ErrorRecord $_ -Target "DCNJSONFolder:\" return } } # Filter host name if ($HostName) { $results = $results | Where-Object { $_.HostName -in $HostName } } # Filter image id if ($Database) { $results = $results | Where-Object { $_.DatabaseName -in $Database } } # Filter image id if ($ImageID) { $results = $results | Where-Object { $_.ImageID -in $ImageID } } # Filter image name if ($ImageName) { $results = $results | Where-Object { $_.ImageName -in $ImageName } } # Filter image location if ($ImageLocation) { $results = $results | Where-Object { $_.ImageLocation -in $ImageLocation } } } process { # Test if there are any errors if (Test-PSFFunctionInterrupt) { return } # Convert the results to the DCLClone data type foreach ($result in $results) { [pscustomobject]@{ CloneID = $result.CloneID CloneLocation = $result.CloneLocation AccessPath = $result.AccessPath SqlInstance = $result.SqlInstance DatabaseName = $result.DatabaseName IsEnabled = $result.IsEnabled ImageID = $result.ImageID ImageName = $result.ImageName ImageLocation = $result.ImageLocation HostName = $result.HostName } } } end { # Test if there are any errors if (Test-PSFFunctionInterrupt) { return } Write-PSFMessage -Message "Finished retrieving clone(s)" -Level Verbose } } function Invoke-DcnRepairClone { <# .SYNOPSIS Invoke-DcnRepairClone repairs the clones .DESCRIPTION Invoke-DcnRepairClone has the ability to repair the clones when they have gotten disconnected from the image. In such a case the clone is no longer available for the database server and the database will either not show any information or the database will have the status (Recovery Pending). By running this command all the clones will be retrieved from the database for a certain host. .PARAMETER HostName Set on or more hostnames to retrieve the configurations for .PARAMETER SqlCredential Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. To use: $scred = Get-Credential, then pass $scred object to the -SqlCredential parameter. Windows Authentication will be used if SqlCredential is not specified. SQL Server does not accept Windows credentials being passed as credentials. To connect as a different Windows user, run PowerShell as that user. .PARAMETER Credential Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. This works similar as SqlCredential but is only meant for authentication to the the host .PARAMETER DcnSqlCredential Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. This works similar as SqlCredential but is only meant for authentication to the PSDatabaseClone database server and database. .PARAMETER EnableException By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .NOTES Author: Sander Stad (@sqlstad, sqlstad.nl) Website: https://psdatabaseclone.org Copyright: (C) Sander Stad, sander@sqlstad.nl License: MIT https://opensource.org/licenses/MIT .LINK https://psdatabaseclone.org/ .EXAMPLE Invoke-DcnRepairClone -Hostname Host1 Repair the clones for Host1 #> [CmdLetBinding(SupportsShouldProcess = $true)] param( [Parameter(Mandatory = $true)] [string[]]$HostName, [PSCredential]$SqlCredential, [PSCredential]$Credential, [PSCredential]$DcnSqlCredential, [switch]$EnableException ) begin { # Check if the console is run in Administrator mode if ( -not (Test-PSFPowerShell -Elevated) ) { Stop-PSFFunction -Message "Module requires elevation. Please run the console in Administrator mode" -Continue } if (-not (Test-DcnModule -SetupStatus)) { Stop-PSFFunction -Message "The module setup has NOT yet successfully run. Please run 'Set-DcnConfiguration'" -Continue } } process { # Test if there are any errors if (Test-PSFFunctionInterrupt) { return } # Loop through each of the hosts foreach ($hst in $HostName) { # Setup the computer object $computer = [PSFComputer]$hst if (-not $computer.IsLocalhost) { # Get the result for the remote test $resultPSRemote = Test-DcnRemoting -ComputerName $hst -Credential $Credential # Check the result if ($resultPSRemote.Result) { $command = [scriptblock]::Create("Import-Module dbaclone") try { Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential } catch { Stop-PSFFunction -Message "Couldn't import module remotely" -Target $command return } } else { Stop-PSFFunction -Message "Couldn't connect to host remotely.`nVerify that the specified computer name is valid, that the computer is accessible over the network, and that a firewall exception for the WinRM service is enabled and allows access from this computer" -Target $resultPSRemote -Continue } } # Get the clones [array]$results = Get-DcnClone -HostName $hst # Loop through the results foreach ($result in $results) { $server = Connect-DbaInstance -SqlInstance $result.SqlInstance -SqlCredential $SqlCredential # Get the databases Write-PSFMessage -Message "Retrieve the databases for $($result.SqlInstance)" -Level Verbose $databases = $server.Databases $image = Get-DcnImage -ImageID $result.ImageID # Check if the parent of the clone can be reached try { $null = New-PSDrive -Name ImagePath -Root (Split-Path $image.ImageLocation) -Credential $Credential -PSProvider FileSystem } catch { Stop-PSFFunction -Message "Could not create drive for image path '$($image.ImageLocation)'" -ErrorRecord $_ -Continue } # Test if the image still exists if (Test-Path -Path "ImagePath:\$($image.ImageName).vhdx") { # Mount the clone try { Write-PSFMessage -Message "Mounting vhd $($result.CloneLocation)" -Level Verbose if (Test-Path -Path $result.CloneLocation) { $disk = Get-Disk | Where-Object Location -eq $result.CloneLocation if (-not $disk) { # Check if computer is local if ($PSCmdlet.ShouldProcess("Mounting $($result.CloneLocation)")) { if ($computer.IsLocalhost) { $null = Mount-DiskImage -ImagePath $result.CloneLocation -NoDriveLetter } else { $command = [ScriptBlock]::Create("Mount-DiskImage -ImagePath '$($result.CloneLocation)' -NoDriveLetter -ErrorAction SilentlyContinue") $null = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential } } } } else { Stop-PSFFunction -Message "Couldn't find clone file '$($result.CloneLocation)'" -Target $result -Continue } } catch { Stop-PSFFunction -Message "Couldn't mount vhd" -Target $result -ErrorRecord $_ -Continue } } else { Stop-PSFFunction -Message "Vhd $($result.CloneLocation) cannot be mounted because image path cannot be reached" -Target $image -Continue } # Remove the PS Drive try { $null = Remove-PSDrive -Name ImagePath } catch { Stop-PSFFunction -Message "Could not remove drive 'ImagePath'" -ErrorRecord $_ -Continue } # Check if the database is already attached if ($result.DatabaseName -in $databases.Name) { $db = $databases | Where-Object Name -eq $result.DatabaseName if ($db.Status -eq 'RecoveryPending') { try { Write-PSFMessage -Message "Setting database offline" -Level Verbose $db.SetOffline() Write-PSFMessage -Message "Setting database online" -Level Verbose $db.SetOnline() } catch { Stop-PSFFunction -Message "Could not detach database [$($result.DatabaseName)]" -ErrorRecord $_ -Continue } } else { try { $null = Detach-DbaDatabase -SqlInstance $result.SQLInstance -SqlCredential $SqlCredential -Database $result.DatabaseName } catch { Stop-PSFFunction -Message "Could not detach database [$($result.DatabaseName)]" -ErrorRecord $_ -Continue } } } else { # Get all the files of the database if ($PSCmdlet.ShouldProcess("Retrieving database files from $($result.AccessPath)")) { # Check if computer is local if ($computer.IsLocalhost) { $databaseFiles = Get-ChildItem -Path $result.AccessPath -Recurse | Where-Object { -not $_.PSIsContainer } } else { $commandText = "Get-ChildItem -Path $($result.AccessPath) -Recurse | " + 'Where-Object {-not $_.PSIsContainer}' $command = [ScriptBlock]::Create($commandText) $databaseFiles = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential } } # Setup the database filestructure $dbFileStructure = New-Object System.Collections.Specialized.StringCollection # Loop through each of the database files and add them to the file structure foreach ($dbFile in $databaseFiles) { $dbFileStructure.Add($dbFile.FullName) | Out-Null } Write-PSFMessage -Message "Mounting database from clone" -Level Verbose # Mount the database using the config file if ($PSCmdlet.ShouldProcess("Mounting database $($result.DatabaseName) to $($result.SQLInstance)")) { try { $null = Mount-DbaDatabase -SqlInstance $result.SQLInstance -Database $result.DatabaseName -FileStructure $dbFileStructure } catch { Stop-PSFFunction -Message "Couldn't mount database $($result.DatabaseName)" -Target $result.DatabaseName -Continue } } } } # End for ech result } # End for each host } # End process end { # Test if there are any errors if (Test-PSFFunctionInterrupt) { return } Write-PSFMessage -Message "Finished repairing clones" -Level Verbose } } function New-DcnClone { <# .SYNOPSIS New-DcnClone creates a new clone .DESCRIPTION New-DcnClone willcreate a new clone based on an image. The clone will be created in a certain directory, mounted and attached to a database server. .PARAMETER SqlInstance SQL Server name or SMO object representing the SQL Server to connect to .PARAMETER SqlCredential Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. To use: $scred = Get-Credential, then pass $scred object to the -SqlCredential parameter. Windows Authentication will be used if SqlCredential is not specified. SQL Server does not accept Windows credentials being passed as credentials. To connect as a different Windows user, run PowerShell as that user. .PARAMETER DcnSqlCredential Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. This works similar as SqlCredential but is only meant for authentication to the PSDatabaseClone database server and database. .PARAMETER Credential Allows you to login to servers using Windows Auth/Integrated/Trusted. To use: $scred = Get-Credential, then pass $scred object to the -Credential parameter. .PARAMETER ParentVhd Points to the parent VHD to create the clone from .PARAMETER Destination Destination directory to save the clone to .PARAMETER CloneName Name of the clone .PARAMETER Database Database name for the clone .PARAMETER LatestImage Automatically get the last image ever created for an specific database .PARAMETER Disabled Registers the clone in the configuration as disabled. If this setting is used the clone will not be recovered when the repair command is run .PARAMETER SkipDatabaseMount If this parameter is used, the database will not be mounted. .PARAMETER Force Forcefully create items when needed .PARAMETER EnableException By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .NOTES Author: Sander Stad (@sqlstad, sqlstad.nl) Website: https://psdatabaseclone.org Copyright: (C) Sander Stad, sander@sqlstad.nl License: MIT https://opensource.org/licenses/MIT .LINK https://psdatabaseclone.org/ .EXAMPLE New-DcnClone -SqlInstance SQLDB1 -ParentVhd C:\Temp\images\DB1_20180623203204.vhdx -Destination C:\Temp\clones\ -CloneName DB1_Clone1 Create a new clone based on the image DB1_20180623203204.vhdx and attach the database to SQLDB1 as DB1_Clone1 .EXAMPLE New-DcnClone -SqlInstance SQLDB1 -Database DB1, DB2 -LatestImage Create a new clone on SQLDB1 for the databases DB1 and DB2 with the latest image for those databases .EXAMPLE New-DcnClone -SqlInstance SQLDB1, SQLDB2 -Database DB1 -LatestImage Create a new clone on SQLDB1 and SQLDB2 for the databases DB1 with the latest image #> [CmdLetBinding(DefaultParameterSetName = 'ByLatest', SupportsShouldProcess = $true)] param( [DbaInstanceParameter]$SqlInstance, [PSCredential]$SqlCredential, [PSCredential]$DcnSqlCredential, [PSCredential]$Credential, [parameter(Mandatory = $true, ParameterSetName = "ByParent")] [string]$ParentVhd, [string]$Destination, [string]$CloneName, [parameter(Mandatory = $true, ParameterSetName = "ByLatest")] [string[]]$Database, [parameter(Mandatory = $true, ParameterSetName = "ByLatest")] [switch]$LatestImage, [switch]$Disabled, [switch]$SkipDatabaseMount, [switch]$Force, [switch]$EnableException ) begin { # Check if the console is run in Administrator mode if ( -not (Test-PSFPowerShell -Elevated) ) { Stop-PSFFunction -Message "Module requires elevation. Please run the console in Administrator mode" -Continue } if (-not (Test-DcnModule -SetupStatus)) { Stop-PSFFunction -Message "The module setup has NOT yet successfully run. Please run 'Set-DcnConfiguration'" -Continue } if (-not $SqlInstance) { $SkipDatabaseMount = $true } if (-not $Destination -and -not $SqlInstance) { Stop-PSFFunction -Message "Please enter a destination or enter a SQL Server instance" -Continue } if (-not $Destination -and $SkipDatabaseMount) { Stop-PSFFunction -Message "Please enter a destination when using -SkipDatabaseMount" -Continue } # Check the available images $images = Get-DcnImage if ($Database -notin $images.DatabaseName) { Stop-PSFFunction -Message "There is no image for database '$Database'" -Continue } # Get the information store $informationStore = Get-PSFConfigValue -FullName psdatabaseclone.informationstore.mode if ($informationStore -eq 'SQL') { # Get the module configurations $pdcSqlInstance = Get-PSFConfigValue -FullName psdatabaseclone.database.Server $pdcDatabase = Get-PSFConfigValue -FullName psdatabaseclone.database.name if (-not $DcnSqlCredential) { $pdcCredential = Get-PSFConfigValue -FullName psdatabaseclone.informationstore.credential -Fallback $null } else { $pdcCredential = $DcnSqlCredential } # Test the module database setup if ($PSCmdlet.ShouldProcess("Test-DcnConfiguration", "Testing module setup")) { try { Test-DcnConfiguration -SqlCredential $pdcCredential -EnableException } catch { Stop-PSFFunction -Message "Something is wrong in the module configuration" -ErrorRecord $_ -Continue } } } Write-PSFMessage -Message "Started clone creation" -Level Verbose # Check the disabled parameter $active = 1 if ($Disabled) { $active = 0 } # Set the location where to save the diskpart command $diskpartScriptFile = Get-PSFConfigValue -FullName psdatabaseclone.diskpart.scriptfile -Fallback "$env:APPDATA\psdatabaseclone\diskpartcommand.txt" if (-not (Test-Path -Path $diskpartScriptFile)) { try { $null = New-Item -Path $diskpartScriptFile -ItemType File } catch { Stop-PSFFunction -Message "Could not create diskpart script file" -ErrorRecord $_ -Continue } } } process { # Test if there are any errors if (Test-PSFFunctionInterrupt) { return } # Loop through all the instances #foreach ($instance in $SqlInstance) { if ($SqlInstance) { if (-not $SkipDatabaseMount) { # Try connecting to the instance Write-PSFMessage -Message "Attempting to connect to Sql Server $SqlInstance.." -Level Verbose try { $server = Connect-DbaInstance -SqlInstance $SqlInstance -SqlCredential $SqlCredential } catch { Stop-PSFFunction -Message "Could not connect to Sql Server instance $instance" -ErrorRecord $_ -Target $instance return } } # Setup the computer object $computer = [PsfComputer]$server.ComputerName } else { $computer = [PsfComputer]"$($env:COMPUTERNAME)" } if (-not $computer.IsLocalhost) { # Get the result for the remote test $resultPSRemote = Test-DcnRemoting -ComputerName $computer.ComputerName -Credential $Credential # Check the result if ($resultPSRemote.Result) { $command = [scriptblock]::Create("Import-Module dbaclone") try { Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential } catch { Stop-PSFFunction -Message "Couldn't import module remotely" -Target $command return } } else { Stop-PSFFunction -Message "Couldn't connect to host remotely.`nVerify that the specified computer name is valid, that the computer is accessible over the network, and that a firewall exception for the WinRM service is enabled and allows access from this computer" -Target $resultPSRemote -Continue } } # Check destination if (-not $Destination) { $Destination = Join-PSFPath -Path $server.DefaultFile -Child "clone" } else { # If the destination is a network path if ($Destination.StartsWith("\\")) { Write-PSFMessage -Message "The destination cannot be an UNC path. Trying to convert to local path" -Level Verbose if ($PSCmdlet.ShouldProcess($Destination, "Converting UNC path '$Destination' to local path")) { try { # Check if computer is local if ($computer.IsLocalhost) { $Destination = Convert-DcnUncPathToLocalPath -UncPath $Destination } else { $command = [ScriptBlock]::Create("Convert-DcnUncPathToLocalPath -UncPath `"$Destination`"") $Destination = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential } } catch { Stop-PSFFunction -Message "Something went wrong getting the local image path" -Target $Destination return } } } # Remove the last "\" from the path it would mess up the mount of the VHD if ($Destination.EndsWith("\")) { $Destination = $Destination.Substring(0, $Destination.Length - 1) } # Test if the destination can be reached # Check if computer is local if ($computer.IsLocalhost) { if (-not (Test-Path -Path $Destination)) { Stop-PSFFunction -Message "Could not find destination path $Destination" -Target $SqlInstance } } else { $command = [ScriptBlock]::Create("Test-Path -Path '$Destination'") $result = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential if (-not $result) { Stop-PSFFunction -Message "Could not find destination path $Destination" -Target $SqlInstance } } } # Loopt through all the databases foreach ($db in $Database) { if ($LatestImage) { $images = Get-DcnImage -Database $db $result = $images[-1] | Sort-Object CreatedOn } # Check the results if ($null -eq $result) { Stop-PSFFunction -Message "No image could be found for database $db" -Target $pdcSqlInstance -Continue } else { $ParentVhd = $result.ImageLocation } # Take apart the vhd directory if ($PSCmdlet.ShouldProcess($ParentVhd, "Setting up parent VHD variables")) { $uri = new-object System.Uri($ParentVhd) $vhdComputer = [PsfComputer]$uri.Host if ($vhdComputer.IsLocalhost) { if ((Test-Path -Path $ParentVhd)) { $parentVhdFileName = $ParentVhd.Split("\")[-1] $parentVhdFile = $parentVhdFileName.Split(".")[0] } else { Stop-PSFFunction -Message "Parent vhd could not be found" -Target $SqlInstance -Continue } } else { $command = [scriptblock]::Create("Test-Path -Path '$ParentVhd'") $result = Invoke-PSFCommand -ComputerName $vhdComputer -ScriptBlock $command -Credential $Credential if ($result) { $parentVhdFileName = $ParentVhd.Split("\")[-1] $parentVhdFile = $parentVhdFileName.Split(".")[0] } else { Stop-PSFFunction -Message "Parent vhd could not be found" -Target $SqlInstance -Continue } } } # Check clone name parameter if ($PSCmdlet.ShouldProcess($ParentVhd, "Setting up clone variables")) { if (-not $CloneName) { $cloneDatabase = $parentVhdFile $CloneName = $parentVhdFile $mountDirectory = "$($parentVhdFile)" } elseif ($CloneName) { $cloneDatabase = $CloneName $mountDirectory = "$($CloneName)" } } # Check if the database is already present if (-not $SkipDatabaseMount) { if ($PSCmdlet.ShouldProcess($cloneDatabase, "Verifying database existence")) { if ($server.Databases.Name -contains $cloneDatabase) { Stop-PSFFunction -Message "Database $cloneDatabase is already present on $SqlInstance" -Target $SqlInstance } } } # Setup access path location $accessPath = Join-PSFPath -Path $Destination -Child $mountDirectory # Check if access path is already present if ($PSCmdlet.ShouldProcess($accessPath, "Testing existence access path $accessPath and create it")) { if ($computer.IsLocalhost) { if (-not (Test-Path -Path $accessPath)) { try { $null = New-Item -Path $accessPath -ItemType Directory -Force } catch { Stop-PSFFunction -Message "Couldn't create access path directory" -ErrorRecord $_ -Target $accessPath -Continue } } } else { $command = [ScriptBlock]::Create("Test-Path -Path '$accessPath'") $result = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential if (-not $result) { try { $command = [ScriptBlock]::Create("New-Item -Path '$accessPath' -ItemType Directory -Force") $null = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential } catch { Stop-PSFFunction -Message "Couldn't create access path directory" -ErrorRecord $_ -Target $accessPath -Continue } } } } # Check if the clone vhd does not yet exist $clonePath = Join-PSFPath -Path $Destination -Child "$($CloneName).vhdx" if ($computer.IsLocalhost) { if (Test-Path -Path "$($clonePath)" -Credential $DestinationCredential) { Stop-PSFFunction -Message "Clone $CloneName already exists" -Target $accessPath -Continue } } else { $command = [ScriptBlock]::Create("Test-Path -Path `"$($clonePath)`"") $result = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential if ($result) { Stop-PSFFunction -Message "Clone $CloneName already exists" -Target $accessPath -Continue } } # Create the new child vhd if ($PSCmdlet.ShouldProcess($ParentVhd, "Creating clone")) { try { Write-PSFMessage -Message "Creating clone from $ParentVhd" -Level Verbose $command = "create vdisk file='$($clonePath)' parent='$ParentVhd'" # Check if computer is local if ($computer.IsLocalhost) { # Set the content of the diskpart script file Set-Content -Path $diskpartScriptFile -Value $command -Force $script = [ScriptBlock]::Create("diskpart /s $diskpartScriptFile") $null = Invoke-PSFCommand -ScriptBlock $script } else { $command = [ScriptBlock]::Create("New-VHD -ParentPath $ParentVhd -Path `"$($clonePath)`" -Differencing") $vhd = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential if (-not $vhd) { return } } } catch { Stop-PSFFunction -Message "Could not create clone" -Target $vhd -Continue -ErrorRecord $_ } } # Mount the vhd if ($PSCmdlet.ShouldProcess("$($clonePath)", "Mounting clone clone")) { try { Write-PSFMessage -Message "Mounting clone" -Level Verbose # Check if computer is local if ($computer.IsLocalhost) { # Mount the disk $null = Mount-DiskImage -ImagePath "$($clonePath)" # Get the disk based on the name of the vhd $diskImage = Get-DiskImage -ImagePath $clonePath $disk = Get-Disk | Where-Object Number -eq $diskImage.Number } else { # Mount the disk $command = [ScriptBlock]::Create("Mount-DiskImage -ImagePath `"$($clonePath)`"") $null = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential # Get the disk based on the name of the vhd $command = [ScriptBlock]::Create(" `$diskImage = Get-DiskImage -ImagePath $($clonePath) Get-Disk | Where-Object Number -eq $($diskImage.Number) ") $disk = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential } } catch { Stop-PSFFunction -Message "Couldn't mount vhd $vhdPath" -ErrorRecord $_ -Target $disk -Continue } } # Check if the disk is offline if ($PSCmdlet.ShouldProcess($disk.Number, "Initializing disk")) { # Check if computer is local if ($computer.IsLocalhost) { $null = Initialize-Disk -Number $disk.Number -PartitionStyle GPT -ErrorAction SilentlyContinue } else { $command = [ScriptBlock]::Create("Initialize-Disk -Number $($disk.Number) -PartitionStyle GPT -ErrorAction SilentlyContinue") $null = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential } } # Mounting disk to access path if ($PSCmdlet.ShouldProcess($disk.Number, "Mounting volume to accesspath")) { try { # Check if computer is local if ($computer.IsLocalhost) { # Get the partition based on the disk $partition = Get-Partition -Disk $disk | Where-Object { $_.Type -ne "Reserved" } | Select-Object -First 1 # Create an access path for the disk $null = Add-PartitionAccessPath -DiskNumber $disk.Number -PartitionNumber $partition.PartitionNumber -AccessPath $accessPath -ErrorAction SilentlyContinue } else { $command = [ScriptBlock]::Create("Get-Partition -DiskNumber $($disk.Number)") $partition = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential | Where-Object { $_.Type -ne "Reserved" } | Select-Object -First 1 $command = [ScriptBlock]::Create("Add-PartitionAccessPath -DiskNumber $($disk.Number) -PartitionNumber $($partition.PartitionNumber) -AccessPath '$accessPath' -ErrorAction Ignore") $null = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential } } catch { Stop-PSFFunction -Message "Couldn't create access path for partition" -ErrorRecord $_ -Target $partition -Continue } } if (-not $SkipDatabaseMount) { # Get all the files of the database if ($computer.IsLocalhost) { $databaseFiles = Get-ChildItem -Path $accessPath -Filter *.*df -Recurse } else { $commandText = "Get-ChildItem -Path '$accessPath' -Filter *.*df -Recurse" $command = [ScriptBlock]::Create($commandText) $databaseFiles = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential } # Setup the database filestructure $dbFileStructure = New-Object System.Collections.Specialized.StringCollection # Loop through each of the database files and add them to the file structure foreach ($dbFile in $databaseFiles) { $null = $dbFileStructure.Add($dbFile.FullName) } # Mount the database if ($PSCmdlet.ShouldProcess($cloneDatabase, "Mounting database $cloneDatabase")) { try { Write-PSFMessage -Message "Mounting database from clone" -Level Verbose $null = Mount-DbaDatabase -SqlInstance $server -SqlCredential $SqlCredential -Database $cloneDatabase -FileStructure $dbFileStructure } catch { Stop-PSFFunction -Message "Couldn't mount database $cloneDatabase" -ErrorRecord $_ -Target $instance -Continue } } } # Write the data to the database try { # Get the data of the host if ($computer.IsLocalhost) { $computerinfo = [System.Net.Dns]::GetHostByName(($env:computerName)) $hostname = $computerinfo.HostName $ipAddress = $computerinfo.AddressList[0] $fqdn = $computerinfo.HostName } else { $command = [scriptblock]::Create('[System.Net.Dns]::GetHostByName(($env:computerName))') $computerinfo = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential $command = [scriptblock]::Create('$env:COMPUTERNAME') $result = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential $hostname = $result.ToString() $ipAddress = $computerinfo.AddressList[0] $fqdn = $computerinfo.HostName } if ($informationStore -eq 'SQL') { # Setup the query to check of the host is already added $query = " IF EXISTS (SELECT HostName FROM Host WHERE HostName ='$hostname') BEGIN SELECT CAST(1 AS BIT) AS HostKnown; END; ELSE BEGIN SELECT CAST(0 AS BIT) AS HostKnown; END; " # Execute the query $hostKnown = (Invoke-DbaQuery -SqlInstance $pdcSqlInstance -SqlCredential $pdcCredential -Database $pdcDatabase -Query $query -EnableException).HostKnown } elseif ($informationStore -eq 'File') { $hosts = Get-ChildItem -Path DCNJSONFolder:\ -Filter *hosts.json | ForEach-Object { Get-Content $_.FullName | ConvertFrom-Json } $hostKnown = [bool]($hostname -in $hosts.HostName) } } catch { Stop-PSFFunction -Message "Couldnt execute query to see if host was known" -Target $query -ErrorRecord $_ -Continue } # Add the host if the host is known if (-not $hostKnown) { if ($PSCmdlet.ShouldProcess($hostname, "Adding hostname to database")) { if ($informationStore -eq 'SQL') { Write-PSFMessage -Message "Adding host $hostname to database" -Level Verbose $query = " DECLARE @HostID INT; EXECUTE dbo.Host_New @HostID = @HostID OUTPUT, -- int @HostName = '$hostname', -- varchar(100) @IPAddress = '$ipAddress', -- varchar(20) @FQDN = '$fqdn' -- varchar(255) SELECT @HostID AS HostID " try { $hostID = (Invoke-DbaQuery -SqlInstance $pdcSqlInstance -SqlCredential $pdcCredential -Database $pdcDatabase -Query $query -EnableException).HostID } catch { Stop-PSFFunction -Message "Couldnt execute query for adding host" -Target $query -ErrorRecord $_ -Continue } } elseif ($informationStore -eq 'File') { [array]$hosts = $null # Get all the images $hosts = Get-ChildItem -Path DCNJSONFolder:\ -Filter *hosts.json | ForEach-Object { Get-Content $_.FullName | ConvertFrom-Json } # Setup the new host id if ($hosts.Count -ge 1) { $hostID = ($hosts[-1].HostID | Sort-Object HostID) + 1 } else { $hostID = 1 } # Add the new information to the array $hosts += [PSCustomObject]@{ HostID = $hostID HostName = $hostname IPAddress = $ipAddress.IPAddressToString FQDN = $fqdn } # Test if the JSON folder can be reached if (-not (Test-Path -Path "DCNJSONFolder:\")) { $command = [scriptblock]::Create("Import-Module dbaclone -Force") try { Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential } catch { Stop-PSFFunction -Message "Couldn't import module remotely" -Target $command return } } # Setup the json file $jsonHostFile = "DCNJSONFolder:\hosts.json" # Convert the data back to JSON $hosts | ConvertTo-Json | Set-Content $jsonHostFile } } } else { if ($informationStore -eq 'SQL') { Write-PSFMessage -Message "Selecting host $hostname from database" -Level Verbose $query = "SELECT HostID FROM Host WHERE HostName = '$hostname'" try { $hostID = (Invoke-DbaQuery -SqlInstance $pdcSqlInstance -SqlCredential $pdcCredential -Database $pdcDatabase -Query $query -EnableException).HostID } catch { Stop-PSFFunction -Message "Couldnt execute query for retrieving host id" -Target $query -ErrorRecord $_ -Continue } } elseif ($informationStore -eq 'File') { $hostID = ($hosts | Where-Object { $_.Hostname -eq $hostname } | Select-Object HostID -Unique).HostID } } # Set privileges for access path try { # Check if computer is local if ($computer.IsLocalhost) { Set-DcnPermission -Path $accessPath } else { [string]$commandText = "Set-DcnPermission -Path '$($accessPath)'" $command = [scriptblock]::Create($commandText) $null = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $DestinationCredential } } catch { Stop-PSFFunction -Message "Couldn't create access path directory" -ErrorRecord $_ -Target $accessPath -Continue } # Setup the clone location $cloneLocation = "$($clonePath)" if ($informationStore -eq 'SQL') { # Get the image id from the database Write-PSFMessage -Message "Selecting image from database" -Level Verbose try { $query = "SELECT ImageID, ImageName FROM dbo.Image WHERE ImageLocation = '$ParentVhd'" $image = Invoke-DbaQuery -SqlInstance $pdcSqlInstance -SqlCredential $pdcCredential -Database $pdcDatabase -Query $query -EnableException } catch { Stop-PSFFunction -Message "Couldnt execute query for retrieving image id" -Target $query -ErrorRecord $_ -Continue } if ($PSCmdlet.ShouldProcess("$($clonePath)", "Adding clone to database")) { if ($null -ne $image.ImageID) { # Setup the query to add the clone to the database Write-PSFMessage -Message "Adding clone $cloneLocation to database" -Level Verbose $query = " DECLARE @CloneID INT; EXECUTE dbo.Clone_New @CloneID = @CloneID OUTPUT, -- int @ImageID = $($image.ImageID), -- int @HostID = $hostId, -- int @CloneLocation = '$cloneLocation', -- varchar(255) @AccessPath = '$accessPath', -- varchar(255) @SqlInstance = '$($server.DomainInstanceName)', -- varchar(50) @DatabaseName = '$cloneDatabase', -- varchar(100) @IsEnabled = $active -- bit SELECT @CloneID AS CloneID " Write-PSFMessage -Message "Query New Clone`n$query" -Level Debug # execute the query try { $result = Invoke-DbaQuery -SqlInstance $pdcSqlInstance -SqlCredential $pdcCredential -Database $pdcDatabase -Query $query -EnableException $cloneID = $result.CloneID } catch { Stop-PSFFunction -Message "Couldnt execute query for adding clone" -Target $query -ErrorRecord $_ -Continue } } else { Stop-PSFFunction -Message "Image couldn't be found" -Target $imageName -Continue } } } elseif ($informationStore -eq 'File') { # Get the image $image = Get-DcnImage -ImageLocation $ParentVhd [array]$clones = $null # Get all the images $clones = Get-DcnClone # Setup the new image id if ($clones.Count -ge 1) { $cloneID = ($clones[-1].CloneID | Sort-Object CloneID) + 1 } else { $cloneID = 1 } # Add the new information to the array $clones += [PSCustomObject]@{ CloneID = $cloneID ImageID = $image.ImageID ImageName = $image.ImageName ImageLocation = $ParentVhd HostID = $hostId HostName = $hostname CloneLocation = $cloneLocation AccessPath = $accessPath SqlInstance = $($server.DomainInstanceName) DatabaseName = $cloneDatabase IsEnabled = $active } # Test if the JSON folder can be reached if (-not (Test-Path -Path "DCNJSONFolder:\")) { $command = [scriptblock]::Create("Import-Module dbaclone -Force") try { Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential } catch { Stop-PSFFunction -Message "Couldn't import module remotely" -Target $command return } } # Set the clone file $jsonCloneFile = "DCNJSONFolder:\clones.json" # Convert the data back to JSON $clones | ConvertTo-Json | Set-Content $jsonCloneFile } if (-not $SkipDatabaseMount) { $cloneInstance = $server.DomainInstanceName } else { $cloneInstance = $null } # Add the results to the custom object [PSCustomObject]@{ CloneID = $cloneID CloneLocation = $cloneLocation AccessPath = $accessPath SqlInstance = $cloneInstance DatabaseName = $cloneDatabase IsEnabled = $active ImageID = $image.ImageID ImageName = $image.ImageName ImageLocation = $ParentVhd HostName = $hostname } } # End for each database #} # End for each sql instance } # End process end { # Test if there are any errors if (Test-PSFFunctionInterrupt) { return } Write-PSFMessage -Message "Finished creating database clone" -Level Verbose } } function Remove-DcnClone { <# .SYNOPSIS Remove-DcnClone removes one or more clones from a host .DESCRIPTION Remove-DcnClone is able to remove one or more clones from a host. The command looks up all the records dor a particular hostname. It will remove the database from the database server and all related files. The filter parameters Database and ExcludeDatabase work like wildcards. There is no need to include the asterisk (*). See the examples for more details .PARAMETER HostName The hostname to filter on .PARAMETER SqlCredential Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. To use: $scred = Get-Credential, then pass $scred object to the -SqlCredential parameter. Windows Authentication will be used if SqlCredential is not specified. SQL Server does not accept Windows credentials being passed as credentials. To connect as a different Windows user, run PowerShell as that user. .PARAMETER DcnSqlCredential Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. This works similar as SqlCredential but is only meant for authentication to the PSDatabaseClone database server and database. .PARAMETER Credential Allows you to login to systems using a credential. To use: $scred = Get-Credential, then pass $scred object to the -Credential parameter. .PARAMETER Database Allows to filter to include specific databases .PARAMETER ExcludeDatabase Allows to filter to exclude specific databases .PARAMETER All Remove all the clones .PARAMETER InputObject The input object that is used for pipeline use .PARAMETER EnableException By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .NOTES Author: Sander Stad (@sqlstad, sqlstad.nl) Website: https://psdatabaseclone.org Copyright: (C) Sander Stad, sander@sqlstad.nl License: MIT https://opensource.org/licenses/MIT .LINK https://psdatabaseclone.org/ .EXAMPLE Remove-DcnClone -HostName Host1 -Database Clone1 Removes the clones that are registered at Host1 and have the text "Clone1" .EXAMPLE Remove-DcnClone -HostName Host1, Host2, Host3 -Database Clone Removes the clones that are registered at multiple hosts and have the text "Clone" .EXAMPLE Remove-DcnClone -HostName Host1 Removes all clones from Host1 #> [CmdLetBinding(DefaultParameterSetName = "HostName", SupportsShouldProcess = $true, ConfirmImpact = 'High')] param( [parameter(ParameterSetName = "HostName")] [string[]]$HostName, [PSCredential]$SqlCredential, [PSCredential]$DcnSqlCredential, [PSCredential]$Credential, [string[]]$Database, [string[]]$ExcludeDatabase, [switch]$All, [parameter(ValueFromPipeline = $true, ParameterSetName = "Clone")] [object[]]$InputObject, [switch]$EnableException ) begin { # Check if the console is run in Administrator mode if ( -not (Test-PSFPowerShell -Elevated) ) { Stop-PSFFunction -Message "Module requires elevation. Please run the console in Administrator mode" -Continue } if (-not (Test-DcnModule -SetupStatus)) { Stop-PSFFunction -Message "The module setup has NOT yet successfully run. Please run 'Set-DcnConfiguration'" -Continue } # Get the information store $informationStore = Get-PSFConfigValue -FullName psdatabaseclone.informationstore.mode if ($informationStore -eq 'SQL') { # Get the module configurations [DbaInstanceParameter]$pdcSqlInstance = Get-PSFConfigValue -FullName psdatabaseclone.database.Server $pdcDatabase = Get-PSFConfigValue -FullName psdatabaseclone.database.name if (-not $DcnSqlCredential) { $pdcCredential = Get-PSFConfigValue -FullName psdatabaseclone.informationstore.credential -Fallback $null } else { $pdcCredential = $DcnSqlCredential } # Test the module database setup if ($PSCmdlet.ShouldProcess("Test-DcnConfiguration", "Testing module setup")) { try { Test-DcnConfiguration -SqlCredential $pdcCredential -EnableException } catch { Stop-PSFFunction -Message "Something is wrong in the module configuration" -ErrorRecord $_ -Continue } } } # Get all the items $items = @() $items += Get-DcnClone if (-not $All) { if ($HostName) { Write-PSFMessage -Message "Filtering hostnames" -Level Verbose $items = $items | Where-Object { $_.HostName -in $HostName } } if ($Database) { Write-PSFMessage -Message "Filtering included databases" -Level Verbose $items = $items | Where-Object { $_.DatabaseName -in $Database } } if ($ExcludeDatabase) { Write-PSFMessage -Message "Filtering excluded databases" -Level Verbose $items = $items | Where-Object { $_.DatabaseName -notin $Database } } } # Append the items $InputObject += $items } process { # Test if there are any errors if (Test-PSFFunctionInterrupt) { return } Write-PSFMessage -Message "Started removing database clones" -Level Verbose # Group the objects to make it easier to go through [array]$clones = $InputObject | Group-Object SqlInstance # Loop through each of the host names foreach ($clone in $clones) { # Connect to the instance if (-not $null -eq $clone.Name) { Write-PSFMessage -Message "Attempting to connect to clone database server $($clone.Name).." -Level Verbose try { $server = Connect-DbaInstance -SqlInstance $clone.Name -SqlCredential $SqlCredential -SqlConnectionOnly } catch { Stop-PSFFunction -Message "Could not connect to Sql Server instance $($clone.Name)" -ErrorRecord $_ -Target $clone.Name -Continue } } # Loop through each of the results foreach ($item in $clone.Group) { # Setup the computer object $computer = [PsfComputer]$item.HostName if (-not $computer.IsLocalhost) { # Get the result for the remote test try { $resultPSRemote = Test-DcnRemoting -ComputerName $item.HostName -Credential $Credential -EnableException # Check the result if ($resultPSRemote.Result) { $command = [scriptblock]::Create("Import-Module dbaclone") try { Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential } catch { Stop-PSFFunction -Message "Couldn't import module remotely" -Target $command return } } else { Stop-PSFFunction -Message "Couldn't connect to host remotely.`nVerify that the specified computer name is valid, that the computer is accessible over the network, and that a firewall exception for the WinRM service is enabled and allows access from this computer" -Target $resultPSRemote -Continue } } catch { Stop-PSFFunction -Message "Something went wrong testing if the host is remote" -Target $item -ErrordRecord $_ return } } if (-not $null -eq $item.SqlInstance) { $server = Connect-DbaInstance -SqlInstance $item.SqlInstance -SqlCredential $SqlCredential if ($item.DatabaseName -in $server.Databases.Name) { if ($PSCmdlet.ShouldProcess($item.DatabaseName, "Removing database $($item.DatabaseName)")) { # Remove the database try { Write-PSFMessage -Message "Removing database $($item.DatabaseName) from $($item.SqlInstance)" -Level Verbose $null = Remove-DbaDatabase -SqlInstance $item.SqlInstance -SqlCredential $SqlCredential -Database $item.DatabaseName -Confirm:$false -EnableException } catch { Stop-PSFFunction -Message "Could not remove database $($item.DatabaseName) from $server" -ErrorRecord $_ -Target $server -Continue } } } else { Write-PSFMessage -Level Verbose -Message "Could not find database [$($item.DatabaseName)] on $($item.SqlInstance)" } } if ($PSCmdlet.ShouldProcess($item.CloneLocation, "Dismounting the vhd")) { # Dismounting the vhd try { if (Test-Path -Path $item.CloneLocation) { if ($computer.IsLocalhost) { Write-PSFMessage -Message "Dismounting disk '$($item.CloneLocation)' from $($item.HostName)" -Level Verbose $null = Dismount-DiskImage -ImagePath $item.CloneLocation } else { $command = [ScriptBlock]::Create("Test-Path -Path '$($item.CloneLocation)'") Write-PSFMessage -Message "Dismounting disk '$($item.CloneLocation)' from $($item.HostName)" -Level Verbose $result = Invoke-PSFCommand -ComputerName $item.HostName -ScriptBlock $command -Credential $Credential $command = [scriptblock]::Create("Dismount-DiskImage -ImagePath '$($item.CloneLocation)'") $null = Invoke-PSFCommand -ComputerName $item.HostName -ScriptBlock $command -Credential $Credential } } else { Write-PSFMessage -Level Verbose -Message "Could not find clone file '$($item.CloneLocation)'" } } catch { Stop-PSFFunction -Message "Could not dismount vhd $($item.CloneLocation)" -ErrorRecord $_ -Target $result -Continue } } if ($PSCmdlet.ShouldProcess($item.CloneLocation, "Removing clone files and folders")) { # Remove clone file and related access path try { if ($computer.IsLocalhost) { if (Test-Path -Path $item.AccessPath) { Write-PSFMessage -Message "Removing vhd access path" -Level Verbose $null = Remove-Item -Path "$($item.AccessPath)" -Credential $Credential -Force } if (Test-Path -Path $item.CloneLocation) { Write-PSFMessage -Message "Removing vhd" -Level Verbose $null = Remove-Item -Path "$($item.CloneLocation)" -Credential $Credential -Force } } else { $command = [scriptblock]::Create("Test-Path -Path '$($item.AccessPath)'") $result = Invoke-PSFCommand -ComputerName $item.HostName -ScriptBlock $command -Credential $Credential if ($result) { Write-PSFMessage -Message "Removing vhd access path" -Level Verbose $command = [scriptblock]::Create("Remove-Item -Path '$($item.AccessPath)' -Force") $null = Invoke-PSFCommand -ComputerName $item.HostName -ScriptBlock $command -Credential $Credential } $command = [scriptblock]::Create("Test-Path -Path '$($item.CloneLocation)'") $result = Invoke-PSFCommand -ComputerName $item.HostName -ScriptBlock $command -Credential $Credential if ($result) { Write-PSFMessage -Message "Removing vhd" -Level Verbose $command = [scriptblock]::Create("Remove-Item -Path '$($item.CloneLocation)' -Force") $null = Invoke-PSFCommand -ComputerName $item.HostName -ScriptBlock $command -Credential $Credential } } } catch { Stop-PSFFunction -Message "Could not remove clone files" -ErrorRecord $_ -Target $result -Continue } } if ($PSCmdlet.ShouldProcess("Clone ID: $($item.CloneID)", "Deleting clone from database")) { if ($informationStore -eq 'SQL') { # Removing records from database try { $query = "DELETE FROM dbo.Clone WHERE CloneID = $($item.CloneID);" $null = Invoke-DbaQuery -SqlInstance $pdcSqlInstance -SqlCredential $pdcCredential -Database $pdcDatabase -Query $query -EnableException } catch { Stop-PSFFunction -Message "Could not remove clone record from database" -ErrorRecord $_ -Target $query -Continue } } elseif ($informationStore -eq 'File') { [array]$cloneData = $null [array]$newCloneData = $null $cloneData = Get-DcnClone $newCloneData = $cloneData | Where-Object { $_.CloneID -ne $item.CloneID } # Set the clone file $jsonCloneFile = "DCNJSONFolder:\clones.json" # Convert the data back to JSON if ($newCloneData.Count -ge 1) { $newCloneData | ConvertTo-Json | Set-Content $jsonCloneFile } else { Clear-Content -Path $jsonCloneFile } } } } # End for each group item } # End for each clone } # End process end { # Test if there are any errors if (Test-PSFFunctionInterrupt) { return } Write-PSFMessage -Message "Finished removing database clone(s)" -Level Verbose } } function Initialize-DcnVhdDisk { <# .SYNOPSIS Initialize-DcnVhdDisk initialized the VHD .DESCRIPTION Initialize-DcnVhdDisk will initialize the VHD. It mounts the disk, creates a volume, creates the partition and sets it to active .PARAMETER Path The path to the VHD .PARAMETER Credential Allows you to use credentials for creating items in other locations To use: $scred = Get-Credential, then pass $scred object to the -Credential parameter. .PARAMETER PartitionStyle A partition can either be initialized as MBR or as GPT. GPT is the default. .PARAMETER AllocationUnitSize Set the allocation unit size for the disk. By default it's 64 KB because that's what SQL Server tends to write most of the time. .PARAMETER EnableException By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .NOTES Author: Sander Stad (@sqlstad, sqlstad.nl) Website: https://psdatabaseclone.org Copyright: (C) Sander Stad, sander@sqlstad.nl License: MIT https://opensource.org/licenses/MIT .LINK https://psdatabaseclone.org/ .EXAMPLE Initialize-DcnVhdDisk -Path $path Initialize the disk pointing to the path with all default settings .EXAMPLE Initialize-DcnVhdDisk -Path $path -AllocationUnitSize 4KB Initialize the disk and format the partition with a 4Kb allocation unit size #> [CmdLetBinding(SupportsShouldProcess = $true)] [OutputType('System.String')] [OutputType('PSCustomObject')] Param( [Parameter(Mandatory = $true)] [string]$Path, [PSCredential]$Credential, [ValidateSet('GPT', 'MBR')] [string]$PartitionStyle, [int]$AllocationUnitSize = 64KB, [switch]$EnableException ) begin { # Check if the console is run in Administrator mode if ( -not (Test-PSFPowerShell -Elevated) ) { Stop-PSFFunction -Message "Module requires elevation. Please run the console in Administrator mode" -Continue } # Check the path to the vhd if (-not (Test-Path -Path $Path -Credential $Credential)) { Stop-PSFFunction -Message "Vhd path $Path cannot be found" -Target $Path -Continue } # Check the partition style if (-not $PartitionStyle) { Write-PSFMessage -Message "Setting partition style to 'GPT'" -Level Verbose $PartitionStyle = 'GPT' } } process { # Test if there are any errors if (Test-PSFFunctionInterrupt) { return } # Get all the disks $disks = Get-Disk # Check if disk is already mounted if ($disks.Location -contains $Path) { Write-PSFMessage -Message "Vhd is already mounted" -Level Warning # retrieve the specific disk $disk = $disks | Where-Object Location -eq $Path } else { if ($PSCmdlet.ShouldProcess("Mounting disk")) { # Mount the vhd try { Write-PSFMessage -Message "Mounting disk $disk" -Level Verbose # Mount the disk Mount-DiskImage -ImagePath $Path # Get the disk $diskImage = Get-DiskImage -ImagePath $Path $disk = Get-Disk | Where-Object Number -eq $diskImage.Number } catch { Stop-PSFFunction -Message "Couldn't mount vhd" -Target $Path -ErrorRecord $_ -Continue } } } if ($PSCmdlet.ShouldProcess("Initializing disk")) { # Check if the disk is already initialized if ($disk.PartitionStyle -eq 'RAW') { try { Write-PSFMessage -Message "Initializing disk $disk" -Level Verbose $disk | Initialize-Disk -PartitionStyle $PartitionStyle -Confirm:$false } catch { Stop-PSFFunction -Message "Couldn't initialize disk" -Target $disk -ErrorRecord $_ -Continue } } } if ($PSCmdlet.ShouldProcess("Partitioning volume")) { # Create the partition, set the drive letter and format the volume try { $params = @{ FileSystem = "NTFS" NewFileSystemLabel = "PSDatabaseClone" AllocationUnitSize = $AllocationUnitSize Confirm = $false } $volume = $disk | New-Partition -UseMaximumSize | Format-Volume @params } catch { # Dismount the drive Dismount-DiskImage -ImagePath $Path Stop-PSFFunction -Message "Couldn't create the partition" -Target $disk -ErrorRecord $_ -Continue } } # Add the results to the custom object [PSCustomObject]@{ Disk = $disk Partition = (Get-Partition -Disk $disk) Volume = $volume } } end { # Test if there are any errors if (Test-PSFFunctionInterrupt) { return } Write-PSFMessage -Message "Finished initializing disk(s)" -Level Verbose } } function New-DcnVhdDisk { <# .SYNOPSIS New-DcnVhdDisk creates a new VHD .DESCRIPTION New-DcnVhdDisk will create a new VHD. .PARAMETER Destination The destination path of the VHD .PARAMETER Name The name of the VHD .PARAMETER FileName The file name of the VHD .PARAMETER VhdType The type of the harddisk. This can either by VHD (version 1) or VHDX (version 2) The default is VHDX. .PARAMETER Size The size of the VHD in MB. If no size is used the default will be set to the type of VHD. The default for VHD is 2 TB and for VHDX 64TB .PARAMETER FixedSize Set the VHD to have a fixed size or not. Be careful using this parameter. Fixed will make the VHD use the space assigned in -Size .PARAMETER ReadOnly Set the VHD to readonly .PARAMETER Force Forcefully create the neccesary items .PARAMETER EnableException By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .NOTES Author: Sander Stad (@sqlstad, sqlstad.nl) Website: https://psdatabaseclone.org Copyright: (C) Sander Stad, sander@sqlstad.nl License: MIT https://opensource.org/licenses/MIT .LINK https://psdatabaseclone.org/ .EXAMPLE New-DcnVhdDisk -Destination C:\temp -Name Database1 -Size 1GB Creates a dynamic VHD in C:\Temp named Database1.vhdx with a size of 1GB #> [CmdLetBinding(SupportsShouldProcess = $true)] [OutputType('System.String')] param( [parameter(Mandatory = $true)] [string]$Destination, [string]$Name, [string]$FileName, [ValidateSet('VHD', 'VHDX', 'vhd', 'vhdx')] [string]$VhdType, [uint64]$Size, [switch]$FixedSize, [switch]$ReadOnly, [switch]$Force, [switch]$EnableException ) begin { # Check if the console is run in Administrator mode if ( -not (Test-PSFPowerShell -Elevated) ) { Stop-PSFFunction -Message "Module requires elevation. Please run the console in Administrator mode" -Continue } # Check the destination path if (-not (Test-Path $Destination)) { if ($PSCmdlet.ShouldProcess($Destination, "Creating destination directory")) { try { Write-PSFMessage -Message "Creating destination directory $Destination" -Level Verbose $null = New-Item -Path $Destination -ItemType Directory -Force } catch { Stop-PSFFunction -Message "Couldn't create directory $Destination" -ErrorRecord $_ -Target $Destination -Continue } } } # Check the vhd type if (-not $VhdType) { Write-PSFMessage -Message "Setting vhd type to 'VHDX'" -Level Verbose $VhdType = 'VHDX' } # Make sure thevalue is in uppercase all th time $VhdType = $VhdType.ToUpper() # Check the size of the file if (-not $Size) { switch ($VhdType) { 'VHD' { $Size = 2048MB } 'VHDX' { $Size = 64TB } } } else { if ($VhdType -eq 'VHD' -and $Size -gt 2TB) { Stop-PSFFunction -Message "Size cannot exceed 2TB when using VHD type." } elseif ($VhdType -eq 'VHDX' -and $Size -gt 64TB) { Stop-PSFFunction -Message "Size cannot exceed 64TB when using VHDX type." } if ($Size -lt 3MB) { Stop-PSFFunction -Message "The size of the vhd cannot be smaller than 3MB" -Continue } } # Make sure the size in MB instead of some other version $Size = $Size / 1MB # Check the name and file name parameters if (-not $Name -and -not $FileName) { Stop-PSFFunction -Message "Either set the Name or FileName parameter" } else { if (-not $FileName) { $FileName = "$Name.$($VhdType.ToLower())" Write-PSFMessage -Message "Setting file name to $FileName" -Level Verbose } elseif ($FileName) { if (($FileName -notlike "*.vhd") -and ($FileName -notlike "*.vhdx")) { Stop-PSFFunction -Message "The filename needs to have the .vhd or .vhdx extension" -Target $FileName -Continue } } } # Set the vhd path if ($Destination.EndsWith("\")) { $vhdPath = "$Destination$FileName" } else { $vhdPath = "$Destination\$FileName" } Write-PSFMessage -Message "Vhd path set to $vhdPath" -Level Verbose # Check if the file does not yet exist if (Test-Path $vhdPath) { if (-not $Force) { Stop-PSFFunction -Message "The vhd file already exists" -Continue } else { try { Remove-Item -Path $vhdPath -Force:$Force } catch { Stop-PSFFunction -Message "Could not remove VHD '$vhdPath'" -Continue -ErrorRecord $_ } } } # Set the location where to save the diskpart command $diskpartScriptFile = Get-PSFConfigValue -FullName psdatabaseclone.diskpart.scriptfile -Fallback "$env:APPDATA\psdatabaseclone\diskpartcommand.txt" if (-not (Test-Path -Path $diskpartScriptFile)) { if ($PSCmdlet.ShouldProcess($diskpartScriptFile, "Creating dispart file")) { try { $null = New-Item -Path $diskpartScriptFile -ItemType File } catch { Stop-PSFFunction -Message "Could not create diskpart script file" -ErrorRecord $_ -Continue } } } } process { # Test if there are any errors if (Test-PSFFunctionInterrupt) { return } if ($PSCmdlet.ShouldProcess($vhdPath, "Creating VHD")) { # Check if the file needs to have a fixed size try { if ($FixedSize) { $command = "create vdisk file='$vhdPath' maximum=$Size type=fixed" } else { $command = "create vdisk file='$vhdPath' maximum=$Size type=expandable" } # Set the content of the diskpart script file Set-Content -Path $diskpartScriptFile -Value $command -Force $script = [ScriptBlock]::Create("diskpart /s $diskpartScriptFile") $null = Invoke-PSFCommand -ScriptBlock $script } catch { Stop-PSFFunction -Message "Something went wrong creating the vhd" -ErrorRecord $_ -Continue } } } end { # Clean up the script file for diskpart Remove-Item $diskpartScriptFile -Force -Confirm:$false # Test if there are any errors if (Test-PSFFunctionInterrupt) { return } Write-PSFMessage "Finished creating vhd file" -Level Verbose } } function Get-DcnHost { <# .SYNOPSIS Get-DcnHost get all the hosts that have clones .DESCRIPTION Get-DcnHost will retrieve the hosts that have clones By default all the hosts are returned .PARAMETER SqlCredential Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. To use: $scred = Get-Credential, then pass $scred object to the -SqlCredential parameter. Windows Authentication will be used if SqlCredential is not specified. SQL Server does not accept Windows credentials being passed as credentials. To connect as a different Windows user, run PowerShell as that user. .PARAMETER DcnSqlCredential Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. This works similar as SqlCredential but is only meant for authentication to the PSDatabaseClone database server and database. .PARAMETER Credential Allows you to login to servers or use authentication to access files and folder/shares $scred = Get-Credential, then pass $scred object to the -Credential parameter. .PARAMETER EnableException By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .NOTES Author: Sander Stad (@sqlstad, sqlstad.nl) Website: https://psdatabaseclone.org Copyright: (C) Sander Stad, sander@sqlstad.nl License: MIT https://opensource.org/licenses/MIT .LINK https://psdatabaseclone.org/ .EXAMPLE Get-DcnHost Get all the hosts #> [CmdLetBinding()] param( [PSCredential]$SqlCredential, [PSCredential]$DcnSqlCredential, [PSCredential]$Credential, [switch]$EnableException ) begin { # Check if the setup has ran if (-not (Test-DcnModule -SetupStatus)) { Stop-PSFFunction -Message "The module setup has NOT yet successfully run. Please run 'Set-DcnConfiguration'" -Continue } # Get the information store $informationStore = Get-PSFConfigValue -FullName psdatabaseclone.informationstore.mode if ($informationStore -eq 'SQL') { # Get the module configurations [DbaInstanceParameter]$pdcSqlInstance = Get-PSFConfigValue -FullName psdatabaseclone.database.Server $pdcDatabase = Get-PSFConfigValue -FullName psdatabaseclone.database.name if (-not $DcnSqlCredential) { $pdcCredential = Get-PSFConfigValue -FullName psdatabaseclone.informationstore.credential -Fallback $null } else { $pdcCredential = $DcnSqlCredential } # Test the module database setup try { Test-DcnConfiguration -SqlCredential $pdcCredential -EnableException } catch { Stop-PSFFunction -Message "Something is wrong in the module configuration" -ErrorRecord $_ -Continue } $query = "SELECT DISTINCT h.HostName FROM dbo.Host AS h" try { $results = @() $results += Invoke-DbaQuery -SqlInstance $pdcSqlInstance -SqlCredential $DcnSqlCredential -Database $pdcDatabase -Query $query -As PSObject } catch { Stop-PSFFunction -Message "Could retrieve images from database $pdcDatabase" -ErrorRecord $_ -Target $query } } elseif ($informationStore -eq 'File') { try { if (Test-Path -Path "DCNJSONFolder:\") { # Get the clones $results = Get-ChildItem -Path "DCNJSONFolder:\" -Filter "*clones.json" | ForEach-Object { Get-Content $_.FullName | ConvertFrom-Json } } else { Stop-PSFFunction -Message "Could not reach image information location 'DCNJSONFolder:\'" -ErrorRecord $_ -Target "DCNJSONFolder:\" return } } catch { Stop-PSFFunction -Message "Couldn't get results from JSN folder" -Target "DCNJSONFolder:\" -ErrorRecord $_ } } } process { # Test if there are any errors if (Test-PSFFunctionInterrupt) { return } foreach ($result in $results) { [pscustomobject]@{ HostName = $result.HostName } } } end { # Test if there are any errors if (Test-PSFFunctionInterrupt) { return } Write-PSFMessage -Message "Finished retrieving host(s)" -Level Verbose } } function Get-DcnImage { <# .SYNOPSIS Get-DcnImage get on or more clones .DESCRIPTION Get-DcnImage will retrieve the clones and apply filters if needed. By default all the clones are returned .PARAMETER SqlCredential Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. To use: $scred = Get-Credential, then pass $scred object to the -SqlCredential parameter. Windows Authentication will be used if SqlCredential is not specified. SQL Server does not accept Windows credentials being passed as credentials. To connect as a different Windows user, run PowerShell as that user. .PARAMETER DcnSqlCredential Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. This works similar as SqlCredential but is only meant for authentication to the PSDatabaseClone database server and database. .PARAMETER Credential Allows you to login to servers or use authentication to access files and folder/shares $scred = Get-Credential, then pass $scred object to the -Credential parameter. .PARAMETER ImageID Filter based on the image id .PARAMETER ImageName Filter based on the image name .PARAMETER ImageLocation Filter based on the image location .PARAMETER Database Filter based on the database .PARAMETER EnableException By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .NOTES Author: Sander Stad (@sqlstad, sqlstad.nl) Website: https://psdatabaseclone.org Copyright: (C) Sander Stad, sander@sqlstad.nl License: MIT https://opensource.org/licenses/MIT .LINK https://psdatabaseclone.org/ .EXAMPLE Get-DcnImage Get all the images .EXAMPLE Get-DcnImage -ImageName DB1_20180704220944, DB2_20180704221144 Retrieve the images for DB1_20180704220944, DB2_20180704221144 .EXAMPLE Get-DcnImage -ImageLocation "\\fileserver1\psdatabaseclone\images\DB1_20180704220944.vhdx" Get all the images that are the same as the image location .EXAMPLE Get-DcnImage -Database DB1, DB2 Get all the images that were made for databases DB1 and DB2 #> [CmdLetBinding()] param( [PSCredential]$SqlCredential, [PSCredential]$DcnSqlCredential, [PSCredential]$Credential, [int[]]$ImageID, [string[]]$ImageName, [string[]]$ImageLocation, [string[]]$Database, [switch]$EnableException ) begin { # Check if the setup has ran if (-not (Test-DcnModule -SetupStatus)) { Stop-PSFFunction -Message "The module setup has NOT yet successfully run. Please run 'Set-DcnConfiguration'" -Continue } # Get the information store $informationStore = Get-PSFConfigValue -FullName psdatabaseclone.informationstore.mode if ($informationStore -eq 'SQL') { # Get the module configurations [DbaInstanceParameter]$pdcSqlInstance = Get-PSFConfigValue -FullName psdatabaseclone.database.Server $pdcDatabase = Get-PSFConfigValue -FullName psdatabaseclone.database.name if (-not $DcnSqlCredential) { $pdcCredential = Get-PSFConfigValue -FullName psdatabaseclone.informationstore.credential -Fallback $null } else { $pdcCredential = $DcnSqlCredential } # Test the module database setup try { Test-DcnConfiguration -SqlCredential $pdcCredential -EnableException } catch { Stop-PSFFunction -Message "Something is wrong in the module configuration" -ErrorRecord $_ -Continue } $query = " SELECT ImageID, ImageName, ImageLocation, SizeMB, DatabaseName, DatabaseTimestamp, CreatedOn FROM dbo.Image; " try { $results = @() $results += Invoke-DbaQuery -SqlInstance $pdcSqlInstance -SqlCredential $DcnSqlCredential -Database $pdcDatabase -Query $query -As PSObject } catch { Stop-PSFFunction -Message "Could retrieve images from database $pdcDatabase" -ErrorRecord $_ -Target $query } } elseif ($informationStore -eq 'File') { try { if (Test-Path -Path "DCNJSONFolder:\") { # Get the clones $results = Get-ChildItem -Path "DCNJSONFolder:\" -Filter "*images.json" | ForEach-Object { Get-Content $_.FullName | ConvertFrom-Json } } else { Stop-PSFFunction -Message "Could not reach image information location 'DCNJSONFolder:\'" -ErrorRecord $_ -Target "DCNJSONFolder:\" return } } catch { Stop-PSFFunction -Message "Couldn't get results from JSN folder" -Target "DCNJSONFolder:\" -ErrorRecord $_ } } # Filter image id if ($ImageID) { $results = $results | Where-Object { $_.ImageID -in $ImageID } } # Filter image name if ($ImageName) { $results = $results | Where-Object { $_.ImageName -in $ImageName } } # Filter image location if ($ImageLocation) { $results = $results | Where-Object { $_.ImageLocation -in $ImageLocation } } # Filter database if ($Database) { $results = $results | Where-Object { $_.DatabaseName -in $Database } } } process { # Test if there are any errors if (Test-PSFFunctionInterrupt) { return } foreach ($result in $results) { [pscustomobject]@{ ImageID = $result.ImageID ImageName = $result.ImageName ImageLocation = $result.ImageLocation SizeMB = $result.SizeMB DatabaseName = $result.DatabaseName DatabaseTimestamp = $result.DatabaseTimestamp CreatedOn = $result.CreatedOn } } } end { # Test if there are any errors if (Test-PSFFunctionInterrupt) { return } Write-PSFMessage -Message "Finished retrieving image(s)" -Level Verbose } } function New-DcnImage { <# .SYNOPSIS New-DcnImage creates a new image .DESCRIPTION New-DcnImage will create a new image based on a SQL Server database The command will either create a full backup or use the last full backup to create the image. Every image is created with the name of the database and a time stamp yyyyMMddHHmmss i.e "DB1_20180622171819.vhdx" .PARAMETER SourceSqlInstance Source SQL Server name or SMO object representing the SQL Server to connect to. This will be where the database is currently located .PARAMETER SourceSqlCredential Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. To use: $scred = Get-Credential, then pass $scred object to the -SourceSqlCredential parameter. Windows Authentication will be used if SqlCredential is not specified. SQL Server does not accept Windows credentials being passed as credentials. To connect as a different Windows user, run PowerShell as that user. .PARAMETER SourceCredential Allows you to login to other parts of a system like folders. To use: $scred = Get-Credential, then pass $scred object to the -SourceCredential parameter. .PARAMETER DestinationSqlInstance SQL Server name or SMO object representing the SQL Server to connect to. This is the server to use to temporarily restore the database to create the image. .PARAMETER DestinationSqlCredential Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. To use: $scred = Get-Credential, then pass $scred object to the -DestinationSqlCredential parameter. Windows Authentication will be used if SqlCredential is not specified. SQL Server does not accept Windows credentials being passed as credentials. To connect as a different Windows user, run PowerShell as that user. .PARAMETER DestinationCredential Allows you to login to other parts of a system like folders. To use: $scred = Get-Credential, then pass $scred object to the -DestinationCredential parameter. .PARAMETER DcnSqlCredential Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. This works similar as SqlCredential but is only meant for authentication to the PSDatabaseClone database server and database. By default the script will try to retrieve the configuration value "psdatabaseclone.informationstore.credential" .PARAMETER ImageNetworkPath Network path where to save the image. This has to be a UNC path .PARAMETER ImageLocalPath Local path where to save the image .PARAMETER Database Databases to create an image of .PARAMETER VhdType The type of the harddisk. This can either by VHD (version 1) or VHDX (version 2) The default is VHDX. .PARAMETER CreateFullBackup Create a new full backup of the database. The backup will be saved in the default backup directory .PARAMETER UseLastFullBackup Use the last full backup created for the database .PARAMETER BackupFilePath Use a specific backup file to create the image .PARAMETER CopyOnlyBackup Create a backup as COPY_ONLY .PARAMETER Force Forcefully execute commands when needed .PARAMETER EnableException By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .NOTES Author: Sander Stad (@sqlstad, sqlstad.nl) Website: https://psdatabaseclone.org Copyright: (C) Sander Stad, sander@sqlstad.nl License: MIT https://opensource.org/licenses/MIT .LINK https://psdatabaseclone.org/ .EXAMPLE New-DcnImage -SourceSqlInstance SQLDB1 -DestinationSqlInstance SQLDB2 -ImageLocalPath C:\Temp\images\ -Database DB1 -CreateFullBackup Create an image for databas DB1 from SQL Server SQLDB1. The temporary destination will be SQLDB2. The image will be saved in C:\Temp\images. .EXAMPLE New-DcnImage -SourceSqlInstance SQLDB1 -DestinationSqlInstance SQLDB2 -ImageLocalPath C:\Temp\images\ -Database DB1 -UseLastFullBackup Create an image from the database DB1 on SQLDB1 using the last full backup and use SQLDB2 as the temporary database server. The image is written to c:\Temp\images #> [CmdLetBinding(SupportsShouldProcess = $true)] param( [parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [DbaInstanceParameter]$SourceSqlInstance, [PSCredential]$SourceSqlCredential, [PSCredential]$SourceCredential, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [DbaInstanceParameter]$DestinationSqlInstance, [PSCredential]$DestinationSqlCredential, [PSCredential]$DestinationCredential, [PSCredential]$DcnSqlCredential, [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [object[]]$Database, [string]$ImageNetworkPath, [string]$ImageLocalPath, [ValidateSet('VHD', 'VHDX', 'vhd', 'vhdx')] [string]$VhdType, [switch]$CreateFullBackup, [switch]$UseLastFullBackup, [string]$BackupFilePath, [switch]$CopyOnlyBackup, [Alias('MaskingConfigFile', 'MaskingConfigFilePath')] [switch]$Force, [switch]$EnableException ) begin { # Check if the console is run in Administrator mode if ( -not (Test-PSFPowerShell -Elevated) ) { Stop-PSFFunction -Message "Module requires elevation. Please run the console in Administrator mode" -Continue } # Check if the setup has ran if (-not (Test-DcnModule -SetupStatus)) { Stop-PSFFunction -Message "The module setup has NOT yet successfully run. Please run 'Set-DcnConfiguration'" -Continue } if(-not $CreateFullBackup -and -not $UseLastFullBackup -and -not $BackupFilePath){ Stop-PSFFunction -Message "Unable to get last backup file. Please use -CreateFullBackup, -UseLastFullBackup or -BackupFile" -Continue } # Checking parameters if (-not $ImageNetworkPath) { Stop-PSFFunction -Message "Please enter the network path where to save the images" -Continue } # Check the vhd type if (-not $VhdType) { Write-PSFMessage -Message "Setting vhd type to 'VHDX'" -Level Verbose $VhdType = 'VHDX' } # Get the information store $informationStore = Get-PSFConfigValue -FullName psdatabaseclone.informationstore.mode if ($informationStore -eq 'SQL') { # Get the module configurations $pdcSqlInstance = Get-PSFConfigValue -FullName psdatabaseclone.database.Server $pdcDatabase = Get-PSFConfigValue -FullName psdatabaseclone.database.name if (-not $DcnSqlCredential) { $pdcCredential = Get-PSFConfigValue -FullName psdatabaseclone.informationstore.credential -Fallback $null } else { $pdcCredential = $DcnSqlCredential } # Test the module database setup if ($PSCmdlet.ShouldProcess("Test-DcnConfiguration", "Testing module setup")) { try { Test-DcnConfiguration -SqlCredential $pdcCredential -EnableException } catch { Stop-PSFFunction -Message "Something is wrong in the module configuration" -ErrorRecord $_ -Continue } } } Write-PSFMessage -Message "Started image creation" -Level Verbose # Try connecting to the instance Write-PSFMessage -Message "Attempting to connect to Sql Server $SourceSqlInstance.." -Level Verbose try { $sourceServer = Connect-DbaInstance -SqlInstance $SourceSqlInstance -SqlCredential $SourceSqlCredential } catch { Stop-PSFFunction -Message "Could not connect to Sql Server instance $SourceSqlInstance" -ErrorRecord $_ -Target $SourceSqlInstance return } # Cleanup the values in the network path if ($ImageNetworkPath.EndsWith("\")) { $ImageNetworkPath = $ImageNetworkPath.Substring(0, $ImageNetworkPath.Length - 1) } # Make up the data from the network path try { [uri]$uri = New-Object System.Uri($ImageNetworkPath) $uriHost = $uri.Host } catch { Stop-PSFFunction -Message "The image network path $ImageNetworkPath is not valid" -ErrorRecord $_ -Target $ImageNetworkPath return } # Setup the computer object $computer = [PsfComputer]$uriHost if (-not $computer.IsLocalhost) { # Get the result for the remote test $resultPSRemote = Test-DcnRemoting -ComputerName $computer -Credential $Credential # Check the result if ($resultPSRemote.Result) { $command = [scriptblock]::Create("Import-Module dbaclone -Force") try { Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential } catch { Stop-PSFFunction -Message "Couldn't import module remotely" -Target $command return } } else { Stop-PSFFunction -Message "Couldn't connect to host remotely.`nVerify that the specified computer name is valid, that the computer is accessible over the network, and that a firewall exception for the WinRM service is enabled and allows access from this computer" -Target $resultPSRemote -Continue } } # Get the local path from the network path if (-not $ImageLocalPath) { if ($PSCmdlet.ShouldProcess($ImageNetworkPath, "Converting UNC path to local path")) { try { # Check if computer is local if ($computer.IsLocalhost) { $ImageLocalPath = Convert-DcnUncPathToLocalPath -UncPath $ImageNetworkPath -EnableException } else { $command = "Convert-DcnUncPathToLocalPath -UncPath `"$ImageNetworkPath`" -EnableException" $commandGetLocalPath = [ScriptBlock]::Create($command) $ImageLocalPath = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $commandGetLocalPath -Credential $DestinationCredential if (-not $ImageLocalPath) { Stop-PSFFunction -Message "Could not convert network path to local path" -Target $ImageLocalPath return } } Write-PSFMessage -Message "Converted '$ImageNetworkPath' to '$ImageLocalPath'" -Level Verbose } catch { Stop-PSFFunction -Message "Something went wrong getting the local image path" -Target $ImageNetworkPath return } } } else { # Cleanup the values in the network path if ($ImageLocalPath.EndsWith("\")) { $ImageLocalPath = $ImageLocalPath.Substring(0, $ImageLocalPath.Length - 1) } # Check if the assigned value in the local path corresponds to the one retrieved try { # Check if computer is local if ($computer.IsLocalhost) { $convertedLocalPath = Convert-DcnUncPathToLocalPath -UncPath $ImageNetworkPath -EnableException } else { $command = "Convert-DcnUncPathToLocalPath -UncPath `"$ImageNetworkPath`" -EnableException" $commandGetLocalPath = [ScriptBlock]::Create($command) $convertedLocalPath = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $commandGetLocalPath -Credential $DestinationCredential } Write-PSFMessage -Message "Converted '$ImageNetworkPath' to '$ImageLocalPath'" -Level Verbose # Check if the ImageLocalPath and convertedLocalPath are the same if ($ImageLocalPath -ne $convertedLocalPath) { Stop-PSFFunction -Message "The local path '$ImageLocalPath' is not the same location as the network path '$ImageNetworkPath'" -Target $ImageNetworkPath return } } catch { Stop-PSFFunction -Message "Something went wrong getting the local image path" -Target $ImageNetworkPath return } } # Check the image local path if ($PSCmdlet.ShouldProcess("Verifying image local path")) { if ((Test-DbaPath -Path $ImageLocalPath -SqlInstance $SourceSqlInstance -SqlCredential $DestinationCredential) -ne $true) { Stop-PSFFunction -Message "Image local path $ImageLocalPath is not valid directory or can't be reached." -Target $SourceSqlInstance return } # Clean up the paths if ($ImageLocalPath.EndsWith("\")) { $ImageLocalPath = $ImageLocalPath.Substring(0, $ImageLocalPath.Length - 1) } $imagePath = $ImageLocalPath } # Check the database parameter if ($Database) { foreach ($db in $Database) { if ($db -notin $sourceServer.Databases.Name) { Stop-PSFFunction -Message "Database $db cannot be found on instance $SourceSqlInstance" -Target $SourceSqlInstance } } $DatabaseCollection = $sourceServer.Databases | Where-Object { $_.Name -in $Database } } else { Stop-PSFFunction -Message "Please supply a database to create an image for" -Target $SourceSqlInstance -Continue } if($BackupFilePath){ if(-not (Test-Path -Path $BackupFilePath)){ Stop-PSFFunction -Message "Could not find backup file '$($BackupFilePath)'" } if($Database.Count -gt 1){ Stop-PSFFunction -Message "You cannot enter multiple databases for the same backup file. Please just enter one" } } # Set time stamp $timestamp = Get-Date -format "yyyyMMddHHmmss" } process { # Test if there are any errors if (Test-PSFFunctionInterrupt) { return } # Loop through each of the databases foreach ($db in $DatabaseCollection) { Write-PSFMessage -Message "Creating image for database $db from $SourceSqlInstance" -Level Verbose if ($PSCmdlet.ShouldProcess($db, "Checking available disk space for database")) { # Check the database size to the available disk space if ($computer.IsLocalhost) { $availableMB = (Get-PSDrive -Name $ImageLocalPath.Substring(0, 1)).Free / 1MB } else { $command = [ScriptBlock]::Create("(Get-PSDrive -Name $($ImageLocalPath.Substring(0, 1)) ).Free / 1MB") $availableMB = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $commandGetLocalPath -Credential $DestinationCredential } $dbSizeMB = $db.Size if ($availableMB -lt $dbSizeMB) { Stop-PSFFunction -Message "Size of database $($db.Name) does not fit within the image local path" -Target $db -Continue } } # Setup the image variables $imageName = "$($db.Name)_$timestamp" # Setup the access path $accessPath = Join-PSFPath -Path $ImageLocalPath -Child $imageName # Setup the vhd path $vhdPath = "$($accessPath).$($VhdType.ToLower())" if ($CreateFullBackup) { if ($PSCmdlet.ShouldProcess($db, "Creating full backup for database $db")) { # Create the backup Write-PSFMessage -Message "Creating new full backup for database $db" -Level Verbose $lastFullBackup = Backup-DbaDatabase -SqlInstance $SourceSqlInstance -SqlCredential $SourceSqlCredential -Database $db.Name -CopyOnly:$CopyOnlyBackup } } elseif ($BackupFilePath) { [pscustomobject]$lastFullBackup = @{ Path = $BackupFilePath } } else { Write-PSFMessage -Message "Trying to retrieve the last full backup for $db" -Level Verbose # Get the last full backup $lastFullBackup = Get-DbaDbBackupHistory -SqlInstance $SourceSqlInstance -SqlCredential $SourceSqlCredential -Database $db.Name -LastFull } if (-not $lastFullBackup.Path) { Stop-PSFFunction -Message "No full backup could be found. Please use -CreateFullBackup or create a full backup manually" -Target $lastFullBackup return } elseif (-not (Test-Path -Path $lastFullBackup.Path)) { Stop-PSFFunction -Message "Could not access the full backup file. Check if it exists or that you have enough privileges to access it" -Target $lastFullBackup return } if ($PSCmdlet.ShouldProcess("$imageName", "Creating the vhd")) { # try to create the new VHD try { Write-PSFMessage -Message "Create the vhd $imageName" -Level Verbose # Check if computer is local if ($computer.IsLocalhost) { $null = New-DcnVhdDisk -Destination $imagePath -Name $imageName -VhdType $VhdType -EnableException } else { $command = [ScriptBlock]::Create("New-DcnVhdDisk -Destination '$imagePath' -Name $imageName -VhdType $VhdType -EnableException") $null = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $DestinationCredential } } catch { Stop-PSFFunction -Message "Couldn't create vhd(x) $imageName" -Target $imageName -ErrorRecord $_ -Continue } } if ($PSCmdlet.ShouldProcess("$imageName", "Initializing the vhd")) { # Try to initialize the vhd try { Write-PSFMessage -Message "Initializing the vhd $imageName" -Level Verbose # Check if computer is local if ($computer.IsLocalhost) { $diskResult = Initialize-DcnVhdDisk -Path $vhdPath -Credential $DestinationCredential -EnableException } else { $command = [ScriptBlock]::Create("Initialize-DcnVhdDisk -Path $vhdPath -EnableException") $diskResult = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $DestinationCredential } } catch { Stop-PSFFunction -Message "Couldn't initialize vhd $vhdPath" -Target $imageName -ErrorRecord $_ } } # Create folder structure for image $imageDataFolder = Join-PSFPath -Path $imagePath -Child "$($imageName)\Data" $imageLogFolder = Join-PSFPath -Path $imagePath -Child "$($imageName)\Log" # try to create access path try { # Check if access path is already present if (-not (Test-Path -Path $accessPath)) { if ($PSCmdlet.ShouldProcess($accessPath, "Creating access path $accessPath")) { try { # Check if computer is local if ($computer.IsLocalhost) { $null = New-Item -Path $accessPath -ItemType Directory -Force } else { $command = [ScriptBlock]::Create("New-Item -Path $accessPath -ItemType Directory -Force") $null = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $DestinationCredential # Set the permissions $script = " `$permission = 'Everyone', 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow' `$accessRule = New-Object System.Security.AccessControl.FileSystemAccessRule `$permission `$acl = Get-Acl -Path '$accessPath' `$acl.SetAccessRule(`$accessRule) `$acl | Set-Acl '$accessPath' " $command = [ScriptBlock]::Create($script) $null = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $DestinationCredential } } catch { Stop-PSFFunction -Message "Couldn't create access path directory" -ErrorRecord $_ -Target $accessPath -Continue } } # Set the permissions #$permission = "Everyone", "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow" $accessRule = New-Object System.Security.AccessControl.FileSystemAccessRule("Everyone", "FullControl", "ContainerInherit,Objectinherit", "None", "Allow") $acl = Get-Acl -Path $accessPath $acl.SetAccessRule($accessRule) Set-Acl -Path $accessPath -AclObject $acl } # Get the properties of the disk and partition $disk = $diskResult.Disk $partition = Get-Partition -DiskNumber $disk.Number | Where-Object { $_.Type -ne "Reserved" } | Select-Object -First 1 if ($PSCmdlet.ShouldProcess($accessPath, "Adding access path '$accessPath' to mounted disk")) { # Add the access path to the mounted disk if ($computer.IsLocalhost) { $null = Add-PartitionAccessPath -DiskNumber $disk.Number -PartitionNumber $partition.PartitionNumber -AccessPath $accessPath -ErrorAction SilentlyContinue } else { $command = [ScriptBlock]::Create("Add-PartitionAccessPath -DiskNumber $($disk.Number) -PartitionNumber $($partition.PartitionNumber) -AccessPath $accessPath -ErrorAction SilentlyContinue") $null = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $DestinationCredential } } } catch { Stop-PSFFunction -Message "Couldn't create access path for partition" -ErrorRecord $_ -Target $diskResult.partition } # Check if image data folder exist if (-not (Test-Path -Path $imageDataFolder)) { if ($PSCmdlet.ShouldProcess($accessPath, "Creating data folder in vhd")) { try { Write-PSFMessage -Message "Creating data folder for image" -Level Verbose # Check if computer is local if ($computer.IsLocalhost) { $null = New-Item -Path $imageDataFolder -ItemType Directory $acl = Get-ACL -Path $imageDataFolder $acl.SetAccessRule($accessRule) Set-Acl -Path $imageDataFolder -AclObject $acl } else { $command = [ScriptBlock]::Create("New-Item -Path $imageDataFolder -ItemType Directory") $null = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $DestinationCredential } } catch { Stop-PSFFunction -Message "Couldn't create image data folder" -Target $imageName -ErrorRecord $_ -Continue } } } # Test if the image log folder exists if (-not (Test-Path -Path $imageLogFolder)) { if ($PSCmdlet.ShouldProcess($accessPath, "Creating log folder in vhd")) { try { Write-PSFMessage -Message "Creating transaction log folder for image" -Level Verbose # Check if computer is local if ($computer.IsLocalhost) { $null = New-Item -Path $imageLogFolder -ItemType Directory $acl = Get-ACL -Path $imageLogFolder $acl.SetAccessRule($accessRule) Set-Acl -Path $imageLogFolder -AclObject $acl } else { $command = [ScriptBlock]::Create("New-Item -Path $imageLogFolder -ItemType Directory") $null = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $DestinationCredential } } catch { Stop-PSFFunction -Message "Couldn't create image data folder" -Target $imageName -ErrorRecord $_ -Continue } } } # Setup the temporary database name $tempDbName = "$($db.Name)-PSDatabaseClone" if ($PSCmdlet.ShouldProcess($tempDbName, "Restoring database")) { # Restore database to image folder try { Write-PSFMessage -Message "Restoring database $db on $DestinationSqlInstance" -Level Verbose $params = @{ SqlInstance = $DestinationSqlInstance SqlCredential = $DestinationSqlCredential DatabaseName = $tempDbName Path = $lastFullBackup.Path DestinationDataDirectory = $imageDataFolder DestinationLogDirectory = $imageLogFolder WithReplace = $true EnableException = $true } $restore = Restore-DbaDatabase @params } catch { Stop-PSFFunction -Message "Couldn't restore database $db as $tempDbName on $DestinationSqlInstance.`n$($_)" -Target $restore -ErrorRecord $_ -Continue } } # Detach database if ($PSCmdlet.ShouldProcess($tempDbName, "Detaching database")) { try { Write-PSFMessage -Message "Detaching database $tempDbName on $DestinationSqlInstance" -Level Verbose $null = Dismount-DbaDatabase -SqlInstance $DestinationSqlInstance -Database $tempDbName -SqlCredential $DestinationSqlCredential -Force } catch { Stop-PSFFunction -Message "Couldn't detach database $db as $tempDbName on $DestinationSqlInstance" -Target $db -ErrorRecord $_ -Continue } } if ($PSCmdlet.ShouldProcess($vhdPath, "Dismounting the vhd")) { # Dismount the vhd try { Write-PSFMessage -Message "Dismounting vhd" -Level Verbose # Check if computer is local if ($computer.IsLocalhost) { # Dismount the VHD $null = Dismount-DiskImage -ImagePath $vhdPath # Remove the access path $null = Remove-Item -Path $accessPath -Recurse -Force } else { $command = [ScriptBlock]::Create("Dismount-DiskImage -ImagePath $vhdPath") $null = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $DestinationCredential $command = [ScriptBlock]::Create("Remove-Item -Path $accessPath -Force") $null = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $DestinationCredential } } catch { Stop-PSFFunction -Message "Couldn't dismount vhd" -Target $imageName -ErrorRecord $_ -Continue } } # Write the data to the database $imageLocation = Join-PSFPath $uri.LocalPath -Child "$($imageName).vhdx" $sizeMB = $dbSizeMB $databaseName = $db.Name $databaseTS = $lastFullBackup.Start if ($informationStore -eq 'SQL') { $query = " DECLARE @ImageID INT; EXECUTE dbo.Image_New @ImageID = @ImageID OUTPUT, -- int @ImageName = '$($imageName)', -- varchar(100) @ImageLocation = '$($imageLocation)', -- varchar(255) @SizeMB = $($sizeMB), -- int @DatabaseName = '$($databaseName)', -- varchar(100) @DatabaseTimestamp = '$($databaseTS)' -- datetime SELECT @ImageID as ImageID " # Add image to database if ($PSCmdlet.ShouldProcess($imageName, "Adding image to database")) { try { Write-PSFMessage -Message "Saving image information in database" -Level Verbose $result += Invoke-DbaQuery -SqlInstance $pdcSqlInstance -SqlCredential $pdcCredential -Database $pdcDatabase -Query $query -EnableException $imageID = $result.ImageID } catch { Stop-PSFFunction -Message "Couldn't add image to database" -Target $imageName -ErrorRecord $_ } } } elseif ($informationStore -eq 'File') { [array]$images = $null # Get all the images try { $images = Get-DcnImage } catch { Stop-PSFFunction -Message "Couldn't get images" -Target $imageName -ErrorRecord $_ return } # Setup the new image id if ($images.Count -ge 1) { $imageID = ($images[-1].ImageID | Sort-Object ImageID) + 1 } else { $imageID = 1 } # Add the new information to the array $images += [PSCustomObject]@{ ImageID = $imageID ImageName = $imageName ImageLocation = $imageLocation SizeMB = $sizeMB DatabaseName = $databaseName DatabaseTimestamp = $databaseTS CreatedOn = (Get-Date -format "yyyyMMddHHmmss") } # Test if the JSON folder can be reached if (-not (Test-Path -Path "DCNJSONFolder:\")) { $command = [scriptblock]::Create("Import-Module dbaclone -Force") try { Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential } catch { Stop-PSFFunction -Message "Couldn't import module remotely" -Target $command return } } # Set the image file $jsonImageFile = "DCNJSONFolder:\images.json" # Convert the data back to JSON $images | ConvertTo-Json | Set-Content $jsonImageFile } # Add the results to the custom object [PSCustomObject]@{ ImageID = $imageID ImageName = $imageName ImageLocation = $imageLocation SizeMB = $sizeMB DatabaseName = $databaseName DatabaseTimestamp = $databaseTS } } # for each database } # end process end { # Test if there are any errors if (Test-PSFFunctionInterrupt) { return } Write-PSFMessage -Message "Finished creating database image" -Level Verbose } } function Remove-DcnImage { <# .SYNOPSIS Remove-DcnImage removes one or more images .DESCRIPTION The command will remove an image from PSDatabaseClone. It will also remove all the clones associated with it on the hosts. .PARAMETER ImageID Remove images based on the image id .PARAMETER ImageName Remove images based on the image name .PARAMETER ImageLocation Location of the image as it's saved in the database or can be seen on the file system. .PARAMETER Database Remove images based on the database .PARAMETER ExcludeDatabase Filter the images based on the excluded database .PARAMETER Unused Remove images not used by any clones .PARAMETER Keep When used with the Unused parameter, sets the number of most recent images to keep .PARAMETER DcnSqlCredential Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. This works similar as SqlCredential but is only meant for authentication to the PSDatabaseClone database server and database. .PARAMETER Credential Allows you to login to servers using Windows Auth/Integrated/Trusted. To use: $scred = Get-Credential, then pass $scred object to the -Credential parameter. .PARAMETER Force Forcefully remove the items. .PARAMETER InputObject The input object that is used for pipeline use .PARAMETER EnableException By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .NOTES Author: Sander Stad (@sqlstad, sqlstad.nl) Website: https://psdatabaseclone.org Copyright: (C) Sander Stad, sander@sqlstad.nl License: MIT https://opensource.org/licenses/MIT .LINK https://psdatabaseclone.org/ .EXAMPLE Remove-DcnImage -ImageLocation "\\server1\images\DB1_20180703193345.vhdx" Remove an image .EXAMPLE Get-DcnImage -Database DB1 | Remove-DcnImage Remove all images and clones based on database DB1 #> [CmdLetBinding(DefaultParameterSetName = "ImageLocation", SupportsShouldProcess = $true, ConfirmImpact = 'High')] param( [int[]]$ImageID, [string[]]$ImageName, [parameter(ParameterSetName = "ImageLocation")] [string[]]$ImageLocation, [string[]]$Database, [string[]]$ExcludeDatabase, [switch]$Unused, [int]$Keep = 0, [PSCredential]$DcnSqlCredential, [PSCredential]$Credential, [switch]$Force, [parameter(ValueFromPipeline = $true, ParameterSetName = "Image")] [object[]]$InputObject, [switch]$EnableException ) begin { # Check if the console is run in Administrator mode if ( -not (Test-PSFPowerShell -Elevated) ) { Stop-PSFFunction -Message "Module requires elevation. Please run the console in Administrator mode" -Continue } # Check if the setup has ran if (-not (Test-DcnModule -SetupStatus)) { Stop-PSFFunction -Message "The module setup has NOT yet successfully run. Please run 'Set-DcnConfiguration'" -Continue } # Get the information store $informationStore = Get-PSFConfigValue -FullName psdatabaseclone.informationstore.mode if ($informationStore -eq 'SQL') { # Get the module configurations $pdcSqlInstance = Get-PSFConfigValue -FullName psdatabaseclone.database.Server $pdcDatabase = Get-PSFConfigValue -FullName psdatabaseclone.database.name if (-not $DcnSqlCredential) { $pdcCredential = Get-PSFConfigValue -FullName psdatabaseclone.informationstore.credential -Fallback $null } else { $pdcCredential = $DcnSqlCredential } # Test the module database setup try { Test-DcnConfiguration -SqlCredential $pdcCredential -EnableException } catch { Stop-PSFFunction -Message "Something is wrong in the module configuration" -ErrorRecord $_ -Continue } } # Get all the items $items = @() $clones = @() $items += Get-DcnImage $clones += Get-DcnClone if ($ImageID) { Write-PSFMessage -Message "Filtering image ids" -Level Verbose $items = $items | Where-Object { $_.ImageID -in $ImageID } } if ($ImageName) { Write-PSFMessage -Message "Filtering image name" -Level Verbose $items = $items | Where-Object { $_.ImageName -in $ImageName } } if ($ImageLocation) { Write-PSFMessage -Message "Filtering image locations" -Level Verbose $items = $items | Where-Object { $_.ImageLocation -in $ImageLocation } } if ($Database) { Write-PSFMessage -Message "Filtering databases" -Level Verbose $items = $items | Where-Object { $_.DatabaseName -in $Database } } if ($ExcludeDatabase) { Write-PSFMessage -Message "Filtering excluded databases" -Level Verbose $items = $items | Where-Object { $_.DatabaseName -notin $Database } } if ($Unused) { Write-PSFMessage -Message "Filtering images with associated clones, keeping latest $Keep images." -Level Verbose $images = $items foreach ($item in $items) { if ($clones.ImageID -contains $item.ImageID) { $images = $images | Where-Object { $_.ImageID -ne $item.ImageID } } } $items = $images | Sort-Object -Property CreatedOn -Descending | Select-Object -Skip $Keep } # Append the items $InputObject += $items } process { # Test if there are any errors if (Test-PSFFunctionInterrupt) { return } Write-PSFMessage -Message "Started removing database images" -Level Verbose # Group the objects to make it easier to go through $images = $InputObject | Group-Object ImageID foreach ($image in $images) { # Loop through each of the results foreach ($item in $image.Group) { # Make up the data from the network path try { [uri]$uri = New-Object System.Uri($item.ImageLocation) $uriHost = $uri.Host } catch { Stop-PSFFunction -Message "The image location $ImageNetworkPath is not valid" -ErrorRecord $_ -Target $ImageNetworkPath return } # Setup the computer object $computer = [PsfComputer]$uriHost if (-not $computer.IsLocalhost) { # Get the result for the remote test $resultPSRemote = Test-DcnRemoting -ComputerName $uriHost -Credential $Credential # Check the result if ($resultPSRemote.Result) { $command = [scriptblock]::Create("Import-Module dbaclone -Force") try { Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential } catch { Stop-PSFFunction -Message "Couldn't import module remotely" -Target $command return } } else { Stop-PSFFunction -Message "Couldn't connect to host remotely.`nVerify that the specified computer name is valid, that the computer is accessible over the network, and that a firewall exception for the WinRM service is enabled and allows access from this computer" -Target $resultPSRemote -Continue } } # Get the clones associated with the image $results = @() $results = Get-DcnClone -ImageID $item.ImageID # Check the results if ($results.Count -ge 1) { # Loop through the results foreach ($result in $results) { if ($PSCmdlet.ShouldProcess($item.CloneID, "Removing clone $($result.CloneLocation) from $($result.HostName)")) { # Remove the clones for the host try { Write-PSFMessage -Message "Removing clones for host $($result.HostName) and database $($result.DatabaseName)" -Level Verbose Remove-DcnClone -HostName $result.HostName -Database $result.DatabaseName -DcnSqlCredential $pdcCredential -Credential $Credential -Confirm:$false } catch { Stop-PSFFunction -Message "Couldn't remove clones from host $($result.HostName)" -ErrorRecord $_ -Target $result -Continue } } } } else { Write-PSFMessage -Message "No clones were found created with image $($image.Name)" -Level Verbose } if ($PSCmdlet.ShouldProcess($item.ImageLocation, "Removing image from system")) { # Remove the image from the file system try { if ($computer.IsLocalhost) { if (Test-Path -Path $item.ImageLocation -Credential $Credential) { Write-PSFMessage -Message "Removing image '$($item.ImageLocation)' from file system" -Level Verbose $null = Remove-Item -Path $item.ImageLocation -Credential $Credential -Force:$Force } else { Write-PSFMessage -Message "Couldn't find image $($item.ImageLocation)" -Level Verbose } } else { $command = [scriptblock]::Create("Test-Path -Path '$($item.ImageLocation)'") $result = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential if ($result) { $command = [scriptblock]::Create("Remove-Item -Path '$($item.ImageLocation)' -Force") $result = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential } else { Write-PSFMessage -Message "Couldn't find image '$($item.ImageLocation)'" -Level Verbose } } } catch { Stop-PSFFunction -Message "Couldn't remove image '$($item.ImageLocation)' from file system" -ErrorRecord $_ -Target $result } } if ($PSCmdlet.ShouldProcess($item.ImageLocation, "Removing image from database")) { if ($informationStore -eq 'SQL') { # Remove the image from the database try { $query = "DELETE FROM dbo.Image WHERE ImageID = $($item.ImageID)" $null = Invoke-DbaQuery -SqlInstance $pdcSqlInstance -SqlCredential $pdcCredential -Database $pdcDatabase -Query $query } catch { Stop-PSFFunction -Message "Couldn't remove image '$($item.ImageLocation)' from database" -ErrorRecord $_ -Target $query } } elseif ($informationStore -eq 'File') { $imageData = Get-DcnImage | Where-Object { $_.ImageID -ne $item.ImageID } # Set the image file $jsonImageFile = "DCNJSONFolder:\images.json" # Convert the data back to JSON if ($imageData) { $imageData | ConvertTo-Json | Set-Content $jsonImageFile } else { Clear-Content -Path $jsonImageFile } } } } # End for each item in group } # End for each image } # End process end { # Test if there are any errors if (Test-PSFFunctionInterrupt) { return } Write-PSFMessage -Message "Finished removing database image(s)" -Level Verbose } } function Convert-DcnUncPathToLocalPath { <# .SYNOPSIS Convert a UNC path on a computer to a local path. .DESCRIPTION In some cases you want to convert a UNC path to a local path. This function will look up which path belongs to the UNC path by supplying only the UNC path .PARAMETER UncPath UNC path to convert to local path .PARAMETER EnableException By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .NOTES Author: Sander Stad (@sqlstad, sqlstad.nl) Website: https://psdatabaseclone.org Copyright: (C) Sander Stad, sander@sqlstad.nl License: MIT https://opensource.org/licenses/MIT .LINK https://psdatabaseclone.org/ .EXAMPLE Convert-DcnUncPathToLocalPath -UncPath "\\server1\share1" Convert path "\\server1\share1" to a local path from server1 #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$UncPath, [switch]$EnableException ) # Create the object try { $uri = New-Object System.Uri($UncPath) } catch { Stop-PSFFunction -Message "Something went wrong converting the UncPath $UncPath to URI" -Target $UncPath -ErrorRecord $_ } # Check if the path is a valid UNC path if (-not $uri.IsUnc) { Stop-PSFFunction -Message "The path $UncPath is not a valid UNC path" -Target $UncPath } # Get the local shares from the computer if (Get-Command "Get-SmbShare" -ErrorAction SilentlyContinue) { $localShares = Get-SmbShare } else { $localShares = Invoke-PSFCommand -ScriptBlock { Get-CimInstance -Class Win32_Share } | Select-Object Name, Path, Description } # Split up the unc path $uncArray = $uri.AbsolutePath -split '/' # Check if the if (($uncArray.Length -lt 2) -or (-not $uncArray[1])) { Stop-PSFFunction -Message "Could not map un path $UncPath. Make sure it consists of at least two segments i.e \\server\directory or \\server\c$)" -Target $uri } # Get the share $share = $localShares | Where-Object { $_.Name -eq $uncArray[1] } # Check if something returned if (!$share) { Stop-PSFFunction -Message "The unc path could not be mapped to a share" -Target $localShares } # Rebuild the array so we have a the same construction with folders $uncArray[1] = $share.Path $uncArray = $uncArray[1..($uncArray.Length - 1)] return ($uncArray -join '\') -replace '\\\\', '\' } function Set-DcnConfiguration { <# .SYNOPSIS Set-DcnConfiguration sets up the module .DESCRIPTION For the module to work properly the module needs a couple of settings. The most important settings are the ones for the database to store the information about the images and the clones. The configurations will be saved in the registry of Windows for all users. If the database does not yet exist on the server it will try to create the database. After that the objects for the database will be created. .PARAMETER InformationStore Where is the information going to be stored. This can be either a SQL Server database or files formatted as JSON. SQL Server has the advantage of being more reliable and have all the information available JSON files have a small footprint, are fast and don't require a database server The best way to save the JSON files is in a network share to make is possible for other clients to get the information. The default is to save the data in JSON files. .PARAMETER SqlInstance SQL Server name or SMO object representing the SQL Server to connect to .PARAMETER SqlCredential Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. To use: $scred = Get-Credential, then pass $scred object to the -SqlCredential parameter. Windows Authentication will be used if SqlCredential is not specified. SQL Server does not accept Windows credentials being passed as credentials. To connect as a different Windows user, run PowerShell as that user. .PARAMETER Credential Allows you to login to servers or use authentication to access files and folder/shares $scred = Get-Credential, then pass $scred object to the -Credential parameter. .PARAMETER Database Database to use to save all the information in .PARAMETER Path Path where the JSON files will be created .PARAMETER InputPrompt Use this parameter to get a question to put in values using user input .PARAMETER EnableException By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .PARAMETER Force Forcefully create items when needed .NOTES Author: Sander Stad (@sqlstad, sqlstad.nl) Website: https://dbaclone.org Copyright: (C) Sander Stad, sander@sqlstad.nl License: MIT https://opensource.org/licenses/MIT .LINK https://dbaclone.org/ .EXAMPLE Set-DcnConfiguration -SqlInstance SQLDB1 -Database dbaclone Set up the module to use SQLDB1 as the database servers and dbaclone to save the values in .EXAMPLE Set-DcnConfiguration The user will be prompted to enter the information to configure the module #> [CmdLetBinding(SupportsShouldProcess = $true, DefaultParameterSetName = "Prompt")] param( [ValidateSet('SQL', 'File')] [string]$InformationStore, [parameter(ParameterSetName = "SQL", Mandatory = $true)] [DbaInstanceParameter]$SqlInstance, [parameter(ParameterSetName = "SQL")] [PSCredential]$SqlCredential, [parameter(ParameterSetName = "SQL")] [string]$Database, [parameter(ParameterSetName = "File", Mandatory = $true)] [string]$Path, [parameter(ParameterSetName = "File")] [PSCredential]$Credential, [switch]$EnableException, [parameter(ParameterSetName = "Prompt")] [switch]$InputPrompt, [switch]$Force ) begin { if ( -not (Test-PSFPowerShell -Elevated) ) { Stop-PSFFunction -Message "Please run the module in elevated/administrator mode" -Continue } Write-PSFMessage -Message "Started dbaclone Setup" -Level Verbose # Check if the user needs to be asked for user input if ($InputPrompt -or (-not $InformationStore) -and (-not $SqlInstance -and -not $SqlCredential -and -not $Credential -and -not $Database)) { # Setup the choices for the user $choiceDatabase = New-Object System.Management.Automation.Host.ChoiceDescription '&Database', 'Save the information in a database' $choiceDatabase.HelpMessage = "Choose to have the information saved in a database. This is reliable and is the default choice" $choiceJSON = New-Object System.Management.Automation.Host.ChoiceDescription '&JSON', 'Save the information in JSON files' $choiceJSON.HelpMessage = "If you don't want to rely on a database you can choose JSON to save your information. " # Create the options $options = [System.Management.Automation.Host.ChoiceDescription[]]($choiceDatabase, $choiceJSON) # Set extra information for the prompt $title = 'Choose your system' $message = 'Where do you want your data to be saved?' # Present the user with the choices $resultSystem = $host.ui.PromptForChoice($title, $message, $options, 0) if ($resultSystem -eq 0) { # Database $SqlInstance = Read-Host ' - Please enter the SQL Server instance' $Database = Read-Host ' - Database Name [dbaclone]' $databaseUser = Read-Host ' - Database Login' $databasePass = Read-Host ' - Password' -AsSecureString # If the credentials are entered create the credential object if (($DatabaseUser -ne '') -and (($databasePass -ne '') -or ($null -ne $databasePass))) { $SqlCredential = New-Object PSCredential ($databaseUser, $databasePass) } # Set the flag for the new database [bool]$newDatabase = $false # Set the variable for the information store $InformationStore = 'SQL' } elseif ($resultSystem -eq 1) { # Make sure other variables are not set $SqlInstance = $null $Database = $null $SqlCredential = $null # Database $filePath = Read-Host ' - Please enter the path to save the files to' $fileUser = Read-Host ' - Login (Optional)' $filePass = Read-Host ' - Password (Optional)' -AsSecureString # If the credentials are entered create the credential object if (($fileUser -ne '') -and (($filePass -ne '') -or ($null -ne $filePass))) { $Credential = New-Object PSCredential ($fileUser, $filePass) } # Clean up the file path if ($filePath.EndsWith("\")) { $filePath = $filePath.Substring(0, $filePath.Length - 1) } # Check the file path if ($filePath.StartsWith("\\")) { # Make up the data from the network path try { # Convert to uri [uri]$uri = New-Object System.Uri($filePath) $uriHost = $uri.Host # Setup the computer object $computer = [PsfComputer]$uriHost # Check if the path is reachable $command = [scriptblock]::Create("Test-Path -Path $filePath") $resultTestFilePath = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential } catch { Stop-PSFFunction -Message "The file path $filePath is not valid" -ErrorRecord $_ -Target $filePath return } } else { $resultTestFilePath = Test-Path -Path $filePath } # Check the result if (-not $resultTestFilePath) { Stop-PSFFunction -Message "Could not access the path $filePath" -Target $filePath return } # Set the variable for the information store $InformationStore = 'File' } } # Unregister any configurations try { Unregister-PSFConfig -Scope SystemDefault -Module dbaclone } catch { Stop-PSFFunction -Message "Something went wrong unregistering the configurations" -ErrorRecord $_ -Target $SqlInstance return } if ($Path -and -not (Test-Path -Path $Path)) { try { $null = New-Item -Path $Path -ItemType Directory -Confirm:$false -Force } catch { Stop-PSFFunction -Message "Could not create working directory" -ErrorRecord $_ -Target $SqlInstance } } } process { # Test if there are any errors if (Test-PSFFunctionInterrupt) { return } if ($InformationStore -eq 'SQL') { # Setup the database name if (-not $Database -or $Database -eq '') { $Database = "dbaclone" } # Try connecting to the instance if ($SqlInstance) { Write-PSFMessage -Message "Attempting to connect to Sql Server $SqlInstance.." -Level Verbose try { $server = Connect-DbaInstance -SqlInstance $SqlInstance -SqlCredential $SqlCredential } catch { Stop-PSFFunction -Message "Could not connect to Sql Server instance $SqlInstance" -ErrorRecord $_ -Target $SqlInstance return } } # Check if the database is already present if (($server.Databases.Name -contains $Database) -or ($server.Databases[$Database].Tables.Count -ge 1)) { if ($Force) { try { Remove-DbaDatabase -SqlInstance $SqlInstance -SqlCredential $SqlCredential -Database $Database } catch { Stop-PSFFunction -Message "Couldn't remove database $Database on $SqlInstance" -ErrorRecord $_ -Target $SqlInstance return } } else { Write-PSFMessage -Message "Database $Database already exists" -Level Verbose } } # Check if the database exists if ($server.Databases.Name -notcontains $Database) { # Set the flag $newDatabase = $true try { # Setup the query to create the database $query = "CREATE DATABASE [$Database]" Write-PSFMessage -Message "Creating database $Database on $SqlInstance" -Level Verbose # Executing the query Invoke-DbaQuery -SqlInstance $SqlInstance -SqlCredential $SqlCredential -Database master -Query $query } catch { Stop-PSFFunction -Message "Couldn't create database $Database on $SqlInstance" -ErrorRecord $_ -Target $SqlInstance } } else { # Check if there are any user objects already in the database $newDatabase = ($server.Databases[$Database].Tables.Count -eq 0) } # Setup the path to the sql file if ($newDatabase) { try { $path = "$($MyInvocation.MyCommand.Module.ModuleBase)\internal\resources\database\database.sql" $query = [System.IO.File]::ReadAllText($path) # Create the objects try { Write-PSFMessage -Message "Creating database objects" -Level Verbose # Executing the query Invoke-DbaQuery -SqlInstance $SqlInstance -SqlCredential $SqlCredential -Database $Database -Query $query } catch { Stop-PSFFunction -Message "Couldn't create database objects" -ErrorRecord $_ -Target $SqlInstance } } catch { Stop-PSFFunction -Message "Couldn't find database script. Make sure you have a valid installation of the module" -ErrorRecord $_ -Target $SqlInstance } } else { Write-PSFMessage -Message "Database already contains objects" -Level Verbose } # Set the database server and database values Set-PSFConfig -Module dbaclone -Name database.server -Value $SqlInstance -Validation string Set-PSFConfig -Module dbaclone -Name database.name -Value $Database -Validation string } else { # Create the JSON files if ($null -eq $filePath) { $filePath = $Path } # Create the PSDrive to be able to use credentials try { $null = New-PSDrive -Name dbaclone -PSProvider FileSystem -Root $filePath -Credential $Credential } catch { Stop-PSFFunction -Message "Couldn not create PS-Drive to JSON files" -Target $filePath } # Create the files try { # Get the files $files = Get-ChildItem -Path dbaclone:\ # Check if we have any files if ($files.Count -eq 0) { $null = New-Item -Path dbaclone:\hosts.json -Force:$Force $null = New-Item -Path dbaclone:\images.json -Force:$Force $null = New-Item -Path dbaclone:\clones.json -Force:$Force } else { if (-not $Force -and ("$filePath\hosts.json" -in $files.FullName)) { Stop-PSFFunction -Message "File 'hosts.json' already exists" -Target $filePath } else { $null = New-Item -Path dbaclone:\hosts.json -Force:$Force } if (-not $Force -and ("$filePath\images.json" -in $files.FullName)) { Stop-PSFFunction -Message "File 'images.json' already exists" -Target $filePath } else { $null = New-Item -Path dbaclone:\images.json -Force:$Force } if (-not $Force -and ("$filePath\clones.json" -in $files.FullName)) { Stop-PSFFunction -Message "File 'clones.json' already exists" -Target $filePath } else { $null = New-Item -Path dbaclone:\clones.json -Force:$Force } } # Set the path in case it's set for file store mode Set-PSFConfig -Module dbaclone -Name informationstore.path -Value "$filePath" -Validation string } catch { Stop-PSFFunction -Message "Could not create the JSON files in path $filePath" -Target $filePath -ErrorRecord $_ return } } # Set the credential for the database if needed if ($SqlCredential) { Set-PSFConfig -Module dbaclone -Name informationstore.credential -Value $SqlCredential } # Set the credential for files and folders if needed if ($Credential) { Set-PSFConfig -Module dbaclone -Name informationstore.credential -Value $Credential } # Set the information store mode Set-PSFConfig -Module dbaclone -Name informationstore.mode -Value $InformationStore # Register the configurations in the system for all users Get-PSFConfig -FullName dbaclone.informationstore.mode | Register-PSFConfig -Scope SystemDefault Get-PSFConfig -FullName dbaclone.informationstore.credential | Register-PSFConfig -Scope SystemDefault Get-PSFConfig -FullName dbaclone.informationstore.path | Register-PSFConfig -Scope SystemDefault Get-PSFConfig -FullName dbaclone.database.server | Register-PSFConfig -Scope SystemDefault Get-PSFConfig -FullName dbaclone.database.name | Register-PSFConfig -Scope SystemDefault # Set the path to the diskpart script file Set-PSFConfig -Module dbaclone -Name diskpart.scriptfile -Value "$env:APPDATA\dbaclone\diskpartcommand.txt" -Validation string # Check if the path exists and create it if neccesary if (Test-Path -Path "$env:APPDATA\dbaclone") { if (-not $Force) { Write-PSFMessage -Message "PSDatabaClone working directory already exists" -Level Verbose } else { try { $null = New-Item -Path "$env:APPDATA\dbaclone" -ItemType Directory -Force:$Force } catch { Stop-PSFFunction -Message "Something went wrong creating the working directory" -ErrorRecord $_ -Continue } } } # Set the configuration Get-PSFConfig -FullName dbaclone.diskpart.scriptfile | Register-PSFConfig -Scope SystemDefault # Check if all the settings have been made if ($InformationStore -eq 'SQL') { $dbServer = Get-PSFConfigValue -FullName dbaclone.database.server $dbName = Get-PSFConfigValue -FullName dbaclone.database.name if (($false -ne $dbServer) -and ($false -ne $dbName)) { Write-PSFMessage -Message "All mandatory configurations have been made" -Level Verbose Set-PSFConfig -Module dbaclone -Name setup.status -Value $true -Validation bool } else { Write-PSFMessage -Message "The mandatory configurations have NOT been made. Please check your settings." -Level Warning Set-PSFConfig -Module dbaclone -Name setup.status -Value $false -Validation bool } } else { $path = Get-PSFConfigValue -FullName dbaclone.informationstore.path if (($null -ne $path) -or ('' -ne $path)) { Write-PSFMessage -Message "All mandatory configurations have been made" -Level Verbose Set-PSFConfig -Module dbaclone -Name setup.status -Value $true -Validation bool } else { Write-PSFMessage -Message "The mandatory configurations have NOT been made. Please check your settings." -Level Warning Set-PSFConfig -Module dbaclone -Name setup.status -Value $false -Validation bool } } # Set the overall status in the configurations Get-PSFConfig -FullName dbaclone.setup.status | Register-PSFConfig -Scope SystemDefault } end { # Test if there are any errors if (Test-PSFFunctionInterrupt) { return } Write-PSFMessage -Message "Finished setting up dbaclone" -Level Verbose } } function Set-DcnPermission { <# .SYNOPSIS New-DcnClone creates a new clone .DESCRIPTION New-DcnClone willcreate a new clone based on an image. The clone will be created in a certain directory, mounted and attached to a database server. .PARAMETER Path Path to set the permissions for .PARAMETER EnableException By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .NOTES Author: Sander Stad (@sqlstad, sqlstad.nl) Website: https://psdatabaseclone.org Copyright: (C) Sander Stad, sander@sqlstad.nl License: MIT https://opensource.org/licenses/MIT .LINK https://psdatabaseclone.org/ .EXAMPLE Set-DcnPermission -Path "C:\projects\dbaclone\clone\AW2017-C1\" Set the permissions for the path #> [CmdLetBinding()] param( [string]$Path, [switch]$EnableException ) begin { if (-not $Path) { Stop-PSFFunction -Message "Please enter a path" } else { if (-not (Test-Path -Path $Path)) { Stop-PSFFunction -Message "Could not enter path. Please check if the path is valid and reachable." } } } process { if (Test-PSFFunctionInterrupt) { return } $everyone = [System.Security.Principal.WellKnownSidType]::WorldSid $sid = New-Object System.Security.Principal.SecurityIdentifier($everyone, $Null) $group = New-Object System.Security.Principal.NTAccount("Everyone") $accessRule = New-Object System.Security.AccessControl.FilesystemAccessrule($sid, "FullControl", "Allow") $accessRule = New-Object System.Security.AccessControl.FilesystemAccessrule("Everyone", "FullControl", "Allow") foreach ($file in $(Get-ChildItem -Path "$Path" -Recurse)) { Write-PSFMessage -Level Verbose -Message "Setting permissions for '$($file.FullName)'" $acl = Get-Acl $file.Fullname # Add this access rule to the ACL $acl.SetAccessRule($accessRule) $acl.SetOwner($group) try { # Write the changes to the object Set-Acl -Path $file.Fullname -AclObject $acl } catch { Stop-PSFFunction -Message "Could not set permissions for '$($file.FullName)'" -Target $file -ErrorRecord $_ -Continue } } } } function Test-DcnModule { <# .SYNOPSIS Tests for conditions in the PSDatabaseClone module. .DESCRIPTION This helper command can evaluate various runtime conditions, such as: - Configuration - Windows version .PARAMETER IsLocal Returns if the computer given is local .PARAMETER SetupStatus Setup status should be set. .PARAMETER WindowsVersion The windows version should be in the supported version list Windows version should be in - 'Microsoft Windows 10 Pro', - 'Microsoft Windows 10 Enterprise', - 'Microsoft Windows 10 Education', - 'Microsoft Windows Server 2008 R2 Standard', - 'Microsoft Windows Server 2008 R2 Enterprise', - 'Microsoft Windows Server 2008 R2 Datacenter' - 'Microsoft Windows Server 2012 R2 Standard', - 'Microsoft Windows Server 2012 R2 Enterprise', - 'Microsoft Windows Server 2012 R2 Datacenter', - 'Microsoft Windows Server 2016 Standard', - 'Microsoft Windows Server 2016 Enterprise', - 'Microsoft Windows Server 2016 Datacenter' - 'Microsoft Windows Server 2019 Datacenter' .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .NOTES Author: Sander Stad (@sqlstad, sqlstad.nl) Website: https://psdatabaseclone.org Copyright: (C) Sander Stad, sander@sqlstad.nl License: MIT https://opensource.org/licenses/MIT .LINK https://psdatabaseclone.org/ .EXAMPLE Test-DcnModule -SetupStatus Return true if the status if correct, if not returns false .EXAMPLE Test-DcnModule -WindowsVersion Return true if the windows version is supported, if not returns false #> param( [PSFComputer]$IsLocal, [switch]$SetupStatus, [switch]$WindowsVersion ) begin { } process { #region Is Local if ($IsLocal) { $computer = [PsfComputer]$IsLocal return $computer.IsLocalhost } # endregion Is Local # region Setup status if ($SetupStatus) { if (-not (Get-PSFConfigValue -FullName psdatabaseclone.setup.status)) { return $false } else { return $true } } # endregion Setup Status #region Windows Version if ($WindowsVersion) { $supportedVersions = @( 'Microsoft Windows 10 Pro', 'Microsoft Windows 10 Enterprise', 'Microsoft Windows 10 Education', 'Microsoft Windows Server 2008 R2 Standard', 'Microsoft Windows Server 2008 R2 Enterprise', 'Microsoft Windows Server 2008 R2 Datacenter' 'Microsoft Windows Server 2012 R2 Standard', 'Microsoft Windows Server 2012 R2 Enterprise', 'Microsoft Windows Server 2012 R2 Datacenter', 'Microsoft Windows Server 2016 Standard', 'Microsoft Windows Server 2016 Enterprise', 'Microsoft Windows Server 2016 Datacenter' 'Microsoft Windows Server 2019 Datacenter' ) # Get the OS details $osDetails = Get-CimInstance Win32_OperatingSystem | Select-Object Caption, Description, Name, OSType, Version $windowsEdition = ($osDetails.Caption).Replace(" Evaluation", "").Trim() # Check which version of windows we're dealing with if ($windowsEdition -notin $supportedVersions ) { if ($windowsEdition -like '*Windows 7*') { return $false #Stop-PSFFunction -Message "Module does not work on Windows 7" -Target $OSDetails -FunctionName 'Pre Import' } else { #Stop-PSFFunction -Message "Unsupported version of Windows." -Target $OSDetails -FunctionName 'Pre Import' return $false } } } # endregion Windows version return $true } end { } } function Test-DcnRemoting { <# .SYNOPSIS Test-DcnRemoting tests if remoting is enabled and configured .DESCRIPTION The function will test if the WSMan service is running. It will also test if it's able to retrieve a value from the remote host. The function will return an object with the results. The following information will be returned: - HostName : Which is the host entered at execution - IsLocalHost : If the host is a local host - Reachable : Was the host reachable by a ping request - WSManServiceRunning : Is the WSMan service running = $resultWSManService - CommandExecuted : Was it possible to execute a command remotely - Result : The overall result of the test .PARAMETER ComputerName Host to connect to .PARAMETER Credential Allows you to login to servers using Windows Auth/Integrated/Trusted. To use: $scred = Get-Credential, then pass $scred object to the -Credential parameter. .PARAMETER EnableException By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .NOTES Author: Sander Stad (@sqlstad, sqlstad.nl) Website: https://psdatabaseclone.org Copyright: (C) Sander Stad, sander@sqlstad.nl License: MIT https://opensource.org/licenses/MIT .LINK https://psdatabaseclone.org/ .EXAMPLE Test-DcnRemoting -HostName SQLDB1 Test the PS remoting for one host .EXAMPLE Test-DcnRemoting -HostName SQLDB1, SQLDB2 Test the PS remoting for multiple hosts .EXAMPLE #> [CmdLetBinding()] param( [Parameter(Mandatory = $true)] [string[]]$ComputerName, [PSCredential]$Credential, [switch]$EnableException ) begin { if (-not $ComputerName) { Stop-PSFFunction -Message "Please enter one or more hostnames to test" -Target $ComputerName -Continue } } process { # Test if there are any errors if (Test-PSFFunctionInterrupt) { return } foreach ($comp in $ComputerName) { # Get the computer object $computer = [PSFComputer]$comp # Initialize the variables $connectionResult = $false $resultWSManService = $false $resultCommand = $false # Test if the computer is reachable [bool]$connectionResult = Test-Connection -ComputerName $comp -BufferSize 16 -Count 1 -ErrorAction 0 -Quiet if ($connectionResult) { # Test if the WSMan service is running try { if ($Credential) { $resultWSManService = [bool](Test-WSMan -ComputerName $comp -Credential $Credential -Authentication Default -ErrorAction SilentlyContinue) } else { $resultWSManService = [bool](Test-WSMan -ComputerName $comp -ErrorAction SilentlyContinue) } } catch { Stop-PSFFunction -Message "Couldn't test WSMAN.`nVerify that the specified computer name is valid, that the computer is accessible over the network, and that a firewall exception for the WinRM service is enabled and allows access from this computer" -Target $comp -ErrorRecord $_ -Continue } if ($resultWSManService) { # Reset the result [string]$result = $null # Setup the command $command = [scriptblock]::Create('$env:COMPUTERNAME') # Get the result $result = Invoke-PSFCommand -ComputerName $comp -ScriptBlock $command -Credential $Credential -ErrorAction SilentlyContinue if ($result) { $resultCommand = $true } } } # Return the results [PSCustomObject]@{ ComputerName = $computer.ComputerName IsLocalHost = $computer.IsLocalhost Reachable = $connectionResult WSManServiceRunning = $resultWSManService CommandExecuted = $resultCommand Result = (($connectionResult) -and ($resultWSManService) -and ($resultCommand)) } } } } <# This is an example configuration file By default, it is enough to have a single one of them, however if you have enough configuration settings to justify having multiple copies of it, feel totally free to split them into multiple files. #> <# # Example Configuration Set-PSFConfig -Module 'dbaclone' -Name 'Example.Setting' -Value 10 -Initialize -Validation 'integer' -Handler { } -Description "Example configuration setting. Your module can then use the setting using 'Get-PSFConfigValue'" #> Set-PSFConfig -Module 'dbaclone' -Name 'Import.DoDotSource' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be dotsourced on import. By default, the files of this module are read as string value and invoked, which is faster but worse on debugging." Set-PSFConfig -Module 'dbaclone' -Name 'Import.IndividualFiles' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be imported individually. During the module build, all module code is compiled into few files, which are imported instead by default. Loading the compiled versions is faster, using the individual files is easier for debugging and testing out adjustments." [array]$availableClones = Get-DcnClone $cloneIds = [scriptblock]::Create($availableClones.CloneID -join ",") $cloneDbNames = [scriptblock]::Create("'$($availableClones.DatabaseName -join "','")'") $cloneHostnames = [scriptblock]::Create("'$($availableClones.HostName -join "','")'") $cloneImageIds = [scriptblock]::Create($availableClones.ImageID -join ",") $cloneImageNames = [scriptblock]::Create("'$($availableClones.ImageName -join "','")'") Register-PSFTeppScriptblock -Name "dbaclone.clones.id" -ScriptBlock $cloneIds Register-PSFTeppScriptblock -Name "dbaclone.clones.databasename" -ScriptBlock $cloneDbNames Register-PSFTeppScriptblock -Name "dbaclone.clones.hostname" -ScriptBlock $cloneHostnames Register-PSFTeppScriptblock -Name "dbaclone.clones.imageid" -ScriptBlock $cloneImageIds Register-PSFTeppScriptblock -Name "dbaclone.clones.imagename" -ScriptBlock $cloneImageNames [array]$availableImages = Get-DcnImage $imageIds = [scriptblock]::Create($availableImages.ImageID -join ",") $imageNames = [scriptblock]::Create("'$($availableImages.ImageName -join "','")'") $imageDbNames = [scriptblock]::Create("'$($availableImages.DatabaseName -join "','")'") $imageLocations = [scriptblock]::Create("'$($availableImages.ImageLocation -join "','")'") Register-PSFTeppScriptblock -Name "dbaclone.images.id" -ScriptBlock $imageIds Register-PSFTeppScriptblock -Name "dbaclone.images.name" -ScriptBlock $imageNames Register-PSFTeppScriptblock -Name "dbaclone.images.database" -ScriptBlock $imageDbNames Register-PSFTeppScriptblock -Name "dbaclone.images.location" -ScriptBlock $imageLocations <# # Example: Register-PSFTeppArgumentCompleter -Command Get-Alcohol -Parameter Type -Name dbaclone.alcohol #> # Image commands Register-PSFTeppArgumentCompleter -Command Get-DcnImage -Parameter ImageID -Name 'dbaclone.images.id' Register-PSFTeppArgumentCompleter -Command Get-DcnImage -Parameter ImageName -Name 'dbaclone.images.name' Register-PSFTeppArgumentCompleter -Command Remove-DcnImage -Parameter Database -Name 'dbaclone.images.database' Register-PSFTeppArgumentCompleter -Command Remove-DcnImage -Parameter ImageID -Name 'dbaclone.images.id' Register-PSFTeppArgumentCompleter -Command Remove-DcnImage -Parameter ImageName -Name 'dbaclone.images.name' Register-PSFTeppArgumentCompleter -Command Remove-DcnImage -Parameter ImageLocation -Name 'dbaclone.images.location' # Clone commands Register-PSFTeppArgumentCompleter -Command Get-DcnClone -Parameter Database -Name 'dbaclone.clones.databasename' Register-PSFTeppArgumentCompleter -Command Get-DcnClone -Parameter HostName -Name 'dbaclone.clones.hostname' Register-PSFTeppArgumentCompleter -Command Get-DcnClone -Parameter ImageID -Name 'dbaclone.clones.imageid' Register-PSFTeppArgumentCompleter -Command Get-DcnClone -Parameter ImageName -Name 'dbaclone.clones.imagename' Register-PSFTeppArgumentCompleter -Command Invoke-DcnRepairClone -Parameter HostName -Name 'dbaclone.clones.hostname' Register-PSFTeppArgumentCompleter -Command New-DcnClone -Parameter Database -Name 'dbaclone.images.database' Register-PSFTeppArgumentCompleter -Command Remove-DcnClone -Parameter Database -Name 'dbaclone.clones.databasename' Register-PSFTeppArgumentCompleter -Command Remove-DcnClone -Parameter HostName -Name 'dbaclone.clones.hostname' New-PSFLicense -Product 'dbaclone' -Manufacturer 'sstad' -ProductVersion $script:ModuleVersion -ProductType Module -Name MIT -Version "1.0.0.0" -Date (Get-Date "2020-02-11") -Text @" Copyright (c) 2020 sstad 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 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. "@ #endregion Load compiled code |