Private/Utilities/Restore-JCOrganization.ps1
<#
ToDo Validate Path contains *.zip file If object exists compare the existing object against backup object for diffs . Why is 'pester.tester2_AoCaBLbI' being associated to two groups? . Running restore twice after objects have been deleted from the org fails #> <# .Synopsis The function exports objects from your JumpCloud organization to local json files .Description The function exports objects from your JumpCloud organization to local json files .Example Restore UserGroups and Users with their associations PS C:\> Restore-JCOrganization -Path:('C:\Temp\JumpCloud_20201222T1324549196.zip') -Type:('UserGroup','User') -Association .Example Restore UserGroups and Users without their associations PS C:\> Restore-JCOrganization -Path:('C:\Temp\JumpCloud_20201222T1324549196.zip') -Type:('UserGroup','User') .Example Restore all avalible JumpCloud objects and their Association PS C:\> Restore-JCOrganization -Path:('C:\Temp\JumpCloud_20201222T1324549196.zip') -All .Notes .Link https://github.com/TheJumpCloud/support/tree/master/PowerShell/JumpCloud%20Module/Docs/Restore-JCOrganization.md #> Function Restore-JCOrganization { [CmdletBinding(DefaultParameterSetName = 'All', PositionalBinding = $false)] Param( [Parameter(Mandatory)] [System.String] # Specify input .zip file path for restore files ${Path}, [Parameter(ParameterSetName = 'All')] [switch] # The Username of the JumpCloud user you wish to search for ${All}, [Parameter(ParameterSetName = 'Type')] [ValidateSet('SystemGroup', 'UserGroup', 'User')] [System.String[]] # Specify the type of JumpCloud objects you want to backup ${Type}, [Parameter(ParameterSetName = 'Type')] [switch] # Include to backup object type Association ${Association} ) Begin { # Unzip folder $ZipArchive = Get-Item -Path:($Path) Expand-Archive -LiteralPath:($Path) -DestinationPath:($ZipArchive.Directory.FullName) -Force $ExpandedArchivePath = Get-Item -Path:(Join-Path -Path:($ZipArchive.Directory) -ChildPath:(($ZipArchive.Name).Replace($ZipArchive.Extension, ''))) # When -All is provided use all type options and Association $Types = If ($PSCmdlet.ParameterSetName -eq 'All') { $PSBoundParameters.Add('Association', $true) (Get-Command $MyInvocation.MyCommand).Parameters.Type.Attributes.ValidValues } Else { $PSBoundParameters.Type } # Map to define how JCAssociation & JcSdk types relate $JcTypesMap = @{ Application = 'application'; Command = 'command'; GSuite = 'g_suite'; LdapServer = 'ldap_server'; Office365 = 'office_365'; Policy = 'policy'; RadiusServer = 'radius_server'; System = 'system'; SystemGroup = 'system_group'; User = 'user'; UserGroup = 'user_group'; } # Get the manifest file from backup $ManifestFile = $ExpandedArchivePath | Get-ChildItem | Where-Object { $_.Name -eq "BackupManifest.json" } Write-Host ("###############################################################") If (-not (Test-Path -Path:($ManifestFile) -ErrorAction:('SilentlyContinue'))) { Write-Error ("Unable to find manifest file: $($ManifestFile)") } Else { $Manifest = Get-Content -Path:($ManifestFile) | ConvertFrom-Json Write-Host ("Backup Org: $($Manifest.organizationID)") Write-Host ("Backup Date: $($Manifest.date)") Write-Host "Contains Object Files:" (-not [system.string]::IsNullOrEmpty(($($Manifest.backupFiles)))) # TODO should we keep this message or change the logic Write-Host "Contains Associations:" (-not [system.string]::IsNullOrEmpty(($($Manifest.associationFiles)))) } Write-Host ("Backup Location: $($ZipArchive.FullName)") Write-Host ("Backup Time: $($ZipArchive.LastWriteTime)") Write-Host ("###############################################################") } Process { # Get list of files from backup location and split into object and association groups $RestoreFiles = Get-ChildItem -Path:($ExpandedArchivePath.FullName) -Exclude:('*Association*') | ForEach-Object { $_ | Where-Object { $_.BaseName -in $Types } } # For each backup file restore object $JcObjectsJobs = $RestoreFiles | ForEach-Object { $RestoreFileFullName = $_.FullName $RestoreFileBaseName = $_.BaseName Start-Job -ScriptBlock:( { Param ($RestoreFileFullName, $RestoreFileBaseName) $JcObjectResults = [PSCustomObject]@{ Updated = @(); New = @(); IdMap = @(); } # Collect old ids and new ids for mapping $ExistingIds = (Invoke-Expression -Command:("Get-JcSdk{0} -Fields id" -f $RestoreFileBaseName)).id $RestoreFileContent = Get-Content -Path:($RestoreFileFullName) | ConvertFrom-Json $RestoreFileContent | ForEach-Object { $CommandType = Invoke-Expression -Command:("[$($_.JcSdkModel)]") $RestoreFileRecord = $CommandType::DeserializeFromPSObject($_) # If User is managed by third-party dont create or update If (-not $RestoreFileRecord.ExternallyManaged) { $CommandResult = If ( $RestoreFileRecord.id -notin $ExistingIds ) { # Invoke command to create new resource $Command = "`$RestoreFileRecord | $("New-JcSdk{0}" -f $RestoreFileBaseName)" Write-Debug ("Running: $Command") $NewJcSdkResult = Invoke-Expression -Command:($Command) If (-not [System.String]::IsNullOrEmpty($NewJcSdkResult)) { $JcObjectResults.New += $NewJcSdkResult $NewJcSdkResult } } Else { # Invoke command to update existing resource $Command = "$("Set-JcSdk{0}" -f $RestoreFileBaseName) -Id:(`$RestoreFileRecord.id) -Body:(`$RestoreFileRecord)" Write-Debug ("Running: $Command") $SetJcSdkResult = Invoke-Expression -Command:($Command) If (-not [System.String]::IsNullOrEmpty($SetJcSdkResult)) { $JcObjectResults.Updated += $SetJcSdkResult $SetJcSdkResult } } } $JcObjectResults.IdMap += [PSCustomObject]@{ OldId = $RestoreFileRecord.id NewId = $CommandResult.Id } } Return $JcObjectResults }) -ArgumentList:($RestoreFileFullName, $RestoreFileBaseName) } $JcObjectsJobStatus = Wait-Job -Id:($JcObjectsJobs.Id) $JcObjectJobResults = $JcObjectsJobStatus | Receive-Job # Foreach type start a new job and restore object association records If ($PSBoundParameters.Association) { $IdMap = $JcObjectJobResults.IdMap $RestoreAssociationFiles = Get-ChildItem -Path:($ExpandedArchivePath.FullName) -Filter:('*Association*') | ForEach-Object { $_ | Where-Object { $_.BaseName.Replace('-Association', '') -in $Types } } $AssociationsJobs = ForEach ($RestoreAssociationFile In $RestoreAssociationFiles) { Start-Job -ScriptBlock:( { Param ($RestoreAssociationFile, $IdMap, $JcTypesMap) $AssociationResults = [PSCustomObject]@{ Existing = @(); New = @(); Failed = @(); } $AssociationContent = Get-Content -Path:($RestoreAssociationFile.FullName) -Raw | ConvertFrom-Json ForEach ($AssociationItem In $AssociationContent) { $Id = If ([System.String]::IsNullOrEmpty(($IdMap | Where-Object { $_.OldId -eq $AssociationItem.Id }).NewId)) { # Check to see if the Id from the file exists in the console $JcTypeLookup = $JcTypesMap.GetEnumerator() | Where-Object { $_.Value -eq $AssociationItem.type } $GetExistingCommand = "Get-JcSdk$($JcTypeLookup.Key) | Where-Object { `$_.id -eq '$($AssociationItem.id)' }" (Invoke-Expression -Command:($GetExistingCommand)).id } Else { ($IdMap | Where-Object { $_.OldId -eq $AssociationItem.Id }).NewId } # If the targetId does not exist in the IdMap then use the targetId from the file $TargetId = If ([System.String]::IsNullOrEmpty(($IdMap | Where-Object { $_.OldId -eq $AssociationItem.TargetId }).NewId)) { # Check to see if the targetId from the file exists in the console $JcTypeLookup = $JcTypesMap.GetEnumerator() | Where-Object { $_.Value -eq $AssociationItem.TargetType } $GetExistingCommand = "Get-JcSdk$($JcTypeLookup.Key) | Where-Object { `$_.id -eq '$($AssociationItem.TargetId)' }" (Invoke-Expression -Command:($GetExistingCommand)).id } Else { ($IdMap | Where-Object { $_.OldId -eq $AssociationItem.TargetId }).NewId } # Only create associations for the ids that were created or updated in the previous step If ([System.String]::IsNullOrEmpty($Id)) { $AssociationResults.Failed += $AssociationItem Write-Error ("Unable to create association. Id does not exist in org: $($AssociationItem.Type) $($AssociationItem.Id)") } ElseIf ([System.String]::IsNullOrEmpty($TargetId)) { $AssociationResults.Failed += $AssociationItem Write-Error ("Unable to create association. TargetId does not exist in org: $($AssociationItem.TargetType) $($AssociationItem.TargetId)") } Else { # Check for existing association $ExistingAssociation = Get-JCAssociation -Type:($AssociationItem.Type) -Id:($Id) -TargetType:($AssociationItem.TargetType) | Where-Object { $_.TargetId -eq $TargetId } If ([System.String]::IsNullOrEmpty($ExistingAssociation)) { $NewAssociationCommand = "New-JCAssociation -Type:('$($AssociationItem.Type)') -Id:('$($Id)') -TargetType:('$($AssociationItem.TargetType)') -TargetId:('$($TargetId)') -Force" Write-Debug ("Running: $NewAssociationCommand") $AssociationResults.New += Invoke-Expression -Command:($NewAssociationCommand) } Else { $AssociationResults.Existing += $ExistingAssociation } } } Return $AssociationResults }) -ArgumentList:($RestoreAssociationFile, $IdMap, $JcTypesMap) } $AssociationsJobStatus = Wait-Job -Id:($AssociationsJobs.Id) $AssociationResults = $AssociationsJobStatus | Receive-Job } } End { # Clean up temp directory If (Test-Path -Path:($ExpandedArchivePath.FullName)) { Remove-Item -Path:($ExpandedArchivePath.FullName) -Force -Recurse } # Output If (-not [System.String]::IsNullOrEmpty($JcObjectJobResults)) { Write-Host "$($JcObjectJobResults.New.Count) Objects have been restored" Write-Host "$($JcObjectJobResults.Updated.Count) Objects existed and have been updated" } If (-not [System.String]::IsNullOrEmpty($AssociationResults)) { Write-Host "$($AssociationResults.New.Count) Associations have been restored" Write-Host "$($AssociationResults.Existing.Count) Associations existed and have been skipped" Write-Host "$($AssociationResults.Failed.Count) Associations failed to restore" } } } |