Skip to content

Instantly share code, notes, and snippets.

@tyconsulting
Last active April 19, 2017 03:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tyconsulting/df78d43e64fe86fe772f947fded3c4da to your computer and use it in GitHub Desktop.
Save tyconsulting/df78d43e64fe86fe772f947fded3c4da to your computer and use it in GitHub Desktop.
#requires -Version 3.0 -Modules AzureRM.Automation, AzureRM.Profile, AzureRM.Resources
<#
==========================================================================
AUTHOR: Tao Yang
DATE: 19/04/2017
Version: 1.1
Runbook Name: Sync-ModuleAssets
Comment:
Azure Automation runbook that sync Azure Automation module assets
from a package repository
Requirement:
1. Create an automation variable called 'ModuleFeedLocation'
to store the module repository source URI
2. Make sure the AzureRunAsConnection is created. This should be created
by default when the Automation account is created.
==========================================================================
#>
#region functions
Function Get-PackageDependencies
{
[CmdletBinding()]
PARAM (
[Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()][String]$SourceLocationUri,
[Parameter(Mandatory = $true)][String]$ModuleName,
[Parameter(Mandatory = $true)][String]$ModuleVersion
)
if ($SourceLocationUri.Substring($SourceLocationUri.length -1, 1) -ne '/')
{
$SourceLocationUri = "$SourceLocationUri`/"
}
#Get the module
$SearchUri = "$SourceLocationUri" + "FindPackagesById()`?id='$ModuleName'"
$SearchRequest = Invoke-WebRequest -UseBasicParsing -Uri $SearchUri -Method Get
$SearchResult = ([xml]$SearchRequest.content).feed.entry.properties
$Module = $SearchResult | Where-Object -FilterScript {
[Version]::Parse($_.Version) -eq [version]::Parse($ModuleVersion)
}
$regex1 = '^(?<DepModuleName>[A-Za-z0-9.]+):\[(?<DepModuleVersion1>[0-9.]+), (?<DepModuleVersion2>[0-9.]+)\]:'
$regex2 = '^(?<DepModuleName>[A-Za-z0-9.]+):\[(?<DepModuleVersion1>[0-9.]+)\]:'
$regex3 = '^(?<DepModuleName>[A-Za-z0-9.]+):\(, \)'
$ModuleDependencies = New-Object -TypeName System.Collections.ArrayList
Foreach ($item in $($Module.Dependencies -split "\:\|"))
{
$RegMatch1 = $item -match $regex1
$bMatch = $false
If ($RegMatch1)
{
$bMatch = $true
$DependencyModuleName = $Matches['DepModuleName']
$DependencyModuleVersion = $Matches['DepModuleVersion1']
$objDependency = New-Object -TypeName psobject -Property @{
ModuleName = $DependencyModuleName
ModuleVersion = $DependencyModuleVersion
}
[Void]$ModuleDependencies.Add($objDependency)
}
if ($bMatch -eq $false)
{
$RegMatch2 = $item -match $regex2
If ($RegMatch2)
{
$bMatch = $true
$DependencyModuleName = $Matches['DepModuleName']
$DependencyModuleVersion = $Matches['DepModuleVersion1']
$objDependency = New-Object -TypeName psobject -Property @{
ModuleName = $DependencyModuleName
ModuleVersion = $DependencyModuleVersion
}
[Void]$ModuleDependencies.Add($objDependency)
}
}
if ($bMatch -eq $false)
{
$RegMatch3 = $item -match $regex3
If ($RegMatch3)
{
$bMatch = $true
$DependencyModuleName = $Matches['DepModuleName']
$DependencyModuleVersion = "0.0"
$objDependency = New-Object -TypeName psobject -Property @{
ModuleName = $DependencyModuleName
ModuleVersion = $DependencyModuleVersion
}
[Void]$ModuleDependencies.Add($objDependency)
}
}
}
,$ModuleDependencies
}
#endregion
#region variables
$SourceLocationUriVariableName = 'ModuleFeedLocation'
$AzureRunAsConnectionName = 'AzureRunAsConnection'
#endregion
#Get automation variables
$SourceLocationUri = Get-AutomationVariable -Name $SourceLocationUriVariableName
$AzureConnection = Get-AutomationConnection -Name $AzureRunAsConnectionName
if ($SourceLocationUri.Substring($SourceLocationUri.length -1, 1) -ne '/')
{
$SourceLocationUri = "$SourceLocationUri`/"
}
#region pre-flight check
Write-Output "Pre-flight check."
$CurrentJobId= $PSPrivateMetadata.JobId.Guid
#Login to Azure
Write-Output -InputObject 'Login to Azure'
try
{
Add-AzureRmAccount -ServicePrincipal -TenantId $AzureConnection.TenantId -ApplicationId $AzureConnection.ApplicationId -CertificateThumbprint $AzureConnection.CertificateThumbprint
}
catch
{
if (!$AzureConnection)
{
$ErrorMessage = "Connection $AzureRunAsConnectionName not found."
throw $ErrorMessage
}
else
{
Write-Error -Message $_.Exception
throw $_.Exception
}
}
$Context = Get-AzureRmContext
Write-Verbose -Message "Current Subscription Name: $($Context.Subscription.SubscriptionName)"
#Get Automation account and resource group names
$AutomationAccounts = Find-AzureRmResource -ResourceType Microsoft.Automation/AutomationAccounts
foreach ($item in $AutomationAccounts)
{
# Loop through each Automation account to find this job
$Job = Get-AzureRmAutomationJob -ResourceGroupName $item.ResourceGroupName -AutomationAccountName $item.Name -Id $CurrentJobId -ErrorAction SilentlyContinue
if ($Job)
{
$AutomationAccountName = $item.Name
$ResourceGroupName = $item.ResourceGroupName
$RunbookName = $Job.RunbookName
break
}
}
Write-Output "Automation Account Name: '$AutomationAccountName'"
Write-Output "Resource Group Name: '$ResourceGroupName'"
Write-Output "Runbook Name: '$RunbookName'"
#Check if the runbook is already running
$CurrentRunningJobs = Get-AzureRmAutomationJob -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -RunbookName $RunbookName | Where-object {($_.Status -imatch '\w+ing$' -or $_.Status -imatch 'queued') -and $_.JobId.tostring() -ine $CurrentJobId}
If ($CurrentRunningJobs)
{
Write-output "Active runbook job detected."
Foreach ($job in $CurrentRunningJobs)
{
Write-Output " - JobId: $($job.JobId), Status: '$($job.Status)'."
}
Write-output "The runbook job will stop now."
Exit
} else {
Write-Output "No concurrent runbook jobs found. OK to continue."
}
#endregion
#Get list of modules from the source feed
Write-Output "The package repository is hosting the following modules:"
$SearchUri = "$SourceLocationUri" + 'feed-state'
$SearchRequest = Invoke-WebRequest -UseBasicParsing -Uri $SearchUri -Method Get
If ($SearchRequest.StatusCode -ge 200 -and $SearchRequest.StatusCode -le 299)
{
#Request success
#Get a list of pacakges
$PackageList = (ConvertFrom-Json -InputObject $SearchRequest.Content).packages | Where-Object -FilterScript {
$_.packagetype -ieq 'nuget'
}
#Get the latest version of each package
$arrPackages = New-Object -TypeName System.Collections.ArrayList
Foreach ($package in $PackageList)
{
$LatestVersion = $package.versions |
Sort-Object -Descending |
Select-Object -First 1
$PackageSourceLocation = $SourceLocationUri + "package/$($package.id)/$LatestVersion/"
$objPackageInfo = New-Object -TypeName psobject -Property @{
Name = $($package.id)
LatestVersion = $LatestVersion
PackageSourceURI = $PackageSourceLocation
ImportOrder = 0
}
[Void]$arrPackages.Add($objPackageInfo)
Write-Output " -$($package.id) `(version: $LatestVersion`)"
}
}
else
{
Throw "module search request failed. HTTP Status code: '$SearchRequest.StatusCode'."
Exit -1
}
#Work out the import order based on module dependencies
Write-Output -InputObject 'Work out the import order based on module dependencies'
#setting the 1st batch - the ones without dependencies
foreach ($package in $arrPackages)
{
#packages without depdencies will be imported first
$packageDependencies = Get-PackageDependencies -SourceLocationUri $SourceLocationUri -ModuleName $package.Name -ModuleVersion $package.LatestVersion
Add-Member -InputObject $package -MemberType NoteProperty -Name Dependencies -Value $packageDependencies
if ($packageDependencies.count -eq 0)
{
Write-Output "$($package.Name) import order: 1"
$package.ImportOrder = 1
}
}
#2nd batch - the ones with dependencies but all dependencies are loaded in the first batch
foreach ($package in ($arrPackages | Where-Object -FilterScript {
$_.ImportOrder -eq 0
}))
{
$PlaceInSecondBatch = $true
foreach ($item in $package.Dependencies)
{
$depdencyInRepo = $arrPackages | Where-Object -FilterScript {
$_.Name -ieq $item.ModuleName -and [Version]::Parse($_.LatestVersion) -ge [version]::parse($item.ModuleVersion)
}
if ($depdencyInRepo.ImportOrder -ne 1)
{
$PlaceInSecondBatch = $false
}
}
If ($PlaceInSecondBatch -eq $true)
{
Write-Output "$($package.Name) import order: 2"
$package.ImportOrder = 2
}
}
#3rd batch - the rest
foreach ($package in ($arrPackages | Where-Object -FilterScript {
$_.ImportOrder -eq 0
}))
{
Write-output "$($package.Name) import order: 3"
$package.ImportOrder = 3
}
#Get import groups
$ImportGroups = $arrPackages |
Group-Object -Property ImportOrder |
Sort-Object -Property name
#Get existing module assets
#Go through each module from the repository
$ImportCount = 0
$SuccessCount = 0
$FailedCount = 0
Foreach ($ImportGroup in $ImportGroups)
{
foreach ($package in $ImportGroup.Group)
{
$ExistingAsset = Get-AzureRmAutomationModule -AutomationAccountName $AutomationAccountName -ResourceGroupName $ResourceGroupName -Name $package.Name -ErrorAction SilentlyContinue
If ($ExistingAsset)
{
if ($ExistingAsset.ProvisioningState -ine 'creating')
{
$ExistingVersion = $ExistingAsset.Version
if ($ExistingVersion -eq $null -or $ExistingVersion.length -eq 0)
{
$ExistingVersion = '0.0.0'
}
if ([version]::Parse($package.LatestVersion) -gt [version]::Parse($ExistingVersion))
{
Write-Output -InputObject "The '$($ExistingAsset.name)' module version in the repository is $($package.LatestVersion), which is greater than the version in Azure Automation account '$ExistingVersion'. Updating it now."
$ImportJob = New-AzureRmAutomationModule -Name $($package.name) -ContentLink $($package.PackageSourceURI) -AutomationAccountName $AutomationAccountName -ResourceGroupName $ResourceGroupName
$ImportCount ++
}
else
{
Write-Output -InputObject "The '$($ExistingAsset.name)' module version in the repository is $($package.LatestVersion), which is the same as the version in Azure Automation account '$ExistingVersion'. Skipped."
}
}
else
{
Write-Output -InputObject "The '$($ExistingAsset.name)' module is being imported right now. Skipped."
}
}
else
{
Write-Output -InputObject "The '$($package.name)' module does not exist in the Azure Automation account. importing it now."
$ImportJob = New-AzureRmAutomationModule -Name $($package.name) -ContentLink $($package.PackageSourceURI) -AutomationAccountName $AutomationAccountName -ResourceGroupName $ResourceGroupName
$ImportCount ++
}
If ($ImportJob -ne $null)
{
Write-Output -InputObject "Extracting module activities for module $($package.name). Please wait."
$bImportCompleted = $false
Do
{
Start-Sleep -Seconds 5
$AAModule = Get-AzureRmAutomationModule -Name $($package.name) -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName
If ($AAModule.ProvisioningState -eq 'Succeeded')
{
Write-Output -InputObject "Module '$($package.name)' import completed."
$bImportCompleted = $true
$SuccessCount ++
}
elseif ($AAModule.ProvisioningState -eq 'Failed')
{
Write-Error -Message "Module '$($package.name)' import failed. Please manually check the error details in the Azure Portal."
$bImportCompleted = $true
$FailedCount ++
}
}
Until ($bImportCompleted -eq $true)
}
$ImportJob = $null
}
}
Write-Output -InputObject "Total module count in the repository: $($arrPackages.count)."
Write-Output -InputObject "Total number of modules attempted to import: $ImportCount."
Write-Output -InputObject "Total number of successfully imported modules: $SuccessCount."
Write-Output -InputObject "Total number of failed imported modules: $FailedCount."
Write-Output -InputObject 'Done.'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment