зеркало из https://github.com/microsoft/Cluster.git
493 строки
17 KiB
PowerShell
493 строки
17 KiB
PowerShell
<##
|
|
# Types.ps1
|
|
##
|
|
# Assumptions:
|
|
# - AzureRM context is initialized
|
|
# Design Considerations:
|
|
# - Resources with globally unique names are given random IDs and should be accessed by resource group, not name
|
|
# - Defining a class "Environment" will cause issues with System.Environment
|
|
#>
|
|
|
|
using namespace Microsoft.Azure.Commands.ResourceManager.Cmdlets.SdkModels
|
|
using namespace Microsoft.Azure.Commands.Common.Authentication.Abstractions
|
|
using namespace System.Collections
|
|
|
|
# required for importing types
|
|
Import-Module "AzureRm", "$PSScriptRoot\AzureBakery"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ClusterResourceGroup {
|
|
|
|
# Indentity vector (path through the service tree)
|
|
[string[]]$Identity
|
|
|
|
# where region-agnostic resources are defined
|
|
static [string] $DefaultRegion = "West US 2"
|
|
|
|
# string preceding a random string on underlying resource names
|
|
static [string] $DefaultResourcePrefix = "cluster"
|
|
|
|
# name of the blob storage container containing service artifacts
|
|
static [string] $ArtifactContainerName = "artifacts"
|
|
|
|
# name of the blob storage container containing VM images
|
|
static [string] $ImageContainerName = "images"
|
|
|
|
# underlying property for lazily instantiating Azure Storage Contexts
|
|
hidden [IStorageContext] $_StorageContext
|
|
|
|
|
|
|
|
# create a ClusterResourceGroup model object from its resource group name
|
|
ClusterResourceGroup([string] $resourceGroupName) {
|
|
$this.Identity = $resourceGroupName -split "-"
|
|
}
|
|
|
|
# use the values encapsulated in this model object to provision Azure resources
|
|
[void] Create() {
|
|
if ($this.Exists()) {
|
|
throw "Resource Group '$this' already exists"
|
|
}
|
|
|
|
# determine if this resource group has a speified region (for Environments and Clusters)
|
|
$region = @{
|
|
$True = $this.Identity[2]
|
|
$False = [ClusterResourceGroup]::DefaultRegion
|
|
}[$this.Identity.Count -ge 3]
|
|
|
|
# create and initialize the Azure resources
|
|
New-AzureRmResourceGroup -Name $this -Location $region
|
|
New-AzureRmStorageAccount `
|
|
-ResourceGroupName $this `
|
|
-Name ([ClusterResourceGroup]::NewResourceName()) `
|
|
-Location $region `
|
|
-Type "Standard_LRS" `
|
|
-EnableEncryptionService "blob" `
|
|
-EnableHttpsTrafficOnly $true
|
|
New-AzureRmKeyVault `
|
|
-VaultName ([ClusterResourceGroup]::NewResourceName()) `
|
|
-ResourceGroupName $this `
|
|
-Location $region
|
|
New-AzureStorageContainer `
|
|
-Context $this.GetStorageContext() `
|
|
-Name ([ClusterResourceGroup]::ArtifactContainerName)
|
|
New-AzureStorageContainer `
|
|
-Context $this.GetStorageContext() `
|
|
-Name ([ClusterResourceGroup]::ImageContainerName)
|
|
|
|
# if the resource group has a parent (isn't a 'Service'), propagate assets from parent to this
|
|
$parentId = ($this.Identity | Select -SkipLast 1) -join "-"
|
|
if ($parentId) {
|
|
$parent = [ClusterResourceGroup]::new($parentId)
|
|
$parent.PropagateArtifacts()
|
|
$parent.PropagateImages()
|
|
$parent.PropagateSecrets()
|
|
}
|
|
}
|
|
|
|
|
|
# returns whether this model's Azure resources have been created
|
|
[bool] Exists() {
|
|
$resourceGroup = Get-AzureRmResourceGroup `
|
|
-ResourceGroupName $this `
|
|
-ErrorAction SilentlyContinue
|
|
return $resourceGroup -as [bool]
|
|
}
|
|
|
|
|
|
# returns a model for each child service tree node that has been provisioned in Azure
|
|
[ClusterResourceGroup[]] GetChildren() {
|
|
$children = Get-AzureRmResourceGroup `
|
|
| % {$_.ResourceGroupName} `
|
|
| ? {$_ -match "^$this-[^-]+$"} `
|
|
| % {[ClusterResourceGroup]::new($_)}
|
|
return @($children)
|
|
}
|
|
|
|
# returns a model for each descendant service tree node (not leaves/Clusters) that has been provisioned in Azure
|
|
[ClusterResourceGroup[]] GetDescendantNodes() {
|
|
$descendants = Get-AzureRmResourceGroup `
|
|
| % {$_.ResourceGroupName} `
|
|
| ? {$_ -like "$this*" -and ($_ -split '-').Count -le 3} `
|
|
| % {[ClusterResourceGroup]::new($_)}
|
|
return @($descendants)
|
|
}
|
|
|
|
# lazily instantiates an Azure Storage Context for use with the Azure.Storage module
|
|
[IStorageContext] GetStorageContext() {
|
|
if (-not $this._StorageContext) {
|
|
$storageAccount = Get-AzureRmStorageAccount -ResourceGroupName $this
|
|
$this._StorageContext = $storageAccount.Context
|
|
}
|
|
return $this._StorageContext
|
|
}
|
|
|
|
|
|
# uses the AzureBakery nested module for creating a generalized Windows VHD containing the specified Windows Features
|
|
[void] NewImage([string[]] $WindowsFeature) {
|
|
New-BakedImage `
|
|
-StorageContext $this.GetStorageContext() `
|
|
-WindowsFeature $WindowsFeature `
|
|
-StorageContainer ([ClusterResourceGroup]::ImageContainerName)
|
|
}
|
|
|
|
|
|
# pushes Artifacts from this service tree node to its descendants
|
|
[void] PropagateArtifacts() {
|
|
$this.PropagateBlobs([ClusterResourceGroup]::ArtifactContainerName)
|
|
}
|
|
|
|
# pushes Images from this service tree node to its descendants
|
|
[void] PropagateImages() {
|
|
$this.PropagateBlobs([ClusterResourceGroup]::ImageContainerName)
|
|
}
|
|
|
|
# pushes Blobs in the specified container from this service tree node to its descendants
|
|
[void] PropagateBlobs([string] $Container) {
|
|
$descendants = $this.GetDescendantNodes()
|
|
if (-not $descendants) {
|
|
return
|
|
}
|
|
$descendantContexts = $descendants.GetStorageContext()
|
|
$artifactNames = Get-AzureStorageBlob `
|
|
-Container $Container `
|
|
-Context $this.GetStorageContext() `
|
|
| % {$_.Name}
|
|
|
|
# async start copying blobs
|
|
$pendingBlobs = [ArrayList]::new()
|
|
foreach ($descendantContext in $descendantContexts) {
|
|
foreach ($artifactName in $artifactNames) {
|
|
$descendantBlob = Get-AzureStorageBlob `
|
|
-Context $descendantContext `
|
|
-Container $Container `
|
|
-Blob $artifactName `
|
|
-ErrorAction SilentlyContinue
|
|
if (-not $descendantBlob) {
|
|
$descendantBlob = Start-AzureStorageBlobCopy `
|
|
-Context $this.GetStorageContext() `
|
|
-DestContext $descendantContext `
|
|
-SrcContainer $Container `
|
|
-DestContainer $Container `
|
|
-SrcBlob $artifactName `
|
|
-DestBlob $artifactName
|
|
$pendingBlobs.Add($descendantBlob)
|
|
}
|
|
}
|
|
}
|
|
|
|
# block until all copies are complete
|
|
foreach ($blob in $pendingBlobs) {
|
|
Get-AzureStorageBlobCopyState `
|
|
-Context $blob.Context `
|
|
-Container $Container `
|
|
-Blob $blob.Name `
|
|
-WaitForComplete
|
|
}
|
|
}
|
|
|
|
|
|
# pushes Azure Key Vault Secrets from this service tree node to its descendants
|
|
[void] PropagateSecrets() {
|
|
$descendants = $this.GetDescendantNodes()
|
|
if (-not $descendants) {
|
|
return
|
|
}
|
|
$keyVaultName = (Get-AzureRmKeyVault -ResourceGroupName $this).VaultName
|
|
$descendantKeyVaultNames = $descendants `
|
|
| % {Get-AzureRmKeyVault -ResourceGroupName $_} `
|
|
| % {$_.VaultName}
|
|
$secretNames = (Get-AzureKeyVaultSecret -VaultName $keyVaultName).Name
|
|
foreach ($childKeyVaultName in $descendantKeyVaultNames) {
|
|
foreach ($secretName in $secretNames) {
|
|
$secret = Get-AzureKeyVaultSecret `
|
|
-VaultName $keyVaultName `
|
|
-Name $secretName
|
|
Set-AzureKeyVaultSecret `
|
|
-VaultName $childKeyVaultName `
|
|
-Name $secretName `
|
|
-SecretValue $secret.SecretValue `
|
|
-ContentType $secret.Attributes.ContentType
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
# returns this model's associated resource group name
|
|
[string] ToString() {
|
|
return $this.Identity -join "-"
|
|
}
|
|
|
|
|
|
# determines the service tree node type (discouraged as it inherently breaks linting)
|
|
[Reflection.TypeInfo] InferType() {
|
|
switch ($this.Identity.Count) {
|
|
1 {return [ClusterService]}
|
|
2 {return [ClusterFlightingRing]}
|
|
3 {return [ClusterEnvironment]}
|
|
4 {return [Cluster]}
|
|
}
|
|
throw "Cannot infer type of '$this'"
|
|
return [void] # return value to not break linting
|
|
}
|
|
|
|
|
|
# uploads an Artifact (file required for the VM/Container/etc to initialize) to the service tree node
|
|
[void] UploadArtifact([string] $ArtifactPath) {
|
|
Set-AzureStorageBlobContent `
|
|
-File $ArtifactPath `
|
|
-Container ([ClusterResourceGroup]::ArtifactContainerName) `
|
|
-Blob (Split-Path -Path $ArtifactPath -Leaf) `
|
|
-Context $this.GetStorageContext() `
|
|
-Force
|
|
}
|
|
|
|
|
|
# creates a base36 GUID with a prefix and valid length for creating globally unique Azure resource names
|
|
static [string] NewResourceName() {
|
|
$Length = 24
|
|
$allowedChars = "abcdefghijklmnopqrstuvwxyz0123456789"
|
|
$chars = 1..($Length - [ClusterResourceGroup]::DefaultResourcePrefix.Length) `
|
|
| % {Get-Random -Maximum $allowedChars.Length} `
|
|
| % {$allowedChars[$_]}
|
|
return [ClusterResourceGroup]::DefaultResourcePrefix + ($chars -join '')
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ClusterService : ClusterResourceGroup {
|
|
[ValidatePattern("^[A-Z][A-z0-9]+$")]
|
|
[string]$Service
|
|
|
|
ClusterService([string] $resourceGroupName) : base($resourceGroupName) {
|
|
$this.Service = $this.Identity
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ClusterFlightingRing : ClusterResourceGroup {
|
|
[ValidateNotNullOrEmpty()]
|
|
[ClusterService]$Service
|
|
|
|
[ValidatePattern("^[A-Z]{3,6}$")]
|
|
[string]$FlightingRing
|
|
|
|
ClusterFlightingRing([string] $resourceGroupName) : base($resourceGroupName) {
|
|
$this.Service = [ClusterService]::new($this.Identity[0])
|
|
$this.FlightingRing = $this.Identity | Select -Last 1
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ClusterEnvironment : ClusterResourceGroup {
|
|
[ValidateNotNullOrEmpty()]
|
|
[ClusterFlightingRing]$FlightingRing
|
|
|
|
[ValidatePattern("^[A-z][A-z0-9 ]+$")]
|
|
[string]$Region
|
|
|
|
static [int] $TTL = 5
|
|
static [string] $MonitorPath = "/"
|
|
|
|
ClusterEnvironment([string] $resourceGroupName) : base($resourceGroupName) {
|
|
$this.FlightingRing = [ClusterFlightingRing]::new($this.Identity[0..1] -join "-")
|
|
$this.Region = $this.Identity | Select -Last 1
|
|
}
|
|
|
|
# creates a Cluster Azure resource group group that is a child of this model and returns the Cluster's model
|
|
[Cluster] NewChildCluster() {
|
|
$indexes = ($this.GetChildren() | % {[Cluster]::new($_)}).Index # get currently used indexes
|
|
for ($index = 0; $index -in $indexes; $index++) {} # determine lowest available index
|
|
$cluster = [Cluster]::new("$this-$index") # create a Cluster model with the index
|
|
$cluster.Create() # create the Azure resources from the Cluster model
|
|
return $cluster # return the Cluster model
|
|
}
|
|
|
|
[void] Create() {
|
|
([ClusterResourceGroup]$this).Create()
|
|
New-AzureRmTrafficManagerProfile `
|
|
-Name "Profile" `
|
|
-ResourceGroupName $this `
|
|
-TrafficRoutingMethod Weighted `
|
|
-RelativeDnsName ([ClusterResourceGroup]::NewResourceName()) `
|
|
-Ttl ([ClusterEnvironment]::TTL) `
|
|
-MonitorProtocol HTTP `
|
|
-MonitorPort 80 `
|
|
-MonitorPath ([ClusterEnvironment]::MonitorPath)
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Cluster : ClusterResourceGroup {
|
|
[ValidateNotNullOrEmpty()]
|
|
[ClusterEnvironment]$Environment
|
|
|
|
[ValidateRange(0, 255)]
|
|
[int]$Index
|
|
|
|
Cluster([string] $resourceGroupName) : base($resourceGroupName) {
|
|
$this.Environment = [ClusterEnvironment]::new($this.Identity[0..2] -join "-")
|
|
$this.Index = $this.Identity | Select -Last 1
|
|
}
|
|
|
|
# create a service tree node resource group and resources, with additional Clsuter-specific resources
|
|
[void] Create() {
|
|
New-AzureRmResourceGroup -Name $this -Location $this.Environment.Region
|
|
New-AzureRmStorageAccount `
|
|
-ResourceGroupName $this `
|
|
-Name ([ClusterResourceGroup]::NewResourceName()) `
|
|
-Location $this.Environment.Region `
|
|
-Type "Standard_LRS" `
|
|
-EnableEncryptionService "blob" `
|
|
-EnableHttpsTrafficOnly $true
|
|
New-AzureStorageContainer `
|
|
-Context $this.GetStorageContext() `
|
|
-Name "configuration"
|
|
New-AzureStorageContainer `
|
|
-Context $this.GetStorageContext() `
|
|
-Name "disks"
|
|
}
|
|
|
|
# uses the Cluster configuration inheritence model (see README) to identify the most specific config with the specified extension
|
|
[string] GetConfig([string]$DefinitionsContainer, [string]$FileExtension) {
|
|
($service, $flightingRing, $region, $index) = $this.Identity
|
|
$config = $service, "Default" `
|
|
| % {"$_.$flightingRing.$region", "$_.$flightingRing", $_} `
|
|
| % {"$DefinitionsContainer\$_.$FileExtension"} `
|
|
| ? {Test-Path $_} `
|
|
| Select -First 1
|
|
return $config
|
|
}
|
|
|
|
|
|
[PSResourceGroupDeployment] PublishConfiguration([string]$DefinitionsContainer, [datetime]$Expiry) {
|
|
$context = $this.GetStorageContext()
|
|
|
|
# build url components
|
|
$vhdContainer = "$($context.BlobEndpoint)disks/"
|
|
$configurationSasToken = New-AzureStorageContainerSASToken `
|
|
-Context $context `
|
|
-Container "configuration" `
|
|
-Permission "r" `
|
|
-ExpiryTime $expiry
|
|
|
|
# template deployment parameters
|
|
$deploymentParams = @{
|
|
ResourceGroupName = $this
|
|
TemplateFile = $this.GetConfig($DefinitionsContainer, "template.json")
|
|
Environment = $this.Environment
|
|
VhdContainer = $vhdContainer
|
|
SasToken = $configurationSasToken
|
|
}
|
|
|
|
# package and upload DSC
|
|
$dscFile = $this.GetConfig($DefinitionsContainer, "dsc.ps1")
|
|
if ($dscFile) {
|
|
$publishDscParams = @{
|
|
ConfigurationPath = $dscFile
|
|
OutputArchivePath = "$env:TEMP\dsc.zip"
|
|
Force = $true
|
|
}
|
|
$dscConfigDataFile = $this.GetConfig($DefinitionsContainer, "dsc.psd1")
|
|
if ($dscConfigDataFile) {
|
|
$publishDscParams["ConfigurationDataPath"] = $dscConfigDataFile
|
|
}
|
|
Publish-AzureRmVMDscConfiguration @publishDscParams
|
|
Set-AzureStorageBlobContent `
|
|
-File "$env:TEMP\dsc.zip" `
|
|
-Container "configuration" `
|
|
-Blob "dsc.zip" `
|
|
-Context $context `
|
|
-Force
|
|
$deploymentParams["DscUrl"] = "$($context.BlobEndpoint)configuration/dsc.zip"
|
|
$deploymentParams["DscFileName"] = Split-Path -Path $dscFile -Leaf
|
|
$deploymentParams["DscHash"] = (Get-FileHash "$env:TEMP\dsc.zip").Hash.Substring(0, 50)
|
|
}
|
|
|
|
# package and upload CSE
|
|
$cseFile = $this.GetConfig($DefinitionsContainer, "cse.ps1")
|
|
if ($cseFile) {
|
|
Set-AzureStorageBlobContent `
|
|
-File $cseFile `
|
|
-Container "configuration" `
|
|
-Blob "cse.ps1" `
|
|
-Context $context `
|
|
-Force
|
|
$deploymentParams["CseUrl"] = "$($context.BlobEndPoint)configuration/cse.ps1"
|
|
}
|
|
|
|
# template parameters
|
|
$templateParameterFile = $this.GetConfig($DefinitionsContainer, "parameters.json")
|
|
if ($templateParameterFile) {
|
|
$deploymentParams["TemplateParameterFile"] = $templateParameterFile
|
|
}
|
|
|
|
# freeform json passed to the DSC
|
|
$configJsonFile = $this.GetConfig($DefinitionsContainer, "config.json")
|
|
if ($configJsonFile) {
|
|
$deploymentParams["ConfigJson"] = Get-Content $configJsonFile -Raw
|
|
}
|
|
|
|
# baked Windows Image URL (from parent Environment)
|
|
$environmentContext = $this.Environment.GetStorageContext()
|
|
$images = Get-AzureStorageBlob `
|
|
-Context $environmentContext `
|
|
-Container ([ClusterResourceGroup]::ImageContainerName)
|
|
if ($images) {
|
|
$imageName = $images | Sort LastModified -Descending | Select -First 1 | % Name
|
|
$deploymentParams["ImageUrl"] = "$($environmentContext.BlobEndPoint)images/$imageName"
|
|
}
|
|
|
|
# deploy template
|
|
$deploymentErrors = $null # redundantly define for linting
|
|
$deployment = New-AzureRmResourceGroupDeployment `
|
|
-Name ((Get-Date -Format "s") -replace "[^\d]") `
|
|
@deploymentParams `
|
|
-Verbose `
|
|
-ErrorVariable deploymentErrors `
|
|
-Force
|
|
if ($deploymentErrors) {
|
|
throw $deploymentErrors
|
|
}
|
|
return $deployment
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|