-
Notifications
You must be signed in to change notification settings - Fork 0
Open
Description
Hi @jorgeasaurus,
I wanted to share with you some functionalities I added to further enhance this (already) great utility and receive some feedback from you regarding some areas.
JamfBackupRestoreFunctions.ps1
Download-JamfObject function
- Fixed subfolder creation when site exists and group types can be smart/static
- Added $hasSites variable to determine folder structure (no site name folders for environments with no configured sites)
- Added additional resources to $siteBasedResources array (users & usergroups)
- Added additional resource to grouptype check (usergroups)
- If environment has existing sites, but resources are not assigned to one, default sitename to GLOB (as it is a global configuration - can be user defined)
- Included support for icon exporting/downloading (@jorgeasaurus this is one of the areas I'm not to happy with. There is no endpoint that lists all the possible object ID's for icons; so I created a 'brute-force' method to loop through a user defined amount of ID's and break loop after 20 HTTP 404's). The alternative is to only backup the icons that are assigned to a policy via self service, but that leaves a gap for icons that have been uploaded to jamf but are not assigned to a policy via self service. There is also an endpoint (/v1/icon/download/{id} that supports downloading the icons in original scale/resolution, but I was not able to get this working (continued to get HTTP 500). Let me know if there is a better method to scrape object IDs for the icons.
function Download-JamfObject {
# Downloads and saves Jamf objects with their associated files
param (
[string]$Id,
[string]$Resource,
[string]$DownloadDirectory
)
try {
# Validate token status before proceeding
Test-AndRenewAPIToken -BaseUrl $Config.BaseUrl -Token $Config.Token
# Get object details from Jamf
$jamfObject = Get-JamfObject -Id $Id -Resource $Resource
$extension = if ($Resource -eq "computer-prestages") { "json" } else { "xml" }
$displayName = Get-SanitizedDisplayName -Id $Id -Name $jamfObject.name
# Define resources that can be organized by site
$siteBasedResources = @(
"computergroups",
"computers",
"macapplications",
"mobiledeviceapplications",
"mobiledeviceconfigurationprofiles",
"mobiledevicegroups",
"osxconfigurationprofiles",
"policies",
"restrictedsoftware",
"users",
"usergroups"
)
$groupType = $null
$siteName = ""
$subfolder = ""
$targetDir = $DownloadDirectory # default target directory
if ($jamfObject.plist) {
# Parse XML content
[xml]$xml = $jamfObject.plist
# Set subfolder based on group type (smart vs static)
if ($Resource -in @("computergroups", "mobiledevicegroups", "usergroups")) {
$groupType = if ($xml.SelectSingleNode("//is_smart").InnerText -eq 'true') { "smart" } else { "static" }
}
# Organize by site if applicable
if ($hasSites -and ($siteBasedResources -contains $Resource)) {
$siteName = switch ($Resource) {
"computergroups" { $xml.computer_group.site.name }
"computers" { $xml.computer.general.site.name }
"macapplications" { $xml.mac_application.general.site.name }
"mobiledeviceapplications" { $xml.mobile_device_application.general.site.name }
"mobiledeviceconfigurationprofiles" { $xml.mobile_device_configuration_profile.general.site.name }
"mobiledevicegroups" { $xml.mobile_device_group.site.name }
"osxconfigurationprofiles" { $xml.os_x_configuration_profile.general.site.name }
"policies" { $xml.policy.general.site.name }
"restrictedsoftware" { $xml.restricted_software.general.site.name }
"users" { $xml.user.sites.site.name }
"usergroups" { $xml.user_group.site.name }
}
# Set site-based subfolder "GLOB" if site exists and site name is 'NONE'
if ([string]::IsNullOrWhiteSpace($siteName) -or $siteName -eq 'NONE') {
$siteName = "GLOB" # Default to 'GLOB' if no site is specified for this resource (can be user defined)
}
}
# Build nested subfolder path based on group type and site name for applicable resources
if ($groupType) {
if ($hasSites -and $siteName) {
# Organize by site name and group type for applicable resources
$subfolder = Join-Path -Path $groupType -ChildPath $siteName
} else {
# Organize by group type only for applicable resources with no site names
$subfolder = $groupType
}
}
elseif ($siteName) {
# Organize by site name when group type not applicable and site exists
$subfolder = $siteName
}
# Build final target directory path
if ($subfolder) {
$targetDir = Join-Path -Path $DownloadDirectory -ChildPath $subfolder
} else {
$targetDir = $DownloadDirectory
}
# Save plist file
Ensure-DirectoryExists -DirectoryPath $targetDir
$plistFilePath = Join-Path -Path $targetDir -ChildPath "$displayName.plist"
$jamfObject.plist | Out-File -FilePath $plistFilePath -Encoding utf8
Format-XML -FilePath $plistFilePath
}
if ($jamfObject.payload) {
if ($Resource -eq "icon") {
# Handle icon resource
$iconObject = $jamfObject.payload | ConvertFrom-Json
$downloadFileName = "$($iconObject.id)_$($iconObject.name)"
$downloadPath = Join-Path -Path $targetDir -ChildPath $downloadFileName
Invoke-WebRequest -Uri $iconObject.url -OutFile $downloadPath -ErrorAction Stop
} else {
# All other resources
Ensure-DirectoryExists -DirectoryPath $targetDir
$payloadFilePath = Join-Path -Path $targetDir -ChildPath "$displayName.$extension"
$jamfObject.payload | Out-File -FilePath $payloadFilePath -Encoding utf8
if ($extension -eq "xml") {
Format-XML -FilePath $payloadFilePath
}
}
}
# Save script content if it exists
if ($jamfObject.script) {
# Remove .sh extension if present in display name
if ($displayName -like "*.sh") {
$displayName = $displayName -replace '\.sh$', ''
}
$scriptFilePath = Join-Path -Path $DownloadDirectory -ChildPath "$displayName.sh"
$jamfObject.script | Out-File -FilePath $scriptFilePath -Encoding utf8
}
} catch {
Write-Error "Error downloading $Resource : ID $Id - $_"
}
}
Get-JamfObject function
- Added support for handling icon resource type
function Get-JamfObject {
param (
[string]$Id, # Unique identifier of the Jamf object
[string]$Resource # Type of resource (e.g., policies, scripts, computer-prestages)
)
# Determine API version - computer-prestages uses v3, icon uses v1, others use classic API
switch ($Resource) {
"computer-prestages" { $apiVersion = "v3" ; break }
"icon" { $apiVersion = "v1" ; break }
default { $apiVersion = "classic" }
}
# Build endpoint URL - computer-prestages and icon have different API formats than other resources
$endpoint = if ($Resource -in @("computer-prestages", "icon")) {
"$Resource/$Id"
} else {
"$Resource/id/$Id"
}
# Make API call - use XML format for classic API, JSON for v2/v3
$response = Invoke-JamfApiCall -Endpoint $endpoint -Method "GET" -ApiVersion $apiVersion -XML:($apiVersion -eq "classic")
# Handle icon resource and modern API responses (v2/v3)
if ( $Resource -eq "icon" -or $apiVersion -match "v2|v3" ) {
return @{
name = $response.displayName # or $response.name if you prefer for icons
payload = $response | ConvertTo-Json -Depth 5
}
}
# Handle classic API responses
else {
$xml = [xml]$response # Convert response to XML object
$payload = $xml.DocumentElement.FirstChild.payloads # Extract payloads
$name = $xml.SelectSingleNode("//name").InnerText # Get object name
# Extract script content if it's a script-related resource
$script = if ($Resource -eq "scripts") {
$xml.SelectSingleNode("//script_contents").InnerText
} elseif ($Resource -eq "computerextensionattributes") {
$xml.SelectSingleNode("//input_type/script").InnerText
} else {
$null
}
# Return structured data including name, payload, original XML, and script content
return @{
name = $name
payload = $payload
plist = $response
script = $script
}
}
}
Download-JamfObjects function
- Created $jamfSites and $hasSites to use in Download-JamfObject function for determining target directories
function Download-JamfObjects {
param(
[string]$Id, # Optional: Specific object ID to download
[string]$Resource, # Required: Type of Jamf resource (e.g., policies, scripts)
[switch]$ClearExports # Optional: Clear existing exports before downloading
)
# Verify token is valid before querying sites
Test-AndRenewAPIToken -BaseUrl $Config.BaseUrl -Token $Config.Token
# Get site details from Jamf
$jamfSites = Invoke-JamfApiCall -Endpoint "sites" -Method "GET" -ApiVersion "classic" -XML:$true
$hasSites = ([int]([xml]$jamfSites).sites.size) -ne 0 # Variable used in Download-JamfObject function to determine folder structure
# Construct the download directory path using the resource type
$downloadDirectory = Join-Path -Path $Config.DataFolder -ChildPath $Resource
# If ClearExports is specified, remove existing directory and its contents
if ($ClearExports -and (Test-Path $downloadDirectory)) { Remove-Item $downloadDirectory -Recurse -Force }
# Create the download directory if it doesn't exist
Ensure-DirectoryExists -DirectoryPath $downloadDirectory
if ($Id) {
# Download a single object if ID is provided
Download-JamfObject -Id $Id -Resource $Resource -DownloadDirectory $downloadDirectory
} else {
# Download all objects of the specified resource type
Write-Host "Exporting all [$Resource] objects" -NoNewline -ForegroundColor Cyan
# Get all object IDs for the specified resource
$objectIds = Get-JamfObjectIds -Resource $Resource
try {
foreach ($objectId in $objectIds) {
# Verify token is valid before each download
Test-AndRenewAPIToken -BaseUrl $Config.BaseUrl -Token $Config.Token
# Download each object individually
Download-JamfObject -Id $objectId -Resource $Resource -DownloadDirectory $downloadDirectory
}
Write-Host " - ✅" -ForegroundColor Green
} catch {
Write-Host " - ❌" -ForegroundColor Red
Write-Error "Failed to download objects for resource '$Resource': $_"
}
}
}
Get-JamfObjectIds function
- Added support for scraping icon object IDs
- Added parameter defined in config.ps1 to define how many icon IDs end-user wants to discover (hopefully better method exists)
function Get-JamfObjectIds {
param (
[string]$Resource,
[int]$IconMaxId = $Config.IconMaxId
)
# Invoke the Jamf API call to get the response
$apiVersion = switch ($Resource) {
"computer-prestages" { "v3" }
"patch-software-title-configurations" { "v2" }
"icon" { "v1" }
default { "classic" }
}
# testing (limitation: static iconmaxid count)
# special-case for icon, because there is no list endpoint
# alternative is to export only icons that are tied to self service policies, but that does not export all uploaded icons
# was unable to find a way to use the /v1/icon/download/{id} endpoint, which, as per documentation, allows to download icon in original scale/resolution (https://developer.jamf.com/jamf-pro/reference/get_v1-icon-download-id)
# No API role required (https://developer.jamf.com/jamf-pro/docs/privileges-and-deprecations)
if ($Resource -eq "icon") {
$validIds = @()
$consecutive404s = 0
foreach ($id in 1..$IconMaxId) {
$endpoint = "icon/$id"
try {
$iconResponse = Invoke-JamfApiCall -Endpoint $endpoint -Method "GET" -ApiVersion $apiVersion
if ($iconResponse -and $iconResponse.id) {
$validIds += $iconResponse.id
$consecutive404s = 0 # reset counter if success
}
$percentComplete = [math]::Round( ($id / $IconMaxId) * 100 )
Write-Progress -Activity "Getting icon IDs" -Status "Checking ID $id" -PercentComplete $percentComplete
}
catch {
# 404s are expected for invalid icon IDs
$consecutive404s++
if ($consecutive404s -ge 20) {
break # Break from loop after 20 consecutive 404s
}
}
}
# Break the progress bar
Write-Progress -Activity "Getting icon IDs" -Completed
return $validIds
}
# For all other resources, proceed normally
$response = Invoke-JamfApiCall -Endpoint $Resource -Method "GET" -ApiVersion $ApiVersion
if (-not $response) {
Write-Host "No response received for resource: $Resource" -ForegroundColor Red
return $null
}
# Get the first NoteProperty from the response
$firstProperty = $response | Get-Member -MemberType NoteProperty | Select-Object -First 1
if (-not $firstProperty) {
Write-Host "No NoteProperties found in response for resource: $Resource" -ForegroundColor Yellow
return $null
}
# Extract the value of the first NoteProperty and get the IDs
$objects = $response.$($firstProperty.Name)
# Uncomment if you want to only export static groups
# if ($Resource -in "computergroups", "mobiledevicegroups") {
# return ($objects | Where-Object { -not $_.is_smart }).id
# }
# For computer-prestages, the property is 'results' with a nested structure
if ($Resource -eq "computer-prestages") {
return $objects.id
}
# Default case: return the IDs directly from the first NoteProperty
return $objects.id
}
Config.ps1
- Added support (optional) to create subdirectory with date that script is ran
- Added IconMaxId (used in Get-JamfObjectIDs) for icon ID scraping
[hashtable]$script:Config = @{
BaseUrl = "https://instance.jamfcloud.com" # URL of your Jamf Pro server
Username = "username" # Leave empty if using API credentials
Password = 'password' # Leave empty if using API credentials
#clientId = "00000000-0000-0000-0000-000000000000" # Fill if using API credentials
#clientSecret = "your-client-secret-here" # Fill if using API credentials
DataFolder = Join-Path (Get-Location) "JAMF_Backup_Production\$($(Get-Date -Format 'MM-dd-yyyy'))"
ApiVersion = "classic" # Use 'v1','v2' or 'classic'
IconMaxId = 300 # Maximum IDs for icons to scan/discover, adjust as needed
Token = $null
}
JamfBackupRestoreSampleUsage.ps1
- Added additional supported resource types
# Download multiple resource objects from Jamf Pro
@(
"osxconfigurationprofiles",
"mobiledeviceconfigurationprofiles",
"scripts",
"restrictedsoftware",
"policies",
"computer-prestages",
"computerextensionattributes",
"computergroups",
"mobiledevicegroups",
"mobiledeviceextensionattributes",
"departments",
"buildings",
"categories",
"computers",
"mobiledevices",
"mobiledeviceapplications",
"macapplications",
"users",
"usergroups",
"icon",
"sites",
"printers",
"dockitems"
) | ForEach-Object {
Download-JamfObjects -resource $_ -ClearExports
}
README.md
- Added additional supported resource types
# Download multiple resource objects from Jamf Pro
@(
"osxconfigurationprofiles",
"mobiledeviceconfigurationprofiles",
"scripts",
"restrictedsoftware",
"policies",
"computer-prestages",
"computerextensionattributes",
"computergroups",
"mobiledevicegroups",
"mobiledeviceextensionattributes",
"departments",
"buildings",
"categories",
"computers",
"mobiledevices",
"mobiledeviceapplications",
"macapplications",
"users",
"usergroups",
"icon",
"sites",
"printers",
"dockitems"
) | ForEach-Object {
Download-JamfObjects -resource $_ -ClearExports
}
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels
