MigrateToChangeTrackingAndInventoryUsingAMA/CTAndIMigrationFromMMAToAMA.ps1 (1,371 lines of code) (raw):
<#
.SYNOPSIS
This script is intended to help customers migrate their Azure and Arc Onboarded Non-Azure machines and respective files, services tracking and registry settings onboarded to the legacy Change Tracking & Inventory using MMA agent to the latest Change Tracking & Inventory solution using AMA agent.
This script will not migrate file content changes and custom alerts configured. Please refer to https://learn.microsoft.com/en-us/azure/automation/change-tracking/guidance-migration-log-analytics-monitoring-agent?tabs=ct-single-vm%2Climit-single-vm for more guidance on the same.
This script will not migrate Non-Azure Machines which are not onboarded to Arc.
This script is designed to migrate at a log analytics workspace level.
.DESCRIPTION
This script will do the following.
1. Get list of all Azure and Arc Onboarded Non-Azure machines onboarded to Input Log Analytics Workspace for Change Tracking solution using MMA Agent.
2. Create the Data Collection Rule (DCR) ARM template by fetching the files, services, tracking & registry settings configured in the legacy solution and translating them to equivalent settings for the latest solution using AMA Agent and Change Tracking Extensions for the Output Log Analytics Workspace.
3. Deploy Change Tracking solution ARM template to Output Log Analytics Workspace. This is done only if migration to same workspace is not done. The output workspace requires the legacy solution to create the log analytics tables for Change Tracking like ConfigurationChange & ConfigurationData. The deployment name will be DeployCTSolution_CTMig_{GUID} and it will be in same resource group as Output Log Analytics Workspace.
4. Deploy the DCR ARM template created in Step 2. The deployment name will be OutputDCRName_CTMig_{GUID} and it will be in same resource group as Output Log Analytics Workspace. The DCR will be created in the same location as the Output Log Analytics Workspace.
5. Removes MMA Agent from machines list populated in Step 1. This is done only if migration to same workspace is carried out. Machines which have the MMA agent installed via the MSI, will not have the MMA agent removed. It will be removed only if the MMA Agent was installed as an extension.
6. Assign DCR to machines and install AMA Agent and CT Extensions. The deployment name witll be MachineName_CTMig and it will be in same resource group as the machine.
6.1 Assign the DCR deployed in Step 4 to all machines populated in Step 1.
6.2 Install the AMA Agent to all machines populated in Step 1.
6.3 Install the CT Agent to all machines populated in Step 1.
.PARAMETER InputLogAnalyticsWorkspaceResourceId
Mandatory
Log Analytics Workspace where legacy Change Tracking solution is being used.
.PARAMETER OutputLogAnalyticsWorkspaceResourceId
Mandatory
Log Analytics Workspace where latest Change Tracking solution using AMA Agent is to be configured.
.PARAMETER OutputDCRName
Mandatory
The Data Collection Rule name for latest Change Tracking solution.
.PARAMETER OutputVerbose
Mandatory
Put true if verbose output is required. Default is false.
.PARAMETER AzureEnvironment
Mandatory
Azure Cloud Environment to which Log Analytics Workspace belongs.
Accepted values are AzureCloud, AzureUSGovernment, AzureChinaCloud.
.EXAMPLE
CTMigration -InputLogAnalyticsWorkspaceResourceId "/subscriptions/{subId}/resourceGroups/{rgName}/providers/Microsoft.OperationalInsights/workspaces/{laName}" -OutputLogAnalyticsWorkspaceResourceId "/subscriptions/{subId}/resourceGroups/{rgName}/providers/Microsoft.OperationalInsights/workspaces/{laName}" -OutputDCRName "DCRMig" -OutputVerbose $false -AzureEnvironment "AzureCloud"
.OUTPUTS
Outputs the status of each steps of the migration.
#>
param (
[Parameter(Mandatory = $True)]
[String]$InputLogAnalyticsWorkspaceResourceId,
[Parameter(Mandatory = $True)]
[String]$OutputLogAnalyticsWorkspaceResourceId,
[Parameter(Mandatory = $True)]
[string]$OutputDCRName,
[Parameter(Mandatory = $False)]
[bool]$OutputVerbose=$False,
[Parameter(Mandatory = $True)]
[String]$AzureEnvironment = "AzureCloud"
)
# Telemetry level.
$Debug = "Debug"
$Verbose = "Verbose"
$Informational = "Informational"
$Warning = "Warning"
$ErrorLvl = "Error"
$Succeeded = "Succeeded"
$Failed = "Failed"
# ARM resource types.
$VMResourceType = "virtualMachines";
$ArcVMResourceType = "machines";
# API versions.
$LogAnalyticsWorkspaceApiVersion = "2015-11-01-preview"
$ARMDepoymentApiVersion = "2021-04-01"
$VirtualMachineExtensionApiVersion = "2024-07-01"
$ArcMachineExtensionApiVersion = "2024-07-10"
$LatestLogAnalyticsWorkspaceApiVersion = "2023-09-01"
# AMA Agent And CT Extension values.
$ArcExtensionType = "Microsoft.HybridCompute/machines/extensions"
$AzureExtensionType = "Microsoft.Compute/virtualmachines/extensions"
$CTWindowsExtensionName = "ChangeTracking-Windows"
$CTLinuxExtensionName = "ChangeTracking-Linux"
$AMAWindowsExtensionName = "AzureMonitorWindowsAgent"
$AMALinuxExtensionName = "AzureMonitorLinuxAgent"
$SolutionsApiVersion = "2015-11-01-preview"
# HTTP methods.
$GET = "GET"
$PATCH = "PATCH"
$PUT = "PUT"
$POST = "POST"
$DELETE = "DELETE"
# ARM endpoints.
$WindowsRegistrySettingsPath = "{0}/datasources?`$filter=kind+eq+%27ChangeTrackingDefaultRegistry%27"
$WindowsTrackingServicesPath = "{0}/datasources?`$filter=kind+eq+%27ChangeTrackingServices%27"
$WindowsFileSettingsPath = "{0}/datasources?`$filter=kind+eq+%27ChangeTrackingCustomPath%27"
$LinuxFileSettingsPath = "{0}/datasources?`$filter=kind+eq+%27ChangeTrackingLinuxPath%27"
$DataTypeConfigurationPath = "{0}/datasources?`$filter=kind+eq+%27ChangeTrackingDataTypeConfiguration%27"
$ARMDeploymentPath = "/subscriptions/{0}/resourcegroups/{1}/providers/Microsoft.Resources/deployments/{2}"
$AssignDCRPath = "{0}/providers/Microsoft.Insights/dataCollectionRuleAssociations/{1}"
$GetExtensionsPath = "{0}/extensions"
$SolutionsWithWorkspaceFilterPath = "/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.OperationsManagement/solutions?`$filter=properties/workspaceResourceId%20eq%20'{2}'"
# Validation values.
$TelemetryLevels = @($Debug, $Verbose, $Informational, $Warning, $ErrorLvl)
$HttpMethods = @($GET, $PATCH, $POST, $PUT, $DELETE)
# MMA Agent Types
$MMAAgentTypes = @("MicrosoftMonitoringAgent", "OmsAgentForLinux")
# DCR assocation Tag.
$MigrationTag = "CTMig"
#Max depth of payload.
$MaxDepth = 32
# Beginning of Payloads.
$DeployDCRARMTemplate = @"
{
"properties": {
"mode": "Incremental",
"template": {
},
"parameters": {
"workspaceLocation": {
"value": null
},
"dataCollectionRuleName": {
"value": null
},
"workspaceResourceId": {
"value": null
}
}
}
}
"@
$DeployChangeTrackingSolutionARMTemplate = @"
{
"properties": {
"mode": "Incremental",
"template": {
"`$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"subscriptionId": {
"defaultValue": "",
"type": "String"
},
"resourcegroupName": {
"defaultValue": "",
"type": "String"
},
"location": {
"defaultValue": "",
"type": "String"
},
"workspaceName": {
"defaultValue": "",
"type": "String"
},
"solutionType": {
"defaultValue": "",
"type": "String"
}
},
"variables": {},
"resources": [
{
"type": "Microsoft.OperationsManagement/solutions",
"apiVersion": "2015-11-01-preview",
"name": "[Concat(parameters('solutionType'), '(', parameters('workspaceName'), ')')]",
"location": "[parameters('location')]",
"plan": {
"name": "[Concat(parameters('solutionType'), '(', parameters('workspaceName'), ')')]",
"product": "[Concat('OMSGallery/', parameters('solutionType'))]",
"promotionCode": "",
"publisher": "Microsoft"
},
"properties": {
"workspaceResourceId": "[Concat('/subscriptions/', parameters('subscriptionId'), '/resourceGroups/', parameters('resourcegroupName'), '/providers/Microsoft.OperationalInsights/workspaces/', parameters('workspaceName'))]"
},
"id": "[Concat('/subscriptions/', parameters('subscriptionId'), '/resourceGroups/', parameters('resourcegroupName'), '/providers/Microsoft.OperationsManagement/solutions/', parameters('solutionType'), '(', parameters('workspaceName'), ')')]"
}
]
},
"parameters": {
"subscriptionId": {
"value": null
},
"resourcegroupName": {
"value": null
},
"location": {
"value": null
},
"workspaceName": {
"value": null
},
"solutionType": {
"value": null
}
}
}
}
"@
$AssignDCRARMTemplate = @"
{
"properties": {
"mode": "Incremental",
"template": {
"`$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {},
"variables": {},
"resources": [
{
"type": "Microsoft.Insights/dataCollectionRuleAssociations",
"apiVersion": "2022-06-01",
"name": null,
"properties": {
"dataCollectionRuleId": null
},
"scope": null
},
{
"type": null,
"apiVersion": null,
"name": null,
"location": null,
"dependsOn": [
],
"properties": {
"publisher": "Microsoft.Azure.ChangeTrackingAndInventory",
"type": null,
"typeHandlerVersion": "2.27",
"autoUpgradeMinorVersion": true
}
},
{
"type": null,
"apiVersion": null,
"name": null,
"location": null,
"dependsOn": [
],
"properties": {
"publisher": "Microsoft.Azure.Monitor",
"type": null,
"typeHandlerVersion": "1.30",
"autoUpgradeMinorVersion": true
}
}
],
"outputs": {}
}
}
}
"@
# End of Payloads.
$MachinesOnboaredToChangeTrackingQuery = 'Heartbeat | where Category == "Direct Agent" | where Solutions contains "changeTracking" | distinct Computer, ResourceId, ResourceType, OSType'
$Global:Machines = [System.Collections.ArrayList]@()
$Global:CTv2JsonObject = $null
$Global:DCRResourceId = $null
$Global:OutputDCRLocation = $null
function Write-Telemetry {
<#
.Synopsis
Writes telemetry to the job logs.
Telemetry levels can be "Informational", "Warning", "Error" or "Verbose".
.PARAMETER Message
Log message to be written.
.PARAMETER Level
Log level.
.EXAMPLE
Write-Telemetry -Message Message -Level Level.
#>
param (
[Parameter(Mandatory = $true, Position = 1)]
[String]$Message,
[Parameter(Mandatory = $false, Position = 2)]
[ValidateScript({ $_ -in $TelemetryLevels })]
[String]$Level = $Informational
)
if ($Level -eq $Informational) {
Write-Host $Message -ForegroundColor Green
}
if ($Level -eq $Warning) {
Write-Warning $Message
}
elseif ($Level -eq $ErrorLvl) {
Write-Error $Message
}
elseif ($OutputVerbose -eq $true) {
Write-Verbose $Message -Verbose
}
}
function Parse-ArmId {
<#
.SYNOPSIS
Parses ARM resource id.
.DESCRIPTION
This function parses ARM id to return subscription, resource group, resource name, etc.
.PARAMETER ResourceId
ARM resourceId of the machine.
.EXAMPLE
Parse-ArmId -ResourceId "/subscriptions/{subId}/resourceGroups/{rgName}/providers/Microsoft.Automation/automationAccounts/{aaName}"
#>
param (
[Parameter(Mandatory = $true, Position = 1)]
[String]$ResourceId
)
$parts = $ResourceId.Split("/")
return @{
Subscription = $parts[2]
ResourceGroup = $parts[4]
ResourceProvider = $parts[6]
ResourceType = $parts[7]
ResourceName = $parts[8]
}
}
function Invoke-RetryWithOutput {
<#
.SYNOPSIS
Generic retry logic.
.DESCRIPTION
This command will perform the action specified until the action generates no errors, unless the retry limit has been reached.
.PARAMETER Command
Accepts an Action object.
You can create a script block by enclosing your script within curly braces.
.PARAMETER Retry
Number of retries to attempt.
.PARAMETER Delay
The maximum delay (in seconds) between each attempt. The default is 5 seconds.
.EXAMPLE
$cmd = { If ((Get-Date) -lt (Get-Date -Second 59)) { Get-Object foo } Else { Write-Host 'ok' } }
Invoke-RetryWithOutput -Command $cmd -Retry 61
#>
[CmdletBinding()]
Param
(
[Parameter(Mandatory = $true, Position = 1)]
[ScriptBlock]$Command,
[Parameter(Mandatory = $false, Position = 2)]
[ValidateRange(0, [UInt32]::MaxValue)]
[UInt32]$Retry = 3,
[Parameter(Mandatory = $false, Position = 3)]
[ValidateRange(0, [UInt32]::MaxValue)]
[UInt32]$Delay = 5
)
$ErrorActionPreferenceToRestore = $ErrorActionPreference
$ErrorActionPreference = "Stop"
for ($i = 0; $i -lt $Retry; $i++) {
$exceptionMessage = ""
try {
Write-Telemetry -Message ("[Debug]Command [{0}] started. Retry: {1}." -f $Command, ($i + 1) + $ForwardSlashSeparator + $Retry) -Level $Verbose
$output = Invoke-Command $Command
Write-Telemetry -Message ("[Debug]Command [{0}] succeeded." -f $Command) -Level $Verbose
$ErrorActionPreference = $ErrorActionPreferenceToRestore
return $output
}
catch [Exception] {
$exceptionMessage = $_.Exception.Message
if ($Global:Error.Count -gt 0) {
$Global:Error.RemoveAt(0)
}
if ($i -eq ($Retry - 1)) {
$message = ("[Debug]Command [{0}] failed even after [{1}] retries. Exception message:{2}." -f $command, $Retry, $exceptionMessage)
Write-Telemetry -Message $message -Level $ErrorLvl
$ErrorActionPreference = $ErrorActionPreferenceToRestore
throw $message
}
$exponential = [math]::Pow(2, ($i + 1))
$retryDelaySeconds = ($exponential - 1) * $Delay # Exponential Backoff Max == (2^n)-1
Write-Telemetry -Message ("[Debug]Command [{0}] failed. Retrying in {1} seconds, exception message:{2}." -f $command, $retryDelaySeconds, $exceptionMessage) -Level $Warning
Start-Sleep -Seconds $retryDelaySeconds
}
}
}
function Invoke-AzRestApiWithRetry {
<#
.SYNOPSIS
Wrapper around Invoke-AzRestMethod.
.DESCRIPTION
This function calls Invoke-AzRestMethod with retries.
.PARAMETER Params
Parameters to the cmdlet.
.PARAMETER Payload
Payload.
.PARAMETER Retry
Number of retries to attempt.
.PARAMETER Delay
The maximum delay (in seconds) between each attempt. The default is 5 seconds.
.EXAMPLE
Invoke-AzRestApiWithRetry -Params @{SubscriptionId = "xxxx" ResourceGroup = "rgName" ResourceName = "resourceName" ResourceProvider = "Microsoft.Compute" ResourceType = "virtualMachines"} -Payload "{'location': 'westeurope'}"
#>
[CmdletBinding()]
Param
(
[Parameter(Mandatory = $true, Position = 1)]
[System.Collections.Hashtable]$Params,
[Parameter(Mandatory = $false, Position = 2)]
[Object]$Payload = $null,
[Parameter(Mandatory = $false, Position = 3)]
[ValidateRange(0, [UInt32]::MaxValue)]
[UInt32]$Retry = 3,
[Parameter(Mandatory = $false, Position = 4)]
[ValidateRange(0, [UInt32]::MaxValue)]
[UInt32]$Delay = 5
)
if ($Payload) {
[void]$Params.Add('Payload', $Payload)
}
$retriableErrorCodes = @(429)
for ($i = 0; $i -lt $Retry; $i++) {
$exceptionMessage = ""
$paramsString = $Params | ConvertTo-Json -Compress -Depth $MaxDepth | ConvertFrom-Json
try {
Write-Telemetry -Message ("[Debug]Invoke-AzRestMethod started with params [{0}]. Retry: {1}." -f $paramsString, ($i + 1) + $ForwardSlashSeparator + $Retry) -Level $Verbose
$output = Invoke-AzRestMethod @Params -ErrorAction Stop
$outputString = $output | ConvertTo-Json -Compress -Depth $MaxDepth | ConvertFrom-Json
if ($retriableErrorCodes.Contains($output.StatusCode) -or $output.StatusCode -ge 500) {
if ($i -eq ($Retry - 1)) {
$message = ("[Debug]Invoke-AzRestMethod with params [{0}] failed even after [{1}] retries. Failure reason:{2}." -f $paramsString, $Retry, $outputString)
Write-Telemetry -Message $message -Level $ErrorLvl
return Process-ApiResponse -Response $output
}
$exponential = [math]::Pow(2, ($i + 1))
$retryDelaySeconds = ($exponential - 1) * $Delay # Exponential Backoff Max == (2^n)-1
Write-Telemetry -Message ("[Debug]Invoke-AzRestMethod with params [{0}] failed with retriable error code. Retrying in {1} seconds, Failure reason:{2}." -f $paramsString, $retryDelaySeconds, $outputString) -Level $Warning
Start-Sleep -Seconds $retryDelaySeconds
}
else {
Write-Telemetry -Message ("[Debug]Invoke-AzRestMethod with params [{0}] succeeded. Output: [{1}]." -f $paramsString, $outputString) -Level $Verbose
return Process-ApiResponse -Response $output
}
}
catch [Exception] {
$exceptionMessage = $_.Exception.Message
Write-Telemetry -Message ("[Debug]Invoke-AzRestMethod with params [{0}] failed with an unhandled exception: {1}." -f $paramsString, $exceptionMessage) -Level $ErrorLvl
throw
}
}
}
function Invoke-ArmApi-WithPath {
<#
.SYNOPSIS
The function prepares payload for Invoke-AzRestMethod
.DESCRIPTION
This function prepares payload for Invoke-AzRestMethod.
.PARAMETER Path
ARM API path.
.PARAMETER ApiVersion
API version.
.PARAMETER Method
HTTP method.
.PARAMETER Payload
Paylod for API call.
.EXAMPLE
Invoke-ArmApi-WithPath -Path "/subscriptions/{subId}/resourceGroups/{rgName}/providers/Microsoft.Compute/virtualMachines/{vmName}/start" -ApiVersion "2023-03-01" -method "PATCH" -Payload "{'location': 'westeurope'}"
#>
[CmdletBinding()]
Param
(
[Parameter(Mandatory = $true, Position = 1)]
[String]$Path,
[Parameter(Mandatory = $true, Position = 2)]
[String]$ApiVersion,
[Parameter(Mandatory = $true, Position = 3)]
[ValidateScript({ $_ -in $HttpMethods })]
[String]$Method,
[Parameter(Mandatory = $false, Position = 4)]
[Object]$Payload = $null
)
$PathWithVersion = "{0}?api-version={1}"
if ($Path.Contains("?")) {
$PathWithVersion = "{0}&api-version={1}"
}
$Uri = ($PathWithVersion -f $Path, $ApiVersion)
$Params = @{
Path = $Uri
Method = $Method
}
return Invoke-AzRestApiWithRetry -Params $Params -Payload $Payload
}
function Process-ApiResponse {
<#
.SYNOPSIS
Process API response and returns data.
.PARAMETER Response
Response object.
.EXAMPLE
Process-ApiResponse -Response {"StatusCode": 200, "Content": "{\"properties\": {\"location\": \"westeurope\"}}" }
#>
[CmdletBinding()]
Param
(
[Parameter(Mandatory = $true, Position = 1)]
[Object]$Response
)
$successErrorCodes = @(200, 201, 202, 204)
$content = $null
if ($Response.Content) {
$content = ConvertFrom-Json $Response.Content
}
if ($successErrorCodes.Contains($Response.StatusCode)) {
return @{
Status = $Succeeded
Response = $content
ErrorCode = [String]::Empty
ErrorMessage = [String]::Empty
}
}
else {
$errorCode = $Unknown
$errorMessage = $Unknown
if ($content.error) {
$errorCode = ("{0}/{1}" -f $Response.StatusCode, $content.error.code)
$errorMessage = $content.error.message
}
return @{
Status = $Failed
Response = $content
ErrorCode = $errorCode
ErrorMessage = $errorMessage
}
}
}
function Get-MachinesFromLogAnalytics {
<#
.SYNOPSIS
Gets machines onboarded to changeTracking solution from Log Analytics Workspace.
.DESCRIPTION
This command will return machines onboarded to changeTracking from LA workspace.
.PARAMETER ResourceId
Resource Id.
.EXAMPLE
Get-MachinesFromLogAnalytics -ResourceId "/subscriptions/{subId}/resourceGroups/{rgName}/providers/Microsoft.OperationalInsights/workspaces/{laName}"
#>
[CmdletBinding()]
Param
(
[Parameter(Mandatory = $true, Position = 1)]
[String]$ResourceId
)
$armComponents = Parse-ArmId -ResourceId $ResourceId
$script = {
Set-AzContext -Subscription $armComponents.Subscription
$Workspace = Get-AzOperationalInsightsWorkspace -ResourceGroupName $armComponents.ResourceGroup -Name $armComponents.ResourceName
$QueryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $Workspace.CustomerId -Query $MachinesOnboaredToChangeTrackingQuery -ErrorAction Stop
return $QueryResults
}
$output = Invoke-RetryWithOutput -command $script
return $output
}
function Populate-AllMachinesOnboardedToChangeTracking {
<#
.SYNOPSIS
Gets all machines onboarded to changeTracking under this log analytics workspace.
.DESCRIPTION
This function gets all machines onboarded to changeTracking under this Log Analytics Workspace.
.PARAMETER LogAnalyticsWorkspaceResourceId
Log Analytics Workspace resource id.
.EXAMPLE
Populate-AllMachinesOnboardedToChangeTracking LogAnalyticsWorkspaceResourceId "/subscriptions/{subId}/resourceGroups/{rgName}/providers/Microsoft.OperationalInsights/workspaces/{laName}"
#>
[CmdletBinding()]
Param
(
[Parameter(Mandatory = $true, Position = 1)]
[String]$LogAnalyticsWorkspaceResourceId
)
try {
$laResults = Get-MachinesFromLogAnalytics -ResourceId $LogAnalyticsWorkspaceResourceId
if ($laResults.Results.Count -eq 0 -and $null -eq $laResults.Error) {
Write-Telemetry -Message ("Zero machines retrieved from Log Analytics Workspace. If machines were recently onboarded, please wait for few minutes for machines to start reporting to Log Analytics Workspace") -Level $ErrorLvl
throw
}
elseif ($laResults.Results.Count -gt 0 -or @($laResults.Results).Count -gt 0) {
Write-Telemetry -Message ("Retrieved machines from Log Analytics Workspace.")
foreach ($record in $laResults.Results) {
if ($record.ResourceType -eq $ArcVMResourceType -or $record.ResourceType -eq $VMResourceType) {
$machineRecord = [PSCustomObject]@{
ResourceId = $record.ResourceId
OsType = $record.OSType
ResourceType = $record.ResourceType
}
[void]$Global:Machines.Add($machineRecord)
}
}
}
else {
Write-Telemetry -Message ("Failed to get machines from Log Analytics Workspace with error {0}." -f $laResults.Error) -Level $ErrorLvl
throw
}
}
catch [Exception] {
Write-Telemetry -Message ("Unhandled exception {0}." -f , $_.Exception.Message) -Level $ErrorLvl
throw
}
}
function Get-WindowsRegistrySettings {
<#
.SYNOPSIS
Gets windows registry settings for legacy Change Tracking solution using Input Log Analytics Workspace and translates them to equivalent settings for latest Change Tracking solution using Output Log Analytics Workspace.
.DESCRIPTION
Gets windows registry settings for legacy Change Tracking solution using Input Log Analytics Workspace and translates them to equivalent settings for latest Change Tracking solution using Output Log Analytics Workspace.
.PARAMETER LogAnalyticsWorkspaceResourceId
Input Log Analytics Workspace Resource Id to get the CT settings from.
.EXAMPLE
Get-WindowsRegistrySettings LogAnalyticsWorkspaceResourceId "/subscriptions/{subId}/resourceGroups/{rgName}/providers/Microsoft.OperationalInsights/workspaces/{laName}"
#>
[CmdletBinding()]
Param
(
[Parameter(Mandatory = $true, Position = 1)]
[String]$LogAnalyticsWorkspaceResourceId
)
try {
$ctv1RegistryResponse = Invoke-ArmApi-WithPath -Path ($WindowsRegistrySettingsPath -f $LogAnalyticsWorkspaceResourceId) -ApiVersion $LogAnalyticsWorkspaceApiVersion -Method $GET
$registrySettings = New-Object System.Collections.ArrayList
foreach ($object in $ctv1RegistryResponse.Response.value) {
foreach ($objectProperties in $object.Properties) {
$ctv2SettingObject = [PSCustomObject]@{
name = ($object.name -replace ("-", "_"))
groupTag = if ($objectProperties.groupTag -eq "") { "Recommended" } else { $objectProperties.groupTag }
enabled = if ($objectProperties.enabled -eq "true") { $true } else { $false }
recurse = if ($objectProperties.recurse -eq "true") { $true } else { $false }
description = ""
keyName = ($objectProperties.keyName -replace ("\\{2}", "\"))
valueName = $objectProperties.valueName
}
$registrySettings.Add($ctv2SettingObject) > $null
}
}
$Global:CTv2JsonObject.resources[0].properties.dataSources.extensions[0].extensionSettings.registrySettings.registryInfo = $registrySettings
Write-Telemetry -Message "Retrieved windows registry settings successfully"
}
catch [Exception] {
$exceptionMessage = $_.Exception.Message
Write-Telemetry -Message ("Retrieving windows registry settings failed with an unhandled exception: {0}." -f $exceptionMessage) -Level $ErrorLvl
}
}
function Get-WindowsFileSetting {
<#
.SYNOPSIS
Gets windows file settings for legacy Change Tracking solution using Input Log Analytics Workspace and translates them to equivalent settings for latest Change Tracking solution using Output Log Analytics Workspace.
.DESCRIPTION
Gets windows file settings for legacy Change Tracking solution using Input Log Analytics Workspace and translates them to equivalent settings for latest Change Tracking solution using Output Log Analytics Workspace.
.PARAMETER LogAnalyticsWorkspaceResourceId
Input Log Analytics Workspace Resource Id to get the CT settings from.
.EXAMPLE
Get-WindowsFileSetting LogAnalyticsWorkspaceResourceId "/subscriptions/{subId}/resourceGroups/{rgName}/providers/Microsoft.OperationalInsights/workspaces/{laName}"
#>
[CmdletBinding()]
Param
(
[Parameter(Mandatory = $true, Position = 1)]
[String]$LogAnalyticsWorkspaceResourceId
)
try {
$ctv1FileResponse = Invoke-ArmApi-WithPath -Path ($WindowsFileSettingsPath -f $LogAnalyticsWorkspaceResourceId) -ApiVersion $LogAnalyticsWorkspaceApiVersion -Method $GET
$fileSettingObjectList = New-Object System.Collections.ArrayList
foreach ($object in $ctv1FileResponse.Response.value) {
foreach ($objectProperties in $object.Properties) {
$ctv2SettingObject = [PSCustomObject]@{
name = ($object.name -replace ("-", "_"))
enabled = if ($objectProperties.enabled -eq "true") { $true } else { $false }
description = ""
path = $objectProperties.path
recurse = if ($objectProperties.recurse -eq "true") { $true } else { $false }
maxContentsReturnable = if ($objectProperties.maxContentsReturnable -eq 0) { 5000000 } else { $objectProperties.maxContentsReturnable }
maxOutputSize = if ($objectProperties.maxOutputSize -eq 0) { 500000 } else { $objectProperties.maxOutputSize }
checksum = $objectProperties.checksum
pathType = $objectProperties.pathType
groupTag = $objectProperties.groupTag
}
$fileSettingObjectList.Add($ctv2SettingObject) > $null
}
}
$Global:CTv2JsonObject.resources[0].properties.dataSources.extensions[0].extensionSettings.fileSettings.fileinfo = $fileSettingObjectList
Write-Telemetry -Message "Retrieved windows file settings successfully"
}
catch [Exception] {
$exceptionMessage = $_.Exception.Message
Write-Telemetry -Message ("Retrieving windows file settings failed with an unhandled exception: {0}." -f $exceptionMessage) -Level $ErrorLvl
}
}
function Get-WindowsTrackingServices {
<#
.SYNOPSIS
Gets windows tracking services settings for legacy Change Tracking solution using Input Log Analytics Workspace and translates them to equivalent settings for latest Change Tracking solution using Output Log Analytics Workspace.
.DESCRIPTION
Gets windows tracking services settings for legacy Change Tracking solution using Input Log Analytics Workspace and translates them to equivalent settings for latest Change Tracking solution using Output Log Analytics Workspace.
.PARAMETER LogAnalyticsWorkspaceResourceId
Input Log Analytics Workspace Resource Id to get the CT settings from.
.EXAMPLE
Get-WindowsTrackingServices LogAnalyticsWorkspaceResourceId "/subscriptions/{subId}/resourceGroups/{rgName}/providers/Microsoft.OperationalInsights/workspaces/{laName}"
#>
[CmdletBinding()]
Param
(
[Parameter(Mandatory = $true, Position = 1)]
[String]$LogAnalyticsWorkspaceResourceId
)
try {
$ctv1FileResponse = Invoke-ArmApi-WithPath -Path ($WindowsTrackingServicesPath -f $LogAnalyticsWorkspaceResourceId) -ApiVersion $LogAnalyticsWorkspaceApiVersion -Method $GET
# check if collectionTimeInterval is greater than 600 or not. if not make it 600(10 minutes)
$cvtv1CollectionTimeInterval = if ($ctv1FileResponse.Response.value.properties.CollectionTimeInterval -gt 600) { $ctv1FileResponse.Response.value.properties.CollectionTimeInterval } else { 600 }
$Global:CTv2JsonObject.resources[0].properties.dataSources.extensions[0].extensionSettings.servicesSettings.serviceCollectionFrequency = $cvtv1CollectionTimeInterval
Write-Telemetry -Message "Retrieved windows tracking services settings successfully"
}
catch [Exception] {
$exceptionMessage = $_.Exception.Message
Write-Telemetry -Message ("Retrieving windows tracking services settings failed with an unhandled exception: {0}." -f $exceptionMessage) -Level $ErrorLvl
}
}
function Get-LinuxFileSettings {
<#
.SYNOPSIS
Gets linux file settings for legacy Change Tracking solution using Input Log Analytics Workspace and translates them to equivalent settings for latest Change Tracking solution using Output Log Analytics Workspace.
.DESCRIPTION
Gets linux file settings for legacy Change Tracking solution using Input Log Analytics Workspace and translates them to equivalent settings for latest Change Tracking solution using Output Log Analytics Workspace.
.PARAMETER LogAnalyticsWorkspaceResourceId
Input Log Analytics Workspace Resource Id to get the CT settings from.
.EXAMPLE
Get-LinuxFileSettings LogAnalyticsWorkspaceResourceId "/subscriptions/{subId}/resourceGroups/{rgName}/providers/Microsoft.OperationalInsights/workspaces/{laName}"
#>
[CmdletBinding()]
Param
(
[Parameter(Mandatory = $true, Position = 1)]
[String]$LogAnalyticsWorkspaceResourceId
)
try {
$ctv1LinuxFileResponse = Invoke-ArmApi-WithPath -Path ($LinuxFileSettingsPath -f $LogAnalyticsWorkspaceResourceId) -ApiVersion $LogAnalyticsWorkspaceApiVersion -Method $GET
$fileSettingObjectList = New-Object System.Collections.ArrayList
foreach ($object in $ctv1LinuxFileResponse.Response.value) {
foreach ($objectProperties in $object.Properties) {
$ctv2SettingObject = [PSCustomObject]@{
name = ($object.name -replace ("-", "_"))
description = ""
uploadContent = $false
checksum = "Sha256"
enabled = if ($objectProperties.enabled -eq "true") { $true } else { $false }
destinationPath = $objectProperties.destinationPath
useSudo = if ($objectProperties.useSudo -eq "true") { $true } else { $false }
recurse = if ($objectProperties.recurse -eq "true") { $true } else { $false }
maxContentsReturnable = if ($objectProperties.maxContentsReturnable -eq 0) { 5000000 } else { $objectProperties.maxContentsReturnable }
pathType = $objectProperties.pathType
links = $objectProperties.links
maxOutputSize = if ($objectProperties.maxOutputSize -eq 0) { 5 } else { $objectProperties.maxOutputSize }
groupTag = $objectProperties.groupTag
}
$fileSettingObjectList.Add($ctv2SettingObject) > $null
}
}
$Global:CTv2JsonObject.resources[0].properties.dataSources.extensions[1].extensionSettings.fileSettings.fileInfo = $fileSettingObjectList
Write-Telemetry -Message "Retrieved linux file settings successfully"
}
catch [Exception] {
$exceptionMessage = $_.Exception.Message
Write-Telemetry -Message ("Retrieving linux file settings failed with an unhandled exception: {0}." -f $exceptionMessage) -Level $ErrorLvl
}
}
function Get-DataTypeConfiguration {
<#
.SYNOPSIS
Gets data type settings for legacy Change Tracking solution using Input Log Analytics Workspace and translates them to equivalent settings for latest Change Tracking solution using Output Log Analytics Workspace.
.DESCRIPTION
Gets data type settings for legacy Change Tracking solution using Input Log Analytics Workspace and translates them to equivalent settings for latest Change Tracking solution using Output Log Analytics Workspace.
.PARAMETER LogAnalyticsWorkspaceResourceId
Input Log Analytics Workspace Resource Id to get the CT settings from.
.EXAMPLE
Get-DataTypeConfiguration LogAnalyticsWorkspaceResourceId "/subscriptions/{subId}/resourceGroups/{rgName}/providers/Microsoft.OperationalInsights/workspaces/{laName}"
#>
[CmdletBinding()]
Param
(
[Parameter(Mandatory = $true, Position = 1)]
[String]$LogAnalyticsWorkspaceResourceId
)
try {
$ctv1DatatypeConfigurationResponse = Invoke-ArmApi-WithPath -Path ($DataTypeConfigurationPath -f $LogAnalyticsWorkspaceResourceId) -ApiVersion $LogAnalyticsWorkspaceApiVersion -Method $GET
foreach ($object in $ctv1DatatypeConfigurationResponse.Response.value) {
if ($object.properties.DataTypeId -eq "Daemons") {
$Global:CTv2JsonObject.resources[0].properties.dataSources.extensions[1].extensionSettings.enableServices = if ($object.Enabled -eq "false") { $false } else { $true }
}
if ($object.properties.DataTypeId -eq "Files") {
$Global:CTv2JsonObject.resources[0].properties.dataSources.extensions[1].extensionSettings.enableFiles = if ($object.Enabled -eq "false") { $false } else { $true }
$Global:CTv2JsonObject.resources[0].properties.dataSources.extensions[0].extensionSettings.enableFiles = if ($object.Enabled -eq "false") { $false } else { $true }
}
if ($object.properties.DataTypeId -eq "Inventory") {
$Global:CTv2JsonObject.resources[0].properties.dataSources.extensions[1].extensionSettings.enableInventory = if ($object.Enabled -eq "false") { $false } else { $true }
$Global:CTv2JsonObject.resources[0].properties.dataSources.extensions[0].extensionSettings.enableInventory = if ($object.Enabled -eq "false") { $false } else { $true }
}
if ($object.properties.DataTypeId -eq "Software") {
$Global:CTv2JsonObject.resources[0].properties.dataSources.extensions[1].extensionSettings.enableSoftware = if ($object.Enabled -eq "false") { $false } else { $true }
$Global:CTv2JsonObject.resources[0].properties.dataSources.extensions[0].extensionSettings.enableSoftware = if ($object.Enabled -eq "false") { $false } else { $true }
}
if ($object.properties.DataTypeId -eq "Registry") {
$Global:CTv2JsonObject.resources[0].properties.dataSources.extensions[0].extensionSettings.enableRegistry = if ($object.Enabled -eq "false") { $false } else { $true }
$Global:CTv2JsonObject.resources[0].properties.dataSources.extensions[1].extensionSettings.enableRegistry = $false
}
if ($object.properties.DataTypeId -eq "WindowsServices") {
$Global:CTv2JsonObject.resources[0].properties.dataSources.extensions[0].extensionSettings.enableServices = if ($object.Enabled -eq "false") { $false } else { $true }
}
}
Write-Telemetry -Message "Retrieved data type settings successfully"
}
catch [Exception] {
$exceptionMessage = $_.Exception.Message
Write-Telemetry -Message ("Retrieving data type settings failed with an unhandled exception: {0}." -f $exceptionMessage) -Level $ErrorLvl
}
}
function Assign-DCR {
<#
.SYNOPSIS
Assign DCR to machine and install AMA Agent and CT Extensions.
.DESCRIPTION
Assign DCR to machines and install AMA Agent and CT Extensions.
.PARAMETER ResourceId
ARM Resource Id of machine.
.PARAMETER ResourceType
Resource type of machine.
.PARAMETER OsType
Os type of machine.
.EXAMPLE
Assign-DCR -ResourceId $ResourceId -ResourceType $ResourceType -OsType $OsType
#>
[CmdletBinding()]
Param
(
[Parameter(Mandatory = $true, Position = 1)]
[String]$ResourceId,
[Parameter(Mandatory = $true, Position = 2)]
[String]$ResourceType,
[Parameter(Mandatory = $true, Position = 3)]
[String]$OsType
)
$armComponents = Parse-ArmId -ResourceId $ResourceId
$dcrAssociationName = ($armComponents.ResourceName -replace "[^a-zA-Z0-9-]") + "_" + $MigrationTag
$ctExtensionName = ($OsType -eq "windows") ? $CTWindowsExtensionName : $CTLinuxExtensionName
$amaExtensionName = ($OsType -eq "windows") ? $AMAWindowsExtensionName : $AMALinuxExtensionName
$extensionType = ($ResourceType -eq $ArcVMResourceType) ? $ArcExtensionType : $AzureExtensionType
$apiVersion = ($ResourceType -eq $ArcVMResourceType) ? $ArcMachineExtensionApiVersion : $VirtualMachineExtensionApiVersion
$dependsOnScope = ($AssignDCRPath -f $ResourceId, $dcrAssociationName)
$amaTypeHandlerVersion = ($ResourceType -eq $VMResourceType -and $OsType -eq "linux") ? "1.0" : ($ResourceType -eq $ArcVMResourceType -and $OsType -eq "linux") ? "1.33" : "1.30"
$resourceLocation = Invoke-ArmApi-WithPath -Path $ResourceId -ApiVersion $apiVersion -Method $GET
# Assign DCR scope to machine.
$assignDCRPayload = ConvertFrom-Json $AssignDCRARMTemplate
$assignDCRPayload.properties.template.resources[0].name = $dcrAssociationName
$assignDCRPayload.properties.template.resources[0].properties.dataCollectionRuleId = $Global:DCRResourceId
$assignDCRPayload.properties.template.resources[0].scope = $ResourceId
# Install CT extension on the machine and associate with DCR.
$assignDCRPayload.properties.template.resources[1].type = $extensionType
$assignDCRPayload.properties.template.resources[1].apiVersion = $apiVersion
$assignDCRPayload.properties.template.resources[1].name = $armComponents.ResourceName + "/" + $ctExtensionName
$assignDCRPayload.properties.template.resources[1].location = $resourceLocation.Response.location
$dependsOn = [System.Collections.ArrayList]@()
[void]$dependsOn.Add($dependsOnScope)
$assignDCRPayload.properties.template.resources[1].dependsOn = $dependsOn
$assignDCRPayload.properties.template.resources[1].properties.type = $ctExtensionName
# Install AMA Agent on the machine and associate with DCR.
$assignDCRPayload.properties.template.resources[2].type = $extensionType
$assignDCRPayload.properties.template.resources[2].apiVersion = $apiVersion
$assignDCRPayload.properties.template.resources[2].name = $armComponents.ResourceName + "/" + $amaExtensionName
$assignDCRPayload.properties.template.resources[2].location = $resourceLocation.Response.location
$dependsOn = [System.Collections.ArrayList]@()
[void]$dependsOn.Add($dependsOnScope)
$assignDCRPayload.properties.template.resources[2].dependsOn = $dependsOn
$assignDCRPayload.properties.template.resources[2].properties.type = $amaExtensionName
$assignDCRPayload.properties.template.resources[2].properties.typeHandlerVersion = $amaTypeHandlerVersion
$assignDCRPayload = ConvertTo-Json -InputObject $assignDCRPayload -Depth $MaxDepth
$assignDCRResponse = Invoke-ArmApi-WithPath -Path ($ARMDeploymentPath -f $armComponents.Subscription, $armComponents.ResourceGroup, $dcrAssociationName) -ApiVersion $ARMDepoymentApiVersion -Method $PUT -Payload $assignDCRPayload
if ($assignDCRResponse.Status -eq $Succeeded)
{
do {
$assignDCRResponse = Invoke-ArmApi-WithPath -Path ($ARMDeploymentPath -f $armComponents.Subscription, $armComponents.ResourceGroup, $dcrAssociationName) -ApiVersion $ARMDepoymentApiVersion -Method $GET
if ($assignDCRResponse.Response.properties.provisioningState -eq $Failed) {
Write-Telemetry -Message ("Failed to deploy assign DCR template {0} for machine {1} with below error." -f $dcrAssociationName, $ResourceId) -Level $ErrorLvl
Write-Telemetry -Message ($assignDCRResponse.response.properties.error | ConvertTo-Json -Depth $MaxDepth) -Level $ErrorLvl
throw
}
}
while ($assignDCRResponse.Response.properties.provisioningState -ne $Succeeded) {
}
Write-Telemetry -Message ("Assigned DCR Template and installed AMA Agent and CT Extension for machine {0}." -f $ResourceId)
}
else {
Write-Telemetry -Message ("Failed to deploy assign DCR template {0} for machine {1} with below error." -f $dcrAssociationName, $ResourceId) -Level $ErrorLvl
Write-Telemetry -Message ($assignDCRResponse.response.properties.error | ConvertTo-Json -Depth $MaxDepth) -Level $ErrorLvl
throw
}
}
function Associate-MachinesWithDCR {
<#
.SYNOPSIS
Assign DCR to machines and install AMA Agent and CT Extensions.
.DESCRIPTION
Assign DCR to machines and install AMA Agent and CT Extensions.
.EXAMPLE
Associate-MachinesWithDCR
#>
foreach ($machine in $Global:Machines) {
try {
Assign-DCR -ResourceId $machine.ResourceId -ResourceType $machine.ResourceType -OSType $machine.OsType
}
catch [Exception] {
Write-Telemetry -Message ("Unhandled Exception {0}." -f $_.Exception.Message) -Level $ErrorLvl
}
}
}
function Remove-MMAAgent {
<#
.SYNOPSIS
Removes MMA Agent from machine.
.DESCRIPTION
Removes MMA Agent from machine.
.PARAMETER ResourceId
ARM Resource Id of machine to remove MMA Agent.
.PARAMETER ApiVersion
Api version to use.
.EXAMPLE
Remove-MMAAgent -ResourceId $ResourceId -ApiVersion $ApiVersion
#>
[CmdletBinding()]
Param
(
[Parameter(Mandatory = $true, Position = 1)]
[String]$ResourceId,
[Parameter(Mandatory = $true, Position = 2)]
[String]$ApiVersion
)
$extensionsResponse = Invoke-ArmApi-WithPath -Path ($GetExtensionsPath -f $ResourceId) -ApiVersion $ApiVersion -Method $GET
foreach ($extension in $extensionsResponse.Response.value)
{
if ($MMAAgentTypes.Contains($extension.properties.type)) {
$removeExtensionResponse = Invoke-ArmApi-WithPath -Path $extension.id -ApiVersion $ApiVersion -Method $DELETE
if ($removeExtensionResponse.Status -eq $Succeeded) {
Write-Telemetry -Message ("Deleted {0}." -f $extension.id)
}
else {
Write-Telemetry -Message ("Failed to delete {0} with error {1}." -f $extension.id, $removeExtensionResponse.ErrorMessage) -Level $ErrorLvl
}
}
}
}
function Remove-MMAAgentFromMachines {
<#
.SYNOPSIS
Removes MMA Agent from Input Log Analytics Workspace if Input and Output Log Analytics Workspaces are the same for migration.
.DESCRIPTION
Removes MMA Agent from Input Log Analytics Workspace if Input and Output Log Analytics Workspaces are the same for migration.
.PARAMETER InputLogAnalyticsWorkspaceResourceId
Input Log Analytics Workspace Resource Id.
.PARAMETER OutputLogAnalyticsWorkspaceResourceId
Output Log Analytics Workspace Resource Id.
.EXAMPLE
Remove-MMAAgentFromMachines -InputLogAnalyticsWorkspaceResourceId "/subscriptions/{subId}/resourceGroups/{rgName}/providers/Microsoft.OperationalInsights/workspaces/{laName}" -OutputLogAnalyticsWorkspaceResourceId "/subscriptions/{subId}/resourceGroups/{rgName}/providers/Microsoft.OperationalInsights/workspaces/{laName}"
#>
[CmdletBinding()]
Param
(
[Parameter(Mandatory = $true, Position = 1)]
[String]$InputLogAnalyticsWorkspaceResourceId,
[Parameter(Mandatory = $true, Position = 2)]
[String]$OutputLogAnalyticsWorkspaceResourceId
)
if ($InputLogAnalyticsWorkspaceResourceId -ne $OutputLogAnalyticsWorkspaceResourceId)
{
Write-Telemetry -Message ("Input and Output Log Analytics Workspaces are different.Not removing MMA agents." -f ($Global:DCRResourceId, $ResourceId))
return
}
foreach ($machine in $Global:Machines) {
try {
if ($machine.ResourceType -eq $ArcVMResourceType) {
Remove-MMAAgent -ResourceId $machine.ResourceId -ApiVersion $ArcMachineExtensionApiVersion
}
else {
Remove-MMAAgent -ResourceId $machine.ResourceId -ApiVersion $VirtualMachineExtensionApiVersion
}
}
catch [Exception] {
Write-Telemetry -Message ("Unhandled Exception {0}." -f $_.Exception.Message) -Level $ErrorLvl
}
}
}
function Deploy-ChangeTrackingSolutionARMTemplate {
<#
.SYNOPSIS
Deploys Change Tracking solution to Output Log Analytics Workspace.
.DESCRIPTION
Deploys Change Tracking solution to Output Log Analytics Workspace.
.PARAMETER InputLogAnalyticsWorkspaceResourceId
Input Log Analytics Workspace Resource Id.
.PARAMETER OutputLogAnalyticsWorkspaceResourceId
Output Log Analytics Workspace Resource Id.
.EXAMPLE
Deploy-ChangeTrackingSolutionARMTemplate -InputLogAnalyticsWorkspaceResourceId "/subscriptions/{subId}/resourceGroups/{rgName}/providers/Microsoft.OperationalInsights/workspaces/{laName}" -OutputLogAnalyticsWorkspaceResourceId "/subscriptions/{subId}/resourceGroups/{rgName}/providers/Microsoft.OperationalInsights/workspaces/{laName}"
#>
[CmdletBinding()]
Param
(
[Parameter(Mandatory = $true, Position = 1)]
[String]$InputLogAnalyticsWorkspaceResourceId,
[Parameter(Mandatory = $true, Position = 2)]
[String]$OutputLogAnalyticsWorkspaceResourceId
)
if ($InputLogAnalyticsWorkspaceResourceId -eq $OutputLogAnalyticsWorkspaceResourceId)
{
Write-Telemetry -Message "Input and Output Log Analytics Workspaces are same.Not required to deploy Change Tracking solution again."
return
}
try {
$armComponents = Parse-ArmId -ResourceId $OutputLogAnalyticsWorkspaceResourceId
$changeTrackingArmTemplatePayload = ConvertFrom-Json $DeployChangeTrackingSolutionARMTemplate
$changeTrackingArmTemplatePayload.properties.parameters.subscriptionId.value = $armComponents.Subscription
$changeTrackingArmTemplatePayload.properties.parameters.resourcegroupName.value = $armComponents.ResourceGroup
$changeTrackingArmTemplatePayload.properties.parameters.location.value = $Global:OutputDCRLocation
$changeTrackingArmTemplatePayload.properties.parameters.workspaceName.value = $armComponents.ResourceName
$changeTrackingArmTemplatePayload.properties.parameters.solutionType.value = "ChangeTracking"
$deploymentName = "DeployCTSolution_" + $MigrationTag + "_" + (New-Guid).Guid.ToString()
$changeTrackingArmTemplatePayload = ConvertTo-Json -InputObject $changeTrackingArmTemplatePayload -Depth $MaxDepth | %{
[Regex]::Replace($_,
"\\u(?<Value>[a-zA-Z0-9]{4})", {
param($m) ([char]([int]::Parse($m.Groups['Value'].Value,
[System.Globalization.NumberStyles]::HexNumber))).ToString() } )}
$changeTrackingArmTemplateResponse = Invoke-ArmApi-WithPath -Path ($ARMDeploymentPath -f $armComponents.Subscription, $armComponents.ResourceGroup, $deploymentName) -ApiVersion $ARMDepoymentApiVersion -Method $PUT -Payload $changeTrackingArmTemplatePayload
if ($changeTrackingArmTemplateResponse.Status -eq $Succeeded)
{
do {
$changeTrackingArmTemplateResponse = Invoke-ArmApi-WithPath -Path ($ARMDeploymentPath -f $armComponents.Subscription, $armComponents.ResourceGroup, $deploymentName) -ApiVersion $ARMDepoymentApiVersion -Method $GET
if ($changeTrackingArmTemplateResponse.Response.properties.provisioningState -eq $Failed) {
Write-Telemetry -Message ("Failed to deploy change tracking solution template {0} with below error." -f $deploymentName) -Level $ErrorLvl
Write-Telemetry -Message ($changeTrackingArmTemplateResponse.response.properties.error | ConvertTo-Json -Depth $MaxDepth) -Level $ErrorLvl
throw
}
}
while ($changeTrackingArmTemplateResponse.Response.properties.provisioningState -ne $Succeeded) {
}
Write-Telemetry -Message "Deployed change tracking solution successfully."
}
else {
Write-Telemetry -Message ("Failed to deploy change tracking solution template {0} with below error." -f $deploymentName) -Level $ErrorLvl
Write-Telemetry -Message ($changeTrackingArmTemplateResponse.response.properties.error | ConvertTo-Json -Depth $MaxDepth) -Level $ErrorLvl
throw
}
}
catch [Exception] {
Write-Telemetry -Message ("Unhandled exception {0} while deploying change tracking solution template." -f , $_.Exception.Message) -Level $ErrorLvl
throw
}
}
function Deploy-DCRARMTemplate {
<#
.SYNOPSIS
Deploys DCR ARM template.
.DESCRIPTION
Deploys DCR ARM template.
.PARAMETER LogAnalyticsWorkspaceResourceId
Log Analytics Workspace Resource Id.
.PARAMETER OutputDCRName
DCR name.
.EXAMPLE
Deploy-DCRARMTemplate LogAnalyticsWorkspaceResourceId "/subscriptions/{subId}/resourceGroups/{rgName}/providers/Microsoft.OperationalInsights/workspaces/{laName}" -OutputDCRName "DCRMig"
#>
[CmdletBinding()]
Param
(
[Parameter(Mandatory = $true, Position = 1)]
[String]$LogAnalyticsWorkspaceResourceId,
[Parameter(Mandatory = $true, Position = 2)]
[String]$OutputDCRName
)
try {
$armComponents = Parse-ArmId -ResourceId $LogAnalyticsWorkspaceResourceId
$dcrArmTemplatePayload = ConvertFrom-Json $DeployDCRARMTemplate
$dcrArmTemplatePayload.properties.template = $Global:CTv2JsonObject
$dcrArmTemplatePayload.properties.parameters.workspaceLocation.value = $Global:OutputDCRLocation
$dcrArmTemplatePayload.properties.parameters.dataCollectionRuleName.value = $OutputDCRName
$dcrArmTemplatePayload.properties.parameters.workspaceResourceId.value = $LogAnalyticsWorkspaceResourceId
$dcrArmTemplatePayload = ConvertTo-Json -InputObject $dcrArmTemplatePayload -Depth $MaxDepth
$deploymentName = $OutputDCRName + "_" + $MigrationTag + "_" + (New-Guid).Guid.ToString()
$dcrArmTemplateResponse = Invoke-ArmApi-WithPath -Path ($ARMDeploymentPath -f $armComponents.Subscription, $armComponents.ResourceGroup, $deploymentName) -ApiVersion $ARMDepoymentApiVersion -Method $PUT -Payload $dcrArmTemplatePayload
if ($dcrArmTemplateResponse.Status -eq $Succeeded)
{
do {
$dcrArmTemplateResponse = Invoke-ArmApi-WithPath -Path ($ARMDeploymentPath -f $armComponents.Subscription, $armComponents.ResourceGroup, $deploymentName) -ApiVersion $ARMDepoymentApiVersion -Method $GET
if ($dcrArmTemplateResponse.Response.properties.provisioningState -eq $Failed) {
Write-Telemetry -Message ("Failed to deploy DCR template {0} with below error." -f $deploymentName) -Level $ErrorLvl
Write-Telemetry -Message ($dcrArmTemplateResponse.response.properties.error | ConvertTo-Json -Depth $MaxDepth) -Level $ErrorLvl
throw
}
}
while ($dcrArmTemplateResponse.Response.properties.provisioningState -ne $Succeeded) {
}
$Global:DCRResourceId = $dcrArmTemplateResponse.Response.properties.outputResources[0].id
Write-Telemetry -Message ("Deployed DCR {0} successfully" -f $Global:DCRResourceId)
}
else {
Write-Telemetry -Message ("Failed to deploy DCR template {0} with below error." -f $deploymentName) -Level $ErrorLvl
Write-Telemetry -Message ($dcrArmTemplateResponse.response.properties.error | ConvertTo-Json -Depth $MaxDepth) -Level $ErrorLvl
throw
}
}
catch [Exception] {
Write-Telemetry -Message ("Unhandled exception {0} while deploying DCR template." -f , $_.Exception.Message) -Level $ErrorLvl
throw
}
}
function Migrate-SettingsToDCR {
<#
.SYNOPSIS
Gets all files, settings, tracking & registry for legacy Change Tracking solution using Input Log Analytics Workspace and translates them to equivalent settings for latest Change Tracking solution using Output Log Analytics Workspace.
.DESCRIPTION
Gets all files, settings, tracking & registry for legacy Change Tracking solution using Input Log Analytics Workspace and translates them to equivalent settings for latest Change Tracking solution using Output Log Analytics Workspace.
.PARAMETER LogAnalyticsWorkspaceResourceId
Input Log Analytics Workspace Resource Id to get the CT settings from.
.EXAMPLE
Migrate-SettingsToDCR LogAnalyticsWorkspaceResourceId "/subscriptions/{subId}/resourceGroups/{rgName}/providers/Microsoft.OperationalInsights/workspaces/{laName}"
#>
[CmdletBinding()]
Param
(
[Parameter(Mandatory = $true, Position = 1)]
[String]$LogAnalyticsWorkspaceResourceId
)
try {
Get-WindowsRegistrySettings -LogAnalyticsWorkspaceResourceId $LogAnalyticsWorkspaceResourceId
Get-WindowsFileSetting -LogAnalyticsWorkspaceResourceId $LogAnalyticsWorkspaceResourceId
Get-WindowsTrackingServices -LogAnalyticsWorkspaceResourceId $LogAnalyticsWorkspaceResourceId
Get-LinuxFileSettings -LogAnalyticsWorkspaceResourceId $LogAnalyticsWorkspaceResourceId
Get-DataTypeConfiguration -LogAnalyticsWorkspaceResourceId $LogAnalyticsWorkspaceResourceId
}
catch [Exception] {
Write-Telemetry -Message ("Unhandled exception {0} while migrating settings to DCR." -f , $_.Exception.Message) -Level $ErrorLvl
throw
}
}
function Create-DCRARMTemplate {
<#
.SYNOPSIS
Creates DCR ARM template for files, settings, tracking & registry for latest Change Tracking solution.
.DESCRIPTION
Creates DCR ARM template for files, settings, tracking & registry for latest Change Tracking solution.
.PARAMETER OutputDCRName
DCR Name.
.PARAMETER OutputLogAnalyticsWorkspaceResourceId
Log Analytics Workspace resource id with which DCR will be associated.
.EXAMPLE
Create-DCRARMTemplate -OutputDCRName "DCRMig" OutputLogAnalyticsWorkspaceResourceId "/subscriptions/{subId}/resourceGroups/{rgName}/providers/Microsoft.OperationalInsights/workspaces/{laName}"
#>
[CmdletBinding()]
Param
(
[Parameter(Mandatory = $true, Position = 1)]
[String]$OutputDCRName,
[Parameter(Mandatory = $true, Position = 2)]
[String]$OutputLogAnalyticsWorkspaceResourceId
)
try {
$logAnalyticsWorkspaceResponse = Invoke-ArmApi-WithPath -Path $OutputLogAnalyticsWorkspaceResourceId -ApiVersion $LatestLogAnalyticsWorkspaceApiVersion -Method $GET
$Global:OutputDCRLocation = $logAnalyticsWorkspaceResponse.Response.location
}
catch [Exception] {
Write-Telemetry -Message ("Unhandled exception {0} while getting log analytics location." -f , $_.Exception.Message) -Level $ErrorLvl
throw
}
$schema = "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#"
$contentVersion = "1.0.0.0"
$dcrTemplate =
[ordered]@{
"`$schema" = $schema;
contentVersion = "$contentVersion"
parameters = [ordered]@{
dataCollectionRuleName = [ordered]@{
type = "string"
metadata = [ordered]@{
description = "Specifies the name of the data collection rule to create."
}
defaultValue = "$OutputDCRName"
}
workspaceResourceId = [ordered]@{
type = "string"
metadata = [ordered]@{
description = "Specifies the Azure resource ID of the Log Analytics workspace to use to store change tracking data."
}
defaultValue = "$OutputLogAnalyticsWorkspaceResourceId"
}
workspaceLocation = [ordered]@{
type = "string"
metadata = [ordered]@{
description = "Specifies location of log analytic workspace"
}
defaultValue = "$Global:OutputDCRLocation"
}
}
resources = @(
[ordered]@{
type = "Microsoft.Insights/dataCollectionRules"
apiVersion = "2022-06-01"
name = "[parameters('dataCollectionRuleName')]"
location = "[parameters('workspaceLocation')]"
properties = [ordered]@{
description = "Data collection rule for CT."
dataSources = [ordered]@{
extensions = @(
[ordered]@{
streams = @(
"Microsoft-ConfigurationChange",
"Microsoft-ConfigurationChangeV2",
"Microsoft-ConfigurationData"
)
extensionName = "ChangeTracking-Windows"
extensionSettings = [ordered]@{
enableFiles = $true
enableSoftware = $true
enableRegistry = $true
enableServices = $true
enableInventory = $true
registrySettings = [ordered]@{
registryCollectionFrequency = 3000
registryInfo = @()
}
fileSettings = [ordered]@{
fileCollectionFrequency = 2700
fileinfo = @()
}
softwareSettings = [ordered]@{
softwareCollectionFrequency = 1800
}
inventorySettings = [ordered]@{
inventoryCollectionFrequency = 36000
}
servicesSettings = [ordered]@{
serviceCollectionFrequency = 1800
}
}
name = "CTDataSource-Windows"
},
[ordered]@{
streams = @(
"Microsoft-ConfigurationChange",
"Microsoft-ConfigurationChangeV2",
"Microsoft-ConfigurationData"
)
extensionName = "ChangeTracking-Linux"
extensionSettings = [ordered]@{
enableFiles = $true
enableSoftware = $true
enableRegistry = $false
enableServices = $true
enableInventory = $true
fileSettings = [ordered]@{
fileCollectionFrequency = 900
fileInfo = @()
}
softwareSettings = [ordered]@{
softwareCollectionFrequency = 300
}
inventorySettings = [ordered]@{
inventoryCollectionFrequency = 36000
}
servicesSettings = [ordered]@{
serviceCollectionFrequency = 300
}
}
name = "CTDataSource-Linux"
}
)
}
destinations = [ordered]@{
logAnalytics = @(
[ordered]@{
workspaceResourceId = "[parameters('workspaceResourceId')]"
name = "Microsoft-CT-Dest"
}
)
}
dataFlows = @(
[ordered]@{
streams = @(
"Microsoft-ConfigurationChange",
"Microsoft-ConfigurationChangeV2",
"Microsoft-ConfigurationData"
)
destinations = @(
"Microsoft-CT-Dest"
)
}
)
}
}
)
}
$Global:CTv2JsonObject = New-Object -TypeName PSObject -Property $dcrTemplate
Write-Telemetry -Message "DCR ARM Template Created"
}
function Remove-CTSolution
{
<#
.SYNOPSIS
Removes Change Tracking solution from linked log analytics workspace.
.DESCRIPTION
Removes Change Tracking solution from linked log analytics workspace.
.PARAMETER InputLogAnalyticsWorkspaceResourceId
Input Log Analytics Workspace Resource Id.
.PARAMETER OutputLogAnalyticsWorkspaceResourceId
Output Log Analytics Workspace Resource Id.
.EXAMPLE
Remove-CTSolution -InputLogAnalyticsWorkspaceResourceId "/subscriptions/{subId}/resourceGroups/{rgName}/providers/Microsoft.OperationalInsights/workspaces/{laName}" -OutputLogAnalyticsWorkspaceResourceId "/subscriptions/{subId}/resourceGroups/{rgName}/providers/Microsoft.OperationalInsights/workspaces/{laName}"
#>
[CmdletBinding()]
Param
(
[Parameter(Mandatory = $true, Position = 1)]
[String]$InputLogAnalyticsWorkspaceResourceId,
[Parameter(Mandatory = $true, Position = 2)]
[String]$OutputLogAnalyticsWorkspaceResourceId
)
if ($InputLogAnalyticsWorkspaceResourceId -eq $OutputLogAnalyticsWorkspaceResourceId) {
Write-Telemetry -Message "Not removing CT solution as input and output log analytics workspaces are the same"
return
}
$linkedWorkspace = $InputLogAnalyticsWorkspaceResourceId
$parts = $linkedWorkspace.Split("/")
$response = Invoke-ArmApi-WithPath -Path ($SolutionsWithWorkspaceFilterPath -f $parts[2], $parts[4], $parts[8]) -ApiVersion $SolutionsApiVersion -Method $GET
if ($response.Status -eq $Failed)
{
Write-Telemetry -Message ("Failed to get solutions for log analytics workspace {0} with error {1}." -f $linkedWorkspace, $response.ErrorMessage) -Level $ErrorLvl
throw
}
foreach ($solution in $response.Response.value)
{
$name = ("ChangeTracking(" + $parts[8] + ")")
if ($solution.name -eq $name )
{
$response = Invoke-ArmApi-WithPath -Path $solution.id -ApiVersion $SolutionsApiVersion -Method $DELETE
if ($response.Status -eq $Failed)
{
Write-Telemetry -Message ("Failed to remove Change Tracking solution from linked log analytics workspace {0} with error {1}." -f $linkedWorkspace, $response.ErrorMessage) -Level $ErrorLvl
}
else
{
Write-Telemetry -Message ("Removed Change Tracking solution from linked log analytics workspace {0}." -f $linkedWorkspace)
}
}
}
}
if ($PSVersionTable.PSVersion.Major -lt 7) {
Write-Telemetry -Message ("his script requires Powershell version 7 or newer to run. Please see https://docs.microsoft.com/en-us/powershell/scripting/whats-new/migrating-from-windows-powershell-51-to-powershell-7?view=powershell-7.1.") -Level $ErrorLvl
exit 1
}
$azConnect = Connect-AzAccount -SubscriptionId $InputLogAnalyticsWorkspaceResourceId.Split("/")[2] -Environment $AzureEnvironment
if ($null -eq $azConnect) {
Write-Telemetry -Message ("Failed to connect to azure in first attempt. Will retry with DeviceCodeAuthentication.") -Level $ErrorLvl
$azConnect = Connect-AzAccount -UseDeviceAuthentication -SubscriptionId $InputLogAnalyticsWorkspaceResourceId.Split("/")[2] -Environment $AzureEnvironment
if ($null -eq $azConnect) {
Write-Telemetry -Message ("Failed to connect to azure with DeviceCodeAuthentication also.") -Level $ErrorLvl
throw
}
}
else {
Write-Telemetry -Message ("Successfully connected with account {0} to subscription {1}" -f $azConnect.Context.Account, $azConnect.Context.Subscription)
}
try {
# Retrieve all machines onboarded to Change Tracking.
Populate-AllMachinesOnboardedToChangeTracking -LogAnalyticsWorkspaceResourceId $InputLogAnalyticsWorkspaceResourceId
# Create DCR ARM Template.
Create-DCRARMTemplate -OutputDCRName $OutputDCRName -OutputLogAnalyticsWorkspaceResourceId $OutputLogAnalyticsWorkspaceResourceId
# Migrate Settings from LA Workspace to Output LA Workspace DCR.
Migrate-SettingsToDCR -LogAnalyticsWorkspaceResourceId $InputLogAnalyticsWorkspaceResourceId
# Deploy Change Tracking Solution to Output Log Analytics Workspace.
Deploy-ChangeTrackingSolutionARMTemplate -InputLogAnalyticsWorkspaceResourceId $InputLogAnalyticsWorkspaceResourceId -OutputLogAnalyticsWorkspaceResourceId $OutputLogAnalyticsWorkspaceResourceId
# Deploy DCR Template.
Deploy-DCRARMTemplate -LogAnalyticsWorkspaceResourceId $OutputLogAnalyticsWorkspaceResourceId -OutputDCRName $OutputDCRName
# Remove MMA Agent.
Remove-MMAAgentFromMachines -InputLogAnalyticsWorkspaceResourceId $InputLogAnalyticsWorkspaceResourceId -OutputLogAnalyticsWorkspaceResourceId $OutputLogAnalyticsWorkspaceResourceId
# Associate DCR with Machines.
Associate-MachinesWithDCR
# Remove CT Solution from Input Log Analytics Workspace.
Remove-CTSolution -InputLogAnalyticsWorkspaceResourceId $InputLogAnalyticsWorkspaceResourceId -OutputLogAnalyticsWorkspaceResourceId $OutputLogAnalyticsWorkspaceResourceId
}
catch [Exception] {
Write-Telemetry -Message ("Unhandled Exception {0}." -f $_.Exception.Message) -Level $ErrorLvl
}